@glbgpt/cli 0.1.3 → 0.2.0

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 (112) hide show
  1. package/README.md +74 -39
  2. package/dist/src/coding/backgroundTasks.d.ts +34 -0
  3. package/dist/src/coding/backgroundTasks.js +154 -0
  4. package/dist/src/coding/backgroundTasks.js.map +1 -0
  5. package/dist/src/coding/backgroundTools.d.ts +9 -0
  6. package/dist/src/coding/backgroundTools.js +63 -0
  7. package/dist/src/coding/backgroundTools.js.map +1 -0
  8. package/dist/src/coding/bashTool.d.ts +27 -0
  9. package/dist/src/coding/bashTool.js +144 -0
  10. package/dist/src/coding/bashTool.js.map +1 -0
  11. package/dist/src/coding/dangerousCommands.d.ts +29 -0
  12. package/dist/src/coding/dangerousCommands.js +156 -0
  13. package/dist/src/coding/dangerousCommands.js.map +1 -0
  14. package/dist/src/coding/editDiff.d.ts +45 -0
  15. package/dist/src/coding/editDiff.js +156 -0
  16. package/dist/src/coding/editDiff.js.map +1 -0
  17. package/dist/src/coding/editTool.d.ts +14 -0
  18. package/dist/src/coding/editTool.js +154 -0
  19. package/dist/src/coding/editTool.js.map +1 -0
  20. package/dist/src/coding/index.d.ts +25 -0
  21. package/dist/src/coding/index.js +30 -0
  22. package/dist/src/coding/index.js.map +1 -0
  23. package/dist/src/coding/outputAccumulator.d.ts +34 -0
  24. package/dist/src/coding/outputAccumulator.js +111 -0
  25. package/dist/src/coding/outputAccumulator.js.map +1 -0
  26. package/dist/src/coding/permissionGate.d.ts +65 -0
  27. package/dist/src/coding/permissionGate.js +138 -0
  28. package/dist/src/coding/permissionGate.js.map +1 -0
  29. package/dist/src/coding/permissionStore.d.ts +36 -0
  30. package/dist/src/coding/permissionStore.js +56 -0
  31. package/dist/src/coding/permissionStore.js.map +1 -0
  32. package/dist/src/coding/permissions.d.ts +34 -0
  33. package/dist/src/coding/permissions.js +18 -0
  34. package/dist/src/coding/permissions.js.map +1 -0
  35. package/dist/src/coding/projectContext.d.ts +21 -0
  36. package/dist/src/coding/projectContext.js +118 -0
  37. package/dist/src/coding/projectContext.js.map +1 -0
  38. package/dist/src/coding/readState.d.ts +22 -0
  39. package/dist/src/coding/readState.js +30 -0
  40. package/dist/src/coding/readState.js.map +1 -0
  41. package/dist/src/coding/truncate.d.ts +39 -0
  42. package/dist/src/coding/truncate.js +83 -0
  43. package/dist/src/coding/truncate.js.map +1 -0
  44. package/dist/src/coding/writeTool.d.ts +14 -0
  45. package/dist/src/coding/writeTool.js +82 -0
  46. package/dist/src/coding/writeTool.js.map +1 -0
  47. package/dist/src/commands/execCommand.d.ts +8 -0
  48. package/dist/src/commands/execCommand.js +47 -7
  49. package/dist/src/commands/execCommand.js.map +1 -1
  50. package/dist/src/core/catalog.js +11 -0
  51. package/dist/src/core/catalog.js.map +1 -1
  52. package/dist/src/core/compaction.d.ts +52 -0
  53. package/dist/src/core/compaction.js +151 -0
  54. package/dist/src/core/compaction.js.map +1 -0
  55. package/dist/src/core/contextPrune.js +8 -1
  56. package/dist/src/core/contextPrune.js.map +1 -1
  57. package/dist/src/core/httpChatBackend.js +21 -2
  58. package/dist/src/core/httpChatBackend.js.map +1 -1
  59. package/dist/src/core/runChat.d.ts +12 -2
  60. package/dist/src/core/runChat.js +72 -32
  61. package/dist/src/core/runChat.js.map +1 -1
  62. package/dist/src/core/session.d.ts +10 -1
  63. package/dist/src/core/session.js +11 -2
  64. package/dist/src/core/session.js.map +1 -1
  65. package/dist/src/core/structuredOutput.d.ts +63 -0
  66. package/dist/src/core/structuredOutput.js +328 -0
  67. package/dist/src/core/structuredOutput.js.map +1 -0
  68. package/dist/src/core/turnLoop.d.ts +18 -2
  69. package/dist/src/core/turnLoop.js +79 -12
  70. package/dist/src/core/turnLoop.js.map +1 -1
  71. package/dist/src/exec/input.d.ts +5 -3
  72. package/dist/src/exec/input.js +7 -3
  73. package/dist/src/exec/input.js.map +1 -1
  74. package/dist/src/exec/output.d.ts +3 -2
  75. package/dist/src/exec/output.js +17 -4
  76. package/dist/src/exec/output.js.map +1 -1
  77. package/dist/src/fs/index.d.ts +4 -1
  78. package/dist/src/fs/index.js +7 -2
  79. package/dist/src/fs/index.js.map +1 -1
  80. package/dist/src/fs/limits.d.ts +1 -1
  81. package/dist/src/fs/limits.js +3 -1
  82. package/dist/src/fs/limits.js.map +1 -1
  83. package/dist/src/fs/readFileTool.d.ts +2 -1
  84. package/dist/src/fs/readFileTool.js +4 -1
  85. package/dist/src/fs/readFileTool.js.map +1 -1
  86. package/dist/src/fs/workspaceGuard.d.ts +8 -0
  87. package/dist/src/fs/workspaceGuard.js +48 -0
  88. package/dist/src/fs/workspaceGuard.js.map +1 -1
  89. package/dist/src/index.js +37 -3
  90. package/dist/src/index.js.map +1 -1
  91. package/dist/src/mcp/server.js +4 -4
  92. package/dist/src/mcp/server.js.map +1 -1
  93. package/dist/src/tui/App.d.ts +10 -1
  94. package/dist/src/tui/App.js +114 -15
  95. package/dist/src/tui/App.js.map +1 -1
  96. package/dist/src/tui/approvalBridge.d.ts +21 -0
  97. package/dist/src/tui/approvalBridge.js +45 -0
  98. package/dist/src/tui/approvalBridge.js.map +1 -0
  99. package/dist/src/tui/chatController.d.ts +21 -0
  100. package/dist/src/tui/chatController.js +155 -16
  101. package/dist/src/tui/chatController.js.map +1 -1
  102. package/dist/src/tui/commands.js +16 -2
  103. package/dist/src/tui/commands.js.map +1 -1
  104. package/dist/src/tui/models.d.ts +9 -0
  105. package/dist/src/tui/models.js +14 -2
  106. package/dist/src/tui/models.js.map +1 -1
  107. package/dist/src/tui/run.d.ts +7 -0
  108. package/dist/src/tui/run.js +69 -9
  109. package/dist/src/tui/run.js.map +1 -1
  110. package/dist/src/web/webFetchTool.js +41 -5
  111. package/dist/src/web/webFetchTool.js.map +1 -1
  112. package/package.json +4 -4
