mixdog 0.9.3 → 0.9.5
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/README.md +112 -38
- package/package.json +10 -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/r5-orchestrated-task.json +7 -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-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -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 +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- 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-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- 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 +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- 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 +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -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 +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -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/provider-catalog-cache.mjs +80 -0
- 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 +275 -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 +278 -402
- 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/stall-policy.mjs +20 -1
- 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 +35 -44
- 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 +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- 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/scheduler.mjs +27 -113
- 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 +313 -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 +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -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/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/ko-morph.mjs +195 -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 +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -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/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- 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 +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +119 -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 +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -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/session-runtime/workflow.mjs +1 -1
- 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 +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- 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 +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -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 +356 -0
- package/src/tui/app/model-options.mjs +334 -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 +489 -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 +677 -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 +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- 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 +13553 -12384
- 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 +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- 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 +77 -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 +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- 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/channels/lib/holidays.mjs +0 -138
- 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,17 +226,122 @@ 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
|
+
// Hard-cap final-answer turn: one tool-less wrap-up turn granted when the
|
|
249
|
+
// hard iteration cap fires, so the session ends with text, not empty.
|
|
250
|
+
let _capFinalTurnUsed = false;
|
|
251
|
+
// Completion-first steering ladder controller. Owns the (cumulative) level-1
|
|
252
|
+
// fire count, the all-read-only / serial-single / same-file-grep streaks,
|
|
253
|
+
// and the level-2 latch. Threaded via live getters so it reads the loop's
|
|
254
|
+
// current `iterations` / `_editCount` on every call (no stale snapshots).
|
|
255
|
+
const _steeringLadder = createSteeringLadder({
|
|
256
|
+
sessionId,
|
|
257
|
+
sessionAgent,
|
|
258
|
+
tools,
|
|
259
|
+
getIterations: () => iterations,
|
|
260
|
+
softCapEnabled: _softCapEnabled,
|
|
261
|
+
getEditCount: () => _editCount,
|
|
262
|
+
readOnlyRole: String(sessionRef?.permission || sessionRef?.toolPermission || '') === 'read',
|
|
263
|
+
pushUserMessage: (msg) => messages.push(msg),
|
|
264
|
+
pushSystemReminder: (text) => messages.push({ role: 'user', content: `<system-reminder>\n${text}\n</system-reminder>`, meta: 'hook' }),
|
|
265
|
+
});
|
|
574
266
|
// Tool execution must use the session cwd even when the caller omitted the
|
|
575
267
|
// legacy positional cwd argument. Agent workers always carry their cwd on
|
|
576
268
|
// sessionRef; falling through to pwd()/process.cwd() resolves relatives
|
|
577
269
|
// against the host/plugin root instead of the worker workspace.
|
|
578
270
|
cwd = cwd || sessionRef?.cwd || undefined;
|
|
271
|
+
// Staged pre-cap warnings + one true hard stop. The ONLY count-based
|
|
272
|
+
// forced termination is the hard cap at maxLoopIterations (default 200):
|
|
273
|
+
// a genuine runaway guard. Before it, staged warnings fire at 50%/75%/90%
|
|
274
|
+
// of the cap steering the model to converge — warnings only, nothing is
|
|
275
|
+
// cut off early. All other runaway protection is behavior-based (steering
|
|
276
|
+
// ladder early wrap-up, REPEAT_FAIL_LIMIT), never a lower count.
|
|
277
|
+
let _iterWarnStage = 0;
|
|
278
|
+
const _iterWarnAt = [
|
|
279
|
+
Math.floor(maxLoopIterations * 0.5),
|
|
280
|
+
Math.floor(maxLoopIterations * 0.75),
|
|
281
|
+
Math.floor(maxLoopIterations * 0.9),
|
|
282
|
+
];
|
|
579
283
|
while (true) {
|
|
580
284
|
throwIfAborted();
|
|
581
285
|
if (iterations >= maxLoopIterations) {
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
286
|
+
// Final-answer turn: instead of breaking mid-transcript (which
|
|
287
|
+
// yields an empty final for locator-style agents that never got to
|
|
288
|
+
// answer), give the model ONE tool-less text turn to wrap up, then
|
|
289
|
+
// stop. Same mechanism as the worker soft cap (empty sendTools).
|
|
290
|
+
if (_capFinalTurnUsed) {
|
|
291
|
+
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); stopping loop.\n`);
|
|
292
|
+
terminatedByCap = true;
|
|
293
|
+
// The granted final turn produced no text (model kept emitting
|
|
294
|
+
// tool calls into refusal stubs, or thinking-only). Synthesize a
|
|
295
|
+
// non-empty final so callers never see an empty response.
|
|
296
|
+
if (response && !String(response.content || '').trim()) {
|
|
297
|
+
response.content = sessionAgent === 'explorer'
|
|
298
|
+
? 'EXPLORATION_FAILED'
|
|
299
|
+
: '[iteration cap reached before final text]';
|
|
300
|
+
if (Array.isArray(response.toolCalls)) response.toolCalls = [];
|
|
301
|
+
}
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
_capFinalTurnUsed = true;
|
|
305
|
+
_softCapActive = true; // reuse soft-cap plumbing: no tool defs, refusal stubs
|
|
306
|
+
messages.push({ role: 'user', content: '<system-reminder>\nIteration cap reached. Tools are disabled. Answer NOW with your best result from what you already found.\n</system-reminder>', meta: 'hook' });
|
|
307
|
+
process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); forcing final text turn.\n`);
|
|
308
|
+
}
|
|
309
|
+
if (_iterWarnStage < _iterWarnAt.length && iterations >= _iterWarnAt[_iterWarnStage]) {
|
|
310
|
+
_iterWarnStage += 1;
|
|
311
|
+
const warnAt = _iterWarnAt[_iterWarnStage - 1];
|
|
312
|
+
const stageMsg = _iterWarnStage === 1
|
|
313
|
+
? `Iteration budget notice: ${warnAt} of ${maxLoopIterations} iterations used. Converge on a conclusion: prefer finishing the current objective over opening new exploration.`
|
|
314
|
+
: `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.`;
|
|
315
|
+
messages.push({ role: 'user', content: `<system-reminder>\n${stageMsg}\n</system-reminder>`, meta: 'hook' });
|
|
316
|
+
process.stderr.write(`[loop] iteration warning stage ${_iterWarnStage} at ${iterations} (sess=${sessionId || 'unknown'}); continuing with steer.\n`);
|
|
317
|
+
try {
|
|
318
|
+
appendAgentTrace({
|
|
319
|
+
sessionId,
|
|
320
|
+
iteration: iterations,
|
|
321
|
+
kind: 'steer',
|
|
322
|
+
payload: { tag: 'iteration_warning', stage: _iterWarnStage, at: iterations, unit: maxLoopIterations },
|
|
323
|
+
agent: sessionAgent || null,
|
|
324
|
+
});
|
|
325
|
+
} catch { /* best-effort */ }
|
|
326
|
+
}
|
|
327
|
+
// Worker soft cap (Step 3): non-lead sessions that reach the soft-cap
|
|
328
|
+
// iteration count switch to a text-only wrap-up. On the FIRST crossing
|
|
329
|
+
// we disable tool defs (below, via _softCapActive) and inject the
|
|
330
|
+
// assistant-visible wrap-up directive as a user message so the next
|
|
331
|
+
// send produces a final text summary. Lead/TUI sessions never enter.
|
|
332
|
+
const _earlySoftCap = _steeringLadder.earlySoftCapArmed();
|
|
333
|
+
if (_softCapEnabled && !_softCapActive && (iterations >= WORKER_SOFT_CAP_ITERATIONS || _earlySoftCap)) {
|
|
334
|
+
_softCapActive = true;
|
|
335
|
+
messages.push({ role: 'user', content: `<system-reminder>\n${SOFT_CAP_WRAPUP_MESSAGE}\n</system-reminder>`, meta: 'hook' });
|
|
336
|
+
try {
|
|
337
|
+
appendAgentTrace({
|
|
338
|
+
sessionId,
|
|
339
|
+
iteration: iterations,
|
|
340
|
+
kind: 'steer',
|
|
341
|
+
payload: { tag: 'soft_cap_wrapup', soft_cap: WORKER_SOFT_CAP_ITERATIONS, early: _earlySoftCap, level2_fires: _steeringLadder.level2FireCount },
|
|
342
|
+
agent: sessionAgent || null,
|
|
343
|
+
});
|
|
344
|
+
} catch { /* best-effort */ }
|
|
585
345
|
}
|
|
586
346
|
// Drain queued steering/prompts BEFORE the
|
|
587
347
|
// pre-send compact check. The compact decision must see the exact
|
|
@@ -614,6 +374,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
614
374
|
const reactivePending = reactiveOverflowRetryPending === true;
|
|
615
375
|
const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, { forceReactive: reactivePending });
|
|
616
376
|
const pressureTokens = compactionTelemetryPressureTokens(messageTokensEst, compactPolicy, { reactivePending });
|
|
377
|
+
// A pending reactive-overflow retry makes THIS compact pass the
|
|
378
|
+
// recovery from a provider overflow refusal, not the proactive
|
|
379
|
+
// pressure trigger. Tag the emitted events so telemetry can tell
|
|
380
|
+
// them apart. Hoisted above the shouldCompact branch because the
|
|
381
|
+
// PostCompact hook below fires on BOTH paths (fixes a
|
|
382
|
+
// ReferenceError on the no-compact path).
|
|
383
|
+
const compactTrigger = reactivePending ? 'reactive' : 'auto';
|
|
617
384
|
const compactBudgetTokens = shouldCompact
|
|
618
385
|
? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
|
|
619
386
|
: compactPolicy.boundaryTokens;
|
|
@@ -625,13 +392,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
625
392
|
pressureTokens,
|
|
626
393
|
});
|
|
627
394
|
} else {
|
|
628
|
-
try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
395
|
+
try { await opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
|
|
629
396
|
const compactStartedAt = Date.now();
|
|
630
|
-
//
|
|
631
|
-
//
|
|
632
|
-
//
|
|
633
|
-
// them apart, then clear the one-shot flag.
|
|
634
|
-
const compactTrigger = reactiveOverflowRetryPending ? 'reactive' : 'auto';
|
|
397
|
+
// Clear the one-shot reactive-overflow flag now that this
|
|
398
|
+
// compact pass is consuming it (compactTrigger already
|
|
399
|
+
// captured it above).
|
|
635
400
|
reactiveOverflowRetryPending = false;
|
|
636
401
|
// PreCompact: bridge to the standard hook bus before compaction
|
|
637
402
|
// runs. session-property hook (manager/loop have no bus access).
|
|
@@ -883,7 +648,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
883
648
|
}, compactErr);
|
|
884
649
|
}
|
|
885
650
|
}
|
|
886
|
-
try { opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
|
|
651
|
+
try { await opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
|
|
887
652
|
const compactChanged = messagesArrayChanged(messages, compacted);
|
|
888
653
|
if (compactChanged) {
|
|
889
654
|
messages.length = 0;
|
|
@@ -992,7 +757,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
992
757
|
} else {
|
|
993
758
|
delete opts.toolChoice;
|
|
994
759
|
}
|
|
995
|
-
|
|
760
|
+
// Soft-cap wrap-up (Step 3a): once active, send NO tool definitions so
|
|
761
|
+
// the provider can only emit text. Overrides the forced-first-tool path.
|
|
762
|
+
const sendTools = _softCapActive
|
|
763
|
+
? []
|
|
764
|
+
: (forcedFirstToolDef && toolCallsTotal === 0 ? [forcedFirstToolDef] : tools);
|
|
996
765
|
// Eager-dispatch queue: when the provider streams a tool-call event,
|
|
997
766
|
// start read-only tools immediately so execution overlaps with the
|
|
998
767
|
// remaining SSE parse. Writes and unknown tools wait until send()
|
|
@@ -1033,6 +802,17 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1033
802
|
const _rfg = sessionRef?._repeatFailGuard;
|
|
1034
803
|
if (_rfg && _rfg.sig === _sig && _rfg.count >= REPEAT_FAIL_LIMIT) return null;
|
|
1035
804
|
}
|
|
805
|
+
// Cross-turn dedup also gates eager dispatch (mirror of the
|
|
806
|
+
// repeat-failure guard above): a read-only call whose (name,args)
|
|
807
|
+
// signature already ran in an EARLIER turn must NOT be eagerly
|
|
808
|
+
// re-executed — the serial for-body pushes the [cross-turn-dedup]
|
|
809
|
+
// stub instead. Without this gate startEagerRun/onToolCall would
|
|
810
|
+
// re-run the call before the serial dedup check ever sees it.
|
|
811
|
+
{
|
|
812
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
813
|
+
const _prior = _crossTurnCalls.get(_ctSig);
|
|
814
|
+
if (_prior && _prior.firstIteration < iterations) return null;
|
|
815
|
+
}
|
|
1036
816
|
const toolKind = getToolKind(call.name);
|
|
1037
817
|
// Shared pre-dispatch deny: identical predicate runs in the
|
|
1038
818
|
// serial path below. If any role/permission guard would reject
|
|
@@ -1374,6 +1154,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1374
1154
|
// tool-call-blocked vs contract-required oscillation.
|
|
1375
1155
|
if (!response.toolCalls?.length) {
|
|
1376
1156
|
// No tool calls. Decide between final-answer accept vs nudge.
|
|
1157
|
+
// Reviewer fix: a zero-tool turn (final-pre-send steering drain or
|
|
1158
|
+
// contract nudge `continue`) must not bridge the all-read-only
|
|
1159
|
+
// streak across non-tool turns — that would fire level-2 early on
|
|
1160
|
+
// a worker that paused to synthesize text mid-run.
|
|
1161
|
+
_steeringLadder.resetAllReadOnlyStreak();
|
|
1377
1162
|
// - has content + non-hidden role → valid final, break.
|
|
1378
1163
|
// - empty content + hidden role → contract allows text-only
|
|
1379
1164
|
// terminal turn, break.
|
|
@@ -1447,6 +1232,37 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1447
1232
|
: {}),
|
|
1448
1233
|
};
|
|
1449
1234
|
messages.push(_assistantTurnMsg);
|
|
1235
|
+
// Soft-cap wrap-up (Step 3b): tools are disabled but the model still
|
|
1236
|
+
// emitted tool calls. Do NOT execute them — push a refusal stub for
|
|
1237
|
+
// each (after the assistant turn is appended so tool_use/tool_result
|
|
1238
|
+
// pairing stays valid) and consume a grace turn. After 2 grace turns,
|
|
1239
|
+
// terminate via the soft-cap path so a model that never complies stops.
|
|
1240
|
+
if (_softCapActive) {
|
|
1241
|
+
for (const _c of calls) {
|
|
1242
|
+
pushToolResultMessage({
|
|
1243
|
+
role: 'tool',
|
|
1244
|
+
content: SOFT_CAP_REFUSAL_STUB,
|
|
1245
|
+
toolCallId: _c.id,
|
|
1246
|
+
toolKind: 'error',
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
_softCapGraceTurns += 1;
|
|
1250
|
+
try {
|
|
1251
|
+
appendAgentTrace({
|
|
1252
|
+
sessionId,
|
|
1253
|
+
iteration: iterations,
|
|
1254
|
+
kind: 'steer',
|
|
1255
|
+
payload: { tag: 'soft_cap_wrapup', grace_turn: _softCapGraceTurns },
|
|
1256
|
+
agent: sessionAgent || null,
|
|
1257
|
+
});
|
|
1258
|
+
} catch { /* best-effort */ }
|
|
1259
|
+
if (_softCapGraceTurns >= 2) {
|
|
1260
|
+
_terminatedBySoftCap = true;
|
|
1261
|
+
break;
|
|
1262
|
+
}
|
|
1263
|
+
if (sessionId) updateSessionStage(sessionId, 'connecting');
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1450
1266
|
// Execute each tool and append results.
|
|
1451
1267
|
//
|
|
1452
1268
|
// Intra-turn duplicate suppression: when an LLM emits two tool_use
|
|
@@ -1501,6 +1317,42 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1501
1317
|
});
|
|
1502
1318
|
continue;
|
|
1503
1319
|
}
|
|
1320
|
+
// Cross-turn identical-call stub (Step 2): a SUCCESSFUL read-only
|
|
1321
|
+
// (isEagerDispatchable) call whose (name,args) signature already ran
|
|
1322
|
+
// in an EARLIER turn is not re-executed — its result is unchanged and
|
|
1323
|
+
// already in context. Warn at the 2nd occurrence; append the "stuck"
|
|
1324
|
+
// escalation tail once the session has emitted 5+ dedup stubs total.
|
|
1325
|
+
// Never applies to write/bash/MCP/skill tools (not eager-dispatchable).
|
|
1326
|
+
if (isEagerDispatchable(call.name, tools)) {
|
|
1327
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
1328
|
+
const _prior = _crossTurnCalls.get(_ctSig);
|
|
1329
|
+
if (_prior && _prior.firstIteration < iterations) {
|
|
1330
|
+
_prior.count += 1;
|
|
1331
|
+
_dedupStubTotal += 1;
|
|
1332
|
+
const _stub = crossTurnDedupStub(call.name, _prior.firstIteration, _dedupStubTotal >= 5);
|
|
1333
|
+
pushToolResultMessage({
|
|
1334
|
+
role: 'tool',
|
|
1335
|
+
content: _stub,
|
|
1336
|
+
toolCallId: call.id,
|
|
1337
|
+
});
|
|
1338
|
+
try {
|
|
1339
|
+
appendAgentTrace({
|
|
1340
|
+
sessionId,
|
|
1341
|
+
iteration: iterations,
|
|
1342
|
+
kind: 'steer',
|
|
1343
|
+
payload: {
|
|
1344
|
+
tag: 'cross_turn_dedup',
|
|
1345
|
+
tool: call.name,
|
|
1346
|
+
occurrence: _prior.count,
|
|
1347
|
+
first_iteration: _prior.firstIteration,
|
|
1348
|
+
dedup_stub_total: _dedupStubTotal,
|
|
1349
|
+
},
|
|
1350
|
+
agent: sessionAgent || null,
|
|
1351
|
+
});
|
|
1352
|
+
} catch { /* best-effort */ }
|
|
1353
|
+
continue;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
1504
1356
|
// Cross-iteration repeat-failure guard. Distinct from the
|
|
1505
1357
|
// intra-turn dedup above (which spans ONE assistant turn and
|
|
1506
1358
|
// resets every turn): when the model re-issues an IDENTICAL
|
|
@@ -1660,7 +1512,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1660
1512
|
// success path below (_executeOk && _resultKind==='normal'). A failed or
|
|
1661
1513
|
// errored call would otherwise leak its entry in
|
|
1662
1514
|
// sessionRef._scopedCacheOutcomeByCallId forever — reclaim it here.
|
|
1663
|
-
if (sessionRef?._scopedCacheOutcomeByCallId && call?.id && (!_executeOk || _resultKind === 'error')) {
|
|
1515
|
+
if (sessionRef?._scopedCacheOutcomeByCallId instanceof Map && call?.id && (!_executeOk || _resultKind === 'error')) {
|
|
1664
1516
|
sessionRef._scopedCacheOutcomeByCallId.delete(call.id);
|
|
1665
1517
|
}
|
|
1666
1518
|
// PostToolUseFailure: a tool that resolved to a failure (thrown-error
|
|
@@ -1851,7 +1703,9 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1851
1703
|
// body via the disk path in that stub.
|
|
1852
1704
|
if (sessionId && _executeOk && _resultKind === 'normal') {
|
|
1853
1705
|
if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
|
|
1854
|
-
const
|
|
1706
|
+
const _outcomeMap = sessionRef?._scopedCacheOutcomeByCallId instanceof Map
|
|
1707
|
+
? sessionRef._scopedCacheOutcomeByCallId : null;
|
|
1708
|
+
const _outcome = _outcomeMap?.get(call.id);
|
|
1855
1709
|
setScopedToolCached({
|
|
1856
1710
|
sessionId,
|
|
1857
1711
|
toolName: _toolBare,
|
|
@@ -1861,7 +1715,7 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1861
1715
|
toolUseId: call.id,
|
|
1862
1716
|
complete: _outcome ? _outcome.complete : true,
|
|
1863
1717
|
});
|
|
1864
|
-
|
|
1718
|
+
_outcomeMap?.delete(call.id);
|
|
1865
1719
|
}
|
|
1866
1720
|
if (_readCacheHit === null && _isReadTool(call.name)) {
|
|
1867
1721
|
// Pass tool_use id so future cache-hits can reference the body's location in history.
|
|
@@ -1885,8 +1739,43 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1885
1739
|
...(_nativeToolSearch ? { nativeToolSearch: _nativeToolSearch } : {}),
|
|
1886
1740
|
...(_applyPatchUiDiff ? { uiDiff: _applyPatchUiDiff } : {}),
|
|
1887
1741
|
});
|
|
1742
|
+
// Completion-first bookkeeping (Steps 1 & 2). Only successful
|
|
1743
|
+
// executions count. Edit/progress = any executed tool whose def
|
|
1744
|
+
// lacks readOnlyHint (apply_patch/bash/MCP-write/skill/...).
|
|
1745
|
+
// Read-only successful calls seed the cross-turn dedup map.
|
|
1746
|
+
if (_executeOk) {
|
|
1747
|
+
const _isEager = isEagerDispatchable(call.name, tools);
|
|
1748
|
+
if (_isEager) {
|
|
1749
|
+
const _ctSig = crossTurnSignature(call.name, call.arguments);
|
|
1750
|
+
if (!_crossTurnCalls.has(_ctSig)) {
|
|
1751
|
+
_crossTurnCalls.set(_ctSig, { count: 1, firstIteration: iterations });
|
|
1752
|
+
if (_crossTurnCalls.size > _CROSS_TURN_CAP) {
|
|
1753
|
+
const _oldest = _crossTurnCalls.keys().next().value;
|
|
1754
|
+
_crossTurnCalls.delete(_oldest);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
} else {
|
|
1758
|
+
// A successful mutating (non-eager) tool invalidates the
|
|
1759
|
+
// cross-turn dedup map wholesale: any prior read/grep may
|
|
1760
|
+
// now return different content, so a post-edit
|
|
1761
|
+
// verification read must NOT be stubbed as "unchanged".
|
|
1762
|
+
if (isEditProgressTool(call.name, false)) {
|
|
1763
|
+
_crossTurnCalls.clear();
|
|
1764
|
+
_editCount += 1;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1888
1768
|
} catch (postErr) {
|
|
1889
1769
|
_postProcessOk = false;
|
|
1770
|
+
// Reviewer fix: the exec itself succeeded — if it was a
|
|
1771
|
+
// mutating edit-progress tool, the file changes are real even
|
|
1772
|
+
// though post-processing failed, so the cross-turn dedup map
|
|
1773
|
+
// must still be invalidated (otherwise a later verification
|
|
1774
|
+
// read could be stubbed as "unchanged" against stale sigs).
|
|
1775
|
+
if (_executeOk && !isEagerDispatchable(call.name, tools) && isEditProgressTool(call.name, false)) {
|
|
1776
|
+
_crossTurnCalls.clear();
|
|
1777
|
+
_editCount += 1;
|
|
1778
|
+
}
|
|
1890
1779
|
// Post-processing failed AFTER a successful exec: the result is
|
|
1891
1780
|
// replaced with an error below, so preserve this call's full body
|
|
1892
1781
|
// too for a clean retry (mirrors the failed-exec path above).
|
|
@@ -1961,6 +1850,11 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1961
1850
|
} catch { /* best-effort: PostToolBatch hook must never break the loop */ }
|
|
1962
1851
|
}
|
|
1963
1852
|
}
|
|
1853
|
+
// Completion-first steering hints (missed-parallelism / all-read-only /
|
|
1854
|
+
// serial-rewording). At most ONE hint per turn (priority: soft-cap >
|
|
1855
|
+
// level-2 > same-file grep > level-1); soft-cap active suppresses all.
|
|
1856
|
+
// The ladder controller owns the cumulative counters and streaks.
|
|
1857
|
+
_steeringLadder.emitPostBatchSteering(calls, _softCapActive);
|
|
1964
1858
|
// Mid-turn steering is drained at the next loop's pre-send point,
|
|
1965
1859
|
// AFTER any auto-compact pass. Draining here would put the steering
|
|
1966
1860
|
// user turn after the fresh tool results before compaction runs; then
|
|
@@ -1971,31 +1865,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
1971
1865
|
}
|
|
1972
1866
|
// Classify WHY the loop ended so agent-tool can promote an empty/abnormal
|
|
1973
1867
|
// 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
|
-
}
|
|
1868
|
+
// "completed" (see classifyTerminationReason in ./loop/termination.mjs).
|
|
1869
|
+
const terminationReason = classifyTerminationReason(response, {
|
|
1870
|
+
terminatedByCap,
|
|
1871
|
+
terminatedBySoftCap: _terminatedBySoftCap,
|
|
1872
|
+
softCapActive: _softCapActive,
|
|
1873
|
+
sessionAgent,
|
|
1874
|
+
});
|
|
1999
1875
|
return {
|
|
2000
1876
|
...response,
|
|
2001
1877
|
usage: lastUsage || response.usage,
|