@polpo-ai/server 0.15.60 → 0.15.63

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 (80) hide show
  1. package/dist/channels/conversation-bridge.d.ts +50 -0
  2. package/dist/channels/conversation-bridge.d.ts.map +1 -0
  3. package/dist/channels/conversation-bridge.js +234 -0
  4. package/dist/channels/conversation-bridge.js.map +1 -0
  5. package/dist/channels/conversation-bridge.test.d.ts +2 -0
  6. package/dist/channels/conversation-bridge.test.d.ts.map +1 -0
  7. package/dist/channels/conversation-bridge.test.js +219 -0
  8. package/dist/channels/conversation-bridge.test.js.map +1 -0
  9. package/dist/index.d.ts +2 -1
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/routes/completions/agent-step-runner.js +2 -2
  14. package/dist/routes/completions/agent-step-runner.js.map +1 -1
  15. package/dist/routes/completions/agent-step-runner.test.js +1 -1
  16. package/dist/routes/completions/agent-step-runner.test.js.map +1 -1
  17. package/dist/routes/completions/chat-handler.d.ts +13 -2
  18. package/dist/routes/completions/chat-handler.d.ts.map +1 -1
  19. package/dist/routes/completions/chat-handler.js +61 -18
  20. package/dist/routes/completions/chat-handler.js.map +1 -1
  21. package/dist/routes/completions/chat-interaction-runtime.d.ts +5 -0
  22. package/dist/routes/completions/chat-interaction-runtime.d.ts.map +1 -0
  23. package/dist/routes/completions/chat-interaction-runtime.js +46 -0
  24. package/dist/routes/completions/chat-interaction-runtime.js.map +1 -0
  25. package/dist/routes/completions/chat-suggestions.d.ts +31 -0
  26. package/dist/routes/completions/chat-suggestions.d.ts.map +1 -0
  27. package/dist/routes/completions/chat-suggestions.js +147 -0
  28. package/dist/routes/completions/chat-suggestions.js.map +1 -0
  29. package/dist/routes/completions/chat-suggestions.test.d.ts +2 -0
  30. package/dist/routes/completions/chat-suggestions.test.d.ts.map +1 -0
  31. package/dist/routes/completions/chat-suggestions.test.js +100 -0
  32. package/dist/routes/completions/chat-suggestions.test.js.map +1 -0
  33. package/dist/routes/completions/chat-via-run-handler.d.ts +5 -0
  34. package/dist/routes/completions/chat-via-run-handler.d.ts.map +1 -1
  35. package/dist/routes/completions/chat-via-run-handler.js +54 -19
  36. package/dist/routes/completions/chat-via-run-handler.js.map +1 -1
  37. package/dist/routes/completions/chat-via-run-handler.test.js +251 -6
  38. package/dist/routes/completions/chat-via-run-handler.test.js.map +1 -1
  39. package/dist/routes/completions/conversation-turn.d.ts.map +1 -1
  40. package/dist/routes/completions/conversation-turn.js +19 -2
  41. package/dist/routes/completions/conversation-turn.js.map +1 -1
  42. package/dist/routes/completions/message-mapping.d.ts.map +1 -1
  43. package/dist/routes/completions/message-mapping.js +7 -24
  44. package/dist/routes/completions/message-mapping.js.map +1 -1
  45. package/dist/routes/completions/runtime-plan-hook.test.js +43 -0
  46. package/dist/routes/completions/runtime-plan-hook.test.js.map +1 -1
  47. package/dist/routes/completions/schemas.d.ts +94 -3
  48. package/dist/routes/completions/schemas.d.ts.map +1 -1
  49. package/dist/routes/completions/schemas.js +52 -0
  50. package/dist/routes/completions/schemas.js.map +1 -1
  51. package/dist/routes/completions/schemas.test.js +118 -0
  52. package/dist/routes/completions/schemas.test.js.map +1 -1
  53. package/dist/routes/completions/sse.d.ts +9 -1
  54. package/dist/routes/completions/sse.d.ts.map +1 -1
  55. package/dist/routes/completions/sse.js +28 -0
  56. package/dist/routes/completions/sse.js.map +1 -1
  57. package/dist/routes/completions/structured-output.d.ts +20 -0
  58. package/dist/routes/completions/structured-output.d.ts.map +1 -0
  59. package/dist/routes/completions/structured-output.js +82 -0
  60. package/dist/routes/completions/structured-output.js.map +1 -0
  61. package/dist/routes/completions/structured-output.test.d.ts +2 -0
  62. package/dist/routes/completions/structured-output.test.d.ts.map +1 -0
  63. package/dist/routes/completions/structured-output.test.js +54 -0
  64. package/dist/routes/completions/structured-output.test.js.map +1 -0
  65. package/dist/routes/completions/tool-disclosure.d.ts +1 -1
  66. package/dist/routes/completions/tool-disclosure.d.ts.map +1 -1
  67. package/dist/routes/completions/tool-disclosure.js +18 -18
  68. package/dist/routes/completions/tool-disclosure.js.map +1 -1
  69. package/dist/routes/completions/tool-disclosure.test.js +29 -29
  70. package/dist/routes/completions/tool-disclosure.test.js.map +1 -1
  71. package/dist/routes/completions/tool-mapping.d.ts +8 -0
  72. package/dist/routes/completions/tool-mapping.d.ts.map +1 -1
  73. package/dist/routes/completions/tool-mapping.js +10 -0
  74. package/dist/routes/completions/tool-mapping.js.map +1 -1
  75. package/dist/routes/completions/tool-mapping.test.js +10 -0
  76. package/dist/routes/completions/tool-mapping.test.js.map +1 -1
  77. package/dist/routes/completions.d.ts +16 -1
  78. package/dist/routes/completions.d.ts.map +1 -1
  79. package/dist/routes/completions.js.map +1 -1
  80. package/package.json +8 -7
