Unified · Split
| @@ -668,6 +668,27 @@ describe("orchestrator MCP toolkit", () => { · 667 unmodified lines | ||
| 668 | 668 | }); |
| 669 | 669 | expect(yield* Ref.get(scheduledStore)).toHaveLength(0); |
| 670 | 670 | |
| 671 | + // OpenCode 1.15 has emitted this exact nested-object-as-JSON-string | |
| 672 | + // shape. Decode it at the MCP boundary rather than failing a task | |
| 673 | + // the model otherwise specified correctly. | |
| 674 | + const serializedScheduleCall = yield* invoke("schedule_task", { | |
| 675 | + prompt: "check for new pull requests", | |
| 676 | + schedule: '{"type":"interval","everyMs":3600000}', | |
| 677 | + clientRequestId: "schedule-opencode-compat-1", | |
| 678 | + }); | |
| 679 | + expect(serializedScheduleCall.isError).toBe(false); | |
| 680 | + expect(serializedScheduleCall.structuredContent).toMatchObject({ | |
| 681 | + schedule: { type: "interval", everyMs: 3_600_000 }, | |
| 682 | + boundThreadId: parentThreadId, | |
| 683 | + }); | |
| 684 | + const serializedScheduledTaskId = ( | |
| 685 | + serializedScheduleCall.structuredContent as { scheduledTaskId: string } | |
| 686 | + ).scheduledTaskId; | |
| 687 | + yield* invoke("delete_scheduled_task", { | |
| 688 | + scheduledTaskId: serializedScheduledTaskId, | |
| 689 | + }); | |
| 690 | + expect(yield* Ref.get(scheduledStore)).toHaveLength(0); | |
| 691 | + | |
| 671 | 692 | const delegatedCall = yield* invoke("delegate_task", { |
| 672 | 693 | task: delegatedPrompt, |
| 673 | 694 | target: { |
| @@ -0,0 +1,28 @@ | ||
| 1 | +import { assert, describe, it } from "@effect/vitest"; | |
| 2 | +import { Tool } from "effect/unstable/ai"; | |
| 3 | + | |
| 4 | +import { CreateThreadsTool, DelegateTaskTool, ScheduleTaskTool } from "./tools.ts"; | |
| 5 | + | |
| 6 | +describe("orchestrator MCP tool guidance", () => { | |
| 7 | + it("directs subagent requests to delegation instead of ordinary threads", () => { | |
| 8 | + assert.include(DelegateTaskTool.description ?? "", "child agent/subagent"); | |
| 9 | + assert.include(DelegateTaskTool.description ?? "", "cross-provider"); | |
| 10 | + assert.include(CreateThreadsTool.description ?? "", "not delegation"); | |
| 11 | + assert.include(CreateThreadsTool.description ?? "", "call delegate_task"); | |
| 12 | + }); | |
| 13 | + | |
| 14 | + it("publishes an actionable schedule schema and compatibility string branch", () => { | |
| 15 | + const schema = Tool.getJsonSchema(ScheduleTaskTool) as { | |
| 16 | + readonly type?: unknown; | |
| 17 | + readonly properties?: Readonly< | |
| 18 | + Record<string, { readonly description?: unknown; readonly anyOf?: ReadonlyArray<unknown> }> | |
| 19 | + >; | |
| 20 | + }; | |
| 21 | + | |
| 22 | + assert.equal(schema.type, "object"); | |
| 23 | + assert.isString(schema.properties?.schedule?.description); | |
| 24 | + assert.isAtLeast(schema.properties?.schedule?.anyOf?.length ?? 0, 2); | |
| 25 | + assert.include(ScheduleTaskTool.description ?? "", "STRUCTURED OBJECT"); | |
| 26 | + assert.include(ScheduleTaskTool.description ?? "", "nextRunAt"); | |
| 27 | + }); | |
| 28 | +}); | |
| @@ -49,7 +49,7 @@ export const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities · 48 unmodified lines | ||
| 49 | 49 | |
| 50 | 50 | export const DelegateTaskTool = Tool.make("delegate_task", { |
| 51 | 51 | description: |
| 52 | − "Create a T3-owned child agent thread and run it with only the supplied task prompt, without copying parent conversation history. Provider, model, model options (e.g. reasoning effort — see orchestrator_capabilities for valid ids), runtime mode, and interaction mode inherit from the parent unless overridden via target. Prefer mode='async' and poll task_status for long work; mode='wait' blocks until completion or timeout.", | |
| 52 | + "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. Use this whenever the user asks for an agent, subagent, worker, delegated task, or parallel help—including cross-provider work. The childThreadId is backing storage, not an ordinary top-level thread. Provider, model, model options (see orchestrator_capabilities), runtime mode, and interaction mode inherit unless target overrides them. Prefer mode='async' and poll task_status for long work; mode='wait' blocks until completion or timeout.", | |
| 53 | 53 | parameters: OrchestratorMcpDelegateTaskInput, |
| 54 | 54 | success: OrchestratorMcpDelegateTaskResult, |
| 55 | 55 | failure: OrchestratorMcpFailure, |
| @@ -88,7 +88,7 @@ export const TaskCancelTool = Tool.make("task_cancel", { · 32 unmodified lines | ||
| 88 | 88 | |
| 89 | 89 | export const ScheduleTaskTool = Tool.make("schedule_task", { |
| 90 | 90 | description: |
| 91 | − "Create a recurring scheduled task that automatically runs a prompt on a schedule. Use this to set up autonomous recurring work — the app's scheduler fires it even when no turn is active. By default (bindToCurrentThread=true) each run posts into THIS thread, which is how you 'wake up' and continue here on an interval or at a fixed time; set bindToCurrentThread=false to launch a fresh thread each run instead. Provider, model, and runtime settings inherit from the calling thread. Schedule is either {type:'interval', everyMs} (e.g. everyMs=60000 for every minute) or {type:'fixed_time', timeOfDay:'HH:MM', weekdays?:[0-6] where 0=Sunday}.", | |
| 91 | + "Create persistent recurring work in the app scheduler, which runs even when no turn is active. Pass schedule as a STRUCTURED OBJECT, never JSON text: {type:'interval', everyMs:3600000} means hourly; {type:'fixed_time', timeOfDay:'09:00', weekdays:[1,2,3,4,5]} means weekday mornings. By default (bindToCurrentThread=true) each run posts into THIS thread; use false only when the user wants a fresh top-level thread per run. Provider, model, and runtime settings inherit from this thread. Report the returned schedule and nextRunAt after success.", | |
| 92 | 92 | parameters: OrchestratorMcpScheduleTaskInput, |
| 93 | 93 | success: OrchestratorMcpScheduleTaskResult, |
| 94 | 94 | failure: OrchestratorMcpFailure, |
| @@ -138,7 +138,7 @@ export const DeleteScheduledTaskTool = Tool.make("delete_scheduled_task", { · 43 unmodified lines | ||
| 138 | 138 | |
| 139 | 139 | export const CreateThreadsTool = Tool.make("create_threads", { |
| 140 | 140 | description: |
| 141 | − "Create one or more ordinary top-level T3 V2 threads. Each entry may have its own prompt, title, provider instance or driver, model, model options (e.g. reasoning effort), runtime mode, and interaction mode. Omitted provider/model/settings inherit from the calling thread; entries without prompts create empty threads.", | |
| 141 | + "Create one or more ORDINARY TOP-LEVEL T3 conversations. This is not delegation and does not create child agents/subagents. If the user asks for agents, subagents, workers, delegation, or parallel help, call delegate_task once per child instead—even when selecting different providers. Use create_threads only when the user explicitly asks for separate/new/top-level threads or conversations. Each entry may override provider, model, options, runtime mode, and interaction mode; omitted settings inherit.", | |
| 142 | 142 | parameters: OrchestratorMcpCreateThreadsInput, |
| 143 | 143 | success: OrchestratorMcpCreateThreadsResult, |
| 144 | 144 | failure: OrchestratorMcpFailure, |
| @@ -151,7 +151,7 @@ export const CreateThreadsTool = Tool.make("create_threads", { · 6 unmodified lines | ||
| 151 | 151 | |
| 152 | 152 | export const ThreadStartTool = Tool.make("t3_thread_start", { |
| 153 | 153 | description: |
| 154 | − "Create an ordinary top-level T3 thread and immediately start its first turn. The new thread inherits this thread's project, checkout, provider, model, and runtime settings unless overridden. Use t3_thread_wait and t3_thread_read to collect its result.", | |
| 154 | + "Create an ordinary TOP-LEVEL T3 conversation and immediately start its first turn. This is not a child agent/subagent; use delegate_task for delegated work. The new thread inherits this thread's project, checkout, provider, model, and runtime settings unless overridden. Use t3_thread_wait and t3_thread_read to collect its result.", | |
| 155 | 155 | parameters: OrchestratorMcpThreadStartInput, |
| 156 | 156 | success: OrchestratorMcpCreatedThread, |
| 157 | 157 | failure: OrchestratorMcpFailure, |
| @@ -55,6 +55,7 @@ import type { · 54 unmodified lines | ||
| 55 | 55 | AcpSessionRuntimeStartResult, |
| 56 | 56 | } from "../../provider/acp/AcpSessionRuntime.ts"; |
| 57 | 57 | import * as AcpSessionRuntime from "../../provider/acp/AcpSessionRuntime.ts"; |
| 58 | +import { t3OrchestrationPromptForFirstRun } from "../../provider/T3OrchestrationInstructions.ts"; | |
| 58 | 59 | import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; |
| 59 | 60 | import { type ProviderContinuationRequest } from "../ProviderContinuationRequests.ts"; |
| 60 | 61 | import { makeProviderFailure } from "../ProviderFailure.ts"; |
| @@ -4227,8 +4228,13 @@ export function makeAcpAdapterV2(options: AcpAdapterV2Options): ProviderAdapterV · 4166 unmodified lines | ||
| 4227 | 4228 | turnInput: ProviderAdapterV2TurnInput, |
| 4228 | 4229 | ) { |
| 4229 | 4230 | const prompt: Array<EffectAcpSchema.ContentBlock> = []; |
| 4230 | − if (turnInput.message.text.length > 0) { | |
| 4231 | − prompt.push({ type: "text", text: turnInput.message.text }); | |
| 4231 | + const text = t3OrchestrationPromptForFirstRun({ | |
| 4232 | + prompt: turnInput.message.text, | |
| 4233 | + runOrdinal: turnInput.runOrdinal, | |
| 4234 | + hasT3Mcp: acpMcpServers(turnInput.threadId).length > 0, | |
| 4235 | + }); | |
| 4236 | + if (text.length > 0) { | |
| 4237 | + prompt.push({ type: "text", text }); | |
| 4232 | 4238 | } |
| 4233 | 4239 | if (turnInput.message.attachments.length > 0 && !supportsImagePrompts) { |
| 4234 | 4240 | return yield* new ProviderAdapterProtocolError({ |
| @@ -519,6 +519,15 @@ describe("ClaudeAdapterV2 native protocol logging", () => { · 518 unmodified lines | ||
| 519 | 519 | cwd: "/workspace", |
| 520 | 520 | ...overrides, |
| 521 | 521 | }); |
| 522 | + assert.isObject(options.systemPrompt); | |
| 523 | + const systemPrompt = options.systemPrompt as { | |
| 524 | + readonly type: string; | |
| 525 | + readonly preset: string; | |
| 526 | + readonly append?: string; | |
| 527 | + }; | |
| 528 | + assert.equal(systemPrompt.type, "preset"); | |
| 529 | + assert.equal(systemPrompt.preset, "claude_code"); | |
| 530 | + assert.include(systemPrompt.append ?? "", "use `delegate_task`"); | |
| 522 | 531 | const logged = loggedClaudeQueryOptions(options); |
| 523 | 532 | assert.equal(logged.hasMcpServers, true); |
| 524 | 533 | assert.notInclude(JSON.stringify(logged), "secret-claude-token"); |
| @@ -67,6 +67,7 @@ import { · 66 unmodified lines | ||
| 67 | 67 | makeEventNdjsonLogger, |
| 68 | 68 | } from "../../provider/Layers/EventNdjsonLogger.ts"; |
| 69 | 69 | import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; |
| 70 | +import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "../../provider/T3OrchestrationInstructions.ts"; | |
| 70 | 71 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 71 | 72 | import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; |
| 72 | 73 | import { makeProviderFailure } from "../ProviderFailure.ts"; |
| @@ -688,6 +689,15 @@ export function makeClaudeQueryOptions(input: { · 615 unmodified lines | ||
| 688 | 689 | : {}), |
| 689 | 690 | ...(input.environment === undefined ? {} : { env: input.environment }), |
| 690 | 691 | ...(input.mcpServers === undefined ? {} : { mcpServers: input.mcpServers }), |
| 692 | + ...(input.mcpServers === undefined | |
| 693 | + ? {} | |
| 694 | + : { | |
| 695 | + systemPrompt: { | |
| 696 | + type: "preset" as const, | |
| 697 | + preset: "claude_code" as const, | |
| 698 | + append: T3_CODE_ORCHESTRATION_INSTRUCTIONS, | |
| 699 | + }, | |
| 700 | + }), | |
| 691 | 701 | ...(Object.keys(extraArgs).length === 0 ? {} : { extraArgs }), |
| 692 | 702 | }; |
| 693 | 703 | return input.cwd === null ? options : { ...options, cwd: input.cwd }; |
| @@ -244,6 +244,35 @@ describe("CodexAdapterV2 runtime policy", () => { · 243 unmodified lines | ||
| 244 | 244 | }), |
| 245 | 245 | ); |
| 246 | 246 | |
| 247 | + it.effect("adds default-mode developer instructions when the T3 MCP server is attached", () => | |
| 248 | + Effect.gen(function* () { | |
| 249 | + const params = yield* buildCodexTurnStartParams({ | |
| 250 | + nativeThreadId: "native-orchestration-instructions", | |
| 251 | + codexInput: [{ type: "text", text: "delegate this task" }], | |
| 252 | + runtimePolicy: { | |
| 253 | + runtimeMode: "full-access", | |
| 254 | + interactionMode: "default", | |
| 255 | + cwd: null, | |
| 256 | + }, | |
| 257 | + modelSelection: { | |
| 258 | + instanceId: ProviderInstanceId.make("codex"), | |
| 259 | + model: "gpt-5.4", | |
| 260 | + }, | |
| 261 | + hasT3Mcp: true, | |
| 262 | + }); | |
| 263 | + | |
| 264 | + assert.equal(params.collaborationMode?.mode, "default"); | |
| 265 | + assert.include( | |
| 266 | + params.collaborationMode?.settings.developer_instructions ?? "", | |
| 267 | + "use `delegate_task`", | |
| 268 | + ); | |
| 269 | + assert.include( | |
| 270 | + params.collaborationMode?.settings.developer_instructions ?? "", | |
| 271 | + "structured object, never as JSON text", | |
| 272 | + ); | |
| 273 | + }), | |
| 274 | + ); | |
| 275 | + | |
| 247 | 276 | it.effect("compiles per-turn Codex model options and cwd from their owning inputs", () => |
| 248 | 277 | Effect.gen(function* () { |
| 249 | 278 | const params = yield* buildCodexTurnStartParams({ |
| @@ -47,7 +47,10 @@ import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; · 46 unmodified lines | ||
| 47 | 47 | import { resolveAttachmentPath } from "../../attachmentStore.ts"; |
| 48 | 48 | import { getCodexServiceTierOptionValue } from "../../codexModelOptions.ts"; |
| 49 | 49 | import { ServerConfig } from "../../config.ts"; |
| 50 | −import { CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS } from "../../provider/CodexDeveloperInstructions.ts"; | |
| 50 | +import { | |
| 51 | + CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS, | |
| 52 | + CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, | |
| 53 | +} from "../../provider/CodexDeveloperInstructions.ts"; | |
| 51 | 54 | import { |
| 52 | 55 | materializeCodexShadowHome, |
| 53 | 56 | resolveCodexHomeLayout, |
| @@ -542,6 +545,7 @@ export function buildCodexTurnStartParams(input: { · 488 unmodified lines | ||
| 542 | 545 | readonly codexInput: ReadonlyArray<CodexSchema.V2TurnStartParams__UserInput>; |
| 543 | 546 | readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; |
| 544 | 547 | readonly modelSelection: ModelSelection; |
| 548 | + readonly hasT3Mcp?: boolean; | |
| 545 | 549 | }) { |
| 546 | 550 | return Effect.gen(function* () { |
| 547 | 551 | const runtimeModeDefaults = codexRuntimeModeTurnDefaults(input.runtimePolicy.runtimeMode); |
| @@ -560,17 +564,23 @@ export function buildCodexTurnStartParams(input: { · 12 unmodified lines | ||
| 560 | 564 | const effort = |
| 561 | 565 | selectedEffort === undefined ? undefined : yield* decodeTurnReasoningEffort(selectedEffort); |
| 562 | 566 | const serviceTier = getCodexServiceTierOptionValue(input.modelSelection); |
| 563 | − const collaborationMode: CodexSchema.ClientRequest__CollaborationMode | undefined = | |
| 567 | + const developerInstructions = | |
| 564 | 568 | input.runtimePolicy.interactionMode === "plan" |
| 565 | − ? { | |
| 566 | − mode: "plan", | |
| 569 | + ? CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS | |
| 570 | + : input.hasT3Mcp === true | |
| 571 | + ? CODEX_DEFAULT_MODE_DEVELOPER_INSTRUCTIONS | |
| 572 | + : undefined; | |
| 573 | + const collaborationMode: CodexSchema.ClientRequest__CollaborationMode | undefined = | |
| 574 | + developerInstructions === undefined | |
| 575 | + ? undefined | |
| 576 | + : { | |
| 577 | + mode: input.runtimePolicy.interactionMode === "plan" ? "plan" : "default", | |
| 567 | 578 | settings: { |
| 568 | 579 | model: input.modelSelection.model, |
| 569 | 580 | reasoning_effort: effort ?? "medium", |
| 570 | − developer_instructions: CODEX_PLAN_MODE_DEVELOPER_INSTRUCTIONS, | |
| 581 | + developer_instructions: developerInstructions, | |
| 571 | 582 | }, |
| 572 | − } | |
| 573 | − : undefined; | |
| 583 | + }; | |
| 574 | 584 | |
| 575 | 585 | return yield* decodeCodexTurnStartParamsWithCollaborationMode({ |
| 576 | 586 | threadId: input.nativeThreadId, |
| @@ -4421,6 +4431,8 @@ export function makeCodexAdapterV2(adapterOptions: CodexAdapterV2Options): Provi · 3844 unmodified lines | ||
| 4421 | 4431 | codexInput, |
| 4422 | 4432 | runtimePolicy: turnInput.runtimePolicy, |
| 4423 | 4433 | modelSelection: turnInput.modelSelection, |
| 4434 | + hasT3Mcp: | |
| 4435 | + McpProviderSession.readMcpProviderSession(turnInput.threadId) !== undefined, | |
| 4424 | 4436 | }); |
| 4425 | 4437 | yield* Ref.update(pendingRootTurns, (current) => { |
| 4426 | 4438 | const updated = new Map(current); |
| @@ -43,6 +43,7 @@ import { ServerConfig } from "../../config.ts"; · 42 unmodified lines | ||
| 43 | 43 | import * as McpProviderSession from "../../mcp/McpProviderSession.ts"; |
| 44 | 44 | import { cursorSdkModelSelection } from "../../provider/cursorSdkModel.ts"; |
| 45 | 45 | import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; |
| 46 | +import { t3OrchestrationPromptForFirstRun } from "../../provider/T3OrchestrationInstructions.ts"; | |
| 46 | 47 | import { IdAllocatorV2, type IdAllocatorV2Shape } from "../IdAllocator.ts"; |
| 47 | 48 | import { makeProviderFailure } from "../ProviderFailure.ts"; |
| 48 | 49 | import { turnScopedSelectionTransition } from "../ProviderSelectionTransition.ts"; |
| @@ -2008,6 +2009,11 @@ export function makeCursorAdapterV2( · 1959 unmodified lines | ||
| 2008 | 2009 | const resolveUserMessage = Effect.fnUntraced(function* ( |
| 2009 | 2010 | turnInput: ProviderAdapterV2TurnInput, |
| 2010 | 2011 | ) { |
| 2012 | + const text = t3OrchestrationPromptForFirstRun({ | |
| 2013 | + prompt: turnInput.message.text, | |
| 2014 | + runOrdinal: turnInput.runOrdinal, | |
| 2015 | + hasT3Mcp: cursorMcpServers(turnInput.threadId) !== undefined, | |
| 2016 | + }); | |
| 2011 | 2017 | const images = yield* Effect.forEach( |
| 2012 | 2018 | turnInput.message.attachments, |
| 2013 | 2019 | (attachment: ChatAttachment) => |
| @@ -2046,9 +2052,9 @@ export function makeCursorAdapterV2( · 32 unmodified lines | ||
| 2046 | 2052 | }); |
| 2047 | 2053 | } |
| 2048 | 2054 | return images.length === 0 |
| 2049 | − ? turnInput.message.text | |
| 2055 | + ? text | |
| 2050 | 2056 | : ({ |
| 2051 | − text: turnInput.message.text, | |
| 2057 | + text, | |
| 2052 | 2058 | images, |
| 2053 | 2059 | } satisfies SDKUserMessage); |
| 2054 | 2060 | }); |
| @@ -59,6 +59,7 @@ import { · 58 unmodified lines | ||
| 59 | 59 | summarizeNativeProtocolPayload, |
| 60 | 60 | } from "../../provider/NativeProtocolLogging.ts"; |
| 61 | 61 | import { mergeProviderInstanceEnvironment } from "../../provider/ProviderInstanceEnvironment.ts"; |
| 62 | +import { t3OrchestrationSystemPrompt } from "../../provider/T3OrchestrationInstructions.ts"; | |
| 62 | 63 | import { |
| 63 | 64 | OpenCodeRuntime, |
| 64 | 65 | OpenCodeRuntimeError, |
| @@ -800,7 +801,9 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid · 735 unmodified lines | ||
| 800 | 801 | }); |
| 801 | 802 | |
| 802 | 803 | const mcpSession = McpProviderSession.readMcpProviderSession(input.threadId); |
| 803 | − if (mcpSession !== undefined && !connection.external) { | |
| 804 | + const hasT3Mcp = mcpSession !== undefined && !connection.external; | |
| 805 | + const orchestrationSystemPrompt = t3OrchestrationSystemPrompt(hasT3Mcp); | |
| 806 | + if (hasT3Mcp) { | |
| 804 | 807 | yield* runOpenCodeSdk("mcp.add", () => |
| 805 | 808 | client.mcp.add({ |
| 806 | 809 | name: "t3-code", |
| @@ -2493,6 +2496,9 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid · 1686 unmodified lines | ||
| 2493 | 2496 | model: parsedModel, |
| 2494 | 2497 | ...(agent === undefined ? {} : { agent }), |
| 2495 | 2498 | ...(variant === undefined ? {} : { variant }), |
| 2499 | + ...(orchestrationSystemPrompt === undefined | |
| 2500 | + ? {} | |
| 2501 | + : { system: orchestrationSystemPrompt }), | |
| 2496 | 2502 | parts, |
| 2497 | 2503 | }, |
| 2498 | 2504 | () => |
| @@ -2501,6 +2507,9 @@ export function makeOpenCodeAdapterV2(options: OpenCodeAdapterV2Options): Provid · 2 unmodified lines | ||
| 2501 | 2507 | model: parsedModel, |
| 2502 | 2508 | ...(agent === undefined ? {} : { agent }), |
| 2503 | 2509 | ...(variant === undefined ? {} : { variant }), |
| 2510 | + ...(orchestrationSystemPrompt === undefined | |
| 2511 | + ? {} | |
| 2512 | + : { system: orchestrationSystemPrompt }), | |
| 2504 | 2513 | parts, |
| 2505 | 2514 | }), |
| 2506 | 2515 | ).pipe( |
| @@ -1,6 +1,10 @@ | ||
| 1 | 1 | import type { ProviderInteractionMode } from "@t3tools/contracts"; |
| 2 | 2 | import { buildRuntimeInstructions } from "./RuntimeInstructions.ts"; |
| 3 | 3 | |
| 4 | +import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "./T3OrchestrationInstructions.ts"; | |
| 5 | + | |
| 6 | +import { T3_CODE_ORCHESTRATION_INSTRUCTIONS } from "./T3OrchestrationInstructions.ts"; | |
| 7 | + | |
| 4 | 8 | const T3_CODE_BROWSER_TOOL_INSTRUCTIONS = ` |
| 5 | 9 | |
| 6 | 10 | ## T3 Code collaborative browser |
| @@ -12,15 +16,6 @@ For browser work, first call \`preview_status\`. If no automation-capable previe · 5 unmodified lines | ||
| 12 | 16 | Do not switch to global browser skills, Chrome, Node REPL browser automation, standalone Playwright, or agent-browser merely because the preview is initially closed or a first call fails. Use an alternative browser system only when the T3 preview tools are absent, the user explicitly requests another browser, or \`preview_open\` returns an explicit unsupported/unavailable error. A failed T3 preview tool call should be inspected and retried with corrected arguments when the error is actionable. |
| 13 | 17 | `; |
| 14 | 18 | |
| 15 | −const T3_CODE_THREAD_ORCHESTRATION_INSTRUCTIONS = ` | |
| 16 | − | |
| 17 | −## T3 Code thread orchestration | |
| 18 | − | |
| 19 | −When the \`t3-code\` MCP server exposes \`t3_thread_*\` tools, you can run a bounded orchestration loop without asking the user to relay updates. Start independent work with \`t3_thread_start\` (or \`create_threads\` for a batch), retain the returned thread/run IDs, wait with \`t3_thread_wait\`, and collect durable output with \`t3_thread_read\`. Use \`t3_thread_send\` for follow-up or steering and \`t3_thread_interrupt\` when work is no longer needed. Use stable \`clientRequestId\` values when retrying mutating calls, and use \`t3_thread_list\` to recover IDs after context loss. | |
| 20 | − | |
| 21 | −Keep loops bounded by an explicit completion condition and timeout. A wait timeout does not stop the target thread. Do not repeatedly start equivalent threads or send duplicate work when a durable run is already active. | |
| 22 | −`; | |
| 23 | − | |
| 24 | 19 | /** |
| 25 | 20 | * The browser block is omitted entirely when the preview tools aren't attached. |
| 26 | 21 | * Describing `preview_*` tools that aren't in the turn's tool list would be |
| @@ -178,7 +173,7 @@ Use the \`request_user_input\` tool only when it is listed in the available tool · 151 unmodified lines | ||
| 178 | 173 | |
| 179 | 174 | In Default mode, strongly prefer making reasonable assumptions and executing the user's request rather than stopping to ask questions. If you absolutely must ask a question because the answer cannot be discovered from local context and a reasonable assumption would be risky, ask the user directly with a concise plain-text question. Never write a multiple choice question as a textual assistant message. |
| 180 | 175 | ${browserToolInstructions(browserToolsAvailable)} |
| 181 | −${T3_CODE_THREAD_ORCHESTRATION_INSTRUCTIONS} | |
| 176 | +${T3_CODE_ORCHESTRATION_INSTRUCTIONS} | |
| 182 | 177 | </collaboration_mode>`; |
| 183 | 178 | |
| 184 | 179 | export interface CodexRuntimeInfo { |
| @@ -0,0 +1,47 @@ | ||
| 1 | +import { assert, describe, it } from "@effect/vitest"; | |
| 2 | + | |
| 3 | +import { | |
| 4 | + T3_CODE_ORCHESTRATION_INSTRUCTIONS, | |
| 5 | + t3OrchestrationPromptForFirstRun, | |
| 6 | + t3OrchestrationSystemPrompt, | |
| 7 | +} from "./T3OrchestrationInstructions.ts"; | |
| 8 | + | |
| 9 | +describe("T3 orchestration provider instructions", () => { | |
| 10 | + it("distinguishes delegated subagents from ordinary top-level threads", () => { | |
| 11 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "use `delegate_task`"); | |
| 12 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "ordinary top-level T3 conversations"); | |
| 13 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "Never use them merely"); | |
| 14 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "different provider"); | |
| 15 | + }); | |
| 16 | + | |
| 17 | + it("documents structured schedules instead of JSON strings", () => { | |
| 18 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "structured object, never as JSON text"); | |
| 19 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, '"everyMs":3600000'); | |
| 20 | + assert.include(T3_CODE_ORCHESTRATION_INSTRUCTIONS, "bindToCurrentThread=false"); | |
| 21 | + }); | |
| 22 | + | |
| 23 | + it("injects prompt fallback only for an MCP-enabled first run", () => { | |
| 24 | + const prompt = "Inspect the repository."; | |
| 25 | + const injected = t3OrchestrationPromptForFirstRun({ | |
| 26 | + prompt, | |
| 27 | + runOrdinal: 1, | |
| 28 | + hasT3Mcp: true, | |
| 29 | + }); | |
| 30 | + | |
| 31 | + assert.include(injected, "<t3_code_orchestration_instructions>"); | |
| 32 | + assert.include(injected, `<user_request>\n${prompt}\n</user_request>`); | |
| 33 | + assert.equal( | |
| 34 | + t3OrchestrationPromptForFirstRun({ prompt, runOrdinal: 2, hasT3Mcp: true }), | |
| 35 | + prompt, | |
| 36 | + ); | |
| 37 | + assert.equal( | |
| 38 | + t3OrchestrationPromptForFirstRun({ prompt, runOrdinal: 1, hasT3Mcp: false }), | |
| 39 | + prompt, | |
| 40 | + ); | |
| 41 | + }); | |
| 42 | + | |
| 43 | + it("only exposes the system prompt when the T3 MCP server is attached", () => { | |
| 44 | + assert.equal(t3OrchestrationSystemPrompt(false), undefined); | |
| 45 | + assert.equal(t3OrchestrationSystemPrompt(true), T3_CODE_ORCHESTRATION_INSTRUCTIONS); | |
| 46 | + }); | |
| 47 | +}); | |
| @@ -0,0 +1,35 @@ | ||
| 1 | +export const T3_CODE_ORCHESTRATION_INSTRUCTIONS = ` | |
| 2 | + | |
| 3 | +## T3 Code orchestration | |
| 4 | + | |
| 5 | +The \`t3-code\` MCP server provides app-owned orchestration. Treat these concepts distinctly: | |
| 6 | + | |
| 7 | +- A delegated task/subagent is child work owned by the current thread. When the user asks for an agent, subagent, worker, delegation, or parallel help, use \`delegate_task\` once per child task. This remains true when targeting a different provider. Use \`orchestrator_capabilities\` to discover provider/model IDs, retain each returned \`taskId\`, and use \`task_status\` or \`task_cancel\` to manage it. The returned \`childThreadId\` is backing storage for the subagent; do not replace delegation with ordinary thread creation. | |
| 8 | +- \`create_threads\` and \`t3_thread_start\` create ordinary top-level T3 conversations. Use them only when the user explicitly asks for separate/new/top-level threads or conversations. Never use them merely because the user said "subagent" or requested parallel delegated work. | |
| 9 | +- \`schedule_task\` creates persistent recurring work in the app scheduler. Pass \`schedule\` as a structured object, never as JSON text: \`{"type":"interval","everyMs":3600000}\` for an interval, or \`{"type":"fixed_time","timeOfDay":"09:00","weekdays":[1,2,3,4,5]}\` for a wall-clock schedule. By default runs return to the current thread; set \`bindToCurrentThread=false\` only when the user wants a fresh thread for every run. After scheduling, report the returned cadence and next run time. | |
| 10 | + | |
| 11 | +Tool names may include an MCP prefix (for example \`mcp__t3-code__delegate_task\`); the semantics are the same. Keep polling/wait loops bounded, do not duplicate active work, and use stable \`clientRequestId\` values when retrying mutations. | |
| 12 | +`; | |
| 13 | + | |
| 14 | +/** | |
| 15 | + * Providers without a system/developer-instruction channel receive this | |
| 16 | + * context in the first prompt. Keep the wrapper explicit so it cannot be | |
| 17 | + * mistaken for text authored by the user. | |
| 18 | + */ | |
| 19 | +export function prependT3OrchestrationInstructions(prompt: string): string { | |
| 20 | + return `<t3_code_orchestration_instructions>${T3_CODE_ORCHESTRATION_INSTRUCTIONS.trim()}</t3_code_orchestration_instructions>\n\n<user_request>\n${prompt}\n</user_request>`; | |
| 21 | +} | |
| 22 | + | |
| 23 | +export function t3OrchestrationPromptForFirstRun(input: { | |
| 24 | + readonly prompt: string; | |
| 25 | + readonly runOrdinal: number; | |
| 26 | + readonly hasT3Mcp: boolean; | |
| 27 | +}): string { | |
| 28 | + return input.runOrdinal === 1 && input.hasT3Mcp | |
| 29 | + ? prependT3OrchestrationInstructions(input.prompt) | |
| 30 | + : input.prompt; | |
| 31 | +} | |
| 32 | + | |
| 33 | +export function t3OrchestrationSystemPrompt(hasT3Mcp: boolean): string | undefined { | |
| 34 | + return hasT3Mcp ? T3_CODE_ORCHESTRATION_INSTRUCTIONS : undefined; | |
| 35 | +} | |
| @@ -31,9 +31,35 @@ import { · 30 unmodified lines | ||
| 31 | 31 | } from "./model.ts"; |
| 32 | 32 | import { ProviderDriverKind, ProviderInstanceId } from "./providerInstance.ts"; |
| 33 | 33 | |
| 34 | −const OrchestratorMcpPrompt = TrimmedNonEmptyString.check(Schema.isMaxLength(120_000)); | |
| 35 | −const OrchestratorMcpTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)); | |
| 36 | −const OrchestratorMcpClientRequestId = TrimmedNonEmptyString.check(Schema.isMaxLength(256)); | |
| 34 | +const OrchestratorMcpPrompt = TrimmedNonEmptyString.check(Schema.isMaxLength(120_000)).annotate({ | |
| 35 | + description: "Complete task or message text for the target agent.", | |
| 36 | +}); | |
| 37 | +const OrchestratorMcpTitle = TrimmedNonEmptyString.check(Schema.isMaxLength(512)).annotate({ | |
| 38 | + description: "Optional concise display title.", | |
| 39 | +}); | |
| 40 | +const OrchestratorMcpClientRequestId = TrimmedNonEmptyString.check( | |
| 41 | + Schema.isMaxLength(256), | |
| 42 | +).annotate({ description: "Stable idempotency key to reuse when retrying this mutation." }); | |
| 43 | + | |
| 44 | +/** | |
| 45 | + * OpenCode 1.15 has been observed serializing nested MCP union objects as JSON | |
| 46 | + * strings. Keep the structured object as the documented form while accepting | |
| 47 | + * that compatibility shape at the tool boundary and decoding it immediately. | |
| 48 | + */ | |
| 49 | +const OrchestratorMcpScheduleFromJsonString = Schema.fromJsonString(ScheduledTaskSchedule).annotate( | |
| 50 | + { | |
| 51 | + description: | |
| 52 | + "Compatibility-only JSON encoding of the schedule object. Prefer a structured object.", | |
| 53 | + }, | |
| 54 | +); | |
| 55 | + | |
| 56 | +const OrchestratorMcpSchedule = Schema.Union([ | |
| 57 | + ScheduledTaskSchedule, | |
| 58 | + OrchestratorMcpScheduleFromJsonString, | |
| 59 | +]).annotate({ | |
| 60 | + description: | |
| 61 | + "Recurring schedule object: {type:'interval', everyMs} or {type:'fixed_time', timeOfDay, weekdays?}. Never stringify it unless the provider requires the compatibility form.", | |
| 62 | +}); | |
| 37 | 63 | |
| 38 | 64 | /** |
| 39 | 65 | * Shorthand `{ id: value }` record form for target model options. Unlike the |
| @@ -65,9 +91,21 @@ export const OrchestratorMcpTargetOptions = Schema.Union([ · 25 unmodified lines | ||
| 65 | 91 | export type OrchestratorMcpTargetOptions = typeof OrchestratorMcpTargetOptions.Type; |
| 66 | 92 | |
| 67 | 93 | export const OrchestratorMcpTarget = Schema.Struct({ |
| 68 | − providerInstanceId: Schema.optional(ProviderInstanceId), | |
| 69 | − driverKind: Schema.optional(ProviderDriverKind), | |
| 70 | − model: Schema.optional(TrimmedNonEmptyString), | |
| 94 | + providerInstanceId: Schema.optional( | |
| 95 | + ProviderInstanceId.annotate({ | |
| 96 | + description: "Configured provider instance id from orchestrator_capabilities.", | |
| 97 | + }), | |
| 98 | + ), | |
| 99 | + driverKind: Schema.optional( | |
| 100 | + ProviderDriverKind.annotate({ | |
| 101 | + description: "Provider driver kind; prefer providerInstanceId when available.", | |
| 102 | + }), | |
| 103 | + ), | |
| 104 | + model: Schema.optional( | |
| 105 | + TrimmedNonEmptyString.annotate({ | |
| 106 | + description: "Model id advertised for the selected provider instance.", | |
| 107 | + }), | |
| 108 | + ), | |
| 71 | 109 | /** |
| 72 | 110 | * Model option selections for the child (for example reasoning effort). |
| 73 | 111 | * Accepts the canonical `[{ id, value }]` array or the shorthand |
| @@ -75,7 +113,11 @@ export const OrchestratorMcpTarget = Schema.Struct({ · 1 unmodified line | ||
| 75 | 113 | * advertised by orchestrator_capabilities. When omitted, options inherit |
| 76 | 114 | * from the parent only when the child runs the parent's provider and model. |
| 77 | 115 | */ |
| 78 | − options: Schema.optional(OrchestratorMcpTargetOptions), | |
| 116 | + options: Schema.optional( | |
| 117 | + OrchestratorMcpTargetOptions.annotate({ | |
| 118 | + description: "Model option selections advertised by orchestrator_capabilities.", | |
| 119 | + }), | |
| 120 | + ), | |
| 79 | 121 | }); |
| 80 | 122 | export type OrchestratorMcpTarget = typeof OrchestratorMcpTarget.Type; |
| 81 | 123 | |
| @@ -110,7 +152,9 @@ export const OrchestratorMcpDelegatedTaskStatus = Schema.Literals([ · 28 unmodified lines | ||
| 110 | 152 | export type OrchestratorMcpDelegatedTaskStatus = typeof OrchestratorMcpDelegatedTaskStatus.Type; |
| 111 | 153 | |
| 112 | 154 | export const OrchestratorMcpDelegateTaskInput = Schema.Struct({ |
| 113 | − task: OrchestratorMcpPrompt, | |
| 155 | + task: OrchestratorMcpPrompt.annotate({ | |
| 156 | + description: "Self-contained task for one delegated child agent/subagent.", | |
| 157 | + }), | |
| 114 | 158 | target: Schema.optional(OrchestratorMcpTarget), |
| 115 | 159 | title: Schema.optional(OrchestratorMcpTitle), |
| 116 | 160 | role: Schema.optional(OrchestratorMcpTaskRole), |
| @@ -418,16 +462,25 @@ export const OrchestratorMcpCapabilitiesResult = Schema.Struct({ · 301 unmodified lines | ||
| 418 | 462 | export type OrchestratorMcpCapabilitiesResult = typeof OrchestratorMcpCapabilitiesResult.Type; |
| 419 | 463 | |
| 420 | 464 | export const OrchestratorMcpScheduleTaskInput = Schema.Struct({ |
| 421 | − prompt: OrchestratorMcpPrompt, | |
| 422 | − schedule: ScheduledTaskSchedule, | |
| 465 | + prompt: OrchestratorMcpPrompt.annotate({ | |
| 466 | + description: "Prompt executed on every scheduled run.", | |
| 467 | + }), | |
| 468 | + schedule: OrchestratorMcpSchedule, | |
| 423 | 469 | title: Schema.optional(OrchestratorMcpTitle), |
| 424 | − enabled: Schema.optional(Schema.Boolean), | |
| 470 | + enabled: Schema.optional( | |
| 471 | + Schema.Boolean.annotate({ description: "Whether the schedule starts enabled; defaults true." }), | |
| 472 | + ), | |
| 425 | 473 | /** |
| 426 | 474 | * When true (the default), the scheduled task fires into the calling thread |
| 427 | 475 | * on each run instead of launching a fresh thread. This is the recurring |
| 428 | 476 | * "wake up in this thread" behaviour reserved for agent-created tasks. |
| 429 | 477 | */ |
| 430 | − bindToCurrentThread: Schema.optional(Schema.Boolean), | |
| 478 | + bindToCurrentThread: Schema.optional( | |
| 479 | + Schema.Boolean.annotate({ | |
| 480 | + description: | |
| 481 | + "True (default) posts each run into this thread; false creates a fresh top-level thread per run.", | |
| 482 | + }), | |
| 483 | + ), | |
| 431 | 484 | clientRequestId: Schema.optional(OrchestratorMcpClientRequestId), |
| 432 | 485 | }); |
| 433 | 486 | export type OrchestratorMcpScheduleTaskInput = typeof OrchestratorMcpScheduleTaskInput.Type; |
| @@ -459,7 +512,7 @@ export const OrchestratorMcpUpdateScheduledTaskInput = Schema.Struct({ · 25 unmodified lines | ||
| 459 | 512 | scheduledTaskId: ScheduledTaskId, |
| 460 | 513 | prompt: Schema.optional(OrchestratorMcpPrompt), |
| 461 | 514 | title: Schema.optional(OrchestratorMcpTitle), |
| 462 | − schedule: Schema.optional(ScheduledTaskSchedule), | |
| 515 | + schedule: Schema.optional(OrchestratorMcpSchedule), | |
| 463 | 516 | enabled: Schema.optional(Schema.Boolean), |
| 464 | 517 | bindToCurrentThread: Schema.optional(Schema.Boolean), |
| 465 | 518 | }); |
| @@ -18,21 +18,42 @@ import { · 17 unmodified lines | ||
| 18 | 18 | import { ProviderInteractionMode, RuntimeMode } from "./providerPolicy.ts"; |
| 19 | 19 | |
| 20 | 20 | /** 24-hour "HH:MM" wall-clock time. Mirrors `parseTimeOfDay` on the server. */ |
| 21 | −const TimeOfDay = TrimmedNonEmptyString.check(Schema.isPattern(/^([01]?\d|2[0-3]):([0-5]\d)$/)); | |
| 21 | +const TimeOfDay = TrimmedNonEmptyString.check( | |
| 22 | + Schema.isPattern(/^([01]?\d|2[0-3]):([0-5]\d)$/), | |
| 23 | +).annotate({ description: "Local wall-clock time in 24-hour HH:MM form, such as 09:30." }); | |
| 22 | 24 | |
| 23 | 25 | export const ScheduledTaskSchedule = Schema.Union([ |
| 24 | 26 | Schema.Struct({ |
| 25 | − type: Schema.Literal("interval"), | |
| 26 | − everyMs: PositiveInt, | |
| 27 | + type: Schema.Literal("interval").annotate({ | |
| 28 | + description: "Select interval scheduling.", | |
| 29 | + }), | |
| 30 | + everyMs: PositiveInt.annotate({ | |
| 31 | + description: "Positive interval in milliseconds; 3600000 means every hour.", | |
| 32 | + }), | |
| 33 | + }).annotate({ | |
| 34 | + description: "Run repeatedly after a fixed number of milliseconds.", | |
| 27 | 35 | }), |
| 28 | 36 | Schema.Struct({ |
| 29 | − type: Schema.Literal("fixed_time"), | |
| 37 | + type: Schema.Literal("fixed_time").annotate({ | |
| 38 | + description: "Select a fixed local wall-clock time.", | |
| 39 | + }), | |
| 30 | 40 | timeOfDay: TimeOfDay, |
| 31 | 41 | weekdays: Schema.optional( |
| 32 | − Schema.Array(Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 6 }))), | |
| 42 | + Schema.Array( | |
| 43 | + Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: 6 })).annotate({ | |
| 44 | + description: "Weekday number where 0 is Sunday and 6 is Saturday.", | |
| 45 | + }), | |
| 46 | + ).annotate({ | |
| 47 | + description: "Optional weekdays; omit to run every day.", | |
| 48 | + }), | |
| 33 | 49 | ), |
| 50 | + }).annotate({ | |
| 51 | + description: "Run at a fixed local wall-clock time on selected weekdays.", | |
| 34 | 52 | }), |
| 35 | −]); | |
| 53 | +]).annotate({ | |
| 54 | + description: | |
| 55 | + "Structured recurring schedule. Pass an object with type 'interval' or 'fixed_time'.", | |
| 56 | +}); | |
| 36 | 57 | export type ScheduledTaskSchedule = typeof ScheduledTaskSchedule.Type; |
| 37 | 58 | |
| 38 | 59 | export const ScheduledTaskRunStatus = Schema.Literals(["never", "running", "succeeded", "failed"]); |