@jsonstudio/llms 0.6.1172 → 0.6.1354
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/conversion/codecs/gemini-openai-codec.d.ts +3 -1
- package/dist/conversion/codecs/gemini-openai-codec.js +10 -4
- package/dist/conversion/compat/actions/gemini-web-search.d.ts +1 -1
- package/dist/conversion/compat/actions/gemini-web-search.js +5 -2
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.d.ts +12 -0
- package/dist/conversion/compat/actions/iflow-tool-text-fallback.js +199 -0
- package/dist/conversion/compat/actions/iflow-web-search.d.ts +1 -1
- package/dist/conversion/compat/actions/iflow-web-search.js +5 -2
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +47 -56
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +1 -13
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +523 -50
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +18 -38
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.d.ts +10 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/adapter-context.js +134 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.d.ts +6 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/anthropic-alias-map.js +79 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/apply-patch-tool-mode.js +46 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-chat-process-entry.js +366 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.d.ts +9 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/execute-request-stage.js +384 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.d.ts +3 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/node-results.js +14 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/payload-normalize.js +144 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.d.ts +4 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/policy.js +32 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.d.ts +8 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/protocol.js +63 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/resolve-protocol-hooks.js +43 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.d.ts +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/semantic-gate.js +29 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.d.ts +2 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/servertool-runtime-config.js +16 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/types.d.ts +116 -0
- package/dist/conversion/hub/pipeline/hub-pipeline/types.js +1 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.d.ts +3 -95
- package/dist/conversion/hub/pipeline/hub-pipeline.js +19 -1281
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.d.ts +7 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +65 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +25 -22
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -1
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +11 -11
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +1 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.d.ts +1 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +4 -2
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +17 -9
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +2 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +40 -2
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +1 -1
- package/dist/conversion/hub/pipeline/target-utils.js +9 -5
- package/dist/conversion/hub/process/chat-process.js +256 -16
- package/dist/conversion/hub/response/provider-response.d.ts +8 -0
- package/dist/conversion/hub/response/provider-response.js +85 -27
- package/dist/conversion/hub/response/response-mappers.d.ts +10 -3
- package/dist/conversion/hub/response/response-mappers.js +30 -6
- package/dist/conversion/hub/response/response-runtime.js +4 -38
- package/dist/conversion/hub/snapshot-recorder.js +5 -1
- package/dist/conversion/hub/standardized-bridge.js +23 -15
- package/dist/conversion/pipeline/codecs/v2/anthropic-openai-pipeline.js +36 -5
- package/dist/conversion/responses/responses-openai-bridge.js +20 -4
- package/dist/conversion/shared/gemini-tool-utils.d.ts +8 -1
- package/dist/conversion/shared/gemini-tool-utils.js +580 -108
- package/dist/conversion/shared/jsonish.js +1 -1
- package/dist/conversion/shared/mcp-injection.js +67 -33
- package/dist/conversion/shared/openai-finalizer.js +2 -1
- package/dist/conversion/shared/openai-message-normalize.js +76 -21
- package/dist/conversion/shared/responses-output-builder.js +6 -0
- package/dist/conversion/shared/runtime-metadata.d.ts +7 -0
- package/dist/conversion/shared/runtime-metadata.js +23 -0
- package/dist/conversion/shared/text-markup-normalizer.d.ts +2 -0
- package/dist/conversion/shared/text-markup-normalizer.js +284 -4
- package/dist/conversion/shared/tool-canonicalizer.js +2 -1
- package/dist/conversion/shared/tool-governor.js +3 -3
- package/dist/filters/engine.js +5 -5
- package/dist/filters/special/request-tool-list-filter.js +194 -60
- package/dist/filters/special/request-tools-normalize.js +1 -1
- package/dist/filters/special/response-tool-text-canonicalize.d.ts +4 -7
- package/dist/filters/special/response-tool-text-canonicalize.js +7 -35
- package/dist/filters/special/tool-filter-hooks.js +58 -62
- package/dist/guidance/index.js +5 -1
- package/dist/http/sse-response.js +6 -6
- package/dist/router/virtual-router/bootstrap.js +48 -4
- package/dist/router/virtual-router/engine-health.d.ts +1 -1
- package/dist/router/virtual-router/engine-health.js +11 -110
- package/dist/router/virtual-router/engine-selection/alias-selection.d.ts +15 -0
- package/dist/router/virtual-router/engine-selection/alias-selection.js +156 -0
- package/dist/router/virtual-router/engine-selection/context-weight-multipliers.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/context-weight-multipliers.js +23 -0
- package/dist/router/virtual-router/engine-selection/direct-provider-model.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/direct-provider-model.js +49 -0
- package/dist/router/virtual-router/engine-selection/instruction-target.d.ts +6 -0
- package/dist/router/virtual-router/engine-selection/instruction-target.js +54 -0
- package/dist/router/virtual-router/engine-selection/key-parsing.d.ts +8 -0
- package/dist/router/virtual-router/engine-selection/key-parsing.js +64 -0
- package/dist/router/virtual-router/engine-selection/route-utils.d.ts +12 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +150 -0
- package/dist/router/virtual-router/engine-selection/routing-state-filter.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/routing-state-filter.js +50 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.d.ts +39 -0
- package/dist/router/virtual-router/engine-selection/selection-deps.js +1 -0
- package/dist/router/virtual-router/engine-selection/sticky-pool.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/sticky-pool.js +109 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.d.ts +12 -0
- package/dist/router/virtual-router/engine-selection/tier-priority.js +55 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +22 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +400 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/tier-selection.js +225 -0
- package/dist/router/virtual-router/engine-selection.d.ts +4 -30
- package/dist/router/virtual-router/engine-selection.js +10 -962
- package/dist/router/virtual-router/engine.d.ts +1 -0
- package/dist/router/virtual-router/engine.js +55 -10
- package/dist/router/virtual-router/routing-instructions.js +6 -1
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +5 -0
- package/dist/router/virtual-router/stop-message-state-sync.js +6 -14
- package/dist/router/virtual-router/types.d.ts +25 -1
- package/dist/servertool/clock/config.d.ts +8 -0
- package/dist/servertool/clock/config.js +22 -0
- package/dist/servertool/clock/log.d.ts +3 -0
- package/dist/servertool/clock/log.js +13 -0
- package/dist/servertool/clock/task-store.d.ts +1 -1
- package/dist/servertool/clock/task-store.js +1 -1
- package/dist/servertool/clock/tasks.js +1 -1
- package/dist/servertool/engine.js +146 -21
- package/dist/servertool/handlers/clock-auto.js +11 -6
- package/dist/servertool/handlers/clock.js +36 -10
- package/dist/servertool/handlers/followup-request-builder.js +8 -2
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +15 -9
- package/dist/servertool/handlers/iflow-model-error-retry.js +6 -4
- package/dist/servertool/handlers/recursive-detection-guard.js +4 -2
- package/dist/servertool/handlers/stop-message-auto.js +100 -10
- package/dist/servertool/handlers/vision.js +4 -1
- package/dist/servertool/handlers/web-search.js +3 -1
- package/dist/servertool/pending-session.d.ts +19 -0
- package/dist/servertool/pending-session.js +97 -0
- package/dist/servertool/reenter-backend.js +5 -3
- package/dist/servertool/server-side-tools.js +235 -6
- package/dist/servertool/types.d.ts +13 -0
- package/dist/sse/json-to-sse/event-generators/responses.js +1 -1
- package/dist/sse/shared/chat-serializer.js +2 -2
- package/dist/sse/shared/constants.js +1 -1
- package/dist/sse/sse-to-json/anthropic-sse-to-json-converter.d.ts +7 -1
- package/dist/sse/sse-to-json/builders/response-builder.js +16 -0
- package/dist/sse/sse-to-json/responses-sse-to-json-converter.d.ts +1 -1
- package/dist/tools/apply-patch/execution-capturer.js +1 -1
- package/dist/tools/exec-command/normalize.js +4 -0
- package/dist/tools/exec-command/regression-capturer.js +1 -1
- package/package.json +10 -5
|
@@ -10,7 +10,7 @@ import { parseRoutingInstructions, applyRoutingInstructions, cleanMessagesFromRo
|
|
|
10
10
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateAsync, saveRoutingInstructionStateSync } from './sticky-session-store.js';
|
|
11
11
|
import { buildHitReason, formatVirtualRouterHit } from './engine-logging.js';
|
|
12
12
|
import { selectDirectProviderModel, selectFromStickyPool, selectProviderImpl } from './engine-selection.js';
|
|
13
|
-
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl,
|
|
13
|
+
import { applyQuotaDepletedImpl, applyQuotaRecoveryImpl, handleProviderFailureImpl, mapProviderErrorImpl } from './engine-health.js';
|
|
14
14
|
import { mergeStopMessageFromPersisted } from './stop-message-state-sync.js';
|
|
15
15
|
export class VirtualRouterEngine {
|
|
16
16
|
routing = {};
|
|
@@ -22,6 +22,9 @@ export class VirtualRouterEngine {
|
|
|
22
22
|
contextAdvisor = new ContextAdvisor();
|
|
23
23
|
contextRouting;
|
|
24
24
|
routeStats = new Map();
|
|
25
|
+
// Alias selection state (global within this VirtualRouterEngine instance).
|
|
26
|
+
// Used by alias-selection strategies to avoid rapid cross-alias switching.
|
|
27
|
+
aliasQueueStore = new Map();
|
|
25
28
|
debug = console; // thin hook; host may monkey-patch for colored logging
|
|
26
29
|
healthConfig = null;
|
|
27
30
|
statsCenter = getStatsCenter();
|
|
@@ -161,7 +164,13 @@ export class VirtualRouterEngine {
|
|
|
161
164
|
: undefined;
|
|
162
165
|
return Boolean(entryText) && entryText === sessionText && entryMax === sessionMax;
|
|
163
166
|
});
|
|
164
|
-
|
|
167
|
+
const used = typeof sessionState.stopMessageUsed === 'number' && Number.isFinite(sessionState.stopMessageUsed)
|
|
168
|
+
? Math.max(0, Math.floor(sessionState.stopMessageUsed))
|
|
169
|
+
: 0;
|
|
170
|
+
const hasLastUsedAt = typeof sessionState.stopMessageLastUsedAt === 'number' &&
|
|
171
|
+
Number.isFinite(sessionState.stopMessageLastUsedAt);
|
|
172
|
+
const alreadyArmed = used === 0 && !hasLastUsedAt;
|
|
173
|
+
if (allSame && alreadyArmed) {
|
|
165
174
|
instructions = parsedInstructions.filter((entry) => entry.type !== 'stopMessageSet');
|
|
166
175
|
}
|
|
167
176
|
}
|
|
@@ -213,9 +222,16 @@ export class VirtualRouterEngine {
|
|
|
213
222
|
typeof maxRepeats === 'number' &&
|
|
214
223
|
Math.floor(sessionState.stopMessageMaxRepeats) === Math.floor(maxRepeats);
|
|
215
224
|
const isSameInstruction = Boolean(text) && sameText && sameMax;
|
|
225
|
+
const used = typeof sessionState.stopMessageUsed === 'number' && Number.isFinite(sessionState.stopMessageUsed)
|
|
226
|
+
? Math.max(0, Math.floor(sessionState.stopMessageUsed))
|
|
227
|
+
: 0;
|
|
228
|
+
const hasLastUsedAt = typeof sessionState.stopMessageLastUsedAt === 'number' &&
|
|
229
|
+
Number.isFinite(sessionState.stopMessageLastUsedAt);
|
|
230
|
+
const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
|
|
216
231
|
nextSessionState.stopMessageText = text || undefined;
|
|
217
232
|
nextSessionState.stopMessageMaxRepeats = maxRepeats;
|
|
218
|
-
|
|
233
|
+
nextSessionState.stopMessageSource = 'explicit';
|
|
234
|
+
if (shouldRearm) {
|
|
219
235
|
nextSessionState.stopMessageUsed = 0;
|
|
220
236
|
nextSessionState.stopMessageUpdatedAt =
|
|
221
237
|
typeof routingState.stopMessageUpdatedAt === 'number'
|
|
@@ -426,7 +442,26 @@ export class VirtualRouterEngine {
|
|
|
426
442
|
else if (routingState.preferTarget) {
|
|
427
443
|
// Auto-clear only when the target becomes invalid or blocked by explicit routing instructions.
|
|
428
444
|
// Do NOT clear for temporary unavailability (e.g. 429 cooldown, quota cooldown, transient health).
|
|
429
|
-
const shouldAutoClear =
|
|
445
|
+
const shouldAutoClear = (() => {
|
|
446
|
+
if (candidateKeys.length === 0) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
// Prefer selection failed despite eligible keys existing: treat as a hard block (e.g. routing rules).
|
|
450
|
+
if (eligibleKeys.length > 0) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
// If quota explicitly marks the preferred target as out-of-pool, clear the prefer instruction so
|
|
454
|
+
// the router can fall back to other targets without repeatedly retrying an impossible preference.
|
|
455
|
+
if (selectionDeps.quotaView) {
|
|
456
|
+
for (const key of candidateKeys) {
|
|
457
|
+
const entry = selectionDeps.quotaView(key);
|
|
458
|
+
if (entry && entry.inPool === false) {
|
|
459
|
+
return true;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return false;
|
|
464
|
+
})();
|
|
430
465
|
if (shouldAutoClear) {
|
|
431
466
|
routingState = {
|
|
432
467
|
...routingState,
|
|
@@ -588,10 +623,18 @@ export class VirtualRouterEngine {
|
|
|
588
623
|
// ignore persistence errors
|
|
589
624
|
}
|
|
590
625
|
}
|
|
591
|
-
//
|
|
592
|
-
//
|
|
593
|
-
//
|
|
626
|
+
// When Host injects quotaView, pool decisions primarily follow quota;
|
|
627
|
+
// however explicit host-provided health signals (quota recovery/depleted/series cooldown)
|
|
628
|
+
// must still be applied so retry selection can avoid obviously blocked runtimes.
|
|
594
629
|
if (this.quotaView) {
|
|
630
|
+
const handledByQuota = applyQuotaRecoveryImpl(event, this.healthManager, (key) => this.clearProviderCooldown(key), this.debug);
|
|
631
|
+
if (handledByQuota) {
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const handledByQuotaDepleted = applyQuotaDepletedImpl(event, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
|
|
635
|
+
if (handledByQuotaDepleted) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
595
638
|
return;
|
|
596
639
|
}
|
|
597
640
|
// 配额恢复事件优先处理:一旦识别到 virtualRouterQuotaRecovery,
|
|
@@ -604,7 +647,6 @@ export class VirtualRouterEngine {
|
|
|
604
647
|
if (handledByQuotaDepleted) {
|
|
605
648
|
return;
|
|
606
649
|
}
|
|
607
|
-
applySeriesCooldownImpl(event, this.providerRegistry, this.healthManager, (key, ttl) => this.markProviderCooldown(key, ttl), this.debug);
|
|
608
650
|
const derived = mapProviderErrorImpl(event, this.providerHealthConfig());
|
|
609
651
|
if (!derived) {
|
|
610
652
|
return;
|
|
@@ -678,7 +720,8 @@ export class VirtualRouterEngine {
|
|
|
678
720
|
loadBalancer: this.loadBalancer,
|
|
679
721
|
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
680
722
|
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
681
|
-
quotaView: this.quotaView
|
|
723
|
+
quotaView: this.quotaView,
|
|
724
|
+
aliasQueueStore: this.aliasQueueStore
|
|
682
725
|
}, { routingState });
|
|
683
726
|
}
|
|
684
727
|
incrementRouteStat(routeName, providerKey) {
|
|
@@ -1015,7 +1058,9 @@ export class VirtualRouterEngine {
|
|
|
1015
1058
|
contextAdvisor: this.contextAdvisor,
|
|
1016
1059
|
loadBalancer: this.loadBalancer,
|
|
1017
1060
|
isProviderCoolingDown: (key) => this.isProviderCoolingDown(key),
|
|
1018
|
-
resolveStickyKey: (m) => this.resolveStickyKey(m)
|
|
1061
|
+
resolveStickyKey: (m) => this.resolveStickyKey(m),
|
|
1062
|
+
quotaView: this.quotaView,
|
|
1063
|
+
aliasQueueStore: this.aliasQueueStore
|
|
1019
1064
|
}, { routingState: state });
|
|
1020
1065
|
}
|
|
1021
1066
|
extractProviderId(providerKey) {
|
|
@@ -504,10 +504,15 @@ export function applyRoutingInstructions(instructions, currentState) {
|
|
|
504
504
|
const sameMax = typeof newState.stopMessageMaxRepeats === 'number' &&
|
|
505
505
|
Math.floor(newState.stopMessageMaxRepeats) === maxRepeats;
|
|
506
506
|
const isSameInstruction = sameText && sameMax;
|
|
507
|
+
const used = typeof newState.stopMessageUsed === 'number' && Number.isFinite(newState.stopMessageUsed)
|
|
508
|
+
? Math.max(0, Math.floor(newState.stopMessageUsed))
|
|
509
|
+
: 0;
|
|
510
|
+
const hasLastUsedAt = typeof newState.stopMessageLastUsedAt === 'number' && Number.isFinite(newState.stopMessageLastUsedAt);
|
|
511
|
+
const shouldRearm = !isSameInstruction || used > 0 || hasLastUsedAt;
|
|
507
512
|
newState.stopMessageText = text;
|
|
508
513
|
newState.stopMessageMaxRepeats = maxRepeats;
|
|
509
514
|
newState.stopMessageSource = 'explicit';
|
|
510
|
-
if (
|
|
515
|
+
if (shouldRearm) {
|
|
511
516
|
newState.stopMessageUsed = 0;
|
|
512
517
|
newState.stopMessageUpdatedAt = Date.now();
|
|
513
518
|
newState.stopMessageLastUsedAt = undefined;
|
|
@@ -10,6 +10,11 @@ type StopMessageSubset = Pick<RoutingInstructionState, 'stopMessageSource' | 'st
|
|
|
10
10
|
* Strategy:
|
|
11
11
|
* - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config.
|
|
12
12
|
* - Otherwise → adopt persisted fully.
|
|
13
|
+
*
|
|
14
|
+
* Note:
|
|
15
|
+
* - We intentionally do NOT merge counters from an older persisted config into a newer in-memory config.
|
|
16
|
+
* A stopMessage "set" is expected to re-arm/reset counters; allowing older lastUsedAt to overwrite
|
|
17
|
+
* would make re-arming flaky until the async persistence catches up.
|
|
13
18
|
*/
|
|
14
19
|
export declare function mergeStopMessageFromPersisted(existing: StopMessageSubset, persisted: StopMessageSubset | null): StopMessageSubset;
|
|
15
20
|
export {};
|
|
@@ -21,6 +21,11 @@ function lastUsedAtOf(state) {
|
|
|
21
21
|
* Strategy:
|
|
22
22
|
* - If existing has a newer stopMessageUpdatedAt than persisted → keep existing config.
|
|
23
23
|
* - Otherwise → adopt persisted fully.
|
|
24
|
+
*
|
|
25
|
+
* Note:
|
|
26
|
+
* - We intentionally do NOT merge counters from an older persisted config into a newer in-memory config.
|
|
27
|
+
* A stopMessage "set" is expected to re-arm/reset counters; allowing older lastUsedAt to overwrite
|
|
28
|
+
* would make re-arming flaky until the async persistence catches up.
|
|
24
29
|
*/
|
|
25
30
|
export function mergeStopMessageFromPersisted(existing, persisted) {
|
|
26
31
|
if (!persisted) {
|
|
@@ -40,18 +45,5 @@ export function mergeStopMessageFromPersisted(existing, persisted) {
|
|
|
40
45
|
stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
|
|
41
46
|
};
|
|
42
47
|
}
|
|
43
|
-
|
|
44
|
-
const existingLastUsedAt = lastUsedAtOf(existing);
|
|
45
|
-
const persistedLastUsedAt = lastUsedAtOf(persisted);
|
|
46
|
-
const countersAreNewer = persistedLastUsedAt !== null &&
|
|
47
|
-
(existingLastUsedAt === null || persistedLastUsedAt > existingLastUsedAt);
|
|
48
|
-
return {
|
|
49
|
-
...existing,
|
|
50
|
-
...(countersAreNewer
|
|
51
|
-
? {
|
|
52
|
-
stopMessageUsed: persisted.stopMessageUsed,
|
|
53
|
-
stopMessageLastUsedAt: persisted.stopMessageLastUsedAt
|
|
54
|
-
}
|
|
55
|
-
: {})
|
|
56
|
-
};
|
|
48
|
+
return { ...existing };
|
|
57
49
|
}
|
|
@@ -22,7 +22,7 @@ export interface RoutePoolTier {
|
|
|
22
22
|
* Optional force flag for this route pool.
|
|
23
23
|
* Currently interpreted for:
|
|
24
24
|
* - routing.vision: force dedicated vision backend handling.
|
|
25
|
-
* - routing.web_search
|
|
25
|
+
* - routing.web_search: force server-side web_search flow.
|
|
26
26
|
*/
|
|
27
27
|
force?: boolean;
|
|
28
28
|
}
|
|
@@ -100,6 +100,15 @@ export interface VirtualRouterClassifierConfig {
|
|
|
100
100
|
export interface LoadBalancingPolicy {
|
|
101
101
|
strategy: 'round-robin' | 'weighted' | 'sticky';
|
|
102
102
|
weights?: Record<string, number>;
|
|
103
|
+
/**
|
|
104
|
+
* Alias-level selection strategy (provider auth aliases).
|
|
105
|
+
*
|
|
106
|
+
* Use this when a provider exposes multiple auth aliases for the same model, and the upstream
|
|
107
|
+
* gateway behaves poorly when requests rapidly switch across keys (e.g. repeated 429 "no capacity"
|
|
108
|
+
* despite quota). Strategies are applied inside VirtualRouter selection only; providers remain
|
|
109
|
+
* transport-only.
|
|
110
|
+
*/
|
|
111
|
+
aliasSelection?: AliasSelectionConfig;
|
|
103
112
|
/**
|
|
104
113
|
* AWRR: health-weighted selection.
|
|
105
114
|
* - Deterministic (no randomness)
|
|
@@ -143,6 +152,21 @@ export interface HealthWeightedLoadBalancingConfig {
|
|
|
143
152
|
*/
|
|
144
153
|
recoverToBestOnRetry?: boolean;
|
|
145
154
|
}
|
|
155
|
+
export type AliasSelectionStrategy = 'none' | 'sticky-queue';
|
|
156
|
+
export interface AliasSelectionConfig {
|
|
157
|
+
/**
|
|
158
|
+
* Global on/off switch. When false, no alias-level selection is applied.
|
|
159
|
+
*/
|
|
160
|
+
enabled?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Default strategy used when a provider has no explicit override.
|
|
163
|
+
*/
|
|
164
|
+
defaultStrategy?: AliasSelectionStrategy;
|
|
165
|
+
/**
|
|
166
|
+
* Per-provider overrides keyed by providerId (e.g. "antigravity").
|
|
167
|
+
*/
|
|
168
|
+
providers?: Record<string, AliasSelectionStrategy>;
|
|
169
|
+
}
|
|
146
170
|
export interface ContextWeightedLoadBalancingConfig {
|
|
147
171
|
/**
|
|
148
172
|
* When false, context-weighted logic is disabled.
|
|
@@ -5,3 +5,11 @@ export declare const CLOCK_CONFIG_DEFAULTS: {
|
|
|
5
5
|
readonly tickMs: 60000;
|
|
6
6
|
};
|
|
7
7
|
export declare function normalizeClockConfig(raw: unknown): ClockConfigSnapshot | null;
|
|
8
|
+
/**
|
|
9
|
+
* Resolve the effective clock config for a request/session.
|
|
10
|
+
*
|
|
11
|
+
* - If a config object exists and enabled=true -> return normalized config.
|
|
12
|
+
* - If the config is explicitly present but disabled/invalid -> return null.
|
|
13
|
+
* - If the config is absent (undefined) -> return null (opt-in only).
|
|
14
|
+
*/
|
|
15
|
+
export declare function resolveClockConfig(raw: unknown): ClockConfigSnapshot | null;
|
|
@@ -3,6 +3,11 @@ export const CLOCK_CONFIG_DEFAULTS = {
|
|
|
3
3
|
dueWindowMs: 60_000,
|
|
4
4
|
tickMs: 60_000
|
|
5
5
|
};
|
|
6
|
+
function isClockDisabledByEnv() {
|
|
7
|
+
const raw = process.env.ROUTECODEX_DISABLE_CLOCK ?? process.env.LLMSWITCH_DISABLE_CLOCK ?? '';
|
|
8
|
+
const v = String(raw).trim().toLowerCase();
|
|
9
|
+
return v === '1' || v === 'true' || v === 'yes' || v === 'on';
|
|
10
|
+
}
|
|
6
11
|
export function normalizeClockConfig(raw) {
|
|
7
12
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
8
13
|
return null;
|
|
@@ -25,3 +30,20 @@ export function normalizeClockConfig(raw) {
|
|
|
25
30
|
: CLOCK_CONFIG_DEFAULTS.tickMs;
|
|
26
31
|
return { enabled: true, retentionMs, dueWindowMs, tickMs };
|
|
27
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Resolve the effective clock config for a request/session.
|
|
35
|
+
*
|
|
36
|
+
* - If a config object exists and enabled=true -> return normalized config.
|
|
37
|
+
* - If the config is explicitly present but disabled/invalid -> return null.
|
|
38
|
+
* - If the config is absent (undefined) -> return null (opt-in only).
|
|
39
|
+
*/
|
|
40
|
+
export function resolveClockConfig(raw) {
|
|
41
|
+
if (isClockDisabledByEnv()) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const normalized = normalizeClockConfig(raw);
|
|
45
|
+
if (normalized) {
|
|
46
|
+
return normalized;
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export const CLOCK_LOG_GOLD = '\x1b[38;5;220m';
|
|
2
|
+
export const CLOCK_LOG_RESET = '\x1b[0m';
|
|
3
|
+
export function logClock(message, extra) {
|
|
4
|
+
try {
|
|
5
|
+
// eslint-disable-next-line no-console
|
|
6
|
+
console.log(`${CLOCK_LOG_GOLD}[servertool][clock] ${message}` +
|
|
7
|
+
(extra ? ` ${JSON.stringify(extra)}` : '') +
|
|
8
|
+
CLOCK_LOG_RESET);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
// best-effort logging
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export type { ClockConfigSnapshot, ClockReservation, ClockScheduleItem, ClockSessionState, ClockTask } from './types.js';
|
|
2
|
-
export { normalizeClockConfig } from './config.js';
|
|
2
|
+
export { normalizeClockConfig, resolveClockConfig } from './config.js';
|
|
3
3
|
export { startClockDaemonIfNeeded, stopClockDaemonForTests } from './daemon.js';
|
|
4
4
|
export { loadClockSessionState, clearClockSession } from './session-store.js';
|
|
5
5
|
export { cancelClockTask, clearClockTasks, commitClockReservation, findNextUndeliveredDueAtMs, listClockTasks, parseDueAtMs, reserveDueTasksForRequest, scheduleClockTasks, selectDueUndeliveredTasks } from './tasks.js';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { normalizeClockConfig } from './config.js';
|
|
1
|
+
export { normalizeClockConfig, resolveClockConfig } from './config.js';
|
|
2
2
|
export { startClockDaemonIfNeeded, stopClockDaemonForTests } from './daemon.js';
|
|
3
3
|
export { loadClockSessionState, clearClockSession } from './session-store.js';
|
|
4
4
|
export { cancelClockTask, clearClockTasks, commitClockReservation, findNextUndeliveredDueAtMs, listClockTasks, parseDueAtMs, reserveDueTasksForRequest, scheduleClockTasks, selectDueUndeliveredTasks } from './tasks.js';
|
|
@@ -15,7 +15,7 @@ function safeJson(value) {
|
|
|
15
15
|
}
|
|
16
16
|
function safeQuoted(text) {
|
|
17
17
|
const normalized = String(text ?? '');
|
|
18
|
-
const escaped = normalized.replace(
|
|
18
|
+
const escaped = normalized.replace(/"/g, '\\"');
|
|
19
19
|
return `"${escaped}"`;
|
|
20
20
|
}
|
|
21
21
|
function buildTaskId() {
|
|
@@ -1,11 +1,39 @@
|
|
|
1
1
|
import { runServerSideToolEngine } from './server-side-tools.js';
|
|
2
2
|
import { ProviderProtocolError } from '../conversion/shared/errors.js';
|
|
3
|
+
import { ensureRuntimeMetadata, readRuntimeMetadata } from '../conversion/shared/runtime-metadata.js';
|
|
3
4
|
import { createHash } from 'node:crypto';
|
|
4
5
|
import { loadRoutingInstructionStateSync, saveRoutingInstructionStateSync } from '../router/virtual-router/sticky-session-store.js';
|
|
5
6
|
import { deserializeRoutingInstructionState, serializeRoutingInstructionState } from '../router/virtual-router/routing-instructions.js';
|
|
6
7
|
import { applyHubFollowupPolicyShadow } from './followup-shadow.js';
|
|
7
8
|
import { buildServerToolFollowupChatPayloadFromInjection } from './handlers/followup-request-builder.js';
|
|
8
|
-
import { findNextUndeliveredDueAtMs, listClockTasks,
|
|
9
|
+
import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig } from './clock/task-store.js';
|
|
10
|
+
import { savePendingServerToolInjection } from './pending-session.js';
|
|
11
|
+
function stripToolHistoryFromFollowupMessages(raw) {
|
|
12
|
+
const messages = Array.isArray(raw) ? raw : null;
|
|
13
|
+
if (!messages) {
|
|
14
|
+
return raw;
|
|
15
|
+
}
|
|
16
|
+
const out = [];
|
|
17
|
+
for (const msg of messages) {
|
|
18
|
+
if (!msg || typeof msg !== 'object' || Array.isArray(msg)) {
|
|
19
|
+
out.push(msg);
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
const record = msg;
|
|
23
|
+
const role = typeof record.role === 'string' ? record.role.trim().toLowerCase() : '';
|
|
24
|
+
// Drop tool-role messages entirely for a "no-tools" recovery followup.
|
|
25
|
+
if (role === 'tool') {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
// Remove OpenAI tool call fields that could trigger Gemini strict validation.
|
|
29
|
+
const cloned = { ...record };
|
|
30
|
+
delete cloned.tool_calls;
|
|
31
|
+
delete cloned.tool_call_id;
|
|
32
|
+
delete cloned.name;
|
|
33
|
+
out.push(cloned);
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
9
37
|
function parseTimeoutMs(raw, fallback) {
|
|
10
38
|
const n = typeof raw === 'string' ? Number(raw.trim()) : typeof raw === 'number' ? raw : NaN;
|
|
11
39
|
if (!Number.isFinite(n) || n <= 0) {
|
|
@@ -207,14 +235,15 @@ async function shouldDisableServerToolTimeoutForClockHold(args) {
|
|
|
207
235
|
return false;
|
|
208
236
|
}
|
|
209
237
|
const record = args.adapterContext;
|
|
210
|
-
const
|
|
211
|
-
if (!clockConfig) {
|
|
212
|
-
return false;
|
|
213
|
-
}
|
|
238
|
+
const rt = readRuntimeMetadata(record);
|
|
214
239
|
const sessionId = typeof record.sessionId === 'string' ? record.sessionId.trim() : '';
|
|
215
240
|
if (!sessionId) {
|
|
216
241
|
return false;
|
|
217
242
|
}
|
|
243
|
+
const clockConfig = resolveClockConfig(rt?.clock);
|
|
244
|
+
if (!clockConfig) {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
218
247
|
// If already within due window, clock_auto won't need long hold.
|
|
219
248
|
try {
|
|
220
249
|
const tasks = await listClockTasks(sessionId, clockConfig);
|
|
@@ -282,6 +311,29 @@ export async function runServerToolOrchestration(options) {
|
|
|
282
311
|
const flowId = engineResult.execution.flowId ?? 'unknown';
|
|
283
312
|
const totalSteps = 5;
|
|
284
313
|
logProgress(1, totalSteps, 'matched', { flowId });
|
|
314
|
+
// Mixed tools: persist servertool outputs for next request, but return remaining tool_calls to client.
|
|
315
|
+
if (engineResult.pendingInjection) {
|
|
316
|
+
const sessionId = engineResult.pendingInjection.sessionId;
|
|
317
|
+
if (sessionId && sessionId.trim()) {
|
|
318
|
+
try {
|
|
319
|
+
await savePendingServerToolInjection(sessionId.trim(), {
|
|
320
|
+
createdAtMs: Date.now(),
|
|
321
|
+
afterToolCallIds: engineResult.pendingInjection.afterToolCallIds,
|
|
322
|
+
messages: engineResult.pendingInjection.messages,
|
|
323
|
+
sourceRequestId: options.requestId
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
catch {
|
|
327
|
+
// best-effort: do not fail the response conversion just because persistence failed
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
logProgress(5, totalSteps, 'completed (mixed tools; no reenter)', { flowId });
|
|
331
|
+
return {
|
|
332
|
+
chat: engineResult.finalChatResponse,
|
|
333
|
+
executed: true,
|
|
334
|
+
flowId: engineResult.execution.flowId
|
|
335
|
+
};
|
|
336
|
+
}
|
|
285
337
|
if (!engineResult.execution.followup || !options.reenterPipeline) {
|
|
286
338
|
logProgress(5, totalSteps, 'completed (no followup)', { flowId });
|
|
287
339
|
return {
|
|
@@ -328,7 +380,41 @@ export async function runServerToolOrchestration(options) {
|
|
|
328
380
|
}
|
|
329
381
|
return null;
|
|
330
382
|
})();
|
|
331
|
-
|
|
383
|
+
// Prevent nested followup execution on serverToolFollowup hops.
|
|
384
|
+
// Followup responses should still be eligible for servertool triggers (e.g. clock/web_search parsing),
|
|
385
|
+
// but they must not start a new followup flow inside an existing followup hop.
|
|
386
|
+
//
|
|
387
|
+
// Exception: allow continuing the same flow when serverToolLoopState.flowId matches.
|
|
388
|
+
const followupSeedPayload = (() => {
|
|
389
|
+
if (!followupPayloadRaw) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
const rt = readRuntimeMetadata(options.adapterContext);
|
|
394
|
+
const followupFlagRaw = rt?.serverToolFollowup;
|
|
395
|
+
const isFollowup = followupFlagRaw === true ||
|
|
396
|
+
(typeof followupFlagRaw === 'string' && followupFlagRaw.trim().toLowerCase() === 'true');
|
|
397
|
+
if (!isFollowup) {
|
|
398
|
+
return followupPayloadRaw;
|
|
399
|
+
}
|
|
400
|
+
const loopState = rt?.serverToolLoopState;
|
|
401
|
+
const loopFlowId = loopState && typeof loopState === 'object' && !Array.isArray(loopState)
|
|
402
|
+
? String(loopState.flowId || '').trim()
|
|
403
|
+
: '';
|
|
404
|
+
const flowId = typeof engineResult.execution?.flowId === 'string' && engineResult.execution.flowId.trim().length
|
|
405
|
+
? engineResult.execution.flowId.trim()
|
|
406
|
+
: '';
|
|
407
|
+
if (loopFlowId && flowId && loopFlowId === flowId) {
|
|
408
|
+
return followupPayloadRaw;
|
|
409
|
+
}
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
catch {
|
|
413
|
+
// best-effort: if metadata is malformed, avoid nested followups
|
|
414
|
+
return null;
|
|
415
|
+
}
|
|
416
|
+
})();
|
|
417
|
+
if (!followupSeedPayload) {
|
|
332
418
|
logProgress(5, totalSteps, 'completed (missing followup payload)', { flowId });
|
|
333
419
|
return {
|
|
334
420
|
chat: engineResult.finalChatResponse,
|
|
@@ -336,7 +422,7 @@ export async function runServerToolOrchestration(options) {
|
|
|
336
422
|
flowId: engineResult.execution.flowId
|
|
337
423
|
};
|
|
338
424
|
}
|
|
339
|
-
const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId,
|
|
425
|
+
const loopState = buildServerToolLoopState(options.adapterContext, engineResult.execution.flowId, followupSeedPayload);
|
|
340
426
|
if (applyAutoLimit && loopState && typeof loopState.repeatCount === 'number' && loopState.repeatCount >= 3) {
|
|
341
427
|
logProgress(5, totalSteps, 'completed (auto limit hit)', { flowId });
|
|
342
428
|
return {
|
|
@@ -354,11 +440,14 @@ export async function runServerToolOrchestration(options) {
|
|
|
354
440
|
};
|
|
355
441
|
}
|
|
356
442
|
const metadata = {
|
|
357
|
-
serverToolFollowup: true,
|
|
358
443
|
stream: false,
|
|
359
|
-
...(loopState ? { serverToolLoopState: loopState } : {}),
|
|
360
444
|
...(engineResult.execution.followup.metadata ?? {})
|
|
361
445
|
};
|
|
446
|
+
const rt = ensureRuntimeMetadata(metadata);
|
|
447
|
+
rt.serverToolFollowup = true;
|
|
448
|
+
if (loopState) {
|
|
449
|
+
rt.serverToolLoopState = loopState;
|
|
450
|
+
}
|
|
362
451
|
// Followup re-enters HubPipeline at chat-process entry with a canonical "chat-like" body.
|
|
363
452
|
// This avoids re-running per-protocol inbound parse/semantic-map for each client protocol.
|
|
364
453
|
metadata.__hubEntry = 'chat_process';
|
|
@@ -366,18 +455,44 @@ export async function runServerToolOrchestration(options) {
|
|
|
366
455
|
// - clear any inherited routeHint
|
|
367
456
|
// - do not inherit sticky target
|
|
368
457
|
// - record original entry endpoint for downstream formatting/debug
|
|
369
|
-
|
|
458
|
+
rt.preserveRouteHint = preserveRouteHint;
|
|
370
459
|
// Use empty string (falsy) to avoid VirtualRouter calling `.trim()` on non-string values.
|
|
371
460
|
metadata.routeHint = '';
|
|
372
|
-
|
|
373
|
-
|
|
461
|
+
rt.disableStickyRoutes = true;
|
|
462
|
+
rt.serverToolOriginalEntryEndpoint =
|
|
374
463
|
(typeof options.entryEndpoint === 'string' && options.entryEndpoint.trim().length
|
|
375
464
|
? options.entryEndpoint
|
|
376
465
|
: followupEntryEndpoint);
|
|
466
|
+
// For stateful auto-followups, keep the same providerKey/alias.
|
|
467
|
+
// Otherwise the followup requestId suffix could cause round-robin alias switching or
|
|
468
|
+
// route re-evaluation (e.g. "continue" prompt being treated as a new intent).
|
|
469
|
+
if (isStopMessageFlow || isGeminiEmptyReplyContinue) {
|
|
470
|
+
const providerKeyRaw = options.adapterContext.providerKey;
|
|
471
|
+
const providerKey = typeof providerKeyRaw === 'string' && providerKeyRaw.trim().length ? providerKeyRaw.trim() : '';
|
|
472
|
+
if (providerKey) {
|
|
473
|
+
metadata.__shadowCompareForcedProviderKey = providerKey;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
377
476
|
const retryEmptyFollowupOnce = isStopMessageFlow || isGeminiEmptyReplyContinue;
|
|
378
477
|
const maxAttempts = retryEmptyFollowupOnce ? 2 : 1;
|
|
379
478
|
const followupRequestId = buildFollowupRequestId(options.requestId, engineResult.execution.followup.requestIdSuffix);
|
|
380
|
-
let followupPayload = coerceFollowupPayloadStream(
|
|
479
|
+
let followupPayload = coerceFollowupPayloadStream(followupSeedPayload, metadata.stream === true);
|
|
480
|
+
if (isGeminiEmptyReplyContinue) {
|
|
481
|
+
// For gemini_empty_reply_continue, the goal is to recover text output from an empty/malformed reply.
|
|
482
|
+
// Force the followup to be non-tool-calling to avoid repeated MALFORMED_FUNCTION_CALL loops.
|
|
483
|
+
const paramsRaw = followupPayload.parameters;
|
|
484
|
+
const params = paramsRaw && typeof paramsRaw === 'object' && !Array.isArray(paramsRaw) ? { ...paramsRaw } : {};
|
|
485
|
+
params.tool_choice = 'none';
|
|
486
|
+
params.parallel_tool_calls = false;
|
|
487
|
+
// Ensure we don't override the tool_choice->toolConfig mapping with an inherited tool_config.
|
|
488
|
+
delete params.tool_config;
|
|
489
|
+
delete params.toolConfig;
|
|
490
|
+
followupPayload.parameters = params;
|
|
491
|
+
// Additionally, strip tool-call history. Gemini/CloudCode can strict-validate
|
|
492
|
+
// (history tool calls) ↔ (current tool declarations). We keep tools declared (so the
|
|
493
|
+
// session can continue), but remove history tool artifacts to avoid malformed loops.
|
|
494
|
+
followupPayload.messages = stripToolHistoryFromFollowupMessages(followupPayload.messages);
|
|
495
|
+
}
|
|
381
496
|
followupPayload = applyHubFollowupPolicyShadow({
|
|
382
497
|
requestId: followupRequestId,
|
|
383
498
|
entryEndpoint: followupEntryEndpoint,
|
|
@@ -499,7 +614,8 @@ function reserveStopMessageUsage(adapterContext) {
|
|
|
499
614
|
}
|
|
500
615
|
let state = loadRoutingInstructionStateSync(stickyKey);
|
|
501
616
|
if (!state || !state.stopMessageText || !state.stopMessageMaxRepeats) {
|
|
502
|
-
const
|
|
617
|
+
const rt = readRuntimeMetadata(adapterContext);
|
|
618
|
+
const fallback = resolveStopMessageSnapshot(rt?.stopMessageState);
|
|
503
619
|
if (!fallback) {
|
|
504
620
|
return null;
|
|
505
621
|
}
|
|
@@ -518,13 +634,18 @@ function reserveStopMessageUsage(adapterContext) {
|
|
|
518
634
|
: 0;
|
|
519
635
|
const nextUsed = used + 1;
|
|
520
636
|
state.stopMessageUsed = nextUsed;
|
|
521
|
-
|
|
637
|
+
const now = Date.now();
|
|
638
|
+
state.stopMessageLastUsedAt = now;
|
|
522
639
|
if (nextUsed >= maxRepeats) {
|
|
640
|
+
// Auto-clear after reaching max repeats. This avoids leaving an "exhausted" stopMessage
|
|
641
|
+
// stuck in sticky state and ensures a fresh `<**stopMessage:...**>` can re-arm cleanly.
|
|
523
642
|
state.stopMessageText = undefined;
|
|
524
643
|
state.stopMessageMaxRepeats = undefined;
|
|
525
644
|
state.stopMessageUsed = undefined;
|
|
526
|
-
state.
|
|
527
|
-
|
|
645
|
+
state.stopMessageSource = undefined;
|
|
646
|
+
// Keep monotonic timestamps as a tombstone to prevent accidental re-application from replayed history.
|
|
647
|
+
state.stopMessageUpdatedAt = now;
|
|
648
|
+
state.stopMessageLastUsedAt = now;
|
|
528
649
|
}
|
|
529
650
|
saveRoutingInstructionStateSync(stickyKey, state);
|
|
530
651
|
return { stickyKey, previousState };
|
|
@@ -544,11 +665,13 @@ function disableStopMessageAfterFailedFollowup(adapterContext, reservation) {
|
|
|
544
665
|
if (!state) {
|
|
545
666
|
return;
|
|
546
667
|
}
|
|
668
|
+
const now = Date.now();
|
|
547
669
|
state.stopMessageText = undefined;
|
|
548
670
|
state.stopMessageMaxRepeats = undefined;
|
|
549
671
|
state.stopMessageUsed = undefined;
|
|
550
|
-
state.
|
|
551
|
-
state.
|
|
672
|
+
state.stopMessageSource = undefined;
|
|
673
|
+
state.stopMessageUpdatedAt = now;
|
|
674
|
+
state.stopMessageLastUsedAt = now;
|
|
552
675
|
saveRoutingInstructionStateSync(key, state);
|
|
553
676
|
}
|
|
554
677
|
catch {
|
|
@@ -715,7 +838,8 @@ function readServerToolLoopState(adapterContext) {
|
|
|
715
838
|
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
716
839
|
return null;
|
|
717
840
|
}
|
|
718
|
-
const
|
|
841
|
+
const rt = readRuntimeMetadata(adapterContext);
|
|
842
|
+
const raw = rt?.serverToolLoopState;
|
|
719
843
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
720
844
|
return null;
|
|
721
845
|
}
|
|
@@ -774,7 +898,8 @@ function getStopMessageSource(adapterContext) {
|
|
|
774
898
|
if (!adapterContext || typeof adapterContext !== 'object') {
|
|
775
899
|
return undefined;
|
|
776
900
|
}
|
|
777
|
-
const
|
|
901
|
+
const rt = readRuntimeMetadata(adapterContext);
|
|
902
|
+
const raw = rt?.stopMessageState;
|
|
778
903
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
779
904
|
return undefined;
|
|
780
905
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { registerServerToolHandler } from '../registry.js';
|
|
2
2
|
import { extractCapturedChatSeed } from './followup-request-builder.js';
|
|
3
|
-
import {
|
|
3
|
+
import { readRuntimeMetadata } from '../../conversion/shared/runtime-metadata.js';
|
|
4
|
+
import { findNextUndeliveredDueAtMs, listClockTasks, resolveClockConfig, startClockDaemonIfNeeded } from '../clock/task-store.js';
|
|
5
|
+
import { logClock } from '../clock/log.js';
|
|
4
6
|
const FLOW_ID = 'clock_hold_flow';
|
|
5
7
|
function resolveClientConnectionState(value) {
|
|
6
8
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -96,15 +98,17 @@ const handler = async (ctx) => {
|
|
|
96
98
|
(typeof clientDisconnectedRaw === 'string' && clientDisconnectedRaw.trim().toLowerCase() === 'true')) {
|
|
97
99
|
return null;
|
|
98
100
|
}
|
|
99
|
-
const
|
|
100
|
-
if (!clockConfig) {
|
|
101
|
-
return null;
|
|
102
|
-
}
|
|
103
|
-
await startClockDaemonIfNeeded(clockConfig);
|
|
101
|
+
const rt = readRuntimeMetadata(ctx.adapterContext);
|
|
104
102
|
const sessionId = resolveSessionId(ctx.adapterContext);
|
|
105
103
|
if (!sessionId) {
|
|
106
104
|
return null;
|
|
107
105
|
}
|
|
106
|
+
// Default-enable clock when config is absent, but keep "explicitly disabled" honored.
|
|
107
|
+
const clockConfig = resolveClockConfig(rt?.clock);
|
|
108
|
+
if (!clockConfig) {
|
|
109
|
+
return null;
|
|
110
|
+
}
|
|
111
|
+
await startClockDaemonIfNeeded(clockConfig);
|
|
108
112
|
const seed = extractCapturedChatSeed(record.capturedChatRequest);
|
|
109
113
|
if (!seed) {
|
|
110
114
|
return null;
|
|
@@ -117,6 +121,7 @@ const handler = async (ctx) => {
|
|
|
117
121
|
}
|
|
118
122
|
// Wait until the "due window" is reached (now >= dueAt - dueWindowMs).
|
|
119
123
|
const thresholdMs = nextDueAtMs - clockConfig.dueWindowMs;
|
|
124
|
+
logClock('hold_start', { sessionId, nextDueAtMs, thresholdMs });
|
|
120
125
|
while (Date.now() < thresholdMs) {
|
|
121
126
|
const state = resolveClientConnectionState(ctx.adapterContext.clientConnectionState);
|
|
122
127
|
if (state?.disconnected === true) {
|