@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,56 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureDir, readSessionDirEnv, resolveClockStateFile } from './paths.js';
|
|
4
|
+
import { buildEmptyState, cleanExpiredTasks, coerceState, nowMs } from './state.js';
|
|
5
|
+
import { readJsonFile, writeJsonFileAtomic } from './io.js';
|
|
6
|
+
export async function loadClockSessionState(sessionId, config) {
|
|
7
|
+
const sessionDir = readSessionDirEnv();
|
|
8
|
+
if (!sessionDir) {
|
|
9
|
+
return buildEmptyState(sessionId);
|
|
10
|
+
}
|
|
11
|
+
const filePath = resolveClockStateFile(sessionDir, sessionId);
|
|
12
|
+
if (!filePath) {
|
|
13
|
+
return buildEmptyState(sessionId);
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
const raw = await readJsonFile(filePath);
|
|
17
|
+
const state = coerceState(raw, sessionId);
|
|
18
|
+
const at = nowMs();
|
|
19
|
+
const cleaned = cleanExpiredTasks(state.tasks, config, at);
|
|
20
|
+
if (cleaned.length !== state.tasks.length) {
|
|
21
|
+
const next = { ...state, tasks: cleaned, updatedAtMs: at };
|
|
22
|
+
if (!next.tasks.length) {
|
|
23
|
+
await fs.rm(filePath, { force: true });
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
await ensureDir(path.dirname(filePath));
|
|
27
|
+
await writeJsonFileAtomic(filePath, next);
|
|
28
|
+
}
|
|
29
|
+
return next;
|
|
30
|
+
}
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const message = error instanceof Error ? error.message : String(error ?? 'unknown');
|
|
35
|
+
if (message.includes('ENOENT')) {
|
|
36
|
+
return buildEmptyState(sessionId);
|
|
37
|
+
}
|
|
38
|
+
return buildEmptyState(sessionId);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function clearClockSession(sessionId) {
|
|
42
|
+
const sessionDir = readSessionDirEnv();
|
|
43
|
+
if (!sessionDir) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const filePath = resolveClockStateFile(sessionDir, sessionId);
|
|
47
|
+
if (!filePath) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
await fs.rm(filePath, { force: true });
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ClockConfigSnapshot, ClockSessionState, ClockTask } from './types.js';
|
|
2
|
+
export declare function nowMs(): number;
|
|
3
|
+
export declare function buildEmptyState(sessionId: string): ClockSessionState;
|
|
4
|
+
export declare function coerceState(raw: unknown, sessionId: string): ClockSessionState;
|
|
5
|
+
export declare function cleanExpiredTasks(tasks: ClockTask[], config: ClockConfigSnapshot, atMs: number): ClockTask[];
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
export function nowMs() {
|
|
2
|
+
return Date.now();
|
|
3
|
+
}
|
|
4
|
+
export function buildEmptyState(sessionId) {
|
|
5
|
+
const t = nowMs();
|
|
6
|
+
return { version: 1, sessionId, tasks: [], updatedAtMs: t };
|
|
7
|
+
}
|
|
8
|
+
export function coerceState(raw, sessionId) {
|
|
9
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
10
|
+
return buildEmptyState(sessionId);
|
|
11
|
+
}
|
|
12
|
+
const record = raw;
|
|
13
|
+
const tasksRaw = Array.isArray(record.tasks) ? record.tasks : [];
|
|
14
|
+
const tasks = [];
|
|
15
|
+
for (const entry of tasksRaw) {
|
|
16
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry))
|
|
17
|
+
continue;
|
|
18
|
+
const e = entry;
|
|
19
|
+
const taskId = typeof e.taskId === 'string' ? e.taskId.trim() : '';
|
|
20
|
+
const dueAtMs = typeof e.dueAtMs === 'number' && Number.isFinite(e.dueAtMs) ? Math.floor(e.dueAtMs) : NaN;
|
|
21
|
+
const createdAtMs = typeof e.createdAtMs === 'number' && Number.isFinite(e.createdAtMs) ? Math.floor(e.createdAtMs) : NaN;
|
|
22
|
+
const updatedAtMs = typeof e.updatedAtMs === 'number' && Number.isFinite(e.updatedAtMs) ? Math.floor(e.updatedAtMs) : NaN;
|
|
23
|
+
const task = typeof e.task === 'string' ? e.task.trim() : '';
|
|
24
|
+
if (!taskId || !task || !Number.isFinite(dueAtMs) || !Number.isFinite(createdAtMs) || !Number.isFinite(updatedAtMs)) {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
const tool = typeof e.tool === 'string' && e.tool.trim() ? e.tool.trim() : undefined;
|
|
28
|
+
const args = e.arguments && typeof e.arguments === 'object' && !Array.isArray(e.arguments)
|
|
29
|
+
? e.arguments
|
|
30
|
+
: undefined;
|
|
31
|
+
const deliveredAtMs = typeof e.deliveredAtMs === 'number' && Number.isFinite(e.deliveredAtMs) ? Math.floor(e.deliveredAtMs) : undefined;
|
|
32
|
+
const deliveryCount = typeof e.deliveryCount === 'number' && Number.isFinite(e.deliveryCount) ? Math.max(0, Math.floor(e.deliveryCount)) : 0;
|
|
33
|
+
tasks.push({
|
|
34
|
+
taskId,
|
|
35
|
+
sessionId,
|
|
36
|
+
dueAtMs,
|
|
37
|
+
createdAtMs,
|
|
38
|
+
updatedAtMs,
|
|
39
|
+
task,
|
|
40
|
+
...(tool ? { tool } : {}),
|
|
41
|
+
...(args ? { arguments: args } : {}),
|
|
42
|
+
...(deliveredAtMs !== undefined ? { deliveredAtMs } : {}),
|
|
43
|
+
deliveryCount
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
const updatedAtMs = typeof record.updatedAtMs === 'number' && Number.isFinite(record.updatedAtMs) ? Math.floor(record.updatedAtMs) : nowMs();
|
|
47
|
+
return { version: 1, sessionId, tasks, updatedAtMs };
|
|
48
|
+
}
|
|
49
|
+
export function cleanExpiredTasks(tasks, config, atMs) {
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const task of tasks) {
|
|
52
|
+
if (!task || typeof task !== 'object')
|
|
53
|
+
continue;
|
|
54
|
+
if (!Number.isFinite(task.dueAtMs))
|
|
55
|
+
continue;
|
|
56
|
+
if (atMs > task.dueAtMs + config.retentionMs) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
out.push(task);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { ClockConfigSnapshot, ClockReservation, ClockScheduleItem, ClockSessionState, ClockTask } from './types.js';
|
|
2
|
+
export { normalizeClockConfig } from './config.js';
|
|
3
|
+
export { startClockDaemonIfNeeded, stopClockDaemonForTests } from './daemon.js';
|
|
4
|
+
export { loadClockSessionState, clearClockSession } from './session-store.js';
|
|
5
|
+
export { cancelClockTask, clearClockTasks, commitClockReservation, findNextUndeliveredDueAtMs, listClockTasks, parseDueAtMs, reserveDueTasksForRequest, scheduleClockTasks, selectDueUndeliveredTasks } from './tasks.js';
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { normalizeClockConfig } from './config.js';
|
|
2
|
+
export { startClockDaemonIfNeeded, stopClockDaemonForTests } from './daemon.js';
|
|
3
|
+
export { loadClockSessionState, clearClockSession } from './session-store.js';
|
|
4
|
+
export { cancelClockTask, clearClockTasks, commitClockReservation, findNextUndeliveredDueAtMs, listClockTasks, parseDueAtMs, reserveDueTasksForRequest, scheduleClockTasks, selectDueUndeliveredTasks } from './tasks.js';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ClockConfigSnapshot, ClockReservation, ClockScheduleItem, ClockTask } from './types.js';
|
|
2
|
+
export declare function parseDueAtMs(value: unknown): number | null;
|
|
3
|
+
export declare function listClockTasks(sessionId: string, config: ClockConfigSnapshot): Promise<ClockTask[]>;
|
|
4
|
+
export declare function scheduleClockTasks(sessionId: string, items: ClockScheduleItem[], config: ClockConfigSnapshot): Promise<ClockTask[]>;
|
|
5
|
+
export declare function cancelClockTask(sessionId: string, taskId: string, config: ClockConfigSnapshot): Promise<boolean>;
|
|
6
|
+
export declare function clearClockTasks(sessionId: string, config: ClockConfigSnapshot): Promise<number>;
|
|
7
|
+
export declare function selectDueUndeliveredTasks(tasks: ClockTask[], config: ClockConfigSnapshot, atMs: number): ClockTask[];
|
|
8
|
+
export declare function findNextUndeliveredDueAtMs(tasks: ClockTask[], atMs: number): number | null;
|
|
9
|
+
export declare function reserveDueTasksForRequest(args: {
|
|
10
|
+
reservationId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
config: ClockConfigSnapshot;
|
|
13
|
+
}): Promise<{
|
|
14
|
+
reservation: ClockReservation | null;
|
|
15
|
+
injectText?: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function commitClockReservation(reservation: ClockReservation, config: ClockConfigSnapshot): Promise<void>;
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import { ensureDir, readSessionDirEnv, resolveClockStateFile } from './paths.js';
|
|
5
|
+
import { cleanExpiredTasks, nowMs } from './state.js';
|
|
6
|
+
import { writeJsonFileAtomic } from './io.js';
|
|
7
|
+
import { clearClockSession, loadClockSessionState } from './session-store.js';
|
|
8
|
+
function safeJson(value) {
|
|
9
|
+
try {
|
|
10
|
+
return JSON.stringify(value ?? {});
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return '{}';
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function safeQuoted(text) {
|
|
17
|
+
const normalized = String(text ?? '');
|
|
18
|
+
const escaped = normalized.replace(/\"/g, '\\"');
|
|
19
|
+
return `"${escaped}"`;
|
|
20
|
+
}
|
|
21
|
+
function buildTaskId() {
|
|
22
|
+
try {
|
|
23
|
+
return crypto.randomUUID();
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return `task_${Math.random().toString(16).slice(2)}_${Date.now()}`;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export function parseDueAtMs(value) {
|
|
30
|
+
if (typeof value !== 'string')
|
|
31
|
+
return null;
|
|
32
|
+
const t = value.trim();
|
|
33
|
+
if (!t)
|
|
34
|
+
return null;
|
|
35
|
+
const ms = Date.parse(t);
|
|
36
|
+
if (!Number.isFinite(ms))
|
|
37
|
+
return null;
|
|
38
|
+
return ms;
|
|
39
|
+
}
|
|
40
|
+
export async function listClockTasks(sessionId, config) {
|
|
41
|
+
const state = await loadClockSessionState(sessionId, config);
|
|
42
|
+
return state.tasks.slice();
|
|
43
|
+
}
|
|
44
|
+
export async function scheduleClockTasks(sessionId, items, config) {
|
|
45
|
+
const sessionDir = readSessionDirEnv();
|
|
46
|
+
if (!sessionDir) {
|
|
47
|
+
throw new Error('clock: missing ROUTECODEX_SESSION_DIR');
|
|
48
|
+
}
|
|
49
|
+
const filePath = resolveClockStateFile(sessionDir, sessionId);
|
|
50
|
+
if (!filePath) {
|
|
51
|
+
throw new Error('clock: invalid sessionId');
|
|
52
|
+
}
|
|
53
|
+
await ensureDir(path.dirname(filePath));
|
|
54
|
+
const at = nowMs();
|
|
55
|
+
const existing = await loadClockSessionState(sessionId, config);
|
|
56
|
+
const cleaned = cleanExpiredTasks(existing.tasks, config, at);
|
|
57
|
+
const scheduled = [];
|
|
58
|
+
for (const item of items) {
|
|
59
|
+
const text = typeof item.task === 'string' ? item.task.trim() : '';
|
|
60
|
+
const dueAtMs = item.dueAtMs;
|
|
61
|
+
if (!text || !Number.isFinite(dueAtMs)) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
const taskId = buildTaskId();
|
|
65
|
+
scheduled.push({
|
|
66
|
+
taskId,
|
|
67
|
+
sessionId,
|
|
68
|
+
dueAtMs: Math.floor(dueAtMs),
|
|
69
|
+
createdAtMs: at,
|
|
70
|
+
updatedAtMs: at,
|
|
71
|
+
task: text,
|
|
72
|
+
...(item.tool ? { tool: item.tool } : {}),
|
|
73
|
+
...(item.arguments ? { arguments: item.arguments } : {}),
|
|
74
|
+
deliveryCount: 0
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const nextTasks = cleanExpiredTasks([...cleaned, ...scheduled], config, at);
|
|
78
|
+
const next = {
|
|
79
|
+
version: 1,
|
|
80
|
+
sessionId,
|
|
81
|
+
tasks: nextTasks,
|
|
82
|
+
updatedAtMs: at
|
|
83
|
+
};
|
|
84
|
+
if (!next.tasks.length) {
|
|
85
|
+
await fs.rm(filePath, { force: true });
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
await writeJsonFileAtomic(filePath, next);
|
|
89
|
+
}
|
|
90
|
+
return scheduled;
|
|
91
|
+
}
|
|
92
|
+
export async function cancelClockTask(sessionId, taskId, config) {
|
|
93
|
+
const sessionDir = readSessionDirEnv();
|
|
94
|
+
if (!sessionDir) {
|
|
95
|
+
throw new Error('clock: missing ROUTECODEX_SESSION_DIR');
|
|
96
|
+
}
|
|
97
|
+
const filePath = resolveClockStateFile(sessionDir, sessionId);
|
|
98
|
+
if (!filePath) {
|
|
99
|
+
throw new Error('clock: invalid sessionId');
|
|
100
|
+
}
|
|
101
|
+
await ensureDir(path.dirname(filePath));
|
|
102
|
+
const at = nowMs();
|
|
103
|
+
const state = await loadClockSessionState(sessionId, config);
|
|
104
|
+
const cleaned = cleanExpiredTasks(state.tasks, config, at);
|
|
105
|
+
const nextTasks = cleaned.filter((t) => t.taskId !== taskId);
|
|
106
|
+
const removed = nextTasks.length !== cleaned.length;
|
|
107
|
+
const next = { version: 1, sessionId, tasks: nextTasks, updatedAtMs: at };
|
|
108
|
+
if (!next.tasks.length) {
|
|
109
|
+
await fs.rm(filePath, { force: true });
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
await writeJsonFileAtomic(filePath, next);
|
|
113
|
+
}
|
|
114
|
+
return removed;
|
|
115
|
+
}
|
|
116
|
+
export async function clearClockTasks(sessionId, config) {
|
|
117
|
+
const items = await listClockTasks(sessionId, config);
|
|
118
|
+
await clearClockSession(sessionId);
|
|
119
|
+
return items.length;
|
|
120
|
+
}
|
|
121
|
+
export function selectDueUndeliveredTasks(tasks, config, atMs) {
|
|
122
|
+
const due = [];
|
|
123
|
+
for (const task of tasks) {
|
|
124
|
+
if (!task || typeof task !== 'object')
|
|
125
|
+
continue;
|
|
126
|
+
if (task.deliveredAtMs !== undefined)
|
|
127
|
+
continue;
|
|
128
|
+
if (!Number.isFinite(task.dueAtMs))
|
|
129
|
+
continue;
|
|
130
|
+
if (atMs < task.dueAtMs - config.dueWindowMs)
|
|
131
|
+
continue;
|
|
132
|
+
if (atMs > task.dueAtMs + config.retentionMs)
|
|
133
|
+
continue;
|
|
134
|
+
due.push(task);
|
|
135
|
+
}
|
|
136
|
+
due.sort((a, b) => a.dueAtMs - b.dueAtMs);
|
|
137
|
+
return due;
|
|
138
|
+
}
|
|
139
|
+
export function findNextUndeliveredDueAtMs(tasks, atMs) {
|
|
140
|
+
let next = null;
|
|
141
|
+
for (const task of tasks) {
|
|
142
|
+
if (!task || typeof task !== 'object')
|
|
143
|
+
continue;
|
|
144
|
+
if (task.deliveredAtMs !== undefined)
|
|
145
|
+
continue;
|
|
146
|
+
if (!Number.isFinite(task.dueAtMs))
|
|
147
|
+
continue;
|
|
148
|
+
if (task.dueAtMs < atMs) {
|
|
149
|
+
// keep past due tasks as "next" too (will be due immediately)
|
|
150
|
+
}
|
|
151
|
+
if (next === null || task.dueAtMs < next) {
|
|
152
|
+
next = task.dueAtMs;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return next;
|
|
156
|
+
}
|
|
157
|
+
export async function reserveDueTasksForRequest(args) {
|
|
158
|
+
const state = await loadClockSessionState(args.sessionId, args.config);
|
|
159
|
+
const at = nowMs();
|
|
160
|
+
const due = selectDueUndeliveredTasks(state.tasks, args.config, at);
|
|
161
|
+
if (!due.length) {
|
|
162
|
+
return { reservation: null };
|
|
163
|
+
}
|
|
164
|
+
const taskIds = due.map((t) => t.taskId);
|
|
165
|
+
const reservation = {
|
|
166
|
+
reservationId: args.reservationId,
|
|
167
|
+
sessionId: args.sessionId,
|
|
168
|
+
taskIds,
|
|
169
|
+
reservedAtMs: at
|
|
170
|
+
};
|
|
171
|
+
const injectText = due
|
|
172
|
+
.map((t) => {
|
|
173
|
+
const dueAtIso = new Date(t.dueAtMs).toISOString();
|
|
174
|
+
const toolLabel = t.tool ? ` tool=${t.tool}` : '';
|
|
175
|
+
const argsLabel = t.arguments ? ` args=${safeJson(t.arguments)}` : '';
|
|
176
|
+
return `[scheduled task:${safeQuoted(t.task)}${toolLabel}${argsLabel} dueAt=${dueAtIso}]`;
|
|
177
|
+
})
|
|
178
|
+
.join('\n');
|
|
179
|
+
return { reservation, injectText };
|
|
180
|
+
}
|
|
181
|
+
export async function commitClockReservation(reservation, config) {
|
|
182
|
+
const sessionDir = readSessionDirEnv();
|
|
183
|
+
if (!sessionDir) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const filePath = resolveClockStateFile(sessionDir, reservation.sessionId);
|
|
187
|
+
if (!filePath) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const at = nowMs();
|
|
191
|
+
const state = await loadClockSessionState(reservation.sessionId, config);
|
|
192
|
+
const cleaned = cleanExpiredTasks(state.tasks, config, at);
|
|
193
|
+
const reservedSet = new Set(reservation.taskIds);
|
|
194
|
+
let touched = false;
|
|
195
|
+
const nextTasks = cleaned.map((t) => {
|
|
196
|
+
if (!reservedSet.has(t.taskId)) {
|
|
197
|
+
return t;
|
|
198
|
+
}
|
|
199
|
+
if (t.deliveredAtMs !== undefined) {
|
|
200
|
+
return t;
|
|
201
|
+
}
|
|
202
|
+
touched = true;
|
|
203
|
+
return {
|
|
204
|
+
...t,
|
|
205
|
+
deliveredAtMs: at,
|
|
206
|
+
deliveryCount: (typeof t.deliveryCount === 'number' ? t.deliveryCount : 0) + 1,
|
|
207
|
+
updatedAtMs: at
|
|
208
|
+
};
|
|
209
|
+
});
|
|
210
|
+
if (!touched) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const next = { version: 1, sessionId: reservation.sessionId, tasks: nextTasks, updatedAtMs: at };
|
|
214
|
+
await ensureDir(path.dirname(filePath));
|
|
215
|
+
if (!next.tasks.length) {
|
|
216
|
+
await fs.rm(filePath, { force: true });
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
await writeJsonFileAtomic(filePath, next);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ClockTask = {
|
|
2
|
+
taskId: string;
|
|
3
|
+
sessionId: string;
|
|
4
|
+
dueAtMs: number;
|
|
5
|
+
createdAtMs: number;
|
|
6
|
+
updatedAtMs: number;
|
|
7
|
+
task: string;
|
|
8
|
+
tool?: string;
|
|
9
|
+
arguments?: Record<string, unknown>;
|
|
10
|
+
deliveredAtMs?: number;
|
|
11
|
+
deliveryCount: number;
|
|
12
|
+
};
|
|
13
|
+
export type ClockSessionState = {
|
|
14
|
+
version: 1;
|
|
15
|
+
sessionId: string;
|
|
16
|
+
tasks: ClockTask[];
|
|
17
|
+
updatedAtMs: number;
|
|
18
|
+
};
|
|
19
|
+
export type ClockReservation = {
|
|
20
|
+
reservationId: string;
|
|
21
|
+
sessionId: string;
|
|
22
|
+
taskIds: string[];
|
|
23
|
+
reservedAtMs: number;
|
|
24
|
+
};
|
|
25
|
+
export type ClockConfigSnapshot = {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
retentionMs: number;
|
|
28
|
+
dueWindowMs: number;
|
|
29
|
+
tickMs: number;
|
|
30
|
+
};
|
|
31
|
+
export type ClockScheduleItem = {
|
|
32
|
+
dueAtMs: number;
|
|
33
|
+
task: string;
|
|
34
|
+
tool?: string;
|
|
35
|
+
arguments?: Record<string, unknown>;
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import type { AdapterContext } from '../conversion/hub/types/chat-envelope.js';
|
|
2
2
|
import type { JsonObject } from '../conversion/hub/types/json.js';
|
|
3
3
|
import type { ProviderInvoker } from './types.js';
|
|
4
|
+
import type { StageRecorder } from '../conversion/hub/format-adapters/index.js';
|
|
4
5
|
export interface ServerToolOrchestrationOptions {
|
|
5
6
|
chat: JsonObject;
|
|
6
7
|
adapterContext: AdapterContext;
|
|
7
8
|
requestId: string;
|
|
8
9
|
entryEndpoint: string;
|
|
9
10
|
providerProtocol: string;
|
|
11
|
+
stageRecorder?: StageRecorder;
|
|
10
12
|
reenterPipeline?: (options: {
|
|
11
13
|
entryEndpoint: string;
|
|
12
14
|
requestId: string;
|
|
@@ -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,
|
|
@@ -193,8 +300,45 @@ export async function runServerToolOrchestration(options) {
|
|
|
193
300
|
const applyAutoLimit = isAutoStopMessage || isErrorAutoFlow || isGeminiEmptyReplyContinue || isApplyPatchGuard || isExecCommandGuard;
|
|
194
301
|
// ServerTool followups must not inherit or inject any routeHint; always route fresh.
|
|
195
302
|
const preserveRouteHint = false;
|
|
196
|
-
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);
|
|
197
340
|
if (applyAutoLimit && loopState && typeof loopState.repeatCount === 'number' && loopState.repeatCount >= 3) {
|
|
341
|
+
logProgress(5, totalSteps, 'completed (auto limit hit)', { flowId });
|
|
198
342
|
return {
|
|
199
343
|
chat: engineResult.finalChatResponse,
|
|
200
344
|
executed: true,
|
|
@@ -202,21 +346,22 @@ export async function runServerToolOrchestration(options) {
|
|
|
202
346
|
};
|
|
203
347
|
}
|
|
204
348
|
if (isAdapterClientDisconnected(options.adapterContext)) {
|
|
349
|
+
logProgress(5, totalSteps, 'completed (client disconnected)', { flowId });
|
|
205
350
|
return {
|
|
206
351
|
chat: engineResult.finalChatResponse,
|
|
207
352
|
executed: true,
|
|
208
353
|
flowId: engineResult.execution.flowId
|
|
209
354
|
};
|
|
210
355
|
}
|
|
211
|
-
const followupEntryEndpoint = engineResult.execution.followup.entryEndpoint ||
|
|
212
|
-
options.entryEndpoint ||
|
|
213
|
-
'/v1/chat/completions';
|
|
214
356
|
const metadata = {
|
|
215
357
|
serverToolFollowup: true,
|
|
216
358
|
stream: false,
|
|
217
359
|
...(loopState ? { serverToolLoopState: loopState } : {}),
|
|
218
360
|
...(engineResult.execution.followup.metadata ?? {})
|
|
219
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';
|
|
220
365
|
// Enforce unified followup contract:
|
|
221
366
|
// - clear any inherited routeHint
|
|
222
367
|
// - do not inherit sticky target
|
|
@@ -232,7 +377,14 @@ export async function runServerToolOrchestration(options) {
|
|
|
232
377
|
const retryEmptyFollowupOnce = isStopMessageFlow || isGeminiEmptyReplyContinue;
|
|
233
378
|
const maxAttempts = retryEmptyFollowupOnce ? 2 : 1;
|
|
234
379
|
const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
|
|
235
|
-
|
|
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
|
+
});
|
|
236
388
|
let followup;
|
|
237
389
|
let lastError;
|
|
238
390
|
// stopMessage 是一种“状态型” servertool:一旦触发,我们需要尽量避免因 followup 失败而把状态留在可继续触发的位置,
|
|
@@ -303,6 +455,7 @@ export async function runServerToolOrchestration(options) {
|
|
|
303
455
|
// 对 stopMessage:避免把 empty followup 升级为 502,直接清理 stopMessage 状态并返回原始响应。
|
|
304
456
|
// 这样客户端至少能拿到本轮输出,且 stopMessage 不会在后续请求里继续触发导致“永远 502”。
|
|
305
457
|
disableStopMessageAfterFailedFollowup(options.adapterContext, stopMessageReservation);
|
|
458
|
+
logProgress(5, totalSteps, 'completed (stopMessage followup empty; cleaned state)', { flowId });
|
|
306
459
|
return {
|
|
307
460
|
chat: engineResult.finalChatResponse,
|
|
308
461
|
executed: true,
|
|
@@ -323,6 +476,7 @@ export async function runServerToolOrchestration(options) {
|
|
|
323
476
|
throw wrapped;
|
|
324
477
|
}
|
|
325
478
|
const decorated = decorateFinalChatWithServerToolContext(followupBody ?? engineResult.finalChatResponse, engineResult.execution);
|
|
479
|
+
logProgress(5, totalSteps, 'completed', { flowId });
|
|
326
480
|
return {
|
|
327
481
|
chat: decorated,
|
|
328
482
|
executed: true,
|