@jsonstudio/llms 0.6.2172 → 0.6.2979
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/README.md +2 -0
- package/dist/conversion/compat/antigravity-session-signature.js +35 -1
- package/dist/conversion/compat/profiles/chat-gemini-cli.json +0 -7
- package/dist/conversion/compat/profiles/chat-gemini.json +0 -6
- package/dist/conversion/hub/core/detour-registry.d.ts +2 -0
- package/dist/conversion/hub/core/hub-context.d.ts +3 -1
- package/dist/conversion/hub/core/index.d.ts +1 -0
- package/dist/conversion/hub/core/stage-driver.js +2 -0
- package/dist/conversion/hub/format-adapters/anthropic-format-adapter.js +15 -4
- package/dist/conversion/hub/format-adapters/chat-format-adapter.js +15 -4
- package/dist/conversion/hub/format-adapters/gemini-format-adapter.js +15 -4
- package/dist/conversion/hub/format-adapters/responses-format-adapter.js +15 -4
- package/dist/conversion/hub/hub-feature.js +3 -2
- package/dist/conversion/hub/node-support.js +9 -4
- package/dist/conversion/hub/operation-table/operation-table-runner.js +10 -6
- package/dist/conversion/hub/operation-table/semantic-mappers/anthropic-mapper.js +2 -2
- package/dist/conversion/hub/operation-table/semantic-mappers/chat-mapper.js +10 -10
- package/dist/conversion/hub/operation-table/semantic-mappers/gemini-mapper.js +4 -8
- package/dist/conversion/hub/operation-table/semantic-mappers/responses-mapper.js +19 -65
- package/dist/conversion/hub/ops/operations.js +2 -121
- package/dist/conversion/hub/pipeline/compat/compat-engine.js +6 -0
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.d.ts +1 -1
- package/dist/conversion/hub/pipeline/compat/compat-pipeline-executor.js +33 -1042
- package/dist/conversion/hub/pipeline/compat/compat-profile-resolver.js +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-profile-store.js +2 -0
- package/dist/conversion/hub/pipeline/compat/compat-types.d.ts +14 -17
- package/dist/conversion/hub/pipeline/compat/native-adapter-context.d.ts +3 -0
- package/dist/conversion/hub/pipeline/compat/native-adapter-context.js +39 -0
- package/dist/conversion/hub/pipeline/hub-pipeline.js +115 -262
- package/dist/conversion/hub/pipeline/session-identifiers.js +6 -196
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.d.ts +1 -2
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage1_format_parse/index.js +37 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/index.js +12 -86
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/semantic-lift.d.ts +14 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage2_semantic_map/semantic-lift.js +24 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/archive/shell-like-tool-call-normalization-fallback.d.ts +2 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/archive/shell-like-tool-call-normalization-fallback.js +157 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-capture-orchestration.d.ts +16 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-capture-orchestration.js +29 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/context-factories.js +3 -1
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.d.ts +2 -15
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/index.js +8 -595
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/responses-context-snapshot.d.ts +8 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/responses-context-snapshot.js +28 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-diagnostics.d.ts +2 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-diagnostics.js +4 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-snapshot.d.ts +10 -0
- package/dist/conversion/hub/pipeline/stages/req_inbound/req_inbound_stage3_context_capture/tool-output-snapshot.js +12 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/context-merge.d.ts +3 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/context-merge.js +30 -0
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage1_semantic_map/index.js +9 -129
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.d.ts +1 -4
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage2_format_build/index.js +9 -26
- package/dist/conversion/hub/pipeline/stages/req_outbound/req_outbound_stage3_compat/index.js +32 -14
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.d.ts +2 -2
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage1_tool_governance/index.js +48 -8
- package/dist/conversion/hub/pipeline/stages/req_process/req_process_stage2_route_select/index.js +18 -3
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/index.js +10 -198
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/stream-json-sniffer.d.ts +3 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage1_sse_decode/stream-json-sniffer.js +81 -0
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.d.ts +1 -2
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage2_format_parse/index.js +36 -1
- package/dist/conversion/hub/pipeline/stages/resp_inbound/resp_inbound_stage3_semantic_map/index.js +3 -1
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/chat-process-semantics-bridge.d.ts +6 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/chat-process-semantics-bridge.js +17 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/client-remap-protocol-switch.d.ts +9 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/client-remap-protocol-switch.js +28 -0
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.d.ts +1 -2
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage1_client_remap/index.js +14 -102
- package/dist/conversion/hub/pipeline/stages/resp_outbound/resp_outbound_stage2_sse_stream/index.js +3 -1
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage1_tool_governance/index.js +12 -10
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage2_finalize/index.js +6 -5
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.d.ts +11 -1
- package/dist/conversion/hub/pipeline/stages/resp_process/resp_process_stage3_servertool_orchestration/index.js +10 -32
- package/dist/conversion/hub/pipeline/stages/utils.js +17 -1
- package/dist/conversion/hub/pipeline/target-utils.js +14 -91
- package/dist/conversion/hub/pipelines/inbound.js +3 -1
- package/dist/conversion/hub/pipelines/outbound.js +2 -0
- package/dist/conversion/hub/policy/policy-engine.js +9 -3
- package/dist/conversion/hub/policy/protocol-spec.js +20 -148
- package/dist/conversion/hub/process/chat-process-anthropic-alias.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-anthropic-alias.js +36 -0
- package/dist/conversion/hub/process/chat-process-clock-directive-parser.d.ts +5 -0
- package/dist/conversion/hub/process/chat-process-clock-directive-parser.js +48 -0
- package/dist/conversion/hub/process/chat-process-clock-directives.d.ts +24 -0
- package/dist/conversion/hub/process/chat-process-clock-directives.js +98 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-directives.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-directives.js +42 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-finalize.d.ts +14 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-finalize.js +10 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-messages.d.ts +5 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-messages.js +10 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-orchestration.d.ts +30 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-orchestration.js +68 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-time-tag.d.ts +9 -0
- package/dist/conversion/hub/process/chat-process-clock-reminder-time-tag.js +18 -0
- package/dist/conversion/hub/process/chat-process-clock-reminders.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-clock-reminders.js +104 -0
- package/dist/conversion/hub/process/chat-process-clock-tool-schemas.d.ts +3 -0
- package/dist/conversion/hub/process/chat-process-clock-tool-schemas.js +233 -0
- package/dist/conversion/hub/process/chat-process-clock-tools.d.ts +6 -0
- package/dist/conversion/hub/process/chat-process-clock-tools.js +41 -0
- package/dist/conversion/hub/process/chat-process-continue-execution.d.ts +11 -0
- package/dist/conversion/hub/process/chat-process-continue-execution.js +82 -0
- package/dist/conversion/hub/process/chat-process-governance-context.d.ts +15 -0
- package/dist/conversion/hub/process/chat-process-governance-context.js +7 -0
- package/dist/conversion/hub/process/chat-process-governance-finalize.d.ts +16 -0
- package/dist/conversion/hub/process/chat-process-governance-finalize.js +11 -0
- package/dist/conversion/hub/process/chat-process-governance-orchestration.d.ts +9 -0
- package/dist/conversion/hub/process/chat-process-governance-orchestration.js +47 -0
- package/dist/conversion/hub/process/chat-process-governed-control-ops.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-governed-control-ops.js +5 -0
- package/dist/conversion/hub/process/chat-process-governed-filter-call.d.ts +12 -0
- package/dist/conversion/hub/process/chat-process-governed-filter-call.js +18 -0
- package/dist/conversion/hub/process/chat-process-governed-merge.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-governed-merge.js +11 -0
- package/dist/conversion/hub/process/chat-process-media.d.ts +3 -0
- package/dist/conversion/hub/process/chat-process-media.js +18 -0
- package/dist/conversion/hub/process/chat-process-node-result.d.ts +23 -0
- package/dist/conversion/hub/process/chat-process-node-result.js +24 -0
- package/dist/conversion/hub/process/chat-process-pending-tool-sync.d.ts +14 -0
- package/dist/conversion/hub/process/chat-process-pending-tool-sync.js +52 -0
- package/dist/conversion/hub/process/chat-process-post-governed-normalization.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-post-governed-normalization.js +16 -0
- package/dist/conversion/hub/process/chat-process-review.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-review.js +8 -0
- package/dist/conversion/hub/process/chat-process-servertool-orchestration.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-servertool-orchestration.js +22 -0
- package/dist/conversion/hub/process/chat-process-tool-normalization.d.ts +2 -0
- package/dist/conversion/hub/process/chat-process-tool-normalization.js +4 -0
- package/dist/conversion/hub/process/chat-process-web-search-intent.d.ts +12 -0
- package/dist/conversion/hub/process/chat-process-web-search-intent.js +13 -0
- package/dist/conversion/hub/process/chat-process-web-search-tool-schema.d.ts +3 -0
- package/dist/conversion/hub/process/chat-process-web-search-tool-schema.js +4 -0
- package/dist/conversion/hub/process/chat-process-web-search.d.ts +8 -0
- package/dist/conversion/hub/process/chat-process-web-search.js +26 -0
- package/dist/conversion/hub/process/chat-process.d.ts +2 -19
- package/dist/conversion/hub/process/chat-process.js +12 -1701
- package/dist/conversion/hub/process/client-inject-readiness.d.ts +1 -0
- package/dist/conversion/hub/process/client-inject-readiness.js +4 -0
- package/dist/conversion/hub/registry.js +5 -2
- package/dist/conversion/hub/response/chat-response-utils.js +5 -86
- package/dist/conversion/hub/response/provider-response.d.ts +9 -0
- package/dist/conversion/hub/response/provider-response.js +6 -21
- package/dist/conversion/hub/response/response-mappers.js +2 -26
- package/dist/conversion/hub/response/response-runtime.js +2 -93
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/anthropic-mapper.js +1 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/chat-mapper.js +1 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/gemini-mapper.js +1 -0
- package/dist/conversion/hub/semantic-mappers/responses-mapper.d.ts +1 -0
- package/dist/conversion/hub/semantic-mappers/responses-mapper.js +1 -0
- package/dist/conversion/hub/snapshot-recorder.js +10 -3
- package/dist/conversion/hub/standardized-bridge.js +11 -288
- package/dist/conversion/hub/tool-governance/engine.js +5 -0
- package/dist/conversion/hub/tool-governance/rules.js +10 -10
- package/dist/conversion/hub/tool-session-compat.d.ts +2 -2
- package/dist/conversion/hub/tool-session-compat.js +17 -231
- package/dist/conversion/hub/tool-surface/tool-surface-engine.js +5 -3
- package/dist/conversion/responses/responses-host-policy.js +2 -12
- package/dist/conversion/responses/responses-openai-bridge/response-payload.js +6 -82
- package/dist/conversion/responses/responses-openai-bridge/types.d.ts +1 -0
- package/dist/conversion/responses/responses-openai-bridge.js +21 -54
- package/dist/conversion/shared/anthropic-message-utils.js +151 -13
- package/dist/conversion/shared/args-mapping.js +2 -146
- package/dist/conversion/shared/bridge-actions.js +203 -718
- package/dist/conversion/shared/bridge-id-utils.js +5 -71
- package/dist/conversion/shared/bridge-instructions.js +2 -1
- package/dist/conversion/shared/bridge-message-types.d.ts +2 -0
- package/dist/conversion/shared/bridge-message-utils.js +1 -2
- package/dist/conversion/shared/bridge-metadata.d.ts +1 -0
- package/dist/conversion/shared/bridge-metadata.js +4 -0
- package/dist/conversion/shared/bridge-policies.js +5 -189
- package/dist/conversion/shared/chat-envelope-validator.js +2 -126
- package/dist/conversion/shared/chat-output-normalizer.js +2 -54
- package/dist/conversion/shared/compaction-detect.js +2 -57
- package/dist/conversion/shared/gemini-tool-utils.js +9 -524
- package/dist/conversion/shared/jsonish.js +3 -160
- package/dist/conversion/shared/mcp-injection.js +3 -169
- package/dist/conversion/shared/media.js +2 -7
- package/dist/conversion/shared/metadata-passthrough.js +9 -46
- package/dist/conversion/shared/openai-finalizer.js +2 -1
- package/dist/conversion/shared/openai-message-normalize.js +11 -283
- package/dist/conversion/shared/output-content-normalizer.js +9 -112
- package/dist/conversion/shared/payload-budget.js +2 -85
- package/dist/conversion/shared/protocol-state.js +11 -7
- package/dist/conversion/shared/reasoning-mapping.js +2 -6
- package/dist/conversion/shared/reasoning-normalizer.js +4 -1
- package/dist/conversion/shared/reasoning-tool-normalizer.js +14 -126
- package/dist/conversion/shared/reasoning-tool-parser.js +4 -87
- package/dist/conversion/shared/reasoning-utils.js +2 -6
- package/dist/conversion/shared/responses-conversation-store.js +4 -82
- package/dist/conversion/shared/responses-output-builder.js +11 -47
- package/dist/conversion/shared/responses-reasoning-registry.js +7 -1
- package/dist/conversion/shared/responses-request-adapter.d.ts +7 -1
- package/dist/conversion/shared/responses-request-adapter.js +14 -1
- package/dist/conversion/shared/responses-response-utils.js +6 -7
- package/dist/conversion/shared/responses-tool-utils.d.ts +1 -0
- package/dist/conversion/shared/responses-tool-utils.js +90 -14
- package/dist/conversion/shared/runtime-metadata.js +13 -5
- package/dist/conversion/shared/streaming-text-extractor.js +2 -7
- package/dist/conversion/shared/text-markup-normalizer/normalize.d.ts +1 -1
- package/dist/conversion/shared/text-markup-normalizer/normalize.js +43 -17
- package/dist/conversion/shared/text-markup-normalizer.d.ts +1 -0
- package/dist/conversion/shared/text-markup-normalizer.js +1 -0
- package/dist/conversion/shared/thought-signature-validator.js +3 -2
- package/dist/conversion/shared/tool-argument-repairer.js +2 -2
- package/dist/conversion/shared/tool-call-id-manager.js +5 -7
- package/dist/conversion/shared/tool-call-utils.js +3 -45
- package/dist/conversion/shared/tool-canonicalizer.js +25 -29
- package/dist/conversion/shared/tool-filter-pipeline.js +4 -99
- package/dist/conversion/shared/tool-governor.d.ts +6 -0
- package/dist/conversion/shared/tool-governor.js +43 -125
- package/dist/conversion/shared/tool-harvester.js +2 -8
- package/dist/conversion/shared/tool-mapping.js +2 -5
- package/dist/conversion/shared/tooling.d.ts +0 -4
- package/dist/conversion/shared/tooling.js +18 -0
- package/dist/native/router_hotpath_napi.node +0 -0
- package/dist/router/virtual-router/engine/provider-key/parse.d.ts +1 -6
- package/dist/router/virtual-router/engine/provider-key/parse.js +1 -43
- package/dist/router/virtual-router/engine/routing-state/store.js +48 -12
- package/dist/router/virtual-router/engine-logging.js +4 -3
- package/dist/router/virtual-router/engine-selection/alias-selection.js +45 -83
- package/dist/router/virtual-router/engine-selection/key-parsing.js +9 -23
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-directive-parser.d.ts +20 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-directive-parser.js +163 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-directives.d.ts +7 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-directives.js +103 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-orchestration-semantics.d.ts +10 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-orchestration-semantics.js +110 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-semantics.d.ts +8 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-semantics.js +281 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-time-tag-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminder-time-tag-semantics.js +25 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminders-semantics.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-reminders-semantics.js +44 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-tool-schema-semantics.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-clock-tool-schema-semantics.js +62 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governance-semantics.d.ts +40 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governance-semantics.js +484 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governed-filter-semantics.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-governed-filter-semantics.js +64 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-node-result-semantics.d.ts +5 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-node-result-semantics.js +163 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-post-governed-normalization-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-post-governed-normalization-semantics.js +49 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-servertool-orchestration-semantics.d.ts +30 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-servertool-orchestration-semantics.js +446 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-web-search-intent-semantics.d.ts +1 -0
- package/dist/router/virtual-router/engine-selection/native-chat-process-web-search-intent-semantics.js +49 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.d.ts +134 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-action-semantics.js +729 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.d.ts +62 -0
- package/dist/router/virtual-router/engine-selection/native-hub-bridge-policy-semantics.js +338 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-edge-stage-semantics.d.ts +18 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-edge-stage-semantics.js +317 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-inbound-outbound-semantics.d.ts +22 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-inbound-outbound-semantics.js +426 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-orchestration-semantics.d.ts +57 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-orchestration-semantics.js +705 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.d.ts +46 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js +503 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.d.ts +146 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-outbound-semantics.js +570 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-process-semantics.d.ts +25 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-req-process-semantics.js +148 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.d.ts +25 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-resp-semantics.js +637 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-session-identifiers-semantics.d.ts +11 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-session-identifiers-semantics.js +207 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-target-semantics.d.ts +3 -0
- package/dist/router/virtual-router/engine-selection/native-hub-pipeline-target-semantics.js +128 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-analysis.d.ts +57 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-analysis.js +217 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.d.ts +5 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-loader.js +284 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-policy.d.ts +5 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-policy.js +18 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-quota-buckets.d.ts +25 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath-quota-buckets.js +85 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath.d.ts +59 -0
- package/dist/router/virtual-router/engine-selection/native-router-hotpath.js +117 -0
- package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.d.ts +76 -0
- package/dist/router/virtual-router/engine-selection/native-shared-conversion-semantics.js +1166 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-alias-selection-semantics.d.ts +16 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-alias-selection-semantics.js +96 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-actions-semantics.d.ts +6 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-actions-semantics.js +85 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-semantics.d.ts +9 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-semantics.js +70 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-state-semantics.d.ts +2 -0
- package/dist/router/virtual-router/engine-selection/native-virtual-router-stop-message-state-semantics.js +76 -0
- package/dist/router/virtual-router/engine-selection/route-utils.js +1 -1
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-session-lease.d.ts +10 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-session-lease.js +231 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-target-split.d.ts +4 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-antigravity-target-split.js +43 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-quota-integration.d.ts +27 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-quota-integration.js +116 -0
- package/dist/router/virtual-router/engine-selection/tier-selection-select.d.ts +1 -1
- package/dist/router/virtual-router/engine-selection/tier-selection-select.js +29 -129
- package/dist/router/virtual-router/engine-selection/tier-selection.js +2 -265
- package/dist/router/virtual-router/engine.js +258 -249
- package/dist/router/virtual-router/features.js +2 -2
- package/dist/router/virtual-router/routing-instructions.d.ts +5 -7
- package/dist/router/virtual-router/routing-instructions.js +93 -66
- package/dist/router/virtual-router/routing-stop-message-actions.js +91 -112
- package/dist/router/virtual-router/routing-stop-message-parser.js +9 -132
- package/dist/router/virtual-router/routing-stop-message-state-codec.d.ts +1 -0
- package/dist/router/virtual-router/routing-stop-message-state-codec.js +58 -71
- package/dist/router/virtual-router/sticky-session-store.js +4 -2
- package/dist/router/virtual-router/stop-message-file-resolver.d.ts +1 -0
- package/dist/router/virtual-router/stop-message-file-resolver.js +10 -0
- package/dist/router/virtual-router/stop-message-state-sync.d.ts +1 -1
- package/dist/router/virtual-router/stop-message-state-sync.js +3 -7
- package/dist/router/virtual-router/token-counter.js +0 -9
- package/dist/router/virtual-router/types.d.ts +9 -7
- package/dist/servertool/clock/config.js +23 -51
- package/dist/servertool/clock/io.js +1 -0
- package/dist/servertool/clock/session-scope.d.ts +2 -2
- package/dist/servertool/clock/session-scope.js +5 -47
- package/dist/servertool/engine.d.ts +9 -0
- package/dist/servertool/engine.js +196 -79
- package/dist/servertool/handlers/antigravity-thought-signature-bootstrap.js +2 -2
- package/dist/servertool/handlers/clock.js +1 -1
- package/dist/servertool/handlers/continue-execution.js +8 -4
- package/dist/servertool/handlers/followup-request-builder.js +18 -1
- package/dist/servertool/handlers/gemini-empty-reply-continue.js +7 -1
- package/dist/servertool/handlers/review.js +180 -0
- package/dist/servertool/handlers/stop-message-auto/blocked-report.js +59 -1
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.d.ts +23 -2
- package/dist/servertool/handlers/stop-message-auto/iflow-followup.js +397 -89
- package/dist/servertool/handlers/stop-message-auto/routing-state.d.ts +5 -15
- package/dist/servertool/handlers/stop-message-auto/routing-state.js +29 -55
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.d.ts +6 -0
- package/dist/servertool/handlers/stop-message-auto/runtime-utils.js +35 -61
- package/dist/servertool/handlers/stop-message-auto.js +392 -76
- package/dist/servertool/server-side-tools.d.ts +1 -0
- package/dist/servertool/server-side-tools.js +90 -52
- package/dist/servertool/types.d.ts +17 -0
- package/dist/tools/apply-patch/patch-text/normalize.js +11 -0
- package/dist/tools/exec-command/validator.d.ts +4 -1
- package/dist/tools/exec-command/validator.js +87 -3
- package/dist/tools/tool-registry.d.ts +7 -1
- package/dist/tools/tool-registry.js +3 -2
- package/package.json +115 -7
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.d.ts +0 -18
- package/dist/servertool/handlers/stop-message-stage-policy/bd-runtime.js +0 -398
- package/dist/servertool/handlers/stop-message-stage-policy/decision.d.ts +0 -9
- package/dist/servertool/handlers/stop-message-stage-policy/decision.js +0 -127
- package/dist/servertool/handlers/stop-message-stage-policy/observation.d.ts +0 -2
- package/dist/servertool/handlers/stop-message-stage-policy/observation.js +0 -179
- package/dist/servertool/handlers/stop-message-stage-policy/templates.d.ts +0 -4
- package/dist/servertool/handlers/stop-message-stage-policy/templates.js +0 -96
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.d.ts +0 -9
- package/dist/servertool/handlers/stop-message-stage-policy/text-utils.js +0 -89
- package/dist/servertool/handlers/stop-message-stage-policy/types.d.ts +0 -59
- package/dist/servertool/handlers/stop-message-stage-policy.d.ts +0 -3
- package/dist/servertool/handlers/stop-message-stage-policy.js +0 -2
- /package/dist/servertool/handlers/{stop-message-stage-policy/types.js → review.d.ts} +0 -0
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { ensureApplyPatchSchema } from '../../shared/tool-mapping.js';
|
|
5
|
-
import { normalizeApplyPatchToolCallsOnRequest } from '../../shared/tool-governor.js';
|
|
6
|
-
import { buildAnthropicToolAliasMap } from '../../shared/anthropic-message-utils.js';
|
|
7
|
-
import { clearClockSession, parseDueAtMs, resolveClockConfig, reserveDueTasksForRequest, scheduleClockTasks, startClockDaemonIfNeeded } from '../../../servertool/clock/task-store.js';
|
|
8
|
-
import { logClock } from '../../../servertool/clock/log.js';
|
|
9
|
-
import { logContinueExecution } from '../../../servertool/continue-execution/log.js';
|
|
10
|
-
import { buildTimeTagLine, getClockTimeSnapshot } from '../../../servertool/clock/ntp.js';
|
|
11
|
-
import { resolveClockSessionScope } from '../../../servertool/clock/session-scope.js';
|
|
12
|
-
import { clearPendingServerToolInjection, loadPendingServerToolInjection } from '../../../servertool/pending-session.js';
|
|
13
|
-
import { loadRoutingInstructionStateSync } from '../../../router/virtual-router/sticky-session-store.js';
|
|
14
|
-
import { isJsonObject } from '../types/json.js';
|
|
15
|
-
import { applyHubOperations } from '../ops/operations.js';
|
|
16
|
-
const toolGovernanceEngine = new ToolGovernanceEngine();
|
|
17
|
-
const CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER = '[routecodex:continue_execution_directive]';
|
|
18
|
-
const CONTINUE_EXECUTION_SYSTEM_DIRECTIVE = [
|
|
19
|
-
CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER,
|
|
20
|
-
'Execution policy (must follow):',
|
|
21
|
-
'- Do not output status-talk placeholders such as \"继续执行中\" / \"I will continue\" before real work.',
|
|
22
|
-
'- Perform at least one real action first (prefer tool calls), then report concise results when needed.',
|
|
23
|
-
'- If you are about to return finish_reason=stop (or protocol-equivalent end_turn), do not end directly; emit a continue_execution tool call first.'
|
|
24
|
-
].join('\n');
|
|
1
|
+
import { buildErrorResult, buildProcessedRequest, buildSuccessResult } from './chat-process-node-result.js';
|
|
2
|
+
import { applyRequestToolGovernance } from './chat-process-governance-orchestration.js';
|
|
3
|
+
import { shouldRunHubChatProcessWithNative } from '../../../router/virtual-router/engine-selection/native-hub-pipeline-req-inbound-semantics.js';
|
|
25
4
|
export async function runHubChatProcess(options) {
|
|
26
5
|
const startTime = Date.now();
|
|
27
6
|
try {
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
7
|
+
const shouldRunGovernance = shouldRunHubChatProcessWithNative(options.requestId, options.entryEndpoint);
|
|
8
|
+
const governed = shouldRunGovernance
|
|
9
|
+
? await applyRequestToolGovernance(options.request, {
|
|
10
|
+
entryEndpoint: options.entryEndpoint,
|
|
11
|
+
requestId: options.requestId,
|
|
12
|
+
metadata: options.metadata,
|
|
13
|
+
rawPayload: options.rawPayload
|
|
14
|
+
})
|
|
15
|
+
: options.request;
|
|
33
16
|
const processedRequest = buildProcessedRequest(governed);
|
|
34
17
|
return {
|
|
35
18
|
processedRequest,
|
|
@@ -42,1675 +25,3 @@ export async function runHubChatProcess(options) {
|
|
|
42
25
|
};
|
|
43
26
|
}
|
|
44
27
|
}
|
|
45
|
-
async function applyRequestToolGovernance(request, context) {
|
|
46
|
-
const entryEndpoint = context.entryEndpoint && context.entryEndpoint.trim()
|
|
47
|
-
? context.entryEndpoint
|
|
48
|
-
: '/v1/chat/completions';
|
|
49
|
-
const metadata = normalizeRecord(context.metadata);
|
|
50
|
-
const providerProtocol = readString(metadata.providerProtocol) ?? readString(metadata.provider) ?? 'openai-chat';
|
|
51
|
-
const metadataToolHints = metadata.toolFilterHints;
|
|
52
|
-
const metadataStreamFlag = metadata.stream;
|
|
53
|
-
const inboundStreamIntent = typeof metadataStreamFlag === 'boolean' ? metadataStreamFlag : request.parameters?.stream === true;
|
|
54
|
-
// /v1/messages: preserve the caller's tool naming (Bash/Glob/Read, etc.) so we can
|
|
55
|
-
// remap tool calls back to the client protocol shape after routing through providers
|
|
56
|
-
// that canonicalize tool names (e.g. OpenAI function names lowercased).
|
|
57
|
-
//
|
|
58
|
-
// NOTE: This is mappable semantics and must live in request.semantics (never metadata).
|
|
59
|
-
try {
|
|
60
|
-
if (entryEndpoint.toLowerCase().includes('/v1/messages')) {
|
|
61
|
-
request.semantics = request.semantics && typeof request.semantics === 'object' && !Array.isArray(request.semantics)
|
|
62
|
-
? request.semantics
|
|
63
|
-
: {};
|
|
64
|
-
const semanticsTools = request.semantics.tools && typeof request.semantics.tools === 'object' && !Array.isArray(request.semantics.tools)
|
|
65
|
-
? request.semantics.tools
|
|
66
|
-
: (request.semantics.tools = {});
|
|
67
|
-
const hasAlias = isJsonObject(semanticsTools.toolNameAliasMap) ||
|
|
68
|
-
isJsonObject(semanticsTools.toolAliasMap);
|
|
69
|
-
if (!hasAlias) {
|
|
70
|
-
const sourceTools = Array.isArray(semanticsTools.clientToolsRaw) && semanticsTools.clientToolsRaw.length
|
|
71
|
-
? semanticsTools.clientToolsRaw
|
|
72
|
-
: request.tools;
|
|
73
|
-
const aliasMap = buildAnthropicToolAliasMap(sourceTools);
|
|
74
|
-
if (aliasMap) {
|
|
75
|
-
semanticsTools.toolNameAliasMap = aliasMap;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// best-effort: never block request handling due to alias map propagation failures
|
|
82
|
-
}
|
|
83
|
-
const shaped = {
|
|
84
|
-
model: request.model,
|
|
85
|
-
messages: deepClone(request.messages),
|
|
86
|
-
tools: request.tools ? deepClone(request.tools) : undefined,
|
|
87
|
-
tool_choice: request.parameters?.tool_choice,
|
|
88
|
-
stream: request.parameters?.stream === true,
|
|
89
|
-
parameters: { ...request.parameters }
|
|
90
|
-
};
|
|
91
|
-
const governedPayload = await runChatRequestToolFilters(shaped, {
|
|
92
|
-
entryEndpoint,
|
|
93
|
-
requestId: context.requestId,
|
|
94
|
-
model: request.model,
|
|
95
|
-
profile: providerProtocol,
|
|
96
|
-
stream: inboundStreamIntent,
|
|
97
|
-
toolFilterHints: metadataToolHints,
|
|
98
|
-
rawPayload: context.metadata?.__raw_request_body && typeof context.metadata.__raw_request_body === 'object'
|
|
99
|
-
? context.metadata.__raw_request_body
|
|
100
|
-
: undefined
|
|
101
|
-
});
|
|
102
|
-
const governed = normalizeRecord(governedPayload);
|
|
103
|
-
const providerStreamIntent = typeof governed.stream === 'boolean' ? governed.stream : undefined;
|
|
104
|
-
let merged = {
|
|
105
|
-
...request,
|
|
106
|
-
messages: Array.isArray(governed.messages)
|
|
107
|
-
? governed.messages
|
|
108
|
-
: request.messages,
|
|
109
|
-
tools: governed.tools !== undefined ? castGovernedTools(governed.tools) : request.tools,
|
|
110
|
-
parameters: {
|
|
111
|
-
...request.parameters,
|
|
112
|
-
...(isRecord(governed.parameters) ? governed.parameters : {})
|
|
113
|
-
},
|
|
114
|
-
metadata: {
|
|
115
|
-
...request.metadata,
|
|
116
|
-
toolChoice: readToolChoice(governed.tool_choice),
|
|
117
|
-
originalStream: inboundStreamIntent,
|
|
118
|
-
stream: inboundStreamIntent,
|
|
119
|
-
providerStream: providerStreamIntent,
|
|
120
|
-
governedTools: governed.tools !== undefined,
|
|
121
|
-
governanceTimestamp: Date.now()
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
// 清理历史图片:仅保留「最新一条 user 消息」中的图片分段,
|
|
125
|
-
// 避免历史对话中的图片在后续多轮工具 / 普通对话中继续作为多模态负载发给不支持图片的模型。
|
|
126
|
-
merged = {
|
|
127
|
-
...merged,
|
|
128
|
-
messages: stripHistoricalImageAttachments(merged.messages)
|
|
129
|
-
};
|
|
130
|
-
merged = normalizeApplyPatchToolCallsOnRequest(merged);
|
|
131
|
-
// Mixed-tools support: if previous response executed servertools alongside client tools,
|
|
132
|
-
// inject servertool tool_call + tool_result messages after the client's tool results on next request.
|
|
133
|
-
merged = await maybeInjectPendingServerToolResultsAfterClientTools(merged, metadata);
|
|
134
|
-
if (containsImageAttachment(merged.messages)) {
|
|
135
|
-
if (!merged.metadata) {
|
|
136
|
-
merged.metadata = {
|
|
137
|
-
originalEndpoint: request.metadata?.originalEndpoint ?? '/v1/chat/completions'
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
merged.metadata.hasImageAttachment = true;
|
|
141
|
-
}
|
|
142
|
-
if (typeof inboundStreamIntent === 'boolean') {
|
|
143
|
-
merged = applyHubOperations(merged, buildInboundStreamingOperations(inboundStreamIntent));
|
|
144
|
-
}
|
|
145
|
-
if (typeof governed.stream === 'boolean') {
|
|
146
|
-
merged = applyHubOperations(merged, buildOutboundStreamingOperations(governed.stream));
|
|
147
|
-
}
|
|
148
|
-
if (governed.tool_choice !== undefined) {
|
|
149
|
-
merged = applyHubOperations(merged, buildToolChoiceOperations(governed.tool_choice));
|
|
150
|
-
}
|
|
151
|
-
if (typeof governed.model === 'string' && governed.model.trim()) {
|
|
152
|
-
merged.model = governed.model.trim();
|
|
153
|
-
}
|
|
154
|
-
// Server-side web_search tool injection (config-driven, best-effort).
|
|
155
|
-
merged = applyHubOperations(merged, buildWebSearchOperations(merged, metadata));
|
|
156
|
-
// Server-side clock tool + scheduled reminders injection (config-driven, best-effort).
|
|
157
|
-
merged = applyHubOperations(merged, buildClockOperations(metadata));
|
|
158
|
-
// Server-side no-op continue tool injection (always-on, lightweight).
|
|
159
|
-
merged = applyHubOperations(merged, buildContinueExecutionOperations(metadata));
|
|
160
|
-
merged = injectContinueExecutionDirectiveIntoUserMessage(merged, metadata);
|
|
161
|
-
merged = await maybeInjectClockRemindersAndApplyDirectives(merged, metadata, context.requestId);
|
|
162
|
-
const { request: sanitized, summary } = toolGovernanceEngine.governRequest(merged, providerProtocol);
|
|
163
|
-
if (summary.applied) {
|
|
164
|
-
sanitized.metadata = {
|
|
165
|
-
...sanitized.metadata,
|
|
166
|
-
toolGovernance: {
|
|
167
|
-
...sanitized.metadata?.toolGovernance,
|
|
168
|
-
request: summary
|
|
169
|
-
}
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return sanitized;
|
|
173
|
-
}
|
|
174
|
-
function resolveSessionIdForPending(metadata, request) {
|
|
175
|
-
const candidate = readString(metadata.sessionId) ?? readString(request.metadata?.sessionId);
|
|
176
|
-
return candidate && candidate.trim() ? candidate.trim() : null;
|
|
177
|
-
}
|
|
178
|
-
function extractToolCallIdsFromToolMessages(messages) {
|
|
179
|
-
const ids = new Set();
|
|
180
|
-
for (const msg of messages) {
|
|
181
|
-
if (!msg || typeof msg !== 'object')
|
|
182
|
-
continue;
|
|
183
|
-
if (msg.role !== 'tool')
|
|
184
|
-
continue;
|
|
185
|
-
const toolCallId = typeof msg.tool_call_id === 'string'
|
|
186
|
-
? String(msg.tool_call_id).trim()
|
|
187
|
-
: typeof msg.toolCallId === 'string'
|
|
188
|
-
? String(msg.toolCallId).trim()
|
|
189
|
-
: '';
|
|
190
|
-
if (toolCallId)
|
|
191
|
-
ids.add(toolCallId);
|
|
192
|
-
}
|
|
193
|
-
return ids;
|
|
194
|
-
}
|
|
195
|
-
function findLastToolMessageIndex(messages) {
|
|
196
|
-
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
197
|
-
const msg = messages[i];
|
|
198
|
-
if (msg && typeof msg === 'object' && msg.role === 'tool') {
|
|
199
|
-
return i;
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
return -1;
|
|
203
|
-
}
|
|
204
|
-
async function maybeInjectPendingServerToolResultsAfterClientTools(request, metadata) {
|
|
205
|
-
const sessionId = resolveSessionIdForPending(metadata, request);
|
|
206
|
-
if (!sessionId) {
|
|
207
|
-
return request;
|
|
208
|
-
}
|
|
209
|
-
const pending = await loadPendingServerToolInjection(sessionId);
|
|
210
|
-
if (!pending) {
|
|
211
|
-
return request;
|
|
212
|
-
}
|
|
213
|
-
const afterIds = Array.isArray(pending.afterToolCallIds) ? pending.afterToolCallIds : [];
|
|
214
|
-
if (!afterIds.length) {
|
|
215
|
-
return request;
|
|
216
|
-
}
|
|
217
|
-
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
218
|
-
const toolIds = extractToolCallIdsFromToolMessages(messages);
|
|
219
|
-
const ready = afterIds.every((id) => toolIds.has(id));
|
|
220
|
-
if (!ready) {
|
|
221
|
-
return request;
|
|
222
|
-
}
|
|
223
|
-
const insertAt = findLastToolMessageIndex(messages);
|
|
224
|
-
if (insertAt < 0) {
|
|
225
|
-
return request;
|
|
226
|
-
}
|
|
227
|
-
const inject = Array.isArray(pending.messages) ? pending.messages : [];
|
|
228
|
-
if (!inject.length) {
|
|
229
|
-
return request;
|
|
230
|
-
}
|
|
231
|
-
const nextMessages = messages.slice();
|
|
232
|
-
nextMessages.splice(insertAt + 1, 0, ...inject);
|
|
233
|
-
try {
|
|
234
|
-
await clearPendingServerToolInjection(sessionId);
|
|
235
|
-
}
|
|
236
|
-
catch {
|
|
237
|
-
// best-effort
|
|
238
|
-
}
|
|
239
|
-
return { ...request, messages: nextMessages };
|
|
240
|
-
}
|
|
241
|
-
function buildProcessedRequest(request) {
|
|
242
|
-
const timestamp = Date.now();
|
|
243
|
-
const streamingEnabled = request.parameters?.stream === true;
|
|
244
|
-
return {
|
|
245
|
-
...request,
|
|
246
|
-
processed: {
|
|
247
|
-
timestamp,
|
|
248
|
-
appliedRules: ['tool-governance'],
|
|
249
|
-
status: 'success'
|
|
250
|
-
},
|
|
251
|
-
processingMetadata: {
|
|
252
|
-
streaming: {
|
|
253
|
-
enabled: streamingEnabled,
|
|
254
|
-
chunkCount: 0
|
|
255
|
-
},
|
|
256
|
-
context: buildContextMetadata(request.metadata)
|
|
257
|
-
}
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
function buildContextMetadata(metadata) {
|
|
261
|
-
if (!metadata?.capturedContext || typeof metadata.capturedContext !== 'object') {
|
|
262
|
-
return undefined;
|
|
263
|
-
}
|
|
264
|
-
return { ...metadata.capturedContext };
|
|
265
|
-
}
|
|
266
|
-
function buildSuccessResult(startTime, processedRequest) {
|
|
267
|
-
const endTime = Date.now();
|
|
268
|
-
return {
|
|
269
|
-
success: true,
|
|
270
|
-
metadata: {
|
|
271
|
-
node: 'hub-chat-process',
|
|
272
|
-
executionTime: endTime - startTime,
|
|
273
|
-
startTime,
|
|
274
|
-
endTime,
|
|
275
|
-
dataProcessed: {
|
|
276
|
-
messages: processedRequest.messages.length,
|
|
277
|
-
tools: processedRequest.tools?.length ?? 0
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
function buildErrorResult(startTime, error) {
|
|
283
|
-
const endTime = Date.now();
|
|
284
|
-
return {
|
|
285
|
-
success: false,
|
|
286
|
-
metadata: {
|
|
287
|
-
node: 'hub-chat-process',
|
|
288
|
-
executionTime: endTime - startTime,
|
|
289
|
-
startTime,
|
|
290
|
-
endTime
|
|
291
|
-
},
|
|
292
|
-
error: {
|
|
293
|
-
message: error instanceof Error ? error.message : String(error)
|
|
294
|
-
}
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
function castGovernedTools(tools) {
|
|
298
|
-
if (!Array.isArray(tools)) {
|
|
299
|
-
return undefined;
|
|
300
|
-
}
|
|
301
|
-
const normalized = [];
|
|
302
|
-
for (const tool of tools) {
|
|
303
|
-
const converted = castSingleTool(tool) ?? castCustomTool(tool);
|
|
304
|
-
if (converted) {
|
|
305
|
-
normalized.push(converted);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
return normalized.length ? normalized : [];
|
|
309
|
-
}
|
|
310
|
-
function castSingleTool(tool) {
|
|
311
|
-
if (!isRecord(tool)) {
|
|
312
|
-
return null;
|
|
313
|
-
}
|
|
314
|
-
const fn = tool.function;
|
|
315
|
-
if (!isRecord(fn) || typeof fn.name !== 'string') {
|
|
316
|
-
return null;
|
|
317
|
-
}
|
|
318
|
-
const parameters = castToolParameters(fn.parameters);
|
|
319
|
-
const description = typeof fn.description === 'string' ? fn.description : undefined;
|
|
320
|
-
const strictFlag = fn.strict === true || tool.strict === true;
|
|
321
|
-
return {
|
|
322
|
-
type: 'function',
|
|
323
|
-
function: {
|
|
324
|
-
name: fn.name,
|
|
325
|
-
description,
|
|
326
|
-
parameters,
|
|
327
|
-
strict: strictFlag ? true : undefined
|
|
328
|
-
}
|
|
329
|
-
};
|
|
330
|
-
}
|
|
331
|
-
function stripHistoricalImageAttachments(messages) {
|
|
332
|
-
if (!Array.isArray(messages) || !messages.length) {
|
|
333
|
-
return messages;
|
|
334
|
-
}
|
|
335
|
-
const placeholderText = '[Image omitted]';
|
|
336
|
-
const last = messages[messages.length - 1];
|
|
337
|
-
const lastRole = typeof last?.role === 'string' ? String(last.role) : '';
|
|
338
|
-
const isNewUserTurn = lastRole === 'user';
|
|
339
|
-
// 找到最新一条 user 消息:
|
|
340
|
-
// - 若本轮是 user 输入(最后一条消息 role=user),则仅允许这一条保留图片分段;
|
|
341
|
-
// - 若本轮是 followup(最后一条消息非 user,例如 tool/assistant),则所有 user 消息的图片都应替换为占位符,
|
|
342
|
-
// 避免在工具循环/多次 followup 中反复透传大体积图片数据。
|
|
343
|
-
let latestUserIndex = -1;
|
|
344
|
-
if (isNewUserTurn) {
|
|
345
|
-
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
346
|
-
const candidate = messages[idx];
|
|
347
|
-
if (candidate && typeof candidate === 'object' && candidate.role === 'user') {
|
|
348
|
-
latestUserIndex = idx;
|
|
349
|
-
break;
|
|
350
|
-
}
|
|
351
|
-
}
|
|
352
|
-
if (latestUserIndex < 0) {
|
|
353
|
-
return messages;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
let changed = false;
|
|
357
|
-
const next = messages.slice();
|
|
358
|
-
for (let idx = 0; idx < messages.length; idx += 1) {
|
|
359
|
-
if (isNewUserTurn && idx === latestUserIndex) {
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
const message = messages[idx];
|
|
363
|
-
if (!message || typeof message !== 'object') {
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (message.role !== 'user') {
|
|
367
|
-
continue;
|
|
368
|
-
}
|
|
369
|
-
const content = message.content;
|
|
370
|
-
if (!Array.isArray(content) || !content.length) {
|
|
371
|
-
continue;
|
|
372
|
-
}
|
|
373
|
-
const filtered = [];
|
|
374
|
-
let removed = false;
|
|
375
|
-
for (const part of content) {
|
|
376
|
-
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
377
|
-
const typeValue = part.type;
|
|
378
|
-
if (typeof typeValue === 'string' && typeValue.toLowerCase().includes('image')) {
|
|
379
|
-
removed = true;
|
|
380
|
-
filtered.push({ type: 'text', text: placeholderText });
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
}
|
|
384
|
-
filtered.push(part);
|
|
385
|
-
}
|
|
386
|
-
if (removed) {
|
|
387
|
-
const cloned = {
|
|
388
|
-
...message,
|
|
389
|
-
content: filtered
|
|
390
|
-
};
|
|
391
|
-
next[idx] = cloned;
|
|
392
|
-
changed = true;
|
|
393
|
-
}
|
|
394
|
-
}
|
|
395
|
-
return changed ? next : messages;
|
|
396
|
-
}
|
|
397
|
-
function containsImageAttachment(messages) {
|
|
398
|
-
if (!Array.isArray(messages) || !messages.length) {
|
|
399
|
-
return false;
|
|
400
|
-
}
|
|
401
|
-
// 仅检查“当前这一轮用户输入”(messages 最后一条必须是 user)是否携带图片。
|
|
402
|
-
// 避免在 tool/assistant followup 请求中,因为历史 user 消息含图片而反复触发 image 标记。
|
|
403
|
-
const last = messages[messages.length - 1];
|
|
404
|
-
if (!last || typeof last !== 'object' || Array.isArray(last) || last.role !== 'user') {
|
|
405
|
-
return false;
|
|
406
|
-
}
|
|
407
|
-
const content = last.content;
|
|
408
|
-
if (!Array.isArray(content)) {
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
for (const part of content) {
|
|
412
|
-
if (!part || typeof part !== 'object') {
|
|
413
|
-
continue;
|
|
414
|
-
}
|
|
415
|
-
const typeValue = part.type;
|
|
416
|
-
if (typeof typeValue !== 'string') {
|
|
417
|
-
continue;
|
|
418
|
-
}
|
|
419
|
-
const normalized = typeValue.toLowerCase();
|
|
420
|
-
if (!normalized.includes('image')) {
|
|
421
|
-
continue;
|
|
422
|
-
}
|
|
423
|
-
const record = part;
|
|
424
|
-
let imageCandidate = '';
|
|
425
|
-
if (typeof record.image_url === 'string') {
|
|
426
|
-
imageCandidate = record.image_url;
|
|
427
|
-
}
|
|
428
|
-
else if (record.image_url && typeof record.image_url.url === 'string') {
|
|
429
|
-
imageCandidate = record.image_url.url ?? '';
|
|
430
|
-
}
|
|
431
|
-
else if (typeof record.url === 'string') {
|
|
432
|
-
imageCandidate = record.url;
|
|
433
|
-
}
|
|
434
|
-
else if (typeof record.uri === 'string') {
|
|
435
|
-
imageCandidate = record.uri;
|
|
436
|
-
}
|
|
437
|
-
else if (typeof record.data === 'string') {
|
|
438
|
-
imageCandidate = record.data;
|
|
439
|
-
}
|
|
440
|
-
if (imageCandidate.trim().length > 0) {
|
|
441
|
-
return true;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return false;
|
|
445
|
-
}
|
|
446
|
-
function castCustomTool(tool) {
|
|
447
|
-
if (!isRecord(tool)) {
|
|
448
|
-
return null;
|
|
449
|
-
}
|
|
450
|
-
const type = typeof tool.type === 'string' ? tool.type.toLowerCase() : '';
|
|
451
|
-
const name = typeof tool.name === 'string' ? tool.name : '';
|
|
452
|
-
if (type !== 'custom' || !name) {
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
if (name.trim() !== 'apply_patch') {
|
|
456
|
-
return null;
|
|
457
|
-
}
|
|
458
|
-
const description = typeof tool.description === 'string' ? tool.description : undefined;
|
|
459
|
-
return {
|
|
460
|
-
type: 'function',
|
|
461
|
-
function: {
|
|
462
|
-
name: 'apply_patch',
|
|
463
|
-
description,
|
|
464
|
-
parameters: ensureApplyPatchSchema(),
|
|
465
|
-
strict: true
|
|
466
|
-
}
|
|
467
|
-
};
|
|
468
|
-
}
|
|
469
|
-
function castToolParameters(value) {
|
|
470
|
-
if (isRecord(value)) {
|
|
471
|
-
const cloned = deepClone(value);
|
|
472
|
-
if (cloned && typeof cloned === 'object' && !Array.isArray(cloned)) {
|
|
473
|
-
const schema = cloned;
|
|
474
|
-
if (!('type' in schema)) {
|
|
475
|
-
schema.type = 'object';
|
|
476
|
-
}
|
|
477
|
-
if (!isRecord(schema.properties)) {
|
|
478
|
-
schema.properties = {};
|
|
479
|
-
}
|
|
480
|
-
return schema;
|
|
481
|
-
}
|
|
482
|
-
}
|
|
483
|
-
return {
|
|
484
|
-
type: 'object',
|
|
485
|
-
properties: {},
|
|
486
|
-
additionalProperties: true
|
|
487
|
-
};
|
|
488
|
-
}
|
|
489
|
-
function normalizeRecord(value) {
|
|
490
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
491
|
-
return value;
|
|
492
|
-
}
|
|
493
|
-
return {};
|
|
494
|
-
}
|
|
495
|
-
function deepClone(value) {
|
|
496
|
-
return JSON.parse(JSON.stringify(value));
|
|
497
|
-
}
|
|
498
|
-
function readString(value) {
|
|
499
|
-
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
500
|
-
}
|
|
501
|
-
function readToolChoice(value) {
|
|
502
|
-
if (!value) {
|
|
503
|
-
return undefined;
|
|
504
|
-
}
|
|
505
|
-
if (typeof value === 'string') {
|
|
506
|
-
return value;
|
|
507
|
-
}
|
|
508
|
-
if (typeof value === 'object') {
|
|
509
|
-
return value;
|
|
510
|
-
}
|
|
511
|
-
return undefined;
|
|
512
|
-
}
|
|
513
|
-
function isRecord(value) {
|
|
514
|
-
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
515
|
-
}
|
|
516
|
-
function maybeInjectWebSearchTool(request, metadata) {
|
|
517
|
-
const ops = buildWebSearchOperations(request, metadata);
|
|
518
|
-
if (!ops.length) {
|
|
519
|
-
return request;
|
|
520
|
-
}
|
|
521
|
-
return applyHubOperations(request, ops);
|
|
522
|
-
}
|
|
523
|
-
function buildInboundStreamingOperations(intent) {
|
|
524
|
-
return [
|
|
525
|
-
{
|
|
526
|
-
op: 'set_request_metadata_fields',
|
|
527
|
-
fields: { inboundStream: intent }
|
|
528
|
-
}
|
|
529
|
-
];
|
|
530
|
-
}
|
|
531
|
-
function buildOutboundStreamingOperations(stream) {
|
|
532
|
-
return [
|
|
533
|
-
{
|
|
534
|
-
op: 'set_request_parameter_fields',
|
|
535
|
-
fields: { stream }
|
|
536
|
-
}
|
|
537
|
-
];
|
|
538
|
-
}
|
|
539
|
-
function buildToolChoiceOperations(toolChoice) {
|
|
540
|
-
return [
|
|
541
|
-
{
|
|
542
|
-
op: 'set_request_parameter_fields',
|
|
543
|
-
fields: { tool_choice: toolChoice }
|
|
544
|
-
}
|
|
545
|
-
];
|
|
546
|
-
}
|
|
547
|
-
function buildWebSearchOperations(request, metadata) {
|
|
548
|
-
const rt = readRuntimeMetadata(metadata);
|
|
549
|
-
// ServerTool 二/三跳(serverToolFollowup=true)不再注入 web_search 工具,
|
|
550
|
-
// 以避免在 web_search 流程内部形成循环命中。
|
|
551
|
-
if (rt?.serverToolFollowup === true) {
|
|
552
|
-
return [];
|
|
553
|
-
}
|
|
554
|
-
const rawConfig = rt?.webSearch;
|
|
555
|
-
if (!rawConfig || !Array.isArray(rawConfig.engines) || rawConfig.engines.length === 0) {
|
|
556
|
-
return [];
|
|
557
|
-
}
|
|
558
|
-
const semanticsWebSearch = extractWebSearchSemantics(request.semantics);
|
|
559
|
-
if (semanticsWebSearch?.disable === true) {
|
|
560
|
-
return [];
|
|
561
|
-
}
|
|
562
|
-
const injectPolicy = semanticsWebSearch?.force === true
|
|
563
|
-
? 'always'
|
|
564
|
-
: rawConfig.injectPolicy === 'always' || rawConfig.injectPolicy === 'selective'
|
|
565
|
-
? rawConfig.injectPolicy
|
|
566
|
-
: 'selective';
|
|
567
|
-
const intent = detectWebSearchIntent(request);
|
|
568
|
-
if (injectPolicy === 'selective') {
|
|
569
|
-
// 仅当当前这一轮用户输入明确表达“联网搜索”意图时才注入 web_search。
|
|
570
|
-
// 不再依赖上一轮工具分类(read/search/websearch),避免形成隐式续写语义。
|
|
571
|
-
if (!intent.hasIntent) {
|
|
572
|
-
return [];
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
const existingTools = Array.isArray(request.tools) ? request.tools : [];
|
|
576
|
-
let engines = rawConfig.engines.filter((engine) => typeof engine?.id === 'string' && !!engine.id.trim() && !engine.serverToolsDisabled);
|
|
577
|
-
// 当用户明确要求「谷歌搜索」时,只暴露 Gemini / Antigravity 类搜索后端:
|
|
578
|
-
// - providerKey 以 gemini-cli. 或 antigravity. 开头;
|
|
579
|
-
// - 或 engine id 中包含 "google"(向前兼容配置中用 id 标识 google 引擎的场景)。
|
|
580
|
-
if (intent.googlePreferred) {
|
|
581
|
-
const preferred = engines.filter((engine) => {
|
|
582
|
-
const id = engine.id.trim().toLowerCase();
|
|
583
|
-
const providerKey = (engine.providerKey || '').toLowerCase();
|
|
584
|
-
if (providerKey.startsWith('gemini-cli.') || providerKey.startsWith('antigravity.')) {
|
|
585
|
-
return true;
|
|
586
|
-
}
|
|
587
|
-
if (id.includes('google')) {
|
|
588
|
-
return true;
|
|
589
|
-
}
|
|
590
|
-
return false;
|
|
591
|
-
});
|
|
592
|
-
if (preferred.length > 0) {
|
|
593
|
-
engines = preferred;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
// DeepSeek native search path:
|
|
597
|
-
// when deepseek web_search engine is configured as top priority,
|
|
598
|
-
// route directly to deepseek-search model instead of servertool tool injection.
|
|
599
|
-
if (shouldBypassServerToolWebSearch(intent, engines, semanticsWebSearch)) {
|
|
600
|
-
return [];
|
|
601
|
-
}
|
|
602
|
-
if (!engines.length) {
|
|
603
|
-
return [];
|
|
604
|
-
}
|
|
605
|
-
const engineIds = engines.map((engine) => engine.id.trim());
|
|
606
|
-
const engineDescriptions = engines
|
|
607
|
-
.map((engine) => {
|
|
608
|
-
const id = engine.id.trim();
|
|
609
|
-
const desc = typeof engine.description === 'string' && engine.description.trim()
|
|
610
|
-
? engine.description.trim()
|
|
611
|
-
: '';
|
|
612
|
-
return desc ? `${id}: ${desc}` : id;
|
|
613
|
-
})
|
|
614
|
-
.join('; ');
|
|
615
|
-
const parameters = {
|
|
616
|
-
type: 'object',
|
|
617
|
-
properties: {
|
|
618
|
-
engine: {
|
|
619
|
-
type: 'string',
|
|
620
|
-
enum: engineIds,
|
|
621
|
-
description: engineDescriptions
|
|
622
|
-
},
|
|
623
|
-
query: {
|
|
624
|
-
type: 'string',
|
|
625
|
-
description: 'Search query or user question.'
|
|
626
|
-
},
|
|
627
|
-
recency: {
|
|
628
|
-
type: 'string',
|
|
629
|
-
enum: ['oneDay', 'oneWeek', 'oneMonth', 'oneYear', 'noLimit'],
|
|
630
|
-
description: 'Optional recency filter for web search results.'
|
|
631
|
-
},
|
|
632
|
-
count: {
|
|
633
|
-
type: 'integer',
|
|
634
|
-
minimum: 1,
|
|
635
|
-
maximum: 50,
|
|
636
|
-
description: 'Number of results to retrieve.'
|
|
637
|
-
}
|
|
638
|
-
},
|
|
639
|
-
// 对于 Responses 内建 web_search,required 需要覆盖 properties 中的所有字段,
|
|
640
|
-
// 否则上游会报 "required is required to be supplied and to be an array including every key in properties"。
|
|
641
|
-
required: ['engine', 'query', 'recency', 'count'],
|
|
642
|
-
additionalProperties: false
|
|
643
|
-
};
|
|
644
|
-
const webSearchTool = {
|
|
645
|
-
type: 'function',
|
|
646
|
-
function: {
|
|
647
|
-
name: 'web_search',
|
|
648
|
-
description: 'Perform web search using configured search engines. Use this when the user asks for up-to-date information or news.',
|
|
649
|
-
parameters,
|
|
650
|
-
strict: true
|
|
651
|
-
}
|
|
652
|
-
};
|
|
653
|
-
const ops = [
|
|
654
|
-
{
|
|
655
|
-
op: 'set_request_metadata_fields',
|
|
656
|
-
fields: { webSearchEnabled: true }
|
|
657
|
-
}
|
|
658
|
-
];
|
|
659
|
-
ops.push({
|
|
660
|
-
op: 'append_tool_if_missing',
|
|
661
|
-
toolName: 'web_search',
|
|
662
|
-
tool: webSearchTool
|
|
663
|
-
});
|
|
664
|
-
return ops;
|
|
665
|
-
}
|
|
666
|
-
function shouldBypassServerToolWebSearch(intent, engines, semanticsWebSearch) {
|
|
667
|
-
if (!intent.hasIntent || intent.googlePreferred) {
|
|
668
|
-
return false;
|
|
669
|
-
}
|
|
670
|
-
// Explicit semantic force should keep servertool orchestration.
|
|
671
|
-
if (semanticsWebSearch?.force === true) {
|
|
672
|
-
return false;
|
|
673
|
-
}
|
|
674
|
-
const deepseekIndex = engines.findIndex(isDeepSeekNativeWebSearchEngine);
|
|
675
|
-
if (deepseekIndex < 0) {
|
|
676
|
-
return false;
|
|
677
|
-
}
|
|
678
|
-
const deepseekEngine = engines[deepseekIndex];
|
|
679
|
-
// Treat explicit default as highest priority; otherwise require first position.
|
|
680
|
-
if (deepseekEngine.default !== true && deepseekIndex !== 0) {
|
|
681
|
-
return false;
|
|
682
|
-
}
|
|
683
|
-
return true;
|
|
684
|
-
}
|
|
685
|
-
function isDeepSeekNativeWebSearchEngine(engine) {
|
|
686
|
-
const id = typeof engine.id === 'string' ? engine.id.trim().toLowerCase() : '';
|
|
687
|
-
const providerKey = typeof engine.providerKey === 'string' ? engine.providerKey.trim().toLowerCase() : '';
|
|
688
|
-
if (id === 'deepseek:web_search') {
|
|
689
|
-
return true;
|
|
690
|
-
}
|
|
691
|
-
if (!providerKey.startsWith('deepseek-web.')) {
|
|
692
|
-
return false;
|
|
693
|
-
}
|
|
694
|
-
return providerKey.endsWith('.deepseek-chat-search') || providerKey.endsWith('.deepseek-reasoner-search');
|
|
695
|
-
}
|
|
696
|
-
function buildClockOperations(metadata) {
|
|
697
|
-
const rt = readRuntimeMetadata(metadata);
|
|
698
|
-
// Do not inject clock tool into internal servertool followup hops.
|
|
699
|
-
const allowClockFollowupToolInjection = rt?.clockFollowupInjectTool === true;
|
|
700
|
-
if (rt?.serverToolFollowup === true && !allowClockFollowupToolInjection) {
|
|
701
|
-
return [];
|
|
702
|
-
}
|
|
703
|
-
const rawConfig = rt?.clock;
|
|
704
|
-
const clockConfig = resolveClockConfig(rawConfig);
|
|
705
|
-
if (!clockConfig) {
|
|
706
|
-
return [];
|
|
707
|
-
}
|
|
708
|
-
const sessionId = readString(metadata.sessionId);
|
|
709
|
-
const hasSessionId = Boolean(sessionId && sessionId.trim());
|
|
710
|
-
logClock('inject_schema', { hasSessionId });
|
|
711
|
-
const parameters = {
|
|
712
|
-
type: 'object',
|
|
713
|
-
properties: {
|
|
714
|
-
action: {
|
|
715
|
-
type: 'string',
|
|
716
|
-
enum: ['get', 'schedule', 'update', 'list', 'cancel', 'clear'],
|
|
717
|
-
description: 'Get current time, or schedule/update/list/cancel/clear session-scoped reminders. For any wait/later requirement, schedule immediately.'
|
|
718
|
-
},
|
|
719
|
-
items: {
|
|
720
|
-
type: 'array',
|
|
721
|
-
description: 'For schedule/update: list of reminder payloads. update uses items[0] as patch source.',
|
|
722
|
-
items: {
|
|
723
|
-
type: 'object',
|
|
724
|
-
properties: {
|
|
725
|
-
dueAt: {
|
|
726
|
-
type: 'string',
|
|
727
|
-
description: 'ISO8601 datetime with timezone (e.g. 2026-01-21T20:30:00-08:00).'
|
|
728
|
-
},
|
|
729
|
-
task: {
|
|
730
|
-
type: 'string',
|
|
731
|
-
description: 'Reminder text that states the exact action to execute on wake-up (no vague placeholders).'
|
|
732
|
-
},
|
|
733
|
-
tool: {
|
|
734
|
-
type: 'string',
|
|
735
|
-
description: 'Optional suggested tool name (hint only).'
|
|
736
|
-
},
|
|
737
|
-
arguments: {
|
|
738
|
-
type: 'string',
|
|
739
|
-
description: 'Optional suggested tool arguments as a JSON string (hint only). Use "{}" when unsure.'
|
|
740
|
-
}
|
|
741
|
-
},
|
|
742
|
-
// For strict tool schemas (OpenAI Responses), required must include every key in properties.
|
|
743
|
-
// We keep tool/arguments as hints but allow empty string when not used.
|
|
744
|
-
required: ['dueAt', 'task', 'tool', 'arguments'],
|
|
745
|
-
additionalProperties: false
|
|
746
|
-
}
|
|
747
|
-
},
|
|
748
|
-
taskId: {
|
|
749
|
-
type: 'string',
|
|
750
|
-
description: 'For cancel/update: target taskId.'
|
|
751
|
-
}
|
|
752
|
-
},
|
|
753
|
-
// For strict tool schemas (OpenAI Responses), required must include every key in properties.
|
|
754
|
-
// Fields unused for certain actions can be set to empty string / empty list.
|
|
755
|
-
required: ['action', 'items', 'taskId'],
|
|
756
|
-
additionalProperties: false
|
|
757
|
-
};
|
|
758
|
-
const clockTool = {
|
|
759
|
-
type: 'function',
|
|
760
|
-
function: {
|
|
761
|
-
name: 'clock',
|
|
762
|
-
description: 'Time + Alarm for this session. MUST use clock.schedule whenever waiting/delay is required. Never promise to wait without scheduling. Use get/schedule/update/list/cancel/clear. Scheduled reminders are injected into future requests.',
|
|
763
|
-
parameters,
|
|
764
|
-
strict: true
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
return [
|
|
768
|
-
{
|
|
769
|
-
op: 'set_request_metadata_fields',
|
|
770
|
-
fields: { clockEnabled: true, ...(hasSessionId ? { serverToolRequired: true } : {}) }
|
|
771
|
-
},
|
|
772
|
-
{ op: 'append_tool_if_missing', toolName: 'clock', tool: clockTool }
|
|
773
|
-
];
|
|
774
|
-
}
|
|
775
|
-
function hasActiveStopMessageStateForContinueExecution(metadata) {
|
|
776
|
-
const rt = readRuntimeMetadata(metadata);
|
|
777
|
-
if (isStopMessageStateActive(rt?.stopMessageState)) {
|
|
778
|
-
return true;
|
|
779
|
-
}
|
|
780
|
-
return hasPersistedActiveStopMessageState(metadata);
|
|
781
|
-
}
|
|
782
|
-
function hasPersistedActiveStopMessageState(metadata) {
|
|
783
|
-
const sessionScope = resolveStopMessageSessionScope(metadata);
|
|
784
|
-
if (!sessionScope) {
|
|
785
|
-
return false;
|
|
786
|
-
}
|
|
787
|
-
try {
|
|
788
|
-
const persisted = loadRoutingInstructionStateSync(sessionScope);
|
|
789
|
-
return isStopMessageStateActive(persisted);
|
|
790
|
-
}
|
|
791
|
-
catch {
|
|
792
|
-
return false;
|
|
793
|
-
}
|
|
794
|
-
}
|
|
795
|
-
function resolveStopMessageSessionScope(metadata) {
|
|
796
|
-
const sessionId = readString(metadata.sessionId);
|
|
797
|
-
if (sessionId) {
|
|
798
|
-
return 'session:' + sessionId;
|
|
799
|
-
}
|
|
800
|
-
const conversationId = readString(metadata.conversationId);
|
|
801
|
-
if (conversationId) {
|
|
802
|
-
return 'conversation:' + conversationId;
|
|
803
|
-
}
|
|
804
|
-
return undefined;
|
|
805
|
-
}
|
|
806
|
-
function isStopMessageStateActive(raw) {
|
|
807
|
-
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
808
|
-
return false;
|
|
809
|
-
}
|
|
810
|
-
const record = raw;
|
|
811
|
-
const text = typeof record.stopMessageText === 'string' ? record.stopMessageText.trim() : '';
|
|
812
|
-
const maxRepeats = typeof record.stopMessageMaxRepeats === 'number' && Number.isFinite(record.stopMessageMaxRepeats)
|
|
813
|
-
? Math.max(1, Math.floor(record.stopMessageMaxRepeats))
|
|
814
|
-
: 0;
|
|
815
|
-
const stageMode = typeof record.stopMessageStageMode === 'string' ? record.stopMessageStageMode.trim().toLowerCase() : '';
|
|
816
|
-
if (stageMode === 'off') {
|
|
817
|
-
return false;
|
|
818
|
-
}
|
|
819
|
-
if (text && maxRepeats > 0) {
|
|
820
|
-
return true;
|
|
821
|
-
}
|
|
822
|
-
return stageMode === 'on' || stageMode === 'auto';
|
|
823
|
-
}
|
|
824
|
-
function buildContinueExecutionOperations(metadata) {
|
|
825
|
-
const rt = readRuntimeMetadata(metadata);
|
|
826
|
-
if (rt?.serverToolFollowup === true) {
|
|
827
|
-
return [];
|
|
828
|
-
}
|
|
829
|
-
const sessionId = readString(metadata.sessionId);
|
|
830
|
-
const hasSessionId = Boolean(sessionId && sessionId.trim());
|
|
831
|
-
if (hasActiveStopMessageStateForContinueExecution(metadata)) {
|
|
832
|
-
logContinueExecution('skip_schema_due_stopmessage', { hasSessionId });
|
|
833
|
-
return [];
|
|
834
|
-
}
|
|
835
|
-
logContinueExecution('inject_schema', { hasSessionId });
|
|
836
|
-
const parameters = {
|
|
837
|
-
type: 'object',
|
|
838
|
-
properties: {},
|
|
839
|
-
required: [],
|
|
840
|
-
additionalProperties: false
|
|
841
|
-
};
|
|
842
|
-
const continueTool = {
|
|
843
|
-
type: 'function',
|
|
844
|
-
function: {
|
|
845
|
-
name: 'continue_execution',
|
|
846
|
-
description: 'No-op control tool. Use this before status summaries or before ending the turn. If you are about to send finish_reason=stop (or protocol-equivalent end_turn), do not end directly; emit a continue_execution tool call instead. Keep it lightweight. The server will auto-followup so execution continues, avoiding interruption and improving execution efficiency.',
|
|
847
|
-
parameters,
|
|
848
|
-
strict: true
|
|
849
|
-
}
|
|
850
|
-
};
|
|
851
|
-
return [
|
|
852
|
-
{
|
|
853
|
-
op: 'set_request_metadata_fields',
|
|
854
|
-
fields: { continueExecutionEnabled: true }
|
|
855
|
-
},
|
|
856
|
-
{ op: 'append_tool_if_missing', toolName: 'continue_execution', tool: continueTool }
|
|
857
|
-
];
|
|
858
|
-
}
|
|
859
|
-
function injectContinueExecutionDirectiveIntoUserMessage(request, metadata) {
|
|
860
|
-
const rt = readRuntimeMetadata(metadata);
|
|
861
|
-
if (rt?.serverToolFollowup === true) {
|
|
862
|
-
return request;
|
|
863
|
-
}
|
|
864
|
-
if (hasActiveStopMessageStateForContinueExecution(metadata)) {
|
|
865
|
-
const sessionId = readString(metadata.sessionId);
|
|
866
|
-
const hasSessionId = Boolean(sessionId && sessionId.trim());
|
|
867
|
-
logContinueExecution('skip_user_directive_due_stopmessage', { hasSessionId });
|
|
868
|
-
return request;
|
|
869
|
-
}
|
|
870
|
-
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
871
|
-
if (messages.length === 0) {
|
|
872
|
-
return request;
|
|
873
|
-
}
|
|
874
|
-
const hasDirective = messages.some((message) => {
|
|
875
|
-
if (!message || typeof message !== 'object' || message.role !== 'user') {
|
|
876
|
-
return false;
|
|
877
|
-
}
|
|
878
|
-
return messageContentContainsDirectiveMarker(message.content);
|
|
879
|
-
});
|
|
880
|
-
if (hasDirective) {
|
|
881
|
-
return request;
|
|
882
|
-
}
|
|
883
|
-
const lastUserIdx = findLastUserMessageIndex(messages);
|
|
884
|
-
if (lastUserIdx < 0) {
|
|
885
|
-
return request;
|
|
886
|
-
}
|
|
887
|
-
const nextMessages = messages.slice();
|
|
888
|
-
const target = nextMessages[lastUserIdx];
|
|
889
|
-
const nextContent = appendDirectiveToUserContent(target.content, CONTINUE_EXECUTION_SYSTEM_DIRECTIVE);
|
|
890
|
-
nextMessages[lastUserIdx] = {
|
|
891
|
-
...target,
|
|
892
|
-
content: nextContent
|
|
893
|
-
};
|
|
894
|
-
const sessionId = readString(metadata.sessionId);
|
|
895
|
-
const hasSessionId = Boolean(sessionId && sessionId.trim());
|
|
896
|
-
logContinueExecution('inject_user_directive', { hasSessionId });
|
|
897
|
-
return {
|
|
898
|
-
...request,
|
|
899
|
-
messages: nextMessages
|
|
900
|
-
};
|
|
901
|
-
}
|
|
902
|
-
function messageContentContainsDirectiveMarker(content) {
|
|
903
|
-
if (typeof content === 'string') {
|
|
904
|
-
return content.includes(CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER);
|
|
905
|
-
}
|
|
906
|
-
if (!Array.isArray(content)) {
|
|
907
|
-
return false;
|
|
908
|
-
}
|
|
909
|
-
for (const part of content) {
|
|
910
|
-
if (!part || typeof part !== 'object') {
|
|
911
|
-
continue;
|
|
912
|
-
}
|
|
913
|
-
const text = typeof part.text === 'string' ? String(part.text) : '';
|
|
914
|
-
if (text.includes(CONTINUE_EXECUTION_SYSTEM_DIRECTIVE_MARKER)) {
|
|
915
|
-
return true;
|
|
916
|
-
}
|
|
917
|
-
}
|
|
918
|
-
return false;
|
|
919
|
-
}
|
|
920
|
-
function appendDirectiveToUserContent(content, directive) {
|
|
921
|
-
if (typeof content === 'string') {
|
|
922
|
-
const base = content.trimEnd();
|
|
923
|
-
return base.length ? `${base}\n\n${directive}` : directive;
|
|
924
|
-
}
|
|
925
|
-
if (!Array.isArray(content)) {
|
|
926
|
-
return directive;
|
|
927
|
-
}
|
|
928
|
-
const next = content.slice();
|
|
929
|
-
for (let idx = next.length - 1; idx >= 0; idx -= 1) {
|
|
930
|
-
const part = next[idx];
|
|
931
|
-
if (!part || typeof part !== 'object' || typeof part.text !== 'string') {
|
|
932
|
-
continue;
|
|
933
|
-
}
|
|
934
|
-
const base = String(part.text).trimEnd();
|
|
935
|
-
next[idx] = {
|
|
936
|
-
...part,
|
|
937
|
-
text: base.length ? `${base}\n\n${directive}` : directive
|
|
938
|
-
};
|
|
939
|
-
return next;
|
|
940
|
-
}
|
|
941
|
-
next.push({ type: 'input_text', text: directive });
|
|
942
|
-
return next;
|
|
943
|
-
}
|
|
944
|
-
function resolveSessionIdForClock(metadata, request) {
|
|
945
|
-
const requestMetadata = request.metadata && typeof request.metadata === 'object' && !Array.isArray(request.metadata)
|
|
946
|
-
? request.metadata
|
|
947
|
-
: null;
|
|
948
|
-
return resolveClockSessionScope(metadata, requestMetadata);
|
|
949
|
-
}
|
|
950
|
-
function stripClockClearDirectiveFromText(text) {
|
|
951
|
-
const pattern = /<\*\*\s*clock\s*:\s*clear\s*\*\*>/gi;
|
|
952
|
-
const hadClear = pattern.test(text);
|
|
953
|
-
if (!hadClear) {
|
|
954
|
-
return { hadClear: false, next: text };
|
|
955
|
-
}
|
|
956
|
-
const replaced = text.replace(pattern, '');
|
|
957
|
-
// Clean up leftover excessive blank lines to keep prompts tidy.
|
|
958
|
-
const next = replaced.replace(/\n{3,}/g, '\n\n').trim();
|
|
959
|
-
return { hadClear: true, next };
|
|
960
|
-
}
|
|
961
|
-
function unquoteMarkerToken(value) {
|
|
962
|
-
const trimmed = String(value || '').trim();
|
|
963
|
-
if (!trimmed) {
|
|
964
|
-
return '';
|
|
965
|
-
}
|
|
966
|
-
if ((trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
967
|
-
(trimmed.startsWith("'") && trimmed.endsWith("'"))) {
|
|
968
|
-
return trimmed.slice(1, -1).trim();
|
|
969
|
-
}
|
|
970
|
-
return trimmed;
|
|
971
|
-
}
|
|
972
|
-
function parseMarkerRecurrenceKind(raw) {
|
|
973
|
-
const value = typeof raw === 'string' ? raw.trim().toLowerCase() : '';
|
|
974
|
-
if (!value) {
|
|
975
|
-
return undefined;
|
|
976
|
-
}
|
|
977
|
-
if (value === 'daily' || value === 'day') {
|
|
978
|
-
return 'daily';
|
|
979
|
-
}
|
|
980
|
-
if (value === 'weekly' || value === 'week') {
|
|
981
|
-
return 'weekly';
|
|
982
|
-
}
|
|
983
|
-
if (value === 'interval' || value === 'every_minutes' || value === 'every-minutes' || value === 'everyminutes') {
|
|
984
|
-
return 'interval';
|
|
985
|
-
}
|
|
986
|
-
return undefined;
|
|
987
|
-
}
|
|
988
|
-
function parseMarkerRecurrence(record) {
|
|
989
|
-
const recurrenceRaw = record.recurrence ?? record.repeat ?? record.cycle;
|
|
990
|
-
if (recurrenceRaw === undefined || recurrenceRaw === null || recurrenceRaw === false) {
|
|
991
|
-
return undefined;
|
|
992
|
-
}
|
|
993
|
-
let kind;
|
|
994
|
-
let everyMinutesRaw;
|
|
995
|
-
let maxRunsRaw;
|
|
996
|
-
if (typeof recurrenceRaw === 'string') {
|
|
997
|
-
kind = parseMarkerRecurrenceKind(recurrenceRaw);
|
|
998
|
-
everyMinutesRaw = record.everyMinutes;
|
|
999
|
-
maxRunsRaw = record.maxRuns;
|
|
1000
|
-
}
|
|
1001
|
-
else if (recurrenceRaw && typeof recurrenceRaw === 'object' && !Array.isArray(recurrenceRaw)) {
|
|
1002
|
-
const recurrenceRecord = recurrenceRaw;
|
|
1003
|
-
kind = parseMarkerRecurrenceKind(recurrenceRecord.kind ?? recurrenceRecord.type ?? recurrenceRecord.mode ?? recurrenceRecord.every);
|
|
1004
|
-
everyMinutesRaw = recurrenceRecord.everyMinutes ?? recurrenceRecord.minutes ?? record.everyMinutes;
|
|
1005
|
-
maxRunsRaw = recurrenceRecord.maxRuns ?? record.maxRuns;
|
|
1006
|
-
}
|
|
1007
|
-
if (!kind) {
|
|
1008
|
-
return undefined;
|
|
1009
|
-
}
|
|
1010
|
-
const maxRuns = Number(maxRunsRaw);
|
|
1011
|
-
if (!Number.isFinite(maxRuns) || Math.floor(maxRuns) <= 0) {
|
|
1012
|
-
return undefined;
|
|
1013
|
-
}
|
|
1014
|
-
if (kind === 'interval') {
|
|
1015
|
-
const everyMinutes = Number(everyMinutesRaw);
|
|
1016
|
-
if (!Number.isFinite(everyMinutes) || Math.floor(everyMinutes) <= 0) {
|
|
1017
|
-
return undefined;
|
|
1018
|
-
}
|
|
1019
|
-
return { kind: 'interval', maxRuns: Math.floor(maxRuns), everyMinutes: Math.floor(everyMinutes) };
|
|
1020
|
-
}
|
|
1021
|
-
return { kind, maxRuns: Math.floor(maxRuns) };
|
|
1022
|
-
}
|
|
1023
|
-
function parseClockScheduleDirectivePayload(payload) {
|
|
1024
|
-
const raw = String(payload || '').trim();
|
|
1025
|
-
if (!raw || /^clear$/i.test(raw)) {
|
|
1026
|
-
return null;
|
|
1027
|
-
}
|
|
1028
|
-
const parseFromRecord = (record) => {
|
|
1029
|
-
const timeRaw = typeof record.time === 'string' ? record.time :
|
|
1030
|
-
typeof record.dueAt === 'string' ? record.dueAt :
|
|
1031
|
-
typeof record.due_at === 'string' ? record.due_at :
|
|
1032
|
-
'';
|
|
1033
|
-
const messageRaw = typeof record.message === 'string' ? record.message :
|
|
1034
|
-
typeof record.task === 'string' ? record.task :
|
|
1035
|
-
'';
|
|
1036
|
-
const dueAt = String(timeRaw || '').trim();
|
|
1037
|
-
const task = String(messageRaw || '').trim();
|
|
1038
|
-
const dueAtMs = parseDueAtMs(dueAt);
|
|
1039
|
-
if (!dueAtMs || !task) {
|
|
1040
|
-
return null;
|
|
1041
|
-
}
|
|
1042
|
-
const hasRecurrenceRaw = Object.prototype.hasOwnProperty.call(record, 'recurrence')
|
|
1043
|
-
|| Object.prototype.hasOwnProperty.call(record, 'repeat')
|
|
1044
|
-
|| Object.prototype.hasOwnProperty.call(record, 'cycle');
|
|
1045
|
-
const recurrence = parseMarkerRecurrence(record);
|
|
1046
|
-
if (hasRecurrenceRaw && !recurrence) {
|
|
1047
|
-
return null;
|
|
1048
|
-
}
|
|
1049
|
-
return {
|
|
1050
|
-
dueAtMs,
|
|
1051
|
-
dueAt: new Date(dueAtMs).toISOString(),
|
|
1052
|
-
task,
|
|
1053
|
-
...(recurrence ? { recurrence } : {})
|
|
1054
|
-
};
|
|
1055
|
-
};
|
|
1056
|
-
try {
|
|
1057
|
-
const parsed = JSON.parse(raw);
|
|
1058
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1059
|
-
const fromJson = parseFromRecord(parsed);
|
|
1060
|
-
if (fromJson) {
|
|
1061
|
-
return fromJson;
|
|
1062
|
-
}
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
catch {
|
|
1066
|
-
// fall through to loose parser
|
|
1067
|
-
}
|
|
1068
|
-
const loose = /^\{\s*time\s*:\s*([^,}]+)\s*,\s*message\s*:\s*([^}]+)\s*\}$/i.exec(raw);
|
|
1069
|
-
if (!loose) {
|
|
1070
|
-
return null;
|
|
1071
|
-
}
|
|
1072
|
-
const dueAt = unquoteMarkerToken(loose[1]);
|
|
1073
|
-
const task = unquoteMarkerToken(loose[2]);
|
|
1074
|
-
const dueAtMs = parseDueAtMs(dueAt);
|
|
1075
|
-
if (!dueAtMs || !task) {
|
|
1076
|
-
return null;
|
|
1077
|
-
}
|
|
1078
|
-
return {
|
|
1079
|
-
dueAtMs,
|
|
1080
|
-
dueAt: new Date(dueAtMs).toISOString(),
|
|
1081
|
-
task
|
|
1082
|
-
};
|
|
1083
|
-
}
|
|
1084
|
-
function extractClockScheduleDirectivesFromText(text) {
|
|
1085
|
-
const pattern = /<\*\*\s*clock\s*:\s*([\s\S]*?)\s*\*\*>/gi;
|
|
1086
|
-
const directives = [];
|
|
1087
|
-
const next = String(text || '').replace(pattern, (full, payload) => {
|
|
1088
|
-
const parsed = parseClockScheduleDirectivePayload(String(payload || ''));
|
|
1089
|
-
if (!parsed) {
|
|
1090
|
-
return full;
|
|
1091
|
-
}
|
|
1092
|
-
directives.push(parsed);
|
|
1093
|
-
return '';
|
|
1094
|
-
});
|
|
1095
|
-
return {
|
|
1096
|
-
directives,
|
|
1097
|
-
next: next.replace(/\n{3,}/g, '\n\n').trim()
|
|
1098
|
-
};
|
|
1099
|
-
}
|
|
1100
|
-
function extractClockScheduleDirectivesFromContent(content) {
|
|
1101
|
-
if (typeof content === 'string') {
|
|
1102
|
-
const result = extractClockScheduleDirectivesFromText(content);
|
|
1103
|
-
return { directives: result.directives, next: result.next };
|
|
1104
|
-
}
|
|
1105
|
-
if (Array.isArray(content)) {
|
|
1106
|
-
const directives = [];
|
|
1107
|
-
const next = content.map((part) => {
|
|
1108
|
-
if (typeof part === 'string') {
|
|
1109
|
-
const result = extractClockScheduleDirectivesFromText(part);
|
|
1110
|
-
if (result.directives.length) {
|
|
1111
|
-
directives.push(...result.directives);
|
|
1112
|
-
}
|
|
1113
|
-
return result.next;
|
|
1114
|
-
}
|
|
1115
|
-
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
1116
|
-
const block = part;
|
|
1117
|
-
const text = typeof block.text === 'string' ? block.text : undefined;
|
|
1118
|
-
if (!text) {
|
|
1119
|
-
return part;
|
|
1120
|
-
}
|
|
1121
|
-
const result = extractClockScheduleDirectivesFromText(text);
|
|
1122
|
-
if (result.directives.length) {
|
|
1123
|
-
directives.push(...result.directives);
|
|
1124
|
-
}
|
|
1125
|
-
return { ...block, text: result.next };
|
|
1126
|
-
}
|
|
1127
|
-
return part;
|
|
1128
|
-
});
|
|
1129
|
-
return { directives, next };
|
|
1130
|
-
}
|
|
1131
|
-
return { directives: [], next: content };
|
|
1132
|
-
}
|
|
1133
|
-
let clockMarkerCallSeq = 0;
|
|
1134
|
-
function buildClockMarkerCallId(requestId, index) {
|
|
1135
|
-
clockMarkerCallSeq += 1;
|
|
1136
|
-
const token = String(requestId || 'req').replace(/[^a-zA-Z0-9_-]+/g, '_') || 'req';
|
|
1137
|
-
return `call_clock_marker_${token}_${index + 1}_${clockMarkerCallSeq}`;
|
|
1138
|
-
}
|
|
1139
|
-
function buildClockMarkerScheduleMessages(requestId, markerIndex, marker, payload) {
|
|
1140
|
-
const callId = buildClockMarkerCallId(requestId, markerIndex);
|
|
1141
|
-
const args = {
|
|
1142
|
-
action: 'schedule',
|
|
1143
|
-
items: [
|
|
1144
|
-
{
|
|
1145
|
-
dueAt: marker.dueAt,
|
|
1146
|
-
task: marker.task,
|
|
1147
|
-
...(marker.recurrence ? { recurrence: marker.recurrence } : {})
|
|
1148
|
-
}
|
|
1149
|
-
]
|
|
1150
|
-
};
|
|
1151
|
-
return [
|
|
1152
|
-
{
|
|
1153
|
-
role: 'assistant',
|
|
1154
|
-
content: null,
|
|
1155
|
-
tool_calls: [
|
|
1156
|
-
{
|
|
1157
|
-
id: callId,
|
|
1158
|
-
type: 'function',
|
|
1159
|
-
function: {
|
|
1160
|
-
name: 'clock',
|
|
1161
|
-
arguments: JSON.stringify(args)
|
|
1162
|
-
}
|
|
1163
|
-
}
|
|
1164
|
-
]
|
|
1165
|
-
},
|
|
1166
|
-
{
|
|
1167
|
-
role: 'tool',
|
|
1168
|
-
tool_call_id: callId,
|
|
1169
|
-
name: 'clock',
|
|
1170
|
-
content: JSON.stringify(payload)
|
|
1171
|
-
}
|
|
1172
|
-
];
|
|
1173
|
-
}
|
|
1174
|
-
function stripClockClearDirectiveFromContent(content) {
|
|
1175
|
-
if (typeof content === 'string') {
|
|
1176
|
-
const { hadClear, next } = stripClockClearDirectiveFromText(content);
|
|
1177
|
-
return { hadClear, next };
|
|
1178
|
-
}
|
|
1179
|
-
if (Array.isArray(content)) {
|
|
1180
|
-
let hadClear = false;
|
|
1181
|
-
const next = content.map((part) => {
|
|
1182
|
-
if (typeof part === 'string') {
|
|
1183
|
-
const stripped = stripClockClearDirectiveFromText(part);
|
|
1184
|
-
if (stripped.hadClear)
|
|
1185
|
-
hadClear = true;
|
|
1186
|
-
return stripped.next;
|
|
1187
|
-
}
|
|
1188
|
-
if (part && typeof part === 'object' && !Array.isArray(part)) {
|
|
1189
|
-
const block = part;
|
|
1190
|
-
const text = typeof block.text === 'string' ? block.text : undefined;
|
|
1191
|
-
if (!text)
|
|
1192
|
-
return part;
|
|
1193
|
-
const stripped = stripClockClearDirectiveFromText(text);
|
|
1194
|
-
if (stripped.hadClear)
|
|
1195
|
-
hadClear = true;
|
|
1196
|
-
return { ...block, text: stripped.next };
|
|
1197
|
-
}
|
|
1198
|
-
return part;
|
|
1199
|
-
});
|
|
1200
|
-
return { hadClear, next };
|
|
1201
|
-
}
|
|
1202
|
-
return { hadClear: false, next: content };
|
|
1203
|
-
}
|
|
1204
|
-
function findLastUserMessageIndex(messages) {
|
|
1205
|
-
if (!Array.isArray(messages) || messages.length === 0) {
|
|
1206
|
-
return -1;
|
|
1207
|
-
}
|
|
1208
|
-
for (let idx = messages.length - 1; idx >= 0; idx -= 1) {
|
|
1209
|
-
const candidate = messages[idx];
|
|
1210
|
-
if (candidate && candidate.role === 'user') {
|
|
1211
|
-
return idx;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
return -1;
|
|
1215
|
-
}
|
|
1216
|
-
async function maybeInjectClockRemindersAndApplyDirectives(request, metadata, requestId) {
|
|
1217
|
-
const rt = readRuntimeMetadata(metadata);
|
|
1218
|
-
// Do not inject reminders or apply clock directives during internal servertool followup hops.
|
|
1219
|
-
const allowClockFollowupReminderInjection = rt?.clockFollowupInjectReminders === true;
|
|
1220
|
-
if (rt?.serverToolFollowup === true && !allowClockFollowupReminderInjection) {
|
|
1221
|
-
return request;
|
|
1222
|
-
}
|
|
1223
|
-
const rawConfig = rt?.clock;
|
|
1224
|
-
const clockConfig = resolveClockConfig(rawConfig);
|
|
1225
|
-
if (!clockConfig) {
|
|
1226
|
-
return request;
|
|
1227
|
-
}
|
|
1228
|
-
try {
|
|
1229
|
-
await startClockDaemonIfNeeded(clockConfig);
|
|
1230
|
-
}
|
|
1231
|
-
catch {
|
|
1232
|
-
// best-effort
|
|
1233
|
-
}
|
|
1234
|
-
const sessionId = resolveSessionIdForClock(metadata, request);
|
|
1235
|
-
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
1236
|
-
const lastUserIdx = findLastUserMessageIndex(messages);
|
|
1237
|
-
// 1) Apply <**clock:clear**> directive (latest user message only).
|
|
1238
|
-
let hadClear = false;
|
|
1239
|
-
let clockScheduleDirectives = [];
|
|
1240
|
-
let baseMessages = messages;
|
|
1241
|
-
if (lastUserIdx >= 0) {
|
|
1242
|
-
const lastUser = messages[lastUserIdx];
|
|
1243
|
-
const stripped = stripClockClearDirectiveFromContent(lastUser.content);
|
|
1244
|
-
hadClear = stripped.hadClear;
|
|
1245
|
-
const extracted = extractClockScheduleDirectivesFromContent(stripped.next);
|
|
1246
|
-
clockScheduleDirectives = extracted.directives;
|
|
1247
|
-
if (hadClear || clockScheduleDirectives.length > 0) {
|
|
1248
|
-
baseMessages = messages.slice();
|
|
1249
|
-
baseMessages[lastUserIdx] = { ...lastUser, content: extracted.next };
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
if (hadClear) {
|
|
1253
|
-
if (sessionId) {
|
|
1254
|
-
try {
|
|
1255
|
-
await clearClockSession(sessionId);
|
|
1256
|
-
logClock('cleared', { sessionId });
|
|
1257
|
-
}
|
|
1258
|
-
catch {
|
|
1259
|
-
// best-effort: user directive should not crash request
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
// Continue: still inject per-request time tag (but skip due reminders).
|
|
1263
|
-
}
|
|
1264
|
-
// 2) Apply private schedule directives: <**clock:{time,message}**>
|
|
1265
|
-
// Convert to actual clock scheduling and append synthetic tool_call/tool_result messages
|
|
1266
|
-
// so downstream model sees canonical tool semantics.
|
|
1267
|
-
let markerToolMessages = [];
|
|
1268
|
-
if (clockScheduleDirectives.length > 0) {
|
|
1269
|
-
for (let index = 0; index < clockScheduleDirectives.length; index += 1) {
|
|
1270
|
-
const marker = clockScheduleDirectives[index];
|
|
1271
|
-
const itemBase = {
|
|
1272
|
-
dueAtMs: marker.dueAtMs,
|
|
1273
|
-
setBy: 'user',
|
|
1274
|
-
task: marker.task,
|
|
1275
|
-
...(marker.recurrence ? { recurrence: marker.recurrence } : {})
|
|
1276
|
-
};
|
|
1277
|
-
const now = Date.now();
|
|
1278
|
-
const guardedItem = marker.dueAtMs <= now + clockConfig.dueWindowMs
|
|
1279
|
-
? { ...itemBase, notBeforeRequestId: requestId }
|
|
1280
|
-
: itemBase;
|
|
1281
|
-
if (!sessionId) {
|
|
1282
|
-
markerToolMessages = markerToolMessages.concat(buildClockMarkerScheduleMessages(requestId, index, marker, {
|
|
1283
|
-
ok: false,
|
|
1284
|
-
action: 'schedule',
|
|
1285
|
-
message: 'clock requires session scope (sessionId/conversationId or clockDaemonId).'
|
|
1286
|
-
}));
|
|
1287
|
-
continue;
|
|
1288
|
-
}
|
|
1289
|
-
try {
|
|
1290
|
-
const scheduled = await scheduleClockTasks(sessionId, [guardedItem], clockConfig);
|
|
1291
|
-
markerToolMessages = markerToolMessages.concat(buildClockMarkerScheduleMessages(requestId, index, marker, {
|
|
1292
|
-
ok: true,
|
|
1293
|
-
action: 'schedule',
|
|
1294
|
-
scheduled: scheduled.map((entry) => ({
|
|
1295
|
-
taskId: entry.taskId,
|
|
1296
|
-
dueAt: new Date(entry.dueAtMs).toISOString(),
|
|
1297
|
-
task: entry.task,
|
|
1298
|
-
deliveryCount: entry.deliveryCount
|
|
1299
|
-
}))
|
|
1300
|
-
}));
|
|
1301
|
-
logClock('schedule', { sessionId, count: scheduled.length, source: 'marker' });
|
|
1302
|
-
}
|
|
1303
|
-
catch (error) {
|
|
1304
|
-
markerToolMessages = markerToolMessages.concat(buildClockMarkerScheduleMessages(requestId, index, marker, {
|
|
1305
|
-
ok: false,
|
|
1306
|
-
action: 'schedule',
|
|
1307
|
-
message: `clock.schedule failed: ${error instanceof Error ? error.message : String(error ?? 'unknown')}`
|
|
1308
|
-
}));
|
|
1309
|
-
}
|
|
1310
|
-
}
|
|
1311
|
-
}
|
|
1312
|
-
// 3) Inject due reminders as a user message + attach reservation for response-side commit.
|
|
1313
|
-
let reservation = null;
|
|
1314
|
-
let dueInjectText = '';
|
|
1315
|
-
if (!hadClear && sessionId) {
|
|
1316
|
-
try {
|
|
1317
|
-
const reserved = await reserveDueTasksForRequest({
|
|
1318
|
-
reservationId: `${requestId}:clock`,
|
|
1319
|
-
sessionId,
|
|
1320
|
-
config: clockConfig,
|
|
1321
|
-
requestId
|
|
1322
|
-
});
|
|
1323
|
-
reservation = reserved.reservation;
|
|
1324
|
-
dueInjectText = typeof reserved.injectText === 'string' ? reserved.injectText.trim() : '';
|
|
1325
|
-
}
|
|
1326
|
-
catch {
|
|
1327
|
-
// best-effort
|
|
1328
|
-
reservation = null;
|
|
1329
|
-
dueInjectText = '';
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
const dueUserMessage = (() => {
|
|
1333
|
-
if (!reservation || !dueInjectText)
|
|
1334
|
-
return null;
|
|
1335
|
-
return {
|
|
1336
|
-
role: 'user',
|
|
1337
|
-
content: [
|
|
1338
|
-
'[Clock Reminder]: scheduled tasks are due.',
|
|
1339
|
-
dueInjectText,
|
|
1340
|
-
'You may call tools to complete these tasks. If the tool list is incomplete, standard tools have been injected. MANDATORY: if waiting is needed, use the clock tool to schedule wake-up (clock.schedule) now; do not only promise to wait.'
|
|
1341
|
-
].join('\n')
|
|
1342
|
-
};
|
|
1343
|
-
})();
|
|
1344
|
-
// 4) When we have due tasks, ensure a standard tool set is present (best-effort).
|
|
1345
|
-
let nextRequest = request;
|
|
1346
|
-
if (dueUserMessage) {
|
|
1347
|
-
const ensureToolsOps = buildClockStandardToolsOperations();
|
|
1348
|
-
nextRequest = applyHubOperations(nextRequest, ensureToolsOps);
|
|
1349
|
-
}
|
|
1350
|
-
// 5) Per-request time injection (user time tag or paired clock.get tool result).
|
|
1351
|
-
let snapshot = null;
|
|
1352
|
-
try {
|
|
1353
|
-
snapshot = await getClockTimeSnapshot();
|
|
1354
|
-
}
|
|
1355
|
-
catch {
|
|
1356
|
-
snapshot = null;
|
|
1357
|
-
}
|
|
1358
|
-
const timeTagLine = snapshot
|
|
1359
|
-
? buildTimeTagLine(snapshot)
|
|
1360
|
-
: '[Time/Date]: utc=`1970-01-01T00:00:00.000Z` local=`1970-01-01 00:00:00.000 +00:00` tz=`unknown` nowMs=`0` ntpOffsetMs=`0`';
|
|
1361
|
-
const baseMetadata = nextRequest.metadata && typeof nextRequest.metadata === 'object'
|
|
1362
|
-
? nextRequest.metadata
|
|
1363
|
-
: {
|
|
1364
|
-
originalEndpoint: readString(metadata.originalEndpoint) ?? '/v1/chat/completions'
|
|
1365
|
-
};
|
|
1366
|
-
const withReservationMetadata = dueUserMessage && reservation
|
|
1367
|
-
? {
|
|
1368
|
-
...baseMetadata,
|
|
1369
|
-
__clockReservation: reservation
|
|
1370
|
-
}
|
|
1371
|
-
: baseMetadata;
|
|
1372
|
-
const messagesWithMarkers = markerToolMessages.length > 0
|
|
1373
|
-
? [...baseMessages, ...markerToolMessages]
|
|
1374
|
-
: baseMessages.slice();
|
|
1375
|
-
const messagesWithDue = dueUserMessage ? [...messagesWithMarkers, dueUserMessage] : messagesWithMarkers;
|
|
1376
|
-
// Always inject time via user-role content to keep the tag visible without adding
|
|
1377
|
-
// extra tool-call semantics that may distract the model.
|
|
1378
|
-
//
|
|
1379
|
-
// IMPORTANT: do not append an extra trailing user message, otherwise the Virtual Router
|
|
1380
|
-
// sees `latestMessageFromUser=true` and will force "thinking:user-input" even during
|
|
1381
|
-
// tool followups (last message role=tool), which breaks `coding/search/tools` routing.
|
|
1382
|
-
const timeInjectedMessages = (() => {
|
|
1383
|
-
const idx = findLastUserMessageIndex(messagesWithDue);
|
|
1384
|
-
if (idx < 0) {
|
|
1385
|
-
return [...messagesWithDue, { role: 'user', content: timeTagLine }];
|
|
1386
|
-
}
|
|
1387
|
-
const msg = messagesWithDue[idx];
|
|
1388
|
-
const content = msg.content;
|
|
1389
|
-
const nextContent = (() => {
|
|
1390
|
-
if (typeof content === 'string') {
|
|
1391
|
-
const base = content.trimEnd();
|
|
1392
|
-
return base ? `${base}\n${timeTagLine}` : timeTagLine;
|
|
1393
|
-
}
|
|
1394
|
-
if (Array.isArray(content)) {
|
|
1395
|
-
return [...content, timeTagLine];
|
|
1396
|
-
}
|
|
1397
|
-
return timeTagLine;
|
|
1398
|
-
})();
|
|
1399
|
-
const cloned = messagesWithDue.slice();
|
|
1400
|
-
cloned[idx] = { ...msg, content: nextContent };
|
|
1401
|
-
return cloned;
|
|
1402
|
-
})();
|
|
1403
|
-
return {
|
|
1404
|
-
...nextRequest,
|
|
1405
|
-
messages: timeInjectedMessages,
|
|
1406
|
-
metadata: withReservationMetadata
|
|
1407
|
-
};
|
|
1408
|
-
}
|
|
1409
|
-
function buildClockStandardToolsOperations() {
|
|
1410
|
-
const tools = [
|
|
1411
|
-
{
|
|
1412
|
-
type: 'function',
|
|
1413
|
-
function: {
|
|
1414
|
-
name: 'clock',
|
|
1415
|
-
description: 'Time + Alarm for this session. Use schedule/update/list/cancel/clear. For any wait requirement, call clock.schedule now.',
|
|
1416
|
-
parameters: {
|
|
1417
|
-
type: 'object',
|
|
1418
|
-
properties: {
|
|
1419
|
-
action: {
|
|
1420
|
-
type: 'string',
|
|
1421
|
-
enum: ['get', 'schedule', 'update', 'list', 'cancel', 'clear']
|
|
1422
|
-
},
|
|
1423
|
-
items: {
|
|
1424
|
-
type: 'array',
|
|
1425
|
-
items: {
|
|
1426
|
-
type: 'object',
|
|
1427
|
-
properties: {
|
|
1428
|
-
dueAt: { type: 'string' },
|
|
1429
|
-
task: { type: 'string' },
|
|
1430
|
-
tool: { type: 'string' },
|
|
1431
|
-
arguments: { type: 'string' }
|
|
1432
|
-
},
|
|
1433
|
-
required: ['dueAt', 'task', 'tool', 'arguments'],
|
|
1434
|
-
additionalProperties: false
|
|
1435
|
-
}
|
|
1436
|
-
},
|
|
1437
|
-
taskId: { type: 'string' }
|
|
1438
|
-
},
|
|
1439
|
-
required: ['action', 'items', 'taskId'],
|
|
1440
|
-
additionalProperties: false
|
|
1441
|
-
}
|
|
1442
|
-
}
|
|
1443
|
-
},
|
|
1444
|
-
{
|
|
1445
|
-
type: 'function',
|
|
1446
|
-
function: {
|
|
1447
|
-
name: 'shell',
|
|
1448
|
-
description: 'Runs a shell command and returns its output.',
|
|
1449
|
-
parameters: {
|
|
1450
|
-
type: 'object',
|
|
1451
|
-
properties: {
|
|
1452
|
-
command: { oneOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }] },
|
|
1453
|
-
workdir: { type: 'string' }
|
|
1454
|
-
},
|
|
1455
|
-
required: ['command'],
|
|
1456
|
-
additionalProperties: false
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
},
|
|
1460
|
-
{
|
|
1461
|
-
type: 'function',
|
|
1462
|
-
function: {
|
|
1463
|
-
name: 'exec_command',
|
|
1464
|
-
description: 'Execute a command in a PTY and return output.',
|
|
1465
|
-
parameters: {
|
|
1466
|
-
type: 'object',
|
|
1467
|
-
properties: {
|
|
1468
|
-
cmd: { type: 'string' },
|
|
1469
|
-
workdir: { type: 'string' },
|
|
1470
|
-
timeout_ms: { type: 'number' },
|
|
1471
|
-
max_output_tokens: { type: 'number' },
|
|
1472
|
-
yield_time_ms: { type: 'number' }
|
|
1473
|
-
},
|
|
1474
|
-
required: ['cmd'],
|
|
1475
|
-
additionalProperties: false
|
|
1476
|
-
}
|
|
1477
|
-
}
|
|
1478
|
-
},
|
|
1479
|
-
{
|
|
1480
|
-
type: 'function',
|
|
1481
|
-
function: {
|
|
1482
|
-
name: 'apply_patch',
|
|
1483
|
-
description: 'Apply a patch to repository files.',
|
|
1484
|
-
parameters: {
|
|
1485
|
-
type: 'object',
|
|
1486
|
-
properties: {
|
|
1487
|
-
patch: { type: 'string' }
|
|
1488
|
-
},
|
|
1489
|
-
required: ['patch'],
|
|
1490
|
-
additionalProperties: false
|
|
1491
|
-
}
|
|
1492
|
-
}
|
|
1493
|
-
},
|
|
1494
|
-
{
|
|
1495
|
-
type: 'function',
|
|
1496
|
-
function: {
|
|
1497
|
-
name: 'update_plan',
|
|
1498
|
-
description: 'Update the task plan.',
|
|
1499
|
-
parameters: {
|
|
1500
|
-
type: 'object',
|
|
1501
|
-
properties: {
|
|
1502
|
-
explanation: { type: 'string' },
|
|
1503
|
-
plan: {
|
|
1504
|
-
type: 'array',
|
|
1505
|
-
items: {
|
|
1506
|
-
type: 'object',
|
|
1507
|
-
properties: {
|
|
1508
|
-
step: { type: 'string' },
|
|
1509
|
-
status: { type: 'string' }
|
|
1510
|
-
},
|
|
1511
|
-
required: ['step', 'status'],
|
|
1512
|
-
additionalProperties: false
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
},
|
|
1516
|
-
required: ['plan'],
|
|
1517
|
-
additionalProperties: false
|
|
1518
|
-
}
|
|
1519
|
-
}
|
|
1520
|
-
},
|
|
1521
|
-
{
|
|
1522
|
-
type: 'function',
|
|
1523
|
-
function: {
|
|
1524
|
-
name: 'view_image',
|
|
1525
|
-
description: 'View a local image by file path.',
|
|
1526
|
-
parameters: {
|
|
1527
|
-
type: 'object',
|
|
1528
|
-
properties: {
|
|
1529
|
-
path: { type: 'string' }
|
|
1530
|
-
},
|
|
1531
|
-
required: ['path'],
|
|
1532
|
-
additionalProperties: false
|
|
1533
|
-
}
|
|
1534
|
-
}
|
|
1535
|
-
},
|
|
1536
|
-
{
|
|
1537
|
-
type: 'function',
|
|
1538
|
-
function: {
|
|
1539
|
-
name: 'list_mcp_resources',
|
|
1540
|
-
description: 'List resources exposed by MCP servers.',
|
|
1541
|
-
parameters: {
|
|
1542
|
-
type: 'object',
|
|
1543
|
-
properties: {
|
|
1544
|
-
server: { type: 'string' },
|
|
1545
|
-
filter: { type: 'string' },
|
|
1546
|
-
root: { type: 'string' }
|
|
1547
|
-
},
|
|
1548
|
-
additionalProperties: false
|
|
1549
|
-
}
|
|
1550
|
-
}
|
|
1551
|
-
},
|
|
1552
|
-
{
|
|
1553
|
-
type: 'function',
|
|
1554
|
-
function: {
|
|
1555
|
-
name: 'list_mcp_resource_templates',
|
|
1556
|
-
description: 'List resource templates exposed by MCP servers.',
|
|
1557
|
-
parameters: {
|
|
1558
|
-
type: 'object',
|
|
1559
|
-
properties: {
|
|
1560
|
-
server: { type: 'string' },
|
|
1561
|
-
cursor: { type: 'string' }
|
|
1562
|
-
},
|
|
1563
|
-
additionalProperties: false
|
|
1564
|
-
}
|
|
1565
|
-
}
|
|
1566
|
-
},
|
|
1567
|
-
{
|
|
1568
|
-
type: 'function',
|
|
1569
|
-
function: {
|
|
1570
|
-
name: 'read_mcp_resource',
|
|
1571
|
-
description: 'Read a specific MCP resource by { server, uri }.',
|
|
1572
|
-
parameters: {
|
|
1573
|
-
type: 'object',
|
|
1574
|
-
properties: {
|
|
1575
|
-
server: { type: 'string' },
|
|
1576
|
-
uri: { type: 'string' }
|
|
1577
|
-
},
|
|
1578
|
-
required: ['server', 'uri'],
|
|
1579
|
-
additionalProperties: false
|
|
1580
|
-
}
|
|
1581
|
-
}
|
|
1582
|
-
}
|
|
1583
|
-
];
|
|
1584
|
-
return tools.map((tool) => ({
|
|
1585
|
-
op: 'append_tool_if_missing',
|
|
1586
|
-
toolName: tool.function.name,
|
|
1587
|
-
tool
|
|
1588
|
-
}));
|
|
1589
|
-
}
|
|
1590
|
-
function extractWebSearchSemantics(semantics) {
|
|
1591
|
-
if (!semantics || typeof semantics !== 'object') {
|
|
1592
|
-
return undefined;
|
|
1593
|
-
}
|
|
1594
|
-
const extras = semantics.providerExtras;
|
|
1595
|
-
if (!extras || !isJsonObject(extras)) {
|
|
1596
|
-
return undefined;
|
|
1597
|
-
}
|
|
1598
|
-
const hint = extras.webSearch;
|
|
1599
|
-
if (typeof hint === 'boolean') {
|
|
1600
|
-
return hint ? { force: true } : { disable: true };
|
|
1601
|
-
}
|
|
1602
|
-
if (isJsonObject(hint)) {
|
|
1603
|
-
const normalized = {};
|
|
1604
|
-
if (hint.force === true) {
|
|
1605
|
-
normalized.force = true;
|
|
1606
|
-
}
|
|
1607
|
-
if (hint.disable === true) {
|
|
1608
|
-
normalized.disable = true;
|
|
1609
|
-
}
|
|
1610
|
-
return Object.keys(normalized).length ? normalized : undefined;
|
|
1611
|
-
}
|
|
1612
|
-
return undefined;
|
|
1613
|
-
}
|
|
1614
|
-
function detectWebSearchIntent(request) {
|
|
1615
|
-
const messages = Array.isArray(request.messages) ? request.messages : [];
|
|
1616
|
-
if (!messages.length) {
|
|
1617
|
-
return { hasIntent: false, googlePreferred: false };
|
|
1618
|
-
}
|
|
1619
|
-
// 仅使用“当前请求最后一条消息且角色为 user”的文本判断 web_search 意图:
|
|
1620
|
-
// - 不读取系统提示词(system/developer)。
|
|
1621
|
-
// - 不读取历史 user 消息(避免历史上下文误触发)。
|
|
1622
|
-
const lastMessage = messages[messages.length - 1];
|
|
1623
|
-
if (!lastMessage || lastMessage.role !== 'user') {
|
|
1624
|
-
return { hasIntent: false, googlePreferred: false };
|
|
1625
|
-
}
|
|
1626
|
-
// 支持多模态 content:既可能是纯文本字符串,也可能是带 image_url 的分段数组。
|
|
1627
|
-
let content = '';
|
|
1628
|
-
if (typeof lastMessage.content === 'string') {
|
|
1629
|
-
content = lastMessage.content;
|
|
1630
|
-
}
|
|
1631
|
-
else if (Array.isArray(lastMessage.content)) {
|
|
1632
|
-
const parts = lastMessage.content;
|
|
1633
|
-
const texts = [];
|
|
1634
|
-
for (const part of parts) {
|
|
1635
|
-
if (typeof part === 'string') {
|
|
1636
|
-
texts.push(part);
|
|
1637
|
-
}
|
|
1638
|
-
else if (part && typeof part === 'object') {
|
|
1639
|
-
const maybeText = part.text;
|
|
1640
|
-
if (typeof maybeText === 'string' && maybeText.trim()) {
|
|
1641
|
-
texts.push(maybeText);
|
|
1642
|
-
}
|
|
1643
|
-
}
|
|
1644
|
-
}
|
|
1645
|
-
content = texts.join('\n');
|
|
1646
|
-
}
|
|
1647
|
-
if (!content) {
|
|
1648
|
-
return { hasIntent: false, googlePreferred: false };
|
|
1649
|
-
}
|
|
1650
|
-
// Hard 100% keywords (中文):明确说明“谷歌搜索 / 谷歌一下 / 百度一下”均视为搜索意图。
|
|
1651
|
-
// 其中“谷歌搜索 / 谷歌一下”会偏向 Google/Gemini 搜索后端。
|
|
1652
|
-
const zh = content;
|
|
1653
|
-
const hasGoogleExplicit = zh.includes('谷歌搜索') ||
|
|
1654
|
-
zh.includes('谷歌一下');
|
|
1655
|
-
const hasBaiduExplicit = zh.includes('百度一下');
|
|
1656
|
-
if (hasGoogleExplicit || hasBaiduExplicit) {
|
|
1657
|
-
// 谷歌 / 百度关键字都会优先尝试走“谷歌搜索”引擎;
|
|
1658
|
-
// 只有在 Virtual Router 未配置任何谷歌相关 engine 时,才回退为普通联网搜索。
|
|
1659
|
-
return {
|
|
1660
|
-
hasIntent: true,
|
|
1661
|
-
googlePreferred: true
|
|
1662
|
-
};
|
|
1663
|
-
}
|
|
1664
|
-
// English intent: simple substring match on lowercased text.
|
|
1665
|
-
const text = content.toLowerCase();
|
|
1666
|
-
// 1) Direct patterns like "web search" / "internet search" / "/search".
|
|
1667
|
-
const englishDirect = [
|
|
1668
|
-
'web search',
|
|
1669
|
-
'web_search',
|
|
1670
|
-
'websearch',
|
|
1671
|
-
'internet search',
|
|
1672
|
-
'search the web',
|
|
1673
|
-
'web-search',
|
|
1674
|
-
'internet-search',
|
|
1675
|
-
'/search'
|
|
1676
|
-
];
|
|
1677
|
-
if (englishDirect.some((keyword) => text.includes(keyword))) {
|
|
1678
|
-
return { hasIntent: true, googlePreferred: text.includes('google') };
|
|
1679
|
-
}
|
|
1680
|
-
// 2) Verb + noun combinations, similar to the Chinese rule:
|
|
1681
|
-
// - verb: search / find / look up / look for / google
|
|
1682
|
-
// - noun: web / internet / online / news / information / info / report / reports / article / articles
|
|
1683
|
-
const verbTokensEn = ['search', 'find', 'look up', 'look for', 'google'];
|
|
1684
|
-
const nounTokensEn = [
|
|
1685
|
-
'web',
|
|
1686
|
-
'internet',
|
|
1687
|
-
'online',
|
|
1688
|
-
'news',
|
|
1689
|
-
'information',
|
|
1690
|
-
'info',
|
|
1691
|
-
'report',
|
|
1692
|
-
'reports',
|
|
1693
|
-
'article',
|
|
1694
|
-
'articles'
|
|
1695
|
-
];
|
|
1696
|
-
const hasVerbEn = verbTokensEn.some((token) => text.includes(token));
|
|
1697
|
-
const hasNounEn = nounTokensEn.some((token) => text.includes(token));
|
|
1698
|
-
if (hasVerbEn && hasNounEn) {
|
|
1699
|
-
return { hasIntent: true, googlePreferred: text.includes('google') };
|
|
1700
|
-
}
|
|
1701
|
-
// 中文规则:
|
|
1702
|
-
// 1. 只要文本中包含“上网”,直接命中(例如“帮我上网看看今天的新闻”)。
|
|
1703
|
-
// 2. 否则,如果同时包含「搜索/查找/搜」中的任意一个动词 + 「网络/联网/新闻/信息/报道」中的任意一个名词,也判定为联网搜索意图。
|
|
1704
|
-
const chineseText = content; // 中文大小写不敏感,这里直接用原文。
|
|
1705
|
-
if (chineseText.includes('上网')) {
|
|
1706
|
-
return { hasIntent: true, googlePreferred: false };
|
|
1707
|
-
}
|
|
1708
|
-
const verbTokens = ['搜索', '查找', '搜'];
|
|
1709
|
-
const nounTokens = ['网络', '联网', '新闻', '信息', '报道'];
|
|
1710
|
-
const hasVerb = verbTokens.some((token) => chineseText.includes(token));
|
|
1711
|
-
const hasNoun = nounTokens.some((token) => chineseText.includes(token));
|
|
1712
|
-
if (hasVerb && hasNoun) {
|
|
1713
|
-
return { hasIntent: true, googlePreferred: false };
|
|
1714
|
-
}
|
|
1715
|
-
return { hasIntent: false, googlePreferred: false };
|
|
1716
|
-
}
|