diff --git a/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts b/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts index c35b605dca4fb..dfaebe9228d52 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.activity.test.ts @@ -136,6 +136,7 @@ it("readThread preserves caller access errors and prefers activity-run status ov makeRun({ id: cancelledRunId, ordinal: 2, status: "cancelled" }), ], visibleTurnItems: [], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], @@ -222,6 +223,7 @@ it("readThread pages cut-off text without splitting an emoji across pages", asyn }), runs: [makeRun({ id: activeRunId, ordinal: 1, status: "running" })], visibleTurnItems: [], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], @@ -312,6 +314,7 @@ it("taskStatus returns task.providerInstanceId rather than the driver kind", asy }), runs: [makeRun({ id: activeRunId, ordinal: 1, status: "running" })], visibleTurnItems: [], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], @@ -365,6 +368,7 @@ it("taskStatus returns task.providerInstanceId rather than the driver kind", asy }), ], visibleTurnItems: [], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [ @@ -438,6 +442,7 @@ it("readThread and sendToThread reach threads in other projects", async () => { return parentRuns; }, visibleTurnItems: [], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], @@ -474,6 +479,7 @@ it("readThread and sendToThread reach threads in other projects", async () => { }, }, ], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], @@ -670,6 +676,7 @@ it("requestSecret saves a pasted value to a file without opening a card", async }), runs: [{ ...makeRun({ id: activeRunId, ordinal: 1, status: "running" }), rootNodeId: taskId }], visibleTurnItems: [], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], @@ -791,6 +798,7 @@ it("requestSecret returns the unsaved note for a declined, cancelled or timed-ou card === undefined ? [] : [{ type: "secret_request", id: card.turnItemId, secretStatus: outcome }], + providerTurns: [], runtimeRequests: [], messages: [], contextTransfers: [], diff --git a/apps/server/src/mcp/OrchestratorMcpService.test.ts b/apps/server/src/mcp/OrchestratorMcpService.test.ts index f52ac95174277..df13d037005a4 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.test.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.test.ts @@ -145,6 +145,7 @@ describe("OrchestratorMcpService", () => { turnItems: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], } as unknown as OrchestrationV2ThreadProjection; let hasNestedWork = true; @@ -288,6 +289,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], turnItems: [], } as unknown as OrchestrationV2ThreadProjection; @@ -385,6 +387,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: pendingRequest ? [{ status: "pending" }] : [], nodes: [], turnItems: [], @@ -485,6 +488,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], } as unknown as OrchestrationV2ThreadProjection; const layerDependencies = Layer.mergeAll( @@ -575,6 +579,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], turnItems: [], } as unknown as OrchestrationV2ThreadProjection; @@ -678,6 +683,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], } as unknown as OrchestrationV2ThreadProjection; const layerDependencies = Layer.mergeAll( @@ -761,6 +767,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], } as unknown as OrchestrationV2ThreadProjection; const layerDependencies = Layer.mergeAll( @@ -850,6 +857,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], } as unknown as OrchestrationV2ThreadProjection; const layerDependencies = Layer.mergeAll( @@ -973,6 +981,7 @@ describe("OrchestratorMcpService", () => { appOwnedTask("node:mcp-cancel-grandchild-below", childThreadId, grandchildThreadId), ], providerThreads: [], + providerTurns: [], runtimeRequests: [], }, ], @@ -997,6 +1006,7 @@ describe("OrchestratorMcpService", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], }, ], @@ -1233,6 +1243,7 @@ describe("OrchestratorMcpService provider resolution", () => { messages: [], subagents: [], providerThreads: [], + providerTurns: [], runtimeRequests: [], turnItems: [], } as unknown as OrchestrationV2ThreadProjection; @@ -3060,9 +3071,18 @@ describe("OrchestratorMcpService provider resolution", () => { ordinal: 1, status: "running", modelSelection: runSelection, + activeAttemptId: "attempt:mcp-restarted-child", }, ] : [], + // Claude ran the requested Opus on Sonnet. + providerTurns: [ + { + runAttemptId: "attempt:mcp-restarted-child", + ordinal: 1, + model: "claude-sonnet-4-6", + }, + ], } as unknown as OrchestrationV2ThreadProjection), ), }), @@ -3076,11 +3096,15 @@ describe("OrchestratorMcpService provider resolution", () => { ); yield* Effect.gen(function* () { const service = yield* OrchestratorMcpService.OrchestratorMcpService; - for (const selection of [runSelection, threadSelection]) { + for (const [selection, executedModel] of [ + [runSelection, "claude-sonnet-4-6"], + [threadSelection, null], + ] as const) { const result = yield* service.taskStatus(scope, taskId); assert.equal(result.providerInstanceId, selection.instanceId); assert.equal(result.model, selection.model); assert.deepEqual(result.options, selection.options); + assert.equal(result.executedModel, executedModel); hasRun = false; } }).pipe(Effect.provide(layerOrchestratorService.pipe(Layer.provide(dependencies)))); diff --git a/apps/server/src/mcp/OrchestratorMcpService.ts b/apps/server/src/mcp/OrchestratorMcpService.ts index 3f91b4b3df2ab..4750e9a335f49 100644 --- a/apps/server/src/mcp/OrchestratorMcpService.ts +++ b/apps/server/src/mcp/OrchestratorMcpService.ts @@ -63,6 +63,7 @@ import { type ServerProviderModel, ThreadId, isOrchestrationV2WorkActive, + latestProviderTurnForAttempt, resolveProjectProviderInstanceEnabled, SECRET_REQUEST_UNSAVED_NOTE, } from "@t3tools/contracts"; @@ -614,7 +615,7 @@ function pageIncludesTerminalTaskResult(input: { readonly task: OrchestrationV2Subagent; readonly target: Pick< OrchestrationV2ThreadProjection, - "thread" | "runs" | "contextTransfers" | "messages" | "turnItems" + "thread" | "runs" | "contextTransfers" | "messages" | "turnItems" | "providerTurns" >; readonly maxChars: number; }): boolean { @@ -875,13 +876,26 @@ function threadDetail( }; } -function threadRun(run: OrchestrationV2Run): OrchestratorMcpThreadRun { +function runExecutedModel( + projection: Pick, + run: Pick | undefined, +): string | null { + return run === undefined + ? null + : (latestProviderTurnForAttempt(projection.providerTurns, run.activeAttemptId)?.model ?? null); +} + +function threadRun( + projection: Pick, + run: OrchestrationV2Run, +): OrchestratorMcpThreadRun { return { runId: run.id, ordinal: run.ordinal, status: run.status, providerInstanceId: run.modelSelection.instanceId, model: run.modelSelection.model, + executedModel: runExecutedModel(projection, run), requestedAt: DateTime.formatIso(run.requestedAt), startedAt: run.startedAt === null ? null : DateTime.formatIso(run.startedAt), completedAt: run.completedAt === null ? null : DateTime.formatIso(run.completedAt), @@ -1280,10 +1294,11 @@ const make = Effect.gen(function* () { const records = parent === undefined ? threadManagement - .getProjectThreadRecords(input, ["runs", "runtimeRequests"]) + .getProjectThreadRecords(input, ["runs", "providerTurns", "runtimeRequests"]) .pipe(Effect.map((target) => ({ ...target, contextTransfers: [] }))) : threadManagement.getProjectThreadRecords(input, [ "runs", + "providerTurns", "runtimeRequests", "contextTransfers", ]); @@ -1509,6 +1524,7 @@ const make = Effect.gen(function* () { "contextTransfers", "subagents", "providerThreads", + "providerTurns", "runtimeRequests", "nodes", ], @@ -1601,7 +1617,12 @@ const make = Effect.gen(function* () { : null; const resultContextTransferId = resultTransfer?.id ?? null; const latestTerminalResultContextTransferId = resultTransferForRun(terminalRun)?.id ?? null; - const modelSelection = childRun?.modelSelection ?? + const executionRun = + task.result === null + ? (taskResultRun ?? childRun) + : (childControls.runs.find((run) => run.id === resultTransfer?.sourcePoint.runId) ?? + childRun); + const modelSelection = executionRun?.modelSelection ?? childControls.thread.modelSelection ?? { instanceId: task.providerInstanceId, model: task.model, @@ -1620,6 +1641,7 @@ const make = Effect.gen(function* () { hasPendingChildRuns: hasPendingChildRuns(childProjection, childRun), providerInstanceId: modelSelection.instanceId, model: modelSelection.model, + executedModel: runExecutedModel(childControls, executionRun), options: modelSelection.options ?? [], summary: derivedResult, resultContextTransferId, @@ -1790,6 +1812,7 @@ const make = Effect.gen(function* () { hasPendingChildRuns: false, providerInstanceId: task.providerInstanceId, model: task.model, + executedModel: null, summary: task.result, resultContextTransferId: null, latestTerminalRunId: null, @@ -3130,7 +3153,7 @@ const make = Effect.gen(function* () { recentRuns: target.runs .toSorted((left, right) => right.ordinal - left.ordinal) .slice(0, input.runLimit ?? DEFAULT_THREAD_RUN_LIMIT) - .map(threadRun), + .map((run) => threadRun(target, run)), items: page.map((row) => timelineItem({ row, diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts index 4877eed64f146..5f446dc2e1888 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts @@ -2970,11 +2970,15 @@ describe("ClaudeAdapterV2 background wake turns", () => { uuid: "00000000-0000-4000-8000-000000000101", session_id: WAKE_NATIVE_SESSION, }); - const makeAssistantTextFrame = (input: { readonly uuid: string; readonly text: string }) => + const makeAssistantTextFrame = (input: { + readonly uuid: string; + readonly text: string; + readonly model?: string; + }) => claudeSdkFrame({ type: "assistant", message: { - model: "claude-sonnet-4-6", + model: input.model ?? "claude-sonnet-4-6", id: `msg_${input.uuid}`, type: "message", role: "assistant", @@ -6838,7 +6842,12 @@ describe("ClaudeAdapterV2 background wake turns", () => { }), ); yield* Queue.offerAll(harness.sdkMessages, [ - makeAssistantTextFrame({ uuid: mainUuid, text: "Delegating." }), + // Claude reports the model it ran, which can differ from the one requested. + makeAssistantTextFrame({ + uuid: mainUuid, + text: "Delegating.", + model: "claude-opus-4-5-20251101", + }), makeAssistantErrorFrame({ uuid: "00000000-0000-4000-8000-000000000642", error: undefined, @@ -6850,6 +6859,7 @@ describe("ClaudeAdapterV2 background wake turns", () => { const providerTurn = lastProviderTurn(harness.events); assert.equal(providerTurn.nativeTurnRef?.nativeId, mainUuid); + assert.equal(providerTurn.model, "claude-opus-4-5"); const rolledBack = yield* harness.runtime.rollbackThread({ providerThread: harness.providerThread, target: { diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts index 2824552b32ea7..6adadf385ad73 100644 --- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts @@ -189,6 +189,7 @@ import { type ClaudeModelCatalog, getClaudeCatalogModelCapabilities, resolveClaudeModelSlug, + resolveClaudeReportedModelSlug, scopeClaudeModelCatalog, } from "../../provider/ClaudeModelCatalog.ts"; import { @@ -4398,6 +4399,8 @@ interface ActiveClaudeTurnContext { nativeWakeStarted: boolean; gatedFramesBeforeEcho: number; readonly heldRootFrames: Array; + // Claude can run another model than requested, such as when an organization disallows it. + executedModel: string | undefined; } interface ActiveClaudeProviderRetry { @@ -5498,6 +5501,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( startedAt: context.startedAt, completedAt: null, tokenUsage, + ...(context.executedModel === undefined ? {} : { model: context.executedModel }), }, }); }); @@ -6008,6 +6012,9 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( status: input.status, startedAt: input.context.startedAt, completedAt: input.completedAt, + ...(input.context.executedModel === undefined + ? {} + : { model: input.context.executedModel }), }); const buildToolCallArtifacts = (input: { @@ -9134,6 +9141,10 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( // CLI notices and API errors arrive as assistant messages from "". if (message.message.model && message.message.model !== "") { context.rootModels.add(message.message.model); + context.executedModel = resolveClaudeReportedModelSlug( + modelCatalog, + message.message.model, + ); } // Subagent uuids exist only in the sidechain transcript, so resume and fork cannot target them. context.nativeMessageCursor = message.uuid; @@ -11262,6 +11273,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* ( nativeWakeStarted: false, gatedFramesBeforeEcho: 0, heldRootFrames: [], + executedModel: undefined, }; const attachTurn = Effect.gen(function* () { querySession.lastTurn = context; diff --git a/apps/server/src/orchestration-v2/SubagentProjection.test.ts b/apps/server/src/orchestration-v2/SubagentProjection.test.ts index ae8f7e09d2068..87ac8a2ad1758 100644 --- a/apps/server/src/orchestration-v2/SubagentProjection.test.ts +++ b/apps/server/src/orchestration-v2/SubagentProjection.test.ts @@ -413,6 +413,14 @@ it("orders a task's assistant messages by turn item, not by timestamps", () => { assert.equal(result.text, "All steps passed."); assert.equal(result.messageId, report.message.id); assert.equal(result.turnItemId, report.turnItem.id); + const reported = { runAttemptId: run.activeAttemptId, ordinal: 1, model: "gpt-5.4-mini" }; + assert.equal( + subagentResultForRun( + { ...projection, messages: [report.message], providerTurns: [reported as never] }, + run, + ).text, + "Requested model gpt-5.4; the provider reported running gpt-5.4-mini.\n\nAll steps passed.", + ); }); it.effect("ends an orphaned native subagent's node when only its record already ended", () => diff --git a/apps/server/src/peer/RemoteDelegation.test.ts b/apps/server/src/peer/RemoteDelegation.test.ts index b487c34918cb6..317a11251aff4 100644 --- a/apps/server/src/peer/RemoteDelegation.test.ts +++ b/apps/server/src/peer/RemoteDelegation.test.ts @@ -100,6 +100,8 @@ interface BoxThread { readonly laterError: Ref.Ref; /** A later run's reply, after the task's own, as a follow-up message would leave. */ readonly laterReply: Ref.Ref; + /** The model the provider there reported running the task's run. */ + readonly executedModel: Ref.Ref; /** The run each wait asked for, in order. */ readonly waitedFor: Ref.Ref>; /** The thread's run ends with a task of its own still running, whose result wakes it again. */ @@ -145,6 +147,7 @@ const makeBoxThread = Effect.gen(function* () { error: yield* Ref.make(null), laterError: yield* Ref.make(null), laterReply: yield* Ref.make(null), + executedModel: yield* Ref.make(null), waitedFor: yield* Ref.make>([]), nested: yield* Ref.make(false), nestedWaiting: yield* Deferred.make(), @@ -226,6 +229,7 @@ const boxToolkitLayer = (thread: BoxThread) => { ordinal: 1, status, modelSelection, + activeAttemptId: "attempt:box", requestedAt: DateTime.makeUnsafe(0), startedAt: DateTime.makeUnsafe(0), completedAt: null, @@ -235,9 +239,16 @@ const boxToolkitLayer = (thread: BoxThread) => { const shell = yield* shellOf(threadId); const status = yield* Ref.get(thread.status); const runless = yield* Ref.get(thread.runless); + const model = yield* Ref.get(thread.executedModel); return shell === null ? (null as never) - : { ...idleThreadProjection(shell), runs: runless ? [] : [runOf(status)] }; + : { + ...idleThreadProjection(shell), + runs: runless ? [] : [runOf(status)], + providerTurns: (model === null + ? [] + : [{ runAttemptId: "attempt:box", ordinal: 1, model }]) as never, + }; }); // One record per command id, as its receipt replays a repeat. const recordStop = (commandId: string, kind: "interrupt" | "thread.stop" | "delegated-tasks") => @@ -1264,11 +1275,14 @@ it.effect("a task's result is its own run's whole last reply, not a later run's" const long = "x".repeat(45_000); yield* Ref.set(thread.reply, long); yield* Ref.set(thread.laterReply, "Someone else asked a follow-up."); + yield* Ref.set(thread.executedModel, "gpt-5.4-mini"); yield* Ref.set(thread.status, "completed"); yield* Deferred.succeed(thread.finished, undefined); const task = yield* taskResult(a, taskId); expect(task?.status).toBe("completed"); - expect(task?.result).toBe(long); + expect(task?.result).toBe( + `Requested model gpt-5.4; the provider reported running gpt-5.4-mini.\n\n${long}`, + ); // Every wait was for the run the launch started. const waits = yield* Ref.get(thread.waitedFor); expect(waits.length).toBeGreaterThan(0); diff --git a/apps/server/src/peer/RemoteDelegation.ts b/apps/server/src/peer/RemoteDelegation.ts index 89917026ff8f7..221043f351030 100644 --- a/apps/server/src/peer/RemoteDelegation.ts +++ b/apps/server/src/peer/RemoteDelegation.ts @@ -5,6 +5,7 @@ import { OrchestratorMcpFailure, type OrchestratorMcpDelegateTaskInput, type OrchestratorMcpTaskCancelResult, + type OrchestratorMcpThreadRun, type OrchestratorMcpThreadTimelineItem, type OrchestrationV2RemoteTaskChild, type OrchestrationV2Subagent, @@ -24,6 +25,7 @@ import * as Schedule from "effect/Schedule"; import * as Scope from "effect/Scope"; import * as Stream from "effect/Stream"; import type * as Tool from "effect/ai/Tool"; +import { withExecutedModelNote } from "@t3tools/provider-core/server/subagentProjection"; import type * as McpInvocationContext from "../mcp/McpInvocationContext.ts"; import { ProjectToolkit } from "../mcp/toolkits/project/tools.ts"; @@ -435,6 +437,7 @@ const make = Effect.gen(function* () { * The last reply `runId` gave on the task's thread there, which is its * result, read whole however long it is. A failed run prefers its error * from the activity view over an earlier reply, without taking another run's. + * Led by a note when its provider reported running another model than it asked for. */ const lastReply = Effect.fn("RemoteDelegation.lastReply")(function* ( scope: ThreadScope, @@ -445,13 +448,16 @@ const make = Effect.gen(function* () { let reply: OrchestratorMcpThreadTimelineItem | undefined; let error: OrchestratorMcpThreadTimelineItem | undefined; let afterPosition: number | undefined; + let run: OrchestratorMcpThreadRun | undefined; while (true) { const read = yield* forwarding.readThread(scope, remote.environmentId, { threadId: remote.threadId, view: failed ? "activity" : "messages", limit: 100, + runLimit: 50, ...(afterPosition === undefined ? {} : { afterPosition }), }); + run ??= read.recentRuns.find((candidate) => candidate.runId === runId); reply = read.items.findLast( (item) => item.type === "assistant_message" && (runId === null || item.runId === runId), @@ -481,7 +487,7 @@ const make = Effect.gen(function* () { text += part.text ?? ""; textOffset = part.textTruncated ? (part.nextTextOffset ?? null) : null; } - return text; + return run === undefined ? text : withExecutedModelNote(text, run.model, run.executedModel); }); /** diff --git a/apps/server/src/provider/ClaudeModelCatalog.ts b/apps/server/src/provider/ClaudeModelCatalog.ts index 57132e652ae08..4efca48f253bf 100644 --- a/apps/server/src/provider/ClaudeModelCatalog.ts +++ b/apps/server/src/provider/ClaudeModelCatalog.ts @@ -239,6 +239,19 @@ export function resolveClaudeModelSlug(catalog: ClaudeModelCatalog, slugOrAlias: return resolveClaudeCatalogModel(catalog, slugOrAlias)?.model.slug ?? slugOrAlias; } +/** Claude reports dated IDs, such as `claude-opus-4-5-20251101`, that the catalog may not list. */ +export function resolveClaudeReportedModelSlug( + catalog: ClaudeModelCatalog, + reported: string, +): string { + return ( + ( + resolveClaudeCatalogModel(catalog, reported) ?? + resolveClaudeCatalogModel(catalog, reported.replace(/-\d{8}$/, "")) + )?.model.slug ?? reported + ); +} + export function getClaudeCatalogModelCapabilities( catalog: ClaudeModelCatalog, slugOrAlias: string | null | undefined, diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts index ee75d861daa7e..77a89ac3520aa 100644 --- a/packages/contracts/src/orchestrationV2.ts +++ b/packages/contracts/src/orchestrationV2.ts @@ -1202,6 +1202,8 @@ export const OrchestrationV2ProviderTurn = Schema.Struct({ completedAt: Schema.NullOr(Schema.DateTimeUtc), tokenUsage: Schema.optional(OrchestrationV2ProviderTurnTokenUsage), turnTokenUsage: Schema.optional(TurnTokenUsage), + /** The model the provider reported running this turn's root agent, which can differ from the requested one. */ + model: Schema.optional(TrimmedNonEmptyString), }); export type OrchestrationV2ProviderTurn = typeof OrchestrationV2ProviderTurn.Type; diff --git a/packages/contracts/src/orchestratorMcp.ts b/packages/contracts/src/orchestratorMcp.ts index 5c57cd1b16fb1..d1bb9961a0c68 100644 --- a/packages/contracts/src/orchestratorMcp.ts +++ b/packages/contracts/src/orchestratorMcp.ts @@ -202,6 +202,12 @@ export const ORCHESTRATOR_MCP_RUNNING_TASK_NEXT = "Completion wakes this thread, so end your turn. Do not wait on it or poll it."; const OrchestratorMcpRunningTaskNext = Schema.Literal(ORCHESTRATOR_MCP_RUNNING_TASK_NEXT); +// Optional: linked environments on older versions omit it. +const OrchestratorMcpExecutedModel = Schema.NullOr(Schema.String).annotate({ + description: + "Model the provider reported running, or null when unavailable. Can differ from the requested `model`.", +}); + export const OrchestratorMcpDelegateTaskResult = Schema.Struct({ taskId: NodeId, /** Null for a task that runs in a linked environment; see `remoteChild`. */ @@ -222,6 +228,7 @@ export const OrchestratorMcpDelegateTaskResult = Schema.Struct({ latestTerminalResultContextTransferId: Schema.NullOr(ContextTransferId), providerInstanceId: ProviderInstanceId, model: Schema.NullOr(Schema.String), + executedModel: Schema.optional(OrchestratorMcpExecutedModel), options: Schema.optional(Schema.Array(ProviderOptionSelection)), summary: Schema.NullOr(Schema.String), resultContextTransferId: Schema.NullOr(ContextTransferId), @@ -463,6 +470,7 @@ export const OrchestratorMcpThreadRun = Schema.Struct({ status: OrchestrationV2RunStatus, providerInstanceId: ProviderInstanceId, model: Schema.String, + executedModel: Schema.optional(OrchestratorMcpExecutedModel), requestedAt: IsoDateTime, startedAt: Schema.NullOr(IsoDateTime), completedAt: Schema.NullOr(IsoDateTime), diff --git a/packages/provider-core/src/server/subagentProjection.ts b/packages/provider-core/src/server/subagentProjection.ts index dbd929192e04d..62145567f6651 100644 --- a/packages/provider-core/src/server/subagentProjection.ts +++ b/packages/provider-core/src/server/subagentProjection.ts @@ -22,7 +22,7 @@ import { backgroundWorkHoldsCompletion } from "@t3tools/shared/orchestrationV2Pe import { runRanAfter } from "@t3tools/shared/orchestrationV2ThreadError"; import * as DateTime from "effect/DateTime"; import * as Effect from "effect/Effect"; -import { isOrchestrationV2WorkActive } from "@t3tools/contracts"; +import { isOrchestrationV2WorkActive, latestProviderTurnForAttempt } from "@t3tools/contracts"; function trimmed(value: string | null | undefined): string | undefined { const result = value?.trim(); @@ -176,8 +176,19 @@ export function makeSubagentConversationArtifacts(input: { return { message, turnItem }; } +/** A delegated result, led by a note when the provider reported running another model than requested. */ +export function withExecutedModelNote( + result: string, + requestedModel: string, + executedModel: string | null | undefined, +): string { + return executedModel == null || executedModel === requestedModel + ? result + : `Requested model ${requestedModel}; the provider reported running ${executedModel}.\n\n${result}`; +} + export function subagentResultForRun( - projection: Pick, + projection: Pick, run: OrchestrationV2Run, ): { readonly text: string; @@ -223,8 +234,15 @@ export function subagentResultForRun( (run.status === "completed" ? "Child task completed without an assistant result." : `Child task ended with status ${run.status}.`); + const executedModel = latestProviderTurnForAttempt( + projection.providerTurns, + run.activeAttemptId, + )?.model; return { - text, + text: + executedModel === undefined + ? text + : withExecutedModelNote(text, run.modelSelection.model, executedModel), messageId: failure === undefined ? (message?.id ?? turnItem?.messageId ?? null) : null, turnItemId: failure?.id ?? turnItem?.id ?? null, };