authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-10-10 20:04:40-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 20:04:40-07:00
loga4a1a803a3a6d10d16e45f505a2b95e99430b1cb
treea41fceee54155b11a3eb33cf0a2138294431087e
parentd404a301189beb5d9ded5bd66bd1fa35db655ef6
signature Signed by PGP key B5690EEEBB952194

fix(server): delegated Muse tasks no longer ask approval for every command (#18065)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

5 files changed, 91 insertions(+), 11 deletions(-)

apps/server/src/mcp/OrchestratorMcpService.test.ts+33
...@@ -1817,4 +1817,37 @@ describe("OrchestratorMcpService provider resolution", () => {...@@ -1817,4 +1817,37 @@ describe("OrchestratorMcpService provider resolution", () => {
1817 }),1817 }),
1818 );1818 );
1819 });1819 });
1820
1821 describe("resolveRuntimeMode", () => {
1822 // Muse offers only these two, and runs any other mode as Supervised.
1823 const muse = {
1824 modelSelection: { instanceId: ProviderInstanceId.make("muse"), model: "muse-model" },
1825 supportedRuntimeModes: ["approval-required", "full-access"] as const,
1826 };
1827
1828 it.effect("refuses an explicit mode the target provider does not offer", () =>
1829 Effect.gen(function* () {
1830 const error = yield* OrchestratorMcpService.resolveRuntimeMode(
1831 "full-access",
1832 "auto-accept-edits",
1833 muse,
1834 ).pipe(Effect.flip);
1835 assert.equal(error.code, "invalid_request");
1836 assert.include(error.message, "approval-required, full-access");
1837 }),
1838 );
1839
1840 it.effect("steps an inherited mode down to the broadest one the provider offers", () =>
1841 Effect.gen(function* () {
1842 assert.equal(
1843 yield* OrchestratorMcpService.resolveRuntimeMode("full-access", "inherit", muse),
1844 "full-access",
1845 );
1846 assert.equal(
1847 yield* OrchestratorMcpService.resolveRuntimeMode("auto", undefined, muse),
1848 "approval-required",
1849 );
1850 }),
1851 );
1852 });
1820});1853});
apps/server/src/mcp/OrchestratorMcpService.ts+48-10
...@@ -113,6 +113,7 @@ const DEFAULT_THREAD_ITEM_MAX_CHARS = 20_000;...@@ -113,6 +113,7 @@ const DEFAULT_THREAD_ITEM_MAX_CHARS = 20_000;
113113
114interface ResolvedTarget {114interface ResolvedTarget {
115 readonly modelSelection: ModelSelection;115 readonly modelSelection: ModelSelection;
116 readonly supportedRuntimeModes: ReadonlyArray<RuntimeMode> | undefined;
116}117}
117118
118type TerminalTaskStatus = Extract<119type TerminalTaskStatus = Extract<
...@@ -531,19 +532,46 @@ function interactionModeRank(mode: ProviderInteractionMode): number {...@@ -531,19 +532,46 @@ function interactionModeRank(mode: ProviderInteractionMode): number {
531 return mode === "plan" ? 0 : 1;532 return mode === "plan" ? 0 : 1;
532}533}
533534
535/**
536 * The mode a new thread runs in: the requested mode, else the parent's, never
537 * broader than the parent's. Pass `target` when the thread runs on a known
538 * provider. A provider runs a mode it does not offer as Supervised, so an
539 * explicit request for one is refused, and an inherited one steps down to the
540 * broadest mode the provider offers.
541 */
534export function resolveRuntimeMode(542export function resolveRuntimeMode(
535 parentMode: RuntimeMode,543 parentMode: RuntimeMode,
536 requested: OrchestratorMcpRuntimeMode | undefined,544 requested: OrchestratorMcpRuntimeMode | undefined,
545 target?: Pick<ResolvedTarget, "modelSelection" | "supportedRuntimeModes">,
537): Effect.Effect<RuntimeMode, OrchestratorMcpFailure> {546): Effect.Effect<RuntimeMode, OrchestratorMcpFailure> {
538 const resolved = requested === undefined || requested === "inherit" ? parentMode : requested;547 const inherited = requested === undefined || requested === "inherit";
539 return runtimeModeRank(resolved) > runtimeModeRank(parentMode)548 const resolved = inherited ? parentMode : requested;
540 ? Effect.fail(549 if (runtimeModeRank(resolved) > runtimeModeRank(parentMode)) {
541 failure(550 return Effect.fail(
542 "runtime_mode_escalation_denied",551 failure(
543 `Child runtime mode ${resolved} is broader than parent mode ${parentMode}.`,552 "runtime_mode_escalation_denied",
544 ),553 `Child runtime mode ${resolved} is broader than parent mode ${parentMode}.`,
545 )554 ),
546 : Effect.succeed(resolved);555 );
556 }
557 const supported = target?.supportedRuntimeModes;
558 if (supported === undefined || supported.length === 0 || supported.includes(resolved)) {
559 return Effect.succeed(resolved);
560 }
561 if (!inherited) {
562 return Effect.fail(
563 failure(
564 "invalid_request",
565 `Provider ${target?.modelSelection.instanceId} does not offer runtime mode ${resolved}, so it would run as approval-required. Use one of: ${supported.join(", ")}.`,
566 ),
567 );
568 }
569 return Effect.succeed(
570 supported
571 .filter((mode) => runtimeModeRank(mode) <= runtimeModeRank(resolved))
572 .toSorted((left, right) => runtimeModeRank(right) - runtimeModeRank(left))[0] ??
573 "approval-required",
574 );
547}575}
548576
549export function resolveInteractionMode(577export function resolveInteractionMode(
...@@ -1238,6 +1266,7 @@ const make = Effect.gen(function* () {...@@ -1238,6 +1266,7 @@ const make = Effect.gen(function* () {
1238 : requestedOptions === undefined1266 : requestedOptions === undefined
1239 ? { instanceId, model }1267 ? { instanceId, model }
1240 : { instanceId, model, options: requestedOptions },1268 : { instanceId, model, options: requestedOptions },
1269 supportedRuntimeModes: provider.supportedRuntimeModes,
1241 };1270 };
1242 });1271 });
12431272
...@@ -1844,6 +1873,10 @@ const make = Effect.gen(function* () {...@@ -1844,6 +1873,10 @@ const make = Effect.gen(function* () {
1844 ? {}1873 ? {}
1845 : { options: model.capabilities.optionDescriptors }),1874 : { options: model.capabilities.optionDescriptors }),
1846 })) ?? [],1875 })) ?? [],
1876 ...(provider.supportedRuntimeModes === undefined ||
1877 provider.supportedRuntimeModes.length === 0
1878 ? {}
1879 : { runtimeModes: [...provider.supportedRuntimeModes] }),
1847 canRunChildTask: constraints.length === 0,1880 canRunChildTask: constraints.length === 0,
1848 canRunCrossProviderChildTask: constraints.length === 0,1881 canRunCrossProviderChildTask: constraints.length === 0,
1849 constraints: [...constraints],1882 constraints: [...constraints],
...@@ -1883,7 +1916,11 @@ const make = Effect.gen(function* () {...@@ -1883,7 +1916,11 @@ const make = Effect.gen(function* () {
1883 target: input.target,1916 target: input.target,
1884 providers,1917 providers,
1885 });1918 });
1886 const runtimeMode = yield* resolveRuntimeMode(parent.thread.runtimeMode, input.runtimeMode);1919 const runtimeMode = yield* resolveRuntimeMode(
1920 parent.thread.runtimeMode,
1921 input.runtimeMode,
1922 target,
1923 );
1887 const interactionMode = yield* resolveInteractionMode(1924 const interactionMode = yield* resolveInteractionMode(
1888 parent.thread.interactionMode,1925 parent.thread.interactionMode,
1889 input.interactionMode,1926 input.interactionMode,
...@@ -2168,6 +2205,7 @@ const make = Effect.gen(function* () {...@@ -2168,6 +2205,7 @@ const make = Effect.gen(function* () {
2168 const runtimeMode = yield* resolveRuntimeMode(2205 const runtimeMode = yield* resolveRuntimeMode(
2169 parent.thread.runtimeMode,2206 parent.thread.runtimeMode,
2170 request.runtimeMode,2207 request.runtimeMode,
2208 target,
2171 );2209 );
2172 const interactionMode = yield* resolveInteractionMode(2210 const interactionMode = yield* resolveInteractionMode(
2173 parent.thread.interactionMode,2211 parent.thread.interactionMode,
apps/server/src/mcp/toolkits/orchestrator/tools.ts+1-1
...@@ -63,7 +63,7 @@ const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", {...@@ -63,7 +63,7 @@ const OrchestratorCapabilitiesTool = Tool.make("orchestrator_capabilities", {
6363
64export const DelegateTaskTool = Tool.make("delegate_task", {64export const DelegateTaskTool = Tool.make("delegate_task", {
65 description:65 description:
66 "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. Provider, model, model options (see orchestrator_capabilities), runtime mode, and interaction mode inherit unless target overrides them. Prefer mode='async' for long work; mode='wait' blocks until completion or timeout. 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.",66 "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. 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. Prefer mode='async' for long work; mode='wait' blocks until completion or timeout. 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.",
67 parameters: OrchestratorMcpDelegateTaskInput,67 parameters: OrchestratorMcpDelegateTaskInput,
68 success: OrchestratorMcpDelegateTaskResult,68 success: OrchestratorMcpDelegateTaskResult,
69 failure: OrchestratorMcpFailure,69 failure: OrchestratorMcpFailure,
docs/orchestration-v2/orchestrator-mcp-server.md+7
...@@ -237,6 +237,13 @@ that driver; an explicit `providerInstanceId` is honored exactly and fails...@@ -237,6 +237,13 @@ that driver; an explicit `providerInstanceId` is honored exactly and fails
237when unavailable. Selecting a different provider without a model uses that237when unavailable. Selecting a different provider without a model uses that
238provider's first advertised model.238provider's first advertised model.
239239
240A provider runs a runtime mode it does not offer as `approval-required`, so a
241child would ask for approval on every command. `orchestrator_capabilities`
242lists `runtimeModes` only for a provider that offers some of the modes, and
243omits it for a provider that offers all of them. When the target lists
244`runtimeModes`, an explicit `runtimeMode` outside that list is refused, and an
245inherited mode steps down to the broadest mode the target offers.
246
240Each delegated review round uses a new `delegate_task` call with the original brief,247Each delegated review round uses a new `delegate_task` call with the original brief,
241prior findings, responses, and unresolved objections. Track each round by its own `taskId` and use248prior findings, responses, and unresolved objections. Track each round by its own `taskId` and use
242a distinct `clientRequestId` per round, stable across retries of that round.249a distinct `clientRequestId` per round, stable across retries of that round.
packages/contracts/src/orchestratorMcp.ts+2
...@@ -487,6 +487,8 @@ export const OrchestratorMcpProviderCapability = Schema.Struct({...@@ -487,6 +487,8 @@ export const OrchestratorMcpProviderCapability = Schema.Struct({
487 options: Schema.optional(Schema.Array(ProviderOptionDescriptor)),487 options: Schema.optional(Schema.Array(ProviderOptionDescriptor)),
488 }),488 }),
489 ),489 ),
490 /** Runtime modes the provider offers; omitted when it offers all of them. */
491 runtimeModes: Schema.optional(Schema.Array(RuntimeMode)),
490 canRunChildTask: Schema.Boolean,492 canRunChildTask: Schema.Boolean,
491 canRunCrossProviderChildTask: Schema.Boolean,493 canRunCrossProviderChildTask: Schema.Boolean,
492 constraints: Schema.Array(Schema.String),494 constraints: Schema.Array(Schema.String),