muonroi-cli 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
- package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
- package/dist/packages/agent-harness-core/src/selector.js +2 -2
- package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
- package/dist/packages/agent-harness-opentui/src/install.js +1 -1
- package/dist/src/__test-stubs__/vitest-setup.js +4 -0
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
- package/dist/src/agent-harness/mock-model.d.ts +1 -1
- package/dist/src/agent-harness/mock-model.js +32 -3
- package/dist/src/agent-harness/test-spawn.js +3 -1
- package/dist/src/chat/chat-keychain.js +9 -1
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/cli/keys.d.ts +2 -2
- package/dist/src/cli/keys.js +107 -28
- package/dist/src/cli/usage-report.js +2 -2
- package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
- package/dist/src/council/__tests__/abort-threading.test.js +193 -0
- package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
- package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
- package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
- package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
- package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
- package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
- package/dist/src/council/__tests__/research-tools.test.js +4 -0
- package/dist/src/council/__tests__/round-tools.test.js +6 -0
- package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
- package/dist/src/council/clarifier-question-cap.test.js +73 -0
- package/dist/src/council/clarifier.d.ts +29 -1
- package/dist/src/council/clarifier.js +64 -1
- package/dist/src/council/debate-planner.d.ts +2 -1
- package/dist/src/council/debate-planner.js +5 -3
- package/dist/src/council/debate.d.ts +9 -0
- package/dist/src/council/debate.js +88 -8
- package/dist/src/council/index.d.ts +46 -1
- package/dist/src/council/index.js +125 -19
- package/dist/src/council/llm.js +41 -13
- package/dist/src/council/prompts.js +56 -21
- package/dist/src/council/types.d.ts +14 -1
- package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
- package/dist/src/ee/bridge.d.ts +3 -1
- package/dist/src/ee/bridge.js +9 -3
- package/dist/src/ee/bridge.test.js +1 -2
- package/dist/src/ee/client.js +15 -2
- package/dist/src/ee/recall-ledger.d.ts +20 -0
- package/dist/src/ee/recall-ledger.js +40 -1
- package/dist/src/ee/search.d.ts +25 -0
- package/dist/src/ee/search.js +73 -0
- package/dist/src/ee/types.d.ts +15 -1
- package/dist/src/ee/who-am-i.d.ts +41 -0
- package/dist/src/ee/who-am-i.js +143 -0
- package/dist/src/ee/who-am-i.test.d.ts +1 -0
- package/dist/src/ee/who-am-i.test.js +101 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/council-answers.js +1 -0
- package/dist/src/headless/output.d.ts +30 -1
- package/dist/src/headless/output.js +180 -0
- package/dist/src/headless/output.test.js +61 -1
- package/dist/src/hooks/index.js +48 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +143 -104
- package/dist/src/lsp/client-normalize.test.d.ts +12 -0
- package/dist/src/lsp/client-normalize.test.js +34 -0
- package/dist/src/lsp/client.d.ts +1 -0
- package/dist/src/lsp/client.js +10 -2
- package/dist/src/lsp/manager.d.ts +1 -1
- package/dist/src/lsp/manager.js +2 -2
- package/dist/src/lsp/manager.test.js +2 -2
- package/dist/src/lsp/runtime.d.ts +1 -1
- package/dist/src/lsp/runtime.js +2 -2
- package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
- package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
- package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
- package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
- package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
- package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
- package/dist/src/mcp/client-pool.d.ts +1 -1
- package/dist/src/mcp/client-pool.js +25 -9
- package/dist/src/mcp/ee-tools.d.ts +10 -0
- package/dist/src/mcp/ee-tools.js +34 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
- package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/agent.test.js +5 -2
- package/dist/src/orchestrator/auto-commit.d.ts +110 -0
- package/dist/src/orchestrator/auto-commit.js +364 -0
- package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
- package/dist/src/orchestrator/batch-turn-runner.js +2 -1
- package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
- package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
- package/dist/src/orchestrator/compaction.d.ts +22 -0
- package/dist/src/orchestrator/compaction.js +42 -0
- package/dist/src/orchestrator/delegations.d.ts +1 -0
- package/dist/src/orchestrator/delegations.js +41 -0
- package/dist/src/orchestrator/error-utils.js +27 -2
- package/dist/src/orchestrator/message-processor.d.ts +21 -5
- package/dist/src/orchestrator/message-processor.js +219 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +8 -2
- package/dist/src/orchestrator/orchestrator.js +187 -34
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +145 -0
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +76 -60
- package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
- package/dist/src/orchestrator/safety-askcard.js +74 -0
- package/dist/src/orchestrator/sandbox.test.js +8 -5
- package/dist/src/orchestrator/scope-ceiling.js +1 -1
- package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
- package/dist/src/orchestrator/stall-watchdog.js +23 -0
- package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
- package/dist/src/orchestrator/stream-runner.js +52 -30
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
- package/dist/src/orchestrator/subagent-compactor.js +83 -10
- package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
- package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
- package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
- package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
- package/dist/src/orchestrator/tool-args-repair.js +1 -1
- package/dist/src/orchestrator/tool-engine.d.ts +151 -0
- package/dist/src/orchestrator/tool-engine.js +3079 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
- package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
- package/dist/src/pil/__tests__/discovery.test.js +89 -145
- package/dist/src/pil/__tests__/dual-run.test.js +2 -0
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
- package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
- package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
- package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
- package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
- package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
- package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
- package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
- package/dist/src/pil/__tests__/pipeline.test.js +11 -8
- package/dist/src/pil/__tests__/response-tools.test.js +52 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +5 -3
- package/dist/src/pil/agent-operating-contract.test.js +2 -2
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +1 -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/config.d.ts +20 -0
- package/dist/src/pil/config.js +35 -0
- package/dist/src/pil/discovery-types.d.ts +32 -9
- package/dist/src/pil/discovery.d.ts +3 -3
- package/dist/src/pil/discovery.js +138 -244
- package/dist/src/pil/index.d.ts +1 -1
- package/dist/src/pil/index.js +1 -1
- package/dist/src/pil/layer1-intent.d.ts +9 -1
- package/dist/src/pil/layer1-intent.js +99 -44
- package/dist/src/pil/layer1-intent.test.js +112 -2
- package/dist/src/pil/layer15-context-scan.js +1 -1
- package/dist/src/pil/layer16-clarity.d.ts +21 -25
- package/dist/src/pil/layer16-clarity.js +52 -88
- package/dist/src/pil/layer18-acceptance.d.ts +13 -3
- package/dist/src/pil/layer18-acceptance.js +1 -75
- package/dist/src/pil/layer2-personality.js +7 -8
- package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
- package/dist/src/pil/layer2_5-ponytail.js +41 -0
- package/dist/src/pil/layer3-ee-injection.js +180 -14
- package/dist/src/pil/layer4-gsd.js +22 -8
- package/dist/src/pil/layer5-context.d.ts +1 -0
- package/dist/src/pil/layer5-context.js +26 -11
- package/dist/src/pil/layer6-output.js +23 -5
- package/dist/src/pil/llm-classify.js +3 -3
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +4 -3
- package/dist/src/pil/pipeline.js +15 -1
- package/dist/src/pil/response-tools.d.ts +16 -0
- package/dist/src/pil/response-tools.js +41 -0
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +24 -3
- package/dist/src/pil/task-tier-map.js +7 -7
- package/dist/src/pil/types.d.ts +9 -3
- package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
- package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
- package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
- package/dist/src/product-loop/discovery-interview.d.ts +20 -0
- package/dist/src/product-loop/discovery-interview.js +121 -7
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +20 -20
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/product-loop/sprint-self-verify.js +2 -2
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
- package/dist/src/providers/auth/gcloud.d.ts +28 -0
- package/dist/src/providers/auth/gcloud.js +102 -0
- package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
- package/dist/src/providers/auth/gemini-oauth.js +292 -105
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
- package/dist/src/providers/auth/grok-oauth.js +1 -1
- package/dist/src/providers/auth/registry.js +28 -6
- package/dist/src/providers/gemini.d.ts +1 -1
- package/dist/src/providers/gemini.js +10 -3
- package/dist/src/providers/keychain.d.ts +2 -2
- package/dist/src/providers/keychain.js +20 -2
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/providers/openai.js +5 -1
- package/dist/src/providers/runtime.d.ts +1 -1
- package/dist/src/providers/runtime.js +11 -2
- package/dist/src/providers/siliconflow-sse-repair.js +1 -1
- package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
- package/dist/src/providers/strategies/google.strategy.js +159 -9
- package/dist/src/providers/strategies/thinking-mode.js +19 -6
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/router/decide.test.js +40 -8
- package/dist/src/router/step-router.d.ts +20 -0
- package/dist/src/router/step-router.js +73 -0
- 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.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +19 -19
- package/dist/src/self-qa/orchestrator.js +5 -5
- package/dist/src/self-qa/scenario-planner.js +1 -1
- package/dist/src/self-qa/spec-emitter.js +4 -4
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.d.ts +6 -0
- package/dist/src/storage/sessions.js +49 -42
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +1 -0
- package/dist/src/storage/transcript.js +55 -0
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.js +6 -0
- package/dist/src/tools/bash.d.ts +36 -23
- package/dist/src/tools/bash.js +59 -301
- package/dist/src/tools/bash.test.js +2 -257
- package/dist/src/tools/file-tracker.d.ts +5 -0
- package/dist/src/tools/file-tracker.js +14 -0
- package/dist/src/tools/file.js +2 -0
- package/dist/src/tools/git-safety.d.ts +38 -1
- package/dist/src/tools/git-safety.js +94 -4
- package/dist/src/tools/git-safety.test.js +45 -1
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +56 -0
- package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
- package/dist/src/tools/registry-git-safety.test.js +17 -6
- package/dist/src/tools/registry.d.ts +5 -0
- package/dist/src/tools/registry.js +185 -11
- package/dist/src/types/index.d.ts +7 -1
- package/dist/src/ui/app.d.ts +2 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.js +37 -3
- package/dist/src/ui/components/structured-response-view.js +7 -0
- package/dist/src/ui/components/task-list-panel.js +3 -12
- package/dist/src/ui/containers/chat-feed.d.ts +40 -0
- package/dist/src/ui/containers/chat-feed.js +66 -0
- package/dist/src/ui/containers/modals-layer.d.ts +87 -0
- package/dist/src/ui/containers/modals-layer.js +18 -0
- package/dist/src/ui/modals/session-picker-modal.js +14 -9
- package/dist/src/ui/modals/update-modal.js +2 -1
- package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +16 -1
- package/dist/src/ui/slash/menu-items.js +1 -0
- package/dist/src/ui/slash/ponytail.d.ts +2 -0
- package/dist/src/ui/slash/ponytail.js +19 -0
- package/dist/src/ui/status-bar/store.js +0 -1
- package/dist/src/ui/status-bar/store.test.js +2 -2
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.d.ts +17 -0
- package/dist/src/ui/utils/relaunch.js +28 -4
- package/dist/src/ui/utils/relaunch.test.js +50 -0
- package/dist/src/ui/utils/text.d.ts +11 -0
- package/dist/src/ui/utils/text.js +21 -1
- package/dist/src/ui/utils/text.test.js +25 -1
- package/dist/src/ui/utils/tools.js +3 -1
- package/dist/src/usage/cost-log.js +2 -2
- package/dist/src/usage/estimator.d.ts +14 -0
- package/dist/src/usage/estimator.js +22 -0
- package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/permission-mode.d.ts +33 -0
- package/dist/src/utils/permission-mode.js +164 -3
- package/dist/src/utils/permission-mode.test.js +92 -1
- package/dist/src/utils/settings.d.ts +27 -31
- package/dist/src/utils/settings.js +100 -109
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -31,6 +31,33 @@ const MAX_EMPTY_WITH_TOOLS = 2;
|
|
|
31
31
|
const ABSOLUTE_MAX_ROUNDS = 8;
|
|
32
32
|
/** Default initial round budget when the planner does not propose one. */
|
|
33
33
|
const DEFAULT_PLANNED_ROUNDS = 3;
|
|
34
|
+
/**
|
|
35
|
+
* Per-kind hard ceiling on debate rounds — the leader may extend up to this, never
|
|
36
|
+
* past it. Discussion-style debates (decision / evaluation / investigation) converge
|
|
37
|
+
* fast, so capping them at 3 stops a simple "X or Y?" from burning 5-8 rounds of
|
|
38
|
+
* diminishing returns. Observed live (2026-06-20): a Redis-vs-in-memory decision ran
|
|
39
|
+
* 5 rounds / ~10 min on a slow provider, and the leader's own round-5 note was
|
|
40
|
+
* "remaining disagreements are minor and further rounds would repeat established
|
|
41
|
+
* positions" — i.e. rounds 4-5 added latency, not signal. Greenfield exploration
|
|
42
|
+
* keeps more breadth (5). Kinds absent here fall back to ABSOLUTE_MAX_ROUNDS.
|
|
43
|
+
*/
|
|
44
|
+
const KIND_MAX_ROUNDS = {
|
|
45
|
+
implementation_plan: 3,
|
|
46
|
+
decision: 3,
|
|
47
|
+
evaluation: 3,
|
|
48
|
+
investigation: 3,
|
|
49
|
+
exploration: 5,
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Resolve the initial round budget + hard ceiling from the plan's output-shape kind
|
|
53
|
+
* and the planner-proposed round count. Pure + exported for unit testing the cap.
|
|
54
|
+
*/
|
|
55
|
+
export function resolveDebateRoundBudget(planKind, plannedRounds) {
|
|
56
|
+
const kindCap = planKind !== undefined ? KIND_MAX_ROUNDS[planKind] : undefined;
|
|
57
|
+
const effectiveCeiling = Math.min(ABSOLUTE_MAX_ROUNDS, kindCap ?? ABSOLUTE_MAX_ROUNDS);
|
|
58
|
+
const maxRounds = Math.min(effectiveCeiling, Math.max(1, typeof plannedRounds === "number" && plannedRounds > 0 ? plannedRounds : DEFAULT_PLANNED_ROUNDS));
|
|
59
|
+
return { maxRounds, effectiveCeiling, kindCapped: kindCap !== undefined };
|
|
60
|
+
}
|
|
34
61
|
/** Cap on the size of a single archived position. Anything longer is
|
|
35
62
|
* trimmed and reported via `length`. Mirrors the goal of keeping the
|
|
36
63
|
* follow-up memory record small enough to be reloaded cheaply. */
|
|
@@ -42,6 +69,39 @@ function makeExcerpt(text) {
|
|
|
42
69
|
length: trimmed.length,
|
|
43
70
|
};
|
|
44
71
|
}
|
|
72
|
+
/**
|
|
73
|
+
* Emit the observe-only `council-turn-length` harness event for one fully-assembled
|
|
74
|
+
* speaker turn (opening statement or discussion turn). Reports char + word count so
|
|
75
|
+
* a harness can measure council verbosity per role/model/round — NO truncation, NO
|
|
76
|
+
* behaviour change. Best-effort: a no-op when the agent runtime is absent (normal
|
|
77
|
+
* user mode) or the event kind is filtered out (MUONROI_HARNESS_EVENTS). Uses the
|
|
78
|
+
* same globalThis.__muonroiAgentRuntime emitter as sprint-runner.ts.
|
|
79
|
+
*/
|
|
80
|
+
function emitCouncilTurnLength(args) {
|
|
81
|
+
try {
|
|
82
|
+
const ar = globalThis.__muonroiAgentRuntime;
|
|
83
|
+
if (!ar || typeof ar.emitEvent !== "function")
|
|
84
|
+
return;
|
|
85
|
+
const trimmed = args.text.trim();
|
|
86
|
+
ar.emitEvent({
|
|
87
|
+
t: "event",
|
|
88
|
+
kind: "council-turn-length",
|
|
89
|
+
role: args.role,
|
|
90
|
+
round: args.round,
|
|
91
|
+
charCount: trimmed.length,
|
|
92
|
+
wordCount: trimmed.length === 0 ? 0 : trimmed.split(/\s+/).filter(Boolean).length,
|
|
93
|
+
model: args.model,
|
|
94
|
+
correlationId: args.correlationId,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
// Observe-only telemetry — swallow so a harness hiccup can't break a debate turn.
|
|
99
|
+
// Logged only under MUONROI_DEBUG_HARNESS (No-Silent-Catch) to keep TUI output clean.
|
|
100
|
+
if (process.env.MUONROI_DEBUG_HARNESS === "1") {
|
|
101
|
+
console.error(`[council] council-turn-length emit failed: ${err?.message ?? String(err)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
45
105
|
/**
|
|
46
106
|
* Lock-phrase detector. Counts what fraction of pair-turns in the latest round
|
|
47
107
|
* contain explicit convergence signals. When ≥80% of pair-turns signal "lock",
|
|
@@ -215,6 +275,10 @@ async function debateWithRetry(llm, model, system, prompt, signal, traceCb, tool
|
|
|
215
275
|
}
|
|
216
276
|
export async function* runDebate(spec, config, llm) {
|
|
217
277
|
const { leaderModelId, participants, conversationContext, signal, debatePlan } = config;
|
|
278
|
+
// Correlation id for the observe-only council-turn-length telemetry (groups
|
|
279
|
+
// per-turn length samples by run). sessionId in production; a stable literal
|
|
280
|
+
// for direct callers/tests that omit runId.
|
|
281
|
+
const turnCorrelationId = config.runId ?? "council";
|
|
218
282
|
const researchSkipOverride = config.researchSkipOverride === true;
|
|
219
283
|
const leaderNeedsResearch = config.leaderNeedsResearch;
|
|
220
284
|
const internetFirst = config.internetFirst === true;
|
|
@@ -352,6 +416,13 @@ export async function* runDebate(spec, config, llm) {
|
|
|
352
416
|
attempts: o.attempts,
|
|
353
417
|
},
|
|
354
418
|
};
|
|
419
|
+
emitCouncilTurnLength({
|
|
420
|
+
role: speakerRole,
|
|
421
|
+
round: 0,
|
|
422
|
+
text: o.position,
|
|
423
|
+
model: o.model,
|
|
424
|
+
correlationId: turnCorrelationId,
|
|
425
|
+
});
|
|
355
426
|
}
|
|
356
427
|
}
|
|
357
428
|
yield phaseDone({
|
|
@@ -378,14 +449,9 @@ export async function* runDebate(spec, config, llm) {
|
|
|
378
449
|
// absolute ceiling still applies there.
|
|
379
450
|
let roundCount = 0;
|
|
380
451
|
const planKind = debatePlan?.outputShape?.kind;
|
|
381
|
-
const
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
const kindCap = planKind ? KIND_MAX_ROUNDS[planKind] : undefined;
|
|
385
|
-
const initialPlanned = debatePlan?.plannedRounds;
|
|
386
|
-
const effectiveCeiling = Math.min(ABSOLUTE_MAX_ROUNDS, kindCap ?? ABSOLUTE_MAX_ROUNDS);
|
|
387
|
-
let maxRounds = Math.min(effectiveCeiling, Math.max(1, typeof initialPlanned === "number" && initialPlanned > 0 ? initialPlanned : DEFAULT_PLANNED_ROUNDS));
|
|
388
|
-
const ceilingNote = kindCap
|
|
452
|
+
const { maxRounds: plannedMaxRounds, effectiveCeiling, kindCapped, } = resolveDebateRoundBudget(planKind, debatePlan?.plannedRounds);
|
|
453
|
+
let maxRounds = plannedMaxRounds;
|
|
454
|
+
const ceilingNote = kindCapped
|
|
389
455
|
? ` (hard ceiling ${effectiveCeiling} for ${planKind})`
|
|
390
456
|
: ` (hard ceiling ${ABSOLUTE_MAX_ROUNDS})`;
|
|
391
457
|
yield {
|
|
@@ -401,6 +467,13 @@ export async function* runDebate(spec, config, llm) {
|
|
|
401
467
|
// — the LLM is clearly under provider stress and more rounds won't help.
|
|
402
468
|
let consecutiveRoundFailures = 0;
|
|
403
469
|
for (let round = 1; round <= maxRounds; round++) {
|
|
470
|
+
// User cancelled mid-debate — stop before spending another round of
|
|
471
|
+
// parallel pair LLM calls. The caller (runCouncil) re-checks the signal at
|
|
472
|
+
// its next phase boundary and skips synthesis too.
|
|
473
|
+
if (signal?.aborted) {
|
|
474
|
+
yield { type: "content", content: `\n> Debate cancelled by user.\n` };
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
404
477
|
roundCount = round;
|
|
405
478
|
const p2Start = Date.now();
|
|
406
479
|
const roundPhaseId = `phase:round-${round}`;
|
|
@@ -627,6 +700,13 @@ export async function* runDebate(spec, config, llm) {
|
|
|
627
700
|
attempts: chunk.attempts,
|
|
628
701
|
},
|
|
629
702
|
};
|
|
703
|
+
emitCouncilTurnLength({
|
|
704
|
+
role: speakerName,
|
|
705
|
+
round,
|
|
706
|
+
text: chunk.text,
|
|
707
|
+
model: modelId,
|
|
708
|
+
correlationId: turnCorrelationId,
|
|
709
|
+
});
|
|
630
710
|
}
|
|
631
711
|
for (const trace of chunk.traces ?? []) {
|
|
632
712
|
yield { type: "council_status", content: trace };
|
|
@@ -1,10 +1,31 @@
|
|
|
1
1
|
import type { ModelMessage } from "ai";
|
|
2
2
|
import type { StreamChunk } from "../types/index.js";
|
|
3
3
|
import type { CouncilLLM, CouncilStats, PreflightResponder, QuestionResponder } from "./types.js";
|
|
4
|
+
/**
|
|
5
|
+
* Wrap a CouncilLLM so every `generate` call inherits the council-wide abort
|
|
6
|
+
* signal. The whole generate-based call path (clarifier, research-need eval,
|
|
7
|
+
* leader round-eval, opening statements, round summary, spec/plan synthesis,
|
|
8
|
+
* and the debate-planner retry) calls `llm.generate(...)` with NO signal arg —
|
|
9
|
+
* none of those sites thread one. Injecting it here in ONE place makes them all
|
|
10
|
+
* cancellable without touching each signature. `debate`/`research` already get
|
|
11
|
+
* `config.signal` explicitly, so they pass through unchanged.
|
|
12
|
+
*
|
|
13
|
+
* An explicit per-call signal (none exist today, but the param is there) wins
|
|
14
|
+
* over the injected one. Returns the original llm untouched when no signal is
|
|
15
|
+
* configured (e.g. the sprint-planner path, which has no user-abort signal).
|
|
16
|
+
*/
|
|
17
|
+
export declare function withCouncilSignal(llm: CouncilLLM, signal: AbortSignal | undefined): CouncilLLM;
|
|
4
18
|
export interface RunCouncilOptions {
|
|
5
19
|
skipClarification?: boolean;
|
|
6
20
|
userModelMessage?: ModelMessage;
|
|
7
21
|
signal?: AbortSignal;
|
|
22
|
+
/**
|
|
23
|
+
* Hard cap on clarification rounds for the explicit /council path. Defaults to
|
|
24
|
+
* EXPLICIT_COUNCIL_CLARIFY_ROUNDS (1). Callers that genuinely want the full
|
|
25
|
+
* multi-round ready-gate can raise it; auto-council/sprint pass
|
|
26
|
+
* skipClarification:true and never reach the clarifier regardless.
|
|
27
|
+
*/
|
|
28
|
+
clarifyMaxRounds?: number;
|
|
8
29
|
/** Working directory used to resolve the "current project" snapshot. */
|
|
9
30
|
cwd?: string;
|
|
10
31
|
/** Shared stats object from orchestrator — when provided, runCouncil uses it instead of a local one so stats.calls is accurate (Phase 14 CQ-01). */
|
|
@@ -15,8 +36,32 @@ export interface RunCouncilOptions {
|
|
|
15
36
|
*/
|
|
16
37
|
runDir?: string;
|
|
17
38
|
}
|
|
39
|
+
export type PostDebateAction = "save_exit" | "generate_plan" | "refine" | "ask_followup" | "retry_synthesis";
|
|
40
|
+
/**
|
|
41
|
+
* Decide the DEFAULT post-debate action surfaced as the recommended option.
|
|
42
|
+
* Extracted as a pure function so the policy is unit-testable.
|
|
43
|
+
*
|
|
44
|
+
* Issue #3 (post-debate default mismatch): when synthesis succeeded and no plan
|
|
45
|
+
* exists yet, only an `implementation_plan`-shaped debate should default to
|
|
46
|
+
* "generate_plan" (Lock plan & execute Sprint 1). For a `decision`, `evaluation`,
|
|
47
|
+
* `investigation`, or `exploration` debate the synthesis IS the deliverable — the
|
|
48
|
+
* user asked a question, not for code — so the default is `save_exit`. The
|
|
49
|
+
* generate_plan OPTION is still offered downstream; it's just no longer the
|
|
50
|
+
* pre-selected default for non-build topics.
|
|
51
|
+
*/
|
|
52
|
+
export declare function pickPostDebateRecommendation(input: {
|
|
53
|
+
synthesisFailed: boolean;
|
|
54
|
+
hasEmptySections: boolean;
|
|
55
|
+
refinementTopics: string[];
|
|
56
|
+
confidenceLevel: "high" | "medium" | "low";
|
|
57
|
+
hasPlan: boolean;
|
|
58
|
+
outputKind: string;
|
|
59
|
+
}): {
|
|
60
|
+
value: PostDebateAction;
|
|
61
|
+
reason: string;
|
|
62
|
+
};
|
|
18
63
|
export declare function runCouncil(topic: string, sessionModelId: string, messages: Array<{
|
|
19
64
|
role: string;
|
|
20
65
|
content: string | unknown;
|
|
21
|
-
}>, sessionId: string | undefined,
|
|
66
|
+
}>, sessionId: string | undefined, rawLlm: CouncilLLM, respondToQuestion: QuestionResponder, respondToPreflight: PreflightResponder, processMessageFn: (message: string) => AsyncGenerator<StreamChunk, void, unknown>, options?: RunCouncilOptions): AsyncGenerator<StreamChunk, string | null, unknown>;
|
|
22
67
|
export type { ClarifiedSpec, CouncilLLM, CouncilParticipant, CouncilStats } from "./types.js";
|
|
@@ -15,9 +15,91 @@ import { resolveLeaderModelDetailed, resolveParticipants } from "./leader.js";
|
|
|
15
15
|
import { phaseDone, phaseStart } from "./phase-events.js";
|
|
16
16
|
import { runPlanning } from "./planner.js";
|
|
17
17
|
import { runPreflight } from "./preflight.js";
|
|
18
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Wrap a CouncilLLM so every `generate` call inherits the council-wide abort
|
|
20
|
+
* signal. The whole generate-based call path (clarifier, research-need eval,
|
|
21
|
+
* leader round-eval, opening statements, round summary, spec/plan synthesis,
|
|
22
|
+
* and the debate-planner retry) calls `llm.generate(...)` with NO signal arg —
|
|
23
|
+
* none of those sites thread one. Injecting it here in ONE place makes them all
|
|
24
|
+
* cancellable without touching each signature. `debate`/`research` already get
|
|
25
|
+
* `config.signal` explicitly, so they pass through unchanged.
|
|
26
|
+
*
|
|
27
|
+
* An explicit per-call signal (none exist today, but the param is there) wins
|
|
28
|
+
* over the injected one. Returns the original llm untouched when no signal is
|
|
29
|
+
* configured (e.g. the sprint-planner path, which has no user-abort signal).
|
|
30
|
+
*/
|
|
31
|
+
export function withCouncilSignal(llm, signal) {
|
|
32
|
+
if (!signal)
|
|
33
|
+
return llm;
|
|
34
|
+
return {
|
|
35
|
+
...llm,
|
|
36
|
+
generate: (modelId, system, prompt, maxTokens, onUsage, sig) => llm.generate(modelId, system, prompt, maxTokens, onUsage, sig ?? signal),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Explicit `/council …` is the ONLY caller that runs the clarifier — auto-council
|
|
41
|
+
* (message-processor) and the sprint-planner both pass `skipClarification: true`.
|
|
42
|
+
* Capping it to a single round (down from the ready-gate's MAX_CLARIFY_ROUNDS=12)
|
|
43
|
+
* stops the 2-3 rounds of follow-up askcards users hit on already-detailed topics
|
|
44
|
+
* (see project-council-subsystem memory). The per-round ready-gate has no
|
|
45
|
+
* production behavioural effect anyway — `spec.ready`/`confidenceScore`/
|
|
46
|
+
* `remainingGaps` are write-only — so its only real cost is the extra rounds.
|
|
47
|
+
* Round 0 still runs, so the user is asked the key questions exactly once.
|
|
48
|
+
*/
|
|
49
|
+
const EXPLICIT_COUNCIL_CLARIFY_ROUNDS = 1;
|
|
50
|
+
/**
|
|
51
|
+
* Decide the DEFAULT post-debate action surfaced as the recommended option.
|
|
52
|
+
* Extracted as a pure function so the policy is unit-testable.
|
|
53
|
+
*
|
|
54
|
+
* Issue #3 (post-debate default mismatch): when synthesis succeeded and no plan
|
|
55
|
+
* exists yet, only an `implementation_plan`-shaped debate should default to
|
|
56
|
+
* "generate_plan" (Lock plan & execute Sprint 1). For a `decision`, `evaluation`,
|
|
57
|
+
* `investigation`, or `exploration` debate the synthesis IS the deliverable — the
|
|
58
|
+
* user asked a question, not for code — so the default is `save_exit`. The
|
|
59
|
+
* generate_plan OPTION is still offered downstream; it's just no longer the
|
|
60
|
+
* pre-selected default for non-build topics.
|
|
61
|
+
*/
|
|
62
|
+
export function pickPostDebateRecommendation(input) {
|
|
63
|
+
if (input.synthesisFailed) {
|
|
64
|
+
return {
|
|
65
|
+
value: "retry_synthesis",
|
|
66
|
+
reason: "Re-run synthesis with a compact prompt — usually clears provider-timeout failures.",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
if (input.hasEmptySections) {
|
|
70
|
+
return { value: "refine", reason: `Fill in ${input.refinementTopics.length} section(s) the debate left empty.` };
|
|
71
|
+
}
|
|
72
|
+
if (input.confidenceLevel === "low") {
|
|
73
|
+
return {
|
|
74
|
+
value: "ask_followup",
|
|
75
|
+
reason: "Press the council on the weakest claims rather than accepting a thin synthesis.",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
if (!input.hasPlan) {
|
|
79
|
+
return input.outputKind === "implementation_plan"
|
|
80
|
+
? { value: "generate_plan", reason: "Convert the agreed outcome into concrete steps." }
|
|
81
|
+
: {
|
|
82
|
+
value: "save_exit",
|
|
83
|
+
reason: `This was a ${input.outputKind} debate — the synthesis above is the deliverable; save it.`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return { value: "save_exit", reason: "Outcome looks solid — save and move on." };
|
|
87
|
+
}
|
|
88
|
+
export async function* runCouncil(topic, sessionModelId, messages, sessionId, rawLlm, respondToQuestion, respondToPreflight, processMessageFn, options) {
|
|
19
89
|
const stats = options?.councilStats ?? { calls: 0, startMs: Date.now(), phases: [] };
|
|
20
90
|
const costAware = isCouncilCostAware();
|
|
91
|
+
// Inject the user-abort signal into every generate-based sub-call (clarify,
|
|
92
|
+
// research-need, leader-eval, opening, summary, synthesis, debate-plan retry).
|
|
93
|
+
// No-op passthrough when options.signal is undefined.
|
|
94
|
+
const llm = withCouncilSignal(rawLlm, options?.signal);
|
|
95
|
+
// Hard-stop guard. Threading the signal into LLM calls makes them abortable,
|
|
96
|
+
// but every council sub-phase wraps its work in fail-open try/catch that
|
|
97
|
+
// swallows the resulting AbortError and returns normally — so without an
|
|
98
|
+
// explicit check at each phase boundary the loop would march on to the next
|
|
99
|
+
// phase after a cancel. `userAborted()` is checked between phases; when true
|
|
100
|
+
// the run stops cleanly rather than burning the remaining (debate, synthesis)
|
|
101
|
+
// LLM budget. Cancellation latency is bounded by one in-flight sub-call.
|
|
102
|
+
const userAborted = () => options?.signal?.aborted === true;
|
|
21
103
|
// ── Resolve models ──────────────────────────────────────────────────────────
|
|
22
104
|
const leaderResolution = await resolveLeaderModelDetailed(sessionModelId);
|
|
23
105
|
const leaderModelId = leaderResolution.modelId;
|
|
@@ -50,6 +132,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
50
132
|
: baseContext;
|
|
51
133
|
const internetFirst = projectInfo.isEmpty;
|
|
52
134
|
const active = participants.map((p) => ({ ...p, position: "" }));
|
|
135
|
+
if (userAborted()) {
|
|
136
|
+
yield { type: "content", content: "\n> Council cancelled by user.\n" };
|
|
137
|
+
yield { type: "done" };
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
53
140
|
// ── Phase A + B loop: Clarify → Confirm ─────────────────────────────────────
|
|
54
141
|
let spec = buildSpecFromTopic(topic, conversationContext);
|
|
55
142
|
let approved = false;
|
|
@@ -76,7 +163,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
76
163
|
content: `\n> Clarification seeded by PIL (${pilSeed.length} gray-area question${pilSeed.length === 1 ? "" : "s"}).\n`,
|
|
77
164
|
};
|
|
78
165
|
}
|
|
79
|
-
const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed,
|
|
166
|
+
const clarifyGen = runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, options?.signal, pilSeed, options?.clarifyMaxRounds ?? EXPLICIT_COUNCIL_CLARIFY_ROUNDS, undefined, costAware);
|
|
80
167
|
let clarifyResult;
|
|
81
168
|
do {
|
|
82
169
|
clarifyResult = await clarifyGen.next();
|
|
@@ -90,6 +177,9 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
90
177
|
spec = buildSpecFromTopic(topic, conversationContext);
|
|
91
178
|
yield { type: "content", content: `\n> Auto-council: skipping clarification (PIL pre-classified).\n` };
|
|
92
179
|
}
|
|
180
|
+
// Cancelled during clarification — don't pop the preflight approval card.
|
|
181
|
+
if (userAborted())
|
|
182
|
+
break;
|
|
93
183
|
const researchNeeded = true;
|
|
94
184
|
const preflightGen = runPreflight(spec, participants, researchNeeded, respondToPreflight, {
|
|
95
185
|
repoEmpty: internetFirst,
|
|
@@ -105,6 +195,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
105
195
|
approved = preflightResult.value;
|
|
106
196
|
}
|
|
107
197
|
stats.phases.push({ name: "clarify+preflight", durationMs: Date.now() - phaseAStart });
|
|
198
|
+
if (userAborted()) {
|
|
199
|
+
yield { type: "content", content: "\n> Council cancelled by user.\n" };
|
|
200
|
+
yield { type: "done" };
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
108
203
|
// ── Research-need check + user override ────────────────────────────────────
|
|
109
204
|
// Leader-LLM decides if research is required. If yes, give the user a chance
|
|
110
205
|
// to skip — research is the slowest part of council and trivial questions
|
|
@@ -171,6 +266,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
171
266
|
content: `\n> [Experience] ${eeResult.warnings.length} past warning(s) loaded — Experience Auditor will calibrate debate.\n`,
|
|
172
267
|
};
|
|
173
268
|
}
|
|
269
|
+
if (userAborted()) {
|
|
270
|
+
yield { type: "content", content: "\n> Council cancelled by user.\n" };
|
|
271
|
+
yield { type: "done" };
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
174
274
|
// ── Phase B.5: Leader plans the debate (stances + output shape) ─────────────
|
|
175
275
|
const planStartMs = Date.now();
|
|
176
276
|
yield phaseStart({
|
|
@@ -179,7 +279,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
179
279
|
label: "Debate plan",
|
|
180
280
|
detail: "stances + output shape",
|
|
181
281
|
});
|
|
182
|
-
const planGenerator = planDebate(spec, leaderModelId, llm, eeResult.warnings, experienceMode, pilCtx?.taskType ?? undefined, pilCtx?.complexityTier ?? undefined);
|
|
282
|
+
const planGenerator = planDebate(spec, leaderModelId, llm, eeResult.warnings, experienceMode, pilCtx?.taskType ?? undefined, pilCtx?.complexityTier ?? undefined, options?.signal);
|
|
183
283
|
let planStep;
|
|
184
284
|
do {
|
|
185
285
|
planStep = await planGenerator.next();
|
|
@@ -222,6 +322,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
222
322
|
active.length = debatePlan.stances.length;
|
|
223
323
|
}
|
|
224
324
|
stats.phases.push({ name: "plan_debate", durationMs: Date.now() - planStartMs });
|
|
325
|
+
if (userAborted()) {
|
|
326
|
+
yield { type: "content", content: "\n> Council cancelled by user.\n" };
|
|
327
|
+
yield { type: "done" };
|
|
328
|
+
return null;
|
|
329
|
+
}
|
|
225
330
|
// ── Phase C: Dynamic Debate ─────────────────────────────────────────────────
|
|
226
331
|
const debateStart = Date.now();
|
|
227
332
|
const debateGen = runDebate(spec, {
|
|
@@ -235,6 +340,7 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
235
340
|
leaderNeedsResearch,
|
|
236
341
|
internetFirst,
|
|
237
342
|
costAware,
|
|
343
|
+
runId: sessionId,
|
|
238
344
|
}, llm);
|
|
239
345
|
let debateResult;
|
|
240
346
|
do {
|
|
@@ -270,6 +376,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
270
376
|
durationMs: Date.now() - debateStart,
|
|
271
377
|
data: { topic, roundCount: debateState.roundCount },
|
|
272
378
|
});
|
|
379
|
+
if (userAborted()) {
|
|
380
|
+
yield { type: "content", content: "\n> Council cancelled by user — skipping synthesis.\n" };
|
|
381
|
+
yield { type: "done" };
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
273
384
|
// ── Phase D: Plan ───────────────────────────────────────────────────────────
|
|
274
385
|
const planStart = Date.now();
|
|
275
386
|
const planGen = runPlanning(debateState, spec, debateState.active, leaderModelId, respondToPreflight, llm, debatePlan, pilCtx?.outputStyle ?? undefined);
|
|
@@ -338,22 +449,17 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ll
|
|
|
338
449
|
: confidenceLevel === "medium"
|
|
339
450
|
? `⚠ Medium confidence (evidence density ${evidenceDensity.toFixed(2)})`
|
|
340
451
|
: `⚠ Low confidence (evidence density ${evidenceDensity.toFixed(2)})`;
|
|
341
|
-
// Recommendation surfaced to the user as the default action.
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
reason: "Press the council on the weakest claims rather than accepting a thin synthesis.",
|
|
353
|
-
}
|
|
354
|
-
: !hasPlan
|
|
355
|
-
? { value: "generate_plan", reason: "Convert the agreed outcome into concrete steps." }
|
|
356
|
-
: { value: "save_exit", reason: "Outcome looks solid — save and move on." };
|
|
452
|
+
// Recommendation surfaced to the user as the default action. The
|
|
453
|
+
// implementation_plan-vs-decision/evaluation split lives in
|
|
454
|
+
// pickPostDebateRecommendation (issue #3 — see its doc comment).
|
|
455
|
+
const recommendation = pickPostDebateRecommendation({
|
|
456
|
+
synthesisFailed,
|
|
457
|
+
hasEmptySections,
|
|
458
|
+
refinementTopics,
|
|
459
|
+
confidenceLevel,
|
|
460
|
+
hasPlan: !!hasPlan,
|
|
461
|
+
outputKind: debatePlan.outputShape.kind,
|
|
462
|
+
});
|
|
357
463
|
const baseOptions = [];
|
|
358
464
|
if (synthesisFailed) {
|
|
359
465
|
baseOptions.push({
|
package/dist/src/council/llm.js
CHANGED
|
@@ -4,8 +4,8 @@ import { getDefaultEEClient } from "../ee/intercept.js";
|
|
|
4
4
|
import { emitMatches } from "../ee/render.js";
|
|
5
5
|
import { buildMcpToolSet } from "../mcp/runtime.js";
|
|
6
6
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
7
|
-
import { loadKeyForProvider } from "../providers/keychain.js";
|
|
8
|
-
import {
|
|
7
|
+
import { loadKeyForProvider, ProviderKeyMissingError } from "../providers/keychain.js";
|
|
8
|
+
import { createProviderFactoryAsync, detectProviderForModel, resolveModelRuntime } from "../providers/runtime.js";
|
|
9
9
|
import { createBuiltinTools as createTools } from "../tools/registry.js";
|
|
10
10
|
import { appendCostLog } from "../usage/cost-log.js";
|
|
11
11
|
import { projectCostUSD } from "../usage/estimator.js";
|
|
@@ -13,6 +13,33 @@ import { withDeadlineRace, withTimeoutSignal } from "../utils/llm-deadline.js";
|
|
|
13
13
|
import { loadMcpServers } from "../utils/settings.js";
|
|
14
14
|
import { withVisibleRetry } from "../utils/visible-retry.js";
|
|
15
15
|
import { buildResearchSystemPrompt } from "./prompts.js";
|
|
16
|
+
/**
|
|
17
|
+
* Resolve a provider factory for a council sub-call, OAuth-aware.
|
|
18
|
+
*
|
|
19
|
+
* The council reachability gate (`isProviderReachable` in leader.ts) counts
|
|
20
|
+
* OAuth-authenticated providers as usable (via `getConfiguredProviders`), so a
|
|
21
|
+
* multi-provider roster can route a stance to an OAuth-only provider such as
|
|
22
|
+
* xai/grok. `loadKeyForProvider` is API-key-only and throws
|
|
23
|
+
* `ProviderKeyMissingError` for those — which previously surfaced as
|
|
24
|
+
* "[Error: No API key found for provider 'xai'.]" on every call of that stance,
|
|
25
|
+
* even though the provider was fully authenticated. Mirror the main
|
|
26
|
+
* orchestrator path: fall back to `createProviderFactoryAsync`, which loads +
|
|
27
|
+
* refreshes the stored OAuth bearer token. Only the expected missing-key case
|
|
28
|
+
* is swallowed; unexpected errors propagate.
|
|
29
|
+
*/
|
|
30
|
+
async function resolveCouncilFactory(providerId) {
|
|
31
|
+
let apiKey;
|
|
32
|
+
try {
|
|
33
|
+
apiKey = await loadKeyForProvider(providerId);
|
|
34
|
+
}
|
|
35
|
+
catch (err) {
|
|
36
|
+
if (!(err instanceof ProviderKeyMissingError))
|
|
37
|
+
throw err;
|
|
38
|
+
// OAuth-only provider — createProviderFactoryAsync injects the bearer token.
|
|
39
|
+
}
|
|
40
|
+
const { factory } = await createProviderFactoryAsync(providerId, apiKey ? { apiKey } : {});
|
|
41
|
+
return factory;
|
|
42
|
+
}
|
|
16
43
|
function getDebugLogPath() {
|
|
17
44
|
const p = process.env.MUONROI_COUNCIL_DEBUG_LOG;
|
|
18
45
|
return p && p.length > 0 ? p : null;
|
|
@@ -184,7 +211,7 @@ const COUNCIL_LLM_TIMEOUT_MS = (() => {
|
|
|
184
211
|
// implementation. Imported at the top of this file.
|
|
185
212
|
export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
186
213
|
return {
|
|
187
|
-
async generate(modelId, system, prompt, maxTokens = 4096, onUsage) {
|
|
214
|
+
async generate(modelId, system, prompt, maxTokens = 4096, onUsage, signal) {
|
|
188
215
|
const mock = getMockLlm();
|
|
189
216
|
if (mock) {
|
|
190
217
|
stats.calls++;
|
|
@@ -192,11 +219,14 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
|
192
219
|
return result.text;
|
|
193
220
|
}
|
|
194
221
|
const providerId = detectProviderForModel(modelId);
|
|
195
|
-
const
|
|
196
|
-
const { factory } = createProviderFactory(providerId, { apiKey: key });
|
|
222
|
+
const factory = await resolveCouncilFactory(providerId);
|
|
197
223
|
const runtime = resolveModelRuntime(factory, modelId);
|
|
198
224
|
const t0 = Date.now();
|
|
199
|
-
|
|
225
|
+
// Combine the user-abort signal (when threaded from runCouncil) with the
|
|
226
|
+
// per-call wall-clock deadline. Without the parent signal, an Esc/Ctrl-C
|
|
227
|
+
// during the longest generate calls (8192-token synthesis, clarify, leader
|
|
228
|
+
// eval) was a no-op — the call ran to completion or hit the 5-min timeout.
|
|
229
|
+
const { signal: timedSignal, cleanup: cleanupTimeout } = withTimeoutSignal(signal, COUNCIL_LLM_TIMEOUT_MS);
|
|
200
230
|
try {
|
|
201
231
|
const result = await withDeadlineRace(() => withVisibleRetry(() => generateText({
|
|
202
232
|
model: runtime.model,
|
|
@@ -211,7 +241,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
|
211
241
|
maxRetries: 0,
|
|
212
242
|
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
213
243
|
abortSignal: timedSignal,
|
|
214
|
-
}), { label: "council.generate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.generate");
|
|
244
|
+
}), { label: "council.generate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.generate", signal);
|
|
215
245
|
cleanupTimeout();
|
|
216
246
|
stats.calls++;
|
|
217
247
|
const durMs = Date.now() - t0;
|
|
@@ -273,8 +303,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
|
273
303
|
return { text: result.text, toolCalls: [] };
|
|
274
304
|
}
|
|
275
305
|
const providerId = detectProviderForModel(modelId);
|
|
276
|
-
const
|
|
277
|
-
const { factory } = createProviderFactory(providerId, { apiKey: key });
|
|
306
|
+
const factory = await resolveCouncilFactory(providerId);
|
|
278
307
|
const runtime = resolveModelRuntime(factory, modelId);
|
|
279
308
|
// Verification tools — re-introduced after the no-tools fix (session
|
|
280
309
|
// a7a5690d2049). The original failure was stepCountIs(4) + full toolset
|
|
@@ -352,7 +381,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
|
352
381
|
maxRetries: 0,
|
|
353
382
|
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
354
383
|
abortSignal: timedSignal,
|
|
355
|
-
}), { label: "council.debate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.debate");
|
|
384
|
+
}), { label: "council.debate" }), COUNCIL_LLM_TIMEOUT_MS + 5_000, "council.debate", signal);
|
|
356
385
|
cleanupTimeout();
|
|
357
386
|
stats.calls++;
|
|
358
387
|
// No tool calls expected, but the AI SDK shape still has the field —
|
|
@@ -430,8 +459,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
|
430
459
|
return result.text;
|
|
431
460
|
}
|
|
432
461
|
const providerId = detectProviderForModel(modelId);
|
|
433
|
-
const
|
|
434
|
-
const { factory } = createProviderFactory(providerId, { apiKey: key });
|
|
462
|
+
const factory = await resolveCouncilFactory(providerId);
|
|
435
463
|
const runtime = resolveModelRuntime(factory, modelId);
|
|
436
464
|
const builtinTools = createTools(bash, mode);
|
|
437
465
|
// CQ-03: Lazy MCP bundle per research call — fail-open so builtins remain available
|
|
@@ -488,7 +516,7 @@ export function createCouncilLLM(bash, mode, sessionId, stats) {
|
|
|
488
516
|
maxRetries: 0,
|
|
489
517
|
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
490
518
|
abortSignal: timedSignal,
|
|
491
|
-
}), { label: "council.research" }), researchTimeoutMs + 5_000, "council.research");
|
|
519
|
+
}), { label: "council.research" }), researchTimeoutMs + 5_000, "council.research", signal);
|
|
492
520
|
cleanupTimeout();
|
|
493
521
|
const researchUsage = logCouncilCost({
|
|
494
522
|
callsite: "council.research",
|