mixdog 0.9.3 → 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 +7 -3
- 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/freevar-smoke.mjs +95 -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/provider-toolcall-test.mjs +7 -3
- 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 +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- 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/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- 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/providers/anthropic-effort.mjs +62 -20
- 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 +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- 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 +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- 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/openai-codex-model.mjs +108 -0
- 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 +40 -1143
- 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 +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- 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/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-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- 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/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -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/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- 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/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 +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- 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 -92
- 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 +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- 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 +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- 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 +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -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/output-forwarder.mjs +1 -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/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/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- 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/embedding-warmup.mjs +58 -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 +24 -842
- 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-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -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 +5 -13
- 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/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.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 +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -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 +11 -9
- 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/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -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 +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- 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 +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- 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 +23 -101
- 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 +52 -594
- 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/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -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.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- 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 +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -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/external-tool-adapters.test.mjs +0 -162
- 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/task-notification-envelope.test.mjs +0 -107
- 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/input-editing.selection.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/paste-fix.test.mjs +0 -119
- 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,31 +1,23 @@
|
|
|
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 { normalizeToolEnvelope
|
|
5
|
+
import { normalizeToolEnvelope } from './tool-envelope.mjs';
|
|
8
6
|
import { traceAgentLoop, traceAgentTool, traceAgentToolFailure, traceAgentCompact, estimateProviderPayloadBytes, messagePrefixHash, appendAgentTrace } from '../agent-trace.mjs';
|
|
9
|
-
import { resolveSessionMaxLoopIterations } from '../agent-runtime/agent-loop-policy.mjs';
|
|
7
|
+
import { resolveSessionMaxLoopIterations, WORKER_SOFT_CAP_ITERATIONS, isWorkerSoftCapSession } from '../agent-runtime/agent-loop-policy.mjs';
|
|
10
8
|
import { isAgentOwner } from '../agent-owner.mjs';
|
|
11
|
-
import { markSessionToolCall, updateSessionStage, SessionClosedError,
|
|
9
|
+
import { markSessionToolCall, updateSessionStage, SessionClosedError, bumpUsageMetricsEpoch } from './manager.mjs';
|
|
12
10
|
import {
|
|
13
|
-
recallFastTrackCompactMessages,
|
|
14
11
|
pruneToolOutputs,
|
|
15
12
|
pruneToolOutputsUnanchored,
|
|
16
13
|
semanticCompactMessages,
|
|
17
14
|
effectiveBudget as compactEffectiveBudget,
|
|
18
15
|
DEFAULT_COMPACT_TYPE,
|
|
19
|
-
drainSessionCycle1,
|
|
20
|
-
countRawPendingRows,
|
|
21
16
|
} from './compact.mjs';
|
|
22
17
|
import { isContextOverflowError } from '../providers/retry-classifier.mjs';
|
|
23
18
|
import { stripSoftWarns } from '../tool-loop-guard.mjs';
|
|
24
19
|
import { maybeOffloadToolResult } from './tool-result-offload.mjs';
|
|
25
20
|
import { tryReadCached, setReadCached, invalidatePathForSession, markPostEdit, consumePostEditMark, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
|
|
26
|
-
import { createScopedCacheOutcome } from './cache/scoped-cache-outcome.mjs';
|
|
27
|
-
import { modelVisibleToolCompletionMessage } from '../../../shared/tool-execution-contract.mjs';
|
|
28
|
-
import { createHash } from 'crypto';
|
|
29
21
|
import { isInvalidToolArgsMarker, formatInvalidToolArgsResult } from '../providers/openai-compat-stream.mjs';
|
|
30
22
|
|
|
31
23
|
import {
|
|
@@ -37,13 +29,8 @@ import {
|
|
|
37
29
|
_intraTurnSig,
|
|
38
30
|
} from './loop/tool-classify.mjs';
|
|
39
31
|
import { preDispatchDenyForSession } from './loop/pre-dispatch-deny.mjs';
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
codeGraphRuntimePromise ??= import('../tools/code-graph.mjs');
|
|
43
|
-
const mod = await codeGraphRuntimePromise;
|
|
44
|
-
if (typeof mod.executeCodeGraphTool !== 'function') throw new Error('code_graph runtime is not available');
|
|
45
|
-
return mod.executeCodeGraphTool(name, args, cwd, signal, options);
|
|
46
|
-
}
|
|
32
|
+
import { runRecallFastTrackCompact } from './loop/recall-fasttrack.mjs';
|
|
33
|
+
import { executeTool, _scopedCacheOutcomeForCall } from './loop/tool-exec.mjs';
|
|
47
34
|
|
|
48
35
|
// classifyResultKind is imported from result-classification.mjs at the top of
|
|
49
36
|
// this file; import it from there directly rather than via this module.
|
|
@@ -58,6 +45,13 @@ import {
|
|
|
58
45
|
compactDebugLog,
|
|
59
46
|
} from './loop/compact-debug.mjs';
|
|
60
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';
|
|
61
55
|
import { agentContextOverflowError } from './loop/context-overflow.mjs';
|
|
62
56
|
import { positiveTokenInt } from './loop/env.mjs';
|
|
63
57
|
import { normalizeUsage, addUsage } from './loop/usage.mjs';
|
|
@@ -76,12 +70,9 @@ import {
|
|
|
76
70
|
isEagerDispatchable,
|
|
77
71
|
messagesArrayChanged,
|
|
78
72
|
getToolKind,
|
|
79
|
-
buildSkillsListResponse,
|
|
80
|
-
viewSkill,
|
|
81
73
|
normalizeHookUpdatedToolOutput,
|
|
82
74
|
resolveToolResultAfterHook,
|
|
83
75
|
parseNativeToolSearchPayload,
|
|
84
|
-
extractBashSessionId,
|
|
85
76
|
buildAgentBashSessionArgs,
|
|
86
77
|
formatMissingToolApprovalUiDenial,
|
|
87
78
|
resolvePreToolAskApproval,
|
|
@@ -93,6 +84,8 @@ import {
|
|
|
93
84
|
restoreToolCallBodyForId,
|
|
94
85
|
} from './loop/stored-tool-args.mjs';
|
|
95
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';
|
|
96
89
|
|
|
97
90
|
// Facade re-exports: these symbols moved to split modules under ./loop/ but
|
|
98
91
|
// remain part of loop.mjs's public surface (imported by scripts/tests and other
|
|
@@ -120,342 +113,8 @@ export {
|
|
|
120
113
|
// this catches tight deterministic-failure loops (e.g. a command that errors
|
|
121
114
|
// the same way every time) far earlier than 100 iterations.
|
|
122
115
|
const REPEAT_FAIL_LIMIT = 3;
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const startedAt = Date.now();
|
|
126
|
-
const diagnostics = {
|
|
127
|
-
hydrateLimit: null,
|
|
128
|
-
ingestMs: null,
|
|
129
|
-
ingestSkipped: false,
|
|
130
|
-
ingestError: null,
|
|
131
|
-
initialDumpMs: null,
|
|
132
|
-
initialDumpBytes: null,
|
|
133
|
-
initialDumpChars: null,
|
|
134
|
-
initialRawPending: null,
|
|
135
|
-
cycle1Ms: null,
|
|
136
|
-
cycle1Skipped: false,
|
|
137
|
-
cycle1SkipReason: null,
|
|
138
|
-
cycle1Passes: null,
|
|
139
|
-
cycle1RawRemaining: null,
|
|
140
|
-
cycle1TextBytes: null,
|
|
141
|
-
cycle1Error: null,
|
|
142
|
-
finalRecallBytes: null,
|
|
143
|
-
finalRecallChars: null,
|
|
144
|
-
totalMs: null,
|
|
145
|
-
};
|
|
146
|
-
const query = `session:${sessionId}:all-chunks`;
|
|
147
|
-
const querySha = createHash('sha256').update(query).digest('hex').slice(0, 16);
|
|
148
|
-
const callerCtx = {
|
|
149
|
-
callerSessionId: sessionId || null,
|
|
150
|
-
callerCwd: sessionRef?.cwd || undefined,
|
|
151
|
-
routingSessionId: sessionId || null,
|
|
152
|
-
clientHostPid: sessionRef?.clientHostPid,
|
|
153
|
-
signal: signal || null,
|
|
154
|
-
};
|
|
155
|
-
const hydrateLimit = positiveTokenInt(sessionRef?.compaction?.recallIngestLimit)
|
|
156
|
-
|| Math.max(500, Math.min(5000, messages.length || 0));
|
|
157
|
-
diagnostics.hydrateLimit = hydrateLimit;
|
|
158
|
-
let t0 = Date.now();
|
|
159
|
-
try {
|
|
160
|
-
await executeInternalTool('memory', {
|
|
161
|
-
action: 'ingest_session',
|
|
162
|
-
sessionId,
|
|
163
|
-
messages,
|
|
164
|
-
cwd: sessionRef?.cwd,
|
|
165
|
-
limit: hydrateLimit,
|
|
166
|
-
}, callerCtx);
|
|
167
|
-
} catch (err) {
|
|
168
|
-
diagnostics.ingestSkipped = true;
|
|
169
|
-
diagnostics.ingestError = compactDiagnosticError(err);
|
|
170
|
-
try { process.stderr.write(`[loop] recall-fasttrack ingest skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
171
|
-
} finally {
|
|
172
|
-
diagnostics.ingestMs = Date.now() - t0;
|
|
173
|
-
}
|
|
174
|
-
const dumpArgs = {
|
|
175
|
-
action: 'dump_session_roots',
|
|
176
|
-
sessionId,
|
|
177
|
-
includeRaw: true,
|
|
178
|
-
limit: positiveTokenInt(sessionRef?.compaction?.recallChunkLimit ?? sessionRef?.compaction?.recallLimit) || hydrateLimit,
|
|
179
|
-
};
|
|
180
|
-
const runTool = (name, args) => executeInternalTool(name, args, callerCtx);
|
|
181
|
-
t0 = Date.now();
|
|
182
|
-
let recallText = await executeInternalTool('memory', dumpArgs, callerCtx);
|
|
183
|
-
diagnostics.initialDumpMs = Date.now() - t0;
|
|
184
|
-
diagnostics.initialDumpChars = String(recallText || '').length;
|
|
185
|
-
diagnostics.initialDumpBytes = compactByteLength(recallText);
|
|
186
|
-
diagnostics.initialRawPending = countRawPendingRows(recallText);
|
|
187
|
-
let cycle1Text = '';
|
|
188
|
-
const hasRawRows = /(?:^|\n)# raw_pending\s+\d+\s+id=/i.test(String(recallText || ''));
|
|
189
|
-
if (hasRawRows) {
|
|
190
|
-
t0 = Date.now();
|
|
191
|
-
try {
|
|
192
|
-
// Drain this session's cycle1 in window×concurrency units until no
|
|
193
|
-
// raw rows remain, so the injected root is fully chunked rather than
|
|
194
|
-
// carrying the unprocessed transcript tail (single-pass left raw in).
|
|
195
|
-
const drained = await drainSessionCycle1(runTool, {
|
|
196
|
-
sessionId,
|
|
197
|
-
dumpArgs,
|
|
198
|
-
deadlineMs: positiveTokenInt(sessionRef?.compaction?.recallCycle1DeadlineMs) || 120_000,
|
|
199
|
-
maxPasses: positiveTokenInt(sessionRef?.compaction?.recallCycle1MaxPasses) || 0,
|
|
200
|
-
cycleArgs: {
|
|
201
|
-
min_batch: 1,
|
|
202
|
-
session_cap: 1,
|
|
203
|
-
batch_size: positiveTokenInt(sessionRef?.compaction?.recallCycle1BatchSize) || 100,
|
|
204
|
-
rows_per_session: positiveTokenInt(sessionRef?.compaction?.recallRowsPerSession) || 100,
|
|
205
|
-
window_size: positiveTokenInt(sessionRef?.compaction?.recallWindowSize) || 20,
|
|
206
|
-
concurrency: positiveTokenInt(sessionRef?.compaction?.recallConcurrency) || 5,
|
|
207
|
-
},
|
|
208
|
-
});
|
|
209
|
-
recallText = drained.recallText;
|
|
210
|
-
cycle1Text = drained.cycle1Text;
|
|
211
|
-
diagnostics.cycle1Passes = drained.passes;
|
|
212
|
-
diagnostics.cycle1RawRemaining = drained.rawRemaining;
|
|
213
|
-
diagnostics.cycle1TextBytes = compactByteLength(cycle1Text);
|
|
214
|
-
if (drained.rawRemaining > 0) {
|
|
215
|
-
try { process.stderr.write(`[loop] recall-fasttrack drained passes=${drained.passes} rawRemaining=${drained.rawRemaining} (sess=${sessionId || 'unknown'})\n`); } catch {}
|
|
216
|
-
}
|
|
217
|
-
} catch (err) {
|
|
218
|
-
diagnostics.cycle1Error = compactDiagnosticError(err);
|
|
219
|
-
try { process.stderr.write(`[loop] recall-fasttrack cycle1 skipped (sess=${sessionId || 'unknown'}): ${err?.message || err}\n`); } catch {}
|
|
220
|
-
} finally {
|
|
221
|
-
diagnostics.cycle1Ms = Date.now() - t0;
|
|
222
|
-
}
|
|
223
|
-
} else {
|
|
224
|
-
diagnostics.cycle1Skipped = true;
|
|
225
|
-
diagnostics.cycle1SkipReason = 'session chunks already hydrated';
|
|
226
|
-
diagnostics.cycle1Passes = 0;
|
|
227
|
-
diagnostics.cycle1RawRemaining = 0;
|
|
228
|
-
cycle1Text = 'cycle1: skipped (session chunks already hydrated)';
|
|
229
|
-
}
|
|
230
|
-
const combinedRecallText = [`session_id=${sessionId}`, cycle1Text, recallText].map(v => String(v || '').trim()).filter(Boolean).join('\n\n');
|
|
231
|
-
diagnostics.finalRecallChars = combinedRecallText.length;
|
|
232
|
-
diagnostics.finalRecallBytes = compactByteLength(combinedRecallText);
|
|
233
|
-
const result = recallFastTrackCompactMessages(messages, compactBudgetTokens, {
|
|
234
|
-
reserveTokens: compactPolicy.reserveTokens,
|
|
235
|
-
force: true,
|
|
236
|
-
recallText: combinedRecallText,
|
|
237
|
-
query,
|
|
238
|
-
querySha,
|
|
239
|
-
allowEmptyRecall: true,
|
|
240
|
-
tailTurns: compactPolicy.tailTurns,
|
|
241
|
-
keepTokens: compactPolicy.keepTokens,
|
|
242
|
-
preserveRecentTokens: compactPolicy.preserveRecentTokens,
|
|
243
|
-
});
|
|
244
|
-
diagnostics.totalMs = Date.now() - startedAt;
|
|
245
|
-
if (result && typeof result === 'object') {
|
|
246
|
-
result.diagnostics = {
|
|
247
|
-
...(result.diagnostics || {}),
|
|
248
|
-
pipeline: diagnostics,
|
|
249
|
-
};
|
|
250
|
-
}
|
|
251
|
-
compactDebugLog('recall-fasttrack pipeline', diagnostics);
|
|
252
|
-
return result;
|
|
253
|
-
}
|
|
254
|
-
function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
|
|
255
|
-
if (executeOpts.scopedCacheOutcome) {
|
|
256
|
-
if (sessionRef && toolCallId) {
|
|
257
|
-
if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
|
|
258
|
-
sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, executeOpts.scopedCacheOutcome);
|
|
259
|
-
}
|
|
260
|
-
return executeOpts.scopedCacheOutcome;
|
|
261
|
-
}
|
|
262
|
-
if (!callerSessionId || !toolCallId || !_isScopedCacheableTool(toolName)) return null;
|
|
263
|
-
const outcome = createScopedCacheOutcome();
|
|
264
|
-
if (sessionRef) {
|
|
265
|
-
if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
|
|
266
|
-
sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, outcome);
|
|
267
|
-
}
|
|
268
|
-
return outcome;
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
async function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
|
|
272
|
-
const scopedCacheOutcome = _scopedCacheOutcomeForCall(
|
|
273
|
-
sessionRef,
|
|
274
|
-
executeOpts.toolCallId,
|
|
275
|
-
name,
|
|
276
|
-
callerSessionId,
|
|
277
|
-
executeOpts,
|
|
278
|
-
);
|
|
279
|
-
const toolOpts = scopedCacheOutcome
|
|
280
|
-
? { ...executeOpts, scopedCacheOutcome }
|
|
281
|
-
: executeOpts;
|
|
282
|
-
const notificationSessionId = String(executeOpts.notifySessionId || sessionRef?.ownerSessionId || callerSessionId || '').trim();
|
|
283
|
-
const notifyFn = typeof executeOpts.notifyFn === 'function'
|
|
284
|
-
? executeOpts.notifyFn
|
|
285
|
-
: (text, meta = {}) => {
|
|
286
|
-
if (!notificationSessionId) return;
|
|
287
|
-
try {
|
|
288
|
-
const visible = modelVisibleToolCompletionMessage(text, meta);
|
|
289
|
-
if (visible) enqueuePendingMessage(notificationSessionId, visible);
|
|
290
|
-
} catch { /* best effort */ }
|
|
291
|
-
};
|
|
292
|
-
const completionToolOpts = {
|
|
293
|
-
...toolOpts,
|
|
294
|
-
sessionId: callerSessionId,
|
|
295
|
-
callerSessionId: notificationSessionId || callerSessionId,
|
|
296
|
-
routingSessionId: callerSessionId,
|
|
297
|
-
clientHostPid: sessionRef?.clientHostPid,
|
|
298
|
-
notifyFn,
|
|
299
|
-
};
|
|
300
|
-
const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
|
|
301
|
-
? executeOpts.beforeToolHook
|
|
302
|
-
: sessionRef?.beforeToolHook;
|
|
303
|
-
const toolApprovalHook = typeof executeOpts.toolApprovalHook === 'function'
|
|
304
|
-
? executeOpts.toolApprovalHook
|
|
305
|
-
: sessionRef?.toolApprovalHook;
|
|
306
|
-
if (beforeToolHook) {
|
|
307
|
-
try {
|
|
308
|
-
const decision = await beforeToolHook({
|
|
309
|
-
name,
|
|
310
|
-
args,
|
|
311
|
-
cwd,
|
|
312
|
-
sessionId: callerSessionId,
|
|
313
|
-
toolCallId: executeOpts.toolCallId || null,
|
|
314
|
-
});
|
|
315
|
-
const action = String(decision?.action || decision?.decision || '').toLowerCase();
|
|
316
|
-
if (action === 'deny' || action === 'block') {
|
|
317
|
-
const reason = decision?.reason ? `: ${decision.reason}` : '';
|
|
318
|
-
return `Error: tool "${name}" denied by hook${reason}`;
|
|
319
|
-
}
|
|
320
|
-
if (action === 'ask') {
|
|
321
|
-
const askReason = String(decision?.reason || 'approval requested by hook').trim();
|
|
322
|
-
const askOutcome = await resolvePreToolAskApproval({
|
|
323
|
-
toolName: name,
|
|
324
|
-
args,
|
|
325
|
-
cwd,
|
|
326
|
-
sessionId: callerSessionId,
|
|
327
|
-
toolCallId: executeOpts.toolCallId || null,
|
|
328
|
-
askReason,
|
|
329
|
-
toolApprovalHook,
|
|
330
|
-
});
|
|
331
|
-
if (askOutcome.denial) return askOutcome.denial;
|
|
332
|
-
const approval = askOutcome.approval;
|
|
333
|
-
if (approval && typeof approval === 'object' && approval.args && typeof approval.args === 'object' && !Array.isArray(approval.args)) {
|
|
334
|
-
args = approval.args;
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
if ((action === 'modify' || action === 'rewrite') && decision?.args && typeof decision.args === 'object' && !Array.isArray(decision.args)) {
|
|
338
|
-
args = decision.args;
|
|
339
|
-
}
|
|
340
|
-
} catch {
|
|
341
|
-
// Hooks are policy extensions. A broken hook must not wedge the agent loop.
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
const afterToolHook = typeof executeOpts.afterToolHook === 'function'
|
|
345
|
-
? executeOpts.afterToolHook
|
|
346
|
-
: sessionRef?.afterToolHook;
|
|
347
|
-
const __result = await (async () => {
|
|
348
|
-
if (name === 'Skill') {
|
|
349
|
-
return viewSkill(cwd, args?.name);
|
|
350
|
-
}
|
|
351
|
-
if (name === 'skills_list') {
|
|
352
|
-
return buildSkillsListResponse(cwd);
|
|
353
|
-
}
|
|
354
|
-
if (name === 'skill_view') {
|
|
355
|
-
return viewSkill(cwd, args?.name);
|
|
356
|
-
}
|
|
357
|
-
if (isMcpTool(name)) {
|
|
358
|
-
// 24h trace data shows ~24% of external MCP calls are cwd-sensitive
|
|
359
|
-
// (bash / grep / read / list / glob etc.) but the worker session's
|
|
360
|
-
// cwd was previously dropped here. Inject cwd only when the tool's
|
|
361
|
-
// inputSchema declares the field — schemas without it would reject
|
|
362
|
-
// an unknown argument.
|
|
363
|
-
const needsCwdInjection = cwd
|
|
364
|
-
&& mcpToolHasField(name, 'cwd')
|
|
365
|
-
&& (args == null || args.cwd == null);
|
|
366
|
-
const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
|
|
367
|
-
return executeMcpTool(name, finalArgs);
|
|
368
|
-
}
|
|
369
|
-
if (name === 'code_graph') {
|
|
370
|
-
// cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
|
|
371
|
-
const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
|
|
372
|
-
return executeCodeGraphToolLazy(name, args, graphCwd, null, toolOpts);
|
|
373
|
-
}
|
|
374
|
-
if (isInternalTool(name)) {
|
|
375
|
-
// callerSessionId propagates into server.mjs dispatchTool so that
|
|
376
|
-
// dispatchAiWrapped can detect and reject recursive calls from a
|
|
377
|
-
// hidden-role session (recall/search/explore → self).
|
|
378
|
-
return executeInternalTool(name, args, {
|
|
379
|
-
callerSessionId,
|
|
380
|
-
callerCwd: cwd,
|
|
381
|
-
clientHostPid: sessionRef?.clientHostPid,
|
|
382
|
-
signal: executeOpts.signal,
|
|
383
|
-
routingSessionId: callerSessionId,
|
|
384
|
-
notifyFn,
|
|
385
|
-
});
|
|
386
|
-
}
|
|
387
|
-
if (name === 'shell') {
|
|
388
|
-
const routedArgs = buildAgentBashSessionArgs(args, sessionRef);
|
|
389
|
-
if (!routedArgs) {
|
|
390
|
-
// clientHostPid scopes background shell-jobs to the dispatching
|
|
391
|
-
// terminal's claude.exe pid (agent sessions store it on sessionRef);
|
|
392
|
-
// without it resolveJobOwnerHostPid falls back to the daemon-global env.
|
|
393
|
-
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
394
|
-
}
|
|
395
|
-
// Thread the session's AbortSignal so agent type=close can interrupt the
|
|
396
|
-
// persistent child process. getSessionAbortSignal is imported at top of
|
|
397
|
-
// loop.mjs from manager.mjs; callerSessionId identifies the controller.
|
|
398
|
-
let _bashAbortSignal = null;
|
|
399
|
-
try { _bashAbortSignal = getSessionAbortSignal(callerSessionId); } catch { /* ignore */ }
|
|
400
|
-
const result = await executeBashSessionTool('bash_session', routedArgs, cwd, {
|
|
401
|
-
sessionId: callerSessionId,
|
|
402
|
-
abortSignal: _bashAbortSignal,
|
|
403
|
-
});
|
|
404
|
-
const bashSid = extractBashSessionId(result);
|
|
405
|
-
if (bashSid) {
|
|
406
|
-
sessionRef.implicitBashSessionId = bashSid;
|
|
407
|
-
// Track all persistent bash sessions for bulk teardown on close.
|
|
408
|
-
if (sessionRef.allBashSessionIds) {
|
|
409
|
-
if (!sessionRef.allBashSessionIds.includes(bashSid)) {
|
|
410
|
-
sessionRef.allBashSessionIds.push(bashSid);
|
|
411
|
-
}
|
|
412
|
-
} else {
|
|
413
|
-
sessionRef.allBashSessionIds = [bashSid];
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return result;
|
|
417
|
-
}
|
|
418
|
-
if (name === 'apply_patch') {
|
|
419
|
-
const patchArgs = typeof args === 'string' ? { patch: args } : args;
|
|
420
|
-
return executePatchTool(name, patchArgs, cwd, { sessionId: callerSessionId, toolCallId: executeOpts.toolCallId || null });
|
|
421
|
-
}
|
|
422
|
-
if (isBuiltinTool(name)) {
|
|
423
|
-
// clientHostPid threaded for the same per-terminal job-scope reason as
|
|
424
|
-
// the bash branch above (see resolveJobOwnerHostPid).
|
|
425
|
-
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
426
|
-
}
|
|
427
|
-
if (isExternalAdapterTool(name)) {
|
|
428
|
-
// Foreign-CLI tool names (StrReplace/Write/bash variants) adapt to a
|
|
429
|
-
// native execution inside executeBuiltinTool's default: case; on a
|
|
430
|
-
// shape mismatch it falls back to the redirect guidance message.
|
|
431
|
-
return executeBuiltinTool(name, args, cwd, completionToolOpts);
|
|
432
|
-
}
|
|
433
|
-
return formatUnknownBuiltinToolMessage(name, args, 'tool');
|
|
434
|
-
})();
|
|
435
|
-
if (typeof afterToolHook === 'function') {
|
|
436
|
-
try {
|
|
437
|
-
const hookResult = await afterToolHook({
|
|
438
|
-
name,
|
|
439
|
-
args,
|
|
440
|
-
cwd,
|
|
441
|
-
sessionId: callerSessionId,
|
|
442
|
-
toolCallId: executeOpts.toolCallId || null,
|
|
443
|
-
result: __result,
|
|
444
|
-
});
|
|
445
|
-
// Envelope-aware hook override: a PostToolUse hook may override the
|
|
446
|
-
// model-VISIBLE tool output (the envelope's `result` / stub), but it
|
|
447
|
-
// must NEVER drop the `newMessages` channel. Split first, apply the
|
|
448
|
-
// override to `result` only, then re-wrap so newMessages survive.
|
|
449
|
-
const { result: __res, newMessages: __nm } = normalizeToolEnvelope(__result);
|
|
450
|
-
const __overridden = resolveToolResultAfterHook(__res, hookResult);
|
|
451
|
-
if (__nm.length) return makeToolEnvelope(__overridden, __nm);
|
|
452
|
-
return __overridden;
|
|
453
|
-
} catch {
|
|
454
|
-
// PostToolUse hooks are best-effort; never let one break the tool result.
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
return __result;
|
|
458
|
-
}
|
|
116
|
+
// _scopedCacheOutcomeForCall and executeTool moved to ./loop/tool-exec.mjs
|
|
117
|
+
// (imported above).
|
|
459
118
|
/**
|
|
460
119
|
* Agent loop: send → tool_call → execute → re-send → repeat until text.
|
|
461
120
|
* sendOpts may include:
|
|
@@ -472,10 +131,6 @@ async function executeTool(name, args, cwd, callerSessionId, sessionRef, execute
|
|
|
472
131
|
// was not done — re-prompt instead of accepting empty as final.
|
|
473
132
|
// Covers Anthropic (pause_turn, max_tokens), OpenAI (length), Gemini
|
|
474
133
|
// (MAX_TOKENS, OTHER), and case variants.
|
|
475
|
-
const INCOMPLETE_STOP_REASONS = new Set([
|
|
476
|
-
'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
|
|
477
|
-
]);
|
|
478
|
-
|
|
479
134
|
export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
|
|
480
135
|
let iterations = 0;
|
|
481
136
|
let toolCallsTotal = 0;
|
|
@@ -571,11 +226,57 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
571
226
|
return true;
|
|
572
227
|
};
|
|
573
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
|
+
});
|
|
574
263
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
575
264
|
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
576
265
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
577
266
|
// against the host/plugin root instead of the worker workspace.
|
|
578
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
|
+
];
|
|
579
280
|
while (true) {
|
|
580
281
|
throwIfAborted();
|
|
581
282
|
if (iterations >= maxLoopIterations) {
|
|
@@ -583,6 +284,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
583
284
|
terminatedByCap = true;
|
|
584
285
|
break;
|
|
585
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
|
+
}
|
|
586
324
|
// Drain queued steering/prompts BEFORE the
|
|
587
325
|
// pre-send compact check. The compact decision must see the exact
|
|
588
326
|
// message set that the next provider.send would receive, including
|
|
@@ -614,6 +352,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
614
352
|
const reactivePending = reactiveOverflowRetryPending === true;
|
|
615
353
|
const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, { forceReactive: reactivePending });
|
|
616
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';
|
|
617
362
|
const compactBudgetTokens = shouldCompact
|
|
618
363
|
? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
|
|
619
364
|
: compactPolicy.boundaryTokens;
|
|
@@ -627,11 +372,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
627
372
|
} else {
|
|
628
373
|
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
629
374
|
const compactStartedAt = Date.now();
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
// them apart, then clear the one-shot flag.
|
|
634
|
-
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).
|
|
635
378
|
reactiveOverflowRetryPending = false;
|
|
636
379
|
// PreCompact: bridge to the standard hook bus before compaction
|
|
637
380
|
// runs. session-property hook (manager/loop have no bus access).
|
|
@@ -992,7 +735,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
992
735
|
} else {
|
|
993
736
|
delete opts.toolChoice;
|
|
994
737
|
}
|
|
995
|
-
|
|
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);
|
|
996
743
|
// Eager-dispatch queue: when the provider streams a tool-call event,
|
|
997
744
|
// start read-only tools immediately so execution overlaps with the
|
|
998
745
|
// remaining SSE parse. Writes and unknown tools wait until send()
|
|
@@ -1033,6 +780,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1033
780
|
const _rfg = sessionRef?._repeatFailGuard;
|
|
1034
781
|
if (_rfg && _rfg.sig === _sig && _rfg.count >= REPEAT_FAIL_LIMIT) return null;
|
|
1035
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
|
+
}
|
|
1036
794
|
const toolKind = getToolKind(call.name);
|
|
1037
795
|
// Shared pre-dispatch deny: identical predicate runs in the
|
|
1038
796
|
// serial path below. If any role/permission guard would reject
|
|
@@ -1374,6 +1132,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1374
1132
|
// tool-call-blocked vs contract-required oscillation.
|
|
1375
1133
|
if (!response.toolCalls?.length) {
|
|
1376
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();
|
|
1377
1140
|
// - has content + non-hidden role → valid final, break.
|
|
1378
1141
|
// - empty content + hidden role → contract allows text-only
|
|
1379
1142
|
// terminal turn, break.
|
|
@@ -1447,6 +1210,37 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1447
1210
|
: {}),
|
|
1448
1211
|
};
|
|
1449
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
|
+
}
|
|
1450
1244
|
// Execute each tool and append results.
|
|
1451
1245
|
//
|
|
1452
1246
|
// Intra-turn duplicate suppression: when an LLM emits two tool_use
|
|
@@ -1501,6 +1295,42 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1501
1295
|
});
|
|
1502
1296
|
continue;
|
|
1503
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
|
+
}
|
|
1504
1334
|
// Cross-iteration repeat-failure guard. Distinct from the
|
|
1505
1335
|
// intra-turn dedup above (which spans ONE assistant turn and
|
|
1506
1336
|
// resets every turn): when the model re-issues an IDENTICAL
|
|
@@ -1660,7 +1490,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1660
1490
|
// success path below (_executeOk && _resultKind==='normal'). A failed or
|
|
1661
1491
|
// errored call would otherwise leak its entry in
|
|
1662
1492
|
// sessionRef._scopedCacheOutcomeByCallId forever — reclaim it here.
|
|
1663
|
-
if (sessionRef?._scopedCacheOutcomeByCallId && call?.id && (!_executeOk || _resultKind === 'error')) {
|
|
1493
|
+
if (sessionRef?._scopedCacheOutcomeByCallId instanceof Map && call?.id && (!_executeOk || _resultKind === 'error')) {
|
|
1664
1494
|
sessionRef._scopedCacheOutcomeByCallId.delete(call.id);
|
|
1665
1495
|
}
|
|
1666
1496
|
// PostToolUseFailure: a tool that resolved to a failure (thrown-error
|
|
@@ -1851,7 +1681,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1851
1681
|
// body via the disk path in that stub.
|
|
1852
1682
|
if (sessionId && _executeOk && _resultKind === 'normal') {
|
|
1853
1683
|
if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
|
|
1854
|
-
const
|
|
1684
|
+
const _outcomeMap = sessionRef?._scopedCacheOutcomeByCallId instanceof Map
|
|
1685
|
+
? sessionRef._scopedCacheOutcomeByCallId : null;
|
|
1686
|
+
const _outcome = _outcomeMap?.get(call.id);
|
|
1855
1687
|
setScopedToolCached({
|
|
1856
1688
|
sessionId,
|
|
1857
1689
|
toolName: _toolBare,
|
|
@@ -1861,7 +1693,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1861
1693
|
toolUseId: call.id,
|
|
1862
1694
|
complete: _outcome ? _outcome.complete : true,
|
|
1863
1695
|
});
|
|
1864
|
-
|
|
1696
|
+
_outcomeMap?.delete(call.id);
|
|
1865
1697
|
}
|
|
1866
1698
|
if (_readCacheHit === null && _isReadTool(call.name)) {
|
|
1867
1699
|
// Pass tool_use id so future cache-hits can reference the body's location in history.
|
|
@@ -1885,8 +1717,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1885
1717
|
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
1886
1718
|
...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
|
|
1887
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
|
+
}
|
|
1888
1746
|
} catch (postErr) {
|
|
1889
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
|
+
}
|
|
1890
1757
|
// Post-processing failed AFTER a successful exec: the result is
|
|
1891
1758
|
// replaced with an error below, so preserve this call's full body
|
|
1892
1759
|
// too for a clean retry (mirrors the failed-exec path above).
|
|
@@ -1961,6 +1828,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1961
1828
|
} catch { /* best-effort: PostToolBatch hook must never break the loop */ }
|
|
1962
1829
|
}
|
|
1963
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);
|
|
1964
1836
|
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
1965
1837
|
// AFTER any auto-compact pass. Draining here would put the steering
|
|
1966
1838
|
// user turn after the fresh tool results before compaction runs; then
|
|
@@ -1971,31 +1843,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1971
1843
|
}
|
|
1972
1844
|
// Classify WHY the loop ended so agent-tool can promote an empty/abnormal
|
|
1973
1845
|
// finish to an explicit Lead-facing error instead of a silent empty
|
|
1974
|
-
// "completed"
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
let terminationReason;
|
|
1982
|
-
if (terminatedByCap) {
|
|
1983
|
-
// Real problem regardless of hidden/public: the loop never terminated
|
|
1984
|
-
// on its own contract.
|
|
1985
|
-
terminationReason = 'iteration_cap';
|
|
1986
|
-
} else if (!_finalHasContent && _finalIncompleteStop) {
|
|
1987
|
-
// Cut short mid-synthesis (token cap / provider pause). Real problem
|
|
1988
|
-
// for hidden agents too.
|
|
1989
|
-
terminationReason = 'truncated';
|
|
1990
|
-
} else if (!_finalHasContent && !_finalIsHidden) {
|
|
1991
|
-
// Empty terminal turn. Only public agents violate their contract by
|
|
1992
|
-
// finishing empty — hidden agents (explorer/cycle/…) legitimately emit
|
|
1993
|
-
// text-only/empty terminal turns per their own role contract, so leave
|
|
1994
|
-
// terminationReason undefined for them.
|
|
1995
|
-
terminationReason = 'empty';
|
|
1996
|
-
} else {
|
|
1997
|
-
terminationReason = undefined;
|
|
1998
|
-
}
|
|
1846
|
+
// "completed" (see classifyTerminationReason in ./loop/termination.mjs).
|
|
1847
|
+
const terminationReason = classifyTerminationReason(response, {
|
|
1848
|
+
terminatedByCap,
|
|
1849
|
+
terminatedBySoftCap: _terminatedBySoftCap,
|
|
1850
|
+
softCapActive: _softCapActive,
|
|
1851
|
+
sessionAgent,
|
|
1852
|
+
});
|
|
1999
1853
|
return {
|
|
2000
1854
|
...response,
|
|
2001
1855
|
usage: lastUsage || response.usage,
|