agent-nuvira 3.3.12 → 3.3.14
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 +67 -1
- package/dist/agents/agents/planner.d.ts.map +1 -1
- package/dist/agents/agents/planner.js +9 -0
- package/dist/agents/agents/planner.js.map +1 -1
- package/dist/agents/agents/reasoner.d.ts.map +1 -1
- package/dist/agents/agents/reasoner.js +13 -0
- package/dist/agents/agents/reasoner.js.map +1 -1
- package/dist/agents/orchestrator.d.ts +31 -0
- package/dist/agents/orchestrator.d.ts.map +1 -1
- package/dist/agents/orchestrator.js +103 -135
- package/dist/agents/orchestrator.js.map +1 -1
- package/dist/cli/chat.d.ts +89 -0
- package/dist/cli/chat.d.ts.map +1 -1
- package/dist/cli/chat.js +798 -39
- package/dist/cli/chat.js.map +1 -1
- package/dist/cli/cli-program.d.ts.map +1 -1
- package/dist/cli/cli-program.js +20 -0
- package/dist/cli/cli-program.js.map +1 -1
- package/dist/cli/config.d.ts +12 -0
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +107 -3
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/decisions.d.ts +28 -0
- package/dist/cli/decisions.d.ts.map +1 -0
- package/dist/cli/decisions.js +140 -0
- package/dist/cli/decisions.js.map +1 -0
- package/dist/cli/doctor.d.ts +6 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +46 -0
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/execute.d.ts.map +1 -1
- package/dist/cli/execute.js +6 -0
- package/dist/cli/execute.js.map +1 -1
- package/dist/cli/failover-runner.d.ts.map +1 -1
- package/dist/cli/failover-runner.js +19 -1
- package/dist/cli/failover-runner.js.map +1 -1
- package/dist/cli/feedback.d.ts.map +1 -1
- package/dist/cli/feedback.js +5 -1
- package/dist/cli/feedback.js.map +1 -1
- package/dist/cli/hooks.d.ts +30 -0
- package/dist/cli/hooks.d.ts.map +1 -0
- package/dist/cli/hooks.js +224 -0
- package/dist/cli/hooks.js.map +1 -0
- package/dist/cli/knowledge.d.ts +31 -0
- package/dist/cli/knowledge.d.ts.map +1 -0
- package/dist/cli/knowledge.js +223 -0
- package/dist/cli/knowledge.js.map +1 -0
- package/dist/cli/loop-executor.d.ts.map +1 -1
- package/dist/cli/loop-executor.js +92 -18
- package/dist/cli/loop-executor.js.map +1 -1
- package/dist/cli/model.d.ts +20 -0
- package/dist/cli/model.d.ts.map +1 -1
- package/dist/cli/model.js +294 -14
- package/dist/cli/model.js.map +1 -1
- package/dist/cli/omniroute-control.d.ts +88 -0
- package/dist/cli/omniroute-control.d.ts.map +1 -0
- package/dist/cli/omniroute-control.js +179 -0
- package/dist/cli/omniroute-control.js.map +1 -0
- package/dist/cli/omniroute.d.ts +21 -0
- package/dist/cli/omniroute.d.ts.map +1 -0
- package/dist/cli/omniroute.js +67 -0
- package/dist/cli/omniroute.js.map +1 -0
- package/dist/cli/rate.d.ts +78 -0
- package/dist/cli/rate.d.ts.map +1 -0
- package/dist/cli/rate.js +294 -0
- package/dist/cli/rate.js.map +1 -0
- package/dist/config/process-env.d.ts.map +1 -1
- package/dist/config/process-env.js +10 -0
- package/dist/config/process-env.js.map +1 -1
- package/dist/config/types.d.ts +8 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/context/history.d.ts +21 -0
- package/dist/context/history.d.ts.map +1 -1
- package/dist/context/history.js +50 -0
- package/dist/context/history.js.map +1 -1
- package/dist/enterprise/vault.d.ts.map +1 -1
- package/dist/enterprise/vault.js +5 -21
- package/dist/enterprise/vault.js.map +1 -1
- package/dist/gateway/hook-contract.d.ts +206 -0
- package/dist/gateway/hook-contract.d.ts.map +1 -0
- package/dist/gateway/hook-contract.js +449 -0
- package/dist/gateway/hook-contract.js.map +1 -0
- package/dist/gateway/hooks.d.ts.map +1 -1
- package/dist/gateway/hooks.js +14 -0
- package/dist/gateway/hooks.js.map +1 -1
- package/dist/inference/gemini-adapter.d.ts.map +1 -1
- package/dist/inference/gemini-adapter.js +4 -0
- package/dist/inference/gemini-adapter.js.map +1 -1
- package/dist/inference/interface.d.ts +54 -0
- package/dist/inference/interface.d.ts.map +1 -1
- package/dist/inference/model-id-validation.d.ts +57 -0
- package/dist/inference/model-id-validation.d.ts.map +1 -0
- package/dist/inference/model-id-validation.js +127 -0
- package/dist/inference/model-id-validation.js.map +1 -0
- package/dist/inference/native-tools.d.ts +16 -1
- package/dist/inference/native-tools.d.ts.map +1 -1
- package/dist/inference/native-tools.js +34 -10
- package/dist/inference/native-tools.js.map +1 -1
- package/dist/inference/openai-compat-adapter.d.ts.map +1 -1
- package/dist/inference/openai-compat-adapter.js +12 -1
- package/dist/inference/openai-compat-adapter.js.map +1 -1
- package/dist/inference/provider-catalog.d.ts +11 -0
- package/dist/inference/provider-catalog.d.ts.map +1 -1
- package/dist/inference/provider-catalog.js +80 -0
- package/dist/inference/provider-catalog.js.map +1 -1
- package/dist/inference/route-resolver.d.ts +32 -8
- package/dist/inference/route-resolver.d.ts.map +1 -1
- package/dist/inference/route-resolver.js +175 -1
- package/dist/inference/route-resolver.js.map +1 -1
- package/dist/inference/tool-call-utils.d.ts.map +1 -1
- package/dist/inference/tool-call-utils.js +10 -8
- package/dist/inference/tool-call-utils.js.map +1 -1
- package/dist/inference/tools.d.ts +17 -1
- package/dist/inference/tools.d.ts.map +1 -1
- package/dist/inference/tools.js +69 -19
- package/dist/inference/tools.js.map +1 -1
- package/dist/learning/acceptance-model.d.ts +247 -0
- package/dist/learning/acceptance-model.d.ts.map +1 -0
- package/dist/learning/acceptance-model.js +708 -0
- package/dist/learning/acceptance-model.js.map +1 -0
- package/dist/learning/auto-router.d.ts +121 -24
- package/dist/learning/auto-router.d.ts.map +1 -1
- package/dist/learning/auto-router.js +374 -96
- package/dist/learning/auto-router.js.map +1 -1
- package/dist/learning/autonomy-policy.d.ts +12 -16
- package/dist/learning/autonomy-policy.d.ts.map +1 -1
- package/dist/learning/autonomy-policy.js +50 -48
- package/dist/learning/autonomy-policy.js.map +1 -1
- package/dist/learning/behavioural-labels.d.ts +86 -0
- package/dist/learning/behavioural-labels.d.ts.map +1 -0
- package/dist/learning/behavioural-labels.js +166 -0
- package/dist/learning/behavioural-labels.js.map +1 -0
- package/dist/learning/capability-evidence.d.ts +167 -0
- package/dist/learning/capability-evidence.d.ts.map +1 -0
- package/dist/learning/capability-evidence.js +210 -0
- package/dist/learning/capability-evidence.js.map +1 -0
- package/dist/learning/capability-readiness.d.ts +64 -0
- package/dist/learning/capability-readiness.d.ts.map +1 -0
- package/dist/learning/capability-readiness.js +74 -0
- package/dist/learning/capability-readiness.js.map +1 -0
- package/dist/learning/capability-types.d.ts +169 -0
- package/dist/learning/capability-types.d.ts.map +1 -0
- package/dist/learning/capability-types.js +20 -0
- package/dist/learning/capability-types.js.map +1 -0
- package/dist/learning/catalog-feed.d.ts +89 -0
- package/dist/learning/catalog-feed.d.ts.map +1 -0
- package/dist/learning/catalog-feed.js +211 -0
- package/dist/learning/catalog-feed.js.map +1 -0
- package/dist/learning/cli-intent-effects.d.ts +60 -0
- package/dist/learning/cli-intent-effects.d.ts.map +1 -0
- package/dist/learning/cli-intent-effects.js +153 -0
- package/dist/learning/cli-intent-effects.js.map +1 -0
- package/dist/learning/command-adaptation.d.ts +41 -0
- package/dist/learning/command-adaptation.d.ts.map +1 -0
- package/dist/learning/command-adaptation.js +122 -0
- package/dist/learning/command-adaptation.js.map +1 -0
- package/dist/learning/consent-picture.d.ts +42 -0
- package/dist/learning/consent-picture.d.ts.map +1 -0
- package/dist/learning/consent-picture.js +53 -0
- package/dist/learning/consent-picture.js.map +1 -0
- package/dist/learning/context-budget.d.ts +37 -6
- package/dist/learning/context-budget.d.ts.map +1 -1
- package/dist/learning/context-budget.js +38 -9
- package/dist/learning/context-budget.js.map +1 -1
- package/dist/learning/decision-log.d.ts +114 -0
- package/dist/learning/decision-log.d.ts.map +1 -0
- package/dist/learning/decision-log.js +296 -0
- package/dist/learning/decision-log.js.map +1 -0
- package/dist/learning/deliverable-class.d.ts.map +1 -1
- package/dist/learning/deliverable-class.js +19 -0
- package/dist/learning/deliverable-class.js.map +1 -1
- package/dist/learning/deliverable-corpus.d.ts +117 -0
- package/dist/learning/deliverable-corpus.d.ts.map +1 -0
- package/dist/learning/deliverable-corpus.js +176 -0
- package/dist/learning/deliverable-corpus.js.map +1 -0
- package/dist/learning/error-repair.d.ts +45 -2
- package/dist/learning/error-repair.d.ts.map +1 -1
- package/dist/learning/error-repair.js +95 -0
- package/dist/learning/error-repair.js.map +1 -1
- package/dist/learning/exhaustion-report.d.ts +136 -0
- package/dist/learning/exhaustion-report.d.ts.map +1 -0
- package/dist/learning/exhaustion-report.js +239 -0
- package/dist/learning/exhaustion-report.js.map +1 -0
- package/dist/learning/failure-bookkeeping.d.ts +16 -0
- package/dist/learning/failure-bookkeeping.d.ts.map +1 -1
- package/dist/learning/failure-bookkeeping.js +51 -5
- package/dist/learning/failure-bookkeeping.js.map +1 -1
- package/dist/learning/gate-friction.d.ts +39 -0
- package/dist/learning/gate-friction.d.ts.map +1 -0
- package/dist/learning/gate-friction.js +91 -0
- package/dist/learning/gate-friction.js.map +1 -0
- package/dist/learning/harness-narration.d.ts +34 -0
- package/dist/learning/harness-narration.d.ts.map +1 -0
- package/dist/learning/harness-narration.js +58 -0
- package/dist/learning/harness-narration.js.map +1 -0
- package/dist/learning/hub-skill-catalog.d.ts.map +1 -1
- package/dist/learning/hub-skill-catalog.js +1 -2
- package/dist/learning/hub-skill-catalog.js.map +1 -1
- package/dist/learning/hybrid-router.d.ts +65 -0
- package/dist/learning/hybrid-router.d.ts.map +1 -1
- package/dist/learning/hybrid-router.js +186 -0
- package/dist/learning/hybrid-router.js.map +1 -1
- package/dist/learning/knowledge-base.d.ts +350 -0
- package/dist/learning/knowledge-base.d.ts.map +1 -0
- package/dist/learning/knowledge-base.js +906 -0
- package/dist/learning/knowledge-base.js.map +1 -0
- package/dist/learning/knowledge-eval.d.ts +171 -0
- package/dist/learning/knowledge-eval.d.ts.map +1 -0
- package/dist/learning/knowledge-eval.js +196 -0
- package/dist/learning/knowledge-eval.js.map +1 -0
- package/dist/learning/knowledge-turn.d.ts +92 -0
- package/dist/learning/knowledge-turn.d.ts.map +1 -0
- package/dist/learning/knowledge-turn.js +190 -0
- package/dist/learning/knowledge-turn.js.map +1 -0
- package/dist/learning/learned-commands.d.ts +64 -0
- package/dist/learning/learned-commands.d.ts.map +1 -0
- package/dist/learning/learned-commands.js +108 -0
- package/dist/learning/learned-commands.js.map +1 -0
- package/dist/learning/lexical-search.d.ts +50 -0
- package/dist/learning/lexical-search.d.ts.map +1 -0
- package/dist/learning/lexical-search.js +111 -0
- package/dist/learning/lexical-search.js.map +1 -0
- package/dist/learning/machine-facts.d.ts +71 -0
- package/dist/learning/machine-facts.d.ts.map +1 -0
- package/dist/learning/machine-facts.js +164 -0
- package/dist/learning/machine-facts.js.map +1 -0
- package/dist/learning/markdown-outline.d.ts +73 -0
- package/dist/learning/markdown-outline.d.ts.map +1 -0
- package/dist/learning/markdown-outline.js +136 -0
- package/dist/learning/markdown-outline.js.map +1 -0
- package/dist/learning/model-first-router.d.ts +33 -1
- package/dist/learning/model-first-router.d.ts.map +1 -1
- package/dist/learning/model-first-router.js +123 -9
- package/dist/learning/model-first-router.js.map +1 -1
- package/dist/learning/model-identity.d.ts +80 -0
- package/dist/learning/model-identity.d.ts.map +1 -0
- package/dist/learning/model-identity.js +115 -0
- package/dist/learning/model-identity.js.map +1 -0
- package/dist/learning/model-registry.d.ts +141 -2
- package/dist/learning/model-registry.d.ts.map +1 -1
- package/dist/learning/model-registry.js +324 -38
- package/dist/learning/model-registry.js.map +1 -1
- package/dist/learning/model-scoring.d.ts.map +1 -1
- package/dist/learning/model-scoring.js +8 -1
- package/dist/learning/model-scoring.js.map +1 -1
- package/dist/learning/model-selection.d.ts +2 -1
- package/dist/learning/model-selection.d.ts.map +1 -1
- package/dist/learning/model-selection.js +76 -2
- package/dist/learning/model-selection.js.map +1 -1
- package/dist/learning/outcome-observation.d.ts +49 -0
- package/dist/learning/outcome-observation.d.ts.map +1 -0
- package/dist/learning/outcome-observation.js +95 -0
- package/dist/learning/outcome-observation.js.map +1 -0
- package/dist/learning/pair-entitlement.d.ts +184 -0
- package/dist/learning/pair-entitlement.d.ts.map +1 -0
- package/dist/learning/pair-entitlement.js +214 -0
- package/dist/learning/pair-entitlement.js.map +1 -0
- package/dist/learning/project-docs.d.ts +83 -0
- package/dist/learning/project-docs.d.ts.map +1 -0
- package/dist/learning/project-docs.js +132 -0
- package/dist/learning/project-docs.js.map +1 -0
- package/dist/learning/provider-fallback.d.ts +23 -1
- package/dist/learning/provider-fallback.d.ts.map +1 -1
- package/dist/learning/provider-fallback.js +24 -0
- package/dist/learning/provider-fallback.js.map +1 -1
- package/dist/learning/quota-ledger.d.ts +13 -0
- package/dist/learning/quota-ledger.d.ts.map +1 -1
- package/dist/learning/quota-ledger.js +28 -0
- package/dist/learning/quota-ledger.js.map +1 -1
- package/dist/learning/reasoning-trace.d.ts +65 -1
- package/dist/learning/reasoning-trace.d.ts.map +1 -1
- package/dist/learning/reasoning-trace.js +123 -1
- package/dist/learning/reasoning-trace.js.map +1 -1
- package/dist/learning/requirement-probe.d.ts +55 -0
- package/dist/learning/requirement-probe.d.ts.map +1 -0
- package/dist/learning/requirement-probe.js +77 -0
- package/dist/learning/requirement-probe.js.map +1 -0
- package/dist/learning/resilient-call.d.ts +20 -1
- package/dist/learning/resilient-call.d.ts.map +1 -1
- package/dist/learning/resilient-call.js +42 -4
- package/dist/learning/resilient-call.js.map +1 -1
- package/dist/learning/response-usability.d.ts +85 -0
- package/dist/learning/response-usability.d.ts.map +1 -0
- package/dist/learning/response-usability.js +135 -0
- package/dist/learning/response-usability.js.map +1 -0
- package/dist/learning/router-bandit.d.ts +132 -0
- package/dist/learning/router-bandit.d.ts.map +1 -1
- package/dist/learning/router-bandit.js +253 -0
- package/dist/learning/router-bandit.js.map +1 -1
- package/dist/learning/routing-history.d.ts +41 -2
- package/dist/learning/routing-history.d.ts.map +1 -1
- package/dist/learning/routing-history.js.map +1 -1
- package/dist/learning/run-trace.d.ts +21 -0
- package/dist/learning/run-trace.d.ts.map +1 -1
- package/dist/learning/run-trace.js +38 -0
- package/dist/learning/run-trace.js.map +1 -1
- package/dist/learning/seeded-benchmark.d.ts +14 -0
- package/dist/learning/seeded-benchmark.d.ts.map +1 -1
- package/dist/learning/seeded-benchmark.js +43 -4
- package/dist/learning/seeded-benchmark.js.map +1 -1
- package/dist/learning/session-grant.d.ts +75 -0
- package/dist/learning/session-grant.d.ts.map +1 -0
- package/dist/learning/session-grant.js +112 -0
- package/dist/learning/session-grant.js.map +1 -0
- package/dist/learning/task-model-continuity.d.ts +73 -0
- package/dist/learning/task-model-continuity.d.ts.map +1 -0
- package/dist/learning/task-model-continuity.js +87 -0
- package/dist/learning/task-model-continuity.js.map +1 -0
- package/dist/learning/turn-feedback.d.ts +93 -0
- package/dist/learning/turn-feedback.d.ts.map +1 -0
- package/dist/learning/turn-feedback.js +127 -0
- package/dist/learning/turn-feedback.js.map +1 -0
- package/dist/learning/turn-report.d.ts +104 -2
- package/dist/learning/turn-report.d.ts.map +1 -1
- package/dist/learning/turn-report.js +161 -6
- package/dist/learning/turn-report.js.map +1 -1
- package/dist/learning/verdict-routing.d.ts +51 -0
- package/dist/learning/verdict-routing.d.ts.map +1 -0
- package/dist/learning/verdict-routing.js +122 -0
- package/dist/learning/verdict-routing.js.map +1 -0
- package/dist/mcp/client.d.ts.map +1 -1
- package/dist/mcp/client.js +5 -0
- package/dist/mcp/client.js.map +1 -1
- package/dist/mcp/manager.d.ts +10 -0
- package/dist/mcp/manager.d.ts.map +1 -1
- package/dist/mcp/manager.js +20 -0
- package/dist/mcp/manager.js.map +1 -1
- package/dist/mcp/mcp-discovery.d.ts +34 -0
- package/dist/mcp/mcp-discovery.d.ts.map +1 -0
- package/dist/mcp/mcp-discovery.js +71 -0
- package/dist/mcp/mcp-discovery.js.map +1 -0
- package/dist/mcp/mcp-schema-cache.d.ts +17 -0
- package/dist/mcp/mcp-schema-cache.d.ts.map +1 -1
- package/dist/mcp/mcp-schema-cache.js +47 -3
- package/dist/mcp/mcp-schema-cache.js.map +1 -1
- package/dist/mcp/types.d.ts +28 -0
- package/dist/mcp/types.d.ts.map +1 -1
- package/dist/mcp/types.js.map +1 -1
- package/dist/security/secret-scan.d.ts +156 -0
- package/dist/security/secret-scan.d.ts.map +1 -0
- package/dist/security/secret-scan.js +525 -0
- package/dist/security/secret-scan.js.map +1 -0
- package/dist/tools/ask-user.d.ts.map +1 -1
- package/dist/tools/ask-user.js +4 -0
- package/dist/tools/ask-user.js.map +1 -1
- package/dist/tools/capability-registry.d.ts +150 -0
- package/dist/tools/capability-registry.d.ts.map +1 -0
- package/dist/tools/capability-registry.js +400 -0
- package/dist/tools/capability-registry.js.map +1 -0
- package/dist/tools/child-agent-runtime.d.ts.map +1 -1
- package/dist/tools/child-agent-runtime.js +19 -5
- package/dist/tools/child-agent-runtime.js.map +1 -1
- package/dist/tools/coding-tools.d.ts +17 -5
- package/dist/tools/coding-tools.d.ts.map +1 -1
- package/dist/tools/coding-tools.js +80 -11
- package/dist/tools/coding-tools.js.map +1 -1
- package/dist/tools/command-memo.d.ts +116 -0
- package/dist/tools/command-memo.d.ts.map +1 -0
- package/dist/tools/command-memo.js +202 -0
- package/dist/tools/command-memo.js.map +1 -0
- package/dist/tools/git-tool.d.ts.map +1 -1
- package/dist/tools/git-tool.js +13 -3
- package/dist/tools/git-tool.js.map +1 -1
- package/dist/tools/harness-directive.d.ts +42 -0
- package/dist/tools/harness-directive.d.ts.map +1 -0
- package/dist/tools/harness-directive.js +45 -0
- package/dist/tools/harness-directive.js.map +1 -0
- package/dist/tools/loop-skill-hint.d.ts +74 -174
- package/dist/tools/loop-skill-hint.d.ts.map +1 -1
- package/dist/tools/loop-skill-hint.js +110 -536
- package/dist/tools/loop-skill-hint.js.map +1 -1
- package/dist/tools/mcp-client-tool.d.ts +2 -0
- package/dist/tools/mcp-client-tool.d.ts.map +1 -1
- package/dist/tools/mcp-client-tool.js +3 -0
- package/dist/tools/mcp-client-tool.js.map +1 -1
- package/dist/tools/modality/shared.d.ts +1 -4
- package/dist/tools/modality/shared.d.ts.map +1 -1
- package/dist/tools/modality/shared.js +7 -22
- package/dist/tools/modality/shared.js.map +1 -1
- package/dist/tools/plan-store.d.ts +44 -4
- package/dist/tools/plan-store.d.ts.map +1 -1
- package/dist/tools/plan-store.js +93 -9
- package/dist/tools/plan-store.js.map +1 -1
- package/dist/tools/registry.d.ts +76 -3
- package/dist/tools/registry.d.ts.map +1 -1
- package/dist/tools/registry.js +623 -15
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/remediation-ladder.d.ts +91 -0
- package/dist/tools/remediation-ladder.d.ts.map +1 -0
- package/dist/tools/remediation-ladder.js +316 -0
- package/dist/tools/remediation-ladder.js.map +1 -0
- package/dist/tools/run-cli.d.ts.map +1 -1
- package/dist/tools/run-cli.js +29 -4
- package/dist/tools/run-cli.js.map +1 -1
- package/dist/tools/run-terminal.d.ts +11 -0
- package/dist/tools/run-terminal.d.ts.map +1 -1
- package/dist/tools/run-terminal.js +289 -10
- package/dist/tools/run-terminal.js.map +1 -1
- package/dist/tools/skills-hub.d.ts +7 -0
- package/dist/tools/skills-hub.d.ts.map +1 -1
- package/dist/tools/skills-hub.js +11 -2
- package/dist/tools/skills-hub.js.map +1 -1
- package/dist/tools/step-artifact.d.ts +35 -0
- package/dist/tools/step-artifact.d.ts.map +1 -0
- package/dist/tools/step-artifact.js +154 -0
- package/dist/tools/step-artifact.js.map +1 -0
- package/dist/tools/tool-loop.d.ts +327 -13
- package/dist/tools/tool-loop.d.ts.map +1 -1
- package/dist/tools/tool-loop.js +891 -66
- package/dist/tools/tool-loop.js.map +1 -1
- package/dist/tools/toolsets.js +4 -4
- package/dist/tools/toolsets.js.map +1 -1
- package/dist/tools/worktree.d.ts +11 -2
- package/dist/tools/worktree.d.ts.map +1 -1
- package/dist/tools/worktree.js +11 -2
- package/dist/tools/worktree.js.map +1 -1
- package/dist/utils/atomic-store.d.ts +53 -0
- package/dist/utils/atomic-store.d.ts.map +1 -0
- package/dist/utils/atomic-store.js +178 -0
- package/dist/utils/atomic-store.js.map +1 -0
- package/dist/utils/binary-probe.d.ts +25 -0
- package/dist/utils/binary-probe.d.ts.map +1 -0
- package/dist/utils/binary-probe.js +48 -0
- package/dist/utils/binary-probe.js.map +1 -0
- package/dist/utils/workspace-path.d.ts +50 -0
- package/dist/utils/workspace-path.d.ts.map +1 -0
- package/dist/utils/workspace-path.js +86 -0
- package/dist/utils/workspace-path.js.map +1 -0
- package/dist/web-dashboard/chat-console.d.ts +24 -0
- package/dist/web-dashboard/chat-console.d.ts.map +1 -1
- package/dist/web-dashboard/chat-console.js +38 -0
- package/dist/web-dashboard/chat-console.js.map +1 -1
- package/dist/web-dashboard/project-context.d.ts +27 -2
- package/dist/web-dashboard/project-context.d.ts.map +1 -1
- package/dist/web-dashboard/project-context.js +32 -2
- package/dist/web-dashboard/project-context.js.map +1 -1
- package/dist/web-dashboard/server.d.ts +40 -0
- package/dist/web-dashboard/server.d.ts.map +1 -1
- package/dist/web-dashboard/server.js +733 -15
- package/dist/web-dashboard/server.js.map +1 -1
- package/dist/web-dashboard/src/types.d.ts +236 -1
- package/dist/web-dashboard/src/types.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.d.ts +18 -2
- package/dist/web-dashboard/workspace-guard.d.ts.map +1 -1
- package/dist/web-dashboard/workspace-guard.js +94 -12
- package/dist/web-dashboard/workspace-guard.js.map +1 -1
- package/dist/web-dashboard/workspace-resolution.d.ts +87 -0
- package/dist/web-dashboard/workspace-resolution.d.ts.map +1 -0
- package/dist/web-dashboard/workspace-resolution.js +105 -0
- package/dist/web-dashboard/workspace-resolution.js.map +1 -0
- package/package.json +1 -1
- package/src/web-dashboard/public/assets/index-tK8Y2qHB.css +1 -0
- package/src/web-dashboard/public/assets/index-zjbdJnO-.js +210 -0
- package/src/web-dashboard/public/assets/index-zjbdJnO-.js.map +1 -0
- package/src/web-dashboard/public/index.html +2 -2
- package/src/web-dashboard/public/assets/index-BrZcYhy6.js +0 -207
- package/src/web-dashboard/public/assets/index-BrZcYhy6.js.map +0 -1
- package/src/web-dashboard/public/assets/index-DsWczTa6.css +0 -1
package/dist/cli/chat.js
CHANGED
|
@@ -5,13 +5,14 @@ import { Command } from 'commander';
|
|
|
5
5
|
import inquirer from 'inquirer';
|
|
6
6
|
import { BaseCommand } from './commands.js';
|
|
7
7
|
import { resolveProvider } from './router.js';
|
|
8
|
-
import { resolveRoute } from '../inference/route-resolver.js';
|
|
8
|
+
import { resolveRoute, strictModelMode, strictPinRefusal } from '../inference/route-resolver.js';
|
|
9
9
|
import { showModelPicker } from './model-picker.js';
|
|
10
10
|
import { ContextParser } from '../context/parser.js';
|
|
11
11
|
import { getCache } from '../context/cache.js';
|
|
12
12
|
import { assembleContext, retrievalOptionsFromConfig, recordRetrievalStats } from '../learning/retrieval.js';
|
|
13
13
|
import { getChatHistory } from '../context/history.js';
|
|
14
14
|
import { maybeAutoRecall, recallContextBlock } from '../context/session-recall.js';
|
|
15
|
+
import { decisionsRecordingEnabled, recallDecisionBlock } from '../learning/decision-log.js';
|
|
15
16
|
import { getMemoryManager } from '../memory/manager.js';
|
|
16
17
|
import { logger } from '../utils/logger.js';
|
|
17
18
|
import { printOrchestrationResult } from './execute.js';
|
|
@@ -24,12 +25,17 @@ import { closeResume, openResume, resolveResumeRequest, } from '../learning/step
|
|
|
24
25
|
import { applyActiveModel } from './model.js';
|
|
25
26
|
import { capabilityReasoningEffort, isMaxCapability } from '../config/capability-mode.js';
|
|
26
27
|
import { getProviderFallback, classifyFallbackError, isRetryableError, isTransientForRetry, recordRegistrySuccess } from '../learning/provider-fallback.js';
|
|
27
|
-
import { recordActionFailure } from '../learning/failure-bookkeeping.js';
|
|
28
|
+
import { recordActionFailure, } from '../learning/failure-bookkeeping.js';
|
|
29
|
+
import { getRouterBandit } from '../learning/router-bandit.js';
|
|
30
|
+
import { UnusableModelResponseError } from '../learning/response-usability.js';
|
|
28
31
|
import { resolveThreadBudgetChars } from '../learning/context-budget.js';
|
|
29
|
-
import { getAutoRouter, isAutoModel, isAutoProvider, governanceVerdict } from '../learning/auto-router.js';
|
|
32
|
+
import { getAutoRouter, isAgentAutoRoute, isAutoModel, isAutoProvider, governanceVerdict, adminBudgetVerdict } from '../learning/auto-router.js';
|
|
30
33
|
import { continuationSoftwareText } from '../learning/continuation-intent.js';
|
|
34
|
+
import { TaskModelContinuity, taskSignature } from '../learning/task-model-continuity.js';
|
|
35
|
+
import { describeFailureKind, renderExhaustionReport, } from '../learning/exhaustion-report.js';
|
|
31
36
|
import { estimateTokens } from '../learning/cost-tracker.js';
|
|
32
37
|
import { getModelRegistry } from '../learning/model-registry.js';
|
|
38
|
+
import { costSince } from '../learning/cost-tracker.js';
|
|
33
39
|
import { refreshModelRegistry } from '../inference/model-probe.js';
|
|
34
40
|
import { startWarmupDaemon } from '../learning/model-warmup.js';
|
|
35
41
|
import { recordRoutingDecision } from '../learning/routing-history.js';
|
|
@@ -47,12 +53,15 @@ import { runToolLoop, extractFallbackToolCalls } from '../tools/tool-loop.js';
|
|
|
47
53
|
import { flushSpans, otelNoticeOnce, startTurnSpan } from '../observability/otel.js';
|
|
48
54
|
import { detectAnswerQualityFailure, answerQualityError, toUserFacingGenerationError, isToolCallingUnsupported, stripToolCallArtifacts, } from '../inference/tool-call-utils.js';
|
|
49
55
|
import { beginTrace, endTrace, recordStep, recordTraceEvent, recordTraceFindings, recordTurnReport, buildTraceOutcome, traceOutcomeSucceeded } from '../learning/reasoning-trace.js';
|
|
50
|
-
import { recordWorkingState, getWorkingState, formatWorkingState, isProjectLedgerDir } from '../learning/working-state.js';
|
|
56
|
+
import { recordWorkingState, getWorkingState, formatWorkingState, isProjectLedgerDir, detectRegressionSignal } from '../learning/working-state.js';
|
|
57
|
+
import { recordDeliverableCandidate, markLastDeliverableRejected } from '../learning/deliverable-corpus.js';
|
|
58
|
+
import { deriveAndApplyBehaviouralLabel } from '../learning/behavioural-labels.js';
|
|
51
59
|
import { getLoopExposureMode } from '../tools/toolsets.js';
|
|
52
60
|
import { resolveModelHarnessProfile, shouldSkipNativeTools, isAgenticCapableModel } from '../learning/model-harness.js';
|
|
53
61
|
import { assertAgenticRoute, setWeakModelConsent, resolveWeakModelPolicy, weakRouteNotice } from '../learning/agentic-route-gate.js';
|
|
54
62
|
import { resolvePromptBudget, measurePromptBudget, formatPromptBudgetBreakdown } from '../learning/prompt-budget.js';
|
|
55
63
|
import { buildTurnReport, formatTurnReport } from '../learning/turn-report.js';
|
|
64
|
+
import { turnOutcomeObservation } from '../learning/outcome-observation.js';
|
|
56
65
|
import { resolveAdapterDefault, hasCredentials } from '../learning/model-selection.js';
|
|
57
66
|
import { buildLoopProjectContext } from '../tools/loop-project-context.js';
|
|
58
67
|
import { sweepTransientFailures, collectionRevivalStore } from '../learning/provider-revival.js';
|
|
@@ -360,7 +369,10 @@ export function turnCarriesHonestyFlag(result) {
|
|
|
360
369
|
result.unverifiedBuildClaim ||
|
|
361
370
|
result.unverifiedEdit ||
|
|
362
371
|
result.unverifiedEditClaim ||
|
|
363
|
-
result.noActionTaken
|
|
372
|
+
result.noActionTaken ||
|
|
373
|
+
result.artifactIncomplete ||
|
|
374
|
+
result.unverifiedFileClaim ||
|
|
375
|
+
result.artifactShortfall);
|
|
364
376
|
}
|
|
365
377
|
// ─── ChatCommand ────────────────────────────────────────────────────────────
|
|
366
378
|
export class ChatCommand extends BaseCommand {
|
|
@@ -391,6 +403,40 @@ export class ChatCommand extends BaseCommand {
|
|
|
391
403
|
* rate-limited (a genuinely shared limit).
|
|
392
404
|
*/
|
|
393
405
|
sessionFailedModels = new Map();
|
|
406
|
+
/**
|
|
407
|
+
* P3 (fix_model_routing) — task-level model CONTINUITY.
|
|
408
|
+
*
|
|
409
|
+
* The pair that produced usable work for a task is remembered per task
|
|
410
|
+
* signature, and the next turn of the SAME task (a "resume"/"continue" whose
|
|
411
|
+
* routing text resolves to the same ask) offers it FIRST. Routing re-decides
|
|
412
|
+
* every turn, and a signal-free continuation re-decided from scratch is how a
|
|
413
|
+
* working run degraded onto a model that then answered with nothing.
|
|
414
|
+
*
|
|
415
|
+
* A preference, never a pin: the recalled pair goes through the SAME
|
|
416
|
+
* credential/exclusion/registry gates as any other candidate, and the normal
|
|
417
|
+
* pool stands behind it — so a model that has since died costs one check.
|
|
418
|
+
*/
|
|
419
|
+
taskContinuity = new TaskModelContinuity();
|
|
420
|
+
/** The task signature of the turn being routed (see routeMessageAuto). */
|
|
421
|
+
lastTaskSignature;
|
|
422
|
+
/**
|
|
423
|
+
* P4 (fix_model_routing) — what THIS turn actually tried, and why each
|
|
424
|
+
* candidate did not answer. Reset per turn; consumed by the exhaustion report
|
|
425
|
+
* when a turn ends without an answer. Chat's own failover walk is the
|
|
426
|
+
* authority here (the shared walk in `resilient-call.ts` keeps its own log for
|
|
427
|
+
* the gateway/orchestrator paths), so the report can name the models the user
|
|
428
|
+
* was actually served by — not a pool that was considered.
|
|
429
|
+
*/
|
|
430
|
+
turnAttempts = [];
|
|
431
|
+
/** Record one failed/skipped attempt for this turn's exhaustion report. */
|
|
432
|
+
recordTurnAttempt(attempt) {
|
|
433
|
+
try {
|
|
434
|
+
this.turnAttempts.push(attempt);
|
|
435
|
+
}
|
|
436
|
+
catch {
|
|
437
|
+
// Best-effort — telemetry must never break a turn.
|
|
438
|
+
}
|
|
439
|
+
}
|
|
394
440
|
// RATE_LIMIT_EXCLUSION_MS + TRANSIENT_FAILURE_EXCLUSION_MS now live in
|
|
395
441
|
// src/learning/failure-bookkeeping.ts (shared with every action) — see
|
|
396
442
|
// recordActionFailure. Behavior is identical: same values, same semantics.
|
|
@@ -478,7 +524,10 @@ export class ChatCommand extends BaseCommand {
|
|
|
478
524
|
// Best-effort — an unreadable config leaves the previous behavior.
|
|
479
525
|
}
|
|
480
526
|
}
|
|
481
|
-
|
|
527
|
+
// A8 — a CONCRETE provider with `-m auto` means that provider's own auto
|
|
528
|
+
// (e.g. an OmniRoute combo), NOT our auto-route. isAgentAutoRoute excludes
|
|
529
|
+
// that case so the pin reaches the provider instead of being re-routed.
|
|
530
|
+
let autoMode = isAgentAutoRoute(mergedOpts.provider, mergedOpts.model);
|
|
482
531
|
let { type, provider } = autoMode
|
|
483
532
|
? await this.getProvider({})
|
|
484
533
|
: await this.getProvider(mergedOpts);
|
|
@@ -486,6 +535,11 @@ export class ChatCommand extends BaseCommand {
|
|
|
486
535
|
// B — captured from the route so the post-route capability gate below can
|
|
487
536
|
// judge the FINAL pair without re-resolving anything.
|
|
488
537
|
let routeVerdict;
|
|
538
|
+
// C6 — the turn's economy window, taken before the FIRST provider call. The
|
|
539
|
+
// turn report states what THIS turn cost from the persisted ledger; a
|
|
540
|
+
// continuation or a resumed turn runs in a fresh process, so a session
|
|
541
|
+
// counter would report zero for it (`costSince` reads timestamps instead).
|
|
542
|
+
const turnCostSince = Date.now();
|
|
489
543
|
let routedText;
|
|
490
544
|
if (autoMode) {
|
|
491
545
|
// Intent-aware escalation: a bare "yes"/"do it" continuing software work
|
|
@@ -606,9 +660,27 @@ export class ChatCommand extends BaseCommand {
|
|
|
606
660
|
recallBlock = undefined;
|
|
607
661
|
}
|
|
608
662
|
}
|
|
663
|
+
// Decision recall — the SAME attached project's recorded decisions
|
|
664
|
+
// (`.nuvira/decisions.jsonl`), when this ask actually mentions one. It is
|
|
665
|
+
// ADVISORY: it is injected as context and NEVER suppresses an `ask_user`, so
|
|
666
|
+
// a situation that has changed can still ask. Best-effort: an empty log (or
|
|
667
|
+
// no matching token) injects nothing, and a read failure never breaks the
|
|
668
|
+
// turn. Gated by `decisionsRecordingEnabled()` so a test-run turn is inert
|
|
669
|
+
// unless it explicitly opts in (NUVIRA_DECISION_LOG=on).
|
|
670
|
+
let decisionBlock;
|
|
671
|
+
if (opts.projectPath && decisionsRecordingEnabled()) {
|
|
672
|
+
try {
|
|
673
|
+
const block = recallDecisionBlock(opts.projectPath, message, 3);
|
|
674
|
+
if (block)
|
|
675
|
+
decisionBlock = block;
|
|
676
|
+
}
|
|
677
|
+
catch {
|
|
678
|
+
decisionBlock = undefined;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
609
681
|
const parsed = parseRequestSync(message);
|
|
610
682
|
const dispatchDecision = resolvePipelineDispatch(parsed, { dev: opts.dev, text: message });
|
|
611
|
-
const answer = await this.runChatAnswer(message, opts.history ?? [], { type, provider, model }, { provider: mergedOpts.provider, model: mergedOpts.model, dev: mergedOpts.dev, cache: true }, true, { auto: autoMode }, parsed, { askUser: opts.askUser, onProgress: opts.onProgress, onToolCall: opts.onToolCall, onPlanChange: opts.onPlanChange, onGitDiff: opts.onGitDiff, onSkillDraft: opts.onSkillDraft, onFinding: opts.onFinding, planStore: opts.planStore ?? this.planStore, gateway: opts.gateway, projectContext: opts.projectContext, recallContext: recallBlock, projectPath: opts.projectPath, onToken: opts.onToken, signal: opts.signal, continuation: opts.continuation, systemPolicy: opts.systemPolicy, debugSurface: opts.debugSurface ?? 'cli-chat', debugSession: opts.debugSession, worktree: opts.worktree, keepWorktree: opts.keepWorktree, resume: opts.resume });
|
|
683
|
+
const answer = await this.runChatAnswer(message, opts.history ?? [], { type, provider, model }, { provider: mergedOpts.provider, model: mergedOpts.model, dev: mergedOpts.dev, cache: true }, true, { auto: autoMode }, parsed, { askUser: opts.askUser, onProgress: opts.onProgress, onToolCall: opts.onToolCall, onPlanChange: opts.onPlanChange, onGitDiff: opts.onGitDiff, onSkillDraft: opts.onSkillDraft, onFinding: opts.onFinding, planStore: opts.planStore ?? this.planStore, gateway: opts.gateway, projectContext: opts.projectContext, recallContext: recallBlock, decisionContext: decisionBlock, projectPath: opts.projectPath, onToken: opts.onToken, signal: opts.signal, continuation: opts.continuation, systemPolicy: opts.systemPolicy, debugSurface: opts.debugSurface ?? 'cli-chat', debugSession: opts.debugSession, worktree: opts.worktree, keepWorktree: opts.keepWorktree, resume: opts.resume });
|
|
612
684
|
// No-model fallback: the tool loop could not generate a single response
|
|
613
685
|
// AND the rules assessed a high-confidence pipeline intent — run the
|
|
614
686
|
// pipeline directly (rules decide only when the model is unavailable; the
|
|
@@ -619,6 +691,15 @@ export class ChatCommand extends BaseCommand {
|
|
|
619
691
|
// dispatching it here would run the ask on another engine entirely — the one
|
|
620
692
|
// path that can run it UNISOLATED while the caller asked for isolation.
|
|
621
693
|
if (answer.generationFailed && !answer.refused && dispatchDecision.dispatch && !dispatchDecision.needConfirm) {
|
|
694
|
+
// E2 — this CHANGES THE EXECUTION MODEL: the ask leaves the single chat tool
|
|
695
|
+
// loop and runs as a multi-agent pipeline. Falling back is legitimate
|
|
696
|
+
// (nothing was generated), but it must never be silent. A pinned ask that
|
|
697
|
+
// flips engines without saying so is the same defect as an unattended
|
|
698
|
+
// auto-pick reported as the user's own choice: the user is owed the reason
|
|
699
|
+
// and the fact that their pin is the pair being carried over.
|
|
700
|
+
opts.onProgress?.(` ⤵️ the chat turn produced no answer on ${provider.name}${model ? ` / ${model}` : ''} — ` +
|
|
701
|
+
`running this ask in the multi-agent pipeline instead (a different execution model` +
|
|
702
|
+
`${autoMode ? '' : ', on the same pinned pair'})`);
|
|
622
703
|
const r = await runPipelineTool(message, this.configManager, { provider: type, model, board: false });
|
|
623
704
|
// `success`, not `error`: a pipeline that RAN and failed reports its
|
|
624
705
|
// outcome in `summary` and only sometimes sets `error`, so keying off
|
|
@@ -664,6 +745,16 @@ export class ChatCommand extends BaseCommand {
|
|
|
664
745
|
successfulToolCalls: answer.successfulToolCalls,
|
|
665
746
|
mutations: answer.runTrace?.mutations,
|
|
666
747
|
changedPaths: answer.runTrace?.paths,
|
|
748
|
+
// E1 — decisions the harness took on the user's behalf (an unattended
|
|
749
|
+
// ask_user default), so the report discloses them instead of letting the
|
|
750
|
+
// answer read as a choice the user made.
|
|
751
|
+
assumptions: answer.runTrace?.assumptions,
|
|
752
|
+
// C6 — what the turn cost, from the ledger, so an expensive turn is visible
|
|
753
|
+
// where the turn is described instead of only in a session total.
|
|
754
|
+
cost: (() => {
|
|
755
|
+
const spend = costSince(turnCostSince);
|
|
756
|
+
return { usd: spend.costUsd, tokens: spend.tokens, calls: spend.requests };
|
|
757
|
+
})(),
|
|
667
758
|
flags: {
|
|
668
759
|
unverifiedActionClaim: answer.unverifiedActionClaim,
|
|
669
760
|
unverifiedEdit: answer.unverifiedEdit,
|
|
@@ -672,6 +763,15 @@ export class ChatCommand extends BaseCommand {
|
|
|
672
763
|
undeliveredArtifact: answer.undeliveredArtifact,
|
|
673
764
|
unfulfilledPromise: answer.unfulfilledPromise,
|
|
674
765
|
noActionTaken: answer.noActionTaken,
|
|
766
|
+
// Bundle 19 — the artifact's own admission travels to the report, so the
|
|
767
|
+
// turn cannot read as finished work on any surface that shows it.
|
|
768
|
+
incompleteArtifactClaim: Boolean(answer.artifactIncomplete),
|
|
769
|
+
// Bundle 20 — a fabricated "I wrote the file" reaches the report for the
|
|
770
|
+
// same reason: no surface may read such a turn as finished work.
|
|
771
|
+
unverifiedFileClaim: Boolean(answer.unverifiedFileClaim),
|
|
772
|
+
// Bundle 23 — a file far short of the stated magnitude is the same:
|
|
773
|
+
// the delivered artifact is not what was asked for.
|
|
774
|
+
artifactShortfall: Boolean(answer.artifactShortfall),
|
|
675
775
|
},
|
|
676
776
|
});
|
|
677
777
|
}
|
|
@@ -682,6 +782,109 @@ export class ChatCommand extends BaseCommand {
|
|
|
682
782
|
// reviewable after the fact (the Trace tab renders it), not only in this
|
|
683
783
|
// turn's return value. Best-effort: a trace write never breaks a turn.
|
|
684
784
|
recordTurnReport(answer.traceId, turnReport);
|
|
785
|
+
// The user's message for THIS turn is also a verdict on the PREVIOUS one
|
|
786
|
+
// ("still broken", "no change", or a near-identical re-ask). It is resolved
|
|
787
|
+
// BEFORE this turn's own delivery is recorded, because these signals judge the
|
|
788
|
+
// PREVIOUS turn — run after the record below, they would label the row this very
|
|
789
|
+
// turn just created. Derived ONCE here, because two independent consumers need
|
|
790
|
+
// the same answer: the bandit's `userAccepted` and the deliverable corpus's
|
|
791
|
+
// label. Neither trusts the model's own account of itself.
|
|
792
|
+
const userReportsRegression = detectRegressionSignal(message);
|
|
793
|
+
if (userReportsRegression) {
|
|
794
|
+
// Label the previous authored delivery as a miss. Independent of routing:
|
|
795
|
+
// the corpus is collected whether or not the bandit is on.
|
|
796
|
+
try {
|
|
797
|
+
markLastDeliverableRejected();
|
|
798
|
+
}
|
|
799
|
+
catch {
|
|
800
|
+
// Collection is best-effort.
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
// Tier 3 — the verdict the harness can infer from BEHAVIOUR, when the user did
|
|
804
|
+
// not report a regression: a re-ask or a hand-edited artifact is a rejection, an
|
|
805
|
+
// untouched artifact the user then REFERENCES is a (weak) acceptance. Recorded
|
|
806
|
+
// `source: 'derived'` so a fit can weigh it below an explicit verdict. Stands
|
|
807
|
+
// aside when a regression was reported — that path already owns the label.
|
|
808
|
+
if (!userReportsRegression) {
|
|
809
|
+
try {
|
|
810
|
+
deriveAndApplyBehaviouralLabel({ newAsk: message, reportsRegression: false });
|
|
811
|
+
}
|
|
812
|
+
catch {
|
|
813
|
+
// Collection is best-effort.
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
// Item 13 (round 3) — COLLECT, never judge. An authored artifact that met the
|
|
817
|
+
// ask's stated magnitude and did not admit its own omission is the "fluent,
|
|
818
|
+
// on-length" delivery no rule can score; the corpus records it so that a
|
|
819
|
+
// LATER correction can label it. This is a dataset, not a detector: nothing
|
|
820
|
+
// reads it, and no score is derived from it. Recorded LAST so the verdict-on-
|
|
821
|
+
// previous above never sees this turn's own row.
|
|
822
|
+
if (answer.authoredDeliverable) {
|
|
823
|
+
try {
|
|
824
|
+
recordDeliverableCandidate({
|
|
825
|
+
ask: message,
|
|
826
|
+
path: answer.authoredDeliverable.path,
|
|
827
|
+
deliveredWords: answer.authoredDeliverable.deliveredWords,
|
|
828
|
+
...(answer.authoredDeliverable.targetWords !== undefined
|
|
829
|
+
? { targetWords: answer.authoredDeliverable.targetWords }
|
|
830
|
+
: {}),
|
|
831
|
+
...(turnReport?.verification ? { verification: turnReport.verification } : {}),
|
|
832
|
+
// The trace link is what lets an EXPLICIT verdict (`nuvira rate`) label
|
|
833
|
+
// THIS delivery rather than "the most recent one".
|
|
834
|
+
...(answer.traceId ? { traceId: answer.traceId } : {}),
|
|
835
|
+
excerpt: answer.authoredDeliverable.excerpt,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
catch {
|
|
839
|
+
// Collection is best-effort — never break a turn on a corpus write.
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
// Feed the MEASURED outcome back into the bandit on the CHAT path too. The
|
|
843
|
+
// orchestrator has done this for pipeline tasks all along; the chat turn —
|
|
844
|
+
// the product's most common entry point — never did, so every turn's real
|
|
845
|
+
// result was thrown away and the router learned from only one of its two
|
|
846
|
+
// execution models. The signal is the turn report's derived VERIFICATION
|
|
847
|
+
// verdict, never the model's own account of itself.
|
|
848
|
+
//
|
|
849
|
+
// Guarded exactly like the orchestrator: only when this turn was AUTO-routed
|
|
850
|
+
// (otherwise `getLastProvider` could reward a stale provider noted by an
|
|
851
|
+
// earlier auto run in this process) and only when bandit learning is on.
|
|
852
|
+
if (autoMode && this.configManager.getAll().routing?.bandit !== false) {
|
|
853
|
+
// `userAccepted: false` for the turn that just ran, applied BEFORE this
|
|
854
|
+
// turn's own outcome is recorded (recording overwrites the attribution the
|
|
855
|
+
// correction refers to). It is walked up to the previous AUTO-routed turn
|
|
856
|
+
// only — `recordUserRejection` no-ops when nothing is pending.
|
|
857
|
+
if (userReportsRegression) {
|
|
858
|
+
try {
|
|
859
|
+
getAutoRouter().recordUserRejection('chat');
|
|
860
|
+
}
|
|
861
|
+
catch {
|
|
862
|
+
// Learning is best-effort — never break a turn on a bandit error.
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
const observation = turnOutcomeObservation(turnReport);
|
|
866
|
+
if (observation) {
|
|
867
|
+
try {
|
|
868
|
+
getAutoRouter().recordOutcome('chat', message, observation.outcome, this.configManager, observation.outcomeData);
|
|
869
|
+
}
|
|
870
|
+
catch {
|
|
871
|
+
// Learning is best-effort — never break a turn on a bandit error.
|
|
872
|
+
}
|
|
873
|
+
// Bundle 3b: the SAME derived verdict also feeds the capability
|
|
874
|
+
// scorecard's accuracy parameter for the pair that actually served the
|
|
875
|
+
// turn. One translation of "did the work verify", two consumers — rather
|
|
876
|
+
// than a second, parallel notion of success that could drift from it.
|
|
877
|
+
// `blocked`/`not-applicable` write nothing (see the registry method).
|
|
878
|
+
try {
|
|
879
|
+
if (type && model) {
|
|
880
|
+
getModelRegistry().recordCapabilityEvidence(type, model, turnReport?.verification ?? 'not-applicable');
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
// Learning is best-effort — never break a turn on a scorecard write.
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
}
|
|
685
888
|
// E3 — surface a non-trivial report on the console. A plain answer (no
|
|
686
889
|
// plan, nothing changed) produces no summary and stays silent.
|
|
687
890
|
if (turnReport?.summary) {
|
|
@@ -701,6 +904,14 @@ export class ChatCommand extends BaseCommand {
|
|
|
701
904
|
unfulfilledPromise: answer.unfulfilledPromise,
|
|
702
905
|
undeliveredArtifact: answer.undeliveredArtifact,
|
|
703
906
|
unverifiedBuildClaim: answer.unverifiedBuildClaim,
|
|
907
|
+
// Bundle 19/20 — the reliability flags must not be dropped at this
|
|
908
|
+
// boundary, or a caller re-deriving the verdict would see a clean turn.
|
|
909
|
+
artifactIncomplete: answer.artifactIncomplete,
|
|
910
|
+
unverifiedFileClaim: answer.unverifiedFileClaim,
|
|
911
|
+
artifactShortfall: answer.artifactShortfall,
|
|
912
|
+
// Item 13 (round 3) — the collectable "fluent, on-length" delivery travels to
|
|
913
|
+
// the caller, which records it. Facts only; no verdict is attached here.
|
|
914
|
+
authoredDeliverable: answer.authoredDeliverable,
|
|
704
915
|
// WS1 — the findings this turn recorded (empty when it recorded none).
|
|
705
916
|
findings: answer.findings ?? [],
|
|
706
917
|
provider: type,
|
|
@@ -739,8 +950,8 @@ export class ChatCommand extends BaseCommand {
|
|
|
739
950
|
.description('Start an interactive chat session with AI')
|
|
740
951
|
.argument('[prompt]', 'Optional initial prompt')
|
|
741
952
|
.option('-f, --file <path>', 'Include file content as context')
|
|
742
|
-
.option('-p, --provider <provider>', 'Inference provider')
|
|
743
|
-
.option('-m, --model <model>', 'Model to
|
|
953
|
+
.option('-p, --provider <provider>', 'Inference provider to pin this chat to')
|
|
954
|
+
.option('-m, --model <model>', 'Model to pin this chat to (if omitted, an interactive picker will appear)')
|
|
744
955
|
.option('--no-cache', 'Disable response caching')
|
|
745
956
|
.option('-d, --dev', 'Always dispatch requests to the coding pipeline (no confirmation)', false)
|
|
746
957
|
// WS5 (#27) — isolation and partial resume, as the two things an operator
|
|
@@ -756,6 +967,12 @@ export class ChatCommand extends BaseCommand {
|
|
|
756
967
|
.option('--worktree', 'Run this turn in its own git worktree of the project and report the diff against the base commit. Refuses rather than running unisolated when the directory cannot be isolated (also asked for by NUVIRA_ISOLATE=1)')
|
|
757
968
|
.option('--keep-worktree', 'Keep the isolated worktree after the turn instead of removing it')
|
|
758
969
|
.option('--resume [id]', 'Replay the recorded steps of this ask whose input is unchanged instead of paying for them again (defaults to the record for this goal + directory; also asked for by NUVIRA_RESUME=1)')
|
|
970
|
+
.addHelpText('after', '\nModel routing:\n' +
|
|
971
|
+
' Pinning -p/--provider (and -m/--model) does NOT stop the router from falling\n' +
|
|
972
|
+
' over to another model if the pinned one is unavailable — auto routing takes\n' +
|
|
973
|
+
' over and the turn says so. To work with the pinned model ONLY, set\n' +
|
|
974
|
+
' NUVIRA_STRICT_MODEL=1: a dead pin then fails with a message naming the pair\n' +
|
|
975
|
+
' instead of substituting another model.\n')
|
|
759
976
|
.action(async (prompt, options) => {
|
|
760
977
|
await this.execute(prompt, options || {});
|
|
761
978
|
});
|
|
@@ -782,8 +999,44 @@ export class ChatCommand extends BaseCommand {
|
|
|
782
999
|
// Apply the active model state from `nuvira model switch` as defaults
|
|
783
1000
|
const activeOpts = applyActiveModel({ provider: options?.provider, model: options?.model });
|
|
784
1001
|
const mergedOpts = { ...options, provider: activeOpts.provider, model: activeOpts.model };
|
|
1002
|
+
// ── The CONFIG's defaultProvider is a routing directive too ──────────────
|
|
1003
|
+
// When the operator supplies neither a provider nor a model — no `-p`, no
|
|
1004
|
+
// `-m`, no `nuvira model switch` state — the config decides, and the shipped
|
|
1005
|
+
// default is `defaultProvider: "auto"`. This is the CLI's counterpart to the
|
|
1006
|
+
// block in `answerOnce` (see `chat-answer-once-auto-parity.test.ts`, which
|
|
1007
|
+
// pinned the dashboard/gateway side of it in 2026-09-20).
|
|
1008
|
+
//
|
|
1009
|
+
// WHY IT IS LOAD-BEARING, measured 2026-10-06: `execute` — the whole CLI
|
|
1010
|
+
// entry (`-t/--task`, `chat "<task>"`, the REPL) — did NOT honour it. With
|
|
1011
|
+
// neither flag nor active-model state, `autoMode` was `false`, so the turn
|
|
1012
|
+
// took the NON-auto path and `resolveProvider(config, undefined)` resolved
|
|
1013
|
+
// `defaultProvider: "auto"` through `rankAvailableProviders()` to ONE
|
|
1014
|
+
// concrete provider. Everything downstream of the auto router was therefore
|
|
1015
|
+
// skipped on the product's most common entry point: no `routeMessageAuto`
|
|
1016
|
+
// (so no routing-history row to audit which model served the turn, no
|
|
1017
|
+
// routing cache, and no capability gate), no pin pre-flight, and
|
|
1018
|
+
// `model explain` — which DOES use the auto router — predicting a model the
|
|
1019
|
+
// runtime never used. Measured with `NUVIRA_TRACE_ROUTE=1`: a `-t` turn
|
|
1020
|
+
// logged `[TRACE] execute {"autoMode":false,"type":"groq"}` and appended
|
|
1021
|
+
// ZERO rows to routing-history.
|
|
1022
|
+
//
|
|
1023
|
+
// Only the AUTO default changes behaviour: an explicit `-p`/`-m` or a
|
|
1024
|
+
// `model switch` pin is a deliberate choice and keeps the non-auto path
|
|
1025
|
+
// exactly as it is (and is checked FIRST, so a pin can never be re-routed).
|
|
1026
|
+
if (!mergedOpts.provider && !mergedOpts.model) {
|
|
1027
|
+
try {
|
|
1028
|
+
const cfg = this.configManager.getAll();
|
|
1029
|
+
if (isAutoProvider(cfg.defaultProvider))
|
|
1030
|
+
mergedOpts.provider = cfg.defaultProvider;
|
|
1031
|
+
}
|
|
1032
|
+
catch {
|
|
1033
|
+
// Best-effort — an unreadable config leaves the previous behavior.
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
785
1036
|
// ── Auto routing mode: agent decides the best provider/model per message ──
|
|
786
|
-
|
|
1037
|
+
// A8 — a pinned concrete provider with `-m auto` is the provider's own auto,
|
|
1038
|
+
// not ours. See isAgentAutoRoute.
|
|
1039
|
+
let autoMode = isAgentAutoRoute(mergedOpts.provider, mergedOpts.model);
|
|
787
1040
|
let { type, provider } = autoMode
|
|
788
1041
|
? await this.getProvider({})
|
|
789
1042
|
: await this.getProvider(mergedOpts);
|
|
@@ -879,6 +1132,10 @@ export class ChatCommand extends BaseCommand {
|
|
|
879
1132
|
// WS5 — never on a REFUSED turn: the pipeline would run it in the real tree
|
|
880
1133
|
// (see the guard in `answerOnce`).
|
|
881
1134
|
if (answer.generationFailed && !answer.refused && dispatchDecision.dispatch && !dispatchDecision.needConfirm) {
|
|
1135
|
+
// E2 — same rule as `answerOnce`: the engine changes here, so say so.
|
|
1136
|
+
logger.info(` ⤵️ the chat turn produced no answer on ${provider.name}${model ? ` / ${model}` : ''} — ` +
|
|
1137
|
+
`running this ask in the multi-agent pipeline instead (a different execution model` +
|
|
1138
|
+
`${autoMode ? '' : ', on the same pinned pair'})`);
|
|
882
1139
|
await runDeveloperMode(prompt, this.configManager, { provider: type, model });
|
|
883
1140
|
// After pipeline execution, show followups and continue conversation
|
|
884
1141
|
// (don't just return — keep user engaged with next steps)
|
|
@@ -1128,7 +1385,20 @@ export class ChatCommand extends BaseCommand {
|
|
|
1128
1385
|
...(ctxOverrides?.debugSession ? { session: ctxOverrides.debugSession } : {}),
|
|
1129
1386
|
backend: { engine: 'loop', provider: session.type, ...(session.model ? { model: session.model } : {}) },
|
|
1130
1387
|
});
|
|
1131
|
-
|
|
1388
|
+
// ── The pair this surface RESOLVED is not the pair that served ───────────
|
|
1389
|
+
// MEASURED (2026-10-07): one log read `# backend.provider: local` /
|
|
1390
|
+
// `# backend.model: qwen2.5:0.5b` while its own first event said
|
|
1391
|
+
// `turn.start {"provider":"gemini"}`. Neither field was computed wrongly —
|
|
1392
|
+
// the header names the pair that ACTUALLY served (via `lastAttempt`, updated
|
|
1393
|
+
// by the provider walk and written at close) and this event named the pair
|
|
1394
|
+
// the turn was CONFIGURED with, before any walk. But a lone `provider` field
|
|
1395
|
+
// in an event called `turn.start` reads as "the turn started on gemini", so
|
|
1396
|
+
// the honest fix is to say which question each field answers: this one is the
|
|
1397
|
+
// REQUESTED route, and `turn.end` now carries the served one from the same
|
|
1398
|
+
// expression as the header, so the two can never disagree.
|
|
1399
|
+
debugLog?.event('turn.start', {
|
|
1400
|
+
requested: session.model ? `${session.type}/${session.model}` : session.type,
|
|
1401
|
+
});
|
|
1132
1402
|
// WS3 (#25) — the turn's span root, when span export is on (else null). Same
|
|
1133
1403
|
// identity as the log's: one surface, one conversation, one turn.
|
|
1134
1404
|
const otelSpan = await startTurnSpan({
|
|
@@ -1303,26 +1573,36 @@ export class ChatCommand extends BaseCommand {
|
|
|
1303
1573
|
// prompt, so on this surface the MODEL decides and the rules are invisible.
|
|
1304
1574
|
const systemText = buildToolSystemPrompt(parsed);
|
|
1305
1575
|
// Skill hint — MODE-DEPENDENT (see resolveSkillHintMode):
|
|
1306
|
-
// - `
|
|
1307
|
-
//
|
|
1308
|
-
//
|
|
1576
|
+
// - `pointer` (default) — a bounded discovery pointer: skills exist, here
|
|
1577
|
+
// are the two ways to find one (the `skill` tool and the capability
|
|
1578
|
+
// search). The MODEL picks; the harness no longer keyword-matches. This
|
|
1579
|
+
// keeps the prompt small and has no false positives to maintain.
|
|
1309
1580
|
// - `catalog` (opt-in) — the full name+description catalog, which lets the
|
|
1310
|
-
//
|
|
1311
|
-
// chosen (`NUVIRA_SKILL_CATALOG=catalog` or
|
|
1581
|
+
// model read every skill in-context but costs ~24K chars on every turn,
|
|
1582
|
+
// so it must be chosen (`NUVIRA_SKILL_CATALOG=catalog` or
|
|
1583
|
+
// `skills.catalogHint`).
|
|
1312
1584
|
// - `off` — never inject one.
|
|
1313
1585
|
// Best-effort: any failure returns '' and the turn proceeds byte-identically.
|
|
1314
1586
|
let skillHint = '';
|
|
1315
1587
|
try {
|
|
1316
|
-
const { buildConfiguredSkillHint
|
|
1317
|
-
|
|
1318
|
-
value: null,
|
|
1319
|
-
};
|
|
1320
|
-
skillHint = await buildConfiguredSkillHint(message, this.configManager, injected);
|
|
1321
|
-
await markLoopSkillUsed(injected.value);
|
|
1588
|
+
const { buildConfiguredSkillHint } = await import('../tools/loop-skill-hint.js');
|
|
1589
|
+
skillHint = await buildConfiguredSkillHint(this.configManager);
|
|
1322
1590
|
}
|
|
1323
1591
|
catch {
|
|
1324
1592
|
skillHint = ''; // best-effort — a hint failure never breaks the turn
|
|
1325
1593
|
}
|
|
1594
|
+
// Machine facts — the host stated ONCE (OS / arch / shell / installed package
|
|
1595
|
+
// managers), so the model chooses a command for THIS machine instead of
|
|
1596
|
+
// assuming one. Detected once per process; best-effort (a detection failure
|
|
1597
|
+
// injects nothing and the turn proceeds byte-identically).
|
|
1598
|
+
let machineBlock = '';
|
|
1599
|
+
try {
|
|
1600
|
+
const { buildMachineFactsBlock } = await import('../learning/machine-facts.js');
|
|
1601
|
+
machineBlock = buildMachineFactsBlock();
|
|
1602
|
+
}
|
|
1603
|
+
catch {
|
|
1604
|
+
machineBlock = '';
|
|
1605
|
+
}
|
|
1326
1606
|
// `-f/--file` file-context parity: the legacy generateWithContext loaded
|
|
1327
1607
|
// + retrieval-reduced file context. Inject it as a context message before
|
|
1328
1608
|
// the user's message so the tool-loop path keeps the flag working.
|
|
@@ -1358,6 +1638,25 @@ export class ChatCommand extends BaseCommand {
|
|
|
1358
1638
|
ambientProjectContext = undefined;
|
|
1359
1639
|
}
|
|
1360
1640
|
}
|
|
1641
|
+
// Phase 1 — deterministic, tag-scoped knowledge. A message that OPENS with
|
|
1642
|
+
// `#tag` retrieves that tag's relevant passages and puts them in front of
|
|
1643
|
+
// the model before the question. It is INERT otherwise: with no marker (or
|
|
1644
|
+
// with no tags defined at all) the builder returns an empty block WITHOUT
|
|
1645
|
+
// touching the embedder, so a turn that did not ask for a document is
|
|
1646
|
+
// byte-identical to one from a build without this feature — no extra cost,
|
|
1647
|
+
// no extra noise, and never a wrong corpus. Dynamically imported so the
|
|
1648
|
+
// retrieval modules are not loaded on a turn that will not use them, the
|
|
1649
|
+
// same best-effort contract as the ambient project context above. See
|
|
1650
|
+
// src/learning/knowledge-turn.ts for what the guarantee rests on.
|
|
1651
|
+
let knowledgeBlock = '';
|
|
1652
|
+
try {
|
|
1653
|
+
const { buildKnowledgeTurnContext } = await import('../learning/knowledge-turn.js');
|
|
1654
|
+
const knowledge = await buildKnowledgeTurnContext(message);
|
|
1655
|
+
knowledgeBlock = knowledge.block;
|
|
1656
|
+
}
|
|
1657
|
+
catch {
|
|
1658
|
+
knowledgeBlock = ''; // best-effort — a retrieval failure never breaks the turn
|
|
1659
|
+
}
|
|
1361
1660
|
// G4 — carry THIS project's working state (files changed, verification debt,
|
|
1362
1661
|
// user-reported regressions) into the turn, so the model does not re-derive
|
|
1363
1662
|
// what previous turns already established. This is the fix for the
|
|
@@ -1379,7 +1678,7 @@ export class ChatCommand extends BaseCommand {
|
|
|
1379
1678
|
// turn, where it used to occupy ~80% of the ask on WhatsApp turns.
|
|
1380
1679
|
const systemPolicyBlock = ctxOverrides?.systemPolicy ? `\n\n${ctxOverrides.systemPolicy}` : '';
|
|
1381
1680
|
const thread = [
|
|
1382
|
-
{ role: 'system', content: systemText + systemPolicyBlock + skillHint },
|
|
1681
|
+
{ role: 'system', content: systemText + systemPolicyBlock + machineBlock + skillHint },
|
|
1383
1682
|
// P3 — the attached project's bounded snapshot (path + file tree +
|
|
1384
1683
|
// symbol map) rides in before the conversation, exactly like --file
|
|
1385
1684
|
// context: the model knows what it is looking at without being told.
|
|
@@ -1390,11 +1689,22 @@ export class ChatCommand extends BaseCommand {
|
|
|
1390
1689
|
: ambientProjectContext
|
|
1391
1690
|
? [{ role: 'user', content: `[Project context]\n${ambientProjectContext}` }]
|
|
1392
1691
|
: []),
|
|
1692
|
+
// Phase 1 — the tagged knowledge this ask selected, when it selected one.
|
|
1693
|
+
// It sits with the other CONTEXT blocks (before the history) so the
|
|
1694
|
+
// conversation stays contiguous, and it is a USER-TURN message rather than
|
|
1695
|
+
// system text, so the system prompt stays byte-stable and cacheable.
|
|
1696
|
+
...(knowledgeBlock ? [{ role: 'user', content: knowledgeBlock }] : []),
|
|
1393
1697
|
// P4 — the recalled project context (prior sessions + facts) rides in
|
|
1394
1698
|
// next, so the model starts from what this project was last doing.
|
|
1395
1699
|
...(ctxOverrides?.recallContext
|
|
1396
1700
|
? [{ role: 'user', content: ctxOverrides.recallContext }]
|
|
1397
1701
|
: []),
|
|
1702
|
+
// Decision recall — the ADVISORY standing answers this project recorded
|
|
1703
|
+
// for related asks. Sits with the other context blocks, and is deliberately
|
|
1704
|
+
// never used to suppress an `ask_user`.
|
|
1705
|
+
...(ctxOverrides?.decisionContext
|
|
1706
|
+
? [{ role: 'user', content: ctxOverrides.decisionContext }]
|
|
1707
|
+
: []),
|
|
1398
1708
|
// G4 — the deterministic working-state ledger (never a summary).
|
|
1399
1709
|
...(workingStateBlock ? [{ role: 'user', content: workingStateBlock }] : []),
|
|
1400
1710
|
...(fileContext
|
|
@@ -1446,6 +1756,18 @@ export class ChatCommand extends BaseCommand {
|
|
|
1446
1756
|
// P4 — when a project is attached, scope tools to its root so the
|
|
1447
1757
|
// agent operates inside the project (not the dashboard server's cwd).
|
|
1448
1758
|
cwd: turnCwd,
|
|
1759
|
+
// Cluster G — the surface's own verdict on whether `turnCwd` is the
|
|
1760
|
+
// user's workspace. `false` means this is an unscoped turn (no attachment,
|
|
1761
|
+
// no folder named in the message, no configured default), so `turnCwd` is
|
|
1762
|
+
// the dashboard process's own directory — a place that belongs to nobody.
|
|
1763
|
+
// A write there must ask; see `unscopedWriteRefusal` in coding-tools.ts.
|
|
1764
|
+
...(ctxOverrides?.unscopedWorkspace ? { workspaceUnscoped: true } : {}),
|
|
1765
|
+
// P7 — is a PERSON watching this turn? It decides `run_terminal`'s DEFAULT
|
|
1766
|
+
// command timeout (60s here vs 120s unattended): two minutes of silence in
|
|
1767
|
+
// front of someone is not patience, it is a bug — the live run spent four
|
|
1768
|
+
// minutes on back-to-back 120-second timeouts while the console showed
|
|
1769
|
+
// nothing, then gave up.
|
|
1770
|
+
interactive: Boolean(ctxOverrides?.askUser) || Boolean(process.stdin.isTTY),
|
|
1449
1771
|
emit: (event, data, source) => {
|
|
1450
1772
|
// G3 — collect mutated file paths from `tool:started` (which carries
|
|
1451
1773
|
// the arguments) so the working-state ledger knows what changed.
|
|
@@ -1613,7 +1935,11 @@ export class ChatCommand extends BaseCommand {
|
|
|
1613
1935
|
{ name: 'system:identity+tool-contract', chars: systemText.length },
|
|
1614
1936
|
{ name: 'system:channel-policy', chars: systemPolicyBlock.length },
|
|
1615
1937
|
{ name: 'skill-hint', chars: skillHint.length, dropPriority: 10 },
|
|
1938
|
+
// Machine facts are FACTS about the host, not a recommendation, so they
|
|
1939
|
+
// drop LAST among the optional blocks (higher priority = kept longer).
|
|
1940
|
+
{ name: 'machine-facts', chars: machineBlock.length, dropPriority: 15 },
|
|
1616
1941
|
{ name: 'working-state', chars: workingStateBlock.length, dropPriority: 25 },
|
|
1942
|
+
{ name: 'decisions', chars: ctxOverrides?.decisionContext?.length ?? 0, dropPriority: 28 },
|
|
1617
1943
|
{ name: 'recall', chars: ctxOverrides?.recallContext?.length ?? 0, dropPriority: 30 },
|
|
1618
1944
|
{
|
|
1619
1945
|
name: 'project-context',
|
|
@@ -1623,10 +1949,17 @@ export class ChatCommand extends BaseCommand {
|
|
|
1623
1949
|
{ name: 'file-context', chars: fileContext?.length ?? 0, dropPriority: 45 },
|
|
1624
1950
|
{ name: 'history+ask', chars: historyChars },
|
|
1625
1951
|
], { budget: contextBudget });
|
|
1626
|
-
// Apply the
|
|
1627
|
-
//
|
|
1628
|
-
|
|
1629
|
-
|
|
1952
|
+
// Apply the ladder steps: drop the optional system blocks the budget names,
|
|
1953
|
+
// rebuilding the system message from the survivors.
|
|
1954
|
+
const trimmed = new Set(report.trims);
|
|
1955
|
+
if ((trimmed.has('skill-hint') || trimmed.has('machine-facts')) && (skillHint || machineBlock)) {
|
|
1956
|
+
thread[0] = {
|
|
1957
|
+
role: 'system',
|
|
1958
|
+
content: systemText +
|
|
1959
|
+
systemPolicyBlock +
|
|
1960
|
+
(trimmed.has('machine-facts') ? '' : machineBlock) +
|
|
1961
|
+
(trimmed.has('skill-hint') ? '' : skillHint),
|
|
1962
|
+
};
|
|
1630
1963
|
}
|
|
1631
1964
|
if (report.level !== 'ok') {
|
|
1632
1965
|
recordTraceEvent(chatTraceId, {
|
|
@@ -1643,6 +1976,9 @@ export class ChatCommand extends BaseCommand {
|
|
|
1643
1976
|
// The instant this turn's model walk began. A failed generation records the
|
|
1644
1977
|
// WALK (below) relative to this mark, so the trace can say what was tried.
|
|
1645
1978
|
const modelWalkMark = Date.now();
|
|
1979
|
+
// P4 — a fresh attempt log for this turn (the report is about what THIS turn
|
|
1980
|
+
// tried, never a stale list from an earlier one).
|
|
1981
|
+
this.turnAttempts = [];
|
|
1646
1982
|
const seenStepDigests = new Set();
|
|
1647
1983
|
const digestPrompt = (p) => {
|
|
1648
1984
|
try {
|
|
@@ -1706,6 +2042,146 @@ export class ChatCommand extends BaseCommand {
|
|
|
1706
2042
|
throw err;
|
|
1707
2043
|
}
|
|
1708
2044
|
};
|
|
2045
|
+
/**
|
|
2046
|
+
* P2 (fix_model_routing) — MID-TURN MODEL HANDOFF.
|
|
2047
|
+
*
|
|
2048
|
+
* The tool loop calls this when the model serving the turn resolved with
|
|
2049
|
+
* NOTHING usable (no answer text, no tool call) and has already had its one
|
|
2050
|
+
* retry. Until now that case re-asked the SAME model: a live dashboard run
|
|
2051
|
+
* took five consecutive empty responses from `local/gpt-oss:120b-cloud` and
|
|
2052
|
+
* ended as `bounded` with zero tool calls, while a healthy Gemini model sat
|
|
2053
|
+
* configured and unused — the user had to say "retry". The loop owns the
|
|
2054
|
+
* retry policy; this owns the POOL.
|
|
2055
|
+
*
|
|
2056
|
+
* Order of business, and why:
|
|
2057
|
+
* 1. Exclude the pair that returned nothing — MODEL-scoped, never the
|
|
2058
|
+
* provider. Free tiers meter per model, so the provider's next-best
|
|
2059
|
+
* model is usually perfectly healthy, and `pushCandidate` in the deep
|
|
2060
|
+
* pool already honours this model-scoped exclusion set.
|
|
2061
|
+
* 2. Auto turns re-resolve through the same deep pool the first route used,
|
|
2062
|
+
* first WITHOUT excluding the provider (its sibling models are the
|
|
2063
|
+
* cheapest, lowest-latency handoff) and then WITH it, so a provider
|
|
2064
|
+
* whose only models are dead is genuinely left behind.
|
|
2065
|
+
* 3. A pinned (non-auto) turn walks the configured fallback chain, and
|
|
2066
|
+
* honours `strictModelMode()` by refusing to substitute at all — the
|
|
2067
|
+
* same rule the thrown-error failover already follows.
|
|
2068
|
+
*
|
|
2069
|
+
* Resolving `false` is honest: it means no other candidate exists, and the
|
|
2070
|
+
* loop then falls back to its own bounded escalation instead of spinning.
|
|
2071
|
+
*/
|
|
2072
|
+
const requestModelSwitch = async () => {
|
|
2073
|
+
const failedModel = session.model;
|
|
2074
|
+
// P4 — this pair is one of the models the turn actually tried, and it
|
|
2075
|
+
// failed by answering with nothing. Recorded here because the report has
|
|
2076
|
+
// to name it, and because "the provider returned an empty response" is a
|
|
2077
|
+
// cause the user should see instead of a generic apology.
|
|
2078
|
+
this.recordTurnAttempt({
|
|
2079
|
+
provider: session.type,
|
|
2080
|
+
model: failedModel ?? 'default',
|
|
2081
|
+
kind: 'empty-response',
|
|
2082
|
+
reason: describeFailureKind('empty-response'),
|
|
2083
|
+
});
|
|
2084
|
+
// ── RC2: the failure must be LEARNED, not just survived ───────────────
|
|
2085
|
+
// The session exclusion alone only lasts this turn: the model stayed
|
|
2086
|
+
// `verified` with `errorRate` 0 across sessions, so the very next chat
|
|
2087
|
+
// could route to it again. `recordAutoProviderFailure` runs the FULL
|
|
2088
|
+
// shared composition for the pair — session exclusion (model-scoped),
|
|
2089
|
+
// registry write-through (which parks it for EMPTY_RESPONSE_PARK_MS), the
|
|
2090
|
+
// quota-timeline event, and the shared breaker (deliberately skipped for
|
|
2091
|
+
// an empty response: the breaker is provider-scoped and the provider was
|
|
2092
|
+
// fine). One `empty-response` failure nobody wrote down was the whole
|
|
2093
|
+
// reason a dead model kept its "verified" badge.
|
|
2094
|
+
try {
|
|
2095
|
+
this.recordAutoProviderFailure(session.type, new UnusableModelResponseError('empty', 'the provider returned nothing usable for this step'), failedModel);
|
|
2096
|
+
}
|
|
2097
|
+
catch {
|
|
2098
|
+
// Best-effort — bookkeeping must never break the turn.
|
|
2099
|
+
}
|
|
2100
|
+
// P6 — and the bandit hears about it too: a step-level empty is NEGATIVE
|
|
2101
|
+
// evidence for this model in the arm the router samples (see
|
|
2102
|
+
// `RouterBandit.penalizeModel`), so the score that flipped a working run to
|
|
2103
|
+
// a broken model can actually decay.
|
|
2104
|
+
if (failedModel && failedModel !== 'default') {
|
|
2105
|
+
try {
|
|
2106
|
+
getRouterBandit().penalizeModel(failedModel, this.lastRouteSnapshot?.complexity ?? undefined, this.lastTaskSignature?.split('|')[0]);
|
|
2107
|
+
}
|
|
2108
|
+
catch {
|
|
2109
|
+
// Best-effort — learning must never break the turn.
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
/** Install a candidate and report whether it is genuinely a NEW pair. */
|
|
2113
|
+
const install = (next) => {
|
|
2114
|
+
const current = `${session.type}|${session.model ?? 'default'}`;
|
|
2115
|
+
const pair = `${next.type}|${next.model ?? 'default'}`;
|
|
2116
|
+
if (pair === current)
|
|
2117
|
+
return false;
|
|
2118
|
+
session.type = next.type;
|
|
2119
|
+
session.provider = next.provider;
|
|
2120
|
+
if (next.model && next.model !== 'default')
|
|
2121
|
+
session.model = next.model;
|
|
2122
|
+
this.lastAttempt = { provider: next.type, model: next.model };
|
|
2123
|
+
logger.warn(` 🔀 ${current} returned nothing usable — handing the turn to ${pair}`);
|
|
2124
|
+
return true;
|
|
2125
|
+
};
|
|
2126
|
+
if (mode.auto) {
|
|
2127
|
+
// First keep the provider (its sibling models are the cheapest handoff),
|
|
2128
|
+
// then drop the provider entirely so a fully-dead provider is left behind.
|
|
2129
|
+
// The same intent hint the initial route and the failover walk use: a
|
|
2130
|
+
// bare continuation ("resume") must re-route on the ask it continues,
|
|
2131
|
+
// not on the signal-free word, or the handoff lands on a trivial model.
|
|
2132
|
+
const switchRoutingText = continuationSoftwareText(message, history) ?? undefined;
|
|
2133
|
+
for (const excludeProviders of [[], [session.type]]) {
|
|
2134
|
+
try {
|
|
2135
|
+
const next = await this.routeMessageAuto(message, excludeProviders, {
|
|
2136
|
+
...(switchRoutingText ? { routingText: switchRoutingText } : {}),
|
|
2137
|
+
fallbackFrom: session.type,
|
|
2138
|
+
// A handoff must NEVER reuse a decision computed before the failure
|
|
2139
|
+
// it is reacting to. The 30-second routing cache is keyed on inputs
|
|
2140
|
+
// that do not include the pair we just excluded, so a cached
|
|
2141
|
+
// decision could hand back the SAME model — making the handoff a
|
|
2142
|
+
// no-op and ending a recoverable turn. Observed shape: two routing
|
|
2143
|
+
// decisions 17 seconds apart carrying an identical score, one of
|
|
2144
|
+
// them a failover the cache had never re-evaluated.
|
|
2145
|
+
noCache: true,
|
|
2146
|
+
});
|
|
2147
|
+
if (install(next))
|
|
2148
|
+
return true;
|
|
2149
|
+
}
|
|
2150
|
+
catch {
|
|
2151
|
+
// Unresolvable through this shape — try the next one.
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
return false;
|
|
2155
|
+
}
|
|
2156
|
+
// A pinned turn: never substitute a model the user explicitly forbade.
|
|
2157
|
+
if (strictModelMode())
|
|
2158
|
+
return false;
|
|
2159
|
+
try {
|
|
2160
|
+
const chain = getProviderFallback(this.configManager, this.configManager.getAll().fallback)
|
|
2161
|
+
.getFallbackChain(session.type);
|
|
2162
|
+
for (const fbType of chain) {
|
|
2163
|
+
if (fbType === session.type)
|
|
2164
|
+
continue;
|
|
2165
|
+
if (!hasCredentials(this.configManager, fbType))
|
|
2166
|
+
continue;
|
|
2167
|
+
if (!governanceVerdict(this.configManager, fbType, { taskText: message }).allowed)
|
|
2168
|
+
continue;
|
|
2169
|
+
try {
|
|
2170
|
+
const resolved = resolveProvider(this.configManager, fbType);
|
|
2171
|
+
const model = getAutoRouter().resolveModel(fbType, 'chat', this.configManager);
|
|
2172
|
+
if (install({ type: resolved.type, provider: resolved.provider, model }))
|
|
2173
|
+
return true;
|
|
2174
|
+
}
|
|
2175
|
+
catch {
|
|
2176
|
+
// Next fallback candidate.
|
|
2177
|
+
}
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
catch {
|
|
2181
|
+
// Fall through to `false` — the loop's bounded path still applies.
|
|
2182
|
+
}
|
|
2183
|
+
return false;
|
|
2184
|
+
};
|
|
1709
2185
|
try {
|
|
1710
2186
|
// R1 — the harness follows the MODEL, not only the config: config says
|
|
1711
2187
|
// what this deployment prefers, the profile decides what this model can
|
|
@@ -1747,6 +2223,13 @@ export class ChatCommand extends BaseCommand {
|
|
|
1747
2223
|
onTraceEvent: (event) => recordTraceEvent(chatTraceId, event),
|
|
1748
2224
|
deps: {
|
|
1749
2225
|
callModel: callModelWithTrace,
|
|
2226
|
+
// P2 — the empty-response handoff (see `requestModelSwitch` above).
|
|
2227
|
+
requestModelSwitch,
|
|
2228
|
+
// C5 — `requestModelSwitch` installs the replacement on `session`, so
|
|
2229
|
+
// re-resolve the thread budget from the session AFTER a handoff: a
|
|
2230
|
+
// handoff to a smaller window is the case that overflows, and the
|
|
2231
|
+
// budget in hand was computed from the model that just failed.
|
|
2232
|
+
threadBudgetAfterSwitch: () => resolveThreadBudgetChars({ provider: session.type, model: session.model }),
|
|
1750
2233
|
executeTool: async (name, args, ctx) => {
|
|
1751
2234
|
const tool = getTool(name);
|
|
1752
2235
|
if (!tool)
|
|
@@ -1767,6 +2250,19 @@ export class ChatCommand extends BaseCommand {
|
|
|
1767
2250
|
},
|
|
1768
2251
|
},
|
|
1769
2252
|
});
|
|
2253
|
+
// P3 — REMEMBER the worker that actually delivered this task, so the next
|
|
2254
|
+
// turn of the SAME task ("resume", "continue") prefers it instead of
|
|
2255
|
+
// re-deciding from a signal-free word. Only a DELIVERED turn is remembered:
|
|
2256
|
+
// a bounded, cancelled or failed one has taught us nothing about which
|
|
2257
|
+
// model can do this job, and remembering it would propagate the failure.
|
|
2258
|
+
if (!result.cancelled &&
|
|
2259
|
+
!result.generationFailed &&
|
|
2260
|
+
!result.bounded &&
|
|
2261
|
+
!result.termination?.includes('no-capable') &&
|
|
2262
|
+
result.content.trim() &&
|
|
2263
|
+
session.model) {
|
|
2264
|
+
this.taskContinuity.remember(this.lastTaskSignature ?? taskSignature(undefined, undefined), session.type, session.model);
|
|
2265
|
+
}
|
|
1770
2266
|
}
|
|
1771
2267
|
catch (err) {
|
|
1772
2268
|
// The tool loop does not throw on its own; this catches the errors that
|
|
@@ -1823,6 +2319,71 @@ export class ChatCommand extends BaseCommand {
|
|
|
1823
2319
|
generationFailed: true,
|
|
1824
2320
|
};
|
|
1825
2321
|
}
|
|
2322
|
+
// ── P4 — THE HONEST EXHAUSTION REPORT ────────────────────────────────
|
|
2323
|
+
// A turn that ends without an answer used to deliver a bare line ("the
|
|
2324
|
+
// language model was unavailable") or, worse, a QUESTION that made the user
|
|
2325
|
+
// the retry button. Instead, the user gets the measured facts: which models
|
|
2326
|
+
// were tried and why each did not answer, which targets are unavailable and
|
|
2327
|
+
// the WALL-CLOCK time each frees up, whether this was a routing gap or a real
|
|
2328
|
+
// shortage, and the levers only they can pull — with no request to retry.
|
|
2329
|
+
//
|
|
2330
|
+
// Gated on an actual ATTEMPT (or the loop's evidenced `no-capable-candidate`):
|
|
2331
|
+
// a governance refusal, an admin-budget stop or a bad pin fails with a
|
|
2332
|
+
// healthy pool and a message that is already the right answer, and dressing
|
|
2333
|
+
// those up as exhaustion would be a new lie in place of the old one.
|
|
2334
|
+
if (!result.cancelled &&
|
|
2335
|
+
(result.generationFailed || result.termination === 'no-capable-candidate')) {
|
|
2336
|
+
try {
|
|
2337
|
+
const breadth = modelBreadthReport(modelWalkMark, this.configManager);
|
|
2338
|
+
const seen = new Set(this.turnAttempts.map((a) => `${a.provider}|${a.model}|${a.kind}`));
|
|
2339
|
+
const attempts = [...this.turnAttempts];
|
|
2340
|
+
for (const a of breadth.tried) {
|
|
2341
|
+
const key = `${a.provider}|${a.model}|${a.kind}`;
|
|
2342
|
+
if (seen.has(key))
|
|
2343
|
+
continue;
|
|
2344
|
+
seen.add(key);
|
|
2345
|
+
attempts.push({
|
|
2346
|
+
provider: a.provider,
|
|
2347
|
+
model: a.model,
|
|
2348
|
+
kind: a.kind,
|
|
2349
|
+
reason: a.reason || describeFailureKind(a.kind),
|
|
2350
|
+
...(a.skipped ? { skipped: true } : {}),
|
|
2351
|
+
});
|
|
2352
|
+
}
|
|
2353
|
+
if (attempts.length > 0) {
|
|
2354
|
+
const exclusions = breadth.parked
|
|
2355
|
+
.filter((r) => r.active)
|
|
2356
|
+
.map((r) => ({
|
|
2357
|
+
provider: r.provider,
|
|
2358
|
+
...(r.model ? { model: r.model } : {}),
|
|
2359
|
+
kind: r.kind,
|
|
2360
|
+
...(r.expiresAt ? { expiresAt: r.expiresAt } : {}),
|
|
2361
|
+
...(r.source ? { source: r.source } : {}),
|
|
2362
|
+
}));
|
|
2363
|
+
const reportText = renderExhaustionReport({
|
|
2364
|
+
attempts,
|
|
2365
|
+
exclusions,
|
|
2366
|
+
...(typeof breadth.poolSize === 'number' ? { poolSize: breadth.poolSize } : {}),
|
|
2367
|
+
...(typeof breadth.poolProviders === 'number' ? { poolProviders: breadth.poolProviders } : {}),
|
|
2368
|
+
task: message,
|
|
2369
|
+
// The underlying line is kept as the REASON, so a specific,
|
|
2370
|
+
// actionable cause is never buried by the report.
|
|
2371
|
+
...(result.generationFailed && result.content.trim() ? { cause: result.content } : {}),
|
|
2372
|
+
});
|
|
2373
|
+
if (reportText) {
|
|
2374
|
+
result.content = reportText;
|
|
2375
|
+
recordTraceEvent(chatTraceId, {
|
|
2376
|
+
kind: 'failover',
|
|
2377
|
+
summary: `exhaustion report delivered — ${attempts.filter((a) => !a.skipped).length} model(s) tried, ` +
|
|
2378
|
+
`${exclusions.length} unavailable, eligible pool ${breadth.poolSize ?? '?'}`,
|
|
2379
|
+
});
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
catch {
|
|
2384
|
+
// The report is a courtesy — the turn's own failure line still stands.
|
|
2385
|
+
}
|
|
2386
|
+
}
|
|
1826
2387
|
// Record WHAT HAPPENED, not just "the model answered": a hallucinated
|
|
1827
2388
|
// "I have sent it" (no tool ran) must be visible as an unverified claim in
|
|
1828
2389
|
// the Trace tab instead of looking like a real delivery.
|
|
@@ -1839,6 +2400,10 @@ export class ChatCommand extends BaseCommand {
|
|
|
1839
2400
|
unverifiedEditClaim: result.unverifiedEditClaim,
|
|
1840
2401
|
undeliveredArtifact: result.undeliveredArtifact,
|
|
1841
2402
|
unverifiedBuildClaim: result.unverifiedBuildClaim,
|
|
2403
|
+
// Bundle 19/20 — the artifact's own omission, and a file claim no write
|
|
2404
|
+
// backs, both make the turn `incomplete` on the trace (not `answered`). incompleteArtifactClaim: Boolean(result.artifactIncomplete),
|
|
2405
|
+
unverifiedFileClaim: result.unverifiedFileClaim,
|
|
2406
|
+
artifactShortfall: Boolean(result.artifactShortfall),
|
|
1842
2407
|
});
|
|
1843
2408
|
// A cancelled / failed / incomplete turn is NOT a success. `!generationFailed`
|
|
1844
2409
|
// used to let a cancelled run record `success: true` while its outcome said
|
|
@@ -1902,6 +2467,31 @@ export class ChatCommand extends BaseCommand {
|
|
|
1902
2467
|
if (result.unverifiedBuildClaim) {
|
|
1903
2468
|
logger.warn(' ⚠️ A build command FAILED this turn and no later build succeeded, but the reply reports success — treat the build as UNVERIFIED.');
|
|
1904
2469
|
}
|
|
2470
|
+
// Bundle 19 — the ARTIFACT-honesty warning. The file this turn wrote says its
|
|
2471
|
+
// own content was omitted, so the prose above describes a document that is not
|
|
2472
|
+
// on disk. The artifact's OWN WORDS are quoted, so the reader sees the evidence
|
|
2473
|
+
// (a specific line the file contains) rather than a verdict they must trust.
|
|
2474
|
+
// Without this the flag only changed the internal verdict; the user still saw
|
|
2475
|
+
// a confident "complete" reply with no sign of the admission inside it.
|
|
2476
|
+
if (result.artifactIncomplete) {
|
|
2477
|
+
logger.warn(` ⚠️ The file ${result.artifactIncomplete.path} says its own content was omitted ("${result.artifactIncomplete.statement}") — the artifact is INCOMPLETE, whatever the reply above claims.`);
|
|
2478
|
+
}
|
|
2479
|
+
// Bundle 20 — the FILE-CLAIM warning. The reply above says a file was
|
|
2480
|
+
// written and the turn's own record says none was. Found live: a turn wrote
|
|
2481
|
+
// nothing and replied "The guide has been written and saved as GUIDE.md …
|
|
2482
|
+
// contains a full ~5,000-word technical guide" — and the user saw no warning,
|
|
2483
|
+
// because the flag that exists for this failure is gated on the ASK and the
|
|
2484
|
+
// ask was not recognised as a deliverable. This one keys on the CLAIM.
|
|
2485
|
+
if (result.unverifiedFileClaim) {
|
|
2486
|
+
logger.warn(' ⚠️ This reply says a file was written, but NO file was written this turn — treat the deliverable as MISSING, not as done.');
|
|
2487
|
+
}
|
|
2488
|
+
// Bundle 23 (item 13) — the ARTIFACT-VS-ASK warning. The ask named a
|
|
2489
|
+
// magnitude and the file this turn wrote is far short of it; the numbers are
|
|
2490
|
+
// quoted so the reader sees the gap rather than a verdict.
|
|
2491
|
+
if (result.artifactShortfall) {
|
|
2492
|
+
const s = result.artifactShortfall;
|
|
2493
|
+
logger.warn(` ⚠️ ${s.path} has ~${s.deliveredWords} words, but the request asked for ~${s.targetWords} (${s.source}) — the deliverable is materially SHORT of what was asked.`);
|
|
2494
|
+
}
|
|
1905
2495
|
}
|
|
1906
2496
|
catch {
|
|
1907
2497
|
// Best-effort.
|
|
@@ -1968,11 +2558,18 @@ export class ChatCommand extends BaseCommand {
|
|
|
1968
2558
|
// affect the answer.
|
|
1969
2559
|
if (debugLog) {
|
|
1970
2560
|
const servedModel = this.lastAttempt?.model ?? session.model;
|
|
2561
|
+
// ONE expression for the served pair, used by BOTH the header and the
|
|
2562
|
+
// `turn.end` event — so "what the header says" and "what the events say"
|
|
2563
|
+
// is a single fact rather than two that can drift (the A5/D3 class of
|
|
2564
|
+
// defect, in its last remaining face).
|
|
2565
|
+
const servedProvider = this.lastAttempt?.provider ?? session.type;
|
|
1971
2566
|
debugLog.backendOf({
|
|
1972
|
-
provider:
|
|
2567
|
+
provider: servedProvider,
|
|
1973
2568
|
...(servedModel ? { model: servedModel } : {}),
|
|
1974
2569
|
transport: result.transport ?? null,
|
|
1975
2570
|
});
|
|
2571
|
+
const requested = session.model ? `${session.type}/${session.model}` : session.type;
|
|
2572
|
+
const served = servedModel ? `${servedProvider}/${servedModel}` : servedProvider;
|
|
1976
2573
|
debugLog.event('turn.end', {
|
|
1977
2574
|
generationFailed: result.generationFailed === true,
|
|
1978
2575
|
cancelled: result.cancelled === true,
|
|
@@ -1980,6 +2577,11 @@ export class ChatCommand extends BaseCommand {
|
|
|
1980
2577
|
contentChars: result.content.length,
|
|
1981
2578
|
toolCalls: result.toolCalls?.length ?? 0,
|
|
1982
2579
|
findings: findings.length,
|
|
2580
|
+
// The pair that answered, and (only when it differs) the pair the turn
|
|
2581
|
+
// asked for — a reader can then tell a straight run from a failover
|
|
2582
|
+
// without cross-referencing the header.
|
|
2583
|
+
served,
|
|
2584
|
+
...(served === requested ? {} : { requested }),
|
|
1983
2585
|
});
|
|
1984
2586
|
const notice = debugLogNotice(ctxOverrides?.debugSurface ?? 'cli-chat', debugLog.write());
|
|
1985
2587
|
if (notice)
|
|
@@ -2014,6 +2616,11 @@ export class ChatCommand extends BaseCommand {
|
|
|
2014
2616
|
unfulfilledPromise: result.unfulfilledPromise,
|
|
2015
2617
|
undeliveredArtifact: result.undeliveredArtifact,
|
|
2016
2618
|
unverifiedBuildClaim: result.unverifiedBuildClaim,
|
|
2619
|
+
// Bundle 20 — an unbacked file claim (or a self-admitted omission) means
|
|
2620
|
+
// the work did not conclude, so the continuation affordances must appear.
|
|
2621
|
+
incompleteArtifactClaim: Boolean(result.artifactIncomplete),
|
|
2622
|
+
unverifiedFileClaim: result.unverifiedFileClaim,
|
|
2623
|
+
artifactShortfall: Boolean(result.artifactShortfall),
|
|
2017
2624
|
});
|
|
2018
2625
|
const chatFollowups = withContinuationFollowups(result.followups, {
|
|
2019
2626
|
unfinished: !traceOutcomeSucceeded(chatOutcomeForFollowups),
|
|
@@ -2030,6 +2637,14 @@ export class ChatCommand extends BaseCommand {
|
|
|
2030
2637
|
unfulfilledPromise: result.unfulfilledPromise,
|
|
2031
2638
|
undeliveredArtifact: result.undeliveredArtifact,
|
|
2032
2639
|
unverifiedBuildClaim: result.unverifiedBuildClaim,
|
|
2640
|
+
// Bundle 19/20 — carried to the caller so the honesty flags survive the
|
|
2641
|
+
// return boundary instead of only living in the turn report.
|
|
2642
|
+
artifactIncomplete: result.artifactIncomplete,
|
|
2643
|
+
unverifiedFileClaim: result.unverifiedFileClaim,
|
|
2644
|
+
artifactShortfall: result.artifactShortfall,
|
|
2645
|
+
// Item 13 (round 3) — the collectable "fluent, on-length" delivery survives
|
|
2646
|
+
// this boundary too, so `answerOnce` can record it as corpus facts.
|
|
2647
|
+
authoredDeliverable: result.authoredDeliverable,
|
|
2033
2648
|
// R2 — the transport this turn travelled on (interactive REPL path).
|
|
2034
2649
|
transport: result.transport,
|
|
2035
2650
|
// WS1 — the findings this turn recorded, with their verdicts.
|
|
@@ -2143,6 +2758,41 @@ export class ChatCommand extends BaseCommand {
|
|
|
2143
2758
|
// refused it). The reason names the provider and the rule.
|
|
2144
2759
|
throw new Error(`Governance policy: ${verdict.reason}`);
|
|
2145
2760
|
}
|
|
2761
|
+
// ADMIN BUDGET (pinned path). The pin still runs the model the user
|
|
2762
|
+
// chose — this only REFUSES it when the user's own declared budget
|
|
2763
|
+
// (routing.quota window, admin cost cap) is already exceeded, instead
|
|
2764
|
+
// of spending past the control they set in the dashboard. A no-op when
|
|
2765
|
+
// no budget is configured.
|
|
2766
|
+
const budget = adminBudgetVerdict(this.configManager, session.type, { model: session.model });
|
|
2767
|
+
if (!budget.allowed) {
|
|
2768
|
+
throw new Error(`Admin budget: ${budget.reason}`);
|
|
2769
|
+
}
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* A2 — REACHABILITY PRE-FLIGHT (pinned path).
|
|
2773
|
+
*
|
|
2774
|
+
* The governance/admin checks above cover POLICY; this covers whether the
|
|
2775
|
+
* pin can work at all. Measured (Run D): `-p openrouter -m
|
|
2776
|
+
* deepseek/deepseek-v4.1-flash` on an unfunded account was sent, and the
|
|
2777
|
+
* operator learned the real cause from a raw 402 body — after a full round
|
|
2778
|
+
* trip, with a live 6-second failure that said nothing about what to do.
|
|
2779
|
+
*
|
|
2780
|
+
* `resolveRoute` refuses that pin, and every one of its callers therefore
|
|
2781
|
+
* does — but this method resolves its own model, so after the first fix the
|
|
2782
|
+
* same run still logged BOTH behaviours: the pipeline refused the pair
|
|
2783
|
+
* before any call ("Refusing to call …") while the chat tool loop sent the
|
|
2784
|
+
* request and got `402 Insufficient credits` back. Same gate, same wording,
|
|
2785
|
+
* same implementation (`strictPinRefusal`) — this is the one seam that was
|
|
2786
|
+
* still missing it.
|
|
2787
|
+
*
|
|
2788
|
+
* Strict mode only, exactly like `resolveRoute`: the refusal sentence says
|
|
2789
|
+
* strict mode forbids substituting, and on a non-strict turn the fallback
|
|
2790
|
+
* walk is the correct repair.
|
|
2791
|
+
*/
|
|
2792
|
+
if (!mode.auto && strictModelMode()) {
|
|
2793
|
+
const refusal = strictPinRefusal(session.type, session.model);
|
|
2794
|
+
if (refusal)
|
|
2795
|
+
throw new Error(refusal);
|
|
2146
2796
|
}
|
|
2147
2797
|
const resolveEffectiveModel = (providerType, requested) => {
|
|
2148
2798
|
if (requested && requested !== 'default')
|
|
@@ -2286,6 +2936,16 @@ export class ChatCommand extends BaseCommand {
|
|
|
2286
2936
|
return await generateWithTransientRetry(() => tryGenerate(session.provider, session.type, session.model), abort, (attempt, err) => logger.warn(` ⏳ ${session.provider.name} transient failure (attempt ${attempt}) — retrying shortly: ${err instanceof Error ? err.message.split('\n')[0] : String(err)}`));
|
|
2287
2937
|
}
|
|
2288
2938
|
catch (err) {
|
|
2939
|
+
// P4 — the model this turn STARTED on failed. Recorded with a human
|
|
2940
|
+
// phrase (never the raw provider text, which is how a JSON quota dump
|
|
2941
|
+
// once reached a user verbatim) so the exhaustion report can name it.
|
|
2942
|
+
const primaryKind = classifyFallbackError(err);
|
|
2943
|
+
this.recordTurnAttempt({
|
|
2944
|
+
provider: session.type,
|
|
2945
|
+
model: session.model ?? 'default',
|
|
2946
|
+
kind: primaryKind,
|
|
2947
|
+
reason: describeFailureKind(primaryKind),
|
|
2948
|
+
});
|
|
2289
2949
|
// Auto mode: fail over across the ranked candidates (never stuck).
|
|
2290
2950
|
if (mode.auto) {
|
|
2291
2951
|
const firstType = session.type;
|
|
@@ -2360,13 +3020,29 @@ export class ChatCommand extends BaseCommand {
|
|
|
2360
3020
|
logger.success(`✅ Auto failover: answered from ${next.provider.name} (${next.model}) after ${firstType} failed`);
|
|
2361
3021
|
return resp;
|
|
2362
3022
|
}
|
|
2363
|
-
catch {
|
|
3023
|
+
catch (nextErr) {
|
|
3024
|
+
// P4 — record THIS candidate's failure too, so the report lists
|
|
3025
|
+
// every model the turn was actually served by, in order.
|
|
3026
|
+
const nextKind = classifyFallbackError(nextErr);
|
|
3027
|
+
this.recordTurnAttempt({
|
|
3028
|
+
provider: next.type,
|
|
3029
|
+
model: next.model ?? 'default',
|
|
3030
|
+
kind: nextKind,
|
|
3031
|
+
reason: describeFailureKind(nextKind),
|
|
3032
|
+
});
|
|
2364
3033
|
// Next candidate.
|
|
2365
3034
|
}
|
|
2366
3035
|
}
|
|
2367
3036
|
}
|
|
2368
|
-
else if (isRetryableError(classifyFallbackError(err))) {
|
|
3037
|
+
else if (!strictModelMode() && isRetryableError(classifyFallbackError(err))) {
|
|
2369
3038
|
// Non-auto: walk the shared fallback chain (retryable errors only).
|
|
3039
|
+
//
|
|
3040
|
+
// Under strict model mode the walk is exactly the substitution the
|
|
3041
|
+
// user forbade: `strictModelMode()` short-circuits this branch, so a
|
|
3042
|
+
// pinned model that cannot answer surfaces its own error instead of
|
|
3043
|
+
// quietly continuing on a provider the user did not choose (A2 — the
|
|
3044
|
+
// same defect the loop engine had, fixed for dashboard + CLI chat
|
|
3045
|
+
// here). The `tryGenerate` call above already reports the raw error.
|
|
2370
3046
|
// Providers the admin policy rules out are collected here so the
|
|
2371
3047
|
// failure can name POLICY as the reason instead of implying the model
|
|
2372
3048
|
// was unreachable.
|
|
@@ -2391,6 +3067,10 @@ export class ChatCommand extends BaseCommand {
|
|
|
2391
3067
|
catch {
|
|
2392
3068
|
// Ordering is an optimization only.
|
|
2393
3069
|
}
|
|
3070
|
+
// The pair the turn was PINNED to. Captured before the walk because
|
|
3071
|
+
// the walk now installs what it serves (see below), and the warning
|
|
3072
|
+
// must still name the provider that actually failed.
|
|
3073
|
+
const pinnedType = session.type;
|
|
2394
3074
|
for (const fbType of ordered) {
|
|
2395
3075
|
if (fbType === session.type)
|
|
2396
3076
|
continue;
|
|
@@ -2428,7 +3108,40 @@ export class ChatCommand extends BaseCommand {
|
|
|
2428
3108
|
// the dashboard console and the gateway chat engine - looked
|
|
2429
3109
|
// dead.) Undefined = that provider's configured/adapter
|
|
2430
3110
|
// default, which tryGenerate resolves per attempt.
|
|
2431
|
-
|
|
3111
|
+
const fallbackModel = resolveEffectiveModel(resolved.type, undefined);
|
|
3112
|
+
const resp = await tryGenerate(resolved.provider, resolved.type, fallbackModel);
|
|
3113
|
+
// ── The pair that ANSWERED is the pair the turn ran on ───────
|
|
3114
|
+
// This walk used to serve the fallback and leave `session` still
|
|
3115
|
+
// naming the provider that had just failed. Everything the turn
|
|
3116
|
+
// then reported or learned read the WRONG provider against the
|
|
3117
|
+
// RIGHT model — one interlocking defect with four faces, all
|
|
3118
|
+
// measured:
|
|
3119
|
+
// • the session debug log's `turn.start` named the pinned
|
|
3120
|
+
// provider while its own header named the one that served
|
|
3121
|
+
// (`backend.provider: deepseek` with `turn.start {"provider":
|
|
3122
|
+
// "groq"}` — A5/D3);
|
|
3123
|
+
// • `servedRoute()` told the MODEL it was a different model
|
|
3124
|
+
// than the one answering;
|
|
3125
|
+
// • the response cache stored `provider: "groq"` beside
|
|
3126
|
+
// `model: "deepseek-flash"`;
|
|
3127
|
+
// • and worst, `recordRegistrySuccess(session.type,
|
|
3128
|
+
// session.model)` wrote a PAIR THAT CANNOT EXIST into the
|
|
3129
|
+
// model registry as `verified`. That is the self-poisoning:
|
|
3130
|
+
// `groq|deepseek-flash` and `groq|gemini-3.1-flash-lite` are
|
|
3131
|
+
// both sitting in the live registry as `telemetry`-verified,
|
|
3132
|
+
// and because `telemetry` outranks `spot-check` in
|
|
3133
|
+
// `preferredModelsFor`, the NEXT groq call picked the bogus
|
|
3134
|
+
// id as its adapter default, sent a foreign model name to
|
|
3135
|
+
// groq, and 404'd into the same fallback walk — each run
|
|
3136
|
+
// making the next one worse.
|
|
3137
|
+
// The auto branch above has always installed `next`; the pinned
|
|
3138
|
+
// branch is the one that drifted.
|
|
3139
|
+
session.type = resolved.type;
|
|
3140
|
+
session.provider = resolved.provider;
|
|
3141
|
+
if (fallbackModel)
|
|
3142
|
+
session.model = fallbackModel;
|
|
3143
|
+
logger.warn(` 🔀 ${pinnedType} was unavailable — answered from ${resolved.provider.name} (${fallbackModel ?? 'default'})`);
|
|
3144
|
+
return resp;
|
|
2432
3145
|
}
|
|
2433
3146
|
catch {
|
|
2434
3147
|
// Next fallback candidate.
|
|
@@ -2642,16 +3355,25 @@ export class ChatCommand extends BaseCommand {
|
|
|
2642
3355
|
.sort()
|
|
2643
3356
|
.join(','),
|
|
2644
3357
|
]);
|
|
2645
|
-
const
|
|
3358
|
+
const resolveFresh = () => getAutoRouter().resolve('chat', taskText, {
|
|
2646
3359
|
...buildAutoResolveOptions(this.configManager, {
|
|
2647
3360
|
verbose: envBuff('DEBUG') === 'true',
|
|
2648
3361
|
contextHintTokens: opts?.contextHintTokens,
|
|
2649
3362
|
}),
|
|
2650
3363
|
circuitBreakerStatus,
|
|
2651
3364
|
...(dispatch.taskIntentHint ? { taskIntentHint: dispatch.taskIntentHint } : {}),
|
|
2652
|
-
}, this.configManager)
|
|
3365
|
+
}, this.configManager);
|
|
3366
|
+
const decision = opts?.noCache === true
|
|
3367
|
+
? resolveFresh()
|
|
3368
|
+
: withRoutingCache(cacheSignature, 30_000, resolveFresh);
|
|
2653
3369
|
// G5 — record the routing snapshot now (the winner + why), so the reasoning
|
|
2654
3370
|
// trace for this turn can show the decision instead of a blank column.
|
|
3371
|
+
// P3 — the TASK this turn belongs to, as far as model continuity is
|
|
3372
|
+
// concerned. Kept on the instance because the turn's SUCCESS path (the only
|
|
3373
|
+
// place that knows a model actually DELIVERED) does not hold the routing
|
|
3374
|
+
// decision; intent+complexity is the same key for the original ask and for a
|
|
3375
|
+
// continuation of it, which is exactly the pair that must match.
|
|
3376
|
+
this.lastTaskSignature = taskSignature(dispatch.taskIntentHint ?? decision.taskProfile?.intent, String(decision.complexity));
|
|
2655
3377
|
this.lastRouteSnapshot = {
|
|
2656
3378
|
provider: decision.provider,
|
|
2657
3379
|
model: decision.model,
|
|
@@ -2662,6 +3384,9 @@ export class ChatCommand extends BaseCommand {
|
|
|
2662
3384
|
// and console can show WHY the pair was chosen, not just which pair.
|
|
2663
3385
|
agenticCapable: decision.agenticCapable,
|
|
2664
3386
|
overrideReason: decision.overrideReason,
|
|
3387
|
+
// Bundle 36 — the intent the bandit will bucket this outcome under, so a
|
|
3388
|
+
// later `nuvira rate bad` can correct the EXACT arm (see `verdict-routing.ts`).
|
|
3389
|
+
...(decision.taskProfile?.intent ? { taskIntent: decision.taskProfile.intent } : {}),
|
|
2665
3390
|
};
|
|
2666
3391
|
// Walk the ranked candidates (winner first) and return the first available
|
|
2667
3392
|
// provider — never a provider that lacks a key or endpoint. Providers that
|
|
@@ -2752,9 +3477,15 @@ export class ChatCommand extends BaseCommand {
|
|
|
2752
3477
|
crossPipelineMemory: false,
|
|
2753
3478
|
registryCheck: false,
|
|
2754
3479
|
});
|
|
3480
|
+
// B2-a — each candidate carries the score OF ITS OWN PAIR, when the pool had
|
|
3481
|
+
// a real measurement for it, plus the scale that score is on. It used to
|
|
3482
|
+
// carry nothing at all and every row was recorded with `decision.score`, so
|
|
3483
|
+
// three different pairs shared one provider-level number (measured live:
|
|
3484
|
+
// `0.43836864406779663` on gemini/gemma-4-26b-a4b-it,
|
|
3485
|
+
// openrouter/cohere/command-r7b-12-2024 and deepseek/deepseek-flash).
|
|
2755
3486
|
const chatCandidates = [];
|
|
2756
3487
|
const seenPairs = new Set();
|
|
2757
|
-
const pushCandidate = (prov, mdl) => {
|
|
3488
|
+
const pushCandidate = (prov, mdl, score, scoreBasis) => {
|
|
2758
3489
|
if (!prov || excluded.has(prov) || registryBlocked.has(prov))
|
|
2759
3490
|
return;
|
|
2760
3491
|
const model = mdl && mdl !== 'default' ? mdl : 'default';
|
|
@@ -2772,7 +3503,13 @@ export class ChatCommand extends BaseCommand {
|
|
|
2772
3503
|
// already ranks healthy models first, so the parked pick is only ever a
|
|
2773
3504
|
// last resort that the repair then fixes.
|
|
2774
3505
|
seenPairs.add(key);
|
|
2775
|
-
chatCandidates.push({
|
|
3506
|
+
chatCandidates.push({
|
|
3507
|
+
provider: prov,
|
|
3508
|
+
model,
|
|
3509
|
+
// Only a labelled measurement travels: an ordering placeholder must not
|
|
3510
|
+
// reach the audit trail as if it described this pair.
|
|
3511
|
+
...(scoreBasis ? { score, scoreBasis } : {}),
|
|
3512
|
+
});
|
|
2776
3513
|
};
|
|
2777
3514
|
// The pool itself is the SAME one the orchestrator/tool/sub-agent path
|
|
2778
3515
|
// walks: primary → model-first TIERED pool (same model on other providers,
|
|
@@ -2780,13 +3517,25 @@ export class ChatCommand extends BaseCommand {
|
|
|
2780
3517
|
// reserve) → ranked placeholders → config fallback. Chat used to build a
|
|
2781
3518
|
// shallower list here, which is why it reached strictly fewer models than
|
|
2782
3519
|
// the orchestrator could.
|
|
3520
|
+
// P3 — TASK-LEVEL CONTINUITY (RC7). A continuation carries no signal about
|
|
3521
|
+
// its own task, so the router re-decides from scratch and can land on a model
|
|
3522
|
+
// that has never served it (the live `resume` degraded from a working Gemini
|
|
3523
|
+
// model to one that returned nothing). Offer the pair that DID serve this
|
|
3524
|
+
// task FIRST. It is pushed through the same `pushCandidate` gate as every
|
|
3525
|
+
// other candidate, so a dead key, an active exclusion or a registry block
|
|
3526
|
+
// simply drops it and the normal pool is used instead.
|
|
3527
|
+
if (opts?.routingText) {
|
|
3528
|
+
const recalled = this.taskContinuity.recall(this.lastTaskSignature);
|
|
3529
|
+
if (recalled)
|
|
3530
|
+
pushCandidate(recalled.provider, recalled.model);
|
|
3531
|
+
}
|
|
2783
3532
|
const pool = buildDeepFailoverPool(decision, {
|
|
2784
3533
|
taskDescription: message,
|
|
2785
3534
|
complexity: decision.complexity,
|
|
2786
3535
|
configManager: this.configManager,
|
|
2787
3536
|
});
|
|
2788
3537
|
for (const c of pool)
|
|
2789
|
-
pushCandidate(c.provider, c.model);
|
|
3538
|
+
pushCandidate(c.provider, c.model, c.score, c.scoreBasis);
|
|
2790
3539
|
// Unique provider list (what callers use for their own failover) — derived
|
|
2791
3540
|
// from the pair list so it stays consistent with what is actually tried.
|
|
2792
3541
|
const candidates = [...new Set(chatCandidates.map((c) => c.provider))];
|
|
@@ -2811,7 +3560,12 @@ export class ChatCommand extends BaseCommand {
|
|
|
2811
3560
|
task: message,
|
|
2812
3561
|
verifyOnDemand: isMaxCapability(this.configManager),
|
|
2813
3562
|
})).model;
|
|
2814
|
-
// Record the actually-used route for the dashboard audit trail
|
|
3563
|
+
// Record the actually-used route for the dashboard audit trail.
|
|
3564
|
+
// B2-a — the score belongs to THE PAIR THIS ROW NAMES. `decision.score`
|
|
3565
|
+
// describes the router's own pick; a walked alternate has its own score
|
|
3566
|
+
// (`candidate.score`, a model-level measurement from the tiered pool) or
|
|
3567
|
+
// none at all (an ordering placeholder / a recalled pair). Writing the
|
|
3568
|
+
// decision's number here is what made a walk look like a tie.
|
|
2815
3569
|
recordRoutingDecision({
|
|
2816
3570
|
source: 'chat',
|
|
2817
3571
|
agentType: 'chat',
|
|
@@ -2819,7 +3573,9 @@ export class ChatCommand extends BaseCommand {
|
|
|
2819
3573
|
complexity: decision.complexity,
|
|
2820
3574
|
provider: candidate.provider,
|
|
2821
3575
|
model,
|
|
2822
|
-
|
|
3576
|
+
...(candidate.scoreBasis
|
|
3577
|
+
? { score: candidate.score, scoreBasis: candidate.scoreBasis }
|
|
3578
|
+
: {}),
|
|
2823
3579
|
agenticCapable: isAgenticCapableModel(model, candidate.provider),
|
|
2824
3580
|
overrideReason: decision.overrideReason,
|
|
2825
3581
|
...(opts?.fallbackFrom ? { fallbackFrom: opts.fallbackFrom } : {}),
|
|
@@ -2852,6 +3608,10 @@ export class ChatCommand extends BaseCommand {
|
|
|
2852
3608
|
// over).
|
|
2853
3609
|
const usableProvider = [decision.provider, ...decision.ranked.map((r) => r.provider)]
|
|
2854
3610
|
.find((p) => !isActiveExclusion(p) && !registryBlocked.has(p)) || decision.provider;
|
|
3611
|
+
// B2-a — this row names `decision.model` possibly on a DIFFERENT provider
|
|
3612
|
+
// than the decision's (`usableProvider`), so the decision's composite does
|
|
3613
|
+
// not describe it. The pair was never ranked as a whole; record it bare
|
|
3614
|
+
// rather than attaching a number that belongs to another provider.
|
|
2855
3615
|
recordRoutingDecision({
|
|
2856
3616
|
source: 'chat',
|
|
2857
3617
|
agentType: 'chat',
|
|
@@ -2859,7 +3619,6 @@ export class ChatCommand extends BaseCommand {
|
|
|
2859
3619
|
complexity: decision.complexity,
|
|
2860
3620
|
provider: usableProvider,
|
|
2861
3621
|
model: decision.model,
|
|
2862
|
-
score: decision.score,
|
|
2863
3622
|
agenticCapable: isAgenticCapableModel(decision.model, usableProvider),
|
|
2864
3623
|
overrideReason: decision.overrideReason,
|
|
2865
3624
|
...(opts?.fallbackFrom ? { fallbackFrom: opts.fallbackFrom } : {}),
|