@cartanova/qgrid-cli 2.5.3 → 2.5.5

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 (65) hide show
  1. package/README.ko.md +7 -2
  2. package/README.md +7 -2
  3. package/bundle/dist/application/monit/monit.frame.js +3 -3
  4. package/bundle/dist/application/qgrid/oauth.js +1 -1
  5. package/bundle/dist/application/qgrid/qgrid.dispatcher.js +36 -14
  6. package/bundle/dist/application/qgrid/qgrid.frame.js +19 -5
  7. package/bundle/dist/application/qgrid/tool-emulation-schema.js +93 -0
  8. package/bundle/dist/application/qgrid/tool-emulation.js +116 -96
  9. package/bundle/dist/application/request-log/request-log.model.js +3 -3
  10. package/bundle/dist/application/request-log-step/request-log-step.model.js +2 -2
  11. package/bundle/dist/application/token/token.model.js +2 -2
  12. package/bundle/dist/server-error-handler.js +15 -0
  13. package/bundle/dist/sonamu.config.js +4 -7
  14. package/bundle/dist/utils/providers/anthropic/anthropic-dispatcher.js +4 -3
  15. package/bundle/dist/utils/providers/anthropic/claude-session.js +7 -5
  16. package/bundle/dist/utils/providers/common/json-schema-keywords.js +77 -0
  17. package/bundle/dist/utils/providers/common/schema-validation.js +178 -0
  18. package/bundle/dist/utils/providers/common/strictifier.js +332 -42
  19. package/bundle/dist/utils/providers/openai/codex-worker.js +16 -8
  20. package/bundle/dist/utils/providers/openai/openai-dispatcher.js +5 -37
  21. package/bundle/dist/utils/providers/openai/openai-worker-pool-config.js +37 -0
  22. package/bundle/src/application/monit/monit.frame.ts +1 -1
  23. package/bundle/src/application/qgrid/qgrid-run-lifecycle.test.ts +103 -0
  24. package/bundle/src/application/qgrid/qgrid.dispatcher.test.ts +337 -0
  25. package/bundle/src/application/qgrid/qgrid.dispatcher.ts +45 -25
  26. package/bundle/src/application/qgrid/qgrid.frame.test.ts +580 -8
  27. package/bundle/src/application/qgrid/qgrid.frame.ts +28 -1
  28. package/bundle/src/application/qgrid/tool-emulation-schema.test.ts +188 -0
  29. package/bundle/src/application/qgrid/tool-emulation-schema.ts +177 -0
  30. package/bundle/src/application/qgrid/tool-emulation.test.ts +285 -67
  31. package/bundle/src/application/qgrid/tool-emulation.ts +199 -102
  32. package/bundle/src/server-error-handler.ts +22 -0
  33. package/bundle/src/sonamu.config.test.ts +37 -0
  34. package/bundle/src/sonamu.config.ts +2 -5
  35. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.test.ts +44 -0
  36. package/bundle/src/utils/providers/anthropic/anthropic-dispatcher.ts +2 -2
  37. package/bundle/src/utils/providers/anthropic/claude-session.test.ts +10 -0
  38. package/bundle/src/utils/providers/anthropic/claude-session.ts +6 -1
  39. package/bundle/src/utils/providers/common/json-schema-keywords.ts +72 -0
  40. package/bundle/src/utils/providers/common/schema-validation.test.ts +284 -0
  41. package/bundle/src/utils/providers/common/schema-validation.ts +308 -0
  42. package/bundle/src/utils/providers/common/strictifier.test.ts +746 -0
  43. package/bundle/src/utils/providers/common/strictifier.ts +520 -52
  44. package/bundle/src/utils/providers/openai/codex-worker.test.ts +31 -0
  45. package/bundle/src/utils/providers/openai/codex-worker.ts +12 -4
  46. package/bundle/src/utils/providers/openai/openai-dispatcher.test.ts +6 -4
  47. package/bundle/src/utils/providers/openai/openai-dispatcher.ts +6 -79
  48. package/bundle/src/utils/providers/openai/openai-worker-pool-config.ts +80 -0
  49. package/bundle/web-dist/client/assets/{index-DXdGBfB8.js → index-CN_kMswh.js} +3 -3
  50. package/bundle/web-dist/client/assets/{logs-D_j1yxC4.js → logs-6nYj9OKL.js} +1 -1
  51. package/bundle/web-dist/client/assets/{monit-CIToyoxW.js → monit-CaPH9Uwc.js} +1 -1
  52. package/bundle/web-dist/client/assets/{routes-Dd21A-wi.js → routes-Dh5HPi-b.js} +1 -1
  53. package/bundle/web-dist/client/assets/{show-DfePWRbc.js → show-Be_LVd9d.js} +1 -1
  54. package/bundle/web-dist/client/assets/{tokens-B4MV2fdk.js → tokens-CAi3zu_0.js} +1 -1
  55. package/bundle/web-dist/client/index.html +1 -1
  56. package/bundle/web-dist/server/entry-server.generated.js +1 -1
  57. package/package.json +1 -1
  58. package/skills/qgrid/references/ai-sdk-provider-contract.md +34 -2
  59. package/skills/qgrid/references/anthropic-claude-code-runtime.md +3 -0
  60. package/skills/qgrid/references/cli-env-and-server-boot.md +1 -1
  61. package/skills/qgrid/references/decision-rationale.md +10 -1
  62. package/skills/qgrid/references/docs-workflow.md +9 -0
  63. package/skills/qgrid/references/repo-map.md +2 -1
  64. package/skills/qgrid/references/tool-calling-and-multiturn.md +145 -6
  65. package/skills/qgrid/references/verification-and-debugging.md +16 -1
