muonroi-cli 1.8.4 → 1.9.0
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/LICENSE +17 -5
- package/dist/packages/agent-harness-core/src/driver.d.ts +27 -1
- package/dist/packages/agent-harness-core/src/driver.js +46 -0
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +64 -0
- package/dist/packages/agent-harness-core/src/event-tee.js +104 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +25 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +169 -21
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +90 -4
- package/dist/packages/agent-harness-core/src/protocol.js +15 -0
- package/dist/packages/agent-harness-core/src/visual-quality.d.ts +58 -0
- package/dist/packages/agent-harness-core/src/visual-quality.js +141 -0
- package/dist/packages/agent-harness-opentui/src/agent-mode.d.ts +6 -0
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +14 -1
- package/dist/packages/agent-harness-opentui/src/input-bridge.d.ts +2 -10
- package/dist/packages/agent-harness-opentui/src/input-bridge.js +103 -16
- package/dist/packages/agent-harness-opentui/src/install.d.ts +8 -0
- package/dist/packages/agent-harness-opentui/src/install.js +10 -0
- package/dist/packages/agent-harness-opentui/src/semantic.js +12 -10
- package/dist/packages/agent-harness-opentui/src/visual-capture.d.ts +56 -0
- package/dist/packages/agent-harness-opentui/src/visual-capture.js +103 -0
- package/dist/src/agent-harness/mock-model.d.ts +38 -0
- package/dist/src/agent-harness/mock-model.js +69 -3
- package/dist/src/agent-harness/test-spawn.js +31 -0
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/config/screen-providers.js +1 -1
- package/dist/src/cli/cost-forensics.d.ts +10 -0
- package/dist/src/cli/cost-forensics.js +18 -3
- package/dist/src/cli/keys-bundle.d.ts +1 -1
- package/dist/src/cli/keys-bundle.js +1 -1
- package/dist/src/cli/keys.d.ts +10 -47
- package/dist/src/cli/keys.js +31 -399
- package/dist/src/council/clarifier.d.ts +31 -3
- package/dist/src/council/clarifier.js +220 -32
- package/dist/src/council/context.js +49 -15
- package/dist/src/council/debate-checkpoint.d.ts +129 -0
- package/dist/src/council/debate-checkpoint.js +176 -0
- package/dist/src/council/debate-planner.js +54 -5
- package/dist/src/council/debate-summary.d.ts +25 -0
- package/dist/src/council/debate-summary.js +85 -0
- package/dist/src/council/debate.d.ts +169 -2
- package/dist/src/council/debate.js +1265 -135
- package/dist/src/council/index.d.ts +108 -1
- package/dist/src/council/index.js +670 -197
- package/dist/src/council/leader.d.ts +26 -0
- package/dist/src/council/leader.js +150 -9
- package/dist/src/council/llm.d.ts +94 -0
- package/dist/src/council/llm.js +348 -55
- package/dist/src/council/panel-select.d.ts +30 -0
- package/dist/src/council/panel-select.js +82 -0
- package/dist/src/council/planner.js +40 -0
- package/dist/src/council/preflight.d.ts +17 -0
- package/dist/src/council/preflight.js +50 -2
- package/dist/src/council/prompts.d.ts +39 -4
- package/dist/src/council/prompts.js +256 -69
- package/dist/src/council/stance-recall.d.ts +42 -0
- package/dist/src/council/stance-recall.js +57 -0
- package/dist/src/council/strip-think.d.ts +17 -0
- package/dist/src/council/strip-think.js +33 -0
- package/dist/src/council/types.d.ts +138 -0
- package/dist/src/ee/artifact-cache.d.ts +16 -0
- package/dist/src/ee/artifact-cache.js +32 -0
- package/dist/src/ee/auth.d.ts +20 -0
- package/dist/src/ee/auth.js +54 -2
- package/dist/src/ee/bridge.d.ts +10 -0
- package/dist/src/ee/bridge.js +58 -0
- package/dist/src/ee/client.js +109 -21
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/ee/export-transcripts.d.ts +1 -0
- package/dist/src/ee/export-transcripts.js +8 -10
- package/dist/src/ee/extract-session.js +29 -0
- package/dist/src/ee/extract-style.d.ts +58 -0
- package/dist/src/ee/extract-style.js +270 -0
- package/dist/src/ee/recall-ledger.d.ts +9 -0
- package/dist/src/ee/recall-ledger.js +3 -0
- package/dist/src/ee/scope.d.ts +1 -0
- package/dist/src/ee/scope.js +26 -1
- package/dist/src/ee/search.d.ts +7 -0
- package/dist/src/ee/search.js +24 -0
- package/dist/src/ee/transcript-emit.js +2 -0
- package/dist/src/ee/types.d.ts +22 -0
- package/dist/src/ee/who-am-i-brain.d.ts +35 -0
- package/dist/src/ee/who-am-i-brain.js +220 -0
- package/dist/src/ee/who-am-i.d.ts +10 -3
- package/dist/src/ee/who-am-i.js +12 -0
- package/dist/src/ee/workflow-event.d.ts +48 -0
- package/dist/src/ee/workflow-event.js +81 -0
- package/dist/src/flow/compaction/compress.d.ts +3 -3
- package/dist/src/flow/compaction/compress.js +58 -8
- package/dist/src/flow/compaction/extract.d.ts +4 -7
- package/dist/src/flow/compaction/extract.js +50 -10
- package/dist/src/flow/compaction/index.d.ts +14 -1
- package/dist/src/flow/compaction/index.js +96 -3
- package/dist/src/flow/compaction/input-guard.d.ts +24 -0
- package/dist/src/flow/compaction/input-guard.js +43 -0
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/flow/fold-planning.d.ts +36 -0
- package/dist/src/flow/fold-planning.js +83 -0
- package/dist/src/flow/hierarchy.d.ts +146 -0
- package/dist/src/flow/hierarchy.js +427 -0
- package/dist/src/flow/index.d.ts +1 -0
- package/dist/src/flow/index.js +2 -0
- package/dist/src/flow/run-artifacts.d.ts +102 -0
- package/dist/src/flow/run-artifacts.js +208 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/assessment-schema.d.ts +44 -0
- package/dist/src/gsd/assessment-schema.js +134 -0
- package/dist/src/gsd/capability-registry.d.ts +45 -0
- package/dist/src/gsd/capability-registry.js +337 -0
- package/dist/src/gsd/complexity-assessor.d.ts +39 -0
- package/dist/src/gsd/complexity-assessor.js +152 -0
- package/dist/src/gsd/config-bridge.d.ts +7 -0
- package/dist/src/gsd/config-bridge.js +114 -0
- package/dist/src/gsd/config-loader.d.ts +27 -0
- package/dist/src/gsd/config-loader.js +50 -0
- package/dist/src/gsd/council-context.d.ts +44 -0
- package/dist/src/gsd/council-context.js +114 -0
- package/dist/src/gsd/ee-closure.d.ts +28 -0
- package/dist/src/gsd/ee-closure.js +49 -0
- package/dist/src/gsd/flags.d.ts +66 -0
- package/dist/src/gsd/flags.js +102 -0
- package/dist/src/gsd/gsd-dispatch.d.ts +58 -0
- package/dist/src/gsd/gsd-dispatch.js +131 -0
- package/dist/src/gsd/gsd-runtime.d.ts +22 -0
- package/dist/src/gsd/gsd-runtime.js +37 -0
- package/dist/src/gsd/host-adapter.d.ts +11 -0
- package/dist/src/gsd/host-adapter.js +29 -0
- package/dist/src/gsd/index.d.ts +24 -1
- package/dist/src/gsd/index.js +27 -0
- package/dist/src/gsd/loop-host-contract.d.ts +21 -0
- package/dist/src/gsd/loop-host-contract.js +39 -0
- package/dist/src/gsd/loop-host.d.ts +69 -0
- package/dist/src/gsd/loop-host.js +245 -0
- package/dist/src/gsd/loop-resolver.d.ts +36 -0
- package/dist/src/gsd/loop-resolver.js +79 -0
- package/dist/src/gsd/model-tier.d.ts +13 -0
- package/dist/src/gsd/model-tier.js +45 -0
- package/dist/src/gsd/mutation-gate.d.ts +16 -0
- package/dist/src/gsd/mutation-gate.js +41 -0
- package/dist/src/gsd/native-roadmap.d.ts +89 -0
- package/dist/src/gsd/native-roadmap.js +343 -0
- package/dist/src/gsd/native-state.d.ts +47 -0
- package/dist/src/gsd/native-state.js +220 -0
- package/dist/src/gsd/paths.d.ts +23 -0
- package/dist/src/gsd/paths.js +66 -0
- package/dist/src/gsd/phase-dag.d.ts +12 -0
- package/dist/src/gsd/phase-dag.js +94 -0
- package/dist/src/gsd/phase-sync.d.ts +42 -0
- package/dist/src/gsd/phase-sync.js +321 -0
- package/dist/src/gsd/pil-gate-context.d.ts +13 -0
- package/dist/src/gsd/pil-gate-context.js +64 -0
- package/dist/src/gsd/pil-gate-critic.d.ts +19 -0
- package/dist/src/gsd/pil-gate-critic.js +74 -0
- package/dist/src/gsd/plan-council-prompts.d.ts +25 -0
- package/dist/src/gsd/plan-council-prompts.js +79 -0
- package/dist/src/gsd/plan-council.d.ts +44 -0
- package/dist/src/gsd/plan-council.js +283 -0
- package/dist/src/gsd/plan-gate-vocabulary.d.ts +40 -0
- package/dist/src/gsd/plan-gate-vocabulary.js +64 -0
- package/dist/src/gsd/product-workspace.d.ts +13 -0
- package/dist/src/gsd/product-workspace.js +124 -0
- package/dist/src/gsd/ship-bridge.d.ts +25 -0
- package/dist/src/gsd/ship-bridge.js +65 -0
- package/dist/src/gsd/state-document.d.ts +40 -0
- package/dist/src/gsd/state-document.js +163 -0
- package/dist/src/gsd/verdict-schema.d.ts +39 -0
- package/dist/src/gsd/verdict-schema.js +144 -0
- package/dist/src/gsd/verify-context.d.ts +22 -0
- package/dist/src/gsd/verify-context.js +27 -0
- package/dist/src/gsd/verify-council-prompts.d.ts +19 -0
- package/dist/src/gsd/verify-council-prompts.js +85 -0
- package/dist/src/gsd/verify-council.d.ts +25 -0
- package/dist/src/gsd/verify-council.js +119 -0
- package/dist/src/gsd/verify-gate-vocabulary.d.ts +25 -0
- package/dist/src/gsd/verify-gate-vocabulary.js +46 -0
- package/dist/src/gsd/workflow-engine.d.ts +60 -0
- package/dist/src/gsd/workflow-engine.js +207 -0
- package/dist/src/gsd/workflow-tools.d.ts +13 -0
- package/dist/src/gsd/workflow-tools.js +277 -0
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/hooks/index.js +1 -1
- package/dist/src/index.js +172 -270
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/maintain/pr-builder.js +23 -13
- package/dist/src/mcp/auto-setup.js +57 -32
- package/dist/src/mcp/client-pool.js +44 -16
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/research-onboarding.js +8 -7
- package/dist/src/mcp/runtime.js +34 -2
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog-client.d.ts +87 -0
- package/dist/src/models/catalog-client.js +105 -38
- package/dist/src/models/catalog.json +528 -265
- package/dist/src/models/registry.d.ts +22 -7
- package/dist/src/models/registry.js +73 -10
- package/dist/src/ops/doctor.js +1 -1
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/auto-commit.js +1 -1
- package/dist/src/orchestrator/batch-turn-runner.js +2 -2
- package/dist/src/orchestrator/cache-prefix.d.ts +67 -0
- package/dist/src/orchestrator/cache-prefix.js +83 -0
- package/dist/src/orchestrator/compact-request.d.ts +32 -0
- package/dist/src/orchestrator/compact-request.js +41 -0
- package/dist/src/orchestrator/compaction.d.ts +12 -3
- package/dist/src/orchestrator/compaction.js +35 -15
- package/dist/src/orchestrator/council-manager.d.ts +12 -3
- package/dist/src/orchestrator/council-manager.js +74 -32
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/cross-turn-dedup.d.ts +6 -0
- package/dist/src/orchestrator/cross-turn-dedup.js +22 -1
- package/dist/src/orchestrator/error-utils.d.ts +29 -0
- package/dist/src/orchestrator/error-utils.js +132 -24
- package/dist/src/orchestrator/grounding-check.js +39 -1
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +268 -41
- package/dist/src/orchestrator/orchestrator.d.ts +64 -3
- package/dist/src/orchestrator/orchestrator.js +823 -120
- package/dist/src/orchestrator/preprocessor.js +3 -3
- package/dist/src/orchestrator/proactive-compact-detector.d.ts +26 -0
- package/dist/src/orchestrator/proactive-compact-detector.js +36 -0
- package/dist/src/orchestrator/prompts.js +17 -17
- package/dist/src/orchestrator/reactive-delegation.d.ts +39 -0
- package/dist/src/orchestrator/reactive-delegation.js +59 -0
- package/dist/src/orchestrator/retry-classifier.d.ts +2 -1
- package/dist/src/orchestrator/retry-classifier.js +46 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +50 -0
- package/dist/src/orchestrator/safety-intercept.js +62 -0
- package/dist/src/orchestrator/scope-reminder.js +1 -1
- package/dist/src/orchestrator/session-experience.d.ts +2 -1
- package/dist/src/orchestrator/session-experience.js +2 -1
- package/dist/src/orchestrator/should-run-gate.d.ts +5 -0
- package/dist/src/orchestrator/should-run-gate.js +18 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +31 -3
- package/dist/src/orchestrator/stall-watchdog.js +65 -10
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +115 -49
- package/dist/src/orchestrator/sub-agent-cap.d.ts +13 -1
- package/dist/src/orchestrator/sub-agent-cap.js +16 -1
- package/dist/src/orchestrator/sub-agent-model-tier.d.ts +13 -1
- package/dist/src/orchestrator/sub-agent-model-tier.js +16 -2
- package/dist/src/orchestrator/subagent-compactor.d.ts +53 -1
- package/dist/src/orchestrator/subagent-compactor.js +126 -15
- package/dist/src/orchestrator/tool-engine.d.ts +41 -0
- package/dist/src/orchestrator/tool-engine.js +846 -66
- package/dist/src/orchestrator/tool-limit-auto-recover.d.ts +22 -0
- package/dist/src/orchestrator/tool-limit-auto-recover.js +30 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +19 -0
- package/dist/src/orchestrator/turn-watchdog.d.ts +44 -0
- package/dist/src/orchestrator/turn-watchdog.js +84 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +5 -1
- package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
- package/dist/src/pil/cheap-model-workbooks.js +1 -1
- package/dist/src/pil/discovery-types.d.ts +1 -0
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +18 -13
- package/dist/src/pil/layer1-intent.d.ts +18 -6
- package/dist/src/pil/layer1-intent.js +66 -757
- package/dist/src/pil/layer15-context-scan.js +15 -1
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/layer3-ee-injection.js +23 -8
- package/dist/src/pil/layer4-gsd.js +69 -16
- package/dist/src/pil/layer5-context.js +7 -3
- package/dist/src/pil/layer6-output.d.ts +23 -0
- package/dist/src/pil/layer6-output.js +5 -1
- package/dist/src/pil/llm-classify.d.ts +111 -5
- package/dist/src/pil/llm-classify.js +421 -189
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +8 -0
- package/dist/src/pil/pipeline.js +36 -2
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/pil/response-tools.js +5 -3
- package/dist/src/pil/schema.d.ts +1 -0
- package/dist/src/pil/schema.js +2 -0
- package/dist/src/pil/types.d.ts +18 -0
- package/dist/src/playbook/directives.d.ts +4 -0
- package/dist/src/playbook/directives.js +17 -5
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/backlog-builder.d.ts +14 -1
- package/dist/src/product-loop/backlog-builder.js +30 -6
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-context-format.js +3 -1
- package/dist/src/product-loop/discovery-ecosystem.js +4 -1
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +60 -12
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +19 -2
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/ideal-trace.d.ts +7 -0
- package/dist/src/product-loop/ideal-trace.js +64 -0
- package/dist/src/product-loop/index.d.ts +13 -1
- package/dist/src/product-loop/index.js +340 -52
- package/dist/src/product-loop/loop-driver.d.ts +7 -0
- package/dist/src/product-loop/loop-driver.js +330 -106
- package/dist/src/product-loop/phase-plan.d.ts +21 -0
- package/dist/src/product-loop/phase-plan.js +81 -6
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +39 -12
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +173 -0
- package/dist/src/product-loop/sprint-runner.js +863 -19
- package/dist/src/product-loop/types.d.ts +61 -5
- package/dist/src/providers/adapter.d.ts +1 -1
- package/dist/src/providers/adapter.js +3 -4
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/browser-flow.d.ts +1 -1
- package/dist/src/providers/auth/browser-flow.js +1 -1
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +15 -1
- package/dist/src/providers/auth/registry.js +0 -34
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -67
- package/dist/src/providers/auth/types.d.ts +9 -1
- package/dist/src/providers/auth/types.js +1 -1
- package/dist/src/providers/capabilities.d.ts +24 -5
- package/dist/src/providers/capabilities.js +42 -24
- package/dist/src/providers/endpoints.d.ts +2 -2
- package/dist/src/providers/endpoints.js +11 -10
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +22 -18
- package/dist/src/providers/keychain.js +127 -140
- package/dist/src/providers/mcp-vision-bridge.js +56 -146
- package/dist/src/providers/openai-compatible.js +8 -1
- package/dist/src/providers/pricing.d.ts +2 -2
- package/dist/src/providers/pricing.js +3 -13
- package/dist/src/providers/runtime.d.ts +43 -3
- package/dist/src/providers/runtime.js +88 -14
- package/dist/src/providers/strategies/base.strategy.d.ts +16 -0
- package/dist/src/providers/strategies/base.strategy.js +24 -1
- package/dist/src/providers/strategies/{siliconflow.strategy.d.ts → opencode-go.strategy.d.ts} +3 -3
- package/dist/src/providers/strategies/opencode-go.strategy.js +83 -0
- package/dist/src/providers/strategies/registry.js +4 -4
- package/dist/src/providers/strategies/thinking-mode.d.ts +109 -0
- package/dist/src/providers/strategies/thinking-mode.js +288 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/strategies/zai.strategy.d.ts +14 -0
- package/dist/src/providers/strategies/zai.strategy.js +44 -0
- package/dist/src/providers/types.d.ts +5 -6
- package/dist/src/providers/types.js +2 -2
- package/dist/src/providers/vision-backend.d.ts +47 -0
- package/dist/src/providers/vision-backend.js +258 -0
- package/dist/src/providers/vision-proxy.d.ts +22 -9
- package/dist/src/providers/vision-proxy.js +63 -132
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/providers/wire-debug.js +95 -0
- package/dist/src/router/decide.d.ts +13 -0
- package/dist/src/router/decide.js +138 -36
- package/dist/src/router/peak-hour.d.ts +38 -0
- package/dist/src/router/peak-hour.js +107 -0
- package/dist/src/router/step-router.js +3 -2
- package/dist/src/router/warm.js +4 -5
- package/dist/src/scaffold/continuation-prompt.d.ts +11 -0
- package/dist/src/scaffold/continuation-prompt.js +26 -0
- package/dist/src/scaffold/point-to-existing.d.ts +21 -0
- package/dist/src/scaffold/point-to-existing.js +25 -0
- package/dist/src/self-qa/agentic-loop.js +6 -5
- package/dist/src/{ui/state → state}/active-run.d.ts +19 -0
- package/dist/src/{ui/state → state}/active-run.js +21 -0
- package/dist/src/{ui/status-bar/store.d.ts → state/status-bar-store.d.ts} +4 -1
- package/dist/src/{ui/status-bar/store.js → state/status-bar-store.js} +12 -9
- package/dist/src/state/turn-trace.d.ts +43 -0
- package/dist/src/state/turn-trace.js +32 -0
- package/dist/src/storage/db.js +2 -1
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.d.ts +1 -1
- package/dist/src/storage/migrations.js +71 -1
- package/dist/src/storage/sessions.d.ts +28 -10
- package/dist/src/storage/sessions.js +78 -21
- package/dist/src/storage/transcript-view.js +1 -1
- package/dist/src/storage/transcript.d.ts +51 -0
- package/dist/src/storage/transcript.js +340 -15
- package/dist/src/tools/file.d.ts +15 -0
- package/dist/src/tools/file.js +32 -0
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +81 -1
- package/dist/src/tools/registry.d.ts +20 -0
- package/dist/src/tools/registry.js +576 -23
- package/dist/src/tools/research.d.ts +29 -0
- package/dist/src/tools/research.js +233 -0
- package/dist/src/types/index.d.ts +147 -4
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/cards/product-status-card.js +1 -1
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/bubble-body-guard.d.ts +27 -0
- package/dist/src/ui/components/bubble-body-guard.js +50 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/context-rail.d.ts +26 -0
- package/dist/src/ui/components/context-rail.js +33 -0
- package/dist/src/ui/components/council-conclusion-card.d.ts +73 -0
- package/dist/src/ui/components/council-conclusion-card.js +420 -0
- package/dist/src/ui/components/council-debate-pill.d.ts +36 -0
- package/dist/src/ui/components/council-debate-pill.js +34 -0
- package/dist/src/ui/components/council-info-card.js +2 -2
- package/dist/src/ui/components/council-leader-bubble.d.ts +10 -3
- package/dist/src/ui/components/council-leader-bubble.js +21 -11
- package/dist/src/ui/components/council-message-bubble.d.ts +15 -2
- package/dist/src/ui/components/council-message-bubble.js +16 -15
- package/dist/src/ui/components/council-phase-timeline.d.ts +9 -1
- package/dist/src/ui/components/council-phase-timeline.js +66 -17
- package/dist/src/ui/components/council-placeholder-bubble.d.ts +16 -9
- package/dist/src/ui/components/council-placeholder-bubble.js +32 -29
- package/dist/src/ui/components/council-question-card.js +13 -12
- package/dist/src/ui/components/council-rail-rounds.d.ts +26 -0
- package/dist/src/ui/components/council-rail-rounds.js +57 -0
- package/dist/src/ui/components/council-round-group.d.ts +38 -0
- package/dist/src/ui/components/council-round-group.js +88 -0
- package/dist/src/ui/components/council-status-list.d.ts +3 -1
- package/dist/src/ui/components/council-status-list.js +36 -24
- package/dist/src/ui/components/council-synthesis-banner.d.ts +7 -2
- package/dist/src/ui/components/council-synthesis-banner.js +20 -5
- package/dist/src/ui/components/halt-recovery-card.js +9 -5
- package/dist/src/ui/components/jump-to-latest-pill.d.ts +11 -0
- package/dist/src/ui/components/jump-to-latest-pill.js +14 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/prompt-box.js +18 -16
- package/dist/src/ui/components/session-tree-card.d.ts +14 -0
- package/dist/src/ui/components/session-tree-card.js +46 -0
- package/dist/src/ui/components/slash-inline-menu.d.ts +12 -0
- package/dist/src/ui/components/slash-inline-menu.js +26 -5
- package/dist/src/ui/components/task-list-panel.d.ts +14 -1
- package/dist/src/ui/components/task-list-panel.js +22 -2
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +4 -2
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/hooks/use-session-picker.d.ts +3 -3
- package/dist/src/ui/mcp-modal.js +2 -4
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/connect-modal.js +4 -3
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/modals/session-picker-modal.d.ts +2 -2
- package/dist/src/ui/modals/session-picker-modal.js +3 -5
- package/dist/src/ui/picker-providers.d.ts +1 -1
- package/dist/src/ui/picker-providers.js +1 -1
- package/dist/src/ui/primitives/index.d.ts +1 -0
- package/dist/src/ui/primitives/index.js +2 -0
- package/dist/src/ui/primitives/semantic-primitives.d.ts +76 -0
- package/dist/src/ui/primitives/semantic-primitives.js +81 -0
- package/dist/src/ui/slash/compact.js +5 -7
- package/dist/src/ui/slash/cost.js +1 -1
- package/dist/src/ui/slash/council.js +19 -1
- package/dist/src/ui/slash/debug.d.ts +3 -31
- package/dist/src/ui/slash/debug.js +9 -20
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/ideal.d.ts +6 -2
- package/dist/src/ui/slash/ideal.js +97 -7
- package/dist/src/ui/slash/menu-items.d.ts +7 -0
- package/dist/src/ui/slash/menu-items.js +23 -20
- package/dist/src/ui/slash/registry.d.ts +2 -0
- package/dist/src/ui/slash/registry.js +4 -0
- package/dist/src/ui/status-bar/cache-hit.d.ts +6 -0
- package/dist/src/ui/status-bar/cache-hit.js +9 -0
- package/dist/src/ui/status-bar/index.d.ts +1 -1
- package/dist/src/ui/status-bar/index.js +7 -3
- package/dist/src/ui/status-bar/usd-meter.d.ts +5 -4
- package/dist/src/ui/status-bar/usd-meter.js +6 -4
- package/dist/src/ui/theme.d.ts +1 -0
- package/dist/src/ui/theme.js +2 -0
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/format.d.ts +14 -0
- package/dist/src/ui/utils/format.js +23 -3
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/usage/downgrade.js +2 -2
- package/dist/src/usage/product-ledger.js +2 -2
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/install-manager.js +2 -1
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/logger.js +2 -2
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/permission-mode.js +5 -3
- package/dist/src/utils/redactor.js +1 -1
- package/dist/src/utils/settings.d.ts +180 -5
- package/dist/src/utils/settings.js +271 -31
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/visible-retry.d.ts +11 -0
- package/dist/src/utils/visible-retry.js +10 -1
- package/dist/src/verify/entrypoint.d.ts +1 -1
- package/dist/src/verify/entrypoint.js +52 -17
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/dist/src/verify/recipes.d.ts +13 -0
- package/dist/src/verify/recipes.js +15 -0
- package/package.json +134 -132
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- package/dist/src/mcp/ee-tools.js +0 -193
- package/dist/src/providers/auth/gcloud.d.ts +0 -28
- package/dist/src/providers/auth/gcloud.js +0 -102
- package/dist/src/providers/auth/gemini-oauth.d.ts +0 -82
- package/dist/src/providers/auth/gemini-oauth.js +0 -472
- package/dist/src/providers/gemini.d.ts +0 -11
- package/dist/src/providers/gemini.js +0 -45
- package/dist/src/providers/siliconflow-sse-repair.d.ts +0 -58
- package/dist/src/providers/siliconflow-sse-repair.js +0 -177
- package/dist/src/providers/strategies/google.strategy.d.ts +0 -22
- package/dist/src/providers/strategies/google.strategy.js +0 -174
- package/dist/src/providers/strategies/siliconflow.strategy.js +0 -29
- package/dist/src/ui/containers/chat-feed.d.ts +0 -40
- package/dist/src/ui/containers/chat-feed.js +0 -66
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { getModelInfo } from "../models/registry.js";
|
|
3
|
+
import { detectProviderForModel } from "../providers/runtime.js";
|
|
4
|
+
import { getIsolatedTaskDeadlineMs, withDeadlineRace } from "../utils/llm-deadline.js";
|
|
5
|
+
import { getCouncilLanguage } from "../utils/settings.js";
|
|
6
|
+
import { buildDebateCheckpoint, checkpointMatches, deleteDebateCheckpoint, restoreExchangeLogs, writeDebateCheckpoint, } from "./debate-checkpoint.js";
|
|
7
|
+
import { resolveDebateSummary } from "./debate-summary.js";
|
|
2
8
|
import { pickCouncilTaskModel } from "./leader.js";
|
|
3
|
-
import { tracedAsync, tracedGenerate } from "./llm.js";
|
|
9
|
+
import { councilStreamLivenessReader, tracedAsync, tracedGenerate } from "./llm.js";
|
|
4
10
|
import { phaseDone, phaseStart } from "./phase-events.js";
|
|
5
11
|
import { buildFollowupPrompt, buildLeaderEvaluationPrompt, buildOpeningPrompt, buildResponsePrompt, buildRoundSummaryPrompt, } from "./prompts.js";
|
|
6
12
|
/**
|
|
@@ -220,7 +226,149 @@ async function openingWithRetry(llm, model, system, prompt) {
|
|
|
220
226
|
}
|
|
221
227
|
return { text: "", attempts: MAX_OPENING_ATTEMPTS, error: lastError };
|
|
222
228
|
}
|
|
223
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Pick a debate fallback model on a DIFFERENT provider than `failedModel`.
|
|
231
|
+
*
|
|
232
|
+
* When a participant's model fails BOTH same-model attempts (e.g. opencode-go
|
|
233
|
+
* proxy overload "Upstream request failed", or a param-reject that survives the
|
|
234
|
+
* adapter-level degrade), retrying the same endpoint just fails again — live
|
|
235
|
+
* session renders showed the same speaker dropped 3× in a row, silently
|
|
236
|
+
* shrinking the debate. Falling back to any pooled council model on another
|
|
237
|
+
* provider keeps that voice in the discussion. Returns undefined when every
|
|
238
|
+
* pooled model resolves to the same provider (nothing to fall back to).
|
|
239
|
+
*/
|
|
240
|
+
function pickDebateFallbackModel(failedModel, pool) {
|
|
241
|
+
let failedProvider;
|
|
242
|
+
try {
|
|
243
|
+
failedProvider = detectProviderForModel(failedModel);
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
failedProvider = undefined;
|
|
247
|
+
}
|
|
248
|
+
for (const candidate of pool) {
|
|
249
|
+
if (candidate === failedModel)
|
|
250
|
+
continue;
|
|
251
|
+
let candidateProvider;
|
|
252
|
+
try {
|
|
253
|
+
candidateProvider = detectProviderForModel(candidate);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
if (candidateProvider && candidateProvider !== failedProvider)
|
|
259
|
+
return candidate;
|
|
260
|
+
}
|
|
261
|
+
return undefined;
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* Marker the CouncilLLM.research catch-block embeds in its return string when a
|
|
265
|
+
* provider crashes (it returns a string, never throws — see llm.ts:737). Used
|
|
266
|
+
* to detect a failed research pass so we can fall back to another provider.
|
|
267
|
+
*/
|
|
268
|
+
const RESEARCH_FAILED_MARKER = "[Research failed:";
|
|
269
|
+
/**
|
|
270
|
+
* Runs council research on `primaryModel`; if the provider crashes (the
|
|
271
|
+
* `[Research failed: …]` marker), retries ONCE on a pooled model resolving to a
|
|
272
|
+
* DIFFERENT provider before giving up. Mirrors {@link pickDebateFallbackModel}.
|
|
273
|
+
*
|
|
274
|
+
* Motivating evidence: session de4bafe5ecb7 routed research to opencode-go
|
|
275
|
+
* (Console Go "Upstream request failed") with no fallback. Research returned
|
|
276
|
+
* only the failure marker → participants had zero citations to tag → evidence
|
|
277
|
+
* density hard-zeroed → the debate scored "Low confidence 0%" despite being
|
|
278
|
+
* substantive. debateWithRetry already had cross-provider fallback; research
|
|
279
|
+
* did not.
|
|
280
|
+
*/
|
|
281
|
+
/**
|
|
282
|
+
* #2 — run the research phase in an ISOLATED explore sub-agent (near-empty
|
|
283
|
+
* context, budget-capped, independent compaction) instead of the in-process
|
|
284
|
+
* 15-step generateText that accretes tool clutter into the council thread.
|
|
285
|
+
* Returns the findings string on success; a `[Research failed: …]` marker string
|
|
286
|
+
* on failure so `researchWithFallback` transparently falls back to `llm.research`.
|
|
287
|
+
*/
|
|
288
|
+
async function runResearchIsolated(runIsolatedTask, model, topic, conversationContext, traceCb, options) {
|
|
289
|
+
traceCb(`[research] isolated explore sub-agent via ${model}`);
|
|
290
|
+
// Keep the child near-empty — pass only the question + a bounded context slice
|
|
291
|
+
// (the whole point is to NOT inherit the council's growing transcript).
|
|
292
|
+
const ctxSlice = conversationContext.length > 4000
|
|
293
|
+
? `${conversationContext.slice(0, 4000)}\n…[context truncated]`
|
|
294
|
+
: conversationContext;
|
|
295
|
+
const sourcePref = options.internetFirst
|
|
296
|
+
? "This is a greenfield task with little/no local source: PREFER web + documentation sources (use web_search / fetch tools if available), then any local files."
|
|
297
|
+
: "PREFER grounding every claim in THIS repo's code — cite concrete file:line. Use web sources only to fill genuine gaps.";
|
|
298
|
+
const prompt = `You are grounding a council debate with EVIDENCE. Research the question below and return concise, sourced findings.\n\n` +
|
|
299
|
+
`## Question\n${topic}\n\n` +
|
|
300
|
+
`## Debate context\n${ctxSlice}\n\n` +
|
|
301
|
+
`## Instructions\n${sourcePref}\n` +
|
|
302
|
+
`Return a compact "## Research Findings" section: only the concrete facts that bear on the question, each with its source (file:line or URL). ` +
|
|
303
|
+
`Be terse and factual — no opinions, no recommendations. If you cannot find solid evidence, say so in one line.`;
|
|
304
|
+
try {
|
|
305
|
+
const result = await withDeadlineRace(() => runIsolatedTask({
|
|
306
|
+
agent: "explore",
|
|
307
|
+
description: `Council research: ${topic.slice(0, 60)}`,
|
|
308
|
+
prompt,
|
|
309
|
+
// Pin the research-role model; bypasses the parent-tier cap in StreamRunner.
|
|
310
|
+
modelId: model,
|
|
311
|
+
}), getIsolatedTaskDeadlineMs(), "council-research");
|
|
312
|
+
if (result.success && result.output?.trim())
|
|
313
|
+
return result.output.trim();
|
|
314
|
+
traceCb(`[research] isolated sub-agent produced nothing: ${result.error ?? "no output"}`);
|
|
315
|
+
return `[Research failed: ${result.error ?? "isolated sub-agent produced no output"}]`;
|
|
316
|
+
}
|
|
317
|
+
catch (err) {
|
|
318
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
319
|
+
traceCb(`[research] isolated sub-agent threw: ${msg}`);
|
|
320
|
+
return `[Research failed: ${msg}]`;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
export async function researchWithFallback(llm, primaryModel, topic, conversationContext, signal, traceCb, options, fallbackPool, runIsolatedTask) {
|
|
324
|
+
// #2 — prefer the isolated explore sub-agent when the orchestrator wired one.
|
|
325
|
+
// On any failure (marker) we transparently fall through to the legacy
|
|
326
|
+
// in-process research below, preserving resilience for headless/degraded runs.
|
|
327
|
+
if (runIsolatedTask && !signal?.aborted) {
|
|
328
|
+
const iso = await runResearchIsolated(runIsolatedTask, primaryModel, topic, conversationContext, traceCb, options);
|
|
329
|
+
if (!iso.includes(RESEARCH_FAILED_MARKER))
|
|
330
|
+
return iso;
|
|
331
|
+
traceCb("[research] isolated sub-agent unavailable — falling back to in-process research");
|
|
332
|
+
}
|
|
333
|
+
const primary = await llm.research(primaryModel, topic, conversationContext, signal, traceCb, options);
|
|
334
|
+
if (!primary.includes(RESEARCH_FAILED_MARKER) || signal?.aborted)
|
|
335
|
+
return primary;
|
|
336
|
+
const fallbackModel = pickDebateFallbackModel(primaryModel, fallbackPool);
|
|
337
|
+
if (!fallbackModel)
|
|
338
|
+
return primary;
|
|
339
|
+
traceCb(`[research] ${primaryModel} failed; retrying via ${fallbackModel} (different provider)`);
|
|
340
|
+
try {
|
|
341
|
+
const fb = await llm.research(fallbackModel, topic, conversationContext, signal, traceCb, options);
|
|
342
|
+
if (!fb.includes(RESEARCH_FAILED_MARKER)) {
|
|
343
|
+
traceCb(`[research] recovered via fallback ${fallbackModel}`);
|
|
344
|
+
return fb;
|
|
345
|
+
}
|
|
346
|
+
// Both providers failed — surface the primary's marker so the existing
|
|
347
|
+
// "research produced nothing" rendering still fires.
|
|
348
|
+
return primary;
|
|
349
|
+
}
|
|
350
|
+
catch (err) {
|
|
351
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
352
|
+
traceCb(`[research] fallback ${fallbackModel} also failed: ${msg}`);
|
|
353
|
+
return primary;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Times every `debateWithRetryInner` exit path — success, retry, fallback and
|
|
358
|
+
* total failure all cost wall-clock, and a slow FAILING speaker is exactly the
|
|
359
|
+
* case worth seeing. Wrapping is why `durationMs` cannot drift out of sync with
|
|
360
|
+
* the four separate returns inside.
|
|
361
|
+
*
|
|
362
|
+
* The number is per-SPEAKER. The only timed council rows today are
|
|
363
|
+
* `debate_complete` (whole debate: 87s..1128s measured) and `council_summary`
|
|
364
|
+
* — aggregates that cannot name the model responsible.
|
|
365
|
+
*/
|
|
366
|
+
async function debateWithRetry(llm, model, system, prompt, signal, traceCb, toolBudget, fallbackPool = []) {
|
|
367
|
+
const startedAt = Date.now();
|
|
368
|
+
const r = await debateWithRetryInner(llm, model, system, prompt, signal, traceCb, toolBudget, fallbackPool);
|
|
369
|
+
return { ...r, durationMs: Date.now() - startedAt };
|
|
370
|
+
}
|
|
371
|
+
async function debateWithRetryInner(llm, model, system, prompt, signal, traceCb, toolBudget, fallbackPool = []) {
|
|
224
372
|
// Respect the circuit breaker — once a model has tripped, it stays
|
|
225
373
|
// tool-disabled for the rest of this council run.
|
|
226
374
|
const allowTools = debateAllowsTools(model) && !toolBudget.disabled.has(model);
|
|
@@ -250,31 +398,69 @@ async function debateWithRetry(llm, model, system, prompt, signal, traceCb, tool
|
|
|
250
398
|
// Retry once — if first attempt was tool-enabled and came back empty, the
|
|
251
399
|
// reasoning model likely hit the step cap on a tool call without producing
|
|
252
400
|
// text. Retry WITHOUT tools to guarantee analytical output.
|
|
401
|
+
let retryError;
|
|
402
|
+
let retryToolCalls = [];
|
|
253
403
|
try {
|
|
254
404
|
const retry = await llm.debate(model, system, prompt, signal, traceCb, { enableVerificationTools: false });
|
|
255
405
|
const text = (retry.text ?? "").trim();
|
|
256
406
|
if (text.length > 0) {
|
|
257
407
|
return { text: retry.text, toolCalls: retry.toolCalls ?? [], attempts: 2 };
|
|
258
408
|
}
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
toolCalls: retry.toolCalls ?? [],
|
|
262
|
-
failureReason: `provider returned empty completion on both attempts (initial: ${firstError})`,
|
|
263
|
-
attempts: 2,
|
|
264
|
-
};
|
|
409
|
+
retryToolCalls = retry.toolCalls ?? [];
|
|
410
|
+
retryError = "provider returned empty completion";
|
|
265
411
|
}
|
|
266
412
|
catch (err) {
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
413
|
+
retryError = err instanceof Error ? err.message : String(err);
|
|
414
|
+
}
|
|
415
|
+
// Both same-model attempts failed. Before dropping this speaker from the
|
|
416
|
+
// debate, try ONE cross-provider fallback — the primary endpoint is either
|
|
417
|
+
// overloaded (opencode-go "Upstream request failed") or param-rejecting, and
|
|
418
|
+
// a third same-model hit would just fail identically. Skip when the caller
|
|
419
|
+
// aborted (user cancellation must not be papered over by a fallback call).
|
|
420
|
+
if (!signal?.aborted) {
|
|
421
|
+
const fallbackModel = pickDebateFallbackModel(model, fallbackPool);
|
|
422
|
+
if (fallbackModel) {
|
|
423
|
+
try {
|
|
424
|
+
const fb = await llm.debate(fallbackModel, system, prompt, signal, traceCb, {
|
|
425
|
+
enableVerificationTools: false,
|
|
426
|
+
});
|
|
427
|
+
const text = (fb.text ?? "").trim();
|
|
428
|
+
if (text.length > 0) {
|
|
429
|
+
traceCb(`[debate] ${model} failed both attempts; recovered via fallback ${fallbackModel}`);
|
|
430
|
+
return { text: fb.text, toolCalls: fb.toolCalls ?? [], attempts: 3 };
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
const fbMsg = err instanceof Error ? err.message : String(err);
|
|
435
|
+
traceCb(`[debate] fallback ${fallbackModel} for ${model} also failed: ${fbMsg}`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
274
438
|
}
|
|
439
|
+
return {
|
|
440
|
+
text: "",
|
|
441
|
+
toolCalls: retryToolCalls,
|
|
442
|
+
failureReason: `both attempts failed — initial: ${firstError}; retry: ${retryError}`,
|
|
443
|
+
attempts: 2,
|
|
444
|
+
};
|
|
275
445
|
}
|
|
276
446
|
export async function* runDebate(spec, config, llm) {
|
|
277
447
|
const { leaderModelId, participants, conversationContext, signal, debatePlan } = config;
|
|
448
|
+
// C — resume from a prior per-round checkpoint when it matches this debate
|
|
449
|
+
// (same problem statement + panel). On a match we skip research + openings and
|
|
450
|
+
// the already-completed rounds, restoring the accumulated transcript. On any
|
|
451
|
+
// mismatch (spec changed, panel re-resolved) we run fresh and let the stale
|
|
452
|
+
// checkpoint be overwritten by the first new round.
|
|
453
|
+
const resumeCp = config.resumeCheckpoint &&
|
|
454
|
+
checkpointMatches(config.resumeCheckpoint, spec.problemStatement, participants.map((p) => p.model))
|
|
455
|
+
? config.resumeCheckpoint
|
|
456
|
+
: undefined;
|
|
457
|
+
const resumed = resumeCp !== undefined;
|
|
458
|
+
// Cross-provider fallback pool for debateWithRetry: leader + every
|
|
459
|
+
// participant model, deduped. When a speaker's model fails both same-model
|
|
460
|
+
// attempts, debateWithRetry retries once on the first pooled model whose
|
|
461
|
+
// provider differs, so an overloaded provider (opencode-go) can't silently
|
|
462
|
+
// drop a voice from the debate.
|
|
463
|
+
const fallbackPool = Array.from(new Set([leaderModelId, ...participants.map((p) => p.model)]));
|
|
278
464
|
// Correlation id for the observe-only council-turn-length telemetry (groups
|
|
279
465
|
// per-turn length samples by run). sessionId in production; a stable literal
|
|
280
466
|
// for direct callers/tests that omit runId.
|
|
@@ -283,6 +469,12 @@ export async function* runDebate(spec, config, llm) {
|
|
|
283
469
|
const leaderNeedsResearch = config.leaderNeedsResearch;
|
|
284
470
|
const internetFirst = config.internetFirst === true;
|
|
285
471
|
const costAware = config.costAware === true;
|
|
472
|
+
// Feature B — resolved council debate language. The chosen language IS the
|
|
473
|
+
// debate language (no translate-back pass). "auto" (default) follows the
|
|
474
|
+
// user's brief; "english" is the historical behavior; any other value pins
|
|
475
|
+
// the debate prose to that locale. Threaded into every debate/eval/summary
|
|
476
|
+
// prompt below. config.debateLanguage lets a caller/test override the setting.
|
|
477
|
+
const debateLanguage = config.debateLanguage ?? getCouncilLanguage();
|
|
286
478
|
const active = [];
|
|
287
479
|
const exchangeLogs = new Map();
|
|
288
480
|
const archive = [];
|
|
@@ -296,13 +488,32 @@ export async function* runDebate(spec, config, llm) {
|
|
|
296
488
|
// Track which models we've already announced as tool-disabled so we don't
|
|
297
489
|
// emit the same "circuit breaker tripped" message every round.
|
|
298
490
|
const announcedDisabled = new Set();
|
|
491
|
+
// C — restore accumulated state before any phase runs when resuming.
|
|
492
|
+
if (resumeCp) {
|
|
493
|
+
for (const p of resumeCp.active)
|
|
494
|
+
active.push(p);
|
|
495
|
+
for (const [k, v] of restoreExchangeLogs(resumeCp))
|
|
496
|
+
exchangeLogs.set(k, v);
|
|
497
|
+
for (const a of resumeCp.archive)
|
|
498
|
+
archive.push(a);
|
|
499
|
+
runningSummary = resumeCp.runningSummary;
|
|
500
|
+
researchFindings = resumeCp.researchFindings;
|
|
501
|
+
yield {
|
|
502
|
+
type: "content",
|
|
503
|
+
content: `\n> Resuming debate from round ${resumeCp.roundCount + 1} — restored ${resumeCp.roundCount} completed round(s), ${active.length} participant(s), skipping research + opening statements.\n`,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
299
506
|
// ── Leader decides: research needed? (skipped if user overrode upstream) ──
|
|
300
507
|
// Reuse the leader's upstream research decision (computed once in runCouncil)
|
|
301
508
|
// when available; only run the classifier here for direct callers that did not
|
|
302
509
|
// pre-compute it. Avoids a duplicate leader-tier LLM call per council run.
|
|
303
|
-
|
|
510
|
+
// On resume the research phase already ran — never re-run it.
|
|
511
|
+
const needsResearch = resumed
|
|
304
512
|
? false
|
|
305
|
-
:
|
|
513
|
+
: researchSkipOverride
|
|
514
|
+
? false
|
|
515
|
+
: (leaderNeedsResearch ??
|
|
516
|
+
(yield* evaluateResearchNeed(spec, leaderModelId, conversationContext, llm, costAware)));
|
|
306
517
|
if (researchSkipOverride) {
|
|
307
518
|
yield {
|
|
308
519
|
type: "content",
|
|
@@ -319,11 +530,16 @@ export async function* runDebate(spec, config, llm) {
|
|
|
319
530
|
detail: `via ${researchCandidate.model}`,
|
|
320
531
|
});
|
|
321
532
|
const researchTraces = [];
|
|
322
|
-
|
|
533
|
+
const researchStartedAt = Date.now();
|
|
534
|
+
researchFindings = yield* tracedAsync(() => researchWithFallback(llm, researchCandidate.model, spec.problemStatement, conversationContext, signal, (t) => researchTraces.push(t), { internetFirst }, fallbackPool, config.runIsolatedTask), {
|
|
323
535
|
phase: "research",
|
|
324
536
|
label: internetFirst ? "Researching (internet-first)" : "Researching codebase",
|
|
325
537
|
detail: spec.problemStatement.slice(0, 80),
|
|
326
538
|
role: "research",
|
|
539
|
+
// Research is a single long generateText (up to ~10 min / 15 tool steps)
|
|
540
|
+
// with a quiet main stream — the phase most easily misread as hung, and
|
|
541
|
+
// the one whose cost the per-round numbers could not account for.
|
|
542
|
+
liveness: councilStreamLivenessReader(),
|
|
327
543
|
});
|
|
328
544
|
// CQ-22: emit research tool traces as council_status
|
|
329
545
|
for (const trace of researchTraces) {
|
|
@@ -336,105 +552,163 @@ export async function* runDebate(spec, config, llm) {
|
|
|
336
552
|
startedAt: p0Start,
|
|
337
553
|
detail: `via ${researchCandidate.model}`,
|
|
338
554
|
});
|
|
555
|
+
// A failed research call returns a "[Research failed: …]" placeholder once
|
|
556
|
+
// both primary and fallback providers are exhausted. It is a truthy string,
|
|
557
|
+
// so injecting it verbatim (below) would plant a fake "Research Findings"
|
|
558
|
+
// block into every opening/round prompt. Detect it and show a clean status
|
|
559
|
+
// note instead of the raw placeholder.
|
|
560
|
+
const researchFailed = !!researchFindings && researchFindings.includes(RESEARCH_FAILED_MARKER);
|
|
339
561
|
yield {
|
|
340
562
|
type: "council_message",
|
|
341
563
|
councilMessage: {
|
|
342
564
|
kind: "research",
|
|
343
565
|
speaker: { role: researchCandidate.role, model: researchCandidate.model },
|
|
344
|
-
text:
|
|
566
|
+
text: researchFailed
|
|
567
|
+
? "⚠ Research unavailable — both providers failed. The council proceeds on the panel's own knowledge (no external findings injected)."
|
|
568
|
+
: (researchFindings ?? ""),
|
|
569
|
+
// Spans researchWithFallback including any fallback provider attempt.
|
|
570
|
+
// Measured per-speaker debate turns summed to only ~3.7 min against a
|
|
571
|
+
// ~507s historical `debate_complete`, so the balance is here.
|
|
572
|
+
durationMs: Date.now() - researchStartedAt,
|
|
345
573
|
},
|
|
346
574
|
};
|
|
347
575
|
}
|
|
348
|
-
|
|
576
|
+
// Only enrich when research actually produced findings — never fold the
|
|
577
|
+
// "[Research failed: …]" placeholder into the debate context.
|
|
578
|
+
const enrichedContext = researchFindings && !researchFindings.includes(RESEARCH_FAILED_MARKER)
|
|
349
579
|
? `${conversationContext}\n\n---\n\n## Research Findings\n${researchFindings}`
|
|
350
580
|
: conversationContext;
|
|
351
581
|
// ── Phase 1: Parallel opening statements ───────────────────────────────────
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const { system, prompt } = buildOpeningPrompt({
|
|
362
|
-
speakerRole: self.role,
|
|
363
|
-
partnerRole: partner.role,
|
|
364
|
-
speakerStance: self.stance,
|
|
365
|
-
partnerStance: partner.stance,
|
|
366
|
-
spec,
|
|
367
|
-
outputShape: debatePlan?.outputShape,
|
|
368
|
-
conversationContext: enrichedContext,
|
|
582
|
+
// Skipped entirely on resume — `active` was restored from the checkpoint so
|
|
583
|
+
// re-running openings would both waste tokens and reset the debated positions.
|
|
584
|
+
if (!resumed) {
|
|
585
|
+
const p1Start = Date.now();
|
|
586
|
+
yield phaseStart({
|
|
587
|
+
phaseId: "phase:opening",
|
|
588
|
+
kind: "opening",
|
|
589
|
+
label: "Opening analysis",
|
|
590
|
+
detail: `${participants.length} participants in parallel`,
|
|
369
591
|
});
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
yield { type: "content", content: "\n── Opening Analysis ──\n" };
|
|
385
|
-
for (const o of openings) {
|
|
386
|
-
const speakerRole = o.stance?.name ?? o.role;
|
|
387
|
-
if (o.error) {
|
|
388
|
-
yield {
|
|
389
|
-
type: "council_message",
|
|
390
|
-
councilMessage: {
|
|
391
|
-
kind: "debate",
|
|
392
|
-
speaker: { role: speakerRole, model: o.model },
|
|
393
|
-
round: 0,
|
|
394
|
-
text: `[Error: ${o.error}]`,
|
|
395
|
-
attempts: o.attempts,
|
|
396
|
-
failureReason: o.error,
|
|
397
|
-
},
|
|
398
|
-
};
|
|
592
|
+
// Sprint-2 item 3 — per-stance recall. Fire one stance-weighted recall per
|
|
593
|
+
// unique role before openings; fold each role's seed into that participant's
|
|
594
|
+
// opening context so every stance opens grounded in the experience its lens
|
|
595
|
+
// cares about. Bounded + failure-tolerant inside the injected fn; a null/empty
|
|
596
|
+
// result leaves openings unchanged.
|
|
597
|
+
let stanceSeeds = null;
|
|
598
|
+
if (config.stanceRecall) {
|
|
599
|
+
try {
|
|
600
|
+
const roles = Array.from(new Set(participants.map((p) => p.role)));
|
|
601
|
+
stanceSeeds = await config.stanceRecall(roles, spec.problemStatement);
|
|
602
|
+
}
|
|
603
|
+
catch {
|
|
604
|
+
stanceSeeds = null;
|
|
605
|
+
}
|
|
399
606
|
}
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
607
|
+
const openingPromises = participants.map((self) => {
|
|
608
|
+
const partner = participants.find((c) => c.role !== self.role) ?? participants[0];
|
|
609
|
+
const seed = stanceSeeds?.get(self.role)?.trim();
|
|
610
|
+
const selfContext = seed
|
|
611
|
+
? `${enrichedContext}\n\n---\n\n## Experience recall — ${self.stance?.name ?? self.role} lens\n${seed}`
|
|
612
|
+
: enrichedContext;
|
|
613
|
+
const { system, prompt } = buildOpeningPrompt({
|
|
614
|
+
speakerRole: self.role,
|
|
615
|
+
partnerRole: partner.role,
|
|
616
|
+
speakerStance: self.stance,
|
|
617
|
+
partnerStance: partner.stance,
|
|
618
|
+
spec,
|
|
619
|
+
outputShape: debatePlan?.outputShape,
|
|
620
|
+
conversationContext: selfContext,
|
|
621
|
+
language: debateLanguage,
|
|
408
622
|
});
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
623
|
+
const startedAt = Date.now();
|
|
624
|
+
return openingWithRetry(llm, self.model, system, prompt).then((r) => ({
|
|
625
|
+
role: self.role,
|
|
626
|
+
model: self.model,
|
|
627
|
+
stance: self.stance,
|
|
628
|
+
position: r.text,
|
|
629
|
+
error: r.text ? null : (r.error ?? "empty completion after retries"),
|
|
630
|
+
attempts: r.attempts,
|
|
631
|
+
durationMs: Date.now() - startedAt,
|
|
632
|
+
}));
|
|
633
|
+
});
|
|
634
|
+
const openings = yield* tracedAsync(() => Promise.all(openingPromises), {
|
|
635
|
+
phase: "opening",
|
|
636
|
+
label: `Generating opening statements (${participants.length} participants in parallel)`,
|
|
637
|
+
// Newline-joined "Name — lens" roster so the composing placeholder shows WHAT
|
|
638
|
+
// each speaker is tasked to argue (A: live debate preview) instead of a bare
|
|
639
|
+
// spinner during the atomic generateText window.
|
|
640
|
+
detail: formatSpeakerRoster(participants),
|
|
641
|
+
// Promise.all does not pump this generator, so `elapsedMs` freezes while
|
|
642
|
+
// the openings run. Push-based stream counters keep the heartbeat honest.
|
|
643
|
+
liveness: councilStreamLivenessReader(),
|
|
644
|
+
});
|
|
645
|
+
yield { type: "content", content: "\n── Opening Analysis ──\n" };
|
|
646
|
+
for (const o of openings) {
|
|
647
|
+
const speakerRole = o.stance?.name ?? o.role;
|
|
648
|
+
if (o.error) {
|
|
649
|
+
yield {
|
|
650
|
+
type: "council_message",
|
|
651
|
+
councilMessage: {
|
|
652
|
+
kind: "debate",
|
|
653
|
+
speaker: { role: speakerRole, model: o.model },
|
|
654
|
+
round: 0,
|
|
655
|
+
text: `[Error: ${o.error}]`,
|
|
656
|
+
attempts: o.attempts,
|
|
657
|
+
failureReason: o.error,
|
|
658
|
+
durationMs: o.durationMs,
|
|
659
|
+
},
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
else {
|
|
663
|
+
active.push({ role: o.role, model: o.model, position: o.position, stance: o.stance });
|
|
664
|
+
archive.push({
|
|
665
|
+
round: 0,
|
|
666
|
+
role: o.role,
|
|
667
|
+
model: o.model,
|
|
668
|
+
stanceName: o.stance?.name,
|
|
669
|
+
...makeExcerpt(o.position),
|
|
670
|
+
});
|
|
671
|
+
yield {
|
|
672
|
+
type: "council_message",
|
|
673
|
+
councilMessage: {
|
|
674
|
+
kind: "debate",
|
|
675
|
+
speaker: { role: speakerRole, model: o.model },
|
|
676
|
+
round: 0,
|
|
677
|
+
text: o.position,
|
|
678
|
+
attempts: o.attempts,
|
|
679
|
+
durationMs: o.durationMs,
|
|
680
|
+
},
|
|
681
|
+
};
|
|
682
|
+
emitCouncilTurnLength({
|
|
683
|
+
role: speakerRole,
|
|
414
684
|
round: 0,
|
|
415
685
|
text: o.position,
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
role: speakerRole,
|
|
421
|
-
round: 0,
|
|
422
|
-
text: o.position,
|
|
423
|
-
model: o.model,
|
|
424
|
-
correlationId: turnCorrelationId,
|
|
425
|
-
});
|
|
686
|
+
model: o.model,
|
|
687
|
+
correlationId: turnCorrelationId,
|
|
688
|
+
});
|
|
689
|
+
}
|
|
426
690
|
}
|
|
691
|
+
yield phaseDone({
|
|
692
|
+
phaseId: "phase:opening",
|
|
693
|
+
kind: "opening",
|
|
694
|
+
label: "Opening analysis",
|
|
695
|
+
startedAt: p1Start,
|
|
696
|
+
detail: `${active.length}/${participants.length} participants succeeded`,
|
|
697
|
+
});
|
|
427
698
|
}
|
|
428
|
-
yield phaseDone({
|
|
429
|
-
phaseId: "phase:opening",
|
|
430
|
-
kind: "opening",
|
|
431
|
-
label: "Opening analysis",
|
|
432
|
-
startedAt: p1Start,
|
|
433
|
-
detail: `${active.length}/${participants.length} participants succeeded`,
|
|
434
|
-
});
|
|
435
699
|
if (active.length < 2) {
|
|
436
700
|
yield { type: "content", content: "\nNot enough successful openings for discussion.\n" };
|
|
437
|
-
|
|
701
|
+
// Even a single-opening debate is worth persisting (F9) — a deterministic
|
|
702
|
+
// synthesis of whatever position survived beats an empty research artifact.
|
|
703
|
+
return {
|
|
704
|
+
spec,
|
|
705
|
+
exchangeLogs,
|
|
706
|
+
runningSummary: resolveDebateSummary({ runningSummary: "", active, archive }),
|
|
707
|
+
roundCount: 0,
|
|
708
|
+
researchFindings,
|
|
709
|
+
active,
|
|
710
|
+
archive,
|
|
711
|
+
};
|
|
438
712
|
}
|
|
439
713
|
// ── Phase 2: Dynamic discussion rounds ─────────────────────────────────────
|
|
440
714
|
// Leader-decided round budget: planner proposes an initial value, leader
|
|
@@ -458,6 +732,16 @@ export async function* runDebate(spec, config, llm) {
|
|
|
458
732
|
type: "content",
|
|
459
733
|
content: `\n> Leader-proposed debate budget: ${maxRounds} round${maxRounds === 1 ? "" : "s"}${ceilingNote}.\n`,
|
|
460
734
|
};
|
|
735
|
+
// P3 — structured budget/ceiling for the context rail. These are locals here,
|
|
736
|
+
// invisible to the council entrypoint, so they ride a separate council_meta
|
|
737
|
+
// patch that the UI upsert-merges with the leader/panel patch.
|
|
738
|
+
yield {
|
|
739
|
+
type: "council_meta",
|
|
740
|
+
councilMeta: {
|
|
741
|
+
roundBudget: maxRounds,
|
|
742
|
+
roundCeiling: kindCapped ? effectiveCeiling : ABSOLUTE_MAX_ROUNDS,
|
|
743
|
+
},
|
|
744
|
+
};
|
|
461
745
|
// Pairs that fail twice in a row are dropped from subsequent rounds so the
|
|
462
746
|
// remaining participants don't keep retrying a broken model and inflating
|
|
463
747
|
// the persistent transcript with failure noise.
|
|
@@ -466,7 +750,74 @@ export async function* runDebate(spec, config, llm) {
|
|
|
466
750
|
// Stop debate entirely after two consecutive rounds where ≥50% of pairs fail
|
|
467
751
|
// — the LLM is clearly under provider stress and more rounds won't help.
|
|
468
752
|
let consecutiveRoundFailures = 0;
|
|
469
|
-
|
|
753
|
+
// P5 — topic carried from the prior round's leader nextRoundFocus, shown as the
|
|
754
|
+
// next round's heading in the round-grouped transcript.
|
|
755
|
+
let nextTopic;
|
|
756
|
+
// B5 — prior round's aligned criteriaMet, so each round's directive/verdict
|
|
757
|
+
// and the post-debate unmet-flag know what is still open. Empty before round 1
|
|
758
|
+
// → the round-1 directive treats every criterion as unmet.
|
|
759
|
+
let lastCriteriaMet = [];
|
|
760
|
+
// B4 — leader auto-remedy progress tracking. `bestCriteriaMetCount` is the
|
|
761
|
+
// high-water mark of pinned criteria met; `roundsSinceProgress` counts
|
|
762
|
+
// consecutive evaluated rounds that produced no NEW met criterion. Auto-extend
|
|
763
|
+
// fires only while progress is being made; a stuck criterion (no progress for
|
|
764
|
+
// 2 rounds) stops the budget burn and drops to a diagnostic closing verdict.
|
|
765
|
+
let bestCriteriaMetCount = 0;
|
|
766
|
+
let roundsSinceProgress = 0;
|
|
767
|
+
// B4 interactive escalation — fires at most once per debate. `escalation`
|
|
768
|
+
// records the user's choice at a stop-with-unmet boundary for the DebateState.
|
|
769
|
+
let escalated = false;
|
|
770
|
+
let escalation;
|
|
771
|
+
// C — restore round-loop continuity from the checkpoint. `maxRounds` takes the
|
|
772
|
+
// checkpointed budget (which may have been extended past the plan). The within-
|
|
773
|
+
// run heuristics that reset safely (droppedPairKeys, consecutiveRoundFailures,
|
|
774
|
+
// consecutivePairFailures, escalated) are intentionally NOT restored — a fresh
|
|
775
|
+
// start for them at most re-tries a previously-dropped pair, never corrupts
|
|
776
|
+
// state. The loop start is derived below from `roundCount`.
|
|
777
|
+
if (resumeCp) {
|
|
778
|
+
roundCount = resumeCp.roundCount;
|
|
779
|
+
maxRounds = Math.max(maxRounds, resumeCp.maxRounds);
|
|
780
|
+
lastCriteriaMet = [...resumeCp.lastCriteriaMet];
|
|
781
|
+
bestCriteriaMetCount = resumeCp.bestCriteriaMetCount;
|
|
782
|
+
roundsSinceProgress = resumeCp.roundsSinceProgress;
|
|
783
|
+
nextTopic = resumeCp.nextTopic;
|
|
784
|
+
}
|
|
785
|
+
// Shared applier for the two stop-with-unmet boundaries (leader voluntarily
|
|
786
|
+
// stopped, or the budget exhausted while stuck/at ceiling). Reassigns the
|
|
787
|
+
// loop's round budget via closure; returns whether the debate should keep
|
|
788
|
+
// going. Caller must have already checked the gate (responder wired, flag on,
|
|
789
|
+
// not yet escalated, criteria unmet).
|
|
790
|
+
async function* escalateStop(stuck, pinnedUnmet, openList) {
|
|
791
|
+
escalated = true;
|
|
792
|
+
// convene_council path: no interactive user is answering mid-tool-call, so a
|
|
793
|
+
// blocking escalation card would hang the council. Auto-accept (= conclude
|
|
794
|
+
// with the best synthesis so far) without emitting the card. Mirrors the
|
|
795
|
+
// "empty/failed answer → accept" fallback runEscalationPrompt already uses.
|
|
796
|
+
if (config.convenePath) {
|
|
797
|
+
escalation = { action: "accept" };
|
|
798
|
+
return "stop";
|
|
799
|
+
}
|
|
800
|
+
const dec = yield* runEscalationPrompt({
|
|
801
|
+
respondToQuestion: config.respondToQuestion,
|
|
802
|
+
openCriteria: openList,
|
|
803
|
+
pinnedUnmet,
|
|
804
|
+
stuck,
|
|
805
|
+
atAbsoluteMax: maxRounds >= ABSOLUTE_MAX_ROUNDS,
|
|
806
|
+
currentMax: maxRounds,
|
|
807
|
+
});
|
|
808
|
+
escalation = { action: dec.action, grantedRounds: dec.grantedRounds || undefined };
|
|
809
|
+
if (dec.action === "extend" && dec.grantedRounds > 0) {
|
|
810
|
+
maxRounds += dec.grantedRounds;
|
|
811
|
+
roundsSinceProgress = 0;
|
|
812
|
+
if (!nextTopic)
|
|
813
|
+
nextTopic = `Close the unmet criteria: ${openList.join("; ")}`;
|
|
814
|
+
return "extend";
|
|
815
|
+
}
|
|
816
|
+
return "stop";
|
|
817
|
+
}
|
|
818
|
+
// C — on resume, `roundCount` was seeded to the last completed round so the
|
|
819
|
+
// loop continues at roundCount+1; a fresh run starts at 1 (roundCount=0).
|
|
820
|
+
for (let round = roundCount + 1; round <= maxRounds; round++) {
|
|
470
821
|
// User cancelled mid-debate — stop before spending another round of
|
|
471
822
|
// parallel pair LLM calls. The caller (runCouncil) re-checks the signal at
|
|
472
823
|
// its next phase boundary and skips synthesis too.
|
|
@@ -475,6 +826,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
475
826
|
break;
|
|
476
827
|
}
|
|
477
828
|
roundCount = round;
|
|
829
|
+
// Set when the user extends the debate at this round's stop boundary (B4
|
|
830
|
+
// escalation). Guards the convergence-exit below so a user "extend" isn't
|
|
831
|
+
// immediately overridden by a lock-phrase convergence break.
|
|
832
|
+
let userExtendedThisRound = false;
|
|
478
833
|
const p2Start = Date.now();
|
|
479
834
|
const roundPhaseId = `phase:round-${round}`;
|
|
480
835
|
yield phaseStart({
|
|
@@ -510,6 +865,50 @@ export async function* runDebate(spec, config, llm) {
|
|
|
510
865
|
roundCount = round - 1;
|
|
511
866
|
break;
|
|
512
867
|
}
|
|
868
|
+
// P5 — round lifecycle for the grouped transcript. `roundRec` closes over
|
|
869
|
+
// this round's participants/pairCount/emergent/topic; a running record now,
|
|
870
|
+
// a guaranteed done record on every exit below.
|
|
871
|
+
// Surface the task-adaptive persona (or model id), never the internal
|
|
872
|
+
// implement/verify/research cost-tier slot — that slot is a routing detail
|
|
873
|
+
// that misleads on analysis/decision topics (observed session dd34c59c63e9:
|
|
874
|
+
// an "evaluation" debate showed a bogus "implement" member).
|
|
875
|
+
const roundParticipants = active.map((p) => p.stance?.name ?? p.model);
|
|
876
|
+
const roundEmergent = round > plannedMaxRounds;
|
|
877
|
+
const roundTopic = nextTopic;
|
|
878
|
+
const roundRec = (state, patch = {}) => ({
|
|
879
|
+
type: "council_round",
|
|
880
|
+
councilRound: {
|
|
881
|
+
round,
|
|
882
|
+
state,
|
|
883
|
+
topic: roundTopic,
|
|
884
|
+
participants: roundParticipants,
|
|
885
|
+
pairCount: pairs.length,
|
|
886
|
+
emergent: roundEmergent,
|
|
887
|
+
...patch,
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
yield roundRec("running");
|
|
891
|
+
// B5 — pre-round leader DIRECTIVE. Before the exchanges run, the leader
|
|
892
|
+
// states this round's goal and which pinned criteria are still unmet, so it
|
|
893
|
+
// visibly conducts each round instead of only grading afterwards. Gated on
|
|
894
|
+
// pinned criteria (nothing to steer toward otherwise) + the conductor flag.
|
|
895
|
+
// Captured in `roundDirective` so it also lands on the round record's
|
|
896
|
+
// `directive` field below — durable in the conclusion card, not only the
|
|
897
|
+
// ephemeral live bubble a user misses if they look away mid-debate.
|
|
898
|
+
let roundDirective;
|
|
899
|
+
if (leaderConductorEnabled() && spec.successCriteria.length > 0) {
|
|
900
|
+
roundDirective = buildLeaderDirective(round, spec.successCriteria, lastCriteriaMet, roundTopic);
|
|
901
|
+
yield {
|
|
902
|
+
type: "council_message",
|
|
903
|
+
councilMessage: {
|
|
904
|
+
kind: "leader",
|
|
905
|
+
phase: "directive",
|
|
906
|
+
speaker: { role: "Leader", model: leaderModelId },
|
|
907
|
+
round,
|
|
908
|
+
text: roundDirective,
|
|
909
|
+
},
|
|
910
|
+
};
|
|
911
|
+
}
|
|
513
912
|
const pairResults = yield* tracedAsync(() => Promise.all(pairs.map(async ({ a, b, key }) => {
|
|
514
913
|
const log = exchangeLogs.get(key);
|
|
515
914
|
const chunks = [];
|
|
@@ -529,9 +928,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
529
928
|
speakerPosition: a.position,
|
|
530
929
|
partnerPosition: b.position,
|
|
531
930
|
spec,
|
|
931
|
+
language: debateLanguage,
|
|
532
932
|
});
|
|
533
933
|
const aTraces = [];
|
|
534
|
-
const aResult = await debateWithRetry(llm, a.model, aPrompt.system, aPrompt.prompt, signal, (t) => aTraces.push(t), toolBudget);
|
|
934
|
+
const aResult = await debateWithRetry(llm, a.model, aPrompt.system, aPrompt.prompt, signal, (t) => aTraces.push(t), toolBudget, fallbackPool);
|
|
535
935
|
aResponse = aResult.text;
|
|
536
936
|
aToolCalls = aResult.toolCalls;
|
|
537
937
|
log.push(`[${aLabel}]: ${aResponse}`);
|
|
@@ -542,6 +942,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
542
942
|
traces: aTraces,
|
|
543
943
|
failureReason: aResult.failureReason,
|
|
544
944
|
attempts: aResult.attempts,
|
|
945
|
+
durationMs: aResult.durationMs,
|
|
545
946
|
});
|
|
546
947
|
const bPrompt = buildResponsePrompt({
|
|
547
948
|
speakerRole: b.role,
|
|
@@ -551,9 +952,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
551
952
|
speakerPosition: b.position,
|
|
552
953
|
partnerPosition: aResponse,
|
|
553
954
|
spec,
|
|
955
|
+
language: debateLanguage,
|
|
554
956
|
});
|
|
555
957
|
const bTraces = [];
|
|
556
|
-
const bResult = await debateWithRetry(llm, b.model, bPrompt.system, bPrompt.prompt, signal, (t) => bTraces.push(t), toolBudget);
|
|
958
|
+
const bResult = await debateWithRetry(llm, b.model, bPrompt.system, bPrompt.prompt, signal, (t) => bTraces.push(t), toolBudget, fallbackPool);
|
|
557
959
|
bResponse = bResult.text;
|
|
558
960
|
bToolCalls = bResult.toolCalls;
|
|
559
961
|
log.push(`[${bLabel}]: ${bResponse}`);
|
|
@@ -564,6 +966,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
564
966
|
traces: bTraces,
|
|
565
967
|
failureReason: bResult.failureReason,
|
|
566
968
|
attempts: bResult.attempts,
|
|
969
|
+
durationMs: bResult.durationMs,
|
|
567
970
|
});
|
|
568
971
|
}
|
|
569
972
|
else {
|
|
@@ -581,9 +984,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
581
984
|
round,
|
|
582
985
|
runningSummary,
|
|
583
986
|
spec,
|
|
987
|
+
language: debateLanguage,
|
|
584
988
|
});
|
|
585
989
|
const aTraces = [];
|
|
586
|
-
const aResult = await debateWithRetry(llm, a.model, aPrompt.system, aPrompt.prompt, signal, (t) => aTraces.push(t), toolBudget);
|
|
990
|
+
const aResult = await debateWithRetry(llm, a.model, aPrompt.system, aPrompt.prompt, signal, (t) => aTraces.push(t), toolBudget, fallbackPool);
|
|
587
991
|
aResponse = aResult.text;
|
|
588
992
|
aToolCalls = aResult.toolCalls;
|
|
589
993
|
log.push(`[${aLabel}] (round ${round}): ${aResponse}`);
|
|
@@ -594,6 +998,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
594
998
|
traces: aTraces,
|
|
595
999
|
failureReason: aResult.failureReason,
|
|
596
1000
|
attempts: aResult.attempts,
|
|
1001
|
+
durationMs: aResult.durationMs,
|
|
597
1002
|
});
|
|
598
1003
|
const bPrompt = buildFollowupPrompt({
|
|
599
1004
|
speakerRole: b.role,
|
|
@@ -605,9 +1010,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
605
1010
|
round,
|
|
606
1011
|
runningSummary,
|
|
607
1012
|
spec,
|
|
1013
|
+
language: debateLanguage,
|
|
608
1014
|
});
|
|
609
1015
|
const bTraces = [];
|
|
610
|
-
const bResult = await debateWithRetry(llm, b.model, bPrompt.system, bPrompt.prompt, signal, (t) => bTraces.push(t), toolBudget);
|
|
1016
|
+
const bResult = await debateWithRetry(llm, b.model, bPrompt.system, bPrompt.prompt, signal, (t) => bTraces.push(t), toolBudget, fallbackPool);
|
|
611
1017
|
bResponse = bResult.text;
|
|
612
1018
|
bToolCalls = bResult.toolCalls;
|
|
613
1019
|
log.push(`[${bLabel}] (round ${round}): ${bResponse}`);
|
|
@@ -618,6 +1024,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
618
1024
|
traces: bTraces,
|
|
619
1025
|
failureReason: bResult.failureReason,
|
|
620
1026
|
attempts: bResult.attempts,
|
|
1027
|
+
durationMs: bResult.durationMs,
|
|
621
1028
|
});
|
|
622
1029
|
}
|
|
623
1030
|
// Only update positions when response is non-empty (avoid clearing on LLM error)
|
|
@@ -633,7 +1040,14 @@ export async function* runDebate(spec, config, llm) {
|
|
|
633
1040
|
})), {
|
|
634
1041
|
phase: "exchange",
|
|
635
1042
|
label: `Discussion round ${round} (${pairs.length} pair${pairs.length === 1 ? "" : "s"})`,
|
|
636
|
-
|
|
1043
|
+
// Distinct speakers with their lens (A: live debate preview) — dedup by
|
|
1044
|
+
// formatted line so a speaker appearing in two pairs shows once.
|
|
1045
|
+
detail: formatSpeakerRoster(pairs.flatMap((p) => [p.a, p.b])),
|
|
1046
|
+
// THE case this exists for: each pair turn can run to the 5-min council
|
|
1047
|
+
// ceiling on a reasoning model, and Promise.all never pumps this
|
|
1048
|
+
// generator — `elapsedMs` freezes and a healthy 15-min round looks hung.
|
|
1049
|
+
// These counters come off the token stream, so they keep advancing.
|
|
1050
|
+
liveness: councilStreamLivenessReader(),
|
|
637
1051
|
});
|
|
638
1052
|
yield { type: "content", content: `\n── Round ${round} ──\n` };
|
|
639
1053
|
// Track failures so the circuit breaker can fire after this round.
|
|
@@ -698,6 +1112,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
698
1112
|
text: chunk.text.trim(),
|
|
699
1113
|
toolCalls: chunk.toolCalls?.map((t) => ({ name: t.toolName })),
|
|
700
1114
|
attempts: chunk.attempts,
|
|
1115
|
+
durationMs: chunk.durationMs,
|
|
701
1116
|
},
|
|
702
1117
|
};
|
|
703
1118
|
emitCouncilTurnLength({
|
|
@@ -769,6 +1184,11 @@ export async function* runDebate(spec, config, llm) {
|
|
|
769
1184
|
type: "content",
|
|
770
1185
|
content: `\n> Circuit breaker: aborting debate after ${consecutiveRoundFailures} consecutive failure-heavy rounds — proceeding to synthesis with what we have.\n`,
|
|
771
1186
|
};
|
|
1187
|
+
yield roundRec("done", {
|
|
1188
|
+
leaderDecision: "circuit-break",
|
|
1189
|
+
leaderReason: "provider stress — circuit breaker",
|
|
1190
|
+
directive: roundDirective,
|
|
1191
|
+
});
|
|
772
1192
|
break;
|
|
773
1193
|
}
|
|
774
1194
|
}
|
|
@@ -784,13 +1204,67 @@ export async function* runDebate(spec, config, llm) {
|
|
|
784
1204
|
label: `Leader evaluation (round ${round})`,
|
|
785
1205
|
});
|
|
786
1206
|
const allExchangeText = [...exchangeLogs.values()].flat().slice(-8).join("\n\n");
|
|
787
|
-
|
|
1207
|
+
let evaluation = yield* evaluateDebate(spec, allExchangeText, round, leaderModelId, llm, costAware, undefined, debateLanguage);
|
|
1208
|
+
// Eval robustness: the leader's cost-tier eval model can be on a flaky proxy
|
|
1209
|
+
// (Console Go glm/kimi → "Upstream request failed") while panel models on
|
|
1210
|
+
// other providers stay healthy. Rather than surface "evaluation unavailable"
|
|
1211
|
+
// and lose the round outcome, retry the eval on each healthy model in the
|
|
1212
|
+
// fallback pool before giving up. Bounded (pool is small) and only runs on the
|
|
1213
|
+
// failure path, so successful evals pay nothing.
|
|
1214
|
+
if (!evaluation) {
|
|
1215
|
+
// Which model the primary eval already tried (skip re-hitting it). Defensive
|
|
1216
|
+
// against a partially-mocked leader module in tests — if we can't resolve
|
|
1217
|
+
// it, skip the fallback loop rather than throw.
|
|
1218
|
+
let firstTried = null;
|
|
1219
|
+
try {
|
|
1220
|
+
firstTried = pickCouncilTaskModel("evaluate_round", leaderModelId, costAware);
|
|
1221
|
+
}
|
|
1222
|
+
catch {
|
|
1223
|
+
firstTried = null;
|
|
1224
|
+
}
|
|
1225
|
+
if (firstTried) {
|
|
1226
|
+
for (const fallbackModel of fallbackPool) {
|
|
1227
|
+
if (fallbackModel === firstTried)
|
|
1228
|
+
continue;
|
|
1229
|
+
evaluation = yield* evaluateDebate(spec, allExchangeText, round, leaderModelId, llm, costAware, fallbackModel, debateLanguage);
|
|
1230
|
+
if (evaluation)
|
|
1231
|
+
break;
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1234
|
+
}
|
|
788
1235
|
if (evaluation) {
|
|
789
1236
|
if (typeof evaluation.evidenceDensity === "number") {
|
|
790
1237
|
lastEvidenceDensity = evaluation.evidenceDensity;
|
|
791
1238
|
}
|
|
792
1239
|
const metCount = evaluation.criteriaStatus.filter((c) => c.met).length;
|
|
793
1240
|
const total = evaluation.criteriaStatus.length;
|
|
1241
|
+
// B2/B3: project this round's evaluation onto the PINNED spec criteria
|
|
1242
|
+
// (index-aligned, best-effort text match as a fallback) and push it to the
|
|
1243
|
+
// rail so the user sees live ✓/○ against the exact outcome they saw — not
|
|
1244
|
+
// an opaque "N/M". Only when the spec has real pinned criteria.
|
|
1245
|
+
const hasPinned = spec.successCriteria.length > 0;
|
|
1246
|
+
const aligned = hasPinned ? alignCriteriaMet(spec.successCriteria, evaluation.criteriaStatus) : [];
|
|
1247
|
+
// Count of pinned criteria still open this round — used by both auto-remedy
|
|
1248
|
+
// and the interactive escalation boundaries below.
|
|
1249
|
+
const pinnedUnmet = hasPinned ? aligned.filter((m) => !m).length : 0;
|
|
1250
|
+
if (hasPinned) {
|
|
1251
|
+
yield {
|
|
1252
|
+
type: "council_meta",
|
|
1253
|
+
councilMeta: { criteriaMet: aligned },
|
|
1254
|
+
};
|
|
1255
|
+
lastCriteriaMet = aligned; // B5: feed next round's directive + final unmet-flag
|
|
1256
|
+
// B4: track progress against the PINNED criteria. A round that meets a new
|
|
1257
|
+
// criterion resets the stuck counter; a round that meets nothing new
|
|
1258
|
+
// increments it. Auto-remedy reads these to decide extend-vs-give-up.
|
|
1259
|
+
const pinnedMetNow = aligned.filter(Boolean).length;
|
|
1260
|
+
if (pinnedMetNow > bestCriteriaMetCount) {
|
|
1261
|
+
bestCriteriaMetCount = pinnedMetNow;
|
|
1262
|
+
roundsSinceProgress = 0;
|
|
1263
|
+
}
|
|
1264
|
+
else {
|
|
1265
|
+
roundsSinceProgress++;
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
794
1268
|
yield phaseDone({
|
|
795
1269
|
phaseId: evalPhaseId,
|
|
796
1270
|
kind: "evaluation",
|
|
@@ -798,15 +1272,45 @@ export async function* runDebate(spec, config, llm) {
|
|
|
798
1272
|
startedAt: evalStart,
|
|
799
1273
|
detail: `${metCount}/${total} criteria met · ${evaluation.reason.slice(0, 80)}`,
|
|
800
1274
|
});
|
|
1275
|
+
// B5: post-round VERDICT. With pinned criteria + conductor on, list each
|
|
1276
|
+
// criterion's ✓/○ and the focus handed to the next round; otherwise fall
|
|
1277
|
+
// back to the plain one-line eval (pre-B5 behavior).
|
|
1278
|
+
const verdictText = leaderConductorEnabled() && hasPinned
|
|
1279
|
+
? buildLeaderVerdict(spec.successCriteria, aligned, evaluation.reason, evaluation.nextRoundFocus)
|
|
1280
|
+
: `${metCount}/${total} criteria met — ${evaluation.reason}`;
|
|
801
1281
|
yield {
|
|
802
1282
|
type: "council_message",
|
|
803
1283
|
councilMessage: {
|
|
804
1284
|
kind: "leader",
|
|
1285
|
+
phase: leaderConductorEnabled() && hasPinned ? "verdict" : undefined,
|
|
805
1286
|
speaker: { role: "Leader", model: leaderModelId },
|
|
806
1287
|
round,
|
|
807
|
-
text:
|
|
1288
|
+
text: verdictText,
|
|
1289
|
+
// Spans every evaluateDebate attempt including fallback models, so a
|
|
1290
|
+
// leader that ate the round is attributable rather than hidden inside
|
|
1291
|
+
// the `debate_complete` aggregate.
|
|
1292
|
+
durationMs: Date.now() - evalStart,
|
|
808
1293
|
},
|
|
809
1294
|
};
|
|
1295
|
+
// P5 — guaranteed done record for this round. Decision reflects the
|
|
1296
|
+
// LEADER's intent (extend / continue / stop); a later code-side convergence
|
|
1297
|
+
// override that stops the loop is a separate mechanism and doesn't rewrite
|
|
1298
|
+
// the leader's stated call. Carry the leader's nextRoundFocus to the next
|
|
1299
|
+
// round's topic.
|
|
1300
|
+
const leaderDecision = typeof evaluation.extendRounds === "number" && evaluation.extendRounds > 0
|
|
1301
|
+
? "extend"
|
|
1302
|
+
: evaluation.shouldContinue
|
|
1303
|
+
? "continue"
|
|
1304
|
+
: "stop";
|
|
1305
|
+
yield roundRec("done", {
|
|
1306
|
+
criteriaMet: metCount,
|
|
1307
|
+
criteriaTotal: total,
|
|
1308
|
+
leaderReason: evaluation.reason,
|
|
1309
|
+
leaderDecision,
|
|
1310
|
+
nextRoundFocus: evaluation.nextRoundFocus,
|
|
1311
|
+
directive: roundDirective,
|
|
1312
|
+
});
|
|
1313
|
+
nextTopic = evaluation.nextRoundFocus;
|
|
810
1314
|
if (evaluation.needsResearch && evaluation.researchQuery) {
|
|
811
1315
|
const midPhaseId = `phase:mid-research-${round}`;
|
|
812
1316
|
const midStart = Date.now();
|
|
@@ -818,7 +1322,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
818
1322
|
});
|
|
819
1323
|
const researchCandidate = participants.find((c) => c.role === "research") ?? participants[0];
|
|
820
1324
|
const midTraces = [];
|
|
821
|
-
const findings = yield* tracedAsync(() => llm
|
|
1325
|
+
const findings = yield* tracedAsync(() => researchWithFallback(llm, researchCandidate.model, evaluation.researchQuery, enrichedContext, signal, (t) => midTraces.push(t), {}, fallbackPool, config.runIsolatedTask), {
|
|
822
1326
|
phase: "research",
|
|
823
1327
|
label: "Mid-debate research",
|
|
824
1328
|
detail: evaluation.researchQuery.slice(0, 80),
|
|
@@ -855,11 +1359,40 @@ export async function* runDebate(spec, config, llm) {
|
|
|
855
1359
|
}
|
|
856
1360
|
}
|
|
857
1361
|
if (!evaluation.shouldContinue) {
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
1362
|
+
// B4 escalation site 1 — the leader is declaring the debate done. If
|
|
1363
|
+
// pinned criteria are still unmet and we have an interactive channel,
|
|
1364
|
+
// ask the user before accepting a partial outcome (the "3/5 → stop,
|
|
1365
|
+
// synthesize as if done" gap). An "extend" cancels the stop and runs
|
|
1366
|
+
// more rounds; accept/rescope confirm it.
|
|
1367
|
+
if (config.respondToQuestion &&
|
|
1368
|
+
leaderEscalationEnabled() &&
|
|
1369
|
+
!escalated &&
|
|
1370
|
+
hasPinned &&
|
|
1371
|
+
pinnedUnmet > 0 &&
|
|
1372
|
+
// Respect the leader's own verdict: if it declared every criterion met,
|
|
1373
|
+
// don't second-guess it with the fuzzy per-criterion alignment (which
|
|
1374
|
+
// can miss a match and show a false unmet). Escalate only on a genuine
|
|
1375
|
+
// stop-with-unmet the leader itself signalled.
|
|
1376
|
+
!evaluation.allCriteriaMet) {
|
|
1377
|
+
const openList = spec.successCriteria.filter((_, i) => !aligned[i]).map((c) => shortCriterion(c, 56));
|
|
1378
|
+
const outcome = yield* escalateStop(roundsSinceProgress >= 2, pinnedUnmet, openList);
|
|
1379
|
+
if (outcome === "extend") {
|
|
1380
|
+
userExtendedThisRound = true;
|
|
1381
|
+
// Fall through — the loop runs the user-granted rounds. Skip the
|
|
1382
|
+
// convergence + auto-remedy stop-logic below (both assume a natural
|
|
1383
|
+
// stop); the inter-round summary still runs.
|
|
1384
|
+
}
|
|
1385
|
+
else {
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
else {
|
|
1390
|
+
yield {
|
|
1391
|
+
type: "content",
|
|
1392
|
+
content: `\n> Leader decided: debate sufficient at round ${round}.\n`,
|
|
1393
|
+
};
|
|
1394
|
+
break;
|
|
1395
|
+
}
|
|
863
1396
|
}
|
|
864
1397
|
// Code-side convergence override. When the latest round had ≥80% of
|
|
865
1398
|
// pair-turns containing lock phrases ("everything locked", "fully
|
|
@@ -875,7 +1408,9 @@ export async function* runDebate(spec, config, llm) {
|
|
|
875
1408
|
const lockRatio = convergenceRatio(lastRoundTurns);
|
|
876
1409
|
const skepticClean = Array.isArray(evaluation.unresolvedPoints) && evaluation.unresolvedPoints.length === 0;
|
|
877
1410
|
const canExitEarly = (round >= 2 && lockRatio >= 0.8) || (round === 1 && lockRatio >= 0.8 && skepticClean);
|
|
878
|
-
|
|
1411
|
+
// A user "extend" at this round's stop boundary overrides a convergence
|
|
1412
|
+
// break — the user explicitly asked for more rounds to close open criteria.
|
|
1413
|
+
if (canExitEarly && !userExtendedThisRound) {
|
|
879
1414
|
const reason = round === 1
|
|
880
1415
|
? `round 1 converged early (lock=${Math.round(lockRatio * 100)}%, no unresolved points)`
|
|
881
1416
|
: `${Math.round(lockRatio * 100)}% of round ${round} turns contained lock phrases`;
|
|
@@ -885,24 +1420,60 @@ export async function* runDebate(spec, config, llm) {
|
|
|
885
1420
|
};
|
|
886
1421
|
break;
|
|
887
1422
|
}
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
//
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1423
|
+
// Budget-exhaustion remedy (B4). At the last planned round with ceiling
|
|
1424
|
+
// headroom, two triggers extend the debate:
|
|
1425
|
+
// 1. the leader explicitly asked (extendRounds > 0), OR
|
|
1426
|
+
// 2. auto-remedy — pinned criteria are still unmet AND progress is being
|
|
1427
|
+
// made (a new criterion was met within the last 2 rounds).
|
|
1428
|
+
// "Done = all pinned criteria met"; the ceiling is a leader-managed budget,
|
|
1429
|
+
// not a give-up at the initial plan. A stuck criterion (no progress for 2
|
|
1430
|
+
// rounds) fails the guard so we don't burn the ceiling chasing it — the
|
|
1431
|
+
// closing verdict below then escalates it as stuck. Both the absolute
|
|
1432
|
+
// ceiling AND the kind cap still apply (leader can't push an
|
|
1433
|
+
// implementation_plan cap of 3 to 4).
|
|
1434
|
+
const leaderAskedExtend = typeof evaluation.extendRounds === "number" && evaluation.extendRounds > 0;
|
|
1435
|
+
const autoRemedy = leaderAutoRemedyEnabled() && autoRemedyWantsExtend(pinnedUnmet, roundsSinceProgress);
|
|
1436
|
+
if (round === maxRounds && maxRounds < effectiveCeiling && (leaderAskedExtend || autoRemedy)) {
|
|
1437
|
+
const requested = leaderAskedExtend ? Math.max(1, Math.floor(evaluation.extendRounds)) : 1;
|
|
896
1438
|
const newMax = Math.min(effectiveCeiling, maxRounds + requested);
|
|
897
1439
|
const grantedExtra = newMax - maxRounds;
|
|
898
1440
|
if (grantedExtra > 0) {
|
|
1441
|
+
const why = leaderAskedExtend
|
|
1442
|
+
? "unresolved points remain"
|
|
1443
|
+
: `${pinnedUnmet} pinned criteri${pinnedUnmet === 1 ? "on" : "a"} still unmet`;
|
|
899
1444
|
yield {
|
|
900
1445
|
type: "content",
|
|
901
|
-
content: `\n> Leader extending debate by ${grantedExtra} round${grantedExtra === 1 ? "" : "s"} (now ${newMax}/${ABSOLUTE_MAX_ROUNDS}) —
|
|
1446
|
+
content: `\n> Leader extending debate by ${grantedExtra} round${grantedExtra === 1 ? "" : "s"} (now ${newMax}/${ABSOLUTE_MAX_ROUNDS}) — ${why}.\n`,
|
|
902
1447
|
};
|
|
903
1448
|
maxRounds = newMax;
|
|
1449
|
+
// Steer the extra round at the open criteria when auto-remedy fired and
|
|
1450
|
+
// the leader set no focus of its own.
|
|
1451
|
+
if (autoRemedy && !leaderAskedExtend && !nextTopic) {
|
|
1452
|
+
const openList = spec.successCriteria.filter((_, i) => !aligned[i]).map((c) => shortCriterion(c, 48));
|
|
1453
|
+
nextTopic = `Close the unmet criteria: ${openList.join("; ")}`;
|
|
1454
|
+
}
|
|
904
1455
|
}
|
|
905
1456
|
}
|
|
1457
|
+
// B4 escalation site 2 — the leader wanted to keep going but we're at the
|
|
1458
|
+
// last round and auto-remedy couldn't extend (stuck, or the ceiling is
|
|
1459
|
+
// reached). Rather than let the loop exit into a diagnostic-only synthesis,
|
|
1460
|
+
// ask the user. An "extend" bumps maxRounds so the loop continues; accept/
|
|
1461
|
+
// rescope let it exit naturally. `!escalated` already prevents a double-ask
|
|
1462
|
+
// if site 1 fired this round.
|
|
1463
|
+
if (round === maxRounds &&
|
|
1464
|
+
config.respondToQuestion &&
|
|
1465
|
+
leaderEscalationEnabled() &&
|
|
1466
|
+
!escalated &&
|
|
1467
|
+
hasPinned &&
|
|
1468
|
+
!evaluation.allCriteriaMet &&
|
|
1469
|
+
escalationWanted({
|
|
1470
|
+
pinnedUnmet,
|
|
1471
|
+
stuck: roundsSinceProgress >= 2,
|
|
1472
|
+
atCeiling: maxRounds >= effectiveCeiling,
|
|
1473
|
+
})) {
|
|
1474
|
+
const openList = spec.successCriteria.filter((_, i) => !aligned[i]).map((c) => shortCriterion(c, 56));
|
|
1475
|
+
yield* escalateStop(roundsSinceProgress >= 2, pinnedUnmet, openList);
|
|
1476
|
+
}
|
|
906
1477
|
}
|
|
907
1478
|
else {
|
|
908
1479
|
yield phaseDone({
|
|
@@ -912,6 +1483,33 @@ export async function* runDebate(spec, config, llm) {
|
|
|
912
1483
|
startedAt: evalStart,
|
|
913
1484
|
detail: "evaluation unavailable — continuing",
|
|
914
1485
|
});
|
|
1486
|
+
// P5 — eval parse failed: still close the round with a done record so the
|
|
1487
|
+
// grouped transcript never shows a round stuck "running". Do NOT set
|
|
1488
|
+
// leaderReason here — the "eval-unavailable" DECISION_LABEL already conveys
|
|
1489
|
+
// it, and a redundant reason line rendered the message twice on the card
|
|
1490
|
+
// (observed session dd34c59c63e9).
|
|
1491
|
+
yield roundRec("done", { leaderDecision: "eval-unavailable", directive: roundDirective });
|
|
1492
|
+
nextTopic = undefined;
|
|
1493
|
+
// F2 — final-round eval-unavailable must not silently drop an unmet
|
|
1494
|
+
// outcome. The eval failed to parse even after the cross-provider fallback
|
|
1495
|
+
// loop, so there is no fresh criteria status; fall back to the last
|
|
1496
|
+
// successful round's alignment (lastCriteriaMet), treating an empty history
|
|
1497
|
+
// as all-unmet. If this is the last round and we have an interactive
|
|
1498
|
+
// channel with criteria still open, consult the user (same B4 escalation)
|
|
1499
|
+
// instead of proceeding straight to synthesis. stuck=true: a broken eval
|
|
1500
|
+
// gives no progress signal, so the diagnostic frames the criteria as
|
|
1501
|
+
// needing evidence/rescope rather than "more debate".
|
|
1502
|
+
if (round === maxRounds &&
|
|
1503
|
+
config.respondToQuestion &&
|
|
1504
|
+
leaderEscalationEnabled() &&
|
|
1505
|
+
!escalated &&
|
|
1506
|
+
spec.successCriteria.length > 0) {
|
|
1507
|
+
const unmetIdx = spec.successCriteria.map((_, i) => i).filter((i) => lastCriteriaMet[i] !== true);
|
|
1508
|
+
if (unmetIdx.length > 0) {
|
|
1509
|
+
const openList = unmetIdx.map((i) => shortCriterion(spec.successCriteria[i], 56));
|
|
1510
|
+
yield* escalateStop(true, unmetIdx.length, openList);
|
|
1511
|
+
}
|
|
1512
|
+
}
|
|
915
1513
|
}
|
|
916
1514
|
// Generate inter-round summary
|
|
917
1515
|
if (round < maxRounds) {
|
|
@@ -924,7 +1522,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
924
1522
|
});
|
|
925
1523
|
try {
|
|
926
1524
|
const allEx = [...exchangeLogs.values()].flat().slice(-6).join("\n\n");
|
|
927
|
-
const { system, prompt } = buildRoundSummaryPrompt(allEx, spec.problemStatement, round);
|
|
1525
|
+
const { system, prompt } = buildRoundSummaryPrompt(allEx, spec.problemStatement, round, debateLanguage);
|
|
928
1526
|
// Round summary is mechanical condensation — drop to "fast" tier on the leader's
|
|
929
1527
|
// provider when cost-aware. Fall back to the first participant's model otherwise
|
|
930
1528
|
// (matches the pre-cost-aware behavior).
|
|
@@ -961,6 +1559,57 @@ export async function* runDebate(spec, config, llm) {
|
|
|
961
1559
|
});
|
|
962
1560
|
}
|
|
963
1561
|
}
|
|
1562
|
+
// C — snapshot the fully-completed round (transcript + summary persisted) so
|
|
1563
|
+
// a break before synthesis resumes from round+1 instead of round 1. roundCount
|
|
1564
|
+
// was set to `round` at the top of this iteration. Non-fatal on write failure.
|
|
1565
|
+
if (config.checkpointDir) {
|
|
1566
|
+
await writeDebateCheckpoint(config.checkpointDir, buildDebateCheckpoint({
|
|
1567
|
+
problemStatement: spec.problemStatement,
|
|
1568
|
+
roundCount,
|
|
1569
|
+
maxRounds,
|
|
1570
|
+
exchangeLogs,
|
|
1571
|
+
runningSummary,
|
|
1572
|
+
researchFindings,
|
|
1573
|
+
active,
|
|
1574
|
+
archive,
|
|
1575
|
+
lastCriteriaMet,
|
|
1576
|
+
bestCriteriaMetCount,
|
|
1577
|
+
roundsSinceProgress,
|
|
1578
|
+
nextTopic,
|
|
1579
|
+
savedAt: new Date().toISOString(),
|
|
1580
|
+
}));
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
// B4 leader remedy: the debate has ended — via leader stop, convergence, or
|
|
1584
|
+
// budget exhaustion (after auto-remedy exhausted the ceiling). If pinned
|
|
1585
|
+
// criteria remain unmet, the leader emits a visible closing verdict that
|
|
1586
|
+
// DIAGNOSES why it stopped and gives an actionable next move, instead of
|
|
1587
|
+
// letting synthesis proceed as if the outcome were fully achieved (the "3/5 →
|
|
1588
|
+
// stop, synthesize as if done" gap). When the user was consulted (B4
|
|
1589
|
+
// escalation) and chose to accept/rescope, the remedy reflects that decision
|
|
1590
|
+
// instead of a generic "re-run" shrug.
|
|
1591
|
+
if (leaderConductorEnabled() && spec.successCriteria.length > 0) {
|
|
1592
|
+
const unmet = spec.successCriteria.filter((_, i) => !lastCriteriaMet[i]);
|
|
1593
|
+
if (unmet.length > 0) {
|
|
1594
|
+
const atCeiling = maxRounds >= effectiveCeiling;
|
|
1595
|
+
const stuck = roundsSinceProgress >= 2;
|
|
1596
|
+
const remedy = escalation?.action === "accept"
|
|
1597
|
+
? "you accepted these as open — synthesis proceeds with them noted as unresolved."
|
|
1598
|
+
: escalation?.action === "rescope"
|
|
1599
|
+
? "you asked to narrow the scope — re-run the council on just these criteria with a tighter problem statement."
|
|
1600
|
+
: diagnoseUnmetRemedy({ stuck, atCeiling, effectiveCeiling, roundsSinceProgress });
|
|
1601
|
+
yield {
|
|
1602
|
+
type: "council_message",
|
|
1603
|
+
councilMessage: {
|
|
1604
|
+
kind: "leader",
|
|
1605
|
+
phase: "verdict",
|
|
1606
|
+
speaker: { role: "Leader", model: leaderModelId },
|
|
1607
|
+
text: `Debate ended with ${unmet.length} of ${spec.successCriteria.length} criteri` +
|
|
1608
|
+
`${unmet.length === 1 ? "on" : "a"} still unmet: ${unmet.map((c) => shortCriterion(c, 56)).join("; ")}. ` +
|
|
1609
|
+
`Synthesis notes these as open — ${remedy}`,
|
|
1610
|
+
},
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
964
1613
|
}
|
|
965
1614
|
// Compute cumulative evidence density across the WHOLE debate, not just
|
|
966
1615
|
// the leader's last per-round evaluation. Citations are concentrated in
|
|
@@ -969,12 +1618,112 @@ export async function* runDebate(spec, config, llm) {
|
|
|
969
1618
|
// slice (as lastEvidenceDensity does) reports 0.00 even when the debate
|
|
970
1619
|
// actually produced [CONFIRMED via …] tags earlier — that's the bug
|
|
971
1620
|
// session ea13da132dec hit despite 2 real web_fetch citations.
|
|
972
|
-
|
|
1621
|
+
// #3 — grounding-verify. When the debate produced weak evidence density
|
|
1622
|
+
// (debaters barely tagged claims, since each is capped at stepCountIs(2)), run
|
|
1623
|
+
// an isolated explore sub-agent to fact-check the load-bearing claims and emit
|
|
1624
|
+
// authoritative [CONFIRMED]/[REFUTED] tags. Its output is folded into the
|
|
1625
|
+
// density INPUT below (NOT into exchangeLogs, keeping the persisted transcript
|
|
1626
|
+
// clean) and surfaced as a council_message. Fires only when a bridge is wired,
|
|
1627
|
+
// the flag is on, we're not aborting, and grounding is genuinely weak.
|
|
1628
|
+
let groundingVerifyText = "";
|
|
1629
|
+
{
|
|
1630
|
+
const preText = [...exchangeLogs.values()].flat().join("\n\n");
|
|
1631
|
+
if (config.runIsolatedTask &&
|
|
1632
|
+
groundingVerifyEnabled() &&
|
|
1633
|
+
!signal?.aborted &&
|
|
1634
|
+
computeEvidenceDensity(preText) < GROUNDING_VERIFY_THRESHOLD) {
|
|
1635
|
+
const gvStart = Date.now();
|
|
1636
|
+
yield phaseStart({
|
|
1637
|
+
phaseId: "phase:grounding-verify",
|
|
1638
|
+
kind: "grounding_verify",
|
|
1639
|
+
label: "Grounding verification",
|
|
1640
|
+
detail: "verifying claims against evidence",
|
|
1641
|
+
});
|
|
1642
|
+
const gvTraces = [];
|
|
1643
|
+
const gvOut = yield* tracedAsync(() => runGroundingVerify(config.runIsolatedTask, leaderModelId, spec.problemStatement, preText, (t) => gvTraces.push(t)), { phase: "research", label: "Verifying claims against evidence", role: "verify" });
|
|
1644
|
+
for (const t of gvTraces)
|
|
1645
|
+
yield { type: "council_status", content: t };
|
|
1646
|
+
yield phaseDone({
|
|
1647
|
+
phaseId: "phase:grounding-verify",
|
|
1648
|
+
kind: "grounding_verify",
|
|
1649
|
+
label: "Grounding verification",
|
|
1650
|
+
startedAt: gvStart,
|
|
1651
|
+
});
|
|
1652
|
+
// Only fold in when it actually produced verified citations — appending
|
|
1653
|
+
// pure [UNVERIFIED] output would LOWER density, defeating the purpose.
|
|
1654
|
+
if (gvOut && countCitations(gvOut) > 0) {
|
|
1655
|
+
groundingVerifyText = gvOut;
|
|
1656
|
+
yield {
|
|
1657
|
+
type: "council_message",
|
|
1658
|
+
councilMessage: {
|
|
1659
|
+
kind: "research",
|
|
1660
|
+
speaker: { role: "verify", model: leaderModelId },
|
|
1661
|
+
text: gvOut,
|
|
1662
|
+
},
|
|
1663
|
+
};
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
const fullExchangeText = [...exchangeLogs.values()].flat().join("\n\n") + (groundingVerifyText ? `\n\n${groundingVerifyText}` : "");
|
|
973
1668
|
const cumulativeDensity = computeEvidenceDensity(fullExchangeText);
|
|
974
1669
|
// Prefer cumulative when it exceeds the leader's last-round measurement —
|
|
975
1670
|
// we don't want a converged final round (which has fewer fact-claims to
|
|
976
1671
|
// tag) to wipe out evidence work done earlier in the debate.
|
|
977
1672
|
const finalEvidenceDensity = Math.max(cumulativeDensity, lastEvidenceDensity ?? 0);
|
|
1673
|
+
// Count of claims participants explicitly TAGGED ([CONFIRMED]/[REFUTED]/
|
|
1674
|
+
// [UNVERIFIED]) across the whole debate. Lets the confidence badge tell
|
|
1675
|
+
// "measured 0% grounding" apart from "no tags emitted → not measurable"
|
|
1676
|
+
// (session de4bafe5ecb7: substantive debate, zero tags → misleading 0%).
|
|
1677
|
+
const finalTaggedClaims = countCitations(fullExchangeText) + countUnverified(fullExchangeText);
|
|
1678
|
+
// F9 root fix — `runningSummary` is only produced as an INTER-round summary
|
|
1679
|
+
// (`if (round < maxRounds)` above), so a debate that ends on its final/only
|
|
1680
|
+
// round, or stops early after round 1, returns an empty summary — and the
|
|
1681
|
+
// /ideal research artifacts (research.md, delegations.md) silently lose the
|
|
1682
|
+
// whole debate. Generate a real closing summary from the full exchange when
|
|
1683
|
+
// none exists; if there were no discussion rounds (openings-only) or the
|
|
1684
|
+
// model call fails, fall back to a deterministic synthesis from the
|
|
1685
|
+
// participants' positions so the returned state is never emptily summarized.
|
|
1686
|
+
if (!runningSummary.trim()) {
|
|
1687
|
+
const exchangeTurns = [...exchangeLogs.values()].flat();
|
|
1688
|
+
if (exchangeTurns.length > 0) {
|
|
1689
|
+
const closeId = "phase:summary-final";
|
|
1690
|
+
const closeStart = Date.now();
|
|
1691
|
+
yield phaseStart({ phaseId: closeId, kind: "summary", label: "Closing summary" });
|
|
1692
|
+
try {
|
|
1693
|
+
const { system, prompt } = buildRoundSummaryPrompt(exchangeTurns.slice(-6).join("\n\n"), spec.problemStatement, roundCount, debateLanguage);
|
|
1694
|
+
const summaryModel = pickCouncilTaskModel("round_summary", leaderModelId, costAware);
|
|
1695
|
+
runningSummary = yield* tracedGenerate(llm, {
|
|
1696
|
+
phase: "summary",
|
|
1697
|
+
label: "Summarizing the debate",
|
|
1698
|
+
modelId: costAware ? summaryModel : active[0].model,
|
|
1699
|
+
system,
|
|
1700
|
+
prompt,
|
|
1701
|
+
maxTokens: 512,
|
|
1702
|
+
});
|
|
1703
|
+
yield phaseDone({ phaseId: closeId, kind: "summary", label: "Closing summary", startedAt: closeStart });
|
|
1704
|
+
}
|
|
1705
|
+
catch {
|
|
1706
|
+
yield phaseDone({
|
|
1707
|
+
phaseId: closeId,
|
|
1708
|
+
kind: "summary",
|
|
1709
|
+
label: "Closing summary",
|
|
1710
|
+
startedAt: closeStart,
|
|
1711
|
+
detail: "skipped",
|
|
1712
|
+
});
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
// Deterministic backstop: openings-only debates (no exchange turns) or a
|
|
1716
|
+
// failed/empty closing-summary call still yield a non-empty summary.
|
|
1717
|
+
if (!runningSummary.trim()) {
|
|
1718
|
+
runningSummary = resolveDebateSummary({ runningSummary, active, archive });
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
// C — the debate reached synthesis (normal completion or user-cancel fall-
|
|
1722
|
+
// through), so the checkpoint is obsolete. A mid-round THROW never reaches
|
|
1723
|
+
// here, leaving the checkpoint for the loop-driver retry / resume path.
|
|
1724
|
+
if (config.checkpointDir) {
|
|
1725
|
+
await deleteDebateCheckpoint(config.checkpointDir);
|
|
1726
|
+
}
|
|
978
1727
|
return {
|
|
979
1728
|
spec,
|
|
980
1729
|
exchangeLogs,
|
|
@@ -984,6 +1733,12 @@ export async function* runDebate(spec, config, llm) {
|
|
|
984
1733
|
active,
|
|
985
1734
|
archive,
|
|
986
1735
|
finalEvidenceDensity,
|
|
1736
|
+
finalTaggedClaims,
|
|
1737
|
+
escalation,
|
|
1738
|
+
// F1 — expose the last successful round's criteria alignment so the
|
|
1739
|
+
// post-debate card can frame an unmet outcome as provisional. Undefined when
|
|
1740
|
+
// no round eval ever produced a criteria status (card treats as all-unmet).
|
|
1741
|
+
finalCriteriaMet: lastCriteriaMet.length > 0 ? lastCriteriaMet : undefined,
|
|
987
1742
|
};
|
|
988
1743
|
}
|
|
989
1744
|
export async function* evaluateResearchNeed(spec, leaderModelId, conversationContext, llm, costAware = false) {
|
|
@@ -1011,21 +1766,39 @@ export async function* evaluateResearchNeed(spec, leaderModelId, conversationCon
|
|
|
1011
1766
|
}
|
|
1012
1767
|
return true;
|
|
1013
1768
|
}
|
|
1014
|
-
async function* evaluateDebate(spec, exchangeText, round, leaderModelId, llm, costAware = false
|
|
1769
|
+
async function* evaluateDebate(spec, exchangeText, round, leaderModelId, llm, costAware = false,
|
|
1770
|
+
// When set, evaluate with this exact model instead of the cost-tier pick — used
|
|
1771
|
+
// by the call-site fallback loop to re-run a round eval on a healthy panel model
|
|
1772
|
+
// after the leader's provider rejected the eval payload (Console Go "Upstream
|
|
1773
|
+
// request failed" on glm/kimi; observed session dd34c59c63e9).
|
|
1774
|
+
modelOverride,
|
|
1775
|
+
/** Feature B — resolved council debate language (undefined → English). */
|
|
1776
|
+
debateLanguage) {
|
|
1015
1777
|
try {
|
|
1016
|
-
const { system, prompt } = buildLeaderEvaluationPrompt({
|
|
1017
|
-
|
|
1778
|
+
const { system, prompt } = buildLeaderEvaluationPrompt({
|
|
1779
|
+
spec,
|
|
1780
|
+
exchangeLogs: exchangeText,
|
|
1781
|
+
round,
|
|
1782
|
+
language: debateLanguage,
|
|
1783
|
+
});
|
|
1784
|
+
const modelId = modelOverride ?? pickCouncilTaskModel("evaluate_round", leaderModelId, costAware);
|
|
1018
1785
|
const raw = yield* tracedGenerate(llm, {
|
|
1019
1786
|
phase: "evaluate",
|
|
1020
|
-
label:
|
|
1787
|
+
label: modelOverride
|
|
1788
|
+
? `Leader evaluating round ${round} (fallback: ${modelOverride})`
|
|
1789
|
+
: `Leader evaluating round ${round}`,
|
|
1021
1790
|
modelId,
|
|
1022
1791
|
system,
|
|
1023
1792
|
prompt,
|
|
1024
|
-
|
|
1793
|
+
// Raised from 1024: nextRoundFocus is now the FIRST schema field, and the
|
|
1794
|
+
// whole eval is parsed by a single JSON.parse that returns null on any
|
|
1795
|
+
// truncation — a tight budget could clip the JSON and null the round's
|
|
1796
|
+
// outcome. 1536 keeps the focus line + criteria array intact.
|
|
1797
|
+
maxTokens: 1536,
|
|
1025
1798
|
});
|
|
1026
|
-
const
|
|
1027
|
-
if (
|
|
1028
|
-
const parsed = JSON.parse(
|
|
1799
|
+
const jsonStr = extractEvalJson(raw);
|
|
1800
|
+
if (jsonStr) {
|
|
1801
|
+
const parsed = JSON.parse(jsonStr);
|
|
1029
1802
|
const citationCount = countCitations(exchangeText);
|
|
1030
1803
|
const evidenceDensity = computeEvidenceDensity(exchangeText);
|
|
1031
1804
|
const disagreementResolved = citationCount;
|
|
@@ -1053,15 +1826,372 @@ async function* evaluateDebate(spec, exchangeText, round, leaderModelId, llm, co
|
|
|
1053
1826
|
evidenceDensity,
|
|
1054
1827
|
disagreementResolved,
|
|
1055
1828
|
extendRounds,
|
|
1829
|
+
nextRoundFocus: typeof parsed.nextRoundFocus === "string" &&
|
|
1830
|
+
parsed.nextRoundFocus.trim()
|
|
1831
|
+
? parsed.nextRoundFocus.trim()
|
|
1832
|
+
: undefined,
|
|
1056
1833
|
};
|
|
1057
1834
|
}
|
|
1835
|
+
// F3a — no JSON object found in the model output. Log a diagnosable snippet
|
|
1836
|
+
// (No-Silent-Catch) before falling through to null so eval-unavailable is
|
|
1837
|
+
// never a black box — earlier this returned null silently and the only signal
|
|
1838
|
+
// was the "evaluation unavailable" round label.
|
|
1839
|
+
console.warn(`[council] round-${round} eval: no JSON object in output from ${modelOverride ?? leaderModelId} ` +
|
|
1840
|
+
`(${raw.length} chars): ${raw.slice(0, 160).replace(/\s+/g, " ")}`);
|
|
1058
1841
|
}
|
|
1059
|
-
catch {
|
|
1060
|
-
//
|
|
1842
|
+
catch (err) {
|
|
1843
|
+
// F3a — parse or generate failure. Log with context instead of swallowing:
|
|
1844
|
+
// which model, which round, the message. The debate still continues (returns
|
|
1845
|
+
// null → eval-unavailable), but the failure is now diagnosable remotely.
|
|
1846
|
+
console.error(`[council] round-${round} eval failed on ${modelOverride ?? leaderModelId}: ${err?.message}`, { round, stack: err?.stack?.split("\n").slice(0, 3) });
|
|
1061
1847
|
}
|
|
1062
1848
|
return null;
|
|
1063
1849
|
}
|
|
1064
|
-
|
|
1850
|
+
/**
|
|
1851
|
+
* F3b — robustly extract the leader-evaluation JSON object from a model's raw
|
|
1852
|
+
* output. Replaces a greedy `/\{[\s\S]*\}/` match that could swallow prose,
|
|
1853
|
+
* multiple objects, or a trailing partial object into an unparseable span.
|
|
1854
|
+
*
|
|
1855
|
+
* Strategy: strip code fences, then brace-scan and return the LAST fully
|
|
1856
|
+
* balanced top-level `{…}` object (the eval schema is emitted last, after any
|
|
1857
|
+
* chain-of-thought prose). Returns null only when no balanced object exists.
|
|
1858
|
+
* Deterministic, no LLM call. Braces inside string values are rare in the
|
|
1859
|
+
* machine-emitted eval payload; a real tokenizer would be overkill here.
|
|
1860
|
+
*/
|
|
1861
|
+
export function extractEvalJson(raw) {
|
|
1862
|
+
if (!raw)
|
|
1863
|
+
return null;
|
|
1864
|
+
const unfenced = raw.replace(/```(?:json)?/gi, "").replace(/```/g, "");
|
|
1865
|
+
let best = null;
|
|
1866
|
+
let depth = 0;
|
|
1867
|
+
let start = -1;
|
|
1868
|
+
for (let i = 0; i < unfenced.length; i++) {
|
|
1869
|
+
const ch = unfenced[i];
|
|
1870
|
+
if (ch === "{") {
|
|
1871
|
+
if (depth === 0)
|
|
1872
|
+
start = i;
|
|
1873
|
+
depth++;
|
|
1874
|
+
}
|
|
1875
|
+
else if (ch === "}") {
|
|
1876
|
+
if (depth > 0) {
|
|
1877
|
+
depth--;
|
|
1878
|
+
if (depth === 0 && start >= 0)
|
|
1879
|
+
best = unfenced.slice(start, i + 1);
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
return best;
|
|
1884
|
+
}
|
|
1885
|
+
/**
|
|
1886
|
+
* Format a speaker list as newline-joined "Name — lens" rows for the composing
|
|
1887
|
+
* placeholder (A: live debate preview). Prefers the concrete `focus`, falls back
|
|
1888
|
+
* to the one-sentence `lens`, then to bare name. Dedups identical rows so a
|
|
1889
|
+
* speaker in multiple pairs renders once. Returns undefined when nothing useful
|
|
1890
|
+
* is available so the UI falls back to label-only.
|
|
1891
|
+
*/
|
|
1892
|
+
/**
|
|
1893
|
+
* Project a leader evaluation's `criteriaStatus` onto the PINNED spec criteria,
|
|
1894
|
+
* returning a boolean[] index-aligned to `pinned` (B2/B3). The eval prompt asks
|
|
1895
|
+
* for one entry per criterion in order, so index alignment is the primary path;
|
|
1896
|
+
* when the model drifts (wrong count/order) we fall back to a case-insensitive
|
|
1897
|
+
* substring match either direction, defaulting unmatched criteria to not-met so
|
|
1898
|
+
* a hallucinated "all met" never silently marks an untouched criterion done.
|
|
1899
|
+
*/
|
|
1900
|
+
export function alignCriteriaMet(pinned, status) {
|
|
1901
|
+
const aligned = status.length === pinned.length;
|
|
1902
|
+
return pinned.map((crit, i) => {
|
|
1903
|
+
if (aligned)
|
|
1904
|
+
return status[i]?.met === true;
|
|
1905
|
+
const norm = crit.trim().toLowerCase();
|
|
1906
|
+
const hit = status.find((s) => {
|
|
1907
|
+
const sc = (s.criterion ?? "").trim().toLowerCase();
|
|
1908
|
+
return sc.length > 0 && (sc.includes(norm) || norm.includes(sc));
|
|
1909
|
+
});
|
|
1910
|
+
return hit?.met === true;
|
|
1911
|
+
});
|
|
1912
|
+
}
|
|
1913
|
+
/**
|
|
1914
|
+
* B5 leader-conductor visibility. Default ON; opt out with
|
|
1915
|
+
* MUONROI_LEADER_CONDUCTOR=0 (fallback = pre-B5 behavior, no directive/verdict
|
|
1916
|
+
* messages — keeps headless/legacy transcripts unchanged).
|
|
1917
|
+
*/
|
|
1918
|
+
export function leaderConductorEnabled() {
|
|
1919
|
+
return process.env.MUONROI_LEADER_CONDUCTOR !== "0";
|
|
1920
|
+
}
|
|
1921
|
+
/**
|
|
1922
|
+
* B4 leader auto-remedy. When pinned criteria remain unmet at the round budget's
|
|
1923
|
+
* end, the leader auto-extends toward them (up to the hard ceiling) instead of
|
|
1924
|
+
* stopping at the initial plan — "done = all criteria met", ceiling is a managed
|
|
1925
|
+
* budget. Default ON under the conductor; opt out with
|
|
1926
|
+
* MUONROI_COUNCIL_AUTO_REMEDY=0 (fallback = only leader-requested extensions).
|
|
1927
|
+
*/
|
|
1928
|
+
export function leaderAutoRemedyEnabled() {
|
|
1929
|
+
return leaderConductorEnabled() && process.env.MUONROI_COUNCIL_AUTO_REMEDY !== "0";
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* #3 — grounding-verify pass. When a debate ends with weak evidence density
|
|
1933
|
+
* (< GROUNDING_VERIFY_THRESHOLD, i.e. debaters barely tagged any [CONFIRMED]/
|
|
1934
|
+
* [REFUTED] claims — the common case since each debater is capped at
|
|
1935
|
+
* stepCountIs(2)), an isolated explore sub-agent verifies the load-bearing
|
|
1936
|
+
* claims against the codebase and emits authoritative tags that raise the
|
|
1937
|
+
* density metric council uses for confidence. Default ON when an isolated-task
|
|
1938
|
+
* bridge is wired; opt out with MUONROI_COUNCIL_GROUNDING_VERIFY=0.
|
|
1939
|
+
*/
|
|
1940
|
+
export function groundingVerifyEnabled() {
|
|
1941
|
+
return process.env.MUONROI_COUNCIL_GROUNDING_VERIFY !== "0";
|
|
1942
|
+
}
|
|
1943
|
+
/** Density below which the grounding-verify pass fires (weak grounding). */
|
|
1944
|
+
const GROUNDING_VERIFY_THRESHOLD = 0.3;
|
|
1945
|
+
/**
|
|
1946
|
+
* #3 — run an isolated explore sub-agent that fact-checks the debate's
|
|
1947
|
+
* load-bearing claims and emits `[CONFIRMED via file:line]` / `[REFUTED via …]`
|
|
1948
|
+
* tags (the exact shape `countCitations` recognises). Returns the tagged output
|
|
1949
|
+
* on success, or "" on any failure (caller only folds it in when it actually
|
|
1950
|
+
* raised the citation count).
|
|
1951
|
+
*/
|
|
1952
|
+
export async function runGroundingVerify(runIsolatedTask, model, problemStatement, exchangeText, traceCb) {
|
|
1953
|
+
traceCb(`[grounding-verify] isolated explore sub-agent via ${model}`);
|
|
1954
|
+
const claims = exchangeText.length > 6000 ? `${exchangeText.slice(0, 6000)}\n…[truncated]` : exchangeText;
|
|
1955
|
+
const prompt = `You are grounding-checking a council debate — verify its load-bearing FACTUAL claims against evidence.\n\n` +
|
|
1956
|
+
`## Debate question\n${problemStatement}\n\n` +
|
|
1957
|
+
`## Debaters' claims\n${claims}\n\n` +
|
|
1958
|
+
`## Instructions\n` +
|
|
1959
|
+
`Pick the up-to-5 most decision-relevant factual claims and check EACH against THIS repository's code ` +
|
|
1960
|
+
`(grep/read files) and web sources if available. For every claim you check, emit exactly one inline tag in ` +
|
|
1961
|
+
`this precise format:\n` +
|
|
1962
|
+
` [CONFIRMED via <file:line or URL>] — evidence supports the claim\n` +
|
|
1963
|
+
` [REFUTED via <file:line or URL>] — evidence contradicts the claim\n` +
|
|
1964
|
+
` [UNVERIFIED] — you genuinely could not find evidence\n` +
|
|
1965
|
+
`Return a compact "## Grounding Check" list: one line per checked claim, each restating the claim in ≤15 ` +
|
|
1966
|
+
`words followed by its tag. Do NOT restate the whole debate or add opinions. Prefer [CONFIRMED]/[REFUTED] ` +
|
|
1967
|
+
`with a real citation over [UNVERIFIED].`;
|
|
1968
|
+
try {
|
|
1969
|
+
const result = await withDeadlineRace(() => runIsolatedTask({
|
|
1970
|
+
agent: "explore",
|
|
1971
|
+
description: `Council grounding-verify: ${problemStatement.slice(0, 50)}`,
|
|
1972
|
+
prompt,
|
|
1973
|
+
modelId: model,
|
|
1974
|
+
}), getIsolatedTaskDeadlineMs(), "council-grounding-verify");
|
|
1975
|
+
if (result.success && result.output?.trim())
|
|
1976
|
+
return result.output.trim();
|
|
1977
|
+
traceCb(`[grounding-verify] produced nothing: ${result.error ?? "no output"}`);
|
|
1978
|
+
return "";
|
|
1979
|
+
}
|
|
1980
|
+
catch (err) {
|
|
1981
|
+
traceCb(`[grounding-verify] threw: ${err instanceof Error ? err.message : String(err)}`);
|
|
1982
|
+
return "";
|
|
1983
|
+
}
|
|
1984
|
+
}
|
|
1985
|
+
/**
|
|
1986
|
+
* B4: does auto-remedy want to extend the budget this round? True while pinned
|
|
1987
|
+
* criteria remain unmet AND progress is still being made (a new criterion was
|
|
1988
|
+
* met within the last 2 rounds). A stuck debate (roundsSinceProgress ≥ 2) returns
|
|
1989
|
+
* false so the ceiling isn't burned chasing a criterion that isn't moving.
|
|
1990
|
+
*/
|
|
1991
|
+
export function autoRemedyWantsExtend(pinnedUnmet, roundsSinceProgress) {
|
|
1992
|
+
return pinnedUnmet > 0 && roundsSinceProgress < 2;
|
|
1993
|
+
}
|
|
1994
|
+
/**
|
|
1995
|
+
* B4: the diagnostic closing-remedy line for a debate that ended with unmet
|
|
1996
|
+
* pinned criteria — distinguishes a stuck criterion (needs evidence/rescope)
|
|
1997
|
+
* from a genuine ceiling hit (needs a higher budget) from an ordinary early
|
|
1998
|
+
* stop, so the leader's final word is an actionable next move, not a shrug.
|
|
1999
|
+
*/
|
|
2000
|
+
export function diagnoseUnmetRemedy(opts) {
|
|
2001
|
+
if (opts.stuck) {
|
|
2002
|
+
return (`these made no progress across the last ${opts.roundsSinceProgress} rounds — ` +
|
|
2003
|
+
`they likely need external evidence (research) or a narrower scope, not more debate.`);
|
|
2004
|
+
}
|
|
2005
|
+
if (opts.atCeiling) {
|
|
2006
|
+
return (`the debate hit its ${opts.effectiveCeiling}-round ceiling with these open — ` +
|
|
2007
|
+
`re-run with a higher round budget or split the scope.`);
|
|
2008
|
+
}
|
|
2009
|
+
return `re-run with an extended round budget or a narrower scope to close them.`;
|
|
2010
|
+
}
|
|
2011
|
+
/** Extra rounds a user "extend" grants — can push past effectiveCeiling, never past ABSOLUTE_MAX_ROUNDS. */
|
|
2012
|
+
const ESCALATION_EXTEND_ROUNDS = 2;
|
|
2013
|
+
/**
|
|
2014
|
+
* B4 interactive escalation. When the debate is about to stop with pinned
|
|
2015
|
+
* criteria unmet AND auto-remedy can't help (stuck / at ceiling), hand the
|
|
2016
|
+
* decision to the user instead of silently synthesizing a partial outcome.
|
|
2017
|
+
* Default ON under the conductor; opt out with MUONROI_COUNCIL_ESCALATE=0
|
|
2018
|
+
* (fallback = diagnostic closing verdict only, no askcard).
|
|
2019
|
+
*/
|
|
2020
|
+
export function leaderEscalationEnabled() {
|
|
2021
|
+
return leaderConductorEnabled() && process.env.MUONROI_COUNCIL_ESCALATE !== "0";
|
|
2022
|
+
}
|
|
2023
|
+
/**
|
|
2024
|
+
* B4: should we interrupt to ask the user at this stop boundary? True only when
|
|
2025
|
+
* pinned criteria remain unmet AND the leader can no longer self-remedy — it is
|
|
2026
|
+
* stuck (no progress for ≥2 rounds) or has hit the round ceiling. While progress
|
|
2027
|
+
* is still being made under the ceiling, auto-remedy handles it silently and we
|
|
2028
|
+
* don't nag the user.
|
|
2029
|
+
*/
|
|
2030
|
+
export function escalationWanted(opts) {
|
|
2031
|
+
return opts.pinnedUnmet > 0 && (opts.stuck || opts.atCeiling);
|
|
2032
|
+
}
|
|
2033
|
+
/**
|
|
2034
|
+
* B4: the three escalation choices. When the debate is already at the absolute
|
|
2035
|
+
* safety ceiling, the "extend" option degrades to a disabled-looking accept
|
|
2036
|
+
* (label says so) — we never let the user push past ABSOLUTE_MAX_ROUNDS.
|
|
2037
|
+
*/
|
|
2038
|
+
export function buildEscalationOptions(unmetCount, atAbsoluteMax) {
|
|
2039
|
+
const noun = `${unmetCount} unmet criteri${unmetCount === 1 ? "on" : "a"}`;
|
|
2040
|
+
return [
|
|
2041
|
+
atAbsoluteMax
|
|
2042
|
+
? {
|
|
2043
|
+
label: "Extend (unavailable — at hard ceiling)",
|
|
2044
|
+
description: `The debate already reached the ${ABSOLUTE_MAX_ROUNDS}-round safety ceiling; more rounds aren't allowed. Picking this accepts the outcome as-is.`,
|
|
2045
|
+
value: "escalate_accept",
|
|
2046
|
+
kind: "choice",
|
|
2047
|
+
}
|
|
2048
|
+
: {
|
|
2049
|
+
label: `Extend ${ESCALATION_EXTEND_ROUNDS} more rounds`,
|
|
2050
|
+
description: `Push past the round budget to keep working the ${noun}.`,
|
|
2051
|
+
value: "escalate_extend",
|
|
2052
|
+
kind: "choice",
|
|
2053
|
+
},
|
|
2054
|
+
{
|
|
2055
|
+
label: "Accept as-is",
|
|
2056
|
+
description: `Proceed to synthesis with the ${noun} noted as open.`,
|
|
2057
|
+
value: "escalate_accept",
|
|
2058
|
+
kind: "choice",
|
|
2059
|
+
},
|
|
2060
|
+
{
|
|
2061
|
+
label: "Narrow the scope",
|
|
2062
|
+
description: "Stop and re-scope — synthesis notes the open criteria for a narrower follow-up.",
|
|
2063
|
+
value: "escalate_rescope",
|
|
2064
|
+
kind: "choice",
|
|
2065
|
+
},
|
|
2066
|
+
];
|
|
2067
|
+
}
|
|
2068
|
+
/**
|
|
2069
|
+
* B4: emit the escalation askcard and resolve the user's choice. Yields the
|
|
2070
|
+
* council_question chunk (rendered by the same consumer as clarifier/post-debate
|
|
2071
|
+
* askcards), awaits the responder, echoes the choice, and returns the decision.
|
|
2072
|
+
* `grantedRounds` is pre-computed here (bounded by ABSOLUTE_MAX_ROUNDS) so the
|
|
2073
|
+
* caller only mutates loop state. Any unmatched / empty answer is treated as
|
|
2074
|
+
* "accept" — never a silent hang.
|
|
2075
|
+
*/
|
|
2076
|
+
export async function* runEscalationPrompt(opts) {
|
|
2077
|
+
const { respondToQuestion, openCriteria, pinnedUnmet, stuck, atAbsoluteMax, currentMax } = opts;
|
|
2078
|
+
const noun = `${pinnedUnmet} criteri${pinnedUnmet === 1 ? "on" : "a"}`;
|
|
2079
|
+
const openList = openCriteria.join("; ");
|
|
2080
|
+
const questionId = randomUUID();
|
|
2081
|
+
yield {
|
|
2082
|
+
type: "council_question",
|
|
2083
|
+
content: `**Debate stalled with ${noun} still unmet.**\n> Open: ${openList}`,
|
|
2084
|
+
councilQuestion: {
|
|
2085
|
+
questionId,
|
|
2086
|
+
// Reuse the existing post-debate phase — same askcard renderer, no new UI.
|
|
2087
|
+
phase: "post-debate",
|
|
2088
|
+
question: `The debate reached its ${stuck ? "progress limit" : "round ceiling"} with ${noun} still unmet. ` +
|
|
2089
|
+
`How do you want to proceed?`,
|
|
2090
|
+
context: `Open criteria: ${openList}`,
|
|
2091
|
+
isRequired: false,
|
|
2092
|
+
options: buildEscalationOptions(pinnedUnmet, atAbsoluteMax),
|
|
2093
|
+
defaultIndex: 0,
|
|
2094
|
+
},
|
|
2095
|
+
};
|
|
2096
|
+
let answer = "";
|
|
2097
|
+
try {
|
|
2098
|
+
answer = (await respondToQuestion(questionId))?.trim() ?? "";
|
|
2099
|
+
}
|
|
2100
|
+
catch (err) {
|
|
2101
|
+
// A failed responder must not hang or crash the debate — treat as accept and
|
|
2102
|
+
// log so a broken UI channel is diagnosable (No-Silent-Catch).
|
|
2103
|
+
console.error(`[council] escalation responder failed — accepting outcome as-is: ${err?.message}`, {
|
|
2104
|
+
questionId,
|
|
2105
|
+
stack: err?.stack?.split("\n").slice(0, 3),
|
|
2106
|
+
});
|
|
2107
|
+
answer = "";
|
|
2108
|
+
}
|
|
2109
|
+
if (answer === "escalate_extend" && !atAbsoluteMax) {
|
|
2110
|
+
const newMax = Math.min(ABSOLUTE_MAX_ROUNDS, currentMax + ESCALATION_EXTEND_ROUNDS);
|
|
2111
|
+
const grantedRounds = Math.max(0, newMax - currentMax);
|
|
2112
|
+
if (grantedRounds > 0) {
|
|
2113
|
+
yield {
|
|
2114
|
+
type: "content",
|
|
2115
|
+
content: `\n> User extended debate by ${grantedRounds} round${grantedRounds === 1 ? "" : "s"} (now ${newMax}/${ABSOLUTE_MAX_ROUNDS}) — pushing past the budget to close the open criteria.\n`,
|
|
2116
|
+
};
|
|
2117
|
+
return { action: "extend", grantedRounds };
|
|
2118
|
+
}
|
|
2119
|
+
// No headroom left even though the option showed — fall through to accept.
|
|
2120
|
+
}
|
|
2121
|
+
if (answer === "escalate_rescope") {
|
|
2122
|
+
yield {
|
|
2123
|
+
type: "content",
|
|
2124
|
+
content: `\n ↳ Narrow the scope — ending the debate; synthesis will note the open criteria for a re-scoped follow-up.\n`,
|
|
2125
|
+
};
|
|
2126
|
+
return { action: "rescope", grantedRounds: 0 };
|
|
2127
|
+
}
|
|
2128
|
+
yield {
|
|
2129
|
+
type: "content",
|
|
2130
|
+
content: `\n ↳ Accepted the current outcome with ${noun} open.\n`,
|
|
2131
|
+
};
|
|
2132
|
+
return { action: "accept", grantedRounds: 0 };
|
|
2133
|
+
}
|
|
2134
|
+
/** One-line criterion label for directive/verdict bodies. */
|
|
2135
|
+
export function shortCriterion(c, max = 64) {
|
|
2136
|
+
const t = c.trim().replace(/\s+/g, " ");
|
|
2137
|
+
return t.length > max ? `${t.slice(0, max - 1)}…` : t;
|
|
2138
|
+
}
|
|
2139
|
+
/**
|
|
2140
|
+
* B5: build the leader's pre-round DIRECTIVE body — the round goal plus the
|
|
2141
|
+
* criteria still unmet going into this round. `metSoFar` is the prior round's
|
|
2142
|
+
* aligned criteriaMet (empty before round 1 → everything is unmet).
|
|
2143
|
+
*/
|
|
2144
|
+
export function buildLeaderDirective(round, criteria, metSoFar, focus) {
|
|
2145
|
+
const pending = criteria.filter((_, i) => !metSoFar[i]);
|
|
2146
|
+
const lines = [];
|
|
2147
|
+
const trimmedFocus = focus?.trim();
|
|
2148
|
+
lines.push(trimmedFocus
|
|
2149
|
+
? `Focus: ${trimmedFocus}`
|
|
2150
|
+
: round === 1
|
|
2151
|
+
? "Establish concrete evidence for every outcome criterion."
|
|
2152
|
+
: "Drive the remaining criteria to done.");
|
|
2153
|
+
if (pending.length > 0) {
|
|
2154
|
+
// One criterion per line (bulleted) rather than a single "; "-joined blob.
|
|
2155
|
+
// The joined form produced a ~300-char logical line (5 criteria × ~56 chars)
|
|
2156
|
+
// that word-wrapped into a dense, unreadable block overflowing the round-
|
|
2157
|
+
// summary card and the leader directive bubble — the "loạn/không rõ ràng"
|
|
2158
|
+
// the user reported. A vertical list reads cleanly and wraps per-criterion.
|
|
2159
|
+
lines.push(`Unmet (${pending.length}/${criteria.length}):`);
|
|
2160
|
+
for (const c of pending)
|
|
2161
|
+
lines.push(` • ${shortCriterion(c, 72)}`);
|
|
2162
|
+
}
|
|
2163
|
+
else {
|
|
2164
|
+
lines.push("All criteria met so far — pressure-test the weakest before closing.");
|
|
2165
|
+
}
|
|
2166
|
+
return lines.join("\n");
|
|
2167
|
+
}
|
|
2168
|
+
/**
|
|
2169
|
+
* B5: build the leader's post-round VERDICT body — per-criterion ✓/○ against the
|
|
2170
|
+
* pinned outcome, the leader's reason, and the focus it hands to the next round.
|
|
2171
|
+
*/
|
|
2172
|
+
export function buildLeaderVerdict(criteria, met, reason, nextFocus) {
|
|
2173
|
+
const metCount = met.filter(Boolean).length;
|
|
2174
|
+
const lines = [`${metCount}/${criteria.length} criteria met — ${reason.trim()}`];
|
|
2175
|
+
criteria.forEach((c, i) => {
|
|
2176
|
+
lines.push(`${met[i] ? "✓" : "○"} ${shortCriterion(c, 56)}`);
|
|
2177
|
+
});
|
|
2178
|
+
const nf = nextFocus?.trim();
|
|
2179
|
+
if (nf && metCount < criteria.length)
|
|
2180
|
+
lines.push(`→ Next: ${nf}`);
|
|
2181
|
+
return lines.join("\n");
|
|
2182
|
+
}
|
|
2183
|
+
export function formatSpeakerRoster(list) {
|
|
2184
|
+
const rows = [];
|
|
2185
|
+
for (const s of list) {
|
|
2186
|
+
const name = s.stance?.name ?? s.model;
|
|
2187
|
+
const angle = s.stance?.focus?.trim() || s.stance?.lens?.trim();
|
|
2188
|
+
const row = angle ? `${name} — ${angle}` : name;
|
|
2189
|
+
if (row && !rows.includes(row))
|
|
2190
|
+
rows.push(row);
|
|
2191
|
+
}
|
|
2192
|
+
return rows.length > 0 ? rows.join("\n") : undefined;
|
|
2193
|
+
}
|
|
2194
|
+
export function countCitations(text) {
|
|
1065
2195
|
const matches = text.match(/\[(REFUTED|CONFIRMED) via [^\]]+\]/g);
|
|
1066
2196
|
return matches?.length ?? 0;
|
|
1067
2197
|
}
|
|
@@ -1087,7 +2217,7 @@ function countUnverified(text) {
|
|
|
1087
2217
|
* shown, low confidence is correct. This biases participants (via the
|
|
1088
2218
|
* EVIDENCE_RULE prompt) to either verify or explicitly mark unverified.
|
|
1089
2219
|
*/
|
|
1090
|
-
function computeEvidenceDensity(text) {
|
|
2220
|
+
export function computeEvidenceDensity(text) {
|
|
1091
2221
|
const cited = countCitations(text);
|
|
1092
2222
|
const unverified = countUnverified(text);
|
|
1093
2223
|
const totalTagged = cited + unverified;
|