1import {
2 OrchestratorMcpCapabilitiesResult,
3 OrchestrationV2StopWorkflowInput,
4 OrchestratorMcpCreateThreadsInput,
5 OrchestratorMcpCreateThreadsResult,
6 OrchestratorMcpDelegateTaskInput,
7 OrchestratorMcpDelegateTaskResult,
8 OrchestratorMcpDeleteScheduledTaskInput,
9 OrchestratorMcpDeleteScheduledTaskResult,
10 OrchestratorMcpRequestSecretInput,
11 OrchestratorMcpRequestSecretResult,
12 OrchestratorMcpFailure,
13 OrchestratorMcpListScheduledTasksInput,
14 OrchestratorMcpListScheduledTasksResult,
15 OrchestratorMcpScheduleTaskInput,
16 OrchestratorMcpScheduleTaskResult,
17 OrchestratorMcpTaskCancelInput,
18 OrchestratorMcpTaskCancelResult,
19 OrchestratorMcpUpdateScheduledTaskInput,
20 OrchestratorMcpTaskStatusInput,
21 OrchestratorMcpThreadInterruptInput,
22 OrchestratorMcpThreadInterruptResult,
23 OrchestratorMcpThreadListInput,
24 OrchestratorMcpThreadListResult,
25 OrchestratorMcpThreadReadInput,
26 OrchestratorMcpThreadReadResult,
27 OrchestratorMcpThreadSendInput,
28 OrchestratorMcpThreadSendResult,
29 OrchestratorMcpThreadWaitInput,
30 OrchestratorMcpThreadWaitResult,
31 ThreadMetadataMcpUpdateInput,
32 ThreadMetadataMcpUpdateResult,
33} from "@t3tools/contracts";
34import { Tool, Toolkit } from "effect/ai";
35import * as Schema from "effect/Schema";
36
37import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts";
38import * as McpInvocationContext from "../../McpInvocationContext.ts";
39import * as OrchestratorMcpService from "../../OrchestratorMcpService.ts";
40import * as ThreadMetadataMcpService from "../../ThreadMetadataMcpService.ts";
41
42const dependencies = [
43 McpInvocationContext.McpInvocationContext,
44 ThreadManagementService.ThreadManagementService,
45 OrchestratorMcpService.OrchestratorMcpService,
46];
47const threadMetadataDependencies = [
48 McpInvocationContext.McpInvocationContext,
49 ThreadManagementService.ThreadManagementService,
50 ThreadMetadataMcpService.ThreadMetadataMcpService,
51];
52
53const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", {
54 description:
55 "List the V2 provider instances and their current models from the same live catalog as the composer, including configured custom models, inherited runtime settings, and app-owned orchestration features available to this caller. For a separate top-level thread in a new or existing worktree, use t3_thread_launch with workspaceStrategy.",
56 success: OrchestratorMcpCapabilitiesResult,
57 failure: OrchestratorMcpFailure,
58 failureMode: "return",
59 dependencies,
60})
61 .annotate(Tool.Title, "Get orchestration capabilities")
62 .annotate(Tool.Readonly, true)
63 .annotate(Tool.Destructive, false)
64 .annotate(Tool.Idempotent, true);
65
66export const DelegateTaskTool = Tool.make("delegate_task", {
67 description:
68 "Needs an agent running inside a T3 thread. Delegate one task to a T3-owned child agent/subagent of THIS thread and run it with only the supplied task prompt, without copying parent conversation history. Choose providers and models from orchestrator_capabilities, which uses the same live catalog as the composer. Prefer native subagent tools for same-provider work only when they support the chosen model. Use this for any model missing from the native tool, including same-provider work, for cross-provider work, or for explicitly T3-owned child tasks. For every T3 delegated review round, call delegate_task again with the original brief, prior findings, responses, and unresolved objections in the task prompt. Track each round by its own taskId and use a distinct clientRequestId per round, stable across retries of that round. The childThreadId is backing storage, not the target for starting another delegated review round through t3_thread_send. Set isolation to worktree for a separate checkout based on the parent local commit, with project setup completed before the child starts. Uncommitted edits are not copied; omitted isolation shares the parent checkout. Provider, model, model options (see orchestrator_capabilities), runtime mode, and interaction mode inherit unless target overrides them. A requested runtimeMode must be one the target provider lists in orchestrator_capabilities runtimeModes; an inherited one steps down to the broadest mode it offers. Leave runtimeMode unset unless the user asks for a supervised child: approval-required and auto-accept-edits usually make the child wait for a person at each action that needs approval. Prefer mode='async' for long work; mode='wait' blocks until completion, a blocking question, or timeout. If hasPendingQuestion, pass threadId=childThreadId to t3_pending_request_list, t3_pending_request_read (with requestId) and t3_pending_request_respond, or ask the user; the task's completion still wakes this thread. timeoutMs on mode=wait is only the parent's wait budget and does not cancel the child. waitTimedOut on that wait call means the timeout fired; keep that taskId and read status on later task_status. An async child's completion wakes this thread through a notification, steered into active turns where supported or queued otherwise, so end the turn instead of polling or spawning watchers; use task_status only when the result is needed mid-turn.",
69 parameters: OrchestratorMcpDelegateTaskInput,
70 success: OrchestratorMcpDelegateTaskResult,
71 failure: OrchestratorMcpFailure,
72 failureMode: "return",
73 dependencies,
74})
75 .annotate(Tool.Title, "Delegate a child task")
76 .annotate(Tool.Destructive, true)
77 .annotate(Tool.OpenWorld, true);
78
79const TaskStatusTool = Tool.make("task_status", {
80 description:
81 "Needs an agent running inside a T3 thread. Read a T3-owned delegated task created by this parent thread. childRunId identifies the original delegated run. workState distinguishes working, waiting_for_children, and result_available; a completed turn with live nested work is not a completed task. summary is the final task result, including provider errors on failure, and remains stable after publication. hasPendingChildRuns reports later queued or executing turns in the backing child thread, even after the task is terminal; it does not reopen the task, and task_cancel stops those turns too. Turns held in a stopped queue wait for the user and do not count. latestTerminal* provides later non-monitor turn results. Reading a terminal result acknowledges its automatic parent delivery.",
82 parameters: OrchestratorMcpTaskStatusInput,
83 success: OrchestratorMcpDelegateTaskResult,
84 failure: OrchestratorMcpFailure,
85 failureMode: "return",
86 dependencies,
87})
88 .annotate(Tool.Title, "Get delegated task status")
89 .annotate(Tool.Readonly, false)
90 .annotate(Tool.Destructive, false)
91 .annotate(Tool.Idempotent, true);
92
93const TaskCancelTool = Tool.make("task_cancel", {
94 description:
95 "Needs an agent running inside a T3 thread. Stop a T3-owned delegated task and dispose its automatic parent delivery. Its child thread stops like a user Stop: the running turn is interrupted, queued turns are held, pull request watches end, and the tasks it delegated stop too. This includes later child-thread runs, even after the task is terminal. A terminal task returns its existing status, and published task results remain available.",
96 parameters: OrchestratorMcpTaskCancelInput,
97 success: OrchestratorMcpTaskCancelResult,
98 failure: OrchestratorMcpFailure,
99 failureMode: "return",
100 dependencies,
101})
102 .annotate(Tool.Title, "Cancel delegated task")
103 .annotate(Tool.Destructive, true);
104
105const WorkflowStopTool = Tool.make("t3_workflow_stop", {
106 description:
107 "Stop one running Claude workflow in a T3 thread. Use the workflow coordinator's subagentId returned by t3_thread_read with view='activity'. Stops the whole workflow, including its phases and agents. Does not stop the parent thread or other workflows. Individual workflow phases and agents cannot be stopped separately.",
108 parameters: OrchestrationV2StopWorkflowInput,
109 success: Schema.Struct({}),
110 failure: OrchestratorMcpFailure,
111 failureMode: "return",
112 dependencies,
113})
114 .annotate(Tool.Title, "Stop a workflow")
115 .annotate(Tool.Destructive, true);
116
117export const ScheduleTaskTool = Tool.make("schedule_task", {
118 description:
119 "Create persistent work in the app scheduler that runs even when no turn is active. Pass schedule as a STRUCTURED OBJECT, never JSON text. Timers: {type:'interval', everyMs:3600000} is hourly; {type:'fixed_time', timeOfDay:'09:00', weekdays:[1,2,3,4,5]} is weekday mornings; report the returned nextRunAt. Webhooks: {type:'webhook'} runs once per request to a generated URL. The run sees the request ONLY through prompt placeholders: {{body.path}} (e.g. {{body.action}}, {{body.release.tag_name}}), {{headers.name}}, {{query.name}}, {{body}}, or {{request}} (method, headers with credentials redacted, and body). For a sender that signs requests, first call request_secret so the user enters the secret privately (never ask for it in chat or invent one), then set signature with the returned secretRef, e.g. GitHub: {type:'webhook', signature:{header:'x-hub-signature-256', encoding:'hex', prefix:'sha256=', secretRef}}. The result's webhookUrl is the public URL to give the user; if it is absent, this environment has no T3 Connect managed tunnel, so tell the user to enable T3 Connect remote access rather than sharing a path. Omit projectId for this thread's project. In this thread's project, runs post into THIS thread by default (bindToCurrentThread=true), which suits an orchestrator that sees every trigger, delegates work, and can dedupe against what is in flight; use false only when the user wants a fresh top-level thread per run. Elsewhere each run launches a fresh thread. Provider, model, and runtime settings inherit from this thread, or from the project default when there is no calling thread.",
120 parameters: OrchestratorMcpScheduleTaskInput,
121 success: OrchestratorMcpScheduleTaskResult,
122 failure: OrchestratorMcpFailure,
123 failureMode: "return",
124 dependencies,
125})
126 .annotate(Tool.Title, "Schedule a recurring task")
127 .annotate(Tool.Destructive, true)
128 .annotate(Tool.OpenWorld, true);
129
130const ListScheduledTasksTool = Tool.make("list_scheduled_tasks", {
131 parameters: OrchestratorMcpListScheduledTasksInput,
132 description:
133 "List recurring scheduled tasks in a project (omit projectId for the calling thread's project, or every project when there is no calling thread), including their id, schedule, prompt, enabled state, bound thread, next run time, and last run status. Use the returned scheduledTaskId with update_scheduled_task or delete_scheduled_task.",
134 success: OrchestratorMcpListScheduledTasksResult,
135 failure: OrchestratorMcpFailure,
136 failureMode: "return",
137 dependencies,
138})
139 .annotate(Tool.Title, "List scheduled tasks")
140 .annotate(Tool.Readonly, true)
141 .annotate(Tool.Destructive, false)
142 .annotate(Tool.Idempotent, true);
143
144const UpdateScheduledTaskTool = Tool.make("update_scheduled_task", {
145 description:
146 "Update an existing scheduled task by scheduledTaskId (from list_scheduled_tasks). Only the provided fields change; omit a field to leave it as-is. Use enabled=false to pause a task without deleting it. Set bindToCurrentThread to move the task between posting into this thread and launching a fresh thread per run.",
147 parameters: OrchestratorMcpUpdateScheduledTaskInput,
148 success: OrchestratorMcpScheduleTaskResult,
149 failure: OrchestratorMcpFailure,
150 failureMode: "return",
151 dependencies,
152})
153 .annotate(Tool.Title, "Update a scheduled task")
154 .annotate(Tool.Destructive, true);
155
156const DeleteScheduledTaskTool = Tool.make("delete_scheduled_task", {
157 description:
158 "Permanently delete a scheduled task by scheduledTaskId (from list_scheduled_tasks). The task stops running immediately. To keep it but stop runs, use update_scheduled_task with enabled=false instead.",
159 parameters: OrchestratorMcpDeleteScheduledTaskInput,
160 success: OrchestratorMcpDeleteScheduledTaskResult,
161 failure: OrchestratorMcpFailure,
162 failureMode: "return",
163 dependencies,
164})
165 .annotate(Tool.Title, "Delete a scheduled task")
166 .annotate(Tool.Destructive, true);
167
168const RequestSecretTool = Tool.make("request_secret", {
169 description:
170 "Ask the user for a secret (a token, API key, signing secret, password) through a private card in this thread, and wait for them to answer. The value is kept by the app and NEVER returned to you or shown in the transcript. When saved, the result carries a secretRef: pass it to a tool that accepts one (e.g. schedule_task's signature.secretRef). It works once. Never ask for secrets in chat, and never invent one.",
171 parameters: OrchestratorMcpRequestSecretInput,
172 success: OrchestratorMcpRequestSecretResult,
173 failure: OrchestratorMcpFailure,
174 failureMode: "return",
175 dependencies,
176})
177 .annotate(Tool.Title, "Request a secret from the user")
178 .annotate(Tool.Destructive, false);
179
180export const CreateThreadsTool = Tool.make("create_threads", {
181 description:
182 "Needs an agent running inside a T3 thread. Create one or more ORDINARY TOP-LEVEL T3 conversations. This is not delegation and does not create child agents/subagents. For delegated work, choose models from orchestrator_capabilities. Prefer native subagents only when they support the chosen model; otherwise call delegate_task, including for same-provider work. Use create_threads for a batch of separate top-level threads sharing this checkout. Prefer t3_thread_launch for a single thread. Both require the user to request separate/new/top-level threads or conversations. Each entry may override provider, model, options, runtime mode, and interaction mode; omitted settings inherit. Project, branch, and worktree always inherit and cannot be overridden here. For independent implementation or a PR stack in its own worktree, use t3_thread_launch with workspaceStrategy instead of asking the agent to create a worktree in its prompt.",
183 parameters: OrchestratorMcpCreateThreadsInput,
184 success: OrchestratorMcpCreateThreadsResult,
185 failure: OrchestratorMcpFailure,
186 failureMode: "return",
187 dependencies,
188})
189 .annotate(Tool.Title, "Create T3 threads")
190 .annotate(Tool.Destructive, true)
191 .annotate(Tool.OpenWorld, true);
192
193const ThreadListTool = Tool.make("t3_thread_list", {
194 description:
195 "List T3 threads in a project, newest first. Omit projectId for the calling thread's project. Filter by durable run status, title, or settled state (settled=true lists threads the user or auto-settlement moved out of the active list), or snoozed state, and paginate with the returned cursor. A snoozed thread wakes early when it asks for something, fails, or completes. 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.",
196 parameters: OrchestratorMcpThreadListInput,
197 success: OrchestratorMcpThreadListResult,
198 failure: OrchestratorMcpFailure,
199 failureMode: "return",
200 dependencies,
201})
202 .annotate(Tool.Title, "List T3 threads")
203 .annotate(Tool.Readonly, true)
204 .annotate(Tool.Destructive, false)
205 .annotate(Tool.Idempotent, true);
206
207const ThreadReadTool = Tool.make("t3_thread_read", {
208 description:
209 "Read durable state and a paginated timeline from any T3 thread in this environment. The default messages view returns user messages, assistant messages, and proposed plans; activity returns all summarized timeline items. Reading an untruncated terminal assistant result from this parent thread's direct app-owned child acknowledges that child's automatic completion delivery. Continue with afterPosition=nextPosition. Recover long item text with itemId and textOffset=nextTextOffset until nextTextOffset is null; offsets count UTF-16 code units. The thread also reports its snooze state. 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.",
210 parameters: OrchestratorMcpThreadReadInput,
211 success: OrchestratorMcpThreadReadResult,
212 failure: OrchestratorMcpFailure,
213 failureMode: "return",
214 dependencies,
215})
216 .annotate(Tool.Title, "Read a T3 thread")
217 // Strict so a misspelled paging param fails instead of silently reading from position 0.
218 .annotate(Tool.Strict, true)
219 .annotate(Tool.Readonly, false)
220 .annotate(Tool.Destructive, false)
221 .annotate(Tool.Idempotent, true);
222
223export const ThreadUpdateTool = Tool.make("t3_thread_update", {
224 description:
225 "Update metadata for a thread. Omit threadId to update this thread. Use action='rename' with title, action='regenerate_title' with no extra field, action='link_pull_request' with pullRequest, or action='unlink_pull_request'. Workspace and branch changes are intentionally not supported. clientRequestId makes retries idempotent.",
226 parameters: ThreadMetadataMcpUpdateInput,
227 success: ThreadMetadataMcpUpdateResult,
228 failure: OrchestratorMcpFailure,
229 failureMode: "return",
230 dependencies: threadMetadataDependencies,
231})
232 .annotate(Tool.Title, "Update T3 thread metadata")
233 .annotate(Tool.Destructive, true)
234 .annotate(Tool.Idempotent, false);
235
236const ThreadSendTool = Tool.make("t3_thread_send", {
237 description:
238 "Send a message to any T3 thread in this environment. The target cannot have broader permission modes than the caller. Use delegate_task with the full review context and a new clientRequestId for independent review rounds. For a follow-up that needs the same child's conversation, send to its childThreadId. After the original task completes, a requested follow-up creates a fresh task in that child thread and returns taskId; its completion notifies the parent, and the original result stays unchanged. mode='auto' starts an idle thread, steers a fully active turn, or queues behind a turn that is not yet steerable. Use queue for a separate follow-up turn, steer for an in-flight update, or restart to interrupt-and-restart the active turn. clientRequestId makes retries idempotent.",
239 parameters: OrchestratorMcpThreadSendInput,
240 success: OrchestratorMcpThreadSendResult,
241 failure: OrchestratorMcpFailure,
242 failureMode: "return",
243 dependencies,
244})
245 .annotate(Tool.Title, "Send to a T3 thread")
246 .annotate(Tool.Destructive, true)
247 .annotate(Tool.OpenWorld, true);
248
249const ThreadWaitTool = Tool.make("t3_thread_wait", {
250 description:
251 "Wait for a T3 thread run to reach a terminal durable state. Without runId, the latest run at call time is selected; an idle thread returns immediately. Timeout does not interrupt work, so call again or use t3_thread_read/list after timedOut=true. Waiting reports status only and does not acknowledge a delegated result.",
252 parameters: OrchestratorMcpThreadWaitInput,
253 success: OrchestratorMcpThreadWaitResult,
254 failure: OrchestratorMcpFailure,
255 failureMode: "return",
256 dependencies,
257})
258 .annotate(Tool.Title, "Wait for a T3 thread")
259 .annotate(Tool.Readonly, true)
260 .annotate(Tool.Destructive, false)
261 .annotate(Tool.Idempotent, true);
262
263const ThreadInterruptTool = Tool.make("t3_thread_interrupt", {
264 description:
265 "Request interruption of a running turn in any T3 thread in this environment. Without runId, the newest interruptible run is selected. Terminal runs and threads without an active turn return without another side effect. clientRequestId makes retries idempotent.",
266 parameters: OrchestratorMcpThreadInterruptInput,
267 success: OrchestratorMcpThreadInterruptResult,
268 failure: OrchestratorMcpFailure,
269 failureMode: "return",
270 dependencies,
271})
272 .annotate(Tool.Title, "Interrupt a T3 thread")
273 .annotate(Tool.Destructive, true);
274
275export const OrchestratorToolkit = Toolkit.make(
276 OrchestratorCapabilitiesTool,
277 DelegateTaskTool,
278 TaskStatusTool,
279 TaskCancelTool,
280 WorkflowStopTool,
281 ScheduleTaskTool,
282 ListScheduledTasksTool,
283 UpdateScheduledTaskTool,
284 DeleteScheduledTaskTool,
285 RequestSecretTool,
286 CreateThreadsTool,
287 ThreadListTool,
288 ThreadReadTool,
289 ThreadUpdateTool,
290 ThreadSendTool,
291 ThreadWaitTool,
292 ThreadInterruptTool,
293);