@@ -1,74 +1,34 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
 
3
+ import { CALLER_SCHEMA_LIMITS } from "../../utils/providers/common/schema-validation";
3
4
  import { type QgridTool } from "./qgrid.types";
4
- import { applyToolCallEmulation, buildToolCallSchema } from "./tool-emulation";
5
+ import { applyToolCallEmulation, ToolCallEmulationError } from "./tool-emulation";
5
6
 
6
- describe("buildToolCallSchema", () => {
7
- it("documents that emulated tools must be requested through StructuredOutput", () => {
8
- const tools: QgridTool[] = [
9
- {
10
- name: "getWeather",
11
- description: "Get weather for a city",
12
- inputSchema: {
13
- type: "object",
14
- properties: { city: { type: "string" } },
15
- required: ["city"],
16
- additionalProperties: false,
17
- },
18
- },
19
- ];
20
-
21
- const schema = buildToolCallSchema(tools) as {
22
- description?: string;
23
- properties?: {
24
- action?: { description?: string };
25
- toolCalls?: {
26
- description?: string;
27
- anyOf?: Array<{
28
- items?: {
29
- properties?: {
30
- toolName?: { description?: string };
31
- };
32
- };
33
- }>;
34
- };
35
- };
36
- };
37
-
38
- expect(schema.description).toContain("StructuredOutput");
39
- expect(schema.description).toContain(
40
- "Do not invoke listed client tools as native Claude Code tools",
41
- );
42
- expect(schema.properties?.action?.description).toContain("tool_call");
43
- expect(schema.properties?.toolCalls?.description).toContain(
44
- "Do not call these tools as native",
45
- );
46
- expect(
47
- schema.properties?.toolCalls?.anyOf?.[0]?.items?.properties?.toolName?.description,
48
- ).toContain("getWeather");
49
- });
50
- });
7
+ const baseResult = {
8
+ text: "",
9
+ tokenName: "token",
10
+ model: "gpt-5.6-terra",
11
+ usage: {
12
+ input_tokens: 1,
13
+ output_tokens: 1,
14
+ cache_creation_input_tokens: 0,
15
+ cache_read_input_tokens: 0,
16
+ },
17
+ durationMs: 10,
18
+ ttftMs: 0,
19
+ costUsd: 0,
20
+ costSource: "pricing_table" as const,
21
+ };
51
22
 
