@jsonstudio/llms 0.6.954 → 0.6.1164

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 (130) hide show
  1. package/dist/conversion/hub/operation-table/operation-table-runner.d.ts +18 -0
  2. package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
  3. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
  4. package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
  5. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
  6. package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
  7. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
  8. package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
  9. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
  10. package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
  11. package/dist/conversion/hub/ops/operations.d.ts +19 -0
  12. package/dist/conversion/hub/ops/operations.js +126 -0
  13. package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
  14. package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
  15. package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
  16. package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
  17. package/dist/conversion/hub/policy/policy-engine.js +41 -9
  18. package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
  19. package/dist/conversion/hub/policy/protocol-spec.js +73 -23
  20. package/dist/conversion/hub/process/chat-process.js +252 -41
  21. package/dist/conversion/hub/response/provider-response.js +175 -2
  22. package/dist/conversion/hub/response/response-runtime.js +1 -1
  23. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
  24. package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
  25. package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
  26. package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
  27. package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
  28. package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
  29. package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
  30. package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
  31. package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
  32. package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
  33. package/dist/conversion/responses/responses-openai-bridge.js +14 -2
  34. package/dist/conversion/shared/bridge-message-utils.js +2 -8
  35. package/dist/conversion/shared/bridge-policies.js +5 -105
  36. package/dist/conversion/shared/gemini-tool-utils.js +89 -15
  37. package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
  38. package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
  39. package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
  40. package/dist/conversion/shared/snapshot-hooks.js +166 -3
  41. package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
  42. package/dist/conversion/shared/text-markup-normalizer.js +345 -9
  43. package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
  44. package/dist/conversion/shared/thought-signature-validator.js +170 -0
  45. package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
  46. package/dist/conversion/shared/tool-argument-repairer.js +56 -0
  47. package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
  48. package/dist/conversion/shared/tool-call-id-manager.js +231 -0
  49. package/dist/conversion/shared/tool-canonicalizer.js +2 -11
  50. package/dist/router/virtual-router/bootstrap.js +54 -5
  51. package/dist/router/virtual-router/engine-selection.js +132 -42
  52. package/dist/router/virtual-router/engine.d.ts +3 -0
  53. package/dist/router/virtual-router/engine.js +142 -33
  54. package/dist/router/virtual-router/health-weighted.d.ts +25 -0
  55. package/dist/router/virtual-router/health-weighted.js +63 -0
  56. package/dist/router/virtual-router/load-balancer.d.ts +2 -0
  57. package/dist/router/virtual-router/load-balancer.js +45 -16
  58. package/dist/router/virtual-router/routing-instructions.js +17 -1
  59. package/dist/router/virtual-router/sticky-session-store.js +136 -24
  60. package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
  61. package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
  62. package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
  63. package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
  64. package/dist/router/virtual-router/types.d.ts +70 -0
  65. package/dist/servertool/clock/config.d.ts +7 -0
  66. package/dist/servertool/clock/config.js +27 -0
  67. package/dist/servertool/clock/daemon.d.ts +3 -0
  68. package/dist/servertool/clock/daemon.js +79 -0
  69. package/dist/servertool/clock/io.d.ts +2 -0
  70. package/dist/servertool/clock/io.js +13 -0
  71. package/dist/servertool/clock/paths.d.ts +4 -0
  72. package/dist/servertool/clock/paths.js +25 -0
  73. package/dist/servertool/clock/session-store.d.ts +3 -0
  74. package/dist/servertool/clock/session-store.js +56 -0
  75. package/dist/servertool/clock/state.d.ts +5 -0
  76. package/dist/servertool/clock/state.js +62 -0
  77. package/dist/servertool/clock/task-store.d.ts +5 -0
  78. package/dist/servertool/clock/task-store.js +4 -0
  79. package/dist/servertool/clock/tasks.d.ts +17 -0
  80. package/dist/servertool/clock/tasks.js +221 -0
  81. package/dist/servertool/clock/types.d.ts +36 -0
  82. package/dist/servertool/clock/types.js +1 -0
  83. package/dist/servertool/engine.d.ts +2 -0
  84. package/dist/servertool/engine.js +161 -7
  85. package/dist/servertool/followup-shadow.d.ts +16 -0
  86. package/dist/servertool/followup-shadow.js +145 -0
  87. package/dist/servertool/handlers/apply-patch-guard.js +1 -265
  88. package/dist/servertool/handlers/clock-auto.d.ts +1 -0
  89. package/dist/servertool/handlers/clock-auto.js +160 -0
  90. package/dist/servertool/handlers/clock.d.ts +1 -0
  91. package/dist/servertool/handlers/clock.js +197 -0
  92. package/dist/servertool/handlers/exec-command-guard.js +7 -555
  93. package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
  94. package/dist/servertool/handlers/followup-request-builder.js +248 -28
  95. package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
  96. package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
  97. package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
  98. package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
  99. package/dist/servertool/handlers/stop-message-auto.js +47 -175
  100. package/dist/servertool/handlers/vision.d.ts +7 -1
  101. package/dist/servertool/handlers/vision.js +61 -117
  102. package/dist/servertool/handlers/web-search.d.ts +7 -1
  103. package/dist/servertool/handlers/web-search.js +122 -105
  104. package/dist/servertool/reenter-backend.d.ts +23 -0
  105. package/dist/servertool/reenter-backend.js +18 -0
  106. package/dist/servertool/server-side-tools.d.ts +3 -2
  107. package/dist/servertool/server-side-tools.js +64 -10
  108. package/dist/servertool/types.d.ts +92 -3
  109. package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
  110. package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
  111. package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
  112. package/dist/sse/shared/writer.js +24 -7
  113. package/dist/tools/apply-patch/execution-capturer.js +3 -1
  114. package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
  115. package/dist/tools/apply-patch/json/parse-loose.js +139 -0
  116. package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
  117. package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
  118. package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
  119. package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
  120. package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
  121. package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
  122. package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
  123. package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
  124. package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
  125. package/dist/tools/apply-patch/structured/coercion.js +82 -0
  126. package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
  127. package/dist/tools/apply-patch/validation/shared.js +6 -0
  128. package/dist/tools/apply-patch/validator.d.ts +2 -2
  129. package/dist/tools/apply-patch/validator.js +6 -556
  130. package/package.json +1 -1
