@jsonstudio/llms 0.6.1164 → 0.6.1354

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 (164) hide show
  1. package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
  2. package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
  3. package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
  4. package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
  5. package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
  6. package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
  7. package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
  8. package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
  9. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  10. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  11. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
  12. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  13. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  14. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  15. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  42. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  43. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  47. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  52. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  53. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  54. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  57. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  58. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  59. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  61. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  62. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  63. package/dist/conversion/hub/process/chat-process.js +256 -16
  64. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  65. package/dist/conversion/hub/response/provider-response.js +85 -27
  66. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  67. package/dist/conversion/hub/response/response-mappers.js +30 -6
  68. package/dist/conversion/hub/response/response-runtime.js +4 -38
  69. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  70. package/dist/conversion/hub/standardized-bridge.js +23 -15
  71. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  72. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  73. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  74. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  75. package/dist/conversion/shared/jsonish.js +1 -1
  76. package/dist/conversion/shared/mcp-injection.js +67 -33
  77. package/dist/conversion/shared/openai-finalizer.js +2 -1
  78. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  79. package/dist/conversion/shared/responses-output-builder.js +6 -0
  80. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  81. package/dist/conversion/shared/runtime-metadata.js +23 -0
  82. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  83. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  84. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  85. package/dist/conversion/shared/tool-governor.js +3 -3
  86. package/dist/filters/engine.js +5 -5
  87. package/dist/filters/special/request-tool-list-filter.js +194 -60
  88. package/dist/filters/special/request-tools-normalize.js +1 -1
  89. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  90. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  91. package/dist/filters/special/tool-filter-hooks.js +58 -62
  92. package/dist/guidance/index.js +5 -1
  93. package/dist/http/sse-response.js +6 -6
  94. package/dist/router/virtual-router/bootstrap.js +65 -5
  95. package/dist/router/virtual-router/context-advisor.d.ts +4 -0
  96. package/dist/router/virtual-router/context-advisor.js +3 -0
  97. package/dist/router/virtual-router/context-weighted.d.ts +31 -0
  98. package/dist/router/virtual-router/context-weighted.js +54 -0
  99. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  100. package/dist/router/virtual-router/engine-health.js +11 -110
  101. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
  102. package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
  103. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  104. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  105. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  106. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  107. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  108. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  109. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  110. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  111. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  112. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  113. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  114. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  115. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  116. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  117. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  118. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  119. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  120. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  121. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  122. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
  123. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  124. package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
  125. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  126. package/dist/router/virtual-router/engine-selection.js +10 -815
  127. package/dist/router/virtual-router/engine.d.ts +1 -0
  128. package/dist/router/virtual-router/engine.js +55 -10
  129. package/dist/router/virtual-router/routing-instructions.js +6 -1
  130. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  131. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  132. package/dist/router/virtual-router/types.d.ts +53 -1
  133. package/dist/servertool/clock/config.d.ts +8 -0
  134. package/dist/servertool/clock/config.js +22 -0
  135. package/dist/servertool/clock/log.d.ts +3 -0
  136. package/dist/servertool/clock/log.js +13 -0
  137. package/dist/servertool/clock/task-store.d.ts +1 -1
  138. package/dist/servertool/clock/task-store.js +1 -1
  139. package/dist/servertool/clock/tasks.js +1 -1
  140. package/dist/servertool/engine.js +146 -21
  141. package/dist/servertool/handlers/clock-auto.js +11 -6
  142. package/dist/servertool/handlers/clock.js +36 -10
  143. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  144. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  145. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  146. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  147. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  148. package/dist/servertool/handlers/vision.js +4 -1
  149. package/dist/servertool/handlers/web-search.js +3 -1
  150. package/dist/servertool/pending-session.d.ts +19 -0
  151. package/dist/servertool/pending-session.js +97 -0
  152. package/dist/servertool/reenter-backend.js +5 -3
  153. package/dist/servertool/server-side-tools.js +235 -6
  154. package/dist/servertool/types.d.ts +13 -0
  155. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  156. package/dist/sse/shared/chat-serializer.js +2 -2
  157. package/dist/sse/shared/constants.js +1 -1
  158. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  159. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  160. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  161. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  162. package/dist/tools/exec-command/normalize.js +4 -0
  163. package/dist/tools/exec-command/regression-capturer.js +1 -1
  164. package/package.json +10 -5
