diff --git a/extensions/acpx/src/runtime-internals/events.test.ts b/extensions/acpx/src/runtime-internals/events.test.ts index bb8067c3327..6358cf339ca 100644 --- a/extensions/acpx/src/runtime-internals/events.test.ts +++ b/extensions/acpx/src/runtime-internals/events.test.ts @@ -67,6 +67,37 @@ describe("parsePromptEventLine", () => { }); }); + it("parses JSON-RPC result stopReason as done", () => { + const line = JSON.stringify({ + jsonrpc: "2.0", + id: 3, + result: { + stopReason: "end_turn", + }, + }); + expect(parsePromptEventLine(line)).toEqual({ + type: "done", + stopReason: "end_turn", + }); + }); + + it("parses JSON-RPC errors as runtime errors", () => { + const line = JSON.stringify({ + jsonrpc: "2.0", + id: 2, + error: { + code: -32002, + message: "Resource not found", + }, + }); + expect(parsePromptEventLine(line)).toEqual({ + type: "error", + message: "Resource not found", + code: undefined, + retryable: undefined, + }); + }); + it("keeps compatibility with simplified text/done lines", () => { expect(parsePromptEventLine(JSON.stringify({ type: "text", content: "alpha" }))).toEqual({ type: "text_delta", diff --git a/extensions/acpx/src/runtime-internals/events.ts b/extensions/acpx/src/runtime-internals/events.ts index ac5f91acd5a..eef615dae02 100644 --- a/extensions/acpx/src/runtime-internals/events.ts +++ b/extensions/acpx/src/runtime-internals/events.ts @@ -65,6 +65,28 @@ function resolveStructuredPromptPayload(parsed: Record): { } } + const result = parsed.result; + if (isRecord(result)) { + const stopReason = asTrimmedString(result.stopReason); + if (stopReason) { + return { + type: "done", + payload: result, + }; + } + } + + const rpcError = parsed.error; + if (isRecord(rpcError)) { + const message = asTrimmedString(rpcError.message); + if (message) { + return { + type: "error", + payload: rpcError, + }; + } + } + const sessionUpdate = asOptionalString(parsed.sessionUpdate) as AcpSessionUpdateTag | undefined; if (sessionUpdate) { return { diff --git a/src/acp/translator.session-rate-limit.test.ts b/src/acp/translator.session-rate-limit.test.ts index 566b61a5027..be8a39a31f7 100644 --- a/src/acp/translator.session-rate-limit.test.ts +++ b/src/acp/translator.session-rate-limit.test.ts @@ -91,12 +91,28 @@ function createToolEvent(params: { } as unknown as EventFrame; } -function createChatFinalEvent(sessionKey: string): EventFrame { +function createChatFinalEvent( + sessionKey: string, + params: { + text?: string; + stopReason?: string; + } = {}, +): EventFrame { return { event: "chat", payload: { sessionKey, state: "final", + ...(params.stopReason ? { stopReason: params.stopReason } : {}), + ...(params.text + ? { + message: { + role: "assistant", + content: [{ type: "text", text: params.text }], + timestamp: Date.now(), + }, + } + : {}), }, } as unknown as EventFrame; } @@ -881,6 +897,39 @@ describe("acp tool streaming bridge behavior", () => { }); describe("acp session metadata and usage updates", () => { + it("flushes final assistant text from chat.final message payload before resolving prompt", async () => { + const sessionStore = createInMemorySessionStore(); + const connection = createAcpConnection(); + const sessionUpdate = connection.__sessionUpdateMock; + const request = vi.fn(async (method: string) => { + if (method === "chat.send") { + return new Promise(() => {}); + } + return { ok: true }; + }) as GatewayClient["request"]; + const agent = new AcpGatewayAgent(connection, createAcpGateway(request), { + sessionStore, + }); + + await agent.loadSession(createLoadSessionRequest("usage-session")); + sessionUpdate.mockClear(); + + const promptPromise = agent.prompt(createPromptRequest("usage-session", "hello")); + + await agent.handleGatewayEvent(createChatFinalEvent("usage-session", { text: "final answer" })); + + await expect(promptPromise).resolves.toEqual({ stopReason: "end_turn" }); + expect(sessionUpdate).toHaveBeenCalledWith({ + sessionId: "usage-session", + update: { + sessionUpdate: "agent_message_chunk", + content: { type: "text", text: "final answer" }, + }, + }); + + sessionStore.clearAllSessionsForTest(); + }); + it("emits a fresh usage snapshot after prompt completion when gateway totals are available", async () => { const sessionStore = createInMemorySessionStore(); const connection = createAcpConnection(); diff --git a/src/acp/translator.ts b/src/acp/translator.ts index 8ab1f821fc8..061fe8b72f1 100644 --- a/src/acp/translator.ts +++ b/src/acp/translator.ts @@ -812,6 +812,9 @@ export class AcpGatewayAgent implements Agent { } if (state === "final") { + if (messageData) { + await this.handleDeltaEvent(pending.sessionId, messageData); + } const rawStopReason = payload.stopReason as string | undefined; const stopReason: StopReason = rawStopReason === "max_tokens" ? "max_tokens" : "end_turn"; await this.finishPrompt(pending.sessionId, pending, stopReason);