@polpo-ai/server 0.15.81 → 0.15.83

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/channels/conversation-bridge.js.map +1 -1
  2. package/dist/routes/completions/chat-handler.d.ts.map +1 -1
  3. package/dist/routes/completions/chat-handler.js +3 -2
  4. package/dist/routes/completions/chat-handler.js.map +1 -1
  5. package/dist/routes/completions/chat-via-run-handler.test.js +97 -0
  6. package/dist/routes/completions/chat-via-run-handler.test.js.map +1 -1
  7. package/dist/routes/completions/client-tools.d.ts +46 -0
  8. package/dist/routes/completions/client-tools.d.ts.map +1 -0
  9. package/dist/routes/completions/client-tools.js +60 -0
  10. package/dist/routes/completions/client-tools.js.map +1 -0
  11. package/dist/routes/completions/client-tools.test.d.ts +2 -0
  12. package/dist/routes/completions/client-tools.test.d.ts.map +1 -0
  13. package/dist/routes/completions/client-tools.test.js +105 -0
  14. package/dist/routes/completions/client-tools.test.js.map +1 -0
  15. package/dist/routes/completions/continuation.d.ts +10 -0
  16. package/dist/routes/completions/continuation.d.ts.map +1 -0
  17. package/dist/routes/completions/continuation.js +18 -0
  18. package/dist/routes/completions/continuation.js.map +1 -0
  19. package/dist/routes/completions/continuation.test.d.ts +2 -0
  20. package/dist/routes/completions/continuation.test.d.ts.map +1 -0
  21. package/dist/routes/completions/continuation.test.js +39 -0
  22. package/dist/routes/completions/continuation.test.js.map +1 -0
  23. package/dist/routes/completions/conversation-turn.d.ts +4 -0
  24. package/dist/routes/completions/conversation-turn.d.ts.map +1 -1
  25. package/dist/routes/completions/conversation-turn.js +119 -7
  26. package/dist/routes/completions/conversation-turn.js.map +1 -1
  27. package/dist/routes/completions/message-mapping.d.ts +3 -0
  28. package/dist/routes/completions/message-mapping.d.ts.map +1 -1
  29. package/dist/routes/completions/message-mapping.js +50 -2
  30. package/dist/routes/completions/message-mapping.js.map +1 -1
  31. package/dist/routes/completions/message-mapping.test.js +70 -13
  32. package/dist/routes/completions/message-mapping.test.js.map +1 -1
  33. package/dist/routes/completions/runtime-plan-hook.test.js +102 -0
  34. package/dist/routes/completions/runtime-plan-hook.test.js.map +1 -1
  35. package/dist/routes/completions/schemas.d.ts +89 -2
  36. package/dist/routes/completions/schemas.d.ts.map +1 -1
  37. package/dist/routes/completions/schemas.js +205 -2
  38. package/dist/routes/completions/schemas.js.map +1 -1
  39. package/dist/routes/completions/schemas.test.js +196 -0
  40. package/dist/routes/completions/schemas.test.js.map +1 -1
  41. package/dist/routes/completions-durable-delivery.test.js +112 -0
  42. package/dist/routes/completions-durable-delivery.test.js.map +1 -1
  43. package/dist/routes/completions.d.ts.map +1 -1
  44. package/dist/routes/completions.js +38 -1
  45. package/dist/routes/completions.js.map +1 -1
  46. package/package.json +7 -7
