@jsonstudio/llms 0.6.1172 → 0.6.1397

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 (167) 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/compat/profiles/chat-gemini.json +5 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
  11. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
  12. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +748 -52
  13. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
  14. package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
  15. package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
  16. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
  17. package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +142 -0
  18. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
  19. package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
  20. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
  21. package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
  22. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
  23. package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
  24. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
  25. package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +390 -0
  26. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
  27. package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
  28. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
  29. package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
  30. package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
  31. package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
  32. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
  33. package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
  34. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
  35. package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
  36. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
  37. package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
  38. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
  39. package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
  40. package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
  41. package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
  42. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
  43. package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
  44. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
  45. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
  46. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
  47. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
  48. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
  49. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
  50. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
  51. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.d.ts +10 -0
  52. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_thought_signature_inject/index.js +172 -0
  53. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
  54. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
  55. package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
  56. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
  57. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
  58. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
  59. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
  60. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.d.ts +10 -0
  61. package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js +71 -0
  62. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
  63. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
  64. package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
  65. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
  66. package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
  67. package/dist/conversion/hub/pipeline/target-utils.js +9 -5
  68. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.d.ts +14 -0
  69. package/dist/conversion/hub/pipeline/thought-signature/thought-signature-center.js +289 -0
  70. package/dist/conversion/hub/process/chat-process.js +256 -16
  71. package/dist/conversion/hub/response/provider-response.d.ts +8 -0
  72. package/dist/conversion/hub/response/provider-response.js +91 -27
  73. package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
  74. package/dist/conversion/hub/response/response-mappers.js +30 -6
  75. package/dist/conversion/hub/response/response-runtime.js +4 -38
  76. package/dist/conversion/hub/snapshot-recorder.js +5 -1
  77. package/dist/conversion/hub/standardized-bridge.js +23 -15
  78. package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
  79. package/dist/conversion/responses/responses-openai-bridge.js +20 -4
  80. package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
  81. package/dist/conversion/shared/gemini-tool-utils.js +580 -108
  82. package/dist/conversion/shared/jsonish.js +1 -1
  83. package/dist/conversion/shared/mcp-injection.js +67 -33
  84. package/dist/conversion/shared/openai-finalizer.js +2 -1
  85. package/dist/conversion/shared/openai-message-normalize.js +76 -21
  86. package/dist/conversion/shared/responses-output-builder.js +6 -0
  87. package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
  88. package/dist/conversion/shared/runtime-metadata.js +23 -0
  89. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  90. package/dist/conversion/shared/text-markup-normalizer.js +284 -4
  91. package/dist/conversion/shared/tool-canonicalizer.js +2 -1
  92. package/dist/conversion/shared/tool-governor.js +3 -3
  93. package/dist/filters/engine.js +5 -5
  94. package/dist/filters/special/request-tool-list-filter.js +194 -60
  95. package/dist/filters/special/request-tools-normalize.js +1 -1
  96. package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
  97. package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
  98. package/dist/filters/special/tool-filter-hooks.js +58 -62
  99. package/dist/guidance/index.js +5 -1
  100. package/dist/http/sse-response.js +6 -6
  101. package/dist/router/virtual-router/bootstrap.js +54 -4
  102. package/dist/router/virtual-router/engine-health.d.ts +1 -1
  103. package/dist/router/virtual-router/engine-health.js +11 -110
  104. package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +30 -0
  105. package/dist/router/virtual-router/engine-selection/alias-selection.js +237 -0
  106. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
  107. package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
  108. package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
  109. package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
  110. package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
  111. package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
  112. package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
  113. package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
  114. package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
  115. package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
  116. package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
  117. package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
  118. package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
  119. package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
  120. package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
  121. package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
  122. package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
  123. package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
  124. package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
  125. package/dist/router/virtual-router/engine-selection/tier-selection-select.js +423 -0
  126. package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
  127. package/dist/router/virtual-router/engine-selection/tier-selection.js +228 -0
  128. package/dist/router/virtual-router/engine-selection.d.ts +4 -30
  129. package/dist/router/virtual-router/engine-selection.js +10 -962
  130. package/dist/router/virtual-router/engine.d.ts +1 -0
  131. package/dist/router/virtual-router/engine.js +64 -11
  132. package/dist/router/virtual-router/routing-instructions.js +6 -1
  133. package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
  134. package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
  135. package/dist/router/virtual-router/types.d.ts +38 -1
  136. package/dist/servertool/clock/config.d.ts +8 -0
  137. package/dist/servertool/clock/config.js +22 -0
  138. package/dist/servertool/clock/log.d.ts +3 -0
  139. package/dist/servertool/clock/log.js +13 -0
  140. package/dist/servertool/clock/task-store.d.ts +1 -1
  141. package/dist/servertool/clock/task-store.js +1 -1
  142. package/dist/servertool/clock/tasks.js +1 -1
  143. package/dist/servertool/engine.js +146 -21
  144. package/dist/servertool/handlers/clock-auto.js +11 -6
  145. package/dist/servertool/handlers/clock.js +36 -10
  146. package/dist/servertool/handlers/followup-request-builder.js +8 -2
  147. package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
  148. package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
  149. package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
  150. package/dist/servertool/handlers/stop-message-auto.js +100 -10
  151. package/dist/servertool/handlers/vision.js +4 -1
  152. package/dist/servertool/handlers/web-search.js +3 -1
  153. package/dist/servertool/pending-session.d.ts +19 -0
  154. package/dist/servertool/pending-session.js +97 -0
  155. package/dist/servertool/reenter-backend.js +5 -3
  156. package/dist/servertool/server-side-tools.js +235 -6
  157. package/dist/servertool/types.d.ts +13 -0
  158. package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
  159. package/dist/sse/shared/chat-serializer.js +2 -2
  160. package/dist/sse/shared/constants.js +1 -1
  161. package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
  162. package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
  163. package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
  164. package/dist/tools/apply-patch/execution-capturer.js +1 -1
  165. package/dist/tools/exec-command/normalize.js +4 -0
  166. package/dist/tools/exec-command/regression-capturer.js +1 -1
  167. package/package.json +10 -5
