@jsonstudio/llms 0.6.633 → 0.6.743

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 (61) hide show
  1. package/dist/conversion/codecs/anthropic-openai-codec.js +0 -5
  2. package/dist/conversion/codecs/openai-openai-codec.js +0 -6
  3. package/dist/conversion/codecs/responses-openai-codec.js +1 -7
  4. package/dist/conversion/hub/node-support.js +5 -4
  5. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +14 -1
  6. package/dist/conversion/hub/pipeline/hub-pipeline.js +82 -18
  7. package/dist/conversion/hub/pipeline/session-identifiers.js +132 -2
  8. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +23 -19
  9. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +47 -0
  10. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +4 -2
  11. package/dist/conversion/hub/process/chat-process.js +2 -0
  12. package/dist/conversion/hub/response/provider-response.js +6 -1
  13. package/dist/conversion/hub/snapshot-recorder.js +8 -1
  14. package/dist/conversion/pipeline/codecs/v2/shared/openai-chat-helpers.js +0 -7
  15. package/dist/conversion/responses/responses-openai-bridge.js +47 -7
  16. package/dist/conversion/shared/compaction-detect.d.ts +2 -0
  17. package/dist/conversion/shared/compaction-detect.js +53 -0
  18. package/dist/conversion/shared/errors.d.ts +1 -1
  19. package/dist/conversion/shared/reasoning-tool-normalizer.js +7 -0
  20. package/dist/conversion/shared/snapshot-hooks.d.ts +2 -0
  21. package/dist/conversion/shared/snapshot-hooks.js +180 -4
  22. package/dist/conversion/shared/snapshot-utils.d.ts +4 -0
  23. package/dist/conversion/shared/snapshot-utils.js +4 -0
  24. package/dist/conversion/shared/tool-filter-pipeline.js +3 -9
  25. package/dist/conversion/shared/tool-governor.d.ts +2 -0
  26. package/dist/conversion/shared/tool-governor.js +101 -13
  27. package/dist/conversion/shared/tool-harvester.js +42 -2
  28. package/dist/filters/index.d.ts +0 -2
  29. package/dist/filters/index.js +0 -2
  30. package/dist/filters/special/request-tools-normalize.d.ts +11 -0
  31. package/dist/filters/special/request-tools-normalize.js +13 -50
  32. package/dist/filters/special/response-apply-patch-toon-decode.js +403 -82
  33. package/dist/filters/special/response-tool-arguments-toon-decode.js +6 -75
  34. package/dist/filters/utils/snapshot-writer.js +42 -4
  35. package/dist/guidance/index.js +8 -2
  36. package/dist/router/virtual-router/engine-health.js +0 -4
  37. package/dist/router/virtual-router/engine-selection.d.ts +2 -1
  38. package/dist/router/virtual-router/engine-selection.js +101 -9
  39. package/dist/router/virtual-router/engine.d.ts +5 -1
  40. package/dist/router/virtual-router/engine.js +188 -5
  41. package/dist/router/virtual-router/routing-instructions.d.ts +6 -0
  42. package/dist/router/virtual-router/routing-instructions.js +18 -3
  43. package/dist/router/virtual-router/sticky-session-store.d.ts +1 -0
  44. package/dist/router/virtual-router/sticky-session-store.js +36 -0
  45. package/dist/router/virtual-router/types.d.ts +22 -0
  46. package/dist/servertool/engine.js +335 -9
  47. package/dist/servertool/handlers/compaction-detect.d.ts +1 -0
  48. package/dist/servertool/handlers/compaction-detect.js +1 -0
  49. package/dist/servertool/handlers/gemini-empty-reply-continue.js +29 -5
  50. package/dist/servertool/handlers/iflow-model-error-retry.js +17 -0
  51. package/dist/servertool/handlers/stop-message-auto.js +199 -19
  52. package/dist/servertool/server-side-tools.d.ts +0 -1
  53. package/dist/servertool/server-side-tools.js +0 -1
  54. package/dist/servertool/types.d.ts +1 -0
  55. package/dist/tools/apply-patch-structured.js +52 -15
  56. package/dist/tools/tool-registry.js +537 -15
  57. package/dist/utils/toon.d.ts +4 -0
  58. package/dist/utils/toon.js +75 -0
  59. package/package.json +4 -2
  60. package/dist/test-output/virtual-router/results.json +0 -1
  61. package/dist/test-output/virtual-router/summary.json +0 -12