@@ -0,0 +1,46 @@
1
+ import { jsonSchema } from "ai";
2
+ type RequestClientTool = {
3
+ type: "function";
4
+ function: {
5
+ name: string;
6
+ description?: string;
7
+ parameters?: Record<string, unknown>;
8
+ strict?: boolean;
9
+ };
10
+ };
11
+ type RequestToolChoice = "auto" | "none" | "required" | {
12
+ type: "function";
13
+ function: {
14
+ name: string;
15
+ };
16
+ };
17
+ type ModelToolCall = {
18
+ toolCallId?: string;
19
+ toolName?: string;
20
+ input?: unknown;
21
+ };
22
+ export declare class RequestClientToolError extends Error {
23
+ readonly code: "tool_name_conflict" | "parallel_client_tool_calls_returned";
24
+ readonly toolName?: string;
25
+ constructor(code: RequestClientToolError["code"], message: string, toolName?: string);
26
+ }
27
+ /**
28
+ * Convert request-declared OpenAI function tools into AI SDK tools without an
29
+ * execute handler. They can be selected by the model and returned to the
30
+ * caller, but can never cross the server execution boundary.
31
+ */
32
+ export declare function createRequestClientTools(tools: RequestClientTool[] | undefined): Record<string, {
33
+ description?: string;
34
+ strict?: boolean;
35
+ inputSchema: ReturnType<typeof jsonSchema>;
36
+ }>;
37
+ export declare function assertRequestClientToolNamesAvailable(requestTools: RequestClientTool[] | undefined, occupiedNames: Iterable<string>): void;
38
+ export declare function requestToolChoiceToAI(choice: RequestToolChoice | undefined): unknown | undefined;
39
+ /**
40
+ * A request-scoped client tool ends the server turn. Executing any sibling
41
+ * call before returning would make the result order-dependent, so mixed or
42
+ * parallel calls fail before a server tool is dispatched.
43
+ */
44
+ export declare function selectRequestClientToolCall<T extends ModelToolCall>(calls: T[], clientToolNames: ReadonlySet<string>): T | undefined;
45
+ export {};
46
+ //# sourceMappingURL=client-tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tools.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/client-tools.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAEhC,KAAK,iBAAiB,GAAG;IACvB,IAAI,EAAE,UAAU,CAAC;IACjB,QAAQ,EAAE;QACR,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,MAAM,CAAC,EAAE,OAAO,CAAC;KAClB,CAAC;CACH,CAAC;AAEF,KAAK,iBAAiB,GAClB,MAAM,GACN,MAAM,GACN,UAAU,GACV;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,QAAQ,EAAE;QAAE,IAAI,EAAE,MAAM,CAAA;KAAE,CAAA;CAAE,CAAC;AAErD,KAAK,aAAa,GAAG;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,CAAC;AAQF,qBAAa,sBAAuB,SAAQ,KAAK;IAC/C,QAAQ,CAAC,IAAI,EAAE,oBAAoB,GAAG,qCAAqC,CAAC;IAC5E,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;gBAGzB,IAAI,EAAE,sBAAsB,CAAC,MAAM,CAAC,EACpC,OAAO,EAAE,MAAM,EACf,QAAQ,CAAC,EAAE,MAAM;CAOpB;AAED;;;;GAIG;AACH,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,iBAAiB,EAAE,GAAG,SAAS,GACrC,MAAM,CAAC,MAAM,EAAE;IAAE,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,OAAO,CAAC;IAAC,WAAW,EAAE,UAAU,CAAC,OAAO,UAAU,CAAC,CAAA;CAAE,CAAC,CASxG;AAED,wBAAgB,qCAAqC,CACnD,YAAY,EAAE,iBAAiB,EAAE,GAAG,SAAS,EAC7C,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,GAC9B,IAAI,CAaN;AAED,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,iBAAiB,GAAG,SAAS,GACpC,OAAO,GAAG,SAAS,CAGrB;AAED;;;;GAIG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,aAAa,EACjE,KAAK,EAAE,CAAC,EAAE,EACV,eAAe,EAAE,WAAW,CAAC,MAAM,CAAC,GACnC,CAAC,GAAG,SAAS,CAYf"}
@@ -0,0 +1,60 @@
1
+ import { jsonSchema } from "ai";
2
+ const EMPTY_PARAMETERS = {
3
+ type: "object",
4
+ properties: {},
5
+ additionalProperties: false,
6
+ };
7
+ export class RequestClientToolError extends Error {
8
+ code;
9
+ toolName;
10
+ constructor(code, message, toolName) {
11
+ super(message);
12
+ this.name = "RequestClientToolError";
13
+ this.code = code;
14
+ this.toolName = toolName;
15
+ }
16
+ }
17
+ /**
18
+ * Convert request-declared OpenAI function tools into AI SDK tools without an
19
+ * execute handler. They can be selected by the model and returned to the
20
+ * caller, but can never cross the server execution boundary.
21
+ */
22
+ export function createRequestClientTools(tools) {
23
+ return Object.fromEntries((tools ?? []).map((tool) => [
24
+ tool.function.name,
25
+ {
26
+ ...(tool.function.description ? { description: tool.function.description } : {}),
27
+ ...(tool.function.strict !== undefined ? { strict: tool.function.strict } : {}),
28
+ inputSchema: jsonSchema(tool.function.parameters ?? EMPTY_PARAMETERS),
29
+ },
30
+ ]));
31
+ }
32
+ export function assertRequestClientToolNamesAvailable(requestTools, occupiedNames) {
33
+ const occupied = new Set([...occupiedNames].map((name) => name.toLocaleLowerCase("en-US")));
34
+ for (const tool of requestTools ?? []) {
35
+ const name = tool.function.name;
36
+ if (!occupied.has(name.toLocaleLowerCase("en-US")))
37
+ continue;
38
+ throw new RequestClientToolError("tool_name_conflict", `Request tool name conflicts with an effective runtime tool: ${name}`, name);
39
+ }
40
+ }
41
+ export function requestToolChoiceToAI(choice) {
42
+ if (choice === undefined || typeof choice === "string")
43
+ return choice;
44
+ return { type: "tool", toolName: choice.function.name };
45
+ }
46
+ /**
47
+ * A request-scoped client tool ends the server turn. Executing any sibling
48
+ * call before returning would make the result order-dependent, so mixed or
49
+ * parallel calls fail before a server tool is dispatched.
50
+ */
51
+ export function selectRequestClientToolCall(calls, clientToolNames) {
52
+ const clientCalls = calls.filter((call) => typeof call.toolName === "string" && clientToolNames.has(call.toolName));
53
+ if (clientCalls.length === 0)
54
+ return undefined;
55
+ if (calls.length !== 1 || clientCalls.length !== 1) {
56
+ throw new RequestClientToolError("parallel_client_tool_calls_returned", "A client-side tool call cannot be combined with another tool call in the same turn");
57
+ }
58
+ return clientCalls[0];
59
+ }
60
+ //# sourceMappingURL=client-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tools.js","sourceRoot":"","sources":["../../../src/routes/completions/client-tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=client-tools.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tools.test.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/client-tools.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,105 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { RequestClientToolError, assertRequestClientToolNamesAvailable, createRequestClientTools, requestToolChoiceToAI, selectRequestClientToolCall, } from "./client-tools.js";
3
+ const requestTool = (name = "configure_site_module") => ({
4
+ type: "function",
5
+ function: {
6
+ name,
7
+ description: "Open the module configuration UI.",
8
+ parameters: {
9
+ type: "object",
10
+ properties: {
11
+ module: { type: "string" },
12
+ },
13
+ required: ["module"],
14
+ additionalProperties: false,
15
+ },
16
+ strict: true,
17
+ },
18
+ });
19
+ function capturedError(run) {
20
+ try {
21
+ run();
22
+ }
23
+ catch (error) {
24
+ if (error instanceof RequestClientToolError)
25
+ return error;
26
+ throw error;
27
+ }
28
+ throw new Error("Expected RequestClientToolError");
29
+ }
30
+ describe("request-scoped client tools", () => {
31
+ it("maps OpenAI function tools to return-only AI SDK tools", () => {
32
+ const tools = createRequestClientTools([requestTool()]);
33
+ expect(Object.keys(tools)).toEqual(["configure_site_module"]);
34
+ expect(tools.configure_site_module).toMatchObject({
35
+ description: "Open the module configuration UI.",
36
+ strict: true,
37
+ inputSchema: {
38
+ jsonSchema: {
39
+ type: "object",
40
+ properties: { module: { type: "string" } },
41
+ required: ["module"],
42
+ additionalProperties: false,
43
+ },
44
+ },
45
+ });
46
+ expect(tools.configure_site_module).not.toHaveProperty("execute");
47
+ });
48
+ it("uses a closed empty object schema for parameterless functions", () => {
49
+ const tools = createRequestClientTools([{
50
+ type: "function",
51
+ function: { name: "open_dialog" },
52
+ }]);
53
+ expect(tools.open_dialog.inputSchema).toMatchObject({
54
+ jsonSchema: {
55
+ type: "object",
56
+ properties: {},
57
+ additionalProperties: false,
58
+ },
59
+ });
60
+ });
61
+ it("rejects collisions with any effective tool name case-insensitively", () => {
62
+ const error = capturedError(() => assertRequestClientToolNamesAvailable([requestTool("Configure_Site_Module")], ["configure_site_module", "bash"]));
63
+ expect(error).toMatchObject({
64
+ code: "tool_name_conflict",
65
+ toolName: "Configure_Site_Module",
66
+ });
67
+ });
68
+ it("accepts request tools that do not overlap the effective palette", () => {
69
+ expect(() => assertRequestClientToolNamesAvailable([requestTool()], ["bash", "tool_search", "ask_user_question"])).not.toThrow();
70
+ });
71
+ it.each([
72
+ ["auto", "auto"],
73
+ ["none", "none"],
74
+ ["required", "required"],
75
+ [
76
+ { type: "function", function: { name: "configure_site_module" } },
77
+ { type: "tool", toolName: "configure_site_module" },
78
+ ],
79
+ ])("maps OpenAI tool_choice %# to AI SDK tool choice", (choice, expected) => {
80
+ expect(requestToolChoiceToAI(choice)).toEqual(expected);
81
+ });
82
+ it("returns the only client-side call without executing it", () => {
83
+ const call = {
84
+ toolCallId: "call_1",
85
+ toolName: "configure_site_module",
86
+ input: { module: "commerce" },
87
+ };
88
+ expect(selectRequestClientToolCall([call], new Set([call.toolName]))).toBe(call);
89
+ });
90
+ it("returns undefined when the turn contains no client-side call", () => {
91
+ expect(selectRequestClientToolCall([
92
+ { toolCallId: "call_1", toolName: "bash", input: {} },
93
+ ], new Set(["configure_site_module"]))).toBeUndefined();
94
+ });
95
+ it("fails closed when a client-side call is mixed with another call", () => {
96
+ const error = capturedError(() => selectRequestClientToolCall([
97
+ { toolCallId: "call_1", toolName: "configure_site_module", input: {} },
98
+ { toolCallId: "call_2", toolName: "bash", input: {} },
99
+ ], new Set(["configure_site_module"])));
100
+ expect(error).toMatchObject({
101
+ code: "parallel_client_tool_calls_returned",
102
+ });
103
+ });
104
+ });
105
+ //# sourceMappingURL=client-tools.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client-tools.test.js","sourceRoot":"","sources":["../../../src/routes/completions/client-tools.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ export declare function continuationFingerprint(input: {
2
+ sessionId: string;
3
+ agent: string;
4
+ loop: string;
5
+ user?: string;
6
+ toolCallId: string;
7
+ expectedSessionVersion: number;
8
+ result: unknown;
9
+ }): string;
10
+ //# sourceMappingURL=continuation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/continuation.ts"],"names":[],"mappings":"AAaA,wBAAgB,uBAAuB,CAAC,KAAK,EAAE;IAC7C,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,sBAAsB,EAAE,MAAM,CAAC;IAC/B,MAAM,EAAE,OAAO,CAAC;CACjB,GAAG,MAAM,CAET"}
@@ -0,0 +1,18 @@
1
+ import { createHash } from "node:crypto";
2
+ function stableJson(value) {
3
+ if (value === undefined)
4
+ return "null";
5
+ if (value === null || typeof value !== "object")
6
+ return JSON.stringify(value);
7
+ if (Array.isArray(value))
8
+ return `[${value.map(stableJson).join(",")}]`;
9
+ return `{${Object.entries(value)
10
+ .filter(([, entry]) => entry !== undefined)
11
+ .sort(([left], [right]) => left.localeCompare(right))
12
+ .map(([key, entry]) => `${JSON.stringify(key)}:${stableJson(entry)}`)
13
+ .join(",")}}`;
14
+ }
15
+ export function continuationFingerprint(input) {
16
+ return createHash("sha256").update(stableJson(input)).digest("hex");
17
+ }
18
+ //# sourceMappingURL=continuation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation.js","sourceRoot":"","sources":["../../../src/routes/completions/continuation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,SAAS,UAAU,CAAC,KAAc;IAChC,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACvC,IAAI,KAAK,KAAK,IAAI,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC;IAC9E,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,IAAI,KAAK,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACxE,OAAO,IAAI,MAAM,CAAC,OAAO,CAAC,KAAgC,CAAC;SACxD,MAAM,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,KAAK,KAAK,SAAS,CAAC;SAC1C,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;SACpD,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,IAAI,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;SACpE,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;AAClB,CAAC;AAED,MAAM,UAAU,uBAAuB,CAAC,KAQvC;IACC,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACtE,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=continuation.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation.test.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/continuation.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,39 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { continuationFingerprint } from "./continuation.js";
3
+ describe("continuationFingerprint", () => {
4
+ it("is stable across nested JSON key ordering", () => {
5
+ const base = {
6
+ sessionId: "session-1",
7
+ agent: "leo",
8
+ loop: "build",
9
+ toolCallId: "call-1",
10
+ expectedSessionVersion: 2,
11
+ };
12
+ expect(continuationFingerprint({
13
+ ...base,
14
+ result: { site: { id: "1", mode: "edit" }, accepted: true },
15
+ })).toBe(continuationFingerprint({
16
+ ...base,
17
+ result: { accepted: true, site: { mode: "edit", id: "1" } },
18
+ }));
19
+ });
20
+ it("changes for a different session boundary or result", () => {
21
+ const base = {
22
+ sessionId: "session-1",
23
+ agent: "leo",
24
+ loop: "build",
25
+ toolCallId: "call-1",
26
+ expectedSessionVersion: 2,
27
+ result: "ok",
28
+ };
29
+ expect(continuationFingerprint(base)).not.toBe(continuationFingerprint({
30
+ ...base,
31
+ sessionId: "session-2",
32
+ }));
33
+ expect(continuationFingerprint(base)).not.toBe(continuationFingerprint({
34
+ ...base,
35
+ result: "different",
36
+ }));
37
+ });
38
+ });
39
+ //# sourceMappingURL=continuation.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"continuation.test.js","sourceRoot":"","sources":["../../../src/routes/completions/continuation.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAC9C,OAAO,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAE5D,QAAQ,CAAC,yBAAyB,EAAE,GAAG,EAAE;IACvC,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,IAAI,GAAG;YACX,SAAS,EAAE,WAAW;YACtB,KAAK,EAAE,KAAK;YACZ,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,QAAQ;YACpB,sBAAsB,EAAE,CAAC;SAC1B,CAAC;QACF,MAAM,CAAC,uBAAuB,CAAC;YAC7B,GAAG,IAAI;YACP,MAAM,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,EAAE,GAAG,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE;SAC5D,CAAC,CAAC,CAAC,IAAI,CAAC,uBAAuB,CAAC;YAC/B,GAAG,IAAI;YACP,MAAM,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,GAAG,EAAE,EAAE;SAC5D,CAAC,CAAC,CAAC;IACN,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,IAAI,GAAG;YACX,SAAS,EAAE,WAAW;YACtB,KAAK,EAAE,KAAK;YACZ,IAAI,EAAE,OAAO;YACb,UAAU,EAAE,QAAQ;YACpB,sBAAsB,EAAE,CAAC;YACzB,MAAM,EAAE,IAAI;SACb,CAAC;QACF,MAAM,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAuB,CAAC;YACrE,GAAG,IAAI;YACP,SAAS,EAAE,WAAW;SACvB,CAAC,CAAC,CAAC;QACJ,MAAM,CAAC,uBAAuB,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,uBAAuB,CAAC;YACrE,GAAG,IAAI;YACP,MAAM,EAAE,WAAW;SACpB,CAAC,CAAC,CAAC;IACN,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
@@ -45,6 +45,10 @@ export interface PrepareConversationTurnOptions {
45
45
  /** Trusted surface identity. Omit for ordinary HTTP/API agent calls. */
46
46
  runtime?: CompletionRuntimeInvocation;
47
47
  signal?: AbortSignal;
48
+ continuation?: {
49
+ idempotencyKey: string;
50
+ fingerprint: string;
51
+ };
48
52
  }
