@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.
- package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
- package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
- package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
- package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
- package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
- package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
- package/dist/conversion/hub/pipeline/target-utils.js +9 -5
- package/dist/conversion/hub/process/chat-process.js +256 -16
- package/dist/conversion/hub/response/provider-response.d.ts +8 -0
- package/dist/conversion/hub/response/provider-response.js +85 -27
- package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
- package/dist/conversion/hub/response/response-mappers.js +30 -6
- package/dist/conversion/hub/response/response-runtime.js +4 -38
- package/dist/conversion/hub/snapshot-recorder.js +5 -1
- package/dist/conversion/hub/standardized-bridge.js +23 -15
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
- package/dist/conversion/responses/responses-openai-bridge.js +20 -4
- package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
- package/dist/conversion/shared/gemini-tool-utils.js +580 -108
- package/dist/conversion/shared/jsonish.js +1 -1
- package/dist/conversion/shared/mcp-injection.js +67 -33
- package/dist/conversion/shared/openai-finalizer.js +2 -1
- package/dist/conversion/shared/openai-message-normalize.js +76 -21
- package/dist/conversion/shared/responses-output-builder.js +6 -0
- package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
- package/dist/conversion/shared/runtime-metadata.js +23 -0
- package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer.js +284 -4
- package/dist/conversion/shared/tool-canonicalizer.js +2 -1
- package/dist/conversion/shared/tool-governor.js +3 -3
- package/dist/filters/engine.js +5 -5
- package/dist/filters/special/request-tool-list-filter.js +194 -60
- package/dist/filters/special/request-tools-normalize.js +1 -1
- package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
- package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
- package/dist/filters/special/tool-filter-hooks.js +58 -62
- package/dist/guidance/index.js +5 -1
- package/dist/http/sse-response.js +6 -6
- package/dist/router/virtual-router/bootstrap.js +65 -5
- package/dist/router/virtual-router/context-advisor.d.ts +4 -0
- package/dist/router/virtual-router/context-advisor.js +3 -0
- package/dist/router/virtual-router/context-weighted.d.ts +31 -0
- package/dist/router/virtual-router/context-weighted.js +54 -0
- package/dist/router/virtual-router/engine-health.d.ts +1 -1
- package/dist/router/virtual-router/engine-health.js +11 -110
- package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
- package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
- package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
- package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
- package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
- package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
- package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
- package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
- package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
- package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
- package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
- package/dist/router/virtual-router/engine-selection.d.ts +4 -30
- package/dist/router/virtual-router/engine-selection.js +10 -815
- package/dist/router/virtual-router/engine.d.ts +1 -0
- package/dist/router/virtual-router/engine.js +55 -10
- package/dist/router/virtual-router/routing-instructions.js +6 -1
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
- package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
- package/dist/router/virtual-router/types.d.ts +53 -1
- package/dist/servertool/clock/config.d.ts +8 -0
- package/dist/servertool/clock/config.js +22 -0
- package/dist/servertool/clock/log.d.ts +3 -0
- package/dist/servertool/clock/log.js +13 -0
- package/dist/servertool/clock/task-store.d.ts +1 -1
- package/dist/servertool/clock/task-store.js +1 -1
- package/dist/servertool/clock/tasks.js +1 -1
- package/dist/servertool/engine.js +146 -21
- package/dist/servertool/handlers/clock-auto.js +11 -6
- package/dist/servertool/handlers/clock.js +36 -10
- package/dist/servertool/handlers/followup-request-builder.js +8 -2
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
- package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
- package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
- package/dist/servertool/handlers/stop-message-auto.js +100 -10
- package/dist/servertool/handlers/vision.js +4 -1
- package/dist/servertool/handlers/web-search.js +3 -1
- package/dist/servertool/pending-session.d.ts +19 -0
- package/dist/servertool/pending-session.js +97 -0
- package/dist/servertool/reenter-backend.js +5 -3
- package/dist/servertool/server-side-tools.js +235 -6
- package/dist/servertool/types.d.ts +13 -0
- package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
- package/dist/sse/shared/chat-serializer.js +2 -2
- package/dist/sse/shared/constants.js +1 -1
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
- package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
- package/dist/tools/apply-patch/execution-capturer.js +1 -1
- package/dist/tools/exec-command/normalize.js +4 -0
- package/dist/tools/exec-command/regression-capturer.js +1 -1
- 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 {
|
|
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
|
|
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 {
|
|
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 =
|
|
94
|
-
|
|
95
|
-
|
|
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
|
|
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 (!
|
|
173
|
+
if (!sessionId) {
|
|
174
|
+
logClock('missing_session', { action });
|
|
156
175
|
return respond({
|
|
157
176
|
ok: false,
|
|
158
177
|
action,
|
|
159
|
-
message: 'clock
|
|
178
|
+
message: 'clock requires sessionId (x-session-id header or metadata.sessionId).'
|
|
160
179
|
});
|
|
161
180
|
}
|
|
162
|
-
|
|
163
|
-
|
|
181
|
+
if (!clockConfig) {
|
|
182
|
+
logClock('disabled', { action, hasSessionId: true });
|
|
164
183
|
return respond({
|
|
165
184
|
ok: false,
|
|
166
185
|
action,
|
|
167
|
-
message: 'clock
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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(
|
|
254
|
-
const flag =
|
|
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 =
|
|
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(
|
|
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(
|
|
82
|
-
const flag =
|
|
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
|
|
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 =
|
|
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 =
|
|
30
|
+
const followupFlagRaw = rt?.serverToolFollowup;
|
|
28
31
|
if (followupFlagRaw === true ||
|
|
29
32
|
(typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true')) {
|
|
30
|
-
|
|
31
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
94
|
-
|
|
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(
|
|
345
|
-
const flag =
|
|
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
|
|
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
|
|
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,
|