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