@jsonstudio/llms 0.6.749 → 0.6.795

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 (41) hide show
  1. package/dist/conversion/compat/actions/apply-patch-fixer.d.ts +1 -0
  2. package/dist/conversion/compat/actions/apply-patch-fixer.js +30 -0
  3. package/dist/conversion/compat/actions/apply-patch-format-fixer.d.ts +1 -0
  4. package/dist/conversion/compat/actions/apply-patch-format-fixer.js +233 -0
  5. package/dist/conversion/compat/actions/index.d.ts +2 -0
  6. package/dist/conversion/compat/actions/index.js +2 -0
  7. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +6 -0
  8. package/dist/conversion/hub/pipeline/hub-pipeline.js +35 -0
  9. package/dist/conversion/shared/bridge-message-utils.d.ts +1 -0
  10. package/dist/conversion/shared/bridge-message-utils.js +7 -0
  11. package/dist/conversion/shared/bridge-policies.js +8 -8
  12. package/dist/conversion/shared/errors.d.ts +1 -1
  13. package/dist/conversion/shared/errors.js +3 -0
  14. package/dist/conversion/shared/tool-governor.js +18 -23
  15. package/dist/filters/special/response-tool-arguments-stringify.js +3 -22
  16. package/dist/router/virtual-router/engine.d.ts +5 -0
  17. package/dist/router/virtual-router/engine.js +21 -0
  18. package/dist/servertool/engine.js +74 -3
  19. package/dist/servertool/server-side-tools.js +19 -8
  20. package/dist/tools/apply-patch/regression-capturer.d.ts +12 -0
  21. package/dist/tools/apply-patch/regression-capturer.js +112 -0
  22. package/dist/tools/apply-patch/structured.d.ts +20 -0
  23. package/dist/tools/apply-patch/structured.js +441 -0
  24. package/dist/tools/apply-patch/validator.d.ts +8 -0
  25. package/dist/tools/apply-patch/validator.js +466 -0
  26. package/dist/tools/apply-patch-structured.d.ts +1 -20
  27. package/dist/tools/apply-patch-structured.js +1 -277
  28. package/dist/tools/args-json.d.ts +1 -0
  29. package/dist/tools/args-json.js +175 -0
  30. package/dist/tools/exec-command/normalize.d.ts +17 -0
  31. package/dist/tools/exec-command/normalize.js +112 -0
  32. package/dist/tools/exec-command/regression-capturer.d.ts +11 -0
  33. package/dist/tools/exec-command/regression-capturer.js +144 -0
  34. package/dist/tools/exec-command/validator.d.ts +6 -0
  35. package/dist/tools/exec-command/validator.js +22 -0
  36. package/dist/tools/patch-args-normalizer.d.ts +15 -0
  37. package/dist/tools/patch-args-normalizer.js +472 -0
  38. package/dist/tools/patch-regression-capturer.d.ts +1 -0
  39. package/dist/tools/patch-regression-capturer.js +1 -0
  40. package/dist/tools/tool-registry.js +36 -541
  41. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ import { registerBridgeAction } from '../../shared/bridge-actions.js';
