mixdog 0.9.2 → 0.9.4
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/package.json +8 -3
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +30 -67
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +5 -2
- package/src/agents/heavy-worker/AGENT.md +21 -11
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +3 -2
- package/src/agents/worker/AGENT.md +21 -11
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +933 -3731
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/repl.mjs +5 -5
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +2 -9
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +47 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +409 -1304
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +551 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1080 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +241 -894
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +258 -2050
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +238 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +6 -14
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +52 -2
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +291 -0
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +126 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +155 -677
- package/src/standalone/channel-worker.mjs +7 -9
- package/src/standalone/explore-tool.mjs +40 -12
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +110 -746
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +125 -0
- package/src/standalone/provider-admin.mjs +15 -19
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1163 -7571
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +259 -80
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +56 -588
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +18034 -17188
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +405 -1385
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +60 -10
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +11 -9
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -462
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -1,284 +1,108 @@
|
|
|
1
1
|
import { classifyResultKind } from './result-classification.mjs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { executeBashSessionTool } from '../tools/bash-session.mjs';
|
|
5
|
-
import { executePatchTool, takeApplyPatchUiDiff } from '../tools/patch.mjs';
|
|
2
|
+
import { canonicalizeBuiltinToolName, executeBuiltinTool, isBuiltinTool } from '../tools/builtin.mjs';
|
|
3
|
+
import { takeApplyPatchUiDiff } from '../tools/patch.mjs';
|
|
6
4
|
import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
|
|
7
|
-
import {
|
|
8
|
-
import { normalizeToolEnvelope, makeToolEnvelope } from './tool-envelope.mjs';
|
|
5
|
+
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
9
6
|
import { traceAgentLoop, traceAgentTool, traceAgentToolFailure, traceAgentCompact, estimateProviderPayloadBytes, messagePrefixHash, appendAgentTrace } from '../agent-trace.mjs';
|
|
10
|
-
import { resolveSessionMaxLoopIterations } from '../agent-runtime/agent-loop-policy.mjs';
|
|
7
|
+
import { resolveSessionMaxLoopIterations, WORKER_SOFT_CAP_ITERATIONS, isWorkerSoftCapSession } from '../agent-runtime/agent-loop-policy.mjs';
|
|
11
8
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
12
|
-
import { markSessionToolCall, updateSessionStage, SessionClosedError,
|
|
9
|
+
import { markSessionToolCall, updateSessionStage, SessionClosedError, bumpUsageMetricsEpoch } from './manager.mjs';
|
|
13
10
|
import {
|
|
14
|
-
estimateMessagesTokens,
|
|
15
|
-
estimateRequestReserveTokens,
|
|
16
|
-
sanitizeToolPairs,
|
|
17
|
-
resolveCompactBufferRatio,
|
|
18
|
-
resolveCompactBufferTokens,
|
|
19
|
-
} from './context-utils.mjs';
|
|
20
|
-
import {
|
|
21
|
-
recallFastTrackCompactMessages,
|
|
22
11
|
pruneToolOutputs,
|
|
23
12
|
pruneToolOutputsUnanchored,
|
|
24
13
|
semanticCompactMessages,
|
|
25
14
|
effectiveBudget as compactEffectiveBudget,
|
|
26
|
-
compactTypeIsRecallFastTrack,
|
|
27
|
-
compactTypeIsSemantic,
|
|
28
|
-
normalizeCompactType,
|
|
29
15
|
DEFAULT_COMPACT_TYPE,
|
|
30
|
-
DEFAULT_COMPACTION_KEEP_TOKENS,
|
|
31
|
-
drainSessionCycle1,
|
|
32
|
-
countRawPendingRows,
|
|
33
16
|
} from './compact.mjs';
|
|
34
17
|
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
35
18
|
import { stripSoftWarns } from '../tool-loop-guard.mjs';
|
|
36
19
|
import { maybeOffloadToolResult } from './tool-result-offload.mjs';
|
|
37
20
|
import { tryReadCached, setReadCached, invalidatePathForSession, markPostEdit, consumePostEditMark, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
|
|
38
|
-
import { createScopedCacheOutcome } from './cache/scoped-cache-outcome.mjs';
|
|
39
|
-
import { modelVisibleToolCompletionMessage } from '../../../shared/tool-execution-contract.mjs';
|
|
40
|
-
import { createHash } from 'crypto';
|
|
41
21
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
42
22
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const n = _stripMcpPrefix(name);
|
|
55
|
-
return n === 'apply_patch';
|
|
56
|
-
}
|
|
57
|
-
const SCOPED_CACHEABLE_TOOLS = new Set([
|
|
58
|
-
'code_graph',
|
|
59
|
-
'grep',
|
|
60
|
-
'list',
|
|
61
|
-
'glob',
|
|
62
|
-
]);
|
|
63
|
-
let codeGraphRuntimePromise = null;
|
|
64
|
-
async function executeCodeGraphToolLazy(name, args, cwd, signal = null, options = {}) {
|
|
65
|
-
codeGraphRuntimePromise ??= import('../tools/code-graph.mjs');
|
|
66
|
-
const mod = await codeGraphRuntimePromise;
|
|
67
|
-
if (typeof mod.executeCodeGraphTool !== 'function') throw new Error('code_graph runtime is not available');
|
|
68
|
-
return mod.executeCodeGraphTool(name, args, cwd, signal, options);
|
|
69
|
-
}
|
|
70
|
-
function _isScopedCacheableTool(name) {
|
|
71
|
-
const n = _stripMcpPrefix(name);
|
|
72
|
-
return SCOPED_CACHEABLE_TOOLS.has(n);
|
|
73
|
-
}
|
|
74
|
-
function _isShellTool(name) {
|
|
75
|
-
const n = _stripMcpPrefix(name);
|
|
76
|
-
return n === 'shell' || n === 'bash_session';
|
|
77
|
-
}
|
|
23
|
+
import {
|
|
24
|
+
_stripMcpPrefix,
|
|
25
|
+
_isReadTool,
|
|
26
|
+
_isMutationTool,
|
|
27
|
+
_isScopedCacheableTool,
|
|
28
|
+
_isShellTool,
|
|
29
|
+
_intraTurnSig,
|
|
30
|
+
} from './loop/tool-classify.mjs';
|
|
31
|
+
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
32
|
+
import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
|
|
33
|
+
import { executeTool, _scopedCacheOutcomeForCall } from './loop/tool-exec.mjs';
|
|
78
34
|
|
|
79
35
|
// classifyResultKind is imported from result-classification.mjs at the top of
|
|
80
36
|
// this file; import it from there directly rather than via this module.
|
|
81
|
-
|
|
82
|
-
// Canonical signature for intra-turn duplicate detection. Sorting keys
|
|
83
|
-
// produces a stable hash regardless of arg-object key order. Anything
|
|
84
|
-
// non-serializable falls back to String(args) — still deterministic for
|
|
85
|
-
// the model's typical structured-arg shape.
|
|
86
|
-
function _canonicalArgs(args) {
|
|
87
|
-
if (args == null || typeof args !== 'object') {
|
|
88
|
-
try { return JSON.stringify(args); } catch { return String(args); }
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
const keys = Object.keys(args).sort();
|
|
92
|
-
const sorted = {};
|
|
93
|
-
for (const k of keys) sorted[k] = args[k];
|
|
94
|
-
return JSON.stringify(sorted);
|
|
95
|
-
} catch { return String(args); }
|
|
96
|
-
}
|
|
97
|
-
function _intraTurnSig(name, args) {
|
|
98
|
-
return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Shared pre-dispatch deny — single source of truth for the remaining
|
|
102
|
-
// control-plane / role scoping rejects. Called by BOTH the eager dispatch
|
|
103
|
-
// path (startEagerTool) and the serial dispatch path (executeTool body).
|
|
104
|
-
// Returns null when the call is allowed to proceed; otherwise returns the
|
|
105
|
-
// Error string the serial path would emit. The eager caller ignores the
|
|
106
|
-
// message body and just treats non-null as "do not start eager".
|
|
107
|
-
//
|
|
108
|
-
// This is NOT a permission gate — runtime permission enforcement was removed
|
|
109
|
-
// (every tool call is trusted). What remains is architectural scoping:
|
|
110
|
-
// agent workers are sandboxed to code/research tools. They must never reach
|
|
111
|
-
// owner/host control surfaces: session management, the ENTIRE channels module
|
|
112
|
-
// (Discord messaging, schedules, webhook/config, channel-bridge toggle,
|
|
113
|
-
// command injection), or host input injection. Explicit name list (no imports)
|
|
114
|
-
// keeps this hot-path gate dependency-free; add new owner/channel tools here.
|
|
115
|
-
const WORKER_DENIED_TOOLS = new Set([
|
|
116
|
-
// session control-plane — unified into the single `agent` tool
|
|
117
|
-
// (type=spawn|send|close|list). Denying the one name blocks all worker
|
|
118
|
-
// session control.
|
|
119
|
-
'agent',
|
|
120
|
-
// channels module (owner/Discord-facing)
|
|
121
|
-
'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
|
|
122
|
-
'schedule_status', 'trigger_schedule', 'schedule_control',
|
|
123
|
-
'activate_channel_bridge', 'reload_config', 'inject_command',
|
|
124
|
-
// host input injection
|
|
125
|
-
'inject_input',
|
|
126
|
-
]);
|
|
127
|
-
function _preDispatchDeny(call, toolKind, sessionRef) {
|
|
128
|
-
const name = call?.name;
|
|
129
|
-
if (typeof name !== 'string' || !name) return null;
|
|
130
|
-
const _agentOwned = sessionRef?.scope?.startsWith?.('agent:')
|
|
131
|
-
|| isAgentOwner(sessionRef);
|
|
132
|
-
const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
|
|
133
|
-
if (_agentOwned && _controlPlaneTool) {
|
|
134
|
-
return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
|
|
135
|
-
}
|
|
136
|
-
const noToolAgent = sessionRef?.agent === 'cycle1-agent' || sessionRef?.agent === 'cycle2-agent';
|
|
137
|
-
if (noToolAgent) {
|
|
138
|
-
return `Error: tool "${name}" is not available in agent "${sessionRef.agent}". Re-emit the answer as pipe-separated text per the agent's output format (first character a digit, NO tool_use blocks, NO JSON, NO prose, NO apology).`;
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
141
|
-
}
|
|
142
|
-
/** Exported for smoke tests — same runtime deny as the agent loop. */
|
|
143
|
-
export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin') {
|
|
144
|
-
return _preDispatchDeny(call, toolKind, sessionRef);
|
|
145
|
-
}
|
|
146
37
|
import { compressToolResult, recordToolBatch } from '../tools/result-compression.mjs';
|
|
147
38
|
|
|
148
39
|
|
|
149
|
-
import {
|
|
150
|
-
import {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
return text ? { content: text, text } : null;
|
|
199
|
-
}
|
|
200
|
-
if (!entry || typeof entry !== 'object') return null;
|
|
201
|
-
const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : entry;
|
|
202
|
-
const text = typeof entry.text === 'string' ? entry.text.trim() : steeringContentText(content).trim();
|
|
203
|
-
if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
|
|
204
|
-
if (typeof content === 'string') {
|
|
205
|
-
const value = content.trim();
|
|
206
|
-
return value ? { content: value, text: text || value } : null;
|
|
207
|
-
}
|
|
208
|
-
const fallback = steeringContentText(content).trim();
|
|
209
|
-
return fallback ? { content: fallback, text: text || fallback } : null;
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function mergeSteeringEntries(entries) {
|
|
213
|
-
const normalized = (Array.isArray(entries) ? entries : [])
|
|
214
|
-
.map(normalizeSteeringEntry)
|
|
215
|
-
.filter(Boolean);
|
|
216
|
-
if (normalized.length === 0) return null;
|
|
217
|
-
const displayText = normalized.map((entry) => entry.text || steeringContentText(entry.content))
|
|
218
|
-
.filter((text) => String(text || '').trim())
|
|
219
|
-
.join('\n');
|
|
220
|
-
if (normalized.every((entry) => typeof entry.content === 'string')) {
|
|
221
|
-
return {
|
|
222
|
-
content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
|
|
223
|
-
text: displayText,
|
|
224
|
-
count: normalized.length,
|
|
225
|
-
};
|
|
226
|
-
}
|
|
227
|
-
const parts = [];
|
|
228
|
-
for (const entry of normalized) {
|
|
229
|
-
if (typeof entry.content === 'string') {
|
|
230
|
-
if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
|
|
231
|
-
} else if (Array.isArray(entry.content)) {
|
|
232
|
-
parts.push(...entry.content);
|
|
233
|
-
} else {
|
|
234
|
-
const text = steeringContentText(entry.content);
|
|
235
|
-
if (text.trim()) parts.push({ type: 'text', text });
|
|
236
|
-
}
|
|
237
|
-
parts.push({ type: 'text', text: '\n' });
|
|
238
|
-
}
|
|
239
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
240
|
-
return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
class AgentContextOverflowError extends Error {
|
|
244
|
-
constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
|
|
245
|
-
const target = [provider, model].filter(Boolean).join('/') || 'target model';
|
|
246
|
-
const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
|
|
247
|
-
super(
|
|
248
|
-
`agent context overflow (${target}, stage=${stage || 'compact'}): ` +
|
|
249
|
-
`latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
|
|
250
|
-
`reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
|
|
251
|
-
`messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
|
|
252
|
-
);
|
|
253
|
-
this.name = 'AgentContextOverflowError';
|
|
254
|
-
this.code = 'AGENT_CONTEXT_OVERFLOW';
|
|
255
|
-
this.sessionId = sessionId || null;
|
|
256
|
-
this.provider = provider || null;
|
|
257
|
-
this.model = model || null;
|
|
258
|
-
this.contextWindow = contextWindow ?? null;
|
|
259
|
-
this.budgetTokens = budgetTokens ?? null;
|
|
260
|
-
this.reserveTokens = reserveTokens ?? null;
|
|
261
|
-
this.messageTokensEst = messageTokensEst ?? null;
|
|
262
|
-
if (cause) this.cause = cause;
|
|
263
|
-
}
|
|
264
|
-
}
|
|
40
|
+
import { resolve as resolvePath, isAbsolute } from 'path';
|
|
41
|
+
import {
|
|
42
|
+
estimateMessagesTokensSafe,
|
|
43
|
+
compactDiagnosticError,
|
|
44
|
+
compactByteLength,
|
|
45
|
+
compactDebugLog,
|
|
46
|
+
} from './loop/compact-debug.mjs';
|
|
47
|
+
import { mergeSteeringEntries, steeringContentText } from './loop/steering.mjs';
|
|
48
|
+
import {
|
|
49
|
+
crossTurnSignature,
|
|
50
|
+
crossTurnDedupStub,
|
|
51
|
+
SOFT_CAP_WRAPUP_MESSAGE,
|
|
52
|
+
SOFT_CAP_REFUSAL_STUB,
|
|
53
|
+
} from './loop/completion-guards.mjs';
|
|
54
|
+
import { isEditProgressTool } from './loop/completion-guards.mjs';
|
|
55
|
+
import { agentContextOverflowError } from './loop/context-overflow.mjs';
|
|
56
|
+
import { positiveTokenInt } from './loop/env.mjs';
|
|
57
|
+
import { normalizeUsage, addUsage } from './loop/usage.mjs';
|
|
58
|
+
import { HIDDEN_AGENT_NAMES } from './loop/hidden-agents.mjs';
|
|
59
|
+
import {
|
|
60
|
+
resolveWorkerCompactPolicy,
|
|
61
|
+
compactionTelemetryPressureTokens,
|
|
62
|
+
compactTargetBudget,
|
|
63
|
+
shouldCompactForSession,
|
|
64
|
+
countPrunedToolOutputs,
|
|
65
|
+
rememberCompactTelemetry,
|
|
66
|
+
emitCompactEvent,
|
|
67
|
+
compactEventType,
|
|
68
|
+
} from './loop/compact-policy.mjs';
|
|
69
|
+
import {
|
|
70
|
+
isEagerDispatchable,
|
|
71
|
+
messagesArrayChanged,
|
|
72
|
+
getToolKind,
|
|
73
|
+
normalizeHookUpdatedToolOutput,
|
|
74
|
+
resolveToolResultAfterHook,
|
|
75
|
+
parseNativeToolSearchPayload,
|
|
76
|
+
buildAgentBashSessionArgs,
|
|
77
|
+
formatMissingToolApprovalUiDenial,
|
|
78
|
+
resolvePreToolAskApproval,
|
|
79
|
+
approvalGranted,
|
|
80
|
+
approvalReason,
|
|
81
|
+
} from './loop/tool-helpers.mjs';
|
|
82
|
+
import {
|
|
83
|
+
compactToolCallsForHistory,
|
|
84
|
+
restoreToolCallBodyForId,
|
|
85
|
+
} from './loop/stored-tool-args.mjs';
|
|
86
|
+
import { repairTranscriptBeforeProviderSend } from './loop/transcript-repair.mjs';
|
|
87
|
+
import { classifyTerminationReason, INCOMPLETE_STOP_REASONS } from './loop/termination.mjs';
|
|
88
|
+
import { createSteeringLadder } from './loop/steering-ladder.mjs';
|
|
265
89
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
90
|
+
// Facade re-exports: these symbols moved to split modules under ./loop/ but
|
|
91
|
+
// remain part of loop.mjs's public surface (imported by scripts/tests and other
|
|
92
|
+
// runtime modules). Re-export the already-imported local bindings so every
|
|
93
|
+
// existing import path keeps working (no duplicate module binding).
|
|
94
|
+
export {
|
|
95
|
+
preDispatchDenyForSession,
|
|
96
|
+
repairTranscriptBeforeProviderSend,
|
|
97
|
+
normalizeHookUpdatedToolOutput,
|
|
98
|
+
resolveToolResultAfterHook,
|
|
99
|
+
buildAgentBashSessionArgs,
|
|
100
|
+
formatMissingToolApprovalUiDenial,
|
|
101
|
+
resolvePreToolAskApproval,
|
|
102
|
+
approvalGranted,
|
|
103
|
+
approvalReason,
|
|
104
|
+
};
|
|
278
105
|
|
|
279
|
-
// Cache-hit results always inline the cached body. The earlier size-gated
|
|
280
|
-
// `[cache-hit-ref]` branch confused agents whose context did not
|
|
281
|
-
// contain the referenced prior tool_result, triggering shell-cat detours.
|
|
282
106
|
// Hard iteration ceiling for every agent loop. Reset to 0 whenever the
|
|
283
107
|
// transcript is compacted (see the trim block below): a long task that keeps
|
|
284
108
|
// compacting can proceed past this count, while a tight NON-compacting loop
|
|
@@ -289,982 +113,8 @@ function agentContextOverflowError({ stage, sessionId, sessionRef, model, budget
|
|
|
289
113
|
// this catches tight deterministic-failure loops (e.g. a command that errors
|
|
290
114
|
// the same way every time) far earlier than 100 iterations.
|
|
291
115
|
const REPEAT_FAIL_LIMIT = 3;
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
function _getHiddenAgents() {
|
|
295
|
-
if (_hiddenAgentsCache) return _hiddenAgentsCache;
|
|
296
|
-
try {
|
|
297
|
-
_hiddenAgentsCache = JSON.parse(_readFileSync(_AGENTS_JSON, 'utf8'));
|
|
298
|
-
} catch { _hiddenAgentsCache = { agents: [] }; }
|
|
299
|
-
return _hiddenAgentsCache;
|
|
300
|
-
}
|
|
301
|
-
// Transcript pairing guard. Anthropic 400-rejects when an assistant message
|
|
302
|
-
// ends with tool_use blocks and the next message isn't tool results for
|
|
303
|
-
// those exact ids. abort/timeout/error race in the loop body can leave a
|
|
304
|
-
// dangling assistant tool_use at the tail (e.g. the structure_probe loop
|
|
305
|
-
// running 12 deep then aborting between push-assistant and push-tool).
|
|
306
|
-
// Strip any trailing assistant tool_use that has no matching tool result
|
|
307
|
-
// so provider.send sees a valid transcript instead of leaking the 400 to
|
|
308
|
-
// the user. Repair runs every iteration but is a no-op on healthy paths.
|
|
309
|
-
function _ensureTranscriptPairing(msgs, sessionId) {
|
|
310
|
-
// Walk backwards to find the last assistant message that emitted
|
|
311
|
-
// tool_use, then validate that every id has a matching tool result
|
|
312
|
-
// inside the CONTIGUOUS tool-message block immediately following it.
|
|
313
|
-
// Earlier guard splice'd the entire tail — which silently deleted any
|
|
314
|
-
// user prompt appended after the dangling assistant by manager.mjs:
|
|
315
|
-
// when the guard fired with shape
|
|
316
|
-
// [..., assistant{a,b}, tool{a}, user{new prompt}]
|
|
317
|
-
// the splice removed user{new prompt} along with the orphan suffix.
|
|
318
|
-
// Fix: remove only assistant + the contiguous tool block; preserve
|
|
319
|
-
// anything past it (user / system / next assistant) untouched.
|
|
320
|
-
let popped = 0;
|
|
321
|
-
while (msgs.length > 0) {
|
|
322
|
-
let lastAssistantIdx = -1;
|
|
323
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
324
|
-
const m = msgs[i];
|
|
325
|
-
if (m?.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
|
|
326
|
-
lastAssistantIdx = i;
|
|
327
|
-
break;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
if (lastAssistantIdx === -1) break;
|
|
331
|
-
// Collect the contiguous tool messages directly after this assistant.
|
|
332
|
-
// Anything past that block is unrelated (next user prompt, system
|
|
333
|
-
// marker, etc.) and must survive the repair.
|
|
334
|
-
let toolBlockEnd = lastAssistantIdx + 1;
|
|
335
|
-
while (toolBlockEnd < msgs.length && msgs[toolBlockEnd]?.role === 'tool') {
|
|
336
|
-
toolBlockEnd += 1;
|
|
337
|
-
}
|
|
338
|
-
const toolBlock = msgs.slice(lastAssistantIdx + 1, toolBlockEnd);
|
|
339
|
-
const ids = msgs[lastAssistantIdx].toolCalls.map(c => c.id);
|
|
340
|
-
const matched = ids.every(id => toolBlock.some(m => m.toolCallId === id));
|
|
341
|
-
if (matched) break;
|
|
342
|
-
const removed = toolBlockEnd - lastAssistantIdx;
|
|
343
|
-
msgs.splice(lastAssistantIdx, removed);
|
|
344
|
-
popped += removed;
|
|
345
|
-
}
|
|
346
|
-
// Second sweep — catch dangling tool results that survived the
|
|
347
|
-
// contiguous-block splice. Anthropic strict spec requires every
|
|
348
|
-
// tool result to sit in a contiguous block right after the
|
|
349
|
-
// assistant whose toolCalls produced it; a `[..., assistant{a,b},
|
|
350
|
-
// tool{a}, user, tool{b}]` shape leaves tool{b} orphaned even
|
|
351
|
-
// after assistant + tool{a} are repaired by the loop above.
|
|
352
|
-
// Walk back from each tool message to the nearest non-tool
|
|
353
|
-
// ancestor; if it is not an assistant whose toolCalls include
|
|
354
|
-
// this id, drop the orphan.
|
|
355
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
356
|
-
const m = msgs[i];
|
|
357
|
-
if (m?.role !== 'tool') continue;
|
|
358
|
-
if (!m.toolCallId) {
|
|
359
|
-
msgs.splice(i, 1);
|
|
360
|
-
popped += 1;
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
let prevIdx = i - 1;
|
|
364
|
-
while (prevIdx >= 0 && msgs[prevIdx]?.role === 'tool') prevIdx--;
|
|
365
|
-
const anchor = prevIdx >= 0 ? msgs[prevIdx] : null;
|
|
366
|
-
const anchorOk = anchor?.role === 'assistant'
|
|
367
|
-
&& Array.isArray(anchor.toolCalls)
|
|
368
|
-
&& anchor.toolCalls.some(c => c.id === m.toolCallId);
|
|
369
|
-
if (!anchorOk) {
|
|
370
|
-
msgs.splice(i, 1);
|
|
371
|
-
popped += 1;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
if (popped > 0 && sessionId) {
|
|
375
|
-
try { process.stderr.write(`[transcript-repair] sess=${sessionId} popped=${popped} dangling assistant tool_use\n`); } catch {}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
/**
|
|
380
|
-
* Pre-provider transcript repair for the agent loop. Reattach valid tool
|
|
381
|
-
* results (non-destructive) before any destructive orphan pairing cleanup.
|
|
382
|
-
* Mutates `messages` in place to preserve the session array reference.
|
|
383
|
-
*/
|
|
384
|
-
export function repairTranscriptBeforeProviderSend(messages, sessionId = null) {
|
|
385
|
-
if (!Array.isArray(messages)) return messages;
|
|
386
|
-
const sanitized = sanitizeToolPairs(messages);
|
|
387
|
-
if (sanitized !== messages) {
|
|
388
|
-
messages.length = 0;
|
|
389
|
-
messages.push(...sanitized);
|
|
390
|
-
}
|
|
391
|
-
_ensureTranscriptPairing(messages, sessionId);
|
|
392
|
-
return messages;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
// Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
|
|
396
|
-
// to execute during SSE parsing so tool work overlaps with the rest of the
|
|
397
|
-
// stream. Writes, bash, MCP and skills stay serial after send() returns.
|
|
398
|
-
function isEagerDispatchable(name, tools) {
|
|
399
|
-
if (!Array.isArray(tools)) return false;
|
|
400
|
-
const def = tools.find(t => t?.name === name);
|
|
401
|
-
return def?.annotations?.readOnlyHint === true;
|
|
402
|
-
}
|
|
403
|
-
function messagesArrayChanged(before, after) {
|
|
404
|
-
if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
|
|
405
|
-
if (before.length !== after.length) return true;
|
|
406
|
-
for (let i = 0; i < before.length; i += 1) {
|
|
407
|
-
if (before[i] !== after[i]) return true;
|
|
408
|
-
}
|
|
409
|
-
return false;
|
|
410
|
-
}
|
|
411
|
-
function normalizeUsage(usage) {
|
|
412
|
-
if (!usage) return null;
|
|
413
|
-
const costUsd = Number(usage.costUsd);
|
|
414
|
-
return {
|
|
415
|
-
inputTokens: usage.inputTokens || 0,
|
|
416
|
-
outputTokens: usage.outputTokens || 0,
|
|
417
|
-
cachedTokens: usage.cachedTokens || 0,
|
|
418
|
-
cacheWriteTokens: usage.cacheWriteTokens || 0,
|
|
419
|
-
promptTokens: usage.promptTokens || 0,
|
|
420
|
-
...(Number.isFinite(costUsd) ? { costUsd } : {}),
|
|
421
|
-
raw: usage.raw,
|
|
422
|
-
};
|
|
423
|
-
}
|
|
424
|
-
function addUsage(total, usage) {
|
|
425
|
-
const delta = normalizeUsage(usage);
|
|
426
|
-
if (!delta) return total;
|
|
427
|
-
if (!total) return { ...delta };
|
|
428
|
-
const next = {
|
|
429
|
-
...total,
|
|
430
|
-
inputTokens: (total.inputTokens || 0) + delta.inputTokens,
|
|
431
|
-
outputTokens: (total.outputTokens || 0) + delta.outputTokens,
|
|
432
|
-
cachedTokens: (total.cachedTokens || 0) + delta.cachedTokens,
|
|
433
|
-
cacheWriteTokens: (total.cacheWriteTokens || 0) + delta.cacheWriteTokens,
|
|
434
|
-
promptTokens: (total.promptTokens || 0) + delta.promptTokens,
|
|
435
|
-
};
|
|
436
|
-
if (delta.costUsd != null || total.costUsd != null) {
|
|
437
|
-
next.costUsd = (total.costUsd || 0) + (delta.costUsd || 0);
|
|
438
|
-
}
|
|
439
|
-
return next;
|
|
440
|
-
}
|
|
441
|
-
function positiveTokenInt(value) {
|
|
442
|
-
const n = Number(value);
|
|
443
|
-
return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
|
|
444
|
-
}
|
|
445
|
-
function envFlag(name, fallback = false) {
|
|
446
|
-
const v = process.env[name];
|
|
447
|
-
if (v === undefined) return fallback;
|
|
448
|
-
return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
|
|
449
|
-
}
|
|
450
|
-
function envTokenInt(name) {
|
|
451
|
-
return positiveTokenInt(process.env[name]);
|
|
452
|
-
}
|
|
453
|
-
function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
|
|
454
|
-
if (process.env.MIXDOG_AGENT_COMPACT_SEMANTIC !== undefined) return envFlag('MIXDOG_AGENT_COMPACT_SEMANTIC', true);
|
|
455
|
-
if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
|
|
456
|
-
if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
|
|
457
|
-
// The compact type is already explicit (`semantic` by default). Honor it
|
|
458
|
-
// directly instead of substituting another compaction path.
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
function resolveCompactTypeSetting(_sessionRef, cfg = {}) {
|
|
463
|
-
const configured = process.env.MIXDOG_AGENT_COMPACT_TYPE
|
|
464
|
-
?? process.env.MIXDOG_COMPACT_TYPE
|
|
465
|
-
?? cfg.type
|
|
466
|
-
?? cfg.compactType
|
|
467
|
-
?? cfg.compact_type;
|
|
468
|
-
return normalizeCompactType(configured, DEFAULT_COMPACT_TYPE);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
async function runRecallFastTrackCompact({ sessionRef, messages, compactBudgetTokens, compactPolicy, sessionId, signal }) {
|
|
472
|
-
if (!sessionId) throw new Error('recall-fasttrack requires a session id');
|
|
473
|
-
const startedAt = Date.now();
|
|
474
|
-
const diagnostics = {
|
|
475
|
-
hydrateLimit: null,
|
|
476
|
-
ingestMs: null,
|
|
477
|
-
ingestSkipped: false,
|
|
478
|
-
ingestError: null,
|
|
479
|
-
initialDumpMs: null,
|
|
480
|
-
initialDumpBytes: null,
|
|
481
|
-
initialDumpChars: null,
|
|
482
|
-
initialRawPending: null,
|
|
483
|
-
cycle1Ms: null,
|
|
484
|
-
cycle1Skipped: false,
|
|
485
|
-
cycle1SkipReason: null,
|
|
486
|
-
cycle1Passes: null,
|
|
487
|
-
cycle1RawRemaining: null,
|
|
488
|
-
cycle1TextBytes: null,
|
|
489
|
-
cycle1Error: null,
|
|
490
|
-
finalRecallBytes: null,
|
|
491
|
-
finalRecallChars: null,
|
|
492
|
-
totalMs: null,
|
|
493
|
-
};
|
|
494
|
-
const query = `session:${sessionId}:all-chunks`;
|
|
495
|
-
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
496
|
-
const callerCtx = {
|
|
497
|
-
callerSessionId: sessionId || null,
|
|
498
|
-
callerCwd: sessionRef?.cwd || undefined,
|
|
499
|
-
routingSessionId: sessionId || null,
|
|
500
|
-
clientHostPid: sessionRef?.clientHostPid,
|
|
501
|
-
signal: signal || null,
|
|
502
|
-
};
|
|
503
|
-
const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
|
|
504
|
-
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
505
|
-
diagnostics.hydrateLimit = hydrateLimit;
|
|
506
|
-
let t0 = Date.now();
|
|
507
|
-
try {
|
|
508
|
-
await executeInternalTool('memory', {
|
|
509
|
-
action: 'ingest_session',
|
|
510
|
-
sessionId,
|
|
511
|
-
messages,
|
|
512
|
-
cwd: sessionRef?.cwd,
|
|
513
|
-
limit: hydrateLimit,
|
|
514
|
-
}, callerCtx);
|
|
515
|
-
} catch (err) {
|
|
516
|
-
diagnostics.ingestSkipped = true;
|
|
517
|
-
diagnostics.ingestError = compactDiagnosticError(err);
|
|
518
|
-
try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
519
|
-
} finally {
|
|
520
|
-
diagnostics.ingestMs = Date.now() - t0;
|
|
521
|
-
}
|
|
522
|
-
const dumpArgs = {
|
|
523
|
-
action: 'dump_session_roots',
|
|
524
|
-
sessionId,
|
|
525
|
-
includeRaw: true,
|
|
526
|
-
limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
|
|
527
|
-
};
|
|
528
|
-
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
529
|
-
t0 = Date.now();
|
|
530
|
-
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
531
|
-
diagnostics.initialDumpMs = Date.now() - t0;
|
|
532
|
-
diagnostics.initialDumpChars = String(recallText || '').length;
|
|
533
|
-
diagnostics.initialDumpBytes = compactByteLength(recallText);
|
|
534
|
-
diagnostics.initialRawPending = countRawPendingRows(recallText);
|
|
535
|
-
let cycle1Text = '';
|
|
536
|
-
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
537
|
-
if (hasRawRows) {
|
|
538
|
-
t0 = Date.now();
|
|
539
|
-
try {
|
|
540
|
-
// Drain this session's cycle1 in window×concurrency units until no
|
|
541
|
-
// raw rows remain, so the injected root is fully chunked rather than
|
|
542
|
-
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
543
|
-
const drained = await drainSessionCycle1(runTool, {
|
|
544
|
-
sessionId,
|
|
545
|
-
dumpArgs,
|
|
546
|
-
deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
547
|
-
maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
|
|
548
|
-
cycleArgs: {
|
|
549
|
-
min_batch: 1,
|
|
550
|
-
session_cap: 1,
|
|
551
|
-
batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
|
|
552
|
-
rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
|
|
553
|
-
window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
|
|
554
|
-
concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
|
|
555
|
-
},
|
|
556
|
-
});
|
|
557
|
-
recallText = drained.recallText;
|
|
558
|
-
cycle1Text = drained.cycle1Text;
|
|
559
|
-
diagnostics.cycle1Passes = drained.passes;
|
|
560
|
-
diagnostics.cycle1RawRemaining = drained.rawRemaining;
|
|
561
|
-
diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
|
|
562
|
-
if (drained.rawRemaining > 0) {
|
|
563
|
-
try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
|
|
564
|
-
}
|
|
565
|
-
} catch (err) {
|
|
566
|
-
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
567
|
-
try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
568
|
-
} finally {
|
|
569
|
-
diagnostics.cycle1Ms = Date.now() - t0;
|
|
570
|
-
}
|
|
571
|
-
} else {
|
|
572
|
-
diagnostics.cycle1Skipped = true;
|
|
573
|
-
diagnostics.cycle1SkipReason = 'session chunks already hydrated';
|
|
574
|
-
diagnostics.cycle1Passes = 0;
|
|
575
|
-
diagnostics.cycle1RawRemaining = 0;
|
|
576
|
-
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
577
|
-
}
|
|
578
|
-
const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
|
|
579
|
-
diagnostics.finalRecallChars = combinedRecallText.length;
|
|
580
|
-
diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
|
|
581
|
-
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
582
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
583
|
-
force: true,
|
|
584
|
-
recallText: combinedRecallText,
|
|
585
|
-
query,
|
|
586
|
-
querySha,
|
|
587
|
-
allowEmptyRecall: true,
|
|
588
|
-
tailTurns: compactPolicy.tailTurns,
|
|
589
|
-
keepTokens: compactPolicy.keepTokens,
|
|
590
|
-
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
591
|
-
});
|
|
592
|
-
diagnostics.totalMs = Date.now() - startedAt;
|
|
593
|
-
if (result && typeof result === 'object') {
|
|
594
|
-
result.diagnostics = {
|
|
595
|
-
...(result.diagnostics || {}),
|
|
596
|
-
pipeline: diagnostics,
|
|
597
|
-
};
|
|
598
|
-
}
|
|
599
|
-
compactDebugLog('recall-fasttrack pipeline', diagnostics);
|
|
600
|
-
return result;
|
|
601
|
-
}
|
|
602
|
-
const COMPACT_TARGET_RATIO = 0.02;
|
|
603
|
-
const COMPACT_TARGET_MIN_TOKENS = 4_000;
|
|
604
|
-
const COMPACT_TARGET_MAX_TOKENS = 16_000;
|
|
605
|
-
function resolveCompactTargetRatio(cfg = {}) {
|
|
606
|
-
const raw = cfg.targetPercent
|
|
607
|
-
?? cfg.targetPct
|
|
608
|
-
?? cfg.targetRatio
|
|
609
|
-
?? cfg.targetFraction
|
|
610
|
-
?? process.env.MIXDOG_AGENT_COMPACT_TARGET_PERCENT
|
|
611
|
-
?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
|
|
612
|
-
?? COMPACT_TARGET_RATIO;
|
|
613
|
-
const n = Number(raw);
|
|
614
|
-
if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
|
|
615
|
-
return n > 1 ? n / 100 : n;
|
|
616
|
-
}
|
|
617
|
-
function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
|
|
618
|
-
const boundary = positiveTokenInt(boundaryTokens);
|
|
619
|
-
if (!boundary) return null;
|
|
620
|
-
const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
|
|
621
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_TOKENS')
|
|
622
|
-
|| envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
|
|
623
|
-
if (explicit) return Math.max(1, Math.min(boundary, explicit));
|
|
624
|
-
const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
|
|
625
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MIN_TOKENS')
|
|
626
|
-
|| envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
|
|
627
|
-
|| COMPACT_TARGET_MIN_TOKENS);
|
|
628
|
-
const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
|
|
629
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_TARGET_MAX_TOKENS')
|
|
630
|
-
|| envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
|
|
631
|
-
|| COMPACT_TARGET_MAX_TOKENS);
|
|
632
|
-
const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
|
|
633
|
-
return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
|
|
634
|
-
}
|
|
635
|
-
function resolveCompactKeepTokens(cfg = {}) {
|
|
636
|
-
return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
|
|
637
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_KEEP_TOKENS')
|
|
638
|
-
|| DEFAULT_COMPACTION_KEEP_TOKENS;
|
|
639
|
-
}
|
|
640
|
-
function resolveWorkerCompactPolicy(sessionRef, tools) {
|
|
641
|
-
if (!sessionRef) return null;
|
|
642
|
-
const cfg = sessionRef.compaction || {};
|
|
643
|
-
const auto = cfg.auto !== false && envFlag('MIXDOG_AGENT_COMPACT_AUTO', true);
|
|
644
|
-
if (!auto) return { auto: false };
|
|
645
|
-
const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
|
|
646
|
-
const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
|
|
647
|
-
const autoLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit ?? cfg.autoCompactTokenLimit);
|
|
648
|
-
const boundaryTokens = explicitBoundary && contextWindow
|
|
649
|
-
? Math.min(explicitBoundary, contextWindow)
|
|
650
|
-
: (explicitBoundary || contextWindow || autoLimit);
|
|
651
|
-
if (!boundaryTokens) return null;
|
|
652
|
-
const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
|
|
653
|
-
// Only an explicit auto-compact limit STRICTLY BELOW the boundary acts as
|
|
654
|
-
// the trigger. A persisted value == boundary (legacy derived full-window
|
|
655
|
-
// autoCompactTokenLimit) would set autoTriggerTokens == boundary and
|
|
656
|
-
// collapse/override the default trigger, so it is ignored in favor of the
|
|
657
|
-
// default boundary trigger.
|
|
658
|
-
const autoTriggerTokens = autoLimit && autoLimit < compactBoundaryTokens ? Math.max(1, autoLimit) : null;
|
|
659
|
-
// Sanitized explicit limit: only a sub-boundary value is a real auto-compact
|
|
660
|
-
// limit. Anything >= boundary is a legacy derived full-window artifact and
|
|
661
|
-
// is reported as null so rememberCompactTelemetry does not re-persist it
|
|
662
|
-
// back onto the session and re-collapse the buffer on the next turn.
|
|
663
|
-
const explicitAutoCompactTokenLimit = autoTriggerTokens;
|
|
664
|
-
const bufferTokens = autoTriggerTokens
|
|
665
|
-
? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
|
|
666
|
-
: resolveCompactBufferTokens(compactBoundaryTokens, cfg);
|
|
667
|
-
const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
|
|
668
|
-
const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
|
|
669
|
-
const configuredReserve = positiveTokenInt(cfg.reservedTokens)
|
|
670
|
-
|| envTokenInt('MIXDOG_AGENT_COMPACT_RESERVED_TOKENS')
|
|
671
|
-
|| 0;
|
|
672
|
-
const requestReserve = estimateRequestReserveTokens(tools);
|
|
673
|
-
const keepTokens = resolveCompactKeepTokens(cfg);
|
|
674
|
-
const compactType = resolveCompactTypeSetting(sessionRef, cfg);
|
|
675
|
-
return {
|
|
676
|
-
auto: true,
|
|
677
|
-
type: compactType,
|
|
678
|
-
compactType,
|
|
679
|
-
prune: cfg.prune === true || envFlag('MIXDOG_AGENT_COMPACT_PRUNE', false),
|
|
680
|
-
boundaryTokens: compactBoundaryTokens,
|
|
681
|
-
triggerTokens,
|
|
682
|
-
bufferTokens,
|
|
683
|
-
bufferRatio,
|
|
684
|
-
contextWindow,
|
|
685
|
-
rawContextWindow: positiveTokenInt(sessionRef.rawContextWindow ?? cfg.rawContextWindow) || contextWindow,
|
|
686
|
-
effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
|
|
687
|
-
? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
|
|
688
|
-
: null,
|
|
689
|
-
autoCompactTokenLimit: explicitAutoCompactTokenLimit,
|
|
690
|
-
semantic: compactTypeIsSemantic(compactType) && resolveSemanticCompactSetting(sessionRef, cfg),
|
|
691
|
-
recallFastTrack: compactTypeIsRecallFastTrack(compactType),
|
|
692
|
-
semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_AGENT_COMPACT_TIMEOUT_MS') || 30_000,
|
|
693
|
-
tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_AGENT_COMPACT_TAIL_TURNS') || 2,
|
|
694
|
-
keepTokens,
|
|
695
|
-
preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_AGENT_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
|
|
696
|
-
reserveTokens: requestReserve + configuredReserve,
|
|
697
|
-
requestReserveTokens: requestReserve,
|
|
698
|
-
configuredReserveTokens: configuredReserve,
|
|
699
|
-
};
|
|
700
|
-
}
|
|
701
|
-
/** Transcript + request reserve only (never provider lastContextTokens). */
|
|
702
|
-
function compactPressureTokens(messageTokensEst, policy) {
|
|
703
|
-
if (messageTokensEst === null) return 0;
|
|
704
|
-
return Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
/** Telemetry pressure when a reactive overflow retry forces the next compact. */
|
|
708
|
-
function compactionTelemetryPressureTokens(messageTokensEst, policy, { reactivePending = false } = {}) {
|
|
709
|
-
const base = compactPressureTokens(messageTokensEst, policy);
|
|
710
|
-
if (!reactivePending) return base;
|
|
711
|
-
const floor = positiveTokenInt(policy?.triggerTokens) || positiveTokenInt(policy?.boundaryTokens) || 0;
|
|
712
|
-
return floor ? Math.max(base, floor) : base;
|
|
713
|
-
}
|
|
714
|
-
function compactTargetBudget(policy) {
|
|
715
|
-
const boundary = positiveTokenInt(policy?.boundaryTokens);
|
|
716
|
-
if (!boundary) return null;
|
|
717
|
-
const reserve = Math.max(0, Number(policy?.reserveTokens) || 0);
|
|
718
|
-
const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
|
|
719
|
-
return Math.max(1, Math.min(boundary, targetEffective + reserve));
|
|
720
|
-
}
|
|
721
|
-
function shouldCompactForSession(messageTokensEst, policy, { forceReactive = false } = {}) {
|
|
722
|
-
if (!policy?.auto || !policy.boundaryTokens) return false;
|
|
723
|
-
if (forceReactive) return true;
|
|
724
|
-
if (messageTokensEst === null) return true;
|
|
725
|
-
return compactPressureTokens(messageTokensEst, policy) >= (policy.triggerTokens || policy.boundaryTokens);
|
|
726
|
-
}
|
|
727
|
-
function countPrunedToolOutputs(before, after) {
|
|
728
|
-
if (!Array.isArray(before) || !Array.isArray(after)) return 0;
|
|
729
|
-
let count = 0;
|
|
730
|
-
const n = Math.min(before.length, after.length);
|
|
731
|
-
for (let i = 0; i < n; i += 1) {
|
|
732
|
-
if (before[i]?.role !== 'tool' || after[i]?.role !== 'tool') continue;
|
|
733
|
-
if (before[i]?.content !== after[i]?.content && after[i]?.compactedKind === 'tool_output_prune') count += 1;
|
|
734
|
-
}
|
|
735
|
-
return count;
|
|
736
|
-
}
|
|
737
|
-
function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
|
|
738
|
-
if (!sessionRef || !policy) return;
|
|
739
|
-
const prev = sessionRef.compaction && typeof sessionRef.compaction === 'object'
|
|
740
|
-
? sessionRef.compaction
|
|
741
|
-
: {};
|
|
742
|
-
const changed = meta.compactChanged === true || meta.pruneCount > 0;
|
|
743
|
-
sessionRef.compaction = {
|
|
744
|
-
...prev,
|
|
745
|
-
auto: policy.auto !== false,
|
|
746
|
-
prune: policy.prune === true,
|
|
747
|
-
reservedTokens: policy.configuredReserveTokens || prev.reservedTokens || null,
|
|
748
|
-
requestReserveTokens: policy.requestReserveTokens || 0,
|
|
749
|
-
reserveTokens: policy.reserveTokens || 0,
|
|
750
|
-
boundaryTokens: policy.boundaryTokens || null,
|
|
751
|
-
triggerTokens: policy.triggerTokens || null,
|
|
752
|
-
bufferTokens: policy.bufferTokens || 0,
|
|
753
|
-
bufferRatio: policy.bufferRatio ?? prev.bufferRatio ?? null,
|
|
754
|
-
contextWindow: policy.contextWindow || null,
|
|
755
|
-
rawContextWindow: policy.rawContextWindow || null,
|
|
756
|
-
effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
|
|
757
|
-
autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
|
|
758
|
-
type: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
759
|
-
compactType: policy.compactType || policy.type || DEFAULT_COMPACT_TYPE,
|
|
760
|
-
semantic: policy.semantic === true ? 'auto' : false,
|
|
761
|
-
recallFastTrack: policy.recallFastTrack === true,
|
|
762
|
-
semanticModel: policy.semanticModel || null,
|
|
763
|
-
semanticTimeoutMs: policy.semanticTimeoutMs || null,
|
|
764
|
-
tailTurns: policy.tailTurns || null,
|
|
765
|
-
keepTokens: policy.keepTokens || null,
|
|
766
|
-
preserveRecentTokens: policy.preserveRecentTokens || null,
|
|
767
|
-
lastCheckedAt: Date.now(),
|
|
768
|
-
lastBeforeTokens: meta.beforeTokens ?? null,
|
|
769
|
-
lastAfterTokens: meta.afterTokens ?? null,
|
|
770
|
-
lastPressureTokens: meta.pressureTokens ?? null,
|
|
771
|
-
currentEstimatedTokens: meta.pressureTokens ?? prev.currentEstimatedTokens ?? null,
|
|
772
|
-
lastApiRequestTokens: positiveTokenInt(sessionRef?.lastContextTokens) || prev.lastApiRequestTokens || null,
|
|
773
|
-
lastStage: meta.stage || prev.lastStage || null,
|
|
774
|
-
lastChanged: changed,
|
|
775
|
-
lastTrigger: meta.trigger || prev.lastTrigger || null,
|
|
776
|
-
lastSemantic: meta.semanticCompact === true,
|
|
777
|
-
lastSemanticError: Object.hasOwn(meta, 'semanticError')
|
|
778
|
-
? (meta.semanticError ?? null)
|
|
779
|
-
: (prev.lastSemanticError ?? null),
|
|
780
|
-
lastRecallFastTrack: meta.recallFastTrack === true,
|
|
781
|
-
lastRecallFastTrackError: Object.hasOwn(meta, 'recallFastTrackError')
|
|
782
|
-
? (meta.recallFastTrackError ?? null)
|
|
783
|
-
: (prev.lastRecallFastTrackError ?? null),
|
|
784
|
-
lastError: Object.hasOwn(meta, 'compactError') || Object.hasOwn(meta, 'lastError')
|
|
785
|
-
? (meta.compactError ?? meta.lastError ?? null)
|
|
786
|
-
: (prev.lastError ?? null),
|
|
787
|
-
lastPruneCount: meta.pruneCount || 0,
|
|
788
|
-
lastDurationMs: meta.durationMs != null && Number.isFinite(Number(meta.durationMs))
|
|
789
|
-
? Math.max(0, Math.round(Number(meta.durationMs)))
|
|
790
|
-
: null,
|
|
791
|
-
compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
|
|
792
|
-
};
|
|
793
|
-
if (changed) {
|
|
794
|
-
const changedAt = Date.now();
|
|
795
|
-
sessionRef.compaction.lastChangedAt = changedAt;
|
|
796
|
-
sessionRef.compaction.lastCompactAt = changedAt;
|
|
797
|
-
sessionRef.lastContextTokensStaleAfterCompact = true;
|
|
798
|
-
}
|
|
799
|
-
sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
|
|
800
|
-
sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
|
|
801
|
-
sessionRef.compactBoundaryTokens = policy.boundaryTokens || sessionRef.compactBoundaryTokens || null;
|
|
802
|
-
// Persist only the sanitized (sub-boundary) explicit limit. policy.autoCompactTokenLimit
|
|
803
|
-
// is already null for legacy derived full-window values, so a stale
|
|
804
|
-
// boundary-sized autoCompactTokenLimit on the session is cleared here rather
|
|
805
|
-
// than carried forward to re-collapse the buffer next turn.
|
|
806
|
-
{
|
|
807
|
-
const _boundary = positiveTokenInt(sessionRef.compactBoundaryTokens);
|
|
808
|
-
const _prevLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit);
|
|
809
|
-
const _keepPrev = _prevLimit && (!_boundary || _prevLimit < _boundary) ? _prevLimit : null;
|
|
810
|
-
sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || _keepPrev || null;
|
|
811
|
-
}
|
|
812
|
-
if (policy.effectiveContextWindowPercent !== null) {
|
|
813
|
-
sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
|
|
817
|
-
function emitCompactEvent(opts, event = {}) {
|
|
818
|
-
if (!opts || typeof opts.onCompactEvent !== 'function') return;
|
|
819
|
-
try { opts.onCompactEvent({ ts: Date.now(), ...event }); }
|
|
820
|
-
catch { /* best-effort UI/log hook */ }
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
function compactEventType(policy, fallback = DEFAULT_COMPACT_TYPE) {
|
|
824
|
-
return policy?.compactType || policy?.type || fallback;
|
|
825
|
-
}
|
|
826
|
-
const SKILL_TOOL_NAMES = new Set(['Skill', 'skills_list', 'skill_view']);
|
|
827
|
-
const SPECIAL_TOOL_NAMES = new Set(['bash_session', 'apply_patch', 'code_graph']);
|
|
828
|
-
const BASH_SESSION_HEADER_RE = /\[session: ([^\]\r\n]+)\]/;
|
|
829
|
-
const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
|
|
830
|
-
const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
|
|
831
|
-
const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
|
|
832
|
-
const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
|
|
833
|
-
const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
|
|
834
|
-
const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
|
|
835
|
-
|
|
836
|
-
function compactStoredToolArgString(value, key = '') {
|
|
837
|
-
if (typeof value !== 'string') return value;
|
|
838
|
-
const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
|
|
839
|
-
const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
|
|
840
|
-
const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
|
|
841
|
-
if (value.length <= limit) return value;
|
|
842
|
-
const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
|
|
843
|
-
const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
|
|
844
|
-
const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
|
|
845
|
-
return `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
|
|
846
|
-
}
|
|
847
|
-
|
|
848
|
-
function compactStoredToolArgValue(value, key = '', depth = 0) {
|
|
849
|
-
if (value === null || value === undefined) return value;
|
|
850
|
-
if (typeof value === 'string') return compactStoredToolArgString(value, key);
|
|
851
|
-
if (typeof value !== 'object') return value;
|
|
852
|
-
if (depth >= 6) return Array.isArray(value) ? `[${value.length} items]` : '{...}';
|
|
853
|
-
if (Array.isArray(value)) {
|
|
854
|
-
return value.map((item) => compactStoredToolArgValue(item, key, depth + 1));
|
|
855
|
-
}
|
|
856
|
-
const out = {};
|
|
857
|
-
for (const [k, v] of Object.entries(value)) {
|
|
858
|
-
out[k] = compactStoredToolArgValue(v, k, depth + 1);
|
|
859
|
-
}
|
|
860
|
-
return out;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
function compactToolCallsForHistory(calls) {
|
|
864
|
-
if (!Array.isArray(calls)) return calls;
|
|
865
|
-
return calls.map((call) => {
|
|
866
|
-
if (!call || typeof call !== 'object') return call;
|
|
867
|
-
return {
|
|
868
|
-
...call,
|
|
869
|
-
arguments: compactStoredToolArgValue(call.arguments),
|
|
870
|
-
};
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Restore the FULL body of ONE tool call inside a history assistant message
|
|
875
|
-
// whose toolCalls were compacted at push time. Used for a failed edit call so
|
|
876
|
-
// the model sees the original patch/old_string on retry instead of a
|
|
877
|
-
// `[mixdog compacted …]` placeholder it cannot act on. Must run BEFORE the
|
|
878
|
-
// message is first transmitted so it never mutates an already-cached prefix
|
|
879
|
-
// (the prompt cache is content-prefix matched).
|
|
880
|
-
//
|
|
881
|
-
// Only the compactable body/long keys (patch, old_string, new_string, content,
|
|
882
|
-
// rewrite, command, script) are restored, and at ANY depth — compaction is
|
|
883
|
-
// recursive (compactStoredToolArgValue), so batch shapes like edits[].old_string
|
|
884
|
-
// or writes[].content carry nested compacted bodies too. Every other field
|
|
885
|
-
// (e.g. `path`, which a tool may mutate in place during execution) is taken from
|
|
886
|
-
// the compacted snapshot captured at push time, before any mutation. The
|
|
887
|
-
// compacted args tree is built fresh by compactToolCallsForHistory and is not
|
|
888
|
-
// shared with originalCalls, so rebuilding it here is safe.
|
|
889
|
-
function restoreToolCallBodyForId(assistantMsg, originalCalls, callId) {
|
|
890
|
-
if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
|
|
891
|
-
if (!Array.isArray(originalCalls)) return;
|
|
892
|
-
const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
|
|
893
|
-
const orig = originalCalls.find((c) => c && c.id === callId);
|
|
894
|
-
if (!tc || !orig) return;
|
|
895
|
-
if (!tc.arguments || typeof tc.arguments !== 'object'
|
|
896
|
-
|| !orig.arguments || typeof orig.arguments !== 'object') return;
|
|
897
|
-
tc.arguments = _restoreCompactedBodies(tc.arguments, orig.arguments, '');
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
// Recursively rebuild a compacted args tree: replace ONLY compactable body/long
|
|
901
|
-
// string fields (matched by key at any depth) with their full originals, and
|
|
902
|
-
// keep every other field from the compacted snapshot. tcVal and origVal share
|
|
903
|
-
// the same structure (compaction only shortens body strings), so the walk
|
|
904
|
-
// descends them in parallel; a missing or non-object origVal falls back to the
|
|
905
|
-
// compacted value rather than throwing.
|
|
906
|
-
function _restoreCompactedBodies(tcVal, origVal, key) {
|
|
907
|
-
if ((STORED_TOOL_ARG_BODY_KEY_RE.test(key) || STORED_TOOL_ARG_LONG_KEY_RE.test(key))
|
|
908
|
-
&& typeof origVal === 'string') {
|
|
909
|
-
return origVal;
|
|
910
|
-
}
|
|
911
|
-
if (Array.isArray(tcVal) && Array.isArray(origVal)) {
|
|
912
|
-
return tcVal.map((item, i) => _restoreCompactedBodies(item, origVal[i], key));
|
|
913
|
-
}
|
|
914
|
-
if (tcVal && typeof tcVal === 'object' && origVal && typeof origVal === 'object') {
|
|
915
|
-
const out = {};
|
|
916
|
-
for (const k of Object.keys(tcVal)) {
|
|
917
|
-
out[k] = (k in origVal) ? _restoreCompactedBodies(tcVal[k], origVal[k], k) : tcVal[k];
|
|
918
|
-
}
|
|
919
|
-
return out;
|
|
920
|
-
}
|
|
921
|
-
return tcVal;
|
|
922
|
-
}
|
|
923
|
-
/**
|
|
924
|
-
* Execute a single tool call — routes to MCP or builtin.
|
|
925
|
-
*/
|
|
926
|
-
function getToolKind(name) {
|
|
927
|
-
if (SKILL_TOOL_NAMES.has(name)) return 'skill';
|
|
928
|
-
if (SPECIAL_TOOL_NAMES.has(name)) return 'builtin';
|
|
929
|
-
if (isMcpTool(name)) return 'mcp';
|
|
930
|
-
if (isInternalTool(name)) return 'internal';
|
|
931
|
-
if (isBuiltinTool(name)) return 'builtin';
|
|
932
|
-
return 'builtin';
|
|
933
|
-
}
|
|
934
|
-
function buildSkillsListResponse(cwd) {
|
|
935
|
-
const skills = collectSkillsCached(cwd);
|
|
936
|
-
const entries = skills.map(s => ({ name: s.name, description: s.description || '' }));
|
|
937
|
-
return JSON.stringify({ skills: entries });
|
|
938
|
-
}
|
|
939
|
-
function viewSkill(cwd, name) {
|
|
940
|
-
if (!name) return 'Error: skill name is required';
|
|
941
|
-
const res = loadSkillResource(name, cwd);
|
|
942
|
-
if (!res) return `Error: skill "${name}" not found`;
|
|
943
|
-
// Return the general tool envelope: the model-visible tool_result is the
|
|
944
|
-
// short stub (`Loaded skill: <name>`) and the full SKILL.md body is
|
|
945
|
-
// delivered ONCE as a separate injected role:'user' message (newMessages).
|
|
946
|
-
return buildSkillToolEnvelope(name, res.content, res.dir);
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
/** Normalize PostToolUse hook override values (legacy MCP text envelopes only). */
|
|
950
|
-
export function normalizeHookUpdatedToolOutput(value) {
|
|
951
|
-
if (typeof value === 'string') return value;
|
|
952
|
-
if (value == null) return '';
|
|
953
|
-
if (typeof value === 'object' && Array.isArray(value.content)) {
|
|
954
|
-
const hasNonText = value.content.some((c) => c && typeof c === 'object' && c.type && c.type !== 'text');
|
|
955
|
-
if (hasNonText) return value;
|
|
956
|
-
return value.content
|
|
957
|
-
.map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
|
|
958
|
-
.join('\n');
|
|
959
|
-
}
|
|
960
|
-
return value;
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
export function resolveToolResultAfterHook(originalResult, hookResult) {
|
|
964
|
-
if (!hookResult || typeof hookResult !== 'object' || hookResult.updatedToolOutput === undefined) {
|
|
965
|
-
return originalResult;
|
|
966
|
-
}
|
|
967
|
-
const updated = normalizeHookUpdatedToolOutput(hookResult.updatedToolOutput);
|
|
968
|
-
return updated === undefined ? originalResult : updated;
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
function parseNativeToolSearchPayload(toolName, result) {
|
|
972
|
-
if (toolName !== 'tool_search' || typeof result !== 'string') return null;
|
|
973
|
-
try {
|
|
974
|
-
const parsed = JSON.parse(result);
|
|
975
|
-
const native = parsed?.nativeToolSearch;
|
|
976
|
-
if (!native || typeof native !== 'object') return null;
|
|
977
|
-
const toolReferences = Array.isArray(native.toolReferences)
|
|
978
|
-
? native.toolReferences.map((name) => String(name || '').trim()).filter(Boolean)
|
|
979
|
-
: [];
|
|
980
|
-
const openaiTools = Array.isArray(native.openaiTools)
|
|
981
|
-
? native.openaiTools.filter((tool) => tool && typeof tool === 'object')
|
|
982
|
-
: [];
|
|
983
|
-
if (!toolReferences.length && !openaiTools.length) return null;
|
|
984
|
-
return {
|
|
985
|
-
toolReferences,
|
|
986
|
-
openaiTools,
|
|
987
|
-
summary: typeof native.summary === 'string' && native.summary
|
|
988
|
-
? native.summary
|
|
989
|
-
: `Loaded deferred tools: ${toolReferences.join(', ') || openaiTools.map((tool) => tool.name).filter(Boolean).join(', ')}`,
|
|
990
|
-
};
|
|
991
|
-
} catch {
|
|
992
|
-
return null;
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
function extractBashSessionId(result) {
|
|
996
|
-
if (typeof result !== 'string') return null;
|
|
997
|
-
const match = BASH_SESSION_HEADER_RE.exec(result);
|
|
998
|
-
return match ? match[1] : null;
|
|
999
|
-
}
|
|
1000
|
-
|
|
1001
|
-
export function buildAgentBashSessionArgs(args, sessionRef) {
|
|
1002
|
-
if (!isAgentOwner(sessionRef)) return null;
|
|
1003
|
-
// run_in_background is a detached one-shot job, incompatible with the
|
|
1004
|
-
// persistent bash session. Fall through to the background-job path
|
|
1005
|
-
// (executeBuiltinTool -> startBackgroundShellJob) so the worker gets a
|
|
1006
|
-
// task_id that task control can resolve — otherwise the persistent
|
|
1007
|
-
// session returns a [session: ...] header and task control reports "task not found".
|
|
1008
|
-
if (args?.run_in_background === true) return null;
|
|
1009
|
-
const routedArgs = { ...(args || {}) };
|
|
1010
|
-
const explicitSessionId = typeof routedArgs.session_id === 'string' && routedArgs.session_id.trim()
|
|
1011
|
-
? routedArgs.session_id.trim()
|
|
1012
|
-
: null;
|
|
1013
|
-
const wantsPersistent = routedArgs.persistent === true || !!explicitSessionId;
|
|
1014
|
-
if (!wantsPersistent) return null;
|
|
1015
|
-
if (!explicitSessionId && sessionRef?.implicitBashSessionId) {
|
|
1016
|
-
routedArgs.session_id = sessionRef.implicitBashSessionId;
|
|
1017
|
-
} else if (explicitSessionId) {
|
|
1018
|
-
routedArgs.session_id = explicitSessionId;
|
|
1019
|
-
}
|
|
1020
|
-
delete routedArgs.persistent;
|
|
1021
|
-
return routedArgs;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
export function formatMissingToolApprovalUiDenial(toolName, askReason) {
|
|
1025
|
-
const reason = String(askReason || 'approval requested by hook').trim();
|
|
1026
|
-
const name = String(toolName || 'tool');
|
|
1027
|
-
return `Error: tool "${name}" denied by hook: approval required but no approval UI is available${reason ? ` (${reason})` : ''}`;
|
|
1028
|
-
}
|
|
1029
|
-
|
|
1030
|
-
/**
|
|
1031
|
-
* Resolve PreToolUse `{ action: 'ask' }` against an optional approval callback.
|
|
1032
|
-
* Returns `{ denial }` when the tool must not run; otherwise `{ approval }`.
|
|
1033
|
-
*/
|
|
1034
|
-
export async function resolvePreToolAskApproval({
|
|
1035
|
-
toolName,
|
|
1036
|
-
args,
|
|
1037
|
-
cwd,
|
|
1038
|
-
sessionId,
|
|
1039
|
-
toolCallId,
|
|
1040
|
-
askReason,
|
|
1041
|
-
toolApprovalHook,
|
|
1042
|
-
}) {
|
|
1043
|
-
const name = String(toolName || 'tool');
|
|
1044
|
-
const reason = String(askReason || 'approval requested by hook').trim();
|
|
1045
|
-
if (typeof toolApprovalHook !== 'function') {
|
|
1046
|
-
return { denial: formatMissingToolApprovalUiDenial(name, reason) };
|
|
1047
|
-
}
|
|
1048
|
-
let approval;
|
|
1049
|
-
try {
|
|
1050
|
-
approval = await toolApprovalHook({
|
|
1051
|
-
name,
|
|
1052
|
-
args,
|
|
1053
|
-
cwd,
|
|
1054
|
-
sessionId,
|
|
1055
|
-
toolCallId: toolCallId || null,
|
|
1056
|
-
reason,
|
|
1057
|
-
});
|
|
1058
|
-
} catch (error) {
|
|
1059
|
-
const detail = error?.message || String(error || 'approval failed');
|
|
1060
|
-
return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
|
|
1061
|
-
}
|
|
1062
|
-
if (!approvalGranted(approval)) {
|
|
1063
|
-
const detail = approvalReason(approval, reason || 'not approved');
|
|
1064
|
-
return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
|
|
1065
|
-
}
|
|
1066
|
-
return { approval };
|
|
1067
|
-
}
|
|
1068
|
-
|
|
1069
|
-
function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
|
|
1070
|
-
if (executeOpts.scopedCacheOutcome) {
|
|
1071
|
-
if (sessionRef && toolCallId) {
|
|
1072
|
-
if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
|
|
1073
|
-
sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, executeOpts.scopedCacheOutcome);
|
|
1074
|
-
}
|
|
1075
|
-
return executeOpts.scopedCacheOutcome;
|
|
1076
|
-
}
|
|
1077
|
-
if (!callerSessionId || !toolCallId || !_isScopedCacheableTool(toolName)) return null;
|
|
1078
|
-
const outcome = createScopedCacheOutcome();
|
|
1079
|
-
if (sessionRef) {
|
|
1080
|
-
if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
|
|
1081
|
-
sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, outcome);
|
|
1082
|
-
}
|
|
1083
|
-
return outcome;
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
async function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
|
|
1087
|
-
const scopedCacheOutcome = _scopedCacheOutcomeForCall(
|
|
1088
|
-
sessionRef,
|
|
1089
|
-
executeOpts.toolCallId,
|
|
1090
|
-
name,
|
|
1091
|
-
callerSessionId,
|
|
1092
|
-
executeOpts,
|
|
1093
|
-
);
|
|
1094
|
-
const toolOpts = scopedCacheOutcome
|
|
1095
|
-
? { ...executeOpts, scopedCacheOutcome }
|
|
1096
|
-
: executeOpts;
|
|
1097
|
-
const notificationSessionId = String(executeOpts.notifySessionId || sessionRef?.ownerSessionId || callerSessionId || '').trim();
|
|
1098
|
-
const notifyFn = typeof executeOpts.notifyFn === 'function'
|
|
1099
|
-
? executeOpts.notifyFn
|
|
1100
|
-
: (text, meta = {}) => {
|
|
1101
|
-
if (!notificationSessionId) return;
|
|
1102
|
-
try {
|
|
1103
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
1104
|
-
if (visible) enqueuePendingMessage(notificationSessionId, visible);
|
|
1105
|
-
} catch { /* best effort */ }
|
|
1106
|
-
};
|
|
1107
|
-
const completionToolOpts = {
|
|
1108
|
-
...toolOpts,
|
|
1109
|
-
sessionId: callerSessionId,
|
|
1110
|
-
callerSessionId: notificationSessionId || callerSessionId,
|
|
1111
|
-
routingSessionId: callerSessionId,
|
|
1112
|
-
clientHostPid: sessionRef?.clientHostPid,
|
|
1113
|
-
notifyFn,
|
|
1114
|
-
};
|
|
1115
|
-
const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
|
|
1116
|
-
? executeOpts.beforeToolHook
|
|
1117
|
-
: sessionRef?.beforeToolHook;
|
|
1118
|
-
const toolApprovalHook = typeof executeOpts.toolApprovalHook === 'function'
|
|
1119
|
-
? executeOpts.toolApprovalHook
|
|
1120
|
-
: sessionRef?.toolApprovalHook;
|
|
1121
|
-
if (beforeToolHook) {
|
|
1122
|
-
try {
|
|
1123
|
-
const decision = await beforeToolHook({
|
|
1124
|
-
name,
|
|
1125
|
-
args,
|
|
1126
|
-
cwd,
|
|
1127
|
-
sessionId: callerSessionId,
|
|
1128
|
-
toolCallId: executeOpts.toolCallId || null,
|
|
1129
|
-
});
|
|
1130
|
-
const action = String(decision?.action || decision?.decision || '').toLowerCase();
|
|
1131
|
-
if (action === 'deny' || action === 'block') {
|
|
1132
|
-
const reason = decision?.reason ? `: ${decision.reason}` : '';
|
|
1133
|
-
return `Error: tool "${name}" denied by hook${reason}`;
|
|
1134
|
-
}
|
|
1135
|
-
if (action === 'ask') {
|
|
1136
|
-
const askReason = String(decision?.reason || 'approval requested by hook').trim();
|
|
1137
|
-
const askOutcome = await resolvePreToolAskApproval({
|
|
1138
|
-
toolName: name,
|
|
1139
|
-
args,
|
|
1140
|
-
cwd,
|
|
1141
|
-
sessionId: callerSessionId,
|
|
1142
|
-
toolCallId: executeOpts.toolCallId || null,
|
|
1143
|
-
askReason,
|
|
1144
|
-
toolApprovalHook,
|
|
1145
|
-
});
|
|
1146
|
-
if (askOutcome.denial) return askOutcome.denial;
|
|
1147
|
-
const approval = askOutcome.approval;
|
|
1148
|
-
if (approval && typeof approval === 'object' && approval.args && typeof approval.args === 'object' && !Array.isArray(approval.args)) {
|
|
1149
|
-
args = approval.args;
|
|
1150
|
-
}
|
|
1151
|
-
}
|
|
1152
|
-
if ((action === 'modify' || action === 'rewrite') && decision?.args && typeof decision.args === 'object' && !Array.isArray(decision.args)) {
|
|
1153
|
-
args = decision.args;
|
|
1154
|
-
}
|
|
1155
|
-
} catch {
|
|
1156
|
-
// Hooks are policy extensions. A broken hook must not wedge the agent loop.
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
const afterToolHook = typeof executeOpts.afterToolHook === 'function'
|
|
1160
|
-
? executeOpts.afterToolHook
|
|
1161
|
-
: sessionRef?.afterToolHook;
|
|
1162
|
-
const __result = await (async () => {
|
|
1163
|
-
if (name === 'Skill') {
|
|
1164
|
-
return viewSkill(cwd, args?.name);
|
|
1165
|
-
}
|
|
1166
|
-
if (name === 'skills_list') {
|
|
1167
|
-
return buildSkillsListResponse(cwd);
|
|
1168
|
-
}
|
|
1169
|
-
if (name === 'skill_view') {
|
|
1170
|
-
return viewSkill(cwd, args?.name);
|
|
1171
|
-
}
|
|
1172
|
-
if (isMcpTool(name)) {
|
|
1173
|
-
// 24h trace data shows ~24% of external MCP calls are cwd-sensitive
|
|
1174
|
-
// (bash / grep / read / list / glob etc.) but the worker session's
|
|
1175
|
-
// cwd was previously dropped here. Inject cwd only when the tool's
|
|
1176
|
-
// inputSchema declares the field — schemas without it would reject
|
|
1177
|
-
// an unknown argument.
|
|
1178
|
-
const needsCwdInjection = cwd
|
|
1179
|
-
&& mcpToolHasField(name, 'cwd')
|
|
1180
|
-
&& (args == null || args.cwd == null);
|
|
1181
|
-
const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
|
|
1182
|
-
return executeMcpTool(name, finalArgs);
|
|
1183
|
-
}
|
|
1184
|
-
if (name === 'code_graph') {
|
|
1185
|
-
// cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
|
|
1186
|
-
const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
|
|
1187
|
-
return executeCodeGraphToolLazy(name, args, graphCwd, null, toolOpts);
|
|
1188
|
-
}
|
|
1189
|
-
if (isInternalTool(name)) {
|
|
1190
|
-
// callerSessionId propagates into server.mjs dispatchTool so that
|
|
1191
|
-
// dispatchAiWrapped can detect and reject recursive calls from a
|
|
1192
|
-
// hidden-role session (recall/search/explore → self).
|
|
1193
|
-
return executeInternalTool(name, args, {
|
|
1194
|
-
callerSessionId,
|
|
1195
|
-
callerCwd: cwd,
|
|
1196
|
-
clientHostPid: sessionRef?.clientHostPid,
|
|
1197
|
-
signal: executeOpts.signal,
|
|
1198
|
-
routingSessionId: callerSessionId,
|
|
1199
|
-
notifyFn,
|
|
1200
|
-
});
|
|
1201
|
-
}
|
|
1202
|
-
if (name === 'shell') {
|
|
1203
|
-
const routedArgs = buildAgentBashSessionArgs(args, sessionRef);
|
|
1204
|
-
if (!routedArgs) {
|
|
1205
|
-
// clientHostPid scopes background shell-jobs to the dispatching
|
|
1206
|
-
// terminal's claude.exe pid (agent sessions store it on sessionRef);
|
|
1207
|
-
// without it resolveJobOwnerHostPid falls back to the daemon-global env.
|
|
1208
|
-
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1209
|
-
}
|
|
1210
|
-
// Thread the session's AbortSignal so agent type=close can interrupt the
|
|
1211
|
-
// persistent child process. getSessionAbortSignal is imported at top of
|
|
1212
|
-
// loop.mjs from manager.mjs; callerSessionId identifies the controller.
|
|
1213
|
-
let _bashAbortSignal = null;
|
|
1214
|
-
try { _bashAbortSignal = getSessionAbortSignal(callerSessionId); } catch { /* ignore */ }
|
|
1215
|
-
const result = await executeBashSessionTool('bash_session', routedArgs, cwd, {
|
|
1216
|
-
sessionId: callerSessionId,
|
|
1217
|
-
abortSignal: _bashAbortSignal,
|
|
1218
|
-
});
|
|
1219
|
-
const bashSid = extractBashSessionId(result);
|
|
1220
|
-
if (bashSid) {
|
|
1221
|
-
sessionRef.implicitBashSessionId = bashSid;
|
|
1222
|
-
// Track all persistent bash sessions for bulk teardown on close.
|
|
1223
|
-
if (sessionRef.allBashSessionIds) {
|
|
1224
|
-
if (!sessionRef.allBashSessionIds.includes(bashSid)) {
|
|
1225
|
-
sessionRef.allBashSessionIds.push(bashSid);
|
|
1226
|
-
}
|
|
1227
|
-
} else {
|
|
1228
|
-
sessionRef.allBashSessionIds = [bashSid];
|
|
1229
|
-
}
|
|
1230
|
-
}
|
|
1231
|
-
return result;
|
|
1232
|
-
}
|
|
1233
|
-
if (name === 'apply_patch') {
|
|
1234
|
-
const patchArgs = typeof args === 'string' ? { patch: args } : args;
|
|
1235
|
-
return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId, toolCallId: executeOpts.toolCallId || null });
|
|
1236
|
-
}
|
|
1237
|
-
if (isBuiltinTool(name)) {
|
|
1238
|
-
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
1239
|
-
// the bash branch above (see resolveJobOwnerHostPid).
|
|
1240
|
-
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
1241
|
-
}
|
|
1242
|
-
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
1243
|
-
})();
|
|
1244
|
-
if (typeof afterToolHook === 'function') {
|
|
1245
|
-
try {
|
|
1246
|
-
const hookResult = await afterToolHook({
|
|
1247
|
-
name,
|
|
1248
|
-
args,
|
|
1249
|
-
cwd,
|
|
1250
|
-
sessionId: callerSessionId,
|
|
1251
|
-
toolCallId: executeOpts.toolCallId || null,
|
|
1252
|
-
result: __result,
|
|
1253
|
-
});
|
|
1254
|
-
// Envelope-aware hook override: a PostToolUse hook may override the
|
|
1255
|
-
// model-VISIBLE tool output (the envelope's `result` / stub), but it
|
|
1256
|
-
// must NEVER drop the `newMessages` channel. Split first, apply the
|
|
1257
|
-
// override to `result` only, then re-wrap so newMessages survive.
|
|
1258
|
-
const { result: __res, newMessages: __nm } = normalizeToolEnvelope(__result);
|
|
1259
|
-
const __overridden = resolveToolResultAfterHook(__res, hookResult);
|
|
1260
|
-
if (__nm.length) return makeToolEnvelope(__overridden, __nm);
|
|
1261
|
-
return __overridden;
|
|
1262
|
-
} catch {
|
|
1263
|
-
// PostToolUse hooks are best-effort; never let one break the tool result.
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
return __result;
|
|
1267
|
-
}
|
|
116
|
+
// _scopedCacheOutcomeForCall and executeTool moved to ./loop/tool-exec.mjs
|
|
117
|
+
// (imported above).
|
|
1268
118
|
/**
|
|
1269
119
|
* Agent loop: send → tool_call → execute → re-send → repeat until text.
|
|
1270
120
|
* sendOpts may include:
|
|
@@ -1276,38 +126,11 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
1276
126
|
* wrapper can propagate a clean cancellation.
|
|
1277
127
|
* - `onStageChange(stage)` / `onStreamDelta()` — forwarded to provider.send for heartbeats
|
|
1278
128
|
*/
|
|
1279
|
-
// Source of truth: defaults/agents.json (loaded via _getHiddenAgents
|
|
1280
|
-
// above). Build the name Set eagerly at module load so HIDDEN_AGENT_NAMES
|
|
1281
|
-
// stays in sync with the declarative registry — no hardcoded duplicate.
|
|
1282
|
-
const HIDDEN_AGENT_NAMES = new Set(
|
|
1283
|
-
(_getHiddenAgents().agents || []).map((r) => r && r.agent).filter((n) => typeof n === 'string' && n.length > 0)
|
|
1284
|
-
);
|
|
1285
|
-
|
|
1286
129
|
// Stop reasons that signal the turn was cut short mid-synthesis (token cap,
|
|
1287
130
|
// provider pause). Empty content + one of these reasons means the worker
|
|
1288
131
|
// was not done — re-prompt instead of accepting empty as final.
|
|
1289
132
|
// Covers Anthropic (pause_turn, max_tokens), OpenAI (length), Gemini
|
|
1290
133
|
// (MAX_TOKENS, OTHER), and case variants.
|
|
1291
|
-
const INCOMPLETE_STOP_REASONS = new Set([
|
|
1292
|
-
'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
|
|
1293
|
-
]);
|
|
1294
|
-
|
|
1295
|
-
export function approvalGranted(value) {
|
|
1296
|
-
if (value === true) return true;
|
|
1297
|
-
if (!value || typeof value !== 'object') return false;
|
|
1298
|
-
if (value.approved === true || value.allow === true || value.allowed === true) return true;
|
|
1299
|
-
const decision = String(value.decision || value.action || value.result || '').trim().toLowerCase();
|
|
1300
|
-
return decision === 'approve' || decision === 'approved' || decision === 'allow' || decision === 'yes';
|
|
1301
|
-
}
|
|
1302
|
-
|
|
1303
|
-
export function approvalReason(value, fallback = '') {
|
|
1304
|
-
if (value && typeof value === 'object') {
|
|
1305
|
-
const reason = String(value.reason || value.message || '').trim();
|
|
1306
|
-
if (reason) return reason;
|
|
1307
|
-
}
|
|
1308
|
-
return fallback;
|
|
1309
|
-
}
|
|
1310
|
-
|
|
1311
134
|
export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
|
|
1312
135
|
let iterations = 0;
|
|
1313
136
|
let toolCallsTotal = 0;
|
|
@@ -1403,11 +226,57 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1403
226
|
return true;
|
|
1404
227
|
};
|
|
1405
228
|
const maxLoopIterations = resolveSessionMaxLoopIterations(sessionRef);
|
|
229
|
+
// ---- Completion-first loop guards (worker runaway prevention) ----
|
|
230
|
+
// Step 1 (escalation ladder) + the missed-parallelism / serial-rewording
|
|
231
|
+
// steering hints live in the createSteeringLadder controller below; it owns
|
|
232
|
+
// their cumulative counters and emits at most one hint per turn.
|
|
233
|
+
// _editCount counts any executed tool call whose def lacks readOnlyHint
|
|
234
|
+
// (i.e. edit/progress: apply_patch, bash, MCP writes, skills, ...).
|
|
235
|
+
let _editCount = 0;
|
|
236
|
+
// Step 2: cross-turn identical read-only call dedup. Map keyed by
|
|
237
|
+
// signature(name + stableStringify(args)) → { count, firstIteration }.
|
|
238
|
+
// Populated only for SUCCESSFUL isEagerDispatchable (read-only) calls.
|
|
239
|
+
// Bounded to 500 entries (drop-oldest / insertion order).
|
|
240
|
+
const _crossTurnCalls = new Map();
|
|
241
|
+
const _CROSS_TURN_CAP = 500;
|
|
242
|
+
let _dedupStubTotal = 0;
|
|
243
|
+
// Step 3: worker soft-cap wrap-up state.
|
|
244
|
+
const _softCapEnabled = isWorkerSoftCapSession(sessionRef);
|
|
245
|
+
let _softCapActive = false; // tools disabled + wrap-up injected
|
|
246
|
+
let _softCapGraceTurns = 0; // text-only grace turns consumed (max 2)
|
|
247
|
+
let _terminatedBySoftCap = false;
|
|
248
|
+
// Completion-first steering ladder controller. Owns the (cumulative) level-1
|
|
249
|
+
// fire count, the all-read-only / serial-single / same-file-grep streaks,
|
|
250
|
+
// and the level-2 latch. Threaded via live getters so it reads the loop's
|
|
251
|
+
// current `iterations` / `_editCount` on every call (no stale snapshots).
|
|
252
|
+
const _steeringLadder = createSteeringLadder({
|
|
253
|
+
sessionId,
|
|
254
|
+
sessionAgent,
|
|
255
|
+
tools,
|
|
256
|
+
getIterations: () => iterations,
|
|
257
|
+
softCapEnabled: _softCapEnabled,
|
|
258
|
+
getEditCount: () => _editCount,
|
|
259
|
+
readOnlyRole: String(sessionRef?.permission || sessionRef?.toolPermission || '') === 'read',
|
|
260
|
+
pushUserMessage: (msg) => messages.push(msg),
|
|
261
|
+
pushSystemReminder: (text) => messages.push({ role: 'user', content: `<system-reminder>\n${text}\n</system-reminder>`, meta: 'hook' }),
|
|
262
|
+
});
|
|
1406
263
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
1407
264
|
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
1408
265
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
1409
266
|
// against the host/plugin root instead of the worker workspace.
|
|
1410
267
|
cwd = cwd || sessionRef?.cwd || undefined;
|
|
268
|
+
// Staged pre-cap warnings + one true hard stop. The ONLY count-based
|
|
269
|
+
// forced termination is the hard cap at maxLoopIterations (default 200):
|
|
270
|
+
// a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
|
|
271
|
+
// of the cap steering the model to converge — warnings only, nothing is
|
|
272
|
+
// cut off early. All other runaway protection is behavior-based (steering
|
|
273
|
+
// ladder early wrap-up, REPEAT_FAIL_LIMIT), never a lower count.
|
|
274
|
+
let _iterWarnStage = 0;
|
|
275
|
+
const _iterWarnAt = [
|
|
276
|
+
Math.floor(maxLoopIterations * 0.5),
|
|
277
|
+
Math.floor(maxLoopIterations * 0.75),
|
|
278
|
+
Math.floor(maxLoopIterations * 0.9),
|
|
279
|
+
];
|
|
1411
280
|
while (true) {
|
|
1412
281
|
throwIfAborted();
|
|
1413
282
|
if (iterations >= maxLoopIterations) {
|
|
@@ -1415,6 +284,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1415
284
|
terminatedByCap = true;
|
|
1416
285
|
break;
|
|
1417
286
|
}
|
|
287
|
+
if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
|
|
288
|
+
_iterWarnStage += 1;
|
|
289
|
+
const warnAt = _iterWarnAt[_iterWarnStage - 1];
|
|
290
|
+
const stageMsg = _iterWarnStage === 1
|
|
291
|
+
? `Iteration budget notice: ${warnAt} of ${maxLoopIterations} iterations used. Converge on a conclusion: prefer finishing the current objective over opening new exploration.`
|
|
292
|
+
: `Iteration budget warning (stage ${_iterWarnStage}): ${warnAt} of ${maxLoopIterations} iterations used — the loop hard-stops at ${maxLoopIterations}. Wrap up now: summarize progress, state what remains, and finish with your best current result.`;
|
|
293
|
+
messages.push({ role: 'user', content: `<system-reminder>\n${stageMsg}\n</system-reminder>`, meta: 'hook' });
|
|
294
|
+
process.stderr.write(`[loop] iteration warning stage ${_iterWarnStage} at ${iterations} (sess=${sessionId || 'unknown'}); continuing with steer.\n`);
|
|
295
|
+
try {
|
|
296
|
+
appendAgentTrace({
|
|
297
|
+
sessionId,
|
|
298
|
+
iteration: iterations,
|
|
299
|
+
kind: 'steer',
|
|
300
|
+
payload: { tag: 'iteration_warning', stage: _iterWarnStage, at: iterations, unit: maxLoopIterations },
|
|
301
|
+
agent: sessionAgent || null,
|
|
302
|
+
});
|
|
303
|
+
} catch { /* best-effort */ }
|
|
304
|
+
}
|
|
305
|
+
// Worker soft cap (Step 3): non-lead sessions that reach the soft-cap
|
|
306
|
+
// iteration count switch to a text-only wrap-up. On the FIRST crossing
|
|
307
|
+
// we disable tool defs (below, via _softCapActive) and inject the
|
|
308
|
+
// assistant-visible wrap-up directive as a user message so the next
|
|
309
|
+
// send produces a final text summary. Lead/TUI sessions never enter.
|
|
310
|
+
const _earlySoftCap = _steeringLadder.earlySoftCapArmed();
|
|
311
|
+
if (_softCapEnabled && !_softCapActive && (iterations >= WORKER_SOFT_CAP_ITERATIONS || _earlySoftCap)) {
|
|
312
|
+
_softCapActive = true;
|
|
313
|
+
messages.push({ role: 'user', content: `<system-reminder>\n${SOFT_CAP_WRAPUP_MESSAGE}\n</system-reminder>`, meta: 'hook' });
|
|
314
|
+
try {
|
|
315
|
+
appendAgentTrace({
|
|
316
|
+
sessionId,
|
|
317
|
+
iteration: iterations,
|
|
318
|
+
kind: 'steer',
|
|
319
|
+
payload: { tag: 'soft_cap_wrapup', soft_cap: WORKER_SOFT_CAP_ITERATIONS, early: _earlySoftCap, level2_fires: _steeringLadder.level2FireCount },
|
|
320
|
+
agent: sessionAgent || null,
|
|
321
|
+
});
|
|
322
|
+
} catch { /* best-effort */ }
|
|
323
|
+
}
|
|
1418
324
|
// Drain queued steering/prompts BEFORE the
|
|
1419
325
|
// pre-send compact check. The compact decision must see the exact
|
|
1420
326
|
// message set that the next provider.send would receive, including
|
|
@@ -1446,6 +352,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1446
352
|
const reactivePending = reactiveOverflowRetryPending === true;
|
|
1447
353
|
const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, { forceReactive: reactivePending });
|
|
1448
354
|
const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, { reactivePending });
|
|
355
|
+
// A pending reactive-overflow retry makes THIS compact pass the
|
|
356
|
+
// recovery from a provider overflow refusal, not the proactive
|
|
357
|
+
// pressure trigger. Tag the emitted events so telemetry can tell
|
|
358
|
+
// them apart. Hoisted above the shouldCompact branch because the
|
|
359
|
+
// PostCompact hook below fires on BOTH paths (fixes a
|
|
360
|
+
// ReferenceError on the no-compact path).
|
|
361
|
+
const compactTrigger = reactivePending ? 'reactive' : 'auto';
|
|
1449
362
|
const compactBudgetTokens = shouldCompact
|
|
1450
363
|
? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
|
|
1451
364
|
: compactPolicy.boundaryTokens;
|
|
@@ -1459,12 +372,22 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1459
372
|
} else {
|
|
1460
373
|
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
1461
374
|
const compactStartedAt = Date.now();
|
|
1462
|
-
//
|
|
1463
|
-
//
|
|
1464
|
-
//
|
|
1465
|
-
// them apart, then clear the one-shot flag.
|
|
1466
|
-
const compactTrigger = reactiveOverflowRetryPending ? 'reactive' : 'auto';
|
|
375
|
+
// Clear the one-shot reactive-overflow flag now that this
|
|
376
|
+
// compact pass is consuming it (compactTrigger already
|
|
377
|
+
// captured it above).
|
|
1467
378
|
reactiveOverflowRetryPending = false;
|
|
379
|
+
// PreCompact: bridge to the standard hook bus before compaction
|
|
380
|
+
// runs. session-property hook (manager/loop have no bus access).
|
|
381
|
+
// { trigger } normalized to 'auto'|'manual'. Best-effort.
|
|
382
|
+
{
|
|
383
|
+
const _preCompactHook = typeof opts.preCompactHook === 'function'
|
|
384
|
+
? opts.preCompactHook
|
|
385
|
+
: sessionRef?.preCompactHook;
|
|
386
|
+
if (typeof _preCompactHook === 'function') {
|
|
387
|
+
try { await _preCompactHook({ sessionId, cwd, trigger: compactTrigger === 'manual' ? 'manual' : 'auto' }); }
|
|
388
|
+
catch { /* best-effort: PreCompact hook must never break compaction */ }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
1468
391
|
rememberCompactTelemetry(sessionRef, compactPolicy, {
|
|
1469
392
|
stage: 'compacting',
|
|
1470
393
|
beforeTokens: messageTokensEst,
|
|
@@ -1792,6 +715,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1792
715
|
durationMs: compactDurationMs,
|
|
1793
716
|
});
|
|
1794
717
|
}
|
|
718
|
+
// PostCompact: bridge to the standard hook bus after compaction
|
|
719
|
+
// completes. session-property hook; { trigger } 'auto'|'manual'.
|
|
720
|
+
{
|
|
721
|
+
const _postCompactHook = typeof opts.postCompactHook === 'function'
|
|
722
|
+
? opts.postCompactHook
|
|
723
|
+
: sessionRef?.postCompactHook;
|
|
724
|
+
if (typeof _postCompactHook === 'function') {
|
|
725
|
+
try { await _postCompactHook({ sessionId, cwd, trigger: compactTrigger === 'manual' ? 'manual' : 'auto' }); }
|
|
726
|
+
catch { /* best-effort: PostCompact hook must never break the loop */ }
|
|
727
|
+
}
|
|
728
|
+
}
|
|
1795
729
|
}
|
|
1796
730
|
const nextIteration = iterations + 1;
|
|
1797
731
|
opts.iteration = nextIteration;
|
|
@@ -1801,7 +735,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1801
735
|
} else {
|
|
1802
736
|
delete opts.toolChoice;
|
|
1803
737
|
}
|
|
1804
|
-
|
|
738
|
+
// Soft-cap wrap-up (Step 3a): once active, send NO tool definitions so
|
|
739
|
+
// the provider can only emit text. Overrides the forced-first-tool path.
|
|
740
|
+
const sendTools = _softCapActive
|
|
741
|
+
? []
|
|
742
|
+
: (forcedFirstToolDef && toolCallsTotal === 0 ? [forcedFirstToolDef] : tools);
|
|
1805
743
|
// Eager-dispatch queue: when the provider streams a tool-call event,
|
|
1806
744
|
// start read-only tools immediately so execution overlaps with the
|
|
1807
745
|
// remaining SSE parse. Writes and unknown tools wait until send()
|
|
@@ -1842,11 +780,22 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1842
780
|
const _rfg = sessionRef?._repeatFailGuard;
|
|
1843
781
|
if (_rfg && _rfg.sig === _sig && _rfg.count >= REPEAT_FAIL_LIMIT) return null;
|
|
1844
782
|
}
|
|
783
|
+
// Cross-turn dedup also gates eager dispatch (mirror of the
|
|
784
|
+
// repeat-failure guard above): a read-only call whose (name,args)
|
|
785
|
+
// signature already ran in an EARLIER turn must NOT be eagerly
|
|
786
|
+
// re-executed — the serial for-body pushes the [cross-turn-dedup]
|
|
787
|
+
// stub instead. Without this gate startEagerRun/onToolCall would
|
|
788
|
+
// re-run the call before the serial dedup check ever sees it.
|
|
789
|
+
{
|
|
790
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
791
|
+
const _prior = _crossTurnCalls.get(_ctSig);
|
|
792
|
+
if (_prior && _prior.firstIteration < iterations) return null;
|
|
793
|
+
}
|
|
1845
794
|
const toolKind = getToolKind(call.name);
|
|
1846
795
|
// Shared pre-dispatch deny: identical predicate runs in the
|
|
1847
796
|
// serial path below. If any role/permission guard would reject
|
|
1848
797
|
// this call there, never start it eagerly here.
|
|
1849
|
-
if (
|
|
798
|
+
if (preDispatchDenyForSession(sessionRef, call, toolKind) !== null) return null;
|
|
1850
799
|
const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: _mutationEpoch };
|
|
1851
800
|
_eagerInFlightSigs.set(_sig, call.id);
|
|
1852
801
|
entry.promise = (async () => {
|
|
@@ -2082,13 +1031,18 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2082
1031
|
// the stateless contract for providers that don't use continuation).
|
|
2083
1032
|
providerState = response?.providerState ?? undefined;
|
|
2084
1033
|
iterations = nextIteration;
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
1034
|
+
// Payload byte estimate serializes the FULL messages+tools array —
|
|
1035
|
+
// only pay that cost when verbose loop tracing is actually enabled
|
|
1036
|
+
// (traceAgentLoop is a no-op otherwise).
|
|
1037
|
+
if (process.env.MIXDOG_AGENT_TRACE_VERBOSE === '1') {
|
|
1038
|
+
traceAgentLoop({
|
|
1039
|
+
sessionId,
|
|
1040
|
+
iteration: iterations,
|
|
1041
|
+
sendMs: Date.now() - sendStartedAt,
|
|
1042
|
+
messageCount: Array.isArray(messages) ? messages.length : 0,
|
|
1043
|
+
bodyBytesEst: estimateProviderPayloadBytes(messages, model, sendTools),
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
2092
1046
|
// Accumulate usage across iterations — every billable slot, not just
|
|
2093
1047
|
// input/output. Anthropic cache_read/cache_write typically stay 0 on
|
|
2094
1048
|
// the first iteration and surge on later ones (warm prefix reuse),
|
|
@@ -2178,6 +1132,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2178
1132
|
// tool-call-blocked vs contract-required oscillation.
|
|
2179
1133
|
if (!response.toolCalls?.length) {
|
|
2180
1134
|
// No tool calls. Decide between final-answer accept vs nudge.
|
|
1135
|
+
// Reviewer fix: a zero-tool turn (final-pre-send steering drain or
|
|
1136
|
+
// contract nudge `continue`) must not bridge the all-read-only
|
|
1137
|
+
// streak across non-tool turns — that would fire level-2 early on
|
|
1138
|
+
// a worker that paused to synthesize text mid-run.
|
|
1139
|
+
_steeringLadder.resetAllReadOnlyStreak();
|
|
2181
1140
|
// - has content + non-hidden role → valid final, break.
|
|
2182
1141
|
// - empty content + hidden role → contract allows text-only
|
|
2183
1142
|
// terminal turn, break.
|
|
@@ -2251,6 +1210,37 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2251
1210
|
: {}),
|
|
2252
1211
|
};
|
|
2253
1212
|
messages.push(_assistantTurnMsg);
|
|
1213
|
+
// Soft-cap wrap-up (Step 3b): tools are disabled but the model still
|
|
1214
|
+
// emitted tool calls. Do NOT execute them — push a refusal stub for
|
|
1215
|
+
// each (after the assistant turn is appended so tool_use/tool_result
|
|
1216
|
+
// pairing stays valid) and consume a grace turn. After 2 grace turns,
|
|
1217
|
+
// terminate via the soft-cap path so a model that never complies stops.
|
|
1218
|
+
if (_softCapActive) {
|
|
1219
|
+
for (const _c of calls) {
|
|
1220
|
+
pushToolResultMessage({
|
|
1221
|
+
role: 'tool',
|
|
1222
|
+
content: SOFT_CAP_REFUSAL_STUB,
|
|
1223
|
+
toolCallId: _c.id,
|
|
1224
|
+
toolKind: 'error',
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
_softCapGraceTurns += 1;
|
|
1228
|
+
try {
|
|
1229
|
+
appendAgentTrace({
|
|
1230
|
+
sessionId,
|
|
1231
|
+
iteration: iterations,
|
|
1232
|
+
kind: 'steer',
|
|
1233
|
+
payload: { tag: 'soft_cap_wrapup', grace_turn: _softCapGraceTurns },
|
|
1234
|
+
agent: sessionAgent || null,
|
|
1235
|
+
});
|
|
1236
|
+
} catch { /* best-effort */ }
|
|
1237
|
+
if (_softCapGraceTurns >= 2) {
|
|
1238
|
+
_terminatedBySoftCap = true;
|
|
1239
|
+
break;
|
|
1240
|
+
}
|
|
1241
|
+
if (sessionId) updateSessionStage(sessionId, 'connecting');
|
|
1242
|
+
continue;
|
|
1243
|
+
}
|
|
2254
1244
|
// Execute each tool and append results.
|
|
2255
1245
|
//
|
|
2256
1246
|
// Intra-turn duplicate suppression: when an LLM emits two tool_use
|
|
@@ -2305,6 +1295,42 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2305
1295
|
});
|
|
2306
1296
|
continue;
|
|
2307
1297
|
}
|
|
1298
|
+
// Cross-turn identical-call stub (Step 2): a SUCCESSFUL read-only
|
|
1299
|
+
// (isEagerDispatchable) call whose (name,args) signature already ran
|
|
1300
|
+
// in an EARLIER turn is not re-executed — its result is unchanged and
|
|
1301
|
+
// already in context. Warn at the 2nd occurrence; append the "stuck"
|
|
1302
|
+
// escalation tail once the session has emitted 5+ dedup stubs total.
|
|
1303
|
+
// Never applies to write/bash/MCP/skill tools (not eager-dispatchable).
|
|
1304
|
+
if (isEagerDispatchable(call.name, tools)) {
|
|
1305
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
1306
|
+
const _prior = _crossTurnCalls.get(_ctSig);
|
|
1307
|
+
if (_prior && _prior.firstIteration < iterations) {
|
|
1308
|
+
_prior.count += 1;
|
|
1309
|
+
_dedupStubTotal += 1;
|
|
1310
|
+
const _stub = crossTurnDedupStub(call.name, _prior.firstIteration, _dedupStubTotal >= 5);
|
|
1311
|
+
pushToolResultMessage({
|
|
1312
|
+
role: 'tool',
|
|
1313
|
+
content: _stub,
|
|
1314
|
+
toolCallId: call.id,
|
|
1315
|
+
});
|
|
1316
|
+
try {
|
|
1317
|
+
appendAgentTrace({
|
|
1318
|
+
sessionId,
|
|
1319
|
+
iteration: iterations,
|
|
1320
|
+
kind: 'steer',
|
|
1321
|
+
payload: {
|
|
1322
|
+
tag: 'cross_turn_dedup',
|
|
1323
|
+
tool: call.name,
|
|
1324
|
+
occurrence: _prior.count,
|
|
1325
|
+
first_iteration: _prior.firstIteration,
|
|
1326
|
+
dedup_stub_total: _dedupStubTotal,
|
|
1327
|
+
},
|
|
1328
|
+
agent: sessionAgent || null,
|
|
1329
|
+
});
|
|
1330
|
+
} catch { /* best-effort */ }
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
2308
1334
|
// Cross-iteration repeat-failure guard. Distinct from the
|
|
2309
1335
|
// intra-turn dedup above (which spans ONE assistant turn and
|
|
2310
1336
|
// resets every turn): when the model re-issues an IDENTICAL
|
|
@@ -2330,8 +1356,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2330
1356
|
let toolStartedAt;
|
|
2331
1357
|
let toolEndedAt;
|
|
2332
1358
|
const toolKind = getToolKind(call.name);
|
|
2333
|
-
// Cross-turn read dedup
|
|
2334
|
-
// fileReadCache.ts: if the path's stat tuple (mtime/size/ino/dev)
|
|
1359
|
+
// Cross-turn read dedup: if the path's stat tuple (mtime/size/ino/dev)
|
|
2335
1360
|
// is unchanged since a prior read in THIS session, return the cached
|
|
2336
1361
|
// body instead of executing. Both scalar and array/object-array path
|
|
2337
1362
|
// forms are cached — keyed by (abs, offset, limit, mode, n) per entry.
|
|
@@ -2415,12 +1440,12 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2415
1440
|
// Runtime pre-dispatch deny. Schema profiles may hide
|
|
2416
1441
|
// tools for routing efficiency, but this remains the
|
|
2417
1442
|
// control-plane boundary for any tool_use that still
|
|
2418
|
-
// reaches the loop.
|
|
1443
|
+
// reaches the loop. preDispatchDenyForSession is the SHARED helper
|
|
2419
1444
|
// used by both the eager dispatch path (startEagerTool)
|
|
2420
1445
|
// and this serial path — keeps the agent-owned control-
|
|
2421
1446
|
// plane reject and no-tool role guards consistent across
|
|
2422
1447
|
// both paths.
|
|
2423
|
-
const _denyMsg =
|
|
1448
|
+
const _denyMsg = preDispatchDenyForSession(sessionRef, call, toolKind);
|
|
2424
1449
|
if (_denyMsg !== null) {
|
|
2425
1450
|
result = _denyMsg;
|
|
2426
1451
|
toolEndedAt = Date.now();
|
|
@@ -2460,6 +1485,36 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2460
1485
|
result = _env.result;
|
|
2461
1486
|
if (_env.newMessages.length) _batchNewMessages.push(..._env.newMessages);
|
|
2462
1487
|
}
|
|
1488
|
+
// Bounded-map cleanup: a scoped-cache outcome recorded for this call.id
|
|
1489
|
+
// (via _scopedCacheOutcomeForCall) is only ever consumed/deleted on the
|
|
1490
|
+
// success path below (_executeOk && _resultKind==='normal'). A failed or
|
|
1491
|
+
// errored call would otherwise leak its entry in
|
|
1492
|
+
// sessionRef._scopedCacheOutcomeByCallId forever — reclaim it here.
|
|
1493
|
+
if (sessionRef?._scopedCacheOutcomeByCallId instanceof Map && call?.id && (!_executeOk || _resultKind === 'error')) {
|
|
1494
|
+
sessionRef._scopedCacheOutcomeByCallId.delete(call.id);
|
|
1495
|
+
}
|
|
1496
|
+
// PostToolUseFailure: a tool that resolved to a failure (thrown-error
|
|
1497
|
+
// path -> `Error:` string, or an is_error result classified as
|
|
1498
|
+
// 'error') fires the optional session failure hook. Same shape as
|
|
1499
|
+
// afterToolHook; `result` carries the error text. Best-effort — a
|
|
1500
|
+
// hook error must never wedge the tool loop.
|
|
1501
|
+
if (!_executeOk || _resultKind === 'error') {
|
|
1502
|
+
const _afterToolFailureHook = typeof opts.afterToolFailureHook === 'function'
|
|
1503
|
+
? opts.afterToolFailureHook
|
|
1504
|
+
: sessionRef?.afterToolFailureHook;
|
|
1505
|
+
if (typeof _afterToolFailureHook === 'function') {
|
|
1506
|
+
try {
|
|
1507
|
+
await _afterToolFailureHook({
|
|
1508
|
+
name: call.name,
|
|
1509
|
+
args: call.arguments,
|
|
1510
|
+
cwd,
|
|
1511
|
+
sessionId,
|
|
1512
|
+
toolCallId: call.id,
|
|
1513
|
+
result: typeof result === 'string' ? result : String(result ?? ''),
|
|
1514
|
+
});
|
|
1515
|
+
} catch { /* best-effort: PostToolUseFailure hook must never break the loop */ }
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
2463
1518
|
// Update the cross-iteration repeat-failure guard with this call's
|
|
2464
1519
|
// outcome: bump the consecutive-failure count for an identical
|
|
2465
1520
|
// signature, or clear it the moment the same call succeeds.
|
|
@@ -2626,7 +1681,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2626
1681
|
// body via the disk path in that stub.
|
|
2627
1682
|
if (sessionId && _executeOk && _resultKind === 'normal') {
|
|
2628
1683
|
if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
|
|
2629
|
-
const
|
|
1684
|
+
const _outcomeMap = sessionRef?._scopedCacheOutcomeByCallId instanceof Map
|
|
1685
|
+
? sessionRef._scopedCacheOutcomeByCallId : null;
|
|
1686
|
+
const _outcome = _outcomeMap?.get(call.id);
|
|
2630
1687
|
setScopedToolCached({
|
|
2631
1688
|
sessionId,
|
|
2632
1689
|
toolName: _toolBare,
|
|
@@ -2636,7 +1693,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2636
1693
|
toolUseId: call.id,
|
|
2637
1694
|
complete: _outcome ? _outcome.complete : true,
|
|
2638
1695
|
});
|
|
2639
|
-
|
|
1696
|
+
_outcomeMap?.delete(call.id);
|
|
2640
1697
|
}
|
|
2641
1698
|
if (_readCacheHit === null && _isReadTool(call.name)) {
|
|
2642
1699
|
// Pass tool_use id so future cache-hits can reference the body's location in history.
|
|
@@ -2660,8 +1717,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2660
1717
|
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
2661
1718
|
...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
|
|
2662
1719
|
});
|
|
1720
|
+
// Completion-first bookkeeping (Steps 1 & 2). Only successful
|
|
1721
|
+
// executions count. Edit/progress = any executed tool whose def
|
|
1722
|
+
// lacks readOnlyHint (apply_patch/bash/MCP-write/skill/...).
|
|
1723
|
+
// Read-only successful calls seed the cross-turn dedup map.
|
|
1724
|
+
if (_executeOk) {
|
|
1725
|
+
const _isEager = isEagerDispatchable(call.name, tools);
|
|
1726
|
+
if (_isEager) {
|
|
1727
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
1728
|
+
if (!_crossTurnCalls.has(_ctSig)) {
|
|
1729
|
+
_crossTurnCalls.set(_ctSig, { count: 1, firstIteration: iterations });
|
|
1730
|
+
if (_crossTurnCalls.size > _CROSS_TURN_CAP) {
|
|
1731
|
+
const _oldest = _crossTurnCalls.keys().next().value;
|
|
1732
|
+
_crossTurnCalls.delete(_oldest);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
} else {
|
|
1736
|
+
// A successful mutating (non-eager) tool invalidates the
|
|
1737
|
+
// cross-turn dedup map wholesale: any prior read/grep may
|
|
1738
|
+
// now return different content, so a post-edit
|
|
1739
|
+
// verification read must NOT be stubbed as "unchanged".
|
|
1740
|
+
if (isEditProgressTool(call.name, false)) {
|
|
1741
|
+
_crossTurnCalls.clear();
|
|
1742
|
+
_editCount += 1;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
2663
1746
|
} catch (postErr) {
|
|
2664
1747
|
_postProcessOk = false;
|
|
1748
|
+
// Reviewer fix: the exec itself succeeded — if it was a
|
|
1749
|
+
// mutating edit-progress tool, the file changes are real even
|
|
1750
|
+
// though post-processing failed, so the cross-turn dedup map
|
|
1751
|
+
// must still be invalidated (otherwise a later verification
|
|
1752
|
+
// read could be stubbed as "unchanged" against stale sigs).
|
|
1753
|
+
if (_executeOk && !isEagerDispatchable(call.name, tools) && isEditProgressTool(call.name, false)) {
|
|
1754
|
+
_crossTurnCalls.clear();
|
|
1755
|
+
_editCount += 1;
|
|
1756
|
+
}
|
|
2665
1757
|
// Post-processing failed AFTER a successful exec: the result is
|
|
2666
1758
|
// replaced with an error below, so preserve this call's full body
|
|
2667
1759
|
// too for a clean retry (mirrors the failed-exec path above).
|
|
@@ -2710,6 +1802,37 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2710
1802
|
if (!_nm || _nm.role !== 'user' || typeof _nm.content !== 'string' || !_nm.content) continue;
|
|
2711
1803
|
messages.push({ role: 'user', content: _nm.content, ...(_nm.meta ? { meta: _nm.meta } : {}) });
|
|
2712
1804
|
}
|
|
1805
|
+
// PostToolBatch: the full parallel batch of tool calls for this
|
|
1806
|
+
// assistant turn has resolved and all tool_results are pushed. Fire the
|
|
1807
|
+
// optional session hook before the next model call. No matcher event.
|
|
1808
|
+
// Block support: if the hook returns blocked===true, inject its reason
|
|
1809
|
+
// as a system-note user message for the next send (natural mechanism —
|
|
1810
|
+
// same channel the newMessages flush just used). Best-effort otherwise.
|
|
1811
|
+
{
|
|
1812
|
+
const _afterToolBatchHook = typeof opts.afterToolBatchHook === 'function'
|
|
1813
|
+
? opts.afterToolBatchHook
|
|
1814
|
+
: sessionRef?.afterToolBatchHook;
|
|
1815
|
+
if (typeof _afterToolBatchHook === 'function' && calls.length > 0) {
|
|
1816
|
+
try {
|
|
1817
|
+
const _batchDecision = await _afterToolBatchHook({
|
|
1818
|
+
sessionId,
|
|
1819
|
+
cwd,
|
|
1820
|
+
toolCount: calls.length,
|
|
1821
|
+
});
|
|
1822
|
+
if (_batchDecision?.blocked === true) {
|
|
1823
|
+
const _reason = String(_batchDecision.reason || 'PostToolBatch hook blocked continuation').trim();
|
|
1824
|
+
if (_reason) {
|
|
1825
|
+
messages.push({ role: 'user', content: `<system-reminder>\n${_reason}\n</system-reminder>`, meta: 'hook' });
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
} catch { /* best-effort: PostToolBatch hook must never break the loop */ }
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
// Completion-first steering hints (missed-parallelism / all-read-only /
|
|
1832
|
+
// serial-rewording). At most ONE hint per turn (priority: soft-cap >
|
|
1833
|
+
// level-2 > same-file grep > level-1); soft-cap active suppresses all.
|
|
1834
|
+
// The ladder controller owns the cumulative counters and streaks.
|
|
1835
|
+
_steeringLadder.emitPostBatchSteering(calls, _softCapActive);
|
|
2713
1836
|
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
2714
1837
|
// AFTER any auto-compact pass. Draining here would put the steering
|
|
2715
1838
|
// user turn after the fresh tool results before compaction runs; then
|
|
@@ -2720,31 +1843,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
2720
1843
|
}
|
|
2721
1844
|
// Classify WHY the loop ended so agent-tool can promote an empty/abnormal
|
|
2722
1845
|
// finish to an explicit Lead-facing error instead of a silent empty
|
|
2723
|
-
// "completed"
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
let terminationReason;
|
|
2731
|
-
if (terminatedByCap) {
|
|
2732
|
-
// Real problem regardless of hidden/public: the loop never terminated
|
|
2733
|
-
// on its own contract.
|
|
2734
|
-
terminationReason = 'iteration_cap';
|
|
2735
|
-
} else if (!_finalHasContent && _finalIncompleteStop) {
|
|
2736
|
-
// Cut short mid-synthesis (token cap / provider pause). Real problem
|
|
2737
|
-
// for hidden agents too.
|
|
2738
|
-
terminationReason = 'truncated';
|
|
2739
|
-
} else if (!_finalHasContent && !_finalIsHidden) {
|
|
2740
|
-
// Empty terminal turn. Only public agents violate their contract by
|
|
2741
|
-
// finishing empty — hidden agents (explorer/cycle/…) legitimately emit
|
|
2742
|
-
// text-only/empty terminal turns per their own role contract, so leave
|
|
2743
|
-
// terminationReason undefined for them.
|
|
2744
|
-
terminationReason = 'empty';
|
|
2745
|
-
} else {
|
|
2746
|
-
terminationReason = undefined;
|
|
2747
|
-
}
|
|
1846
|
+
// "completed" (see classifyTerminationReason in ./loop/termination.mjs).
|
|
1847
|
+
const terminationReason = classifyTerminationReason(response, {
|
|
1848
|
+
terminatedByCap,
|
|
1849
|
+
terminatedBySoftCap: _terminatedBySoftCap,
|
|
1850
|
+
softCapActive: _softCapActive,
|
|
1851
|
+
sessionAgent,
|
|
1852
|
+
});
|
|
2748
1853
|
return {
|
|
2749
1854
|
...response,
|
|
2750
1855
|
usage: lastUsage || response.usage,
|