@@ -1,6 +1,8 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { extractCapturedChatSeed } from './followup-request-builder.js';
3
- import { findNextUndeliveredDueAtMs, listClockTasks, normalizeClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
3
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
+ import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
5
+ import { logClock } from '../clock/log.js';
4
6
  const FLOW_ID = 'clock_hold_flow';
5
7
  function resolveClientConnectionState(value) {
6
8
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -96,15 +98,17 @@ const handler = async (ctx) => {
96
98
  (typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
97
99
  return null;
98
100
  }
99
- const clockConfig = normalizeClockConfig(record.clock);
100
- if (!clockConfig) {
101
- return null;
102
- }
103
- await startClockDaemonIfNeeded(clockConfig);
101
+ const rt = readRuntimeMetadata(ctx.adapterContext);
104
102
  const sessionId = resolveSessionId(ctx.adapterContext);
105
103
  if (!sessionId) {
106
104
  return null;
107
105
  }
106
+ // Default-enable clock when config is absent, but keep "explicitly disabled" honored.
107
+ const clockConfig = resolveClockConfig(rt?.clock);
108
+ if (!clockConfig) {
109
+ return null;
110
+ }
111
+ await startClockDaemonIfNeeded(clockConfig);
108
112
  const seed = extractCapturedChatSeed(record.capturedChatRequest);
109
113
  if (!seed) {
110
114
  return null;
@@ -117,6 +121,7 @@ const handler = async (ctx) => {
117
121
  }
118
122
  // Wait until the "due window" is reached (now >= dueAt - dueWindowMs).
119
123
  const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
124
+ logClock('hold_start', { sessionId, nextDueAtMs, thresholdMs });
120
125
  while (Date.now() < thresholdMs) {
121
126
  const state = resolveClientConnectionState(ctx.adapterContext.clientConnectionState);
122
127
  if (state?.disconnected === true) {
@@ -1,7 +1,9 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { cloneJson } from '../server-side-tools.js';
3
3
  import { extractCapturedChatSeed } from './followup-request-builder.js';
4
- import { cancelClockTask, clearClockTasks, listClockTasks, normalizeClockConfig, parseDueAtMs, scheduleClockTasks, startClockDaemonIfNeeded } from '../clock/task-store.js';
4
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
5
+ import { cancelClockTask, clearClockTasks, listClockTasks, resolveClockConfig, parseDueAtMs, scheduleClockTasks, startClockDaemonIfNeeded } from '../clock/task-store.js';
6
+ import { logClock } from '../clock/log.js';
5
7
  const FLOW_ID = 'clock_flow';
6
8
  function parseToolArguments(toolCall) {
7
9
  if (!toolCall.arguments || typeof toolCall.arguments !== 'string') {
@@ -90,9 +92,22 @@ function normalizeScheduleItems(parsed) {
90
92
  return { ok: false, items: [], message: 'clock.schedule task must be a non-empty string' };
91
93
  }
92
94
  const tool = typeof rec.tool === 'string' && rec.tool.trim().length ? rec.tool.trim() : undefined;
93
- const argsObj = rec.arguments && typeof rec.arguments === 'object' && !Array.isArray(rec.arguments)
94
- ? cloneJson(rec.arguments)
95
- : undefined;
95
+ const argsObj = (() => {
96
+ const raw = rec.arguments;
97
+ if (raw && typeof raw === 'object' && !Array.isArray(raw)) {
98
+ return cloneJson(raw);
99
+ }
100
+ if (typeof raw === 'string' && raw.trim().length) {
101
+ try {
102
+ const parsed = JSON.parse(raw);
103
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
104
+ }
105
+ catch {
106
+ return undefined;
107
+ }
108
+ }
109
+ return undefined;
110
+ })();
96
111
  items.push({
97
112
  dueAtMs,
98
113
  task,
@@ -118,8 +133,11 @@ const handler = async (ctx) => {
118
133
  if (!toolCall || toolCall.name !== 'clock') {
119
134
  return null;
120
135
  }
121
- const clockConfig = normalizeClockConfig(ctx.adapterContext.clock);
136
+ const rt = readRuntimeMetadata(ctx.adapterContext);
122
137
  const sessionId = resolveSessionId(ctx.adapterContext);
138
+ const rawConfig = rt?.clock ?? ctx.adapterContext.clock;
139
+ // Default-enable clock when config is absent, but keep "explicitly disabled" honored.
140
+ const clockConfig = resolveClockConfig(rawConfig);
123
141
  const parsedArgs = parseToolArguments(toolCall);
124
142
  const action = normalizeAction(parsedArgs.action);
125
143
  const respond = (payload) => {
@@ -152,42 +170,50 @@ const handler = async (ctx) => {
152
170
  }
153
171
  };
154
172
  };
155
- if (!clockConfig) {
173
+ if (!sessionId) {
174
+ logClock('missing_session', { action });
156
175
  return respond({
157
176
  ok: false,
158
177
  action,
159
- message: 'clock tool is not enabled (virtualrouter.clock.enabled=true required).'
178
+ message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
160
179
  });
161
180
  }
162
- await startClockDaemonIfNeeded(clockConfig);
163
- if (!sessionId) {
181
+ if (!clockConfig) {
182
+ logClock('disabled', { action, hasSessionId: true });
164
183
  return respond({
165
184
  ok: false,
166
185
  action,
167
- message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
186
+ message: 'clock tool is not enabled (virtualrouter.clock.enabled=true required).'
168
187
  });
169
188
  }
189
+ await startClockDaemonIfNeeded(clockConfig);
170
190
  if (action === 'list') {
171
191
  const items = await listClockTasks(sessionId, clockConfig);
192
+ logClock('list', { sessionId, count: items.length });
172
193
  return respond({ ok: true, action, items: items.map(mapTaskForTool) });
173
194
  }
174
195
  if (action === 'clear') {
175
196
  const removedCount = await clearClockTasks(sessionId, clockConfig);
197
+ logClock('clear', { sessionId, removedCount });
176
198
  return respond({ ok: true, action, removedCount });
177
199
  }
178
200
  if (action === 'cancel') {
179
201
  const taskId = typeof parsedArgs.taskId === 'string' ? parsedArgs.taskId.trim() : '';
180
202
  if (!taskId) {
203
+ logClock('cancel_invalid', { sessionId, action });
181
204
  return respond({ ok: false, action, message: 'clock.cancel requires taskId' });
182
205
  }
183
206
  const removed = await cancelClockTask(sessionId, taskId, clockConfig);
207
+ logClock('cancel', { sessionId, taskId, removed });
184
208
  return respond({ ok: true, action, removed: removed ? taskId : null });
185
209
  }
186
210
  const normalized = normalizeScheduleItems(parsedArgs);
187
211
  if (!normalized.ok) {
212
+ logClock('schedule_invalid', { sessionId, message: normalized.message ?? 'invalid schedule items' });
188
213
  return respond({ ok: false, action, message: normalized.message ?? 'invalid schedule items' });
189
214
  }
190
215
  const scheduled = await scheduleClockTasks(sessionId, normalized.items, clockConfig);
216
+ logClock('schedule', { sessionId, count: scheduled.length });
191
217
  return respond({
192
218
  ok: true,
193
219
  action,
@@ -269,12 +269,18 @@ export function buildServerToolFollowupChatPayloadFromInjection(args) {
269
269
  return null;
270
270
  }
271
271
  let messages = Array.isArray(seed.messages) ? cloneJson(seed.messages) : [];
272
- let tools = seed.tools ? cloneJson(seed.tools) : undefined;
273
- const parameters = seed.parameters ? cloneJson(seed.parameters) : undefined;
274
272
  const ops = Array.isArray(args.injection?.ops) ? args.injection.ops : [];
273
+ // Followup is a normal request hop: inherit tool schema from the captured request and
274
+ // let compat/tool-governance apply standard sanitization rules.
275
+ let tools = Array.isArray(seed.tools) ? cloneJson(seed.tools) : undefined;
276
+ const parameters = seed.parameters ? cloneJson(seed.parameters) : undefined;
275
277
  for (const op of ops) {
276
278
  if (!op || typeof op !== 'object')
277
279
  continue;
280
+ if (op.op === 'preserve_tools') {
281
+ // No-op: tools are preserved by default. Kept for backward compatibility.
282
+ continue;
283
+ }
278
284
  if (op.op === 'trim_openai_messages') {
279
285
  const maxNonSystemMessages = typeof op.maxNonSystemMessages === 'number'
280
286
  ? op.maxNonSystemMessages
@@ -1,6 +1,7 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { isCompactionRequest } from './compaction-detect.js';
3
3
  import { extractCapturedChatSeed } from './followup-request-builder.js';
4
+ import { ensureRuntimeMetadata, readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
5
  const FLOW_ID = 'gemini_empty_reply_continue';
5
6
  const handler = async (ctx) => {
6
7
  if (!ctx.capabilities.reenterPipeline) {
@@ -8,11 +9,12 @@ const handler = async (ctx) => {
8
9
  }
9
10
  // 避免在 followup 请求里再次触发,防止循环。
10
11
  const adapterRecord = ctx.adapterContext;
11
- const followupRaw = adapterRecord.serverToolFollowup;
12
+ const rt = readRuntimeMetadata(ctx.adapterContext);
13
+ const followupRaw = rt?.serverToolFollowup;
12
14
  if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
13
15
  return null;
14
16
  }
15
- if (hasCompactionFlag(adapterRecord)) {
17
+ if (hasCompactionFlag(rt)) {
16
18
  return null;
17
19
  }
18
20
  // 仅针对 gemini-chat 协议 + antigravity.* providerKey 的 /v1/responses 路径启用。
@@ -40,7 +42,7 @@ const handler = async (ctx) => {
40
42
  return null;
41
43
  }
42
44
  // 统计连续空回复次数,超过上限后不再自动续写,而是返回一个可重试错误。
43
- const previousCountRaw = adapterRecord.geminiEmptyReplyCount;
45
+ const previousCountRaw = rt?.geminiEmptyReplyCount;
44
46
  const previousCount = typeof previousCountRaw === 'number' && Number.isFinite(previousCountRaw) && previousCountRaw >= 0
45
47
  ? previousCountRaw
46
48
  : 0;
@@ -90,13 +92,17 @@ const handler = async (ctx) => {
90
92
  entryEndpoint: ctx.entryEndpoint,
91
93
  injection: {
92
94
  ops: [
95
+ { op: 'trim_openai_messages', maxNonSystemMessages: 16 },
93
96
  { op: 'append_assistant_message', required: false },
94
- { op: 'append_user_text', text: '继续执行' }
97
+ { op: 'append_user_text', text: 'continue' }
95
98
  ]
96
99
  },
97
- metadata: {
98
- geminiEmptyReplyCount: nextCount
99
- }
100
+ metadata: (() => {
101
+ const meta = {};
102
+ const runtime = ensureRuntimeMetadata(meta);
103
+ runtime.geminiEmptyReplyCount = nextCount;
104
+ return meta;
105
+ })()
100
106
  }
101
107
  }
102
108
  })
@@ -250,8 +256,8 @@ function getCapturedRequest(adapterContext) {
250
256
  }
251
257
  return captured;
252
258
  }
253
- function hasCompactionFlag(record) {
254
- const flag = record.compactionRequest;
259
+ function hasCompactionFlag(rt) {
260
+ const flag = rt && typeof rt === 'object' && !Array.isArray(rt) ? rt.compactionRequest : undefined;
255
261
  if (flag === true) {
256
262
  return true;
257
263
  }
@@ -1,18 +1,20 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
2
  import { isCompactionRequest } from './compaction-detect.js';
3
3
  import { extractCapturedChatSeed } from './followup-request-builder.js';
4
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
5
  const FLOW_ID = 'iflow_model_error_retry';
5
6
  const handler = async (ctx) => {
6
7
  if (!ctx.capabilities.reenterPipeline) {
7
8
  return null;
8
9
  }
9
10
  const adapterRecord = ctx.adapterContext;
11
+ const rt = readRuntimeMetadata(ctx.adapterContext);
10
12
  // 避免在 followup 请求里再次触发,防止循环。
11
- const followupRaw = adapterRecord.serverToolFollowup;
13
+ const followupRaw = rt?.serverToolFollowup;
12
14
  if (followupRaw === true || (typeof followupRaw === 'string' && followupRaw.trim().toLowerCase() === 'true')) {
13
15
  return null;
14
16
  }
15
- if (hasCompactionFlag(adapterRecord)) {
17
+ if (hasCompactionFlag(rt)) {
16
18
  return null;
17
19
  }
18
20
  // 仅针对 openai-chat 协议 + iflow.* providerKey 的 /v1/responses 路径启用。
@@ -78,8 +80,8 @@ function getCapturedRequest(adapterContext) {
78
80
  }
79
81
  return captured;
80
82
  }
81
- function hasCompactionFlag(record) {
82
- const flag = record.compactionRequest;
83
+ function hasCompactionFlag(rt) {
84
+ const flag = rt && typeof rt === 'object' && !Array.isArray(rt) ? rt.compactionRequest : undefined;
83
85
  if (flag === true) {
84
86
  return true;
85
87
  }
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { registerServerToolHandler } from '../registry.js';
3
3
  import { cloneJson } from '../server-side-tools.js';
4
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
4
5
  import { extractCapturedChatSeed } from './followup-request-builder.js';
5
6
  const FLOW_ID = 'recursive_detection_guard';
6
7
  const CONSECUTIVE_TRIGGER_COUNT = 10;
@@ -28,11 +29,12 @@ function getRecursiveDetectionConfig() {
28
29
  }
29
30
  function shouldSkipFollowup(adapterContext) {
30
31
  const record = adapterContext;
31
- const loopState = record ? record.serverToolLoopState : undefined;
32
+ const rt = readRuntimeMetadata(record ?? undefined);
33
+ const loopState = rt ? rt.serverToolLoopState : undefined;
32
34
  if (loopState && typeof loopState === 'object' && !Array.isArray(loopState)) {
33
35
  return true;
34
36
  }
35
- const raw = record ? record.serverToolFollowup : undefined;
37
+ const raw = rt ? rt.serverToolFollowup : undefined;
36
38
  if (raw === true) {
37
39
  return true;
38
40
  }
@@ -2,7 +2,9 @@ import { registerServerToolHandler } from '../registry.js';
2
2
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
3
3
  import { isCompactionRequest } from './compaction-detect.js';
4
4
  import { extractCapturedChatSeed } from './followup-request-builder.js';
5
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
5
6
  const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
7
+ const STOPMESSAGE_IMPLICIT_GEMINI = (process.env.ROUTECODEX_STOPMESSAGE_IMPLICIT_GEMINI || '').trim() === '1';
6
8
  function debugLog(message, extra) {
7
9
  if (!STOPMESSAGE_DEBUG) {
8
10
  return;
@@ -20,17 +22,26 @@ function debugLog(message, extra) {
20
22
  const FLOW_ID = 'stop_message_flow';
21
23
  const handler = async (ctx) => {
22
24
  const record = ctx.adapterContext;
25
+ const rt = readRuntimeMetadata(ctx.adapterContext);
23
26
  debugLog('handler_start', {
24
27
  requestId: record.requestId,
25
28
  providerProtocol: record.providerProtocol
26
29
  });
27
- const followupFlagRaw = record.serverToolFollowup;
30
+ const followupFlagRaw = rt?.serverToolFollowup;
28
31
  if (followupFlagRaw === true ||
29
32
  (typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
30
- debugLog('skip_followup_loop');
31
- return null;
33
+ // Allow chained followups only within the stop_message_flow loop itself.
34
+ // Other servertool followups must not re-trigger stopMessage (prevents cross-flow loops).
35
+ const loopState = rt?.serverToolLoopState;
36
+ const flowId = loopState && typeof loopState === 'object' && !Array.isArray(loopState)
37
+ ? String(loopState.flowId || '').trim()
38
+ : '';
39
+ if (flowId !== FLOW_ID) {
40
+ debugLog('skip_followup_loop');
41
+ return null;
42
+ }
32
43
  }
33
- if (hasCompactionFlag(record)) {
44
+ if (hasCompactionFlag(rt)) {
34
45
  debugLog('skip_compaction_flag');
35
46
  return null;
36
47
  }
@@ -51,13 +62,28 @@ const handler = async (ctx) => {
51
62
  return null;
52
63
  }
53
64
  let state = loadRoutingInstructionStateSync(stickyKey);
65
+ // If stopMessage was created implicitly (auto) but implicit mode is disabled, do not run it.
66
+ // This avoids surprising followups like "继续执行" when the user never enabled stopMessage.
67
+ if (state &&
68
+ typeof state.stopMessageSource === 'string' &&
69
+ state.stopMessageSource.trim().toLowerCase() === 'auto' &&
70
+ !STOPMESSAGE_IMPLICIT_GEMINI) {
71
+ state.stopMessageText = undefined;
72
+ state.stopMessageMaxRepeats = undefined;
73
+ state.stopMessageUsed = undefined;
74
+ state.stopMessageUpdatedAt = undefined;
75
+ state.stopMessageLastUsedAt = undefined;
76
+ saveRoutingInstructionStateAsync(stickyKey, state);
77
+ debugLog('skip_auto_state_disabled', { stickyKey });
78
+ return null;
79
+ }
54
80
  if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
55
- const fallback = resolveStopMessageSnapshot(record.stopMessageState);
81
+ const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
56
82
  if (fallback) {
57
83
  state = createStopMessageState(fallback);
58
84
  }
59
85
  else {
60
- const implicit = resolveImplicitGeminiStopMessageSnapshot(ctx, record);
86
+ const implicit = STOPMESSAGE_IMPLICIT_GEMINI ? resolveImplicitGeminiStopMessageSnapshot(ctx, record) : null;
61
87
  if (!implicit) {
62
88
  debugLog('skip_no_state', { stickyKey });
63
89
  return null;
@@ -87,11 +113,15 @@ const handler = async (ctx) => {
87
113
  used,
88
114
  maxRepeats
89
115
  });
116
+ // Auto-clear after reaching max repeats to avoid leaving an "exhausted" stopMessage stuck in sticky state.
117
+ const now = Date.now();
90
118
  state.stopMessageText = undefined;
91
119
  state.stopMessageMaxRepeats = undefined;
92
120
  state.stopMessageUsed = undefined;
93
- state.stopMessageUpdatedAt = undefined;
94
- state.stopMessageLastUsedAt = undefined;
121
+ state.stopMessageSource = undefined;
122
+ // Keep monotonic timestamps as a tombstone to prevent accidental re-application from replayed history.
123
+ state.stopMessageUpdatedAt = now;
124
+ state.stopMessageLastUsedAt = now;
95
125
  saveRoutingInstructionStateAsync(stickyKey, state);
96
126
  return null;
97
127
  }
@@ -131,6 +161,7 @@ const handler = async (ctx) => {
131
161
  injection: {
132
162
  ops: [
133
163
  { op: 'append_assistant_message', required: false },
164
+ { op: 'preserve_tools' },
134
165
  { op: 'append_user_text', text }
135
166
  ]
136
167
  },
@@ -341,8 +372,8 @@ function resolveStopMessageSnapshot(raw) {
341
372
  ...(lastUsedAt ? { lastUsedAt } : {})
342
373
  };
343
374
  }
344
- function hasCompactionFlag(record) {
345
- const flag = record.compactionRequest;
375
+ function hasCompactionFlag(rt) {
376
+ const flag = rt && typeof rt === 'object' && !Array.isArray(rt) ? rt.compactionRequest : undefined;
346
377
  if (flag === true) {
347
378
  return true;
348
379
  }
@@ -376,6 +407,12 @@ function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
376
407
  if (!isStopFinishReason(ctx.base)) {
377
408
  return null;
378
409
  }
410
+ // 仅在“空回复”时触发隐式 stopMessage:
411
+ // - 这个场景由 gemini_empty_reply_continue 专门处理;
412
+ // - stop_message_auto 里的隐式逻辑只作为兼容兜底(且默认关闭),避免对正常 stop 响应追加“继续执行”。
413
+ if (!isEmptyAssistantReply(ctx.base)) {
414
+ return null;
415
+ }
379
416
  return {
380
417
  text: '继续执行',
381
418
  maxRepeats: 1,
@@ -387,6 +424,59 @@ function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
387
424
  return null;
388
425
  }
389
426
  }
427
+ function isEmptyAssistantReply(base) {
428
+ if (!base || typeof base !== 'object' || Array.isArray(base)) {
429
+ return false;
430
+ }
431
+ const payload = base;
432
+ const choicesRaw = payload.choices;
433
+ if (Array.isArray(choicesRaw) && choicesRaw.length) {
434
+ const first = choicesRaw[0];
435
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
436
+ return false;
437
+ }
438
+ const finishReasonRaw = first.finish_reason;
439
+ const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
440
+ ? finishReasonRaw.trim().toLowerCase()
441
+ : '';
442
+ // 仅接受 stop:length 截断通常并非“空回复”,而是需要续写(由 gemini_empty_reply_continue 负责)。
443
+ if (finishReason !== 'stop') {
444
+ return false;
445
+ }
446
+ const message = first.message &&
447
+ typeof first.message === 'object' &&
448
+ !Array.isArray(first.message)
449
+ ? first.message
450
+ : null;
451
+ if (!message) {
452
+ return false;
453
+ }
454
+ const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
455
+ if (toolCalls.length > 0) {
456
+ return false;
457
+ }
458
+ const contentRaw = message.content;
459
+ const text = typeof contentRaw === 'string' ? contentRaw.trim() : '';
460
+ return text.length === 0;
461
+ }
462
+ // OpenAI Responses shape: treat empty output_text + no tool-like output as empty reply.
463
+ const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
464
+ if (statusRaw && statusRaw !== 'completed') {
465
+ return false;
466
+ }
467
+ if (payload.required_action && typeof payload.required_action === 'object') {
468
+ return false;
469
+ }
470
+ const outputText = extractResponsesOutputText(payload);
471
+ if (outputText.length > 0) {
472
+ return false;
473
+ }
474
+ const outputRaw = Array.isArray(payload.output) ? payload.output : [];
475
+ if (outputRaw.some((item) => hasToolLikeOutput(item))) {
476
+ return false;
477
+ }
478
+ return true;
479
+ }
390
480
  function createStopMessageState(snapshot) {
391
481
  return {
392
482
  forcedTarget: undefined,
@@ -2,6 +2,7 @@ import { registerServerToolHandler } from '../registry.js';
2
2
  import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
3
3
  import { extractCapturedChatSeed } from './followup-request-builder.js';
4
4
  import { reenterServerToolBackend } from '../reenter-backend.js';
5
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
5
6
  const FLOW_ID = 'vision_flow';
6
7
  const handler = async (ctx) => {
7
8
  if (!ctx.capabilities.reenterPipeline) {
@@ -84,7 +85,9 @@ export async function executeVisionBackendPlan(args) {
84
85
  }
85
86
  function shouldRunVisionFlow(ctx) {
86
87
  const record = ctx.adapterContext;
87
- const followupFlag = record.serverToolFollowup === true || record.serverToolFollowup === 'true';
88
+ const rt = readRuntimeMetadata(record);
89
+ const followupRaw = rt?.serverToolFollowup;
90
+ const followupFlag = followupRaw === true || followupRaw === 'true';
88
91
  if (followupFlag) {
89
92
  return false;
90
93
  }
@@ -3,6 +3,7 @@ import { registerServerToolHandler } from '../registry.js';
3
3
  import { cloneJson, extractTextFromChatLike } from '../server-side-tools.js';
4
4
  import { extractCapturedChatSeed } from './followup-request-builder.js';
5
5
  import { reenterServerToolBackend } from '../reenter-backend.js';
6
+ import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
6
7
  const FLOW_ID = 'web_search_flow';
7
8
  const handler = async (ctx) => {
8
9
  const toolCall = ctx.toolCall;
@@ -98,7 +99,8 @@ function parseToolArguments(toolCall) {
98
99
  }
99
100
  }
100
101
  function getWebSearchConfig(ctx) {
101
- const raw = ctx && typeof ctx === 'object' ? ctx.webSearch : undefined;
102
+ const rt = readRuntimeMetadata(ctx);
103
+ const raw = rt ? rt.webSearch : undefined;
102
104
  const record = raw && typeof raw === 'object' && !Array.isArray(raw) ? raw : null;
103
105
  if (!record)
104
106
  return undefined;
@@ -0,0 +1,19 @@
1
+ import type { JsonObject } from '../conversion/hub/types/json.js';
2
+ export interface PendingServerToolInjection {
3
+ version: 1;
4
+ sessionId: string;
5
+ createdAtMs: number;
6
+ /**
7
+ * Client tool_call ids that must appear in the next request's tool messages
8
+ * before we apply the pending servertool injection.
9
+ */
10
+ afterToolCallIds: string[];
11
+ /**
12
+ * Chat messages to inject (assistant tool_call message + tool result messages).
13
+ */
14
+ messages: JsonObject[];
15
+ sourceRequestId?: string;
16
+ }
17
+ export declare function savePendingServerToolInjection(sessionId: string, pending: Omit<PendingServerToolInjection, 'version' | 'sessionId'>): Promise<void>;
18
+ export declare function loadPendingServerToolInjection(sessionId: string): Promise<PendingServerToolInjection | null>;
19
+ export declare function clearPendingServerToolInjection(sessionId: string): Promise<void>;
@@ -0,0 +1,97 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readJsonFile, writeJsonFileAtomic } from './clock/io.js';
4
+ function readSessionDirEnv() {
5
+ return String(process.env.ROUTECODEX_SESSION_DIR || '').trim();
6
+ }
7
+ function sanitizeSegment(value) {
8
+ return String(value || '')
9
+ .trim()
10
+ .replace(/[^a-zA-Z0-9_.-]/g, '_')
11
+ .replace(/_+/g, '_')
12
+ .replace(/^_+|_+$/g, '');
13
+ }
14
+ function resolvePendingDir(sessionDir) {
15
+ return path.join(sessionDir, 'servertool-pending');
16
+ }
17
+ function resolvePendingFile(sessionDir, sessionId) {
18
+ const safe = sanitizeSegment(sessionId);
19
+ if (!safe)
20
+ return null;
21
+ return path.join(resolvePendingDir(sessionDir), `${safe}.json`);
22
+ }
23
+ function coercePending(value) {
24
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
25
+ return null;
26
+ }
27
+ const rec = value;
28
+ const sessionId = typeof rec.sessionId === 'string' ? rec.sessionId.trim() : '';
29
+ const createdAtMs = typeof rec.createdAtMs === 'number' && Number.isFinite(rec.createdAtMs) ? Math.floor(rec.createdAtMs) : 0;
30
+ const afterToolCallIds = Array.isArray(rec.afterToolCallIds)
31
+ ? rec.afterToolCallIds.filter((x) => typeof x === 'string' && x.trim().length).map((x) => String(x).trim())
32
+ : [];
33
+ const messages = Array.isArray(rec.messages)
34
+ ? rec.messages.filter((m) => m && typeof m === 'object' && !Array.isArray(m))
35
+ : [];
36
+ if (!sessionId || !createdAtMs || !afterToolCallIds.length || !messages.length) {
37
+ return null;
38
+ }
39
+ const sourceRequestId = typeof rec.sourceRequestId === 'string' && rec.sourceRequestId.trim().length
40
+ ? rec.sourceRequestId.trim()
41
+ : undefined;
42
+ return {
43
+ version: 1,
44
+ sessionId,
45
+ createdAtMs,
46
+ afterToolCallIds,
47
+ messages,
48
+ ...(sourceRequestId ? { sourceRequestId } : {})
49
+ };
50
+ }
51
+ export async function savePendingServerToolInjection(sessionId, pending) {
52
+ const base = readSessionDirEnv();
53
+ if (!base)
54
+ return;
55
+ const file = resolvePendingFile(base, sessionId);
56
+ if (!file)
57
+ return;
58
+ await fs.mkdir(path.dirname(file), { recursive: true });
59
+ const payload = {
60
+ version: 1,
61
+ sessionId,
62
+ createdAtMs: pending.createdAtMs,
63
+ afterToolCallIds: pending.afterToolCallIds,
64
+ messages: pending.messages,
65
+ ...(pending.sourceRequestId ? { sourceRequestId: pending.sourceRequestId } : {})
66
+ };
67
+ await writeJsonFileAtomic(file, payload);
68
+ }
69
+ export async function loadPendingServerToolInjection(sessionId) {
70
+ const base = readSessionDirEnv();
71
+ if (!base)
72
+ return null;
73
+ const file = resolvePendingFile(base, sessionId);
74
+ if (!file)
75
+ return null;
76
+ try {
77
+ const raw = await readJsonFile(file);
78
+ return coercePending(raw);
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ export async function clearPendingServerToolInjection(sessionId) {
85
+ const base = readSessionDirEnv();
86
+ if (!base)
87
+ return;
88
+ const file = resolvePendingFile(base, sessionId);
89
+ if (!file)
90
+ return;
91
+ try {
92
+ await fs.rm(file, { force: true });
93
+ }
94
+ catch {
95
+ // ignore
96
+ }
97
+ }
@@ -1,14 +1,16 @@
1
+ import { ensureRuntimeMetadata } from '../conversion/shared/runtime-metadata.js';
1
2
  export async function reenterServerToolBackend(args) {
2
3
  const routeHint = typeof args.routeHint === 'string' && args.routeHint.trim().length ? args.routeHint.trim() : undefined;
3
4
  const merged = {
4
5
  providerProtocol: args.providerProtocol,
5
- serverToolFollowup: true,
6
6
  stream: false,
7
- preserveRouteHint: false,
8
- disableStickyRoutes: true,
9
7
  ...(routeHint ? { routeHint } : {}),
10
8
  ...(args.metadata ?? {})
11
9
  };
10
+ const rt = ensureRuntimeMetadata(merged);
11
+ rt.serverToolFollowup = true;
12
+ rt.preserveRouteHint = false;
13
+ rt.disableStickyRoutes = true;
12
14
  return await args.reenterPipeline({
13
15
  entryEndpoint: args.entryEndpoint,
14
16
  requestId: args.requestId,