2
+ import { ensureMessagesArray } from '../../shared/bridge-message-utils.js';
3
+ import { validateToolCall } from '../../../tools/tool-registry.js';
4
+ const fixApplyPatchAction = (ctx) => {
5
+ const messages = ensureMessagesArray(ctx.state);
6
+ for (const message of messages) {
7
+ if (message.role !== 'assistant')
8
+ continue;
9
+ if (!Array.isArray(message.tool_calls))
10
+ continue;
11
+ for (const toolCall of message.tool_calls) {
12
+ if (toolCall.type !== 'function')
13
+ continue;
14
+ const fn = toolCall.function;
15
+ if (!fn || fn.name !== 'apply_patch')
16
+ continue;
17
+ const rawArgs = fn.arguments;
18
+ if (typeof rawArgs !== 'string')
19
+ continue;
20
+ const validation = validateToolCall('apply_patch', rawArgs);
21
+ if (validation?.ok && typeof validation.normalizedArgs === 'string') {
22
+ if (validation.normalizedArgs !== rawArgs) {
23
+ fn.arguments = validation.normalizedArgs;
24
+ toolCall._fixed_apply_patch = true;
25
+ }
26
+ }
27
+ }
28
+ }
29
+ };
30
+ registerBridgeAction('compat.fix-apply-patch', fixApplyPatchAction);
@@ -0,0 +1,233 @@
1
+ import { registerBridgeAction } from '../../shared/bridge-actions.js';
2
+ import { ensureMessagesArray } from '../../shared/bridge-message-utils.js';
3
+ /**
4
+ * apply-patch-format-fixer.ts
5
+ *
6
+ * 专门处理 apply_patch 的格式错误修复,包括:
7
+ * 1. 修复缺失 '*** Begin Patch' 包装的 raw diff
8
+ * 2. 修复混用 git diff 头部的情况
9
+ * 3. 修复 hunk 中缺少前缀的代码行
10
+ *
11
+ * 注意:仅修复格式问题,不修复上下文匹配问题
12
+ */
13
+ import { captureApplyPatchRegression } from '../../../tools/patch-regression-capturer.js';
14
+ /**
15
+ * 检测是否是裸 diff 格式(没有 Begin Patch 包装)
16
+ */
17
+ function isBareDiff(text) {
18
+ const trimmed = text.trim();
19
+ // 检测典型的 git diff 标记
20
+ const hasGitDiffMarkers = trimmed.startsWith('diff --git') ||
21
+ /^---\s+a\//.test(trimmed) ||
22
+ /^\+\+\+\s+b\//.test(trimmed) ||
23
+ /^@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@/.test(trimmed);
24
+ // 确保不是标准的 apply_patch 格式
25
+ const hasApplyPatchMarkers = trimmed.includes('*** Begin Patch') ||
26
+ trimmed.includes('*** Update File:') ||
27
+ trimmed.includes('*** Add File:');
28
+ return hasGitDiffMarkers && !hasApplyPatchMarkers;
29
+ }
30
+ /**
31
+ * 转换裸 diff 为 apply_patch 格式
32
+ */
33
+ function convertBareDiffToApplyPatch(diffText) {
34
+ const lines = diffText.replace(/\r\n/g, '\n').split('\n');
35
+ const patches = new Map();
36
+ let currentFile = '';
37
+ let currentHunk = [];
38
+ let inHunk = false;
39
+ for (const line of lines) {
40
+ // 检测文件头
41
+ const diffMatch = line.match(/^diff --git\s+a\/(.+?)\s+b\/(.+)$/);
42
+ if (diffMatch) {
43
+ // 保存上一个文件的 hunk
44
+ if (currentFile && currentHunk.length > 0) {
45
+ patches.set(currentFile, {
46
+ header: `*** Update File: ${diffMatch[2]}`,
47
+ hunk: [...currentHunk]
48
+ });
49
+ }
50
+ currentFile = diffMatch[2];
51
+ currentHunk = [];
52
+ inHunk = false;
53
+ continue;
54
+ }
55
+ // 检测 hunk 头
56
+ if (/^@@\s+-\d+/.test(line)) {
57
+ currentHunk.push(line);
58
+ inHunk = true;
59
+ continue;
60
+ }
61
+ // 在 hunk 中收集行
62
+ if (inHunk) {
63
+ // 确保每行都有正确的前缀
64
+ if (line.startsWith('+') || line.startsWith('-') || line.startsWith(' ')) {
65
+ currentHunk.push(line);
66
+ }
67
+ else if (line.trim() === '') {
68
+ currentHunk.push(' ');
69
+ }
70
+ else {
71
+ // 行缺少前缀,作为上下文行处理
72
+ currentHunk.push(' ' + line);
73
+ }
74
+ }
75
+ }
76
+ // 保存最后一个文件
77
+ if (currentFile && currentHunk.length > 0) {
78
+ patches.set(currentFile, {
79
+ header: `*** Update File: ${currentFile}`,
80
+ hunk: [...currentHunk]
81
+ });
82
+ }
83
+ if (patches.size === 0) {
84
+ // 如果无法解析,返回原始文本包装
85
+ return `*** Begin Patch\n${diffText}\n*** End Patch`;
86
+ }
87
+ // 构建 apply_patch 格式
88
+ const result = ['*** Begin Patch'];
89
+ for (const [file, { header, hunk }] of patches.entries()) {
90
+ result.push(header);
91
+ result.push(...hunk);
92
+ }
93
+ result.push('*** End Patch');
94
+ return result.join('\n');
95
+ }
96
+ /**
97
+ * 修复 hunk 中缺少前缀的行
98
+ */
99
+ function fixHunkPrefixes(patchText) {
100
+ const lines = patchText.split('\n');
101
+ const result = [];
102
+ let inUpdateSection = false;
103
+ let afterHeader = false;
104
+ for (let i = 0; i < lines.length; i++) {
105
+ const line = lines[i];
106
+ if (line.startsWith('*** Begin Patch')) {
107
+ result.push(line);
108
+ continue;
109
+ }
110
+ if (line.startsWith('*** End Patch')) {
111
+ result.push(line);
112
+ inUpdateSection = false;
113
+ afterHeader = false;
114
+ continue;
115
+ }
116
+ if (line.startsWith('*** Update File:') || line.startsWith('*** Add File:') || line.startsWith('*** Delete File:')) {
117
+ result.push(line);
118
+ inUpdateSection = true;
119
+ afterHeader = true;
120
+ continue;
121
+ }
122
+ if (inUpdateSection) {
123
+ // 跳过紧跟在 header 后的空行
124
+ if (afterHeader && line.trim() === '') {
125
+ continue;
126
+ }
127
+ afterHeader = false;
128
+ // 检查是否是 hunk 行
129
+ if (/^@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@/.test(line)) {
130
+ result.push(line);
131
+ continue;
132
+ }
133
+ // 检查是否已经有正确前缀
134
+ if (/^[+-]\s/.test(line) || /^\s/.test(line)) {
135
+ result.push(line);
136
+ continue;
137
+ }
138
+ // 行缺少前缀,作为上下文行处理
139
+ if (line.trim() === '') {
140
+ result.push(' ');
141
+ }
142
+ else {
143
+ result.push(' ' + line);
144
+ }
145
+ }
146
+ else {
147
+ result.push(line);
148
+ }
149
+ }
150
+ return result.join('\n');
151
+ }
152
+ /**
153
+ * 主要的修复函数
154
+ */
155
+ function fixApplyPatchFormat(argsStr) {
156
+ try {
157
+ let args;
158
+ try {
159
+ args = JSON.parse(argsStr);
160
+ }
161
+ catch {
162
+ return { fixed: argsStr }; // 无法解析,返回原值
163
+ }
164
+ if (!args || typeof args !== 'object') {
165
+ return { fixed: argsStr };
166
+ }
167
+ let patch = args.patch || args.input || '';
168
+ if (typeof patch !== 'string') {
169
+ return { fixed: argsStr };
170
+ }
171
+ let modified = false;
172
+ let errorType;
173
+ // 修复 1: 裸 diff 缺少包装
174
+ if (isBareDiff(patch)) {
175
+ patch = convertBareDiffToApplyPatch(patch);
176
+ modified = true;
177
+ errorType = 'missing_begin_patch';
178
+ }
179
+ // 修复 2: hunk 中缺少前缀的行
180
+ const hasInvalidHunkLine = /"(.*?)".*Unexpected line found in update hunk/.test(patch);
181
+ if (hasInvalidHunkLine || !patch.match(/^@@/m)) {
182
+ const originalLength = patch.length;
183
+ patch = fixHunkPrefixes(patch);
184
+ if (patch.length !== originalLength) {
185
+ modified = true;
186
+ errorType = errorType || 'invalid_hunk_prefix';
187
+ }
188
+ }
189
+ if (modified) {
190
+ const newArgs = { ...args, patch, input: patch };
191
+ return { fixed: JSON.stringify(newArgs), errorType };
192
+ }
193
+ return { fixed: argsStr };
194
+ }
195
+ catch {
196
+ return { fixed: argsStr };
197
+ }
198
+ }
199
+ /**
200
+ * Bridge action: 遍历消息,修复 apply_patch 格式问题
201
+ */
202
+ const fixApplyPatchFormatAction = (ctx) => {
203
+ const messages = ensureMessagesArray(ctx.state);
204
+ for (const message of messages) {
205
+ if (message.role !== 'assistant')
206
+ continue;
207
+ if (!Array.isArray(message.tool_calls))
208
+ continue;
209
+ for (const toolCall of message.tool_calls) {
210
+ if (toolCall.type !== 'function')
211
+ continue;
212
+ const fn = toolCall.function;
213
+ if (!fn || fn.name !== 'apply_patch')
214
+ continue;
215
+ const rawArgs = fn.arguments;
216
+ if (typeof rawArgs !== 'string')
217
+ continue;
218
+ const { fixed, errorType } = fixApplyPatchFormat(rawArgs);
219
+ if (fixed !== rawArgs) {
220
+ // 保存可修复样本
221
+ captureApplyPatchRegression({
222
+ errorType: errorType || 'unknown_format_issue',
223
+ originalArgs: rawArgs,
224
+ fixerResult: fixed,
225
+ source: 'compat.fix-apply-patch-format'
226
+ });
227
+ fn.arguments = fixed;
228
+ toolCall._format_fixed = true;
229
+ }
230
+ }
231
+ }
232
+ };
233
+ registerBridgeAction('compat.fix-apply-patch-format', fixApplyPatchFormatAction);
@@ -0,0 +1,2 @@
1
+ import './apply-patch-fixer.js';
2
+ import './apply-patch-format-fixer.js';
@@ -0,0 +1,2 @@
1
+ import './apply-patch-fixer.js';
2
+ import './apply-patch-format-fixer.js';
@@ -64,7 +64,13 @@ export declare class HubPipeline {
64
64
  private config;
65
65
  private unsubscribeProviderErrors?;
66
66
  constructor(config: HubPipelineConfig);
67
+ updateRuntimeDeps(deps: {
68
+ healthStore?: HubPipelineConfig['healthStore'] | null;
69
+ routingStateStore?: HubPipelineConfig['routingStateStore'] | null;
70
+ quotaView?: HubPipelineConfig['quotaView'] | null;
71
+ }): void;
67
72
  updateVirtualRouterConfig(nextConfig: VirtualRouterConfig): void;
73
+ dispose(): void;
68
74
  private executeRequestStagePipeline;
69
75
  execute(request: HubPipelineRequest): Promise<HubPipelineResult>;
70
76
  private captureAnthropicAliasMap;
@@ -51,6 +51,30 @@ export class HubPipeline {
51
51
  this.unsubscribeProviderErrors = undefined;
52
52
  }
53
53
  }
