@jsonstudio/llms 0.6.954 → 0.6.1172
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/hub/operation-table/operation-table-runner.d.ts +18 -0
- package/dist/conversion/hub/operation-table/operation-table-runner.js +158 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +303 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.d.ts +8 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +413 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.d.ts +7 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +841 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.d.ts +21 -0
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +535 -0
- package/dist/conversion/hub/ops/operations.d.ts +19 -0
- package/dist/conversion/hub/ops/operations.js +126 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +9 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +489 -19
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +11 -0
- package/dist/conversion/hub/policy/policy-engine.js +41 -9
- package/dist/conversion/hub/policy/protocol-spec.d.ts +25 -0
- package/dist/conversion/hub/policy/protocol-spec.js +73 -23
- package/dist/conversion/hub/process/chat-process.js +252 -41
- package/dist/conversion/hub/response/provider-response.js +175 -2
- package/dist/conversion/hub/response/response-runtime.js +1 -1
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -8
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -365
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -8
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -467
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -903
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -21
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -593
- package/dist/conversion/hub/tool-surface/tool-surface-engine.d.ts +18 -0
- package/dist/conversion/hub/tool-surface/tool-surface-engine.js +571 -0
- package/dist/conversion/responses/responses-openai-bridge.js +14 -2
- package/dist/conversion/shared/bridge-message-utils.js +2 -8
- package/dist/conversion/shared/bridge-policies.js +5 -105
- package/dist/conversion/shared/gemini-tool-utils.js +89 -15
- package/dist/conversion/shared/protocol-field-allowlists.d.ts +7 -0
- package/dist/conversion/shared/protocol-field-allowlists.js +145 -0
- package/dist/conversion/shared/reasoning-tool-normalizer.js +4 -2
- package/dist/conversion/shared/snapshot-hooks.js +166 -3
- package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer.js +345 -9
- package/dist/conversion/shared/thought-signature-validator.d.ts +52 -0
- package/dist/conversion/shared/thought-signature-validator.js +170 -0
- package/dist/conversion/shared/tool-argument-repairer.d.ts +39 -0
- package/dist/conversion/shared/tool-argument-repairer.js +56 -0
- package/dist/conversion/shared/tool-call-id-manager.d.ts +113 -0
- package/dist/conversion/shared/tool-call-id-manager.js +231 -0
- package/dist/conversion/shared/tool-canonicalizer.js +2 -11
- package/dist/router/virtual-router/bootstrap.js +70 -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-selection.js +284 -47
- package/dist/router/virtual-router/engine.d.ts +3 -0
- package/dist/router/virtual-router/engine.js +142 -33
- package/dist/router/virtual-router/health-weighted.d.ts +25 -0
- package/dist/router/virtual-router/health-weighted.js +63 -0
- package/dist/router/virtual-router/load-balancer.d.ts +2 -0
- package/dist/router/virtual-router/load-balancer.js +45 -16
- package/dist/router/virtual-router/routing-instructions.js +17 -1
- package/dist/router/virtual-router/sticky-session-store.js +136 -24
- package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
- package/dist/router/virtual-router/stop-message-file-resolver.js +74 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +15 -0
- package/dist/router/virtual-router/stop-message-state-sync.js +57 -0
- package/dist/router/virtual-router/types.d.ts +98 -0
- package/dist/servertool/clock/config.d.ts +7 -0
- package/dist/servertool/clock/config.js +27 -0
- package/dist/servertool/clock/daemon.d.ts +3 -0
- package/dist/servertool/clock/daemon.js +79 -0
- package/dist/servertool/clock/io.d.ts +2 -0
- package/dist/servertool/clock/io.js +13 -0
- package/dist/servertool/clock/paths.d.ts +4 -0
- package/dist/servertool/clock/paths.js +25 -0
- package/dist/servertool/clock/session-store.d.ts +3 -0
- package/dist/servertool/clock/session-store.js +56 -0
- package/dist/servertool/clock/state.d.ts +5 -0
- package/dist/servertool/clock/state.js +62 -0
- package/dist/servertool/clock/task-store.d.ts +5 -0
- package/dist/servertool/clock/task-store.js +4 -0
- package/dist/servertool/clock/tasks.d.ts +17 -0
- package/dist/servertool/clock/tasks.js +221 -0
- package/dist/servertool/clock/types.d.ts +36 -0
- package/dist/servertool/clock/types.js +1 -0
- package/dist/servertool/engine.d.ts +2 -0
- package/dist/servertool/engine.js +161 -7
- package/dist/servertool/followup-shadow.d.ts +16 -0
- package/dist/servertool/followup-shadow.js +145 -0
- package/dist/servertool/handlers/apply-patch-guard.js +1 -265
- package/dist/servertool/handlers/clock-auto.d.ts +1 -0
- package/dist/servertool/handlers/clock-auto.js +160 -0
- package/dist/servertool/handlers/clock.d.ts +1 -0
- package/dist/servertool/handlers/clock.js +197 -0
- package/dist/servertool/handlers/exec-command-guard.js +7 -555
- package/dist/servertool/handlers/followup-request-builder.d.ts +15 -7
- package/dist/servertool/handlers/followup-request-builder.js +248 -28
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +62 -169
- package/dist/servertool/handlers/iflow-model-error-retry.js +18 -28
- package/dist/servertool/handlers/recursive-detection-guard.d.ts +1 -0
- package/dist/servertool/handlers/recursive-detection-guard.js +333 -0
- package/dist/servertool/handlers/stop-message-auto.js +47 -175
- package/dist/servertool/handlers/vision.d.ts +7 -1
- package/dist/servertool/handlers/vision.js +61 -117
- package/dist/servertool/handlers/web-search.d.ts +7 -1
- package/dist/servertool/handlers/web-search.js +122 -105
- package/dist/servertool/reenter-backend.d.ts +23 -0
- package/dist/servertool/reenter-backend.js +18 -0
- package/dist/servertool/server-side-tools.d.ts +3 -2
- package/dist/servertool/server-side-tools.js +64 -10
- package/dist/servertool/types.d.ts +92 -3
- package/dist/sse/json-to-sse/event-generators/responses.js +3 -21
- package/dist/sse/shared/serializers/responses-event-serializer.d.ts +8 -0
- package/dist/sse/shared/serializers/responses-event-serializer.js +19 -0
- package/dist/sse/shared/writer.js +24 -7
- package/dist/tools/apply-patch/execution-capturer.js +3 -1
- package/dist/tools/apply-patch/json/parse-loose.d.ts +3 -0
- package/dist/tools/apply-patch/json/parse-loose.js +139 -0
- package/dist/tools/apply-patch/patch-text/context-diff.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/context-diff.js +173 -0
- package/dist/tools/apply-patch/patch-text/git-diff.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/git-diff.js +138 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.d.ts +1 -0
- package/dist/tools/apply-patch/patch-text/looks-like-patch.js +13 -0
- package/dist/tools/apply-patch/patch-text/normalize.d.ts +3 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +262 -0
- package/dist/tools/apply-patch/structured/coercion.d.ts +3 -0
- package/dist/tools/apply-patch/structured/coercion.js +82 -0
- package/dist/tools/apply-patch/validation/shared.d.ts +3 -0
- package/dist/tools/apply-patch/validation/shared.js +6 -0
- package/dist/tools/apply-patch/validator.d.ts +2 -2
- package/dist/tools/apply-patch/validator.js +6 -556
- 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 {
|
|
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
|
|
119
|
-
if (!
|
|
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
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
|
218
|
-
if (!
|
|
214
|
+
function extractAssistantMessageForFollowup(chatResponse) {
|
|
215
|
+
if (!chatResponse || typeof chatResponse !== 'object' || Array.isArray(chatResponse)) {
|
|
219
216
|
return null;
|
|
220
217
|
}
|
|
221
|
-
const
|
|
222
|
-
|
|
218
|
+
const choices = Array.isArray(chatResponse.choices)
|
|
219
|
+
? chatResponse.choices
|
|
220
|
+
: [];
|
|
221
|
+
if (!choices.length) {
|
|
223
222
|
return null;
|
|
224
223
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
245
|
-
|
|
246
|
-
|
|
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
|
-
|
|
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
|
|
274
|
-
|
|
275
|
-
|
|
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
|
-
|
|
346
|
-
...chatPayload,
|
|
347
|
-
...(parametersForFollowup ? parametersForFollowup : {})
|
|
348
|
-
};
|
|
349
|
-
return openaiChatPayload;
|
|
240
|
+
return { role: 'assistant', content: content.trim() };
|
|
350
241
|
}
|
|
351
|
-
function
|
|
352
|
-
if (!
|
|
242
|
+
function getCapturedRequest(adapterContext) {
|
|
243
|
+
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
353
244
|
return null;
|
|
354
245
|
}
|
|
355
|
-
const
|
|
356
|
-
|
|
357
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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>;
|