1import { McpAttachmentInput } from "../attachment/input.ts";
2import {
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";
20import * as FileSystem from "effect/FileSystem";
21import * as ServerConfig from "../../../config.ts";
22import * as ServerSettings from "../../../serverSettings.ts";
23import * as ThreadLaunchService from "../../../orchestration-v2/ThreadLaunchService.ts";
24import * as Crypto from "effect/Crypto";
25import * as Schema from "effect/Schema";
26import { Tool, Toolkit } from "effect/ai";
27import * as ProjectService from "../../../project/ProjectService.ts";
28import * as ManagedProjectFolders from "../../../project/ManagedProjectFolders.ts";
29import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
30import * as SourceControlRepositoryService from "../../../sourceControl/SourceControlRepositoryService.ts";
31import * as GitVcsDriver from "../../../vcs/GitVcsDriver.ts";
32import * as McpInvocationContext from "../../McpInvocationContext.ts";
33
34const 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};
45const 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);
60const 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);
68const 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);
79const 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);
85const 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);
91const 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);
104const 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
154export const ProjectToolkit = Toolkit.make(
155 ThreadLaunchTool,
156 ProjectListTool,
157 ProjectReadTool,
158 ProjectCreateTool,
159 ProjectUpdateTool,
160 ProjectDeleteTool,
161 ProjectCloneTool,
162);