@@ -1,8 +1,11 @@
1
1
  import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js';
2
2
  import { ToolGovernanceEngine } from '../tool-governance/index.js';
3
+ import { readRuntimeMetadata } from '../../shared/runtime-metadata.js';
3
4
  import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
4
5
  import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
5
- import { clearClockSession, normalizeClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
6
+ import { clearClockSession, resolveClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
7
+ import { logClock } from '../../../servertool/clock/log.js';
8
+ import { clearPendingServerToolInjection, loadPendingServerToolInjection } from '../../../servertool/pending-session.js';
6
9
  import { isJsonObject } from '../types/json.js';
7
10
  import { applyHubOperations } from '../ops/operations.js';
8
11
  const toolGovernanceEngine = new ToolGovernanceEngine();
@@ -83,6 +86,9 @@ async function applyRequestToolGovernance(request, context) {
83
86
  messages: stripHistoricalImageAttachments(merged.messages)
84
87
  };
85
88
  merged = normalizeApplyPatchToolCallsOnRequest(merged);
89
+ // Mixed-tools support: if previous response executed servertools alongside client tools,
90
+ // inject servertool tool_call + tool_result messages after the client's tool results on next request.
91
+ merged = await maybeInjectPendingServerToolResultsAfterClientTools(merged, metadata);
86
92
  if (containsImageAttachment(merged.messages)) {
87
93
  if (!merged.metadata) {
88
94
  merged.metadata = {
@@ -106,7 +112,7 @@ async function applyRequestToolGovernance(request, context) {
106
112
  // Server-side web_search tool injection (config-driven, best-effort).
107
113
  merged = applyHubOperations(merged, buildWebSearchOperations(merged, metadata));
108
114
  // Server-side clock tool + scheduled reminders injection (config-driven, best-effort).
109
- merged = applyHubOperations(merged, buildClockOperations(metadata));
115
+ merged = applyHubOperations(merged, buildClockOperations(merged, metadata));
110
116
  merged = await maybeInjectClockRemindersAndApplyDirectives(merged, metadata, context.requestId);
111
117
  const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
112
118
  if (summary.applied) {
@@ -118,7 +124,191 @@ async function applyRequestToolGovernance(request, context) {
118
124
  }
119
125
  };
120
126
  }
121
- return sanitized;
127
+ return pruneUndeclaredToolHistory(sanitized);
128
+ }
129
+ function resolveSessionIdForPending(metadata, request) {
130
+ const candidate = readString(metadata.sessionId) ?? readString(request.metadata?.sessionId);
131
+ return candidate && candidate.trim() ? candidate.trim() : null;
132
+ }
133
+ function pruneUndeclaredToolHistory(request) {
134
+ const tools = Array.isArray(request.tools) ? request.tools : [];
135
+ const allowedToolNames = new Set();
136
+ for (const tool of tools) {
137
+ const name = tool && typeof tool === 'object' && tool.function && typeof tool.function.name === 'string'
138
+ ? String(tool.function.name).trim()
139
+ : '';
140
+ if (name) {
141
+ allowedToolNames.add(name);
142
+ }
143
+ }
144
+ // If no tools are declared, do not carry any tool calls/results in history.
145
+ const hasAnyTools = allowedToolNames.size > 0;
146
+ let prunedToolCalls = 0;
147
+ let prunedToolResults = 0;
148
+ const prunedToolNames = new Set();
149
+ const keptToolCallIds = new Set();
150
+ const nextMessages = [];
151
+ for (const msg of request.messages) {
152
+ if (!msg || typeof msg !== 'object')
153
+ continue;
154
+ if (msg.role === 'assistant' && Array.isArray(msg.tool_calls) && msg.tool_calls.length) {
155
+ if (!hasAnyTools) {
156
+ prunedToolCalls += msg.tool_calls.length;
157
+ for (const tc of msg.tool_calls) {
158
+ const n = tc && tc.function && typeof tc.function.name === 'string' ? tc.function.name.trim() : '';
159
+ if (n)
160
+ prunedToolNames.add(n);
161
+ }
162
+ // Drop tool_calls when no tools are declared.
163
+ const keptContent = msg.content;
164
+ const hasContent = typeof keptContent === 'string'
165
+ ? keptContent.trim().length > 0
166
+ : Array.isArray(keptContent)
167
+ ? keptContent.length > 0
168
+ : false;
169
+ if (hasContent) {
170
+ nextMessages.push({ ...msg, tool_calls: [] });
171
+ }
172
+ continue;
173
+ }
174
+ const keptCalls = [];
175
+ for (const tc of msg.tool_calls) {
176
+ if (!tc || typeof tc !== 'object')
177
+ continue;
178
+ const name = tc.function && typeof tc.function.name === 'string' ? tc.function.name.trim() : '';
179
+ if (!name || !allowedToolNames.has(name)) {
180
+ prunedToolCalls += 1;
181
+ if (name)
182
+ prunedToolNames.add(name);
183
+ continue;
184
+ }
185
+ keptCalls.push(tc);
186
+ if (typeof tc.id === 'string' && tc.id.trim()) {
187
+ keptToolCallIds.add(tc.id.trim());
188
+ }
189
+ }
190
+ const keptContent = msg.content;
191
+ const hasContent = typeof keptContent === 'string'
192
+ ? keptContent.trim().length > 0
193
+ : Array.isArray(keptContent)
194
+ ? keptContent.length > 0
195
+ : false;
196
+ if (!hasContent && keptCalls.length === 0) {
197
+ continue;
198
+ }
199
+ if (keptCalls.length === msg.tool_calls.length) {
200
+ nextMessages.push(msg);
201
+ }
202
+ else {
203
+ nextMessages.push({ ...msg, tool_calls: keptCalls.length ? keptCalls : undefined });
204
+ }
205
+ continue;
206
+ }
207
+ if (msg.role === 'tool') {
208
+ const name = typeof msg.name === 'string' ? msg.name.trim() : '';
209
+ const toolCallId = typeof msg.tool_call_id === 'string' ? msg.tool_call_id.trim() : '';
210
+ if (!hasAnyTools) {
211
+ prunedToolResults += 1;
212
+ if (name)
213
+ prunedToolNames.add(name);
214
+ continue;
215
+ }
216
+ if (name && !allowedToolNames.has(name)) {
217
+ prunedToolResults += 1;
218
+ prunedToolNames.add(name);
219
+ continue;
220
+ }
221
+ // If the result references a tool_call_id that no longer exists, drop it to avoid
222
+ // provider-side strict pairing errors (Gemini/Anthropic tool pairing).
223
+ if (toolCallId && !keptToolCallIds.has(toolCallId)) {
224
+ prunedToolResults += 1;
225
+ if (name)
226
+ prunedToolNames.add(name);
227
+ continue;
228
+ }
229
+ nextMessages.push(msg);
230
+ continue;
231
+ }
232
+ nextMessages.push(msg);
233
+ }
234
+ if (prunedToolCalls === 0 && prunedToolResults === 0) {
235
+ return request;
236
+ }
237
+ return {
238
+ ...request,
239
+ messages: nextMessages,
240
+ metadata: {
241
+ ...request.metadata,
242
+ toolHistoryPrune: {
243
+ prunedToolCalls,
244
+ prunedToolResults,
245
+ prunedToolNames: Array.from(prunedToolNames).sort()
246
+ }
247
+ }
248
+ };
249
+ }
250
+ function extractToolCallIdsFromToolMessages(messages) {
251
+ const ids = new Set();
252
+ for (const msg of messages) {
253
+ if (!msg || typeof msg !== 'object')
254
+ continue;
255
+ if (msg.role !== 'tool')
256
+ continue;
257
+ const toolCallId = typeof msg.tool_call_id === 'string'
258
+ ? String(msg.tool_call_id).trim()
259
+ : typeof msg.toolCallId === 'string'
260
+ ? String(msg.toolCallId).trim()
261
+ : '';
262
+ if (toolCallId)
263
+ ids.add(toolCallId);
264
+ }
265
+ return ids;
266
+ }
267
+ function findLastToolMessageIndex(messages) {
268
+ for (let i = messages.length - 1; i >= 0; i -= 1) {
269
+ const msg = messages[i];
270
+ if (msg && typeof msg === 'object' && msg.role === 'tool') {
271
+ return i;
272
+ }
273
+ }
274
+ return -1;
275
+ }
276
+ async function maybeInjectPendingServerToolResultsAfterClientTools(request, metadata) {
277
+ const sessionId = resolveSessionIdForPending(metadata, request);
278
+ if (!sessionId) {
279
+ return request;
280
+ }
281
+ const pending = await loadPendingServerToolInjection(sessionId);
282
+ if (!pending) {
283
+ return request;
284
+ }
285
+ const afterIds = Array.isArray(pending.afterToolCallIds) ? pending.afterToolCallIds : [];
286
+ if (!afterIds.length) {
287
+ return request;
288
+ }
289
+ const messages = Array.isArray(request.messages) ? request.messages : [];
290
+ const toolIds = extractToolCallIdsFromToolMessages(messages);
291
+ const ready = afterIds.every((id) => toolIds.has(id));
292
+ if (!ready) {
293
+ return request;
294
+ }
295
+ const insertAt = findLastToolMessageIndex(messages);
296
+ if (insertAt < 0) {
297
+ return request;
298
+ }
299
+ const inject = Array.isArray(pending.messages) ? pending.messages : [];
300
+ if (!inject.length) {
301
+ return request;
302
+ }
303
+ const nextMessages = messages.slice();
304
+ nextMessages.splice(insertAt + 1, 0, ...inject);
305
+ try {
306
+ await clearPendingServerToolInjection(sessionId);
307
+ }
308
+ catch {
309
+ // best-effort
310
+ }
311
+ return { ...request, messages: nextMessages };
122
312
  }
123
313
  function buildProcessedRequest(request) {
124
314
  const timestamp = Date.now();
@@ -423,13 +613,38 @@ function buildToolChoiceOperations(toolChoice) {
423
613
  }
424
614
  ];
425
615
  }
616
+ function isGeminiAntigravityLike(metadata) {
617
+ const providerProtocol = readString(metadata.providerProtocol) ?? readString(metadata.provider) ?? '';
618
+ if (!providerProtocol.toLowerCase().includes('gemini')) {
619
+ return false;
620
+ }
621
+ const rt = readRuntimeMetadata(metadata);
622
+ const providerId = readString(metadata.providerId) ??
623
+ readString(rt?.providerId) ??
624
+ '';
625
+ const providerKey = readString(metadata.providerKey) ??
626
+ readString(rt?.providerKey) ??
627
+ '';
628
+ const loweredId = providerId.toLowerCase();
629
+ const loweredKey = providerKey.toLowerCase();
630
+ return (loweredId.includes('antigravity') ||
631
+ loweredId === 'gemini-cli' ||
632
+ loweredKey.startsWith('antigravity.') ||
633
+ loweredKey.startsWith('gemini-cli.'));
634
+ }
426
635
  function buildWebSearchOperations(request, metadata) {
636
+ const rt = readRuntimeMetadata(metadata);
637
+ // gcli2api-style: do not force-inject server-side web_search tools for Gemini/Antigravity.
638
+ // Tools must be present only when supplied by the client payload/config, not auto-appended.
639
+ if (isGeminiAntigravityLike(metadata)) {
640
+ return [];
641
+ }
427
642
  // ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
428
643
  // 以避免在 web_search 流程内部形成循环命中。
429
- if (metadata.serverToolFollowup === true) {
644
+ if (rt?.serverToolFollowup === true) {
430
645
  return [];
431
646
  }
432
- const rawConfig = metadata.webSearch;
647
+ const rawConfig = rt?.webSearch;
433
648
  if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
434
649
  return [];
435
650
  }
@@ -535,12 +750,20 @@ function buildWebSearchOperations(request, metadata) {
535
750
  });
536
751
  return ops;
537
752
  }
538
- function buildClockOperations(metadata) {
539
- const rawConfig = metadata.clock;
540
- const clockConfig = normalizeClockConfig(rawConfig);
753
+ function buildClockOperations(request, metadata) {
754
+ const rt = readRuntimeMetadata(metadata);
755
+ // gcli2api-style: do not force-inject server-side clock tools for Gemini/Antigravity.
756
+ if (isGeminiAntigravityLike(metadata)) {
757
+ return [];
758
+ }
759
+ const rawConfig = rt?.clock;
760
+ const clockConfig = resolveClockConfig(rawConfig);
541
761
  if (!clockConfig) {
542
762
  return [];
543
763
  }
764
+ const sessionId = readString(metadata.sessionId);
765
+ const hasSessionId = Boolean(sessionId && sessionId.trim());
766
+ logClock('inject_schema', { hasSessionId });
544
767
  const parameters = {
545
768
  type: 'object',
546
769
  properties: {
@@ -568,12 +791,13 @@ function buildClockOperations(metadata) {
568
791
  description: 'Optional suggested tool name (hint only).'
569
792
  },
570
793
  arguments: {
571
- type: 'object',
572
- description: 'Optional suggested tool arguments (hint only).',
573
- additionalProperties: true
794
+ type: 'string',
795
+ description: 'Optional suggested tool arguments as a JSON string (hint only). Use "{}" when unsure.'
574
796
  }
575
797
  },
576
- required: ['dueAt', 'task'],
798
+ // For strict tool schemas (OpenAI Responses), required must include every key in properties.
799
+ // We keep tool/arguments as hints but allow empty string when not used.
800
+ required: ['dueAt', 'task', 'tool', 'arguments'],
577
801
  additionalProperties: false
578
802
  }
579
803
  },
@@ -582,7 +806,9 @@ function buildClockOperations(metadata) {
582
806
  description: 'For cancel: taskId to remove.'
583
807
  }
584
808
  },
585
- required: ['action'],
809
+ // For strict tool schemas (OpenAI Responses), required must include every key in properties.
810
+ // Fields unused for certain actions can be set to empty string / empty list.
811
+ required: ['action', 'items', 'taskId'],
586
812
  additionalProperties: false
587
813
  };
588
814
  const clockTool = {
@@ -595,7 +821,10 @@ function buildClockOperations(metadata) {
595
821
  }
596
822
  };
597
823
  return [
598
- { op: 'set_request_metadata_fields', fields: { clockEnabled: true, serverToolRequired: true } },
824
+ {
825
+ op: 'set_request_metadata_fields',
826
+ fields: { clockEnabled: true, ...(hasSessionId ? { serverToolRequired: true } : {}) }
827
+ },
599
828
  { op: 'append_tool_if_missing', toolName: 'clock', tool: clockTool }
600
829
  ];
601
830
  }
