fix(acp): preserve hidden thought chunks from gateway chat

This commit is contained in:
Vincent Koc
2026-03-22 19:42:53 -07:00
parent a83b7bca15
commit 742c005ac8
2 changed files with 77 additions and 4 deletions

View File

@@ -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 }]);

View File

@@ -68,6 +68,8 @@ type PendingPrompt = {
reject: (err: Error) => void;
sentTextLength?: number;
sentText?: string;
sentThoughtLength?: number;
sentThought?: string;
toolCalls?: Map<string, PendingToolCall>;
};
@@ -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<string, unknown>,
): Promise<void> {
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: {