| 1 | import { |
| 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"; |
| 34 | import { Tool, Toolkit } from "effect/ai"; |
| 35 | import * as Schema from "effect/Schema"; |
| 36 | |
| 37 | import * as ThreadManagementService from "../../../orchestration-v2/ThreadManagementService.ts"; |
| 38 | import * as McpInvocationContext from "../../McpInvocationContext.ts"; |
| 39 | import * as OrchestratorMcpService from "../../OrchestratorMcpService.ts"; |
| 40 | import * as ThreadMetadataMcpService from "../../ThreadMetadataMcpService.ts"; |
| 41 | |
| 42 | const dependencies = [ |
| 43 | McpInvocationContext.McpInvocationContext, |
| 44 | ThreadManagementService.ThreadManagementService, |
| 45 | OrchestratorMcpService.OrchestratorMcpService, |
| 46 | ]; |
| 47 | const threadMetadataDependencies = [ |
| 48 | McpInvocationContext.McpInvocationContext, |
| 49 | ThreadManagementService.ThreadManagementService, |
| 50 | ThreadMetadataMcpService.ThreadMetadataMcpService, |
| 51 | ]; |
| 52 | |
| 53 | const 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 | |
| 66 | export 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 | |
| 79 | const 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 | |
| 93 | const 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 | |
| 105 | const 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 | |
| 117 | export 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 | |
| 130 | const 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 | |
| 144 | const 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 | |
| 156 | const 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 | |
| 168 | const 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 | |
| 180 | export 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 | |
| 193 | const 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 | |
| 207 | const 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 | |
| 223 | export 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 | |
| 236 | const 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 | |
| 249 | const 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 | |
| 263 | const 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 | |
| 275 | export 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 | ); |