@jsonstudio/llms 0.6.1739 → 0.6.1890

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 (107) hide show
  1. package/dist/conversion/compat/actions/deepseek-web-request.d.ts +3 -0
  2. package/dist/conversion/compat/actions/deepseek-web-request.js +350 -0
  3. package/dist/conversion/compat/actions/deepseek-web-response.d.ts +3 -0
  4. package/dist/conversion/compat/actions/deepseek-web-response.js +886 -0
  5. package/dist/conversion/compat/actions/gemini-cli-request.js +3 -1
  6. package/dist/conversion/compat/profiles/chat-deepseek-web.json +18 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +166 -2
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +169 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +6 -0
  10. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +12 -0
  11. package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +1 -0
  12. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +4 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.js +365 -144
  14. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +9 -0
  15. package/dist/conversion/hub/policy/policy-engine.d.ts +2 -0
  16. package/dist/conversion/hub/policy/policy-engine.js +8 -0
  17. package/dist/conversion/hub/process/chat-process.js +466 -16
  18. package/dist/conversion/hub/response/provider-response.js +0 -35
  19. package/dist/conversion/responses/responses-openai-bridge.d.ts +2 -0
  20. package/dist/conversion/responses/responses-openai-bridge.js +166 -8
  21. package/dist/conversion/shared/anthropic-message-utils.js +10 -1
  22. package/dist/conversion/shared/protocol-field-allowlists.d.ts +2 -2
  23. package/dist/conversion/shared/protocol-field-allowlists.js +4 -0
  24. package/dist/conversion/shared/tool-governor.js +102 -0
  25. package/dist/guidance/index.js +17 -0
  26. package/dist/router/virtual-router/bootstrap.js +46 -1
  27. package/dist/router/virtual-router/classifier.js +59 -4
  28. package/dist/router/virtual-router/engine/health/index.js +6 -6
  29. package/dist/router/virtual-router/engine/routing-state/store.js +16 -3
  30. package/dist/router/virtual-router/engine-logging.js +62 -24
  31. package/dist/router/virtual-router/engine-selection/route-utils.js +20 -20
  32. package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -2
  33. package/dist/router/virtual-router/engine.d.ts +3 -1
  34. package/dist/router/virtual-router/engine.js +359 -39
  35. package/dist/router/virtual-router/features.js +2 -1
  36. package/dist/router/virtual-router/pre-command-file-resolver.d.ts +2 -0
  37. package/dist/router/virtual-router/pre-command-file-resolver.js +90 -0
  38. package/dist/router/virtual-router/provider-registry.js +3 -1
  39. package/dist/router/virtual-router/routing-instructions.d.ts +15 -1
  40. package/dist/router/virtual-router/routing-instructions.js +110 -151
  41. package/dist/router/virtual-router/routing-pre-command-actions.d.ts +3 -0
  42. package/dist/router/virtual-router/routing-pre-command-actions.js +26 -0
  43. package/dist/router/virtual-router/routing-pre-command-parser.d.ts +2 -0
  44. package/dist/router/virtual-router/routing-pre-command-parser.js +85 -0
  45. package/dist/router/virtual-router/routing-pre-command-state-codec.d.ts +3 -0
  46. package/dist/router/virtual-router/routing-pre-command-state-codec.js +24 -0
  47. package/dist/router/virtual-router/routing-stop-message-actions.d.ts +2 -0
  48. package/dist/router/virtual-router/routing-stop-message-actions.js +96 -0
  49. package/dist/router/virtual-router/routing-stop-message-parser.d.ts +3 -0
  50. package/dist/router/virtual-router/routing-stop-message-parser.js +142 -0
  51. package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +4 -0
  52. package/dist/router/virtual-router/routing-stop-message-state-codec.js +85 -0
  53. package/dist/router/virtual-router/sticky-session-store.js +206 -57
  54. package/dist/router/virtual-router/stop-message-stage-template-files.d.ts +12 -0
  55. package/dist/router/virtual-router/stop-message-stage-template-files.js +67 -0
  56. package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
  57. package/dist/router/virtual-router/stop-message-state-sync.js +5 -0
  58. package/dist/router/virtual-router/token-file-scanner.d.ts +9 -0
  59. package/dist/router/virtual-router/token-file-scanner.js +64 -3
  60. package/dist/router/virtual-router/tool-signals.d.ts +5 -0
  61. package/dist/router/virtual-router/tool-signals.js +42 -3
  62. package/dist/router/virtual-router/types.d.ts +19 -1
  63. package/dist/router/virtual-router/types.js +1 -0
  64. package/dist/servertool/clock/config.d.ts +1 -1
  65. package/dist/servertool/clock/config.js +27 -4
  66. package/dist/servertool/clock/state.js +41 -2
  67. package/dist/servertool/clock/task-store.d.ts +2 -2
  68. package/dist/servertool/clock/task-store.js +1 -1
  69. package/dist/servertool/clock/tasks.d.ts +3 -1
  70. package/dist/servertool/clock/tasks.js +209 -18
  71. package/dist/servertool/clock/types.d.ts +17 -0
  72. package/dist/servertool/continue-execution/log.d.ts +3 -0
  73. package/dist/servertool/continue-execution/log.js +13 -0
  74. package/dist/servertool/engine.js +414 -68
  75. package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +6 -6
  76. package/dist/servertool/handlers/clock-auto.js +54 -71
  77. package/dist/servertool/handlers/clock.js +121 -6
  78. package/dist/servertool/handlers/continue-execution.d.ts +1 -0
  79. package/dist/servertool/handlers/continue-execution.js +91 -0
  80. package/dist/servertool/handlers/followup-request-builder.js +13 -0
  81. package/dist/servertool/handlers/gemini-empty-reply-continue.js +1 -1
  82. package/dist/servertool/handlers/iflow-model-error-retry.js +1 -1
  83. package/dist/servertool/handlers/recursive-detection-guard.js +1 -1
  84. package/dist/servertool/handlers/stop-message-auto.js +386 -257
  85. package/dist/servertool/handlers/stop-message-stage-policy.d.ts +43 -0
  86. package/dist/servertool/handlers/stop-message-stage-policy.js +684 -0
  87. package/dist/servertool/handlers/vision.js +1 -1
  88. package/dist/servertool/log/progress-file.d.ts +14 -0
  89. package/dist/servertool/log/progress-file.js +88 -0
  90. package/dist/servertool/pre-command-hooks.d.ts +17 -0
  91. package/dist/servertool/pre-command-hooks.js +491 -0
  92. package/dist/servertool/registry.d.ts +23 -6
  93. package/dist/servertool/registry.js +66 -1
  94. package/dist/servertool/server-side-tools.d.ts +1 -0
  95. package/dist/servertool/server-side-tools.js +216 -14
  96. package/dist/servertool/stop-gateway-context.d.ts +14 -0
  97. package/dist/servertool/stop-gateway-context.js +167 -0
  98. package/dist/servertool/stop-message-compare-context.d.ts +24 -0
  99. package/dist/servertool/stop-message-compare-context.js +133 -0
  100. package/dist/servertool/types.d.ts +12 -0
  101. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +1 -0
  102. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.js +36 -1
  103. package/dist/sse/sse-to-json/builders/anthropic-response-builder.js +3 -0
  104. package/dist/sse/sse-to-json/chat-sse-to-json-converter.d.ts +3 -0
  105. package/dist/sse/sse-to-json/chat-sse-to-json-converter.js +118 -1
  106. package/dist/tools/apply-patch/args-normalizer/default-actions.js +1 -1
  107. package/package.json +1 -1
