Merge 519c1d3406e7c83234cf26ae76eaf5f46ba3bcfd into 5e417b44e1540f528d2ae63e3e20229a902d1db2

This commit is contained in:
Alexander Ramirez Kiriushenko 2026-03-21 02:54:03 +00:00 committed by GitHub
commit 6c88d997f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 106 additions and 1 deletions

View File

@ -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",

View File

@ -65,6 +65,28 @@ function resolveStructuredPromptPayload(parsed: Record<string, unknown>): {
}
}
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 {

View File

@ -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();

View File

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