@jsonstudio/llms 0.6.567 → 0.6.586

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 (62) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.js +33 -4
  2. package/dist/conversion/codecs/openai-openai-codec.js +2 -1
  3. package/dist/conversion/codecs/responses-openai-codec.js +3 -2
  4. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  5. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  6. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -2
  7. package/dist/conversion/hub/pipeline/hub-pipeline.js +72 -81
  8. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +0 -34
  9. package/dist/conversion/hub/process/chat-process.js +68 -24
  10. package/dist/conversion/hub/response/provider-response.js +0 -8
  11. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +22 -3
  12. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +267 -14
  13. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  14. package/dist/conversion/responses/responses-openai-bridge.d.ts +3 -2
  15. package/dist/conversion/responses/responses-openai-bridge.js +1 -13
  16. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  17. package/dist/conversion/shared/args-mapping.js +11 -3
  18. package/dist/conversion/shared/responses-output-builder.js +42 -21
  19. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  20. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  21. package/dist/conversion/shared/text-markup-normalizer.d.ts +20 -0
  22. package/dist/conversion/shared/text-markup-normalizer.js +118 -31
  23. package/dist/conversion/shared/tool-filter-pipeline.js +56 -30
  24. package/dist/conversion/shared/tool-harvester.js +43 -12
  25. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  26. package/dist/conversion/shared/tool-mapping.js +33 -19
  27. package/dist/filters/index.d.ts +1 -0
  28. package/dist/filters/index.js +1 -0
  29. package/dist/filters/special/request-tools-normalize.js +14 -4
  30. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  31. package/dist/filters/special/response-apply-patch-toon-decode.js +117 -0
  32. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  33. package/dist/filters/special/response-tool-arguments-toon-decode.js +154 -26
  34. package/dist/guidance/index.js +71 -42
  35. package/dist/router/virtual-router/bootstrap.js +10 -5
  36. package/dist/router/virtual-router/classifier.js +16 -7
  37. package/dist/router/virtual-router/engine-health.d.ts +11 -0
  38. package/dist/router/virtual-router/engine-health.js +217 -4
  39. package/dist/router/virtual-router/engine-logging.d.ts +2 -1
  40. package/dist/router/virtual-router/engine-logging.js +35 -3
  41. package/dist/router/virtual-router/engine.d.ts +17 -1
  42. package/dist/router/virtual-router/engine.js +184 -6
  43. package/dist/router/virtual-router/routing-instructions.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-instructions.js +19 -1
  45. package/dist/router/virtual-router/tool-signals.d.ts +2 -1
  46. package/dist/router/virtual-router/tool-signals.js +324 -119
  47. package/dist/router/virtual-router/types.d.ts +31 -1
  48. package/dist/router/virtual-router/types.js +2 -2
  49. package/dist/servertool/engine.js +3 -0
  50. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  51. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  52. package/dist/servertool/handlers/stop-message-auto.js +61 -4
  53. package/dist/servertool/server-side-tools.d.ts +1 -0
  54. package/dist/servertool/server-side-tools.js +27 -0
  55. package/dist/sse/json-to-sse/event-generators/responses.js +9 -2
  56. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +23 -3
  57. package/dist/tools/apply-patch-structured.d.ts +20 -0
  58. package/dist/tools/apply-patch-structured.js +240 -0
  59. package/dist/tools/tool-description-utils.d.ts +5 -0
  60. package/dist/tools/tool-description-utils.js +50 -0
  61. package/dist/tools/tool-registry.js +11 -193
  62. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  // Streaming textual tool intent extractor (对齐)
2
- // Detects <function=execute> blocks and unified diff patches
2
+ // Detects <function=execute> blocks and structured apply_patch payloads
3
3
  // and converts them into OpenAI tool_calls incrementally.
4
+ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
4
5
  function isObject(v) {
5
6
  return !!v && typeof v === 'object' && !Array.isArray(v);
6
7
  }
