muonroi-cli 1.7.2 → 1.8.1
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 +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 +143 -6
- 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 +7 -4
- 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 +36 -1
- 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/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -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 +59 -62
- 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 +13 -5
- package/dist/src/lsp/manager.test.js +41 -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/auto-setup.js +0 -8
- 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/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- 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 +3 -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 +2 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -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 +366 -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 +23 -5
- package/dist/src/orchestrator/message-processor.js +226 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +11 -2
- package/dist/src/orchestrator/orchestrator.js +530 -43
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +146 -0
- package/dist/src/orchestrator/prompts.d.ts +16 -0
- package/dist/src/orchestrator/prompts.js +130 -67
- 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-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
- package/dist/src/orchestrator/subagent-compactor.js +148 -24
- package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
- 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 +153 -0
- package/dist/src/orchestrator/tool-engine.js +3138 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -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__/llm-classify.test.js +79 -1
- 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 +62 -7
- 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.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +196 -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/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +2 -2
- 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/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/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/self-qa/agentic-loop.js +30 -21
- 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__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/sweep-stale-pending.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/migrations.js +13 -1
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +3 -0
- package/dist/src/storage/transcript.js +254 -73
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +22 -5
- 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 +66 -1
- 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 +6 -0
- package/dist/src/tools/registry.js +215 -13
- package/dist/src/types/index.d.ts +8 -2
- package/dist/src/ui/app.d.ts +1 -0
- 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/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/expand.js +14 -1
- 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/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.js +1 -1
- 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/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- 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 +36 -32
- package/dist/src/utils/settings.js +151 -123
- package/dist/src/utils/update-checker.test.js +5 -1
- package/package.json +1 -1
|
@@ -169,5 +169,34 @@ describe("StreamRunner — DI surface", () => {
|
|
|
169
169
|
const b = new StreamRunner(makeDeps());
|
|
170
170
|
expect(a).not.toBe(b);
|
|
171
171
|
});
|
|
172
|
+
describe("StreamRunner — maxSteps custom overrides", () => {
|
|
173
|
+
it("respects default maxSteps logic when request.maxToolRounds is undefined", async () => {
|
|
174
|
+
const deps = makeDeps({ getMaxToolRounds: () => 50 });
|
|
175
|
+
const runner = new StreamRunner(deps);
|
|
176
|
+
const outcome = await runner.setup({
|
|
177
|
+
agent: "explore",
|
|
178
|
+
description: "test",
|
|
179
|
+
prompt: "test",
|
|
180
|
+
});
|
|
181
|
+
expect(outcome.kind).toBe("prepared");
|
|
182
|
+
if (outcome.kind === "prepared") {
|
|
183
|
+
expect(outcome.prepared.maxSteps).toBe(100); // deps.getMaxToolRounds() * 2
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
it("uses request.maxToolRounds when it is explicitly defined", async () => {
|
|
187
|
+
const deps = makeDeps({ getMaxToolRounds: () => 50 });
|
|
188
|
+
const runner = new StreamRunner(deps);
|
|
189
|
+
const outcome = await runner.setup({
|
|
190
|
+
agent: "explore",
|
|
191
|
+
description: "test",
|
|
192
|
+
prompt: "test",
|
|
193
|
+
maxToolRounds: 75,
|
|
194
|
+
});
|
|
195
|
+
expect(outcome.kind).toBe("prepared");
|
|
196
|
+
if (outcome.kind === "prepared") {
|
|
197
|
+
expect(outcome.prepared.maxSteps).toBe(75);
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
});
|
|
172
201
|
});
|
|
173
202
|
//# sourceMappingURL=stream-runner.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { installMockModel, textOnlyStream } from "../../agent-harness/mock-model.js";
|
|
3
|
+
import { loadCatalog } from "../../models/registry.js";
|
|
4
|
+
import { Agent } from "../orchestrator.js";
|
|
5
|
+
// Mock generateText from 'ai'
|
|
6
|
+
const mockGenerateText = vi.fn().mockResolvedValue({ text: "Mocked parent advice response" });
|
|
7
|
+
vi.mock("ai", () => {
|
|
8
|
+
return {
|
|
9
|
+
generateText: (...args) => mockGenerateText(...args),
|
|
10
|
+
};
|
|
11
|
+
});
|
|
12
|
+
// 1. Mock classifySubSessionAction
|
|
13
|
+
const mockClassifySubSessionAction = vi.fn();
|
|
14
|
+
vi.mock("../../pil/llm-classify.js", () => {
|
|
15
|
+
return {
|
|
16
|
+
classifySubSessionAction: (...args) => mockClassifySubSessionAction.apply(null, args),
|
|
17
|
+
};
|
|
18
|
+
});
|
|
19
|
+
// Mock deliberateCompact
|
|
20
|
+
const mockDeliberateCompact = vi.fn().mockResolvedValue({
|
|
21
|
+
summary: "Compact summary",
|
|
22
|
+
tokensBeforeCompress: 100,
|
|
23
|
+
});
|
|
24
|
+
vi.mock("../../flow/compaction/index.js", () => {
|
|
25
|
+
return {
|
|
26
|
+
deliberateCompact: (...args) => mockDeliberateCompact.apply(null, args),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
// 2. Mock DB
|
|
30
|
+
const mockDb = {
|
|
31
|
+
prepare: vi.fn().mockReturnValue({
|
|
32
|
+
run: vi.fn().mockReturnValue({ changes: 1 }),
|
|
33
|
+
get: vi.fn(),
|
|
34
|
+
all: vi.fn().mockReturnValue([]),
|
|
35
|
+
}),
|
|
36
|
+
};
|
|
37
|
+
vi.mock("../../storage/db.js", () => {
|
|
38
|
+
return {
|
|
39
|
+
getDatabase: () => mockDb,
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
// 3. Mock Storage/Transcript module functions
|
|
43
|
+
const mockLoadLatestCompaction = vi.fn();
|
|
44
|
+
const mockGetNextMessageSequence = vi.fn(() => 1);
|
|
45
|
+
const mockAppendCompaction = vi.fn();
|
|
46
|
+
const mockLoadTranscriptState = vi.fn();
|
|
47
|
+
const mockAppendMessages = vi.fn((sessId, msgs) => msgs.map((_, idx) => idx + 100));
|
|
48
|
+
const mockMarkMessageCompleted = vi.fn();
|
|
49
|
+
vi.mock("../../storage/transcript.js", () => {
|
|
50
|
+
return {
|
|
51
|
+
loadLatestCompaction: (...args) => mockLoadLatestCompaction.apply(null, args),
|
|
52
|
+
getNextMessageSequence: (...args) => mockGetNextMessageSequence.apply(null, args),
|
|
53
|
+
appendCompaction: (...args) => mockAppendCompaction.apply(null, args),
|
|
54
|
+
loadTranscriptState: (...args) => mockLoadTranscriptState.apply(null, args),
|
|
55
|
+
appendMessages: (...args) => mockAppendMessages.apply(null, args),
|
|
56
|
+
markMessageCompleted: (...args) => mockMarkMessageCompleted.apply(null, args),
|
|
57
|
+
buildChatEntries: vi.fn(() => []),
|
|
58
|
+
getLastTodoWriteArgs: vi.fn(() => null),
|
|
59
|
+
loadTranscript: vi.fn(() => []),
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
// 4. Mock the main storage index barrel file
|
|
63
|
+
const parentSessionState = {
|
|
64
|
+
messages: [{ role: "user", content: "Hello parent" }],
|
|
65
|
+
seqs: [1],
|
|
66
|
+
};
|
|
67
|
+
vi.mock("../../storage/index.js", () => {
|
|
68
|
+
return {
|
|
69
|
+
appendCompaction: (...args) => mockAppendCompaction.apply(null, args),
|
|
70
|
+
appendMessages: (...args) => mockAppendMessages.apply(null, args),
|
|
71
|
+
getNextMessageSequence: (...args) => mockGetNextMessageSequence.apply(null, args),
|
|
72
|
+
loadTranscriptState: (...args) => mockLoadTranscriptState.apply(null, args),
|
|
73
|
+
loadTranscript: vi.fn(() => []),
|
|
74
|
+
buildChatEntries: vi.fn(() => []),
|
|
75
|
+
getLastTodoWriteArgs: vi.fn(() => null),
|
|
76
|
+
markMessageCompleted: (...args) => mockMarkMessageCompleted.apply(null, args),
|
|
77
|
+
appendSystemMessage: vi.fn(() => 0),
|
|
78
|
+
getSessionTotalTokens: vi.fn(() => ({ inputTokens: 0, outputTokens: 0 })),
|
|
79
|
+
logInteraction: vi.fn(),
|
|
80
|
+
recordUsageEvent: vi.fn(),
|
|
81
|
+
SessionStore: class {
|
|
82
|
+
getWorkspace() {
|
|
83
|
+
return { id: "workspace-1", rootPath: "/dummy" };
|
|
84
|
+
}
|
|
85
|
+
openSession(selector, model, mode, cwd) {
|
|
86
|
+
return {
|
|
87
|
+
id: "session-parent",
|
|
88
|
+
workspaceId: "workspace-1",
|
|
89
|
+
model,
|
|
90
|
+
mode,
|
|
91
|
+
cwdAtStart: cwd,
|
|
92
|
+
cwdLast: cwd,
|
|
93
|
+
status: "active",
|
|
94
|
+
createdAt: new Date(),
|
|
95
|
+
updatedAt: new Date(),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
createSession(model, mode, cwd) {
|
|
99
|
+
return {
|
|
100
|
+
id: "session-child",
|
|
101
|
+
workspaceId: "workspace-1",
|
|
102
|
+
model,
|
|
103
|
+
mode,
|
|
104
|
+
cwdAtStart: cwd,
|
|
105
|
+
cwdLast: cwd,
|
|
106
|
+
status: "active",
|
|
107
|
+
createdAt: new Date(),
|
|
108
|
+
updatedAt: new Date(),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
getRequiredSession(id) {
|
|
112
|
+
return {
|
|
113
|
+
id,
|
|
114
|
+
workspaceId: "workspace-1",
|
|
115
|
+
model: "dummy-model",
|
|
116
|
+
mode: "agent",
|
|
117
|
+
cwdAtStart: "/dummy",
|
|
118
|
+
cwdLast: "/dummy",
|
|
119
|
+
status: "active",
|
|
120
|
+
createdAt: new Date(),
|
|
121
|
+
updatedAt: new Date(),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
touchSession() { }
|
|
125
|
+
setMode() { }
|
|
126
|
+
setStatus() { }
|
|
127
|
+
setModel() { }
|
|
128
|
+
setTitle() { }
|
|
129
|
+
getLatestSession() {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
getSessionById() {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
// 5. Mock MessageProcessor so we can simulate the messages it writes
|
|
139
|
+
vi.mock("../message-processor.js", () => {
|
|
140
|
+
return {
|
|
141
|
+
MessageProcessor: class {
|
|
142
|
+
deps;
|
|
143
|
+
constructor(deps) {
|
|
144
|
+
this.deps = deps;
|
|
145
|
+
}
|
|
146
|
+
async *run(userMessage) {
|
|
147
|
+
if (userMessage === "trigger error") {
|
|
148
|
+
throw new Error("Simulated MessageProcessor crash");
|
|
149
|
+
}
|
|
150
|
+
if (userMessage === "trigger transient error") {
|
|
151
|
+
globalThis.__transientAttempts = (globalThis.__transientAttempts || 0) + 1;
|
|
152
|
+
if (globalThis.__transientAttempts < 3) {
|
|
153
|
+
throw new Error("fetch failed");
|
|
154
|
+
}
|
|
155
|
+
this.deps.messages.push({ role: "assistant", content: "Sub-session final structured response after retry" }, { role: "tool", content: "Final tool outcome (should be copied)" });
|
|
156
|
+
yield { type: "content", content: "processing after retry..." };
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (userMessage === "test consult") {
|
|
160
|
+
if (this.deps.consultParentSession) {
|
|
161
|
+
const advice = await this.deps.consultParentSession("How to handle this error?");
|
|
162
|
+
this.deps.messages.push({ role: "assistant", content: `Advice: ${advice}` }, { role: "tool", content: "Final tool outcome (should be copied)" });
|
|
163
|
+
}
|
|
164
|
+
yield { type: "content", content: "consulting..." };
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
// Simulating writing messages during turn execution
|
|
168
|
+
// Under a sub-session, we want to write intermediate clutter,
|
|
169
|
+
// and check that only final assistant response gets absorbed.
|
|
170
|
+
this.deps.messages.push({ role: "assistant", content: "Intermediate assistant prompt analysis" }, { role: "tool", content: "Intermediate tool result that should be ignored" }, { role: "assistant", content: "Sub-session final structured response" }, { role: "tool", content: "Final tool outcome (should be copied)" });
|
|
171
|
+
yield { type: "content", content: "processing..." };
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
describe("Agent - Sub-Session Delegation & Absorption", () => {
|
|
177
|
+
let cleanup = null;
|
|
178
|
+
beforeAll(async () => {
|
|
179
|
+
await loadCatalog();
|
|
180
|
+
});
|
|
181
|
+
beforeEach(() => {
|
|
182
|
+
process.env.MUONROI_FORCE_ROUTING_CLASSIFY = "1";
|
|
183
|
+
vi.clearAllMocks();
|
|
184
|
+
globalThis.__transientAttempts = 0;
|
|
185
|
+
parentSessionState.messages = [{ role: "user", content: "Hello parent" }];
|
|
186
|
+
parentSessionState.seqs = [1];
|
|
187
|
+
mockLoadTranscriptState.mockReturnValue(parentSessionState);
|
|
188
|
+
const mockModelHandle = installMockModel({ fixture: { stream: textOnlyStream("ignored mock stream") } });
|
|
189
|
+
cleanup = () => {
|
|
190
|
+
delete process.env.MUONROI_FORCE_ROUTING_CLASSIFY;
|
|
191
|
+
mockModelHandle.uninstall();
|
|
192
|
+
};
|
|
193
|
+
});
|
|
194
|
+
afterEach(() => {
|
|
195
|
+
cleanup?.();
|
|
196
|
+
});
|
|
197
|
+
it("routes DIRECT_ANSWER directly without spawning a sub-session", async () => {
|
|
198
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
199
|
+
action: "DIRECT_ANSWER",
|
|
200
|
+
confidence: 0.95,
|
|
201
|
+
reason: "simple question",
|
|
202
|
+
});
|
|
203
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
204
|
+
persistSession: true,
|
|
205
|
+
session: "session-parent",
|
|
206
|
+
});
|
|
207
|
+
// Clear initial messages
|
|
208
|
+
agent.messages = [];
|
|
209
|
+
const generator = agent.processMessage("What is 2+2?");
|
|
210
|
+
const chunks = [];
|
|
211
|
+
for await (const chunk of generator) {
|
|
212
|
+
chunks.push(chunk);
|
|
213
|
+
}
|
|
214
|
+
// Since it's DIRECT_ANSWER, it should NOT try to insert child sub-sessions
|
|
215
|
+
expect(mockDb.prepare).not.toHaveBeenCalledWith(expect.stringContaining("parent_session_id"));
|
|
216
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
217
|
+
});
|
|
218
|
+
it("routes SPAWN_SUB_SESSION: creates child session, injects overlay prompt, runs in isolation, and absorbs final outcome", async () => {
|
|
219
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
220
|
+
action: "SPAWN_SUB_SESSION",
|
|
221
|
+
confidence: 0.98,
|
|
222
|
+
reason: "requires multi-step tool execution",
|
|
223
|
+
});
|
|
224
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
225
|
+
persistSession: true,
|
|
226
|
+
session: "session-parent",
|
|
227
|
+
});
|
|
228
|
+
// Start with empty messages in parent
|
|
229
|
+
agent.messages = [];
|
|
230
|
+
const generator = agent.processMessage("Implement auth and write tests");
|
|
231
|
+
const chunks = [];
|
|
232
|
+
for await (const chunk of generator) {
|
|
233
|
+
chunks.push(chunk);
|
|
234
|
+
}
|
|
235
|
+
// 1. Verify DB parent session link update is triggered
|
|
236
|
+
expect(mockDb.prepare).toHaveBeenCalledWith("UPDATE sessions SET parent_session_id = ? WHERE id = ?");
|
|
237
|
+
// 2. Verify parent session was restored at the end
|
|
238
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
239
|
+
// 3. Verify final messages in parent contain the absorbed outcome
|
|
240
|
+
// We expect the final messages list to have absorbed:
|
|
241
|
+
// - last assistant message: "Sub-session final structured response"
|
|
242
|
+
// - subsequent tool message(s): "Final tool outcome (should be copied)"
|
|
243
|
+
// It should NOT contain:
|
|
244
|
+
// - intermediate assistant: "Intermediate assistant prompt analysis"
|
|
245
|
+
// - intermediate tool: "Intermediate tool result that should be ignored"
|
|
246
|
+
expect(agent.messages).toHaveLength(3); // Hello parent + absorbed assistant + absorbed tool
|
|
247
|
+
expect(agent.messages[1]).toEqual({
|
|
248
|
+
role: "assistant",
|
|
249
|
+
content: "Sub-session final structured response",
|
|
250
|
+
});
|
|
251
|
+
expect(agent.messages[2]).toEqual({
|
|
252
|
+
role: "tool",
|
|
253
|
+
content: "Final tool outcome (should be copied)",
|
|
254
|
+
});
|
|
255
|
+
// 4. Verify appendMessages was called to persist the absorbed turn in the parent
|
|
256
|
+
expect(mockAppendMessages).toHaveBeenCalledWith("session-parent", expect.arrayContaining([
|
|
257
|
+
expect.objectContaining({ role: "user", content: "Implement auth and write tests" }),
|
|
258
|
+
expect.objectContaining({ role: "assistant", content: "Sub-session final structured response" }),
|
|
259
|
+
expect.objectContaining({ role: "tool", content: "Final tool outcome (should be copied)" }),
|
|
260
|
+
]));
|
|
261
|
+
});
|
|
262
|
+
it("restores parent session if sub-session execution crashes", async () => {
|
|
263
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
264
|
+
action: "SPAWN_SUB_SESSION",
|
|
265
|
+
confidence: 0.98,
|
|
266
|
+
reason: "requires sub-session",
|
|
267
|
+
});
|
|
268
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
269
|
+
persistSession: true,
|
|
270
|
+
session: "session-parent",
|
|
271
|
+
});
|
|
272
|
+
// Start with empty messages in parent
|
|
273
|
+
agent.messages = [];
|
|
274
|
+
// Run a turn that will crash
|
|
275
|
+
let threw = false;
|
|
276
|
+
try {
|
|
277
|
+
const generator = agent.processMessage("trigger error");
|
|
278
|
+
for await (const chunk of generator) {
|
|
279
|
+
// consume stream
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch (err) {
|
|
283
|
+
expect(err.message).toBe("Simulated MessageProcessor crash");
|
|
284
|
+
threw = true;
|
|
285
|
+
}
|
|
286
|
+
expect(threw).toBe(true);
|
|
287
|
+
// Verify parent session is restored even after crash
|
|
288
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
289
|
+
});
|
|
290
|
+
it("resumes an existing active sub-session if found in database", async () => {
|
|
291
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
292
|
+
action: "SPAWN_SUB_SESSION",
|
|
293
|
+
confidence: 0.98,
|
|
294
|
+
reason: "requires sub-session",
|
|
295
|
+
});
|
|
296
|
+
// Mock active sub-session row lookup
|
|
297
|
+
mockDb.prepare.mockImplementation((sql) => {
|
|
298
|
+
if (sql.includes("status = 'active'")) {
|
|
299
|
+
return {
|
|
300
|
+
get: () => ({ id: "session-active-child" }),
|
|
301
|
+
run: () => ({ changes: 1 }),
|
|
302
|
+
all: () => [],
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
return {
|
|
306
|
+
run: () => ({ changes: 1 }),
|
|
307
|
+
get: () => undefined,
|
|
308
|
+
all: () => [],
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
// Mock loadTranscriptState to return a state for the child session or parent session depending on input
|
|
312
|
+
mockLoadTranscriptState.mockImplementation((sessId) => {
|
|
313
|
+
if (sessId === "session-parent") {
|
|
314
|
+
return parentSessionState;
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
messages: [{ role: "user", content: "Previous attempt" }],
|
|
318
|
+
seqs: [1],
|
|
319
|
+
};
|
|
320
|
+
});
|
|
321
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
322
|
+
persistSession: true,
|
|
323
|
+
session: "session-parent",
|
|
324
|
+
});
|
|
325
|
+
// Start with empty messages in parent
|
|
326
|
+
agent.messages = [];
|
|
327
|
+
const generator = agent.processMessage("Implement auth and write tests");
|
|
328
|
+
const chunks = [];
|
|
329
|
+
for await (const chunk of generator) {
|
|
330
|
+
chunks.push(chunk);
|
|
331
|
+
}
|
|
332
|
+
// 1. Verify that we yielded the resumption status message
|
|
333
|
+
const hasResumeMessage = chunks.some((c) => c.type === "content" && c.content?.includes("Phát hiện sub-session trước đó bị gián đoạn"));
|
|
334
|
+
expect(hasResumeMessage).toBe(true);
|
|
335
|
+
// 2. Verify that the agent restored parent session at the end
|
|
336
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
337
|
+
// 3. Verify that loadTranscriptState was called with the resumed sub-session ID
|
|
338
|
+
expect(mockLoadTranscriptState).toHaveBeenCalledWith("session-active-child");
|
|
339
|
+
});
|
|
340
|
+
it("triggers session rotation if the model decides ROTATE_SESSION", async () => {
|
|
341
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
342
|
+
action: "ROTATE_SESSION",
|
|
343
|
+
confidence: 0.9,
|
|
344
|
+
reason: "New topic",
|
|
345
|
+
});
|
|
346
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
347
|
+
persistSession: true,
|
|
348
|
+
session: "session-parent",
|
|
349
|
+
});
|
|
350
|
+
agent.messages = [{ role: "user", content: "Switch topics" }];
|
|
351
|
+
const generator = agent.processMessage("switch topic");
|
|
352
|
+
const chunks = [];
|
|
353
|
+
for await (const chunk of generator) {
|
|
354
|
+
chunks.push(chunk);
|
|
355
|
+
}
|
|
356
|
+
// Verify deliberateCompact was called
|
|
357
|
+
expect(mockDeliberateCompact).toHaveBeenCalled();
|
|
358
|
+
// Verify a new session was created (the active session ID changes)
|
|
359
|
+
expect(agent.getSessionId()).toBe("session-child");
|
|
360
|
+
});
|
|
361
|
+
it("triggers session rotation if currentChars exceeds the hard safety threshold (threshold * 2)", async () => {
|
|
362
|
+
process.env.MUONROI_SILENT_ROTATION_THRESHOLD = "100";
|
|
363
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
364
|
+
action: "DIRECT_ANSWER",
|
|
365
|
+
confidence: 0.95,
|
|
366
|
+
reason: "simple answer",
|
|
367
|
+
});
|
|
368
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
369
|
+
persistSession: true,
|
|
370
|
+
session: "session-parent",
|
|
371
|
+
});
|
|
372
|
+
agent.messages = [{ role: "user", content: "A".repeat(250) }];
|
|
373
|
+
try {
|
|
374
|
+
const generator = agent.processMessage("short message");
|
|
375
|
+
const chunks = [];
|
|
376
|
+
for await (const chunk of generator) {
|
|
377
|
+
chunks.push(chunk);
|
|
378
|
+
}
|
|
379
|
+
// Verify deliberateCompact was called
|
|
380
|
+
expect(mockDeliberateCompact).toHaveBeenCalled();
|
|
381
|
+
// Verify it rotated to session-child
|
|
382
|
+
expect(agent.getSessionId()).toBe("session-child");
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
delete process.env.MUONROI_SILENT_ROTATION_THRESHOLD;
|
|
386
|
+
}
|
|
387
|
+
});
|
|
388
|
+
it("does not resume a stale active sub-session (timeout), marks it as abandoned, and forks a new child session", async () => {
|
|
389
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
390
|
+
action: "SPAWN_SUB_SESSION",
|
|
391
|
+
confidence: 0.98,
|
|
392
|
+
reason: "requires sub-session",
|
|
393
|
+
});
|
|
394
|
+
const twentyMinsAgo = new Date(Date.now() - 20 * 60 * 1000).toISOString();
|
|
395
|
+
// Mock active sub-session row lookup returning a stale row
|
|
396
|
+
mockDb.prepare.mockImplementation((sql) => {
|
|
397
|
+
if (sql.includes("status = 'active'")) {
|
|
398
|
+
return {
|
|
399
|
+
get: () => ({ id: "session-stale-child", updated_at: twentyMinsAgo }),
|
|
400
|
+
run: () => ({ changes: 1 }),
|
|
401
|
+
all: () => [],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
return {
|
|
405
|
+
run: () => ({ changes: 1 }),
|
|
406
|
+
get: () => undefined,
|
|
407
|
+
all: () => [],
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
411
|
+
persistSession: true,
|
|
412
|
+
session: "session-parent",
|
|
413
|
+
});
|
|
414
|
+
// Start with empty messages in parent
|
|
415
|
+
agent.messages = [];
|
|
416
|
+
const generator = agent.processMessage("Implement auth and write tests");
|
|
417
|
+
const chunks = [];
|
|
418
|
+
for await (const chunk of generator) {
|
|
419
|
+
chunks.push(chunk);
|
|
420
|
+
}
|
|
421
|
+
// 1. Verify that we did NOT yield the resumption message
|
|
422
|
+
const hasResumeMessage = chunks.some((c) => c.type === "content" && c.content?.includes("Phát hiện sub-session trước đó bị gián đoạn"));
|
|
423
|
+
expect(hasResumeMessage).toBe(false);
|
|
424
|
+
// 2. Verify that we marked the stale sub-session as abandoned
|
|
425
|
+
expect(mockDb.prepare).toHaveBeenCalledWith("UPDATE sessions SET status = 'abandoned', updated_at = ? WHERE id = ?");
|
|
426
|
+
// 3. Verify that we created a new session
|
|
427
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
428
|
+
});
|
|
429
|
+
it("registers and successfully executes consultParentSession tool in sub-session", async () => {
|
|
430
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
431
|
+
action: "SPAWN_SUB_SESSION",
|
|
432
|
+
confidence: 0.98,
|
|
433
|
+
reason: "requires sub-session for consultation test",
|
|
434
|
+
});
|
|
435
|
+
mockDb.prepare.mockImplementation((sql) => {
|
|
436
|
+
if (sql.includes("SELECT parent_session_id")) {
|
|
437
|
+
return {
|
|
438
|
+
get: () => ({ parent_session_id: "session-parent" }),
|
|
439
|
+
run: () => ({ changes: 1 }),
|
|
440
|
+
all: () => [],
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
if (sql.includes("SELECT model_id")) {
|
|
444
|
+
return {
|
|
445
|
+
get: () => ({ model_id: "deepseek-v4-flash" }),
|
|
446
|
+
run: () => ({ changes: 1 }),
|
|
447
|
+
all: () => [],
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
return {
|
|
451
|
+
run: () => ({ changes: 1 }),
|
|
452
|
+
get: () => undefined,
|
|
453
|
+
all: () => [],
|
|
454
|
+
};
|
|
455
|
+
});
|
|
456
|
+
mockLoadTranscriptState.mockReturnValue({
|
|
457
|
+
messages: [{ role: "user", content: "Hello parent" }],
|
|
458
|
+
seqs: [1],
|
|
459
|
+
});
|
|
460
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
461
|
+
persistSession: true,
|
|
462
|
+
session: "session-parent",
|
|
463
|
+
});
|
|
464
|
+
// Start with empty messages in parent
|
|
465
|
+
agent.messages = [];
|
|
466
|
+
const generator = agent.processMessage("test consult");
|
|
467
|
+
const chunks = [];
|
|
468
|
+
for await (const chunk of generator) {
|
|
469
|
+
chunks.push(chunk);
|
|
470
|
+
}
|
|
471
|
+
// Verify that generateText was called to advice the sub-session
|
|
472
|
+
expect(mockGenerateText).toHaveBeenCalled();
|
|
473
|
+
// Verify parent session is restored
|
|
474
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
475
|
+
// The parent's final messages should have absorbed the assistant message which contains Advice
|
|
476
|
+
expect(agent.messages).toHaveLength(3); // Hello parent + absorbed assistant (advice) + absorbed tool
|
|
477
|
+
expect(agent.messages[1].content).toContain("Advice: Mocked parent advice response");
|
|
478
|
+
});
|
|
479
|
+
it("automatically retries transient errors in sub-session before succeeding", async () => {
|
|
480
|
+
mockClassifySubSessionAction.mockResolvedValue({
|
|
481
|
+
action: "SPAWN_SUB_SESSION",
|
|
482
|
+
confidence: 0.98,
|
|
483
|
+
reason: "requires sub-session for transient error test",
|
|
484
|
+
});
|
|
485
|
+
const agent = new Agent("sk-dummy", undefined, "deepseek-v4-flash", undefined, {
|
|
486
|
+
persistSession: true,
|
|
487
|
+
session: "session-parent",
|
|
488
|
+
});
|
|
489
|
+
// Start with empty messages in parent
|
|
490
|
+
agent.messages = [];
|
|
491
|
+
const generator = agent.processMessage("trigger transient error");
|
|
492
|
+
const chunks = [];
|
|
493
|
+
for await (const chunk of generator) {
|
|
494
|
+
chunks.push(chunk);
|
|
495
|
+
}
|
|
496
|
+
// Verify parent session is restored
|
|
497
|
+
expect(agent.getSessionId()).toBe("session-parent");
|
|
498
|
+
console.log("TEST MESSAGES: ", JSON.stringify(agent.messages, null, 2));
|
|
499
|
+
// The sub-session should have succeeded on the 3rd attempt and its output should be absorbed
|
|
500
|
+
expect(agent.messages).toHaveLength(3); // Hello parent + absorbed assistant (after retry) + absorbed tool
|
|
501
|
+
expect(agent.messages[1].content).toContain("Sub-session final structured response after retry");
|
|
502
|
+
// Verify warning chunks were emitted
|
|
503
|
+
const warningChunks = chunks.filter((c) => c.type === "content" && c.content?.includes("Có lỗi mạng hoặc rate limit xảy ra trong sub-session"));
|
|
504
|
+
expect(warningChunks.length).toBe(2);
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
//# sourceMappingURL=sub-session-delegation.test.js.map
|
|
@@ -76,13 +76,16 @@ describe("Agent class", { timeout: 30_000 }, () => {
|
|
|
76
76
|
agent.setModel("claude-sonnet-4-6-20250514");
|
|
77
77
|
expect(agent.getModel()).toBe("claude-sonnet-4-6-20250514");
|
|
78
78
|
});
|
|
79
|
-
it("constructs with sandbox mode", async () => {
|
|
79
|
+
it("constructs with sandbox mode — always returns 'off' (sandbox removed)", async () => {
|
|
80
80
|
const { Agent } = await importAgentModule();
|
|
81
|
+
// Sandbox has been removed; passing sandboxMode: "shuru" is accepted for
|
|
82
|
+
// back-compat but getSandboxMode() always returns "off" until a new
|
|
83
|
+
// sandbox implementation is wired in.
|
|
81
84
|
const agent = new Agent(undefined, undefined, undefined, undefined, {
|
|
82
85
|
persistSession: false,
|
|
83
86
|
sandboxMode: "shuru",
|
|
84
87
|
});
|
|
85
|
-
expect(agent.getSandboxMode()).toBe("
|
|
88
|
+
expect(agent.getSandboxMode()).toBe("off");
|
|
86
89
|
});
|
|
87
90
|
it("defaults sandbox mode to off", async () => {
|
|
88
91
|
const { Agent } = await importAgentModule();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { LspDiagnostic, LspDiagnosticFile } from "../lsp/types.js";
|
|
2
|
+
/** Attribution line every auto-commit message ends with (mirrors the prompt rule). */
|
|
3
|
+
export declare const AUTO_COMMIT_ATTRIBUTION = "Coding by - Muonroi-CLI";
|
|
4
|
+
export interface AutoCommitResult {
|
|
5
|
+
committed: boolean;
|
|
6
|
+
sha?: string;
|
|
7
|
+
fileCount?: number;
|
|
8
|
+
reason?: string;
|
|
9
|
+
/** G1: when reason === "lsp-errors", the per-file diagnostic summary the agent should fix. */
|
|
10
|
+
detail?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* G1 commit quality gate. Default ON; disable with `MUONROI_COMMIT_GATE=0`
|
|
14
|
+
* (mirrors the `MUONROI_AUTO_COMMIT=0` convention). Off automatically under the
|
|
15
|
+
* unit-test suite so specs that commit fixtures aren't gated.
|
|
16
|
+
*/
|
|
17
|
+
export declare function isCommitGateEnabled(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* G1: per-file LSP-errors gate. Runs the SAME LSP diagnostics produced at
|
|
20
|
+
* write-time on each staged path; blocks the commit if any staged file has an
|
|
21
|
+
* ERROR (severity 1). Scoped to each file's OWN diagnostics so unrelated repo
|
|
22
|
+
* breakage never blocks. Files with no registered LSP server return no
|
|
23
|
+
* diagnostics and pass (so docs/config/non-source commits are unaffected).
|
|
24
|
+
*
|
|
25
|
+
* Fails OPEN on timeout or any error — the gate must never hang a turn or block
|
|
26
|
+
* a commit on its own failure. Only a clean, in-budget run with a real
|
|
27
|
+
* severity-1 diagnostic blocks.
|
|
28
|
+
*/
|
|
29
|
+
/**
|
|
30
|
+
* G1 (pure): the gate's BLOCK decision for one staged file — its own
|
|
31
|
+
* severity-1 (error) diagnostics. Scoped by absolute path so a diagnostic LSP
|
|
32
|
+
* reports against a DIFFERENT file (cross-file type breakage) never blocks this
|
|
33
|
+
* commit, and warnings/infos (severity >= 2) are ignored. Exported for testing.
|
|
34
|
+
*/
|
|
35
|
+
export declare function blockingErrorsForFile(diagFiles: LspDiagnosticFile[], absPath: string): LspDiagnostic[];
|
|
36
|
+
export declare function gateStagedPaths(cwd: string, paths: string[], budgetMs?: number): Promise<{
|
|
37
|
+
ok: boolean;
|
|
38
|
+
summary?: string;
|
|
39
|
+
}>;
|
|
40
|
+
/**
|
|
41
|
+
* G1 follow-up: the set of paths a bash-tool `git commit` would include, so the
|
|
42
|
+
* LSP commit gate can run on them. A raw `git commit` (unlike the `git_commit`
|
|
43
|
+
* tool) doesn't tell us its paths, so derive them from git state:
|
|
44
|
+
* - always: the already-staged set (`git diff --cached --name-only`)
|
|
45
|
+
* - `git commit -a`: + tracked modifications it auto-stages at commit time
|
|
46
|
+
* (`git diff --name-only`)
|
|
47
|
+
* - `git add -A`/`.`/`--all` chained in the SAME command: + the whole
|
|
48
|
+
* working-tree change set (`git status --porcelain`), since the add hasn't
|
|
49
|
+
* run yet at pre-exec time so it isn't reflected in `--cached`.
|
|
50
|
+
* Deleted/binary/unreadable paths are skipped later by gateStagedPaths when it
|
|
51
|
+
* reads them. KNOWN GAP: `git add <specific-path> && git commit` in one command
|
|
52
|
+
* where <specific-path> was not pre-staged is NOT covered (we don't parse
|
|
53
|
+
* pathspecs); the `git_commit` tool + auto-commit backstop remain the primary
|
|
54
|
+
* gates. Returns repo-relative paths (gateStagedPaths resolves them against cwd).
|
|
55
|
+
*/
|
|
56
|
+
export declare function pathsForCommitGate(cwd: string, opts: {
|
|
57
|
+
broadAdd: boolean;
|
|
58
|
+
commitAll: boolean;
|
|
59
|
+
}): Promise<string[]>;
|
|
60
|
+
export declare function isAutoCommitEnabled(): boolean;
|
|
61
|
+
/**
|
|
62
|
+
* Parse `git status --porcelain` output into the set of changed paths. Handles
|
|
63
|
+
* renames (`orig -> new`, keeps the new path) and quoted paths with spaces.
|
|
64
|
+
*/
|
|
65
|
+
export declare function parsePorcelainPaths(out: string): Set<string>;
|
|
66
|
+
export declare function isSensitivePath(path: string): boolean;
|
|
67
|
+
/** CLI artifact / build junk that must never be folded into an agent commit. */
|
|
68
|
+
export declare function isCliArtifactPath(path: string): boolean;
|
|
69
|
+
/** Any path the auto-commit must skip: a secret or a CLI/build artifact. */
|
|
70
|
+
export declare function isExcludedPath(path: string): boolean;
|
|
71
|
+
/**
|
|
72
|
+
* Backstop subject naming the changed FILES — used only by the deterministic
|
|
73
|
+
* end-of-turn safety net (when the agent did not commit its own work via the
|
|
74
|
+
* git_commit tool). Deliberately NOT derived from the raw user prompt (a
|
|
75
|
+
* truncated prompt is meaningless, especially for a multi-step plan); a file list
|
|
76
|
+
* at least says what changed. The meaningful, model-authored message is the
|
|
77
|
+
* git_commit tool's job (the agent writes it per chunk/plan-step).
|
|
78
|
+
*/
|
|
79
|
+
export declare function buildFileListSubject(paths: string[]): string;
|
|
80
|
+
/**
|
|
81
|
+
* Split an AGENT-authored message into a bounded subject (first line, <=72 so it
|
|
82
|
+
* passes conventional-commit hooks) and a bounded body. Strips CRs and drops any
|
|
83
|
+
* attribution line the agent already added (we append exactly one ourselves).
|
|
84
|
+
* Callers pass subject/body/attribution as SEPARATE `-m` flags so git inserts the
|
|
85
|
+
* blank-line separators itself — embedding "\n\n" in a single `-m` arg gets
|
|
86
|
+
* mangled by Windows execFile (the attribution then glues onto the subject).
|
|
87
|
+
*/
|
|
88
|
+
export declare function splitCommitMessage(message: string): {
|
|
89
|
+
subject: string;
|
|
90
|
+
body: string;
|
|
91
|
+
};
|
|
92
|
+
/** Snapshot the dirty/untracked path set before a turn (empty when not a repo). */
|
|
93
|
+
export declare function snapshotDirtyPaths(cwd: string): Promise<Set<string>>;
|
|
94
|
+
/**
|
|
95
|
+
* Commit the files the agent changed this turn. `dirtyBefore` is the snapshot
|
|
96
|
+
* from before the turn. Returns a result describing what happened; never throws.
|
|
97
|
+
*/
|
|
98
|
+
export declare function maybeAutoCommitTurn(opts: {
|
|
99
|
+
cwd: string;
|
|
100
|
+
dirtyBefore: Set<string>;
|
|
101
|
+
userMessage: string;
|
|
102
|
+
}): Promise<AutoCommitResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Commit a SPECIFIC set of paths (absolute or repo-relative) with an
|
|
105
|
+
* AGENT-AUTHORED message — backs the git_commit tool. Stages only `paths` (minus
|
|
106
|
+
* secrets/artifacts), commits with the model's message + the attribution line,
|
|
107
|
+
* scoped by pathspec. No-op if nothing among `paths` is actually staged
|
|
108
|
+
* (e.g. already committed). Never throws.
|
|
109
|
+
*/
|
|
110
|
+
export declare function commitSpecificPaths(cwd: string, paths: string[], message: string): Promise<AutoCommitResult>;
|