@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
|
@@ -2,7 +2,9 @@ import { runChatRequestToolFilters } from '../../shared/tool-filter-pipeline.js'
|
|
|
2
2
|
import { ToolGovernanceEngine } from '../tool-governance/index.js';
|
|
3
3
|
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
4
4
|
import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
|
|
5
|
+
import { clearClockSession, normalizeClockConfig, reserveDueTasksForRequest, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
|
|
5
6
|
import { isJsonObject } from '../types/json.js';
|
|
7
|
+
import { applyHubOperations } from '../ops/operations.js';
|
|
6
8
|
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
7
9
|
export async function runHubChatProcess(options) {
|
|
8
10
|
const startTime = Date.now();
|
|
@@ -90,28 +92,22 @@ async function applyRequestToolGovernance(request, context) {
|
|
|
90
92
|
merged.metadata.hasImageAttachment = true;
|
|
91
93
|
}
|
|
92
94
|
if (typeof inboundStreamIntent === 'boolean') {
|
|
93
|
-
merged
|
|
94
|
-
...merged.metadata,
|
|
95
|
-
inboundStream: inboundStreamIntent
|
|
96
|
-
};
|
|
95
|
+
merged = applyHubOperations(merged, buildInboundStreamingOperations(inboundStreamIntent));
|
|
97
96
|
}
|
|
98
97
|
if (typeof governed.stream === 'boolean') {
|
|
99
|
-
merged
|
|
100
|
-
...merged.parameters,
|
|
101
|
-
stream: governed.stream
|
|
102
|
-
};
|
|
98
|
+
merged = applyHubOperations(merged, buildOutboundStreamingOperations(governed.stream));
|
|
103
99
|
}
|
|
104
100
|
if (governed.tool_choice !== undefined) {
|
|
105
|
-
merged
|
|
106
|
-
...merged.parameters,
|
|
107
|
-
tool_choice: governed.tool_choice
|
|
108
|
-
};
|
|
101
|
+
merged = applyHubOperations(merged, buildToolChoiceOperations(governed.tool_choice));
|
|
109
102
|
}
|
|
110
103
|
if (typeof governed.model === 'string' && governed.model.trim()) {
|
|
111
104
|
merged.model = governed.model.trim();
|
|
112
105
|
}
|
|
113
106
|
// Server-side web_search tool injection (config-driven, best-effort).
|
|
114
|
-
merged =
|
|
107
|
+
merged = applyHubOperations(merged, buildWebSearchOperations(merged, metadata));
|
|
108
|
+
// Server-side clock tool + scheduled reminders injection (config-driven, best-effort).
|
|
109
|
+
merged = applyHubOperations(merged, buildClockOperations(metadata));
|
|
110
|
+
merged = await maybeInjectClockRemindersAndApplyDirectives(merged, metadata, context.requestId);
|
|
115
111
|
const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
|
|
116
112
|
if (summary.applied) {
|
|
117
113
|
sanitized.metadata = {
|
|
@@ -397,18 +393,49 @@ function isRecord(value) {
|
|
|
397
393
|
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
398
394
|
}
|
|
399
395
|
function maybeInjectWebSearchTool(request, metadata) {
|
|
396
|
+
const ops = buildWebSearchOperations(request, metadata);
|
|
397
|
+
if (!ops.length) {
|
|
398
|
+
return request;
|
|
399
|
+
}
|
|
400
|
+
return applyHubOperations(request, ops);
|
|
401
|
+
}
|
|
402
|
+
function buildInboundStreamingOperations(intent) {
|
|
403
|
+
return [
|
|
404
|
+
{
|
|
405
|
+
op: 'set_request_metadata_fields',
|
|
406
|
+
fields: { inboundStream: intent }
|
|
407
|
+
}
|
|
408
|
+
];
|
|
409
|
+
}
|
|
410
|
+
function buildOutboundStreamingOperations(stream) {
|
|
411
|
+
return [
|
|
412
|
+
{
|
|
413
|
+
op: 'set_request_parameter_fields',
|
|
414
|
+
fields: { stream }
|
|
415
|
+
}
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
function buildToolChoiceOperations(toolChoice) {
|
|
419
|
+
return [
|
|
420
|
+
{
|
|
421
|
+
op: 'set_request_parameter_fields',
|
|
422
|
+
fields: { tool_choice: toolChoice }
|
|
423
|
+
}
|
|
424
|
+
];
|
|
425
|
+
}
|
|
426
|
+
function buildWebSearchOperations(request, metadata) {
|
|
400
427
|
// ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
|
|
401
428
|
// 以避免在 web_search 流程内部形成循环命中。
|
|
402
429
|
if (metadata.serverToolFollowup === true) {
|
|
403
|
-
return
|
|
430
|
+
return [];
|
|
404
431
|
}
|
|
405
432
|
const rawConfig = metadata.webSearch;
|
|
406
433
|
if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
|
|
407
|
-
return
|
|
434
|
+
return [];
|
|
408
435
|
}
|
|
409
436
|
const semanticsWebSearch = extractWebSearchSemantics(request.semantics);
|
|
410
437
|
if (semanticsWebSearch?.disable === true) {
|
|
411
|
-
return
|
|
438
|
+
return [];
|
|
412
439
|
}
|
|
413
440
|
const injectPolicy = semanticsWebSearch?.force === true
|
|
414
441
|
? 'always'
|
|
@@ -420,26 +447,10 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
420
447
|
// 仅当当前这一轮用户输入明确表达“联网搜索”意图时才注入 web_search。
|
|
421
448
|
// 不再依赖上一轮工具分类(read/search/websearch),避免形成隐式续写语义。
|
|
422
449
|
if (!intent.hasIntent) {
|
|
423
|
-
return
|
|
450
|
+
return [];
|
|
424
451
|
}
|
|
425
452
|
}
|
|
426
453
|
const existingTools = Array.isArray(request.tools) ? request.tools : [];
|
|
427
|
-
const hasWebSearch = existingTools.some((tool) => {
|
|
428
|
-
if (!tool || typeof tool !== 'object')
|
|
429
|
-
return false;
|
|
430
|
-
const fn = tool.function;
|
|
431
|
-
return typeof fn?.name === 'string' && fn.name.trim() === 'web_search';
|
|
432
|
-
});
|
|
433
|
-
if (hasWebSearch) {
|
|
434
|
-
const nextMetadata = {
|
|
435
|
-
...(request.metadata ?? {}),
|
|
436
|
-
webSearchEnabled: true
|
|
437
|
-
};
|
|
438
|
-
return {
|
|
439
|
-
...request,
|
|
440
|
-
metadata: nextMetadata
|
|
441
|
-
};
|
|
442
|
-
}
|
|
443
454
|
let engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim() && !engine.serverToolsDisabled);
|
|
444
455
|
// 当用户明确要求「谷歌搜索」时,只暴露 Gemini / Antigravity 类搜索后端:
|
|
445
456
|
// - providerKey 以 gemini-cli. 或 antigravity. 开头;
|
|
@@ -461,7 +472,7 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
461
472
|
}
|
|
462
473
|
}
|
|
463
474
|
if (!engines.length) {
|
|
464
|
-
return
|
|
475
|
+
return [];
|
|
465
476
|
}
|
|
466
477
|
const engineIds = engines.map((engine) => engine.id.trim());
|
|
467
478
|
const engineDescriptions = engines
|
|
@@ -511,15 +522,215 @@ function maybeInjectWebSearchTool(request, metadata) {
|
|
|
511
522
|
strict: true
|
|
512
523
|
}
|
|
513
524
|
};
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
525
|
+
const ops = [
|
|
526
|
+
{
|
|
527
|
+
op: 'set_request_metadata_fields',
|
|
528
|
+
fields: { webSearchEnabled: true }
|
|
529
|
+
}
|
|
530
|
+
];
|
|
531
|
+
ops.push({
|
|
532
|
+
op: 'append_tool_if_missing',
|
|
533
|
+
toolName: 'web_search',
|
|
534
|
+
tool: webSearchTool
|
|
535
|
+
});
|
|
536
|
+
return ops;
|
|
537
|
+
}
|
|
538
|
+
function buildClockOperations(metadata) {
|
|
539
|
+
const rawConfig = metadata.clock;
|
|
540
|
+
const clockConfig = normalizeClockConfig(rawConfig);
|
|
541
|
+
if (!clockConfig) {
|
|
542
|
+
return [];
|
|
543
|
+
}
|
|
544
|
+
const parameters = {
|
|
545
|
+
type: 'object',
|
|
546
|
+
properties: {
|
|
547
|
+
action: {
|
|
548
|
+
type: 'string',
|
|
549
|
+
enum: ['schedule', 'list', 'cancel', 'clear'],
|
|
550
|
+
description: 'Schedule, list, cancel, or clear session-scoped reminders.'
|
|
551
|
+
},
|
|
552
|
+
items: {
|
|
553
|
+
type: 'array',
|
|
554
|
+
description: 'For schedule: list of reminders to add.',
|
|
555
|
+
items: {
|
|
556
|
+
type: 'object',
|
|
557
|
+
properties: {
|
|
558
|
+
dueAt: {
|
|
559
|
+
type: 'string',
|
|
560
|
+
description: 'ISO8601 datetime with timezone (e.g. 2026-01-21T20:30:00-08:00).'
|
|
561
|
+
},
|
|
562
|
+
task: {
|
|
563
|
+
type: 'string',
|
|
564
|
+
description: 'Reminder text (should include which tool to use and what to do).'
|
|
565
|
+
},
|
|
566
|
+
tool: {
|
|
567
|
+
type: 'string',
|
|
568
|
+
description: 'Optional suggested tool name (hint only).'
|
|
569
|
+
},
|
|
570
|
+
arguments: {
|
|
571
|
+
type: 'object',
|
|
572
|
+
description: 'Optional suggested tool arguments (hint only).',
|
|
573
|
+
additionalProperties: true
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
required: ['dueAt', 'task'],
|
|
577
|
+
additionalProperties: false
|
|
578
|
+
}
|
|
579
|
+
},
|
|
580
|
+
taskId: {
|
|
581
|
+
type: 'string',
|
|
582
|
+
description: 'For cancel: taskId to remove.'
|
|
583
|
+
}
|
|
584
|
+
},
|
|
585
|
+
required: ['action'],
|
|
586
|
+
additionalProperties: false
|
|
517
587
|
};
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
588
|
+
const clockTool = {
|
|
589
|
+
type: 'function',
|
|
590
|
+
function: {
|
|
591
|
+
name: 'clock',
|
|
592
|
+
description: 'Schedule session-scoped reminders. Use schedule/list/cancel/clear. Scheduled reminders will be injected into future requests as [scheduled task:"..."].',
|
|
593
|
+
parameters,
|
|
594
|
+
strict: true
|
|
595
|
+
}
|
|
522
596
|
};
|
|
597
|
+
return [
|
|
598
|
+
{ op: 'set_request_metadata_fields', fields: { clockEnabled: true, serverToolRequired: true } },
|
|
599
|
+
{ op: 'append_tool_if_missing', toolName: 'clock', tool: clockTool }
|
|
600
|
+
];
|
|
601
|
+
}
|
|
602
|
+
function resolveSessionIdForClock(metadata, request) {
|
|
603
|
+
const candidate = readString(metadata.sessionId) ?? readString(request.metadata?.sessionId);
|
|
604
|
+
return candidate && candidate.trim() ? candidate.trim() : null;
|
|
605
|
+
}
|
|
606
|
+
function stripClockClearDirectiveFromText(text) {
|
|
607
|
+
const pattern = /<\*\*\s*clock\s*:\s*clear\s*\*\*>/gi;
|
|
608
|
+
const hadClear = pattern.test(text);
|
|
609
|
+
if (!hadClear) {
|
|
610
|
+
return { hadClear: false, next: text };
|
|
611
|
+
}
|
|
612
|
+
const replaced = text.replace(pattern, '');
|
|
613
|
+
// Clean up leftover excessive blank lines to keep prompts tidy.
|
|
614
|
+
const next = replaced.replace(/\n{3,}/g, '\n\n').trim();
|
|
615
|
+
return { hadClear: true, next };
|
|
616
|
+
}
|
|
617
|
+
function stripClockClearDirectiveFromContent(content) {
|
|
618
|
+
if (typeof content === 'string') {
|
|
619
|
+
const { hadClear, next } = stripClockClearDirectiveFromText(content);
|
|
620
|
+
return { hadClear, next };
|
|
621
|
+
}
|
|
622
|
+
if (Array.isArray(content)) {
|
|
623
|
+
let hadClear = false;
|
|
624
|
+
const next = content.map((part) => {
|
|
625
|
+
if (typeof part === 'string') {
|
|
626
|
+
const stripped = stripClockClearDirectiveFromText(part);
|
|
627
|
+
if (stripped.hadClear)
|
|
628
|
+
hadClear = true;
|
|
629
|
+
return stripped.next;
|
|
630
|
+
}
|
|
631
|
+
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
632
|
+
const block = part;
|
|
633
|
+
const text = typeof block.text === 'string' ? block.text : undefined;
|
|
634
|
+
if (!text)
|
|
635
|
+
return part;
|
|
636
|
+
const stripped = stripClockClearDirectiveFromText(text);
|
|
637
|
+
if (stripped.hadClear)
|
|
638
|
+
hadClear = true;
|
|
639
|
+
return { ...block, text: stripped.next };
|
|
640
|
+
}
|
|
641
|
+
return part;
|
|
642
|
+
});
|
|
643
|
+
return { hadClear, next };
|
|
644
|
+
}
|
|
645
|
+
return { hadClear: false, next: content };
|
|
646
|
+
}
|
|
647
|
+
function findLastUserMessageIndex(messages) {
|
|
648
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
649
|
+
return -1;
|
|
650
|
+
}
|
|
651
|
+
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
652
|
+
const candidate = messages[idx];
|
|
653
|
+
if (candidate && candidate.role === 'user') {
|
|
654
|
+
return idx;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return -1;
|
|
658
|
+
}
|
|
659
|
+
async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, requestId) {
|
|
660
|
+
const rawConfig = metadata.clock;
|
|
661
|
+
const clockConfig = normalizeClockConfig(rawConfig);
|
|
662
|
+
if (!clockConfig) {
|
|
663
|
+
return request;
|
|
664
|
+
}
|
|
665
|
+
try {
|
|
666
|
+
await startClockDaemonIfNeeded(clockConfig);
|
|
667
|
+
}
|
|
668
|
+
catch {
|
|
669
|
+
// best-effort
|
|
670
|
+
}
|
|
671
|
+
const sessionId = resolveSessionIdForClock(metadata, request);
|
|
672
|
+
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
673
|
+
const lastUserIdx = findLastUserMessageIndex(messages);
|
|
674
|
+
// 1) Apply <**clock:clear**> directive (latest user message only).
|
|
675
|
+
let hadClear = false;
|
|
676
|
+
let nextMessages = messages;
|
|
677
|
+
if (lastUserIdx >= 0) {
|
|
678
|
+
const lastUser = messages[lastUserIdx];
|
|
679
|
+
const stripped = stripClockClearDirectiveFromContent(lastUser.content);
|
|
680
|
+
hadClear = stripped.hadClear;
|
|
681
|
+
if (hadClear) {
|
|
682
|
+
nextMessages = messages.slice();
|
|
683
|
+
nextMessages[lastUserIdx] = { ...lastUser, content: stripped.next };
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
if (hadClear) {
|
|
687
|
+
if (sessionId) {
|
|
688
|
+
try {
|
|
689
|
+
await clearClockSession(sessionId);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// best-effort: user directive should not crash request
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
return { ...request, messages: nextMessages };
|
|
696
|
+
}
|
|
697
|
+
// 2) Inject due reminders as a system message + attach reservation for response-side commit.
|
|
698
|
+
if (!sessionId) {
|
|
699
|
+
return request;
|
|
700
|
+
}
|
|
701
|
+
try {
|
|
702
|
+
const { reservation, injectText } = await reserveDueTasksForRequest({
|
|
703
|
+
reservationId: `${requestId}:clock`,
|
|
704
|
+
sessionId,
|
|
705
|
+
config: clockConfig
|
|
706
|
+
});
|
|
707
|
+
if (!reservation || typeof injectText !== 'string' || !injectText.trim()) {
|
|
708
|
+
return request;
|
|
709
|
+
}
|
|
710
|
+
const baseMetadata = request.metadata && typeof request.metadata === 'object'
|
|
711
|
+
? request.metadata
|
|
712
|
+
: {
|
|
713
|
+
originalEndpoint: readString(metadata.originalEndpoint) ?? '/v1/chat/completions'
|
|
714
|
+
};
|
|
715
|
+
return {
|
|
716
|
+
...request,
|
|
717
|
+
messages: [
|
|
718
|
+
...messages,
|
|
719
|
+
{
|
|
720
|
+
role: 'system',
|
|
721
|
+
content: injectText.trim()
|
|
722
|
+
}
|
|
723
|
+
],
|
|
724
|
+
metadata: {
|
|
725
|
+
...baseMetadata,
|
|
726
|
+
__clockReservation: reservation
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// best-effort: never break request due to reminder injection failures
|
|
732
|
+
return request;
|
|
733
|
+
}
|
|
523
734
|
}
|
|
524
735
|
function extractWebSearchSemantics(semantics) {
|
|
525
736
|
if (!semantics || typeof semantics !== 'object') {
|
|
@@ -15,6 +15,7 @@ import { runRespOutboundStage1ClientRemap } from '../pipeline/stages/resp_outbou
|
|
|
15
15
|
import { runRespOutboundStage2SseStream } from '../pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js';
|
|
16
16
|
import { recordResponsesResponse } from '../../shared/responses-conversation-store.js';
|
|
17
17
|
import { runServerToolOrchestration } from '../../../servertool/engine.js';
|
|
18
|
+
import { commitClockReservation, normalizeClockConfig } from '../../../servertool/clock/task-store.js';
|
|
18
19
|
const PROVIDER_RESPONSE_REGISTRY = {
|
|
19
20
|
'openai-chat': {
|
|
20
21
|
createFormatAdapter: () => new ChatFormatAdapter(),
|
|
@@ -52,6 +53,123 @@ function resolveClientProtocol(entryEndpoint) {
|
|
|
52
53
|
return 'anthropic-messages';
|
|
53
54
|
return 'openai-chat';
|
|
54
55
|
}
|
|
56
|
+
function isToolSurfaceShadowEnabled() {
|
|
57
|
+
const raw = String(process.env.ROUTECODEX_HUB_TOOL_SURFACE_MODE || '').trim().toLowerCase();
|
|
58
|
+
if (!raw)
|
|
59
|
+
return false;
|
|
60
|
+
if (raw === 'off' || raw === '0' || raw === 'false')
|
|
61
|
+
return false;
|
|
62
|
+
return raw === 'observe' || raw === 'shadow' || raw === 'enforce';
|
|
63
|
+
}
|
|
64
|
+
function isJsonRecord(value) {
|
|
65
|
+
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
|
66
|
+
}
|
|
67
|
+
function coerceClockReservation(value) {
|
|
68
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const rec = value;
|
|
72
|
+
const reservationId = typeof rec.reservationId === 'string' ? rec.reservationId.trim() : '';
|
|
73
|
+
const sessionId = typeof rec.sessionId === 'string' ? rec.sessionId.trim() : '';
|
|
74
|
+
const taskIdsRaw = Array.isArray(rec.taskIds) ? rec.taskIds : [];
|
|
75
|
+
const taskIds = taskIdsRaw
|
|
76
|
+
.filter((t) => typeof t === 'string' && t.trim().length)
|
|
77
|
+
.map((t) => String(t).trim());
|
|
78
|
+
const reservedAtMs = typeof rec.reservedAtMs === 'number' && Number.isFinite(rec.reservedAtMs)
|
|
79
|
+
? Math.floor(rec.reservedAtMs)
|
|
80
|
+
: Date.now();
|
|
81
|
+
if (!reservationId || !sessionId || taskIds.length === 0) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return { reservationId, sessionId, taskIds, reservedAtMs };
|
|
85
|
+
}
|
|
86
|
+
async function maybeCommitClockReservationFromContext(context) {
|
|
87
|
+
try {
|
|
88
|
+
const clockConfig = normalizeClockConfig(context.clock);
|
|
89
|
+
if (!clockConfig) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const reservation = coerceClockReservation(context.__clockReservation);
|
|
93
|
+
if (!reservation) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
await commitClockReservation(reservation, clockConfig);
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
// best-effort: never break response conversion due to clock persistence errors
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
function detectProviderResponseShape(payload) {
|
|
103
|
+
if (!isJsonRecord(payload))
|
|
104
|
+
return 'unknown';
|
|
105
|
+
const obj = payload;
|
|
106
|
+
if (Array.isArray(obj.choices))
|
|
107
|
+
return 'openai-chat';
|
|
108
|
+
if (Array.isArray(obj.output) || obj.object === 'response')
|
|
109
|
+
return 'openai-responses';
|
|
110
|
+
if (typeof obj.type === 'string' && String(obj.type).toLowerCase() === 'message' && Array.isArray(obj.content)) {
|
|
111
|
+
return 'anthropic-messages';
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(obj.candidates))
|
|
114
|
+
return 'gemini-chat';
|
|
115
|
+
return 'unknown';
|
|
116
|
+
}
|
|
117
|
+
function summarizeToolCallsFromProviderResponse(payload) {
|
|
118
|
+
try {
|
|
119
|
+
if (!isJsonRecord(payload))
|
|
120
|
+
return {};
|
|
121
|
+
const obj = payload;
|
|
122
|
+
// openai-chat
|
|
123
|
+
if (Array.isArray(obj.choices)) {
|
|
124
|
+
const msg = obj.choices?.[0]?.message;
|
|
125
|
+
const tcs = Array.isArray(msg?.tool_calls) ? msg.tool_calls : [];
|
|
126
|
+
const names = tcs
|
|
127
|
+
.map((tc) => String(tc?.function?.name || '').trim())
|
|
128
|
+
.filter((s) => s.length)
|
|
129
|
+
.slice(0, 10);
|
|
130
|
+
return { toolCallCount: tcs.length, toolNames: names.length ? names : undefined };
|
|
131
|
+
}
|
|
132
|
+
// openai-responses
|
|
133
|
+
if (Array.isArray(obj.output)) {
|
|
134
|
+
const out = obj.output;
|
|
135
|
+
const fnCalls = out.filter((it) => it && typeof it === 'object' && String(it.type || '').toLowerCase() === 'function_call');
|
|
136
|
+
const names = fnCalls
|
|
137
|
+
.map((it) => String(it?.name || '').trim())
|
|
138
|
+
.filter((s) => s.length)
|
|
139
|
+
.slice(0, 10);
|
|
140
|
+
return { toolCallCount: fnCalls.length, toolNames: names.length ? names : undefined };
|
|
141
|
+
}
|
|
142
|
+
// anthropic-messages
|
|
143
|
+
if (Array.isArray(obj.content)) {
|
|
144
|
+
const blocks = obj.content;
|
|
145
|
+
const uses = blocks.filter((b) => b && typeof b === 'object' && String(b.type || '').toLowerCase() === 'tool_use');
|
|
146
|
+
const names = uses
|
|
147
|
+
.map((b) => String(b?.name || '').trim())
|
|
148
|
+
.filter((s) => s.length)
|
|
149
|
+
.slice(0, 10);
|
|
150
|
+
return { toolCallCount: uses.length, toolNames: names.length ? names : undefined };
|
|
151
|
+
}
|
|
152
|
+
return {};
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
function stripInternalPolicyDebugFields(payload) {
|
|
159
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
const target = payload;
|
|
163
|
+
// These are internal debug/transport markers that must not leak across hub boundaries.
|
|
164
|
+
// They also break hub policy allowlists (Phase 0/1 observation) and confuse tool-surface detection.
|
|
165
|
+
delete target._transformed;
|
|
166
|
+
delete target._originalFormat;
|
|
167
|
+
delete target._targetFormat;
|
|
168
|
+
delete target.__responses_output_text_meta;
|
|
169
|
+
delete target.__responses_reasoning;
|
|
170
|
+
delete target.__responses_payload_snapshot;
|
|
171
|
+
delete target.__responses_passthrough;
|
|
172
|
+
}
|
|
55
173
|
function supportsSseProtocol(protocol) {
|
|
56
174
|
return protocol === 'openai-chat' || protocol === 'openai-responses' || protocol === 'anthropic-messages' || protocol === 'gemini-chat';
|
|
57
175
|
}
|
|
@@ -68,6 +186,16 @@ function extractDisplayModel(context) {
|
|
|
68
186
|
}
|
|
69
187
|
return undefined;
|
|
70
188
|
}
|
|
189
|
+
function extractClientFacingRequestId(context) {
|
|
190
|
+
const contextAny = context;
|
|
191
|
+
const candidates = [contextAny.clientRequestId, contextAny.groupRequestId, context.requestId];
|
|
192
|
+
for (const candidate of candidates) {
|
|
193
|
+
if (typeof candidate === 'string' && candidate.trim()) {
|
|
194
|
+
return candidate.trim();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
71
199
|
function applyModelOverride(payload, model) {
|
|
72
200
|
if (!model || !payload || typeof payload !== 'object') {
|
|
73
201
|
return;
|
|
@@ -107,6 +235,7 @@ export async function convertProviderResponse(options) {
|
|
|
107
235
|
/* logging best-effort */
|
|
108
236
|
}
|
|
109
237
|
const displayModel = extractDisplayModel(options.context);
|
|
238
|
+
const clientFacingRequestId = extractClientFacingRequestId(options.context) ?? options.context.requestId;
|
|
110
239
|
const plan = PROVIDER_RESPONSE_REGISTRY[options.providerProtocol];
|
|
111
240
|
if (!plan) {
|
|
112
241
|
throw new Error(`Unknown provider protocol: ${options.providerProtocol}`);
|
|
@@ -142,6 +271,27 @@ export async function convertProviderResponse(options) {
|
|
|
142
271
|
adapterContext: options.context,
|
|
143
272
|
stageRecorder: options.stageRecorder
|
|
144
273
|
});
|
|
274
|
+
stripInternalPolicyDebugFields(formatEnvelope.payload);
|
|
275
|
+
// Phase 2 (shadow): response tool surface mismatch detection (provider inbound).
|
|
276
|
+
// Only records diffs; does not rewrite payload.
|
|
277
|
+
try {
|
|
278
|
+
if (options.stageRecorder && isToolSurfaceShadowEnabled()) {
|
|
279
|
+
const detected = detectProviderResponseShape(formatEnvelope.payload);
|
|
280
|
+
if (detected !== 'unknown' && detected !== options.providerProtocol) {
|
|
281
|
+
const summary = summarizeToolCallsFromProviderResponse(formatEnvelope.payload);
|
|
282
|
+
options.stageRecorder.record('hub_toolsurface.shadow.provider_inbound', {
|
|
283
|
+
kind: 'provider_inbound',
|
|
284
|
+
expectedProtocol: options.providerProtocol,
|
|
285
|
+
detectedProtocol: detected,
|
|
286
|
+
...(summary.toolCallCount !== undefined ? { toolCallCount: summary.toolCallCount } : {}),
|
|
287
|
+
...(summary.toolNames ? { toolNames: summary.toolNames } : {})
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch {
|
|
293
|
+
// never break response conversion
|
|
294
|
+
}
|
|
145
295
|
// Phase 0/1: observe provider inbound payload violations (best-effort; no rewrites here).
|
|
146
296
|
try {
|
|
147
297
|
if (formatEnvelope.payload && typeof formatEnvelope.payload === 'object' && !Array.isArray(formatEnvelope.payload)) {
|
|
@@ -183,6 +333,7 @@ export async function convertProviderResponse(options) {
|
|
|
183
333
|
requestId: options.context.requestId,
|
|
184
334
|
entryEndpoint: options.entryEndpoint,
|
|
185
335
|
providerProtocol: options.providerProtocol,
|
|
336
|
+
stageRecorder: options.stageRecorder,
|
|
186
337
|
providerInvoker: options.providerInvoker,
|
|
187
338
|
reenterPipeline: options.reenterPipeline
|
|
188
339
|
});
|
|
@@ -229,11 +380,31 @@ export async function convertProviderResponse(options) {
|
|
|
229
380
|
const clientPayload = runRespOutboundStage1ClientRemap({
|
|
230
381
|
payload: finalizeResult.finalizedPayload,
|
|
231
382
|
clientProtocol,
|
|
232
|
-
requestId:
|
|
383
|
+
requestId: clientFacingRequestId,
|
|
233
384
|
adapterContext: options.context,
|
|
234
385
|
stageRecorder: options.stageRecorder
|
|
235
386
|
});
|
|
236
387
|
applyModelOverride(clientPayload, displayModel);
|
|
388
|
+
stripInternalPolicyDebugFields(clientPayload);
|
|
389
|
+
// Phase 2 (shadow): response tool surface mismatch detection (client outbound).
|
|
390
|
+
try {
|
|
391
|
+
if (options.stageRecorder && isToolSurfaceShadowEnabled()) {
|
|
392
|
+
const detected = detectProviderResponseShape(clientPayload);
|
|
393
|
+
if (detected !== 'unknown' && detected !== clientProtocol) {
|
|
394
|
+
const summary = summarizeToolCallsFromProviderResponse(clientPayload);
|
|
395
|
+
options.stageRecorder.record('hub_toolsurface.shadow.client_outbound', {
|
|
396
|
+
kind: 'client_outbound',
|
|
397
|
+
expectedProtocol: clientProtocol,
|
|
398
|
+
detectedProtocol: detected,
|
|
399
|
+
...(summary.toolCallCount !== undefined ? { toolCallCount: summary.toolCallCount } : {}),
|
|
400
|
+
...(summary.toolNames ? { toolNames: summary.toolNames } : {})
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
// never break response conversion
|
|
407
|
+
}
|
|
237
408
|
// Phase 0/1: observe client outbound payload violations (best-effort; no rewrites here).
|
|
238
409
|
try {
|
|
239
410
|
if (clientPayload && typeof clientPayload === 'object' && !Array.isArray(clientPayload)) {
|
|
@@ -252,10 +423,12 @@ export async function convertProviderResponse(options) {
|
|
|
252
423
|
const outbound = await runRespOutboundStage2SseStream({
|
|
253
424
|
clientPayload,
|
|
254
425
|
clientProtocol,
|
|
255
|
-
requestId:
|
|
426
|
+
requestId: clientFacingRequestId,
|
|
256
427
|
wantsStream,
|
|
257
428
|
stageRecorder: options.stageRecorder
|
|
258
429
|
});
|
|
430
|
+
// Commit scheduled-task delivery only after a successful client payload/stream is prepared.
|
|
431
|
+
await maybeCommitClockReservationFromContext(options.context);
|
|
259
432
|
if (outbound.stream) {
|
|
260
433
|
return { __sse_responses: outbound.stream, format: clientProtocol };
|
|
261
434
|
}
|
|
@@ -189,7 +189,7 @@ export function buildOpenAIChatFromAnthropicMessage(payload, options) {
|
|
|
189
189
|
switch (reason) {
|
|
190
190
|
case 'tool_use': return 'tool_calls';
|
|
191
191
|
case 'max_tokens': return 'length';
|
|
192
|
-
case 'stop_sequence': return '
|
|
192
|
+
case 'stop_sequence': return 'stop';
|
|
193
193
|
default: return 'stop';
|
|
194
194
|
}
|
|
195
195
|
};
|
|
@@ -1,8 +1 @@
|
|
|
1
|
-
|
|
2
|
-
import type { AdapterContext, ChatEnvelope } from '../types/chat-envelope.js';
|
|
3
|
-
import type { FormatEnvelope } from '../types/format-envelope.js';
|
|
4
|
-
export declare class AnthropicSemanticMapper implements SemanticMapper {
|
|
5
|
-
private readonly chatMapper;
|
|
6
|
-
toChat(format: FormatEnvelope, ctx: AdapterContext): Promise<ChatEnvelope>;
|
|
7
|
-
fromChat(chat: ChatEnvelope, ctx: AdapterContext): Promise<FormatEnvelope>;
|
|
8
|
-
}
|
|
1
|
+
export { AnthropicSemanticMapper } from '../operation-table/semantic-mappers/anthropic-mapper.js';
|