@@ -1,6 +1,7 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { cloneJson } from '../server-side-tools.js';
3
3
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
4
+ import { isCompactionRequest } from './compaction-detect.js';
4
5
  const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
5
6
  function debugLog(message, extra) {
6
7
  if (!STOPMESSAGE_DEBUG) {
@@ -23,9 +24,14 @@ const handler = async (ctx) => {
23
24
  requestId: record.requestId,
24
25
  providerProtocol: record.providerProtocol
25
26
  });
26
- const followupRaw = record.serverToolFollowup;
27
- if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
28
- debugLog('skip_servertool_followup_flag');
27
+ const followupFlagRaw = record.serverToolFollowup;
28
+ if (followupFlagRaw === true ||
29
+ (typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
30
+ debugLog('skip_followup_loop');
31
+ return null;
32
+ }
33
+ if (hasCompactionFlag(record)) {
34
+ debugLog('skip_compaction_flag');
29
35
  return null;
30
36
  }
31
37
  const connectionState = resolveClientConnectionState(record.clientConnectionState);
@@ -44,10 +50,21 @@ const handler = async (ctx) => {
44
50
  debugLog('skip_no_sticky_key');
45
51
  return null;
46
52
  }
47
- const state = loadRoutingInstructionStateSync(stickyKey);
53
+ let state = loadRoutingInstructionStateSync(stickyKey);
48
54
  if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
49
- debugLog('skip_no_state', { stickyKey });
50
- return null;
55
+ const fallback = resolveStopMessageSnapshot(record.stopMessageState);
56
+ if (fallback) {
57
+ state = createStopMessageState(fallback);
58
+ }
59
+ else {
60
+ const implicit = resolveImplicitGeminiStopMessageSnapshot(ctx, record);
61
+ if (!implicit) {
62
+ debugLog('skip_no_state', { stickyKey });
63
+ return null;
64
+ }
65
+ state = createStopMessageState(implicit);
66
+ }
67
+ saveRoutingInstructionStateAsync(stickyKey, state);
51
68
  }
52
69
  const text = typeof state.stopMessageText === 'string' ? state.stopMessageText.trim() : '';
53
70
  const maxRepeats = typeof state.stopMessageMaxRepeats === 'number' && Number.isFinite(state.stopMessageMaxRepeats)
@@ -70,6 +87,12 @@ const handler = async (ctx) => {
70
87
  used,
71
88
  maxRepeats
72
89
  });
90
+ state.stopMessageText = undefined;
91
+ state.stopMessageMaxRepeats = undefined;
92
+ state.stopMessageUsed = undefined;
93
+ state.stopMessageUpdatedAt = undefined;
94
+ state.stopMessageLastUsedAt = undefined;
95
+ saveRoutingInstructionStateAsync(stickyKey, state);
73
96
  return null;
74
97
  }
75
98
  if (!isStopFinishReason(ctx.base)) {
@@ -85,16 +108,33 @@ const handler = async (ctx) => {
85
108
  });
86
109
  return null;
87
110
  }
88
- state.stopMessageUsed = used + 1;
89
- state.stopMessageLastUsedAt = Date.now();
90
- saveRoutingInstructionStateAsync(stickyKey, state);
91
- const followupPayload = buildStopMessageFollowupPayload(captured, text);
111
+ if (isCompactionRequest(captured)) {
112
+ debugLog('skip_compaction_request', { stickyKey });
113
+ return null;
114
+ }
115
+ const entryEndpoint = resolveEntryEndpoint(record);
116
+ const followupPayload = buildStopMessageFollowupPayload(captured, text, ctx.base);
92
117
  if (!followupPayload) {
93
118
  debugLog('skip_failed_build_followup', {
94
119
  stickyKey
95
120
  });
96
121
  return null;
97
122
  }
