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
|
@@ -13,7 +13,7 @@ import { getModelInfo, normalizeModelId } from "../models/registry.js";
|
|
|
13
13
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
14
14
|
import { apiBaseFor } from "../providers/endpoints.js";
|
|
15
15
|
import { loadKeyForProvider } from "../providers/keychain.js";
|
|
16
|
-
import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime
|
|
16
|
+
import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime, resolveTemperatureParam, } from "../providers/runtime.js";
|
|
17
17
|
import { ALL_PROVIDER_IDS } from "../providers/types.js";
|
|
18
18
|
import { statusBarStore } from "../state/status-bar-store.js";
|
|
19
19
|
import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionChainInfos, getSessionTotalTokens, loadSessionChainTranscriptState, logInteraction, markMessageCompleted, persistApprovedPlan, recordUsageEvent, SessionStore, } from "../storage/index.js";
|
|
@@ -30,6 +30,7 @@ import { runSideQuestion } from "../utils/side-question.js";
|
|
|
30
30
|
import { buildVerifyDetectPrompt, inferVerifyProjectProfile, normalizeVerifyRecipe, shouldTrustDeterministicRecipe, } from "../verify/entrypoint.js";
|
|
31
31
|
import { runVerifyOrchestration } from "../verify/orchestrator.js";
|
|
32
32
|
import { COUNCIL_COLOR_BG, COUNCIL_COLOR_RESET, COUNCIL_ROLE_COLORS, } from "./agent-options.js";
|
|
33
|
+
import { ASK_USER_DISMISSED } from "./ask-user.js";
|
|
33
34
|
import { AUTO_COMMIT_ATTRIBUTION, isAutoCommitEnabled, maybeAutoCommitTurn, snapshotDirtyPaths, } from "./auto-commit.js";
|
|
34
35
|
import { BatchTurnRunner } from "./batch-turn-runner.js";
|
|
35
36
|
import { accumulateUsage, buildAssistantBatchMessage, buildBatchChatCompletionRequest, buildBatchName, buildToolBatchMessage, extractJsonObject, getBatchUsage, hasUsage, parseToolArgumentsOrRaw, toLocalToolCall, } from "./batch-utils.js";
|
|
@@ -38,6 +39,7 @@ import { CouncilManager } from "./council-manager.js";
|
|
|
38
39
|
import { CrossTurnDedup, isCrossTurnDedupEnabled } from "./cross-turn-dedup.js";
|
|
39
40
|
import { DelegationManager } from "./delegations.js";
|
|
40
41
|
import { loadFlowResumeDigest } from "./flow-resume.js";
|
|
42
|
+
import { beginInteractivePause, endInteractivePause, isInteractivePaused } from "./interactive-pause.js";
|
|
41
43
|
import { MessageProcessor } from "./message-processor.js";
|
|
42
44
|
import { lastPersistedSeq } from "./message-seq.js";
|
|
43
45
|
import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
|
|
@@ -109,10 +111,10 @@ function sanitizeTitle(raw) {
|
|
|
109
111
|
* Generate a session title with a single lightweight LLM call on the current
|
|
110
112
|
* session model. Falls back to a truncated first-message title on any failure.
|
|
111
113
|
*/
|
|
112
|
-
async function genTitle(
|
|
114
|
+
async function genTitle(userMessage, modelId) {
|
|
113
115
|
try {
|
|
114
116
|
const { generateText } = await import("ai");
|
|
115
|
-
const runtime = resolveModelRuntime(
|
|
117
|
+
const runtime = resolveModelRuntime(modelId);
|
|
116
118
|
const snippet = userMessage.length > 1500 ? `${userMessage.slice(0, 1500)}…` : userMessage;
|
|
117
119
|
const { text, usage } = await generateText({
|
|
118
120
|
model: runtime.model,
|
|
@@ -133,13 +135,6 @@ async function genTitle(provider, userMessage, modelId) {
|
|
|
133
135
|
}
|
|
134
136
|
return { title: fallbackTitle(userMessage), modelId };
|
|
135
137
|
}
|
|
136
|
-
/**
|
|
137
|
-
* Resolve a model ID to a runnable AI SDK LanguageModel.
|
|
138
|
-
* Uses the Anthropic provider factory created by createProvider().
|
|
139
|
-
*/
|
|
140
|
-
function resolveModelRuntime(provider, modelId) {
|
|
141
|
-
return resolveRuntime(provider, modelId);
|
|
142
|
-
}
|
|
143
138
|
async function toolSetToBatchTools(_tools) {
|
|
144
139
|
// Batch API not supported with Anthropic in Phase 0. Phase 1 may add this.
|
|
145
140
|
throw new Error("Batch API not available in Phase 0. Use standard streaming mode.");
|
|
@@ -403,7 +398,22 @@ export class Agent {
|
|
|
403
398
|
getActiveRunId() {
|
|
404
399
|
return this._activeRunId;
|
|
405
400
|
}
|
|
406
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Point the agent at `model`, re-resolving the provider when the new model
|
|
403
|
+
* belongs to a different one.
|
|
404
|
+
*
|
|
405
|
+
* EVERY path that changes `this.modelId` must go through here. Changing the
|
|
406
|
+
* id alone leaves `providerId` / `provider` / `baseURL` / `apiKey` describing
|
|
407
|
+
* the PREVIOUS provider, so the next turn POSTs the new model to the old
|
|
408
|
+
* provider's endpoint — which rejects it as unknown (session 0c6728ba1a25:
|
|
409
|
+
* `gpt-5.4` sent to api.x.ai → 404 "The model gpt-5.4 does not exist", a
|
|
410
|
+
* wiring bug that reads like a bad model name). `setMode` used to do exactly
|
|
411
|
+
* that; it now calls this too.
|
|
412
|
+
*
|
|
413
|
+
* Session persistence is NOT done here — callers own that, because setMode
|
|
414
|
+
* writes mode+model together.
|
|
415
|
+
*/
|
|
416
|
+
_applyModelId(model) {
|
|
407
417
|
this.modelId = normalizeModelId(model);
|
|
408
418
|
const newProviderId = detectProviderForModel(this.modelId);
|
|
409
419
|
if (newProviderId !== this.providerId) {
|
|
@@ -437,6 +447,9 @@ export class Agent {
|
|
|
437
447
|
this.provider = null;
|
|
438
448
|
this._oauthInitDone = false;
|
|
439
449
|
}
|
|
450
|
+
}
|
|
451
|
+
setModel(model) {
|
|
452
|
+
this._applyModelId(model);
|
|
440
453
|
if (this.sessionStore && this.session) {
|
|
441
454
|
this.sessionStore.setModel(this.session.id, this.modelId);
|
|
442
455
|
this.session = this.sessionStore.getRequiredSession(this.session.id);
|
|
@@ -464,7 +477,11 @@ export class Agent {
|
|
|
464
477
|
this.mode = mode;
|
|
465
478
|
const modeModel = getModeSpecificModel(mode);
|
|
466
479
|
if (modeModel) {
|
|
467
|
-
this.modelId =
|
|
480
|
+
// Via _applyModelId, NOT a bare `this.modelId = ...`: a mode-specific
|
|
481
|
+
// model can belong to a different provider than the session's, and
|
|
482
|
+
// assigning the id alone would leave provider/baseURL/apiKey pointing at
|
|
483
|
+
// the old one — the cross-wire class in _applyModelId's docstring.
|
|
484
|
+
this._applyModelId(modeModel);
|
|
468
485
|
}
|
|
469
486
|
if (this.sessionStore && this.session) {
|
|
470
487
|
this.sessionStore.setMode(this.session.id, mode);
|
|
@@ -565,7 +582,7 @@ export class Agent {
|
|
|
565
582
|
if (!provider) {
|
|
566
583
|
return "New session";
|
|
567
584
|
}
|
|
568
|
-
const generated = await genTitle(
|
|
585
|
+
const generated = await genTitle(userMessage, this.modelId);
|
|
569
586
|
this.recordUsage(generated.usage, "title", generated.modelId);
|
|
570
587
|
if (this.sessionStore && this.session && !this.session.title && generated.title) {
|
|
571
588
|
this.sessionStore.setTitle(this.session.id, generated.title);
|
|
@@ -598,7 +615,7 @@ export class Agent {
|
|
|
598
615
|
charBudget -= snippet.length;
|
|
599
616
|
}
|
|
600
617
|
const conversationContext = contextParts.join("\n\n");
|
|
601
|
-
const result = await runSideQuestion(question, this.
|
|
618
|
+
const result = await runSideQuestion(question, this.modelId, conversationContext, signal);
|
|
602
619
|
this.recordUsage(result.usage, "other");
|
|
603
620
|
return result;
|
|
604
621
|
}
|
|
@@ -660,6 +677,15 @@ export class Agent {
|
|
|
660
677
|
setSafetyOverrideHandler(fn) {
|
|
661
678
|
this._safetyOverrideHandler = fn;
|
|
662
679
|
}
|
|
680
|
+
// ask_user handler — set by the UI (app.tsx). Invoked when the model calls the
|
|
681
|
+
// `ask_user` tool: the UI surfaces an agent-authored card and resolves with the
|
|
682
|
+
// human's answer string, which becomes the tool result. When unset (headless
|
|
683
|
+
// without an answerer), the tool returns the dismissed sentinel so the agent
|
|
684
|
+
// still continues from its own judgment.
|
|
685
|
+
_askUserHandler = null;
|
|
686
|
+
setAskUserHandler(fn) {
|
|
687
|
+
this._askUserHandler = fn;
|
|
688
|
+
}
|
|
663
689
|
respondToToolApproval(approvalId, approved) {
|
|
664
690
|
const toolApprovalResponse = {
|
|
665
691
|
role: "tool",
|
|
@@ -1190,7 +1216,6 @@ export class Agent {
|
|
|
1190
1216
|
async runTaskRequest(request, onActivity, abortSignal) {
|
|
1191
1217
|
const provider = this.requireProvider();
|
|
1192
1218
|
const deps = {
|
|
1193
|
-
getProvider: () => provider,
|
|
1194
1219
|
resolveModelForTask: (task) => this._resolveModelForTask(task),
|
|
1195
1220
|
getModelId: () => this.modelId,
|
|
1196
1221
|
getProviderId: () => this.providerId,
|
|
@@ -1422,7 +1447,7 @@ export class Agent {
|
|
|
1422
1447
|
// Phase 1: ask the compaction proposer model whether to compact and what to keep/drop.
|
|
1423
1448
|
// Only compact if the model says yes. On error/skip, fall back to heuristic.
|
|
1424
1449
|
const compactModelId = this._resolveCompactModel();
|
|
1425
|
-
const proposal = await proposeCompaction(
|
|
1450
|
+
const proposal = await proposeCompaction(compactModelId, this.messages, signal);
|
|
1426
1451
|
if (proposal !== null) {
|
|
1427
1452
|
// Model decided — compact only if model says shouldCompact
|
|
1428
1453
|
if (force) {
|
|
@@ -1485,7 +1510,7 @@ export class Agent {
|
|
|
1485
1510
|
const customInstructions = isSubSession
|
|
1486
1511
|
? "This is a temporary sub-session. Under sub-sessions, it is CRITICAL to preserve active files being worked on, compiler/linter error states, and exact line coordinates in the summary. Do not omit details of files edited, tests run, or compiler diagnostics, as the model needs this specific context to continue working without re-reading the files."
|
|
1487
1512
|
: undefined;
|
|
1488
|
-
const { summary, usage: compactUsage } = await generateCompactionSummary(
|
|
1513
|
+
const { summary, usage: compactUsage } = await generateCompactionSummary(compactModelId, preparation, customInstructions, signal);
|
|
1489
1514
|
// Record compaction call in cost-log — bypasses recordUsage because
|
|
1490
1515
|
// compaction returns usage separately and isn't routed through the
|
|
1491
1516
|
// status-bar / usage event pipeline (intentional: it's overhead, not user spend).
|
|
@@ -1652,7 +1677,7 @@ export class Agent {
|
|
|
1652
1677
|
// Council v2 — Clarify → Confirm → Debate → Plan → Execute
|
|
1653
1678
|
// ========================================================================
|
|
1654
1679
|
async *runCouncilV2(topic, options) {
|
|
1655
|
-
const { runCouncil,
|
|
1680
|
+
const { runCouncil, buildNeutralPostCouncilContinuation } = await import("../council/index.js");
|
|
1656
1681
|
const { createCouncilLLM } = await import("../council/llm.js");
|
|
1657
1682
|
const councilStats = { calls: 0, startMs: Date.now(), phases: [] };
|
|
1658
1683
|
const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, councilStats);
|
|
@@ -1702,6 +1727,9 @@ export class Agent {
|
|
|
1702
1727
|
cwd: this.bash.getCwd(),
|
|
1703
1728
|
councilStats, // NEW — share orchestrator's stats object with runCouncil (Phase 14 CQ-01)
|
|
1704
1729
|
signal,
|
|
1730
|
+
// convene_council path — suppress ALL hardcoded post-debate decision
|
|
1731
|
+
// surface; the agent decides what happens after the synthesis.
|
|
1732
|
+
convenePath: options?.convenePath,
|
|
1705
1733
|
// When the Context Rail is active it carries leader/panel/cost as
|
|
1706
1734
|
// ambient sidebar rows, so suppress the duplicate inline summary.
|
|
1707
1735
|
suppressInlineMeta: isContextRailEnabled(),
|
|
@@ -1753,7 +1781,12 @@ export class Agent {
|
|
|
1753
1781
|
// fires ONLY on the top-level slash path, never when nested inside
|
|
1754
1782
|
// processMessage (auto-council) or drained by the runDebate tool — those
|
|
1755
1783
|
// callers manage their own continuation.
|
|
1756
|
-
|
|
1784
|
+
// convenePath suppresses the hardcoded card (chosenAction stays undefined),
|
|
1785
|
+
// so always hand the synthesis to a normal agent turn via the neutral
|
|
1786
|
+
// continuation and let the agent decide. ownsController scopes this to the
|
|
1787
|
+
// top-level /council slash path (auto-council nests with ownsController
|
|
1788
|
+
// false and continues in tool-engine instead).
|
|
1789
|
+
const continuationPrompt = ownsController && synthesis ? buildNeutralPostCouncilContinuation(synthesis) || null : null;
|
|
1757
1790
|
const isBuildContinuation = chosenAction === "implement" || chosenAction === "generate_plan";
|
|
1758
1791
|
if (continuationPrompt && isBuildContinuation && process.env.MUONROI_COUNCIL_ISOLATE_IMPL !== "0") {
|
|
1759
1792
|
// #1 — build the council decision in an ISOLATED sub-agent instead of
|
|
@@ -1839,6 +1872,7 @@ export class Agent {
|
|
|
1839
1872
|
idleMs,
|
|
1840
1873
|
totalMs,
|
|
1841
1874
|
label: "council continuation turn",
|
|
1875
|
+
shouldSuppressFire: isInteractivePaused,
|
|
1842
1876
|
});
|
|
1843
1877
|
}
|
|
1844
1878
|
catch (err) {
|
|
@@ -1881,12 +1915,45 @@ export class Agent {
|
|
|
1881
1915
|
phases: [],
|
|
1882
1916
|
};
|
|
1883
1917
|
const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, productStats);
|
|
1884
|
-
|
|
1918
|
+
// Autonomous-execution permission for the product loop. /ideal's consent
|
|
1919
|
+
// boundary is the preflight plan-approval askcard; once the PO approves the
|
|
1920
|
+
// plan, the sprint IMPLEMENT turn must apply its own file-op mutations without
|
|
1921
|
+
// a per-tool approval prompt. In `safe` mode a Write/Edit surfaces a
|
|
1922
|
+
// tool_approval_request and awaits respondToToolApproval — but in the driven
|
|
1923
|
+
// product-loop context nothing answers it and no approval askcard renders, so
|
|
1924
|
+
// the impl turn wedges forever right after finishReason:tool-calls (observed
|
|
1925
|
+
// live 2026-07-14: 0 files written across grok/opencode/deepseek + isolated &
|
|
1926
|
+
// streamed paths — tool-engine.ts:2997). Elevating safe→auto-edit for the turn
|
|
1927
|
+
// auto-approves file ops (yolo stays yolo); catastrophic bash stays hard-blocked
|
|
1928
|
+
// by permission-mode's CATASTROPHIC_PATTERNS regardless of mode.
|
|
1929
|
+
const self = this;
|
|
1930
|
+
const processMessageFn = (m) => (async function* () {
|
|
1931
|
+
const prev = self.permissionMode;
|
|
1932
|
+
if (self.permissionMode === "safe")
|
|
1933
|
+
self.permissionMode = "auto-edit";
|
|
1934
|
+
try {
|
|
1935
|
+
yield* self.processMessage(m, options?.observer);
|
|
1936
|
+
}
|
|
1937
|
+
finally {
|
|
1938
|
+
self.permissionMode = prev;
|
|
1939
|
+
}
|
|
1940
|
+
})();
|
|
1885
1941
|
// Isolated bounded task-runner bridge for the sprint implement stage: a fresh
|
|
1886
1942
|
// child context (getSubAgentBudgetChars cap, independent compaction) that does
|
|
1887
1943
|
// NOT inherit this turn's council-debate history — the root fix for the live
|
|
1888
1944
|
// ctx-overflow wedge. Returns a compact ToolResult (absorbed, no parent bloat).
|
|
1889
|
-
|
|
1945
|
+
// Same autonomous-permission elevation as the streamed path above.
|
|
1946
|
+
const runIsolatedTask = async (request) => {
|
|
1947
|
+
const prev = self.permissionMode;
|
|
1948
|
+
if (self.permissionMode === "safe")
|
|
1949
|
+
self.permissionMode = "auto-edit";
|
|
1950
|
+
try {
|
|
1951
|
+
return await this.runTaskRequest(request, undefined, this.abortController?.signal);
|
|
1952
|
+
}
|
|
1953
|
+
finally {
|
|
1954
|
+
self.permissionMode = prev;
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1890
1957
|
const flowDir = nodePath.join(this.bash.getCwd(), ".muonroi-flow");
|
|
1891
1958
|
// P2.7 (LLM-first — no-regex routing): the work-depth tier that decides
|
|
1892
1959
|
// /ideal's route is judged by the MODEL (the same depthTier the PIL Layer-1
|
|
@@ -1903,7 +1970,15 @@ export class Agent {
|
|
|
1903
1970
|
let depth = "standard";
|
|
1904
1971
|
try {
|
|
1905
1972
|
const { createLlmClassifier } = await import("../pil/llm-classify.js");
|
|
1906
|
-
const
|
|
1973
|
+
const { probeRepoGrounding } = await import("../pil/repo-grounding-probe.js");
|
|
1974
|
+
const { getRepoStructureHints } = await import("../pil/repo-structure-hints.js");
|
|
1975
|
+
const classify = createLlmClassifier(this.modelId, {
|
|
1976
|
+
routeFastTier: true,
|
|
1977
|
+
// /ideal routing needs a reliable depth verdict; allow cross-provider
|
|
1978
|
+
// fallback so an agentic/no-fast-tier session model (e.g. xai) doesn't
|
|
1979
|
+
// strand the classify at fail-open. Bounded by CLASSIFY_TOTAL_BUDGET_MS.
|
|
1980
|
+
crossProviderFallback: true,
|
|
1981
|
+
});
|
|
1907
1982
|
const res = await classify(payload.idea);
|
|
1908
1983
|
if (res?.depthTier) {
|
|
1909
1984
|
depth = res.depthTier;
|
|
@@ -1916,32 +1991,72 @@ export class Agent {
|
|
|
1916
1991
|
// LLM-first clarity signal — feeds the downstream interview as a hint.
|
|
1917
1992
|
if (res?.needsClarification === true)
|
|
1918
1993
|
needsClarification = true;
|
|
1994
|
+
// Design B — deterministic repo-grounding probe. Trigger on Layer-1
|
|
1995
|
+
// OUTPUT (ambiguity), NOT a prompt regex: run only when the verdict is
|
|
1996
|
+
// non-trivial (depth !== "quick") OR low-confidence, so a well-specified
|
|
1997
|
+
// trivial task still hot-paths on one cheap call. The probe itself is
|
|
1998
|
+
// deterministic (REPO_DEEP_MAP index + bounded on-disk reads, no LLM).
|
|
1999
|
+
// Once it runs its MEASURED facts drive routing — confidence is
|
|
2000
|
+
// trigger-only and can no longer override grounding.
|
|
2001
|
+
const lowConfidence = (res?.confidence ?? 1) < 0.7;
|
|
2002
|
+
if (depth !== "quick" || lowConfidence) {
|
|
2003
|
+
try {
|
|
2004
|
+
const hints = getRepoStructureHints(process.cwd());
|
|
2005
|
+
const probe = probeRepoGrounding(payload.idea, hints, { cwd: process.cwd() });
|
|
2006
|
+
if (probe.ran) {
|
|
2007
|
+
// Invariant: probe ran AND zero matched files, or a symbol
|
|
2008
|
+
// collision → route INTO Council (the arbiter). Never direct-
|
|
2009
|
+
// dispatch from weak grounding.
|
|
2010
|
+
if (probe.groundingUncertainty) {
|
|
2011
|
+
needsClarification = true;
|
|
2012
|
+
depth = depth === "quick" ? "standard" : depth;
|
|
2013
|
+
}
|
|
2014
|
+
// Measured size escalation: a large grounded surface routes heavy
|
|
2015
|
+
// regardless of the sentence-level classify verdict.
|
|
2016
|
+
if (probe.bucket === "large")
|
|
2017
|
+
depth = "heavy";
|
|
2018
|
+
console.error(`[ideal/route] grounding probe: files=${probe.matchedFiles} loc=${probe.totalLoc} ` +
|
|
2019
|
+
`dirs=${probe.matchedDirs} collision=${probe.collision} bucket=${probe.bucket} ` +
|
|
2020
|
+
`uncertainty=${probe.groundingUncertainty} → depth=${depth}`);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
catch (err) {
|
|
2024
|
+
// Fail-open: a probe hiccup must never block /ideal (No-Silent-Catch).
|
|
2025
|
+
console.error(`[ideal/route] grounding probe failed, ignoring: ${err?.message}`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
1919
2028
|
}
|
|
1920
2029
|
catch (err) {
|
|
1921
2030
|
// Fail-open: never block /ideal on a classify hiccup (No-Silent-Catch).
|
|
1922
2031
|
console.error(`[ideal/route] depth classify failed, defaulting to "standard": ${err?.message}`);
|
|
1923
2032
|
}
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1928
|
-
//
|
|
1929
|
-
//
|
|
1930
|
-
//
|
|
1931
|
-
//
|
|
1932
|
-
//
|
|
1933
|
-
//
|
|
1934
|
-
//
|
|
1935
|
-
//
|
|
1936
|
-
//
|
|
1937
|
-
//
|
|
1938
|
-
//
|
|
1939
|
-
//
|
|
1940
|
-
//
|
|
2033
|
+
// Council routing gate (model-signal, no hardcoded depth→council table).
|
|
2034
|
+
//
|
|
2035
|
+
// History: /ideal used to force Council UNCONDITIONALLY because the cheap
|
|
2036
|
+
// classify was too noisy to gate on — the SAME architectural prompt scored
|
|
2037
|
+
// `heavy` on one run and `standard` the next, so routing flip-flopped. That
|
|
2038
|
+
// noise was root-caused (2026-07-15, harness-measured): the session model
|
|
2039
|
+
// could be an agentic model (e.g. grok-composer) that ignores the terse
|
|
2040
|
+
// 8-word classify contract and emits task-planning prose → null → fail-open
|
|
2041
|
+
// "standard"; and stream errors were swallowed into the same null. Both are
|
|
2042
|
+
// now fixed in llm-classify.ts (cross-provider route to a keyed instruction-
|
|
2043
|
+
// follower + No-Silent-Catch error surfacing), so the depth/clarity signal
|
|
2044
|
+
// is reliable enough to gate on.
|
|
2045
|
+
//
|
|
2046
|
+
// Gate: only a genuinely TRIVIAL /ideal — the model says depth=quick AND
|
|
2047
|
+
// not-underspecified — skips Council and takes the product-loop hot-path
|
|
2048
|
+
// (single sprint, no debate; index.ts complexity==="low" branch). Anything
|
|
2049
|
+
// else (standard, heavy, or any underspecified request) still runs the full
|
|
2050
|
+
// Council/loop-driver pipeline: scan source → adaptive interview → leader-
|
|
2051
|
+
// tier assessment + debate AFTER scope is understood. When the model is
|
|
2052
|
+
// uncertain (null depth → "medium") it routes INTO Council, the arbiter —
|
|
2053
|
+
// never a hardcoded default. Explicit `--maintain` still opts out entirely.
|
|
1941
2054
|
complexity = depth === "heavy" ? "high" : depth === "quick" ? "low" : "medium";
|
|
1942
2055
|
sufficiencyMissing = undefined;
|
|
1943
2056
|
if (payload.flags.mode !== "maintain") {
|
|
1944
|
-
|
|
2057
|
+
const trivial = depth === "quick" && needsClarification !== true;
|
|
2058
|
+
if (!trivial)
|
|
2059
|
+
routeForceCouncil = true;
|
|
1945
2060
|
}
|
|
1946
2061
|
}
|
|
1947
2062
|
const gen = runProductLoop({
|
|
@@ -2552,7 +2667,7 @@ export class Agent {
|
|
|
2552
2667
|
// continuation ("ok làm phần đó đi") is judged in isolation and can be
|
|
2553
2668
|
// mis-routed as a fresh/unrelated task. Passing contextInfo also turns
|
|
2554
2669
|
// on the session-size metadata block the ROTATE_SESSION rule relies on.
|
|
2555
|
-
const routeResult = await classifySubSessionAction(this.
|
|
2670
|
+
const routeResult = await classifySubSessionAction(this.modelId, userMessage, {
|
|
2556
2671
|
currentChars,
|
|
2557
2672
|
threshold,
|
|
2558
2673
|
recentTurns: this._buildRecentTurnsSummary(),
|
|
@@ -2596,7 +2711,7 @@ export class Agent {
|
|
|
2596
2711
|
const { deliberateCompact } = await import("../flow/compaction/index.js");
|
|
2597
2712
|
const { getDatabase } = await import("../storage/db.js");
|
|
2598
2713
|
const { appendCompaction, getNextMessageSequence } = await import("../storage/transcript.js");
|
|
2599
|
-
const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.
|
|
2714
|
+
const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.modelId);
|
|
2600
2715
|
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2601
2716
|
const db = getDatabase();
|
|
2602
2717
|
this.sessionStore.linkChild(newSession.id, parentSessionId, "rotation");
|
|
@@ -2753,6 +2868,7 @@ export class Agent {
|
|
|
2753
2868
|
idleMs: turnIdleMs,
|
|
2754
2869
|
totalMs: turnTotalMs,
|
|
2755
2870
|
label: "assistant turn",
|
|
2871
|
+
shouldSuppressFire: isInteractivePaused,
|
|
2756
2872
|
});
|
|
2757
2873
|
}
|
|
2758
2874
|
catch (stallErr) {
|
|
@@ -3031,9 +3147,8 @@ export class Agent {
|
|
|
3031
3147
|
`${serializedParent}\n\n` +
|
|
3032
3148
|
`Provide clear, actionable guidance to resolve the child's query.`;
|
|
3033
3149
|
const { generateText } = await import("ai");
|
|
3034
|
-
const provider = self.requireProvider();
|
|
3035
3150
|
const modelId = self.modelId;
|
|
3036
|
-
const runtime = resolveModelRuntime(
|
|
3151
|
+
const runtime = resolveModelRuntime(modelId);
|
|
3037
3152
|
const result = await generateText({
|
|
3038
3153
|
model: runtime.model,
|
|
3039
3154
|
system: systemPrompt,
|
|
@@ -3090,6 +3205,25 @@ export class Agent {
|
|
|
3090
3205
|
return { action: "block" };
|
|
3091
3206
|
}
|
|
3092
3207
|
},
|
|
3208
|
+
askUser: async (info) => {
|
|
3209
|
+
const h = self._askUserHandler;
|
|
3210
|
+
if (!h) {
|
|
3211
|
+
logger.warn("orchestrator", "askUser called but no handler registered — returning dismissed sentinel");
|
|
3212
|
+
return ASK_USER_DISMISSED;
|
|
3213
|
+
}
|
|
3214
|
+
// Hold both watchdogs open while the human answers (see interactive-pause.ts).
|
|
3215
|
+
beginInteractivePause();
|
|
3216
|
+
try {
|
|
3217
|
+
return await h(info);
|
|
3218
|
+
}
|
|
3219
|
+
catch (err) {
|
|
3220
|
+
logger.error("orchestrator", "askUser crashed", { error: err });
|
|
3221
|
+
return ASK_USER_DISMISSED;
|
|
3222
|
+
}
|
|
3223
|
+
finally {
|
|
3224
|
+
endInteractivePause();
|
|
3225
|
+
}
|
|
3226
|
+
},
|
|
3093
3227
|
runCouncilV2: (msg, opts) => self.runCouncilV2(msg, opts),
|
|
3094
3228
|
processMessage: (msg, obs, imgs) => self.processMessage(msg, obs, imgs),
|
|
3095
3229
|
processMessageBatchTurn: (args) => self.processMessageBatchTurn(args),
|
|
@@ -3213,14 +3347,34 @@ export class Agent {
|
|
|
3213
3347
|
// and must be re-resolved from the NEW provider's own credentials.
|
|
3214
3348
|
const providerDeferred = this.provider === null;
|
|
3215
3349
|
const keyIsSentinelOrEmpty = !this.apiKey || this.apiKey === "oauth";
|
|
3216
|
-
if (!providerDeferred && !keyIsSentinelOrEmpty)
|
|
3217
|
-
return;
|
|
3218
3350
|
try {
|
|
3219
|
-
const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
|
|
3351
|
+
const { listOAuthProviderIds, getOAuthProviderConfig } = await import("../providers/auth/registry.js");
|
|
3220
3352
|
const ids = await listOAuthProviderIds();
|
|
3221
|
-
|
|
3222
|
-
//
|
|
3223
|
-
|
|
3353
|
+
const oauthCapable = ids.includes(this.providerId);
|
|
3354
|
+
// One auth mode per provider (OAuth XOR API key): if this OAuth-capable
|
|
3355
|
+
// provider has a live token, OAuth wins OUTRIGHT — even over a real held
|
|
3356
|
+
// API key. This is the shadowing-bug fix: a stale sk-proj key must not
|
|
3357
|
+
// beat a valid ChatGPT OAuth login. Check token presence BEFORE the
|
|
3358
|
+
// early-out so a held key can no longer suppress OAuth.
|
|
3359
|
+
let hasOAuthTokens = false;
|
|
3360
|
+
if (oauthCapable) {
|
|
3361
|
+
try {
|
|
3362
|
+
const cfg = await getOAuthProviderConfig(this.providerId);
|
|
3363
|
+
const tokens = cfg ? await cfg.loadTokensWithRefresh().catch(() => null) : null;
|
|
3364
|
+
hasOAuthTokens = !!tokens?.accessToken;
|
|
3365
|
+
}
|
|
3366
|
+
catch {
|
|
3367
|
+
hasOAuthTokens = false;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
// Nothing to do only when: no OAuth token, a real key is held, and the
|
|
3371
|
+
// provider was not deferred by a model switch.
|
|
3372
|
+
if (!hasOAuthTokens && !providerDeferred && !keyIsSentinelOrEmpty)
|
|
3373
|
+
return;
|
|
3374
|
+
// OAuth path: inject subscription tokens as Bearer headers. Taken when a
|
|
3375
|
+
// token exists (override a held key), OR when no real key is held for an
|
|
3376
|
+
// OAuth-capable provider.
|
|
3377
|
+
if (oauthCapable && (hasOAuthTokens || keyIsSentinelOrEmpty)) {
|
|
3224
3378
|
const effectiveBaseURL = this.baseURL &&
|
|
3225
3379
|
this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
|
|
3226
3380
|
? this.baseURL
|
|
@@ -29,7 +29,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
|
|
|
29
29
|
let llmFallback;
|
|
30
30
|
try {
|
|
31
31
|
const { createLlmClassifier } = await import("../pil/llm-classify.js");
|
|
32
|
-
llmFallback = createLlmClassifier(deps.
|
|
32
|
+
llmFallback = createLlmClassifier(deps.modelId, { routeFastTier: true });
|
|
33
33
|
}
|
|
34
34
|
catch (err) {
|
|
35
35
|
logger.error("pil", "LLM fallback wiring failed", { error: err });
|
|
@@ -40,7 +40,7 @@ export async function* prepareTurnContext(deps, userMessage, _budgetOverride) {
|
|
|
40
40
|
let clarificationProposer;
|
|
41
41
|
try {
|
|
42
42
|
const { createModelClarificationProposer } = await import("../pil/discovery.js");
|
|
43
|
-
clarificationProposer = createModelClarificationProposer(deps.
|
|
43
|
+
clarificationProposer = createModelClarificationProposer(deps.modelId);
|
|
44
44
|
}
|
|
45
45
|
catch (err) {
|
|
46
46
|
logger.error("pil", "clarification proposer wiring failed", { error: err });
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*
|
|
9
9
|
* Pure — no React, no side effects. Unit-testable in isolation.
|
|
10
10
|
*/
|
|
11
|
-
export type SafetyBlockKind = "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
|
|
11
|
+
export type SafetyBlockKind = "catastrophic" | "destructive-revert" | "dangerous" | "git-safety" | "empty-bash";
|
|
12
12
|
export interface SafetyBlockInfo {
|
|
13
13
|
kind: SafetyBlockKind;
|
|
14
14
|
/** The blocked command or tool call description */
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
const CATEGORY_LABELS = {
|
|
12
12
|
catastrophic: "Lỗi bảo mật nghiêm trọng",
|
|
13
|
+
"destructive-revert": "Hủy thay đổi chưa lưu (không thể hoàn tác)",
|
|
13
14
|
dangerous: "Lệnh nguy hiểm",
|
|
14
15
|
"git-safety": "Git safety gate",
|
|
15
16
|
"empty-bash": "Bash call trống",
|
|
@@ -38,8 +39,10 @@ export function planSafetyAskcard(info) {
|
|
|
38
39
|
description: "Không chạy lệnh này, trả về lỗi cho agent",
|
|
39
40
|
},
|
|
40
41
|
];
|
|
41
|
-
// Allow-session only for less severe blocks
|
|
42
|
-
|
|
42
|
+
// Allow-session only for less severe blocks. A destructive-revert (discards
|
|
43
|
+
// uncommitted work irreversibly) is treated like catastrophic: allow-once
|
|
44
|
+
// only, never a blanket session allow.
|
|
45
|
+
if (info.kind !== "catastrophic" && info.kind !== "destructive-revert") {
|
|
43
46
|
options.splice(1, 0, {
|
|
44
47
|
label: "Cho phép cả phiên (Allow session)",
|
|
45
48
|
value: "allow-session",
|
|
@@ -38,6 +38,11 @@ export declare function parseSafetyBlock(outputText: string): ParsedSafetyBlock
|
|
|
38
38
|
* the "don't ask me" mode actually stops asking for routine guardrails.
|
|
39
39
|
* - `catastrophic` ALWAYS shows the askcard, even in yolo — an irreversible
|
|
40
40
|
* destroyer (rm -rf /dev, mkfs, sudo …) must never run unattended.
|
|
41
|
+
* - `destructive-revert` ALWAYS shows the askcard, even in yolo — discarding
|
|
42
|
+
* uncommitted work (git checkout --/restore/reset --hard/clean, rm of a
|
|
43
|
+
* tracked file) is irreversible; headless it hard-blocks. This is the guard
|
|
44
|
+
* that stops an autonomous /ideal sub-agent from silently reverting the
|
|
45
|
+
* user's unrelated working-tree changes.
|
|
41
46
|
* - `empty-bash` is handled separately (auto-block, no card) and never
|
|
42
47
|
* reaches this policy.
|
|
43
48
|
* - `safe` / `auto-edit` always show the card for any real block.
|
|
@@ -39,6 +39,11 @@ export function parseSafetyBlock(outputText) {
|
|
|
39
39
|
* the "don't ask me" mode actually stops asking for routine guardrails.
|
|
40
40
|
* - `catastrophic` ALWAYS shows the askcard, even in yolo — an irreversible
|
|
41
41
|
* destroyer (rm -rf /dev, mkfs, sudo …) must never run unattended.
|
|
42
|
+
* - `destructive-revert` ALWAYS shows the askcard, even in yolo — discarding
|
|
43
|
+
* uncommitted work (git checkout --/restore/reset --hard/clean, rm of a
|
|
44
|
+
* tracked file) is irreversible; headless it hard-blocks. This is the guard
|
|
45
|
+
* that stops an autonomous /ideal sub-agent from silently reverting the
|
|
46
|
+
* user's unrelated working-tree changes.
|
|
42
47
|
* - `empty-bash` is handled separately (auto-block, no card) and never
|
|
43
48
|
* reaches this policy.
|
|
44
49
|
* - `safe` / `auto-edit` always show the card for any real block.
|
|
@@ -48,6 +53,8 @@ export function shouldAutoAllowYolo(kind, mode) {
|
|
|
48
53
|
return false;
|
|
49
54
|
if (kind === "catastrophic")
|
|
50
55
|
return false;
|
|
56
|
+
if (kind === "destructive-revert")
|
|
57
|
+
return false;
|
|
51
58
|
if (kind === "empty-bash")
|
|
52
59
|
return false;
|
|
53
60
|
return true;
|
|
@@ -130,4 +130,11 @@ export declare function shouldContinueAfterMidLoopStall(s: MidLoopStallState): b
|
|
|
130
130
|
* (1-based): 500 → 1000 → 2000 → 4000 → 4000.
|
|
131
131
|
*/
|
|
132
132
|
export declare function stallRepromptBackoffMs(attempt: number): number;
|
|
133
|
-
export declare function createStallWatchdog(timeoutMs: number, onFire?: () => void, progressOpts?: StallWatchdogProgressOpts
|
|
133
|
+
export declare function createStallWatchdog(timeoutMs: number, onFire?: () => void, progressOpts?: StallWatchdogProgressOpts,
|
|
134
|
+
/**
|
|
135
|
+
* Optional gate consulted the instant a timer would fire. When it returns
|
|
136
|
+
* true the watchdog RE-ARMS instead of aborting — used to hold the stream
|
|
137
|
+
* open while a blocking interactive card (`ask_user`) awaits a human, without
|
|
138
|
+
* counting that wait as a provider stall.
|
|
139
|
+
*/
|
|
140
|
+
shouldSuppressFire?: () => boolean): StallWatchdog;
|
|
@@ -70,7 +70,14 @@ export function shouldContinueAfterMidLoopStall(s) {
|
|
|
70
70
|
export function stallRepromptBackoffMs(attempt) {
|
|
71
71
|
return Math.min(500 * 2 ** (Math.max(1, attempt) - 1), 4_000);
|
|
72
72
|
}
|
|
73
|
-
export function createStallWatchdog(timeoutMs, onFire, progressOpts
|
|
73
|
+
export function createStallWatchdog(timeoutMs, onFire, progressOpts,
|
|
74
|
+
/**
|
|
75
|
+
* Optional gate consulted the instant a timer would fire. When it returns
|
|
76
|
+
* true the watchdog RE-ARMS instead of aborting — used to hold the stream
|
|
77
|
+
* open while a blocking interactive card (`ask_user`) awaits a human, without
|
|
78
|
+
* counting that wait as a provider stall.
|
|
79
|
+
*/
|
|
80
|
+
shouldSuppressFire) {
|
|
74
81
|
const controller = new AbortController();
|
|
75
82
|
let firedFlag = false;
|
|
76
83
|
let timer = null;
|
|
@@ -99,14 +106,28 @@ export function createStallWatchdog(timeoutMs, onFire, progressOpts) {
|
|
|
99
106
|
const arm = () => {
|
|
100
107
|
if (!enabled)
|
|
101
108
|
return;
|
|
102
|
-
timer = setTimeout(() =>
|
|
109
|
+
timer = setTimeout(() => {
|
|
110
|
+
// Hold open (re-arm) while an interactive card blocks the turn — the
|
|
111
|
+
// human is thinking, not a stalled provider.
|
|
112
|
+
if (shouldSuppressFire?.()) {
|
|
113
|
+
arm();
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
fire(onFire);
|
|
117
|
+
}, timeoutMs);
|
|
103
118
|
// Don't keep the event loop alive solely for the watchdog (Node).
|
|
104
119
|
timer.unref?.();
|
|
105
120
|
};
|
|
106
121
|
const armProgress = () => {
|
|
107
122
|
if (!progressEnabled)
|
|
108
123
|
return;
|
|
109
|
-
progressTimer = setTimeout(() =>
|
|
124
|
+
progressTimer = setTimeout(() => {
|
|
125
|
+
if (shouldSuppressFire?.()) {
|
|
126
|
+
armProgress();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
fire(progressOpts?.onProgressFire);
|
|
130
|
+
}, progressTimeoutMs);
|
|
110
131
|
progressTimer.unref?.();
|
|
111
132
|
};
|
|
112
133
|
const clear = () => {
|
|
@@ -3,7 +3,6 @@ import { type ResolvedModelRuntime } from "../providers/runtime.js";
|
|
|
3
3
|
import type { ProviderId } from "../providers/types.js";
|
|
4
4
|
import { BashTool } from "../tools/bash.js";
|
|
5
5
|
import type { AgentMode, TaskRequest, ToolResult } from "../types/index.js";
|
|
6
|
-
import type { LegacyProvider } from "./agent-options.js";
|
|
7
6
|
import type { CrossTurnDedup } from "./cross-turn-dedup.js";
|
|
8
7
|
import type { ReadPathBudget } from "./read-path-budget.js";
|
|
9
8
|
/**
|
|
@@ -11,8 +10,6 @@ import type { ReadPathBudget } from "./read-path-budget.js";
|
|
|
11
10
|
* without holding a circular reference. Mirrors the CouncilManager DI pattern.
|
|
12
11
|
*/
|
|
13
12
|
export interface StreamRunnerDeps {
|
|
14
|
-
/** Current top-level provider instance (already validated). */
|
|
15
|
-
getProvider(): LegacyProvider;
|
|
16
13
|
/** Resolve a specific task tier's model id (uses Agent's role config). */
|
|
17
14
|
resolveModelForTask(task: "compact" | "explore" | "general" | "title" | "verify"): string;
|
|
18
15
|
/** Top-level model id (for routed_from telemetry). */
|
|
@@ -108,6 +105,19 @@ export type SetupOutcome = {
|
|
|
108
105
|
kind: "prepared";
|
|
109
106
|
prepared: PreparedSubAgentCall;
|
|
110
107
|
} | SetupShortCircuit;
|
|
108
|
+
/**
|
|
109
|
+
* Emit one sub-agent diagnostic line (MUONROI_DEBUG_SUBAGENT=1).
|
|
110
|
+
*
|
|
111
|
+
* Writes to the file named by MUONROI_SUBAGENT_DEBUG_LOG when set, else stderr.
|
|
112
|
+
* The file sink exists because stderr is a dead end for the case this
|
|
113
|
+
* diagnostic was written for: under the MCP harness the TUI runs as a child
|
|
114
|
+
* whose stderr nobody reads (opentui-spawn.ts consumes only the fd3/named-pipe
|
|
115
|
+
* stream), so every line went into a pipe no one drains. G1 — "Task failed: No
|
|
116
|
+
* output generated" — was named, given this flag, and never diagnosed, because
|
|
117
|
+
* the diagnostic could not reach whoever turned it on. Mirrors the working
|
|
118
|
+
* MUONROI_COUNCIL_DEBUG_LOG sink in council/llm.ts.
|
|
119
|
+
*/
|
|
120
|
+
export declare function writeSubagentDebug(enabled: boolean, line: string): void;
|
|
111
121
|
/**
|
|
112
122
|
* StreamRunner — extracted sub-agent stream lifecycle.
|
|
113
123
|
*
|