49
53
  export interface RunConversationTurnInput extends PrepareConversationTurnOptions {
50
54
  body: CompletionRequestBody;
@@ -1 +1 @@
1
- {"version":3,"file":"conversation-turn.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/conversation-turn.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"conversation-turn.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/conversation-turn.ts"],"names":[],"mappings":"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"}
@@ -1,8 +1,10 @@
1
1
  import { nanoid } from "nanoid";
2
2
  import { agentMemoryScope, createToolInvocationContext, createRuntimePromptContextSegment, compileExecutionRouteManifest, createExecutionRouteResolvedEvent, createExplicitExecutionRoute, createRuntimePlanResolvedEvent, executionRouteRuntimePlanFields, normalizeRuntimeContextTrustMode, normalizeRuntimePlan, renderRuntimePromptContextSegment, renderRuntimePromptContextSegments, renderRuntimeContextPrompt, replacesLegacyAgentMemory, replacesLegacySharedMemory, resolveRuntimeContext, resolveExecutionRoute, resolveLoopSelection, validateExecutionRouterConfig, } from "@polpo-ai/core";
3
3
  import { normalizeChatInteractionSettings, resolveChatInteractionCapabilities, } from "@polpo-ai/core/chat-interactions";
4
- import { convertMessages, extractText } from "./message-mapping.js";
4
+ import { convertMessages, extractText, sessionMessagesToCompletionMessages, } from "./message-mapping.js";
5
+ import { SessionContinuationError } from "@polpo-ai/core/session-continuation";
5
6
  import { clientSideToolsForCapabilities, toAIToolChoice, } from "./tool-mapping.js";
7
+ import { RequestClientToolError, assertRequestClientToolNamesAvailable, createRequestClientTools, requestToolChoiceToAI, } from "./client-tools.js";
6
8
  import { createGuardedCompletionToolExecutor, } from "./tool-guardrails.js";
7
9
  import { assertCompletionMessageContent, assertModelPreflightValue, assertRuntimeContextResolution, } from "./preflight-validation.js";
8
10
  import { agentConfigForModelPrimary, modelSelectionForAgent, modelSelectionForResolvedModel, } from "./agent-step-runner.js";
@@ -23,6 +25,16 @@ function completionError(message, status, code, type = "invalid_request_error")
23
25
  },
24
26
  };