123
+ const nextUsed = used + 1;
124
+ state.stopMessageUsed = nextUsed;
125
+ state.stopMessageLastUsedAt = Date.now();
126
+ saveRoutingInstructionStateAsync(stickyKey, state);
127
+ const followupMetadata = {
128
+ serverToolFollowup: true,
129
+ stream: false,
130
+ preserveRouteHint: false,
131
+ disableStickyRoutes: true,
132
+ serverToolOriginalEntryEndpoint: entryEndpoint,
133
+ ...(connectionState ? { clientConnectionState: connectionState } : {})
134
+ };
135
+ if (shouldForceChatProtocol(entryEndpoint)) {
136
+ followupMetadata.serverToolFollowupProtocol = 'openai-chat';
137
+ }
98
138
  return {
99
139
  chatResponse: ctx.base,
100
140
  execution: {
@@ -102,13 +142,8 @@ const handler = async (ctx) => {
102
142
  followup: {
103
143
  requestIdSuffix: ':stop_followup',
104
144
  payload: followupPayload,
105
- metadata: {
106
- serverToolFollowup: true,
107
- stream: false,
108
- preserveRouteHint: false,
109
- disableStickyRoutes: true,
110
- ...(connectionState ? { clientConnectionState: connectionState } : {})
111
- }
145
+ entryEndpoint,
146
+ metadata: followupMetadata
112
147
  }
113
148
  }
114
149
  };
@@ -144,7 +179,14 @@ function isStopFinishReason(base) {
144
179
  const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
145
180
  ? finishReasonRaw.trim().toLowerCase()
146
181
  : '';
147
- if (finishReason !== 'stop') {
182
+ // 将模型视为“自然结束”的场景:
183
+ // - OpenAI 兼容:finish_reason === 'stop'
184
+ // - 截断场景:finish_reason === 'length'(例如 Gemini/Claude 流式输出被 max token 截断)
185
+ // 统一视作可触发 stopMessage 的终止点;仅排除显式的 tool_calls。
186
+ if (!finishReason || finishReason === 'tool_calls') {
187
+ return false;
188
+ }
189
+ if (finishReason !== 'stop' && finishReason !== 'length') {
148
190
  return false;
149
191
  }
150
192
  const message = first.message &&
@@ -157,6 +199,7 @@ function isStopFinishReason(base) {
157
199
  }
158
200
  const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
159
201
  if (toolCalls.length > 0) {
202
+ // 如果当前响应仍在发起工具调用,则由工具执行驱动后续轮次,不触发 stopMessage。
160
203
  return false;
161
204
  }
162
205
  return true;
@@ -171,7 +214,7 @@ function getCapturedRequest(adapterContext) {
171
214
  }
172
215
  return captured;
173
216
  }
174
- function buildStopMessageFollowupPayload(source, text) {
217
+ function buildStopMessageFollowupPayload(source, text, baseResponse) {
175
218
  if (!source || typeof source !== 'object') {
176
219
  return null;
177
220
  }
@@ -181,6 +224,25 @@ function buildStopMessageFollowupPayload(source, text) {
181
224
  }
182
225
  const rawMessages = source.messages;
183
226
  const messages = Array.isArray(rawMessages) ? cloneJson(rawMessages) : [];
227
+ try {
228
+ if (baseResponse && typeof baseResponse === 'object' && !Array.isArray(baseResponse)) {
229
+ const base = baseResponse;
230
+ const choices = Array.isArray(base.choices) ? base.choices : [];
231
+ const primary = choices[0] && typeof choices[0] === 'object' ? choices[0] : null;
232
+ const msg = primary &&
233
+ primary.message &&
234
+ typeof primary.message === 'object' &&
235
+ !Array.isArray(primary.message)
236
+ ? primary.message
237
+ : null;
238
+ if (msg) {
239
+ messages.push(cloneJson(msg));
240
+ }
241
+ }
242
+ }
243
+ catch {
244
+ // best-effort: 如果无法解析上一条响应,就只使用已捕获的请求消息
245
+ }
184
246
  messages.push({
185
247
  role: 'user',
186
248
  content: text
@@ -202,3 +264,121 @@ function resolveClientConnectionState(value) {
202
264
  }
203
265
  return value;
204
266
  }
267
+ function resolveStopMessageSnapshot(raw) {
268
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
269
+ return null;
270
+ }
271
+ const record = raw;
272
+ const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
273
+ const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
274
+ ? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
275
+ : 0;
276
+ if (!text || maxRepeats <= 0) {
277
+ return null;
278
+ }
279
+ const used = typeof record.stopMessageUsed === 'number' && Number.isFinite(record.stopMessageUsed)
280
+ ? Math.max(0, Math.floor(record.stopMessageUsed))
281
+ : 0;
282
+ const updatedAt = typeof record.stopMessageUpdatedAt === 'number' && Number.isFinite(record.stopMessageUpdatedAt)
283
+ ? record.stopMessageUpdatedAt
284
+ : undefined;
285
+ const lastUsedAt = typeof record.stopMessageLastUsedAt === 'number' && Number.isFinite(record.stopMessageLastUsedAt)
286
+ ? record.stopMessageLastUsedAt
287
+ : undefined;
288
+ const source = typeof record.stopMessageSource === 'string' && record.stopMessageSource.trim()
289
+ ? record.stopMessageSource.trim()
290
+ : undefined;
291
+ return {
292
+ text,
293
+ maxRepeats,
294
+ used,
295
+ ...(source ? { source } : {}),
296
+ ...(updatedAt ? { updatedAt } : {}),
297
+ ...(lastUsedAt ? { lastUsedAt } : {})
298
+ };
299
+ }
300
+ function hasCompactionFlag(record) {
301
+ const flag = record.compactionRequest;
302
+ if (flag === true) {
303
+ return true;
304
+ }
305
+ if (typeof flag === 'string' && flag.trim().toLowerCase() === 'true') {
306
+ return true;
307
+ }
308
+ return false;
309
+ }
310
+ function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
311
+ try {
312
+ const protoFromCtx = ctx.options?.providerProtocol;
313
+ const protoFromRecord = typeof record.providerProtocol === 'string' && record.providerProtocol.trim()
314
+ ? String(record.providerProtocol).trim()
315
+ : undefined;
316
+ const providerProtocol = (protoFromCtx || protoFromRecord || '').toString().toLowerCase();
317
+ if (providerProtocol !== 'gemini-chat') {
318
+ return null;
319
+ }
320
+ const entryFromRecord = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
321
+ ? String(record.entryEndpoint).trim()
322
+ : undefined;
323
+ const metaEntry = record.metadata &&
324
+ typeof record.metadata === 'object' &&
325
+ record.metadata.entryEndpoint;
326
+ const entryFromMeta = typeof metaEntry === 'string' && metaEntry.trim() ? metaEntry.trim() : undefined;
327
+ const entryEndpoint = (entryFromRecord || entryFromMeta || '').toLowerCase();
328
+ if (!entryEndpoint.includes('/v1/responses')) {
329
+ return null;
330
+ }
331
+ // 仅在本轮响应被视为“自然结束”(stop/length,且无 tool_calls)时触发,避免干扰正常对话。
332
+ if (!isStopFinishReason(ctx.base)) {
333
+ return null;
334
+ }
335
+ return {
336
+ text: '继续执行',
337
+ maxRepeats: 1,
338
+ used: 0,
339
+ source: 'auto'
340
+ };
341
+ }
342
+ catch {
343
+ return null;
344
+ }
345
+ }
346
+ function createStopMessageState(snapshot) {
347
+ return {
348
+ forcedTarget: undefined,
349
+ stickyTarget: undefined,
350
+ allowedProviders: new Set(),
351
+ disabledProviders: new Set(),
352
+ disabledKeys: new Map(),
353
+ disabledModels: new Map(),
354
+ stopMessageSource: snapshot.source && snapshot.source.trim() ? snapshot.source.trim() : 'explicit',
355
+ stopMessageText: snapshot.text,
356
+ stopMessageMaxRepeats: snapshot.maxRepeats,
357
+ stopMessageUsed: snapshot.used,
358
+ stopMessageUpdatedAt: snapshot.updatedAt,
359
+ stopMessageLastUsedAt: snapshot.lastUsedAt
360
+ };
361
+ }
362
+ function resolveEntryEndpoint(record) {
363
+ const raw = typeof record.entryEndpoint === 'string' && record.entryEndpoint.trim()
364
+ ? record.entryEndpoint.trim()
365
+ : undefined;
366
+ if (raw) {
367
+ return raw;
368
+ }
369
+ const metaEntry = record.metadata && typeof record.metadata === 'object' && record.metadata.entryEndpoint;
370
+ if (typeof metaEntry === 'string' && metaEntry.trim()) {
371
+ return metaEntry.trim();
372
+ }
373
+ return '/v1/chat/completions';
374
+ }
375
+ function shouldForceChatProtocol(entryEndpoint) {
376
+ if (typeof entryEndpoint !== 'string') {
377
+ return false;
378
+ }
379
+ const normalized = entryEndpoint.trim().toLowerCase();
380
+ if (!normalized) {
381
+ return false;
382
+ }
383
+ return !normalized.includes('/v1/chat/completions');
384
+ }
@@ -2,7 +2,6 @@ import type { JsonObject } from '../conversion/hub/types/json.js';
2
2
  import type { ServerSideToolEngineOptions, ServerSideToolEngineResult, ToolCall } from './types.js';
3
3
  import './handlers/web-search.js';
4
4
  import './handlers/vision.js';
5
- import './handlers/gemini-empty-reply-continue.js';
6
5
  import './handlers/iflow-model-error-retry.js';
7
6
  import './handlers/stop-message-auto.js';
8
7
  export declare function runServerSideToolEngine(options: ServerSideToolEngineOptions): Promise<ServerSideToolEngineResult>;
@@ -1,7 +1,6 @@
1
1
  import { getServerToolHandler, listAutoServerToolHandlers } from './registry.js';
2
2
  import './handlers/web-search.js';
3
3
  import './handlers/vision.js';
4
- import './handlers/gemini-empty-reply-continue.js';
5
4
  import './handlers/iflow-model-error-retry.js';
6
5
  import './handlers/stop-message-auto.js';
7
6
  export async function runServerSideToolEngine(options) {
@@ -53,6 +53,7 @@ export interface ServerToolFollowupPlan {
53
53
  requestIdSuffix: string;
54
54
  payload: JsonObject;
55
55
  metadata?: JsonObject;
56
+ entryEndpoint?: string;
56
57
  }
57
58
  export interface ServerToolExecution {
58
59
  flowId: string;
@@ -14,7 +14,27 @@ const SUPPORTED_KINDS = [
14
14
  'delete_file'
15
15
  ];
16
16
  const FILE_PATH_INVALID_RE = /[\r\n]/;
17
- const INVALID_FILE_FORMAT_RE = /^([A-Za-z]:|\/)/;
17
+ const decodeEscapedNewlinesIfObvious = (value) => {
18
+ if (!value)
19
+ return value;
20
+ if (value.includes('\n'))
21
+ return value;
22
+ const lower = value.toLowerCase();
23
+ const looksEscaped = value.includes('\\r\\n') ||
24
+ (value.includes('\\n') && /\\n[ \t]/.test(value)) ||
25
+ lower.includes('\\u000a') ||
26
+ lower.includes('\\u000d');
27
+ if (!looksEscaped) {
28
+ return value;
29
+ }
30
+ let out = value;
31
+ out = out.replace(/\\r\\n/g, '\n');
32
+ out = out.replace(/\\n/g, '\n');
33
+ out = out.replace(/\\r/g, '\n');
34
+ out = out.replace(/\\u000a/gi, '\n');
35
+ out = out.replace(/\\u000d/gi, '\n');
36
+ return out;
37
+ };
18
38
  const toSafeString = (value, label) => {
19
39
  const str = typeof value === 'string' ? value : '';
20
40
  if (!str.trim()) {
@@ -23,20 +43,22 @@ const toSafeString = (value, label) => {
23
43
  return str;
24
44
  };
25
45
  const normalizeFilePath = (raw, label) => {
26
- const trimmed = raw.trim();
46
+ let trimmed = raw.trim();
27
47
  if (!trimmed) {
28
48
  throw new StructuredApplyPatchError('invalid_file', `${label} must not be empty`);
29
49
  }
30
50
  if (FILE_PATH_INVALID_RE.test(trimmed)) {
31
- throw new StructuredApplyPatchError('invalid_file', `${label} must be a single-line relative path`);
32
- }
33
- if (INVALID_FILE_FORMAT_RE.test(trimmed)) {
34
- throw new StructuredApplyPatchError('invalid_file', `${label} must be relative to the workspace root`);
51
+ const firstLine = trimmed.split(/[\r\n]/)[0]?.trim() ?? '';
52
+ if (!firstLine) {
53
+ throw new StructuredApplyPatchError('invalid_file', `${label} must be a single-line path`);
54
+ }
55
+ trimmed = firstLine;
35
56
  }
36
57
  return trimmed.replace(/\\/g, '/');
37
58
  };
38
59
  const splitTextIntoLines = (input) => {
39
- const normalized = input.replace(/\r/g, '');
60
+ const decoded = decodeEscapedNewlinesIfObvious(input);
61
+ const normalized = decoded.replace(/\r/g, '');
40
62
  const parts = normalized.split('\n');
41
63
  if (parts.length && parts[parts.length - 1] === '') {
42
64
  parts.pop();
@@ -48,16 +70,27 @@ const normalizeLines = (value, label) => {
48
70
  if (!value.length) {
49
71
  return [];
50
72
  }
51
- return value.map((entry, idx) => {
73
+ const out = [];
74
+ for (const [idx, entry] of value.entries()) {
52
75
  if (typeof entry !== 'string') {
53
76
  if (entry === null || entry === undefined) {
54
- return '';
77
+ out.push('');
78
+ continue;
55
79
  }
56
- return String(entry);
80
+ out.push(String(entry));
81
+ continue;
57
82
  }
58
83
  // Preserve intentional whitespace
59
- return entry.replace(/\r/g, '');
60
- });
84
+ const normalized = entry.replace(/\r/g, '');
85
+ const decoded = decodeEscapedNewlinesIfObvious(normalized);
86
+ if (decoded.includes('\n')) {
87
+ out.push(...splitTextIntoLines(decoded));
88
+ }
89
+ else {
90
+ out.push(decoded);
91
+ }
92
+ }
93
+ return out;
61
94
  }
62
95
  if (typeof value === 'string') {
63
96
  return splitTextIntoLines(value);
@@ -176,7 +209,9 @@ export function buildStructuredPatch(payload) {
176
209
  break;
177
210
  }
178
211
  case 'replace': {
179
- const target = toSafeString(change.target, `changes[${index}].target`);
212
+ // 兼容仅提供 anchor 的 replace 形态:将 anchor 视为 target 以尽可能保留用户意图。
213
+ const targetSource = change.target ?? change.anchor;
214
+ const target = toSafeString(targetSource, `changes[${index}].target`);
180
215
  const replacements = normalizeLines(change.lines, `changes[${index}].lines`);
181
216
  const hunkBody = [
182
217
  ...buildPrefixedLines(splitTextIntoLines(target), '-'),
@@ -215,8 +250,10 @@ export function buildStructuredPatch(payload) {
215
250
  }
216
251
  else {
217
252
  lines.push(`*** Update File: ${file}`);
218
- for (const hunk of section.hunks) {
219
- // 每个 hunk 仅需一个开头的 "@@" 行,后面直接跟上下文/增删行。
253
+ const hunks = section.hunks || [];
254
+ for (const hunk of hunks) {
255
+ // 结构化补丁仅负责生成统一 diff 形态,不对多段 hunk 做逻辑裁剪;
256
+ // 具体哪些 hunk 能成功应用由 apply_patch 客户端自行校验并返回错误信息。
220
257
  for (const entry of hunk) {
221
258
  if (!entry.startsWith('@@')) {
222
259
  lines.push(entry);