muonroi-cli 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
- package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
- package/dist/packages/agent-harness-core/src/selector.js +2 -2
- package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
- package/dist/packages/agent-harness-opentui/src/install.js +1 -1
- package/dist/src/__test-stubs__/vitest-setup.js +4 -0
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
- package/dist/src/agent-harness/mock-model.d.ts +1 -1
- package/dist/src/agent-harness/mock-model.js +32 -3
- package/dist/src/agent-harness/test-spawn.js +3 -1
- package/dist/src/chat/chat-keychain.js +9 -1
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/cli/keys.d.ts +2 -2
- package/dist/src/cli/keys.js +107 -28
- package/dist/src/cli/usage-report.js +2 -2
- package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
- package/dist/src/council/__tests__/abort-threading.test.js +193 -0
- package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
- package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
- package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
- package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
- package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
- package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
- package/dist/src/council/__tests__/research-tools.test.js +4 -0
- package/dist/src/council/__tests__/round-tools.test.js +6 -0
- package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
- package/dist/src/council/clarifier-question-cap.test.js +73 -0
- package/dist/src/council/clarifier.d.ts +29 -1
- package/dist/src/council/clarifier.js +64 -1
- package/dist/src/council/debate-planner.d.ts +2 -1
- package/dist/src/council/debate-planner.js +5 -3
- package/dist/src/council/debate.d.ts +9 -0
- package/dist/src/council/debate.js +88 -8
- package/dist/src/council/index.d.ts +46 -1
- package/dist/src/council/index.js +125 -19
- package/dist/src/council/llm.js +41 -13
- package/dist/src/council/prompts.js +56 -21
- package/dist/src/council/types.d.ts +14 -1
- package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
- package/dist/src/ee/bridge.d.ts +3 -1
- package/dist/src/ee/bridge.js +9 -3
- package/dist/src/ee/bridge.test.js +1 -2
- package/dist/src/ee/client.js +15 -2
- package/dist/src/ee/recall-ledger.d.ts +20 -0
- package/dist/src/ee/recall-ledger.js +40 -1
- package/dist/src/ee/search.d.ts +25 -0
- package/dist/src/ee/search.js +73 -0
- package/dist/src/ee/types.d.ts +15 -1
- package/dist/src/ee/who-am-i.d.ts +41 -0
- package/dist/src/ee/who-am-i.js +143 -0
- package/dist/src/ee/who-am-i.test.d.ts +1 -0
- package/dist/src/ee/who-am-i.test.js +101 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/council-answers.js +1 -0
- package/dist/src/headless/output.d.ts +30 -1
- package/dist/src/headless/output.js +180 -0
- package/dist/src/headless/output.test.js +61 -1
- package/dist/src/hooks/index.js +48 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +143 -104
- package/dist/src/lsp/client-normalize.test.d.ts +12 -0
- package/dist/src/lsp/client-normalize.test.js +34 -0
- package/dist/src/lsp/client.d.ts +1 -0
- package/dist/src/lsp/client.js +10 -2
- package/dist/src/lsp/manager.d.ts +1 -1
- package/dist/src/lsp/manager.js +2 -2
- package/dist/src/lsp/manager.test.js +2 -2
- package/dist/src/lsp/runtime.d.ts +1 -1
- package/dist/src/lsp/runtime.js +2 -2
- package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
- package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
- package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
- package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
- package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
- package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
- package/dist/src/mcp/client-pool.d.ts +1 -1
- package/dist/src/mcp/client-pool.js +25 -9
- package/dist/src/mcp/ee-tools.d.ts +10 -0
- package/dist/src/mcp/ee-tools.js +34 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
- package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/agent.test.js +5 -2
- package/dist/src/orchestrator/auto-commit.d.ts +110 -0
- package/dist/src/orchestrator/auto-commit.js +364 -0
- package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
- package/dist/src/orchestrator/batch-turn-runner.js +2 -1
- package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
- package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
- package/dist/src/orchestrator/compaction.d.ts +22 -0
- package/dist/src/orchestrator/compaction.js +42 -0
- package/dist/src/orchestrator/delegations.d.ts +1 -0
- package/dist/src/orchestrator/delegations.js +41 -0
- package/dist/src/orchestrator/error-utils.js +27 -2
- package/dist/src/orchestrator/message-processor.d.ts +21 -5
- package/dist/src/orchestrator/message-processor.js +219 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +8 -2
- package/dist/src/orchestrator/orchestrator.js +187 -34
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +145 -0
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +76 -60
- package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
- package/dist/src/orchestrator/safety-askcard.js +74 -0
- package/dist/src/orchestrator/sandbox.test.js +8 -5
- package/dist/src/orchestrator/scope-ceiling.js +1 -1
- package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
- package/dist/src/orchestrator/stall-watchdog.js +23 -0
- package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
- package/dist/src/orchestrator/stream-runner.js +52 -30
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
- package/dist/src/orchestrator/subagent-compactor.js +83 -10
- package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
- package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
- package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
- package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
- package/dist/src/orchestrator/tool-args-repair.js +1 -1
- package/dist/src/orchestrator/tool-engine.d.ts +151 -0
- package/dist/src/orchestrator/tool-engine.js +3079 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
- package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
- package/dist/src/pil/__tests__/discovery.test.js +89 -145
- package/dist/src/pil/__tests__/dual-run.test.js +2 -0
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
- package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
- package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
- package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
- package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
- package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
- package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
- package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
- package/dist/src/pil/__tests__/pipeline.test.js +11 -8
- package/dist/src/pil/__tests__/response-tools.test.js +52 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +5 -3
- package/dist/src/pil/agent-operating-contract.test.js +2 -2
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +1 -1
- package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
- package/dist/src/pil/cheap-model-workbooks.js +1 -1
- package/dist/src/pil/config.d.ts +20 -0
- package/dist/src/pil/config.js +35 -0
- package/dist/src/pil/discovery-types.d.ts +32 -9
- package/dist/src/pil/discovery.d.ts +3 -3
- package/dist/src/pil/discovery.js +138 -244
- package/dist/src/pil/index.d.ts +1 -1
- package/dist/src/pil/index.js +1 -1
- package/dist/src/pil/layer1-intent.d.ts +9 -1
- package/dist/src/pil/layer1-intent.js +99 -44
- package/dist/src/pil/layer1-intent.test.js +112 -2
- package/dist/src/pil/layer15-context-scan.js +1 -1
- package/dist/src/pil/layer16-clarity.d.ts +21 -25
- package/dist/src/pil/layer16-clarity.js +52 -88
- package/dist/src/pil/layer18-acceptance.d.ts +13 -3
- package/dist/src/pil/layer18-acceptance.js +1 -75
- package/dist/src/pil/layer2-personality.js +7 -8
- package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
- package/dist/src/pil/layer2_5-ponytail.js +41 -0
- package/dist/src/pil/layer3-ee-injection.js +180 -14
- package/dist/src/pil/layer4-gsd.js +22 -8
- package/dist/src/pil/layer5-context.d.ts +1 -0
- package/dist/src/pil/layer5-context.js +26 -11
- package/dist/src/pil/layer6-output.js +23 -5
- package/dist/src/pil/llm-classify.js +3 -3
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +4 -3
- package/dist/src/pil/pipeline.js +15 -1
- package/dist/src/pil/response-tools.d.ts +16 -0
- package/dist/src/pil/response-tools.js +41 -0
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +24 -3
- package/dist/src/pil/task-tier-map.js +7 -7
- package/dist/src/pil/types.d.ts +9 -3
- package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
- package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
- package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
- package/dist/src/product-loop/discovery-interview.d.ts +20 -0
- package/dist/src/product-loop/discovery-interview.js +121 -7
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +20 -20
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/product-loop/sprint-self-verify.js +2 -2
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
- package/dist/src/providers/auth/gcloud.d.ts +28 -0
- package/dist/src/providers/auth/gcloud.js +102 -0
- package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
- package/dist/src/providers/auth/gemini-oauth.js +292 -105
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
- package/dist/src/providers/auth/grok-oauth.js +1 -1
- package/dist/src/providers/auth/registry.js +28 -6
- package/dist/src/providers/gemini.d.ts +1 -1
- package/dist/src/providers/gemini.js +10 -3
- package/dist/src/providers/keychain.d.ts +2 -2
- package/dist/src/providers/keychain.js +20 -2
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/providers/openai.js +5 -1
- package/dist/src/providers/runtime.d.ts +1 -1
- package/dist/src/providers/runtime.js +11 -2
- package/dist/src/providers/siliconflow-sse-repair.js +1 -1
- package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
- package/dist/src/providers/strategies/google.strategy.js +159 -9
- package/dist/src/providers/strategies/thinking-mode.js +19 -6
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/router/decide.test.js +40 -8
- package/dist/src/router/step-router.d.ts +20 -0
- package/dist/src/router/step-router.js +73 -0
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +19 -19
- package/dist/src/self-qa/orchestrator.js +5 -5
- package/dist/src/self-qa/scenario-planner.js +1 -1
- package/dist/src/self-qa/spec-emitter.js +4 -4
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.d.ts +6 -0
- package/dist/src/storage/sessions.js +49 -42
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +1 -0
- package/dist/src/storage/transcript.js +55 -0
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.js +6 -0
- package/dist/src/tools/bash.d.ts +36 -23
- package/dist/src/tools/bash.js +59 -301
- package/dist/src/tools/bash.test.js +2 -257
- package/dist/src/tools/file-tracker.d.ts +5 -0
- package/dist/src/tools/file-tracker.js +14 -0
- package/dist/src/tools/file.js +2 -0
- package/dist/src/tools/git-safety.d.ts +38 -1
- package/dist/src/tools/git-safety.js +94 -4
- package/dist/src/tools/git-safety.test.js +45 -1
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +56 -0
- package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
- package/dist/src/tools/registry-git-safety.test.js +17 -6
- package/dist/src/tools/registry.d.ts +5 -0
- package/dist/src/tools/registry.js +185 -11
- package/dist/src/types/index.d.ts +7 -1
- package/dist/src/ui/app.d.ts +2 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.js +37 -3
- package/dist/src/ui/components/structured-response-view.js +7 -0
- package/dist/src/ui/components/task-list-panel.js +3 -12
- package/dist/src/ui/containers/chat-feed.d.ts +40 -0
- package/dist/src/ui/containers/chat-feed.js +66 -0
- package/dist/src/ui/containers/modals-layer.d.ts +87 -0
- package/dist/src/ui/containers/modals-layer.js +18 -0
- package/dist/src/ui/modals/session-picker-modal.js +14 -9
- package/dist/src/ui/modals/update-modal.js +2 -1
- package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +16 -1
- package/dist/src/ui/slash/menu-items.js +1 -0
- package/dist/src/ui/slash/ponytail.d.ts +2 -0
- package/dist/src/ui/slash/ponytail.js +19 -0
- package/dist/src/ui/status-bar/store.js +0 -1
- package/dist/src/ui/status-bar/store.test.js +2 -2
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.d.ts +17 -0
- package/dist/src/ui/utils/relaunch.js +28 -4
- package/dist/src/ui/utils/relaunch.test.js +50 -0
- package/dist/src/ui/utils/text.d.ts +11 -0
- package/dist/src/ui/utils/text.js +21 -1
- package/dist/src/ui/utils/text.test.js +25 -1
- package/dist/src/ui/utils/tools.js +3 -1
- package/dist/src/usage/cost-log.js +2 -2
- package/dist/src/usage/estimator.d.ts +14 -0
- package/dist/src/usage/estimator.js +22 -0
- package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/permission-mode.d.ts +33 -0
- package/dist/src/utils/permission-mode.js +164 -3
- package/dist/src/utils/permission-mode.test.js +92 -1
- package/dist/src/utils/settings.d.ts +27 -31
- package/dist/src/utils/settings.js +100 -109
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/package.json +1 -1
|
@@ -7,17 +7,17 @@ import { loadCustomInstructions } from "../utils/instructions.js";
|
|
|
7
7
|
import { loadValidSubAgents, } from "../utils/settings.js";
|
|
8
8
|
import { resolveShell } from "../utils/shell.js";
|
|
9
9
|
import { discoverSkills, formatSkillsForPrompt } from "../utils/skills.js";
|
|
10
|
-
// F3 — hard cap on tool rounds per user turn. Default reduced
|
|
10
|
+
// F3 — hard cap on tool rounds per user turn. Default reduced 400 → 100
|
|
11
11
|
// after session bca83bcbaad1 logged 178 tool calls in a single turn while
|
|
12
|
-
// monotonically growing billed input. Env override allowed range 10..
|
|
12
|
+
// monotonically growing billed input. Env override allowed range 10..400.
|
|
13
13
|
function readMaxToolRoundsFromEnv() {
|
|
14
14
|
const raw = process.env.MUONROI_MAX_TOOL_ROUNDS;
|
|
15
15
|
if (!raw)
|
|
16
|
-
return
|
|
16
|
+
return 100;
|
|
17
17
|
const n = Number(raw);
|
|
18
18
|
if (!Number.isFinite(n))
|
|
19
|
-
return
|
|
20
|
-
return Math.max(10, Math.min(
|
|
19
|
+
return 100;
|
|
20
|
+
return Math.max(10, Math.min(400, Math.floor(n)));
|
|
21
21
|
}
|
|
22
22
|
export const MAX_TOOL_ROUNDS = readMaxToolRoundsFromEnv();
|
|
23
23
|
export const VISION_MODEL = "grok-4-1-fast-reasoning";
|
|
@@ -143,6 +143,7 @@ function buildEnvironmentBlock() {
|
|
|
143
143
|
if (platform === "win32") {
|
|
144
144
|
shellRules.push('- When a Windows-native command is genuinely needed, invoke it explicitly: `cmd.exe /c "command"` or `powershell -NoProfile -Command "command"`.');
|
|
145
145
|
}
|
|
146
|
+
shellRules.push("- CRITICAL: Batch independent commands in ONE call with `&&` or `;` instead of N sequential calls — each extra call adds ~500 tokens of overhead and prevents cross-request cache reuse. Examples: `ls && cat file` or `a; b; c; d` instead of four separate bash calls.");
|
|
146
147
|
}
|
|
147
148
|
else if (shell.kind === "powershell") {
|
|
148
149
|
shellRules.push("- The bash tool runs PowerShell. Use PowerShell cmdlets: Get-ChildItem, Select-String, Measure-Object, ConvertTo-Json, $env:VAR.", "- DO NOT use POSIX-only commands: grep, sed, awk, wc (use Select-String / Measure-Object / -split instead).", "- For pipe redirection, use PowerShell syntax: `cmd | Select-Object -First 10`, not `cmd | head -10`.");
|
|
@@ -272,11 +273,28 @@ IMPORTANT:
|
|
|
272
273
|
- Use write_file only for new files or when most of the file is changing. For very large files (>500 lines), split into multiple edit_file calls or write smaller chunks.
|
|
273
274
|
- Use read_file instead of cat/head/tail for reading files.
|
|
274
275
|
- When the user asks for an automated recurring or one-time run, use the schedule tools instead of only describing the setup.
|
|
275
|
-
-
|
|
276
|
-
-
|
|
276
|
+
- If you have worked for a long time or hit a tool execution limit, DO NOT tell the user to move to a new session. Instead, advise them to run the \`/compact\` command to free up memory before continuing.
|
|
277
|
+
- Use the experience brain actively (it is how you stop repeating mistakes across sessions): at the start of an unfamiliar or risky step call ee_query to recall past lessons, and after acting on a recalled \`[id col]\` rate it with ee_feedback. The MOMENT you hit a mistake / error / dead-end and find the working fix, call ee_write to save the lesson (the pitfall AND the fix, concise and generalizable) — it is embedded immediately and recallable via ee_query in this and future sessions. Saving a hard-won fix is part of doing the work, not optional.
|
|
278
|
+
- Commit your own work as you go (in any git repo, without being asked): use the git_commit tool — YOU write the commit message — the moment a cohesive, working chunk passes its checks, and after EACH step of a multi-step plan. Prefer several small, logically-scoped commits with clear messages (describe WHAT changed) over one catch-all at the end. git_commit stages only the files you wrote, excludes secrets/artifacts, and appends the "Coding by - Muonroi-CLI" attribution for you. (Any commit you instead make by hand via bash must still end with that attribution line, verbatim, on its own final line.)
|
|
277
279
|
- After creating a recurring schedule, check the daemon status and start it with \`schedule_daemon_start\` if needed.
|
|
278
280
|
|
|
279
|
-
|
|
281
|
+
|
|
282
|
+
Be direct. Execute, don't just describe. Show results, not plans.
|
|
283
|
+
|
|
284
|
+
TOKEN BUDGET:
|
|
285
|
+
- Each tool round sends ~17K system prompt tokens + accumulated tool results to the model.
|
|
286
|
+
- Task(explore) / task(general) isolates context in a sub-agent — much cheaper than 5+ top-level rounds.
|
|
287
|
+
- Consider: 1-2 rounds → direct; 3-5 rounds → consider task(explore); >5 rounds → should use task(explore).
|
|
288
|
+
WORKFLOW RULES:
|
|
289
|
+
- RESEARCH FIRST: Always prioritize research before proposing edits. DeepSeek and other models have knowledge cutoffs; do not assume you know the exact codebase structure or latest external libraries. Use 'grep', 'lsp', and 'read_file' to search the local codebase. Use MCP tools (like web search or documentation readers) to research external knowledge, APIs, or libraries. Use 'delegate' for deep background research. Read before you write.
|
|
290
|
+
- CLARIFY GRAY AREAS: If the user's request is ambiguous or leaves critical design decisions unspecified, STOP and ask the user for clarification before writing code. Do not hallucinate requirements.
|
|
291
|
+
|
|
292
|
+
SELF-LIMIT:
|
|
293
|
+
- When you've read 5+ files and haven't concluded, summarize findings and propose next step instead of reading more.
|
|
294
|
+
- BATCH TOOL CALLS: You MUST combine and invoke independent tool calls in parallel (e.g. read multiple files, or run grep and read a file concurrently) in a SINGLE turn. Do not wait for the result of one tool call before invoking another if you already know both are needed. This dramatically reduces conversation turns, roundtrip latency, and input token accumulation.
|
|
295
|
+
- BATCH BASH COMMANDS: Combine independent commands into ONE bash call (a; b; c) rather than sequential single calls — each separate call adds ~500 tokens of overhead and prevents prompt-cache reuse across the session.
|
|
296
|
+
- Read only specific file sections (start_line/end_line) instead of whole files.
|
|
297
|
+
- When a clear direction emerges from the first 2-3 tool results, act on it — don't over-investigate.`,
|
|
280
298
|
plan: `You are muonroi-cli in Plan mode — you analyze and plan but DO NOT execute changes.
|
|
281
299
|
|
|
282
300
|
${ENVIRONMENT}
|
|
@@ -386,40 +404,60 @@ export function buildMcpCapabilityBlock(toolNames) {
|
|
|
386
404
|
"to bash or hand-write JSON-RPC to reach an MCP server:\n" +
|
|
387
405
|
lines.join("\n"));
|
|
388
406
|
}
|
|
389
|
-
|
|
390
|
-
|
|
407
|
+
const _staticPrefixCache = new Map();
|
|
408
|
+
const STATIC_PREFIX_CACHE_TTL_MS = 300_000; // 5 min — ample; inputs are session-stable
|
|
409
|
+
function staticPrefixCacheKey(cwd, mode, providerId, isChitchat, subagentsHash, subAgent = false) {
|
|
410
|
+
return `${cwd}|${mode}|${providerId}|${isChitchat}|${subagentsHash}|${subAgent}`;
|
|
411
|
+
}
|
|
412
|
+
function computeStaticPrefix(cwd, mode, subagents, providerId, chitchat, subAgent = false) {
|
|
391
413
|
const custom = loadCustomInstructions(cwd);
|
|
392
|
-
const customSection = custom
|
|
393
|
-
?
|
|
394
|
-
:
|
|
395
|
-
|
|
396
|
-
// routing context the model doesn't need to answer "Hi". On this codebase
|
|
397
|
-
// they account for ~3K tokens per call.
|
|
398
|
-
const skillsText = chitchat ? "" : formatSkillsForPrompt(discoverSkills(cwd));
|
|
414
|
+
const customSection = subAgent || !custom
|
|
415
|
+
? ""
|
|
416
|
+
: `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n`;
|
|
417
|
+
const skillsText = chitchat || subAgent ? "" : formatSkillsForPrompt(discoverSkills(cwd));
|
|
399
418
|
const skillsSection = skillsText ? `\n\n${skillsText}\n` : "";
|
|
400
419
|
const subagentsSection = chitchat ? "" : formatCustomSubagentsPromptSection(subagents ?? loadValidSubAgents());
|
|
401
|
-
const sandboxSection = formatSandboxPromptSection(sandboxMode, sandboxSettings);
|
|
402
420
|
let modePrompt = MODE_PROMPTS[mode];
|
|
403
|
-
// Phase 12.2-G5: defer the "is this an anthropic-style prompt or not?"
|
|
404
|
-
// decision to the provider capability. Default (no providerId) keeps the
|
|
405
|
-
// pre-G5 behaviour by treating the prompt as anthropic-native.
|
|
406
421
|
if (!providerId)
|
|
407
422
|
throw new Error("providerId is required to build system prompt — cannot determine prompt style.");
|
|
408
423
|
const promptStyle = getProviderCapabilities(providerId).systemPromptStyle();
|
|
409
424
|
if (promptStyle !== "anthropic") {
|
|
410
425
|
modePrompt = stripToolsSection(modePrompt) + NON_ANTHROPIC_TOOL_PREAMBLE;
|
|
411
426
|
}
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
|
|
415
|
-
|
|
427
|
+
// Agent mode: strip tool descriptions for tools rarely needed in coding tasks
|
|
428
|
+
// to reduce system-prompt bloat. The tools remain available via API.
|
|
429
|
+
if (mode === "agent") {
|
|
430
|
+
modePrompt = modePrompt.replace(/\n- (wallet_|paid_|fetch_payment|schedule_|generate_|computer_|search_x).*/g, "");
|
|
431
|
+
}
|
|
416
432
|
const contractSection = buildContractSection({ chitchat });
|
|
417
|
-
// Native capability manifest (agent mode only) — gives the in-CLI agent a
|
|
418
|
-
// self-model of the tools, sub-agents, and CLI subsystems it has, so it
|
|
419
|
-
// wields them instead of reconstructing its own runtime by grepping source.
|
|
420
|
-
// See native-capabilities-workbook.ts (session d95113d3be09 motivation).
|
|
421
433
|
const nativeCapabilitiesSection = buildNativeCapabilitiesSection({ mode, chitchat });
|
|
422
|
-
const
|
|
434
|
+
const prefix = `${contractSection}${nativeCapabilitiesSection}${modePrompt}${customSection}${skillsSection}${subagentsSection}`;
|
|
435
|
+
return { prefix };
|
|
436
|
+
}
|
|
437
|
+
export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options) {
|
|
438
|
+
const chitchat = options?.chitchat === true;
|
|
439
|
+
const subAgent = options?.subAgent ?? false;
|
|
440
|
+
const pid = providerId ?? "default";
|
|
441
|
+
// Subagents rarely change mid-session, but when they do we need a cache miss.
|
|
442
|
+
// JSON-stable stringify is fast for typical configs (< 10 entries, no circular refs).
|
|
443
|
+
const subagentsHash = subagents ? JSON.stringify(subagents) : "none";
|
|
444
|
+
// Try cache for the static prefix
|
|
445
|
+
const key = staticPrefixCacheKey(cwd, mode, pid, chitchat, subagentsHash, subAgent);
|
|
446
|
+
const now = Date.now();
|
|
447
|
+
const cached = _staticPrefixCache.get(key);
|
|
448
|
+
let staticPrefix;
|
|
449
|
+
if (cached && now - cached.cachedAt < STATIC_PREFIX_CACHE_TTL_MS) {
|
|
450
|
+
staticPrefix = cached.prefix;
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
// Cache miss — compute and store
|
|
454
|
+
const result = computeStaticPrefix(cwd, mode, subagents, pid, chitchat, subAgent);
|
|
455
|
+
staticPrefix = result.prefix;
|
|
456
|
+
_staticPrefixCache.set(key, {
|
|
457
|
+
prefix: staticPrefix,
|
|
458
|
+
cachedAt: now,
|
|
459
|
+
});
|
|
460
|
+
}
|
|
423
461
|
const planSection = planContext
|
|
424
462
|
? `\n\nAPPROVED PLAN:\nThe following plan has been approved by the user. Execute it now.\n${planContext}\n`
|
|
425
463
|
: "";
|
|
@@ -429,6 +467,10 @@ export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, suba
|
|
|
429
467
|
const dynamicSuffix = `${planSection}${resumeSection}\n\nCurrent working directory: ${cwd}`;
|
|
430
468
|
return { staticPrefix, dynamicSuffix };
|
|
431
469
|
}
|
|
470
|
+
/** Reset the static prefix cache (for tests). */
|
|
471
|
+
export function resetStaticPrefixCache() {
|
|
472
|
+
_staticPrefixCache.clear();
|
|
473
|
+
}
|
|
432
474
|
export function buildSystemPrompt(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options) {
|
|
433
475
|
const { staticPrefix, dynamicSuffix } = buildSystemPromptParts(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options);
|
|
434
476
|
return `${staticPrefix}${dynamicSuffix}`;
|
|
@@ -556,39 +598,13 @@ export function buildSubagentPrompt(request, cwd, custom, sandboxMode, subagents
|
|
|
556
598
|
"",
|
|
557
599
|
`Delegated task: ${request.description}`,
|
|
558
600
|
"",
|
|
559
|
-
buildSystemPrompt(cwd, mode, sandboxMode, undefined, subagents, sandboxSettings, providerId
|
|
601
|
+
buildSystemPrompt(cwd, mode, sandboxMode, undefined, subagents, sandboxSettings, providerId, undefined, {
|
|
602
|
+
subAgent: true,
|
|
603
|
+
}),
|
|
560
604
|
].join("\n");
|
|
561
605
|
}
|
|
562
606
|
export function formatSandboxPromptSection(sandboxMode, settings) {
|
|
563
|
-
|
|
564
|
-
return "";
|
|
565
|
-
const s = settings ?? {};
|
|
566
|
-
let networkLine;
|
|
567
|
-
if (s.allowNet) {
|
|
568
|
-
networkLine = s.allowedHosts?.length
|
|
569
|
-
? `- Network access is restricted to: ${s.allowedHosts.join(", ")}.`
|
|
570
|
-
: "- Network access is enabled.";
|
|
571
|
-
}
|
|
572
|
-
else {
|
|
573
|
-
networkLine = "- Network is disabled.";
|
|
574
|
-
}
|
|
575
|
-
const lines = [
|
|
576
|
-
"",
|
|
577
|
-
"SANDBOX MODE:",
|
|
578
|
-
"- Bash commands run inside a Shuru sandbox.",
|
|
579
|
-
networkLine,
|
|
580
|
-
"- The current workspace is mounted inside the sandbox at `/workspace`.",
|
|
581
|
-
"- Shell-side workspace file changes do not persist back to the host in this version.",
|
|
582
|
-
"- Use `read_file`, `edit_file`, and `write_file` for durable source edits.",
|
|
583
|
-
"- If a task needs a host-persistent shell mutation, explain that sandbox mode blocks that workflow and ask whether to disable sandbox mode.",
|
|
584
|
-
];
|
|
585
|
-
if (s.ports?.length) {
|
|
586
|
-
lines.push(`- Port forwards: ${s.ports.join(", ")}.`);
|
|
587
|
-
}
|
|
588
|
-
if (s.from) {
|
|
589
|
-
lines.push(`- Starting from checkpoint: ${s.from}.`);
|
|
590
|
-
}
|
|
591
|
-
return lines.join("\n");
|
|
607
|
+
return "";
|
|
592
608
|
}
|
|
593
609
|
export function applyModelConstraints(system, modelId) {
|
|
594
610
|
const modelInfo = getModelInfo(modelId);
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/orchestrator/safety-askcard.ts
|
|
3
|
+
*
|
|
4
|
+
* Plans an askcard layout when a tool call is blocked by the safety filter
|
|
5
|
+
* (catastrophic, dangerous, git-safety, empty-bash).
|
|
6
|
+
*
|
|
7
|
+
* The askcard gives the user a choice: allow the block once, or confirm the block.
|
|
8
|
+
*
|
|
9
|
+
* Pure — no React, no side effects. Unit-testable in isolation.
|
|
10
|
+
*/
|
|
11
|
+
export type SafetyBlockKind = "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
|
|
12
|
+
export interface SafetyBlockInfo {
|
|
13
|
+
kind: SafetyBlockKind;
|
|
14
|
+
/** The blocked command or tool call description */
|
|
15
|
+
blockedItem: string;
|
|
16
|
+
/** Human-readable reason from the filter */
|
|
17
|
+
reason: string;
|
|
18
|
+
/** Suggested "safe" alternative, if any */
|
|
19
|
+
alternative?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface SafetyAskcardLayout {
|
|
22
|
+
/** Question shown in the dialog title */
|
|
23
|
+
question: string;
|
|
24
|
+
/** Detail text shown below the question */
|
|
25
|
+
detail: string;
|
|
26
|
+
/** Options in render order. First = default (Enter applies). */
|
|
27
|
+
options: SafetyAskcardOption[];
|
|
28
|
+
/** Index of the default-selected option (0 = first). */
|
|
29
|
+
defaultIndex: number;
|
|
30
|
+
}
|
|
31
|
+
export interface SafetyAskcardOption {
|
|
32
|
+
label: string;
|
|
33
|
+
value: SafetyAskResult;
|
|
34
|
+
description?: string;
|
|
35
|
+
}
|
|
36
|
+
export type SafetyAskResult = "allow-once" | "allow-session" | "block";
|
|
37
|
+
/**
|
|
38
|
+
* Plan the askcard layout for a safety block.
|
|
39
|
+
* Pure function — easy to unit test and swap UI strategies.
|
|
40
|
+
*/
|
|
41
|
+
export declare function planSafetyAskcard(info: SafetyBlockInfo): SafetyAskcardLayout;
|
|
42
|
+
/** Info passed to the safety-override handler when a tool block is detected. */
|
|
43
|
+
export interface SafetyOverrideAskInfo {
|
|
44
|
+
kind: SafetyBlockKind;
|
|
45
|
+
/** The blocked tool name (e.g. "bash", "edit_file"). */
|
|
46
|
+
toolName: string;
|
|
47
|
+
/** The command/args that were blocked. */
|
|
48
|
+
blockedItem: string;
|
|
49
|
+
/** Human-readable reason from the filter. */
|
|
50
|
+
reason: string;
|
|
51
|
+
/** Source block kind tag for the agent to decide retry. */
|
|
52
|
+
source: "bash.execute" | "registry.precheck";
|
|
53
|
+
}
|
|
54
|
+
/** Verdict the UI returns after the user answers the askcard. */
|
|
55
|
+
export type SafetyOverrideVerdict = {
|
|
56
|
+
action: "allow-once";
|
|
57
|
+
} | {
|
|
58
|
+
action: "allow-session";
|
|
59
|
+
} | {
|
|
60
|
+
action: "block";
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Build a safe alternative suggestion for a blocked command.
|
|
64
|
+
* Returns null when no canned alternative exists.
|
|
65
|
+
*/
|
|
66
|
+
export declare function suggestAlternative(kind: SafetyBlockKind, command: string): string | undefined;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/orchestrator/safety-askcard.ts
|
|
3
|
+
*
|
|
4
|
+
* Plans an askcard layout when a tool call is blocked by the safety filter
|
|
5
|
+
* (catastrophic, dangerous, git-safety, empty-bash).
|
|
6
|
+
*
|
|
7
|
+
* The askcard gives the user a choice: allow the block once, or confirm the block.
|
|
8
|
+
*
|
|
9
|
+
* Pure — no React, no side effects. Unit-testable in isolation.
|
|
10
|
+
*/
|
|
11
|
+
const CATEGORY_LABELS = {
|
|
12
|
+
catastrophic: "Lỗi bảo mật nghiêm trọng",
|
|
13
|
+
dangerous: "Lệnh nguy hiểm",
|
|
14
|
+
"git-safety": "Git safety gate",
|
|
15
|
+
"empty-bash": "Bash call trống",
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Plan the askcard layout for a safety block.
|
|
19
|
+
* Pure function — easy to unit test and swap UI strategies.
|
|
20
|
+
*/
|
|
21
|
+
export function planSafetyAskcard(info) {
|
|
22
|
+
const kindLabel = CATEGORY_LABELS[info.kind];
|
|
23
|
+
const cmdPreview = info.blockedItem.length > 120 ? `${info.blockedItem.slice(0, 117)}...` : info.blockedItem;
|
|
24
|
+
const question = `⚠️ ${kindLabel}: Cho phép thực thi?`;
|
|
25
|
+
const detailLines = [`Lệnh bị chặn: \`${cmdPreview}\``, `Lý do: ${info.reason}`];
|
|
26
|
+
if (info.alternative) {
|
|
27
|
+
detailLines.push(`Gợi ý thay thế: ${info.alternative}`);
|
|
28
|
+
}
|
|
29
|
+
const options = [
|
|
30
|
+
{
|
|
31
|
+
label: "Cho phép 1 lần (Allow once)",
|
|
32
|
+
value: "allow-once",
|
|
33
|
+
description: "Chỉ chạy lệnh này một lần duy nhất",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
label: "Chặn (Block)",
|
|
37
|
+
value: "block",
|
|
38
|
+
description: "Không chạy lệnh này, trả về lỗi cho agent",
|
|
39
|
+
},
|
|
40
|
+
];
|
|
41
|
+
// Allow-session only for less severe blocks
|
|
42
|
+
if (info.kind !== "catastrophic") {
|
|
43
|
+
options.splice(1, 0, {
|
|
44
|
+
label: "Cho phép cả phiên (Allow session)",
|
|
45
|
+
value: "allow-session",
|
|
46
|
+
description: "Cho phép tất cả lệnh tương tự trong phiên này",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
question,
|
|
51
|
+
detail: detailLines.join("\n"),
|
|
52
|
+
options,
|
|
53
|
+
defaultIndex: options.findIndex((o) => o.value === "block"),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Build a safe alternative suggestion for a blocked command.
|
|
58
|
+
* Returns null when no canned alternative exists.
|
|
59
|
+
*/
|
|
60
|
+
export function suggestAlternative(kind, command) {
|
|
61
|
+
if (kind === "empty-bash") {
|
|
62
|
+
return 'Provide a real command, e.g. {"command":"ls -la"}';
|
|
63
|
+
}
|
|
64
|
+
// sqlite3 / .db access on Windows
|
|
65
|
+
if (/sqlite3|\.db\b/i.test(command)) {
|
|
66
|
+
return "Use the filesystem MCP or write_file to read the DB file instead of shelling sqlite3";
|
|
67
|
+
}
|
|
68
|
+
// rm -rf patterns
|
|
69
|
+
if (/rm\s+-rf/i.test(command)) {
|
|
70
|
+
return "Explicitly list files to remove, or use move to trash";
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=safety-askcard.js.map
|
|
@@ -39,21 +39,23 @@ afterEach(() => {
|
|
|
39
39
|
vi.doUnmock("../storage/index.js");
|
|
40
40
|
});
|
|
41
41
|
describe("Agent sandbox mode", { timeout: 30_000 }, () => {
|
|
42
|
-
it("can switch sandbox mode at runtime", async () => {
|
|
42
|
+
it("can switch sandbox mode at runtime — always 'off' (sandbox removed)", async () => {
|
|
43
43
|
const { Agent } = await importAgentModule();
|
|
44
44
|
const agent = new Agent(undefined, undefined, undefined, undefined, {
|
|
45
45
|
persistSession: false,
|
|
46
46
|
sandboxMode: "off",
|
|
47
47
|
});
|
|
48
48
|
expect(agent.getSandboxMode()).toBe("off");
|
|
49
|
+
// Sandbox has been removed: setSandboxMode is a no-op stub.
|
|
50
|
+
// The mode remains "off" regardless of what is passed.
|
|
49
51
|
agent.setSandboxMode("shuru");
|
|
50
|
-
expect(agent.getSandboxMode()).toBe("
|
|
52
|
+
expect(agent.getSandboxMode()).toBe("off");
|
|
51
53
|
});
|
|
52
|
-
it("passes sandbox mode into background delegations", async () => {
|
|
54
|
+
it("passes sandbox mode into background delegations — always 'off' (sandbox removed)", async () => {
|
|
53
55
|
const { Agent } = await importAgentModule();
|
|
54
56
|
const agent = new Agent(undefined, undefined, undefined, undefined, {
|
|
55
57
|
persistSession: false,
|
|
56
|
-
sandboxMode: "shuru",
|
|
58
|
+
sandboxMode: "shuru", // accepted for back-compat but ignored
|
|
57
59
|
});
|
|
58
60
|
const startMock = vi.fn(async () => ({ success: true, output: "ok" }));
|
|
59
61
|
agent.delegations.start = startMock;
|
|
@@ -62,7 +64,8 @@ describe("Agent sandbox mode", { timeout: 30_000 }, () => {
|
|
|
62
64
|
description: "Inspect",
|
|
63
65
|
prompt: "Look around",
|
|
64
66
|
});
|
|
65
|
-
|
|
67
|
+
// Sandbox removed: delegations always receive sandboxMode: "off".
|
|
68
|
+
expect(startMock).toHaveBeenCalledWith(expect.objectContaining({ agent: "explore" }), expect.objectContaining({ sandboxMode: "off" }));
|
|
66
69
|
});
|
|
67
70
|
it("can get and set sandbox settings", async () => {
|
|
68
71
|
const { Agent } = await importAgentModule();
|
|
@@ -80,7 +80,7 @@ export function parseBudgetOverride(raw) {
|
|
|
80
80
|
return { override: undefined, cleanedPrompt: raw };
|
|
81
81
|
// Strip the entire matched segment including its leading/trailing whitespace
|
|
82
82
|
// and re-collapse the surrounding spaces.
|
|
83
|
-
const cleaned =
|
|
83
|
+
const cleaned = `${raw.slice(0, match.index ?? 0)} ${raw.slice((match.index ?? 0) + match[0].length)}`
|
|
84
84
|
.replace(/\s+/g, " ")
|
|
85
85
|
.trim();
|
|
86
86
|
return { override: n, cleanedPrompt: cleaned };
|
|
@@ -57,6 +57,53 @@ export interface StallRepromptState {
|
|
|
57
57
|
* effects) so it is unit-testable in isolation from the orchestrator loop.
|
|
58
58
|
*/
|
|
59
59
|
export declare function shouldRepromptStall(s: StallRepromptState): boolean;
|
|
60
|
+
/** Inputs to the mid-loop stall continuation decision — see {@link shouldContinueAfterMidLoopStall}. */
|
|
61
|
+
export interface MidLoopStallState {
|
|
62
|
+
/** The watchdog fired for this attempt. */
|
|
63
|
+
stallTriggered: boolean;
|
|
64
|
+
/**
|
|
65
|
+
* Real content parts received across the WHOLE streamText attempt (all steps).
|
|
66
|
+
* `> 0` proves earlier tool steps already ran — so this is NOT a time-to-first-
|
|
67
|
+
* byte stall (that case is handled by {@link shouldRepromptStall}).
|
|
68
|
+
*/
|
|
69
|
+
chunksThisAttempt: number;
|
|
70
|
+
/**
|
|
71
|
+
* Real content parts received since the last step boundary (reset in
|
|
72
|
+
* `prepareStep`). `0` means the in-flight step's provider request produced no
|
|
73
|
+
* byte before the watchdog fired — a dead socket on a SINGLE step, with every
|
|
74
|
+
* prior step fully completed. Safe to continue: the completed steps'
|
|
75
|
+
* assistant+tool messages are appended to history before re-issuing, so no
|
|
76
|
+
* tool is re-run and no text is duplicated.
|
|
77
|
+
*/
|
|
78
|
+
chunksThisStep: number;
|
|
79
|
+
/** How many mid-loop continuations have already happened this turn. */
|
|
80
|
+
retryCount: number;
|
|
81
|
+
/** Configured cap (getProviderStallRetries); 0 disables continuation. */
|
|
82
|
+
maxRetries: number;
|
|
83
|
+
/** True on genuine user cancel (never continue over a cancel). */
|
|
84
|
+
aborted: boolean;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Decide whether a fired stall watchdog should CONTINUE the turn (append the
|
|
88
|
+
* completed steps' messages, then re-issue streamText to resume from the
|
|
89
|
+
* stalled step) instead of falling through to the partial-answer rescue.
|
|
90
|
+
*
|
|
91
|
+
* This is the mid-loop counterpart to {@link shouldRepromptStall}. The TTFB
|
|
92
|
+
* re-prompt restarts the WHOLE request from the original prompt, so it is gated
|
|
93
|
+
* on `chunksThisAttempt === 0` to avoid re-running tools. Continuation instead
|
|
94
|
+
* preserves all completed steps in history (assistant tool-calls + their
|
|
95
|
+
* tool-results), so re-issuing cannot re-run a tool — making it safe even when
|
|
96
|
+
* earlier steps had side effects (writes, commits).
|
|
97
|
+
*
|
|
98
|
+
* Qualifies ONLY when: the watchdog fired, earlier steps ran
|
|
99
|
+
* (`chunksThisAttempt > 0`), the CURRENT step produced nothing
|
|
100
|
+
* (`chunksThisStep === 0` → a clean dead socket, no partial text to duplicate),
|
|
101
|
+
* under the retry cap, and not a user cancel. A step that emitted partial text
|
|
102
|
+
* then stalled (`chunksThisStep > 0`) falls through to rescue instead, since
|
|
103
|
+
* re-issuing would duplicate that partial output. Pure (no side effects) so it
|
|
104
|
+
* is unit-testable in isolation from the orchestrator loop.
|
|
105
|
+
*/
|
|
106
|
+
export declare function shouldContinueAfterMidLoopStall(s: MidLoopStallState): boolean;
|
|
60
107
|
/**
|
|
61
108
|
* Exponential backoff (ms, capped at 4s) before the Nth stall re-prompt
|
|
62
109
|
* (1-based): 500 → 1000 → 2000 → 4000 → 4000.
|
|
@@ -40,6 +40,29 @@ export function shouldRepromptStall(s) {
|
|
|
40
40
|
s.assistantTextEmpty &&
|
|
41
41
|
!s.aborted);
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Decide whether a fired stall watchdog should CONTINUE the turn (append the
|
|
45
|
+
* completed steps' messages, then re-issue streamText to resume from the
|
|
46
|
+
* stalled step) instead of falling through to the partial-answer rescue.
|
|
47
|
+
*
|
|
48
|
+
* This is the mid-loop counterpart to {@link shouldRepromptStall}. The TTFB
|
|
49
|
+
* re-prompt restarts the WHOLE request from the original prompt, so it is gated
|
|
50
|
+
* on `chunksThisAttempt === 0` to avoid re-running tools. Continuation instead
|
|
51
|
+
* preserves all completed steps in history (assistant tool-calls + their
|
|
52
|
+
* tool-results), so re-issuing cannot re-run a tool — making it safe even when
|
|
53
|
+
* earlier steps had side effects (writes, commits).
|
|
54
|
+
*
|
|
55
|
+
* Qualifies ONLY when: the watchdog fired, earlier steps ran
|
|
56
|
+
* (`chunksThisAttempt > 0`), the CURRENT step produced nothing
|
|
57
|
+
* (`chunksThisStep === 0` → a clean dead socket, no partial text to duplicate),
|
|
58
|
+
* under the retry cap, and not a user cancel. A step that emitted partial text
|
|
59
|
+
* then stalled (`chunksThisStep > 0`) falls through to rescue instead, since
|
|
60
|
+
* re-issuing would duplicate that partial output. Pure (no side effects) so it
|
|
61
|
+
* is unit-testable in isolation from the orchestrator loop.
|
|
62
|
+
*/
|
|
63
|
+
export function shouldContinueAfterMidLoopStall(s) {
|
|
64
|
+
return (s.stallTriggered && s.chunksThisAttempt > 0 && s.chunksThisStep === 0 && s.retryCount < s.maxRetries && !s.aborted);
|
|
65
|
+
}
|
|
43
66
|
/**
|
|
44
67
|
* Exponential backoff (ms, capped at 4s) before the Nth stall re-prompt
|
|
45
68
|
* (1-based): 500 → 1000 → 2000 → 4000 → 4000.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { createStallWatchdog, STALL_ABORT_REASON, shouldRepromptStall, stallRepromptBackoffMs, } from "./stall-watchdog.js";
|
|
2
|
+
import { createStallWatchdog, STALL_ABORT_REASON, shouldContinueAfterMidLoopStall, shouldRepromptStall, stallRepromptBackoffMs, } from "./stall-watchdog.js";
|
|
3
3
|
describe("createStallWatchdog", () => {
|
|
4
4
|
beforeEach(() => vi.useFakeTimers());
|
|
5
5
|
afterEach(() => vi.useRealTimers());
|
|
@@ -103,6 +103,41 @@ describe("shouldRepromptStall", () => {
|
|
|
103
103
|
expect(shouldRepromptStall(ttfb({ aborted: true }))).toBe(false);
|
|
104
104
|
});
|
|
105
105
|
});
|
|
106
|
+
describe("shouldContinueAfterMidLoopStall", () => {
|
|
107
|
+
// A clean mid-loop dead socket: watchdog fired AFTER earlier steps ran
|
|
108
|
+
// (chunksThisAttempt > 0), but the in-flight step produced nothing
|
|
109
|
+
// (chunksThisStep === 0). The ONLY shape that continues. Distinct from the
|
|
110
|
+
// TTFB re-prompt, which requires chunksThisAttempt === 0.
|
|
111
|
+
const midLoop = (over = {}) => ({
|
|
112
|
+
stallTriggered: true,
|
|
113
|
+
chunksThisAttempt: 132, // 66 prior steps' worth of chunks
|
|
114
|
+
chunksThisStep: 0,
|
|
115
|
+
retryCount: 0,
|
|
116
|
+
maxRetries: 1,
|
|
117
|
+
aborted: false,
|
|
118
|
+
...over,
|
|
119
|
+
});
|
|
120
|
+
it("continues a mid-loop dead-socket stall under the cap", () => {
|
|
121
|
+
expect(shouldContinueAfterMidLoopStall(midLoop())).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
it("does NOT continue when the watchdog never fired", () => {
|
|
124
|
+
expect(shouldContinueAfterMidLoopStall(midLoop({ stallTriggered: false }))).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
it("does NOT continue a time-to-first-byte stall (chunksThisAttempt === 0 → TTFB re-prompt owns it)", () => {
|
|
127
|
+
expect(shouldContinueAfterMidLoopStall(midLoop({ chunksThisAttempt: 0 }))).toBe(false);
|
|
128
|
+
});
|
|
129
|
+
it("does NOT continue when the stalled step already emitted output (would duplicate)", () => {
|
|
130
|
+
expect(shouldContinueAfterMidLoopStall(midLoop({ chunksThisStep: 3 }))).toBe(false);
|
|
131
|
+
});
|
|
132
|
+
it("does NOT continue once the retry cap is reached", () => {
|
|
133
|
+
expect(shouldContinueAfterMidLoopStall(midLoop({ retryCount: 1, maxRetries: 1 }))).toBe(false);
|
|
134
|
+
// maxRetries=0 means the feature is disabled — never continue.
|
|
135
|
+
expect(shouldContinueAfterMidLoopStall(midLoop({ retryCount: 0, maxRetries: 0 }))).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
it("does NOT continue over a genuine user cancel", () => {
|
|
138
|
+
expect(shouldContinueAfterMidLoopStall(midLoop({ aborted: true }))).toBe(false);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
106
141
|
describe("stallRepromptBackoffMs", () => {
|
|
107
142
|
it("grows exponentially and caps at 4s", () => {
|
|
108
143
|
expect(stallRepromptBackoffMs(1)).toBe(500);
|