@jsonstudio/llms 0.6.473 → 0.6.568

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 (82) 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/claude-thinking-tools.d.ts +15 -0
  5. package/dist/conversion/compat/actions/claude-thinking-tools.js +72 -0
  6. package/dist/conversion/compat/actions/glm-history-image-trim.d.ts +2 -0
  7. package/dist/conversion/compat/actions/glm-history-image-trim.js +88 -0
  8. package/dist/conversion/compat/profiles/chat-gemini.json +15 -14
  9. package/dist/conversion/compat/profiles/chat-glm.json +194 -194
  10. package/dist/conversion/compat/profiles/chat-iflow.json +199 -199
  11. package/dist/conversion/compat/profiles/chat-lmstudio.json +43 -43
  12. package/dist/conversion/compat/profiles/chat-qwen.json +20 -20
  13. package/dist/conversion/compat/profiles/responses-c4m.json +42 -42
  14. package/dist/conversion/compat/profiles/responses-output2choices-test.json +12 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  16. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +2 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -1
  18. package/dist/conversion/hub/pipeline/hub-pipeline.js +40 -13
  19. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +15 -0
  20. package/dist/conversion/hub/process/chat-process.js +107 -26
  21. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +8 -0
  22. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +28 -10
  23. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +51 -2
  24. package/dist/conversion/hub/tool-session-compat.d.ts +26 -0
  25. package/dist/conversion/hub/tool-session-compat.js +299 -0
  26. package/dist/conversion/hub/types/chat-envelope.d.ts +1 -0
  27. package/dist/conversion/responses/responses-openai-bridge.d.ts +0 -1
  28. package/dist/conversion/responses/responses-openai-bridge.js +0 -71
  29. package/dist/conversion/shared/anthropic-message-utils.js +54 -0
  30. package/dist/conversion/shared/args-mapping.js +11 -3
  31. package/dist/conversion/shared/gemini-tool-utils.js +8 -0
  32. package/dist/conversion/shared/responses-output-builder.js +47 -88
  33. package/dist/conversion/shared/streaming-text-extractor.d.ts +25 -0
  34. package/dist/conversion/shared/streaming-text-extractor.js +31 -38
  35. package/dist/conversion/shared/text-markup-normalizer.js +42 -27
  36. package/dist/conversion/shared/tool-filter-pipeline.js +2 -1
  37. package/dist/conversion/shared/tool-governor.js +75 -4
  38. package/dist/conversion/shared/tool-harvester.js +43 -12
  39. package/dist/conversion/shared/tool-mapping.d.ts +1 -0
  40. package/dist/conversion/shared/tool-mapping.js +33 -13
  41. package/dist/filters/index.d.ts +1 -0
  42. package/dist/filters/index.js +1 -0
  43. package/dist/filters/special/request-toolcalls-stringify.js +5 -55
  44. package/dist/filters/special/request-tools-normalize.js +14 -23
  45. package/dist/filters/special/response-apply-patch-toon-decode.d.ts +23 -0
  46. package/dist/filters/special/response-apply-patch-toon-decode.js +109 -0
  47. package/dist/filters/special/response-tool-arguments-toon-decode.d.ts +10 -0
  48. package/dist/filters/special/response-tool-arguments-toon-decode.js +55 -13
  49. package/dist/guidance/index.js +70 -27
  50. package/dist/router/virtual-router/bootstrap.js +10 -5
  51. package/dist/router/virtual-router/classifier.js +9 -4
  52. package/dist/router/virtual-router/engine-health.d.ts +22 -0
  53. package/dist/router/virtual-router/engine-health.js +423 -0
  54. package/dist/router/virtual-router/engine-logging.d.ts +20 -0
  55. package/dist/router/virtual-router/engine-logging.js +197 -0
  56. package/dist/router/virtual-router/engine-selection.d.ts +32 -0
  57. package/dist/router/virtual-router/engine-selection.js +649 -0
  58. package/dist/router/virtual-router/engine.d.ts +21 -14
  59. package/dist/router/virtual-router/engine.js +200 -523
  60. package/dist/router/virtual-router/message-utils.js +22 -0
  61. package/dist/router/virtual-router/routing-instructions.d.ts +8 -1
  62. package/dist/router/virtual-router/routing-instructions.js +137 -3
  63. package/dist/router/virtual-router/tool-signals.js +57 -11
  64. package/dist/router/virtual-router/types.d.ts +30 -0
  65. package/dist/router/virtual-router/types.js +1 -1
  66. package/dist/servertool/engine.js +3 -0
  67. package/dist/servertool/handlers/gemini-empty-reply-continue.d.ts +1 -0
  68. package/dist/servertool/handlers/gemini-empty-reply-continue.js +120 -0
  69. package/dist/servertool/handlers/iflow-model-error-retry.d.ts +1 -0
  70. package/dist/servertool/handlers/iflow-model-error-retry.js +93 -0
  71. package/dist/servertool/handlers/stop-message-auto.d.ts +1 -0
  72. package/dist/servertool/handlers/stop-message-auto.js +204 -0
  73. package/dist/servertool/handlers/vision.js +105 -7
  74. package/dist/servertool/server-side-tools.d.ts +3 -0
  75. package/dist/servertool/server-side-tools.js +29 -0
  76. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +16 -0
  77. package/dist/tools/apply-patch-structured.d.ts +20 -0
  78. package/dist/tools/apply-patch-structured.js +239 -0
  79. package/dist/tools/tool-description-utils.d.ts +5 -0
  80. package/dist/tools/tool-description-utils.js +50 -0
  81. package/dist/tools/tool-registry.js +14 -5
  82. package/package.json +2 -2