@@ -0,0 +1,46 @@
1
+ import { normalizeChatInteractionSettings, } from "@polpo-ai/core/chat-interactions";
2
+ import { completionResolvedModelInfo } from "./agent-step-runner.js";
3
+ import { generateChatSuggestions } from "./chat-suggestions.js";
4
+ import { isStructuredResponseFormat } from "./structured-output.js";
5
+ /** Run the optional, fail-open suggestion pass for a successful text turn. */
6
+ export async function suggestionsForCompletion(execution, finalText, signal) {
7
+ if (!execution.interactionCapabilities?.suggestions
8
+ || isStructuredResponseFormat(execution.body.response_format)
9
+ || !finalText.trim()
10
+ || signal?.aborted) {
11
+ return [];
12
+ }
13
+ let settings;
14
+ try {
15
+ settings = normalizeChatInteractionSettings(execution.deps.getConfig()?.settings?.chat).suggestions;
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ const result = await generateChatSuggestions({
21
+ model: execution.m.aiModel,
22
+ providerOptions: execution.providerOpts,
23
+ messages: execution.body.messages ?? [],
24
+ finalText,
25
+ maxItems: settings.maxItems,
26
+ guidance: settings.guidance,
27
+ signal,
28
+ });
29
+ if (result.usage) {
30
+ try {
31
+ execution.deps.onAuxiliaryModelFinished?.({
32
+ operation: "chat_suggestions",
33
+ usage: result.usage,
34
+ model: execution.m.id ?? execution.m.provider,
35
+ resolvedModel: completionResolvedModelInfo(execution.m),
36
+ agent: execution.body.agent,
37
+ sessionId: execution.sessionId ?? undefined,
38
+ user: execution.body.user,
39
+ providerMetadata: result.providerMetadata,
40
+ });
41
+ }
42
+ catch { /* auxiliary accounting never fails the main response */ }
43
+ }
44
+ return result.suggestions;
45
+ }
46
+ //# sourceMappingURL=chat-interaction-runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-interaction-runtime.js","sourceRoot":"","sources":["../../../src/routes/completions/chat-interaction-runtime.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,gCAAgC,GAEjC,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAAE,2BAA2B,EAAE,MAAM,wBAAwB,CAAC;AAErE,OAAO,EAAE,uBAAuB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,wBAAwB,CAAC;AAEpE,8EAA8E;AAC9E,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,SAAkC,EAClC,SAAiB,EACjB,MAAoB;IAEpB,IACE,CAAC,SAAS,CAAC,uBAAuB,EAAE,WAAW;WAC5C,0BAA0B,CAAC,SAAS,CAAC,IAAI,CAAC,eAAe,CAAC;WAC1D,CAAC,SAAS,CAAC,IAAI,EAAE;WACjB,MAAM,EAAE,OAAO,EAClB,CAAC;QACD,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,IAAI,QAAQ,CAAC;IACb,IAAI,CAAC;QACH,QAAQ,GAAG,gCAAgC,CACzC,SAAS,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,QAAQ,EAAE,IAAI,CAC3C,CAAC,WAAW,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,MAAM,GAAG,MAAM,uBAAuB,CAAC;QAC3C,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO;QAC1B,eAAe,EAAE,SAAS,CAAC,YAAY;QACvC,QAAQ,EAAE,SAAS,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE;QACvC,SAAS;QACT,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,QAAQ,EAAE,QAAQ,CAAC,QAAQ;QAC3B,MAAM;KACP,CAAC,CAAC;IAEH,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QACjB,IAAI,CAAC;YACH,SAAS,CAAC,IAAI,CAAC,wBAAwB,EAAE,CAAC;gBACxC,SAAS,EAAE,kBAAkB;gBAC7B,KAAK,EAAE,MAAM,CAAC,KAAK;gBACnB,KAAK,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,SAAS,CAAC,CAAC,CAAC,QAAQ;gBAC7C,aAAa,EAAE,2BAA2B,CAAC,SAAS,CAAC,CAAC,CAAC;gBACvD,KAAK,EAAE,SAAS,CAAC,IAAI,CAAC,KAAK;gBAC3B,SAAS,EAAE,SAAS,CAAC,SAAS,IAAI,SAAS;gBAC3C,IAAI,EAAE,SAAS,CAAC,IAAI,CAAC,IAAI;gBACzB,gBAAgB,EAAE,MAAM,CAAC,gBAAgB;aAC1C,CAAC,CAAC;QACL,CAAC;QAAC,MAAM,CAAC,CAAC,wDAAwD,CAAC,CAAC;IACtE,CAAC;IAED,OAAO,MAAM,CAAC,WAAW,CAAC;AAC5B,CAAC"}
@@ -0,0 +1,31 @@
1
+ import type { ChatSuggestion } from "@polpo-ai/core/chat-interactions";
2
+ import { type LanguageModel, type LanguageModelUsage } from "ai";
3
+ interface SuggestionGenerationResponse {
4
+ output?: unknown;
5
+ usage?: LanguageModelUsage;
6
+ providerMetadata?: Record<string, unknown>;
7
+ }
8
+ export type ChatSuggestionGenerate = (options: Record<string, unknown>) => Promise<SuggestionGenerationResponse>;
9
+ export interface GenerateChatSuggestionsInput {
10
+ model: LanguageModel;
11
+ providerOptions?: Record<string, any>;
12
+ messages: Array<{
13
+ role?: unknown;
14
+ content?: unknown;
15
+ }>;
16
+ finalText: string;
17
+ maxItems: number;
18
+ guidance?: string;
19
+ signal?: AbortSignal;
20
+ timeoutMs?: number;
21
+ generate?: ChatSuggestionGenerate;
22
+ }
23
+ export interface ChatSuggestionGenerationResult {
24
+ suggestions: ChatSuggestion[];
25
+ usage?: LanguageModelUsage;
26
+ providerMetadata?: Record<string, unknown>;
27
+ }
28
+ /** Generate optional next-message suggestions without affecting the main turn. */
29
+ export declare function generateChatSuggestions(input: GenerateChatSuggestionsInput): Promise<ChatSuggestionGenerationResult>;
30
+ export {};
31
+ //# sourceMappingURL=chat-suggestions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-suggestions.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/chat-suggestions.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kCAAkC,CAAC;AACvE,OAAO,EAAwB,KAAK,aAAa,EAAE,KAAK,kBAAkB,EAAE,MAAM,IAAI,CAAC;AAiBvF,UAAU,4BAA4B;IACpC,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAED,MAAM,MAAM,sBAAsB,GAAG,CACnC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAC7B,OAAO,CAAC,4BAA4B,CAAC,CAAC;AAE3C,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,aAAa,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;IACtC,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,CAAC,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;IACvD,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,sBAAsB,CAAC;CACnC;AAED,MAAM,WAAW,8BAA8B;IAC7C,WAAW,EAAE,cAAc,EAAE,CAAC;IAC9B,KAAK,CAAC,EAAE,kBAAkB,CAAC;IAC3B,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC5C;AAuED,kFAAkF;AAClF,wBAAsB,uBAAuB,CAC3C,KAAK,EAAE,4BAA4B,GAClC,OAAO,CAAC,8BAA8B,CAAC,CA2DzC"}
@@ -0,0 +1,147 @@
1
+ import { generateText, Output } from "ai";
2
+ import { z } from "zod";
3
+ const MAX_CONTEXT_MESSAGES = 8;
4
+ const MAX_CONTEXT_CHARS = 7_000;
5
+ const MAX_FINAL_TEXT_CHARS = 4_000;
6
+ const MAX_LABEL_CHARS = 64;
7
+ const MAX_PROMPT_CHARS = 500;
8
+ const DEFAULT_TIMEOUT_MS = 4_000;
9
+ const generatedSuggestionsSchema = z.object({
10
+ suggestions: z.array(z.object({
11
+ label: z.string().min(1).max(MAX_LABEL_CHARS),
12
+ prompt: z.string().min(1).max(MAX_PROMPT_CHARS),
13
+ }).strict()).max(4),
14
+ }).strict();
15
+ function textFromContent(content) {
16
+ if (typeof content === "string")
17
+ return content;
18
+ if (!Array.isArray(content))
19
+ return "";
20
+ return content
21
+ .map((part) => {
22
+ if (!part || typeof part !== "object")
23
+ return "";
24
+ const value = part;
25
+ if (value.type === "text" && typeof value.text === "string")
26
+ return value.text;
27
+ return "";
28
+ })
29
+ .filter(Boolean)
30
+ .join("\n");
31
+ }
32
+ function boundedConversation(messages) {
33
+ const selected = [];
34
+ let remaining = MAX_CONTEXT_CHARS;
35
+ for (const message of messages.slice(-MAX_CONTEXT_MESSAGES).reverse()) {
36
+ if (remaining <= 0)
37
+ break;
38
+ if (message.role !== "user" && message.role !== "assistant")
39
+ continue;
40
+ const content = textFromContent(message.content).trim();
41
+ if (!content)
42
+ continue;
43
+ const bounded = content.slice(-remaining);
44
+ remaining -= bounded.length;
45
+ selected.push({ role: message.role, content: bounded });
46
+ }
47
+ return selected.reverse();
48
+ }
49
+ function compactWhitespace(value) {
50
+ return value.trim().replace(/\s+/g, " ");
51
+ }
52
+ function stableSuggestionId(label, prompt) {
53
+ let hash = 2166136261;
54
+ const value = `${label}\u0000${prompt}`;
55
+ for (let index = 0; index < value.length; index += 1) {
56
+ hash ^= value.charCodeAt(index);
57
+ hash = Math.imul(hash, 16777619);
58
+ }
59
+ return `suggestion_${(hash >>> 0).toString(36)}`;
60
+ }
61
+ function sanitizeSuggestions(value, maxItems) {
62
+ const parsed = generatedSuggestionsSchema.safeParse(value);
63
+ if (!parsed.success)
64
+ return [];
65
+ const suggestions = [];
66
+ const seenPrompts = new Set();
67
+ for (const item of parsed.data.suggestions) {
68
+ const label = compactWhitespace(item.label).slice(0, MAX_LABEL_CHARS);
69
+ const prompt = compactWhitespace(item.prompt).slice(0, MAX_PROMPT_CHARS);
70
+ const key = prompt.toLocaleLowerCase();
71
+ if (!label || !prompt || seenPrompts.has(key))
72
+ continue;
73
+ seenPrompts.add(key);
74
+ suggestions.push({
75
+ id: stableSuggestionId(label, prompt),
76
+ label,
77
+ prompt,
78
+ });
79
+ if (suggestions.length >= maxItems)
80
+ break;
81
+ }
82
+ return suggestions;
83
+ }
84
+ /** Generate optional next-message suggestions without affecting the main turn. */
85
+ export async function generateChatSuggestions(input) {
86
+ const finalText = input.finalText.trim();
87
+ if (!finalText || input.signal?.aborted)
88
+ return { suggestions: [] };
89
+ const maxItems = Math.max(2, Math.min(4, Math.trunc(input.maxItems)));
90
+ const controller = new AbortController();
91
+ const abortFromParent = () => controller.abort(input.signal?.reason);
92
+ input.signal?.addEventListener("abort", abortFromParent, { once: true });
93
+ const timeoutMs = Math.max(1, input.timeoutMs ?? DEFAULT_TIMEOUT_MS);
94
+ let timeoutId;
95
+ const timeout = new Promise((_, reject) => {
96
+ timeoutId = setTimeout(() => {
97
+ controller.abort(new Error("Chat suggestion generation timed out"));
98
+ reject(new Error("Chat suggestion generation timed out"));
99
+ }, timeoutMs);
100
+ });
101
+ try {
102
+ const generate = input.generate
103
+ ?? generateText;
104
+ const response = await Promise.race([
105
+ generate({
106
+ model: input.model,
107
+ system: [
108
+ "Suggest concise, useful next messages the user could send after this assistant response.",
109
+ "Each label is a short action. Each prompt is the exact standalone user message to send.",
110
+ "Do not repeat the assistant response. Do not invent unsupported capabilities.",
111
+ "Return no suggestions when there is no meaningful next step.",
112
+ ].join(" "),
113
+ prompt: JSON.stringify({
114
+ maxItems,
115
+ ...(input.guidance ? { guidance: input.guidance } : {}),
116
+ conversation: boundedConversation(input.messages),
117
+ assistantResponse: finalText.slice(-MAX_FINAL_TEXT_CHARS),
118
+ }),
119
+ output: Output.object({ schema: generatedSuggestionsSchema }),
120
+ temperature: 0.2,
121
+ maxOutputTokens: 384,
122
+ abortSignal: controller.signal,
123
+ ...(input.providerOptions
124
+ ? { providerOptions: input.providerOptions }
125
+ : {}),
126
+ }),
127
+ timeout,
128
+ ]);
129
+ const suggestions = sanitizeSuggestions(response.output, maxItems);
130
+ return {
131
+ suggestions,
132
+ ...(response.usage ? { usage: response.usage } : {}),
133
+ ...(response.providerMetadata
134
+ ? { providerMetadata: response.providerMetadata }
135
+ : {}),
136
+ };
137
+ }
138
+ catch {
139
+ return { suggestions: [] };
140
+ }
141
+ finally {
142
+ if (timeoutId)
143
+ clearTimeout(timeoutId);
144
+ input.signal?.removeEventListener("abort", abortFromParent);
145
+ }
146
+ }
147
+ //# sourceMappingURL=chat-suggestions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-suggestions.js","sourceRoot":"","sources":["../../../src/routes/completions/chat-suggestions.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=chat-suggestions.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-suggestions.test.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/chat-suggestions.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,100 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { generateChatSuggestions } from "./chat-suggestions.js";
3
+ const model = {};
4
+ describe("generateChatSuggestions", () => {
5
+ it("returns a bounded, kind-free public primitive", async () => {
6
+ const generate = vi.fn().mockResolvedValue({
7
+ output: {
8
+ suggestions: [
9
+ { label: " Add tests ", prompt: " Add tests for this change. " },
10
+ { label: "Duplicate", prompt: "Add tests for this change." },
11
+ { label: "Review", prompt: "Review the implementation for edge cases." },
12
+ { label: "Document", prompt: "Document the public API." },
13
+ ],
14
+ },
15
+ usage: { inputTokens: 10, outputTokens: 8, totalTokens: 18 },
16
+ providerMetadata: { gateway: { generationId: "gen_1" } },
17
+ });
18
+ const result = await generateChatSuggestions({
19
+ model,
20
+ messages: [{ role: "user", content: "Implement this" }],
21
+ finalText: "The implementation is ready.",
22
+ maxItems: 2,
23
+ generate,
24
+ });
25
+ expect(result.suggestions).toHaveLength(2);
26
+ expect(result.suggestions[0]).toEqual({
27
+ id: expect.stringMatching(/^suggestion_/),
28
+ label: "Add tests",
29
+ prompt: "Add tests for this change.",
30
+ });
31
+ expect(result.suggestions[0]).not.toHaveProperty("kind");
32
+ expect(result.suggestions[1]?.label).toBe("Review");
33
+ expect(result.usage).toEqual({ inputTokens: 10, outputTokens: 8, totalTokens: 18 });
34
+ expect(result.providerMetadata).toEqual({ gateway: { generationId: "gen_1" } });
35
+ });
36
+ it("passes only bounded conversation text and project guidance", async () => {
37
+ const generate = vi.fn().mockResolvedValue({ output: { suggestions: [] } });
38
+ await generateChatSuggestions({
39
+ model,
40
+ messages: Array.from({ length: 20 }, (_, index) => ({
41
+ role: index % 2 === 0 ? "user" : "assistant",
42
+ content: `message-${index}-${"x".repeat(2_000)}`,
43
+ })),
44
+ finalText: "done",
45
+ maxItems: 3,
46
+ guidance: "Prefer concrete next actions.",
47
+ generate,
48
+ });
49
+ const call = generate.mock.calls[0]?.[0];
50
+ expect(String(call.prompt).length).toBeLessThan(10_000);
51
+ expect(String(call.prompt)).toContain("Prefer concrete next actions.");
52
+ expect(call).not.toHaveProperty("tools");
53
+ });
54
+ it("fails open for invalid model output", async () => {
55
+ const result = await generateChatSuggestions({
56
+ model,
57
+ messages: [{ role: "user", content: "hello" }],
58
+ finalText: "hi",
59
+ maxItems: 3,
60
+ generate: vi.fn().mockResolvedValue({ output: { suggestions: "invalid" } }),
61
+ });
62
+ expect(result).toEqual({ suggestions: [] });
63
+ });
64
+ it("fails open when the auxiliary model call rejects", async () => {
65
+ const result = await generateChatSuggestions({
66
+ model,
67
+ messages: [{ role: "user", content: "hello" }],
68
+ finalText: "hi",
69
+ maxItems: 3,
70
+ generate: vi.fn().mockRejectedValue(new Error("provider unavailable")),
71
+ });
72
+ expect(result).toEqual({ suggestions: [] });
73
+ });
74
+ it("times out without failing the main completion", async () => {
75
+ const result = await generateChatSuggestions({
76
+ model,
77
+ messages: [{ role: "user", content: "hello" }],
78
+ finalText: "hi",
79
+ maxItems: 3,
80
+ timeoutMs: 5,
81
+ generate: vi.fn().mockImplementation(({ abortSignal }) => new Promise((_, reject) => {
82
+ abortSignal.addEventListener("abort", () => reject(new Error("aborted")), { once: true });
83
+ })),
84
+ });
85
+ expect(result).toEqual({ suggestions: [] });
86
+ });
87
+ it("skips generation for blank assistant output", async () => {
88
+ const generate = vi.fn();
89
+ const result = await generateChatSuggestions({
90
+ model,
91
+ messages: [{ role: "user", content: "hello" }],
92
+ finalText: " ",
93
+ maxItems: 3,
94
+ generate,
95
+ });
96
+ expect(result).toEqual({ suggestions: [] });
97
+ expect(generate).not.toHaveBeenCalled();
98
+ });
99
+ });
100
+ //# sourceMappingURL=chat-suggestions.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chat-suggestions.test.js","sourceRoot":"","sources":["../../../src/routes/completions/chat-suggestions.test.ts"],"names":[],"mappings":"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"}
@@ -91,6 +91,11 @@ export declare function runNonStreamingChatViaRun(c: Context, execution: ChatCom
91
91
  message: string;
92
92
  type: "model_error";
93
93
  code: "model_request_failed";
94
+ } | {
95
+ message: string;
96
+ type: "invalid_request_error";
97
+ code: "invalid_response_format_output";
98
+ param: "response_format";
94
99
  };
95
100
  }, any, "json">) | (Response & import("hono").TypedResponse<{
96
101
  id: string;
@@ -1 +1 @@
1
- {"version":3,"file":"chat-via-run-handler.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/chat-via-run-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAE3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAiCjE,+DAA+D;AAC/D,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,uBAAuB,GAAG,oBAAoB,CA6C9F;AAiBD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,cAAc,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAClE,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAgMD,gDAAgD;AAChD,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,uBAAuB,qBAuGpF;AAED,oDAAoD;AACpD,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEA4E7F;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,uBAAuB,EAClC,KAAK,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACtF,OAAO,CAAC,oBAAoB,CAAC,CAyE/B"}
1
+ {"version":3,"file":"chat-via-run-handler.d.ts","sourceRoot":"","sources":["../../../src/routes/completions/chat-via-run-handler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAG3D,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAwCjE,+DAA+D;AAC/D,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,uBAAuB,GAAG,oBAAoB,CA8C9F;AAiBD,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,GAAG,EAAE,CAAC;IACjB,KAAK,EAAE;QAAE,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1E,gBAAgB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC3C,cAAc,CAAC,EAAE;QAAE,EAAE,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAA;KAAE,CAAC;IAClE,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC;IAC1E,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;CACjE;AAuMD,gDAAgD;AAChD,wBAAsB,gBAAgB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,uBAAuB,qBAqIpF;AAED,oDAAoD;AACpD,wBAAsB,yBAAyB,CAAC,CAAC,EAAE,OAAO,EAAE,SAAS,EAAE,uBAAuB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;oEA6G7F;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,SAAS,EAAE,uBAAuB,EAClC,KAAK,GAAE;IAAE,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAAC,MAAM,CAAC,EAAE,WAAW,CAAA;CAAO,GACtF,OAAO,CAAC,oBAAoB,CAAC,CA6E/B"}
@@ -1,8 +1,10 @@
1
1
  import { streamSSE } from "hono/streaming";
2
2
  import { agentConfigForModelAttempt, completionResolvedModelInfo, MAX_TURNS } from "./agent-step-runner.js";
3
- import { completionResponse, guardrailErrorEnvelope, modelErrorEnvelope, modelNotFoundEnvelope, sseChunk, } from "./sse.js";
3
+ import { completionResponse, guardrailErrorEnvelope, modelErrorEnvelope, modelNotFoundEnvelope, sseChunk, ssePolpoChunk, structuredOutputErrorEnvelope, } from "./sse.js";
4
4
  import { persistAssistantMessage, emitFileChanged, toAITools, CLIENT_SIDE_TOOLS, CLIENT_SIDE_TOOL_NAMES, } from "./tool-mapping.js";
5
+ import { suggestionsForCompletion } from "./chat-interaction-runtime.js";
5
6
  import { applyCompletionOutputPolicy, streamingOutputPolicyMode, } from "./output-guardrails.js";
7
+ import { finalizeResponseFormatText, isStructuredResponseFormat, } from "./structured-output.js";
6
8
  /** Best-effort session title from the first user message. */
7
9
  function firstUserText(aiMessages) {
8
10
  const u = aiMessages.find((m) => m?.role === "user");
@@ -40,11 +42,12 @@ export function buildChatRunInjection(execution) {
40
42
  providerOptions: providerOpts,
41
43
  maxTurns: MAX_TURNS,
42
44
  toolChoice: modelToolChoice,
45
+ output: execution.modelOutput,
43
46
  seedMessages: aiMessages,
44
- toolSet: { ...toAITools(effectiveTools), ...(extraAiTools ?? {}), ...CLIENT_SIDE_TOOLS },
47
+ toolSet: { ...toAITools(effectiveTools), ...(extraAiTools ?? {}), ...(execution.clientSideTools ?? CLIENT_SIDE_TOOLS) },
45
48
  activeToolNames: execution.activeToolNames,
46
49
  executor: effectiveToolExecutor,
47
- clientSideToolNames: CLIENT_SIDE_TOOL_NAMES,
50
+ clientSideToolNames: execution.clientSideToolNames ?? CLIENT_SIDE_TOOL_NAMES,
48
51
  providerToolNames: new Set(Object.keys(extraAiTools ?? {})),
49
52
  sandbox: execution.body.sandbox,
50
53
  compactionTools: effectiveTools,
@@ -181,7 +184,7 @@ function newState() {
181
184
  outputPolicyApplied: false,
182
185
  };
183
186
  }
184
- async function applyStateOutputPolicy(execution, state, mode, signal) {
187
+ async function applyStateOutputPolicy(execution, state, mode, signal, validateStructured = true) {
185
188
  if (state.outputPolicyApplied)
186
189
  return;
187
190
  state.finalText = await applyCompletionOutputPolicy({
@@ -193,6 +196,9 @@ async function applyStateOutputPolicy(execution, state, mode, signal) {
193
196
  sessionId: execution.sessionId,
194
197
  signal,
195
198
  });
199
+ if (validateStructured) {
200
+ state.finalText = await finalizeResponseFormatText(execution.body.response_format, state.finalText);
201
+ }
196
202
  state.outputPolicyApplied = true;
197
203
  }
198
204
  function captureRunFailure(state, outcome) {
@@ -238,9 +244,11 @@ export async function streamChatViaRun(c, execution) {
238
244
  let assistantMsgId = null;
239
245
  const state = newState();
240
246
  const outputMode = streamingOutputPolicyMode(deps.runOutputPolicy);
247
+ const structuredResponse = isStructuredResponseFormat(body.response_format);
248
+ let suggestions = [];
241
249
  let writeChain = Promise.resolve();
242
250
  const write = (data) => { writeChain = writeChain.then(() => stream.writeSSE({ data })).catch(() => { }); };
243
- const onEvent = makeOnEvent(execution, state, write, outputMode !== "buffer");
251
+ const onEvent = makeOnEvent(execution, state, write, outputMode !== "buffer" && !structuredResponse);
244
252
  try {
245
253
  await stream.writeSSE({ data: sseChunk(completionId, { role: "assistant" }) });
246
254
  if (sessionStore && sessionId) {
@@ -256,10 +264,13 @@ export async function streamChatViaRun(c, execution) {
256
264
  captureRunFailure(state, outcome);
257
265
  await writeChain;
258
266
  if (state.errorEvent && !abortController.signal.aborted) {
267
+ const structuredOutput = structuredOutputErrorEnvelope(state.errorEvent, structuredResponse);
259
268
  const guardrail = guardrailErrorEnvelope(state.errorEvent);
260
269
  const notFound = modelNotFoundEnvelope(state.errorEvent, m?.id, body.agent);
261
- const error = guardrail ?? notFound ?? modelErrorEnvelope(state.errorEvent);
262
- const text = guardrail || notFound ? "" : `Model request failed: ${error.message}`;
270
+ const error = structuredOutput ?? guardrail ?? notFound ?? modelErrorEnvelope(state.errorEvent);
271
+ const text = structuredOutput || guardrail || notFound
272
+ ? ""
273
+ : `Model request failed: ${error.message}`;
263
274
  if (text) {
264
275
  state.finalText += text;
265
276
  await stream.writeSSE({ data: sseChunk(completionId, { content: text }) });
@@ -268,8 +279,8 @@ export async function streamChatViaRun(c, execution) {
268
279
  await stream.writeSSE({ data: "[DONE]" });
269
280
  }
270
281
  else if (state.clientReturn) {
271
- await applyStateOutputPolicy(execution, state, outputMode === "buffer" ? "enforce" : "audit", abortController.signal);
272
- if (outputMode === "buffer" && state.finalText) {
282
+ await applyStateOutputPolicy(execution, state, outputMode === "buffer" ? "enforce" : "audit", abortController.signal, false);
283
+ if ((outputMode === "buffer" || structuredResponse) && state.finalText) {
273
284
  await stream.writeSSE({ data: sseChunk(completionId, { content: state.finalText }) });
274
285
  }
275
286
  state.toolCallsAccum.push({ id: state.clientReturn.id, name: state.clientReturn.name, arguments: state.clientReturn.arguments, state: "interrupted" });
@@ -278,18 +289,29 @@ export async function streamChatViaRun(c, execution) {
278
289
  }
279
290
  else if (!abortController.signal.aborted) {
280
291
  await applyStateOutputPolicy(execution, state, outputMode === "buffer" ? "enforce" : "audit", abortController.signal);
281
- if (outputMode === "buffer" && state.finalText) {
292
+ if ((outputMode === "buffer" || structuredResponse) && state.finalText) {
282
293
  await stream.writeSSE({ data: sseChunk(completionId, { content: state.finalText }) });
283
294
  }
295
+ suggestions = await suggestionsForCompletion(execution, state.finalText, abortController.signal);
296
+ if (suggestions.length > 0) {
297
+ await stream.writeSSE({
298
+ data: ssePolpoChunk(completionId, { suggestions }),
299
+ });
300
+ }
284
301
  await stream.writeSSE({ data: sseChunk(completionId, {}, "stop") });
285
302
  await stream.writeSSE({ data: "[DONE]" });
286
303
  }
287
304
  }
288
305
  catch (err) {
289
306
  if (!((err instanceof DOMException && err.name === "AbortError") || abortController.signal.aborted)) {
307
+ const structuredOutput = structuredOutputErrorEnvelope(err, structuredResponse);
290
308
  const guardrail = guardrailErrorEnvelope(err);
291
309
  const notFound = modelNotFoundEnvelope(err, m?.id, body.agent);
292
- if (guardrail) {
310
+ if (structuredOutput) {
311
+ await stream.writeSSE({ data: sseChunk(completionId, {}, "stop", { error: structuredOutput }) });
312
+ await stream.writeSSE({ data: "[DONE]" });
313
+ }
314
+ else if (guardrail) {
293
315
  state.finalText = guardrail.message;
294
316
  state.outputPolicyApplied = true;
295
317
  await stream.writeSSE({ data: sseChunk(completionId, {}, "stop", { error: guardrail }) });
@@ -312,7 +334,7 @@ export async function streamChatViaRun(c, execution) {
312
334
  finally {
313
335
  clearInterval(heartbeat);
314
336
  try {
315
- await finishCommon(execution, state, assistantMsgId);
337
+ await finishCommon(execution, state, assistantMsgId, suggestions.length > 0 ? { suggestions } : undefined);
316
338
  }
317
339
  finally {
318
340
  await steeringScope.release();
@@ -327,6 +349,7 @@ export async function runNonStreamingChatViaRun(c, execution) {
327
349
  const steeringScope = await createRunSteeringScope(execution);
328
350
  let assistantMsgId = null;
329
351
  const state = newState();
352
+ let suggestions = [];
330
353
  const onEvent = makeOnEvent(execution, state, () => { });
331
354
  try {
332
355
  if (sessionStore && sessionId) {
@@ -341,7 +364,7 @@ export async function runNonStreamingChatViaRun(c, execution) {
341
364
  });
342
365
  captureRunFailure(state, outcome);
343
366
  if (!state.errorEvent) {
344
- await applyStateOutputPolicy(execution, state, "enforce");
367
+ await applyStateOutputPolicy(execution, state, "enforce", undefined, !state.clientReturn);
345
368
  }
346
369
  if (state.clientReturn) {
347
370
  return c.json({
@@ -357,6 +380,7 @@ export async function runNonStreamingChatViaRun(c, execution) {
357
380
  });
358
381
  }
359
382
  if (state.errorEvent) {
383
+ const structuredOutput = structuredOutputErrorEnvelope(state.errorEvent, isStructuredResponseFormat(body.response_format));
360
384
  const guardrail = guardrailErrorEnvelope(state.errorEvent);
361
385
  if (guardrail) {
362
386
  return c.json({ error: guardrail }, (guardrail.code === "guardrail_approval_required" ? 409 : 403));
@@ -364,13 +388,20 @@ export async function runNonStreamingChatViaRun(c, execution) {
364
388
  const notFound = modelNotFoundEnvelope(state.errorEvent, m?.id, body.agent);
365
389
  if (notFound)
366
390
  return c.json({ error: notFound }, 400);
367
- const error = modelErrorEnvelope(state.errorEvent);
368
- state.finalText = state.finalText || `Model request failed: ${error.message}`;
391
+ const error = structuredOutput ?? modelErrorEnvelope(state.errorEvent);
392
+ if (!structuredOutput) {
393
+ state.finalText = state.finalText || `Model request failed: ${error.message}`;
394
+ }
369
395
  return c.json({ error }, 400);
370
396
  }
371
- return c.json(completionResponse(completionId, state.finalText, state.totalUsage));
397
+ suggestions = await suggestionsForCompletion(execution, state.finalText, c.req.raw.signal);
398
+ return c.json(completionResponse(completionId, state.finalText, state.totalUsage, suggestions.length > 0 ? { polpo: { suggestions } } : undefined));
372
399
  }
373
400
  catch (err) {
401
+ const structuredOutput = structuredOutputErrorEnvelope(err, isStructuredResponseFormat(body.response_format));
402
+ if (structuredOutput) {
403
+ return c.json({ error: structuredOutput }, 400);
404
+ }
374
405
  const guardrail = guardrailErrorEnvelope(err);
375
406
  if (guardrail) {
376
407
  state.finalText = guardrail.message;
@@ -386,7 +417,10 @@ export async function runNonStreamingChatViaRun(c, execution) {
386
417
  }
387
418
  finally {
388
419
  try {
389
- await finishCommon(execution, state, assistantMsgId, { emptyFallback: "[Response interrupted]" });
420
+ await finishCommon(execution, state, assistantMsgId, {
421
+ emptyFallback: "[Response interrupted]",
422
+ ...(suggestions.length > 0 ? { suggestions } : {}),
423
+ });
390
424
  }
391
425
  finally {
392
426
  await steeringScope.release();
@@ -454,10 +488,11 @@ export async function runChatTurnViaRun(execution, hooks = {}) {
454
488
  }
455
489
  let error;
456
490
  if (state.errorEvent) {
491
+ const structuredOutput = structuredOutputErrorEnvelope(state.errorEvent, isStructuredResponseFormat(body.response_format));
457
492
  const guardrail = guardrailErrorEnvelope(state.errorEvent);
458
493
  const notFound = modelNotFoundEnvelope(state.errorEvent, m?.id, body.agent);
459
- error = guardrail ?? notFound ?? modelErrorEnvelope(state.errorEvent);
460
- if (!guardrail && !notFound && !state.finalText) {
494
+ error = structuredOutput ?? guardrail ?? notFound ?? modelErrorEnvelope(state.errorEvent);
495
+ if (!structuredOutput && !guardrail && !notFound && !state.finalText) {
461
496
  state.finalText = `Model request failed: ${error.message}`;
462
497
  }
463
498
  }