25
27
  }
28
+ async function cleanupFailedPreparation(cleanup) {
29
+ if (!cleanup)
30
+ return;
31
+ try {
32
+ await cleanup();
33
+ }
34
+ catch {
35
+ // Preserve the deterministic preflight error; cleanup is best-effort here.
36
+ }
37
+ }
26
38
  function preflightError(error) {
27
39
  const guardrail = guardrailErrorEnvelope(error);
28
40
  if (guardrail) {
@@ -313,7 +325,8 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
313
325
  let executionRoute;
314
326
  let activatedSkills = [];
315
327
  let deferredAgentTools = false;
316
- const completionId = options.completionId ?? `chatcmpl-${nanoid(24)}`;
328
+ let completionId = options.completionId ?? `chatcmpl-${nanoid(24)}`;
329
+ const sessionStore = deps.getSessionStore();
317
330
  const invocation = completionInvocation(options.runtime);
318
331
  const interactionSettings = normalizeChatInteractionSettings(initialAgentConfig?.chat);
319
332
  const interactionCapabilities = resolveChatInteractionCapabilities({
@@ -321,12 +334,26 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
321
334
  settings: interactionSettings,
322
335
  client: body.polpo?.capabilities,
323
336
  });
324
- const clientSideTools = clientSideToolsForCapabilities(interactionCapabilities);
337
+ const builtInClientSideTools = clientSideToolsForCapabilities(interactionCapabilities);
338
+ try {
339
+ assertRequestClientToolNamesAvailable(body.tools, Object.keys(builtInClientSideTools));
340
+ }
341
+ catch (error) {
342
+ if (error instanceof RequestClientToolError) {
343
+ return completionError(error.message, 400, error.code);
344
+ }
345
+ throw error;
346
+ }
347
+ const requestClientSideTools = createRequestClientTools(body.tools);
348
+ const clientSideTools = {
349
+ ...builtInClientSideTools,
350
+ ...requestClientSideTools,
351
+ };
325
352
  const clientSideToolNames = new Set(Object.keys(clientSideTools));
326
353
  const contextTrust = normalizeRuntimeContextTrustMode(deps.getConfig()?.settings?.contextTrust);
327
354
  const converted = convertMessages(body.messages, contextTrust);
328
355
  let { aiMessages, extraSystemParts, promptContextSegments, } = converted;
329
- const callerSystemParts = contextTrust === "enforce" && promptContextSegments.length > 0
356
+ let callerSystemParts = contextTrust === "enforce" && promptContextSegments.length > 0
330
357
  ? [renderRuntimePromptContextSegments(promptContextSegments)]
331
358
  : extraSystemParts;
332
359
  if (agentMode) {
@@ -388,6 +415,56 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
388
415
  }
389
416
  }
390
417
  resolvedAgentConfig = agentConfig;
418
+ const continuation = body.polpo?.continuation;
419
+ if (continuation) {
420
+ if (!projectLoopRuntime) {
421
+ return completionError("The selected loop is not available as a Project Loop", 422, "loop_not_available");
422
+ }
423
+ if (!options.sessionId || !options.continuation) {
424
+ return completionError("Client-tool continuation requires an existing session and Idempotency-Key", 400, "invalid_continuation_request");
425
+ }
426
+ if (!sessionStore?.prepareContinuation) {
427
+ return completionError("Session continuation is not supported by the configured store", 501, "session_continuation_unavailable");
428
+ }
429
+ const toolResult = body.messages[0];
430
+ try {
431
+ const prepared = await sessionStore.prepareContinuation({
432
+ sessionId: options.sessionId,
433
+ agent: body.agent,
434
+ user: options.runtime?.user ?? body.user,
435
+ ...(options.runtime?.scope ? { scope: options.runtime.scope } : {}),
436
+ toolCallId: continuation.tool_call_id,
437
+ result: toolResult.content,
438
+ expectedSessionVersion: continuation.expected_session_version,
439
+ idempotencyKey: options.continuation.idempotencyKey,
440
+ fingerprint: options.continuation.fingerprint,
441
+ runId: completionId,
442
+ });
443
+ completionId = prepared.runId;
444
+ const canonicalMessages = sessionMessagesToCompletionMessages(prepared.messages);
445
+ body = { ...body, messages: canonicalMessages };
446
+ const canonical = convertMessages(body.messages, contextTrust);
447
+ aiMessages = canonical.aiMessages;
448
+ extraSystemParts = canonical.extraSystemParts;
449
+ promptContextSegments = canonical.promptContextSegments;
450
+ callerSystemParts = contextTrust === "enforce" && promptContextSegments.length > 0
451
+ ? [renderRuntimePromptContextSegments(promptContextSegments)]
452
+ : extraSystemParts;
453
+ options.setHeader?.("x-polpo-run-id", completionId);
454
+ options.setHeader?.("x-session-version", String(prepared.sessionVersion + 1));
455
+ }
456
+ catch (error) {
457
+ if (error instanceof SessionContinuationError) {
458
+ const status = error.code === "session_not_found"
459
+ ? 404
460
+ : error.code === "continuation_scope_mismatch"
461
+ ? 403
462
+ : 409;
463
+ return completionError(error.message, status, error.code);
464
+ }
465
+ throw error;
466
+ }
467
+ }
391
468
  if (requestedSkills.length > 0) {
392
469
  const assignedSkills = new Set(Array.isArray(agentConfig.skills)
393
470
  ? agentConfig.skills.filter((skill) => typeof skill === "string")
@@ -555,7 +632,6 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
555
632
  if (projectLoopRuntime && isStructuredResponseFormat(body.response_format)) {
556
633
  return completionError("response_format is not supported with project loop execution", 400, "unsupported_response_format");
557
634
  }
558
- const sessionStore = deps.getSessionStore();
559
635
  let sessionId = options.sessionId ?? null;
560
636
  if (sessionStore) {
561
637
  if (!sessionId) {
@@ -565,14 +641,20 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
565
641
  sessionId = await sessionStore.create({
566
642
  title: sessionTitle,
567
643
  agent: agentScope ?? undefined,
568
- user: body.user,
644
+ user: options.runtime?.user ?? body.user,
569
645
  metadata: body.metadata,
646
+ ...(options.runtime?.scope ? { scope: options.runtime.scope } : {}),
570
647
  });
571
648
  }
572
649
  const lastUserMsg = [...body.messages].reverse().find((message) => message.role === "user");
573
- if (lastUserMsg && sessionId) {
650
+ if (!body.polpo?.continuation && lastUserMsg && sessionId) {
574
651
  await sessionStore.addMessage(sessionId, "user", lastUserMsg.content);
575
652
  }
653
+ if (!body.polpo?.continuation && sessionId && sessionStore.getSession) {
654
+ const current = await sessionStore.getSession(sessionId);
655
+ if (current)
656
+ options.setHeader?.("x-session-version", String(Number(current.version ?? 0) + 1));
657
+ }
576
658
  }
577
659
  if (deferredAgentTools) {
578
660
  const toolInvocation = createCompletionToolInvocation({
@@ -589,6 +671,21 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
589
671
  extraAiTools = resolvedTools.extraAiTools;
590
672
  }
591
673
  if (!projectLoopRuntime) {
674
+ try {
675
+ assertRequestClientToolNamesAvailable(body.tools, [
676
+ ...effectiveTools
677
+ .map((tool) => tool?.name)
678
+ .filter((name) => typeof name === "string"),
679
+ ...Object.keys(extraAiTools ?? {}),
680
+ ]);
681
+ }
682
+ catch (error) {
683
+ if (error instanceof RequestClientToolError) {
684
+ await cleanupFailedPreparation(onResponseFinished);
685
+ return completionError(error.message, 400, error.code);
686
+ }
687
+ throw error;
688
+ }
592
689
  effectiveToolExecutor = createGuardedCompletionToolExecutor({
593
690
  executor: effectiveToolExecutor,
594
691
  tools: effectiveTools,
@@ -615,6 +712,18 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
615
712
  maxLoadBatch: toolDisclosure.maxLoadBatch,
616
713
  maxSearchResults: toolDisclosure.maxSearchResults,
617
714
  });
715
+ try {
716
+ assertRequestClientToolNamesAvailable(body.tools, pool.tools
717
+ .map((tool) => tool?.name)
718
+ .filter((name) => typeof name === "string"));
719
+ }
720
+ catch (error) {
721
+ if (error instanceof RequestClientToolError) {
722
+ await cleanupFailedPreparation(onResponseFinished);
723
+ return completionError(error.message, 400, error.code);
724
+ }
725
+ throw error;
726
+ }
618
727
  const alwaysActive = [
619
728
  ...Object.keys(extraAiTools ?? {}),
620
729
  ...clientSideToolNames,
@@ -649,6 +758,9 @@ export async function prepareChatCompletionExecution(deps, body, options = {}) {
649
758
  activatedSkills,
650
759
  };
651
760
  }
761
+ if (body.tool_choice !== undefined) {
762
+ modelToolChoice = requestToolChoiceToAI(body.tool_choice);
763
+ }
652
764
  const execution = {
653
765
  deps,
654
766
  body,