From eb7f45ea9b3049b3f82484478eb911f6d880c00d Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:01:49 +0200 Subject: [PATCH 01/22] fix(acp): agents get T3's runtime notes once per session, not on every message The ACP adapter appended the runtime_info and pull request linking block to every non-slash prompt. ACP agents keep their own history, so the block piled up, one copy per user message. Remember per session the block last delivered and resend it only when its text changes (a model switch), after compaction, or in a new or replacement session. Delivery is recorded after a successful session/prompt, next to the existing instruction state. Replay fixtures expect the block until a prompt in the session succeeds, matching the adapter. Fixes #24185 Co-authored-by: Mariusz Mazur Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 39 ++++++++++++++----- .../testkit/ReplayRuntimeInstructions.ts | 20 ++++++++++ packages/provider-acp/src/server/adapter.ts | 30 ++++++++++---- 3 files changed, 71 insertions(+), 18 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 8a68e92b89160..569022bc67155 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -416,6 +416,7 @@ function assertReinjectedInstructions( assert.include(text, "## T3 Code browser"); assert.include(text, "T3 Code orchestration"); assert.include(text, `\n${request}\n`); + assert.include(blocks[1]?.text ?? "", ""); } describe("AcpAdapterV2", () => { @@ -959,6 +960,7 @@ describe("AcpAdapterV2", () => { ordinal: number, runtimePolicy: ProviderAdapter.ProviderAdapterV2RuntimePolicy, messageText: string, + turnModelSelection: ModelSelection = modelSelection, ) { yield* runtime.startTurn( makeTurnInput({ @@ -969,6 +971,7 @@ describe("AcpAdapterV2", () => { now, ordinal, messageText, + modelSelection: turnModelSelection, }), ); yield* runtime.events.pipe( @@ -1006,15 +1009,15 @@ describe("AcpAdapterV2", () => { assert.include(firstDefault.prompt, "T3 Code interaction mode: Default"); assert.include(firstDefault.prompt, "## T3 Code browser"); assert.include(firstDefault.prompt, "T3 Code orchestration"); + assert.include(firstDefault.prompt, ""); assert.notInclude( firstDefault.methods, "session/set_config_option", "Build should preserve the agent's advertised mode default", ); - assert.include( - (yield* runTurn(3, defaultPolicy, "Second default request.")).prompt, - "Second default request.", - ); + const secondDefault = yield* runTurn(3, defaultPolicy, "Second default request."); + assert.equal(secondDefault.prompt, "Second default request."); + assert.equal(secondDefault.textBlockCount, 1); const planPolicy = policy("plan"); const firstPlan = yield* runTurn(4, planPolicy, "Plan this change."); @@ -1031,6 +1034,12 @@ describe("AcpAdapterV2", () => { "session/set_config_option", "Build should restore the native mode that T3 temporarily replaced for Plan", ); + const switchedModel = yield* runTurn(7, defaultPolicy, "Switch models.", { + instanceId, + model: "composer-2", + }); + assert.equal(switchedModel.textBlockCount, 2); + assert.include(switchedModel.prompt, "as composer-2"); }).pipe(Effect.provide(layerTest), Effect.scoped), ); @@ -1054,7 +1063,7 @@ describe("AcpAdapterV2", () => { yield* runTurn("Continue after the second compaction."), "Continue after the second compaction.", ); - assert.equal((yield* runTurn("Finish work."))[0]?.text, "Finish work."); + assert.deepEqual(yield* runTurn("Finish work."), [{ type: "text", text: "Finish work." }]); }).pipe(Effect.provide(layerTest), Effect.scoped), ); @@ -1069,14 +1078,13 @@ describe("AcpAdapterV2", () => { assert.deepEqual(yield* runTurn("/compact", "failed"), [ { type: "text", text: "/compact" }, ]); - assert.equal( - (yield* runTurn("Continue after failure."))[0]?.text, - "Continue after failure.", - ); + assert.deepEqual(yield* runTurn("Continue after failure."), [ + { type: "text", text: "Continue after failure." }, + ]); assert.deepEqual(yield* runTurn("/compact"), [{ type: "text", text: "/compact" }]); assert.deepEqual(yield* runTurn("/repro-skill"), [{ type: "text", text: "/repro-skill" }]); assertReinjectedInstructions(yield* runTurn("Continue work."), "Continue work."); - assert.equal((yield* runTurn("Finish work."))[0]?.text, "Finish work."); + assert.deepEqual(yield* runTurn("Finish work."), [{ type: "text", text: "Finish work." }]); }).pipe(Effect.provide(layerTest), Effect.scoped), ); @@ -3285,9 +3293,11 @@ describe("AcpAdapterV2", () => { browserToolsAvailable: false, }); const runtimeInputs: Array = []; + const protocolEvents = yield* Queue.unbounded(); const makeRuntime = makeMockRuntime({ childProcessSpawner, mockAgentPath, + protocolEvents, environment: { T3_ACP_PROMPT_DELAY_MS: "100" }, }); const adapter = yield* makeAcpAdapterV2({ @@ -3419,6 +3429,15 @@ describe("AcpAdapterV2", () => { } } assert.equal(secondStatus, "completed"); + const prompts = Array.from(yield* Queue.takeAll(protocolEvents)) + .filter( + (event) => + event.direction === "outgoing" && rawProtocolMethod(event) === "session/prompt", + ) + .map(rawProtocolPromptTextBlocks); + assert.lengthOf(prompts, 2); + // The mock agent reuses its session id, so only the rollback reset resends these notes. + assert.include(prompts[1]?.at(-1)?.text ?? "", ""); }).pipe(Effect.provide(layerTest), Effect.scoped), ); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index 661856720fc27..a939b4be94c5e 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -18,10 +18,28 @@ export function materializeReplayTranscriptRuntimeInstructions( : undefined; if (harness === undefined) return transcript; const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); + // The ACP adapter resends these instructions until a prompt in that session succeeds. + const instructedSessionIds = new Set(); + let promptSessionId: unknown; return { ...transcript, entries: transcript.entries.map((entry) => { + if (entry.type === "emit_inbound") { + const frame = entry.frame; + if ( + typeof frame === "object" && + frame !== null && + "kind" in frame && + frame.kind === "response" && + "method" in frame && + frame.method === "session/prompt" && + "result" in frame + ) { + instructedSessionIds.add(promptSessionId); + } + return entry; + } if (entry.type !== "expect_outbound") return entry; const frame = entry.frame; if (typeof frame !== "object" || frame === null) return entry; @@ -45,6 +63,8 @@ export function materializeReplayTranscriptRuntimeInstructions( "prompt" in frame.params && Array.isArray(frame.params.prompt) ) { + promptSessionId = "sessionId" in frame.params ? frame.params.sessionId : undefined; + if (instructedSessionIds.has(promptSessionId)) return entry; const prompt: ReadonlyArray = frame.params.prompt; const lastPart = prompt.at(-1); if ( diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 398eac2170e70..6099704ee443f 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -1594,6 +1594,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( const activeSelection = yield* Ref.make(null); const activeInteractionMode = yield* Ref.make(null); const promptInstructionStates = yield* Ref.make(new Map()); + const promptRuntimeInstructions = yield* Ref.make(new Map()); // Unique markers keep delayed prompt completions from clearing later compactions. const compactionMarkers = yield* Ref.make(new Map()); const runtimeRestartRequired = yield* Ref.make(false); @@ -6574,6 +6575,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( yield* Ref.set(activeSelection, null); yield* Ref.set(activeInteractionMode, null); yield* Ref.set(promptInstructionStates, new Map()); + yield* Ref.set(promptRuntimeInstructions, new Map()); yield* Ref.set(compactionMarkers, new Map()); yield* Ref.set(providerTurns, new Map()); yield* Ref.set(snapshot, { @@ -7279,18 +7281,22 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( detail: "ACP turn requires non-empty text or attachments", }); } - if (!isNativeSlashCommand) { - prompt.push({ - type: "text", - text: buildRuntimeInstructions({ - harness: flavor.runtimeHarness ?? driver, - model: turnInput.modelSelection.model, - }), - }); + const runtimeInstructions = buildRuntimeInstructions({ + harness: flavor.runtimeHarness ?? driver, + model: turnInput.modelSelection.model, + }); + // The agent keeps its own history, so resend only after compaction or when the text changes. + const sendRuntimeInstructions = + !isNativeSlashCommand && + (compactionMarker !== undefined || + (yield* Ref.get(promptRuntimeInstructions)).get(sessionId) !== runtimeInstructions); + if (sendRuntimeInstructions) { + prompt.push({ type: "text", text: runtimeInstructions }); } return { prompt, instructionState: text === messageText ? undefined : instructionState, + runtimeInstructions: sendRuntimeInstructions ? runtimeInstructions : undefined, compactionMarker, }; }); @@ -7603,6 +7609,14 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( return updated; }), ), + Effect.tap(() => + Ref.update(promptRuntimeInstructions, (current) => { + if (promptParts?.runtimeInstructions === undefined) return current; + const updated = new Map(current); + updated.set(requestedSessionId, promptParts.runtimeInstructions); + return updated; + }), + ), Effect.tap(() => Ref.update(compactionMarkers, (current) => { if (promptParts?.instructionState === undefined) return current; From 3c990efa449881d3fef004b6b7a86adb91dacd5c Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:29:48 +0200 Subject: [PATCH 02/22] fix(acp): resend T3's notes after idle and Copilot compactions A compaction_update that completed while no turn was active, or after the turn settled, never set the compaction marker, so the next prompt went out without T3's instructions or runtime notes. Mark a completed root-session compaction where session updates arrive, before turn projection. Copilot reports compaction only in its raw session-event feed. Subscribe to session.compaction_complete and, for an applied root compaction, mark the session compacted through a new markSessionCompacted extension hook. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 116 +++++++++++++----- .../src/server/copilotAcp.ts | 8 ++ packages/provider-acp/src/server/adapter.ts | 35 ++++-- 3 files changed, 121 insertions(+), 38 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 569022bc67155..29652742ec7f7 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -345,12 +345,18 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( authorizationHeader: "Bearer instruction-compaction-token", browserToolsAvailable: true, }); + type Runtime = AcpSessionRuntime.AcpSessionRuntime["Service"]; + let sessionUpdate: Parameters[0] | undefined; + let copilotSessionEvent: + | ((event: unknown) => Effect.Effect) + | undefined; const adapter = yield* makeAcpAdapterV2({ instanceId, flavor: { driver: ACP_TEST_DRIVER, capabilities: AcpProviderCapabilitiesV2, supportsCompaction: true, + registerExtensions: makeCopilotSubagentRouting().registerExtensions, makeRuntime: makeMockRuntime({ childProcessSpawner, mockAgentPath, @@ -358,6 +364,17 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( environment, wrapRuntime: (runtime) => ({ ...runtime, + // The captured handlers deliver frames between turns, while no prompt is in flight. + handleSessionUpdate: (next) => + Effect.sync(() => { + sessionUpdate = next; + }).pipe(Effect.andThen(runtime.handleSessionUpdate(next))), + handleExtNotification: (method, payload, next) => + Effect.sync(() => { + if (method === "github.com/copilot/sessionEvent") { + copilotSessionEvent = next as typeof copilotSessionEvent; + } + }).pipe(Effect.andThen(runtime.handleExtNotification(method, payload, next))), // Make compaction during prompt completion deterministic at the adapter boundary. prompt: (payload, options) => runtime.prompt(payload, options).pipe(Effect.tap(() => runtime.drainEvents)), @@ -381,30 +398,45 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( const providerThread = yield* runtime.ensureThread({ threadId, modelSelection, runtimePolicy }); const now = yield* DateTime.now; let ordinal = 0; - return Effect.fnUntraced(function* (messageText: string, expectedStatus = "completed") { - yield* runtime.startTurn( - makeTurnInput({ - threadId, - providerThread, - instanceId, - runtimePolicy, - now, - ordinal: ++ordinal, - messageText, - }), - ); - const terminal = yield* runtime.events.pipe( - Stream.filter((event) => event.type === "turn.terminal"), - Stream.runHead, - ); - assert.equal(Option.getOrThrow(terminal).status, expectedStatus); - while (true) { - const event = yield* Queue.take(protocolEvents); - if (event.direction === "outgoing" && rawProtocolMethod(event) === "session/prompt") { - return rawProtocolPromptTextBlocks(event); + const sessionId = "mock-session-1"; + return { + runTurn: Effect.fnUntraced(function* (messageText: string, expectedStatus = "completed") { + yield* runtime.startTurn( + makeTurnInput({ + threadId, + providerThread, + instanceId, + runtimePolicy, + now, + ordinal: ++ordinal, + messageText, + }), + ); + const terminal = yield* runtime.events.pipe( + Stream.filter((event) => event.type === "turn.terminal"), + Stream.runHead, + ); + assert.equal(Option.getOrThrow(terminal).status, expectedStatus); + while (true) { + const event = yield* Queue.take(protocolEvents); + if (event.direction === "outgoing" && rawProtocolMethod(event) === "session/prompt") { + return rawProtocolPromptTextBlocks(event); + } } - } - }); + }), + idleSessionUpdate: Effect.fnUntraced(function* (update: EffectAcpSchema.SessionUpdate) { + assert.isDefined(sessionUpdate); + yield* sessionUpdate({ sessionId, update }); + }), + idleCopilotSessionEvent: Effect.fnUntraced(function* (event: { + readonly type: string; + readonly agentId?: string; + readonly data: Record; + }) { + assert.isDefined(copilotSessionEvent); + yield* copilotSessionEvent({ sessionId, ...event }); + }), + }; }); function assertReinjectedInstructions( @@ -1044,12 +1076,13 @@ describe("AcpAdapterV2", () => { ); it.live( - "reinjects ACP instructions after protocol compaction, including during reinjection", + "reinjects ACP instructions after protocol compaction, during reinjection or while idle", () => Effect.gen(function* () { - const runTurn = yield* makeInstructionCompactionHarness({ - T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", - }); + const { runTurn, idleSessionUpdate, idleCopilotSessionEvent } = + yield* makeInstructionCompactionHarness({ + T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", + }); yield* runTurn("Start work."); yield* runTurn("Continue until context compacts."); assert.deepEqual(yield* runTurn(" /repro-skill"), [ @@ -1064,6 +1097,33 @@ describe("AcpAdapterV2", () => { "Continue after the second compaction.", ); assert.deepEqual(yield* runTurn("Finish work."), [{ type: "text", text: "Finish work." }]); + + yield* idleSessionUpdate({ + sessionUpdate: "compaction_update", + compactionId: "compact-idle", + status: "completed", + }); + assertReinjectedInstructions( + yield* runTurn("Continue after idle compaction."), + "Continue after idle compaction.", + ); + const copilotCompaction = { success: true, outcome: "applied", trigger: "threshold" }; + yield* idleCopilotSessionEvent({ + type: "session.compaction_complete", + agentId: "agent-a", + data: copilotCompaction, + }); + assert.deepEqual(yield* runTurn("Continue after subagent compaction."), [ + { type: "text", text: "Continue after subagent compaction." }, + ]); + yield* idleCopilotSessionEvent({ + type: "session.compaction_complete", + data: copilotCompaction, + }); + assertReinjectedInstructions( + yield* runTurn("Continue after Copilot compaction."), + "Continue after Copilot compaction.", + ); }).pipe(Effect.provide(layerTest), Effect.scoped), ); @@ -1071,7 +1131,7 @@ describe("AcpAdapterV2", () => { "reinjects ACP instructions after successful native compact, but not failed compact", () => Effect.gen(function* () { - const runTurn = yield* makeInstructionCompactionHarness({ + const { runTurn } = yield* makeInstructionCompactionHarness({ T3_ACP_FAIL_PROMPT_ORDINAL: "2", }); yield* runTurn("Start work."); diff --git a/packages/provider-acp-registry/src/server/copilotAcp.ts b/packages/provider-acp-registry/src/server/copilotAcp.ts index 6414d37b05097..553f8828da695 100644 --- a/packages/provider-acp-registry/src/server/copilotAcp.ts +++ b/packages/provider-acp-registry/src/server/copilotAcp.ts @@ -20,6 +20,7 @@ export const copilotClientCapabilitiesMeta = { "subagent.failed", "assistant.message_delta", "assistant.reasoning_delta", + "session.compaction_complete", ], }, }; @@ -97,6 +98,13 @@ export function makeCopilotSubagentRouting() { result: typeof data.error === "string" ? data.error : null, }); } + // Copilot sends no ACP compaction_update; agentId marks a subagent's own compaction. + case "session.compaction_complete": { + if (event.agentId !== undefined || data.success !== true || data.outcome !== "applied") { + return Effect.void; + } + return context.markSessionCompacted(event.sessionId); + } default: return Effect.void; } diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 6099704ee443f..a00b3796d98fd 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -185,6 +185,8 @@ export interface AcpAdapterV2ExtensionContext { readonly output?: string; readonly report?: BackgroundWork; }) => Effect.Effect; + /** For compactions reported outside ACP `compaction_update`; ignores non-root sessions. */ + readonly markSessionCompacted: (sessionId: string) => Effect.Effect; readonly requestUserInput: ( input: AcpAdapterV2UserInputRequest, requestContext: EffectAcpProtocol.AcpRequestContext, @@ -1597,6 +1599,15 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( const promptRuntimeInstructions = yield* Ref.make(new Map()); // Unique markers keep delayed prompt completions from clearing later compactions. const compactionMarkers = yield* Ref.make(new Map()); + // The next prompt in a compacted root session resends T3's instructions and runtime notes. + const markSessionCompacted = Effect.fn("AcpAdapterV2.markSessionCompacted")(function* ( + sessionId: string, + ) { + if (sessionId !== (yield* Ref.get(activeSessionId))) return; + yield* Ref.update(compactionMarkers, (current) => + new Map(current).set(sessionId, Symbol()), + ); + }); const runtimeRestartRequired = yield* Ref.make(false); const runtimeTeardownState = yield* Ref.make({ _tag: "Idle" }); const runtimeCallbackGeneration = yield* Ref.make(0); @@ -3670,11 +3681,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( updated.delete(context.nativeThreadId); return updated; }); - yield* Ref.update(compactionMarkers, (current) => { - const updated = new Map(current); - updated.set(context.nativeThreadId, Symbol()); - return updated; - }); + yield* markSessionCompacted(context.nativeThreadId); } } else if (mcpIdentity !== undefined) { const toolName = `${mcpIdentity.server}.${mcpIdentity.tool}`; @@ -5826,6 +5833,13 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( ) ?? Effect.void ); } + // Turn projection misses compactions that finish while idle or after settlement. + if ( + notification.update.sessionUpdate === "compaction_update" && + notification.update.status === "completed" + ) { + yield* markSessionCompacted(notification.sessionId); + } yield* handleSessionUpdate( flavor.normalizeSessionUpdate?.(notification) ?? notification, ); @@ -6289,6 +6303,11 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( yield* finishSubagentFromNotice(notice); }), ).pipe(Effect.asVoid), + markSessionCompacted: (sessionId) => + runRuntimeCallbackAtGeneration( + handlerGeneration, + markSessionCompacted(sessionId), + ).pipe(Effect.asVoid), applyBackgroundTaskMutation: (incoming) => runRuntimeCallbackAtGeneration( handlerGeneration, @@ -7019,11 +7038,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( settledStatus === "completed" ) { const nativeItemId = `${context.nativeTurnId}:compaction`; - yield* Ref.update(compactionMarkers, (current) => { - const updated = new Map(current); - updated.set(context.nativeThreadId, Symbol()); - return updated; - }); + yield* markSessionCompacted(context.nativeThreadId); yield* emitProviderEvent({ type: "turn_item.updated", driver, From 492c421922eb45f412ac0190b53c6826e60631e1 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:53:06 +0200 Subject: [PATCH 03/22] fix(acp): mark compactions after routing and accept older Copilot reports Mark a completed compaction_update only after the flavor routes it, so a Devin subagent's compaction no longer resends the root instructions. Accept Copilot compaction reports without `outcome`, which older builds omit. The replay helper now skips native slash commands and records delivery only for prompts that carried the runtime notes, matching the adapter. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 19 +++++++++ .../testkit/ReplayRuntimeInstructions.ts | 39 +++++++++++-------- .../src/server/copilotAcp.ts | 7 +++- packages/provider-acp/src/server/adapter.ts | 11 +++--- 4 files changed, 53 insertions(+), 23 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 29652742ec7f7..e3babd26a85ea 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -357,6 +357,8 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( capabilities: AcpProviderCapabilitiesV2, supportsCompaction: true, registerExtensions: makeCopilotSubagentRouting().registerExtensions, + // Routes updates tagged with a Devin subagent context to that child session. + normalizeSessionUpdate: normalizeDevinSessionUpdate, makeRuntime: makeMockRuntime({ childProcessSpawner, mockAgentPath, @@ -1124,6 +1126,23 @@ describe("AcpAdapterV2", () => { yield* runTurn("Continue after Copilot compaction."), "Continue after Copilot compaction.", ); + yield* idleCopilotSessionEvent({ + type: "session.compaction_complete", + data: { success: true, trigger: "threshold" }, + }); + assertReinjectedInstructions( + yield* runTurn("Continue after legacy Copilot compaction."), + "Continue after legacy Copilot compaction.", + ); + yield* idleSessionUpdate({ + sessionUpdate: "compaction_update", + compactionId: "compact-child", + status: "completed", + _meta: { "cognition.ai/subagent_context": { parentAgentId: "agent-b" } }, + }); + assert.deepEqual(yield* runTurn("Continue after child compaction."), [ + { type: "text", text: "Continue after child compaction." }, + ]); }).pipe(Effect.provide(layerTest), Effect.scoped), ); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index a939b4be94c5e..180c522a1e084 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -18,9 +18,9 @@ export function materializeReplayTranscriptRuntimeInstructions( : undefined; if (harness === undefined) return transcript; const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); - // The ACP adapter resends these instructions until a prompt in that session succeeds. + // The ACP adapter resends these instructions until a prompt that carried them succeeds. const instructedSessionIds = new Set(); - let promptSessionId: unknown; + let pendingInstructedPrompt: { readonly sessionId: unknown } | undefined; return { ...transcript, @@ -34,9 +34,11 @@ export function materializeReplayTranscriptRuntimeInstructions( frame.kind === "response" && "method" in frame && frame.method === "session/prompt" && - "result" in frame + "result" in frame && + pendingInstructedPrompt !== undefined ) { - instructedSessionIds.add(promptSessionId); + instructedSessionIds.add(pendingInstructedPrompt.sessionId); + pendingInstructedPrompt = undefined; } return entry; } @@ -63,20 +65,14 @@ export function materializeReplayTranscriptRuntimeInstructions( "prompt" in frame.params && Array.isArray(frame.params.prompt) ) { - promptSessionId = "sessionId" in frame.params ? frame.params.sessionId : undefined; + const promptSessionId = "sessionId" in frame.params ? frame.params.sessionId : undefined; + pendingInstructedPrompt = undefined; if (instructedSessionIds.has(promptSessionId)) return entry; const prompt: ReadonlyArray = frame.params.prompt; - const lastPart = prompt.at(-1); - if ( - typeof lastPart === "object" && - lastPart !== null && - "type" in lastPart && - lastPart.type === "text" && - "text" in lastPart && - lastPart.text === instructions - ) { - return entry; - } + // Like the adapter, native slash commands go out without instructions. + if (promptPartText(prompt[0])?.trimStart().startsWith("/") === true) return entry; + pendingInstructedPrompt = { sessionId: promptSessionId }; + if (promptPartText(prompt.at(-1)) === instructions) return entry; return { ...entry, frame: { @@ -89,3 +85,14 @@ export function materializeReplayTranscriptRuntimeInstructions( }), }; } + +function promptPartText(part: unknown): string | undefined { + return typeof part === "object" && + part !== null && + "type" in part && + part.type === "text" && + "text" in part && + typeof part.text === "string" + ? part.text + : undefined; +} diff --git a/packages/provider-acp-registry/src/server/copilotAcp.ts b/packages/provider-acp-registry/src/server/copilotAcp.ts index 553f8828da695..3c362635f98b8 100644 --- a/packages/provider-acp-registry/src/server/copilotAcp.ts +++ b/packages/provider-acp-registry/src/server/copilotAcp.ts @@ -99,8 +99,13 @@ export function makeCopilotSubagentRouting() { }); } // Copilot sends no ACP compaction_update; agentId marks a subagent's own compaction. + // Older Copilot builds report a successful compaction without `outcome`. case "session.compaction_complete": { - if (event.agentId !== undefined || data.success !== true || data.outcome !== "applied") { + if ( + event.agentId !== undefined || + data.success !== true || + (data.outcome !== undefined && data.outcome !== "applied") + ) { return Effect.void; } return context.markSessionCompacted(event.sessionId); diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index a00b3796d98fd..ffd8921cb6e95 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -5833,16 +5833,15 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( ) ?? Effect.void ); } + const normalized = flavor.normalizeSessionUpdate?.(notification) ?? notification; // Turn projection misses compactions that finish while idle or after settlement. if ( - notification.update.sessionUpdate === "compaction_update" && - notification.update.status === "completed" + normalized.update.sessionUpdate === "compaction_update" && + normalized.update.status === "completed" ) { - yield* markSessionCompacted(notification.sessionId); + yield* markSessionCompacted(normalized.sessionId); } - yield* handleSessionUpdate( - flavor.normalizeSessionUpdate?.(notification) ?? notification, - ); + yield* handleSessionUpdate(normalized); }, Effect.mapError( (cause) => From 6757893fa12e54727fa6bbab204f46b25302c005 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:35:48 +0200 Subject: [PATCH 04/22] fix(acp): keep compaction marks for every session and mark each compaction once A compaction reported while another session was active, for example during a thread snapshot load, was dropped, so that session's next prompt skipped T3's instructions. Markers are per session and only root sessions receive prompts, so drop the active-session check. Remember completed compaction ids so a later patch of the same compaction does not resend the instructions again. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 8 ++++++ packages/provider-acp/src/server/adapter.ts | 26 +++++++++++++++---- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index e3babd26a85ea..68cf837137078 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1109,6 +1109,14 @@ describe("AcpAdapterV2", () => { yield* runTurn("Continue after idle compaction."), "Continue after idle compaction.", ); + yield* idleSessionUpdate({ + sessionUpdate: "compaction_update", + compactionId: "compact-idle", + status: "completed", + }); + assert.deepEqual(yield* runTurn("Continue after a repeated compaction update."), [ + { type: "text", text: "Continue after a repeated compaction update." }, + ]); const copilotCompaction = { success: true, outcome: "applied", trigger: "threshold" }; yield* idleCopilotSessionEvent({ type: "session.compaction_complete", diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index ffd8921cb6e95..213ab11a2fa5a 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -185,7 +185,7 @@ export interface AcpAdapterV2ExtensionContext { readonly output?: string; readonly report?: BackgroundWork; }) => Effect.Effect; - /** For compactions reported outside ACP `compaction_update`; ignores non-root sessions. */ + /** For compactions reported outside ACP `compaction_update`. */ readonly markSessionCompacted: (sessionId: string) => Effect.Effect; readonly requestUserInput: ( input: AcpAdapterV2UserInputRequest, @@ -1599,11 +1599,23 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( const promptRuntimeInstructions = yield* Ref.make(new Map()); // Unique markers keep delayed prompt completions from clearing later compactions. const compactionMarkers = yield* Ref.make(new Map()); - // The next prompt in a compacted root session resends T3's instructions and runtime notes. + // ACP may patch a completed compaction again; each compaction id marks its session once. + const completedCompactionIds = yield* Ref.make(new Set()); + // The next prompt in a compacted session resends T3's instructions and runtime notes. + // Only root sessions get prompts, so a marker on a child session is never read. const markSessionCompacted = Effect.fn("AcpAdapterV2.markSessionCompacted")(function* ( sessionId: string, + compactionId?: string, ) { - if (sessionId !== (yield* Ref.get(activeSessionId))) return; + if (compactionId !== undefined) { + const key = `${sessionId}\u0000${compactionId}`; + const alreadyMarked = yield* Ref.modify( + completedCompactionIds, + (current): [boolean, Set] => + current.has(key) ? [true, current] : [false, new Set(current).add(key)], + ); + if (alreadyMarked) return; + } yield* Ref.update(compactionMarkers, (current) => new Map(current).set(sessionId, Symbol()), ); @@ -3681,7 +3693,10 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( updated.delete(context.nativeThreadId); return updated; }); - yield* markSessionCompacted(context.nativeThreadId); + yield* markSessionCompacted( + context.nativeThreadId, + toolCall.toolCallId.slice("acp-compaction:".length), + ); } } else if (mcpIdentity !== undefined) { const toolName = `${mcpIdentity.server}.${mcpIdentity.tool}`; @@ -5839,7 +5854,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( normalized.update.sessionUpdate === "compaction_update" && normalized.update.status === "completed" ) { - yield* markSessionCompacted(normalized.sessionId); + yield* markSessionCompacted(normalized.sessionId, normalized.update.compactionId); } yield* handleSessionUpdate(normalized); }, @@ -6595,6 +6610,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( yield* Ref.set(promptInstructionStates, new Map()); yield* Ref.set(promptRuntimeInstructions, new Map()); yield* Ref.set(compactionMarkers, new Map()); + yield* Ref.set(completedCompactionIds, new Set()); yield* Ref.set(providerTurns, new Map()); yield* Ref.set(snapshot, { order: [], From 3c00ca3110319f2ffde0ab02b0235150e9d762ea Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 16:52:20 +0200 Subject: [PATCH 05/22] fix(acp): a compaction left open at turn end still counts when it really finishes When a turn ended with its compaction still in progress, settling projected the compaction as completed and recorded its id, so the agent's real completion later was ignored and the next prompt skipped T3's instructions. Record compaction ids only for the agent's own updates. Co-Authored-By: Claude Opus 5.5 --- apps/server/scripts/acp-mock-agent.ts | 14 ++++++++++++++ .../Adapters/AcpAdapterV2.test.ts | 15 +++++++++++++++ packages/provider-acp/src/server/adapter.ts | 5 ++++- 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/apps/server/scripts/acp-mock-agent.ts b/apps/server/scripts/acp-mock-agent.ts index a10f9855cbbbf..1cfc355ea84aa 100644 --- a/apps/server/scripts/acp-mock-agent.ts +++ b/apps/server/scripts/acp-mock-agent.ts @@ -54,6 +54,10 @@ const failPromptOrdinal = Number(process.env.T3_ACP_FAIL_PROMPT_ORDINAL); const compactionPromptOrdinals = new Set( (process.env.T3_ACP_COMPACTION_PROMPT_ORDINALS ?? "").split(",").map(Number), ); +// These prompts end while their compaction is still in progress. +const openCompactionPromptOrdinals = new Set( + (process.env.T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS ?? "").split(",").map(Number), +); const failSetModeConfigOption = process.env.T3_ACP_FAIL_SET_MODE_CONFIG_OPTION === "1"; const emitEmptyDefaultAgentOption = process.env.T3_ACP_EMIT_EMPTY_DEFAULT_AGENT_OPTION === "1"; const promptResponseText = process.env.T3_ACP_PROMPT_RESPONSE_TEXT; @@ -1249,6 +1253,16 @@ const program = Effect.gen(function* () { yield* Effect.sleep(`${promptDelayMs} millis`); } + if (openCompactionPromptOrdinals.has(promptCount)) { + yield* agent.client.sessionUpdate({ + sessionId: requestedSessionId, + update: { + sessionUpdate: "compaction_update", + compactionId: `compact-${promptCount}`, + status: "in_progress", + }, + }); + } if (compactionPromptOrdinals.has(promptCount)) { for (const status of ["in_progress", "completed"] as const) { yield* agent.client.sessionUpdate({ diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 68cf837137078..89d479e0ecebc 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1084,6 +1084,7 @@ describe("AcpAdapterV2", () => { const { runTurn, idleSessionUpdate, idleCopilotSessionEvent } = yield* makeInstructionCompactionHarness({ T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", + T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", }); yield* runTurn("Start work."); yield* runTurn("Continue until context compacts."); @@ -1151,6 +1152,20 @@ describe("AcpAdapterV2", () => { assert.deepEqual(yield* runTurn("Continue after child compaction."), [ { type: "text", text: "Continue after child compaction." }, ]); + yield* runTurn("End this turn while compaction is still running."); + assertReinjectedInstructions( + yield* runTurn("Continue after the turn ended mid-compaction."), + "Continue after the turn ended mid-compaction.", + ); + yield* idleSessionUpdate({ + sessionUpdate: "compaction_update", + compactionId: "compact-13", + status: "completed", + }); + assertReinjectedInstructions( + yield* runTurn("Continue after the compaction really finished."), + "Continue after the compaction really finished.", + ); }).pipe(Effect.provide(layerTest), Effect.scoped), ); diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 213ab11a2fa5a..9887660996e8d 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -3693,9 +3693,12 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( updated.delete(context.nativeThreadId); return updated; }); + // A completion projected at turn end must not swallow the agent's later real one. yield* markSessionCompacted( context.nativeThreadId, - toolCall.toolCallId.slice("acp-compaction:".length), + projectedStatus === undefined + ? toolCall.toolCallId.slice("acp-compaction:".length) + : undefined, ); } } else if (mcpIdentity !== undefined) { From a6966ef14901339f188b249901246e845eb69d79 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 17:28:40 +0200 Subject: [PATCH 06/22] fix(acp): a replaced runtime's late prompt no longer hides notes from the next session An agent can report idle before its prompt RPC settles. If a rollback then replaces the runtime, the replacement clears what T3 recorded as delivered. Closing the old runtime settles its open prompt as cancelled, and that prompt's completion then recorded its instructions and runtime notes as delivered again. When the replacement reused the session id, its first prompt left them out. The prompt now records delivery inside its runtime callback, under the permit and generation check, so a stale runtime's prompt changes nothing. Wire settlement still happens before the permit request. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 121 ++++++++++++++++++ packages/provider-acp/src/server/adapter.ts | 56 ++++---- 2 files changed, 147 insertions(+), 30 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 89d479e0ecebc..46dd822321398 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -3543,6 +3543,127 @@ describe("AcpAdapterV2", () => { }).pipe(Effect.provide(layerTest), Effect.scoped), ); + it.effect("resends T3 notes after rollback when the replaced runtime's prompt settles late", () => + Effect.gen(function* () { + const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; + const path = yield* Path.Path; + const selfInvocation = yield* resolveSelfInvocation(); + const mockAgentPath = yield* path.fromFileUrl( + new URL("../../../scripts/acp-mock-agent.ts", import.meta.url), + ); + type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"]; + let firstSessionUpdate: Parameters[0] | undefined; + const firstPromptGate = yield* Deferred.make(); + const replacementPromptText = yield* Deferred.make(); + const instanceId = ProviderInstanceId.make("acp-test-late-prompt-rollback"); + const adapter = yield* makeAcpAdapterV2({ + instanceId, + flavor: { + driver: ACP_TEST_DRIVER, + capabilities: AcpProviderCapabilitiesV2, + followAgentWorkState: true, + makeRuntime: makeMockRuntime({ + childProcessSpawner, + mockAgentPath, + wrapRuntime: (runtime, runtimeOrdinal) => + runtimeOrdinal === 1 + ? { + ...runtime, + handleSessionUpdate: (handler) => + Effect.sync(() => { + firstSessionUpdate = handler; + }).pipe(Effect.andThen(runtime.handleSessionUpdate(handler))), + prompt: () => Deferred.await(firstPromptGate), + } + : { + ...runtime, + prompt: (payload, options) => + Deferred.succeed( + replacementPromptText, + payload.prompt + .flatMap((block) => (block.type === "text" ? [block.text] : [])) + .join("\n"), + ).pipe(Effect.andThen(runtime.prompt(payload, options))), + }, + }), + }, + selfInvocation, + continuationRequests: { offer: () => Effect.void }, + }); + const threadId = ThreadId.make("thread-acp-late-prompt-rollback"); + const runtimePolicy = ProviderAdapter.ProviderAdapterV2RuntimePolicy.make({ + runtimeMode: "full-access", + interactionMode: "default", + cwd: process.cwd(), + }); + const modelSelection = { instanceId, model: "default" } as const; + const runtime = yield* adapter.openSession({ + threadId, + providerSessionId: ProviderSessionId.make("provider-session-acp-late-prompt-rollback"), + modelSelection, + runtimePolicy, + }); + const events = yield* Queue.unbounded(); + yield* runtime.events.pipe( + Stream.runForEach((event) => Queue.offer(events, event)), + Effect.forkScoped, + ); + const providerThread = yield* runtime.ensureThread({ + threadId, + modelSelection, + runtimePolicy, + }); + yield* runtime.startTurn( + makeTurnInput({ + threadId, + providerThread, + instanceId, + runtimePolicy, + now: yield* DateTime.now, + }), + ); + if (firstSessionUpdate === undefined) { + return yield* Effect.die("the first runtime's session handler must be wired"); + } + // The agent goes idle while its prompt RPC is still open, which ends the turn. + yield* firstSessionUpdate({ + sessionId: "mock-session-1", + update: { sessionUpdate: "user_message_chunk", content: { type: "text", text: "echo" } }, + }); + yield* firstSessionUpdate({ + sessionId: "mock-session-1", + update: { sessionUpdate: "state_update", state: "idle", stopReason: "end_turn" }, + }); + while ((yield* Queue.take(events)).type !== "turn.terminal") {} + + const rolledBack = yield* runtime.rollbackThread({ + providerThread, + providerThreadTurns: [], + target: { + type: "thread_start", + checkpointId: CheckpointId.make("checkpoint-acp-late-prompt-rollback"), + appRunOrdinal: 0, + }, + }); + // Closing the replaced runtime settles its open prompt as cancelled. The waiting + // prompt fiber resumes inside this call, before the next turn reads delivery state. + yield* Deferred.succeed(firstPromptGate, { stopReason: "cancelled" }); + yield* runtime.startTurn( + makeTurnInput({ + threadId, + providerThread: rolledBack.providerThread, + instanceId, + runtimePolicy, + now: yield* DateTime.now, + ordinal: 2, + }), + ); + const replacementPrompt = yield* Deferred.await(replacementPromptText); + assert.include(replacementPrompt, "T3 Code interaction mode: Default"); + assert.include(replacementPrompt, ""); + }).pipe(Effect.provide(layerTest), Effect.scoped), + ); + it.effect("quarantines callbacks from a failed rollback replacement before retrying", () => Effect.gen(function* () { const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 9887660996e8d..1282761d3770d 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -7634,36 +7634,6 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( } const promptGeneration = yield* Ref.get(runtimeCallbackGeneration); yield* runtime.prompt({ prompt: promptParts!.prompt }).pipe( - Effect.tap(() => - Ref.update(promptInstructionStates, (current) => { - if (promptParts?.instructionState === undefined) return current; - const updated = new Map(current); - updated.set(requestedSessionId, promptParts.instructionState); - return updated; - }), - ), - Effect.tap(() => - Ref.update(promptRuntimeInstructions, (current) => { - if (promptParts?.runtimeInstructions === undefined) return current; - const updated = new Map(current); - updated.set(requestedSessionId, promptParts.runtimeInstructions); - return updated; - }), - ), - Effect.tap(() => - Ref.update(compactionMarkers, (current) => { - if (promptParts?.instructionState === undefined) return current; - const observedMarker = promptParts.compactionMarker; - if ( - observedMarker === undefined || - current.get(requestedSessionId) !== observedMarker - ) - return current; - const updated = new Map(current); - updated.delete(requestedSessionId); - return updated; - }), - ), // Wire settlement precedes the completion callback's permit request so // settled-soft classification can observe the native return even when // the completion fiber has not yet set promptSettled under the permit. @@ -7674,6 +7644,32 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( runRuntimeCallbackAtGeneration( promptGeneration, Effect.gen(function* () { + // Record delivery only for the current runtime. A replacement can reuse + // this session id after clearing what was sent to it. + yield* Ref.update(promptInstructionStates, (current) => { + if (promptParts?.instructionState === undefined) return current; + const updated = new Map(current); + updated.set(requestedSessionId, promptParts.instructionState); + return updated; + }); + yield* Ref.update(promptRuntimeInstructions, (current) => { + if (promptParts?.runtimeInstructions === undefined) return current; + const updated = new Map(current); + updated.set(requestedSessionId, promptParts.runtimeInstructions); + return updated; + }); + yield* Ref.update(compactionMarkers, (current) => { + if (promptParts?.instructionState === undefined) return current; + const observedMarker = promptParts.compactionMarker; + if ( + observedMarker === undefined || + current.get(requestedSessionId) !== observedMarker + ) + return current; + const updated = new Map(current); + updated.delete(requestedSessionId); + return updated; + }); if (context.finalized || followsAgentWorkState) return; // ACP v2 reports a failed prompt as an idle `error`, not an RPC error. const idleError = result.stopReason === "error" && !context.interrupted; From f3bdcb6b84ed790a9cf0df86e0f53c3e497de304 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 18:26:20 +0200 Subject: [PATCH 07/22] fix(acp): compactions during prompt preparation and from ACP v1 agents resend T3 notes T3 read the compaction mark before preparing the prompt's text and attachments. A root compaction that finished during that preparation left the prompt without instructions or runtime notes, so both arrived one prompt late. Delivery state is now read after those awaits. T3 also never advertised ACP v1 compaction support. Conforming v1 agents only send compaction_update to clients that advertise it, and this PR resends notes only after an observed compaction, so their later prompts would have dropped the notes for good. initialize now advertises `session.compaction: {}` next to `notices`. The recorded Fx and Grok initialize frames follow. The late-prompt rollback test now waits on a new afterPromptCallback test hook, so it fails if the stale prompt's delivery write is not generation checked. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 55 ++++++++++-- .../fx_early_stop/fx_transcript.ndjson | 2 +- .../fixtures/fx_subagent/fx_transcript.ndjson | 2 +- .../grok_transcript.ndjson | 2 +- .../grok_transcript.ndjson | 2 +- .../grok_transcript.ndjson | 2 +- .../grok_prompt_error/grok_transcript.ndjson | 2 +- .../message_steering/fx_transcript.ndjson | 2 +- .../fixtures/multi_turn/fx_transcript.ndjson | 2 +- .../fixtures/queued_turn/fx_transcript.ndjson | 2 +- .../fixtures/simple/fx_transcript.ndjson | 2 +- .../fixtures/todo_list/grok_transcript.ndjson | 2 +- .../fx_transcript.ndjson | 2 +- .../fx_transcript.ndjson | 2 +- .../grok_transcript.ndjson | 2 +- .../turn_interrupt/fx_transcript.ndjson | 2 +- packages/effect-acp/src/compat.ts | 6 +- .../src/server/AcpSessionRuntime.ts | 5 +- packages/provider-acp/src/server/adapter.ts | 88 ++++++++++--------- 19 files changed, 116 insertions(+), 68 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 46dd822321398..876851b7444ce 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -350,12 +350,20 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( let copilotSessionEvent: | ((event: unknown) => Effect.Effect) | undefined; + let duringNextPreparation: Effect.Effect = Effect.void; const adapter = yield* makeAcpAdapterV2({ instanceId, flavor: { driver: ACP_TEST_DRIVER, capabilities: AcpProviderCapabilitiesV2, supportsCompaction: true, + // Prompt preparation awaits this hook, so frames sent from it land mid-preparation. + rewriteUserText: (text) => + Effect.suspend(() => { + const during = duringNextPreparation; + duringNextPreparation = Effect.void; + return during.pipe(Effect.orDie, Effect.as(text)); + }), registerExtensions: makeCopilotSubagentRouting().registerExtensions, // Routes updates tagged with a Devin subagent context to that child session. normalizeSessionUpdate: normalizeDevinSessionUpdate, @@ -430,6 +438,13 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( assert.isDefined(sessionUpdate); yield* sessionUpdate({ sessionId, update }); }), + sessionUpdateDuringNextPreparation: (update: EffectAcpSchema.SessionUpdate) => + Effect.sync(() => { + duringNextPreparation = Effect.suspend(() => { + assert.isDefined(sessionUpdate); + return sessionUpdate({ sessionId, update }); + }); + }), idleCopilotSessionEvent: Effect.fnUntraced(function* (event: { readonly type: string; readonly agentId?: string; @@ -1078,14 +1093,18 @@ describe("AcpAdapterV2", () => { ); it.live( - "reinjects ACP instructions after protocol compaction, during reinjection or while idle", + "reinjects ACP instructions after protocol compaction, during reinjection, prompt preparation, or while idle", () => Effect.gen(function* () { - const { runTurn, idleSessionUpdate, idleCopilotSessionEvent } = - yield* makeInstructionCompactionHarness({ - T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", - T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", - }); + const { + runTurn, + idleSessionUpdate, + idleCopilotSessionEvent, + sessionUpdateDuringNextPreparation, + } = yield* makeInstructionCompactionHarness({ + T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", + T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", + }); yield* runTurn("Start work."); yield* runTurn("Continue until context compacts."); assert.deepEqual(yield* runTurn(" /repro-skill"), [ @@ -1166,6 +1185,18 @@ describe("AcpAdapterV2", () => { yield* runTurn("Continue after the compaction really finished."), "Continue after the compaction really finished.", ); + yield* sessionUpdateDuringNextPreparation({ + sessionUpdate: "compaction_update", + compactionId: "compact-while-preparing", + status: "completed", + }); + assertReinjectedInstructions( + yield* runTurn("Continue after a compaction during prompt preparation."), + "Continue after a compaction during prompt preparation.", + ); + assert.deepEqual(yield* runTurn("Finish after that reinjection."), [ + { type: "text", text: "Finish after that reinjection." }, + ]); }).pipe(Effect.provide(layerTest), Effect.scoped), ); @@ -2253,7 +2284,7 @@ describe("AcpAdapterV2", () => { Object(rawProtocolRequestParam(initialize, "clientCapabilities")), "session", ), - { notices: {} }, + { notices: {}, compaction: {} }, ); const title = "skill discovery warning: candidate was skipped because its metadata is invalid"; @@ -3554,10 +3585,15 @@ describe("AcpAdapterV2", () => { type RuntimeService = AcpSessionRuntime.AcpSessionRuntime["Service"]; let firstSessionUpdate: Parameters[0] | undefined; const firstPromptGate = yield* Deferred.make(); + const firstPromptCallbackDone = yield* Deferred.make(); const replacementPromptText = yield* Deferred.make(); const instanceId = ProviderInstanceId.make("acp-test-late-prompt-rollback"); const adapter = yield* makeAcpAdapterV2({ instanceId, + testHooks: { + afterPromptCallback: () => + Deferred.succeed(firstPromptCallbackDone, undefined).pipe(Effect.asVoid), + }, flavor: { driver: ACP_TEST_DRIVER, capabilities: AcpProviderCapabilitiesV2, @@ -3645,9 +3681,10 @@ describe("AcpAdapterV2", () => { appRunOrdinal: 0, }, }); - // Closing the replaced runtime settles its open prompt as cancelled. The waiting - // prompt fiber resumes inside this call, before the next turn reads delivery state. + // Closing the replaced runtime settles its open prompt as cancelled. Its completion + // callback must finish before the next turn reads delivery state. yield* Deferred.succeed(firstPromptGate, { stopReason: "cancelled" }); + yield* Deferred.await(firstPromptCallbackDone); yield* runtime.startTurn( makeTurnInput({ threadId, diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson index 5c684b21bc327..07b2388875a77 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"fx_early_stop","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/fx_subagent/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/fx_subagent/fx_transcript.ndjson index e2ddcf46159a3..99ce6d8a0b7ba 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/fx_subagent/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/fx_subagent/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"fx_subagent","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.14","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.14"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"openai/gpt-5.2","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2-codex","name":"openai/gpt-5.2-codex"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-codex-max","name":"openai/gpt-5.1-codex-max"},{"value":"openai/gpt-5.1-codex","name":"openai/gpt-5.1-codex"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5-codex","name":"openai/gpt-5-codex"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"moonshotai/kimi-k3.1-pro","name":"moonshotai/kimi-k3.1-pro"},{"value":"moonshotai/kimi-k3.1-pro-highspeed","name":"moonshotai/kimi-k3.1-pro-highspeed"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"xiaomi/mimo-v2.6-pro","name":"xiaomi/mimo-v2.6-pro"},{"value":"xiaomi/mimo-v2.6-pro-ultraspeed","name":"xiaomi/mimo-v2.6-pro-ultraspeed"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"xiaomi/mimo-v2.5-pro","name":"xiaomi/mimo-v2.5-pro"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-terra","name":"openai/gpt-6.1-terra"},{"value":"openai/gpt-6.1-terra-fast","name":"openai/gpt-6.1-terra-fast"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"google/gemma-4-31b-it","name":"google/gemma-4-31b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-v3.1","name":"deepseek/deepseek-v3.1"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/muse-spark-open","name":"meta/muse-spark-open"},{"value":"meta/muse-wonder-1.0","name":"meta/muse-wonder-1.0"},{"value":"meta/muse-spark-1.3","name":"meta/muse-spark-1.3"},{"value":"meta/muse-spark-1.3-contributor","name":"meta/muse-spark-1.3-contributor"},{"value":"meta/muse-glimmer-30b","name":"meta/muse-glimmer-30b"},{"value":"meta/muse-spark-1.2","name":"meta/muse-spark-1.2"},{"value":"meta/muse-spark-1.2-contributor","name":"meta/muse-spark-1.2-contributor"},{"value":"meta/muse-spark-1.1","name":"meta/muse-spark-1.1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"mistral/mistral-large-4","name":"mistral/mistral-large-4"},{"value":"mistral/mistral-medium-3.5","name":"mistral/mistral-medium-3.5"},{"value":"mistral/mistral-large-3","name":"mistral/mistral-large-3"},{"value":"mistral/mistral-small","name":"mistral/mistral-small"},{"value":"mistral/mistral-nemo","name":"mistral/mistral-nemo"},{"value":"mistral/codestral","name":"mistral/codestral"},{"value":"alibaba/qwen3.8-max-prime","name":"alibaba/qwen3.8-max-prime"},{"value":"alibaba/qwen3.8-max-0902","name":"alibaba/qwen3.8-max-0902"},{"value":"alibaba/qwen3.8-27b","name":"alibaba/qwen3.8-27b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3.7-plus","name":"alibaba/qwen3.7-plus"},{"value":"alibaba/qwen3.7-max","name":"alibaba/qwen3.7-max"},{"value":"alibaba/qwen3.6-27b","name":"alibaba/qwen3.6-27b"},{"value":"alibaba/qwen3.6-plus","name":"alibaba/qwen3.6-plus"},{"value":"alibaba/qwen3.5-plus","name":"alibaba/qwen3.5-plus"},{"value":"alibaba/qwen3-max-thinking","name":"alibaba/qwen3-max-thinking"},{"value":"alibaba/qwen3-vl-instruct","name":"alibaba/qwen3-vl-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-14b","name":"alibaba/qwen-3-14b"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-30b","name":"alibaba/qwen-3-30b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"inception/mercury-3","name":"inception/mercury-3"},{"value":"reflection/beam","name":"reflection/beam"},{"value":"reflection/beam-free","name":"reflection/beam-free"},{"value":"nvidia/nemotron-3.5-super-vl","name":"nvidia/nemotron-3.5-super-vl"},{"value":"nvidia/nemotron-3.5-lightning-free","name":"nvidia/nemotron-3.5-lightning-free"},{"value":"liquid/lfm2.5-24b-a2b","name":"liquid/lfm2.5-24b-a2b"},{"value":"callstack/apex","name":"callstack/apex"},{"value":"stealth/glyph-cluster","name":"stealth/glyph-cluster"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"sakana/fugu-max","name":"sakana/fugu-max"},{"value":"sakana/fugu-ultra-v2","name":"sakana/fugu-ultra-v2"},{"value":"inception/mercury-2.5","name":"inception/mercury-2.5"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"mixedbread/toast-1","name":"mixedbread/toast-1"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"sakana/namazu","name":"sakana/namazu"},{"value":"thinkingmachines/inkling-small","name":"thinkingmachines/inkling-small"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"poolside/laguna-s-2.1-free","name":"poolside/laguna-s-2.1-free"},{"value":"poolside/laguna-s-2.1","name":"poolside/laguna-s-2.1"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"thinkingmachines/inkling","name":"thinkingmachines/inkling"},{"value":"tencent/hy3","name":"tencent/hy3"},{"value":"bytedance/seed-2.1-turbo","name":"bytedance/seed-2.1-turbo"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"sakana/fugu-ultra","name":"sakana/fugu-ultra"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"moonshotai/kimi-k2.7-code-highspeed","name":"moonshotai/kimi-k2.7-code-highspeed"},{"value":"moonshotai/kimi-k2.7-code","name":"moonshotai/kimi-k2.7-code"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"xiaomi/mimo-v2.5","name":"xiaomi/mimo-v2.5"},{"value":"moonshotai/kimi-k2.6","name":"moonshotai/kimi-k2.6"},{"value":"zai/glm-5.1","name":"zai/glm-5.1"},{"value":"arcee-ai/trinity-large-thinking","name":"arcee-ai/trinity-large-thinking"},{"value":"zai/glm-5v-turbo","name":"zai/glm-5v-turbo"},{"value":"zai/glm-5-turbo","name":"zai/glm-5-turbo"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"inception/mercury-2","name":"inception/mercury-2"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-3-nano-30b-a3b","name":"nvidia/nemotron-3-nano-30b-a3b"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"zai/glm-4.6","name":"zai/glm-4.6"},{"value":"bytedance/seed-1.6","name":"bytedance/seed-1.6"},{"value":"bytedance/seed-1.8","name":"bytedance/seed-1.8"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"zai/glm-4.5v","name":"zai/glm-4.5v"},{"value":"zai/glm-4.5","name":"zai/glm-4.5"},{"value":"zai/glm-4.5-air","name":"zai/glm-4.5-air"},{"value":"moonshotai/kimi-k2","name":"moonshotai/kimi-k2"},{"value":"cohere/command-a","name":"cohere/command-a"},{"value":"inception/mercury-coder-small","name":"inception/mercury-coder-small"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash-1017","name":"deepseek/deepseek-v4.1-flash-1017"},{"value":"deepseek/deepseek-v4.1-flash-1017-fast","name":"deepseek/deepseek-v4.1-flash-1017-fast"},{"value":"deepseek/deepseek-v4.1-flash-fast","name":"deepseek/deepseek-v4.1-flash-fast"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash-vision-exp","name":"deepseek/deepseek-v4-flash-vision-exp"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"alibaba/qwen3.8-omni-flash","name":"alibaba/qwen3.8-omni-flash"},{"value":"alibaba/qwen3.8-flash","name":"alibaba/qwen3.8-flash"},{"value":"alibaba/qwen3.7-flash","name":"alibaba/qwen3.7-flash"},{"value":"alibaba/qwen3.5-flash","name":"alibaba/qwen3.5-flash"},{"value":"inclusionai/ling-3.1-flash","name":"inclusionai/ling-3.1-flash"},{"value":"xiaomi/mimo-v2.6-flash","name":"xiaomi/mimo-v2.6-flash"},{"value":"zai/glm-5.3-flashx","name":"zai/glm-5.3-flashx"},{"value":"inclusionai/ling-3.0-flash-vl","name":"inclusionai/ling-3.0-flash-vl"},{"value":"inclusionai/ling-3.0-flash-sante","name":"inclusionai/ling-3.0-flash-sante"},{"value":"inclusionai/ling-3.0-flash-fin","name":"inclusionai/ling-3.0-flash-fin"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"inclusionai/ling-3.0-flash","name":"inclusionai/ling-3.0-flash"},{"value":"stepfun/step-3.7-flash","name":"stepfun/step-3.7-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"zai/glm-4.7-flashx","name":"zai/glm-4.7-flashx"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5.1-codex-mini","name":"openai/gpt-5.1-codex-mini"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-4-argon","name":"google/gemini-4-argon"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"mistral/ministral-14b","name":"mistral/ministral-14b"},{"value":"mistral/ministral-3b","name":"mistral/ministral-3b"},{"value":"mistral/ministral-8b","name":"mistral/ministral-8b"},{"value":"minimax/minimax-m3.1-flash","name":"minimax/minimax-m3.1-flash"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.7","name":"minimax/minimax-m2.7"},{"value":"minimax/minimax-m2.7-highspeed","name":"minimax/minimax-m2.7-highspeed"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.5-highspeed","name":"minimax/minimax-m2.5-highspeed"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"minimax/minimax-m2.1-lightning","name":"minimax/minimax-m2.1-lightning"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"minimax/minimax-m2","name":"minimax/minimax-m2"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"meituan/longcat-2.5-preview","name":"meituan/longcat-2.5-preview"},{"value":"stepfun/step-5-preview","name":"stepfun/step-5-preview"},{"value":"tencent/hy4-preview","name":"tencent/hy4-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"},{"value":"interfaze/interfaze-beta","name":"interfaze/interfaze-beta"},{"value":"tencent/hy-mt2-pro","name":"tencent/hy-mt2-pro"},{"value":"scaledown/scaledown-extract-1","name":"scaledown/scaledown-extract-1"},{"value":"scaledown/scaledown-summarize-1","name":"scaledown/scaledown-summarize-1"},{"value":"elorian/glimpse","name":"elorian/glimpse"},{"value":"tencent/hy-mt2-plus","name":"tencent/hy-mt2-plus"},{"value":"inference-net/schematron-v2-small","name":"inference-net/schematron-v2-small"},{"value":"inference-net/schematron-v2-turbo","name":"inference-net/schematron-v2-turbo"},{"value":"morph/morph-v3-fast","name":"morph/morph-v3-fast"},{"value":"morph/morph-v3-large","name":"morph/morph-v3-large"},{"value":"perplexity/sonar","name":"perplexity/sonar"},{"value":"tencent/hy-mt2-lite","name":"tencent/hy-mt2-lite"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"auto","name":"Auto","description":"Check actions with an automatic safety review instead of asking","_meta":{"fx":{"permissionMode":"auto"}}},{"value":"ask","name":"Ask","description":"Request permission before making any changes","_meta":{"fx":{"permissionMode":"ask"}}},{"value":"full-access","name":"Full access","description":"Run every action without asking, outside the sandbox","_meta":{"fx":{"permissionMode":"yolo"}}}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"auto","options":[{"value":"auto","name":"default"},{"value":"none","name":"none"},{"value":"minimal","name":"minimal"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"}]},{"id":"fast","name":"Fast Mode","description":"Uses the model's fast lane","category":"model","type":"select","currentValue":"false","options":[{"value":"false","name":"off"},{"value":"true","name":"on"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"auto","name":"Auto","description":"Check actions with an automatic safety review instead of asking"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"},{"id":"full-access","name":"Full access","description":"Run every action without asking, outside the sandbox"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/grok_transcript.ndjson index df6aab256644c..cf27f37b09eda 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/grok_auto_blocked_command/grok_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"grok_auto_blocked_command","metadata":{"generatedBy":"live-grok-recorder","grokVersion":"1.0.41","normalization":"Session ids are fixed UUIDs, the workspace is , HOME is /home/grok-replay and the recording user is grok-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . Personal skills, machine identity, account settings and announcement broadcasts are removed, as are responses to Grok-internal request ids that T3's protocol drops. Timestamps are kept as recorded.","droppedFrames":5}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true},"sessionCapabilities":{"list":{},"resume":{},"close":{}},"auth":{},"_meta":{"x.ai/fs_notify":true,"x.ai/hooks":{"blockingEvents":["pre_tool_use","stop","subagent_stop"],"decisions":["deny","block"],"stopSignals":["continue","stopReason","additionalContext"]},"x.ai/capabilities":{"toolOverrides":{"x_keyword_search":true,"x_semantic_search":true,"x_user_search":false,"x_thread_fetch":false}}}},"authMethods":[{"id":"cached_token","name":"cached_token","description":"Cached token from ~/.grok/auth.json"},{"id":"grok.com","name":"Grok","description":"Sign in with Grok"}],"_meta":{"grokShell":true,"defaultAuthMethodId":"cached_token","x.ai/mcp/sdk":true,"x.ai/pluginDirs":true,"currentWorkingDirectory":"","agentVersion":"1.0.41","agentId":"00000000-0000-4000-8000-000000000000","agentInstanceId":"00000000-0000-4000-8000-000000000000","hostname":"grok-replay","modelState":{"currentModelId":"grok-4.7","availableModels":[{"modelId":"grok-4.7","name":"Grok 4.7","description":"SpaceXAI's latest frontier model","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.7-build-fast","name":"Grok 4.7 Fast","description":"Fast variant. 2x the price.","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.6","name":"Grok 4.6","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Highest effort and reasoning level","default":false},{"id":"high","value":"high","label":"High","description":"Higher implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}},{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"high","value":"high","label":"High","description":"Highest implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}}]},"mcpServers":[],"mcpApps":false,"metadata":null,"availableCommands":[{"name":"compact","description":"Compress conversation history to save context window","input":{"hint":"optional context about what to preserve"}},{"name":"always-approve","description":"Toggle always-approve mode (skip all permission prompts)","input":{"hint":"on|off"}},{"name":"context","description":"Show context window usage and session stats","input":null},{"name":"session-info","description":"Show session details (model, turns, context usage)","input":null},{"name":"deep-research","description":"Research with bounded parallel agents, cross-check evidence, and write a cited report","input":{"hint":""}},{"name":"workflow","description":"Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save)","input":{"hint":" [--agent-budget N] [--effort LEVEL] [args] | runs | pause|resume|stop|save [name]"}},{"name":"goal","description":"Set, manage, or check an autonomous goal","input":{"hint":" [--budget ] | status | pause | resume | clear"}}],"cancelRewind":true,"sessionRecap":true,"feedbackTraceOffer":false,"voiceMode":true}}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"notification:_x.ai/mcp/servers_updated","frame":{"kind":"notification","method":"_x.ai/mcp/servers_updated","params":{"mcpServers":[]}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash/grok_transcript.ndjson index d8ecb1438f14f..3509ee229698f 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash/grok_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"grok_background_bash","metadata":{"generatedBy":"live-grok-recorder","grokVersion":"1.0.41","normalization":"Session ids are fixed UUIDs, the workspace is , HOME is /home/grok-replay and the recording user is grok-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . Personal skills, machine identity, account settings and announcement broadcasts are removed, as are responses to Grok-internal request ids that T3's protocol drops. Timestamps are kept as recorded.","droppedFrames":5}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true},"sessionCapabilities":{"list":{},"resume":{},"close":{}},"auth":{},"_meta":{"x.ai/fs_notify":true,"x.ai/hooks":{"blockingEvents":["pre_tool_use","stop","subagent_stop"],"decisions":["deny","block"],"stopSignals":["continue","stopReason","additionalContext"]},"x.ai/capabilities":{"toolOverrides":{"x_keyword_search":true,"x_semantic_search":true,"x_user_search":false,"x_thread_fetch":false}}}},"authMethods":[{"id":"cached_token","name":"cached_token","description":"Cached token from ~/.grok/auth.json"},{"id":"grok.com","name":"Grok","description":"Sign in with Grok"}],"_meta":{"grokShell":true,"defaultAuthMethodId":"cached_token","x.ai/mcp/sdk":true,"x.ai/pluginDirs":true,"currentWorkingDirectory":"","agentVersion":"1.0.41","agentId":"00000000-0000-4000-8000-000000000000","agentInstanceId":"00000000-0000-4000-8000-000000000000","hostname":"grok-replay","modelState":{"currentModelId":"grok-4.7","availableModels":[{"modelId":"grok-4.7","name":"Grok 4.7","description":"SpaceXAI's latest frontier model","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.7-build-fast","name":"Grok 4.7 Fast","description":"Fast variant. 2x the price.","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.6","name":"Grok 4.6","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Highest effort and reasoning level","default":false},{"id":"high","value":"high","label":"High","description":"Higher implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}},{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"high","value":"high","label":"High","description":"Highest implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}}]},"mcpServers":[],"mcpApps":false,"metadata":null,"availableCommands":[{"name":"compact","description":"Compress conversation history to save context window","input":{"hint":"optional context about what to preserve"}},{"name":"always-approve","description":"Toggle always-approve mode (skip all permission prompts)","input":{"hint":"on|off"}},{"name":"context","description":"Show context window usage and session stats","input":null},{"name":"session-info","description":"Show session details (model, turns, context usage)","input":null},{"name":"deep-research","description":"Research with bounded parallel agents, cross-check evidence, and write a cited report","input":{"hint":""}},{"name":"workflow","description":"Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save)","input":{"hint":" [--agent-budget N] [--effort LEVEL] [args] | runs | pause|resume|stop|save [name]"}},{"name":"goal","description":"Set, manage, or check an autonomous goal","input":{"hint":" [--budget ] | status | pause | resume | clear"}}],"cancelRewind":true,"sessionRecap":true,"feedbackTraceOffer":false,"voiceMode":true}}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"notification:_x.ai/mcp/servers_updated","frame":{"kind":"notification","method":"_x.ai/mcp/servers_updated","params":{"mcpServers":[]}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash_fast_wake/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash_fast_wake/grok_transcript.ndjson index 816301f66a9ae..d321f2e60a105 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash_fast_wake/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/grok_background_bash_fast_wake/grok_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"grok_background_bash_fast_wake","metadata":{"generatedBy":"live-grok-recorder","grokVersion":"1.0.44","normalization":"Session ids are fixed UUIDs, the workspace is , HOME is /home/grok-replay and the recording user is grok-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . Personal skills, machine identity, account settings and announcement broadcasts are removed, as are responses to Grok-internal request ids that T3's protocol drops. Timestamps are kept as recorded.","droppedFrames":5}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true},"sessionCapabilities":{"list":{},"resume":{},"close":{}},"auth":{},"_meta":{"x.ai/fs_notify":true,"x.ai/hooks":{"blockingEvents":["pre_tool_use","stop","subagent_stop"],"decisions":["deny","block"],"stopSignals":["continue","stopReason","additionalContext"]},"x.ai/capabilities":{"toolOverrides":{"x_keyword_search":true,"x_semantic_search":true,"x_user_search":false,"x_thread_fetch":false}}}},"authMethods":[{"id":"cached_token","name":"cached_token","description":"Cached token from ~/.grok/auth.json"},{"id":"grok.com","name":"Grok","description":"Sign in with Grok"}],"_meta":{"grokShell":true,"defaultAuthMethodId":"cached_token","x.ai/mcp/sdk":true,"x.ai/pluginDirs":true,"currentWorkingDirectory":"","agentVersion":"1.0.44","agentId":"00000000-0000-4000-8000-000000000000","agentInstanceId":"00000000-0000-4000-8000-000000000000","hostname":"grok-replay","modelState":{"currentModelId":"grok-4.7","availableModels":[{"modelId":"grok-4.7","name":"Grok 4.7","description":"SpaceXAI's latest frontier model","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}],"contextWindows":[256000,500000]}},{"modelId":"grok-4.7-build-fast","name":"Grok 4.7 Fast","description":"Fast variant. 2x the price.","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}],"contextWindows":[256000,500000]}},{"modelId":"grok-4.6","name":"Grok 4.6","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Highest effort and reasoning level","default":false},{"id":"high","value":"high","label":"High","description":"Higher implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}],"contextWindows":[256000,500000]}},{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"high","value":"high","label":"High","description":"Highest implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}],"contextWindows":[256000,500000]}}]},"mcpServers":[],"mcpApps":false,"metadata":null,"availableCommands":[{"name":"compact","description":"Compress conversation history to save context window","input":null},{"name":"always-approve","description":"Toggle always-approve mode (skip all permission prompts)","input":{"hint":"on|off"}},{"name":"context","description":"Show context window usage and session stats","input":null},{"name":"session-info","description":"Show session details (model, turns, context usage)","input":null},{"name":"deep-research","description":"Research with bounded parallel agents, cross-check evidence, and write a cited report","input":{"hint":""}},{"name":"workflow","description":"Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save)","input":{"hint":" [--agent-budget N] [--effort LEVEL] [args] | runs | pause|resume|stop|save [name]"}},{"name":"goal","description":"Set, manage, or check an autonomous goal","input":{"hint":" [--budget ] | status | pause | resume | clear"}}],"cancelRewind":true,"sessionRecap":true,"feedbackTraceOffer":false,"voiceMode":true}}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"notification:_x.ai/session/setup","frame":{"kind":"notification","method":"_x.ai/session/setup","params":{"method":"session/new","phase":"auth","sessionId":null}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/grok_prompt_error/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/grok_prompt_error/grok_transcript.ndjson index 9fa307acc195a..8ed6a2537ab3f 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/grok_prompt_error/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/grok_prompt_error/grok_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"grok_prompt_error","metadata":{"generatedBy":"live-grok-recorder","grokVersion":"1.0.44","normalization":"Session ids are fixed UUIDs, the workspace is , HOME is /home/grok-replay and the recording user is grok-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . Personal skills, machine identity, account settings and announcement broadcasts are removed, as are responses to Grok-internal request ids that T3's protocol drops. Timestamps are kept as recorded.","droppedFrames":5}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","_meta":{"clientType":"extension"},"clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true},"sessionCapabilities":{"list":{},"resume":{},"close":{}},"auth":{},"_meta":{"x.ai/fs_notify":true,"x.ai/hooks":{"blockingEvents":["pre_tool_use","stop","subagent_stop"],"decisions":["deny","block"],"stopSignals":["continue","stopReason","additionalContext"]},"x.ai/capabilities":{"toolOverrides":{"x_keyword_search":true,"x_semantic_search":true,"x_user_search":false,"x_thread_fetch":false}}}},"authMethods":[{"id":"cached_token","name":"cached_token","description":"Cached token from ~/.grok/auth.json"},{"id":"grok.com","name":"Grok","description":"Sign in with Grok"}],"_meta":{"grokShell":true,"defaultAuthMethodId":"cached_token","x.ai/mcp/sdk":true,"x.ai/pluginDirs":true,"currentWorkingDirectory":"","agentVersion":"1.0.44","agentId":"00000000-0000-4000-8000-000000000000","agentInstanceId":"00000000-0000-4000-8000-000000000000","hostname":"grok-replay","modelState":{"currentModelId":"grok-4.7","availableModels":[{"modelId":"grok-4.7","name":"Grok 4.7","description":"SpaceXAI's latest frontier model","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}],"contextWindows":[256000,500000]}},{"modelId":"grok-4.7-build-fast","name":"Grok 4.7 Fast","description":"Fast variant. 2x the price.","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}],"contextWindows":[256000,500000]}},{"modelId":"grok-4.6","name":"Grok 4.6","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Highest effort and reasoning level","default":false},{"id":"high","value":"high","label":"High","description":"Higher implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}],"contextWindows":[256000,500000]}},{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"totalContextTokens":256000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"high","value":"high","label":"High","description":"Highest implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}],"contextWindows":[256000,500000]}}]},"mcpServers":[],"mcpApps":false,"metadata":null,"availableCommands":[{"name":"compact","description":"Compress conversation history to save context window","input":null},{"name":"always-approve","description":"Toggle always-approve mode (skip all permission prompts)","input":{"hint":"on|off"}},{"name":"context","description":"Show context window usage and session stats","input":null},{"name":"session-info","description":"Show session details (model, turns, context usage)","input":null},{"name":"deep-research","description":"Research with bounded parallel agents, cross-check evidence, and write a cited report","input":{"hint":""}},{"name":"workflow","description":"Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save)","input":{"hint":" [--agent-budget N] [--effort LEVEL] [args] | runs | pause|resume|stop|save [name]"}},{"name":"goal","description":"Set, manage, or check an autonomous goal","input":{"hint":" [--budget ] | status | pause | resume | clear"}}],"cancelRewind":true,"sessionRecap":true,"feedbackTraceOffer":false,"voiceMode":true}}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"notification:_x.ai/session/setup","frame":{"kind":"notification","method":"_x.ai/session/setup","params":{"method":"session/new","phase":"auth","sessionId":null}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson index 75ed31f7fdb3c..29f8049d0ef16 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"message_steering","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/fx_transcript.ndjson index d394fdf4c778a..d7c5cd808504d 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/multi_turn/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"multi_turn","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/fx_transcript.ndjson index 6780dd2a3ad48..28d92260dcecd 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/queued_turn/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"queued_turn","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/simple/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/simple/fx_transcript.ndjson index 84d694c6a1575..923aa3d611f4a 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/simple/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/simple/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"simple","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson index ae5bb950d93c8..4c842c100944f 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/todo_list/grok_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"todo_list","metadata":{"generatedBy":"live-grok-recorder","grokVersion":"1.0.41","normalization":"Session ids are fixed UUIDs, the workspace is , HOME is /home/grok-replay and the recording user is grok-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities are . Personal skills, machine identity, account settings and announcement broadcasts are removed, as are responses to Grok-internal request ids that T3's protocol drops. Timestamps are kept as recorded.","droppedFrames":5}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":"","_meta":{"clientType":"extension"}}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":"","_meta":{"clientType":"extension"}}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true},"sessionCapabilities":{"list":{},"resume":{},"close":{}},"auth":{},"_meta":{"x.ai/fs_notify":true,"x.ai/hooks":{"blockingEvents":["pre_tool_use","stop","subagent_stop"],"decisions":["deny","block"],"stopSignals":["continue","stopReason","additionalContext"]},"x.ai/capabilities":{"toolOverrides":{"x_keyword_search":true,"x_semantic_search":true,"x_user_search":false,"x_thread_fetch":false}}}},"authMethods":[{"id":"cached_token","name":"cached_token","description":"Cached token from ~/.grok/auth.json"},{"id":"grok.com","name":"Grok","description":"Sign in with Grok"}],"_meta":{"grokShell":true,"defaultAuthMethodId":"cached_token","x.ai/mcp/sdk":true,"x.ai/pluginDirs":true,"currentWorkingDirectory":"","agentVersion":"1.0.41","agentId":"00000000-0000-4000-8000-000000000000","agentInstanceId":"00000000-0000-4000-8000-000000000000","hostname":"grok-replay","modelState":{"currentModelId":"grok-4.7","availableModels":[{"modelId":"grok-4.7","name":"Grok 4.7","description":"SpaceXAI's latest frontier model","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.7-build-fast","name":"Grok 4.7 Fast","description":"Fast variant. 2x the price.","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.6","name":"Grok 4.6","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Highest effort and reasoning level","default":false},{"id":"high","value":"high","label":"High","description":"Higher implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}},{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"high","value":"high","label":"High","description":"Highest implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}}]},"mcpServers":[],"mcpApps":false,"metadata":null,"availableCommands":[{"name":"compact","description":"Compress conversation history to save context window","input":{"hint":"optional context about what to preserve"}},{"name":"always-approve","description":"Toggle always-approve mode (skip all permission prompts)","input":{"hint":"on|off"}},{"name":"context","description":"Show context window usage and session stats","input":null},{"name":"session-info","description":"Show session details (model, turns, context usage)","input":null},{"name":"deep-research","description":"Research with bounded parallel agents, cross-check evidence, and write a cited report","input":{"hint":""}},{"name":"workflow","description":"Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save)","input":{"hint":" [--agent-budget N] [--effort LEVEL] [args] | runs | pause|resume|stop|save [name]"}},{"name":"goal","description":"Set, manage, or check an autonomous goal","input":{"hint":" [--budget ] | status | pause | resume | clear"}}],"cancelRewind":true,"sessionRecap":true,"feedbackTraceOffer":false,"voiceMode":true}}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"notification:_x.ai/mcp/servers_updated","frame":{"kind":"notification","method":"_x.ai/mcp/servers_updated","params":{"mcpServers":[]}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_denied_write/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_denied_write/fx_transcript.ndjson index 6a1c048f6aff9..5a643d4df438c 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_denied_write/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_denied_write/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"tool_call_denied_write","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/fx_transcript.ndjson index 34f26d8efb330..1f9c4cd5e1cfe 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"tool_call_read_only_on_request","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson index 7d11fc4746fb3..c3de2afdf4ce3 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/tool_call_read_only_on_request/grok_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"grok","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"tool_call_read_only_on_request","metadata":{"generatedBy":"live-grok-recorder","grokVersion":"1.0.41","normalization":"Session ids are fixed UUIDs, the workspace is , HOME is /home/grok-replay and the recording user is grok-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities are . Personal skills, machine identity, account settings and announcement broadcasts are removed, as are responses to Grok-internal request ids that T3's protocol drops. Timestamps are kept as recorded.","droppedFrames":5}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":"","_meta":{"clientType":"extension"}}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":"","_meta":{"clientType":"extension"}}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":false,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true},"sessionCapabilities":{"list":{},"resume":{},"close":{}},"auth":{},"_meta":{"x.ai/fs_notify":true,"x.ai/hooks":{"blockingEvents":["pre_tool_use","stop","subagent_stop"],"decisions":["deny","block"],"stopSignals":["continue","stopReason","additionalContext"]},"x.ai/capabilities":{"toolOverrides":{"x_keyword_search":true,"x_semantic_search":true,"x_user_search":false,"x_thread_fetch":false}}}},"authMethods":[{"id":"cached_token","name":"cached_token","description":"Cached token from ~/.grok/auth.json"},{"id":"grok.com","name":"Grok","description":"Sign in with Grok"}],"_meta":{"grokShell":true,"defaultAuthMethodId":"cached_token","x.ai/mcp/sdk":true,"x.ai/pluginDirs":true,"currentWorkingDirectory":"","agentVersion":"1.0.41","agentId":"00000000-0000-4000-8000-000000000000","agentInstanceId":"00000000-0000-4000-8000-000000000000","hostname":"grok-replay","modelState":{"currentModelId":"grok-4.7","availableModels":[{"modelId":"grok-4.7","name":"Grok 4.7","description":"SpaceXAI's latest frontier model","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.7-build-fast","name":"Grok 4.7 Fast","description":"Fast variant. 2x the price.","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Maximum reasoning for the hardest tasks.","default":false},{"id":"high","value":"high","label":"High","description":"Thorough reasoning and quality. Recommended.","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Strong quality with a faster turnaround.","default":false},{"id":"low","value":"low","label":"Low","description":"Fastest responses. Best for simple tasks.","default":false}]}},{"modelId":"grok-4.6","name":"Grok 4.6","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"xhigh","value":"xhigh","label":"Extra High","description":"Highest effort and reasoning level","default":false},{"id":"high","value":"high","label":"High","description":"Higher implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}},{"modelId":"grok-4.5","name":"Grok 4.5","_meta":{"totalContextTokens":500000,"agentType":"grok-build-plan","supportsReasoningEffort":true,"reasoningEffort":"high","reasoningEfforts":[{"id":"high","value":"high","label":"High","description":"Highest implementation quality with extensive reasoning","default":true},{"id":"medium","value":"medium","label":"Medium","description":"Balanced effort with standard implementation and testing","default":false},{"id":"low","value":"low","label":"Low","description":"Quick, fast implementations","default":false}]}}]},"mcpServers":[],"mcpApps":false,"metadata":null,"availableCommands":[{"name":"compact","description":"Compress conversation history to save context window","input":{"hint":"optional context about what to preserve"}},{"name":"always-approve","description":"Toggle always-approve mode (skip all permission prompts)","input":{"hint":"on|off"}},{"name":"context","description":"Show context window usage and session stats","input":null},{"name":"session-info","description":"Show session details (model, turns, context usage)","input":null},{"name":"deep-research","description":"Research with bounded parallel agents, cross-check evidence, and write a cited report","input":{"hint":""}},{"name":"workflow","description":"Launch a saved workflow, list runs, or manage a run (pause, resume, stop, save)","input":{"hint":" [--agent-budget N] [--effort LEVEL] [args] | runs | pause|resume|stop|save [name]"}},{"name":"goal","description":"Set, manage, or check an autonomous goal","input":{"hint":" [--budget ] | status | pause | resume | clear"}}],"cancelRewind":true,"sessionRecap":true,"feedbackTraceOffer":false,"voiceMode":true}}}} {"type":"emit_inbound","label":"notification:_x.ai/mcp/servers_updated","frame":{"kind":"notification","method":"_x.ai/mcp/servers_updated","params":{"mcpServers":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/fx_transcript.ndjson index c0e0b356fdd8d..23191a20397de 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/turn_interrupt/fx_transcript.ndjson @@ -1,5 +1,5 @@ {"type":"transcript_start","provider":"fx","protocol":"acp.ndjson-jsonrpc","version":"1","scenario":"turn_interrupt","metadata":{"generatedBy":"live-fx-recorder","fxVersion":"0.0.13","normalization":"Session ids are fx-session-NN, the workspace is , HOME is /home/fx-replay and the recording user is fx-replay. T3-owned prompt text, MCP servers and initialize params other than clientCapabilities and _meta are . User skill names, emails and token-shaped strings are removed. Timestamps are kept as recorded.","droppedFrames":0}} -{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{}},"elicitation":{"form":{}}},"clientInfo":""}}} +{"type":"expect_outbound","label":"request:initialize","frame":{"kind":"request","method":"initialize","params":{"protocolVersion":2,"info":"","capabilities":"","clientCapabilities":{"fs":{"readTextFile":false,"writeTextFile":false},"terminal":false,"session":{"notices":{},"compaction":{}},"elicitation":{"form":{}}},"clientInfo":""}}} {"type":"emit_inbound","label":"response:initialize","frame":{"kind":"response","method":"initialize","result":{"protocolVersion":1,"agentCapabilities":{"loadSession":true,"promptCapabilities":{"image":true,"audio":false,"embeddedContext":true},"mcpCapabilities":{"http":true,"sse":true,"acp":true},"sessionCapabilities":{"list":{},"resume":{},"close":{},"systemPrompt":{}},"_meta":{"fx":{"steering":true}}},"agentInfo":{"name":"fx","title":"fx","version":"0.0.13"},"authMethods":[]}}} {"type":"expect_outbound","label":"request:session/new","frame":{"kind":"request","method":"session/new","params":{"cwd":"","mcpServers":""}}} {"type":"emit_inbound","label":"response:session/new","frame":{"kind":"response","method":"session/new","result":{"sessionId":"fx-session-01","configOptions":[{"id":"provider","name":"Provider","category":"model","type":"select","currentValue":"gateway","options":[{"value":"gateway","name":"Vercel AI Gateway"},{"value":"codex","name":"Codex subscription"},{"value":"grok","name":"Grok subscription"}]},{"id":"model","name":"Model","category":"model","type":"select","currentValue":"anthropic/claude-opus-5.5","options":[{"value":"anthropic/claude-sonnet-5.5","name":"anthropic/claude-sonnet-5.5"},{"value":"anthropic/claude-opus-5.5","name":"anthropic/claude-opus-5.5"},{"value":"anthropic/claude-opus-5.5-fast","name":"anthropic/claude-opus-5.5-fast"},{"value":"anthropic/claude-opus-5","name":"anthropic/claude-opus-5"},{"value":"anthropic/claude-opus-5-fast","name":"anthropic/claude-opus-5-fast"},{"value":"anthropic/claude-sonnet-5","name":"anthropic/claude-sonnet-5"},{"value":"anthropic/claude-opus-4.8","name":"anthropic/claude-opus-4.8"},{"value":"anthropic/claude-opus-4.8-fast","name":"anthropic/claude-opus-4.8-fast"},{"value":"anthropic/claude-opus-4.7","name":"anthropic/claude-opus-4.7"},{"value":"anthropic/claude-sonnet-4.6","name":"anthropic/claude-sonnet-4.6"},{"value":"anthropic/claude-opus-4.6","name":"anthropic/claude-opus-4.6"},{"value":"anthropic/claude-opus-4.5","name":"anthropic/claude-opus-4.5"},{"value":"anthropic/claude-sonnet-4.5","name":"anthropic/claude-sonnet-4.5"},{"value":"anthropic/claude-opus-4","name":"anthropic/claude-opus-4"},{"value":"anthropic/claude-sonnet-4","name":"anthropic/claude-sonnet-4"},{"value":"openai/gpt-5.6-luna","name":"openai/gpt-5.6-luna"},{"value":"openai/gpt-5.6-luna-fast","name":"openai/gpt-5.6-luna-fast"},{"value":"openai/gpt-5.6-sol","name":"openai/gpt-5.6-sol"},{"value":"openai/gpt-5.6-sol-fast","name":"openai/gpt-5.6-sol-fast"},{"value":"openai/gpt-5.6-terra","name":"openai/gpt-5.6-terra"},{"value":"openai/gpt-5.6-terra-fast","name":"openai/gpt-5.6-terra-fast"},{"value":"openai/gpt-5.5","name":"openai/gpt-5.5"},{"value":"openai/gpt-5.5-fast","name":"openai/gpt-5.5-fast"},{"value":"openai/gpt-5.5-pro","name":"openai/gpt-5.5-pro"},{"value":"openai/gpt-5.4-nano","name":"openai/gpt-5.4-nano"},{"value":"openai/gpt-5.4","name":"openai/gpt-5.4"},{"value":"openai/gpt-5.4-fast","name":"openai/gpt-5.4-fast"},{"value":"openai/gpt-5.4-pro","name":"openai/gpt-5.4-pro"},{"value":"openai/gpt-5.3-codex","name":"openai/gpt-5.3-codex"},{"value":"openai/gpt-5.3-codex-fast","name":"openai/gpt-5.3-codex-fast"},{"value":"openai/gpt-5.2","name":"openai/gpt-5.2"},{"value":"openai/gpt-5.2-fast","name":"openai/gpt-5.2-fast"},{"value":"openai/gpt-5.2-pro","name":"openai/gpt-5.2-pro"},{"value":"openai/gpt-5.1-thinking","name":"openai/gpt-5.1-thinking"},{"value":"openai/gpt-5.1-thinking-fast","name":"openai/gpt-5.1-thinking-fast"},{"value":"openai/gpt-5-pro","name":"openai/gpt-5-pro"},{"value":"openai/gpt-5","name":"openai/gpt-5"},{"value":"openai/gpt-5-fast","name":"openai/gpt-5-fast"},{"value":"openai/gpt-5-nano","name":"openai/gpt-5-nano"},{"value":"openai/o3-pro","name":"openai/o3-pro"},{"value":"openai/o3","name":"openai/o3"},{"value":"openai/o3-fast","name":"openai/o3-fast"},{"value":"openai/o1","name":"openai/o1"},{"value":"deepseek/deepseek-v4-pro-0813","name":"deepseek/deepseek-v4-pro-0813"},{"value":"deepseek/deepseek-v4-pro","name":"deepseek/deepseek-v4-pro"},{"value":"spacexai/grok-4.7","name":"spacexai/grok-4.7"},{"value":"spacexai/grok-4.6","name":"spacexai/grok-4.6"},{"value":"spacexai/grok-4.5","name":"spacexai/grok-4.5"},{"value":"spacexai/grok-4.3","name":"spacexai/grok-4.3"},{"value":"spacexai/grok-4.20-multi-agent","name":"spacexai/grok-4.20-multi-agent"},{"value":"spacexai/grok-4.20-non-reasoning","name":"spacexai/grok-4.20-non-reasoning"},{"value":"spacexai/grok-4.20-reasoning","name":"spacexai/grok-4.20-reasoning"},{"value":"spacexai/grok-4.1-fast-non-reasoning","name":"spacexai/grok-4.1-fast-non-reasoning"},{"value":"spacexai/grok-4.1-fast-reasoning","name":"spacexai/grok-4.1-fast-reasoning"},{"value":"amazon/nova-pro","name":"amazon/nova-pro"},{"value":"anthropic/claude-fable-5.1","name":"anthropic/claude-fable-5.1"},{"value":"anthropic/claude-fable-5","name":"anthropic/claude-fable-5"},{"value":"openai/gpt-6.1-sol","name":"openai/gpt-6.1-sol"},{"value":"openai/gpt-6.1-sol-fast","name":"openai/gpt-6.1-sol-fast"},{"value":"openai/gpt-6-luna","name":"openai/gpt-6-luna"},{"value":"openai/gpt-6-luna-fast","name":"openai/gpt-6-luna-fast"},{"value":"openai/gpt-6-sol","name":"openai/gpt-6-sol"},{"value":"openai/gpt-6-sol-fast","name":"openai/gpt-6-sol-fast"},{"value":"openai/gpt-6-astra","name":"openai/gpt-6-astra"},{"value":"openai/gpt-6-astra-fast","name":"openai/gpt-6-astra-fast"},{"value":"openai/gpt-oss-safeguard-120b","name":"openai/gpt-oss-safeguard-120b"},{"value":"openai/gpt-oss-safeguard-20b","name":"openai/gpt-oss-safeguard-20b"},{"value":"openai/gpt-oss-120b","name":"openai/gpt-oss-120b"},{"value":"openai/gpt-oss-20b","name":"openai/gpt-oss-20b"},{"value":"openai/gpt-4.1","name":"openai/gpt-4.1"},{"value":"openai/gpt-4.1-fast","name":"openai/gpt-4.1-fast"},{"value":"openai/gpt-4.1-nano","name":"openai/gpt-4.1-nano"},{"value":"openai/gpt-4.1-nano-fast","name":"openai/gpt-4.1-nano-fast"},{"value":"openai/gpt-4o","name":"openai/gpt-4o"},{"value":"openai/gpt-4o-fast","name":"openai/gpt-4o-fast"},{"value":"openai/gpt-4-turbo","name":"openai/gpt-4-turbo"},{"value":"openai/gpt-3.5-turbo","name":"openai/gpt-3.5-turbo"},{"value":"google/gemma-4-26b-a4b-it","name":"google/gemma-4-26b-a4b-it"},{"value":"deepseek/deepseek-v3.2","name":"deepseek/deepseek-v3.2"},{"value":"deepseek/deepseek-v3.2-thinking","name":"deepseek/deepseek-v3.2-thinking"},{"value":"deepseek/deepseek-r1","name":"deepseek/deepseek-r1"},{"value":"meta/llama-4-maverick","name":"meta/llama-4-maverick"},{"value":"meta/llama-4-scout","name":"meta/llama-4-scout"},{"value":"meta/llama-3.3-70b","name":"meta/llama-3.3-70b"},{"value":"meta/llama-3.1-70b","name":"meta/llama-3.1-70b"},{"value":"meta/llama-3.1-8b","name":"meta/llama-3.1-8b"},{"value":"alibaba/qwen3.8-2.4t-a95b","name":"alibaba/qwen3.8-2.4t-a95b"},{"value":"alibaba/qwen3.8-max","name":"alibaba/qwen3.8-max"},{"value":"alibaba/qwen3-next-80b-a3b-instruct","name":"alibaba/qwen3-next-80b-a3b-instruct"},{"value":"alibaba/qwen3-next-80b-a3b-thinking","name":"alibaba/qwen3-next-80b-a3b-thinking"},{"value":"alibaba/qwen3-coder-30b-a3b","name":"alibaba/qwen3-coder-30b-a3b"},{"value":"alibaba/qwen3-coder","name":"alibaba/qwen3-coder"},{"value":"alibaba/qwen3-coder-next","name":"alibaba/qwen3-coder-next"},{"value":"alibaba/qwen-3-235b","name":"alibaba/qwen-3-235b"},{"value":"alibaba/qwen-3-32b","name":"alibaba/qwen-3-32b"},{"value":"fireworks/ember-1","name":"fireworks/ember-1"},{"value":"zai/glm-5.3-fast","name":"zai/glm-5.3-fast"},{"value":"zai/glm-5.3","name":"zai/glm-5.3"},{"value":"nvidia/nemotron-3.5-lightning","name":"nvidia/nemotron-3.5-lightning"},{"value":"moonshotai/kimi-k3-fast","name":"moonshotai/kimi-k3-fast"},{"value":"moonshotai/kimi-k3","name":"moonshotai/kimi-k3"},{"value":"zai/glm-5.2-fast","name":"zai/glm-5.2-fast"},{"value":"zai/glm-5.2","name":"zai/glm-5.2"},{"value":"nvidia/nemotron-3-ultra-550b-a55b","name":"nvidia/nemotron-3-ultra-550b-a55b"},{"value":"spacexai/grok-build-0.1","name":"spacexai/grok-build-0.1"},{"value":"nvidia/nemotron-3-super-120b-a12b","name":"nvidia/nemotron-3-super-120b-a12b"},{"value":"zai/glm-5","name":"zai/glm-5"},{"value":"moonshotai/kimi-k2.5","name":"moonshotai/kimi-k2.5"},{"value":"zai/glm-4.7","name":"zai/glm-4.7"},{"value":"nvidia/nemotron-nano-12b-v2-vl","name":"nvidia/nemotron-nano-12b-v2-vl"},{"value":"nvidia/nemotron-nano-9b-v2","name":"nvidia/nemotron-nano-9b-v2"},{"value":"amazon/nova-micro","name":"amazon/nova-micro"},{"value":"deepseek/deepseek-v4.1-flash","name":"deepseek/deepseek-v4.1-flash"},{"value":"deepseek/deepseek-v4-flash","name":"deepseek/deepseek-v4-flash"},{"value":"deepseek/deepseek-v4-flash-0731","name":"deepseek/deepseek-v4-flash-0731"},{"value":"zai/glm-5.3-flash","name":"zai/glm-5.3-flash"},{"value":"zai/glm-4.7-flash","name":"zai/glm-4.7-flash"},{"value":"anthropic/claude-haiku-5.5","name":"anthropic/claude-haiku-5.5"},{"value":"anthropic/claude-haiku-4.5","name":"anthropic/claude-haiku-4.5"},{"value":"anthropic/claude-3-haiku","name":"anthropic/claude-3-haiku"},{"value":"openai/gpt-5.4-mini","name":"openai/gpt-5.4-mini"},{"value":"openai/gpt-5.4-mini-fast","name":"openai/gpt-5.4-mini-fast"},{"value":"openai/gpt-5-mini","name":"openai/gpt-5-mini"},{"value":"openai/gpt-5-mini-fast","name":"openai/gpt-5-mini-fast"},{"value":"openai/gpt-4.1-mini","name":"openai/gpt-4.1-mini"},{"value":"openai/gpt-4.1-mini-fast","name":"openai/gpt-4.1-mini-fast"},{"value":"openai/o4-mini","name":"openai/o4-mini"},{"value":"openai/o4-mini-fast","name":"openai/o4-mini-fast"},{"value":"openai/o3-mini","name":"openai/o3-mini"},{"value":"openai/gpt-4o-mini","name":"openai/gpt-4o-mini"},{"value":"openai/gpt-4o-mini-fast","name":"openai/gpt-4o-mini-fast"},{"value":"google/gemini-3.8-flash","name":"google/gemini-3.8-flash"},{"value":"google/gemini-3.7-flash","name":"google/gemini-3.7-flash"},{"value":"google/gemini-3.5-flash-lite","name":"google/gemini-3.5-flash-lite"},{"value":"google/gemini-3.6-flash","name":"google/gemini-3.6-flash"},{"value":"google/gemini-3.5-flash","name":"google/gemini-3.5-flash"},{"value":"google/gemini-3.1-flash-lite","name":"google/gemini-3.1-flash-lite"},{"value":"google/gemini-3-flash","name":"google/gemini-3-flash"},{"value":"google/gemini-2.5-flash-lite","name":"google/gemini-2.5-flash-lite"},{"value":"google/gemini-2.5-flash","name":"google/gemini-2.5-flash"},{"value":"google/gemini-2.5-pro","name":"google/gemini-2.5-pro"},{"value":"minimax/minimax-m3","name":"minimax/minimax-m3"},{"value":"minimax/minimax-m2.5","name":"minimax/minimax-m2.5"},{"value":"minimax/minimax-m2.1","name":"minimax/minimax-m2.1"},{"value":"amazon/nova-2-lite","name":"amazon/nova-2-lite"},{"value":"amazon/nova-lite","name":"amazon/nova-lite"},{"value":"google/gemini-3.1-pro-preview","name":"google/gemini-3.1-pro-preview"},{"value":"spacexai/grok-4.20-multi-agent-beta","name":"spacexai/grok-4.20-multi-agent-beta"},{"value":"spacexai/grok-4.20-non-reasoning-beta","name":"spacexai/grok-4.20-non-reasoning-beta"},{"value":"spacexai/grok-4.20-reasoning-beta","name":"spacexai/grok-4.20-reasoning-beta"}]},{"id":"mode","name":"Session Mode","description":"Controls how the agent requests permission","category":"mode","type":"select","currentValue":"ask","options":[{"value":"code","name":"Code","description":"Write and modify code with full tool access","permissionMode":"auto"},{"value":"ask","name":"Ask","description":"Request permission before making any changes","permissionMode":"ask"}]},{"id":"effort","name":"Reasoning Effort","description":"Controls how much the model thinks before responding","category":"thought_level","type":"select","currentValue":"xhigh","options":[{"value":"auto","name":"default"},{"value":"low","name":"low"},{"value":"medium","name":"medium"},{"value":"high","name":"high"},{"value":"xhigh","name":"xhigh"},{"value":"max","name":"max"}]}],"modes":{"currentModeId":"ask","availableModes":[{"id":"code","name":"Code","description":"Write and modify code with full tool access"},{"id":"ask","name":"Ask","description":"Request permission before making any changes"}]}}}} diff --git a/packages/effect-acp/src/compat.ts b/packages/effect-acp/src/compat.ts index 8e21d16fd198a..b73884d39dd9b 100644 --- a/packages/effect-acp/src/compat.ts +++ b/packages/effect-acp/src/compat.ts @@ -62,7 +62,11 @@ export type ToolKind = V2.ToolKind; export type ClientCapabilities = { readonly fs?: { readonly readTextFile?: boolean; readonly writeTextFile?: boolean }; readonly terminal?: boolean; - readonly session?: { readonly list?: Meta; readonly notices?: Meta } | null; + readonly session?: { + readonly list?: Meta; + readonly notices?: Meta; + readonly compaction?: Meta; + } | null; readonly plan?: Meta; readonly auth?: { readonly terminal?: boolean }; readonly elicitation?: V2.ElicitationCapabilities | null; diff --git a/packages/provider-acp/src/server/AcpSessionRuntime.ts b/packages/provider-acp/src/server/AcpSessionRuntime.ts index 300a9359e40dd..30364f173d98b 100644 --- a/packages/provider-acp/src/server/AcpSessionRuntime.ts +++ b/packages/provider-acp/src/server/AcpSessionRuntime.ts @@ -1770,8 +1770,9 @@ export const make = Effect.fn("AcpSessionRuntime.make")(function* ( ...options.clientCapabilities?.fs, }, terminal: options.clientCapabilities?.terminal ?? false, - // ACP v1 agents send session notices only to clients that advertise them; v2 needs no capability. - session: { ...options.clientCapabilities?.session, notices: {} }, + // ACP v1 agents send session notices and compaction updates only to clients that advertise + // them; v2 needs no capability. + session: { ...options.clientCapabilities?.session, notices: {}, compaction: {} }, ...(options.clientCapabilities?.auth ? { auth: options.clientCapabilities.auth } : {}), ...(options.clientCapabilities?.elicitation ? { elicitation: options.clientCapabilities.elicitation } diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 1282761d3770d..4852d594088a2 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -444,6 +444,7 @@ export interface AcpAdapterV2Options { readonly testHooks?: { readonly onDeferredFinalizeScheduled?: (debounce: Duration.Input) => Effect.Effect; readonly afterPromptSettledWithBackgroundWork?: () => Effect.Effect; + readonly afterPromptCallback?: () => Effect.Effect; readonly onNativeResponseLifecycle?: (event: { readonly generation?: number; readonly requestId?: string; @@ -7217,15 +7218,12 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( turnInput: ProviderAdapter.ProviderAdapterV2TurnInput, sessionId: string, ) { - const prompt: Array = []; const mcpSession = yield* mcpSessions.read(turnInput.threadId); const instructionState = { interactionMode: turnInput.runtimePolicy.interactionMode, hasT3Mcp: acpMcpContext(mcpSession, self).servers.length > 0, hasDeviceTools: mcpSession?.capabilities?.has("device") === true, } satisfies T3AcpInstructionState; - const previousInstructionState = (yield* Ref.get(promptInstructionStates)).get(sessionId); - const compactionMarker = (yield* Ref.get(compactionMarkers)).get(sessionId); const userText = flavor.rewriteUserText === undefined ? turnInput.message.text @@ -7244,16 +7242,6 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( resolveExtractedTextPath: host.resolveExtractedTextPath, }); const isNativeSlashCommand = userText.trimStart().startsWith("/"); - const text = t3AcpPromptWithInstructions({ - prompt: messageText, - state: instructionState, - ...(previousInstructionState === undefined || compactionMarker !== undefined - ? {} - : { previousState: previousInstructionState }), - }); - if (text.length > 0) { - prompt.push({ type: "text", text }); - } const imageAttachments = turnInput.message.attachments.filter( isProviderNativeImageAttachment, ); @@ -7263,26 +7251,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( detail: "ACP driver did not negotiate image prompt support", }); } - if (flavor.buildPrompt) { - const setup = yield* Ref.get(activeSessionSetup); - const blocks = yield* flavor - .buildPrompt({ - text, - attachments: turnInput.message.attachments, - promptCapabilities: setup?.initializeResult.agentCapabilities?.promptCapabilities, - }) - .pipe( - Effect.mapError( - (cause) => - new ProviderAdapter.ProviderAdapterProtocolError({ - driver, - detail: "Failed to build ACP attachment prompt", - payload: cause, - }), - ), - ); - prompt.splice(0, prompt.length, ...blocks); - } + const imageBlocks: Array = []; for (const attachment of flavor.buildPrompt ? [] : imageAttachments) { const path = host.resolveAttachmentPath(attachment as ChatAttachment); if (path === null) { @@ -7302,18 +7271,23 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( }), ), ); - prompt.push({ + imageBlocks.push({ type: "image", data: Buffer.from(bytes).toString("base64"), mimeType: attachment.mimeType, }); } - if (prompt.length === 0) { - return yield* new ProviderAdapter.ProviderAdapterProtocolError({ - driver, - detail: "ACP turn requires non-empty text or attachments", - }); - } + // Read delivery state after the awaits above, so a compaction that lands + // while the prompt is prepared still resends the notes in this prompt. + const previousInstructionState = (yield* Ref.get(promptInstructionStates)).get(sessionId); + const compactionMarker = (yield* Ref.get(compactionMarkers)).get(sessionId); + const text = t3AcpPromptWithInstructions({ + prompt: messageText, + state: instructionState, + ...(previousInstructionState === undefined || compactionMarker !== undefined + ? {} + : { previousState: previousInstructionState }), + }); const runtimeInstructions = buildRuntimeInstructions({ harness: flavor.runtimeHarness ?? driver, model: turnInput.modelSelection.model, @@ -7323,6 +7297,35 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( !isNativeSlashCommand && (compactionMarker !== undefined || (yield* Ref.get(promptRuntimeInstructions)).get(sessionId) !== runtimeInstructions); + const prompt: Array = + text.length > 0 ? [{ type: "text", text }] : []; + if (flavor.buildPrompt) { + const setup = yield* Ref.get(activeSessionSetup); + const blocks = yield* flavor + .buildPrompt({ + text, + attachments: turnInput.message.attachments, + promptCapabilities: setup?.initializeResult.agentCapabilities?.promptCapabilities, + }) + .pipe( + Effect.mapError( + (cause) => + new ProviderAdapter.ProviderAdapterProtocolError({ + driver, + detail: "Failed to build ACP attachment prompt", + payload: cause, + }), + ), + ); + prompt.splice(0, prompt.length, ...blocks); + } + prompt.push(...imageBlocks); + if (prompt.length === 0) { + return yield* new ProviderAdapter.ProviderAdapterProtocolError({ + driver, + detail: "ACP turn requires non-empty text or attachments", + }); + } if (sendRuntimeInstructions) { prompt.push({ type: "text", text: runtimeInstructions }); } @@ -7710,7 +7713,10 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( } yield* finalizeTurn(context, status); }), - ).pipe(Effect.asVoid), + ).pipe( + Effect.andThen(() => options.testHooks?.afterPromptCallback?.() ?? Effect.void), + Effect.asVoid, + ), ), Effect.catchCause((cause) => runRuntimeCallbackAtGeneration( From c3c16d2192ad787aa6789ccab4e66ff62e25f78c Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:13:56 +0200 Subject: [PATCH 08/22] fix(acp): cancelled prompts resend T3 notes, and compaction patches can clear fields A prompt that ends `cancelled` no longer records T3's instructions, runtime notes, or a cleared compaction mark as delivered. Grok's local cancel reports that result before `session/prompt` may have been written, so the agent might never have seen the notes. A prompt the agent really saw and then cancelled now resends the notes once. The Grok and registry steering transcripts now expect that resend. Compaction updates now follow ACP patch semantics. An omitted `summary`, `error`, or `_meta` keeps the stored value, null (or an empty summary) clears it, and a value replaces it. Previously a retracted summary reappeared. The error is stored apart from the summary, and the compaction item still shows the error in place of the summary while one is set. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 37 +++++++++++- .../testkit/ReplayRuntimeInstructions.ts | 1 + .../message_steering/grok_transcript.ndjson | 2 +- .../registry_transcript.ndjson | 2 +- packages/provider-acp/src/server/adapter.ts | 57 ++++++++++--------- .../src/server/runtimeModel.test.ts | 53 +++++++++++++++++ .../provider-acp/src/server/runtimeModel.ts | 15 ++++- 7 files changed, 133 insertions(+), 34 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 876851b7444ce..861abe551e98f 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -351,6 +351,7 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( | ((event: unknown) => Effect.Effect) | undefined; let duringNextPreparation: Effect.Effect = Effect.void; + let reportNextPromptCancelled = false; const adapter = yield* makeAcpAdapterV2({ instanceId, flavor: { @@ -387,7 +388,15 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( }).pipe(Effect.andThen(runtime.handleExtNotification(method, payload, next))), // Make compaction during prompt completion deterministic at the adapter boundary. prompt: (payload, options) => - runtime.prompt(payload, options).pipe(Effect.tap(() => runtime.drainEvents)), + runtime.prompt(payload, options).pipe( + Effect.tap(() => runtime.drainEvents), + // Grok's local cancel reports `cancelled` even when the agent never saw the prompt. + Effect.map((result) => { + if (!reportNextPromptCancelled) return result; + reportNextPromptCancelled = false; + return { ...result, stopReason: "cancelled" as const }; + }), + ), }), }), }, @@ -438,6 +447,9 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( assert.isDefined(sessionUpdate); yield* sessionUpdate({ sessionId, update }); }), + reportNextPromptCancelled: Effect.sync(() => { + reportNextPromptCancelled = true; + }), sessionUpdateDuringNextPreparation: (update: EffectAcpSchema.SessionUpdate) => Effect.sync(() => { duringNextPreparation = Effect.suspend(() => { @@ -1101,12 +1113,17 @@ describe("AcpAdapterV2", () => { idleSessionUpdate, idleCopilotSessionEvent, sessionUpdateDuringNextPreparation, + reportNextPromptCancelled, } = yield* makeInstructionCompactionHarness({ T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", }); - yield* runTurn("Start work."); - yield* runTurn("Continue until context compacts."); + yield* reportNextPromptCancelled; + assertReinjectedInstructions(yield* runTurn("Start work.", "cancelled"), "Start work."); + assertReinjectedInstructions( + yield* runTurn("Continue until context compacts."), + "Continue until context compacts.", + ); assert.deepEqual(yield* runTurn(" /repro-skill"), [ { type: "text", text: " /repro-skill" }, ]); @@ -1197,6 +1214,20 @@ describe("AcpAdapterV2", () => { assert.deepEqual(yield* runTurn("Finish after that reinjection."), [ { type: "text", text: "Finish after that reinjection." }, ]); + yield* idleSessionUpdate({ + sessionUpdate: "compaction_update", + compactionId: "compact-before-local-cancel", + status: "completed", + }); + yield* reportNextPromptCancelled; + assertReinjectedInstructions( + yield* runTurn("Stop before the agent sees this.", "cancelled"), + "Stop before the agent sees this.", + ); + assertReinjectedInstructions( + yield* runTurn("Continue after the local cancel."), + "Continue after the local cancel.", + ); }).pipe(Effect.provide(layerTest), Effect.scoped), ); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index 180c522a1e084..47a829baceb4d 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -19,6 +19,7 @@ export function materializeReplayTranscriptRuntimeInstructions( if (harness === undefined) return transcript; const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); // The ACP adapter resends these instructions until a prompt that carried them succeeds. + // A prompt the adapter sees as cancelled does not count; its transcript spells out the resend. const instructedSessionIds = new Set(); let pendingInstructedPrompt: { readonly sessionId: unknown } | undefined; diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson index a7f4f9fbbd8cf..da8314feb536e 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/grok_transcript.ndjson @@ -30,7 +30,7 @@ {"type":"emit_inbound","label":"notification:_x.ai/session/prompt_complete","frame":{"kind":"notification","method":"_x.ai/session/prompt_complete","params":{"sessionId":"00000000-0000-4000-8000-000000000001","promptId":"t3-xai-prompt-1","stopReason":"cancelled","agentResult":null,"cancelTrigger":"ctrl_c","cancellationCategory":"MidTurnAbort","cancellationContext":{"trigger":"ctrl_c"}}}} {"type":"emit_inbound","label":"notification:_x.ai/sessions/changed","frame":{"kind":"notification","method":"_x.ai/sessions/changed","params":{"upserted":[{"sessionId":"00000000-0000-4000-8000-000000000001","title":null,"cwd":"","isWorktree":false,"modelId":"grok-4.7","reasoningEffort":"high","yolo":false,"activity":"idle","resident":true,"lastChangeUnixMs":1790300235649,"origin":{"kind":"local"}}],"removed":[]}}} {"type":"emit_inbound","label":"response:session/prompt","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"cancelled","_meta":{"sessionId":"00000000-0000-4000-8000-000000000001","requestId":"t3-xai-prompt-1","promptId":"t3-xai-prompt-1","totalTokens":2263,"modelId":"grok-4.7","cancellationCategory":"MidTurnAbort","cancellationContext":{"trigger":"ctrl_c"},"cancelTrigger":"ctrl_c"}}}} -{"type":"expect_outbound","label":"request:session/prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"00000000-0000-4000-8000-000000000001","prompt":[{"type":"text","text":"Actually, respond with exactly: steering fixture observed"}],"_meta":{"promptId":"t3-xai-prompt-2","requestId":"t3-xai-prompt-2"}}}} +{"type":"expect_outbound","label":"request:session/prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"00000000-0000-4000-8000-000000000001","prompt":[{"type":"text","text":"\n\n\n\n\nActually, respond with exactly: steering fixture observed\n"},{"type":"text","text":""}],"_meta":{"promptId":"t3-xai-prompt-2","requestId":"t3-xai-prompt-2"}}}} {"type":"emit_inbound","label":"notification:_x.ai/sessions/changed","frame":{"kind":"notification","method":"_x.ai/sessions/changed","params":{"upserted":[{"sessionId":"00000000-0000-4000-8000-000000000001","title":null,"cwd":"","isWorktree":false,"modelId":"grok-4.7","reasoningEffort":"high","yolo":false,"activity":"working","resident":true,"lastChangeUnixMs":1790300235729,"origin":{"kind":"local"}}],"removed":[]}}} {"type":"emit_inbound","label":"notification:_x.ai/queue/changed","frame":{"kind":"notification","method":"_x.ai/queue/changed","params":{"sessionId":"00000000-0000-4000-8000-000000000001","entries":[{"id":"t3-xai-prompt-2","version":0,"kind":"prompt","text":"Actually, respond with exactly: steering fixture observed\n\nIn case you're asked: you are running in T3 Code through the Grok harness, as grok-build. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.\n\n\nWhen the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through your host's CLI (including its stack commands) or its API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked.\n","position":0}]}}} {"type":"emit_inbound","label":"notification:_x.ai/queue/changed","frame":{"kind":"notification","method":"_x.ai/queue/changed","params":{"sessionId":"00000000-0000-4000-8000-000000000001","entries":[],"runningPromptId":"t3-xai-prompt-2","runningText":"Actually, respond with exactly: steering fixture observed\n\nIn case you're asked: you are running in T3 Code through the Grok harness, as grok-build. No need to mention this otherwise. You can embed images and videos in your response using Markdown with absolute file paths.\n\n\nWhen the t3-code MCP server exposes link_pull_request, you must use it to register every pull request you create or work on for this thread. Call link_pull_request with the full PR URL immediately after creating a PR or starting work on an existing PR. For a stack, call it for every layer, not just the current branch or the top PR. This applies when creating or updating PRs through your host's CLI (including its stack commands) or its API: those operations do not register the PRs with this thread. Linking an already-linked PR is safe. Before finishing PR work, call list_thread_pull_requests and link any PR from your work that is missing. Do not link unrelated PRs mentioned only as background. If a linking call fails, report that failure instead of claiming the PR is linked.\n","runningKind":"prompt"}}} diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/registry_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/registry_transcript.ndjson index 0ffbf65d3f37c..c644f675c5501 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/registry_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/registry_transcript.ndjson @@ -7,7 +7,7 @@ {"type":"emit_inbound","label":"assistant.partial","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"ste"}}}}} {"type":"expect_outbound","label":"session.cancel","frame":{"kind":"notification","method":"session/cancel","params":{"sessionId":"grok-replay-session-1"}}} {"type":"emit_inbound","label":"prompt.cancelled","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"cancelled"}}} -{"type":"expect_outbound","label":"session.prompt.2","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"Actually, respond with exactly: steering fixture observed"}]}}} +{"type":"expect_outbound","label":"session.prompt.2","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"grok-replay-session-1","prompt":[{"type":"text","text":"\n\n\n\n\nActually, respond with exactly: steering fixture observed\n"},{"type":"text","text":""}]}}} {"type":"emit_inbound","label":"assistant.1","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"steering fixture "}}}}} {"type":"emit_inbound","label":"assistant.2","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"grok-replay-session-1","update":{"sessionUpdate":"agent_message_chunk","content":{"type":"text","text":"observed"}}}}} {"type":"emit_inbound","label":"prompt.completed","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"end_turn"}}} diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 4852d594088a2..97154c396203d 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -3677,7 +3677,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( }); let turnItem: OrchestrationV2TurnItem; if (toolCall.toolCallId.startsWith("acp-compaction:")) { - const summary = textFromUnknown(rawOutput); + const summary = textFromUnknown(toolCall.data.error ?? rawOutput); turnItem = { ...base, type: "compaction", @@ -7647,32 +7647,35 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( runRuntimeCallbackAtGeneration( promptGeneration, Effect.gen(function* () { - // Record delivery only for the current runtime. A replacement can reuse - // this session id after clearing what was sent to it. - yield* Ref.update(promptInstructionStates, (current) => { - if (promptParts?.instructionState === undefined) return current; - const updated = new Map(current); - updated.set(requestedSessionId, promptParts.instructionState); - return updated; - }); - yield* Ref.update(promptRuntimeInstructions, (current) => { - if (promptParts?.runtimeInstructions === undefined) return current; - const updated = new Map(current); - updated.set(requestedSessionId, promptParts.runtimeInstructions); - return updated; - }); - yield* Ref.update(compactionMarkers, (current) => { - if (promptParts?.instructionState === undefined) return current; - const observedMarker = promptParts.compactionMarker; - if ( - observedMarker === undefined || - current.get(requestedSessionId) !== observedMarker - ) - return current; - const updated = new Map(current); - updated.delete(requestedSessionId); - return updated; - }); + // Record delivery only for the current runtime, since a replacement can + // reuse this session id. A cancelled prompt may never have reached the + // agent (Grok cancels locally), so the next prompt resends the notes. + if (result.stopReason !== "cancelled") { + yield* Ref.update(promptInstructionStates, (current) => { + if (promptParts?.instructionState === undefined) return current; + const updated = new Map(current); + updated.set(requestedSessionId, promptParts.instructionState); + return updated; + }); + yield* Ref.update(promptRuntimeInstructions, (current) => { + if (promptParts?.runtimeInstructions === undefined) return current; + const updated = new Map(current); + updated.set(requestedSessionId, promptParts.runtimeInstructions); + return updated; + }); + yield* Ref.update(compactionMarkers, (current) => { + if (promptParts?.instructionState === undefined) return current; + const observedMarker = promptParts.compactionMarker; + if ( + observedMarker === undefined || + current.get(requestedSessionId) !== observedMarker + ) + return current; + const updated = new Map(current); + updated.delete(requestedSessionId); + return updated; + }); + } if (context.finalized || followsAgentWorkState) return; // ACP v2 reports a failed prompt as an idle `error`, not an RPC error. const idleError = result.stopReason === "error" && !context.interrupted; diff --git a/packages/provider-acp/src/server/runtimeModel.test.ts b/packages/provider-acp/src/server/runtimeModel.test.ts index e60671c775d51..0f7b0e8f28ffa 100644 --- a/packages/provider-acp/src/server/runtimeModel.test.ts +++ b/packages/provider-acp/src/server/runtimeModel.test.ts @@ -990,6 +990,59 @@ describe("AcpRuntimeModel", () => { _tag: "UsageUpdated", usage: { usedTokens: 420 }, }); + + // Later updates patch fields: omission keeps a value, null or [] clears it. + let patched: AcpToolCallState | undefined; + const patchCompaction = ( + patch: Omit< + Extract, + "sessionUpdate" | "compactionId" + >, + ) => { + const event = parseSessionUpdateEvent({ + sessionId: "session-1", + update: { sessionUpdate: "compaction_update", compactionId: "compact-2", ...patch }, + }).events[0]; + if (event?._tag !== "ToolCallUpdated") throw new Error("expected a compaction tool update"); + patched = mergeToolCallState(patched, event.toolCall); + const { rawOutput, error, meta } = patched.data; + return { summary: rawOutput, error, meta }; + }; + const kept = [{ type: "text" as const, text: "Kept the plan." }]; + expect( + patchCompaction({ status: "completed", summary: kept, _meta: { source: "agent" } }), + ).toEqual({ summary: "Kept the plan.", meta: { source: "agent" } }); + expect(patchCompaction({ status: "completed" })).toEqual({ + summary: "Kept the plan.", + meta: { source: "agent" }, + }); + expect(patchCompaction({ status: "completed", summary: null, _meta: null })).toEqual({ + summary: null, + meta: null, + }); + expect(patchCompaction({ status: "completed", summary: kept })).toEqual({ + summary: "Kept the plan.", + meta: null, + }); + expect(patchCompaction({ status: "completed", summary: [] })).toEqual({ + summary: null, + meta: null, + }); + expect(patchCompaction({ status: "failed", error: "Context is still too large." })).toEqual({ + summary: null, + error: "Context is still too large.", + meta: null, + }); + expect(patchCompaction({ status: "failed" })).toEqual({ + summary: null, + error: "Context is still too large.", + meta: null, + }); + expect(patchCompaction({ status: "failed", error: null })).toEqual({ + summary: null, + error: null, + meta: null, + }); }); it("applies ACP v2 agent-owned terminal snapshots and output chunks", () => { diff --git a/packages/provider-acp/src/server/runtimeModel.ts b/packages/provider-acp/src/server/runtimeModel.ts index 43fa2bf758e2f..98ef72f97a021 100644 --- a/packages/provider-acp/src/server/runtimeModel.ts +++ b/packages/provider-acp/src/server/runtimeModel.ts @@ -1526,16 +1526,27 @@ export function parseSessionUpdateEvent(params: EffectAcpSchema.SessionNotificat : upd.status === "failed" || upd.status === "cancelled" ? ("failed" as const) : ("inProgress" as const); + // ACP patch semantics: an omitted field keeps its stored value; null (or an empty + // summary) clears it. The merge keeps any data key this update leaves out. const toolCall = makeToolCallState({ toolCallId: `acp-compaction:${upd.compactionId}`, title: "Compact context", kind: "think", status, - rawOutput: (upd.error ?? summary) || undefined, + ...("summary" in upd ? { rawOutput: summary || null } : {}), _meta: upd._meta, }); if (toolCall !== undefined) { - events.push({ _tag: "ToolCallUpdated", toolCall, rawPayload: params }); + const data = { + ...toolCall.data, + ...("error" in upd ? { error: upd.error ?? null } : {}), + ...(upd._meta === null ? { meta: null } : {}), + }; + events.push({ + _tag: "ToolCallUpdated", + toolCall: { ...toolCall, data }, + rawPayload: params, + }); } break; } From 4359e8c437a9001414ca7d68831d1da514ae5d3c Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:29:23 +0200 Subject: [PATCH 09/22] test(acp): the late-prompt rollback case settles the stale prompt successfully A cancelled stale prompt never records delivery, so the case no longer proved the generation check. A successful one does, and the case fails without the check. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Adapters/AcpAdapterV2.test.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 861abe551e98f..884a791c8d2fe 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -3712,9 +3712,10 @@ describe("AcpAdapterV2", () => { appRunOrdinal: 0, }, }); - // Closing the replaced runtime settles its open prompt as cancelled. Its completion - // callback must finish before the next turn reads delivery state. - yield* Deferred.succeed(firstPromptGate, { stopReason: "cancelled" }); + // The replaced runtime's prompt settles late. A successful result would record delivery + // without the generation check; a cancelled one never records it, so it proves nothing. + // Its completion callback must finish before the next turn reads delivery state. + yield* Deferred.succeed(firstPromptGate, { stopReason: "end_turn" }); yield* Deferred.await(firstPromptCallbackDone); yield* runtime.startTurn( makeTurnInput({ From c52a2d38ec9b2568c5871f9651f477eb81798f14 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Thu, 29 Oct 2026 19:40:00 +0200 Subject: [PATCH 10/22] fix(acp): /compact with preservation instructions also resends T3 notes Only a bare /compact counted as a native compaction, so after '/compact keep the plan' the next prompts kept omitting the runtime notes the compaction may have removed. Co-Authored-By: Claude Opus 5.5 --- .../src/orchestration-v2/Adapters/AcpAdapterV2.test.ts | 8 ++++++++ packages/provider-acp/src/server/adapter.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index 884a791c8d2fe..c4959cc90517c 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1249,6 +1249,14 @@ describe("AcpAdapterV2", () => { assert.deepEqual(yield* runTurn("/repro-skill"), [{ type: "text", text: "/repro-skill" }]); assertReinjectedInstructions(yield* runTurn("Continue work."), "Continue work."); assert.deepEqual(yield* runTurn("Finish work."), [{ type: "text", text: "Finish work." }]); + // Agents such as Grok accept preservation instructions after the command. + assert.deepEqual(yield* runTurn("/compact keep the plan"), [ + { type: "text", text: "/compact keep the plan" }, + ]); + assertReinjectedInstructions( + yield* runTurn("Continue after the focused compact."), + "Continue after the focused compact.", + ); }).pipe(Effect.provide(layerTest), Effect.scoped), ); diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 97154c396203d..f0e4fe6a2c234 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -7052,7 +7052,8 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( const now = yield* DateTime.now; if ( flavor.supportsCompaction === true && - context.input.message.text.trim() === "/compact" && + // Some agents accept preservation instructions after the command. + /^\/compact(?:\s|$)/.test(context.input.message.text.trim()) && context.input.message.attachments.length === 0 && settledStatus === "completed" ) { From 17d766be90cdc84090312a621ace72ffd61501a1 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 01:10:15 +0000 Subject: [PATCH 11/22] test(acp): keep prompt echo coverage with session runtime notes --- .../src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts index 9c7ea03fe7510..5fc26f8f2b8c8 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpRegistryAdapterV2.test.ts @@ -1064,7 +1064,7 @@ describe("AcpRegistryAdapterV2", () => { }) : "test prompt", }, - runtimeInstructions, + ...(ordinal === 1 ? [runtimeInstructions] : []), ]; const echo = (messageId: string, content: ReadonlyArray) => claudeFrames.update(CLAUDE_ROOT_SESSION, { @@ -1085,6 +1085,7 @@ describe("AcpRegistryAdapterV2", () => { { type: "text", text: promptText(ordinal), + annotations: { audience: ["user"] }, }, ]), ...(ordering === "before" ? [matching, acknowledgment] : [acknowledgment, matching]), From c34d5b40862fe25e6061b8b9a6421aecdc927422 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 01:13:08 +0000 Subject: [PATCH 12/22] fix(acp): retry runtime notes after refused or failed prompts --- .../Adapters/AcpAdapterV2.test.ts | 44 +++++++++++-------- .../testkit/ReplayRuntimeInstructions.ts | 14 +++++- .../message_steering/fx_transcript.ndjson | 2 +- packages/provider-acp/src/server/adapter.ts | 8 +++- 4 files changed, 45 insertions(+), 23 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index e6ad106e7f6a8..cc1bdfd03f0f2 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -471,7 +471,7 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( | ((event: unknown) => Effect.Effect) | undefined; let duringNextPreparation: Effect.Effect = Effect.void; - let reportNextPromptCancelled = false; + let nextPromptStopReason: "cancelled" | "refusal" | "error" | undefined; const adapter = yield* makeAcpAdapterV2({ instanceId, flavor: { @@ -512,9 +512,10 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( Effect.tap(() => runtime.drainEvents), // Grok's local cancel reports `cancelled` even when the agent never saw the prompt. Effect.map((result) => { - if (!reportNextPromptCancelled) return result; - reportNextPromptCancelled = false; - return { ...result, stopReason: "cancelled" as const }; + if (nextPromptStopReason === undefined) return result; + const stopReason = nextPromptStopReason; + nextPromptStopReason = undefined; + return { ...result, stopReason }; }), ), }), @@ -567,9 +568,10 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( assert.isDefined(sessionUpdate); yield* sessionUpdate({ sessionId, update }); }), - reportNextPromptCancelled: Effect.sync(() => { - reportNextPromptCancelled = true; - }), + reportNextPromptStopped: (stopReason: "cancelled" | "refusal" | "error") => + Effect.sync(() => { + nextPromptStopReason = stopReason; + }), sessionUpdateDuringNextPreparation: (update: EffectAcpSchema.SessionUpdate) => Effect.sync(() => { duringNextPreparation = Effect.suspend(() => { @@ -1914,22 +1916,28 @@ describe("AcpAdapterV2", () => { }).pipe(Effect.provide(layerTest), Effect.scoped), ); - it.live( - "reinjects ACP instructions after protocol compaction, during reinjection, prompt preparation, or while idle", - () => + it.live.each(["cancelled", "refusal", "error"] as const)( + "reinjects ACP instructions after protocol compaction, during reinjection, prompt preparation, or while idle (%s)", + (stopReason) => Effect.gen(function* () { const { runTurn, idleSessionUpdate, idleCopilotSessionEvent, sessionUpdateDuringNextPreparation, - reportNextPromptCancelled, + reportNextPromptStopped, } = yield* makeInstructionCompactionHarness({ T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", }); - yield* reportNextPromptCancelled; - assertReinjectedInstructions(yield* runTurn("Start work.", "cancelled"), "Start work."); + const expectedStatus = + stopReason === "error" + ? "failed" + : stopReason === "cancelled" + ? "cancelled" + : "completed"; + yield* reportNextPromptStopped(stopReason); + assertReinjectedInstructions(yield* runTurn("Start work.", expectedStatus), "Start work."); assertReinjectedInstructions( yield* runTurn("Continue until context compacts."), "Continue until context compacts.", @@ -2026,16 +2034,16 @@ describe("AcpAdapterV2", () => { ]); yield* idleSessionUpdate({ sessionUpdate: "compaction_update", - compactionId: "compact-before-local-cancel", + compactionId: "compact-before-undelivered-prompt", status: "completed", }); - yield* reportNextPromptCancelled; + yield* reportNextPromptStopped(stopReason); assertReinjectedInstructions( - yield* runTurn("Stop before the agent sees this.", "cancelled"), - "Stop before the agent sees this.", + yield* runTurn("Do not retain this prompt.", expectedStatus), + "Do not retain this prompt.", ); assertReinjectedInstructions( - yield* runTurn("Continue after the local cancel."), + yield* runTurn("Continue after the undelivered prompt."), "Continue after the local cancel.", ); }).pipe(Effect.provide(layerTest), Effect.scoped), diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index 47a829baceb4d..4a47a1b9cbec5 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -19,7 +19,7 @@ export function materializeReplayTranscriptRuntimeInstructions( if (harness === undefined) return transcript; const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); // The ACP adapter resends these instructions until a prompt that carried them succeeds. - // A prompt the adapter sees as cancelled does not count; its transcript spells out the resend. + // Cancelled, refused, and failed prompts do not establish delivery. const instructedSessionIds = new Set(); let pendingInstructedPrompt: { readonly sessionId: unknown } | undefined; @@ -38,7 +38,17 @@ export function materializeReplayTranscriptRuntimeInstructions( "result" in frame && pendingInstructedPrompt !== undefined ) { - instructedSessionIds.add(pendingInstructedPrompt.sessionId); + const result = frame.result; + if ( + typeof result !== "object" || + result === null || + !("stopReason" in result) || + (result.stopReason !== "cancelled" && + result.stopReason !== "refusal" && + result.stopReason !== "error") + ) { + instructedSessionIds.add(pendingInstructedPrompt.sessionId); + } pendingInstructedPrompt = undefined; } return entry; diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson index 29f8049d0ef16..86c9b5d36e00e 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/message_steering/fx_transcript.ndjson @@ -12,7 +12,7 @@ {"type":"expect_outbound","label":"notification:session/cancel","frame":{"kind":"notification","method":"session/cancel","params":{"sessionId":"fx-session-01"}}} {"type":"emit_inbound","label":"notification:session/update:session_info_update","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"session_info_update","title":"Steering Fixture Initial Response","updatedAt":"2026-10-11T13:54:11Z"}}}} {"type":"emit_inbound","label":"response:session/prompt","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"cancelled","usage":{}}}} -{"type":"expect_outbound","label":"request:session/prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"fx-session-01","prompt":[{"type":"text","text":"Actually, respond with exactly: steering fixture observed"}]}}} +{"type":"expect_outbound","label":"request:session/prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"fx-session-01","prompt":[{"type":"text","text":"\n\n\n\n\nActually, respond with exactly: steering fixture observed\n"},{"type":"text","text":""}]}}} {"type":"emit_inbound","label":"notification:session/update:agent_message_chunk","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"agent_message_chunk","messageId":"10c63c6eb70c5fb1cb62ee14ceec92ac","content":{"type":"text","text":"steering fixture observed"}}}}} {"type":"emit_inbound","label":"notification:session/update:session_info_update","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"session_info_update","title":"Steering Fixture Initial Response","updatedAt":"2026-10-11T13:54:14Z"}}}} {"type":"emit_inbound","label":"notification:session/update:usage_update","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"usage_update","used":20096,"size":1000000}}}} diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 007d769bfdaab..82fdb8ca4d1fb 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -7745,8 +7745,12 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( Effect.gen(function* () { // Record delivery only for the current runtime, since a replacement can // reuse this session id. A cancelled prompt may never have reached the - // agent (Grok cancels locally), so the next prompt resends the notes. - if (result.stopReason !== "cancelled") { + // agent (Grok cancels locally); refusals discard it and errors need a retry. + if ( + result.stopReason !== "cancelled" && + result.stopReason !== "refusal" && + result.stopReason !== "error" + ) { yield* Ref.update(promptInstructionStates, (current) => { if (promptParts?.instructionState === undefined) return current; const updated = new Map(current); From 2300f2566827fe3f13eb7a5e52222d93340af61c Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 01:17:49 +0000 Subject: [PATCH 13/22] fix(acp): finish attachment preparation before checking delivery --- .../Adapters/AcpAdapterV2.test.ts | 33 +++++++----- .../Adapters/AntigravityAdapterV2.ts | 3 +- packages/provider-acp/src/server/adapter.ts | 53 ++++++++++--------- 3 files changed, 50 insertions(+), 39 deletions(-) diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index cc1bdfd03f0f2..54af6b453deeb 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -446,6 +446,7 @@ const pollProtocolMethods = (events: Queue.Queue>, + prepareAttachments = false, ) { const childProcessSpawner = yield* ChildProcessSpawner.ChildProcessSpawner; const path = yield* Path.Path; @@ -472,19 +473,24 @@ const makeInstructionCompactionHarness = Effect.fnUntraced(function* ( | undefined; let duringNextPreparation: Effect.Effect = Effect.void; let nextPromptStopReason: "cancelled" | "refusal" | "error" | undefined; + const prepare = Effect.suspend(() => { + const during = duringNextPreparation; + duringNextPreparation = Effect.void; + return during.pipe(Effect.orDie); + }); const adapter = yield* makeAcpAdapterV2({ instanceId, flavor: { driver: ACP_TEST_DRIVER, capabilities: AcpProviderCapabilitiesV2, supportsCompaction: true, - // Prompt preparation awaits this hook, so frames sent from it land mid-preparation. rewriteUserText: (text) => - Effect.suspend(() => { - const during = duringNextPreparation; - duringNextPreparation = Effect.void; - return during.pipe(Effect.orDie, Effect.as(text)); - }), + prepareAttachments ? Effect.succeed(text) : prepare.pipe(Effect.as(text)), + ...(prepareAttachments + ? { + buildAttachmentPrompt: () => prepare.pipe(Effect.as([])), + } + : {}), registerExtensions: makeCopilotSubagentRouting().registerExtensions, // Routes updates tagged with a Devin subagent context to that child session. normalizeSessionUpdate: normalizeDevinSessionUpdate, @@ -1926,10 +1932,13 @@ describe("AcpAdapterV2", () => { idleCopilotSessionEvent, sessionUpdateDuringNextPreparation, reportNextPromptStopped, - } = yield* makeInstructionCompactionHarness({ - T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", - T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", - }); + } = yield* makeInstructionCompactionHarness( + { + T3_ACP_COMPACTION_PROMPT_ORDINALS: "2,4", + T3_ACP_OPEN_COMPACTION_PROMPT_ORDINALS: "13", + }, + stopReason === "error", + ); const expectedStatus = stopReason === "error" ? "failed" @@ -1943,7 +1952,7 @@ describe("AcpAdapterV2", () => { "Continue until context compacts.", ); assert.deepEqual(yield* runTurn(" /repro-skill"), [ - { type: "text", text: " /repro-skill" }, + { type: "text", text: stopReason === "error" ? "/repro-skill" : " /repro-skill" }, ]); assertReinjectedInstructions( yield* runTurn("Continue after compaction."), @@ -2044,7 +2053,7 @@ describe("AcpAdapterV2", () => { ); assertReinjectedInstructions( yield* runTurn("Continue after the undelivered prompt."), - "Continue after the local cancel.", + "Continue after the undelivered prompt.", ); }).pipe(Effect.provide(layerTest), Effect.scoped), ); diff --git a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts index bc842d2ec9724..59735bd43646f 100644 --- a/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts +++ b/apps/server/src/orchestration-v2/Adapters/AntigravityAdapterV2.ts @@ -190,13 +190,14 @@ export function makeAntigravityAcpAdapterFlavor( runtimeHarness: "Antigravity", capabilities: AntigravityProviderCapabilitiesV2, makeRuntime, - buildPrompt: ({ text, attachments, promptCapabilities }) => + buildAttachmentPrompt: ({ text, attachments, promptCapabilities }) => buildAntigravityPrompt({ input: text, attachments, attachmentsDir: options.host.paths.attachmentsDir, promptCapabilities, }).pipe( + Effect.map((blocks) => (text.trim() ? blocks.slice(1) : blocks)), Effect.provideService(FileSystem.FileSystem, options.fileSystem), Effect.provideService(Path.Path, options.path), ), diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index 82fdb8ca4d1fb..aba834f0d8b96 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -259,7 +259,7 @@ export function acpPerTurnPromptUsage( } export interface AcpAdapterV2Flavor { - readonly buildPrompt?: (input: { + readonly buildAttachmentPrompt?: (input: { readonly text: string; readonly attachments: ReadonlyArray; readonly promptCapabilities: EffectAcpSchema.AgentCapabilities["promptCapabilities"]; @@ -7333,8 +7333,8 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( detail: "ACP driver did not negotiate image prompt support", }); } - const imageBlocks: Array = []; - for (const attachment of flavor.buildPrompt ? [] : imageAttachments) { + const attachmentBlocks: Array = []; + for (const attachment of flavor.buildAttachmentPrompt ? [] : imageAttachments) { const path = host.resolveAttachmentPath(attachment as ChatAttachment); if (path === null) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ @@ -7353,12 +7353,32 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( }), ), ); - imageBlocks.push({ + attachmentBlocks.push({ type: "image", data: Buffer.from(bytes).toString("base64"), mimeType: attachment.mimeType, }); } + if (flavor.buildAttachmentPrompt) { + const setup = yield* Ref.get(activeSessionSetup); + const blocks = yield* flavor + .buildAttachmentPrompt({ + text: messageText, + attachments: turnInput.message.attachments, + promptCapabilities: setup?.initializeResult.agentCapabilities?.promptCapabilities, + }) + .pipe( + Effect.mapError( + (cause) => + new ProviderAdapter.ProviderAdapterProtocolError({ + driver, + detail: "Failed to build ACP attachment prompt", + payload: cause, + }), + ), + ); + attachmentBlocks.push(...blocks); + } // Read delivery state after the awaits above, so a compaction that lands // while the prompt is prepared still resends the notes in this prompt. const previousInstructionState = (yield* Ref.get(promptInstructionStates)).get(sessionId); @@ -7379,29 +7399,10 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( !isNativeSlashCommand && (compactionMarker !== undefined || (yield* Ref.get(promptRuntimeInstructions)).get(sessionId) !== runtimeInstructions); + const promptText = flavor.buildAttachmentPrompt ? text.trim() : text; const prompt: Array = - text.length > 0 ? [{ type: "text", text }] : []; - if (flavor.buildPrompt) { - const setup = yield* Ref.get(activeSessionSetup); - const blocks = yield* flavor - .buildPrompt({ - text, - attachments: turnInput.message.attachments, - promptCapabilities: setup?.initializeResult.agentCapabilities?.promptCapabilities, - }) - .pipe( - Effect.mapError( - (cause) => - new ProviderAdapter.ProviderAdapterProtocolError({ - driver, - detail: "Failed to build ACP attachment prompt", - payload: cause, - }), - ), - ); - prompt.splice(0, prompt.length, ...blocks); - } - prompt.push(...imageBlocks); + promptText.length > 0 ? [{ type: "text", text: promptText }] : []; + prompt.push(...attachmentBlocks); if (prompt.length === 0) { return yield* new ProviderAdapter.ProviderAdapterProtocolError({ driver, From 49a5f5526ee1b4e4645be044154b6572f556eb1a Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 01:22:55 +0000 Subject: [PATCH 14/22] fix(acp): preserve stop reasons when v1 usage is incomplete --- .../testkit/ReplayRuntimeInstructions.ts | 2 +- packages/effect-acp/src/client.test.ts | 15 +++++++++++++++ packages/effect-acp/src/rpc.ts | 7 ++++++- 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index 4a47a1b9cbec5..f1ed2feec43ab 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -83,7 +83,7 @@ export function materializeReplayTranscriptRuntimeInstructions( // Like the adapter, native slash commands go out without instructions. if (promptPartText(prompt[0])?.trimStart().startsWith("/") === true) return entry; pendingInstructedPrompt = { sessionId: promptSessionId }; - if (promptPartText(prompt.at(-1)) === instructions) return entry; + if (promptPartText(prompt.at(-1))?.startsWith("") === true) return entry; return { ...entry, frame: { diff --git a/packages/effect-acp/src/client.test.ts b/packages/effect-acp/src/client.test.ts index 1d5b78849f08a..5a1759a8ec801 100644 --- a/packages/effect-acp/src/client.test.ts +++ b/packages/effect-acp/src/client.test.ts @@ -1022,6 +1022,21 @@ it.layer(NodeServices.layer)("effect-acp client", (it) => { ]), ); assert.deepEqual(yield* Fiber.join(promptFiber), { stopReason: "end_turn" }); + const cancelledPrompt = yield* acp.agent + .prompt({ sessionId: "pi-session-1", prompt: [{ type: "text", text: "retry" }] }) + .pipe(Effect.forkScoped); + const cancelledRequest = yield* Queue.take(output).pipe( + Effect.flatMap(Schema.decodeEffect(Schema.fromJsonString(PromptRequestV1))), + ); + yield* Queue.offer( + input, + yield* encodeJsonl(Schema.Unknown, { + jsonrpc: "2.0", + id: cancelledRequest.id, + result: { stopReason: "cancelled", usage: {} }, + }), + ); + assert.deepEqual(yield* Fiber.join(cancelledPrompt), { stopReason: "cancelled" }); assert.deepEqual(yield* Ref.get(updates), [ { sessionId: "pi-session-1", diff --git a/packages/effect-acp/src/rpc.ts b/packages/effect-acp/src/rpc.ts index 9d21c7cbe33c6..5154b948b0ad4 100644 --- a/packages/effect-acp/src/rpc.ts +++ b/packages/effect-acp/src/rpc.ts @@ -245,7 +245,12 @@ const CompatDisableProviderRpc = Rpc.make(AGENT_METHODS.providers_disable, { const CompatPromptRpc = Rpc.make(AGENT_METHODS.session_prompt, { payload: Schema.Union([AcpSchema.PromptRequest, AcpSchemaV1.PromptRequest]), - success: Schema.Union([AcpSchemaV1.PromptResponse, AcpSchema.PromptResponse]), + success: Schema.Union([ + AcpSchemaV1.PromptResponse, + // fx sends empty usage on cancellation; retain its stop reason when usage is invalid. + Schema.Struct({ stopReason: AcpSchemaV1.StopReason }), + AcpSchema.PromptResponse, + ]), error: AcpSchema.Error, }); From 00f5ff103b78f9311818c958e5984e658cd68fab Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sat, 31 Oct 2026 01:39:57 +0000 Subject: [PATCH 15/22] test(acp): expect notes after an early fx cancellation --- .../testkit/fixtures/fx_early_stop/fx_transcript.ndjson | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson b/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson index 07b2388875a77..443a33b13ca30 100644 --- a/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson +++ b/apps/server/src/orchestration-v2/testkit/fixtures/fx_early_stop/fx_transcript.ndjson @@ -12,7 +12,7 @@ {"type":"expect_outbound","label":"notification:session/cancel","frame":{"kind":"notification","method":"session/cancel","params":{"sessionId":"fx-session-01"}}} {"type":"emit_inbound","label":"notification:session/update:session_info_update","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"session_info_update","title":"Interrupt Fixture Should Not Finish Naturally","updatedAt":"2026-10-11T12:47:36Z"}}}} {"type":"emit_inbound","label":"response:session/prompt","frame":{"kind":"response","method":"session/prompt","result":{"stopReason":"cancelled","usage":{}}}} -{"type":"expect_outbound","label":"request:session/prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"fx-session-01","prompt":[{"type":"text","text":"Respond with the following text: fixture simple ok"}]}}} +{"type":"expect_outbound","label":"request:session/prompt","frame":{"kind":"request","method":"session/prompt","params":{"sessionId":"fx-session-01","prompt":[{"type":"text","text":"\n\n\n\n\nRespond with the following text: fixture simple ok\n"},{"type":"text","text":""}]}}} {"type":"emit_inbound","label":"notification:session/update:agent_message_chunk","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"agent_message_chunk","messageId":"40d0598a1dc44d74bf44dfcc62d4ab1e","content":{"type":"text","text":"fixture simple ok"}}}}} {"type":"emit_inbound","label":"notification:session/update:session_info_update","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"session_info_update","title":"Interrupt Fixture Should Not Finish Naturally","updatedAt":"2026-10-11T12:47:39Z"}}}} {"type":"emit_inbound","label":"notification:session/update:usage_update","frame":{"kind":"notification","method":"session/update","params":{"sessionId":"fx-session-01","update":{"sessionUpdate":"usage_update","used":20133,"size":1000000}}}} From e085954c590b64b579fbb415e74922a92768032c Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:29:05 +0200 Subject: [PATCH 16/22] fix(acp): a runtime restarted after a hidden failure resends T3 notes restartRuntimeAfterTeardownIfRequired kept the delivery records, so after an end_turn the outcome check rejected, the fresh runtime sent neither the instruction block nor the runtime notes. It now clears them like a replacement. The replay helper now tracks delivery like the adapter: a completed compaction_update, a successful /compact to Grok and a successful Kiro compaction reset it; a refused or failed Kiro compaction does not. Co-Authored-By: Claude Opus 5.5 --- .../Adapters/AcpAdapterV2.test.ts | 11 +- .../testkit/ReplayRuntimeInstructions.test.ts | 119 ++++++++++++++++++ .../testkit/ReplayRuntimeInstructions.ts | 107 +++++++++++----- packages/provider-acp/src/server/adapter.ts | 5 + 4 files changed, 211 insertions(+), 31 deletions(-) create mode 100644 apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts diff --git a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts index af1f1b05faeed..365a233feecb8 100644 --- a/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts +++ b/apps/server/src/orchestration-v2/Adapters/AcpAdapterV2.test.ts @@ -1061,6 +1061,7 @@ describe("AcpAdapterV2", () => { } as const; // The runtime ordinal of each prompt, and the session each check watched. const promptRuntimes: Array = []; + const promptTexts: Array = []; const watchedSessions: Array = []; const adapter = yield* makeAcpAdapterV2({ instanceId, @@ -1081,9 +1082,14 @@ describe("AcpAdapterV2", () => { ), wrapRuntime: (runtime, runtimeOrdinal) => ({ ...runtime, - prompt: () => + prompt: (payload) => Effect.sync(() => { promptRuntimes.push(runtimeOrdinal); + promptTexts.push( + payload.prompt + .flatMap((block) => (block.type === "text" ? [block.text] : [])) + .join("\n"), + ); return { stopReason: "end_turn" as const }; }), }), @@ -1134,6 +1140,9 @@ describe("AcpAdapterV2", () => { const completed = yield* runTurn(2); assert.equal(completed?.type === "turn.terminal" ? completed.status : null, "completed"); assert.deepEqual(promptRuntimes, [1, 2], "the next turn runs on a fresh runtime"); + // The fresh runtime has no record of what reached the agent, so it sends T3's notes again. + assert.include(promptTexts[1] ?? "", ""); + assert.include(promptTexts[1] ?? "", "T3 Code interaction mode: Default"); assert.equal(watchedSessions.length, 2); assert.equal(watchedSessions[0], watchedSessions[1]); }).pipe(Effect.provide(layerTest), Effect.scoped), diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts new file mode 100644 index 0000000000000..b49a178dd569e --- /dev/null +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts @@ -0,0 +1,119 @@ +import { ProviderDriverKind, type ProviderReplayTranscript } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; + +const sessionId = "session-1"; +const prompt = (text: string) => ({ + type: "expect_outbound" as const, + frame: { + kind: "request", + method: "session/prompt", + params: { sessionId, prompt: [{ type: "text", text }] }, + }, +}); +const promptAnswer = { + type: "emit_inbound" as const, + frame: { kind: "response", method: "session/prompt", result: { stopReason: "end_turn" } }, +}; +const kiroCompact = { + type: "expect_outbound" as const, + frame: { kind: "request", method: "_kiro/session/compact", params: { sessionId } }, +}; +const update = (update: Record) => ({ + type: "emit_inbound" as const, + frame: { kind: "notification", method: "session/update", params: { sessionId, update } }, +}); + +/** Whether each prompt in the materialized transcript ends with T3's runtime notes. */ +const promptsWithNotes = ( + entries: ProviderReplayTranscript["entries"], + driver: "kiro" | "grok" = "kiro", +) => + materializeReplayTranscriptRuntimeInstructions( + { provider: driver, protocol: "acp", version: "1", scenario: "test", entries }, + { driver: ProviderDriverKind.make(driver), model: "auto" }, + ).entries.flatMap((entry) => { + if (entry.type !== "expect_outbound") return []; + const frame = entry.frame as { + readonly method?: string; + readonly params?: { readonly prompt?: ReadonlyArray<{ readonly text?: string }> }; + }; + return frame.method === "session/prompt" + ? [frame.params?.prompt?.at(-1)?.text?.startsWith("") === true] + : []; + }); + +describe("materializeReplayTranscriptRuntimeInstructions", () => { + it.each([ + { + name: "a successful Kiro compaction", + between: [ + kiroCompact, + { + type: "emit_inbound" as const, + frame: { kind: "response", method: "_kiro/session/compact", result: null }, + }, + ], + resend: true, + }, + { + name: "a Kiro compaction the agent refused", + between: [ + kiroCompact, + { + type: "emit_inbound" as const, + frame: { kind: "response", method: "_kiro/session/compact", result: { success: false } }, + }, + ], + resend: false, + }, + { + name: "a failed Kiro compaction", + between: [ + kiroCompact, + { + type: "emit_inbound" as const, + frame: { + kind: "response", + method: "_kiro/session/compact", + error: { code: -32603, message: "failed" }, + }, + }, + ], + resend: false, + }, + { + name: "a completed compaction the agent reports", + between: [ + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), + ], + resend: true, + }, + { + name: "a compaction still in progress", + between: [ + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "in_progress" }), + ], + resend: false, + }, + { + name: "a successful /compact prompt to Grok", + between: [prompt("/compact keep the plan"), promptAnswer], + driver: "grok" as const, + resend: true, + }, + { + name: "a /compact prompt Kiro takes as chat", + between: [prompt("/compact keep the plan"), promptAnswer], + resend: false, + }, + ])("after $name, resend is $resend", ({ between, resend, driver }) => { + const notes = promptsWithNotes( + [prompt("first"), promptAnswer, ...between, prompt("second"), promptAnswer], + driver, + ); + expect(notes.at(0)).toBe(true); + expect(notes.at(-1)).toBe(resend); + }); +}); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index e35d3ecbf919c..f0ff896a156e9 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -20,38 +20,57 @@ export function materializeReplayTranscriptRuntimeInstructions( : undefined; if (harness === undefined) return transcript; const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); + // Grok's flavor sets `supportsCompaction`; Kiro compacts through its own request instead. + const compactsThroughPrompt = runtime.driver === "grok"; // The ACP adapter resends these instructions until a prompt that carried them succeeds. // Cancelled, refused, and failed prompts do not establish delivery. + // Compactions the adapter observes, and closed sessions, reset that. const instructedSessionIds = new Set(); - let pendingInstructedPrompt: { readonly sessionId: unknown } | undefined; + let pendingPrompt: + | { readonly sessionId: unknown; readonly instructed: boolean; readonly compact: boolean } + | undefined; + let pendingKiroCompact: { readonly sessionId: unknown } | undefined; return { ...transcript, entries: transcript.entries.map((entry) => { if (entry.type === "emit_inbound") { - const frame = entry.frame; + const frame = asFrame(entry.frame); + if (frame?.kind === "response" && frame.method === "session/prompt" && pendingPrompt) { + const stopReason = isRecord(frame.result) ? frame.result.stopReason : undefined; + const delivered = + frame.error === undefined && + stopReason !== "cancelled" && + stopReason !== "refusal" && + stopReason !== "error"; + if (delivered && pendingPrompt.compact) { + instructedSessionIds.delete(pendingPrompt.sessionId); + } else if (delivered && pendingPrompt.instructed) { + instructedSessionIds.add(pendingPrompt.sessionId); + } + pendingPrompt = undefined; + } if ( - typeof frame === "object" && - frame !== null && - "kind" in frame && - frame.kind === "response" && - "method" in frame && - frame.method === "session/prompt" && - "result" in frame && - pendingInstructedPrompt !== undefined + frame?.kind === "response" && + frame.method === "_kiro/session/compact" && + pendingKiroCompact ) { - const result = frame.result; - if ( - typeof result !== "object" || - result === null || - !("stopReason" in result) || - (result.stopReason !== "cancelled" && - result.stopReason !== "refusal" && - result.stopReason !== "error") - ) { - instructedSessionIds.add(pendingInstructedPrompt.sessionId); - } - pendingInstructedPrompt = undefined; + const refused = isRecord(frame.result) && frame.result.success === false; + if (frame.error === undefined && !refused) + instructedSessionIds.delete(pendingKiroCompact.sessionId); + pendingKiroCompact = undefined; + } + const update = + frame?.method === "session/update" && isRecord(frame.params) + ? frame.params.update + : undefined; + if ( + isRecord(update) && + update.sessionUpdate === "compaction_update" && + update.status === "completed" && + isRecord(frame?.params) + ) { + instructedSessionIds.delete(frame.params.sessionId); } return entry; } @@ -72,7 +91,7 @@ export function materializeReplayTranscriptRuntimeInstructions( if ("method" in frame && frame.method === "session/close") { // A closed session comes back in a new runtime, which resends the instructions. instructedSessionIds.clear(); - pendingInstructedPrompt = undefined; + pendingPrompt = undefined; return entry; } if ( @@ -83,8 +102,8 @@ export function materializeReplayTranscriptRuntimeInstructions( frame.params !== null && "sessionId" in frame.params ) { - // Like a compaction the agent reports, Kiro's native one makes the next prompt resend them. - instructedSessionIds.delete(frame.params.sessionId); + // Once it succeeds, Kiro's native compaction makes the next prompt resend them. + pendingKiroCompact = { sessionId: frame.params.sessionId }; return entry; } if ( @@ -97,12 +116,24 @@ export function materializeReplayTranscriptRuntimeInstructions( Array.isArray(frame.params.prompt) ) { const promptSessionId = "sessionId" in frame.params ? frame.params.sessionId : undefined; - pendingInstructedPrompt = undefined; - if (instructedSessionIds.has(promptSessionId)) return entry; const prompt: ReadonlyArray = frame.params.prompt; - // Like the adapter, native slash commands go out without instructions. - if (promptPartText(prompt[0])?.trimStart().startsWith("/") === true) return entry; - pendingInstructedPrompt = { sessionId: promptSessionId }; + const firstText = promptPartText(prompt[0])?.trim(); + // Like the adapter, native slash commands go out without instructions. Where the agent + // compacts through the prompt, a successful `/compact` makes the next prompt resend them. + if (firstText?.startsWith("/") === true) { + pendingPrompt = { + sessionId: promptSessionId, + instructed: false, + compact: + compactsThroughPrompt && prompt.length === 1 && /^\/compact(?:\s|$)/.test(firstText), + }; + return entry; + } + if (instructedSessionIds.has(promptSessionId)) { + pendingPrompt = undefined; + return entry; + } + pendingPrompt = { sessionId: promptSessionId, instructed: true, compact: false }; const lastText = promptPartText(prompt.at(-1)); // A recording that switched models mid-thread already carries the // instructions for the model each prompt ran on. @@ -132,3 +163,19 @@ function promptPartText(part: unknown): string | undefined { ? part.text : undefined; } + +type ReplayFrame = { + readonly kind?: unknown; + readonly method?: unknown; + readonly params?: unknown; + readonly result?: unknown; + readonly error?: unknown; +}; + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null; +} + +function asFrame(value: unknown): ReplayFrame | undefined { + return isRecord(value) ? value : undefined; +} diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index dee0c03fc4002..f4979bdce4943 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -8098,6 +8098,11 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( yield* Ref.set(activeSessionSetup, null); yield* Ref.set(activeSelection, null); yield* Ref.set(activeInteractionMode, null); + // A failed prompt may have been dropped, so the fresh runtime sends T3's notes again. + yield* Ref.set(promptInstructionStates, new Map()); + yield* Ref.set(promptRuntimeInstructions, new Map()); + yield* Ref.set(compactionMarkers, new Map()); + yield* Ref.set(completedCompactionIds, new Set()); yield* Ref.set(snapshot, { order: [], messages: new Map(), From 45cb6201a5b55f4f2f429fb65eb53d5571220791 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:38:48 +0200 Subject: [PATCH 17/22] test(acp): the replay helper keeps compactions that finish while a prompt runs A prompt's success no longer records delivery over a compaction that completed after it was sent, a repeated patch of one compaction resets delivery once, a new initialize resets it like session/close, and a Kiro compaction counts only when Kiro answers success: true. Co-Authored-By: Claude Opus 5.5 --- .../testkit/ReplayRuntimeInstructions.test.ts | 48 ++++++++++++++- .../testkit/ReplayRuntimeInstructions.ts | 58 +++++++++++++++---- 2 files changed, 93 insertions(+), 13 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts index b49a178dd569e..86c5de7e05fae 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts @@ -52,7 +52,7 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { kiroCompact, { type: "emit_inbound" as const, - frame: { kind: "response", method: "_kiro/session/compact", result: null }, + frame: { kind: "response", method: "_kiro/session/compact", result: { success: true } }, }, ], resend: true, @@ -90,6 +90,42 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { ], resend: true, }, + { + name: "a Kiro compaction answered without success", + between: [ + kiroCompact, + { + type: "emit_inbound" as const, + frame: { kind: "response", method: "_kiro/session/compact", result: null }, + }, + ], + resend: false, + }, + { + name: "a repeated patch of a compaction already resent for", + between: [ + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), + prompt("middle"), + promptAnswer, + update({ + sessionUpdate: "compaction_update", + compactionId: "c1", + status: "completed", + summary: [{ type: "text", text: "Summary" }], + }), + ], + resend: false, + }, + { + name: "a runtime restart without session/close", + between: [ + { + type: "expect_outbound" as const, + frame: { kind: "request", method: "initialize", params: { protocolVersion: 1 } }, + }, + ], + resend: true, + }, { name: "a compaction still in progress", between: [ @@ -116,4 +152,14 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { expect(notes.at(0)).toBe(true); expect(notes.at(-1)).toBe(resend); }); + it("resends after a compaction that completes while the instructed prompt runs", () => { + const notes = promptsWithNotes([ + prompt("first"), + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), + promptAnswer, + prompt("second"), + promptAnswer, + ]); + expect(notes).toEqual([true, true]); + }); }); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index f0ff896a156e9..3a3eab4b263b1 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -23,11 +23,29 @@ export function materializeReplayTranscriptRuntimeInstructions( // Grok's flavor sets `supportsCompaction`; Kiro compacts through its own request instead. const compactsThroughPrompt = runtime.driver === "grok"; // The ACP adapter resends these instructions until a prompt that carried them succeeds. - // Cancelled, refused, and failed prompts do not establish delivery. - // Compactions the adapter observes, and closed sessions, reset that. + // Cancelled, refused, and failed prompts do not establish delivery. A new runtime and a + // compaction reset it. Copilot's compaction events and compactions routed to a child session + // are not mirrored here; no recording has them yet. const instructedSessionIds = new Set(); + // A prompt's success records delivery only if no compaction completed after it was sent. + const compactionCounts = new Map(); + const completedCompactionIds = new Set(); + const markCompacted = (sessionId: unknown, compactionId?: unknown) => { + if (typeof compactionId === "string") { + const key = `${String(sessionId)}\u0000${compactionId}`; + if (completedCompactionIds.has(key)) return; + completedCompactionIds.add(key); + } + instructedSessionIds.delete(sessionId); + compactionCounts.set(sessionId, (compactionCounts.get(sessionId) ?? 0) + 1); + }; let pendingPrompt: - | { readonly sessionId: unknown; readonly instructed: boolean; readonly compact: boolean } + | { + readonly sessionId: unknown; + readonly instructed: boolean; + readonly compact: boolean; + readonly compactionsAtSend: number; + } | undefined; let pendingKiroCompact: { readonly sessionId: unknown } | undefined; @@ -44,8 +62,12 @@ export function materializeReplayTranscriptRuntimeInstructions( stopReason !== "refusal" && stopReason !== "error"; if (delivered && pendingPrompt.compact) { - instructedSessionIds.delete(pendingPrompt.sessionId); - } else if (delivered && pendingPrompt.instructed) { + markCompacted(pendingPrompt.sessionId); + } else if ( + delivered && + pendingPrompt.instructed && + (compactionCounts.get(pendingPrompt.sessionId) ?? 0) === pendingPrompt.compactionsAtSend + ) { instructedSessionIds.add(pendingPrompt.sessionId); } pendingPrompt = undefined; @@ -55,9 +77,10 @@ export function materializeReplayTranscriptRuntimeInstructions( frame.method === "_kiro/session/compact" && pendingKiroCompact ) { - const refused = isRecord(frame.result) && frame.result.success === false; - if (frame.error === undefined && !refused) - instructedSessionIds.delete(pendingKiroCompact.sessionId); + // Kiro's adapter accepts only `{ success: true }`. + if (isRecord(frame.result) && frame.result.success === true) { + markCompacted(pendingKiroCompact.sessionId); + } pendingKiroCompact = undefined; } const update = @@ -70,7 +93,7 @@ export function materializeReplayTranscriptRuntimeInstructions( update.status === "completed" && isRecord(frame?.params) ) { - instructedSessionIds.delete(frame.params.sessionId); + markCompacted(frame.params.sessionId, update.compactionId); } return entry; } @@ -88,10 +111,15 @@ export function materializeReplayTranscriptRuntimeInstructions( ? entry : { ...entry, frame: { ...frame, message: `${frame.message}\n\n${instructions}` } }; } - if ("method" in frame && frame.method === "session/close") { - // A closed session comes back in a new runtime, which resends the instructions. + if ( + "method" in frame && + (frame.method === "session/close" || frame.method === "initialize") + ) { + // A closed or restarted session comes back in a new runtime, which resends the instructions. instructedSessionIds.clear(); + completedCompactionIds.clear(); pendingPrompt = undefined; + pendingKiroCompact = undefined; return entry; } if ( @@ -126,6 +154,7 @@ export function materializeReplayTranscriptRuntimeInstructions( instructed: false, compact: compactsThroughPrompt && prompt.length === 1 && /^\/compact(?:\s|$)/.test(firstText), + compactionsAtSend: compactionCounts.get(promptSessionId) ?? 0, }; return entry; } @@ -133,7 +162,12 @@ export function materializeReplayTranscriptRuntimeInstructions( pendingPrompt = undefined; return entry; } - pendingPrompt = { sessionId: promptSessionId, instructed: true, compact: false }; + pendingPrompt = { + sessionId: promptSessionId, + instructed: true, + compact: false, + compactionsAtSend: compactionCounts.get(promptSessionId) ?? 0, + }; const lastText = promptPartText(prompt.at(-1)); // A recording that switched models mid-thread already carries the // instructions for the model each prompt ran on. From 2291dadc48f20837d6a703d3979603cf5f75c781 Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:46:49 +0200 Subject: [PATCH 18/22] test(acp): the replay helper resends after a compaction a completed turn settles Co-Authored-By: Claude Opus 5.5 --- .../testkit/ReplayRuntimeInstructions.test.ts | 14 ++++++++++++++ .../testkit/ReplayRuntimeInstructions.ts | 18 ++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts index 86c5de7e05fae..37f896dc28f8a 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts @@ -162,4 +162,18 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { ]); expect(notes).toEqual([true, true]); }); + + it("resends after a turn that completes with a compaction still open, and after its real end", () => { + const notes = promptsWithNotes([ + prompt("first"), + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "in_progress" }), + promptAnswer, + prompt("second"), + promptAnswer, + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), + prompt("third"), + promptAnswer, + ]); + expect(notes).toEqual([true, true, true]); + }); }); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index 3a3eab4b263b1..febf94430306d 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -30,6 +30,8 @@ export function materializeReplayTranscriptRuntimeInstructions( // A prompt's success records delivery only if no compaction completed after it was sent. const compactionCounts = new Map(); const completedCompactionIds = new Set(); + // A turn that completes with a compaction still open projects it as completed. + const openCompactions = new Map>(); const markCompacted = (sessionId: unknown, compactionId?: unknown) => { if (typeof compactionId === "string") { const key = `${String(sessionId)}\u0000${compactionId}`; @@ -61,6 +63,11 @@ export function materializeReplayTranscriptRuntimeInstructions( stopReason !== "cancelled" && stopReason !== "refusal" && stopReason !== "error"; + const open = openCompactions.get(pendingPrompt.sessionId); + if (delivered && open !== undefined && open.size > 0) { + openCompactions.delete(pendingPrompt.sessionId); + markCompacted(pendingPrompt.sessionId); + } if (delivered && pendingPrompt.compact) { markCompacted(pendingPrompt.sessionId); } else if ( @@ -90,10 +97,16 @@ export function materializeReplayTranscriptRuntimeInstructions( if ( isRecord(update) && update.sessionUpdate === "compaction_update" && - update.status === "completed" && isRecord(frame?.params) ) { - markCompacted(frame.params.sessionId, update.compactionId); + const sessionId = frame.params.sessionId; + const open = openCompactions.get(sessionId) ?? new Set(); + if (update.status === "in_progress") { + openCompactions.set(sessionId, open.add(update.compactionId)); + } else { + open.delete(update.compactionId); + } + if (update.status === "completed") markCompacted(sessionId, update.compactionId); } return entry; } @@ -118,6 +131,7 @@ export function materializeReplayTranscriptRuntimeInstructions( // A closed or restarted session comes back in a new runtime, which resends the instructions. instructedSessionIds.clear(); completedCompactionIds.clear(); + openCompactions.clear(); pendingPrompt = undefined; pendingKiroCompact = undefined; return entry; From 89c8622b19c1d1fd5d2323f17a03ba44968d46ad Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Mon, 9 Nov 2026 11:55:52 +0200 Subject: [PATCH 19/22] test(acp): the replay helper tracks the exact notes each session received A note-free turn still settles a compaction it leaves open, and switching back to the first model after a recorded second one resends the first model's notes, as the adapter does. Co-Authored-By: Claude Opus 5.5 --- .../testkit/ReplayRuntimeInstructions.test.ts | 46 ++++++++++++++++--- .../testkit/ReplayRuntimeInstructions.ts | 43 ++++++++--------- 2 files changed, 61 insertions(+), 28 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts index 37f896dc28f8a..ae6a7b0b6a001 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts @@ -4,12 +4,12 @@ import { describe, expect, it } from "vite-plus/test"; import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; const sessionId = "session-1"; -const prompt = (text: string) => ({ +const prompt = (...texts: ReadonlyArray) => ({ type: "expect_outbound" as const, frame: { kind: "request", method: "session/prompt", - params: { sessionId, prompt: [{ type: "text", text }] }, + params: { sessionId, prompt: texts.map((text) => ({ type: "text", text })) }, }, }); const promptAnswer = { @@ -25,8 +25,8 @@ const update = (update: Record) => ({ frame: { kind: "notification", method: "session/update", params: { sessionId, update } }, }); -/** Whether each prompt in the materialized transcript ends with T3's runtime notes. */ -const promptsWithNotes = ( +/** The last text block of each prompt in the materialized transcript. */ +const lastPromptBlocks = ( entries: ProviderReplayTranscript["entries"], driver: "kiro" | "grok" = "kiro", ) => @@ -39,11 +39,15 @@ const promptsWithNotes = ( readonly method?: string; readonly params?: { readonly prompt?: ReadonlyArray<{ readonly text?: string }> }; }; - return frame.method === "session/prompt" - ? [frame.params?.prompt?.at(-1)?.text?.startsWith("") === true] - : []; + return frame.method === "session/prompt" ? [frame.params?.prompt?.at(-1)?.text ?? ""] : []; }); +/** Whether each prompt in the materialized transcript ends with T3's runtime notes. */ +const promptsWithNotes = ( + entries: ProviderReplayTranscript["entries"], + driver: "kiro" | "grok" = "kiro", +) => lastPromptBlocks(entries, driver).map((text) => text.startsWith("")); + describe("materializeReplayTranscriptRuntimeInstructions", () => { it.each([ { @@ -176,4 +180,32 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { ]); expect(notes).toEqual([true, true, true]); }); + + it("resends after a compaction that a later, note-free turn leaves open", () => { + const notes = promptsWithNotes([ + prompt("first"), + promptAnswer, + prompt("second"), + update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "in_progress" }), + promptAnswer, + prompt("third"), + promptAnswer, + ]); + expect(notes).toEqual([true, false, true]); + }); + + it("resends the first model's notes after switching back from a recorded second model", () => { + const otherModel = "Other model notes"; + const [onA, onB, backOnA] = lastPromptBlocks([ + prompt("on A"), + promptAnswer, + prompt("on B", otherModel), + promptAnswer, + prompt("back on A"), + promptAnswer, + ]); + expect(onA).toMatch(/^/); + expect(onB).toBe(otherModel); + expect(backOnA).toBe(onA); + }); }); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index febf94430306d..d6f23d42b5b0d 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -22,11 +22,11 @@ export function materializeReplayTranscriptRuntimeInstructions( const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); // Grok's flavor sets `supportsCompaction`; Kiro compacts through its own request instead. const compactsThroughPrompt = runtime.driver === "grok"; - // The ACP adapter resends these instructions until a prompt that carried them succeeds. - // Cancelled, refused, and failed prompts do not establish delivery. A new runtime and a - // compaction reset it. Copilot's compaction events and compactions routed to a child session + // The ACP adapter resends the runtime notes until a prompt that carried that exact text + // succeeds. Cancelled, refused, and failed prompts do not establish delivery. A new runtime and + // a compaction reset it. Copilot's compaction events and compactions routed to a child session // are not mirrored here; no recording has them yet. - const instructedSessionIds = new Set(); + const deliveredNotes = new Map(); // A prompt's success records delivery only if no compaction completed after it was sent. const compactionCounts = new Map(); const completedCompactionIds = new Set(); @@ -38,13 +38,14 @@ export function materializeReplayTranscriptRuntimeInstructions( if (completedCompactionIds.has(key)) return; completedCompactionIds.add(key); } - instructedSessionIds.delete(sessionId); + deliveredNotes.delete(sessionId); compactionCounts.set(sessionId, (compactionCounts.get(sessionId) ?? 0) + 1); }; let pendingPrompt: | { readonly sessionId: unknown; - readonly instructed: boolean; + /** The runtime notes the prompt carried, if any. */ + readonly notes: string | undefined; readonly compact: boolean; readonly compactionsAtSend: number; } @@ -72,10 +73,10 @@ export function materializeReplayTranscriptRuntimeInstructions( markCompacted(pendingPrompt.sessionId); } else if ( delivered && - pendingPrompt.instructed && + pendingPrompt.notes !== undefined && (compactionCounts.get(pendingPrompt.sessionId) ?? 0) === pendingPrompt.compactionsAtSend ) { - instructedSessionIds.add(pendingPrompt.sessionId); + deliveredNotes.set(pendingPrompt.sessionId, pendingPrompt.notes); } pendingPrompt = undefined; } @@ -129,7 +130,7 @@ export function materializeReplayTranscriptRuntimeInstructions( (frame.method === "session/close" || frame.method === "initialize") ) { // A closed or restarted session comes back in a new runtime, which resends the instructions. - instructedSessionIds.clear(); + deliveredNotes.clear(); completedCompactionIds.clear(); openCompactions.clear(); pendingPrompt = undefined; @@ -165,29 +166,29 @@ export function materializeReplayTranscriptRuntimeInstructions( if (firstText?.startsWith("/") === true) { pendingPrompt = { sessionId: promptSessionId, - instructed: false, + notes: undefined, compact: compactsThroughPrompt && prompt.length === 1 && /^\/compact(?:\s|$)/.test(firstText), compactionsAtSend: compactionCounts.get(promptSessionId) ?? 0, }; return entry; } - if (instructedSessionIds.has(promptSessionId)) { - pendingPrompt = undefined; - return entry; - } + const lastText = promptPartText(prompt.at(-1)); + // A recording that switched models mid-thread already carries the + // instructions for the model each prompt ran on. + const recordedNotes = + lastText === instructions || lastText?.startsWith("") === true + ? lastText + : undefined; + const resend = + recordedNotes === undefined && deliveredNotes.get(promptSessionId) !== instructions; pendingPrompt = { sessionId: promptSessionId, - instructed: true, + notes: recordedNotes ?? (resend ? instructions : undefined), compact: false, compactionsAtSend: compactionCounts.get(promptSessionId) ?? 0, }; - const lastText = promptPartText(prompt.at(-1)); - // A recording that switched models mid-thread already carries the - // instructions for the model each prompt ran on. - if (lastText === instructions || lastText?.startsWith("") === true) { - return entry; - } + if (!resend) return entry; return { ...entry, frame: { From 1cc7db1bd4988eeae1ba9bfbfa4198871a5aa07b Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:07:30 +0200 Subject: [PATCH 20/22] test(acp): the replay helper expects the notes for each session's current model Co-Authored-By: Claude Opus 5.5 --- .../testkit/ReplayRuntimeInstructions.test.ts | 23 +++++++++++---- .../testkit/ReplayRuntimeInstructions.ts | 28 ++++++++++++++++--- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts index ae6a7b0b6a001..f4adc052db45a 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts @@ -20,6 +20,14 @@ const kiroCompact = { type: "expect_outbound" as const, frame: { kind: "request", method: "_kiro/session/compact", params: { sessionId } }, }; +const selectModel = (value: string) => ({ + type: "expect_outbound" as const, + frame: { + kind: "request", + method: "session/set_config_option", + params: { sessionId, configId: "model", value }, + }, +}); const update = (update: Record) => ({ type: "emit_inbound" as const, frame: { kind: "notification", method: "session/update", params: { sessionId, update } }, @@ -194,18 +202,23 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { expect(notes).toEqual([true, false, true]); }); - it("resends the first model's notes after switching back from a recorded second model", () => { - const otherModel = "Other model notes"; - const [onA, onB, backOnA] = lastPromptBlocks([ + it("sends each model's notes once, and the first model's again after switching back", () => { + const [onA, onB, againOnB, backOnA] = lastPromptBlocks([ prompt("on A"), promptAnswer, - prompt("on B", otherModel), + selectModel("model-b"), + prompt("on B"), + promptAnswer, + prompt("again on B"), promptAnswer, + selectModel("auto"), prompt("back on A"), promptAnswer, ]); expect(onA).toMatch(/^/); - expect(onB).toBe(otherModel); + expect(onB).toMatch(/^/); + expect(onB).toContain("model-b"); + expect(againOnB).toBe("again on B"); expect(backOnA).toBe(onA); }); }); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index d6f23d42b5b0d..04b84486cd628 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -20,6 +20,12 @@ export function materializeReplayTranscriptRuntimeInstructions( : undefined; if (harness === undefined) return transcript; const instructions = buildRuntimeInstructions({ harness, model: runtime.model }); + // The notes name the model, so each prompt expects the ones for its session's current model. + const sessionModels = new Map(); + const notesFor = (sessionId: unknown) => { + const model = sessionModels.get(sessionId) ?? runtime.model; + return model === runtime.model ? instructions : buildRuntimeInstructions({ harness, model }); + }; // Grok's flavor sets `supportsCompaction`; Kiro compacts through its own request instead. const compactsThroughPrompt = runtime.driver === "grok"; // The ACP adapter resends the runtime notes until a prompt that carried that exact text @@ -137,6 +143,19 @@ export function materializeReplayTranscriptRuntimeInstructions( pendingKiroCompact = undefined; return entry; } + const params = "params" in frame && isRecord(frame.params) ? frame.params : undefined; + const selectedModel = + "method" in frame && frame.method === "session/set_config_option" + ? params?.configId === "model" + ? params.value + : undefined + : "method" in frame && frame.method === "session/set_model" + ? params?.modelId + : undefined; + if (typeof selectedModel === "string") { + sessionModels.set(params?.sessionId, selectedModel); + return entry; + } if ( "method" in frame && frame.method === "_kiro/session/compact" && @@ -174,17 +193,18 @@ export function materializeReplayTranscriptRuntimeInstructions( return entry; } const lastText = promptPartText(prompt.at(-1)); + const expected = notesFor(promptSessionId); // A recording that switched models mid-thread already carries the // instructions for the model each prompt ran on. const recordedNotes = - lastText === instructions || lastText?.startsWith("") === true + lastText === expected || lastText?.startsWith("") === true ? lastText : undefined; const resend = - recordedNotes === undefined && deliveredNotes.get(promptSessionId) !== instructions; + recordedNotes === undefined && deliveredNotes.get(promptSessionId) !== expected; pendingPrompt = { sessionId: promptSessionId, - notes: recordedNotes ?? (resend ? instructions : undefined), + notes: recordedNotes ?? (resend ? expected : undefined), compact: false, compactionsAtSend: compactionCounts.get(promptSessionId) ?? 0, }; @@ -193,7 +213,7 @@ export function materializeReplayTranscriptRuntimeInstructions( ...entry, frame: { ...frame, - params: { ...frame.params, prompt: [...prompt, { type: "text", text: instructions }] }, + params: { ...frame.params, prompt: [...prompt, { type: "text", text: expected }] }, }, }; } From 6e454273206532d989ad691492ab20c8429f2d0e Mon Sep 17 00:00:00 2001 From: Radu-Stefan Popa <150839549+Radu028@users.noreply.github.com> Date: Mon, 9 Nov 2026 12:17:27 +0200 Subject: [PATCH 21/22] test(acp): the replay helper counts a recorded notes block as the current model's Recorders normalize the notes they keep for another model, so their text never matches the generated notes exactly. Co-Authored-By: Claude Opus 5.5 --- .../testkit/ReplayRuntimeInstructions.test.ts | 4 ++-- .../testkit/ReplayRuntimeInstructions.ts | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts index f4adc052db45a..b22a5dd09e670 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts @@ -207,7 +207,8 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { prompt("on A"), promptAnswer, selectModel("model-b"), - prompt("on B"), + // As the Kiro recorder keeps another model's notes: normalized, not the exact text. + prompt("on B", "\n\n"), promptAnswer, prompt("again on B"), promptAnswer, @@ -217,7 +218,6 @@ describe("materializeReplayTranscriptRuntimeInstructions", () => { ]); expect(onA).toMatch(/^/); expect(onB).toMatch(/^/); - expect(onB).toContain("model-b"); expect(againOnB).toBe("again on B"); expect(backOnA).toBe(onA); }); diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts index 04b84486cd628..e594a5ab5003a 100644 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts +++ b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.ts @@ -28,10 +28,9 @@ export function materializeReplayTranscriptRuntimeInstructions( }; // Grok's flavor sets `supportsCompaction`; Kiro compacts through its own request instead. const compactsThroughPrompt = runtime.driver === "grok"; - // The ACP adapter resends the runtime notes until a prompt that carried that exact text - // succeeds. Cancelled, refused, and failed prompts do not establish delivery. A new runtime and - // a compaction reset it. Copilot's compaction events and compactions routed to a child session - // are not mirrored here; no recording has them yet. + // Like the adapter: notes go out until a prompt carrying that text succeeds, and a new runtime + // or compaction resets that. Not mirrored, as no recording has them: Copilot compaction events, + // child-session compactions, and ACP v2 prompts that settle through an idle `state_update`. const deliveredNotes = new Map(); // A prompt's success records delivery only if no compaction completed after it was sent. const compactionCounts = new Map(); @@ -204,7 +203,8 @@ export function materializeReplayTranscriptRuntimeInstructions( recordedNotes === undefined && deliveredNotes.get(promptSessionId) !== expected; pendingPrompt = { sessionId: promptSessionId, - notes: recordedNotes ?? (resend ? expected : undefined), + // Recorders normalize the notes they keep, so a recorded block stands for this model's. + notes: recordedNotes !== undefined || resend ? expected : undefined, compact: false, compactionsAtSend: compactionCounts.get(promptSessionId) ?? 0, }; From ff3455b43fe2bcc1f9e4174529e252f65689fc29 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 12 Nov 2026 21:12:33 +0000 Subject: [PATCH 22/22] chore(acp): keep the runtime notes change to its scope Compaction patch semantics in the runtime model change how compaction items display, which is a separate fix. The unit test of the replay helper is dropped; the replay fixture suites exercise it against real recordings. AI-assisted; verified with the tests and tools listed above. --- .../testkit/ReplayRuntimeInstructions.test.ts | 224 ------------------ packages/provider-acp/src/server/adapter.ts | 2 +- .../src/server/runtimeModel.test.ts | 53 ----- .../provider-acp/src/server/runtimeModel.ts | 15 +- 4 files changed, 3 insertions(+), 291 deletions(-) delete mode 100644 apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts diff --git a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts b/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts deleted file mode 100644 index b22a5dd09e670..0000000000000 --- a/apps/server/src/orchestration-v2/testkit/ReplayRuntimeInstructions.test.ts +++ /dev/null @@ -1,224 +0,0 @@ -import { ProviderDriverKind, type ProviderReplayTranscript } from "@t3tools/contracts"; -import { describe, expect, it } from "vite-plus/test"; - -import { materializeReplayTranscriptRuntimeInstructions } from "./ReplayRuntimeInstructions.ts"; - -const sessionId = "session-1"; -const prompt = (...texts: ReadonlyArray) => ({ - type: "expect_outbound" as const, - frame: { - kind: "request", - method: "session/prompt", - params: { sessionId, prompt: texts.map((text) => ({ type: "text", text })) }, - }, -}); -const promptAnswer = { - type: "emit_inbound" as const, - frame: { kind: "response", method: "session/prompt", result: { stopReason: "end_turn" } }, -}; -const kiroCompact = { - type: "expect_outbound" as const, - frame: { kind: "request", method: "_kiro/session/compact", params: { sessionId } }, -}; -const selectModel = (value: string) => ({ - type: "expect_outbound" as const, - frame: { - kind: "request", - method: "session/set_config_option", - params: { sessionId, configId: "model", value }, - }, -}); -const update = (update: Record) => ({ - type: "emit_inbound" as const, - frame: { kind: "notification", method: "session/update", params: { sessionId, update } }, -}); - -/** The last text block of each prompt in the materialized transcript. */ -const lastPromptBlocks = ( - entries: ProviderReplayTranscript["entries"], - driver: "kiro" | "grok" = "kiro", -) => - materializeReplayTranscriptRuntimeInstructions( - { provider: driver, protocol: "acp", version: "1", scenario: "test", entries }, - { driver: ProviderDriverKind.make(driver), model: "auto" }, - ).entries.flatMap((entry) => { - if (entry.type !== "expect_outbound") return []; - const frame = entry.frame as { - readonly method?: string; - readonly params?: { readonly prompt?: ReadonlyArray<{ readonly text?: string }> }; - }; - return frame.method === "session/prompt" ? [frame.params?.prompt?.at(-1)?.text ?? ""] : []; - }); - -/** Whether each prompt in the materialized transcript ends with T3's runtime notes. */ -const promptsWithNotes = ( - entries: ProviderReplayTranscript["entries"], - driver: "kiro" | "grok" = "kiro", -) => lastPromptBlocks(entries, driver).map((text) => text.startsWith("")); - -describe("materializeReplayTranscriptRuntimeInstructions", () => { - it.each([ - { - name: "a successful Kiro compaction", - between: [ - kiroCompact, - { - type: "emit_inbound" as const, - frame: { kind: "response", method: "_kiro/session/compact", result: { success: true } }, - }, - ], - resend: true, - }, - { - name: "a Kiro compaction the agent refused", - between: [ - kiroCompact, - { - type: "emit_inbound" as const, - frame: { kind: "response", method: "_kiro/session/compact", result: { success: false } }, - }, - ], - resend: false, - }, - { - name: "a failed Kiro compaction", - between: [ - kiroCompact, - { - type: "emit_inbound" as const, - frame: { - kind: "response", - method: "_kiro/session/compact", - error: { code: -32603, message: "failed" }, - }, - }, - ], - resend: false, - }, - { - name: "a completed compaction the agent reports", - between: [ - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), - ], - resend: true, - }, - { - name: "a Kiro compaction answered without success", - between: [ - kiroCompact, - { - type: "emit_inbound" as const, - frame: { kind: "response", method: "_kiro/session/compact", result: null }, - }, - ], - resend: false, - }, - { - name: "a repeated patch of a compaction already resent for", - between: [ - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), - prompt("middle"), - promptAnswer, - update({ - sessionUpdate: "compaction_update", - compactionId: "c1", - status: "completed", - summary: [{ type: "text", text: "Summary" }], - }), - ], - resend: false, - }, - { - name: "a runtime restart without session/close", - between: [ - { - type: "expect_outbound" as const, - frame: { kind: "request", method: "initialize", params: { protocolVersion: 1 } }, - }, - ], - resend: true, - }, - { - name: "a compaction still in progress", - between: [ - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "in_progress" }), - ], - resend: false, - }, - { - name: "a successful /compact prompt to Grok", - between: [prompt("/compact keep the plan"), promptAnswer], - driver: "grok" as const, - resend: true, - }, - { - name: "a /compact prompt Kiro takes as chat", - between: [prompt("/compact keep the plan"), promptAnswer], - resend: false, - }, - ])("after $name, resend is $resend", ({ between, resend, driver }) => { - const notes = promptsWithNotes( - [prompt("first"), promptAnswer, ...between, prompt("second"), promptAnswer], - driver, - ); - expect(notes.at(0)).toBe(true); - expect(notes.at(-1)).toBe(resend); - }); - it("resends after a compaction that completes while the instructed prompt runs", () => { - const notes = promptsWithNotes([ - prompt("first"), - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), - promptAnswer, - prompt("second"), - promptAnswer, - ]); - expect(notes).toEqual([true, true]); - }); - - it("resends after a turn that completes with a compaction still open, and after its real end", () => { - const notes = promptsWithNotes([ - prompt("first"), - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "in_progress" }), - promptAnswer, - prompt("second"), - promptAnswer, - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "completed" }), - prompt("third"), - promptAnswer, - ]); - expect(notes).toEqual([true, true, true]); - }); - - it("resends after a compaction that a later, note-free turn leaves open", () => { - const notes = promptsWithNotes([ - prompt("first"), - promptAnswer, - prompt("second"), - update({ sessionUpdate: "compaction_update", compactionId: "c1", status: "in_progress" }), - promptAnswer, - prompt("third"), - promptAnswer, - ]); - expect(notes).toEqual([true, false, true]); - }); - - it("sends each model's notes once, and the first model's again after switching back", () => { - const [onA, onB, againOnB, backOnA] = lastPromptBlocks([ - prompt("on A"), - promptAnswer, - selectModel("model-b"), - // As the Kiro recorder keeps another model's notes: normalized, not the exact text. - prompt("on B", "\n\n"), - promptAnswer, - prompt("again on B"), - promptAnswer, - selectModel("auto"), - prompt("back on A"), - promptAnswer, - ]); - expect(onA).toMatch(/^/); - expect(onB).toMatch(/^/); - expect(againOnB).toBe("again on B"); - expect(backOnA).toBe(onA); - }); -}); diff --git a/packages/provider-acp/src/server/adapter.ts b/packages/provider-acp/src/server/adapter.ts index d07d3fda38f54..80daa1442f44e 100644 --- a/packages/provider-acp/src/server/adapter.ts +++ b/packages/provider-acp/src/server/adapter.ts @@ -4194,7 +4194,7 @@ export const makeAcpAdapterV2 = Effect.fn("makeAcpAdapterV2")(function* ( }); let turnItem: OrchestrationV2TurnItem; if (toolCall.toolCallId.startsWith("acp-compaction:")) { - const summary = textFromUnknown(toolCall.data.error ?? rawOutput); + const summary = textFromUnknown(rawOutput); turnItem = { ...base, type: "compaction", diff --git a/packages/provider-acp/src/server/runtimeModel.test.ts b/packages/provider-acp/src/server/runtimeModel.test.ts index ee61880804a4f..5b0e3327d9819 100644 --- a/packages/provider-acp/src/server/runtimeModel.test.ts +++ b/packages/provider-acp/src/server/runtimeModel.test.ts @@ -1048,59 +1048,6 @@ describe("AcpRuntimeModel", () => { }, }); expect(idle.events).toEqual([]); - - // Later updates patch fields: omission keeps a value, null or [] clears it. - let patched: AcpToolCallState | undefined; - const patchCompaction = ( - patch: Omit< - Extract, - "sessionUpdate" | "compactionId" - >, - ) => { - const event = parseSessionUpdateEvent({ - sessionId: "session-1", - update: { sessionUpdate: "compaction_update", compactionId: "compact-2", ...patch }, - }).events[0]; - if (event?._tag !== "ToolCallUpdated") throw new Error("expected a compaction tool update"); - patched = mergeToolCallState(patched, event.toolCall); - const { rawOutput, error, meta } = patched.data; - return { summary: rawOutput, error, meta }; - }; - const kept = [{ type: "text" as const, text: "Kept the plan." }]; - expect( - patchCompaction({ status: "completed", summary: kept, _meta: { source: "agent" } }), - ).toEqual({ summary: "Kept the plan.", meta: { source: "agent" } }); - expect(patchCompaction({ status: "completed" })).toEqual({ - summary: "Kept the plan.", - meta: { source: "agent" }, - }); - expect(patchCompaction({ status: "completed", summary: null, _meta: null })).toEqual({ - summary: null, - meta: null, - }); - expect(patchCompaction({ status: "completed", summary: kept })).toEqual({ - summary: "Kept the plan.", - meta: null, - }); - expect(patchCompaction({ status: "completed", summary: [] })).toEqual({ - summary: null, - meta: null, - }); - expect(patchCompaction({ status: "failed", error: "Context is still too large." })).toEqual({ - summary: null, - error: "Context is still too large.", - meta: null, - }); - expect(patchCompaction({ status: "failed" })).toEqual({ - summary: null, - error: "Context is still too large.", - meta: null, - }); - expect(patchCompaction({ status: "failed", error: null })).toEqual({ - summary: null, - error: null, - meta: null, - }); }); it("applies ACP v2 agent-owned terminal snapshots and output chunks", () => { diff --git a/packages/provider-acp/src/server/runtimeModel.ts b/packages/provider-acp/src/server/runtimeModel.ts index 0304684fcb734..6b9f0a01dde43 100644 --- a/packages/provider-acp/src/server/runtimeModel.ts +++ b/packages/provider-acp/src/server/runtimeModel.ts @@ -1556,27 +1556,16 @@ export function parseSessionUpdateEvent(params: EffectAcpSchema.SessionNotificat : upd.status === "failed" || upd.status === "cancelled" ? ("failed" as const) : ("inProgress" as const); - // ACP patch semantics: an omitted field keeps its stored value; null (or an empty - // summary) clears it. The merge keeps any data key this update leaves out. const toolCall = makeToolCallState({ toolCallId: `acp-compaction:${upd.compactionId}`, title: "Compact context", kind: "think", status, - ...("summary" in upd ? { rawOutput: summary || null } : {}), + rawOutput: (upd.error ?? summary) || undefined, _meta: upd._meta, }); if (toolCall !== undefined) { - const data = { - ...toolCall.data, - ...("error" in upd ? { error: upd.error ?? null } : {}), - ...(upd._meta === null ? { meta: null } : {}), - }; - events.push({ - _tag: "ToolCallUpdated", - toolCall: { ...toolCall, data }, - rawPayload: params, - }); + events.push({ _tag: "ToolCallUpdated", toolCall, rawPayload: params }); } break; }