@@ -657,8 +886,13 @@ function findLastUserMessageIndex(messages) {
657
886
  return -1;
658
887
  }
659
888
  async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, requestId) {
660
- const rawConfig = metadata.clock;
661
- const clockConfig = normalizeClockConfig(rawConfig);
889
+ const rt = readRuntimeMetadata(metadata);
890
+ // Do not inject reminders or apply clock directives during internal servertool followup hops.
891
+ if (rt?.serverToolFollowup === true) {
892
+ return request;
893
+ }
894
+ const rawConfig = rt?.clock;
895
+ const clockConfig = resolveClockConfig(rawConfig);
662
896
  if (!clockConfig) {
663
897
  return request;
664
898
  }
@@ -687,6 +921,7 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
687
921
  if (sessionId) {
688
922
  try {
689
923
  await clearClockSession(sessionId);
924
+ logClock('cleared', { sessionId });
690
925
  }
691
926
  catch {
692
927
  // best-effort: user directive should not crash request
@@ -707,6 +942,11 @@ async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, re
707
942
  if (!reservation || typeof injectText !== 'string' || !injectText.trim()) {
708
943
  return request;
709
944
  }
945
+ logClock('inject_due', {
946
+ sessionId,
947
+ reservationId: reservation.reservationId,
948
+ taskIds: Array.isArray(reservation.taskIds) ? reservation.taskIds.length : 0
949
+ });
710
950
  const baseMetadata = request.metadata && typeof request.metadata === 'object'
711
951
  ? request.metadata
712
952
  : {
@@ -10,6 +10,14 @@ export interface ProviderResponseConversionOptions {
10
10
  context: AdapterContext;
11
11
  entryEndpoint: string;
12
12
  wantsStream: boolean;
13
+ /**
14
+ * Canonical chat semantics from the request-side chat_process output.
15
+ * This is the only allowed carrier for mappable cross-protocol semantics
16
+ * (tool alias maps, client tool schemas, responses resume, etc.).
17
+ *
18
+ * Must not be stuffed into metadata/AdapterContext.
19
+ */
20
+ requestSemantics?: JsonObject;
13
21
  stageRecorder?: StageRecorder;
14
22
  providerInvoker?: ProviderInvoker;
15
23
  /**
@@ -8,12 +8,15 @@ import { OpenAIChatResponseMapper, ResponsesResponseMapper, AnthropicResponseMap
8
8
  import { runRespInboundStage1SseDecode } from '../pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js';
9
9
  import { runRespInboundStage2FormatParse } from '../pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js';
10
10
  import { runRespInboundStage3SemanticMap } from '../pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js';
11
+ import { runRespInboundStage3ThoughtSignatureCapture } from '../pipeline/stages/resp_inbound/resp_inbound_stage3_thought_signature_capture/index.js';
11
12
  import { runRespInboundStageCompatResponse } from '../pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js';
12
13
  import { runRespProcessStage1ToolGovernance } from '../pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js';
13
14
  import { runRespProcessStage2Finalize } from '../pipeline/stages/resp_process/resp_process_stage2_finalize/index.js';
14
15
  import { runRespOutboundStage1ClientRemap } from '../pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js';
15
16
  import { runRespOutboundStage2SseStream } from '../pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js';
16
17
  import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
18
+ import { ProviderProtocolError } from '../../shared/errors.js';
19
+ import { readRuntimeMetadata } from '../../shared/runtime-metadata.js';
17
20
  import { runServerToolOrchestration } from '../../../servertool/engine.js';
18
21
  import { commitClockReservation, normalizeClockConfig } from '../../../servertool/clock/task-store.js';
19
22
  const PROVIDER_RESPONSE_REGISTRY = {
@@ -35,7 +38,8 @@ const PROVIDER_RESPONSE_REGISTRY = {
35
38
  }
36
39
  };
37
40
  function isServerToolFollowup(context) {
38
- const raw = context.serverToolFollowup;
41
+ const rt = readRuntimeMetadata(context);
42
+ const raw = rt?.serverToolFollowup;
39
43
  if (raw === true) {
40
44
  return true;
41
45
  }
@@ -85,7 +89,8 @@ function coerceClockReservation(value) {
85
89
  }
86
90
  async function maybeCommitClockReservationFromContext(context) {
87
91
  try {
88
- const clockConfig = normalizeClockConfig(context.clock);
92
+ const rt = readRuntimeMetadata(context);
93
+ const clockConfig = normalizeClockConfig(rt?.clock);
89
94
  if (!clockConfig) {
90
95
  return;
91
96
  }
@@ -114,6 +119,42 @@ function detectProviderResponseShape(payload) {
114
119
  return 'gemini-chat';
115
120
  return 'unknown';
116
121
  }
122
+ function isCanonicalChatCompletion(payload) {
123
+ if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
124
+ return false;
125
+ }
126
+ const obj = payload;
127
+ const choices = Array.isArray(obj.choices) ? obj.choices : [];
128
+ if (!choices.length)
129
+ return false;
130
+ const first = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0]) ? choices[0] : null;
131
+ if (!first)
132
+ return false;
133
+ const msg = first.message;
134
+ return Boolean(msg && typeof msg === 'object' && !Array.isArray(msg));
135
+ }
136
+ async function coerceClientPayloadToCanonicalChatCompletionOrThrow(options) {
137
+ if (isCanonicalChatCompletion(options.payload)) {
138
+ return options.payload;
139
+ }
140
+ const detected = detectProviderResponseShape(options.payload);
141
+ if (detected === 'unknown') {
142
+ throw new ProviderProtocolError(`[hub_response] Non-canonical response payload at ${options.scope}`, {
143
+ code: 'MALFORMED_RESPONSE',
144
+ details: { detected }
145
+ });
146
+ }
147
+ const plan = PROVIDER_RESPONSE_REGISTRY[detected];
148
+ const mapper = plan.createMapper();
149
+ const coerced = await mapper.toChatCompletion({ payload: options.payload }, options.adapterContext, { requestSemantics: options.requestSemantics });
150
+ if (isCanonicalChatCompletion(coerced)) {
151
+ return coerced;
152
+ }
153
+ throw new ProviderProtocolError(`[hub_response] Failed to canonicalize response payload at ${options.scope}`, {
154
+ code: 'MALFORMED_RESPONSE',
155
+ details: { detected }
156
+ });
157
+ }
117
158
  function summarizeToolCallsFromProviderResponse(payload) {
118
159
  try {
119
160
  if (!isJsonRecord(payload))
@@ -169,6 +210,7 @@ function stripInternalPolicyDebugFields(payload) {
169
210
  delete target.__responses_reasoning;
170
211
  delete target.__responses_payload_snapshot;
171
212
  delete target.__responses_passthrough;
213
+ delete target.anthropicToolNameMap;
172
214
  }
173
215
  function supportsSseProtocol(protocol) {
174
216
  return protocol === 'openai-chat' || protocol === 'openai-responses' || protocol === 'anthropic-messages' || protocol === 'gemini-chat';
@@ -212,23 +254,20 @@ function resolveChatReasoningMode(_entryEndpoint) {
212
254
  return 'keep';
213
255
  }
214
256
  export async function convertProviderResponse(options) {
215
- const clientProtocol = resolveClientProtocol(options.entryEndpoint);
257
+ const isFollowup = isServerToolFollowup(options.context);
258
+ // ServerTool followups are internal hops. They must return canonical OpenAI-chat-like payloads
259
+ // to re-enter the hub chat process deterministically (tools harvesting, governance, etc.).
260
+ // Client protocol remapping happens only on the outermost request.
261
+ const clientProtocol = isFollowup ? 'openai-chat' : resolveClientProtocol(options.entryEndpoint);
216
262
  const hasServerToolSupport = Boolean(options.providerInvoker) || Boolean(options.reenterPipeline);
217
- // 是否跳过 ServerTool 编排:
218
- // - 仅在当前 Provider 完全不支持 ServerTool(没有 invoker/reenterPipeline)时跳过;
219
- // - 对于 serverToolFollowup=true 的二/三跳请求,也允许再次进入 ServerTool 流程,
220
- // 由具体 handler(例如 gemini_empty_reply_continue / iflow_model_error_retry 等)
221
- // 自行通过 serverToolFollowup 标记决定是否生效。
222
- const skipServerTools = !hasServerToolSupport;
223
- // 对于由 server-side 工具触发的内部跳转(二跳/三跳),统一禁用 SSE 聚合输出,
224
- // 始终返回完整的 ChatCompletion JSON,便于在 llms 内部直接解析,而不是拿到
225
- // __sse_responses 可读流。
226
- const wantsStream = isServerToolFollowup(options.context) ? false : options.wantsStream;
263
+ // ServerTool followup hop: always return a fully materialized ChatCompletion JSON (non-SSE).
264
+ // This is an internal hop used by the servertool engine, and it must be deterministic.
265
+ const wantsStream = isFollowup ? false : options.wantsStream;
227
266
  try {
228
267
  // eslint-disable-next-line no-console
229
268
  console.log(`\x1b[38;5;33m[servertool][orchestrator][debug] requestId=${options.context.requestId} ` +
230
269
  `protocol=${options.providerProtocol} endpoint=${options.entryEndpoint} ` +
231
- `skipServerTools=${skipServerTools} hasInvoker=${Boolean(options.providerInvoker)} ` +
270
+ `serverTools=${hasServerToolSupport} hasInvoker=${Boolean(options.providerInvoker)} ` +
232
271
  `hasReenter=${Boolean(options.reenterPipeline)}\x1b[0m`);
233
272
  }
234
273
  catch {
@@ -255,23 +294,17 @@ export async function convertProviderResponse(options) {
255
294
  formatAdapter,
256
295
  stageRecorder: options.stageRecorder
257
296
  });
258
- if (options.providerProtocol === 'openai-responses') {
259
- try {
260
- recordResponsesResponse({
261
- requestId: options.context.requestId,
262
- response: formatEnvelope.payload
263
- });
264
- }
265
- catch {
266
- // ignore conversation capture errors
267
- }
268
- }
269
297
  formatEnvelope.payload = runRespInboundStageCompatResponse({
270
298
  payload: formatEnvelope.payload,
271
299
  adapterContext: options.context,
272
300
  stageRecorder: options.stageRecorder
273
301
  });
274
302
  stripInternalPolicyDebugFields(formatEnvelope.payload);
303
+ formatEnvelope.payload = await runRespInboundStage3ThoughtSignatureCapture({
304
+ payload: formatEnvelope.payload,
305
+ adapterContext: options.context,
306
+ stageRecorder: options.stageRecorder
307
+ });
275
308
  // Phase 2 (shadow): response tool surface mismatch detection (provider inbound).
276
309
  // Only records diffs; does not rewrite payload.
277
310
  try {
@@ -311,14 +344,15 @@ export async function convertProviderResponse(options) {
311
344
  adapterContext: options.context,
312
345
  formatEnvelope,
313
346
  mapper,
347
+ requestSemantics: options.requestSemantics,
314
348
  stageRecorder: options.stageRecorder
315
349
  });
316
350
  // 记录语义映射后的 ChatCompletion,便于回放 server-side 工具流程。
317
- recordStage(options.stageRecorder, 'resp_inbound_stage3_semantic_map.chat', chatResponse);
351
+ recordStage(options.stageRecorder, 'chat_process.resp.stage4.semantic_map_to_chat.chat', chatResponse);
318
352
  // 检查是否需要进行 ServerTool 编排
319
353
  // 使用新的 ChatEnvelope 级别的 servertool 实现
320
354
  let effectiveChatResponse = chatResponse;
321
- if (!skipServerTools && options.reenterPipeline) {
355
+ if (hasServerToolSupport) {
322
356
  try {
323
357
  // eslint-disable-next-line no-console
324
358
  console.log(`\x1b[38;5;33m[servertool][orchestrator] start requestId=${options.context.requestId} ` +
@@ -348,6 +382,12 @@ export async function convertProviderResponse(options) {
348
382
  /* logging best-effort */
349
383
  }
350
384
  effectiveChatResponse = orchestration.chat;
385
+ recordStage(options.stageRecorder, 'chat_process.resp.stage5.servertool_orchestration', {
386
+ executed: true,
387
+ flowId: orchestration.flowId,
388
+ inputShape: detectProviderResponseShape(chatResponse),
389
+ outputShape: detectProviderResponseShape(effectiveChatResponse)
390
+ });
351
391
  }
352
392
  else {
353
393
  try {
@@ -358,8 +398,20 @@ export async function convertProviderResponse(options) {
358
398
  catch {
359
399
  /* logging best-effort */
360
400
  }
401
+ recordStage(options.stageRecorder, 'chat_process.resp.stage5.servertool_orchestration', {
402
+ executed: false,
403
+ inputShape: detectProviderResponseShape(chatResponse)
404
+ });
361
405
  }
362
406
  }
407
+ // Hard gate: response-side chat_process requires an OpenAI-chat-like surface (choices[].message).
408
+ // ServerTool followups must never replace the canonical chat completion with a client-protocol shape.
409
+ effectiveChatResponse = await coerceClientPayloadToCanonicalChatCompletionOrThrow({
410
+ payload: effectiveChatResponse,
411
+ adapterContext: options.context,
412
+ requestSemantics: options.requestSemantics,
413
+ scope: 'chat_process.response.entry'
414
+ });
363
415
  // 如果没有执行 servertool,继续原来的处理流程
364
416
  const governanceResult = await runRespProcessStage1ToolGovernance({
365
417
  payload: effectiveChatResponse,
@@ -382,10 +434,22 @@ export async function convertProviderResponse(options) {
382
434
  clientProtocol,
383
435
  requestId: clientFacingRequestId,
384
436
  adapterContext: options.context,
437
+ requestSemantics: options.requestSemantics,
385
438
  stageRecorder: options.stageRecorder
386
439
  });
387
440
  applyModelOverride(clientPayload, displayModel);
388
441
  stripInternalPolicyDebugFields(clientPayload);
442
+ if (clientProtocol === 'openai-responses') {
443
+ try {
444
+ recordResponsesResponse({
445
+ requestId: options.context.requestId,
446
+ response: clientPayload
447
+ });
448
+ }
449
+ catch {
450
+ // ignore conversation capture errors
451
+ }
452
+ }
389
453
  // Phase 2 (shadow): response tool surface mismatch detection (client outbound).
390
454
  try {
391
455
  if (options.stageRecorder && isToolSurfaceShadowEnabled()) {
@@ -1,9 +1,12 @@
1
1
  import type { FormatEnvelope } from '../types/format-envelope.js';
2
2
  import type { AdapterContext } from '../types/chat-envelope.js';
3
3
  import type { JsonObject } from '../types/json.js';
4
+ import type { ChatSemantics } from '../types/chat-envelope.js';
4
5
  export type ChatCompletionLike = JsonObject;
5
6
  export interface ResponseMapper {
6
- toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatCompletionLike> | ChatCompletionLike;
7
+ toChatCompletion(format: FormatEnvelope, ctx: AdapterContext, options?: {
8
+ requestSemantics?: ChatSemantics | JsonObject;
9
+ }): Promise<ChatCompletionLike> | ChatCompletionLike;
7
10
  }
8
11
  export declare class OpenAIChatResponseMapper implements ResponseMapper {
9
12
  toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
@@ -12,8 +15,12 @@ export declare class ResponsesResponseMapper implements ResponseMapper {
12
15
  toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
13
16
  }
14
17
  export declare class AnthropicResponseMapper implements ResponseMapper {
15
- toChatCompletion(format: FormatEnvelope, ctx: AdapterContext): ChatCompletionLike;
18
+ toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext, options?: {
19
+ requestSemantics?: ChatSemantics | JsonObject;
20
+ }): ChatCompletionLike;
16
21
  }
17
22
  export declare class GeminiResponseMapper implements ResponseMapper {
18
- toChatCompletion(format: FormatEnvelope, _ctx: AdapterContext): ChatCompletionLike;
23
+ toChatCompletion(format: FormatEnvelope, ctx: AdapterContext, options?: {
24
+ requestSemantics?: ChatSemantics | JsonObject;
25
+ }): ChatCompletionLike;
19
26
  }