@@ -8,22 +9,20 @@ export class StreamingTextToolExtractor {
8
9
  opts;
9
10
  buffer = '';
10
11
  idCounter = 0;
11
- pendingPatch = { active: false, lines: [] };
12
12
  constructor(opts = {}) {
13
13
  this.opts = opts;
14
14
  }
15
15
  reset() {
16
16
  this.buffer = '';
17
17
  this.idCounter = 0;
18
- this.pendingPatch = { active: false, lines: [] };
19
18
  }
20
19
  feedText(text) {
21
20
  const out = [];
22
21
  if (typeof text !== 'string' || !text)
23
22
  return out;
24
23
  this.buffer += text;
25
- // 1) Unified diff apply_patch block detection (*** Begin Patch ... *** End Patch)
26
- out.push(...this.tryExtractUnifiedDiffBlocks());
24
+ // 1) Structured apply_patch block detection (```json ... ```)
25
+ out.push(...this.tryExtractStructuredBlocks());
27
26
  // 2) <function=execute> compact blocks detection
28
27
  out.push(...this.tryExtractFunctionExecuteBlocks());
29
28
  return out;
@@ -42,42 +41,36 @@ export class StreamingTextToolExtractor {
42
41
  }
43
42
  return { id: this.genId(), type: 'function', function: { name, arguments: argStr } };
44
43
  }
45
- tryExtractUnifiedDiffBlocks() {
44
+ tryExtractStructuredBlocks() {
46
45
  const out = [];
47
- // Stream-friendly: if we see Begin Patch, start accumulating until End Patch
48
- const beginIdx = this.buffer.indexOf('*** Begin Patch');
49
- if (beginIdx >= 0) {
50
- // Start patch if not already active
51
- if (!this.pendingPatch.active) {
52
- this.pendingPatch.active = true;
53
- this.pendingPatch.lines = [];
54
- this.pendingPatch.lines.push(this.buffer.slice(beginIdx));
55
- // trim buffer before begin to reduce size
56
- this.buffer = this.buffer.slice(beginIdx);
57
- }
58
- else {
59
- // append new data
60
- this.pendingPatch.lines.push(this.buffer);
61
- }
62
- }
63
- if (this.pendingPatch.active) {
64
- const joined = this.pendingPatch.lines.join('');
65
- const endIdx = joined.indexOf('*** End Patch');
66
- if (endIdx >= 0) {
67
- const patchEnd = endIdx + '*** End Patch'.length;
68
- const patchText = joined.slice(0, patchEnd);
69
- out.push(this.toToolCall('apply_patch', { patch: patchText }));
70
- // consume used part
71
- const remainder = joined.slice(patchEnd);
72
- this.pendingPatch = { active: false, lines: [] };
73
- this.buffer = remainder;
74
- }
75
- else {
76
- // keep accumulating, but limit memory
77
- if (joined.length > 200000) {
78
- this.pendingPatch.lines = [joined.slice(-100000)];
46
+ let searchIdx = 0;
47
+ while (searchIdx < this.buffer.length) {
48
+ const startIdx = this.buffer.indexOf('```', searchIdx);
49
+ if (startIdx < 0)
50
+ break;
51
+ const headerEnd = this.buffer.indexOf('\n', startIdx + 3);
52
+ if (headerEnd < 0)
53
+ break;
54
+ const language = this.buffer.slice(startIdx + 3, headerEnd).trim().toLowerCase();
55
+ const endIdx = this.buffer.indexOf('```', headerEnd + 1);
56
+ if (endIdx < 0)
57
+ break;
58
+ const body = this.buffer.slice(headerEnd + 1, endIdx);
59
+ if (!language || language === 'json' || language === 'apply_patch' || language === 'toon') {
60
+ try {
61
+ const parsed = JSON.parse(body);
62
+ if (isStructuredApplyPatchPayload(parsed)) {
63
+ out.push(this.toToolCall('apply_patch', parsed));
64
+ this.buffer = this.buffer.slice(0, startIdx) + this.buffer.slice(endIdx + 3);
65
+ searchIdx = 0;
66
+ continue;
67
+ }
68
+ }
69
+ catch {
70
+ /* ignore parse errors */
79
71
  }
80
72
  }
73
+ searchIdx = endIdx + 3;
81
74
  }
82
75
  return out;
83
76
  }
@@ -0,0 +1,20 @@
1
+ export type ToolCallLite = {
2
+ id?: string;
3
+ name: string;
4
+ args: string;
5
+ };
6
+ export declare function extractApplyPatchCallsFromText(text: string): ToolCallLite[] | null;
7
+ export declare function extractExecuteBlocksFromText(text: string): ToolCallLite[] | null;
8
+ export declare function extractXMLToolCallsFromText(text: string): ToolCallLite[] | null;
9
+ /**
10
+ * 提取简单 XML 形式的工具调用块,例如:
11
+ *
12
+ * <list_directory>
13
+ * <path>/path/to/dir</path>
14
+ * <recursive>false</recursive>
15
+ * </list_directory>
16
+ *
17
+ * 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
18
+ */
19
+ export declare function extractSimpleXmlToolsFromText(text: string): ToolCallLite[] | null;
20
+ export declare function normalizeAssistantTextToToolCalls(message: Record<string, any>): Record<string, any>;
@@ -1,6 +1,7 @@
1
1
  // Normalize textual markup into OpenAI tool_calls shape.
2
2
  // Gated by RCC_TEXT_MARKUP_COMPAT=1 to avoid overreach.
3
3
  import { isImagePath } from './media.js';
4
+ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
4
5
  // Strict allowlist for tool names and their argument keys to avoid picking up
5
6
  // stray markup or free-form text as JSON keys (reduces false positives).
6
7
  const KNOWN_TOOLS = new Set([
@@ -10,16 +11,19 @@ const KNOWN_TOOLS = new Set([
10
11
  'view_image',
11
12
  'list_mcp_resources',
12
13
  'read_mcp_resource',
13
- 'list_mcp_resource_templates'
14
+ 'list_mcp_resource_templates',
15
+ // 文件/目录类工具(CLI 侧已有约定;此处只做文本→tool_calls 收割)
16
+ 'list_directory'
14
17
  ]);
15
18
  const ALLOWED_KEYS = {
16
19
  shell: new Set(['command', 'justification', 'timeout_ms', 'with_escalated_permissions', 'workdir']),
17
- apply_patch: new Set(['patch']),
20
+ apply_patch: new Set(['file', 'instructions', 'changes']),
18
21
  update_plan: new Set(['explanation', 'plan']),
19
22
  view_image: new Set(['path']),
20
23
  list_mcp_resources: new Set(['server', 'cursor', 'filter', 'root']),
21
24
  read_mcp_resource: new Set(['server', 'uri', 'cursor']),
22
- list_mcp_resource_templates: new Set(['server', 'cursor'])
25
+ list_mcp_resource_templates: new Set(['server', 'cursor']),
26
+ list_directory: new Set(['path', 'recursive'])
23
27
  };
24
28
  function normalizeKey(raw) {
25
29
  try {
@@ -70,40 +74,54 @@ function enabled() {
70
74
  }
71
75
  }
72
76
  // 已移除所有 rcc.tool.v1 相关处理:不再识别或剥离 rcc 封装
73
- export function extractApplyPatchCallsFromText(text) {
77
+ function extractStructuredApplyPatchPayloads(text) {
78
+ const payloads = [];
74
79
  try {
75
- if (typeof text !== 'string' || !text)
76
- return null;
77
- const out = [];
78
- const candidates = [];
79
- const fenceRe = /```(?:patch)?\s*([\s\S]*?)\s*```/gi;
80
+ const fenceRe = /```(?:json|apply_patch|toon)?\s*([\s\S]*?)\s*```/gi;
80
81
  let fm;
81
82
  while ((fm = fenceRe.exec(text)) !== null) {
82
83
  const body = fm[1] || '';
83
- if (/\*\*\*\s+Begin Patch[\s\S]*?\*\*\*\s+End Patch/.test(body))
84
- candidates.push(body);
85
- }
86
- if (/\*\*\*\s+Begin Patch[\s\S]*?\*\*\*\s+End Patch/.test(text))
87
- candidates.push(text);
88
- const genId = () => `call_${Math.random().toString(36).slice(2, 10)}`;
89
- for (const src of candidates) {
90
- const pg = /\*\*\*\s+Begin Patch[\s\S]*?\*\*\*\s+End Patch/gm;
91
- let pm;
92
- while ((pm = pg.exec(src)) !== null) {
93
- const patch = pm[0];
94
- if (!patch || patch.length < 32)
95
- continue;
96
- let argsStr = '{}';
97
- try {
98
- argsStr = JSON.stringify({ patch });
84
+ try {
85
+ const parsed = JSON.parse(body);
86
+ if (isStructuredApplyPatchPayload(parsed)) {
87
+ payloads.push(parsed);
99
88
  }
100
- catch {
101
- argsStr = '{"patch":""}';
89
+ }
90
+ catch { /* ignore invalid JSON */ }
91
+ }
92
+ if (!payloads.length && typeof text === 'string' && text.includes('"changes"')) {
93
+ try {
94
+ const parsed = JSON.parse(text);
95
+ if (isStructuredApplyPatchPayload(parsed)) {
96
+ payloads.push(parsed);
102
97
  }
103
- out.push({ id: genId(), name: 'apply_patch', args: argsStr });
104
98
  }
99
+ catch { /* ignore */ }
105
100
  }
106
- return out.length ? out : null;
101
+ }
102
+ catch { /* ignore */ }
103
+ return payloads;
104
+ }
105
+ export function extractApplyPatchCallsFromText(text) {
106
+ try {
107
+ if (typeof text !== 'string' || !text)
108
+ return null;
109
+ const payloads = extractStructuredApplyPatchPayloads(text);
110
+ if (!payloads.length)
111
+ return null;
112
+ const out = [];
113
+ const genId = () => `call_${Math.random().toString(36).slice(2, 10)}`;
114
+ for (const payload of payloads) {
115
+ let argsStr = '{}';
116
+ try {
117
+ argsStr = JSON.stringify(payload);
118
+ }
119
+ catch {
120
+ argsStr = '{"changes":[]}';
121
+ }
122
+ out.push({ id: genId(), name: 'apply_patch', args: argsStr });
123
+ }
124
+ return out;
107
125
  }
108
126
  catch {
109
127
  return null;
@@ -290,6 +308,74 @@ export function extractXMLToolCallsFromText(text) {
290
308
  return null;
291
309
  }
292
310
  }
311
+ /**
312
+ * 提取简单 XML 形式的工具调用块,例如:
313
+ *
314
+ * <list_directory>
315
+ * <path>/path/to/dir</path>
316
+ * <recursive>false</recursive>
317
+ * </list_directory>
318
+ *
319
+ * 仅针对已知工具名(目前为 list_directory),避免误伤普通 XML 文本。
320
+ */
321
+ export function extractSimpleXmlToolsFromText(text) {
322
+ try {
323
+ if (typeof text !== 'string' || !text)
324
+ return null;
325
+ const out = [];
326
+ const blockRe = /<\s*([A-Za-z0-9_.-]+)\s*>([\s\S]*?)<\/\s*\1\s*>/gi;
327
+ let bm;
328
+ while ((bm = blockRe.exec(text)) !== null) {
329
+ const rawName = (bm[1] || '').trim();
330
+ const lname = rawName.toLowerCase();
331
+ if (!lname || lname === 'tool_call')
332
+ continue;
333
+ // 目前仅支持 list_directory,后续按需扩展
334
+ if (lname !== 'list_directory')
335
+ continue;
336
+ const inner = bm[2] || '';
337
+ const args = {};
338
+ const argRe = /<\s*([A-Za-z0-9_]+)\s*>([\s\S]*?)<\/\s*\1\s*>/gi;
339
+ let am;
340
+ while ((am = argRe.exec(inner)) !== null) {
341
+ const key = normalizeKey((am[1] || '').trim());
342
+ if (!key)
343
+ continue;
344
+ let rawVal = (am[2] || '').trim();
345
+ let v = rawVal;
346
+ if ((rawVal.startsWith('[') && rawVal.endsWith(']')) || (rawVal.startsWith('{') && rawVal.endsWith('}'))) {
347
+ try {
348
+ v = JSON.parse(rawVal);
349
+ }
350
+ catch {
351
+ v = rawVal;
352
+ }
353
+ }
354
+ else if (rawVal === 'true' || rawVal === 'false') {
355
+ v = rawVal === 'true';
356
+ }
357
+ args[key] = v;
358
+ }
359
+ const filtered = filterArgsForTool(lname, args);
360
+ let argsStr = '{}';
361
+ try {
362
+ argsStr = JSON.stringify(filtered);
363
+ }
364
+ catch {
365
+ argsStr = '{}';
366
+ }
367
+ out.push({
368
+ id: `call_${Math.random().toString(36).slice(2, 10)}`,
369
+ name: lname,
370
+ args: argsStr
371
+ });
372
+ }
373
+ return out.length ? out : null;
374
+ }
375
+ catch {
376
+ return null;
377
+ }
378
+ }
293
379
  export function normalizeAssistantTextToToolCalls(message) {
294
380
  if (!enabled())
295
381
  return message;
@@ -302,10 +388,11 @@ export function normalizeAssistantTextToToolCalls(message) {
302
388
  const text = typeof content === 'string' ? content : null;
303
389
  if (!text)
304
390
  return message;
305
- // Order: xml-like tool_call → apply_patch → execute blocks
391
+ // Order: xml-like <tool_call> → apply_patch → execute blocks → 简单 XML 工具(list_directory 等)
306
392
  const calls = (extractXMLToolCallsFromText(text) ||
307
393
  extractApplyPatchCallsFromText(text) ||
308
- extractExecuteBlocksFromText(text));
394
+ extractExecuteBlocksFromText(text) ||
395
+ extractSimpleXmlToolsFromText(text));
309
396
  if (calls && calls.length) {
310
397
  const toolCalls = calls.map((c) => ({ id: c.id, type: 'function', function: { name: c.name, arguments: c.args } }));
311
398
  const copy = { ...message };
@@ -134,42 +134,67 @@ function applyLocalToolGovernance(chatRequest, rawPayload) {
134
134
  };
135
135
  }
136
136
  function detectImageHint(messages, rawPayload) {
137
- const candidates = [];
138
- const collect = (value) => {
139
- if (typeof value === 'string' && value) {
140
- candidates.push(value);
141
- }
137
+ const patterns = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
138
+ const hasImageExt = (value) => {
139
+ if (typeof value !== 'string' || !value)
140
+ return false;
141
+ const lower = value.toLowerCase();
142
+ return patterns.some(ext => lower.includes(ext));
142
143
  };
143
- if (Array.isArray(messages)) {
144
- for (const msg of messages) {
145
- if (msg && typeof msg === 'object') {
146
- const text = msg.content;
147
- if (typeof text === 'string') {
148
- collect(text);
149
- }
150
- else if (Array.isArray(text)) {
151
- for (const part of text) {
152
- if (part && typeof part === 'object') {
153
- collect(part.text);
154
- }
155
- }
144
+ const hasImageInMessage = (msg) => {
145
+ if (!msg || typeof msg !== 'object')
146
+ return false;
147
+ const m = msg;
148
+ const content = m.content;
149
+ if (typeof content === 'string') {
150
+ if (hasImageExt(content))
151
+ return true;
152
+ }
153
+ else if (Array.isArray(content)) {
154
+ for (const part of content) {
155
+ if (!part || typeof part !== 'object')
156
+ continue;
157
+ const p = part;
158
+ const t = String(p.type || '').toLowerCase();
159
+ if (t.includes('image')) {
160
+ return true;
156
161
  }
162
+ if (hasImageExt(p.text))
163
+ return true;
164
+ const imageUrl = typeof p.image_url === 'string'
165
+ ? p.image_url
166
+ : p.image_url && typeof p.image_url.url === 'string'
167
+ ? p.image_url.url
168
+ : typeof p.url === 'string'
169
+ ? p.url
170
+ : undefined;
171
+ if (hasImageExt(imageUrl))
172
+ return true;
173
+ if (hasImageExt(p.path))
174
+ return true;
157
175
  }
158
176
  }
159
- }
160
- if (rawPayload && typeof rawPayload === 'object') {
161
- collect(rawPayload.content);
162
- }
163
- if (!candidates.length) {
164
177
  return false;
165
- }
166
- const patterns = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg'];
167
- for (const text of candidates) {
168
- const lower = text.toLowerCase();
169
- for (const ext of patterns) {
170
- if (lower.includes(ext)) {
178
+ };
179
+ // 仅考虑“当前这一轮”的用户输入是否包含图片链接或图片负载,避免因为历史上下文中曾经出现过图片而在后续轮次持续暴露 view_image。
180
+ if (Array.isArray(messages)) {
181
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
182
+ const msg = messages[i];
183
+ if (!msg || typeof msg !== 'object')
184
+ continue;
185
+ const role = String(msg.role || '').toLowerCase();
186
+ if (role !== 'user')
187
+ continue;
188
+ if (hasImageInMessage(msg)) {
171
189
  return true;
172
190
  }
191
+ break;
192
+ }
193
+ }
194
+ if (rawPayload && typeof rawPayload === 'object') {
195
+ const text = rawPayload.content;
196
+ if (hasImageExt(text)) {
197
+ return true;
173
198
  }
174
199
  }
175
200
  return false;
@@ -212,8 +237,9 @@ export async function runChatResponseToolFilters(chatJson, options = {}) {
212
237
  const { ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } = await import('../../filters/index.js');
213
238
  register(new ResponseToolTextCanonicalizeFilter());
214
239
  try {
215
- const { ResponseToolArgumentsToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
240
+ const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
216
241
  register(new ResponseToolArgumentsToonDecodeFilter());
242
+ register(new ResponseApplyPatchToonDecodeFilter());
217
243
  try {
218
244
  register(new ResponseToolArgumentsSchemaConvergeFilter());
219
245
  }
@@ -1,8 +1,9 @@
1
1
  // Unified tool harvesting (对齐, single entry)
2
2
  // - First-time harvesting only (no late-stage repair)
3
- // - Handles textual markers (<function=execute>, unified diff)
3
+ // - Handles textual markers (<function=execute>, structured apply_patch payloads)
4
4
  // - Handles structural shapes (function_call legacy, tool_calls)
5
5
  // - Normalizes arguments (single JSON string), sets finish_reason when applicable
6
+ import { isStructuredApplyPatchPayload } from '../../tools/apply-patch-structured.js';
6
7
  function isObject(v) {
7
8
  return !!v && typeof v === 'object' && !Array.isArray(v);
8
9
  }
@@ -48,17 +49,19 @@ function extractFromTextual(content, ctx) {
48
49
  const events = [];
49
50
  if (typeof content !== 'string' || !content)
50
51
  return events;
51
- // 1) unified diff
52
- const beginIdx = content.indexOf('*** Begin Patch');
53
- const endIdx = content.indexOf('*** End Patch');
54
- if (beginIdx >= 0 && endIdx > beginIdx) {
55
- const patchText = content.slice(beginIdx, endIdx + '*** End Patch'.length);
56
- const id = genId(ctx, 0);
57
- const argStr = toJsonString({ patch: patchText });
58
- events.push({ tool_calls: [{ index: 0, id, type: 'function', function: { name: 'apply_patch' } }] });
59
- const parts = chunkString(argStr, Math.max(32, Math.min(1024, ctx?.chunkSize || 256)));
60
- for (const d of parts) {
61
- events.push({ tool_calls: [{ index: 0, id, type: 'function', function: { arguments: d } }] });
52
+ // 1) structured apply_patch payload
53
+ const structuredPayloads = extractStructuredApplyPatchPayloads(content);
54
+ if (structuredPayloads.length) {
55
+ let idx = 0;
56
+ for (const payload of structuredPayloads) {
57
+ const id = genId(ctx, idx);
58
+ const argStr = toJsonString(payload);
59
+ events.push({ tool_calls: [{ index: idx, id, type: 'function', function: { name: 'apply_patch' } }] });
60
+ const parts = chunkString(argStr, Math.max(32, Math.min(1024, ctx?.chunkSize || 256)));
61
+ for (const d of parts) {
62
+ events.push({ tool_calls: [{ index: idx, id, type: 'function', function: { arguments: d } }] });
63
+ }
64
+ idx += 1;
62
65
  }
63
66
  return events;
64
67
  }
@@ -157,6 +160,34 @@ function extractFromTextual(content, ctx) {
157
160
  catch { /* ignore textual tool_call parse errors */ }
158
161
  return events;
159
162
  }
163
+ function extractStructuredApplyPatchPayloads(text) {
164
+ const payloads = [];
165
+ try {
166
+ const fenceRe = /```(?:json|apply_patch|toon)?\s*([\s\S]*?)\s*```/gi;
167
+ let fm;
168
+ while ((fm = fenceRe.exec(text)) !== null) {
169
+ const body = fm[1] || '';
170
+ try {
171
+ const parsed = JSON.parse(body);
172
+ if (isStructuredApplyPatchPayload(parsed)) {
173
+ payloads.push(parsed);
174
+ }
175
+ }
176
+ catch { /* ignore invalid JSON */ }
177
+ }
178
+ if (!payloads.length && typeof text === 'string' && text.includes('"changes"')) {
179
+ try {
180
+ const parsed = JSON.parse(text);
181
+ if (isStructuredApplyPatchPayload(parsed)) {
182
+ payloads.push(parsed);
183
+ }
184
+ }
185
+ catch { /* ignore */ }
186
+ }
187
+ }
188
+ catch { /* ignore */ }
189
+ return payloads;
190
+ }
160
191
  function splitCommand(s) {
161
192
  try {
162
193
  const out = [];
@@ -13,6 +13,7 @@ export declare function stringifyArgs(args: unknown): string;
13
13
  export interface BridgeToolMapOptions {
14
14
  sanitizeName?: (raw: unknown) => string | undefined;
15
15
  }
16
+ export declare function ensureApplyPatchSchema(seed?: Record<string, unknown>): Record<string, unknown>;
16
17
  export declare function bridgeToolToChatDefinition(rawTool: BridgeToolDefinition | Record<string, unknown> | null | undefined, options?: BridgeToolMapOptions): ChatToolDefinition | null;
17
18
  export declare function mapBridgeToolsToChat(rawTools: unknown, options?: BridgeToolMapOptions): ChatToolDefinition[] | undefined;
18
19
  export declare function chatToolToBridgeDefinition(rawTool: ChatToolDefinition | Record<string, unknown> | null | undefined, options?: BridgeToolMapOptions): BridgeToolDefinition | null;
@@ -25,30 +25,44 @@ function asSchema(value) {
25
25
  }
26
26
  return undefined;
27
27
  }
28
- function ensureApplyPatchSchema(seed) {
28
+ export function ensureApplyPatchSchema(seed) {
29
29
  const schema = seed ? { ...seed } : {};
30
30
  schema.type = typeof schema.type === 'string' ? schema.type : 'object';
31
31
  const properties = isPlainObject(schema.properties) ? { ...schema.properties } : {};
32
- delete properties.input;
33
- delete properties.patch;
34
- properties.patch = {
35
- type: 'string',
36
- description: 'Unified diff patch text. The first line MUST be exactly "*** Begin Patch", followed by one or more file sections starting with ' +
37
- '"*** Add File: {path}", "*** Update File: {path}", or "*** Delete File: {path}", using hunks with lines prefixed by " ", "+", or "-". ' +
38
- 'The last non-empty line MUST be exactly "*** End Patch". Git-style headers like "--- a/file" / "+++ b/file" and merge markers ' +
39
- '"<<<<<<<", "=======", ">>>>>>>" are NOT allowed.'
32
+ properties.file = { type: 'string', description: 'Optional default file path for all changes' };
33
+ properties.instructions = { type: 'string', description: 'Optional summary of the edit' };
34
+ properties.changes = {
35
+ type: 'array',
36
+ minItems: 1,
37
+ items: {
38
+ type: 'object',
39
+ additionalProperties: false,
40
+ required: ['kind'],
41
+ properties: {
42
+ file: { type: 'string', description: 'Relative path for this change' },
43
+ kind: {
44
+ type: 'string',
45
+ description: 'insert_after | insert_before | replace | delete | create_file | delete_file'
46
+ },
47
+ anchor: { type: 'string', description: 'Context snippet for insert operations' },
48
+ target: { type: 'string', description: 'Snippet to replace/delete' },
49
+ lines: {
50
+ description: 'New content for insert/replace/create operations',
51
+ oneOf: [
52
+ { type: 'string' },
53
+ { type: 'array', items: { type: 'string' } }
54
+ ]
55
+ },
56
+ use_anchor_indent: { type: 'boolean', description: 'Reuse indentation from the anchor snippet' }
57
+ }
58
+ }
40
59
  };
41
- if (!isPlainObject(properties.paths)) {
42
- properties.paths = {
43
- type: 'array',
44
- description: 'Optional explicit list of relative file paths that the patch touches. Each entry must be a relative workspace path (packages/foo/file.ts).',
45
- items: { type: 'string' }
46
- };
47
- }
48
60
  schema.properties = properties;
49
- const requiredList = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === 'string') : [];
50
- if (!requiredList.includes('patch')) {
51
- requiredList.push('patch');
61
+ const requiredList = Array.isArray(schema.required)
62
+ ? schema.required.filter((entry) => typeof entry === 'string')
63
+ : [];
64
+ if (!requiredList.includes('changes')) {
65
+ requiredList.push('changes');
52
66
  }
53
67
  schema.required = requiredList;
54
68
  if (typeof schema.additionalProperties !== 'boolean') {
@@ -12,6 +12,7 @@ export * from './special/response-tool-arguments-stringify.js';
12
12
  export * from './special/response-finish-invariants.js';
13
13
  export * from './special/request-tools-normalize.js';
14
14
  export * from './special/response-tool-arguments-toon-decode.js';
15
+ export * from './special/response-apply-patch-toon-decode.js';
15
16
  export * from './special/response-tool-arguments-blacklist.js';
16
17
  export * from './special/response-tool-arguments-schema-converge.js';
17
18
  export * from './special/response-tool-arguments-whitelist.js';
@@ -15,6 +15,7 @@ export * from './special/response-finish-invariants.js';
15
15
  // TOON support (default ON via RCC_TOON_ENABLE unless explicitly disabled)
16
16
  export * from './special/request-tools-normalize.js';
17
17
  export * from './special/response-tool-arguments-toon-decode.js';
18
+ export * from './special/response-apply-patch-toon-decode.js';
18
19
  // Arguments policy filters (synced)
19
20
  export * from './special/response-tool-arguments-blacklist.js';
20
21
  export * from './special/response-tool-arguments-schema-converge.js';