@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
@@ -0,0 +1,188 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import { type QgridTool } from "./qgrid.types";
4
+ import { buildToolCallSchema } from "./tool-emulation-schema";
5
+
6
+ const userPath = (...path: Array<string | number>) => [
7
+ "$defs",
8
+ "__qgrid_user_output",
9
+ ...path,
10
+ ];
11
+
12
+ describe("buildToolCallSchema", () => {
13
+ const tools: QgridTool[] = [
14
+ {
15
+ name: "getWeather",
16
+ description: "Get weather for a city",
17
+ inputSchema: {
18
+ type: "object",
19
+ properties: { city: { type: "string" } },
20
+ required: ["city"],
21
+ additionalProperties: false,
22
+ },
23
+ },
24
+ ];
25
+
26
+ it("documents that emulated tools must be requested through StructuredOutput", () => {
27
+ const schema = buildToolCallSchema(tools);
28
+
29
+ expect(schema).toHaveProperty(
30
+ "description",
31
+ expect.stringContaining("Do not invoke listed client tools as native Claude Code tools"),
32
+ );
33
+ expect(schema).toHaveProperty(
34
+ ["properties", "action", "description"],
35
+ expect.stringContaining("tool_call"),
36
+ );
37
+ expect(schema).toHaveProperty(
38
+ ["properties", "toolCalls", "description"],
39
+ expect.stringContaining("Do not call these tools as native"),
40
+ );
41
+ expect(schema).toHaveProperty(
42
+ ["properties", "toolCalls", "anyOf", 0, "items", "properties", "toolName", "description"],
43
+ expect.stringContaining("getWeather"),
44
+ );
45
+ });
46
+
47
+ it("preserves the legacy string answer shape when no user schema is supplied", () => {
48
+ const schema = buildToolCallSchema(tools);
49
+
50
+ expect(schema).not.toHaveProperty("$defs");
51
+ expect(schema).toHaveProperty("properties.answer.anyOf", [
52
+ { type: "string" },
53
+ { type: "null" },
54
+ ]);
55
+ });
56
+
57
+ it("namespaces a user schema and references it from answer", () => {
58
+ const userSchema = {
59
+ type: "object",
60
+ properties: { translated: { type: "string" } },
61
+ };
62
+ const schema = buildToolCallSchema(tools, userSchema);
63
+
64
+ expect(schema).toHaveProperty("properties.answer.anyOf", [
65
+ { $ref: "#/$defs/__qgrid_user_output" },
66
+ { type: "null" },
67
+ ]);
68
+ expect(schema).toHaveProperty(userPath(), userSchema);
69
+ });
70
+
71
+ it("rebases root and nested local refs into the user-schema namespace", () => {
72
+ const schema = buildToolCallSchema(tools, {
73
+ type: "object",
74
+ properties: {
75
+ self: { $ref: "#" },
76
+ item: { $ref: "#/$defs/Item" },
77
+ },
78
+ $defs: {
79
+ Item: {
80
+ type: "object",
81
+ properties: { parent: { $ref: "#" } },
82
+ },
83
+ },
84
+ });
85
+
86
+ expect(schema).toHaveProperty(
87
+ userPath("properties", "self", "$ref"),
88
+ "#/$defs/__qgrid_user_output",
89
+ );
90
+ expect(schema).toHaveProperty(
91
+ userPath("properties", "item", "$ref"),
92
+ "#/$defs/__qgrid_user_output/$defs/Item",
93
+ );
94
+ expect(schema).toHaveProperty(
95
+ userPath("$defs", "Item", "properties", "parent", "$ref"),
96
+ "#/$defs/__qgrid_user_output",
97
+ );
98
+ });
99
+
100
+ it("only traverses schema positions and preserves property names and literal data", () => {
101
+ const literal = {
102
+ $id: "literal-id",
103
+ $ref: "https://example.com/literal.json",
104
+ $dynamicRef: "#literal",
105
+ };
106
+ const schema = buildToolCallSchema(tools, {
107
+ type: "object",
108
+ properties: {
109
+ $id: { type: "string" },
110
+ $ref: { type: "string" },
111
+ payload: {
112
+ type: "object",
113
+ const: literal,
114
+ enum: [literal],
115
+ default: literal,
116
+ examples: [literal],
117
+ },
118
+ },
119
+ patternProperties: {
120
+ "^\\$dynamicRef$": { type: "string" },
121
+ },
122
+ dependentSchemas: {
123
+ $dynamicRef: {
124
+ type: "object",
125
+ properties: { value: { $ref: "#" } },
126
+ },
127
+ },
128
+ });
129
+
130
+ expect(schema).toHaveProperty(userPath("properties", "$id"), { type: "string" });
131
+ expect(schema).toHaveProperty(userPath("properties", "$ref"), { type: "string" });
132
+ expect(schema).toHaveProperty(userPath("patternProperties", "^\\$dynamicRef$"), {
133
+ type: "string",
134
+ });
135
+ expect(schema).toHaveProperty(userPath("properties", "payload", "const"), literal);
136
+ expect(schema).toHaveProperty(userPath("properties", "payload", "enum"), [literal]);
137
+ expect(schema).toHaveProperty(userPath("properties", "payload", "default"), literal);
138
+ expect(schema).toHaveProperty(userPath("properties", "payload", "examples"), [literal]);
139
+ expect(schema).toHaveProperty(
140
+ userPath("dependentSchemas", "$dynamicRef", "properties", "value", "$ref"),
141
+ "#/$defs/__qgrid_user_output",
142
+ );
143
+ });
144
+
145
+ it("rebases draft-07 additionalItems and schema dependencies while preserving property dependencies", () => {
146
+ const schema = buildToolCallSchema(tools, {
147
+ type: "array",
148
+ items: [{ type: "string" }],
149
+ additionalItems: { $ref: "#/$defs/Extra" },
150
+ dependencies: {
151
+ triggerSchema: {
152
+ type: "object",
153
+ properties: { extra: { $ref: "#/$defs/Extra" } },
154
+ },
155
+ triggerProperties: ["first", "$ref", "$id"],
156
+ },
157
+ $defs: {
158
+ Extra: { type: "string" },
159
+ },
160
+ });
161
+
162
+ expect(schema).toHaveProperty(
163
+ userPath("additionalItems", "$ref"),
164
+ "#/$defs/__qgrid_user_output/$defs/Extra",
165
+ );
166
+ expect(schema).toHaveProperty(
167
+ userPath("dependencies", "triggerSchema", "properties", "extra", "$ref"),
168
+ "#/$defs/__qgrid_user_output/$defs/Extra",
169
+ );
170
+ expect(schema).toHaveProperty(
171
+ userPath("dependencies", "triggerProperties"),
172
+ ["first", "$ref", "$id"],
173
+ );
174
+ });
175
+
176
+ it.each([
177
+ [{ $ref: "https://example.com/schema.json" }, "$.$ref"],
178
+ [{ $ref: "#named-anchor" }, "$.$ref"],
179
+ [{ $id: "urn:example:schema", type: "object" }, "$.$id"],
180
+ [{ $dynamicRef: "#node" }, "$.$dynamicRef"],
181
+ [{ $recursiveRef: "#" }, "$.$recursiveRef"],
182
+ [{ $recursiveAnchor: true }, "$.$recursiveAnchor"],
183
+ ])("rejects unsupported reference features with their path", (answerSchema, path) => {
184
+ expect(() => buildToolCallSchema(tools, answerSchema)).toThrowError(
185
+ expect.objectContaining({ path }),
186
+ );
187
+ });
188
+ });
@@ -0,0 +1,177 @@
1
+ /**
2
+ * tool envelope + 사용자 output 스키마 합성 (요청 방향). 응답 해석은 tool-emulation.ts.
3
+ *
4
+ * tools 가 있어도 최종 answer 가 사용자 스키마로 강제되게 하려고 envelope 의 answer 브랜치에
5
+ * 사용자 스키마를 심는다. 통째로 중첩할 수 없는 이유는 `$ref` 가 문서 절대 경로이기 때문 —
6
+ * `#` 은 "이 조각의 루트"가 아니라 "최종 문서의 루트"라, 그대로 넣으면 포인터가 envelope 을
7
+ * 가리켜 조용히 엉뚱한 걸 검증하게 된다. 그래서 예약 이름 아래로 옮기고 로컬 포인터를 재작성한다.
8
+ *
9
+ * 배경·실측·예시·거부 목록은 스킬 문서를 볼 것:
10
+ * packages/cli/skills/qgrid/references/tool-calling-and-multiturn.md
11
+ * → "Composing Tools With A User Output Schema"
12
+ */
13
+
14
+ import { type JsonValue } from "../../codex-protocol/serde_json/JsonValue";
15
+ import {
16
+ SCHEMA_ARRAY_KEYWORDS,
17
+ SCHEMA_DEPENDENCIES_KEYWORD,
18
+ SCHEMA_MAP_KEYWORDS,
19
+ SCHEMA_SINGLE_KEYWORDS,
20
+ UNSUPPORTED_REFERENCE_KEYWORDS,
21
+ } from "../../utils/providers/common/json-schema-keywords";
22
+ import { type QgridTool } from "./qgrid.types";
23
+
24
+ const USER_OUTPUT_DEFINITION = "__qgrid_user_output";
25
+ const USER_OUTPUT_REF = `#/$defs/${USER_OUTPUT_DEFINITION}`;
26
+
27
+ export class ToolSchemaCompositionError extends Error {
28
+ constructor(
29
+ message: string,
30
+ public path: string,
31
+ ) {
32
+ super(`tool schema composition: ${message} (at ${path})`);
33
+ this.name = "ToolSchemaCompositionError";
34
+ }
35
+ }
36
+
37
+ export function buildToolCallSchema(tools: QgridTool[], answerSchema?: JsonValue): JsonValue {
38
+ const toolDescriptions = tools
39
+ .map((tool) => {
40
+ const schema = JSON.stringify(tool.inputSchema);
41
+ return `- ${tool.name}: ${tool.description ?? ""}\n inputSchema: ${schema}`;
42
+ })
43
+ .join("\n");
44
+
45
+ const normalizedAnswerSchema =
46
+ answerSchema === undefined ? undefined : rebaseUserOutputSchema(answerSchema);
47
+
48
+ return {
49
+ type: "object",
50
+ description:
51
+ "Use this schema only through StructuredOutput. Do not invoke listed client tools as native Claude Code tools. To request client-side tool execution, set action to tool_call and include toolCalls. Use action answer only when no further client tool result is needed.",
52
+ properties: {
53
+ action: {
54
+ type: "string",
55
+ enum: ["answer", "tool_call"],
56
+ description:
57
+ "Use tool_call to request client-side tool execution through this structured output. Use answer only for a final answer.",
58
+ },
59
+ answer: {
60
+ anyOf: [
61
+ normalizedAnswerSchema === undefined ? { type: "string" } : { $ref: USER_OUTPUT_REF },
62
+ { type: "null" },
63
+ ],
64
+ },
65
+ toolCalls: {
66
+ description:
67
+ "Client-side tool calls requested through structured output. Do not call these tools as native Claude Code tools.",
68
+ anyOf: [
69
+ {
70
+ type: "array",
71
+ items: {
72
+ type: "object",
73
+ properties: {
74
+ toolName: {
75
+ type: "string",
76
+ enum: tools.map((tool) => tool.name),
77
+ description: `Client-side tool name to request through structured output. Do not call this as a native Claude Code tool.\n${toolDescriptions}`,
78
+ },
79
+ args: {
80
+ type: "string",
81
+ description: "Tool arguments as a JSON string for the client-side tool.",
82
+ },
83
+ },
84
+ required: ["toolName", "args"],
85
+ additionalProperties: false,
86
+ },
87
+ },
88
+ { type: "null" },
89
+ ],
90
+ },
91
+ },
92
+ required: ["action", "answer", "toolCalls"],
93
+ additionalProperties: false,
94
+ ...(normalizedAnswerSchema === undefined
95
+ ? {}
96
+ : {
97
+ $defs: {
98
+ [USER_OUTPUT_DEFINITION]: normalizedAnswerSchema,
99
+ },
100
+ }),
101
+ };
102
+ }
103
+
104
+ function rebaseUserOutputSchema(schema: JsonValue): JsonValue {
105
+ return rewriteSchemaNode(schema, "$");
106
+ }
107
+
108
+ const MAP_KEYWORDS = new Set<string>(SCHEMA_MAP_KEYWORDS);
109
+ const ARRAY_KEYWORDS = new Set<string>(SCHEMA_ARRAY_KEYWORDS);
110
+ const SINGLE_KEYWORDS = new Set<string>(SCHEMA_SINGLE_KEYWORDS);
111
+
112
+ function rewriteSchemaNode(value: JsonValue, path: string): JsonValue {
113
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return value;
114
+
115
+ const source = value as Record<string, JsonValue>;
116
+ const result: Record<string, JsonValue> = { ...source };
117
+
118
+ for (const unsupportedKeyword of UNSUPPORTED_REFERENCE_KEYWORDS) {
119
+ if (Object.hasOwn(source, unsupportedKeyword)) {
120
+ throw new ToolSchemaCompositionError(
121
+ `${unsupportedKeyword} is not supported`,
122
+ `${path}.${unsupportedKeyword}`,
123
+ );
124
+ }
125
+ }
126
+
127
+ if ("$ref" in source) {
128
+ const ref = source.$ref;
129
+ if (typeof ref !== "string" || (ref !== "#" && !ref.startsWith("#/"))) {
130
+ throw new ToolSchemaCompositionError(
131
+ "only root-relative JSON Pointer $ref values are supported",
132
+ `${path}.$ref`,
133
+ );
134
+ }
135
+ result.$ref = `${USER_OUTPUT_REF}${ref.slice(1)}`;
136
+ }
137
+
138
+ for (const [keyword, child] of Object.entries(source)) {
139
+ const keywordPath = `${path}.${keyword}`;
140
+
141
+ if (MAP_KEYWORDS.has(keyword) && isJsonObject(child)) {
142
+ result[keyword] = Object.fromEntries(
143
+ Object.entries(child).map(([name, schema]) => [
144
+ name,
145
+ rewriteSchemaNode(schema, `${keywordPath}.${name}`),
146
+ ]),
147
+ ) as JsonValue;
148
+ } else if (keyword === SCHEMA_DEPENDENCIES_KEYWORD && isJsonObject(child)) {
149
+ result[keyword] = Object.fromEntries(
150
+ Object.entries(child).map(([name, dependency]) => [
151
+ name,
152
+ Array.isArray(dependency)
153
+ ? dependency
154
+ : rewriteSchemaNode(dependency, `${keywordPath}.${name}`),
155
+ ]),
156
+ ) as JsonValue;
157
+ } else if (ARRAY_KEYWORDS.has(keyword) && Array.isArray(child)) {
158
+ result[keyword] = child.map((schema, index) =>
159
+ rewriteSchemaNode(schema, `${keywordPath}[${index}]`),
160
+ ) as JsonValue;
161
+ } else if (SINGLE_KEYWORDS.has(keyword)) {
162
+ if (keyword === "items" && Array.isArray(child)) {
163
+ result[keyword] = child.map((schema, index) =>
164
+ rewriteSchemaNode(schema, `${keywordPath}[${index}]`),
165
+ ) as JsonValue;
166
+ } else {
167
+ result[keyword] = rewriteSchemaNode(child, keywordPath);
168
+ }
169
+ }
170
+ }
171
+
172
+ return result as JsonValue;
173
+ }
174
+
175
+ function isJsonObject(value: JsonValue): value is Record<string, JsonValue> {
176
+ return value !== null && typeof value === "object" && !Array.isArray(value);
177
+ }