From 742c005ac8bc4a2fb47a8d6654edda8c8ff35272 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 22 Mar 2026 19:42:53 -0700 Subject: [PATCH] fix(acp): preserve hidden thought chunks from gateway chat --- src/acp/translator.cancel-scoping.test.ts | 43 +++++++++++++++++++++++ src/acp/translator.ts | 38 +++++++++++++++++--- 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/src/acp/translator.cancel-scoping.test.ts b/src/acp/translator.cancel-scoping.test.ts index e862222f7a0..12f015008f2 100644 --- a/src/acp/translator.cancel-scoping.test.ts +++ b/src/acp/translator.cancel-scoping.test.ts @@ -194,6 +194,49 @@ describe("acp translator cancel and run scoping", () => { await expect(pending.promptPromise).resolves.toEqual({ stopReason: "end_turn" }); }); + it("projects gateway thinking blocks into hidden ACP thought chunks", async () => { + const sessionKey = "agent:main:shared"; + const harness = createHarness([{ sessionId: "session-1", sessionKey }]); + const pending = await startPendingPrompt(harness, "session-1"); + harness.sessionUpdateSpy.mockClear(); + + await harness.agent.handleGatewayEvent( + createChatEvent({ + runId: pending.runId, + sessionKey, + seq: 1, + state: "delta", + message: { + content: [ + { type: "thinking", thinking: "Internal loop about NO_REPLY" }, + { type: "text", text: "Final visible reply" }, + ], + }, + }), + ); + + expect(harness.sessionUpdateSpy).toHaveBeenNthCalledWith( + 1, + expect.objectContaining({ + sessionId: "session-1", + update: expect.objectContaining({ + sessionUpdate: "agent_thought_chunk", + content: { type: "text", text: "Internal loop about NO_REPLY" }, + }), + }), + ); + expect(harness.sessionUpdateSpy).toHaveBeenNthCalledWith( + 2, + expect.objectContaining({ + sessionId: "session-1", + update: expect.objectContaining({ + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: "Final visible reply" }, + }), + }), + ); + }); + it("drops tool events when runId does not match the active prompt", async () => { const sessionKey = "agent:main:shared"; const harness = createHarness([{ sessionId: "session-1", sessionKey }]); diff --git a/src/acp/translator.ts b/src/acp/translator.ts index 8ab1f821fc8..d275913800e 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -68,6 +68,8 @@ type PendingPrompt = { reject: (err: Error) => void; sentTextLength?: number; sentText?: string; + sentThoughtLength?: number; + sentThought?: string; toolCalls?: Map; }; @@ -126,6 +128,12 @@ type GatewayTranscriptMessage = { content?: unknown; }; +type GatewayChatContentBlock = { + type?: string; + text?: string; + thinking?: string; +}; + const SESSION_CREATE_RATE_LIMIT_DEFAULT_MAX_REQUESTS = 120; const SESSION_CREATE_RATE_LIMIT_DEFAULT_WINDOW_MS = 10_000; @@ -834,22 +842,44 @@ export class AcpGatewayAgent implements Agent { sessionId: string, messageData: Record, ): Promise { - const content = messageData.content as Array<{ type: string; text?: string }> | undefined; - const fullText = content?.find((c) => c.type === "text")?.text ?? ""; + const content = messageData.content as GatewayChatContentBlock[] | undefined; const pending = this.pendingPrompts.get(sessionId); if (!pending) { return; } + const fullThought = content + ?.filter((block) => block?.type === "thinking") + .map((block) => block.thinking ?? "") + .join("\n") + .trimEnd(); + const sentThoughtSoFar = pending.sentThoughtLength ?? 0; + if (fullThought && fullThought.length > sentThoughtSoFar) { + const newThought = fullThought.slice(sentThoughtSoFar); + pending.sentThoughtLength = fullThought.length; + pending.sentThought = fullThought; + await this.connection.sessionUpdate({ + sessionId, + update: { + sessionUpdate: "agent_thought_chunk", + content: { type: "text", text: newThought }, + }, + }); + } + + const fullText = content + ?.filter((block) => block?.type === "text") + .map((block) => block.text ?? "") + .join("\n") + .trimEnd(); const sentSoFar = pending.sentTextLength ?? 0; - if (fullText.length <= sentSoFar) { + if (!fullText || fullText.length <= sentSoFar) { return; } const newText = fullText.slice(sentSoFar); pending.sentTextLength = fullText.length; pending.sentText = fullText; - await this.connection.sessionUpdate({ sessionId, update: {