| 1 | import { McpAttachmentInput } from "../attachment/input.ts"; |
| 2 | import { |
| 3 | NonNegativeInt, |
| 4 | ModelSelection, |
| 5 | TrimmedNonEmptyString, |
| 6 | ThreadId, |
| 7 | RunId, |
| 8 | OrchestrationV2RunStatus, |
| 9 | OrchestrationV2ThreadLaunchWorkspaceStrategy, |
| 10 | RuntimeMode, |
| 11 | ProviderInteractionMode, |
| 12 | Project, |
| 13 | ProjectCreatePayload, |
| 14 | ProjectUpdatePayload, |
| 15 | ProjectId, |
| 16 | OrchestratorMcpFailure, |
| 17 | SourceControlCloneRepositoryInput, |
| 18 | SourceControlCloneRepositoryResult, |
| 19 | } from "@t3tools/contracts"; |
| 20 | import * as FileSystem from "effect/FileSystem"; |
| 21 | import * as ServerConfig from "../../../config.ts"; |
| 22 | import * as ServerSettings from "../../../serverSettings.ts"; |
| 23 | import * as ThreadLaunchService from "../../../orchestration-v2/ThreadLaunchService.ts"; |
| 24 | import * as Crypto from "effect/Crypto"; |
| 25 | import * as Schema from "effect/Schema"; |
| 26 | import { Tool, Toolkit } from "effect/ai"; |
| 27 | import * as ProjectService from "../../../project/ProjectService.ts"; |
| 28 | import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts"; |
| 29 | import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts"; |
| 30 | import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts"; |
| 31 | import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts"; |
| 32 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; |
| 33 | |
| 34 | const shared = { |
| 35 | success: Project, |
| 36 | failure: OrchestratorMcpFailure, |
| 37 | failureMode: "return" as const, |
| 38 | dependencies: [ |
| 39 | McpInvocationContext.McpInvocationContext, |
| 40 | ThreadManagementService.ThreadManagementService, |
| 41 | ProjectService.ProjectService, |
| 42 | Crypto.Crypto, |
| 43 | ], |
| 44 | }; |
| 45 | const ProjectListTool = Tool.make("t3_project_list", { |
| 46 | ...shared, |
| 47 | description: |
| 48 | "List registered projects in this environment. Pages use the current project snapshot and may shift between calls.", |
| 49 | parameters: Schema.Struct({ |
| 50 | cursor: Schema.optional(NonNegativeInt), |
| 51 | limit: Schema.optional(Schema.Int.check(Schema.isBetween({ minimum: 1, maximum: 100 }))), |
| 52 | }), |
| 53 | success: Schema.Struct({ |
| 54 | projects: Schema.Array(Project), |
| 55 | nextCursor: Schema.NullOr(NonNegativeInt), |
| 56 | }), |
| 57 | }) |
| 58 | .annotate(Tool.Readonly, true) |
| 59 | .annotate(Tool.Destructive, false); |
| 60 | const ProjectReadTool = Tool.make("t3_project_read", { |
| 61 | ...shared, |
| 62 | description: |
| 63 | "Read a registered project in this environment, including its workspace and saved scripts.", |
| 64 | parameters: Schema.Struct({ projectId: ProjectId }), |
| 65 | }) |
| 66 | .annotate(Tool.Readonly, true) |
| 67 | .annotate(Tool.Destructive, false); |
| 68 | const ProjectCreateTool = Tool.make("t3_project_create", { |
| 69 | ...shared, |
| 70 | description: |
| 71 | "Register a project directory through the existing project service. Set createWorkspaceRootIfMissing to create a directory. Omit workspaceRoot to start a new project from just its title: the app makes a Git repository for it in its own projects folder, with a README, an icon, and a first commit (commitError says why a commit failed; the project exists either way). Each call creates a new request; an existing registered workspace is rejected. Clone separately with t3_project_clone when needed.", |
| 72 | parameters: Schema.Struct({ |
| 73 | ...ProjectCreatePayload.fields, |
| 74 | workspaceRoot: Schema.optional(ProjectCreatePayload.fields.workspaceRoot), |
| 75 | }), |
| 76 | success: Schema.Struct({ ...Project.fields, commitError: Schema.optional(Schema.String) }), |
| 77 | dependencies: [...shared.dependencies, ManagedProjectFolders.ManagedProjectFolders], |
| 78 | }).annotate(Tool.Destructive, true); |
| 79 | const ProjectUpdateTool = Tool.make("t3_project_update", { |
| 80 | ...shared, |
| 81 | description: |
| 82 | "Update a registered project's settings. Omitted fields are preserved. Uses the same project service as the app.", |
| 83 | parameters: Schema.Struct({ projectId: ProjectId, ...ProjectUpdatePayload.fields }), |
| 84 | }).annotate(Tool.Destructive, true); |
| 85 | const ProjectDeleteTool = Tool.make("t3_project_delete", { |
| 86 | ...shared, |
| 87 | description: |
| 88 | "Delete a project using the existing project deletion lifecycle. Nonempty projects require force=true. This does not delete the repository directory or promise a deleted-thread count.", |
| 89 | parameters: Schema.Struct({ projectId: ProjectId, force: Schema.optionalKey(Schema.Boolean) }), |
| 90 | }).annotate(Tool.Destructive, true); |
| 91 | const ProjectCloneTool = Tool.make("t3_project_clone", { |
| 92 | ...shared, |
| 93 | description: |
| 94 | "Clone a repository using the app's source-control service. This only clones; register the returned cwd with t3_project_create. An existing destination is not adopted or removed on failure.", |
| 95 | parameters: SourceControlCloneRepositoryInput, |
| 96 | success: SourceControlCloneRepositoryResult, |
| 97 | dependencies: [ |
| 98 | ...shared.dependencies, |
| 99 | SourceControlRepositoryService.SourceControlRepositoryService, |
| 100 | ], |
| 101 | }) |
| 102 | .annotate(Tool.Destructive, true) |
| 103 | .annotate(Tool.OpenWorld, true); |
| 104 | const ThreadLaunchTool = Tool.make("t3_thread_launch", { |
| 105 | ...shared, |
| 106 | description: |
| 107 | 'Create an ordinary TOP-LEVEL thread with an explicit workspace binding before its agent starts. Use this when the user requests independent work, a new thread, or a PR stack in its own worktree; use delegate_task for child subagents. Set workspaceStrategy to {type:"worktree",baseRef:"parent-branch",branch:"new-branch",startFromOrigin:false} for a new worktree based on local commits, or {type:"existing_worktree",worktreePath:"/absolute/path",branch:"existing-branch"} to use an existing checkout. For upstream commits, set startFromOrigin:true. Omitted workspaceStrategy means the project root, NOT the caller\'s worktree. Omit projectId/modelSelection/modes to inherit those settings from the calling thread; a caller outside a T3 thread must pass projectId and gets the project\'s default model. Set scratch:true instead of projectId for a thread without a project: it runs in a fresh folder of its own, outside any repository. Put the task in message. Do not ask the agent to create its own worktree via shell: that does not update the thread binding. Each call creates a new launch with no retry key; retain threadId and use t3_thread_read/t3_thread_wait to follow preparation. To link a thread for the user, write `[title](t3-thread://v1/<threadId>)` with the threadId exactly as returned, not URL-encoded; T3 Code shows the thread\'s current title. After errors or lost responses, inspect t3_thread_list before retrying. Attachments must be pending uploads. The new thread may not run with broader runtime or interaction modes than the caller: the calling T3 thread\'s own modes, or the permission mode an outside agent was approved with.', |
| 108 | parameters: Schema.Struct({ |
| 109 | projectId: Schema.optional(ProjectId), |
| 110 | scratch: Schema.optional( |
| 111 | Schema.Boolean.annotate({ |
| 112 | description: |
| 113 | "Launch without a project, in its own folder under the environment's Scratch project. Not with projectId or workspaceStrategy.", |
| 114 | }), |
| 115 | ), |
| 116 | title: TrimmedNonEmptyString, |
| 117 | modelSelection: Schema.optional(ModelSelection), |
| 118 | runtimeMode: Schema.optional(RuntimeMode), |
| 119 | interactionMode: Schema.optional(ProviderInteractionMode), |
| 120 | workspaceStrategy: Schema.optional( |
| 121 | OrchestrationV2ThreadLaunchWorkspaceStrategy.annotate({ |
| 122 | description: |
| 123 | "Choose where this thread runs before starting its agent: worktree creates and binds a new checkout from baseRef; existing_worktree binds worktreePath, which must be one of the project's git worktrees; root uses the project checkout. Omitted means root, not the caller's worktree. For a PR stack use the parent branch as baseRef and startFromOrigin:false. Uncommitted changes are not copied.", |
| 124 | }), |
| 125 | ), |
| 126 | message: Schema.optional( |
| 127 | Schema.String.check(Schema.isMaxLength(120000)).annotate({ |
| 128 | description: |
| 129 | "First task prompt, delivered after workspace preparation. Omit message and attachments to create an idle thread.", |
| 130 | }), |
| 131 | ), |
| 132 | attachments: Schema.optional(Schema.Array(McpAttachmentInput).check(Schema.isMaxLength(8))), |
| 133 | }), |
| 134 | success: Schema.Struct({ |
| 135 | threadId: ThreadId, |
| 136 | projectId: ProjectId, |
| 137 | modelSelection: ModelSelection, |
| 138 | runId: Schema.NullOr(RunId), |
| 139 | status: Schema.NullOr(OrchestrationV2RunStatus), |
| 140 | }), |
| 141 | dependencies: [ |
| 142 | ...shared.dependencies, |
| 143 | ThreadLaunchService.ThreadLaunchService, |
| 144 | ManagedProjectFolders.ManagedProjectFolders, |
| 145 | GitVcsDriver.GitVcsDriver, |
| 146 | FileSystem.FileSystem, |
| 147 | ServerConfig.ServerConfig, |
| 148 | ServerSettings.ServerSettingsService, |
| 149 | ], |
| 150 | }) |
| 151 | .annotate(Tool.Destructive, true) |
| 152 | .annotate(Tool.OpenWorld, true); |
| 153 | |
| 154 | export const ProjectToolkit = Toolkit.make( |
| 155 | ThreadLaunchTool, |
| 156 | ProjectListTool, |
| 157 | ProjectReadTool, |
| 158 | ProjectCreateTool, |
| 159 | ProjectUpdateTool, |
| 160 | ProjectDeleteTool, |
| 161 | ProjectCloneTool, |
| 162 | ); |