@@ -0,0 +1,333 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { registerServerToolHandler } from '../registry.js';
3
+ import { cloneJson } from '../server-side-tools.js';
4
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
5
+ const FLOW_ID = 'recursive_detection_guard';
6
+ const CONSECUTIVE_TRIGGER_COUNT = 10;
7
+ const sessionStates = new Map();
8
+ const DEBUG_RECURSIVE_DETECTION = String(process.env.ROUTECODEX_RECURSIVE_DETECTION_DEBUG || '').trim() === '1';
9
+ function debugLog(message, extra) {
10
+ if (!DEBUG_RECURSIVE_DETECTION) {
11
+ return;
12
+ }
13
+ try {
14
+ // eslint-disable-next-line no-console
15
+ console.log(`[recursive-detection][debug] ${message}${extra ? ` ${JSON.stringify(extra)}` : ''}`);
16
+ }
17
+ catch {
18
+ /* ignore logging failures */
19
+ }
20
+ }
21
+ function getRecursiveDetectionConfig() {
22
+ const enabled = String(process.env.ROUTECODEX_RECURSIVE_DETECTION_ENABLED ?? '').trim().toLowerCase() !== 'false';
23
+ return {
24
+ enabled,
25
+ ttlMs: 5 * 60 * 1000,
26
+ maxSessions: 2000
27
+ };
28
+ }
29
+ function shouldSkipFollowup(adapterContext) {
30
+ const record = adapterContext;
31
+ const loopState = record ? record.serverToolLoopState : undefined;
32
+ if (loopState && typeof loopState === 'object' && !Array.isArray(loopState)) {
33
+ return true;
34
+ }
35
+ const raw = record ? record.serverToolFollowup : undefined;
36
+ if (raw === true) {
37
+ return true;
38
+ }
39
+ if (typeof raw === 'string') {
40
+ const normalized = raw.trim().toLowerCase();
41
+ return normalized === '1' || normalized === 'true';
42
+ }
43
+ return false;
44
+ }
45
+ function resolveSessionKey(adapterContext) {
46
+ if (!adapterContext || typeof adapterContext !== 'object' || Array.isArray(adapterContext)) {
47
+ return 'default';
48
+ }
49
+ const record = adapterContext;
50
+ const sessionId = typeof record.sessionId === 'string'
51
+ ? record.sessionId.trim()
52
+ : (typeof record.session_id === 'string' ? record.session_id.trim() : '');
53
+ const conversationId = typeof record.conversationId === 'string'
54
+ ? record.conversationId.trim()
55
+ : (typeof record.conversation_id === 'string' ? record.conversation_id.trim() : '');
56
+ const requestId = typeof record.requestId === 'string' ? record.requestId.trim() : '';
57
+ if (sessionId) {
58
+ return `session:${sessionId}`;
59
+ }
60
+ if (conversationId) {
61
+ return `conversation:${conversationId}`;
62
+ }
63
+ if (requestId) {
64
+ return `request:${requestId}`;
65
+ }
66
+ return 'default';
67
+ }
68
+ function normalizeToolName(name) {
69
+ return typeof name === 'string' ? name.trim().toLowerCase() : '';
70
+ }
71
+ function sha256(value) {
72
+ try {
73
+ return createHash('sha256').update(value).digest('hex');
74
+ }
75
+ catch {
76
+ return '';
77
+ }
78
+ }
79
+ function stableStringify(value) {
80
+ if (value === null)
81
+ return 'null';
82
+ const t = typeof value;
83
+ if (t === 'string')
84
+ return JSON.stringify(value);
85
+ if (t === 'number')
86
+ return Number.isFinite(value) ? String(value) : 'null';
87
+ if (t === 'boolean')
88
+ return value ? 'true' : 'false';
89
+ if (t === 'bigint')
90
+ return JSON.stringify(String(value));
91
+ if (t === 'undefined' || t === 'function' || t === 'symbol')
92
+ return 'null';
93
+ if (Array.isArray(value)) {
94
+ return `[${value.map((item) => stableStringify(item)).join(',')}]`;
95
+ }
96
+ if (value && typeof value === 'object') {
97
+ const record = value;
98
+ const keys = Object.keys(record).sort();
99
+ const parts = [];
100
+ for (const key of keys) {
101
+ const v = record[key];
102
+ if (v === undefined)
103
+ continue;
104
+ parts.push(`${JSON.stringify(key)}:${stableStringify(v)}`);
105
+ }
106
+ return `{${parts.join(',')}}`;
107
+ }
108
+ try {
109
+ return JSON.stringify(value);
110
+ }
111
+ catch {
112
+ return 'null';
113
+ }
114
+ }
115
+ function normalizeToolArgs(toolCall) {
116
+ const raw = typeof toolCall.arguments === 'string' ? toolCall.arguments : '';
117
+ if (!raw || !raw.trim())
118
+ return '{}';
119
+ try {
120
+ const parsed = JSON.parse(raw);
121
+ return stableStringify(parsed);
122
+ }
123
+ catch {
124
+ return raw.trim();
125
+ }
126
+ }
127
+ function buildCallSignature(toolCall) {
128
+ const toolName = normalizeToolName(toolCall.name);
129
+ const args = normalizeToolArgs(toolCall);
130
+ return sha256(`${toolName}\n${args}`);
131
+ }
132
+ function cleanupSessions(now, config) {
133
+ const cutoff = now - config.ttlMs;
134
+ if (sessionStates.size > config.maxSessions) {
135
+ for (const [key, state] of sessionStates.entries()) {
136
+ if (!state || state.updatedAt <= cutoff) {
137
+ sessionStates.delete(key);
138
+ }
139
+ }
140
+ return;
141
+ }
142
+ for (const [key, state] of sessionStates.entries()) {
143
+ if (!state || state.updatedAt <= cutoff) {
144
+ sessionStates.delete(key);
145
+ }
146
+ }
147
+ }
148
+ function injectBlockedToolResult(base, toolCall, options) {
149
+ const cloned = cloneJson(base);
150
+ const existingOutputs = Array.isArray(cloned.tool_outputs)
151
+ ? cloned.tool_outputs
152
+ : [];
153
+ const payload = {
154
+ ok: false,
155
+ blocked: true,
156
+ reason: 'RECURSIVE_TOOL_CALL_DETECTED',
157
+ rule: {
158
+ kind: 'consecutive_same_tool_and_args',
159
+ consecutive: CONSECUTIVE_TRIGGER_COUNT
160
+ },
161
+ tool: toolCall.name,
162
+ signature: options.signature
163
+ };
164
+ cloned.tool_outputs = [
165
+ ...existingOutputs,
166
+ {
167
+ tool_call_id: toolCall.id,
168
+ name: toolCall.name,
169
+ content: JSON.stringify(payload)
170
+ }
171
+ ];
172
+ return cloned;
173
+ }
174
+ function buildSingleToolCallAssistantMessage(toolCall) {
175
+ return {
176
+ role: 'assistant',
177
+ content: null,
178
+ tool_calls: [
179
+ {
180
+ id: toolCall.id,
181
+ type: 'function',
182
+ function: {
183
+ name: toolCall.name,
184
+ arguments: typeof toolCall.arguments === 'string' ? toolCall.arguments : ''
185
+ }
186
+ }
187
+ ]
188
+ };
189
+ }
190
+ function buildToolMessages(chatResponse) {
191
+ const toolOutputs = Array.isArray(chatResponse.tool_outputs)
192
+ ? chatResponse.tool_outputs
193
+ : [];
194
+ const messages = [];
195
+ for (const entry of toolOutputs) {
196
+ if (!entry || typeof entry !== 'object' || Array.isArray(entry))
197
+ continue;
198
+ const record = entry;
199
+ const toolCallId = typeof record.tool_call_id === 'string' ? record.tool_call_id : undefined;
200
+ if (!toolCallId)
201
+ continue;
202
+ const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : 'tool';
203
+ const rawContent = record.content;
204
+ let contentText;
205
+ if (typeof rawContent === 'string') {
206
+ contentText = rawContent;
207
+ }
208
+ else {
209
+ try {
210
+ contentText = JSON.stringify(rawContent ?? {});
211
+ }
212
+ catch {
213
+ contentText = String(rawContent ?? '');
214
+ }
215
+ }
216
+ messages.push({
217
+ role: 'tool',
218
+ tool_call_id: toolCallId,
219
+ name,
220
+ content: contentText
221
+ });
222
+ }
223
+ return messages;
224
+ }
225
+ function hasAssistantMessageFromChatLike(chatResponse) {
226
+ if (!chatResponse || typeof chatResponse !== 'object') {
227
+ return false;
228
+ }
229
+ const choices = Array.isArray(chatResponse.choices)
230
+ ? chatResponse.choices
231
+ : [];
232
+ if (!choices.length) {
233
+ return false;
234
+ }
235
+ const first = choices[0];
236
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
237
+ return false;
238
+ }
239
+ const message = first.message;
240
+ return Boolean(message && typeof message === 'object' && !Array.isArray(message));
241
+ }
242
+ const handler = async (ctx) => {
243
+ const config = getRecursiveDetectionConfig();
244
+ if (!config.enabled) {
245
+ return null;
246
+ }
247
+ const now = Date.now();
248
+ cleanupSessions(now, config);
249
+ const sessionKey = resolveSessionKey(ctx.adapterContext);
250
+ // Treat followup hops as an interruption: do not count them, and clear any ongoing streak
251
+ // so that post-followup calls always restart from 0.
252
+ if (shouldSkipFollowup(ctx.adapterContext)) {
253
+ sessionStates.delete(sessionKey);
254
+ return null;
255
+ }
256
+ const existing = sessionStates.get(sessionKey) ?? { updatedAt: now, consecutiveCount: 0 };
257
+ // Any interruption (no tool calls) resets the counter.
258
+ if (!ctx.toolCalls || !ctx.toolCalls.length) {
259
+ if (existing.signature || existing.consecutiveCount) {
260
+ sessionStates.delete(sessionKey);
261
+ }
262
+ return null;
263
+ }
264
+ let state = { ...existing, updatedAt: now };
265
+ for (const toolCall of ctx.toolCalls) {
266
+ const signature = buildCallSignature(toolCall);
267
+ if (state.signature && state.signature === signature) {
268
+ state.consecutiveCount += 1;
269
+ }
270
+ else {
271
+ state.signature = signature;
272
+ state.consecutiveCount = 1;
273
+ }
274
+ state.updatedAt = now;
275
+ debugLog('observe', {
276
+ sessionKey,
277
+ toolName: toolCall.name,
278
+ consecutiveCount: state.consecutiveCount
279
+ });
280
+ if (state.consecutiveCount < CONSECUTIVE_TRIGGER_COUNT) {
281
+ continue;
282
+ }
283
+ // Triggered: reset counter immediately (including "trigger info sent").
284
+ sessionStates.delete(sessionKey);
285
+ // Must send a followup request to provider (not a direct client warning).
286
+ if (!ctx.capabilities.reenterPipeline) {
287
+ return null;
288
+ }
289
+ const patched = injectBlockedToolResult(ctx.base, toolCall, { signature });
290
+ // Fail-closed: if we cannot build a followup request, do not intercept.
291
+ const captured = ctx.adapterContext && typeof ctx.adapterContext === 'object'
292
+ ? ctx.adapterContext.capturedChatRequest
293
+ : undefined;
294
+ const seed = extractCapturedChatSeed(captured);
295
+ if (!seed) {
296
+ return null;
297
+ }
298
+ if (!hasAssistantMessageFromChatLike(patched)) {
299
+ return null;
300
+ }
301
+ const toolMessages = buildToolMessages(patched);
302
+ if (!toolMessages.length) {
303
+ return null;
304
+ }
305
+ const reminder = `请停下来检查你的任务,你已经反复进行了调用,是否考虑别的方式处理,如果无法处理请停下来,报告问题。` +
306
+ `(触发条件:同一工具同一参数连续 ${CONSECUTIVE_TRIGGER_COUNT} 次:${toolCall.name})` +
307
+ `请不要在回复中提及“检测器/守卫/servertool”,也不要继续调用该工具,除非用户明确要求且参数发生变化。`;
308
+ return {
309
+ flowId: FLOW_ID,
310
+ finalize: async () => ({
311
+ chatResponse: patched,
312
+ execution: {
313
+ flowId: FLOW_ID,
314
+ followup: {
315
+ requestIdSuffix: ':recursive_detection_guard_followup',
316
+ entryEndpoint: ctx.entryEndpoint,
317
+ injection: {
318
+ ops: [
319
+ { op: 'inject_system_text', text: reminder },
320
+ { op: 'append_assistant_message' },
321
+ { op: 'append_tool_messages_from_tool_outputs' },
322
+ { op: 'drop_tool_by_name', name: toolCall.name }
323
+ ]
324
+ }
325
+ }
326
+ }
327
+ })
328
+ };
329
+ }
330
+ sessionStates.set(sessionKey, state);
331
+ return null;
332
+ };
333
+ registerServerToolHandler('recursive_detection_guard', handler, { trigger: 'auto' });
@@ -1,9 +1,7 @@
1
1
  import { registerServerToolHandler } from '../registry.js';