@@ -0,0 +1,63 @@
1
+ export type StructuredOutputMode = 'json_schema' | 'json_object';
2
+ export interface OutputSchemaSpec {
3
+ /** Schema name sent to json_schema-capable providers (default "output"). */
4
+ name?: string;
5
+ schema: Record<string, unknown>;
6
+ }
7
+ export interface StructuredOutputPlan {
8
+ mode: StructuredOutputMode;
9
+ /** OpenAI `response_format` value to send. */
10
+ responseFormat: Record<string, unknown>;
11
+ /** Appended to the system prompt when the provider cannot enforce the schema itself. */
12
+ systemAddendum?: string;
13
+ }
14
+ /**
15
+ * Which structured-output mechanism a model supports. The server catalog may say so explicitly
16
+ * (`structured_output`); otherwise fall back to the known-family list, defaulting to json_schema.
17
+ */
18
+ export declare function structuredOutputMode(model: string): StructuredOutputMode;
19
+ /**
20
+ * True when `schema` already satisfies OpenAI strict-mode rules, so `strict:true` cannot be
21
+ * rejected: every object node has `additionalProperties:false` and a `required` array covering
22
+ * all of its `properties`; the check recurses through properties, array items, $defs/definitions
23
+ * and anyOf/oneOf/allOf. Anything unusual (non-object schema, boolean subschemas, `$ref` to
24
+ * elsewhere) is treated conservatively as non-compliant.
25
+ */
26
+ export declare function isStrictCompliant(schema: unknown): boolean;
27
+ /**
28
+ * Rewrite `schema` into an OpenAI strict-mode-compliant equivalent, or null when it uses a
29
+ * construct strict mode cannot express (allOf, patternProperties, an `additionalProperties`
30
+ * schema, tuple items, if/then/not, boolean subschemas).
31
+ *
32
+ * Rules applied recursively: every object gets `additionalProperties:false` and `required` listing
33
+ * ALL of its properties; properties that were optional become nullable (`type: [T, "null"]`, or an
34
+ * `anyOf` with `{type:"null"}`); `oneOf` becomes `anyOf`. Because optional properties are only
35
+ * *representable* as required-nullable in strict mode, {@link stripSyntheticNulls} removes the
36
+ * nulls the model emits for them so the caller sees its own schema's semantics again.
37
+ *
38
+ * Why not just send `strict:false`? Some upstream channels validate strictly regardless of the
39
+ * flag (observed 2026-08-18: gpt-5.6-luna channel rejected a loose schema with strict:false).
40
+ */
41
+ export declare function strictify(schema: unknown): Record<string, unknown> | null;
42
+ /**
43
+ * Undo the nullability {@link strictify} introduced: walk `value` alongside the CALLER's original
44
+ * schema and drop object properties that are null when the original schema neither required them
45
+ * nor allowed null. Everything else is left untouched (in place; returns `value` for chaining).
46
+ */
47
+ export declare function stripSyntheticNulls(value: unknown, originalSchema: unknown): unknown;
48
+ /** Build the request-side plan for `schema` on `model` (see module doc). */
49
+ export declare function planStructuredOutput(model: string, spec: OutputSchemaSpec): StructuredOutputPlan;
50
+ /** Same as {@link planStructuredOutput} but for an explicit mode (used for the json_object fallback). */
51
+ export declare function planForMode(mode: StructuredOutputMode, spec: OutputSchemaSpec): StructuredOutputPlan;
52
+ /**
53
+ * Whether a turn error is the provider rejecting our structured-output request itself (schema
54
+ * validation / unsupported response_format) — the case where retrying once in json_object mode
55
+ * is worth it. Never matches generic network/auth/billing errors.
56
+ */
57
+ export declare function isResponseFormatRejection(message: string): boolean;
58
+ /**
59
+ * Parse a model's structured reply. Accepts clean JSON, JSON inside ``` fences, and JSON with a
60
+ * short preamble/trailer (some models add "Here is the JSON:" despite instructions). Throws when
61
+ * no JSON value can be recovered.
62
+ */
63
+ export declare function parseStructuredJson(text: string): unknown;
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Structured output (JSON schema) planning per chat model.
3
+ *
4
+ * Why this exists (prod cli_chat_turn audit, 2026-08-18): the MCP `glbgpt_chat` tool and exec
5
+ * `--output-schema` used to send `response_format: {type:'json_schema', strict:true, schema}`
6
+ * with the caller's schema untouched. Two failure classes accounted for ~70% of the anywhere-side
7
+ * chat errors of the last 10 days:
8
+ * - OpenAI strict mode rejects any schema that is not fully "strict" (every object needs
9
+ * `additionalProperties:false` and `required` listing every property key). Agents pass ordinary
10
+ * JSON Schemas → 400 "Invalid schema for response_format 'output'".
11
+ * - DeepSeek does not support `json_schema` at all (400 "This response_format type is unavailable
12
+ * now"); it only offers `json_object`.
13
+ * Both surfaced to the user as a misleading "Network error" after 3 pointless retries.
14
+ *
15
+ * Plan: strict only when the schema already qualifies (keeps the guarantee for callers who did the
16
+ * work), non-strict otherwise; models without json_schema support get `json_object` plus the schema
17
+ * spelled out in the system prompt. Parsing tolerates code fences / surrounding prose either way.
18
+ */
19
+ import { getChatModels } from '../tui/models.js';
20
+ /** Model families known to reject `response_format.type = json_schema` (only json_object). */
21
+ const JSON_OBJECT_ONLY_PREFIXES = ['deepseek'];
22
+ /**
23
+ * Which structured-output mechanism a model supports. The server catalog may say so explicitly
24
+ * (`structured_output`); otherwise fall back to the known-family list, defaulting to json_schema.
25
+ */
26
+ export function structuredOutputMode(model) {
27
+ const fromCatalog = getChatModels().find((m) => m.id === model)?.structuredOutput;
28
+ if (fromCatalog)
29
+ return fromCatalog;
30
+ const id = model.toLowerCase();
31
+ return JSON_OBJECT_ONLY_PREFIXES.some((p) => id.startsWith(p)) ? 'json_object' : 'json_schema';
32
+ }
33
+ /**
34
+ * True when `schema` already satisfies OpenAI strict-mode rules, so `strict:true` cannot be
35
+ * rejected: every object node has `additionalProperties:false` and a `required` array covering
36
+ * all of its `properties`; the check recurses through properties, array items, $defs/definitions
37
+ * and anyOf/oneOf/allOf. Anything unusual (non-object schema, boolean subschemas, `$ref` to
38
+ * elsewhere) is treated conservatively as non-compliant.
39
+ */
40
+ export function isStrictCompliant(schema) {
41
+ return checkStrict(schema, 0);
42
+ }
43
+ function checkStrict(node, depth) {
44
+ if (depth > 32)
45
+ return false;
46
+ if (typeof node !== 'object' || node === null || Array.isArray(node))
47
+ return false;
48
+ const s = node;
49
+ const type = s.type;
50
+ const isObject = type === 'object' || (Array.isArray(type) && type.includes('object')) || s.properties !== undefined;
51
+ if (isObject) {
52
+ if (s.additionalProperties !== false)
53
+ return false;
54
+ const props = s.properties;
55
+ const keys = typeof props === 'object' && props !== null ? Object.keys(props) : [];
56
+ const required = Array.isArray(s.required) ? s.required : [];
57
+ for (const k of keys)
58
+ if (!required.includes(k))
59
+ return false;
60
+ for (const k of keys)
61
+ if (!checkStrict(props[k], depth + 1))
62
+ return false;
63
+ }
64
+ if (s.items !== undefined) {
65
+ if (Array.isArray(s.items)) {
66
+ for (const it of s.items)
67
+ if (!checkStrict(it, depth + 1))
68
+ return false;
69
+ }
70
+ else if (!checkStrict(s.items, depth + 1))
71
+ return false;
72
+ }
73
+ for (const key of ['anyOf', 'oneOf', 'allOf']) {
74
+ const branches = s[key];
75
+ if (branches !== undefined) {
76
+ if (!Array.isArray(branches))
77
+ return false;
78
+ for (const b of branches)
79
+ if (!checkStrict(b, depth + 1))
80
+ return false;
81
+ }
82
+ }
83
+ for (const key of ['$defs', 'definitions']) {
84
+ const defs = s[key];
85
+ if (defs !== undefined) {
86
+ if (typeof defs !== 'object' || defs === null)
87
+ return false;
88
+ for (const d of Object.values(defs))
89
+ if (!checkStrict(d, depth + 1))
90
+ return false;
91
+ }
92
+ }
93
+ return true;
94
+ }
95
+ /**
96
+ * Rewrite `schema` into an OpenAI strict-mode-compliant equivalent, or null when it uses a
97
+ * construct strict mode cannot express (allOf, patternProperties, an `additionalProperties`
98
+ * schema, tuple items, if/then/not, boolean subschemas).
99
+ *
100
+ * Rules applied recursively: every object gets `additionalProperties:false` and `required` listing
101
+ * ALL of its properties; properties that were optional become nullable (`type: [T, "null"]`, or an
102
+ * `anyOf` with `{type:"null"}`); `oneOf` becomes `anyOf`. Because optional properties are only
103
+ * *representable* as required-nullable in strict mode, {@link stripSyntheticNulls} removes the
104
+ * nulls the model emits for them so the caller sees its own schema's semantics again.
105
+ *
106
+ * Why not just send `strict:false`? Some upstream channels validate strictly regardless of the
107
+ * flag (observed 2026-08-18: gpt-5.6-luna channel rejected a loose schema with strict:false).
108
+ */
109
+ export function strictify(schema) {
110
+ const out = strictifyNode(schema, 0);
111
+ return out;
112
+ }
113
+ const UNSUPPORTED_KEYS = ['allOf', 'patternProperties', 'prefixItems', 'if', 'then', 'else', 'not', 'dependentSchemas', 'unevaluatedProperties'];
114
+ function strictifyNode(node, depth) {
115
+ if (depth > 32)
116
+ return null;
117
+ if (typeof node !== 'object' || node === null || Array.isArray(node))
118
+ return null;
119
+ const s = node;
120
+ for (const k of UNSUPPORTED_KEYS)
121
+ if (s[k] !== undefined)
122
+ return null;
123
+ const out = { ...s };
124
+ const type = s.type;
125
+ const isObject = type === 'object' || (Array.isArray(type) && type.includes('object')) || s.properties !== undefined;
126
+ if (isObject) {
127
+ if (s.additionalProperties !== undefined && s.additionalProperties !== false && s.additionalProperties !== true)
128
+ return null;
129
+ const props = (typeof s.properties === 'object' && s.properties !== null ? s.properties : {});
130
+ const required = new Set(Array.isArray(s.required) ? s.required.filter((r) => typeof r === 'string') : []);
131
+ const newProps = {};
132
+ for (const [k, v] of Object.entries(props)) {
133
+ const child = strictifyNode(v, depth + 1);
134
+ if (!child)
135
+ return null;
136
+ newProps[k] = required.has(k) ? child : makeNullable(child);
137
+ }
138
+ out.properties = newProps;
139
+ out.required = Object.keys(props);
140
+ out.additionalProperties = false;
141
+ }
142
+ if (s.items !== undefined) {
143
+ if (Array.isArray(s.items))
144
+ return null;
145
+ const items = strictifyNode(s.items, depth + 1);
146
+ if (!items)
147
+ return null;
148
+ out.items = items;
149
+ }
150
+ const branches = s.anyOf ?? s.oneOf;
151
+ if (branches !== undefined) {
152
+ if (!Array.isArray(branches) || (s.anyOf !== undefined && s.oneOf !== undefined))
153
+ return null;
154
+ const conv = [];
155
+ for (const b of branches) {
156
+ const c = strictifyNode(b, depth + 1);
157
+ if (!c)
158
+ return null;
159
+ conv.push(c);
160
+ }
161
+ delete out.oneOf;
162
+ out.anyOf = conv;
163
+ }
164
+ for (const key of ['$defs', 'definitions']) {
165
+ const defs = s[key];
166
+ if (defs !== undefined) {
167
+ if (typeof defs !== 'object' || defs === null || Array.isArray(defs))
168
+ return null;
169
+ const conv = {};
170
+ for (const [k, v] of Object.entries(defs)) {
171
+ const c = strictifyNode(v, depth + 1);
172
+ if (!c)
173
+ return null;
174
+ conv[k] = c;
175
+ }
176
+ out[key] = conv;
177
+ }
178
+ }
179
+ return out;
180
+ }
181
+ /** Make a (strictified) subschema accept null in the way strict mode understands. */
182
+ function makeNullable(node) {
183
+ if (allowsNull(node))
184
+ return node;
185
+ if (typeof node.type === 'string')
186
+ return { ...node, type: [node.type, 'null'] };
187
+ if (Array.isArray(node.type))
188
+ return { ...node, type: [...node.type, 'null'] };
189
+ if (Array.isArray(node.anyOf))
190
+ return { ...node, anyOf: [...node.anyOf, { type: 'null' }] };
191
+ if (Array.isArray(node.enum))
192
+ return { ...node, enum: [...node.enum, null] };
193
+ // $ref / const / bare descriptions: wrap.
194
+ return { anyOf: [node, { type: 'null' }] };
195
+ }
196
+ /** Whether a schema node already admits null (by type, anyOf/oneOf branch, enum member or const). */
197
+ function allowsNull(node) {
198
+ if (typeof node !== 'object' || node === null)
199
+ return false;
200
+ const s = node;
201
+ if (s.type === 'null' || (Array.isArray(s.type) && s.type.includes('null')))
202
+ return true;
203
+ if (Array.isArray(s.enum) && s.enum.includes(null))
204
+ return true;
205
+ if (s.const === null)
206
+ return true;
207
+ for (const key of ['anyOf', 'oneOf']) {
208
+ if (Array.isArray(s[key]) && s[key].some(allowsNull))
209
+ return true;
210
+ }
211
+ return false;
212
+ }
213
+ /**
214
+ * Undo the nullability {@link strictify} introduced: walk `value` alongside the CALLER's original
215
+ * schema and drop object properties that are null when the original schema neither required them
216
+ * nor allowed null. Everything else is left untouched (in place; returns `value` for chaining).
217
+ */
218
+ export function stripSyntheticNulls(value, originalSchema) {
219
+ walkStrip(value, originalSchema, 0);
220
+ return value;
221
+ }
222
+ function walkStrip(value, schema, depth) {
223
+ if (depth > 32 || typeof schema !== 'object' || schema === null || typeof value !== 'object' || value === null)
224
+ return;
225
+ const s = schema;
226
+ if (Array.isArray(value)) {
227
+ if (s.items !== undefined && !Array.isArray(s.items))
228
+ for (const item of value)
229
+ walkStrip(item, s.items, depth + 1);
230
+ return;
231
+ }
232
+ const branches = (Array.isArray(s.anyOf) ? s.anyOf : Array.isArray(s.oneOf) ? s.oneOf : null);
233
+ if (branches && s.properties === undefined) {
234
+ // Cannot know which branch produced the value; only descend when exactly one branch is an object schema.
235
+ const objectBranches = branches.filter((b) => typeof b === 'object' && b !== null && b.properties !== undefined);
236
+ if (objectBranches.length === 1)
237
+ walkStrip(value, objectBranches[0], depth + 1);
238
+ return;
239
+ }
240
+ const props = (typeof s.properties === 'object' && s.properties !== null ? s.properties : null);
241
+ if (!props)
242
+ return;
243
+ const required = new Set(Array.isArray(s.required) ? s.required : []);
244
+ const obj = value;
245
+ for (const [k, sub] of Object.entries(props)) {
246
+ if (!(k in obj))
247
+ continue;
248
+ if (obj[k] === null && !required.has(k) && !allowsNull(sub)) {
249
+ delete obj[k];
250
+ continue;
251
+ }
252
+ walkStrip(obj[k], sub, depth + 1);
253
+ }
254
+ }
255
+ /** Build the request-side plan for `schema` on `model` (see module doc). */
256
+ export function planStructuredOutput(model, spec) {
257
+ return planForMode(structuredOutputMode(model), spec);
258
+ }
259
+ /** Same as {@link planStructuredOutput} but for an explicit mode (used for the json_object fallback). */
260
+ export function planForMode(mode, spec) {
261
+ const name = spec.name?.trim() || 'output';
262
+ if (mode === 'json_object') {
263
+ return {
264
+ mode,
265
+ responseFormat: { type: 'json_object' },
266
+ // The word "JSON" must appear in the prompt for json_object mode (DeepSeek/OpenAI requirement).
267
+ systemAddendum: 'Respond ONLY with a single JSON object that conforms to the following JSON Schema. ' +
268
+ 'No prose, no markdown code fences, nothing before or after the JSON.\n' +
269
+ JSON.stringify(spec.schema),
270
+ };
271
+ }
272
+ if (isStrictCompliant(spec.schema)) {
273
+ return { mode, responseFormat: { type: 'json_schema', json_schema: { name, strict: true, schema: spec.schema } } };
274
+ }
275
+ const strict = strictify(spec.schema);
276
+ if (strict) {
277
+ return { mode, responseFormat: { type: 'json_schema', json_schema: { name, strict: true, schema: strict } } };
278
+ }
279
+ // Constructs strict mode cannot express: best effort, non-strict.
280
+ return { mode, responseFormat: { type: 'json_schema', json_schema: { name, strict: false, schema: spec.schema } } };
281
+ }
282
+ /**
283
+ * Whether a turn error is the provider rejecting our structured-output request itself (schema
284
+ * validation / unsupported response_format) — the case where retrying once in json_object mode
285
+ * is worth it. Never matches generic network/auth/billing errors.
286
+ */
287
+ export function isResponseFormatRejection(message) {
288
+ return /response_format|json_schema|schema for response|structured output/i.test(message);
289
+ }
290
+ /**
291
+ * Parse a model's structured reply. Accepts clean JSON, JSON inside ``` fences, and JSON with a
292
+ * short preamble/trailer (some models add "Here is the JSON:" despite instructions). Throws when
293
+ * no JSON value can be recovered.
294
+ */
295
+ export function parseStructuredJson(text) {
296
+ const trimmed = text.trim();
297
+ try {
298
+ return JSON.parse(trimmed);
299
+ }
300
+ catch {
301
+ /* fall through */
302
+ }
303
+ const fence = /```(?:json)?\s*([\s\S]*?)```/i.exec(trimmed);
304
+ if (fence) {
305
+ try {
306
+ return JSON.parse(fence[1].trim());
307
+ }
308
+ catch {
309
+ /* fall through */
310
+ }
311
+ }
312
+ const starts = [trimmed.indexOf('{'), trimmed.indexOf('[')].filter((i) => i >= 0);
313
+ if (starts.length > 0) {
314
+ const start = Math.min(...starts);
315
+ const closer = trimmed[start] === '{' ? '}' : ']';
316
+ const end = trimmed.lastIndexOf(closer);
317
+ if (end > start) {
318
+ try {
319
+ return JSON.parse(trimmed.slice(start, end + 1));
320
+ }
321
+ catch {
322
+ /* fall through */
323
+ }
324
+ }
325
+ }
326
+ throw new Error('Model output did not parse as JSON despite the requested schema.');
327
+ }
328
+ //# sourceMappingURL=structuredOutput.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structuredOutput.js","sourceRoot":"","sources":["../../../src/core/structuredOutput.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,12 @@
1
1
  import type { ChatBackend, EngineEvent } from './types.js';
2
2
  import type { Session } from './session.js';
3
3
  import type { ToolRegistry } from './toolRegistry.js';
4
- export type Emit = (event: EngineEvent) => void;
4
+ /**
5
+ * Event sink. A returned promise applies BACKPRESSURE on the streaming pump (pi A3):
6
+ * consumeStream awaits it before pulling the next chunk, so a slow consumer (exec piping
7
+ * into a slow reader) bounds memory instead of buffering the whole model stream.
8
+ */
9
+ export type Emit = (event: EngineEvent) => void | Promise<void>;
5
10
  /** Per-turn options. `exec --output-schema` passes a `response_format` value here. */
6
11
  export interface TurnOptions {
7
12
  responseFormat?: unknown;
@@ -11,6 +16,14 @@ export interface TurnOptions {
11
16
  includeGatedTools?: boolean;
12
17
  /** Hard cap on tool calls dispatched in this turn. Default MAX_TOOL_CALLS_PER_TURN. */
13
18
  maxToolCalls?: number;
19
+ /**
20
+ * Steering (pi B2): called at each round boundary AFTER tool results are appended and
21
+ * BEFORE the next model call. Returned items are appended as user messages, so text the
22
+ * user typed mid-turn reaches the model without waiting for the whole turn to finish.
23
+ * Only multi-round (tool-using) turns have injection points; anything not injected by
24
+ * turn end stays queued for the caller's normal follow-up flush.
25
+ */
26
+ takeSteering?: () => Array<string | import('./types.js').ContentPart[]>;
14
27
  }
15
28
  /**
16
29
  * Drives one user turn to completion, following the OpenAI tool-calling protocol:
@@ -32,6 +45,9 @@ export declare class TurnLoop {
32
45
  runTurn(session: Session, userContent: string | import('./types.js').ContentPart[], emit: Emit, opts?: TurnOptions): Promise<void>;
33
46
  /** Stream a single model call, emitting text deltas and accumulating tool_calls. */
34
47
  private streamOneCall;
35
- /** Iterate the backend stream, forwarding deltas/usage and accumulating tool-call pieces. */
48
+ /**
49
+ * Iterate the backend stream, forwarding deltas/usage and accumulating tool-call pieces.
50
+ * Returns the last non-null finish_reason seen (providers send it on the closing chunk).
51
+ */
36
52
  private consumeStream;
37
53
  }
@@ -1,7 +1,16 @@
1
1
  import { BUDGET_EXHAUSTED_MESSAGE, MAX_TOOL_CALLS_PER_TURN } from '../fs/limits.js';
2
2
  import { track } from '../telemetry/telemetry.js';
3
3
  /** Pathological backstop only — the tool budget below governs normal turns. */
4
- const MAX_ROUNDS = 24;
4
+ const MAX_ROUNDS = 32;
5
+ /**
6
+ * Fed back for every tool call on a message the provider cut off with finish_reason=length:
7
+ * streamed tool-call arguments are accumulated incrementally, so a length-truncated message
8
+ * can yield arguments that parse as valid JSON yet are semantically incomplete. With tools
9
+ * that mutate files or spend credits, executing them would corrupt state — discard them all
10
+ * and let the model re-issue (pi agent-loop behavior, 2026-08-08 调研 A1).
11
+ */
12
+ const LENGTH_TRUNCATED_TOOLS_MESSAGE = 'Response was truncated by the output limit; this tool call was discarded because its ' +
13
+ 'arguments may be incomplete. Re-issue the call (smaller, if possible).';
5
14
  /**
6
15
  * Drives one user turn to completion, following the OpenAI tool-calling protocol:
7
16
  *
@@ -39,8 +48,9 @@ export class TurnLoop {
39
48
  for (let round = 0; round < MAX_ROUNDS; round++) {
40
49
  let content;
41
50
  let toolCalls;
51
+ let finishReason;
42
52
  try {
43
- ({ content, toolCalls } = await this.streamOneCall(session, emit, opts, toolCallsUsed >= budget));
53
+ ({ content, toolCalls, finishReason } = await this.streamOneCall(session, emit, opts, toolCallsUsed >= budget));
44
54
  }
45
55
  catch (err) {
46
56
  // User interruption (Esc): keep whatever partial text already streamed so the
@@ -61,7 +71,10 @@ export class TurnLoop {
61
71
  if (content.length === 0 && toolCalls.length === 0) {
62
72
  // The request itself succeeded (no throw) — the model just returned nothing. Keep the
63
73
  // user message (not a poison payload) so a retry has context; only add nothing more.
64
- emit({ type: 'error', message: 'The model returned no output. Please try again.' });
74
+ emit({
75
+ type: 'error',
76
+ message: 'The model returned no output. Please try again.',
77
+ });
65
78
  return;
66
79
  }
67
80
  session.addAssistantMessage(content.length > 0 ? content : null, toolCalls);
@@ -74,6 +87,28 @@ export class TurnLoop {
74
87
  emit({ type: 'turn_complete' });
75
88
  return;
76
89
  }
90
+ // Length-truncated message with tool calls: never execute them (arguments may be
91
+ // silently incomplete). Answer every call with an error result and loop so the
92
+ // model re-issues. Consumes no budget — nothing ran.
93
+ if (finishReason === 'length') {
94
+ for (const call of toolCalls) {
95
+ const result = { error: LENGTH_TRUNCATED_TOOLS_MESSAGE };
96
+ emit({
97
+ type: 'tool_call_begin',
98
+ id: call.id,
99
+ name: call.function.name,
100
+ args: safeParseArgs(call.function.arguments),
101
+ });
102
+ emit({
103
+ type: 'tool_call_end',
104
+ id: call.id,
105
+ name: call.function.name,
106
+ result,
107
+ });
108
+ session.addToolMessage(call.id, call.function.name, result);
109
+ }
110
+ continue;
111
+ }
77
112
  // Execute the requested tools and feed results back into the conversation.
78
113
  // Tool execution itself is never aborted mid-flight (a paid submit must not be torn),
79
114
  // but an interrupt takes effect before the next model call.
@@ -116,7 +151,12 @@ export class TurnLoop {
116
151
  ? { error: denied }
117
152
  : await this.registry.dispatch(call.function.name, call.function.arguments);
118
153
  // Ends may arrive out of completion order; the TUI matches rows by call id.
119
- emit({ type: 'tool_call_end', id: call.id, name: call.function.name, result });
154
+ emit({
155
+ type: 'tool_call_end',
156
+ id: call.id,
157
+ name: call.function.name,
158
+ result,
159
+ });
120
160
  return result;
121
161
  }));
122
162
  // Tool messages must follow the tool_calls order the model emitted.
@@ -126,14 +166,24 @@ export class TurnLoop {
126
166
  for (let i = 0; i < toolCalls.length; i++) {
127
167
  const call = toolCalls[i];
128
168
  const args = safeParseArgs(call.function.arguments);
129
- emit({ type: 'tool_call_begin', id: call.id, name: call.function.name, args });
169
+ emit({
170
+ type: 'tool_call_begin',
171
+ id: call.id,
172
+ name: call.function.name,
173
+ args,
174
+ });
130
175
  // Past a budget/cap, don't dispatch: feed the model an error instead, so it
131
176
  // wraps up with what it has (a normal answer, not a hard stop).
132
177
  const denied = denial[i];
133
178
  const result = denied
134
179
  ? { error: denied }
135
180
  : await this.registry.dispatch(call.function.name, call.function.arguments);
136
- emit({ type: 'tool_call_end', id: call.id, name: call.function.name, result });
181
+ emit({
182
+ type: 'tool_call_end',
183
+ id: call.id,
184
+ name: call.function.name,
185
+ result,
186
+ });
137
187
  session.addToolMessage(call.id, call.function.name, result);
138
188
  }
139
189
  }
@@ -141,10 +191,17 @@ export class TurnLoop {
141
191
  emit({ type: 'turn_interrupted' });
142
192
  return;
143
193
  }
144
- // Loop: stream again now that tool results are in the messages.
194
+ // Steering: text typed mid-turn joins the conversation before the next model call.
195
+ for (const content of opts.takeSteering?.() ?? []) {
196
+ session.addUserMessage(content);
197
+ }
198
+ // Loop: stream again now that tool results (and any steering) are in the messages.
145
199
  }
146
200
  session.rollbackTo(checkpoint);
147
- emit({ type: 'error', message: `Turn exceeded ${MAX_ROUNDS} tool-calling rounds` });
201
+ emit({
202
+ type: 'error',
203
+ message: `Turn exceeded ${MAX_ROUNDS} tool-calling rounds`,
204
+ });
148
205
  }
149
206
  /** Stream a single model call, emitting text deltas and accumulating tool_calls. */
150
207
  async streamOneCall(session, emit, opts, forceNoTools) {
@@ -159,8 +216,9 @@ export class TurnLoop {
159
216
  };
160
217
  let content = '';
161
218
  const toolAcc = new Map();
219
+ let finishReason = null;
162
220
  try {
163
- await this.consumeStream(req, opts, emit, (text) => (content += text), toolAcc);
221
+ finishReason = await this.consumeStream(req, opts, emit, (text) => (content += text), toolAcc);
164
222
  }
165
223
  catch (err) {
166
224
  // Attach the partial text so an interrupt can preserve what already streamed.
@@ -178,10 +236,14 @@ export class TurnLoop {
178
236
  ...(e.extraContent !== undefined ? { extra_content: e.extraContent } : {}),
179
237
  }))
180
238
  .filter((c) => c.function.name.length > 0);
181
- return { content, toolCalls };
239
+ return { content, toolCalls, finishReason };
182
240
  }