@@ -0,0 +1,25 @@
1
+ export interface StreamingToolCall {
2
+ id?: string;
3
+ type: 'function';
4
+ function: {
5
+ name?: string;
6
+ arguments?: string;
7
+ };
8
+ }
9
+ export interface StreamingToolExtractorOptions {
10
+ idPrefix?: string;
11
+ }
12
+ export declare class StreamingTextToolExtractor {
13
+ private opts;
14
+ private buffer;
15
+ private idCounter;
16
+ constructor(opts?: StreamingToolExtractorOptions);
17
+ reset(): void;
18
+ feedText(text: string): StreamingToolCall[];
19
+ private genId;
20
+ private toToolCall;
21
+ private tryExtractStructuredBlocks;
22
+ private tryExtractFunctionExecuteBlocks;
23
+ private splitCommand;
24
+ }
25
+ export declare function createStreamingToolExtractor(opts?: StreamingToolExtractorOptions): StreamingTextToolExtractor;
@@ -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
  }
@@ -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([
@@ -14,7 +15,7 @@ const KNOWN_TOOLS = new Set([
14
15
  ]);
15
16
  const ALLOWED_KEYS = {
16
17
  shell: new Set(['command', 'justification', 'timeout_ms', 'with_escalated_permissions', 'workdir']),
17
- apply_patch: new Set(['patch']),
18
+ apply_patch: new Set(['file', 'instructions', 'changes']),
18
19
  update_plan: new Set(['explanation', 'plan']),
19
20
  view_image: new Set(['path']),
20
21
  list_mcp_resources: new Set(['server', 'cursor', 'filter', 'root']),
@@ -70,40 +71,54 @@ function enabled() {
70
71
  }
71
72
  }
72
73
  // 已移除所有 rcc.tool.v1 相关处理:不再识别或剥离 rcc 封装
73
- export function extractApplyPatchCallsFromText(text) {
74
+ function extractStructuredApplyPatchPayloads(text) {
75
+ const payloads = [];
74
76
  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;
77
+ const fenceRe = /```(?:json|apply_patch|toon)?\s*([\s\S]*?)\s*```/gi;
80
78
  let fm;
81
79
  while ((fm = fenceRe.exec(text)) !== null) {
82
80
  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 });
81
+ try {
82
+ const parsed = JSON.parse(body);
83
+ if (isStructuredApplyPatchPayload(parsed)) {
84
+ payloads.push(parsed);
99
85
  }
100
- catch {
101
- argsStr = '{"patch":""}';
86
+ }
87
+ catch { /* ignore invalid JSON */ }
88
+ }
89
+ if (!payloads.length && typeof text === 'string' && text.includes('"changes"')) {
90
+ try {
91
+ const parsed = JSON.parse(text);
92
+ if (isStructuredApplyPatchPayload(parsed)) {
93
+ payloads.push(parsed);
102
94
  }
103
- out.push({ id: genId(), name: 'apply_patch', args: argsStr });
104
95
  }
96
+ catch { /* ignore */ }
105
97
  }
106
- return out.length ? out : null;
98
+ }
99
+ catch { /* ignore */ }
100
+ return payloads;
101
+ }
102
+ export function extractApplyPatchCallsFromText(text) {
103
+ try {
104
+ if (typeof text !== 'string' || !text)
105
+ return null;
106
+ const payloads = extractStructuredApplyPatchPayloads(text);
107
+ if (!payloads.length)
108
+ return null;
109
+ const out = [];
110
+ const genId = () => `call_${Math.random().toString(36).slice(2, 10)}`;
111
+ for (const payload of payloads) {
112
+ let argsStr = '{}';
113
+ try {
114
+ argsStr = JSON.stringify(payload);
115
+ }
116
+ catch {
117
+ argsStr = '{"changes":[]}';
118
+ }
119
+ out.push({ id: genId(), name: 'apply_patch', args: argsStr });
120
+ }
121
+ return out;
107
122
  }
108
123
  catch {
109
124
  return null;
@@ -212,8 +212,9 @@ export async function runChatResponseToolFilters(chatJson, options = {}) {
212
212
  const { ResponseToolTextCanonicalizeFilter, ResponseToolArgumentsStringifyFilter, ResponseFinishInvariantsFilter } = await import('../../filters/index.js');
213
213
  register(new ResponseToolTextCanonicalizeFilter());
214
214
  try {
215
- const { ResponseToolArgumentsToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
215
+ const { ResponseToolArgumentsToonDecodeFilter, ResponseApplyPatchToonDecodeFilter, ResponseToolArgumentsBlacklistFilter, ResponseToolArgumentsSchemaConvergeFilter } = await import('../../filters/index.js');
216
216
  register(new ResponseToolArgumentsToonDecodeFilter());
217
+ register(new ResponseApplyPatchToonDecodeFilter());
217
218
  try {
218
219
  register(new ResponseToolArgumentsSchemaConvergeFilter());
219
220
  }
@@ -3,6 +3,7 @@
3
3
  // canonicalizer 按需加载(避免在请求侧仅注入时引入不必要的模块)
4
4
  // enforceChatBudget: 为避免在请求侧引入多余依赖,这里提供最小实现(保留形状,不裁剪)。
5
5
  import { augmentOpenAITools } from '../../guidance/index.js';
6
+ import { validateToolCall } from '../../tools/tool-registry.js';
6
7
  function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
7
8
  // Note: tool schema strict augmentation removed per alignment
8
9
  function enforceChatBudget(chat, _modelId) { return chat; }
@@ -173,8 +174,7 @@ export function processChatRequestTools(request, opts) {
173
174
  continue;
174
175
  const typeStr = String(t.type || '').toLowerCase();
175
176
  const nameStr = typeof fn.name === 'string' ? String(fn.name).toLowerCase() : '';
176
- const shouldPatch = typeStr === 'function' ||
177
- nameStr === 'apply_patch';
177
+ const shouldPatch = typeStr === 'function' || nameStr === 'apply_patch';
178
178
  if (!shouldPatch)
179
179
  continue;
180
180
  if (!Object.prototype.hasOwnProperty.call(fn, 'parameters')) {
@@ -208,7 +208,8 @@ export function processChatRequestTools(request, opts) {
208
208
  catch { /* ignore */ }
209
209
  // 4) Enforce payload budget (context bytes) with minimal loss policy
210
210
  const modelId = typeof canonical?.model === 'string' ? String(canonical.model) : 'unknown';
211
- return enforceChatBudget(canonical, modelId);
211
+ const budgeted = enforceChatBudget(canonical, modelId);
212
+ return normalizeApplyPatchToolCallsOnRequest(budgeted);
212
213
  }
213
214
  catch {
214
215
  return out;
@@ -219,6 +220,75 @@ export function processChatRequestTools(request, opts) {
219
220
  * - Canonicalize textual tool markup to tool_calls; ensure finish_reason and content=null policy
220
221
  */
221
222
  import { repairArgumentsToString, parseLenient } from './jsonish.js';
223
+ function normalizeApplyPatchToolCallsOnResponse(chat) {
224
+ try {
225
+ const out = JSON.parse(JSON.stringify(chat));
226
+ const choices = Array.isArray(out?.choices) ? out.choices : [];
227
+ for (const ch of choices) {
228
+ const msg = ch && ch.message ? ch.message : undefined;
229
+ const tcs = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
230
+ if (!tcs.length)
231
+ continue;
232
+ for (const tc of tcs) {
233
+ try {
234
+ const fn = tc && tc.function ? tc.function : undefined;
235
+ const name = typeof fn?.name === 'string' ? String(fn.name).trim().toLowerCase() : '';
236
+ if (name !== 'apply_patch')
237
+ continue;
238
+ const rawArgs = fn?.arguments;
239
+ const argsStr = repairArgumentsToString(rawArgs);
240
+ const validation = validateToolCall('apply_patch', argsStr);
241
+ if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
242
+ fn.arguments = validation.normalizedArgs;
243
+ }
244
+ }
245
+ catch {
246
+ // best-effort per tool_call
247
+ }
248
+ }
249
+ }
250
+ return out;
251
+ }
252
+ catch {
253
+ return chat;
254
+ }
255
+ }
256
+ function normalizeApplyPatchToolCallsOnRequest(request) {
257
+ try {
258
+ const out = JSON.parse(JSON.stringify(request));
259
+ const messages = Array.isArray(out?.messages) ? out.messages : [];
260
+ for (const msg of messages) {
261
+ if (!msg || typeof msg !== 'object')
262
+ continue;
263
+ if (msg.role !== 'assistant')
264
+ continue;
265
+ const tcs = Array.isArray(msg.tool_calls) ? msg.tool_calls : [];
266
+ if (!tcs.length)
267
+ continue;
268
+ for (const tc of tcs) {
269
+ try {
270
+ const fn = tc && tc.function ? tc.function : undefined;
271
+ const name = typeof fn?.name === 'string' ? String(fn.name).trim().toLowerCase() : '';
272
+ if (name !== 'apply_patch')
273
+ continue;
274
+ const rawArgs = fn?.arguments;
275
+ const argsStr = repairArgumentsToString(rawArgs);
276
+ const validation = validateToolCall('apply_patch', argsStr);
277
+ if (validation && validation.ok && typeof validation.normalizedArgs === 'string') {
278
+ fn.arguments = validation.normalizedArgs;
279
+ }
280
+ }
281
+ catch {
282
+ // best-effort per tool_call
283
+ }
284
+ }
285
+ }
286
+ return out;
287
+ }
288
+ catch {
289
+ return request;
290
+ }
291
+ }
222
292
  function enhanceResponseToolArguments(chat) {
223
293
  try {
224
294
  const enable = String(process?.env?.RCC_TOOL_ENHANCE ?? '1').trim() !== '0';
@@ -336,7 +406,8 @@ export function processChatResponseTools(resp) {
336
406
  try {
337
407
  const { canonicalizeChatResponseTools } = require('./tool-canonicalizer.js');
338
408
  const canon = canonicalizeChatResponseTools(resp);
339
- return enhanceResponseToolArguments(canon);
409
+ const withPatch = normalizeApplyPatchToolCallsOnResponse(canon);
410
+ return enhanceResponseToolArguments(withPatch);
340
411
  }
341
412
  catch {
342
413
  return resp;
@@ -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,24 +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
- properties.input = {
33
- type: 'string',
34
- description: 'Unified diff patch body between *** Begin Patch/*** End Patch.'
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
+ }
35
59
  };
36
- if (!properties.patch || typeof properties.patch !== 'object') {
37
- properties.patch = {
38
- type: 'string',
39
- description: 'Alias of input for backwards compatibility.'
40
- };
41
- }
42
60
  schema.properties = properties;
43
- const requiredList = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === 'string') : [];
44
- if (!requiredList.includes('input')) {
45
- requiredList.push('input');
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');
46
66
  }
47
67
  schema.required = requiredList;
48
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';
@@ -27,12 +27,14 @@ export class RequestToolCallsStringifyFilter {
27
27
  const fnName = typeof fn.name === 'string' ? fn.name.trim() : '';
28
28
  // Case 1: non-string arguments → stringify directly
29
29
  if (currentArgs !== undefined && typeof currentArgs !== 'string') {
30
+ let argsJson = '{}';
30
31
  try {
31
- fn.arguments = JSON.stringify(currentArgs ?? {});
32
+ argsJson = JSON.stringify(currentArgs ?? {});
32
33
  }
33
34
  catch {
34
- fn.arguments = '{}';
35
+ argsJson = '{}';
35
36
  }
37
+ fn.arguments = argsJson;
36
38
  tc.function = fn;
37
39
  continue;
38
40
  }
@@ -61,9 +63,6 @@ export class RequestToolCallsStringifyFilter {
61
63
  if (fnName === 'shell') {
62
64
  fn.arguments = JSON.stringify({ command: currentArgs });
63
65
  }
64
- else if (fnName === 'apply_patch') {
65
- fn.arguments = JSON.stringify({ patch: currentArgs });
66
- }
67
66
  else {
68
67
  fn.arguments = JSON.stringify({ input: currentArgs });
69
68
  }
@@ -74,56 +73,7 @@ export class RequestToolCallsStringifyFilter {
74
73
  tc.function = fn;
75
74
  continue;
76
75
  }
77
- // 已经是合法 JSON 的场景下,仅对特定工具做形状修复。
78
- if (parsedOk && fnName === 'apply_patch') {
79
- try {
80
- let obj = parsedValue;
81
- // 1) 若整体是字符串,则视为补丁文本
82
- if (typeof obj === 'string') {
83
- obj = { patch: obj };
84
- }
85
- if (obj && typeof obj === 'object') {
86
- const container = obj;
87
- const rawPatch = container.patch;
88
- const rawInput = container.input;
89
- // 2) 若 patch 是形如 JSON 的字符串,尝试解包 {"input": "..."} 或 {"patch": "..."}
90
- if (typeof rawPatch === 'string') {
91
- const ptrim = rawPatch.trim();
92
- if (ptrim.startsWith('{') && ptrim.endsWith('}')) {
93
- try {
94
- const inner = JSON.parse(ptrim);
95
- if (typeof inner.patch === 'string') {
96
- container.patch = inner.patch;
97
- }
98
- else if (typeof inner.input === 'string') {
99
- container.patch = inner.input;
100
- }
101
- else {
102
- container.patch = ptrim;
103
- }
104
- }
105
- catch {
106
- container.patch = rawPatch;
107
- }
108
- }
109
- }
110
- else if (rawPatch === undefined && typeof rawInput === 'string') {
111
- // 3) 若只有 input 字段,则复制一份到 patch,避免双层包装
112
- container.patch = rawInput;
113
- }
114
- fn.arguments = JSON.stringify(container);
115
- tc.function = fn;
116
- continue;
117
- }
118
- }
119
- catch {
120
- // 回退到原始字符串形状
121
- fn.arguments = trimmed;
122
- tc.function = fn;
123
- continue;
124
- }
125
- }
126
- // 其它合法 JSON 场景保持原样
76
+ // 合法 JSON 场景保持原样
127
77
  fn.arguments = trimmed;
128
78
  tc.function = fn;
129
79
  }