52
23
  describe("applyToolCallEmulation image parts", () => {
53
- const baseResult = {
54
- text: "here is your image",
55
- tokenName: "token",
56
- model: "gpt-5.5",
57
- usage: {
58
- input_tokens: 1,
59
- output_tokens: 1,
60
- cache_creation_input_tokens: 0,
61
- cache_read_input_tokens: 0,
62
- },
63
- durationMs: 10,
64
- ttftMs: 0,
65
- costUsd: 0,
66
- costSource: "pricing_table" as const,
67
- };
68
24
  const img = { data: "iVBORw0KGgoBAgM", revisedPrompt: "a red circle" };
69
25
 
70
26
  it("appends an image part after text when images are present (no tools)", () => {
71
- const out = applyToolCallEmulation(baseResult, undefined, undefined, [img]);
27
+ const out = applyToolCallEmulation(
28
+ { ...baseResult, text: "here is your image" },
29
+ undefined,
30
+ { images: [img] },
31
+ );
72
32
  expect(out.content).toEqual([
73
33
  { type: "text", text: "here is your image" },
74
34
  { type: "image", data: "iVBORw0KGgoBAgM", revisedPrompt: "a red circle" },
@@ -78,17 +38,275 @@ describe("applyToolCallEmulation image parts", () => {
78
38
  });
79
39
 
80
40
  it("appends multiple images preserving order", () => {
81
- const out = applyToolCallEmulation(baseResult, undefined, undefined, [
82
- { data: "iVBORw0KGgoAAA", revisedPrompt: "one" },
83
- { data: "iVBORw0KGgoBBB", revisedPrompt: "two" },
84
- ]);
41
+ const out = applyToolCallEmulation(
42
+ { ...baseResult, text: "here is your image" },
43
+ undefined,
44
+ {
45
+ images: [
46
+ { data: "iVBORw0KGgoAAA", revisedPrompt: "one" },
47
+ { data: "iVBORw0KGgoBBB", revisedPrompt: "two" },
48
+ ],
49
+ },
50
+ );
85
51
  const images = out.content.filter((c) => c.type === "image");
86
52
  expect(images).toHaveLength(2);
87
53
  expect(images.map((c) => (c.type === "image" ? c.revisedPrompt : null))).toEqual(["one", "two"]);
88
54
  });
89
55
 
90
56
  it("leaves content unchanged when no images are passed (regression guard)", () => {
91
- const out = applyToolCallEmulation(baseResult, undefined, undefined);
57
+ const out = applyToolCallEmulation({ ...baseResult, text: "here is your image" });
92
58
  expect(out.content).toEqual([{ type: "text", text: "here is your image" }]);
93
59
  });
94
60
  });
61
+
62
+ describe("applyToolCallEmulation envelope validation", () => {
63
+ const tools: QgridTool[] = [
64
+ {
65
+ name: "lookup",
66
+ inputSchema: {
67
+ type: "object",
68
+ properties: { key: { type: "string" } },
69
+ required: ["key"],
70
+ },
71
+ },
72
+ ];
73
+
74
+ function applyEnvelope(
75
+ envelope: unknown,
76
+ answerMode: "legacy" | "structured" = "structured",
77
+ ) {
78
+ return applyToolCallEmulation(
79
+ { ...baseResult, text: JSON.stringify(envelope) },
80
+ tools,
81
+ { answerMode },
82
+ );
83
+ }
84
+
85
+ it("preserves a legacy tools-only string answer", () => {
86
+ const out = applyEnvelope(
87
+ { action: "answer", answer: "done", toolCalls: null },
88
+ "legacy",
89
+ );
90
+
91
+ expect(out.text).toBe("done");
92
+ expect(out.content).toEqual([{ type: "text", text: "done" }]);
93
+ expect(out.finishReason).toBe("stop");
94
+ });
95
+
96
+ it.each([
97
+ [{ translated: "완료" }, '{"translated":"완료"}'],
98
+ [["first", 2, true], '["first",2,true]'],
99
+ ["scalar", '"scalar"'],
100
+ [42, "42"],
101
+ [false, "false"],
102
+ ])("serializes structured answer %j as JSON text", (answer, expected) => {
103
+ const out = applyEnvelope({ action: "answer", answer, toolCalls: null });
104
+
105
+ expect(out.text).toBe(expected);
106
+ expect(out.content).toEqual([{ type: "text", text: expected }]);
107
+ expect(out.finishReason).toBe("stop");
108
+ });
109
+
110
+ it("preserves prototype-sensitive keys in a structured answer", () => {
111
+ const out = applyToolCallEmulation(
112
+ {
113
+ ...baseResult,
114
+ text: String.raw`{"action":"answer","answer":{"__proto__":"kept","nested":{"__proto__":1}},"toolCalls":null}`,
115
+ },
116
+ tools,
117
+ { answerMode: "structured" },
118
+ );
119
+
120
+ expect(out.text).toBe(
121
+ String.raw`{"__proto__":"kept","nested":{"__proto__":1}}`,
122
+ );
123
+ });
124
+
125
+ it("accepts null or empty toolCalls on a structured answer branch", () => {
126
+ for (const envelope of [
127
+ { action: "answer", answer: { ok: true }, toolCalls: null },
128
+ { action: "answer", answer: { ok: true }, toolCalls: [] },
129
+ ]) {
130
+ expect(applyEnvelope(envelope).text).toBe('{"ok":true}');
131
+ }
132
+ });
133
+
134
+ it("keeps absent toolCalls compatible on a legacy answer branch", () => {
135
+ expect(applyEnvelope({ action: "answer", answer: "done" }, "legacy").text).toBe("done");
136
+ });
137
+
138
+ it.each([
139
+ [{ action: "answer", answer: null, toolCalls: null }, undefined],
140
+ [{ action: "answer", toolCalls: [] }, undefined],
141
+ [{ action: "unexpected", answer: "fallback answer", toolCalls: null }, "fallback answer"],
142
+ [{ answer: "", toolCalls: null }, ""],
143
+ [42, undefined],
144
+ ])("preserves tolerant legacy final-answer fallback for %j", (envelope, expected) => {
145
+ const outerText = JSON.stringify(envelope);
146
+ const out = applyToolCallEmulation(
147
+ { ...baseResult, text: outerText },
148
+ tools,
149
+ { answerMode: "legacy" },
150
+ );
151
+
152
+ expect(out.text).toBe(expected ?? outerText);
153
+ expect(out.finishReason).toBe("stop");
154
+ });
155
+
156
+ it.each([
157
+ { action: "tool_call", answer: null, toolCalls: null },
158
+ { action: "tool_call", answer: null },
159
+ { action: "tool_call", answer: null, toolCalls: [] },
160
+ ])("preserves tolerant legacy empty tool_call envelopes: %j", (envelope) => {
161
+ const out = applyEnvelope(envelope, "legacy");
162
+
163
+ expect(out.content).toEqual([]);
164
+ expect(out.finishReason).toBe("tool-calls");
165
+ });
166
+
167
+ it("maps a coherent non-empty tool_call branch", () => {
168
+ const out = applyEnvelope({
169
+ action: "tool_call",
170
+ answer: null,
171
+ toolCalls: [{ toolName: "lookup", args: '{"key":"value"}' }],
172
+ });
173
+
174
+ expect(out.content).toEqual([
175
+ expect.objectContaining({
176
+ type: "tool-call",
177
+ toolName: "lookup",
178
+ input: '{"key":"value"}',
179
+ }),
180
+ ]);
181
+ expect(out.finishReason).toBe("tool-calls");
182
+ });
183
+
184
+ it.each([
185
+ { action: "answer", answer: null, toolCalls: null },
186
+ { action: "answer", answer: "done", toolCalls: [{}] },
187
+ { action: "tool_call", answer: "mixed", toolCalls: [{}] },
188
+ { action: "tool_call", answer: null, toolCalls: [] },
189
+ { action: "tool_call", answer: null, toolCalls: [{ toolName: "lookup" }] },
190
+ { action: "unexpected", answer: "done", toolCalls: null },
191
+ ["not", "an", "object"],
192
+ { action: "answer", answer: { ok: true } },
193
+ { action: "answer", answer: { ok: true }, toolCalls: null, extra: true },
194
+ {
195
+ action: "tool_call",
196
+ answer: null,
197
+ toolCalls: [{ toolName: "lookup", args: "{}", extra: true }],
198
+ },
199
+ ])("rejects incoherent structured envelope %j", (envelope) => {
200
+ expect(() => applyEnvelope(envelope)).toThrowError(ToolCallEmulationError);
201
+ });
202
+
203
+ it("rejects unknown tools after decoding a structured tool call", () => {
204
+ expect(() =>
205
+ applyEnvelope({
206
+ action: "tool_call",
207
+ answer: null,
208
+ toolCalls: [{ toolName: "missing", args: "{}" }],
209
+ }),
210
+ ).toThrowError(/unknown emulated tool/);
211
+ });
212
+
213
+ it("does not apply the structured decoder to legacy responses", () => {
214
+ const answer = { ok: true };
215
+ const out = applyEnvelope(
216
+ { action: "answer", answer, toolCalls: [{ ignored: true }] },
217
+ "legacy",
218
+ );
219
+
220
+ expect(out.text).toEqual(answer);
221
+ });
222
+
223
+ it("fails malformed outer JSON explicitly in structured mode", () => {
224
+ expect(() =>
225
+ applyToolCallEmulation(
226
+ { ...baseResult, text: "not-json" },
227
+ tools,
228
+ { answerMode: "structured" },
229
+ ),
230
+ ).toThrowError(ToolCallEmulationError);
231
+ });
232
+
233
+ it("rejects a deeply nested structured envelope without overflowing the stack", () => {
234
+ let answer: unknown = 0;
235
+ for (let depth = 0; depth < CALLER_SCHEMA_LIMITS.maxDepth + 5; depth += 1) {
236
+ answer = [answer];
237
+ }
238
+
239
+ try {
240
+ applyEnvelope({ action: "answer", answer, toolCalls: null });
241
+ throw new Error("expected the structured envelope to be rejected");
242
+ } catch (error) {
243
+ expect(error).toBeInstanceOf(ToolCallEmulationError);
244
+ expect(error).not.toBeInstanceOf(RangeError);
245
+ expect((error as Error).message).toContain("depth limit");
246
+ }
247
+ });
248
+
249
+ it("rejects structured envelopes above the node budget", () => {
250
+ expect(() =>
251
+ applyEnvelope({
252
+ action: "answer",
253
+ answer: Array.from({ length: CALLER_SCHEMA_LIMITS.maxNodes }, () => 0),
254
+ toolCalls: null,
255
+ }),
256
+ ).toThrowError(/structured-output envelope exceeds node limit/);
257
+ });
258
+
259
+ it("rejects structured envelopes above the UTF-8 byte budget before parsing", () => {
260
+ const text = JSON.stringify({
261
+ action: "answer",
262
+ answer: "x".repeat(CALLER_SCHEMA_LIMITS.maxUtf8Bytes),
263
+ toolCalls: null,
264
+ });
265
+
266
+ expect(() =>
267
+ applyToolCallEmulation(
268
+ { ...baseResult, text },
269
+ tools,
270
+ { answerMode: "structured" },
271
+ ),
272
+ ).toThrowError(/structured-output envelope exceeds UTF-8 byte limit/);
273
+ });
274
+
275
+ it("preserves malformed outer JSON fallback in legacy mode", () => {
276
+ const out = applyToolCallEmulation({ ...baseResult, text: "not-json" }, tools);
277
+
278
+ expect(out.text).toBe("not-json");
279
+ expect(out.content).toEqual([{ type: "text", text: "not-json" }]);
280
+ expect(out.finishReason).toBe("stop");
281
+ });
282
+
283
+ it("preserves images after a structured final answer", () => {
284
+ const out = applyToolCallEmulation(
285
+ {
286
+ ...baseResult,
287
+ text: '{"action":"answer","answer":{"ok":true},"toolCalls":null}',
288
+ },
289
+ tools,
290
+ {
291
+ images: [{ data: "image-data", revisedPrompt: "revised" }],
292
+ answerMode: "structured",
293
+ },
294
+ );
295
+
296
+ expect(out.content).toEqual([
297
+ { type: "text", text: '{"ok":true}' },
298
+ { type: "image", data: "image-data", revisedPrompt: "revised" },
299
+ ]);
300
+ });
301
+
302
+ it.each([
303
+ '{"action":"answer","answer":1e400,"toolCalls":null}',
304
+ '{"action":"answer","answer":{"nested":1e400},"toolCalls":null}',
305
+ ])("rejects non-finite JSON numbers: %s", (text) => {
306
+ expect(() =>
307
+ applyToolCallEmulation({ ...baseResult, text }, tools, {
308
+ answerMode: "structured",
309
+ }),
310
+ ).toThrowError(/answer must be valid JSON/);
311
+ });
312
+ });