183
- /** Iterate the backend stream, forwarding deltas/usage and accumulating tool-call pieces. */
241
+ /**
242
+ * Iterate the backend stream, forwarding deltas/usage and accumulating tool-call pieces.
243
+ * Returns the last non-null finish_reason seen (providers send it on the closing chunk).
244
+ */
184
245
  async consumeStream(req, opts, emit, onText, toolAcc) {
246
+ let finishReason = null;
185
247
  for await (const chunk of this.backend.stream(req, opts.signal)) {
186
248
  // The final chunk (include_usage) carries token counts with empty/no choices; surface
187
249
  // them before the delta guard below skips it.
@@ -193,12 +255,16 @@ export class TurnLoop {
193
255
  });
194
256
  }
195
257
  const choice = chunk.choices?.[0];
258
+ if (typeof choice?.finish_reason === 'string')
259
+ finishReason = choice.finish_reason;
196
260
  const delta = choice?.delta;
197
261
  if (!delta)
198
262
  continue;
199
263
  if (typeof delta.content === 'string' && delta.content.length > 0) {
200
264
  onText(delta.content);
201
- emit({ type: 'assistant_delta', text: delta.content });
265
+ const r = emit({ type: 'assistant_delta', text: delta.content });
266
+ if (r)
267
+ await r; // backpressure: wait for the consumer to drain
202
268
  }
203
269
  if (delta.tool_calls) {
204
270
  for (const tc of delta.tool_calls) {
@@ -215,6 +281,7 @@ export class TurnLoop {
215
281
  }
216
282
  }
217
283
  }
284
+ return finishReason;
218
285
  }
219
286
  }
220
287
  /** Thrown when the user aborts mid-stream; carries the text streamed so far. */