muonroi-cli 1.8.5 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/packages/agent-harness-core/src/event-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +20 -4
- package/dist/packages/agent-harness-core/src/event-tee.js +32 -5
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +14 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +86 -10
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +24 -2
- package/dist/src/agent-harness/mock-model.d.ts +10 -0
- package/dist/src/agent-harness/mock-model.js +6 -2
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- package/dist/src/cli/keys.d.ts +8 -45
- package/dist/src/cli/keys.js +18 -324
- package/dist/src/council/clarifier.d.ts +4 -2
- package/dist/src/council/clarifier.js +158 -36
- package/dist/src/council/debate-planner.js +3 -2
- package/dist/src/council/debate.js +59 -5
- package/dist/src/council/index.d.ts +23 -0
- package/dist/src/council/index.js +37 -2
- package/dist/src/council/llm.d.ts +62 -0
- package/dist/src/council/llm.js +123 -23
- package/dist/src/council/panel-select.js +13 -3
- package/dist/src/council/planner.js +17 -0
- package/dist/src/council/preflight.d.ts +10 -0
- package/dist/src/council/preflight.js +36 -0
- package/dist/src/council/prompts.d.ts +9 -1
- package/dist/src/council/prompts.js +23 -6
- package/dist/src/council/types.d.ts +10 -0
- package/dist/src/ee/auth.d.ts +19 -0
- package/dist/src/ee/auth.js +39 -0
- package/dist/src/ee/client.js +28 -3
- package/dist/src/ee/ee-onboarding.js +6 -26
- package/dist/src/flow/compaction/compress.d.ts +2 -2
- package/dist/src/flow/compaction/compress.js +21 -8
- package/dist/src/flow/compaction/extract.d.ts +3 -3
- package/dist/src/flow/compaction/extract.js +6 -6
- package/dist/src/flow/compaction/index.d.ts +2 -1
- package/dist/src/flow/compaction/index.js +29 -3
- package/dist/src/flow/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/flags.d.ts +11 -0
- package/dist/src/gsd/flags.js +19 -0
- package/dist/src/gsd/plan-council.js +104 -72
- package/dist/src/gsd/verdict-schema.d.ts +1 -1
- package/dist/src/headless/council-answers.js +4 -0
- package/dist/src/index.js +129 -260
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- package/dist/src/mcp/client-pool.js +43 -15
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- package/dist/src/models/catalog.json +19 -19
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- package/dist/src/orchestrator/compaction.d.ts +2 -3
- package/dist/src/orchestrator/compaction.js +8 -8
- package/dist/src/orchestrator/council-manager.js +9 -8
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- package/dist/src/orchestrator/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +26 -8
- package/dist/src/orchestrator/orchestrator.d.ts +25 -0
- package/dist/src/orchestrator/orchestrator.js +204 -50
- package/dist/src/orchestrator/preprocessor.js +2 -2
- package/dist/src/orchestrator/safety-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +5 -0
- package/dist/src/orchestrator/safety-intercept.js +7 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +8 -1
- package/dist/src/orchestrator/stall-watchdog.js +24 -3
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +54 -21
- package/dist/src/orchestrator/tool-engine.d.ts +19 -0
- package/dist/src/orchestrator/tool-engine.js +241 -25
- package/dist/src/orchestrator/turn-watchdog.d.ts +7 -0
- package/dist/src/orchestrator/turn-watchdog.js +38 -9
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- package/dist/src/pil/discovery.d.ts +1 -1
- package/dist/src/pil/discovery.js +2 -2
- package/dist/src/pil/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- package/dist/src/pil/llm-classify.d.ts +78 -3
- package/dist/src/pil/llm-classify.js +351 -111
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +7 -0
- package/dist/src/pil/pipeline.js +2 -0
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- package/dist/src/product-loop/artifact-io.js +4 -0
- package/dist/src/product-loop/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- package/dist/src/product-loop/discovery-interview.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +37 -18
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +14 -1
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- package/dist/src/product-loop/index.js +7 -0
- package/dist/src/product-loop/loop-driver.js +21 -8
- package/dist/src/product-loop/phase-plan.d.ts +16 -0
- package/dist/src/product-loop/phase-plan.js +42 -4
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +30 -11
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +62 -0
- package/dist/src/product-loop/sprint-runner.js +309 -8
- package/dist/src/product-loop/types.d.ts +25 -0
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +14 -0
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -70
- package/dist/src/providers/auth/types.d.ts +8 -0
- package/dist/src/providers/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +21 -17
- package/dist/src/providers/keychain.js +124 -135
- package/dist/src/providers/runtime.d.ts +24 -9
- package/dist/src/providers/runtime.js +48 -37
- package/dist/src/providers/strategies/thinking-mode.js +9 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- package/dist/src/providers/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/self-qa/agentic-loop.js +3 -2
- package/dist/src/storage/transcript.js +56 -2
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +76 -1
- package/dist/src/tools/registry.d.ts +17 -0
- package/dist/src/tools/registry.js +116 -1
- package/dist/src/types/index.d.ts +29 -1
- package/dist/src/ui/app.js +67 -4
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- package/dist/src/ui/components/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -0
- package/dist/src/ui/components/council-phase-timeline.js +17 -2
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- package/dist/src/ui/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- package/dist/src/ui/slash/ee.js +81 -0
- package/dist/src/ui/slash/menu-items.js +11 -2
- package/dist/src/ui/use-app-logic.js +354 -224
- package/dist/src/ui/utils/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/settings.d.ts +27 -0
- package/dist/src/utils/settings.js +38 -2
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/verify/entrypoint.js +51 -16
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/package.json +1 -2
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- package/dist/src/mcp/ee-tools.js +0 -194
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { getMcpKey } from "../mcp/mcp-keychain.js";
|
|
2
|
+
import { getWebResearchModel } from "../models/registry.js";
|
|
3
|
+
import { getCouncilLanguage } from "../utils/settings.js";
|
|
1
4
|
import { pickCouncilTaskModel } from "./leader.js";
|
|
2
|
-
import { tracedGenerate, tracedGenerateWithFallback } from "./llm.js";
|
|
5
|
+
import { tracedAsync, tracedGenerate, tracedGenerateWithFallback } from "./llm.js";
|
|
3
6
|
import { phaseDone, phaseError, phaseStart } from "./phase-events.js";
|
|
4
7
|
import { buildClarificationPrompt, buildReadinessJudgePrompt, buildSpecSynthesisPrompt } from "./prompts.js";
|
|
5
8
|
/** P5: Hard cap on clarification rounds regardless of judge verdict. */
|
|
@@ -97,6 +100,51 @@ function grayAreaToRoundQuestion(g) {
|
|
|
97
100
|
isRequired: true,
|
|
98
101
|
};
|
|
99
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the concrete interview language from the council language setting +
|
|
105
|
+
* the topic. Pinned locale → that locale; "english" → English; "auto" (default)
|
|
106
|
+
* → detect from the topic (Vietnamese diacritics, mirroring
|
|
107
|
+
* `detectReplyLanguageHint`). Returned as a concrete name so the clarify prompt
|
|
108
|
+
* FORCES it (immune to an English "## Scope Research" block) and the escape-hatch
|
|
109
|
+
* labels can be localized.
|
|
110
|
+
*/
|
|
111
|
+
const VI_DIACRITIC_RE = /[à-ỹÀ-Ỹ]/;
|
|
112
|
+
export function resolveInterviewLanguage(topic) {
|
|
113
|
+
const setting = getCouncilLanguage();
|
|
114
|
+
if (setting && setting !== "auto" && setting !== "english")
|
|
115
|
+
return setting;
|
|
116
|
+
if (setting === "english")
|
|
117
|
+
return "english";
|
|
118
|
+
return VI_DIACRITIC_RE.test(topic ?? "") ? "vietnamese" : "english";
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* The two escape-hatch options appended to every clarification card, localized to
|
|
122
|
+
* the interview language so a Vietnamese interview doesn't show English buttons.
|
|
123
|
+
* `lang` is a concrete language name; anything not recognized as Vietnamese falls
|
|
124
|
+
* back to English labels (the LLM-authored question itself is still localized).
|
|
125
|
+
* When `lang` is undefined the historical English-label pair is kept byte-
|
|
126
|
+
* identical so existing callers/tests are unaffected.
|
|
127
|
+
*/
|
|
128
|
+
function escapeHatchOptions(lang) {
|
|
129
|
+
const isVi = typeof lang === "string" && /vietnamese|tiếng việt/i.test(lang.trim());
|
|
130
|
+
if (isVi) {
|
|
131
|
+
return [
|
|
132
|
+
{ label: "Nhập câu trả lời", description: "Nhập câu trả lời tự do", value: "", kind: "freetext" },
|
|
133
|
+
{ label: "Thảo luận thêm", description: "Thảo luận thêm trước khi trả lời", value: "", kind: "chat" },
|
|
134
|
+
];
|
|
135
|
+
}
|
|
136
|
+
if (lang === undefined) {
|
|
137
|
+
// Historical default preserved byte-identical (EN label + VN description).
|
|
138
|
+
return [
|
|
139
|
+
{ label: "Type something", description: "Nhập câu trả lời tự do", value: "", kind: "freetext" },
|
|
140
|
+
{ label: "Chat about this", description: "Thảo luận thêm trước khi trả lời", value: "", kind: "chat" },
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
return [
|
|
144
|
+
{ label: "Type something", description: "Type a free-form answer", value: "", kind: "freetext" },
|
|
145
|
+
{ label: "Chat about this", description: "Discuss before answering", value: "", kind: "chat" },
|
|
146
|
+
];
|
|
147
|
+
}
|
|
100
148
|
/**
|
|
101
149
|
* Convert legacy `suggestions: string[]` into the new options schema with
|
|
102
150
|
* standard "Type something" / "Chat about this" escape-hatches appended.
|
|
@@ -110,25 +158,11 @@ function grayAreaToRoundQuestion(g) {
|
|
|
110
158
|
* returned `defaultIndex` points to it. Otherwise `defaultIndex` is omitted
|
|
111
159
|
* so the UI knows to suppress the "(Recommended)" tag.
|
|
112
160
|
*/
|
|
113
|
-
export function buildClarifyOptions(suggestions, recommended) {
|
|
161
|
+
export function buildClarifyOptions(suggestions, recommended, lang) {
|
|
114
162
|
const choices = (suggestions ?? [])
|
|
115
163
|
.filter((s) => typeof s === "string" && s.trim().length > 0)
|
|
116
164
|
.map((s) => ({ label: s.trim(), value: s.trim(), kind: "choice" }));
|
|
117
|
-
const options = [
|
|
118
|
-
...choices,
|
|
119
|
-
{
|
|
120
|
-
label: "Type something",
|
|
121
|
-
description: "Nhập câu trả lời tự do",
|
|
122
|
-
value: "",
|
|
123
|
-
kind: "freetext",
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
label: "Chat about this",
|
|
127
|
-
description: "Thảo luận thêm trước khi trả lời",
|
|
128
|
-
value: "",
|
|
129
|
-
kind: "chat",
|
|
130
|
-
},
|
|
131
|
-
];
|
|
165
|
+
const options = [...choices, ...escapeHatchOptions(lang)];
|
|
132
166
|
let defaultIndex;
|
|
133
167
|
if (typeof recommended === "string" && recommended.trim().length > 0) {
|
|
134
168
|
const target = recommended.trim().toLowerCase();
|
|
@@ -146,7 +180,7 @@ export function buildClarifyOptions(suggestions, recommended) {
|
|
|
146
180
|
* "Chat about this" escape-hatches are appended as in `buildClarifyOptions`.
|
|
147
181
|
* Pure + exported for unit testing.
|
|
148
182
|
*/
|
|
149
|
-
export function buildClarifyOptionsRich(specs) {
|
|
183
|
+
export function buildClarifyOptionsRich(specs, lang) {
|
|
150
184
|
const cleaned = (specs ?? []).filter((s) => !!s && typeof s.label === "string" && s.label.trim().length > 0);
|
|
151
185
|
const choices = cleaned.map((s) => ({
|
|
152
186
|
label: s.label.trim(),
|
|
@@ -156,27 +190,105 @@ export function buildClarifyOptionsRich(specs) {
|
|
|
156
190
|
? { description: s.description.trim() }
|
|
157
191
|
: {}),
|
|
158
192
|
}));
|
|
159
|
-
const options = [
|
|
160
|
-
...choices,
|
|
161
|
-
{
|
|
162
|
-
label: "Type something",
|
|
163
|
-
description: "Nhập câu trả lời tự do",
|
|
164
|
-
value: "",
|
|
165
|
-
kind: "freetext",
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
label: "Chat about this",
|
|
169
|
-
description: "Thảo luận thêm trước khi trả lời",
|
|
170
|
-
value: "",
|
|
171
|
-
kind: "chat",
|
|
172
|
-
},
|
|
173
|
-
];
|
|
193
|
+
const options = [...choices, ...escapeHatchOptions(lang)];
|
|
174
194
|
let defaultIndex;
|
|
175
195
|
const recIdx = cleaned.findIndex((s) => s.recommended === true);
|
|
176
196
|
if (recIdx !== -1)
|
|
177
197
|
defaultIndex = recIdx;
|
|
178
198
|
return { options, defaultIndex };
|
|
179
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Pre-clarify scope research (research-first grounding).
|
|
202
|
+
*
|
|
203
|
+
* Before asking ANY clarification question, research the topic — codebase for an
|
|
204
|
+
* existing repo (the research method reads the tree) plus web — so the
|
|
205
|
+
* question-generator surfaces the REAL gray areas the evidence exposes instead
|
|
206
|
+
* of guessing blind ("hỏi tào lao"). The returned brief is appended to the
|
|
207
|
+
* `conversationContext` that `buildClarificationPrompt` already reads, so both
|
|
208
|
+
* `/council` and `/ideal` (which reuses this clarifier as its gather engine)
|
|
209
|
+
* inherit evidence-grounded questions from one shared code path.
|
|
210
|
+
*
|
|
211
|
+
* Web-capability policy (owner's Part E rule): PREFER a model with native web
|
|
212
|
+
* research integrated (catalog `nativeWebResearch`), switching the research call
|
|
213
|
+
* to it when one is reachable. Only when no native-web model is reachable fall
|
|
214
|
+
* back to Tavily (via the research method's builtin web tools). When NEITHER a
|
|
215
|
+
* native-web model NOR a Tavily key exists, warn the user before continuing —
|
|
216
|
+
* research proceeds on the codebase + model knowledge only, never silently blind.
|
|
217
|
+
* (Codebase evidence is always gathered by the research tool regardless of the
|
|
218
|
+
* web tier, so an existing repo is still grounded.)
|
|
219
|
+
*
|
|
220
|
+
* Never throws and never blocks the interview: if the flag is off, the llm has
|
|
221
|
+
* no `research` method (most unit-test mocks), the run is aborted, or the call
|
|
222
|
+
* fails, it yields nothing and returns "". Default ON; opt out with
|
|
223
|
+
* MUONROI_CLARIFY_RESEARCH_FIRST=0.
|
|
224
|
+
*/
|
|
225
|
+
async function hasTavilyKey() {
|
|
226
|
+
try {
|
|
227
|
+
const k = ((await getMcpKey("tavily")) || process.env.TAVILY_API_KEY || "").trim();
|
|
228
|
+
return k.length >= 10;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return (process.env.TAVILY_API_KEY ?? "").trim().length >= 10;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
export async function* researchScopeForClarification(topic, conversationContext, leaderModelId, llm, signal, reachableModels) {
|
|
235
|
+
if (process.env.MUONROI_CLARIFY_RESEARCH_FIRST === "0")
|
|
236
|
+
return "";
|
|
237
|
+
if (signal?.aborted)
|
|
238
|
+
return "";
|
|
239
|
+
// Skip cleanly when the injected llm can't research (generate-only test mocks)
|
|
240
|
+
// — keeps every existing clarifier test on its current, research-free path.
|
|
241
|
+
if (typeof llm.research !== "function")
|
|
242
|
+
return "";
|
|
243
|
+
// Choose the research model + web tier. Prefer a reachable native-web model.
|
|
244
|
+
const reachable = new Set([leaderModelId, ...reachableModels].filter(Boolean));
|
|
245
|
+
const nativeModel = getWebResearchModel(reachable);
|
|
246
|
+
let researchModel = leaderModelId;
|
|
247
|
+
let webTier;
|
|
248
|
+
if (nativeModel) {
|
|
249
|
+
researchModel = nativeModel.id;
|
|
250
|
+
webTier = "native";
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
webTier = (await hasTavilyKey()) ? "tavily" : "none";
|
|
254
|
+
}
|
|
255
|
+
const phaseId = "phase:clarification-scope-research";
|
|
256
|
+
const startedAt = Date.now();
|
|
257
|
+
yield phaseStart({ phaseId, kind: "clarification", label: "Scope research" });
|
|
258
|
+
// Owner's Part E principle: never research the web fully blind without telling
|
|
259
|
+
// the user. Codebase + model-knowledge research still runs, but web findings
|
|
260
|
+
// will be absent — surface that so they can wire a native-web model or Tavily.
|
|
261
|
+
if (webTier === "none") {
|
|
262
|
+
yield {
|
|
263
|
+
type: "content",
|
|
264
|
+
content: `\n> ⚠ Scope research: no web-research-native model is reachable and no Tavily key is configured. ` +
|
|
265
|
+
`Researching from the codebase + model knowledge only — no live web findings. ` +
|
|
266
|
+
`Configure a native-web model or a Tavily API key for grounded web research.\n`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
try {
|
|
270
|
+
const goal = `${topic}\n\n(Research goal: narrow the scope of this request. Report verified findings, and — ` +
|
|
271
|
+
`critically — END with an explicit "## Open Questions / Assumptions" section listing what you could ` +
|
|
272
|
+
`NOT verify and had to ASSUME (the real goal/pain, the concrete task, the system of record, the ` +
|
|
273
|
+
`target user, hard constraints). Those unresolved items are exactly what the user must clarify next, ` +
|
|
274
|
+
`so name them plainly rather than papering over them with a plausible guess.)`;
|
|
275
|
+
const brief = yield* tracedAsync(() => llm.research(researchModel, goal, conversationContext, signal, undefined, {
|
|
276
|
+
internetFirst: webTier !== "none",
|
|
277
|
+
}), { phase: "clarify", label: "Scope research" });
|
|
278
|
+
yield phaseDone({ phaseId, kind: "clarification", label: "Scope research", startedAt });
|
|
279
|
+
return typeof brief === "string" ? brief.trim() : "";
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
yield phaseError({
|
|
283
|
+
phaseId,
|
|
284
|
+
kind: "clarification",
|
|
285
|
+
label: "Scope research",
|
|
286
|
+
startedAt,
|
|
287
|
+
errorMessage: err instanceof Error ? err.message : String(err),
|
|
288
|
+
});
|
|
289
|
+
return "";
|
|
290
|
+
}
|
|
291
|
+
}
|
|
180
292
|
export async function* runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, signal, seedQuestions, maxRounds,
|
|
181
293
|
/**
|
|
182
294
|
* Optional pre-filled answers keyed by seed-question id. When the loop's
|
|
@@ -201,12 +313,22 @@ fallbackModels = []) {
|
|
|
201
313
|
// P5: use MAX_CLARIFY_ROUNDS (12) as the hard cap; respect explicit override
|
|
202
314
|
// from callers that pass maxRounds (e.g. tests that want old 3-round behavior).
|
|
203
315
|
const max = typeof maxRounds === "number" && maxRounds > 0 ? maxRounds : MAX_CLARIFY_ROUNDS;
|
|
316
|
+
// Interview language (the user's conversation language, not English-by-default).
|
|
317
|
+
// Resolved once and forced into the clarify prompt + escape-hatch labels.
|
|
318
|
+
const uiLang = resolveInterviewLanguage(topic);
|
|
204
319
|
const allQA = [];
|
|
205
320
|
// P5: full Q&A history with timestamps for clarifyHistory field
|
|
206
321
|
const clarifyHistory = [];
|
|
207
322
|
const phaseStartedAt = Date.now();
|
|
208
323
|
yield phaseStart({ phaseId: "phase:clarification", kind: "clarification", label: "Clarification" });
|
|
209
324
|
const seeded = (seedQuestions ?? []).map(grayAreaToRoundQuestion);
|
|
325
|
+
// Research-first: ground the interview in evidence BEFORE asking anything, so
|
|
326
|
+
// the question-generator targets the real gray areas the research surfaced
|
|
327
|
+
// rather than guessing. Appended to the context every round's prompt reads.
|
|
328
|
+
const scopeBrief = yield* researchScopeForClarification(topic, conversationContext, leaderModelId, llm, signal, fallbackModels);
|
|
329
|
+
if (scopeBrief) {
|
|
330
|
+
conversationContext = `${conversationContext}${conversationContext ? "\n\n" : ""}## Scope Research\n${scopeBrief}`;
|
|
331
|
+
}
|
|
210
332
|
// P5: track ready-gate state across rounds
|
|
211
333
|
let gateReady = false;
|
|
212
334
|
let gateConfidence = 0;
|
|
@@ -240,7 +362,7 @@ fallbackModels = []) {
|
|
|
240
362
|
const gapHint = pendingGaps.length > 0
|
|
241
363
|
? `\n\n## Known Gaps (target these with follow-up questions)\n${pendingGaps.map((g) => `- ${g}`).join("\n")}`
|
|
242
364
|
: "";
|
|
243
|
-
const { system, prompt } = buildClarificationPrompt(topic, conversationContext, allQA.length > 0 ? allQA : undefined);
|
|
365
|
+
const { system, prompt } = buildClarificationPrompt(topic, conversationContext, allQA.length > 0 ? allQA : undefined, uiLang);
|
|
244
366
|
let questionsRaw;
|
|
245
367
|
try {
|
|
246
368
|
const clarifyModel = pickCouncilTaskModel("clarify_questions", leaderModelId, costAware);
|
|
@@ -334,8 +456,8 @@ fallbackModels = []) {
|
|
|
334
456
|
// pre-selected. Fall back to the legacy string `suggestions` shape (PIL
|
|
335
457
|
// gray-area seeds and any model still emitting the old format).
|
|
336
458
|
const { options, defaultIndex } = q.options && q.options.length > 0
|
|
337
|
-
? buildClarifyOptionsRich(q.options)
|
|
338
|
-
: buildClarifyOptions(q.suggestions, q.recommended);
|
|
459
|
+
? buildClarifyOptionsRich(q.options, uiLang)
|
|
460
|
+
: buildClarifyOptions(q.suggestions, q.recommended, uiLang);
|
|
339
461
|
// Keep the deprecated `suggestions` mirror populated for consumers that
|
|
340
462
|
// still read it (audit/replay), deriving labels from rich options.
|
|
341
463
|
const suggestionLabels = q.suggestions ?? (q.options ? q.options.map((o) => o.label).filter((l) => typeof l === "string") : undefined);
|
|
@@ -177,8 +177,9 @@ signal) {
|
|
|
177
177
|
try {
|
|
178
178
|
const providerId = detectProviderForModel(leaderModelId);
|
|
179
179
|
const key = await loadKeyForProvider(providerId);
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
// Registers the leader provider's factory so resolveModelRuntime can derive it.
|
|
181
|
+
createProviderFactory(providerId, { apiKey: key });
|
|
182
|
+
const runtime = resolveModelRuntime(leaderModelId);
|
|
182
183
|
// Bound attempt-1: a wedged provider response here would freeze the whole
|
|
183
184
|
// council/loop silently (no streamText stall watchdog covers generateObject).
|
|
184
185
|
// On timeout this rejects → caught below → retry (guarded) → fallback plan.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { getModelInfo } from "../models/registry.js";
|
|
3
3
|
import { detectProviderForModel } from "../providers/runtime.js";
|
|
4
|
+
import { getIsolatedTaskDeadlineMs, withDeadlineRace } from "../utils/llm-deadline.js";
|
|
4
5
|
import { getCouncilLanguage } from "../utils/settings.js";
|
|
5
6
|
import { buildDebateCheckpoint, checkpointMatches, deleteDebateCheckpoint, restoreExchangeLogs, writeDebateCheckpoint, } from "./debate-checkpoint.js";
|
|
6
7
|
import { resolveDebateSummary } from "./debate-summary.js";
|
|
7
8
|
import { pickCouncilTaskModel } from "./leader.js";
|
|
8
|
-
import { tracedAsync, tracedGenerate } from "./llm.js";
|
|
9
|
+
import { councilStreamLivenessReader, tracedAsync, tracedGenerate } from "./llm.js";
|
|
9
10
|
import { phaseDone, phaseStart } from "./phase-events.js";
|
|
10
11
|
import { buildFollowupPrompt, buildLeaderEvaluationPrompt, buildOpeningPrompt, buildResponsePrompt, buildRoundSummaryPrompt, } from "./prompts.js";
|
|
11
12
|
/**
|
|
@@ -301,13 +302,13 @@ async function runResearchIsolated(runIsolatedTask, model, topic, conversationCo
|
|
|
301
302
|
`Return a compact "## Research Findings" section: only the concrete facts that bear on the question, each with its source (file:line or URL). ` +
|
|
302
303
|
`Be terse and factual — no opinions, no recommendations. If you cannot find solid evidence, say so in one line.`;
|
|
303
304
|
try {
|
|
304
|
-
const result = await runIsolatedTask({
|
|
305
|
+
const result = await withDeadlineRace(() => runIsolatedTask({
|
|
305
306
|
agent: "explore",
|
|
306
307
|
description: `Council research: ${topic.slice(0, 60)}`,
|
|
307
308
|
prompt,
|
|
308
309
|
// Pin the research-role model; bypasses the parent-tier cap in StreamRunner.
|
|
309
310
|
modelId: model,
|
|
310
|
-
});
|
|
311
|
+
}), getIsolatedTaskDeadlineMs(), "council-research");
|
|
311
312
|
if (result.success && result.output?.trim())
|
|
312
313
|
return result.output.trim();
|
|
313
314
|
traceCb(`[research] isolated sub-agent produced nothing: ${result.error ?? "no output"}`);
|
|
@@ -352,7 +353,22 @@ export async function researchWithFallback(llm, primaryModel, topic, conversatio
|
|
|
352
353
|
return primary;
|
|
353
354
|
}
|
|
354
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Times every `debateWithRetryInner` exit path — success, retry, fallback and
|
|
358
|
+
* total failure all cost wall-clock, and a slow FAILING speaker is exactly the
|
|
359
|
+
* case worth seeing. Wrapping is why `durationMs` cannot drift out of sync with
|
|
360
|
+
* the four separate returns inside.
|
|
361
|
+
*
|
|
362
|
+
* The number is per-SPEAKER. The only timed council rows today are
|
|
363
|
+
* `debate_complete` (whole debate: 87s..1128s measured) and `council_summary`
|
|
364
|
+
* — aggregates that cannot name the model responsible.
|
|
365
|
+
*/
|
|
355
366
|
async function debateWithRetry(llm, model, system, prompt, signal, traceCb, toolBudget, fallbackPool = []) {
|
|
367
|
+
const startedAt = Date.now();
|
|
368
|
+
const r = await debateWithRetryInner(llm, model, system, prompt, signal, traceCb, toolBudget, fallbackPool);
|
|
369
|
+
return { ...r, durationMs: Date.now() - startedAt };
|
|
370
|
+
}
|
|
371
|
+
async function debateWithRetryInner(llm, model, system, prompt, signal, traceCb, toolBudget, fallbackPool = []) {
|
|
356
372
|
// Respect the circuit breaker — once a model has tripped, it stays
|
|
357
373
|
// tool-disabled for the rest of this council run.
|
|
358
374
|
const allowTools = debateAllowsTools(model) && !toolBudget.disabled.has(model);
|
|
@@ -514,11 +530,16 @@ export async function* runDebate(spec, config, llm) {
|
|
|
514
530
|
detail: `via ${researchCandidate.model}`,
|
|
515
531
|
});
|
|
516
532
|
const researchTraces = [];
|
|
533
|
+
const researchStartedAt = Date.now();
|
|
517
534
|
researchFindings = yield* tracedAsync(() => researchWithFallback(llm, researchCandidate.model, spec.problemStatement, conversationContext, signal, (t) => researchTraces.push(t), { internetFirst }, fallbackPool, config.runIsolatedTask), {
|
|
518
535
|
phase: "research",
|
|
519
536
|
label: internetFirst ? "Researching (internet-first)" : "Researching codebase",
|
|
520
537
|
detail: spec.problemStatement.slice(0, 80),
|
|
521
538
|
role: "research",
|
|
539
|
+
// Research is a single long generateText (up to ~10 min / 15 tool steps)
|
|
540
|
+
// with a quiet main stream — the phase most easily misread as hung, and
|
|
541
|
+
// the one whose cost the per-round numbers could not account for.
|
|
542
|
+
liveness: councilStreamLivenessReader(),
|
|
522
543
|
});
|
|
523
544
|
// CQ-22: emit research tool traces as council_status
|
|
524
545
|
for (const trace of researchTraces) {
|
|
@@ -545,6 +566,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
545
566
|
text: researchFailed
|
|
546
567
|
? "⚠ Research unavailable — both providers failed. The council proceeds on the panel's own knowledge (no external findings injected)."
|
|
547
568
|
: (researchFindings ?? ""),
|
|
569
|
+
// Spans researchWithFallback including any fallback provider attempt.
|
|
570
|
+
// Measured per-speaker debate turns summed to only ~3.7 min against a
|
|
571
|
+
// ~507s historical `debate_complete`, so the balance is here.
|
|
572
|
+
durationMs: Date.now() - researchStartedAt,
|
|
548
573
|
},
|
|
549
574
|
};
|
|
550
575
|
}
|
|
@@ -595,6 +620,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
595
620
|
conversationContext: selfContext,
|
|
596
621
|
language: debateLanguage,
|
|
597
622
|
});
|
|
623
|
+
const startedAt = Date.now();
|
|
598
624
|
return openingWithRetry(llm, self.model, system, prompt).then((r) => ({
|
|
599
625
|
role: self.role,
|
|
600
626
|
model: self.model,
|
|
@@ -602,6 +628,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
602
628
|
position: r.text,
|
|
603
629
|
error: r.text ? null : (r.error ?? "empty completion after retries"),
|
|
604
630
|
attempts: r.attempts,
|
|
631
|
+
durationMs: Date.now() - startedAt,
|
|
605
632
|
}));
|
|
606
633
|
});
|
|
607
634
|
const openings = yield* tracedAsync(() => Promise.all(openingPromises), {
|
|
@@ -611,6 +638,9 @@ export async function* runDebate(spec, config, llm) {
|
|
|
611
638
|
// each speaker is tasked to argue (A: live debate preview) instead of a bare
|
|
612
639
|
// spinner during the atomic generateText window.
|
|
613
640
|
detail: formatSpeakerRoster(participants),
|
|
641
|
+
// Promise.all does not pump this generator, so `elapsedMs` freezes while
|
|
642
|
+
// the openings run. Push-based stream counters keep the heartbeat honest.
|
|
643
|
+
liveness: councilStreamLivenessReader(),
|
|
614
644
|
});
|
|
615
645
|
yield { type: "content", content: "\n── Opening Analysis ──\n" };
|
|
616
646
|
for (const o of openings) {
|
|
@@ -625,6 +655,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
625
655
|
text: `[Error: ${o.error}]`,
|
|
626
656
|
attempts: o.attempts,
|
|
627
657
|
failureReason: o.error,
|
|
658
|
+
durationMs: o.durationMs,
|
|
628
659
|
},
|
|
629
660
|
};
|
|
630
661
|
}
|
|
@@ -645,6 +676,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
645
676
|
round: 0,
|
|
646
677
|
text: o.position,
|
|
647
678
|
attempts: o.attempts,
|
|
679
|
+
durationMs: o.durationMs,
|
|
648
680
|
},
|
|
649
681
|
};
|
|
650
682
|
emitCouncilTurnLength({
|
|
@@ -757,6 +789,14 @@ export async function* runDebate(spec, config, llm) {
|
|
|
757
789
|
// not yet escalated, criteria unmet).
|
|
758
790
|
async function* escalateStop(stuck, pinnedUnmet, openList) {
|
|
759
791
|
escalated = true;
|
|
792
|
+
// convene_council path: no interactive user is answering mid-tool-call, so a
|
|
793
|
+
// blocking escalation card would hang the council. Auto-accept (= conclude
|
|
794
|
+
// with the best synthesis so far) without emitting the card. Mirrors the
|
|
795
|
+
// "empty/failed answer → accept" fallback runEscalationPrompt already uses.
|
|
796
|
+
if (config.convenePath) {
|
|
797
|
+
escalation = { action: "accept" };
|
|
798
|
+
return "stop";
|
|
799
|
+
}
|
|
760
800
|
const dec = yield* runEscalationPrompt({
|
|
761
801
|
respondToQuestion: config.respondToQuestion,
|
|
762
802
|
openCriteria: openList,
|
|
@@ -902,6 +942,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
902
942
|
traces: aTraces,
|
|
903
943
|
failureReason: aResult.failureReason,
|
|
904
944
|
attempts: aResult.attempts,
|
|
945
|
+
durationMs: aResult.durationMs,
|
|
905
946
|
});
|
|
906
947
|
const bPrompt = buildResponsePrompt({
|
|
907
948
|
speakerRole: b.role,
|
|
@@ -925,6 +966,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
925
966
|
traces: bTraces,
|
|
926
967
|
failureReason: bResult.failureReason,
|
|
927
968
|
attempts: bResult.attempts,
|
|
969
|
+
durationMs: bResult.durationMs,
|
|
928
970
|
});
|
|
929
971
|
}
|
|
930
972
|
else {
|
|
@@ -956,6 +998,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
956
998
|
traces: aTraces,
|
|
957
999
|
failureReason: aResult.failureReason,
|
|
958
1000
|
attempts: aResult.attempts,
|
|
1001
|
+
durationMs: aResult.durationMs,
|
|
959
1002
|
});
|
|
960
1003
|
const bPrompt = buildFollowupPrompt({
|
|
961
1004
|
speakerRole: b.role,
|
|
@@ -981,6 +1024,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
981
1024
|
traces: bTraces,
|
|
982
1025
|
failureReason: bResult.failureReason,
|
|
983
1026
|
attempts: bResult.attempts,
|
|
1027
|
+
durationMs: bResult.durationMs,
|
|
984
1028
|
});
|
|
985
1029
|
}
|
|
986
1030
|
// Only update positions when response is non-empty (avoid clearing on LLM error)
|
|
@@ -999,6 +1043,11 @@ export async function* runDebate(spec, config, llm) {
|
|
|
999
1043
|
// Distinct speakers with their lens (A: live debate preview) — dedup by
|
|
1000
1044
|
// formatted line so a speaker appearing in two pairs shows once.
|
|
1001
1045
|
detail: formatSpeakerRoster(pairs.flatMap((p) => [p.a, p.b])),
|
|
1046
|
+
// THE case this exists for: each pair turn can run to the 5-min council
|
|
1047
|
+
// ceiling on a reasoning model, and Promise.all never pumps this
|
|
1048
|
+
// generator — `elapsedMs` freezes and a healthy 15-min round looks hung.
|
|
1049
|
+
// These counters come off the token stream, so they keep advancing.
|
|
1050
|
+
liveness: councilStreamLivenessReader(),
|
|
1002
1051
|
});
|
|
1003
1052
|
yield { type: "content", content: `\n── Round ${round} ──\n` };
|
|
1004
1053
|
// Track failures so the circuit breaker can fire after this round.
|
|
@@ -1063,6 +1112,7 @@ export async function* runDebate(spec, config, llm) {
|
|
|
1063
1112
|
text: chunk.text.trim(),
|
|
1064
1113
|
toolCalls: chunk.toolCalls?.map((t) => ({ name: t.toolName })),
|
|
1065
1114
|
attempts: chunk.attempts,
|
|
1115
|
+
durationMs: chunk.durationMs,
|
|
1066
1116
|
},
|
|
1067
1117
|
};
|
|
1068
1118
|
emitCouncilTurnLength({
|
|
@@ -1236,6 +1286,10 @@ export async function* runDebate(spec, config, llm) {
|
|
|
1236
1286
|
speaker: { role: "Leader", model: leaderModelId },
|
|
1237
1287
|
round,
|
|
1238
1288
|
text: verdictText,
|
|
1289
|
+
// Spans every evaluateDebate attempt including fallback models, so a
|
|
1290
|
+
// leader that ate the round is attributable rather than hidden inside
|
|
1291
|
+
// the `debate_complete` aggregate.
|
|
1292
|
+
durationMs: Date.now() - evalStart,
|
|
1239
1293
|
},
|
|
1240
1294
|
};
|
|
1241
1295
|
// P5 — guaranteed done record for this round. Decision reflects the
|
|
@@ -1912,12 +1966,12 @@ export async function runGroundingVerify(runIsolatedTask, model, problemStatemen
|
|
|
1912
1966
|
`words followed by its tag. Do NOT restate the whole debate or add opinions. Prefer [CONFIRMED]/[REFUTED] ` +
|
|
1913
1967
|
`with a real citation over [UNVERIFIED].`;
|
|
1914
1968
|
try {
|
|
1915
|
-
const result = await runIsolatedTask({
|
|
1969
|
+
const result = await withDeadlineRace(() => runIsolatedTask({
|
|
1916
1970
|
agent: "explore",
|
|
1917
1971
|
description: `Council grounding-verify: ${problemStatement.slice(0, 50)}`,
|
|
1918
1972
|
prompt,
|
|
1919
1973
|
modelId: model,
|
|
1920
|
-
});
|
|
1974
|
+
}), getIsolatedTaskDeadlineMs(), "council-grounding-verify");
|
|
1921
1975
|
if (result.success && result.output?.trim())
|
|
1922
1976
|
return result.output.trim();
|
|
1923
1977
|
traceCb(`[grounding-verify] produced nothing: ${result.error ?? "no output"}`);
|
|
@@ -88,6 +88,19 @@ export interface RunCouncilOptions {
|
|
|
88
88
|
* decisions.lock artifact actually get written for sprint-runner injection.
|
|
89
89
|
*/
|
|
90
90
|
sprintPlanningMode?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* Convene-tool path (the `convene_council` builtin). When true, render
|
|
93
|
+
* clarify(optional)+debate+synthesis exactly as normal, then RETURN the
|
|
94
|
+
* synthesis string WITHOUT any post-debate decision surface: NO
|
|
95
|
+
* `pickPostDebateRecommendation`, NO option set, NO `council_question`
|
|
96
|
+
* post-debate card, NO `onPostDebateAction`, NO `postDebateContinuation`
|
|
97
|
+
* routing. The calling AGENT decides what happens next (continue silently,
|
|
98
|
+
* ask the user via `ask_user`, or hand off to `/ideal`) — the CLI hardcodes
|
|
99
|
+
* none of it (user directive: no CLI-hardcoded post-council branch). The
|
|
100
|
+
* persistence block (decisions.lock, judge, council record) still runs — it
|
|
101
|
+
* is an audit trail, not a decision.
|
|
102
|
+
*/
|
|
103
|
+
convenePath?: boolean;
|
|
91
104
|
/**
|
|
92
105
|
* #2 — isolated sub-agent bridge (orchestrator.runTaskRequest). When wired,
|
|
93
106
|
* the debate's research phase runs in a budget-capped explore sub-agent
|
|
@@ -144,6 +157,16 @@ export declare function pickPostDebateRecommendation(input: {
|
|
|
144
157
|
reason: string;
|
|
145
158
|
};
|
|
146
159
|
export declare function postDebateContinuation(action: string | undefined, synthesis: string, outputKind?: string): string | null;
|
|
160
|
+
/**
|
|
161
|
+
* Neutral post-council continuation. Used by the auto-council path (tool-engine)
|
|
162
|
+
* and the `/council` slash path (runCouncilV2) once they run with
|
|
163
|
+
* `convenePath: true` — the hardcoded post-debate option card is suppressed, so
|
|
164
|
+
* there is no `chosenAction` to branch on. Instead of the CLI deciding the next
|
|
165
|
+
* step, we hand the synthesis back to a normal agent turn with a NON-BINDING
|
|
166
|
+
* nudge and let the agent's own intent drive the follow-up (respond / ask_user /
|
|
167
|
+
* implement). Returns "" for an empty synthesis so the caller skips re-entry.
|
|
168
|
+
*/
|
|
169
|
+
export declare function buildNeutralPostCouncilContinuation(synthesis: string): string;
|
|
147
170
|
export declare function runCouncil(topic: string, sessionModelId: string, messages: Array<{
|
|
148
171
|
role: string;
|
|
149
172
|
content: string | unknown;
|
|
@@ -179,6 +179,28 @@ export function postDebateContinuation(action, synthesis, outputKind) {
|
|
|
179
179
|
}
|
|
180
180
|
return null;
|
|
181
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Neutral post-council continuation. Used by the auto-council path (tool-engine)
|
|
184
|
+
* and the `/council` slash path (runCouncilV2) once they run with
|
|
185
|
+
* `convenePath: true` — the hardcoded post-debate option card is suppressed, so
|
|
186
|
+
* there is no `chosenAction` to branch on. Instead of the CLI deciding the next
|
|
187
|
+
* step, we hand the synthesis back to a normal agent turn with a NON-BINDING
|
|
188
|
+
* nudge and let the agent's own intent drive the follow-up (respond / ask_user /
|
|
189
|
+
* implement). Returns "" for an empty synthesis so the caller skips re-entry.
|
|
190
|
+
*/
|
|
191
|
+
export function buildNeutralPostCouncilContinuation(synthesis) {
|
|
192
|
+
if (!synthesis || !synthesis.trim())
|
|
193
|
+
return "";
|
|
194
|
+
return (`Council debate completed. Conclusion:\n\n${synthesis}\n\n` +
|
|
195
|
+
`You now decide the next step based on the user's original request — do not ` +
|
|
196
|
+
`stop without doing one of these:\n` +
|
|
197
|
+
` • If the conclusion IS the deliverable (analysis/evaluation/decision), ` +
|
|
198
|
+
`respond to the user with it.\n` +
|
|
199
|
+
` • If a choice genuinely needs the human before proceeding, call ask_user.\n` +
|
|
200
|
+
` • If the task calls for building and the conclusion is a sufficient spec, ` +
|
|
201
|
+
`implement it now through your normal workflow — do NOT re-litigate the ` +
|
|
202
|
+
`decision or expand scope beyond it.`);
|
|
203
|
+
}
|
|
182
204
|
export async function* runCouncil(topic, sessionModelId, messages, sessionId, rawLlm, respondToQuestion, respondToPreflight, processMessageFn, options) {
|
|
183
205
|
const stats = options?.councilStats ?? { calls: 0, startMs: Date.now(), phases: [] };
|
|
184
206
|
const costAware = isCouncilCostAware();
|
|
@@ -349,7 +371,11 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
|
|
|
349
371
|
const preflightGen = runPreflight(spec, participants, researchNeeded, respondToPreflight, {
|
|
350
372
|
repoEmpty: internetFirst,
|
|
351
373
|
researchOverridable: true,
|
|
352
|
-
|
|
374
|
+
// convenePath auto-approves the pre-debate plan card too: the agent
|
|
375
|
+
// already decided to convene, so re-gating the discussion plan is a
|
|
376
|
+
// redundant interruption of the autonomous tool call (same rationale as
|
|
377
|
+
// sprintPlanningMode). The brief is still shown; it just isn't blocking.
|
|
378
|
+
autoApprove: spec.ready === true || options?.autoApprovePreflight === true || options?.convenePath === true,
|
|
353
379
|
});
|
|
354
380
|
let preflightResult;
|
|
355
381
|
do {
|
|
@@ -503,6 +529,9 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
|
|
|
503
529
|
// unmet, runDebate asks the user (extend / accept / rescope) instead of
|
|
504
530
|
// silently synthesizing a partial outcome.
|
|
505
531
|
respondToQuestion,
|
|
532
|
+
// convene_council path — auto-accept escalation (no blocking card) since
|
|
533
|
+
// the council runs autonomously mid-agent-turn with no interactive user.
|
|
534
|
+
convenePath: options?.convenePath,
|
|
506
535
|
}, llm);
|
|
507
536
|
let debateResult;
|
|
508
537
|
do {
|
|
@@ -568,7 +597,13 @@ export async function* runCouncil(topic, sessionModelId, messages, sessionId, ra
|
|
|
568
597
|
data: { topic, roundCount: debateState.roundCount, participantCount: debateState.active.length },
|
|
569
598
|
});
|
|
570
599
|
// ── Post-Debate AskCard: What next? ─────────────────────────────────────────
|
|
571
|
-
|
|
600
|
+
// convenePath skips this ENTIRE interactive block (recommendation, option set,
|
|
601
|
+
// card, respondToQuestion, postDebateAction, onPostDebateAction, and the whole
|
|
602
|
+
// routing tree). On that path the agent that called `convene_council` decides
|
|
603
|
+
// what happens next — the CLI must not hardcode a post-council pick. The
|
|
604
|
+
// persistence block below still runs (audit trail, not a decision), and the
|
|
605
|
+
// function returns synthesisText as usual.
|
|
606
|
+
if (sessionId && !options?.convenePath) {
|
|
572
607
|
try {
|
|
573
608
|
const { randomUUID } = await import("crypto");
|
|
574
609
|
const refinementTopics = [];
|