2
- import { cloneJson } from '../server-side-tools.js';
3
2
  import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync } from '../../router/virtual-router/sticky-session-store.js';
4
3
  import { isCompactionRequest } from './compaction-detect.js';
5
- import { buildResponsesRequestFromChat, captureResponsesContext, buildChatRequestFromResponses } from '../../conversion/responses/responses-openai-bridge.js';
6
- import { buildAnthropicRequestFromOpenAIChat } from '../../conversion/codecs/anthropic-openai-codec.js';
4
+ import { extractCapturedChatSeed } from './followup-request-builder.js';
7
5
  const STOPMESSAGE_DEBUG = (process.env.ROUTECODEX_STOPMESSAGE_DEBUG || '').trim() === '1';
8
6
  function debugLog(message, extra) {
9
7
  if (!STOPMESSAGE_DEBUG) {
@@ -115,32 +113,31 @@ const handler = async (ctx) => {
115
113
  return null;
116
114
  }
117
115
  const entryEndpoint = resolveEntryEndpoint(record);
118
- const followupPayload = buildStopMessageFollowupPayload(captured, text, ctx.base, entryEndpoint);
119
- if (!followupPayload) {
120
- debugLog('skip_failed_build_followup', {
121
- stickyKey
122
- });
116
+ const seed = extractCapturedChatSeed(captured);
117
+ if (!seed) {
118
+ debugLog('skip_failed_build_followup', { stickyKey });
123
119
  return null;
124
120
  }
125
- const followupMetadata = {
126
- serverToolFollowup: true,
127
- stream: false,
128
- preserveRouteHint: false,
129
- disableStickyRoutes: true,
130
- serverToolOriginalEntryEndpoint: entryEndpoint,
131
- ...(connectionState ? { clientConnectionState: connectionState } : {})
132
- };
121
+ const assistantMessage = extractAssistantMessageForFollowup(ctx.base);
133
122
  return {
134
- chatResponse: ctx.base,
135
- execution: {
136
- flowId: FLOW_ID,
137
- followup: {
138
- requestIdSuffix: ':stop_followup',
139
- payload: followupPayload,
140
- entryEndpoint,
141
- metadata: followupMetadata
123
+ flowId: FLOW_ID,
124
+ finalize: async () => ({
125
+ chatResponse: ctx.base,
126
+ execution: {
127
+ flowId: FLOW_ID,
128
+ followup: {
129
+ requestIdSuffix: ':stop_followup',
130
+ entryEndpoint,
131
+ injection: {
132
+ ops: [
133
+ { op: 'append_assistant_message', required: false },
134
+ { op: 'append_user_text', text }
135
+ ]
136
+ },
137
+ metadata: (connectionState ? { clientConnectionState: connectionState } : {})
138
+ }
142
139
  }
143
- }
140
+ })
144
141
  };
145
142
  };
146
143
  registerServerToolHandler('stop_message_auto', handler, { trigger: 'auto' });
@@ -214,168 +211,43 @@ function isStopFinishReason(base) {
214
211
  }
215
212
  return true;
216
213
  }
217
- function getCapturedRequest(adapterContext) {
218
- if (!adapterContext || typeof adapterContext !== 'object') {
214
+ function extractAssistantMessageForFollowup(chatResponse) {
215
+ if (!chatResponse || typeof chatResponse !== 'object' || Array.isArray(chatResponse)) {
219
216
  return null;
220
217
  }
221
- const captured = adapterContext.capturedChatRequest;
222
- if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
218
+ const choices = Array.isArray(chatResponse.choices)
219
+ ? chatResponse.choices
220
+ : [];
221
+ if (!choices.length) {
223
222
  return null;
224
223
  }
225
- return captured;
226
- }
227
- function extractChatSeedFromCapturedRequest(source) {
228
- const model = typeof source.model === 'string' && source.model.trim()
229
- ? source.model.trim()
230
- : undefined;
231
- const rawMessages = Array.isArray(source.messages)
232
- ? source.messages
233
- : null;
234
- if (rawMessages) {
235
- const tools = Array.isArray(source.tools)
236
- ? cloneJson(source.tools)
237
- : undefined;
238
- return {
239
- ...(model ? { model } : {}),
240
- messages: cloneJson(rawMessages),
241
- ...(tools ? { tools } : {})
242
- };
224
+ const first = choices[0];
225
+ if (!first || typeof first !== 'object' || Array.isArray(first)) {
226
+ return null;
243
227
  }
244
- const rawInput = Array.isArray(source.input)
245
- ? source.input
246
- : null;
247
- if (rawInput) {
248
- try {
249
- const ctx = captureResponsesContext(source);
250
- if (!ctx.isResponsesPayload) {
251
- return null;
252
- }
253
- const rebuilt = buildChatRequestFromResponses(source, ctx).request;
254
- const rebuiltModel = typeof rebuilt.model === 'string' && rebuilt.model.trim().length ? String(rebuilt.model).trim() : model;
255
- const rebuiltMessages = Array.isArray(rebuilt.messages) ? rebuilt.messages : [];
256
- const rebuiltTools = Array.isArray(rebuilt.tools) ? rebuilt.tools : undefined;
257
- return {
258
- ...(rebuiltModel ? { model: rebuiltModel } : {}),
259
- messages: cloneJson(rebuiltMessages),
260
- ...(rebuiltTools ? { tools: cloneJson(rebuiltTools) } : {})
261
- };
262
- }
263
- catch {
264
- return null;
265
- }
228
+ const message = first.message;
229
+ if (!message || typeof message !== 'object' || Array.isArray(message)) {
230
+ return null;
266
231
  }
267
- return null;
268
- }
269
- function buildStopMessageFollowupPayload(source, text, baseResponse, entryEndpoint) {
270
- if (!source || typeof source !== 'object') {
232
+ const role = typeof message.role === 'string' ? String(message.role) : '';
233
+ if (role && role.toLowerCase() !== 'assistant') {
271
234
  return null;
272
235
  }
273
- const chatSeed = extractChatSeedFromCapturedRequest(source);
274
- const model = chatSeed?.model;
275
- const originalMessages = chatSeed?.messages ? cloneJson(chatSeed.messages) : [];
276
- const originalTools = chatSeed?.tools ? cloneJson(chatSeed.tools) : undefined;
277
- const originalParameters = (() => {
278
- const direct = source.parameters;
279
- if (direct && typeof direct === 'object' && !Array.isArray(direct)) {
280
- return cloneJson(direct);
281
- }
282
- // Backward/compat: captured request might be a raw `/v1/responses` payload with
283
- // top-level parameters (max_output_tokens, temperature, ...), not nested under `parameters`.
284
- const record = source;
285
- const allowed = new Set([
286
- 'temperature',
287
- 'top_p',
288
- 'max_output_tokens',
289
- 'seed',
290
- 'logit_bias',
291
- 'user',
292
- 'parallel_tool_calls',
293
- 'tool_choice',
294
- 'response_format',
295
- 'stream'
296
- ]);
297
- const out = {};
298
- if (record.max_output_tokens === undefined && record.max_tokens !== undefined) {
299
- out.max_output_tokens = record.max_tokens;
300
- }
301
- for (const key of Object.keys(record)) {
302
- if (!allowed.has(key))
303
- continue;
304
- out[key] = record[key];
305
- }
306
- return Object.keys(out).length ? cloneJson(out) : undefined;
307
- })();
308
- const parametersForFollowup = originalParameters
309
- ? (() => {
310
- const cloned = cloneJson(originalParameters);
311
- // Followup requests are always non-streaming (servertool orchestration enforces this),
312
- // so remove any inherited stream hints to avoid conflicting flags.
313
- delete cloned.stream;
314
- return cloned;
315
- })()
316
- : undefined;
317
- const assistantMessage = extractAssistantMessage(baseResponse);
318
- const messages = assistantMessage
319
- ? [...originalMessages, assistantMessage]
320
- : [...originalMessages];
321
- messages.push({
322
- role: 'user',
323
- content: text
324
- });
325
- // Build canonical OpenAI Chat payload first (deep cloned).
326
- const chatPayload = {
327
- ...(model ? { model } : {}),
328
- messages,
329
- ...(originalTools ? { tools: originalTools } : {})
330
- };
331
- const normalizedEntry = typeof entryEndpoint === 'string' ? entryEndpoint.trim().toLowerCase() : '';
332
- if (normalizedEntry.includes('/v1/responses')) {
333
- return buildResponsesRequestFromChat(chatPayload, {
334
- stream: false,
335
- ...(parametersForFollowup ? { parameters: parametersForFollowup } : {})
336
- }).request;
337
- }
338
- if (normalizedEntry.includes('/v1/messages')) {
339
- const anthropicChatPayload = {
340
- ...chatPayload,
341
- ...(parametersForFollowup ? parametersForFollowup : {})
342
- };
343
- return buildAnthropicRequestFromOpenAIChat(anthropicChatPayload);
236
+ const content = message.content;
237
+ if (typeof content !== 'string' || !content.trim()) {
238
+ return null;
344
239
  }
345
- const openaiChatPayload = {
346
- ...chatPayload,
347
- ...(parametersForFollowup ? parametersForFollowup : {})
348
- };
349
- return openaiChatPayload;
240
+ return { role: 'assistant', content: content.trim() };
350
241
  }
351
- function extractAssistantMessage(baseResponse) {
352
- if (!baseResponse || typeof baseResponse !== 'object' || Array.isArray(baseResponse)) {
242
+ function getCapturedRequest(adapterContext) {
243
+ if (!adapterContext || typeof adapterContext !== 'object') {
353
244
  return null;
354
245
  }
355
- const base = baseResponse;
356
- const choices = Array.isArray(base.choices) ? base.choices : [];
357
- if (choices.length > 0) {
358
- const first = choices[0] && typeof choices[0] === 'object' && !Array.isArray(choices[0])
359
- ? choices[0]
360
- : null;
361
- const msg = first &&
362
- first.message &&
363
- typeof first.message === 'object' &&
364
- !Array.isArray(first.message)
365
- ? first.message
366
- : null;
367
- if (msg) {
368
- return cloneJson(msg);
369
- }
370
- }
371
- const outputText = extractResponsesOutputText(base);
372
- if (outputText.length > 0) {
373
- return {
374
- role: 'assistant',
375
- content: outputText
376
- };
246
+ const captured = adapterContext.capturedChatRequest;
247
+ if (!captured || typeof captured !== 'object' || Array.isArray(captured)) {
248
+ return null;
377
249
  }
378
- return null;
250
+ return captured;
379
251
  }
380
252
  function extractResponsesOutputText(base) {
381
253
  const raw = base.output_text;
@@ -481,7 +353,7 @@ function hasCompactionFlag(record) {
481
353
  }
482
354
  function resolveImplicitGeminiStopMessageSnapshot(ctx, record) {
483
355
  try {
484
- const protoFromCtx = ctx.options?.providerProtocol;
356
+ const protoFromCtx = ctx.providerProtocol;
485
357
  const protoFromRecord = typeof record.providerProtocol === 'string' && record.providerProtocol.trim()
486
358
  ? String(record.providerProtocol).trim()
487
359
  : undefined;
@@ -1 +1,7 @@
1
- export {};
1
+ import type { ServerSideToolEngineOptions, ServerToolBackendPlan, ServerToolBackendResult } from '../types.js';
2
+ export declare function executeVisionBackendPlan(args: {
3
+ plan: Extract<ServerToolBackendPlan, {
4
+ kind: 'vision_analysis';
5
+ }>;
6
+ options: ServerSideToolEngineOptions;
7
+ }): Promise<ServerToolBackendResult>;