diff --git a/packages/poe-acp-client/README.md b/packages/poe-acp-client/README.md index d8f9e49f1..26d8c7f02 100644 --- a/packages/poe-acp-client/README.md +++ b/packages/poe-acp-client/README.md @@ -27,6 +27,10 @@ const update = parseSessionUpdate({ - Run-report helpers: `generateRunReportFromSessionUpdateStream`, `formatRunReportSummary`, and `saveRunReport`. - ACP protocol types and error-code helpers. +## Validation + +ACP helpers validate protocol boundaries before forwarding data to callers. Prompt responses must use one of the supported stop reasons: `completed`, `cancelled`, `max_tokens`, or `end_turn`. + ## Config Options This package does not load a config file. Runtime behavior is controlled by diff --git a/packages/poe-acp-client/src/acp-client-unified.test.ts b/packages/poe-acp-client/src/acp-client-unified.test.ts index 813e7d1a8..b274066e4 100644 --- a/packages/poe-acp-client/src/acp-client-unified.test.ts +++ b/packages/poe-acp-client/src/acp-client-unified.test.ts @@ -1981,6 +1981,19 @@ describe("AcpClient", () => { await expect(turn.response).rejects.toThrow(/stopReason/i); }); + it("accepts end_turn prompt responses from ACP agents", async () => { + const { transport, sendRequestMock } = createTransportMock(); + sendRequestMock + .mockResolvedValueOnce({ protocolVersion: 1 } satisfies InitializeResponse) + .mockResolvedValueOnce({ stopReason: "end_turn" } satisfies PromptResponse); + const client = new AcpClient({ transport, protocolVersion: 1 }); + await client.initialize(); + + const turn = client.prompt("session-1", [{ type: "text", text: "Hello agent" }]); + + await expect(turn.response).resolves.toEqual({ stopReason: "end_turn" }); + }); + it("validates prompt content using promptCapabilities", async () => { const { transport, sendRequestMock } = createTransportMock(); sendRequestMock.mockResolvedValueOnce({ protocolVersion: 1 } satisfies InitializeResponse); diff --git a/packages/poe-acp-client/src/acp-client.ts b/packages/poe-acp-client/src/acp-client.ts index a9986e9c3..86e8131b7 100644 --- a/packages/poe-acp-client/src/acp-client.ts +++ b/packages/poe-acp-client/src/acp-client.ts @@ -114,6 +114,7 @@ const validStopReasons = new Set([ "completed", "cancelled", "max_tokens", + "end_turn", ]); export interface AcpClientHandlers { @@ -241,7 +242,7 @@ function assertPromptResponse(value: PromptResponse): void { if (!validStopReasons.has(value.stopReason)) { throw invalidResponse( "session/prompt", - '"stopReason" must be "completed", "cancelled", or "max_tokens".' + '"stopReason" must be "completed", "cancelled", "max_tokens", or "end_turn".' ); } } diff --git a/packages/poe-acp-client/src/types.compile-check.ts b/packages/poe-acp-client/src/types.compile-check.ts index bbb798b60..2d731d66d 100644 --- a/packages/poe-acp-client/src/types.compile-check.ts +++ b/packages/poe-acp-client/src/types.compile-check.ts @@ -803,5 +803,5 @@ const ignoredLegacyToolCallContent: ToolCallContent = { content: { type: "text", text: "legacy" }, }; -// @ts-expect-error stop reason values are restricted to completed/cancelled/max_tokens -const ignoredInvalidStopReason: StopReason = "end_turn"; +// @ts-expect-error stop reason values are restricted to supported ACP values +const ignoredInvalidStopReason: StopReason = "totally-not-valid"; diff --git a/packages/poe-acp-client/src/types.ts b/packages/poe-acp-client/src/types.ts index 2e6b9c225..33a0b02f1 100644 --- a/packages/poe-acp-client/src/types.ts +++ b/packages/poe-acp-client/src/types.ts @@ -302,7 +302,7 @@ export type ProtocolVersion = number; export type RequestId = null | number | string; -export type StopReason = "completed" | "cancelled" | "max_tokens"; +export type StopReason = "completed" | "cancelled" | "max_tokens" | "end_turn"; export interface Implementation extends AcpExtensible { name: string;