@@ -8,6 +8,12 @@ import { applyHubFollowupPolicyShadow } from './followup-shadow.js';
8
8
  import { buildServerToolFollowupChatPayloadFromInjection, extractCapturedChatSeed } from './handlers/followup-request-builder.js';
9
9
  import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig } from './clock/task-store.js';
10
10
  import { savePendingServerToolInjection } from './pending-session.js';
11
+ import { appendServerToolProgressFileEvent } from './log/progress-file.js';
12
+ import { attachStopGatewayContext, inspectStopGatewaySignal } from './stop-gateway-context.js';
13
+ import { formatStopMessageCompareContext, readStopMessageCompareContext } from './stop-message-compare-context.js';
14
+ const STOP_MESSAGE_STAGE_TIMEOUT_MS = 900_000;
15
+ const STOP_MESSAGE_LOOP_WARN_THRESHOLD = 5;
16
+ const STOP_MESSAGE_LOOP_FAIL_THRESHOLD = 10;
11
17
  function parseTimeoutMs(raw, fallback) {
12
18
  const n = typeof raw === 'string' ? Number(raw.trim()) : typeof raw === 'number' ? raw : NaN;
13
19
  if (!Number.isFinite(n) || n <= 0) {
@@ -63,6 +69,36 @@ function createServerToolTimeoutError(options) {
63
69
  err.status = 504;
64
70
  return err;
65
71
  }
72
+ function createStopMessageFetchFailedError(options) {
73
+ const baseMessage = options.reason === 'loop_limit'
74
+ ? 'fetch failed: network error (stopMessage loop detected)'
75
+ : 'fetch failed: network error (stopMessage exceeded stage timeout)';
76
+ const err = new ProviderProtocolError(baseMessage, {
77
+ code: 'SERVERTOOL_TIMEOUT',
78
+ category: 'EXTERNAL_ERROR',
79
+ details: {
80
+ requestId: options.requestId,
81
+ reason: options.reason,
82
+ ...(typeof options.elapsedMs === 'number' && Number.isFinite(options.elapsedMs)
83
+ ? { elapsedMs: Math.max(0, Math.floor(options.elapsedMs)) }
84
+ : {}),
85
+ ...(typeof options.repeatCount === 'number' && Number.isFinite(options.repeatCount)
86
+ ? { repeatCount: Math.max(0, Math.floor(options.repeatCount)) }
87
+ : {}),
88
+ ...(typeof options.timeoutMs === 'number' && Number.isFinite(options.timeoutMs)
89
+ ? { timeoutMs: Math.max(0, Math.floor(options.timeoutMs)) }
90
+ : {}),
91
+ ...(typeof options.attempt === 'number' && Number.isFinite(options.attempt)
92
+ ? { attempt: Math.max(1, Math.floor(options.attempt)) }
93
+ : {}),
94
+ ...(typeof options.maxAttempts === 'number' && Number.isFinite(options.maxAttempts)
95
+ ? { maxAttempts: Math.max(1, Math.floor(options.maxAttempts)) }
96
+ : {})
97
+ }
98
+ });
99
+ err.status = 502;
100
+ return err;
101
+ }
66
102
  function coerceFollowupPayloadStream(payload, stream) {
67
103
  if (!payload || typeof payload !== 'object') {
68
104
  return payload;
@@ -159,49 +195,7 @@ function isEmptyClientResponsePayload(payload) {
159
195
  return true;
160
196
  }
161
197
  function isStopFinishReasonWithoutToolCalls(base) {
162
- if (!base || typeof base !== 'object' || Array.isArray(base)) {
163
- return false;
164
- }
165
- const payload = base;
166
- const choicesRaw = payload.choices;
167
- if (Array.isArray(choicesRaw) && choicesRaw.length) {
168
- const first = choicesRaw[0];
169
- if (!first || typeof first !== 'object' || Array.isArray(first)) {
170
- return false;
171
- }
172
- const finishReasonRaw = first.finish_reason;
173
- const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
174
- ? finishReasonRaw.trim().toLowerCase()
175
- : '';
176
- if (!finishReason || finishReason === 'tool_calls') {
177
- return false;
178
- }
179
- if (finishReason !== 'stop' && finishReason !== 'length') {
180
- return false;
181
- }
182
- const message = first.message &&
183
- typeof first.message === 'object' &&
184
- !Array.isArray(first.message)
185
- ? first.message
186
- : null;
187
- if (!message) {
188
- return false;
189
- }
190
- const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
191
- if (toolCalls.length > 0) {
192
- return false;
193
- }
194
- return true;
195
- }
196
- // Responses-like: completed without required_action generally counts as stop.
197
- const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
198
- if (statusRaw && statusRaw !== 'completed') {
199
- return false;
200
- }
201
- if (payload.required_action && typeof payload.required_action === 'object') {
202
- return false;
203
- }
204
- return true;
198
+ return inspectStopGatewaySignal(base).eligible;
205
199
  }
206
200
  async function shouldDisableServerToolTimeoutForClockHold(args) {
207
201
  // Only relevant for stop/length responses: clock_auto may hold indefinitely.
@@ -241,19 +235,183 @@ async function shouldDisableServerToolTimeoutForClockHold(args) {
241
235
  }
242
236
  }
243
237
  export async function runServerToolOrchestration(options) {
238
+ const BLUE = '\x1b[38;5;39m';
244
239
  const YELLOW = '\x1b[38;5;214m';
240
+ const GOLD = '\x1b[38;5;220m';
245
241
  const RESET = '\x1b[0m';
246
- const logProgress = (step, total, message, extra) => {
242
+ const resolveToolName = (flowId) => {
243
+ const normalized = flowId.trim();
244
+ if (!normalized)
245
+ return 'unknown';
246
+ const mapping = {
247
+ continue_execution_flow: 'continue_execution',
248
+ stop_message_flow: 'stop_message_auto',
249
+ empty_reply_continue: 'empty_reply_continue',
250
+ apply_patch_guard: 'apply_patch_guard',
251
+ exec_command_guard: 'exec_command_guard',
252
+ iflow_model_error_retry: 'iflow_model_error_retry',
253
+ antigravity_thought_signature_bootstrap: 'antigravity_thought_signature_bootstrap',
254
+ web_search_flow: 'web_search',
255
+ vision_flow: 'vision_auto',
256
+ clock_flow: 'clock',
257
+ clock_hold_flow: 'clock_auto',
258
+ recursive_detection_guard: 'recursive_detection_guard'
259
+ };
260
+ return mapping[normalized] ?? normalized;
261
+ };
262
+ const resolveStage = (step, message) => {
263
+ const normalized = message.trim().toLowerCase();
264
+ if (normalized === 'matched' || step <= 1)
265
+ return 'match';
266
+ if (normalized.startsWith('completed') || step >= 5)
267
+ return 'final';
268
+ return 'followup';
269
+ };
270
+ const normalizeResult = (message) => {
271
+ const normalized = message.trim().toLowerCase();
272
+ if (!normalized)
273
+ return 'unknown';
274
+ const group = /^completed\s*\(([^)]+)\)/.exec(normalized);
275
+ if (group && group[1]) {
276
+ return 'completed_' + group[1].trim().replace(/[^a-z0-9]+/g, '_');
277
+ }
278
+ return normalized.replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '') || 'unknown';
279
+ };
280
+ const logStopEntry = (stage, result, extra) => {
281
+ const color = BLUE;
282
+ const viewStage = stage === 'trigger' ? 'match' : 'entry';
283
+ const source = typeof extra?.source === 'string' ? extra.source : 'unknown';
284
+ const reason = typeof extra?.reason === 'string' ? extra.reason : 'unknown';
285
+ const eligible = typeof extra?.eligible === 'boolean' ? String(extra.eligible) : 'unknown';
286
+ const flowId = typeof extra?.flowId === 'string' ? extra.flowId : '';
287
+ const brief = stage === 'entry'
288
+ ? `source=${source} reason=${reason} eligible=${eligible}`
289
+ : `result=${result} flow=${flowId || 'none'}`;
290
+ try {
291
+ // eslint-disable-next-line no-console
292
+ console.log(`${color}[servertool][stop_watch] requestId=${options.requestId} stage=${viewStage} ${brief}${RESET}`);
293
+ }
294
+ catch {
295
+ /* best-effort logging */
296
+ }
297
+ appendServerToolProgressFileEvent({
298
+ requestId: options.requestId,
299
+ flowId: 'stop_message_flow',
300
+ tool: 'stop_message_auto',
301
+ stage,
302
+ result,
303
+ message: result,
304
+ step: stage === 'entry' ? 0 : 2,
305
+ entryEndpoint: options.entryEndpoint,
306
+ providerProtocol: options.providerProtocol
307
+ });
308
+ };
309
+ const logProgress = (step, _total, message, extra) => {
310
+ const flowId = typeof extra?.flowId === 'string' ? extra.flowId.trim() : '';
311
+ const tool = resolveToolName(flowId);
312
+ const stage = resolveStage(step, message);
313
+ const result = normalizeResult(message);
314
+ const color = flowId === 'continue_execution_flow' ? GOLD : YELLOW;
247
315
  try {
248
316
  // eslint-disable-next-line no-console
249
- console.log(`${YELLOW}[servertool][progress ${step}/${total}] requestId=${options.requestId} ${message}` +
250
- (extra ? ` ${JSON.stringify(extra)}` : '') +
251
- RESET);
317
+ console.log(`${color}[servertool] requestId=${options.requestId} tool=${tool} stage=${stage} result=${result}${RESET}`);
252
318
  }
253
319
  catch {
254
320
  /* best-effort logging */
255
321
  }
322
+ appendServerToolProgressFileEvent({
323
+ requestId: options.requestId,
324
+ flowId: flowId || 'none',
325
+ tool,
326
+ stage,
327
+ result,
328
+ message,
329
+ step,
330
+ entryEndpoint: options.entryEndpoint,
331
+ providerProtocol: options.providerProtocol
332
+ });
333
+ };
334
+ const logAutoHookTrace = (event) => {
335
+ const reasonToken = typeof event.reason === 'string' && event.reason.trim()
336
+ ? event.reason.trim().toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '')
337
+ : 'unknown';
338
+ appendServerToolProgressFileEvent({
339
+ requestId: options.requestId,
340
+ flowId: event.flowId || `hook:${event.hookId}`,
341
+ tool: event.hookId,
342
+ stage: 'hook',
343
+ result: `${event.result}_${reasonToken || 'unknown'}`,
344
+ message: `${event.result} (${event.reason}) queue=${event.queue}[${event.queueIndex}/${event.queueTotal}] phase=${event.phase} priority=${event.priority}`,
345
+ step: 2,
346
+ entryEndpoint: options.entryEndpoint,
347
+ providerProtocol: options.providerProtocol
348
+ });
349
+ try {
350
+ options.stageRecorder?.record('servertool.hook', {
351
+ hookId: event.hookId,
352
+ phase: event.phase,
353
+ priority: event.priority,
354
+ result: event.result,
355
+ reason: event.reason,
356
+ queue: event.queue,
357
+ queueIndex: event.queueIndex,
358
+ queueTotal: event.queueTotal,
359
+ ...(event.flowId ? { flowId: event.flowId } : {})
360
+ });
361
+ }
362
+ catch {
363
+ // best-effort only
364
+ }
365
+ };
366
+ const logStopCompare = (stage, flowId) => {
367
+ const compareContext = readStopMessageCompareContext(options.adapterContext);
368
+ const summary = formatStopMessageCompareContext(compareContext);
369
+ const viewStage = stage === 'trigger' ? 'match' : 'entry';
370
+ const flowToken = flowId && flowId.trim() ? flowId.trim() : 'none';
371
+ try {
372
+ // eslint-disable-next-line no-console
373
+ console.log(`${BLUE}[servertool][stop_compare] requestId=${options.requestId} stage=${viewStage} flow=${flowToken} ${summary}${RESET}`);
374
+ }
375
+ catch {
376
+ // best-effort logging
377
+ }
378
+ const compareResult = compareContext
379
+ ? `${compareContext.decision}_${compareContext.reason.toLowerCase().replace(/[^a-z0-9]+/g, '_') || 'unknown'}`
380
+ : 'unknown_no_context';
381
+ appendServerToolProgressFileEvent({
382
+ requestId: options.requestId,
383
+ flowId: flowToken,
384
+ tool: 'stop_message_auto',
385
+ stage: 'compare',
386
+ result: compareResult,
387
+ message: summary,
388
+ step: stage === 'entry' ? 1 : 3,
389
+ entryEndpoint: options.entryEndpoint,
390
+ providerProtocol: options.providerProtocol
391
+ });
392
+ try {
393
+ options.stageRecorder?.record('servertool.stop_compare', {
394
+ stage: viewStage,
395
+ flowId: flowToken,
396
+ summary,
397
+ ...(compareContext ? { compare: compareContext } : {})
398
+ });
399
+ }
400
+ catch {
401
+ // best-effort only
402
+ }
256
403
  };
404
+ const stopSignal = inspectStopGatewaySignal(options.chat);
405
+ attachStopGatewayContext(options.adapterContext, stopSignal);
406
+ if (stopSignal.observed) {
407
+ logStopEntry('entry', 'observed', {
408
+ reason: stopSignal.reason,
409
+ source: stopSignal.source,
410
+ eligible: stopSignal.eligible,
411
+ ...(typeof stopSignal.choiceIndex === 'number' ? { choiceIndex: stopSignal.choiceIndex } : {}),
412
+ ...(typeof stopSignal.hasToolCalls === 'boolean' ? { hasToolCalls: stopSignal.hasToolCalls } : {})
413
+ });
414
+ }
257
415
  const serverToolTimeoutMs = resolveServerToolTimeoutMs();
258
416
  const shouldDisableTimeout = await shouldDisableServerToolTimeoutForClockHold({
259
417
  chat: options.chat,
@@ -269,7 +427,8 @@ export async function runServerToolOrchestration(options) {
269
427
  requestId: options.requestId,
270
428
  providerProtocol: options.providerProtocol,
271
429
  providerInvoker: options.providerInvoker,
272
- reenterPipeline: options.reenterPipeline
430
+ reenterPipeline: options.reenterPipeline,
431
+ onAutoHookTrace: logAutoHookTrace
273
432
  };
274
433
  const engineResult = await withTimeout(runServerSideToolEngine(engineOptions), effectiveServerToolTimeoutMs, () => createServerToolTimeoutError({
275
434
  requestId: options.requestId,
@@ -277,22 +436,51 @@ export async function runServerToolOrchestration(options) {
277
436
  timeoutMs: effectiveServerToolTimeoutMs || serverToolTimeoutMs
278
437
  }));
279
438
  if (engineResult.mode === 'passthrough' || !engineResult.execution) {
439
+ const skipReason = engineResult.mode === 'passthrough' ? 'passthrough' : 'no_execution';
440
+ if (stopSignal.observed) {
441
+ logStopEntry('trigger', `skipped_${skipReason}`, {
442
+ reason: stopSignal.reason,
443
+ source: stopSignal.source,
444
+ eligible: stopSignal.eligible
445
+ });
446
+ logStopCompare('trigger');
447
+ }
280
448
  try {
281
449
  options.stageRecorder?.record('servertool.match', {
282
450
  matched: false,
283
451
  mode: engineResult.mode,
284
- reason: engineResult.mode === 'passthrough' ? 'passthrough' : 'no_execution'
452
+ reason: skipReason
285
453
  });
286
454
  }
287
455
  catch {
288
456
  // best-effort only
289
457
  }
458
+ appendServerToolProgressFileEvent({
459
+ requestId: options.requestId,
460
+ flowId: 'none',
461
+ tool: 'none',
462
+ stage: 'match',
463
+ result: 'skipped_' + skipReason,
464
+ message: 'skipped (' + skipReason + ')',
465
+ step: 0,
466
+ entryEndpoint: options.entryEndpoint,
467
+ providerProtocol: options.providerProtocol
468
+ });
290
469
  return {
291
470
  chat: engineResult.finalChatResponse,
292
471
  executed: false
293
472
  };
294
473
  }
295
474
  const flowId = engineResult.execution.flowId ?? 'unknown';
475
+ if (stopSignal.observed) {
476
+ logStopEntry('trigger', flowId === 'stop_message_flow' ? 'activated' : 'non_stop_flow', {
477
+ flowId,
478
+ reason: stopSignal.reason,
479
+ source: stopSignal.source,
480
+ eligible: stopSignal.eligible
481
+ });
482
+ logStopCompare('trigger', flowId);
483
+ }
296
484
  try {
297
485
  options.stageRecorder?.record('servertool.match', {
298
486
  matched: true,
@@ -337,13 +525,12 @@ export async function runServerToolOrchestration(options) {
337
525
  };
338
526
  }
339
527
  const isStopMessageFlow = engineResult.execution.flowId === 'stop_message_flow';
528
+ const isClockHoldFlow = engineResult.execution.flowId === 'clock_hold_flow';
340
529
  const isEmptyReplyContinue = engineResult.execution.flowId === 'empty_reply_continue';
341
530
  const isApplyPatchGuard = engineResult.execution.flowId === 'apply_patch_guard';
342
531
  const isExecCommandGuard = engineResult.execution.flowId === 'exec_command_guard';
343
- const stopMessageSource = isStopMessageFlow ? getStopMessageSource(options.adapterContext) : undefined;
344
- const isAutoStopMessage = isStopMessageFlow && stopMessageSource !== 'explicit';
345
532
  const isErrorAutoFlow = engineResult.execution.flowId === 'iflow_model_error_retry';
346
- const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow || isEmptyReplyContinue || isApplyPatchGuard || isExecCommandGuard;
533
+ const applyAutoLimit = isErrorAutoFlow || isEmptyReplyContinue || isApplyPatchGuard || isExecCommandGuard;
347
534
  // ServerTool followups must not inherit or inject any routeHint; always route fresh.
348
535
  const preserveRouteHint = false;
349
536
  const followupPlan = engineResult.execution.followup;
@@ -382,7 +569,7 @@ export async function runServerToolOrchestration(options) {
382
569
  flowId: engineResult.execution.flowId
383
570
  };
384
571
  }
385
- const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, followupPayloadRaw);
572
+ const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, followupPayloadRaw, engineResult.finalChatResponse);
386
573
  if (applyAutoLimit && loopState && typeof loopState.repeatCount === 'number' && loopState.repeatCount >= 3) {
387
574
  logProgress(5, totalSteps, 'completed (auto limit hit)', { flowId });
388
575
  return {
@@ -391,6 +578,36 @@ export async function runServerToolOrchestration(options) {
391
578
  flowId: engineResult.execution.flowId
392
579
  };
393
580
  }
581
+ let shouldInjectStopLoopWarning = false;
582
+ if (isStopMessageFlow && loopState) {
583
+ const elapsedMs = typeof loopState.startedAtMs === 'number' && Number.isFinite(loopState.startedAtMs)
584
+ ? Math.max(0, Date.now() - loopState.startedAtMs)
585
+ : 0;
586
+ if (elapsedMs >= STOP_MESSAGE_STAGE_TIMEOUT_MS) {
587
+ throw createStopMessageFetchFailedError({
588
+ requestId: options.requestId,
589
+ reason: 'stage_timeout',
590
+ elapsedMs,
591
+ timeoutMs: STOP_MESSAGE_STAGE_TIMEOUT_MS
592
+ });
593
+ }
594
+ const pairRepeatCount = typeof loopState.stopPairRepeatCount === 'number' && Number.isFinite(loopState.stopPairRepeatCount)
595
+ ? Math.max(0, Math.floor(loopState.stopPairRepeatCount))
596
+ : 0;
597
+ if (pairRepeatCount >= STOP_MESSAGE_LOOP_FAIL_THRESHOLD) {
598
+ throw createStopMessageFetchFailedError({
599
+ requestId: options.requestId,
600
+ reason: 'loop_limit',
601
+ elapsedMs,
602
+ repeatCount: pairRepeatCount
603
+ });
604
+ }
605
+ if (pairRepeatCount >= STOP_MESSAGE_LOOP_WARN_THRESHOLD && !loopState.stopPairWarned) {
606
+ loopState.stopPairWarned = true;
607
+ shouldInjectStopLoopWarning = true;
608
+ logProgress(2, totalSteps, 'loop warning armed', { flowId });
609
+ }
610
+ }
394
611
  if (isAdapterClientDisconnected(options.adapterContext)) {
395
612
  logProgress(5, totalSteps, 'completed (client disconnected)', { flowId });
396
613
  return {
@@ -423,9 +640,9 @@ export async function runServerToolOrchestration(options) {
423
640
  (typeof options.entryEndpoint === 'string' && options.entryEndpoint.trim().length
424
641
  ? options.entryEndpoint
425
642
  : followupEntryEndpoint);
426
- // For stateful auto-followups (e.g. stop_message_flow), keep the same providerKey/alias.
427
- // Otherwise the followup requestId suffix would cause round-robin alias switching.
428
- if (isStopMessageFlow) {
643
+ // For stateful auto-followups (e.g. stop_message_flow / clock_hold_flow), keep the same providerKey/alias.
644
+ // Otherwise the followup requestId suffix would cause round-robin alias switching and compatibility drift.
645
+ if (isStopMessageFlow || isClockHoldFlow) {
429
646
  const providerKeyRaw = options.adapterContext.providerKey;
430
647
  const providerKey = typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length ? providerKeyRaw.trim() : '';
431
648
  if (providerKey) {
@@ -436,6 +653,9 @@ export async function runServerToolOrchestration(options) {
436
653
  const maxAttempts = retryEmptyFollowupOnce ? 2 : 1;
437
654
  const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
438
655
  let followupPayload = coerceFollowupPayloadStream(followupPayloadRaw, metadata.stream === true);
656
+ if (shouldInjectStopLoopWarning) {
657
+ appendStopMessageLoopWarning(followupPayload, loopState?.stopPairRepeatCount ?? STOP_MESSAGE_LOOP_WARN_THRESHOLD);
658
+ }
439
659
  followupPayload = applyHubFollowupPolicyShadow({
440
660
  requestId: followupRequestId,
441
661
  entryEndpoint: followupEntryEndpoint,
@@ -451,20 +671,45 @@ export async function runServerToolOrchestration(options) {
451
671
  // stop_message_flow 的计数器递增由 handler 在决定触发时处理,engine 不再提前递增。
452
672
  const stopMessageReservation = null;
453
673
  for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
674
+ const elapsedBeforeAttempt = isStopMessageFlow && loopState && typeof loopState.startedAtMs === 'number' && Number.isFinite(loopState.startedAtMs)
675
+ ? Math.max(0, Date.now() - loopState.startedAtMs)
676
+ : 0;
677
+ if (isStopMessageFlow && elapsedBeforeAttempt >= STOP_MESSAGE_STAGE_TIMEOUT_MS) {
678
+ throw createStopMessageFetchFailedError({
679
+ requestId: options.requestId,
680
+ reason: 'stage_timeout',
681
+ elapsedMs: elapsedBeforeAttempt,
682
+ timeoutMs: STOP_MESSAGE_STAGE_TIMEOUT_MS,
683
+ attempt,
684
+ maxAttempts
685
+ });
686
+ }
687
+ const attemptTimeoutMs = isStopMessageFlow && STOP_MESSAGE_STAGE_TIMEOUT_MS > elapsedBeforeAttempt
688
+ ? Math.max(1, Math.min(followupTimeoutMs, STOP_MESSAGE_STAGE_TIMEOUT_MS - elapsedBeforeAttempt))
689
+ : followupTimeoutMs;
454
690
  try {
455
691
  followup = await withTimeout(options.reenterPipeline({
456
692
  entryEndpoint: followupEntryEndpoint,
457
693
  requestId: followupRequestId,
458
694
  body: followupPayload,
459
695
  metadata
460
- }), followupTimeoutMs, () => createServerToolTimeoutError({
461
- requestId: options.requestId,
462
- phase: 'followup',
463
- timeoutMs: followupTimeoutMs,
464
- flowId: engineResult.execution.flowId,
465
- attempt,
466
- maxAttempts
467
- }));
696
+ }), attemptTimeoutMs, () => isStopMessageFlow
697
+ ? createStopMessageFetchFailedError({
698
+ requestId: options.requestId,
699
+ reason: 'stage_timeout',
700
+ elapsedMs: elapsedBeforeAttempt,
701
+ timeoutMs: STOP_MESSAGE_STAGE_TIMEOUT_MS,
702
+ attempt,
703
+ maxAttempts
704
+ })
705
+ : createServerToolTimeoutError({
706
+ requestId: options.requestId,
707
+ phase: 'followup',
708
+ timeoutMs: attemptTimeoutMs,
709
+ flowId: engineResult.execution.flowId,
710
+ attempt,
711
+ maxAttempts
712
+ }));
468
713
  // Treat empty followup as failure for auto followup flows:
469
714
  // - retry once (maxAttempts=2)
470
715
  // - if still empty, surface as HTTP error so client can retry.
@@ -784,7 +1029,7 @@ function resolveRouteHint(adapterContext, flowId) {
784
1029
  }
785
1030
  return routeId;
786
1031
  }
787
- function buildServerToolLoopState(adapterContext, flowId, payload) {
1032
+ function buildServerToolLoopState(adapterContext, flowId, payload, response) {
788
1033
  if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
789
1034
  return null;
790
1035
  }
@@ -800,11 +1045,33 @@ function buildServerToolLoopState(adapterContext, flowId, payload) {
800
1045
  ? Math.max(0, Math.floor(previous.repeatCount))
801
1046
  : 0;
802
1047
  const repeatCount = sameFlow && samePayload ? prevCount + 1 : 1;
803
- return {
1048
+ const previousStartedAtMs = sameFlow && previous && typeof previous.startedAtMs === 'number' && Number.isFinite(previous.startedAtMs)
1049
+ ? Math.max(0, Math.floor(previous.startedAtMs))
1050
+ : undefined;
1051
+ const startedAtMs = previousStartedAtMs ?? Date.now();
1052
+ const base = {
804
1053
  ...(flowId ? { flowId } : {}),
805
1054
  payloadHash,
806
- repeatCount
1055
+ repeatCount,
1056
+ startedAtMs
807
1057
  };
1058
+ if (flowId === 'stop_message_flow') {
1059
+ const pairHash = hashStopMessageRequestResponsePair(payload, response);
1060
+ if (pairHash) {
1061
+ const previousPairHash = sameFlow && previous && typeof previous.stopPairHash === 'string' ? previous.stopPairHash : undefined;
1062
+ const previousPairCount = sameFlow && previous && typeof previous.stopPairRepeatCount === 'number' && Number.isFinite(previous.stopPairRepeatCount)
1063
+ ? Math.max(0, Math.floor(previous.stopPairRepeatCount))
1064
+ : 0;
1065
+ const stopPairRepeatCount = previousPairHash === pairHash ? previousPairCount + 1 : 1;
1066
+ const stopPairWarned = previousPairHash === pairHash && previous && typeof previous.stopPairWarned === 'boolean'
1067
+ ? previous.stopPairWarned
1068
+ : false;
1069
+ base.stopPairHash = pairHash;
1070
+ base.stopPairRepeatCount = stopPairRepeatCount;
1071
+ base.stopPairWarned = stopPairWarned;
1072
+ }
1073
+ }
1074
+ return base;
808
1075
  }
809
1076
  function readServerToolLoopState(adapterContext) {
810
1077
  if (!adapterContext || typeof adapterContext !== 'object') {
@@ -821,13 +1088,27 @@ function readServerToolLoopState(adapterContext) {
821
1088
  const repeatCount = typeof record.repeatCount === 'number' && Number.isFinite(record.repeatCount)
822
1089
  ? Math.max(0, Math.floor(record.repeatCount))
823
1090
  : undefined;
1091
+ const startedAtMs = typeof record.startedAtMs === 'number' && Number.isFinite(record.startedAtMs)
1092
+ ? Math.max(0, Math.floor(record.startedAtMs))
1093
+ : undefined;
1094
+ const stopPairHash = typeof record.stopPairHash === 'string' && record.stopPairHash.trim().length
1095
+ ? record.stopPairHash.trim()
1096
+ : undefined;
1097
+ const stopPairRepeatCount = typeof record.stopPairRepeatCount === 'number' && Number.isFinite(record.stopPairRepeatCount)
1098
+ ? Math.max(0, Math.floor(record.stopPairRepeatCount))
1099
+ : undefined;
1100
+ const stopPairWarned = typeof record.stopPairWarned === 'boolean' ? record.stopPairWarned : undefined;
824
1101
  if (!payloadHash) {
825
1102
  return null;
826
1103
  }
827
1104
  return {
828
1105
  ...(flowId ? { flowId } : {}),
829
1106
  payloadHash,
830
- ...(repeatCount !== undefined ? { repeatCount } : {})
1107
+ ...(repeatCount !== undefined ? { repeatCount } : {}),
1108
+ ...(startedAtMs !== undefined ? { startedAtMs } : {}),
1109
+ ...(stopPairHash ? { stopPairHash } : {}),
1110
+ ...(stopPairRepeatCount !== undefined ? { stopPairRepeatCount } : {}),
1111
+ ...(stopPairWarned !== undefined ? { stopPairWarned } : {})
831
1112
  };
832
1113
  }
833
1114
  function hashPayload(payload) {
@@ -839,6 +1120,71 @@ function hashPayload(payload) {
839
1120
  return null;
840
1121
  }
841
1122
  }
1123
+ function hashStopMessageRequestResponsePair(payload, response) {
1124
+ try {
1125
+ const normalizedPayload = sanitizeLoopHashValue(payload);
1126
+ const normalizedResponse = sanitizeLoopHashValue(response ?? {});
1127
+ const stable = stableStringify({ request: normalizedPayload, response: normalizedResponse });
1128
+ return createHash('sha1').update(stable).digest('hex');
1129
+ }
1130
+ catch {
1131
+ return null;
1132
+ }
1133
+ }
1134
+ function sanitizeLoopHashValue(value) {
1135
+ if (value === null || value === undefined) {
1136
+ return value;
1137
+ }
1138
+ if (Array.isArray(value)) {
1139
+ return value.map((entry) => sanitizeLoopHashValue(entry));
1140
+ }
1141
+ if (typeof value !== 'object') {
1142
+ return value;
1143
+ }
1144
+ const record = value;
1145
+ const normalized = {};
1146
+ const volatileKeys = new Set([
1147
+ 'id',
1148
+ 'created',
1149
+ 'created_at',
1150
+ 'timestamp',
1151
+ 'request_id',
1152
+ 'requestId',
1153
+ 'trace_id',
1154
+ 'response_id',
1155
+ 'system_fingerprint'
1156
+ ]);
1157
+ for (const key of Object.keys(record)) {
1158
+ if (volatileKeys.has(key)) {
1159
+ continue;
1160
+ }
1161
+ normalized[key] = sanitizeLoopHashValue(record[key]);
1162
+ }
1163
+ return normalized;
1164
+ }
1165
+ function appendStopMessageLoopWarning(payload, repeatCountRaw) {
1166
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
1167
+ return;
1168
+ }
1169
+ const messages = Array.isArray(payload.messages)
1170
+ ? payload.messages
1171
+ : null;
1172
+ if (!messages) {
1173
+ return;
1174
+ }
1175
+ const repeatCount = Number.isFinite(repeatCountRaw)
1176
+ ? Math.max(STOP_MESSAGE_LOOP_WARN_THRESHOLD, Math.floor(repeatCountRaw))
1177
+ : STOP_MESSAGE_LOOP_WARN_THRESHOLD;
1178
+ const warningText = [
1179
+ `检测到 stopMessage 请求/响应参数已连续 ${repeatCount} 轮一致。`,
1180
+ '请立即尝试跳出循环(换路径、换验证方法、或直接给结论)。',
1181
+ `若继续达到 ${STOP_MESSAGE_LOOP_FAIL_THRESHOLD} 轮一致,将返回 fetch failed 网络错误并停止自动续跑。`
1182
+ ].join('\n');
1183
+ messages.push({
1184
+ role: 'system',
1185
+ content: warningText
1186
+ });
1187
+ }
842
1188
  function stableStringify(value) {
843
1189
  if (value === null || typeof value !== 'object') {
844
1190
  return JSON.stringify(value);
@@ -57,19 +57,19 @@ function buildClockToolSchema() {
57
57
  type: 'function',
58
58
  function: {
59
59
  name: 'clock',
60
- description: 'Time + Alarm for this session. Use get/schedule/list/cancel/clear. Scheduled reminders will be injected into future requests.',
60
+ description: 'Time + Alarm for this session. Use get/schedule/update/list/cancel/clear. Scheduled reminders will be injected into future requests.',
61
61
  strict: true,
62
62
  parameters: {
63
63
  type: 'object',
64
64
  properties: {
65
65
  action: {
66
66
  type: 'string',
67
- enum: ['get', 'schedule', 'list', 'cancel', 'clear'],
68
- description: 'Get current time, or schedule/list/cancel/clear session-scoped reminders.'
67
+ enum: ['get', 'schedule', 'update', 'list', 'cancel', 'clear'],
68
+ description: 'Get current time, or schedule/update/list/cancel/clear session-scoped reminders.'
69
69
  },
70
70
  items: {
71
71
  type: 'array',
72
- description: 'For schedule: list of reminders to add.',
72
+ description: 'For schedule/update: list of reminders (update uses items[0]).',
73
73
  items: {
74
74
  type: 'object',
75
75
  properties: {
@@ -96,7 +96,7 @@ function buildClockToolSchema() {
96
96
  },
97
97
  taskId: {
98
98
  type: 'string',
99
- description: 'For cancel: taskId to remove.'
99
+ description: 'For cancel/update: taskId to target.'
100
100
  }
101
101
  },
102
102
  required: ['action', 'items', 'taskId'],
@@ -198,4 +198,4 @@ const handler = async (ctx) => {
198
198
  })
199
199
  };
200
200
  };
201
- registerServerToolHandler(FLOW_ID, handler, { trigger: 'auto' });
201
+ registerServerToolHandler(FLOW_ID, handler, { trigger: 'auto', hook: { phase: 'default', priority: 30 } });