54
+ updateRuntimeDeps(deps) {
55
+ if (!deps || typeof deps !== 'object') {
56
+ return;
57
+ }
58
+ if ('healthStore' in deps) {
59
+ this.config.healthStore = deps.healthStore ?? undefined;
60
+ }
61
+ if ('routingStateStore' in deps) {
62
+ this.config.routingStateStore = (deps.routingStateStore ?? undefined);
63
+ }
64
+ if ('quotaView' in deps) {
65
+ this.config.quotaView = deps.quotaView ?? undefined;
66
+ }
67
+ try {
68
+ this.routerEngine.updateDeps({
69
+ healthStore: this.config.healthStore ?? null,
70
+ routingStateStore: (this.config.routingStateStore ?? null),
71
+ quotaView: this.config.quotaView ?? null
72
+ });
73
+ }
74
+ catch {
75
+ // best-effort: runtime deps updates must never break routing
76
+ }
77
+ }
54
78
  updateVirtualRouterConfig(nextConfig) {
55
79
  if (!nextConfig || typeof nextConfig !== 'object') {
56
80
  throw new Error('HubPipeline updateVirtualRouterConfig requires VirtualRouterConfig payload');
@@ -58,6 +82,17 @@ export class HubPipeline {
58
82
  this.config.virtualRouter = nextConfig;
59
83
  this.routerEngine.initialize(nextConfig);
60
84
  }
85
+ dispose() {
86
+ if (this.unsubscribeProviderErrors) {
87
+ try {
88
+ this.unsubscribeProviderErrors();
89
+ }
90
+ catch {
91
+ // ignore dispose failures
92
+ }
93
+ this.unsubscribeProviderErrors = undefined;
94
+ }
95
+ }
61
96
  async executeRequestStagePipeline(normalized, hooks) {
62
97
  const formatAdapter = hooks.createFormatAdapter();
63
98
  const semanticMapper = hooks.createSemanticMapper();
@@ -20,3 +20,4 @@ export interface BridgeInputToChatOptions {
20
20
  toolResultFallbackText?: string;
21
21
  }
22
22
  export declare function convertBridgeInputToChatMessages(options: BridgeInputToChatOptions): Array<Record<string, unknown>>;
23
+ export declare function ensureMessagesArray(state: any): Array<Record<string, unknown>>;
@@ -644,3 +644,10 @@ export function convertBridgeInputToChatMessages(options) {
644
644
  }
645
645
  return messages;
646
646
  }
647
+ export function ensureMessagesArray(state) {
648
+ if (Array.isArray(state.messages))
649
+ return state.messages;
650
+ if (!state.messages)
651
+ state.messages = [];
652
+ return state.messages;
653
+ }
@@ -171,7 +171,7 @@ const OPENAI_CHAT_POLICY = {
171
171
  request: {
172
172
  inbound: [
173
173
  reasoningAction('openai_chat_reasoning'),
174
- toolCallNormalizationAction('openai_chat_tool_call'),
174
+ toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
175
175
  { name: 'messages.ensure-system-instruction' },
176
176
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } },
177
177
  { name: 'metadata.provider-field', options: { field: 'metadata', target: 'providerMetadata' } },
@@ -180,7 +180,7 @@ const OPENAI_CHAT_POLICY = {
180
180
  outbound: [
181
181
  { name: 'messages.normalize-history' },
182
182
  { name: 'tools.capture-results' },
183
- toolCallNormalizationAction('openai_chat_tool_call'),
183
+ toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
184
184
  { name: 'tools.ensure-placeholders' },
185
185
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
186
186
  { name: 'messages.ensure-system-instruction' },
@@ -193,12 +193,12 @@ const OPENAI_CHAT_POLICY = {
193
193
  response: {
194
194
  inbound: [
195
195
  reasoningAction('openai_chat_reasoning'),
196
- toolCallNormalizationAction('openai_chat_tool_call'),
196
+ toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
197
197
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
198
198
  ],
199
199
  outbound: [
200
200
  reasoningAction('openai_chat_reasoning'),
201
- toolCallNormalizationAction('openai_chat_tool_call'),
201
+ toolCallNormalizationAction('openai_chat_tool_call'), { name: 'compat.fix-apply-patch' },
202
202
  { name: 'metadata.extra-fields', options: { allowedKeys: OPENAI_CHAT_ALLOWED_FIELDS } }
203
203
  ]
204
204
  }
@@ -242,7 +242,7 @@ const GEMINI_POLICY = {
242
242
  protocol: 'gemini-chat',
243
243
  request: {
244
244
  inbound: [
245
- reasoningAction('gemini_reasoning'),
245
+ reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
246
246
  { name: 'messages.ensure-system-instruction' },
247
247
  { name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
248
248
  ],
@@ -252,17 +252,17 @@ const GEMINI_POLICY = {
252
252
  { name: 'tools.ensure-placeholders' },
253
253
  { name: 'messages.ensure-output-fields', options: { toolFallback: 'Tool call completed (no output).' } },
254
254
  { name: 'messages.ensure-system-instruction' },
255
- reasoningAction('gemini_reasoning'),
255
+ reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
256
256
  { name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
257
257
  ]
258
258
  },
259
259
  response: {
260
260
  inbound: [
261
- reasoningAction('gemini_reasoning'),
261
+ reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
262
262
  { name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
263
263
  ],
264
264
  outbound: [
265
- reasoningAction('gemini_reasoning'),
265
+ reasoningAction('gemini_reasoning'), { name: 'compat.fix-apply-patch' },
266
266
  { name: 'metadata.extra-fields', options: { allowedKeys: GEMINI_ALLOWED_FIELDS } }
267
267
  ]
268
268
  }
@@ -1,4 +1,4 @@
1
- export type ProviderProtocolErrorCode = 'TOOL_PROTOCOL_ERROR' | 'SSE_DECODE_ERROR' | 'MALFORMED_RESPONSE' | 'MALFORMED_REQUEST' | 'SERVERTOOL_FOLLOWUP_FAILED';
1
+ export type ProviderProtocolErrorCode = 'TOOL_PROTOCOL_ERROR' | 'SSE_DECODE_ERROR' | 'MALFORMED_RESPONSE' | 'MALFORMED_REQUEST' | 'SERVERTOOL_FOLLOWUP_FAILED' | 'SERVERTOOL_TIMEOUT' | 'SERVERTOOL_HANDLER_FAILED';
2
2
  export type ProviderErrorCategory = 'EXTERNAL_ERROR' | 'TOOL_ERROR' | 'INTERNAL_ERROR';
3
3
  export interface ProviderProtocolErrorOptions {
4
4
  code: ProviderProtocolErrorCode;
@@ -2,6 +2,9 @@ function inferCategoryFromCode(code) {
2
2
  switch (code) {
3
3
  case 'TOOL_PROTOCOL_ERROR':
4
4
  return 'TOOL_ERROR';
5
+ case 'SERVERTOOL_TIMEOUT':
6
+ case 'SERVERTOOL_HANDLER_FAILED':
7
+ return 'INTERNAL_ERROR';
5
8
  case 'SSE_DECODE_ERROR':
6
9
  case 'MALFORMED_RESPONSE':
7
10
  case 'MALFORMED_REQUEST':
@@ -4,7 +4,8 @@
4
4
  // enforceChatBudget: 为避免在请求侧引入多余依赖,这里提供最小实现(保留形状,不裁剪)。
5
5
  import { augmentOpenAITools } from '../../guidance/index.js';
6
6
  import { validateToolCall } from '../../tools/tool-registry.js';
7
- import { repairFindMeta } from './tooling.js';
7
+ import { captureApplyPatchRegression } from '../../tools/patch-regression-capturer.js';
8
+ import { normalizeExecCommandArgs } from '../../tools/exec-command/normalize.js';
8
9
  function isObject(v) { return !!v && typeof v === 'object' && !Array.isArray(v); }
9
10
  // Note: tool schema strict augmentation removed per alignment
10
11
  function enforceChatBudget(chat, _modelId) { return chat; }
@@ -245,6 +246,13 @@ export function normalizeApplyPatchToolCallsOnResponse(chat) {
245
246
  else if (validation && !validation.ok) {
246
247
  try {
247
248
  const reason = validation.reason ?? 'unknown';
249
+ captureApplyPatchRegression({
250
+ errorType: reason,
251
+ originalArgs: rawArgs,
252
+ normalizedArgs: argsStr,
253
+ validationError: reason,
254
+ source: 'tool-governor.response'
255
+ });
248
256
  const snippet = typeof argsStr === 'string' && argsStr.trim().length
249
257
  ? argsStr.trim().slice(0, 200).replace(/\s+/g, ' ')
250
258
  : '';
@@ -312,6 +320,13 @@ function normalizeSpecialToolCallsOnRequest(request) {
312
320
  else if (validation && !validation.ok) {
313
321
  try {
314
322
  const reason = validation.reason ?? 'unknown';
323
+ captureApplyPatchRegression({
324
+ errorType: reason,
325
+ originalArgs: rawArgs,
326
+ normalizedArgs: argsStr,
327
+ validationError: reason,
328
+ source: 'tool-governor.request'
329
+ });
315
330
  const snippet = typeof argsStr === 'string' && argsStr.trim().length
316
331
  ? argsStr.trim().slice(0, 200).replace(/\s+/g, ' ')
317
332
  : '';
@@ -336,8 +351,9 @@ function normalizeSpecialToolCallsOnRequest(request) {
336
351
  }
337
352
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
338
353
  const normalized = normalizeExecCommandArgs(parsed);
354
+ const next = normalized.ok ? normalized.normalized : parsed;
339
355
  try {
340
- fn.arguments = JSON.stringify(normalized ?? {});
356
+ fn.arguments = JSON.stringify(next ?? {});
341
357
  }
342
358
  catch {
343
359
  fn.arguments = '{}';
@@ -356,27 +372,6 @@ function normalizeSpecialToolCallsOnRequest(request) {
356
372
  return request;
357
373
  }
358
374
  }
359
- function normalizeExecCommandArgs(args) {
360
- try {
361
- const out = { ...args };
362
- const rawCmd = typeof out.cmd === 'string' && out.cmd.trim().length
363
- ? String(out.cmd)
364
- : typeof out.command === 'string' && out.command.trim().length
365
- ? String(out.command)
366
- : undefined;
367
- if (rawCmd) {
368
- const fixed = repairFindMeta(rawCmd);
369
- out.cmd = fixed;
370
- if (typeof out.command === 'string') {
371
- out.command = fixed;
372
- }
373
- }
374
- return out;
375
- }
376
- catch {
377
- return args;
378
- }
379
- }
380
375
  function enhanceResponseToolArguments(chat) {
381
376
  try {
382
377
  const enable = String(process?.env?.RCC_TOOL_ENHANCE ?? '1').trim() !== '0';
@@ -1,28 +1,8 @@
1
1
  import { repairFindMeta } from '../../conversion/shared/tooling.js';
2
+ import { normalizeExecCommandArgs } from '../../tools/exec-command/normalize.js';
2
3
  function isObject(v) {
3
4
  return !!v && typeof v === 'object' && !Array.isArray(v);
4
5
  }
5
- function normalizeExecCommandArgs(args) {
6
- try {
7
- const out = { ...args };
8
- const rawCmd = typeof out.cmd === 'string' && out.cmd.trim().length
9
- ? String(out.cmd)
10
- : typeof out.command === 'string' && out.command.trim().length
11
- ? String(out.command)
12
- : undefined;
13
- if (rawCmd) {
14
- const fixed = repairFindMeta(rawCmd);
15
- out.cmd = fixed;
16
- if (typeof out.command === 'string') {
17
- out.command = fixed;
18
- }
19
- }
20
- return out;
21
- }
22
- catch {
23
- return args;
24
- }
25
- }
26
6
  function packShellCommand(cmd) {
27
7
  // Normalize into ["bash","-lc","<single string>"] to support pipes, parens, -exec, etc.
28
8
  const normalizeArray = (argv) => {
@@ -104,8 +84,9 @@ export class ResponseToolArgumentsStringifyFilter {
104
84
  }
105
85
  else if ((name === 'exec_command' || name === 'shell_command' || name === 'bash') && isObject(parsed)) {
106
86
  const normalized = normalizeExecCommandArgs(parsed);
87
+ const next = normalized.ok ? normalized.normalized : parsed;
107
88
  try {
108
- fn.arguments = JSON.stringify(normalized ?? {});
89
+ fn.arguments = JSON.stringify(next ?? {});
109
90
  }
110
91
  catch {
111
92
  fn.arguments = '{}';
@@ -29,6 +29,11 @@ export declare class VirtualRouterEngine {
29
29
  routingStateStore?: RoutingInstructionStateStore;
30
30
  quotaView?: ProviderQuotaView;
31
31
  });
32
+ updateDeps(deps: {
33
+ healthStore?: VirtualRouterHealthStore | null;
34
+ routingStateStore?: RoutingInstructionStateStore | null;
35
+ quotaView?: ProviderQuotaView | null;
36
+ }): void;
32
37
  private parseDirectProviderModel;
33
38
  initialize(config: VirtualRouterConfig): void;
34
39
  route(request: StandardizedRequest | ProcessedRequest, metadata: RouterMetadataInput): {
@@ -44,6 +44,27 @@ export class VirtualRouterEngine {
44
44
  this.quotaView = deps.quotaView;
45
45
  }
46
46
  }
47
+ updateDeps(deps) {
48
+ if (!deps || typeof deps !== 'object') {
49
+ return;
50
+ }
51
+ if ('healthStore' in deps) {
52
+ this.healthStore = deps.healthStore ?? undefined;
53
+ }
54
+ if ('routingStateStore' in deps) {
55
+ this.routingStateStore =
56
+ deps.routingStateStore ??
57
+ {
58
+ loadSync: loadRoutingInstructionStateSync,
59
+ saveAsync: saveRoutingInstructionStateAsync
60
+ };
61
+ // Routing state store changes require clearing in-memory cache to avoid stale reads.
62
+ this.routingInstructionState.clear();
63
+ }
64
+ if ('quotaView' in deps) {
65
+ this.quotaView = deps.quotaView ?? undefined;
66
+ }
67
+ }
47
68
  parseDirectProviderModel(model) {
48
69
  const raw = typeof model === 'string' ? model.trim() : '';
49
70
  if (!raw) {