@jsonstudio/llms 0.6.938 → 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.
- 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 +533 -24
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +6 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +6 -3
- 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 -436
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -7
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -894
- 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 +121 -4
- 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 +54 -5
- package/dist/router/virtual-router/engine-selection.js +132 -42
- 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 +70 -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 +164 -8
- 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
|
@@ -3,6 +3,9 @@ import { ProviderProtocolError } from '../conversion/shared/errors.js';
|
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateSync } from '../router/virtual-router/sticky-session-store.js';
|
|
5
5
|
import { deserializeRoutingInstructionState, serializeRoutingInstructionState } from '../router/virtual-router/routing-instructions.js';
|
|
6
|
+
import { applyHubFollowupPolicyShadow } from './followup-shadow.js';
|
|
7
|
+
import { buildServerToolFollowupChatPayloadFromInjection } from './handlers/followup-request-builder.js';
|
|
8
|
+
import { findNextUndeliveredDueAtMs, listClockTasks, normalizeClockConfig } from './clock/task-store.js';
|
|
6
9
|
function parseTimeoutMs(raw, fallback) {
|
|
7
10
|
const n = typeof raw === 'string' ? Number(raw.trim()) : typeof raw === 'number' ? raw : NaN;
|
|
8
11
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -153,8 +156,108 @@ function isEmptyClientResponsePayload(payload) {
|
|
|
153
156
|
}
|
|
154
157
|
return true;
|
|
155
158
|
}
|
|
159
|
+
function isStopFinishReasonWithoutToolCalls(base) {
|
|
160
|
+
if (!base || typeof base !== 'object' || Array.isArray(base)) {
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
const payload = base;
|
|
164
|
+
const choicesRaw = payload.choices;
|
|
165
|
+
if (Array.isArray(choicesRaw) && choicesRaw.length) {
|
|
166
|
+
const first = choicesRaw[0];
|
|
167
|
+
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
const finishReasonRaw = first.finish_reason;
|
|
171
|
+
const finishReason = typeof finishReasonRaw === 'string' && finishReasonRaw.trim()
|
|
172
|
+
? finishReasonRaw.trim().toLowerCase()
|
|
173
|
+
: '';
|
|
174
|
+
if (!finishReason || finishReason === 'tool_calls') {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
if (finishReason !== 'stop' && finishReason !== 'length') {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
const message = first.message &&
|
|
181
|
+
typeof first.message === 'object' &&
|
|
182
|
+
!Array.isArray(first.message)
|
|
183
|
+
? first.message
|
|
184
|
+
: null;
|
|
185
|
+
if (!message) {
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
const toolCalls = Array.isArray(message.tool_calls) ? message.tool_calls : [];
|
|
189
|
+
if (toolCalls.length > 0) {
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
// Responses-like: completed without required_action generally counts as stop.
|
|
195
|
+
const statusRaw = typeof payload.status === 'string' ? payload.status.trim().toLowerCase() : '';
|
|
196
|
+
if (statusRaw && statusRaw !== 'completed') {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
if (payload.required_action && typeof payload.required_action === 'object') {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
async function shouldDisableServerToolTimeoutForClockHold(args) {
|
|
205
|
+
// Only relevant for stop/length responses: clock_auto may hold indefinitely.
|
|
206
|
+
if (!isStopFinishReasonWithoutToolCalls(args.chat)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const record = args.adapterContext;
|
|
210
|
+
const clockConfig = normalizeClockConfig(record.clock);
|
|
211
|
+
if (!clockConfig) {
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
const sessionId = typeof record.sessionId === 'string' ? record.sessionId.trim() : '';
|
|
215
|
+
if (!sessionId) {
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
// If already within due window, clock_auto won't need long hold.
|
|
219
|
+
try {
|
|
220
|
+
const tasks = await listClockTasks(sessionId, clockConfig);
|
|
221
|
+
const at = Date.now();
|
|
222
|
+
const nextDueAtMs = findNextUndeliveredDueAtMs(tasks, at);
|
|
223
|
+
if (!nextDueAtMs) {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
|
|
227
|
+
if (thresholdMs <= at) {
|
|
228
|
+
return false;
|
|
229
|
+
}
|
|
230
|
+
// Only disable when the wait exceeds current timeout.
|
|
231
|
+
if (args.serverToolTimeoutMs > 0 && thresholdMs - at <= args.serverToolTimeoutMs) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
156
240
|
export async function runServerToolOrchestration(options) {
|
|
241
|
+
const YELLOW = '\x1b[38;5;214m';
|
|
242
|
+
const RESET = '\x1b[0m';
|
|
243
|
+
const logProgress = (step, total, message, extra) => {
|
|
244
|
+
try {
|
|
245
|
+
// eslint-disable-next-line no-console
|
|
246
|
+
console.log(`${YELLOW}[servertool][progress ${step}/${total}] requestId=${options.requestId} ${message}` +
|
|
247
|
+
(extra ? ` ${JSON.stringify(extra)}` : '') +
|
|
248
|
+
RESET);
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* best-effort logging */
|
|
252
|
+
}
|
|
253
|
+
};
|
|
157
254
|
const serverToolTimeoutMs = resolveServerToolTimeoutMs();
|
|
255
|
+
const shouldDisableTimeout = await shouldDisableServerToolTimeoutForClockHold({
|
|
256
|
+
chat: options.chat,
|
|
257
|
+
adapterContext: options.adapterContext,
|
|
258
|
+
serverToolTimeoutMs
|
|
259
|
+
});
|
|
260
|
+
const effectiveServerToolTimeoutMs = shouldDisableTimeout ? 0 : serverToolTimeoutMs;
|
|
158
261
|
const followupTimeoutMs = resolveServerToolFollowupTimeoutMs(serverToolTimeoutMs);
|
|
159
262
|
const engineOptions = {
|
|
160
263
|
chatResponse: options.chat,
|
|
@@ -165,10 +268,10 @@ export async function runServerToolOrchestration(options) {
|
|
|
165
268
|
providerInvoker: options.providerInvoker,
|
|
166
269
|
reenterPipeline: options.reenterPipeline
|
|
167
270
|
};
|
|
168
|
-
const engineResult = await withTimeout(runServerSideToolEngine(engineOptions),
|
|
271
|
+
const engineResult = await withTimeout(runServerSideToolEngine(engineOptions), effectiveServerToolTimeoutMs, () => createServerToolTimeoutError({
|
|
169
272
|
requestId: options.requestId,
|
|
170
273
|
phase: 'engine',
|
|
171
|
-
timeoutMs: serverToolTimeoutMs
|
|
274
|
+
timeoutMs: effectiveServerToolTimeoutMs || serverToolTimeoutMs
|
|
172
275
|
}));
|
|
173
276
|
if (engineResult.mode === 'passthrough' || !engineResult.execution) {
|
|
174
277
|
return {
|
|
@@ -176,7 +279,11 @@ export async function runServerToolOrchestration(options) {
|
|
|
176
279
|
executed: false
|
|
177
280
|
};
|
|
178
281
|
}
|
|
282
|
+
const flowId = engineResult.execution.flowId ?? 'unknown';
|
|
283
|
+
const totalSteps = 5;
|
|
284
|
+
logProgress(1, totalSteps, 'matched', { flowId });
|
|
179
285
|
if (!engineResult.execution.followup || !options.reenterPipeline) {
|
|
286
|
+
logProgress(5, totalSteps, 'completed (no followup)', { flowId });
|
|
180
287
|
return {
|
|
181
288
|
chat: engineResult.finalChatResponse,
|
|
182
289
|
executed: true,
|
|
@@ -185,14 +292,53 @@ export async function runServerToolOrchestration(options) {
|
|
|
185
292
|
}
|
|
186
293
|
const isStopMessageFlow = engineResult.execution.flowId === 'stop_message_flow';
|
|
187
294
|
const isGeminiEmptyReplyContinue = engineResult.execution.flowId === 'gemini_empty_reply_continue';
|
|
295
|
+
const isApplyPatchGuard = engineResult.execution.flowId === 'apply_patch_guard';
|
|
296
|
+
const isExecCommandGuard = engineResult.execution.flowId === 'exec_command_guard';
|
|
188
297
|
const stopMessageSource = isStopMessageFlow ? getStopMessageSource(options.adapterContext) : undefined;
|
|
189
298
|
const isAutoStopMessage = isStopMessageFlow && stopMessageSource !== 'explicit';
|
|
190
299
|
const isErrorAutoFlow = engineResult.execution.flowId === 'iflow_model_error_retry';
|
|
191
|
-
const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow;
|
|
300
|
+
const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow || isGeminiEmptyReplyContinue || isApplyPatchGuard || isExecCommandGuard;
|
|
192
301
|
// ServerTool followups must not inherit or inject any routeHint; always route fresh.
|
|
193
302
|
const preserveRouteHint = false;
|
|
194
|
-
const
|
|
303
|
+
const followupPlan = engineResult.execution.followup;
|
|
304
|
+
const followupEntryEndpoint = engineResult.execution.followup.entryEndpoint ||
|
|
305
|
+
options.entryEndpoint ||
|
|
306
|
+
'/v1/chat/completions';
|
|
307
|
+
const followupPayloadRaw = (() => {
|
|
308
|
+
if (followupPlan &&
|
|
309
|
+
typeof followupPlan === 'object' &&
|
|
310
|
+
!Array.isArray(followupPlan) &&
|
|
311
|
+
Object.prototype.hasOwnProperty.call(followupPlan, 'payload')) {
|
|
312
|
+
const candidate = followupPlan.payload;
|
|
313
|
+
return candidate && typeof candidate === 'object' && !Array.isArray(candidate) ? candidate : null;
|
|
314
|
+
}
|
|
315
|
+
if (followupPlan &&
|
|
316
|
+
typeof followupPlan === 'object' &&
|
|
317
|
+
!Array.isArray(followupPlan) &&
|
|
318
|
+
Object.prototype.hasOwnProperty.call(followupPlan, 'injection')) {
|
|
319
|
+
const injection = followupPlan.injection;
|
|
320
|
+
if (!injection || typeof injection !== 'object' || Array.isArray(injection)) {
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
return buildServerToolFollowupChatPayloadFromInjection({
|
|
324
|
+
adapterContext: options.adapterContext,
|
|
325
|
+
chatResponse: engineResult.finalChatResponse,
|
|
326
|
+
injection: injection
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return null;
|
|
330
|
+
})();
|
|
331
|
+
if (!followupPayloadRaw) {
|
|
332
|
+
logProgress(5, totalSteps, 'completed (missing followup payload)', { flowId });
|
|
333
|
+
return {
|
|
334
|
+
chat: engineResult.finalChatResponse,
|
|
335
|
+
executed: true,
|
|
336
|
+
flowId: engineResult.execution.flowId
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, followupPayloadRaw);
|
|
195
340
|
if (applyAutoLimit && loopState && typeof loopState.repeatCount === 'number' && loopState.repeatCount >= 3) {
|
|
341
|
+
logProgress(5, totalSteps, 'completed (auto limit hit)', { flowId });
|
|
196
342
|
return {
|
|
197
343
|
chat: engineResult.finalChatResponse,
|
|
198
344
|
executed: true,
|
|
@@ -200,21 +346,22 @@ export async function runServerToolOrchestration(options) {
|
|
|
200
346
|
};
|
|
201
347
|
}
|
|
202
348
|
if (isAdapterClientDisconnected(options.adapterContext)) {
|
|
349
|
+
logProgress(5, totalSteps, 'completed (client disconnected)', { flowId });
|
|
203
350
|
return {
|
|
204
351
|
chat: engineResult.finalChatResponse,
|
|
205
352
|
executed: true,
|
|
206
353
|
flowId: engineResult.execution.flowId
|
|
207
354
|
};
|
|
208
355
|
}
|
|
209
|
-
const followupEntryEndpoint = engineResult.execution.followup.entryEndpoint ||
|
|
210
|
-
options.entryEndpoint ||
|
|
211
|
-
'/v1/chat/completions';
|
|
212
356
|
const metadata = {
|
|
213
357
|
serverToolFollowup: true,
|
|
214
358
|
stream: false,
|
|
215
359
|
...(loopState ? { serverToolLoopState: loopState } : {}),
|
|
216
360
|
...(engineResult.execution.followup.metadata ?? {})
|
|
217
361
|
};
|
|
362
|
+
// Followup re-enters HubPipeline at chat-process entry with a canonical "chat-like" body.
|
|
363
|
+
// This avoids re-running per-protocol inbound parse/semantic-map for each client protocol.
|
|
364
|
+
metadata.__hubEntry = 'chat_process';
|
|
218
365
|
// Enforce unified followup contract:
|
|
219
366
|
// - clear any inherited routeHint
|
|
220
367
|
// - do not inherit sticky target
|
|
@@ -230,7 +377,14 @@ export async function runServerToolOrchestration(options) {
|
|
|
230
377
|
const retryEmptyFollowupOnce = isStopMessageFlow || isGeminiEmptyReplyContinue;
|
|
231
378
|
const maxAttempts = retryEmptyFollowupOnce ? 2 : 1;
|
|
232
379
|
const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
|
|
233
|
-
|
|
380
|
+
let followupPayload = coerceFollowupPayloadStream(followupPayloadRaw, metadata.stream === true);
|
|
381
|
+
followupPayload = applyHubFollowupPolicyShadow({
|
|
382
|
+
requestId: followupRequestId,
|
|
383
|
+
entryEndpoint: followupEntryEndpoint,
|
|
384
|
+
flowId: engineResult.execution.flowId,
|
|
385
|
+
payload: followupPayload,
|
|
386
|
+
stageRecorder: options.stageRecorder
|
|
387
|
+
});
|
|
234
388
|
let followup;
|
|
235
389
|
let lastError;
|
|
236
390
|
// stopMessage 是一种“状态型” servertool:一旦触发,我们需要尽量避免因 followup 失败而把状态留在可继续触发的位置,
|
|
@@ -301,6 +455,7 @@ export async function runServerToolOrchestration(options) {
|
|
|
301
455
|
// 对 stopMessage:避免把 empty followup 升级为 502,直接清理 stopMessage 状态并返回原始响应。
|
|
302
456
|
// 这样客户端至少能拿到本轮输出,且 stopMessage 不会在后续请求里继续触发导致“永远 502”。
|
|
303
457
|
disableStopMessageAfterFailedFollowup(options.adapterContext, stopMessageReservation);
|
|
458
|
+
logProgress(5, totalSteps, 'completed (stopMessage followup empty; cleaned state)', { flowId });
|
|
304
459
|
return {
|
|
305
460
|
chat: engineResult.finalChatResponse,
|
|
306
461
|
executed: true,
|
|
@@ -321,6 +476,7 @@ export async function runServerToolOrchestration(options) {
|
|
|
321
476
|
throw wrapped;
|
|
322
477
|
}
|
|
323
478
|
const decorated = decorateFinalChatWithServerToolContext(followupBody ?? engineResult.finalChatResponse, engineResult.execution);
|
|
479
|
+
logProgress(5, totalSteps, 'completed', { flowId });
|
|
324
480
|
return {
|
|
325
481
|
chat: decorated,
|
|
326
482
|
executed: true,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { StageRecorder } from '../conversion/hub/format-adapters/index.js';
|
|
2
|
+
import type { JsonObject } from '../conversion/hub/types/json.js';
|
|
3
|
+
export type HubFollowupMode = 'off' | 'shadow' | 'enforce';
|
|
4
|
+
export interface HubFollowupConfig {
|
|
5
|
+
mode: HubFollowupMode;
|
|
6
|
+
sampleRate?: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function resolveHubFollowupConfigFromEnv(): HubFollowupConfig;
|
|
9
|
+
export declare function applyHubFollowupPolicyShadow(args: {
|
|
10
|
+
config?: HubFollowupConfig;
|
|
11
|
+
requestId?: string;
|
|
12
|
+
entryEndpoint?: string;
|
|
13
|
+
flowId?: string;
|
|
14
|
+
payload: JsonObject;
|
|
15
|
+
stageRecorder?: StageRecorder;
|
|
16
|
+
}): JsonObject;
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { isJsonObject, jsonClone } from '../conversion/hub/types/json.js';
|
|
2
|
+
function clampSampleRate(value) {
|
|
3
|
+
const num = typeof value === 'number' && Number.isFinite(value) ? value : 1;
|
|
4
|
+
if (num <= 0)
|
|
5
|
+
return 0;
|
|
6
|
+
if (num >= 1)
|
|
7
|
+
return 1;
|
|
8
|
+
return num;
|
|
9
|
+
}
|
|
10
|
+
function fnv1a32(input) {
|
|
11
|
+
let hash = 0x811c9dc5;
|
|
12
|
+
for (let i = 0; i < input.length; i++) {
|
|
13
|
+
hash ^= input.charCodeAt(i);
|
|
14
|
+
hash = (hash * 0x01000193) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
return hash >>> 0;
|
|
17
|
+
}
|
|
18
|
+
function shouldSample(config, requestId) {
|
|
19
|
+
const rate = clampSampleRate(config.sampleRate);
|
|
20
|
+
if (rate <= 0)
|
|
21
|
+
return false;
|
|
22
|
+
if (rate >= 1)
|
|
23
|
+
return true;
|
|
24
|
+
const key = typeof requestId === 'string' && requestId.trim().length ? requestId.trim() : 'no_request_id';
|
|
25
|
+
const bucket = fnv1a32(key) / 0xffffffff;
|
|
26
|
+
return bucket < rate;
|
|
27
|
+
}
|
|
28
|
+
function readMode(raw) {
|
|
29
|
+
const normalized = raw.trim().toLowerCase();
|
|
30
|
+
if (!normalized)
|
|
31
|
+
return null;
|
|
32
|
+
if (normalized === 'off' || normalized === '0' || normalized === 'false')
|
|
33
|
+
return 'off';
|
|
34
|
+
if (normalized === 'shadow')
|
|
35
|
+
return 'shadow';
|
|
36
|
+
if (normalized === 'enforce')
|
|
37
|
+
return 'enforce';
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
export function resolveHubFollowupConfigFromEnv() {
|
|
41
|
+
const raw = String(process.env.ROUTECODEX_HUB_FOLLOWUP_MODE || '').trim();
|
|
42
|
+
const mode = readMode(raw) ?? 'off';
|
|
43
|
+
const sampleRateRaw = String(process.env.ROUTECODEX_HUB_FOLLOWUP_SAMPLE_RATE || '').trim();
|
|
44
|
+
const sampleRate = sampleRateRaw ? Number(sampleRateRaw) : undefined;
|
|
45
|
+
return {
|
|
46
|
+
mode,
|
|
47
|
+
...(Number.isFinite(sampleRate) ? { sampleRate } : {})
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function dropKeyByPrefix(root, prefixes) {
|
|
51
|
+
for (const key of Object.keys(root)) {
|
|
52
|
+
if (prefixes.some((p) => key.startsWith(p))) {
|
|
53
|
+
try {
|
|
54
|
+
delete root[key];
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
root[key] = undefined;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function normalizeFollowupPayload(payload) {
|
|
63
|
+
const out = jsonClone(payload);
|
|
64
|
+
const record = out;
|
|
65
|
+
// Followup requests must be non-streaming and must not carry route hints.
|
|
66
|
+
if (record.stream !== undefined) {
|
|
67
|
+
record.stream = false;
|
|
68
|
+
}
|
|
69
|
+
if (record.routeHint !== undefined) {
|
|
70
|
+
delete record.routeHint;
|
|
71
|
+
}
|
|
72
|
+
if (record.route_hint !== undefined) {
|
|
73
|
+
delete record.route_hint;
|
|
74
|
+
}
|
|
75
|
+
// Remove internal/private carriers from the body (metadata belongs to request metadata, not body).
|
|
76
|
+
dropKeyByPrefix(record, ['__']);
|
|
77
|
+
const parameters = record.parameters;
|
|
78
|
+
if (isJsonObject(parameters)) {
|
|
79
|
+
const params = parameters;
|
|
80
|
+
if (params.stream !== undefined) {
|
|
81
|
+
delete params.stream;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
function diffPayloads(baseline, candidate, p = '<root>') {
|
|
87
|
+
if (Object.is(baseline, candidate))
|
|
88
|
+
return [];
|
|
89
|
+
if (typeof baseline !== typeof candidate)
|
|
90
|
+
return [{ path: p, baseline, candidate }];
|
|
91
|
+
if (Array.isArray(baseline) && Array.isArray(candidate)) {
|
|
92
|
+
const max = Math.max(baseline.length, candidate.length);
|
|
93
|
+
const diffs = [];
|
|
94
|
+
for (let i = 0; i < max; i += 1) {
|
|
95
|
+
diffs.push(...diffPayloads(baseline[i], candidate[i], `${p}[${i}]`));
|
|
96
|
+
}
|
|
97
|
+
return diffs;
|
|
98
|
+
}
|
|
99
|
+
if (baseline && typeof baseline === 'object' && candidate && typeof candidate === 'object') {
|
|
100
|
+
const a = baseline;
|
|
101
|
+
const b = candidate;
|
|
102
|
+
const keys = new Set([...Object.keys(a), ...Object.keys(b)]);
|
|
103
|
+
const diffs = [];
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
const next = p === '<root>' ? key : `${p}.${key}`;
|
|
106
|
+
if (!(key in b))
|
|
107
|
+
diffs.push({ path: next, baseline: a[key], candidate: undefined });
|
|
108
|
+
else if (!(key in a))
|
|
109
|
+
diffs.push({ path: next, baseline: undefined, candidate: b[key] });
|
|
110
|
+
else
|
|
111
|
+
diffs.push(...diffPayloads(a[key], b[key], next));
|
|
112
|
+
}
|
|
113
|
+
return diffs;
|
|
114
|
+
}
|
|
115
|
+
return [{ path: p, baseline, candidate }];
|
|
116
|
+
}
|
|
117
|
+
export function applyHubFollowupPolicyShadow(args) {
|
|
118
|
+
const cfg = args.config ?? resolveHubFollowupConfigFromEnv();
|
|
119
|
+
if (!cfg || cfg.mode === 'off') {
|
|
120
|
+
return args.payload;
|
|
121
|
+
}
|
|
122
|
+
if (!shouldSample(cfg, args.requestId)) {
|
|
123
|
+
return args.payload;
|
|
124
|
+
}
|
|
125
|
+
const candidate = normalizeFollowupPayload(args.payload);
|
|
126
|
+
const diffs = diffPayloads(args.payload, candidate);
|
|
127
|
+
if (diffs.length > 0) {
|
|
128
|
+
const stage = `hub_followup.${cfg.mode}.payload`;
|
|
129
|
+
args.stageRecorder?.record(stage, {
|
|
130
|
+
kind: 'hub_followup_payload_shadow',
|
|
131
|
+
requestId: args.requestId,
|
|
132
|
+
entryEndpoint: args.entryEndpoint,
|
|
133
|
+
flowId: args.flowId,
|
|
134
|
+
diffCount: diffs.length,
|
|
135
|
+
diffPaths: diffs.slice(0, 50).map((d) => d.path),
|
|
136
|
+
diffHead: diffs.slice(0, 50).map((d) => ({ path: d.path, baseline: d.baseline, candidate: d.candidate })),
|
|
137
|
+
baseline: jsonClone(args.payload),
|
|
138
|
+
candidate: jsonClone(candidate)
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
if (cfg.mode === 'enforce') {
|
|
142
|
+
return candidate;
|
|
143
|
+
}
|
|
144
|
+
return args.payload;
|
|
145
|
+
}
|
|
@@ -1,269 +1,5 @@
|
|
|
1
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
2
|
-
import { cloneJson } from '../server-side-tools.js';
|
|
3
|
-
import { validateToolCall } from '../../tools/tool-registry.js';
|
|
4
|
-
import { buildEntryAwareFollowupPayload, extractCapturedChatSeed } from './followup-request-builder.js';
|
|
5
|
-
const FLOW_ID = 'apply_patch_guard';
|
|
6
2
|
const handler = async (ctx) => {
|
|
7
|
-
|
|
8
|
-
if (!toolCall) {
|
|
9
|
-
return null;
|
|
10
|
-
}
|
|
11
|
-
if (!ctx.options.reenterPipeline) {
|
|
12
|
-
return null;
|
|
13
|
-
}
|
|
14
|
-
const rawArgs = typeof toolCall.arguments === 'string' ? toolCall.arguments : '';
|
|
15
|
-
const validation = validateToolCall('apply_patch', rawArgs);
|
|
16
|
-
if (validation?.ok) {
|
|
17
|
-
return null;
|
|
18
|
-
}
|
|
19
|
-
const reason = typeof validation?.reason === 'string' && validation.reason.trim()
|
|
20
|
-
? validation.reason.trim()
|
|
21
|
-
: 'unknown';
|
|
22
|
-
const snippet = rawArgs && rawArgs.trim().length
|
|
23
|
-
? rawArgs.trim().slice(0, 200).replace(/\s+/g, ' ')
|
|
24
|
-
: '';
|
|
25
|
-
const isExecCommandShape = extractLikelyExecCommandShape(rawArgs);
|
|
26
|
-
// For obviously wrong shapes (e.g. exec_command-like payload passed into apply_patch),
|
|
27
|
-
// do NOT attempt a followup "recovery" loop. Return a plain assistant message instead.
|
|
28
|
-
if (isExecCommandShape && (reason === 'missing_changes' || reason === 'missing_payload' || reason === 'invalid_json')) {
|
|
29
|
-
const patched = injectApplyPatchHardFailureMessage(ctx.base, toolCall, {
|
|
30
|
-
reason,
|
|
31
|
-
snippet,
|
|
32
|
-
rawArgs
|
|
33
|
-
});
|
|
34
|
-
return {
|
|
35
|
-
chatResponse: patched,
|
|
36
|
-
execution: { flowId: FLOW_ID }
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
const patched = injectApplyPatchRejectedToolResult(ctx.base, toolCall, {
|
|
40
|
-
reason,
|
|
41
|
-
snippet,
|
|
42
|
-
rawArgs
|
|
43
|
-
});
|
|
44
|
-
const followupPayload = buildToolFollowupPayload(ctx.adapterContext, patched, ctx.options.entryEndpoint || ctx.adapterContext?.entryEndpoint || '/v1/chat/completions');
|
|
45
|
-
if (!followupPayload) {
|
|
46
|
-
// Fail-closed: if we cannot perform reenter, do not intercept.
|
|
47
|
-
// (This should be rare because HubPipeline always provides capturedChatRequest.)
|
|
48
|
-
return null;
|
|
49
|
-
}
|
|
50
|
-
return {
|
|
51
|
-
chatResponse: patched,
|
|
52
|
-
execution: {
|
|
53
|
-
flowId: FLOW_ID,
|
|
54
|
-
followup: {
|
|
55
|
-
requestIdSuffix: ':apply_patch_guard_followup',
|
|
56
|
-
payload: followupPayload
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
};
|
|
3
|
+
return null;
|
|
60
4
|
};
|
|
61
5
|
registerServerToolHandler('apply_patch', handler);
|
|
62
|
-
function injectApplyPatchHardFailureMessage(base, toolCall, options) {
|
|
63
|
-
const cloned = cloneJson(base);
|
|
64
|
-
const choices = Array.isArray(cloned.choices)
|
|
65
|
-
? cloned.choices
|
|
66
|
-
: [];
|
|
67
|
-
if (!choices.length) {
|
|
68
|
-
return cloned;
|
|
69
|
-
}
|
|
70
|
-
const first = choices[0];
|
|
71
|
-
if (!first || typeof first !== 'object' || Array.isArray(first)) {
|
|
72
|
-
return cloned;
|
|
73
|
-
}
|
|
74
|
-
const message = first.message;
|
|
75
|
-
if (!message || typeof message !== 'object' || Array.isArray(message)) {
|
|
76
|
-
return cloned;
|
|
77
|
-
}
|
|
78
|
-
// Replace tool-call response with a plain assistant message.
|
|
79
|
-
const guidance = buildApplyPatchGuidance(options.rawArgs);
|
|
80
|
-
const text = `[apply_patch rejected] reason=${options.reason}` +
|
|
81
|
-
(options.snippet ? ` args=${options.snippet}` : '') +
|
|
82
|
-
`\n\n${guidance}`;
|
|
83
|
-
message.content = text;
|
|
84
|
-
if (Object.prototype.hasOwnProperty.call(message, 'tool_calls')) {
|
|
85
|
-
delete message.tool_calls;
|
|
86
|
-
}
|
|
87
|
-
if (typeof first.finish_reason === 'string' && first.finish_reason === 'tool_calls') {
|
|
88
|
-
first.finish_reason = 'stop';
|
|
89
|
-
}
|
|
90
|
-
// Remove tool_outputs to avoid confusing clients that don't implement tool loops.
|
|
91
|
-
if (Object.prototype.hasOwnProperty.call(cloned, 'tool_outputs')) {
|
|
92
|
-
delete cloned.tool_outputs;
|
|
93
|
-
}
|
|
94
|
-
return cloned;
|
|
95
|
-
}
|
|
96
|
-
function injectApplyPatchRejectedToolResult(base, toolCall, options) {
|
|
97
|
-
const cloned = cloneJson(base);
|
|
98
|
-
const existingOutputs = Array.isArray(cloned.tool_outputs)
|
|
99
|
-
? cloned.tool_outputs
|
|
100
|
-
: [];
|
|
101
|
-
const payload = {
|
|
102
|
-
ok: false,
|
|
103
|
-
tool: 'apply_patch',
|
|
104
|
-
reason: options.reason,
|
|
105
|
-
...(options.snippet ? { argsSnippet: options.snippet } : {}),
|
|
106
|
-
guidance: buildApplyPatchGuidance(options.rawArgs)
|
|
107
|
-
};
|
|
108
|
-
cloned.tool_outputs = [
|
|
109
|
-
...existingOutputs,
|
|
110
|
-
{
|
|
111
|
-
tool_call_id: toolCall.id,
|
|
112
|
-
name: 'apply_patch',
|
|
113
|
-
content: JSON.stringify(payload)
|
|
114
|
-
}
|
|
115
|
-
];
|
|
116
|
-
return cloned;
|
|
117
|
-
}
|
|
118
|
-
function buildApplyPatchGuidance(rawArgs) {
|
|
119
|
-
const base = 'apply_patch 参数预检失败。请改用统一 diff(*** Begin Patch ... *** End Patch),' +
|
|
120
|
-
'或使用结构化 JSON:{ file, changes:[{ file, kind, target/anchor, lines/newText, ... }] }。';
|
|
121
|
-
const schemaHint = extractLikelyExecCommandShape(rawArgs);
|
|
122
|
-
if (schemaHint) {
|
|
123
|
-
return (base +
|
|
124
|
-
'\n\n' +
|
|
125
|
-
'你传入的参数看起来像 exec_command(例如包含 "command"/"cmd" 字段),但工具名是 apply_patch。\n' +
|
|
126
|
-
'apply_patch 必须提供非空的 "changes" 数组;如果你需要执行命令请改用 exec_command(注意:禁止通过 shell 写文件)。');
|
|
127
|
-
}
|
|
128
|
-
// User-requested hint (directory deletion is not supported via delete_file).
|
|
129
|
-
// Heuristic: detect delete_file with a directory-like target (trailing slash).
|
|
130
|
-
const hintTarget = extractDeleteFileTarget(rawArgs);
|
|
131
|
-
if (!hintTarget) {
|
|
132
|
-
return base;
|
|
133
|
-
}
|
|
134
|
-
return (base +
|
|
135
|
-
'\n\n' +
|
|
136
|
-
`- {"changes":[{"kind":"delete_file","target":"${hintTarget}"}]} ❌ 仍会 invalid_file(因为是目录)\n\n` +
|
|
137
|
-
'如果你真要删目录:请用 exec_command(例如 rm -rf tmp web-container-manager),或者用 apply_patch 逐个删目录下的文件(更安全、可审计)。');
|
|
138
|
-
}
|
|
139
|
-
function extractDeleteFileTarget(rawArgs) {
|
|
140
|
-
if (!rawArgs || typeof rawArgs !== 'string') {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
let parsed;
|
|
144
|
-
try {
|
|
145
|
-
parsed = JSON.parse(rawArgs);
|
|
146
|
-
}
|
|
147
|
-
catch {
|
|
148
|
-
return null;
|
|
149
|
-
}
|
|
150
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
151
|
-
return null;
|
|
152
|
-
}
|
|
153
|
-
const record = parsed;
|
|
154
|
-
const changes = Array.isArray(record.changes) ? record.changes : [];
|
|
155
|
-
for (const change of changes) {
|
|
156
|
-
if (!change || typeof change !== 'object' || Array.isArray(change))
|
|
157
|
-
continue;
|
|
158
|
-
const c = change;
|
|
159
|
-
const kind = typeof c.kind === 'string' ? c.kind.trim().toLowerCase() : '';
|
|
160
|
-
if (kind !== 'delete_file')
|
|
161
|
-
continue;
|
|
162
|
-
const target = typeof c.target === 'string' ? c.target.trim() : '';
|
|
163
|
-
if (!target)
|
|
164
|
-
continue;
|
|
165
|
-
if (target.endsWith('/')) {
|
|
166
|
-
return target;
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
return null;
|
|
170
|
-
}
|
|
171
|
-
function extractLikelyExecCommandShape(rawArgs) {
|
|
172
|
-
if (!rawArgs || typeof rawArgs !== 'string') {
|
|
173
|
-
return false;
|
|
174
|
-
}
|
|
175
|
-
let parsed;
|
|
176
|
-
try {
|
|
177
|
-
parsed = JSON.parse(rawArgs);
|
|
178
|
-
}
|
|
179
|
-
catch {
|
|
180
|
-
return false;
|
|
181
|
-
}
|
|
182
|
-
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
183
|
-
return false;
|
|
184
|
-
}
|
|
185
|
-
const record = parsed;
|
|
186
|
-
const command = record.command;
|
|
187
|
-
const cmd = record.cmd;
|
|
188
|
-
if (typeof command === 'string' && command.trim()) {
|
|
189
|
-
return true;
|
|
190
|
-
}
|
|
191
|
-
if (typeof cmd === 'string' && cmd.trim()) {
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
function buildToolFollowupPayload(adapterContext, chatResponse, entryEndpoint) {
|
|
197
|
-
const captured = adapterContext && typeof adapterContext === 'object'
|
|
198
|
-
? adapterContext.capturedChatRequest
|
|
199
|
-
: undefined;
|
|
200
|
-
const seed = extractCapturedChatSeed(captured);
|
|
201
|
-
if (!seed) {
|
|
202
|
-
return null;
|
|
203
|
-
}
|
|
204
|
-
const assistantMessage = extractAssistantMessage(chatResponse);
|
|
205
|
-
if (!assistantMessage) {
|
|
206
|
-
return null;
|
|
207
|
-
}
|
|
208
|
-
const toolMessages = buildToolMessages(chatResponse);
|
|
209
|
-
if (!toolMessages.length) {
|
|
210
|
-
return null;
|
|
211
|
-
}
|
|
212
|
-
const reconstructed = [...seed.messages, assistantMessage, ...toolMessages];
|
|
213
|
-
return buildEntryAwareFollowupPayload({
|
|
214
|
-
entryEndpoint,
|
|
215
|
-
model: seed.model,
|
|
216
|
-
messages: reconstructed,
|
|
217
|
-
...(seed.tools ? { tools: seed.tools } : {}),
|
|
218
|
-
...(seed.parameters ? { parameters: seed.parameters } : {})
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
function extractAssistantMessage(chatResponse) {
|
|
222
|
-
const choices = Array.isArray(chatResponse.choices)
|
|
223
|
-
? chatResponse.choices
|
|
224
|
-
: [];
|
|
225
|
-
if (!choices.length)
|
|
226
|
-
return null;
|
|
227
|
-
const first = choices[0];
|
|
228
|
-
if (!first || typeof first !== 'object' || Array.isArray(first))
|
|
229
|
-
return null;
|
|
230
|
-
const message = first.message;
|
|
231
|
-
if (!message || typeof message !== 'object' || Array.isArray(message))
|
|
232
|
-
return null;
|
|
233
|
-
return cloneJson(message);
|
|
234
|
-
}
|
|
235
|
-
function buildToolMessages(chatResponse) {
|
|
236
|
-
const toolOutputs = Array.isArray(chatResponse.tool_outputs)
|
|
237
|
-
? chatResponse.tool_outputs
|
|
238
|
-
: [];
|
|
239
|
-
const messages = [];
|
|
240
|
-
for (const entry of toolOutputs) {
|
|
241
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
242
|
-
continue;
|
|
243
|
-
const record = entry;
|
|
244
|
-
const toolCallId = typeof record.tool_call_id === 'string' ? record.tool_call_id : undefined;
|
|
245
|
-
if (!toolCallId)
|
|
246
|
-
continue;
|
|
247
|
-
const name = typeof record.name === 'string' && record.name.trim() ? record.name.trim() : 'apply_patch';
|
|
248
|
-
const rawContent = record.content;
|
|
249
|
-
let contentText;
|
|
250
|
-
if (typeof rawContent === 'string') {
|
|
251
|
-
contentText = rawContent;
|
|
252
|
-
}
|
|
253
|
-
else {
|
|
254
|
-
try {
|
|
255
|
-
contentText = JSON.stringify(rawContent ?? {});
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
contentText = String(rawContent ?? '');
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
messages.push({
|
|
262
|
-
role: 'tool',
|
|
263
|
-
tool_call_id: toolCallId,
|
|
264
|
-
name,
|
|
265
|
-
content: contentText
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
return messages;
|
|
269
|
-
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|