@pentoshi/clai 3.9.2 → 3.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/dist/agent/compaction-attempt.d.ts +22 -0
- package/dist/agent/compaction-attempt.js +69 -0
- package/dist/agent/compaction-attempt.js.map +1 -0
- package/dist/agent/compaction-summary.d.ts +27 -0
- package/dist/agent/compaction-summary.js +62 -0
- package/dist/agent/compaction-summary.js.map +1 -1
- package/dist/agent/context-breakdown.d.ts +2 -0
- package/dist/agent/context-breakdown.js +37 -0
- package/dist/agent/context-breakdown.js.map +1 -1
- package/dist/agent/context-manager.d.ts +7 -1
- package/dist/agent/context-manager.js +64 -15
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/continue-orient.d.ts +8 -0
- package/dist/agent/continue-orient.js +26 -3
- package/dist/agent/continue-orient.js.map +1 -1
- package/dist/agent/durable-envelope.d.ts +39 -0
- package/dist/agent/durable-envelope.js +195 -0
- package/dist/agent/durable-envelope.js.map +1 -0
- package/dist/agent/events.d.ts +21 -0
- package/dist/agent/loop-guard.d.ts +13 -5
- package/dist/agent/loop-guard.js +114 -1
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/outcomes.d.ts +29 -0
- package/dist/agent/outcomes.js +118 -3
- package/dist/agent/outcomes.js.map +1 -1
- package/dist/agent/plan-decision.js +8 -3
- package/dist/agent/plan-decision.js.map +1 -1
- package/dist/agent/plan-tool.d.ts +5 -0
- package/dist/agent/plan-tool.js +166 -6
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/reliability-policy.d.ts +11 -6
- package/dist/agent/reliability-policy.js +19 -13
- package/dist/agent/reliability-policy.js.map +1 -1
- package/dist/agent/request-budget.d.ts +24 -0
- package/dist/agent/request-budget.js +59 -0
- package/dist/agent/request-budget.js.map +1 -0
- package/dist/agent/responder-context.js +2 -2
- package/dist/agent/responder-context.js.map +1 -1
- package/dist/agent/responder-parent.d.ts +30 -0
- package/dist/agent/responder-parent.js +82 -0
- package/dist/agent/responder-parent.js.map +1 -0
- package/dist/agent/runner.d.ts +15 -2
- package/dist/agent/runner.js +766 -213
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/scope-context.d.ts +5 -0
- package/dist/agent/scope-context.js +7 -1
- package/dist/agent/scope-context.js.map +1 -1
- package/dist/agent/session-policy.js +1 -0
- package/dist/agent/session-policy.js.map +1 -1
- package/dist/agent/stop-summary.js +13 -7
- package/dist/agent/stop-summary.js.map +1 -1
- package/dist/agent/task-analyzer.js +7 -3
- package/dist/agent/task-analyzer.js.map +1 -1
- package/dist/agent/task-evidence.js +2 -0
- package/dist/agent/task-evidence.js.map +1 -1
- package/dist/agent/task-sync.d.ts +11 -0
- package/dist/agent/task-sync.js +31 -0
- package/dist/agent/task-sync.js.map +1 -1
- package/dist/agent/tool-call-parser.d.ts +6 -13
- package/dist/agent/tool-call-parser.js +186 -82
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/tool-history.d.ts +8 -2
- package/dist/agent/tool-history.js +8 -1
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/agent/turn-outcome.d.ts +4 -1
- package/dist/agent/turn-outcome.js +16 -2
- package/dist/agent/turn-outcome.js.map +1 -1
- package/dist/app/adapters/agent-event-adapter.js +27 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/adapters/current-agent-adapter.js +1 -0
- package/dist/app/adapters/current-agent-adapter.js.map +1 -1
- package/dist/app/adapters/current-interactive-sessions-adapter.d.ts +4 -0
- package/dist/app/adapters/current-interactive-sessions-adapter.js +11 -0
- package/dist/app/adapters/current-interactive-sessions-adapter.js.map +1 -0
- package/dist/app/adapters/current-jobs-adapter.js +4 -3
- package/dist/app/adapters/current-jobs-adapter.js.map +1 -1
- package/dist/app/adapters/current-store-adapter.js +4 -3
- package/dist/app/adapters/current-store-adapter.js.map +1 -1
- package/dist/app/adapters/current-updates-adapter.d.ts +1 -1
- package/dist/app/adapters/current-updates-adapter.js +34 -5
- package/dist/app/adapters/current-updates-adapter.js.map +1 -1
- package/dist/app/adapters/quiet-meta-tools.js +2 -1
- package/dist/app/adapters/quiet-meta-tools.js.map +1 -1
- package/dist/app/controllers/cancel-all-result.d.ts +8 -0
- package/dist/app/controllers/cancel-all-result.js +32 -0
- package/dist/app/controllers/cancel-all-result.js.map +1 -0
- package/dist/app/controllers/job-controller.d.ts +4 -3
- package/dist/app/controllers/job-controller.js +9 -6
- package/dist/app/controllers/job-controller.js.map +1 -1
- package/dist/app/controllers/plan-controller.js +14 -0
- package/dist/app/controllers/plan-controller.js.map +1 -1
- package/dist/app/controllers/responder-settlement.js +2 -2
- package/dist/app/controllers/responder-settlement.js.map +1 -1
- package/dist/app/controllers/session-compact-helper.d.ts +23 -1
- package/dist/app/controllers/session-compact-helper.js +117 -7
- package/dist/app/controllers/session-compact-helper.js.map +1 -1
- package/dist/app/controllers/session-context-usage.d.ts +24 -0
- package/dist/app/controllers/session-context-usage.js +89 -0
- package/dist/app/controllers/session-context-usage.js.map +1 -0
- package/dist/app/controllers/session-controller.d.ts +25 -17
- package/dist/app/controllers/session-controller.js +142 -128
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-prompt-queue.d.ts +3 -0
- package/dist/app/controllers/session-prompt-queue.js +20 -1
- package/dist/app/controllers/session-prompt-queue.js.map +1 -1
- package/dist/app/controllers/session-responder.js +16 -6
- package/dist/app/controllers/session-responder.js.map +1 -1
- package/dist/app/controllers/session-turn-request.d.ts +18 -0
- package/dist/app/controllers/session-turn-request.js +31 -0
- package/dist/app/controllers/session-turn-request.js.map +1 -0
- package/dist/app/controllers/turn-continuation.d.ts +14 -0
- package/dist/app/controllers/turn-continuation.js +46 -0
- package/dist/app/controllers/turn-continuation.js.map +1 -0
- package/dist/app/events/app-event.d.ts +21 -0
- package/dist/app/events/app-event.js +1 -0
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/app/ports/agent-port.d.ts +7 -0
- package/dist/app/ports/interactive-sessions-port.d.ts +21 -0
- package/dist/app/ports/interactive-sessions-port.js +9 -0
- package/dist/app/ports/interactive-sessions-port.js.map +1 -0
- package/dist/app/ports/jobs-port.d.ts +4 -3
- package/dist/app/ports/persistence-port.d.ts +11 -0
- package/dist/app/ports/updates-port.d.ts +8 -0
- package/dist/attachments/service.js +13 -2
- package/dist/attachments/service.js.map +1 -1
- package/dist/commands/providers.js +13 -10
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/update.d.ts +4 -0
- package/dist/commands/update.js +14 -0
- package/dist/commands/update.js.map +1 -1
- package/dist/index.js +21 -8
- package/dist/index.js.map +1 -1
- package/dist/interactive-session/artifact-writer.d.ts +54 -0
- package/dist/interactive-session/artifact-writer.js +161 -0
- package/dist/interactive-session/artifact-writer.js.map +1 -0
- package/dist/interactive-session/cleanup.d.ts +36 -0
- package/dist/interactive-session/cleanup.js +157 -0
- package/dist/interactive-session/cleanup.js.map +1 -0
- package/dist/interactive-session/config.d.ts +118 -0
- package/dist/interactive-session/config.js +141 -0
- package/dist/interactive-session/config.js.map +1 -0
- package/dist/interactive-session/input-policy.d.ts +41 -0
- package/dist/interactive-session/input-policy.js +163 -0
- package/dist/interactive-session/input-policy.js.map +1 -0
- package/dist/interactive-session/manager.d.ts +107 -0
- package/dist/interactive-session/manager.js +927 -0
- package/dist/interactive-session/manager.js.map +1 -0
- package/dist/interactive-session/output-store.d.ts +81 -0
- package/dist/interactive-session/output-store.js +239 -0
- package/dist/interactive-session/output-store.js.map +1 -0
- package/dist/interactive-session/output-view.d.ts +20 -0
- package/dist/interactive-session/output-view.js +142 -0
- package/dist/interactive-session/output-view.js.map +1 -0
- package/dist/interactive-session/recovery-journal.d.ts +55 -0
- package/dist/interactive-session/recovery-journal.js +115 -0
- package/dist/interactive-session/recovery-journal.js.map +1 -0
- package/dist/interactive-session/registry.d.ts +59 -0
- package/dist/interactive-session/registry.js +170 -0
- package/dist/interactive-session/registry.js.map +1 -0
- package/dist/interactive-session/runtime.d.ts +107 -0
- package/dist/interactive-session/runtime.js +191 -0
- package/dist/interactive-session/runtime.js.map +1 -0
- package/dist/interactive-session/session-runtime.d.ts +50 -0
- package/dist/interactive-session/session-runtime.js +63 -0
- package/dist/interactive-session/session-runtime.js.map +1 -0
- package/dist/interactive-session/streaming-redactor.d.ts +53 -0
- package/dist/interactive-session/streaming-redactor.js +263 -0
- package/dist/interactive-session/streaming-redactor.js.map +1 -0
- package/dist/interactive-session/telemetry.d.ts +30 -0
- package/dist/interactive-session/telemetry.js +47 -0
- package/dist/interactive-session/telemetry.js.map +1 -0
- package/dist/interactive-session/transport-factory.d.ts +8 -0
- package/dist/interactive-session/transport-factory.js +16 -0
- package/dist/interactive-session/transport-factory.js.map +1 -0
- package/dist/interactive-session/transport-node-pty.d.ts +49 -0
- package/dist/interactive-session/transport-node-pty.js +219 -0
- package/dist/interactive-session/transport-node-pty.js.map +1 -0
- package/dist/interactive-session/transport-pipe.d.ts +9 -0
- package/dist/interactive-session/transport-pipe.js +215 -0
- package/dist/interactive-session/transport-pipe.js.map +1 -0
- package/dist/interactive-session/transport.d.ts +104 -0
- package/dist/interactive-session/transport.js +89 -0
- package/dist/interactive-session/transport.js.map +1 -0
- package/dist/interactive-session/types.d.ts +254 -0
- package/dist/interactive-session/types.js +191 -0
- package/dist/interactive-session/types.js.map +1 -0
- package/dist/llm/adapters/anthropic-tools.d.ts +14 -1
- package/dist/llm/adapters/anthropic-tools.js +36 -5
- package/dist/llm/adapters/anthropic-tools.js.map +1 -1
- package/dist/llm/adapters/gemini-tools.d.ts +3 -1
- package/dist/llm/adapters/gemini-tools.js +5 -2
- package/dist/llm/adapters/gemini-tools.js.map +1 -1
- package/dist/llm/adapters/ollama-tools.js +8 -3
- package/dist/llm/adapters/ollama-tools.js.map +1 -1
- package/dist/llm/adapters/openai-tools.js +8 -1
- package/dist/llm/adapters/openai-tools.js.map +1 -1
- package/dist/llm/agentrouter.js +2 -0
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.d.ts +8 -0
- package/dist/llm/anthropic.js +68 -9
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +60 -12
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +2 -0
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +7 -0
- package/dist/llm/capabilities.js +22 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/gemini.d.ts +3 -0
- package/dist/llm/gemini.js +71 -11
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.d.ts +20 -1
- package/dist/llm/groq.js +32 -4
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +29 -0
- package/dist/llm/http.js +122 -29
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +2 -0
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/nvidia.js +2 -0
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.d.ts +5 -0
- package/dist/llm/ollama.js +53 -6
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +2 -0
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +2 -0
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/qwen-cloud.js +2 -0
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/router.d.ts +16 -0
- package/dist/llm/router.js +79 -23
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/sampling.d.ts +32 -0
- package/dist/llm/sampling.js +48 -0
- package/dist/llm/sampling.js.map +1 -0
- package/dist/llm/stream-progress.d.ts +4 -0
- package/dist/llm/stream-progress.js +43 -0
- package/dist/llm/stream-progress.js.map +1 -0
- package/dist/llm/system-messages.d.ts +29 -0
- package/dist/llm/system-messages.js +51 -0
- package/dist/llm/system-messages.js.map +1 -0
- package/dist/llm/token-usage.d.ts +4 -1
- package/dist/llm/token-usage.js +56 -4
- package/dist/llm/token-usage.js.map +1 -1
- package/dist/llm/tool-protocol.js +30 -0
- package/dist/llm/tool-protocol.js.map +1 -1
- package/dist/os/process-identity.d.ts +46 -0
- package/dist/os/process-identity.js +147 -0
- package/dist/os/process-identity.js.map +1 -0
- package/dist/os/process-tree.d.ts +15 -0
- package/dist/os/process-tree.js +77 -0
- package/dist/os/process-tree.js.map +1 -0
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/system.agent.md +13 -12
- package/dist/repl/prompt-line.d.ts +5 -0
- package/dist/repl/prompt-line.js +7 -1
- package/dist/repl/prompt-line.js.map +1 -1
- package/dist/repl.js +71 -41
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js +98 -44
- package/dist/safety/classifier.js.map +1 -1
- package/dist/safety/engagement-policy.d.ts +8 -0
- package/dist/safety/engagement-policy.js +52 -7
- package/dist/safety/engagement-policy.js.map +1 -1
- package/dist/safety/patterns.d.ts +44 -0
- package/dist/safety/patterns.js +168 -37
- package/dist/safety/patterns.js.map +1 -1
- package/dist/store/config.d.ts +9 -1
- package/dist/store/config.js +51 -1
- package/dist/store/config.js.map +1 -1
- package/dist/store/history-index.d.ts +12 -0
- package/dist/store/history-index.js +47 -18
- package/dist/store/history-index.js.map +1 -1
- package/dist/store/history.d.ts +5 -2
- package/dist/store/history.js +153 -62
- package/dist/store/history.js.map +1 -1
- package/dist/store/plan.d.ts +81 -5
- package/dist/store/plan.js +406 -46
- package/dist/store/plan.js.map +1 -1
- package/dist/store/redaction-cache.d.ts +12 -0
- package/dist/store/redaction-cache.js +51 -0
- package/dist/store/redaction-cache.js.map +1 -0
- package/dist/store/responder-settlement.d.ts +5 -1
- package/dist/store/responder-settlement.js +42 -30
- package/dist/store/responder-settlement.js.map +1 -1
- package/dist/store/scope.js +36 -27
- package/dist/store/scope.js.map +1 -1
- package/dist/store/task-transitions.d.ts +19 -0
- package/dist/store/task-transitions.js +46 -0
- package/dist/store/task-transitions.js.map +1 -0
- package/dist/tools/capabilities.js +33 -18
- package/dist/tools/capabilities.js.map +1 -1
- package/dist/tools/command-intent.d.ts +24 -0
- package/dist/tools/command-intent.js +158 -4
- package/dist/tools/command-intent.js.map +1 -1
- package/dist/tools/definitions.d.ts +5 -1
- package/dist/tools/definitions.js +257 -21
- package/dist/tools/definitions.js.map +1 -1
- package/dist/tools/fs.d.ts +16 -0
- package/dist/tools/fs.js +122 -46
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/image.js +1 -1
- package/dist/tools/image.js.map +1 -1
- package/dist/tools/interactive-session-tools.d.ts +11 -0
- package/dist/tools/interactive-session-tools.js +261 -0
- package/dist/tools/interactive-session-tools.js.map +1 -0
- package/dist/tools/jobs.d.ts +78 -4
- package/dist/tools/jobs.js +486 -127
- package/dist/tools/jobs.js.map +1 -1
- package/dist/tools/net-ping-sweep.d.ts +3 -1
- package/dist/tools/net-ping-sweep.js +64 -26
- package/dist/tools/net-ping-sweep.js.map +1 -1
- package/dist/tools/nmap-runner.js +5 -2
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/pdf.js +50 -7
- package/dist/tools/pdf.js.map +1 -1
- package/dist/tools/registry.js +158 -96
- package/dist/tools/registry.js.map +1 -1
- package/dist/tools/shell.d.ts +24 -2
- package/dist/tools/shell.js +113 -28
- package/dist/tools/shell.js.map +1 -1
- package/dist/tools/tool-types.d.ts +2 -0
- package/dist/tools/validate.d.ts +8 -5
- package/dist/tools/validate.js +177 -7
- package/dist/tools/validate.js.map +1 -1
- package/dist/tools/wordlists.js +69 -17
- package/dist/tools/wordlists.js.map +1 -1
- package/dist/tui/state.d.ts +4 -0
- package/dist/tui/state.js +88 -2
- package/dist/tui/state.js.map +1 -1
- package/dist/tui-v2/actions/keymap.js +3 -8
- package/dist/tui-v2/actions/keymap.js.map +1 -1
- package/dist/tui-v2/app/App.js +52 -4
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/app/commands/config-commands.js +5 -2
- package/dist/tui-v2/app/commands/config-commands.js.map +1 -1
- package/dist/tui-v2/app/commands/picker-commands.js +3 -0
- package/dist/tui-v2/app/commands/picker-commands.js.map +1 -1
- package/dist/tui-v2/app/plan-lifecycle.d.ts +1 -1
- package/dist/tui-v2/app/plan-lifecycle.js +2 -1
- package/dist/tui-v2/app/plan-lifecycle.js.map +1 -1
- package/dist/tui-v2/bootstrap/composition-root.d.ts +3 -0
- package/dist/tui-v2/bootstrap/composition-root.js +3 -0
- package/dist/tui-v2/bootstrap/composition-root.js.map +1 -1
- package/dist/tui-v2/bootstrap/start-tui-v2.js +10 -0
- package/dist/tui-v2/bootstrap/start-tui-v2.js.map +1 -1
- package/dist/tui-v2/components/composer/composer-input-box.d.ts +1 -1
- package/dist/tui-v2/components/composer/composer-input-box.js +12 -3
- package/dist/tui-v2/components/composer/composer-input-box.js.map +1 -1
- package/dist/tui-v2/components/jobs/jobs-panel.js +19 -6
- package/dist/tui-v2/components/jobs/jobs-panel.js.map +1 -1
- package/dist/tui-v2/components/plan/plan-view.js +9 -6
- package/dist/tui-v2/components/plan/plan-view.js.map +1 -1
- package/dist/tui-v2/components/status/status-line.d.ts +15 -0
- package/dist/tui-v2/components/status/status-line.js +33 -25
- package/dist/tui-v2/components/status/status-line.js.map +1 -1
- package/dist/tui-v2/components/transcript/assistant-message.js +17 -9
- package/dist/tui-v2/components/transcript/assistant-message.js.map +1 -1
- package/dist/tui-v2/components/transcript/compacted-row.js +49 -22
- package/dist/tui-v2/components/transcript/compacted-row.js.map +1 -1
- package/dist/tui-v2/components/transcript/intro-card.js +3 -0
- package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
- package/dist/tui-v2/components/transcript/thinking-block.js +2 -1
- package/dist/tui-v2/components/transcript/thinking-block.js.map +1 -1
- package/dist/tui-v2/components/transcript/transcript-view.js +27 -2
- package/dist/tui-v2/components/transcript/transcript-view.js.map +1 -1
- package/dist/tui-v2/components/transcript/use-transcript-selection.js +57 -6
- package/dist/tui-v2/components/transcript/use-transcript-selection.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +12 -6
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/tui-v2/rendering/incremental-strip.d.ts +11 -0
- package/dist/tui-v2/rendering/incremental-strip.js +89 -0
- package/dist/tui-v2/rendering/incremental-strip.js.map +1 -0
- package/dist/tui-v2/rendering/plan-view.d.ts +12 -0
- package/dist/tui-v2/rendering/plan-view.js +71 -2
- package/dist/tui-v2/rendering/plan-view.js.map +1 -1
- package/dist/tui-v2/rendering/streaming-markdown.d.ts +25 -0
- package/dist/tui-v2/rendering/streaming-markdown.js +108 -0
- package/dist/tui-v2/rendering/streaming-markdown.js.map +1 -0
- package/dist/tui-v2/rendering/thinking-tail.d.ts +5 -0
- package/dist/tui-v2/rendering/thinking-tail.js +26 -0
- package/dist/tui-v2/rendering/thinking-tail.js.map +1 -0
- package/dist/tui-v2/state/transcript-hydrate.js +6 -2
- package/dist/tui-v2/state/transcript-hydrate.js.map +1 -1
- package/dist/tui-v2/state/transcript-reducer.js +146 -25
- package/dist/tui-v2/state/transcript-reducer.js.map +1 -1
- package/dist/tui-v2/state/transcript-types.d.ts +6 -0
- package/dist/tui-v2/state/transcript-types.js +17 -0
- package/dist/tui-v2/state/transcript-types.js.map +1 -1
- package/dist/types.d.ts +34 -0
- package/dist/types.js.map +1 -1
- package/dist/ui/intro-header.d.ts +1 -0
- package/dist/ui/intro-header.js +10 -13
- package/dist/ui/intro-header.js.map +1 -1
- package/dist/ui/markdown.js +9 -2
- package/dist/ui/markdown.js.map +1 -1
- package/dist/ui/mentions.d.ts +5 -0
- package/dist/ui/mentions.js +66 -7
- package/dist/ui/mentions.js.map +1 -1
- package/dist/ui/thinking.d.ts +5 -1
- package/dist/ui/thinking.js +3 -2
- package/dist/ui/thinking.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +2 -1
package/dist/agent/runner.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
-
import {
|
|
3
|
+
import { completeWithProvider, streamWithProvider } from "../llm/router.js";
|
|
4
|
+
import { streamAlreadyEmitted } from "../llm/stream-progress.js";
|
|
4
5
|
import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, createStreamRecoveryState, resetStreamRecoveryState, } from "./stream-recovery.js";
|
|
5
6
|
import { resolveToolDialect } from "../llm/capabilities.js";
|
|
6
7
|
import { syntheticToolCallId, isTextOnlyModel, fromWireName, } from "../llm/tool-protocol.js";
|
|
7
8
|
import { sanitizeAssistantText } from "../ui/ansi-box.js";
|
|
8
9
|
import { randomUUID } from "node:crypto";
|
|
9
10
|
import { jobManager, } from "../tools/jobs.js";
|
|
10
|
-
import { responderContextMessage, upsertResponderContextMessage, upsertResponderResultLedger, } from "./responder-context.js";
|
|
11
|
+
import { isResponderResultLedgerMessage, responderContextMessage, upsertResponderContextMessage, upsertResponderResultLedger, } from "./responder-context.js";
|
|
11
12
|
import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, scratchDirFor, toolNudge, } from "../prompts/index.js";
|
|
12
13
|
import { getConfig } from "../store/config.js";
|
|
13
14
|
import { beginSessionWorkspace, getActiveSessionWorkspace, } from "../store/session-workspace.js";
|
|
14
15
|
import { groqInputTokenBudget } from "../llm/groq.js";
|
|
15
|
-
import { classifyToolCall, isPentestToolCall,
|
|
16
|
+
import { classifyToolCall, isPentestToolCall, scopeTargetForToolCall, } from "../safety/classifier.js";
|
|
16
17
|
/**
|
|
17
18
|
* Scope/engagement classification runs on every tool call and parses
|
|
18
19
|
* model-supplied arguments (URLs, hosts, commands). A malformed argument must
|
|
@@ -27,20 +28,20 @@ function safeScopeTargetForToolCall(call) {
|
|
|
27
28
|
return undefined;
|
|
28
29
|
}
|
|
29
30
|
}
|
|
30
|
-
function
|
|
31
|
+
function safeEngagementActionsForToolCall(call) {
|
|
31
32
|
try {
|
|
32
|
-
return
|
|
33
|
+
return engagementActionsForToolCall(call);
|
|
33
34
|
}
|
|
34
35
|
catch {
|
|
35
|
-
return
|
|
36
|
+
return [];
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
|
|
39
|
-
import { getToolDefinitions, getCompactToolDefinitions,
|
|
40
|
+
import { getToolDefinitions, getCompactToolDefinitions, RUNNER_META_TOOL_NAMES, } from "../tools/definitions.js";
|
|
40
41
|
import { appendAssistantWithTools, ensureUniqueToolCallIds, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
|
|
41
42
|
import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
|
|
42
43
|
import { compactMessagesWithSummary, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
|
|
43
|
-
import { buildContextBreakdown, contextBreakdownAuditPayload, } from "./context-breakdown.js";
|
|
44
|
+
import { buildContextBreakdown, contextBreakdownAuditPayload, describeDominantContextBlock, toolSchemaHash, } from "./context-breakdown.js";
|
|
44
45
|
import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, resolveStepMaxTokens, } from "./reliability-policy.js";
|
|
45
46
|
import { auditLog } from "../store/logs.js";
|
|
46
47
|
import { loadProjectContext } from "../store/project.js";
|
|
@@ -53,24 +54,29 @@ import { safeCwd } from "../os/cwd.js";
|
|
|
53
54
|
import { analyzeTask, formatTaskAnalysisHint, isNarrowExplicitNmapOperation, } from "./task-analyzer.js";
|
|
54
55
|
import { computeMaxIterations, computeStepBudget } from "./step-budget.js";
|
|
55
56
|
import { isScratchOnlyWrite } from "./scratch-write.js";
|
|
57
|
+
import { buildDurableEnvelope, WorkLedger, } from "./durable-envelope.js";
|
|
56
58
|
import { COMPACTION_SYSTEM_PROMPT, } from "./compaction-summary.js";
|
|
57
59
|
import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
|
|
58
60
|
import { LoopGuard } from "./loop-guard.js";
|
|
59
|
-
import {
|
|
60
|
-
import {
|
|
61
|
+
import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-attempt.js";
|
|
62
|
+
import { resolveRequestBudget } from "./request-budget.js";
|
|
63
|
+
import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
|
|
64
|
+
import { stat } from "node:fs/promises";
|
|
65
|
+
import { isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools/fs.js";
|
|
61
66
|
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, looksLikePlanNarration, looksLikeErrorDiagnosisWithFixIntent, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
62
67
|
import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
|
|
63
68
|
import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
|
|
64
69
|
import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
|
|
65
|
-
import { renderPlanForTerminal, planContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
|
|
66
|
-
import {
|
|
67
|
-
import {
|
|
70
|
+
import { renderPlanForTerminal, planContextMessage, upsertPlanContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
|
|
71
|
+
import { readDeclaredParentTaskId, resolveResponderParent, isExplicitResponderDelegation, delegationTaskTitle, } from "./responder-parent.js";
|
|
72
|
+
import { readTaskUpdateArgs, distinctAdvancingTaskIds, isSimultaneousTaskAdvance, batchUpdateSignature, buildMultiUpdateReminder, buildMultiOpenRejection, multiUpdateToast, multiOpenToast, openingTaskIds, } from "./task-sync.js";
|
|
73
|
+
import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteObservationTask, isRemoteReconToolCall, isRuntimeObservationTask, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
|
|
68
74
|
import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
|
|
69
|
-
import { buildContinueOrientation } from "./continue-orient.js";
|
|
75
|
+
import { buildContinueOrientation, looksLikeContinueOrResumePrompt, } from "./continue-orient.js";
|
|
70
76
|
import { detectPackageManager } from "./workspace-orient.js";
|
|
71
77
|
import { budgetRemaining, consumeBudget, createRecoveryBudgets, freestyleClaimsAppReady, looksLikeShallowPentestReport, recoveryForErrorDiagnosis, recoveryForFailedProbe, recoveryForFreshness, recoveryForMissingFeature, recoveryForMissingPlan, recoveryForNarration, recoveryForPrematureComplete, recoveryForRuntimeVerify, recoveryForShallowPentest, } from "./must-continue.js";
|
|
72
|
-
import { scopeContextMessage } from "./scope-context.js";
|
|
73
|
-
import { EngagementPolicyEngine, actionFromUrl,
|
|
78
|
+
import { outOfScopeToolMessage, scopeContextMessage } from "./scope-context.js";
|
|
79
|
+
import { EngagementPolicyEngine, actionFromUrl, engagementActionsForToolCall, evaluateEngagementAction, } from "../safety/engagement-policy.js";
|
|
74
80
|
import { patchPlanMeta } from "../store/plan.js";
|
|
75
81
|
import { extractProjectRootFromPlan, extractProjectRootFromScaffold, extractProjectRootFromText, getActiveProjectRoot, setActiveProjectRootIfValid, } from "./project-root.js";
|
|
76
82
|
import { buildWorkspaceOrientation, discoverImmediateProjectRoots, guessProjectFolderName, isBareParentDirectory, isScaffoldCancelledOutput, scaffoldLooksMaterialized, scaffoldTargetConflictMessage, resolveScaffoldTargetPath, } from "./workspace-orient.js";
|
|
@@ -80,28 +86,51 @@ import { composeAgentSystemPrompt } from "./prompt-composer.js";
|
|
|
80
86
|
import { createGovernorState, governProgress, } from "./evidence-governor.js";
|
|
81
87
|
import { createTurnState, transitionTurn, } from "./turn-state.js";
|
|
82
88
|
import { deriveOutcomeStatus, inferOutcomeKind, openOutcomeState, recordAnswerEvidence, recordFailedHypothesis, recordToolEvidence, saveOutcomeState, validateCriterionEvidence, } from "./outcomes.js";
|
|
83
|
-
import { createTurnOutcome, renderTurnOutcome } from "./turn-outcome.js";
|
|
89
|
+
import { createTurnOutcome, normalizeTurnOutcomeInput, renderTurnOutcome, } from "./turn-outcome.js";
|
|
84
90
|
import { beginEngagementAction, finishEngagementAction, recordEngagementCheckpoint, reconcileEngagementJob, openEngagement, saveEngagement, } from "../store/engagement.js";
|
|
85
91
|
export * from "./tool-call-parser.js";
|
|
86
92
|
export { createSessionPolicy, isPreApprovalAllowedTool, isPlanApprovedByStatus, planHasOpenWork, shouldEnableImageOcr, } from "./session-policy.js";
|
|
87
93
|
export function styleToolChatter(call, text) {
|
|
88
94
|
return shouldDimToolChatter(call) ? chalk.dim(text) : text;
|
|
89
95
|
}
|
|
90
|
-
|
|
96
|
+
/**
|
|
97
|
+
* A foreground task waits for a responder child only when the plan
|
|
98
|
+
* Declares that dependency. Report titles carry no scheduling meaning: any
|
|
99
|
+
* Dependency-ready foreground work is executed while children keep running, and
|
|
100
|
+
* Their results arrive as addenda.
|
|
101
|
+
*
|
|
102
|
+
* A declared dependency blocks only while the child is genuinely live (running
|
|
103
|
+
* Job or an undelivered/unanalyzed receipt), so an orphaned child can never
|
|
104
|
+
* Stall the turn forever.
|
|
105
|
+
*/
|
|
106
|
+
export function shouldYieldForDeclaredResponderDependency(plan, runningJobs, notifications, currentNotificationId) {
|
|
91
107
|
if (!plan)
|
|
92
108
|
return false;
|
|
93
109
|
const unfinished = plan.tasks.filter((task) => !task.responderOwned &&
|
|
94
110
|
(task.state === "pending" || task.state === "in_progress"));
|
|
95
|
-
if (unfinished.length === 0
|
|
96
|
-
!unfinished.every((task) => classifyTaskTitle(task.title, { planKind: plan.kind }) === "report")) {
|
|
111
|
+
if (unfinished.length === 0)
|
|
97
112
|
return false;
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
113
|
+
const childById = new Map(plan.tasks.filter((task) => task.responderOwned).map((task) => [task.id, task]));
|
|
114
|
+
const isLive = (child) => {
|
|
115
|
+
if (child.state === "done" || child.state === "skipped" || child.state === "failed") {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
const running = runningJobs.some((job) => job.responder &&
|
|
119
|
+
(job.taskId === child.id || (!!child.jobId && job.id === child.jobId)));
|
|
120
|
+
if (running)
|
|
121
|
+
return true;
|
|
122
|
+
return notifications.some((notification) => notification.responder &&
|
|
102
123
|
!notification.archivedAt &&
|
|
103
124
|
!notification.readAt &&
|
|
104
|
-
!notification.analyzedAt
|
|
125
|
+
!notification.analyzedAt &&
|
|
126
|
+
(!currentNotificationId || notification.id !== currentNotificationId) &&
|
|
127
|
+
(notification.taskId === child.id ||
|
|
128
|
+
(!!child.jobId && notification.jobId === child.jobId)));
|
|
129
|
+
};
|
|
130
|
+
return unfinished.every((task) => (task.dependencies ?? []).some((dependency) => {
|
|
131
|
+
const child = childById.get(dependency);
|
|
132
|
+
return !!child && isLive(child);
|
|
133
|
+
}));
|
|
105
134
|
}
|
|
106
135
|
/**
|
|
107
136
|
* Cancellable backoff. Resolves after `ms`, or rejects immediately if the
|
|
@@ -246,33 +275,61 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
246
275
|
};
|
|
247
276
|
/** Strip a known prefix from a string, returning the remainder unchanged. */
|
|
248
277
|
const insertedText = (value, prefix) => value.startsWith(prefix) ? value.slice(prefix.length) : value;
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
278
|
+
const writeCompactionStarted = (id, beforeTokens) => {
|
|
279
|
+
emit({ type: "compaction-start", id, beforeTokens });
|
|
280
|
+
if (writesDirectly) {
|
|
281
|
+
process.stdout.write(chalk.dim(" ✦ Compacted Context · streaming Markdown\n\n"));
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
const writeCompactionDelta = (id, text) => {
|
|
285
|
+
if (!text)
|
|
286
|
+
return;
|
|
287
|
+
emit({ type: "compaction-delta", id, text });
|
|
288
|
+
if (writesDirectly)
|
|
289
|
+
process.stdout.write(text);
|
|
290
|
+
};
|
|
291
|
+
const writeCompactionCompleted = (id, summary, beforeTokens, afterTokens) => {
|
|
292
|
+
emit({
|
|
293
|
+
type: "compaction-completed",
|
|
294
|
+
id,
|
|
295
|
+
summary,
|
|
296
|
+
beforeTokens,
|
|
297
|
+
afterTokens,
|
|
298
|
+
});
|
|
299
|
+
if (writesDirectly) {
|
|
300
|
+
const footer = chalk.dim(`\n ~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens`);
|
|
301
|
+
process.stdout.write(`${footer}\n`);
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
const writeCompactionFailed = (id, message, retainedTokens) => {
|
|
305
|
+
emit({ type: "compaction-failed", id, message, retainedTokens });
|
|
256
306
|
if (writesDirectly) {
|
|
257
|
-
|
|
258
|
-
const footer = chalk.dim(` ~${beforeTokens.toLocaleString()} \u2192 ~${afterTokens.toLocaleString()} tokens`);
|
|
259
|
-
const body = summary ? renderMarkdown(summary) : "(empty summary)";
|
|
260
|
-
process.stdout.write(`${header}\n\n${body}\n${footer}\n`);
|
|
307
|
+
process.stdout.write(chalk.yellow(`\n Compaction failed: ${message} (~${retainedTokens.toLocaleString()} tokens retained)\n`));
|
|
261
308
|
}
|
|
262
309
|
};
|
|
263
310
|
// Points at the live message array so finishTurn can hand the full
|
|
264
311
|
// conversation back to the caller. Assigned once `messages` is built below;
|
|
265
312
|
// all later mutations are in-place so this reference stays current.
|
|
266
313
|
let liveMessages = [];
|
|
314
|
+
let suppressOutcomeDiagnostics = false;
|
|
315
|
+
const unreadResponderNotificationIds = new Set();
|
|
316
|
+
const releaseUnreadResponderClaims = () => {
|
|
317
|
+
for (const notificationId of unreadResponderNotificationIds) {
|
|
318
|
+
jobManager.releaseResponderNotificationClaim(notificationId);
|
|
319
|
+
}
|
|
320
|
+
};
|
|
267
321
|
const finishTurn = (answer, steps, status = "succeeded", remainingCriteria = [], reason) => {
|
|
268
|
-
|
|
322
|
+
releaseUnreadResponderClaims();
|
|
323
|
+
const outcome = createTurnOutcome(normalizeTurnOutcomeInput({
|
|
269
324
|
status,
|
|
270
325
|
answer,
|
|
271
326
|
steps,
|
|
272
327
|
remainingCriteria,
|
|
273
328
|
reason,
|
|
329
|
+
}));
|
|
330
|
+
const rendered = renderTurnOutcome(outcome, {
|
|
331
|
+
diagnostics: !suppressOutcomeDiagnostics,
|
|
274
332
|
});
|
|
275
|
-
const rendered = renderTurnOutcome(outcome);
|
|
276
333
|
writeAssistantMessage(rendered);
|
|
277
334
|
if (options.onMessages) {
|
|
278
335
|
try {
|
|
@@ -323,6 +380,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
323
380
|
// freshness retries — a false "act don't narrate" path burned tokens on
|
|
324
381
|
// web.search recovery loops after a simple "hi".
|
|
325
382
|
const idleOrSocialPrompt = looksLikeIdleOrSocialPrompt(prompt);
|
|
383
|
+
suppressOutcomeDiagnostics =
|
|
384
|
+
informationalQuery ||
|
|
385
|
+
idleOrSocialPrompt ||
|
|
386
|
+
looksLikeContinueOrResumePrompt(prompt);
|
|
326
387
|
const freshWebSearchRequired = !buildLikeTurn &&
|
|
327
388
|
!pentestLikeTurn &&
|
|
328
389
|
!idleOrSocialPrompt &&
|
|
@@ -348,7 +409,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
348
409
|
const base = compact
|
|
349
410
|
? getCompactToolDefinitions()
|
|
350
411
|
: getToolDefinitions();
|
|
351
|
-
const allow = new Set([...toolNames, ...
|
|
412
|
+
const allow = new Set([...toolNames, ...RUNNER_META_TOOL_NAMES]);
|
|
352
413
|
return base.filter((d) => allow.has(d.name));
|
|
353
414
|
};
|
|
354
415
|
let lastAnswer = "";
|
|
@@ -379,6 +440,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
379
440
|
// behind a stale "awaiting approval" gate for a plan that already ran.
|
|
380
441
|
session.planApproved.value = true;
|
|
381
442
|
}
|
|
443
|
+
suppressOutcomeDiagnostics ||= !session.planApproved.value;
|
|
382
444
|
const destinationHint = resolveUserDestinationHint(prompt);
|
|
383
445
|
const orientationSourceText = [
|
|
384
446
|
prompt,
|
|
@@ -469,9 +531,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
469
531
|
return sections.join("\n\n");
|
|
470
532
|
};
|
|
471
533
|
const systemSections = [buildSystemContent(nativeToolsActive)];
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
534
|
+
// The live plan is mutable state: it is injected once as a keyed request
|
|
535
|
+
// suffix (upsertPlanContextMessage) instead of being frozen into the stable
|
|
536
|
+
// system prefix, so the model never sees a stale and a fresh plan together.
|
|
475
537
|
// Soft mid-work recovery (any domain): re-attach to jobs / open tasks /
|
|
476
538
|
// last tools after interrupt or "continue" — not a hard gate.
|
|
477
539
|
if (!informationalQuery && !idleOrSocialPrompt) {
|
|
@@ -483,6 +545,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
483
545
|
recentJobs: jobManager.getRecentJobs(12, session.sessionId),
|
|
484
546
|
informationalQuery,
|
|
485
547
|
idleOrSocial: idleOrSocialPrompt,
|
|
548
|
+
...(options.previousTurn ? { previousTurn: options.previousTurn } : {}),
|
|
486
549
|
});
|
|
487
550
|
if (continueBrief) {
|
|
488
551
|
systemSections.push(continueBrief);
|
|
@@ -585,7 +648,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
585
648
|
if (!has("plan")) {
|
|
586
649
|
sections.push({
|
|
587
650
|
kind: "plan",
|
|
588
|
-
content: "
|
|
651
|
+
content: "PLAN PROTOCOL\nThe live plan, when one exists, is appended to this request as a single ACTIVE PLAN message. Treat that message as the only authoritative plan state; never rely on plan details quoted in earlier turns.",
|
|
589
652
|
mandatory: true,
|
|
590
653
|
});
|
|
591
654
|
}
|
|
@@ -636,13 +699,31 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
636
699
|
userMessage,
|
|
637
700
|
];
|
|
638
701
|
liveMessages = messages;
|
|
639
|
-
|
|
702
|
+
if (activePlan) {
|
|
703
|
+
upsertPlanContextMessage(messages, planContextMessage(activePlan, session.planApproved.value));
|
|
704
|
+
}
|
|
705
|
+
const responderWakeTurn = options.displayPrompt === null &&
|
|
706
|
+
prompt.startsWith("Responder result arrived");
|
|
640
707
|
const responderWakeNotificationId = responderWakeTurn
|
|
641
708
|
? /^notification=(.+)$/m.exec(prompt)?.[1]?.trim()
|
|
642
709
|
: undefined;
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
710
|
+
const responderWakeJobId = responderWakeTurn
|
|
711
|
+
? /^job=(.+)$/m.exec(prompt)?.[1]?.trim()
|
|
712
|
+
: undefined;
|
|
713
|
+
const responderWakeResultRevision = responderWakeTurn
|
|
714
|
+
? Number(/^resultRevision=(\d+)$/m.exec(prompt)?.[1]) || undefined
|
|
715
|
+
: undefined;
|
|
716
|
+
const matchesWakeRevision = (notification) => responderWakeResultRevision === undefined ||
|
|
717
|
+
(notification.resultRevision ?? 1) === responderWakeResultRevision;
|
|
718
|
+
const wakeNotification = responderWakeNotificationId
|
|
719
|
+
? jobManager
|
|
720
|
+
.getPendingNotifications(session.sessionId)
|
|
721
|
+
.find((notification) => notification.id === responderWakeNotificationId &&
|
|
722
|
+
(!responderWakeJobId || notification.jobId === responderWakeJobId) &&
|
|
723
|
+
matchesWakeRevision(notification))
|
|
724
|
+
: undefined;
|
|
725
|
+
if (wakeNotification) {
|
|
726
|
+
unreadResponderNotificationIds.add(wakeNotification.id);
|
|
646
727
|
}
|
|
647
728
|
const refreshResponderInbox = () => {
|
|
648
729
|
const running = jobManager
|
|
@@ -653,6 +734,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
653
734
|
.getPendingNotifications(session.sessionId)
|
|
654
735
|
.filter((notification) => notification.responder &&
|
|
655
736
|
unreadResponderNotificationIds.has(notification.id) &&
|
|
737
|
+
matchesWakeRevision(notification) &&
|
|
656
738
|
!notification.readAt &&
|
|
657
739
|
!notification.analyzedAt &&
|
|
658
740
|
!notification.archivedAt)
|
|
@@ -715,6 +797,33 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
715
797
|
};
|
|
716
798
|
const loopGuard = new LoopGuard();
|
|
717
799
|
const engagementPolicy = new EngagementPolicyEngine();
|
|
800
|
+
const probeStateKey = (call) => {
|
|
801
|
+
const project = (job) => job
|
|
802
|
+
? [
|
|
803
|
+
job.id,
|
|
804
|
+
job.status,
|
|
805
|
+
job.exitCode ?? null,
|
|
806
|
+
job.signal ?? null,
|
|
807
|
+
job.stdoutArtifact,
|
|
808
|
+
job.artifacts.stdout.bytes,
|
|
809
|
+
job.artifacts.stdout.sha256,
|
|
810
|
+
job.stderrArtifact,
|
|
811
|
+
job.artifacts.stderr.bytes,
|
|
812
|
+
job.artifacts.stderr.sha256,
|
|
813
|
+
]
|
|
814
|
+
: undefined;
|
|
815
|
+
if (call.name === "shell.tail" && typeof call.args.id === "string") {
|
|
816
|
+
const projected = project(jobManager.getJob(call.args.id));
|
|
817
|
+
return projected ? JSON.stringify(projected) : undefined;
|
|
818
|
+
}
|
|
819
|
+
if (call.name === "shell.jobs") {
|
|
820
|
+
return JSON.stringify(jobManager
|
|
821
|
+
.getRecentJobs(100, session.sessionId)
|
|
822
|
+
.map((job) => project(job))
|
|
823
|
+
.filter(Boolean));
|
|
824
|
+
}
|
|
825
|
+
return undefined;
|
|
826
|
+
};
|
|
718
827
|
// Track consecutive thinking-only responses so we can nudge the model
|
|
719
828
|
// to actually act instead of silently returning an empty answer.
|
|
720
829
|
let emptyVisibleRetries = 0;
|
|
@@ -807,7 +916,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
807
916
|
});
|
|
808
917
|
};
|
|
809
918
|
const reconcileOpenTaskBeforeFinalizing = async () => {
|
|
810
|
-
|
|
919
|
+
let plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
811
920
|
const open = plan?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
|
|
812
921
|
if (!plan || !open)
|
|
813
922
|
return plan;
|
|
@@ -815,42 +924,80 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
815
924
|
if (!gate.ok)
|
|
816
925
|
return plan;
|
|
817
926
|
const reconciledTaskIds = [open.id];
|
|
818
|
-
|
|
927
|
+
const working = plan;
|
|
928
|
+
markTask(working, open.id, "done", "Completion reconciled from verified task evidence.");
|
|
819
929
|
while (true) {
|
|
820
|
-
const observation = readyPlanTasks(
|
|
821
|
-
(
|
|
930
|
+
const observation = readyPlanTasks(working).find((task) => isRuntimeObservationTask(task.title) ||
|
|
931
|
+
(working.kind === "pentest" && isRemoteObservationTask(task.title)));
|
|
822
932
|
if (!observation)
|
|
823
933
|
break;
|
|
824
|
-
const observationGate = completionGateForTask(
|
|
934
|
+
const observationGate = completionGateForTask(working, observation.id);
|
|
825
935
|
if (!observationGate.ok)
|
|
826
936
|
break;
|
|
827
|
-
markTask(
|
|
937
|
+
markTask(working, observation.id, "done", working.kind === "pentest"
|
|
828
938
|
? "Satisfied by verified remote evidence from the preceding task."
|
|
829
939
|
: "Satisfied by the verified runtime evidence from the preceding task.");
|
|
830
940
|
reconciledTaskIds.push(observation.id);
|
|
831
941
|
}
|
|
832
|
-
if (
|
|
833
|
-
|
|
942
|
+
if (working.status === "draft" || working.status === "approved") {
|
|
943
|
+
working.status = "in_progress";
|
|
834
944
|
}
|
|
835
|
-
if (isPlanTerminal(
|
|
836
|
-
|
|
945
|
+
if (isPlanTerminal(working)) {
|
|
946
|
+
working.status = isPlanSuccessful(working) ? "completed" : "abandoned";
|
|
837
947
|
}
|
|
838
|
-
|
|
948
|
+
// Replay the same reconciliation as a reducer so a concurrent
|
|
949
|
+
// responder settlement is preserved instead of overwritten.
|
|
950
|
+
const reconciledNotes = new Map(reconciledTaskIds.map((id) => [
|
|
951
|
+
id,
|
|
952
|
+
working.tasks.find((task) => task.id === id)?.note,
|
|
953
|
+
]));
|
|
954
|
+
const reconcileResult = await mutatePlan(session.sessionId, (draft) => {
|
|
955
|
+
let changed = false;
|
|
956
|
+
for (const [id, note] of reconciledNotes) {
|
|
957
|
+
const task = draft.tasks.find((candidate) => candidate.id === id);
|
|
958
|
+
if (!task || task.state === "done")
|
|
959
|
+
continue;
|
|
960
|
+
task.state = "done";
|
|
961
|
+
if (note !== undefined)
|
|
962
|
+
task.note = note;
|
|
963
|
+
changed = true;
|
|
964
|
+
}
|
|
965
|
+
if (draft.status === "draft" || draft.status === "approved") {
|
|
966
|
+
draft.status = "in_progress";
|
|
967
|
+
changed = true;
|
|
968
|
+
}
|
|
969
|
+
if (isPlanTerminal(draft)) {
|
|
970
|
+
draft.status = isPlanSuccessful(draft) ? "completed" : "abandoned";
|
|
971
|
+
changed = true;
|
|
972
|
+
}
|
|
973
|
+
return changed;
|
|
974
|
+
}).catch(() => undefined);
|
|
975
|
+
if (reconcileResult?.ok && reconcileResult.plan)
|
|
976
|
+
plan = reconcileResult.plan;
|
|
839
977
|
writePlanUpdate(plan, renderPlanForTerminal(plan) + "\n");
|
|
840
978
|
writeNotice("info", `reconciled ${reconciledTaskIds.map((id) => `[${id}]`).join(", ")} from verified evidence`, chalk.dim(` ℹ reconciled ${reconciledTaskIds.map((id) => `[${id}]`).join(", ")} from verified evidence — no duplicate verification\n`));
|
|
841
979
|
taskWorkLedger = null;
|
|
842
980
|
return plan;
|
|
843
981
|
};
|
|
844
982
|
async function persistProjectRootOnPlan(root) {
|
|
845
|
-
const live = await loadPlan(session.sessionId).catch(() => undefined);
|
|
846
|
-
if (!live)
|
|
847
|
-
return;
|
|
848
983
|
const pm = detectPackageManager(root);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
984
|
+
// Metadata patches go through the transactional boundary so a
|
|
985
|
+
// concurrent task transition or responder settlement is not clobbered.
|
|
986
|
+
await mutatePlan(session.sessionId, (draft) => {
|
|
987
|
+
patchPlanMeta(draft, {
|
|
988
|
+
projectRoot: root,
|
|
989
|
+
...(pm ? { packageManager: pm } : {}),
|
|
990
|
+
});
|
|
991
|
+
}).catch(() => undefined);
|
|
992
|
+
}
|
|
993
|
+
/** Persist task evidence without rewriting the whole plan. */
|
|
994
|
+
async function persistTaskEvidence(taskId, evidence) {
|
|
995
|
+
await mutatePlan(session.sessionId, (draft) => {
|
|
996
|
+
const task = draft.tasks.find((candidate) => candidate.id === taskId);
|
|
997
|
+
if (!task)
|
|
998
|
+
return false;
|
|
999
|
+
task.evidence = evidence;
|
|
1000
|
+
}).catch(() => undefined);
|
|
854
1001
|
}
|
|
855
1002
|
refreshSessionState = (plan) => {
|
|
856
1003
|
if (idleOrSocialPrompt || informationalQuery)
|
|
@@ -901,6 +1048,11 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
901
1048
|
: undefined,
|
|
902
1049
|
};
|
|
903
1050
|
snap.nextHint = inferNextHint(snap);
|
|
1051
|
+
// One live plan copy, refreshed at the same protocol-safe points as
|
|
1052
|
+
// SESSION STATE so advancing tasks are never contradicted by a stale copy.
|
|
1053
|
+
if (p) {
|
|
1054
|
+
upsertPlanContextMessage(messages, planContextMessage(p, session.planApproved.value));
|
|
1055
|
+
}
|
|
904
1056
|
upsertSessionStateMessage(messages, buildSessionStateBlock(snap));
|
|
905
1057
|
};
|
|
906
1058
|
refreshSessionState(activePlan);
|
|
@@ -937,7 +1089,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
937
1089
|
kind: inferOutcomeKind({ userIntent: prompt, buildLike, pentestLike }),
|
|
938
1090
|
continueExisting: continueExistingOutcome,
|
|
939
1091
|
});
|
|
1092
|
+
loopGuard.restoreCompletedOperations(outcomeState.completedOperations ?? []);
|
|
940
1093
|
await saveOutcomeState(outcomeState);
|
|
1094
|
+
// Canonical mutation/artifact ledger feeding the durable compaction envelope.
|
|
1095
|
+
const workLedger = new WorkLedger();
|
|
941
1096
|
let governorState = createGovernorState();
|
|
942
1097
|
let governorPauseReason;
|
|
943
1098
|
let turnState = createTurnState();
|
|
@@ -992,10 +1147,54 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
992
1147
|
return release;
|
|
993
1148
|
},
|
|
994
1149
|
};
|
|
1150
|
+
/**
|
|
1151
|
+
* Apply a salvaged partial write through the NORMAL tool path so the
|
|
1152
|
+
* classifier, scope/engagement gates, confirmation prompt, and receipts
|
|
1153
|
+
* all apply exactly as they would for a model-emitted call. Salvage must
|
|
1154
|
+
* never mutate a file with `confirmed: true`, and an `fs.append` that was
|
|
1155
|
+
* cut off must stay an append (with its precondition) instead of becoming
|
|
1156
|
+
* a full overwrite.
|
|
1157
|
+
*/
|
|
1158
|
+
async function applySalvagedWrite(salvaged) {
|
|
1159
|
+
const args = {
|
|
1160
|
+
path: salvaged.path,
|
|
1161
|
+
content: salvaged.content,
|
|
1162
|
+
};
|
|
1163
|
+
if (salvaged.operation === "append") {
|
|
1164
|
+
args.position = "end";
|
|
1165
|
+
if (typeof salvaged.expectedPriorBytes === "number") {
|
|
1166
|
+
args.expectedPriorBytes = salvaged.expectedPriorBytes;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
const call = {
|
|
1170
|
+
name: salvaged.operation === "append" ? "fs.append" : "fs.write",
|
|
1171
|
+
args,
|
|
1172
|
+
};
|
|
1173
|
+
const eventId = `tool-${++nextToolEventId}`;
|
|
1174
|
+
const res = await executeSingleTool(call, eventId, options.signal || new AbortController().signal);
|
|
1175
|
+
const ok = res.ok && res.result.ok;
|
|
1176
|
+
let bytesOnDisk = Buffer.byteLength(salvaged.content, "utf8");
|
|
1177
|
+
if (ok) {
|
|
1178
|
+
try {
|
|
1179
|
+
const stats = await stat(resolveFsToolPath(salvaged.path));
|
|
1180
|
+
bytesOnDisk = stats.size;
|
|
1181
|
+
}
|
|
1182
|
+
catch {
|
|
1183
|
+
// Keep the content-length estimate when the file cannot be stat'ed.
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
return {
|
|
1187
|
+
ok,
|
|
1188
|
+
cancelled: Boolean(res.blockOrCancel),
|
|
1189
|
+
output: res.result.output,
|
|
1190
|
+
bytesOnDisk,
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
995
1193
|
async function executeSingleTool(rawCall, toolEventId, parentSignal) {
|
|
996
1194
|
const scratchDir = scratchDirFor(safeCwd());
|
|
997
1195
|
let call = normalizeToolCall(rawCall);
|
|
998
1196
|
let dispatchedTaskId;
|
|
1197
|
+
let delegation;
|
|
999
1198
|
let engagementLease;
|
|
1000
1199
|
let engagementGraph;
|
|
1001
1200
|
let engagementRecord;
|
|
@@ -1016,7 +1215,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1016
1215
|
return { ok: true, call, result, contextOutput: recoveryText };
|
|
1017
1216
|
}
|
|
1018
1217
|
if (narrowNmapOperation) {
|
|
1019
|
-
const allowed = new Set([
|
|
1218
|
+
const allowed = new Set([
|
|
1219
|
+
"net.scan",
|
|
1220
|
+
"shell.tail",
|
|
1221
|
+
"shell.jobs",
|
|
1222
|
+
"job.read",
|
|
1223
|
+
"task.read",
|
|
1224
|
+
]);
|
|
1020
1225
|
if (!allowed.has(call.name)) {
|
|
1021
1226
|
const reason = `Narrow nmap request: ${call.name} was not run because the user requested only one nmap operation. ` +
|
|
1022
1227
|
`Call net.scan with the requested target/options; do not create a plan or add DNS, WHOIS, HTTP, recon, or vulnerability steps.`;
|
|
@@ -1056,17 +1261,27 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1056
1261
|
detail: String(retryReasonRaw.detail ?? ""),
|
|
1057
1262
|
}
|
|
1058
1263
|
: undefined;
|
|
1059
|
-
|
|
1060
|
-
// Successful re-calls always run for real — no "already succeeded /
|
|
1061
|
-
// use prior results" nags (those caused model abnormalities).
|
|
1264
|
+
const currentProbeState = probeStateKey(call);
|
|
1062
1265
|
const loopCheck = loopGuard.shouldBlock(call.name, call.args, {
|
|
1063
1266
|
dependenciesChanged: retryDependenciesChanged,
|
|
1064
1267
|
environmentChanged: retryEnvironmentChanged,
|
|
1268
|
+
...(currentProbeState ? { stateKey: currentProbeState } : {}),
|
|
1065
1269
|
...(retryReason ? { retryReason } : {}),
|
|
1066
1270
|
});
|
|
1067
1271
|
if (loopCheck.block) {
|
|
1068
1272
|
const reason = loopCheck.reason ??
|
|
1069
1273
|
`${call.name} previously failed with identical arguments. Change the command/args and retry.`;
|
|
1274
|
+
if (loopCheck.kind === "unchanged-success") {
|
|
1275
|
+
const result = { ok: true, output: reason, exitCode: 0 };
|
|
1276
|
+
emitToolResult(toolEventId, result, reason);
|
|
1277
|
+
return {
|
|
1278
|
+
ok: true,
|
|
1279
|
+
call,
|
|
1280
|
+
result,
|
|
1281
|
+
contextOutput: reason,
|
|
1282
|
+
suppressedRepeat: true,
|
|
1283
|
+
};
|
|
1284
|
+
}
|
|
1070
1285
|
writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
|
|
1071
1286
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1072
1287
|
emitToolResult(toolEventId, result, reason);
|
|
@@ -1080,29 +1295,63 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1080
1295
|
if (call.name === "plan.create" ||
|
|
1081
1296
|
call.name === "task.add" ||
|
|
1082
1297
|
call.name === "task.move" ||
|
|
1298
|
+
call.name === "job.read" ||
|
|
1083
1299
|
call.name === "task.read" ||
|
|
1084
1300
|
call.name === "task.update") {
|
|
1085
|
-
if (call.name === "task.read") {
|
|
1086
|
-
const
|
|
1301
|
+
if (call.name === "job.read" || call.name === "task.read") {
|
|
1302
|
+
const requestedNotificationId = typeof call.args.notificationId === "string"
|
|
1087
1303
|
? call.args.notificationId.trim()
|
|
1088
1304
|
: "";
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1305
|
+
const requestedJobId = typeof call.args.jobId === "string" ? call.args.jobId.trim() : "";
|
|
1306
|
+
const pending = jobManager.getPendingNotifications(session.sessionId);
|
|
1307
|
+
const eligible = responderWakeTurn
|
|
1308
|
+
? pending.filter(matchesWakeRevision)
|
|
1309
|
+
: pending;
|
|
1310
|
+
const byNotification = requestedNotificationId
|
|
1311
|
+
? eligible.find((candidate) => candidate.id === requestedNotificationId)
|
|
1312
|
+
: undefined;
|
|
1313
|
+
const byJob = requestedJobId
|
|
1314
|
+
? eligible.find((candidate) => candidate.jobId === requestedJobId)
|
|
1315
|
+
: undefined;
|
|
1316
|
+
const identifiersConflict = Boolean((byNotification && requestedJobId && byNotification.jobId !== requestedJobId) ||
|
|
1317
|
+
(byJob && requestedNotificationId && byJob.id !== requestedNotificationId));
|
|
1318
|
+
const notification = identifiersConflict
|
|
1319
|
+
? undefined
|
|
1320
|
+
: (byNotification ?? byJob);
|
|
1321
|
+
const visible = Boolean(notification && unreadResponderNotificationIds.has(notification.id));
|
|
1322
|
+
const wakeIdentityMatches = Boolean(responderWakeTurn &&
|
|
1323
|
+
(requestedNotificationId || requestedJobId) &&
|
|
1324
|
+
(!requestedNotificationId ||
|
|
1325
|
+
requestedNotificationId === responderWakeNotificationId) &&
|
|
1326
|
+
(!requestedJobId || requestedJobId === responderWakeJobId));
|
|
1327
|
+
const staleWakeSettled = wakeIdentityMatches && !identifiersConflict && !notification;
|
|
1328
|
+
const persistedRead = Boolean(notification &&
|
|
1094
1329
|
visible &&
|
|
1095
|
-
jobManager.markRead(
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1330
|
+
jobManager.markRead(notification.id, session.sessionId));
|
|
1331
|
+
const marked = persistedRead || staleWakeSettled;
|
|
1332
|
+
const identifier = requestedJobId || requestedNotificationId;
|
|
1333
|
+
const revisionLabel = responderWakeResultRevision
|
|
1334
|
+
? ` revision ${responderWakeResultRevision}`
|
|
1335
|
+
: "";
|
|
1336
|
+
const output = persistedRead
|
|
1337
|
+
? `Responder job ${notification.jobId} (${notification.id}) marked delivered and read after model analysis.`
|
|
1338
|
+
: staleWakeSettled
|
|
1339
|
+
? `Responder result ${identifier}${revisionLabel} was already settled or discarded; the stale wake is acknowledged idempotently.`
|
|
1340
|
+
: !requestedNotificationId && !requestedJobId
|
|
1341
|
+
? `${call.name} failed: jobId or notificationId is required.`
|
|
1342
|
+
: identifiersConflict
|
|
1343
|
+
? `${call.name} failed: jobId and notificationId refer to different Responder results.`
|
|
1344
|
+
: !notification
|
|
1345
|
+
? `${call.name} failed: Responder result ${identifier} is unavailable, consumed, or archived.`
|
|
1346
|
+
: !visible
|
|
1347
|
+
? `${call.name} failed: Responder result ${identifier} was not delivered to this model turn. Analyze a delivered result before marking it read.`
|
|
1348
|
+
: `${call.name} failed: read state for Responder result ${identifier} could not be persisted.`;
|
|
1349
|
+
if (persistedRead && notification) {
|
|
1104
1350
|
upsertResponderResultLedger(messages, notification);
|
|
1105
|
-
unreadResponderNotificationIds.delete(
|
|
1351
|
+
unreadResponderNotificationIds.delete(notification.id);
|
|
1352
|
+
}
|
|
1353
|
+
else if (staleWakeSettled && responderWakeNotificationId) {
|
|
1354
|
+
unreadResponderNotificationIds.delete(responderWakeNotificationId);
|
|
1106
1355
|
}
|
|
1107
1356
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1108
1357
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
@@ -1219,7 +1468,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1219
1468
|
taskWorkLedger = led;
|
|
1220
1469
|
if (planResult.plan && led && led.successWorkCount > 0 && persisted) {
|
|
1221
1470
|
persisted.evidence = taskEvidenceFromLedger(led);
|
|
1222
|
-
await
|
|
1471
|
+
await persistTaskEvidence(persisted.id, persisted.evidence);
|
|
1223
1472
|
}
|
|
1224
1473
|
}
|
|
1225
1474
|
else if (stateRaw === "done" && resolved) {
|
|
@@ -1228,7 +1477,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1228
1477
|
const t = planResult.plan.tasks.find((x) => x.id === resolved);
|
|
1229
1478
|
if (t) {
|
|
1230
1479
|
t.evidence = taskEvidenceFromLedger(taskWorkLedger);
|
|
1231
|
-
await
|
|
1480
|
+
await persistTaskEvidence(t.id, t.evidence);
|
|
1232
1481
|
}
|
|
1233
1482
|
}
|
|
1234
1483
|
taskWorkLedger = null;
|
|
@@ -1357,7 +1606,18 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1357
1606
|
livePlanForGate.status === "approved") {
|
|
1358
1607
|
livePlanForGate.status = "in_progress";
|
|
1359
1608
|
}
|
|
1360
|
-
|
|
1609
|
+
// Opening a task is a transition applied by
|
|
1610
|
+
// the reducer, which also enforces the single-active invariant.
|
|
1611
|
+
await mutatePlan(session.sessionId, (draft) => {
|
|
1612
|
+
const target = draft.tasks.find((candidate) => candidate.id === nextPending.id);
|
|
1613
|
+
if (!target || target.state === "in_progress")
|
|
1614
|
+
return false;
|
|
1615
|
+
target.state = "in_progress";
|
|
1616
|
+
if (draft.status === "draft" || draft.status === "approved") {
|
|
1617
|
+
draft.status = "in_progress";
|
|
1618
|
+
}
|
|
1619
|
+
return true;
|
|
1620
|
+
}).catch(() => undefined);
|
|
1361
1621
|
// Preserve evidence already credited to this task (e.g. pentest
|
|
1362
1622
|
// recon that ran before the task was formally opened).
|
|
1363
1623
|
if (!taskWorkLedger ||
|
|
@@ -1415,22 +1675,29 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1415
1675
|
alreadyPrintedIds.add(toolEventId);
|
|
1416
1676
|
}
|
|
1417
1677
|
const scopeTarget = safeScopeTargetForToolCall(call);
|
|
1418
|
-
const
|
|
1419
|
-
?
|
|
1420
|
-
:
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1678
|
+
const engagementActions = pentestSession || isPentestToolCall(call) || Boolean(scope)
|
|
1679
|
+
? safeEngagementActionsForToolCall(call)
|
|
1680
|
+
: [];
|
|
1681
|
+
// The primary action carries the URL/port/path detail used for leases and
|
|
1682
|
+
// network-hop authorization; every action (one per named target) must pass
|
|
1683
|
+
// the scope check below before the tool runs.
|
|
1684
|
+
const engagementAction = engagementActions[0];
|
|
1685
|
+
let engagementDecision;
|
|
1686
|
+
for (const action of engagementActions) {
|
|
1687
|
+
const decisionForAction = evaluateEngagementAction(scope, action);
|
|
1688
|
+
if (!decisionForAction)
|
|
1689
|
+
continue;
|
|
1690
|
+
if (action === engagementAction)
|
|
1691
|
+
engagementDecision = decisionForAction;
|
|
1425
1692
|
if (scope) {
|
|
1426
1693
|
engagementGraph = await openEngagement(scope);
|
|
1427
1694
|
engagementRecord = beginEngagementAction(engagementGraph, {
|
|
1428
1695
|
tool: call.name,
|
|
1429
|
-
target:
|
|
1430
|
-
phase:
|
|
1431
|
-
capability:
|
|
1432
|
-
authorized:
|
|
1433
|
-
reason:
|
|
1696
|
+
target: decisionForAction.normalizedTarget || action.target,
|
|
1697
|
+
phase: decisionForAction.phase,
|
|
1698
|
+
capability: decisionForAction.capability,
|
|
1699
|
+
authorized: decisionForAction.allowed,
|
|
1700
|
+
reason: decisionForAction.reason,
|
|
1434
1701
|
});
|
|
1435
1702
|
await saveEngagement(engagementGraph);
|
|
1436
1703
|
}
|
|
@@ -1438,16 +1705,22 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1438
1705
|
...(engagementGraph ? { engagementId: engagementGraph.id } : {}),
|
|
1439
1706
|
...(engagementRecord ? { actionId: engagementRecord.id } : {}),
|
|
1440
1707
|
tool: call.name,
|
|
1441
|
-
target:
|
|
1442
|
-
phase:
|
|
1443
|
-
capability:
|
|
1444
|
-
allowed:
|
|
1445
|
-
reason:
|
|
1708
|
+
target: decisionForAction.normalizedTarget,
|
|
1709
|
+
phase: decisionForAction.phase,
|
|
1710
|
+
capability: decisionForAction.capability,
|
|
1711
|
+
allowed: decisionForAction.allowed,
|
|
1712
|
+
reason: decisionForAction.reason,
|
|
1446
1713
|
});
|
|
1447
|
-
if (!
|
|
1448
|
-
const target =
|
|
1449
|
-
|
|
1450
|
-
|
|
1714
|
+
if (!decisionForAction.allowed) {
|
|
1715
|
+
const target = decisionForAction.normalizedTarget ||
|
|
1716
|
+
action.target ||
|
|
1717
|
+
scopeTarget ||
|
|
1718
|
+
"requested target";
|
|
1719
|
+
const reason = outOfScopeToolMessage({
|
|
1720
|
+
target,
|
|
1721
|
+
reason: decisionForAction.reason,
|
|
1722
|
+
allowed: scope?.authorizedTargets,
|
|
1723
|
+
});
|
|
1451
1724
|
writeToolBlocked(toolEventId, call.name, reason, chalk.red(` ✗ ${reason}\n`));
|
|
1452
1725
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1453
1726
|
emitToolResult(toolEventId, result, reason);
|
|
@@ -1554,6 +1827,53 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1554
1827
|
const candidate = pickPendingTaskForToolCall(readyPlanTasks(planAtDispatch), call, planAtDispatch.tasks.map((task) => task.title));
|
|
1555
1828
|
dispatchedTaskId = candidate?.id;
|
|
1556
1829
|
}
|
|
1830
|
+
// An explicitly declared responder parent wins over inference.
|
|
1831
|
+
const declaredParent = readDeclaredParentTaskId(call);
|
|
1832
|
+
if (declaredParent) {
|
|
1833
|
+
const resolvedParent = resolveResponderParent({
|
|
1834
|
+
plan: planAtDispatch,
|
|
1835
|
+
declared: declaredParent,
|
|
1836
|
+
activeForegroundTaskIds: dispatchedTaskId ? [dispatchedTaskId] : [],
|
|
1837
|
+
});
|
|
1838
|
+
if (!resolvedParent.ok) {
|
|
1839
|
+
const reason = `${call.name} failed: ${resolvedParent.reason}`;
|
|
1840
|
+
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1841
|
+
emitToolResult(toolEventId, result, reason);
|
|
1842
|
+
return { ok: false, call, result, contextOutput: reason };
|
|
1843
|
+
}
|
|
1844
|
+
dispatchedTaskId = resolvedParent.taskId ?? dispatchedTaskId;
|
|
1845
|
+
}
|
|
1846
|
+
// For an explicit delegation, create the child subtask before the
|
|
1847
|
+
// Process starts and launch the job already bound to it. The job therefore
|
|
1848
|
+
// Never carries the foreground parent in `taskId`, and settlement has a
|
|
1849
|
+
// Durable row to advance even if this turn dies right after spawn.
|
|
1850
|
+
if (isExplicitResponderDelegation(call) && planAtDispatch) {
|
|
1851
|
+
delegation = { id: `dg-${randomUUID().slice(0, 8)}` };
|
|
1852
|
+
const created = await mutatePlan(session.sessionId, (draft) => {
|
|
1853
|
+
const parentExists = !!dispatchedTaskId &&
|
|
1854
|
+
draft.tasks.some((task) => task.id === dispatchedTaskId);
|
|
1855
|
+
const child = appendPlanTask(draft, {
|
|
1856
|
+
title: delegationTaskTitle(call),
|
|
1857
|
+
state: "in_progress",
|
|
1858
|
+
note: `delegation=${delegation.id} awaiting launch`,
|
|
1859
|
+
dependencies: [],
|
|
1860
|
+
resourceLocks: [],
|
|
1861
|
+
...(parentExists ? { parentTaskId: dispatchedTaskId } : {}),
|
|
1862
|
+
responderOwned: true,
|
|
1863
|
+
delegationId: delegation.id,
|
|
1864
|
+
});
|
|
1865
|
+
delegation.taskId = child.id;
|
|
1866
|
+
return true;
|
|
1867
|
+
}).catch(() => undefined);
|
|
1868
|
+
if (!created?.ok || !delegation.taskId) {
|
|
1869
|
+
delegation = undefined;
|
|
1870
|
+
writeNotice("warn", "Responder delegation record could not be persisted — the job will be linked after launch", chalk.yellow(" ⚠ responder delegation record not persisted; linking after launch\n"));
|
|
1871
|
+
}
|
|
1872
|
+
else if (created.plan) {
|
|
1873
|
+
pendingSessionStatePlan = created.plan;
|
|
1874
|
+
writePlanUpdate(created.plan, renderPlanForTerminal(created.plan) + "\n");
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1557
1877
|
if (dispatchedTaskId &&
|
|
1558
1878
|
(!taskWorkLedger || taskWorkLedger.taskId !== dispatchedTaskId)) {
|
|
1559
1879
|
const dispatchedTask = planAtDispatch?.tasks.find((task) => task.id === dispatchedTaskId);
|
|
@@ -1562,7 +1882,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1562
1882
|
if (engagementAction) {
|
|
1563
1883
|
engagementLease = engagementPolicy.acquire(scope, engagementAction);
|
|
1564
1884
|
if (!engagementLease.decision.allowed) {
|
|
1565
|
-
const
|
|
1885
|
+
const target = engagementLease.decision.normalizedTarget || engagementAction.target;
|
|
1886
|
+
const reason = outOfScopeToolMessage({
|
|
1887
|
+
target,
|
|
1888
|
+
reason: engagementLease.decision.reason,
|
|
1889
|
+
allowed: scope?.authorizedTargets,
|
|
1890
|
+
});
|
|
1566
1891
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1567
1892
|
emitToolResult(toolEventId, result, reason);
|
|
1568
1893
|
return { ok: false, call, result, contextOutput: reason };
|
|
@@ -1664,7 +1989,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1664
1989
|
confirmed: true,
|
|
1665
1990
|
userPrompt: prompt,
|
|
1666
1991
|
sessionId: session.sessionId,
|
|
1667
|
-
...(
|
|
1992
|
+
...(delegation?.taskId ? { taskId: delegation.taskId } : {}),
|
|
1993
|
+
...(delegation ? { delegationId: delegation.id } : {}),
|
|
1994
|
+
...(dispatchedTaskId ? { parentTaskId: dispatchedTaskId } : {}),
|
|
1668
1995
|
wakeOnCompletion: true,
|
|
1669
1996
|
monitor: {
|
|
1670
1997
|
toolName: call.name,
|
|
@@ -1775,13 +2102,18 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1775
2102
|
// User Esc/Ctrl+C: force-settle may resolve with a cancel result
|
|
1776
2103
|
// instead of throwing — still end the turn as aborted.
|
|
1777
2104
|
if (parentSignal.aborted && !stalledByWatchdog && !hardTimedOut) {
|
|
1778
|
-
|
|
2105
|
+
const result = {
|
|
2106
|
+
ok: false,
|
|
2107
|
+
output: "Cancelled by user.",
|
|
2108
|
+
exitCode: 130,
|
|
2109
|
+
};
|
|
2110
|
+
emitToolResult(toolEventId, result, result.output);
|
|
1779
2111
|
return {
|
|
1780
2112
|
ok: false,
|
|
1781
2113
|
call,
|
|
1782
|
-
result
|
|
1783
|
-
contextOutput:
|
|
1784
|
-
|
|
2114
|
+
result,
|
|
2115
|
+
contextOutput: result.output,
|
|
2116
|
+
aborted: true,
|
|
1785
2117
|
};
|
|
1786
2118
|
}
|
|
1787
2119
|
if (liveBytes > 0) {
|
|
@@ -1793,13 +2125,18 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1793
2125
|
jobManager.updateJobStatus(jobId, "failed", 1);
|
|
1794
2126
|
if (isAbortError(toolError, toolAc.signal) || forceSettled) {
|
|
1795
2127
|
if (parentSignal.aborted && !stalledByWatchdog && !hardTimedOut) {
|
|
1796
|
-
|
|
2128
|
+
const result = {
|
|
2129
|
+
ok: false,
|
|
2130
|
+
output: "Cancelled by user.",
|
|
2131
|
+
exitCode: 130,
|
|
2132
|
+
};
|
|
2133
|
+
emitToolResult(toolEventId, result, result.output);
|
|
1797
2134
|
return {
|
|
1798
2135
|
ok: false,
|
|
1799
2136
|
call,
|
|
1800
|
-
result
|
|
1801
|
-
contextOutput:
|
|
1802
|
-
|
|
2137
|
+
result,
|
|
2138
|
+
contextOutput: result.output,
|
|
2139
|
+
aborted: true,
|
|
1803
2140
|
};
|
|
1804
2141
|
}
|
|
1805
2142
|
result = {
|
|
@@ -1903,6 +2240,27 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1903
2240
|
writeNotice("info", `project root → ${fromScaffold}`, chalk.dim(` ℹ project root set to ${fromScaffold}\n`));
|
|
1904
2241
|
}
|
|
1905
2242
|
}
|
|
2243
|
+
if (delegation?.taskId && !result.backgroundJob) {
|
|
2244
|
+
// The delegation never became a durable job: settle its child instead of
|
|
2245
|
+
// Leaving a permanently yellow subtask behind.
|
|
2246
|
+
const delegationId = delegation.id;
|
|
2247
|
+
const settledState = result.ok ? "skipped" : "failed";
|
|
2248
|
+
const settlement = await mutatePlan(session.sessionId, (draft) => {
|
|
2249
|
+
const child = draft.tasks.find((task) => task.delegationId === delegationId);
|
|
2250
|
+
if (!child)
|
|
2251
|
+
return false;
|
|
2252
|
+
child.state = settledState;
|
|
2253
|
+
child.note = result.ok
|
|
2254
|
+
? `delegation=${delegationId} ran in the foreground; no durable job was created`
|
|
2255
|
+
: `delegation=${delegationId} failed to launch`;
|
|
2256
|
+
return true;
|
|
2257
|
+
}).catch(() => undefined);
|
|
2258
|
+
if (settlement?.ok && settlement.plan) {
|
|
2259
|
+
pendingSessionStatePlan = settlement.plan;
|
|
2260
|
+
writePlanUpdate(settlement.plan, renderPlanForTerminal(settlement.plan) + "\n");
|
|
2261
|
+
}
|
|
2262
|
+
delegation = undefined;
|
|
2263
|
+
}
|
|
1906
2264
|
if (result.backgroundJob) {
|
|
1907
2265
|
const durableJob = jobManager.getJob(result.backgroundJob.id);
|
|
1908
2266
|
// Responder linkage is opt-in: only jobs launched with responder:true
|
|
@@ -1910,9 +2268,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1910
2268
|
// Plain background jobs stay pollable (shell.jobs/shell.tail) as before.
|
|
1911
2269
|
if (durableJob?.responder) {
|
|
1912
2270
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1913
|
-
let linkedTaskId =
|
|
2271
|
+
let linkedTaskId = delegation?.taskId;
|
|
1914
2272
|
let linkedParentTaskId;
|
|
1915
2273
|
let responderTaskId;
|
|
2274
|
+
let responderChildId;
|
|
1916
2275
|
if (durableJob && livePlan) {
|
|
1917
2276
|
const existing = livePlan.tasks.find((task) => task.jobId === durableJob.id);
|
|
1918
2277
|
const parentTaskId = dispatchedTaskId && livePlan.tasks.some((task) => task.id === dispatchedTaskId)
|
|
@@ -1927,46 +2286,64 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1927
2286
|
: "in_progress";
|
|
1928
2287
|
const note = `job=${durableJob.id} pid=${durableJob.pid ?? "?"} status=${durableJob.status} ` +
|
|
1929
2288
|
`artifact=${durableJob.stdoutArtifact}`;
|
|
1930
|
-
const
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
2289
|
+
const responderTitle = `Responder · ${durableJob.name ?? durableJob.commandDisplay.slice(0, 96)}`;
|
|
2290
|
+
// Upsert the child by delegation/job identity
|
|
2291
|
+
// inside the transactional boundary. A concurrent settlement that
|
|
2292
|
+
// already turned the child green is therefore never reverted, and the
|
|
2293
|
+
// child is never written as the foreground parent.
|
|
2294
|
+
const upsert = await mutatePlan(session.sessionId, (draft) => {
|
|
2295
|
+
const target = (durableJob.delegationId
|
|
2296
|
+
? draft.tasks.find((task) => task.delegationId === durableJob.delegationId)
|
|
2297
|
+
: undefined) ??
|
|
2298
|
+
draft.tasks.find((task) => task.jobId === durableJob.id);
|
|
2299
|
+
const child = target ??
|
|
2300
|
+
appendPlanTask(draft, {
|
|
2301
|
+
title: responderTitle,
|
|
2302
|
+
state: terminalState,
|
|
2303
|
+
note,
|
|
2304
|
+
dependencies: [],
|
|
2305
|
+
resourceLocks: [],
|
|
2306
|
+
parentTaskId,
|
|
2307
|
+
jobId: durableJob.id,
|
|
2308
|
+
processId: durableJob.pid,
|
|
2309
|
+
responderOwned: true,
|
|
2310
|
+
...(durableJob.delegationId
|
|
2311
|
+
? { delegationId: durableJob.delegationId }
|
|
2312
|
+
: {}),
|
|
2313
|
+
});
|
|
2314
|
+
// Never regress a child that process settlement already finished.
|
|
2315
|
+
const settledTerminal = child.state === "done" || child.state === "failed";
|
|
2316
|
+
if (!settledTerminal) {
|
|
2317
|
+
child.state = terminalState;
|
|
2318
|
+
child.note = note;
|
|
2319
|
+
}
|
|
2320
|
+
child.jobId = durableJob.id;
|
|
2321
|
+
child.processId = durableJob.pid;
|
|
2322
|
+
child.responderOwned = true;
|
|
2323
|
+
if (durableJob.delegationId) {
|
|
2324
|
+
child.delegationId = durableJob.delegationId;
|
|
2325
|
+
}
|
|
2326
|
+
if (parentTaskId)
|
|
2327
|
+
child.parentTaskId = parentTaskId;
|
|
2328
|
+
if (isPlanTerminal(draft)) {
|
|
2329
|
+
draft.status = isPlanSuccessful(draft) ? "completed" : "abandoned";
|
|
2330
|
+
}
|
|
2331
|
+
else if (draft.status !== "draft") {
|
|
2332
|
+
draft.status = "in_progress";
|
|
2333
|
+
}
|
|
2334
|
+
responderChildId = child.id;
|
|
2335
|
+
return true;
|
|
2336
|
+
}).catch(() => undefined);
|
|
2337
|
+
if (!upsert?.ok || !responderChildId) {
|
|
1962
2338
|
writeNotice("warn", `Responder job ${durableJob.id} started, but its plan subtask could not be persisted`, chalk.yellow(` ⚠ job ${durableJob.id} is running, but task linkage persistence failed\n`));
|
|
1963
2339
|
}
|
|
1964
|
-
|
|
1965
|
-
linkedTaskId =
|
|
2340
|
+
else {
|
|
2341
|
+
linkedTaskId = responderChildId;
|
|
1966
2342
|
linkedParentTaskId = parentTaskId;
|
|
1967
|
-
responderTaskId =
|
|
1968
|
-
pendingSessionStatePlan = livePlan;
|
|
1969
|
-
|
|
2343
|
+
responderTaskId = responderChildId;
|
|
2344
|
+
pendingSessionStatePlan = upsert.plan ?? livePlan;
|
|
2345
|
+
const rendered = upsert.plan ?? livePlan;
|
|
2346
|
+
writePlanUpdate(rendered, renderPlanForTerminal(rendered) + "\n");
|
|
1970
2347
|
}
|
|
1971
2348
|
}
|
|
1972
2349
|
if (durableJob) {
|
|
@@ -2051,13 +2428,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2051
2428
|
}
|
|
2052
2429
|
await saveEngagement(engagementGraph);
|
|
2053
2430
|
}
|
|
2431
|
+
workLedger.recordToolCall(call, result.ok, savedOutputPath);
|
|
2432
|
+
const completedProbeState = probeStateKey(call);
|
|
2054
2433
|
const newEvidence = recordToolEvidence(outcomeState, {
|
|
2055
2434
|
tool: call.name,
|
|
2056
2435
|
callId: toolEventId,
|
|
2057
2436
|
ok: result.ok,
|
|
2437
|
+
...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
|
|
2058
2438
|
output: result.output,
|
|
2059
2439
|
...(savedOutputPath ? { artifact: savedOutputPath } : {}),
|
|
2060
2440
|
...(dispatchedTaskId ? { taskId: dispatchedTaskId } : {}),
|
|
2441
|
+
...(completedProbeState ? { stateKey: completedProbeState } : {}),
|
|
2061
2442
|
args: call.args,
|
|
2062
2443
|
});
|
|
2063
2444
|
let hypothesisDelta = 0;
|
|
@@ -2130,7 +2511,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2130
2511
|
}
|
|
2131
2512
|
}
|
|
2132
2513
|
await saveOutcomeState(outcomeState);
|
|
2133
|
-
loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode, result.output);
|
|
2514
|
+
loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode, result.output, completedProbeState ? { stateKey: completedProbeState } : undefined);
|
|
2134
2515
|
// Evidence for verify-before-done: only successful real work counts.
|
|
2135
2516
|
if (result.ok && isEvidenceWorkTool(call.name)) {
|
|
2136
2517
|
const liveAfter = await loadPlan(session.sessionId).catch(() => undefined);
|
|
@@ -2202,7 +2583,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2202
2583
|
taskWorkLedger.successWorkCount < absorbed.successWorkCount) {
|
|
2203
2584
|
taskWorkLedger = absorbed;
|
|
2204
2585
|
}
|
|
2205
|
-
await
|
|
2586
|
+
await persistTaskEvidence(task.id, task.evidence);
|
|
2206
2587
|
}
|
|
2207
2588
|
}
|
|
2208
2589
|
}
|
|
@@ -2211,7 +2592,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2211
2592
|
const task = liveAfter.tasks.find((candidate) => candidate.id === creditId);
|
|
2212
2593
|
if (task) {
|
|
2213
2594
|
task.evidence = taskEvidenceFromLedger(taskWorkLedger);
|
|
2214
|
-
await
|
|
2595
|
+
await persistTaskEvidence(task.id, task.evidence);
|
|
2215
2596
|
}
|
|
2216
2597
|
}
|
|
2217
2598
|
// Do NOT refreshSessionState here. executeSingleTool often finishes
|
|
@@ -2279,6 +2660,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2279
2660
|
// Align with /compact default: small recency + dense memory (not keepRecent=6 fat tails).
|
|
2280
2661
|
const AUTO_COMPACT_KEEP_RECENT = 2;
|
|
2281
2662
|
let lastCompactionMsgCount = 0;
|
|
2663
|
+
const compactionAttempts = new CompactionAttemptLedger();
|
|
2664
|
+
let activeCompactionId;
|
|
2282
2665
|
/** E5: identical tool bodies within this turn → pointer instead of re-append. */
|
|
2283
2666
|
const toolResultHashes = new Map();
|
|
2284
2667
|
/** E4: consecutive free-tier stream failures this turn. */
|
|
@@ -2288,8 +2671,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2288
2671
|
// once per turn — the recovery planner already narrates each retry, so
|
|
2289
2672
|
// repeating this on every failure just adds noise.
|
|
2290
2673
|
let freeTierAdvisoryShown = false;
|
|
2291
|
-
const summarizeForCompaction = async (summaryPrompt) => {
|
|
2292
|
-
const
|
|
2674
|
+
const summarizeForCompaction = async (summaryPrompt, stage) => {
|
|
2675
|
+
const streamFinalSummary = stage?.phase !== "map";
|
|
2676
|
+
const compactionId = streamFinalSummary ? activeCompactionId : undefined;
|
|
2677
|
+
const deltaParser = compactionId
|
|
2678
|
+
? createThinkingStreamParser((text) => writeCompactionDelta(compactionId, text), undefined, { remember: false })
|
|
2679
|
+
: undefined;
|
|
2680
|
+
const request = {
|
|
2293
2681
|
provider,
|
|
2294
2682
|
model,
|
|
2295
2683
|
messages: [
|
|
@@ -2297,11 +2685,32 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2297
2685
|
{ role: "user", content: summaryPrompt },
|
|
2298
2686
|
],
|
|
2299
2687
|
temperature: 0.1,
|
|
2300
|
-
// Prefer dense memory; enough room for findings without forcing a tiny stub.
|
|
2301
2688
|
maxTokens: 2_048,
|
|
2302
2689
|
signal: options.signal,
|
|
2690
|
+
};
|
|
2691
|
+
const response = await streamWithProvider(request, (token) => deltaParser?.push(token), { onStatus: () => undefined });
|
|
2692
|
+
deltaParser?.finish();
|
|
2693
|
+
const parsed = stripThinking(response.text);
|
|
2694
|
+
if (parsed.visible.trim() || !parsed.hasThinking)
|
|
2695
|
+
return response.text;
|
|
2696
|
+
const retry = await completeWithProvider({
|
|
2697
|
+
...request,
|
|
2698
|
+
messages: [
|
|
2699
|
+
{
|
|
2700
|
+
role: "system",
|
|
2701
|
+
content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only the continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
|
|
2702
|
+
},
|
|
2703
|
+
{ role: "user", content: summaryPrompt },
|
|
2704
|
+
],
|
|
2705
|
+
temperature: 0,
|
|
2706
|
+
maxTokens: 8_192,
|
|
2707
|
+
thinking: { enabled: false, effort: "none" },
|
|
2303
2708
|
});
|
|
2304
|
-
|
|
2709
|
+
const retryVisible = stripThinking(retry.text).visible.trim();
|
|
2710
|
+
if (retryVisible && compactionId) {
|
|
2711
|
+
writeCompactionDelta(compactionId, retryVisible);
|
|
2712
|
+
}
|
|
2713
|
+
return retry.text;
|
|
2305
2714
|
};
|
|
2306
2715
|
/**
|
|
2307
2716
|
* Estimate the complete next model request, including attached native-tool
|
|
@@ -2314,20 +2723,85 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2314
2723
|
const nextTools = selectToolDefs(native, useCompactSystemPrompt);
|
|
2315
2724
|
return buildContextBreakdown(contextMessages, nextTools).estimatedTotalTokens;
|
|
2316
2725
|
};
|
|
2726
|
+
/**
|
|
2727
|
+
* Canonical state that must survive compaction verbatim. Built from the
|
|
2728
|
+
* plan store, outcome contract, responder ledger and mutation ledger — never
|
|
2729
|
+
* from the narrative summary.
|
|
2730
|
+
*/
|
|
2731
|
+
async function buildTurnDurableEnvelope() {
|
|
2732
|
+
const plan = (await loadPlan(session.sessionId).catch(() => undefined)) ?? undefined;
|
|
2733
|
+
const root = getActiveProjectRoot() ?? plan?.meta?.projectRoot;
|
|
2734
|
+
const consumed = [];
|
|
2735
|
+
for (const message of messages) {
|
|
2736
|
+
if (!isResponderResultLedgerMessage(message))
|
|
2737
|
+
continue;
|
|
2738
|
+
for (const line of message.content.split("\n")) {
|
|
2739
|
+
const match = /notification=(\S+)/.exec(line);
|
|
2740
|
+
if (match?.[1])
|
|
2741
|
+
consumed.push(match[1]);
|
|
2742
|
+
}
|
|
2743
|
+
}
|
|
2744
|
+
const unread = jobManager
|
|
2745
|
+
.getPendingNotifications(session.sessionId)
|
|
2746
|
+
.map((notification) => notification.id);
|
|
2747
|
+
const toEnvelopeJob = (job) => ({
|
|
2748
|
+
id: job.id,
|
|
2749
|
+
status: job.status,
|
|
2750
|
+
command: job.commandDisplay || job.command,
|
|
2751
|
+
...(job.taskId ? { taskId: job.taskId } : {}),
|
|
2752
|
+
...(job.stdoutArtifact ? { artifact: job.stdoutArtifact } : {}),
|
|
2753
|
+
});
|
|
2754
|
+
const liveJobs = jobManager
|
|
2755
|
+
.getRunningJobs(session.sessionId)
|
|
2756
|
+
.map(toEnvelopeJob);
|
|
2757
|
+
const liveIds = new Set(liveJobs.map((job) => job.id));
|
|
2758
|
+
const finishedJobs = jobManager
|
|
2759
|
+
.getRecentJobs(12, session.sessionId)
|
|
2760
|
+
.filter((job) => !liveIds.has(job.id))
|
|
2761
|
+
.map(toEnvelopeJob);
|
|
2762
|
+
return buildDurableEnvelope({
|
|
2763
|
+
...(plan ? { plan } : {}),
|
|
2764
|
+
outcome: outcomeState,
|
|
2765
|
+
ledger: workLedger,
|
|
2766
|
+
...(root ? { projectRoot: root } : {}),
|
|
2767
|
+
...(root
|
|
2768
|
+
? { packageManager: plan?.meta?.packageManager ?? detectPackageManager(root) }
|
|
2769
|
+
: {}),
|
|
2770
|
+
responder: { unread, consumed: [...new Set(consumed)] },
|
|
2771
|
+
...(liveJobs.length > 0 ? { liveJobs } : {}),
|
|
2772
|
+
...(finishedJobs.length > 0 ? { finishedJobs } : {}),
|
|
2773
|
+
});
|
|
2774
|
+
}
|
|
2317
2775
|
async function maybeAutoCompact(reason, force = false) {
|
|
2318
2776
|
const beforeTokens = estimateNextRequestTokens(messages);
|
|
2319
|
-
|
|
2320
|
-
const compactTrigger =
|
|
2777
|
+
const budget = resolveRequestBudget({ provider, model });
|
|
2778
|
+
const compactTrigger = budget.effectiveTrigger;
|
|
2321
2779
|
if (!force && beforeTokens < compactTrigger)
|
|
2322
2780
|
return;
|
|
2781
|
+
// Structural eligibility only: there must be closed history to summarize.
|
|
2323
2782
|
if (messages.length <= AUTO_COMPACT_KEEP_RECENT + 2)
|
|
2324
2783
|
return;
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2784
|
+
const durableEnvelope = await buildTurnDurableEnvelope();
|
|
2785
|
+
const attemptKey = compactionAttemptKey({
|
|
2786
|
+
messages,
|
|
2787
|
+
provider,
|
|
2788
|
+
model,
|
|
2789
|
+
dialect: toolDialect,
|
|
2790
|
+
triggerTokens: compactTrigger,
|
|
2791
|
+
schemaHash: toolSchemaHash(selectToolDefs(nativeToolsActive, useCompactSystemPrompt)),
|
|
2792
|
+
...(durableEnvelope ? { durableEnvelope } : {}),
|
|
2793
|
+
});
|
|
2794
|
+
if (!force && compactionAttempts.isSuppressed(attemptKey))
|
|
2328
2795
|
return;
|
|
2796
|
+
const compactionId = `compact-${randomUUID().slice(0, 12)}`;
|
|
2797
|
+
activeCompactionId = compactionId;
|
|
2798
|
+
writeCompactionStarted(compactionId, beforeTokens);
|
|
2329
2799
|
try {
|
|
2330
|
-
const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
|
|
2800
|
+
const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
|
|
2801
|
+
budgetTokens: 0,
|
|
2802
|
+
keepRecent: AUTO_COMPACT_KEEP_RECENT,
|
|
2803
|
+
...(durableEnvelope ? { durableEnvelope } : {}),
|
|
2804
|
+
});
|
|
2331
2805
|
const summaryBody = result.messages.find((m) => isCompactionMemoryMessage(m))?.content ??
|
|
2332
2806
|
"";
|
|
2333
2807
|
if (!shouldApplyAutoCompact({
|
|
@@ -2337,9 +2811,27 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2337
2811
|
afterTokens: result.afterTokens,
|
|
2338
2812
|
afterMessages: result.messages,
|
|
2339
2813
|
})) {
|
|
2814
|
+
writeCompactionFailed(compactionId, "The generated summary was not accepted; the original context was retained.", beforeTokens);
|
|
2815
|
+
return;
|
|
2816
|
+
}
|
|
2817
|
+
// otherwise the oversized request is sent anyway and corrective
|
|
2818
|
+
// compaction is suppressed as "already compacted".
|
|
2819
|
+
const candidateTokens = estimateNextRequestTokens(result.messages);
|
|
2820
|
+
if (!force && candidateTokens >= compactTrigger) {
|
|
2821
|
+
const dominant = describeDominantContextBlock(result.messages);
|
|
2822
|
+
compactionAttempts.recordFailure(attemptKey);
|
|
2823
|
+
await auditLog("agent.compact.overflow", {
|
|
2824
|
+
reason,
|
|
2825
|
+
candidateTokens,
|
|
2826
|
+
trigger: compactTrigger,
|
|
2827
|
+
dominant,
|
|
2828
|
+
});
|
|
2829
|
+
writeNotice("warn", `context is still ~${candidateTokens.toLocaleString()} tokens after compaction (limit ~${compactTrigger.toLocaleString()}) — largest block: ${dominant}`, chalk.yellow(` ⚠ context still ~${candidateTokens.toLocaleString()} tokens after compaction; largest block: ${dominant}\n`));
|
|
2830
|
+
writeCompactionFailed(compactionId, `Summary remained over the context limit; largest block: ${dominant}.`, beforeTokens);
|
|
2340
2831
|
return;
|
|
2341
2832
|
}
|
|
2342
2833
|
messages.splice(0, messages.length, ...result.messages);
|
|
2834
|
+
compactionAttempts.recordSuccess(attemptKey);
|
|
2343
2835
|
loopGuard.resetReadOnly();
|
|
2344
2836
|
// Token stats use the same complete request estimate as the trigger.
|
|
2345
2837
|
const compactedTokens = estimateNextRequestTokens(messages);
|
|
@@ -2347,10 +2839,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2347
2839
|
// after older turns (which carried the plan context) were summarized.
|
|
2348
2840
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
2349
2841
|
if (livePlan) {
|
|
2350
|
-
messages.
|
|
2351
|
-
role: "system",
|
|
2352
|
-
content: planContextMessage(livePlan, session.planApproved.value),
|
|
2353
|
-
});
|
|
2842
|
+
upsertPlanContextMessage(messages, planContextMessage(livePlan, session.planApproved.value));
|
|
2354
2843
|
}
|
|
2355
2844
|
// Re-inject live SESSION STATE after compaction (older flags survive).
|
|
2356
2845
|
refreshSessionState(livePlan);
|
|
@@ -2371,22 +2860,25 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2371
2860
|
? PLAN_IMPLEMENT_MEMORY_PREFIX
|
|
2372
2861
|
: COMPACTION_MEMORY_PREFIX);
|
|
2373
2862
|
// Card shows pre/post of the summarization; plan re-injection is noted.
|
|
2374
|
-
|
|
2863
|
+
writeCompactionCompleted(compactionId, summaryText, beforeTokens, compactedTokens);
|
|
2375
2864
|
const planNote = afterTokens > compactedTokens
|
|
2376
2865
|
? ` (compacted to ~${compactedTokens.toLocaleString()}, +plan → ~${afterTokens.toLocaleString()})`
|
|
2377
2866
|
: "";
|
|
2378
2867
|
writeNotice("info", `context auto-compacted to fit the window (~${beforeTokens.toLocaleString()} → ~${compactedTokens.toLocaleString()} tokens${planNote})`, chalk.dim(` ℹ context auto-compacted (~${beforeTokens.toLocaleString()} → ~${compactedTokens.toLocaleString()} tokens${planNote})\n`));
|
|
2379
2868
|
}
|
|
2380
2869
|
catch (error) {
|
|
2870
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2871
|
+
writeCompactionFailed(compactionId, /aborted/i.test(message) ? "Compaction was cancelled." : message, beforeTokens);
|
|
2381
2872
|
if (error instanceof Error &&
|
|
2382
2873
|
(error.name === "AbortError" || error.message.includes("aborted"))) {
|
|
2383
2874
|
throw error;
|
|
2384
2875
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2876
|
+
compactionAttempts.recordFailure(attemptKey);
|
|
2877
|
+
await auditLog("agent.compact.failed", { reason: message });
|
|
2878
|
+
}
|
|
2879
|
+
finally {
|
|
2880
|
+
if (activeCompactionId === compactionId)
|
|
2881
|
+
activeCompactionId = undefined;
|
|
2390
2882
|
}
|
|
2391
2883
|
}
|
|
2392
2884
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
@@ -2549,16 +3041,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2549
3041
|
});
|
|
2550
3042
|
try {
|
|
2551
3043
|
if (responderDelivery &&
|
|
2552
|
-
!jobManager.
|
|
3044
|
+
!jobManager.markDeliveryStarted(responderDelivery.id, session.sessionId)) {
|
|
2553
3045
|
jobManager.releaseResponderNotificationClaim(responderDelivery.id);
|
|
2554
|
-
throw new Error(`failed to
|
|
3046
|
+
throw new Error(`failed to record responder delivery attempt ${responderDelivery.id}`);
|
|
2555
3047
|
}
|
|
2556
3048
|
completion = await streamWithProvider({
|
|
2557
3049
|
provider,
|
|
2558
3050
|
model,
|
|
2559
3051
|
allowModelFallback,
|
|
2560
3052
|
messages,
|
|
2561
|
-
|
|
3053
|
+
// Sampling is provider/model policy (llm/sampling.ts).
|
|
3054
|
+
// Sending a fixed 0.2 here overrode it for every model.
|
|
2562
3055
|
maxTokens: stepMaxTokens,
|
|
2563
3056
|
signal: options.signal,
|
|
2564
3057
|
thinking: retryWithoutThinking
|
|
@@ -2737,6 +3230,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2737
3230
|
throw streamError;
|
|
2738
3231
|
}
|
|
2739
3232
|
recordRecoveryAttempt(recoveryState, failureKind);
|
|
3233
|
+
// The router refuses transparent retries after emission,
|
|
3234
|
+
// but the recovery ladder may still re-run the step. Say so, since
|
|
3235
|
+
// the visible answer restarts from scratch.
|
|
3236
|
+
if (streamAlreadyEmitted(streamError)) {
|
|
3237
|
+
const restartNotice = "partial answer discarded after a mid-stream failure — the reply restarts below";
|
|
3238
|
+
writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
|
|
3239
|
+
}
|
|
2740
3240
|
if (plan.notice) {
|
|
2741
3241
|
writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
|
|
2742
3242
|
}
|
|
@@ -2764,6 +3264,14 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2764
3264
|
// Always clear the spinner — abort, network error, or success.
|
|
2765
3265
|
spinner.stop();
|
|
2766
3266
|
}
|
|
3267
|
+
if (responderDelivery) {
|
|
3268
|
+
// The result text is now part of this turn, so consumption is durable.
|
|
3269
|
+
// A stream that aborted or threw above never reaches this point and the
|
|
3270
|
+
// receipt stays deliverable.
|
|
3271
|
+
if (!jobManager.markDelivered(responderDelivery.id, session.sessionId)) {
|
|
3272
|
+
jobManager.releaseResponderNotificationClaim(responderDelivery.id);
|
|
3273
|
+
}
|
|
3274
|
+
}
|
|
2767
3275
|
provider = completion.provider;
|
|
2768
3276
|
model = completion.model;
|
|
2769
3277
|
if (completion.usage) {
|
|
@@ -2772,6 +3280,22 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2772
3280
|
usage: completion.usage,
|
|
2773
3281
|
model: completion.model,
|
|
2774
3282
|
});
|
|
3283
|
+
// Cache telemetry: without read/create counts there is no way to tell
|
|
3284
|
+
// whether the stable prefix is actually being reused.
|
|
3285
|
+
const cacheRead = completion.usage.cachedPromptTokens ?? 0;
|
|
3286
|
+
const cacheCreated = completion.usage.cacheCreationTokens ?? 0;
|
|
3287
|
+
if (cacheRead > 0 || cacheCreated > 0) {
|
|
3288
|
+
await auditLog("agent.prompt.cache", {
|
|
3289
|
+
provider: completion.provider,
|
|
3290
|
+
model: completion.model,
|
|
3291
|
+
promptTokens: completion.usage.promptTokens,
|
|
3292
|
+
cacheReadTokens: cacheRead,
|
|
3293
|
+
cacheCreationTokens: cacheCreated,
|
|
3294
|
+
hitRatio: completion.usage.promptTokens > 0
|
|
3295
|
+
? Number((cacheRead / completion.usage.promptTokens).toFixed(3))
|
|
3296
|
+
: 0,
|
|
3297
|
+
});
|
|
3298
|
+
}
|
|
2775
3299
|
}
|
|
2776
3300
|
deltaParser?.finish();
|
|
2777
3301
|
// Sticky text-only may have flipped dialect during stream retry.
|
|
@@ -2903,25 +3427,28 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2903
3427
|
truncatedToolRetries += 1;
|
|
2904
3428
|
if (truncatedToolRetries <= 5) {
|
|
2905
3429
|
try {
|
|
2906
|
-
const writeResult = await
|
|
3430
|
+
const writeResult = await applySalvagedWrite(salvaged);
|
|
2907
3431
|
if (writeResult.ok) {
|
|
2908
3432
|
const lineCount = salvaged.content.split("\n").length;
|
|
2909
3433
|
writeNotice("info", `native tool call was truncated — salvaged ${lineCount} lines and wrote to ${salvaged.path}`, chalk.cyan(` ℹ native tool call was truncated — salvaged ${lineCount} lines to ${salvaged.path}\n`));
|
|
2910
3434
|
// Pair assistant tool_calls with synthetic results so the
|
|
2911
3435
|
// next turn is not orphaned, then nudge for append.
|
|
2912
|
-
appendAssistantWithTools(messages, assistantText.visible, nativeToolCalls);
|
|
3436
|
+
appendAssistantWithTools(messages, assistantText.visible, nativeToolCalls, completion.reasoningBlock);
|
|
2913
3437
|
for (const tc of nativeToolCalls) {
|
|
2914
3438
|
appendToolResult(messages, tc.id, tc.id === writeTc.id
|
|
2915
3439
|
? `Tool ${tc.name} result (exit=0, ok=true):\nSalvaged partial write: ${lineCount} lines to ${salvaged.path}`
|
|
2916
3440
|
: `Tool ${tc.name} result (exit=1, ok=false):\nCancelled — sibling write was truncated and salvaged.`, tc.name, tc.id === writeTc.id);
|
|
2917
3441
|
}
|
|
2918
|
-
const priorBytes =
|
|
3442
|
+
const priorBytes = writeResult.bytesOnDisk;
|
|
3443
|
+
const salvagedToolName = salvaged.operation === "append"
|
|
3444
|
+
? "fs.append"
|
|
3445
|
+
: "fs.write";
|
|
2919
3446
|
const appendNudge = toolsAttached
|
|
2920
|
-
? `Your ${
|
|
3447
|
+
? `Your ${salvagedToolName} tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines (file is now ${priorBytes} bytes) to ${salvaged.path}. ` +
|
|
2921
3448
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
2922
3449
|
`CONTINUE by calling fs.append now with path=${JSON.stringify(salvaged.path)}, expectedPriorBytes=${priorBytes}, and content set to ONLY the remaining content not already on disk (prefer hundreds of lines per call). ` +
|
|
2923
3450
|
`Do not re-read the full file; do not re-send content already saved. Use the platform tool interface — no markdown fences.`
|
|
2924
|
-
: `Your
|
|
3451
|
+
: `Your ${salvagedToolName} tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines (file is now ${priorBytes} bytes) to ${salvaged.path}. ` +
|
|
2925
3452
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
2926
3453
|
`CONTINUE with ONE large fs.append of the remaining content:\n` +
|
|
2927
3454
|
'```tool\n{"name":"fs.append","args":{"path":' +
|
|
@@ -3055,23 +3582,23 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3055
3582
|
truncatedToolRetries += 1;
|
|
3056
3583
|
const salvaged = salvageTruncatedWrite(assistantText.visible);
|
|
3057
3584
|
if (salvaged && truncatedToolRetries <= 5) {
|
|
3058
|
-
// Write the salvaged partial content
|
|
3585
|
+
// Write the salvaged partial content through the normal
|
|
3586
|
+
// authorization path (classifier + confirmation + receipt).
|
|
3059
3587
|
try {
|
|
3060
|
-
const writeResult = await
|
|
3061
|
-
confirmed: true,
|
|
3062
|
-
});
|
|
3588
|
+
const writeResult = await applySalvagedWrite(salvaged);
|
|
3063
3589
|
if (writeResult.ok) {
|
|
3064
3590
|
const lineCount = salvaged.content.split("\n").length;
|
|
3065
3591
|
writeNotice("info", `tool call was truncated — salvaged ${lineCount} lines and wrote to ${salvaged.path}`, chalk.cyan(` ℹ tool call was truncated — salvaged ${lineCount} lines to ${salvaged.path}\n`));
|
|
3066
3592
|
pushAssistantHistory(stripThinking(assistantText.visible).visible);
|
|
3067
|
-
const priorBytes =
|
|
3593
|
+
const priorBytes = writeResult.bytesOnDisk;
|
|
3594
|
+
const salvagedToolName = salvaged.operation === "append" ? "fs.append" : "fs.write";
|
|
3068
3595
|
messages.push({
|
|
3069
3596
|
role: "user",
|
|
3070
3597
|
content: toolsAttached
|
|
3071
|
-
? `Your
|
|
3598
|
+
? `Your ${salvagedToolName} tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines (file is now ${priorBytes} bytes) to ${salvaged.path}. ` +
|
|
3072
3599
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
3073
3600
|
`CONTINUE by calling fs.append now with path=${JSON.stringify(salvaged.path)}, expectedPriorBytes=${priorBytes}, and content set to ONLY the remaining content (prefer large chunks). Use the platform tool interface — no markdown fences.`
|
|
3074
|
-
: `Your
|
|
3601
|
+
: `Your ${salvagedToolName} tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines (file is now ${priorBytes} bytes) to ${salvaged.path}. ` +
|
|
3075
3602
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
3076
3603
|
`CONTINUE with ONE large fs.append of the remaining content (prefer hundreds of lines per call — do NOT use tiny ~100-line chunks):\n` +
|
|
3077
3604
|
'```tool\n{"name":"fs.append","args":{"path":' +
|
|
@@ -3120,9 +3647,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3120
3647
|
const salvaged = salvageTruncatedWrite(assistantText.visible);
|
|
3121
3648
|
if (salvaged) {
|
|
3122
3649
|
try {
|
|
3123
|
-
const writeResult = await
|
|
3124
|
-
confirmed: true,
|
|
3125
|
-
});
|
|
3650
|
+
const writeResult = await applySalvagedWrite(salvaged);
|
|
3126
3651
|
if (writeResult.ok) {
|
|
3127
3652
|
const lineCount = salvaged.content.split("\n").length;
|
|
3128
3653
|
writeNotice("info", `malformed tool call salvaged — wrote ${lineCount} lines to ${salvaged.path}`, chalk.cyan(` ℹ malformed tool call salvaged — wrote ${lineCount} lines to ${salvaged.path}\n`));
|
|
@@ -3169,7 +3694,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3169
3694
|
const unread = [...unreadResponderNotificationIds];
|
|
3170
3695
|
pushAssistantHistory(assistantText.visible);
|
|
3171
3696
|
messages.push(recoveryUserMessage(`You have ${unread.length} delivered Responder result(s) that remain unread: ${unread.join(", ")}. ` +
|
|
3172
|
-
"If analysis is incomplete, call the
|
|
3697
|
+
"If analysis is incomplete, call only the bounded evidence tool needed now. If each result has been analyzed and is satisfactory, you MUST call job.read with its jobId or exact notificationId before giving a final response. job.read does not require an active plan; do not create or update a plan merely to acknowledge a result."));
|
|
3173
3698
|
continue;
|
|
3174
3699
|
}
|
|
3175
3700
|
const narratedAction = looksLikeActionNarration(cleaned);
|
|
@@ -3201,6 +3726,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3201
3726
|
looksLikeErrorDiagnosisWithFixIntent(cleaned);
|
|
3202
3727
|
const shouldRetryBeforeFinalizing = productiveSteps === 0 ||
|
|
3203
3728
|
planNarrated ||
|
|
3729
|
+
((narratedAction || narratedWebAction) && !informationalQuery) ||
|
|
3204
3730
|
(session.planApproved.value &&
|
|
3205
3731
|
planHasOpenWorkNow &&
|
|
3206
3732
|
(narratedAction || errorFixNarration)) ||
|
|
@@ -3348,7 +3874,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3348
3874
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
3349
3875
|
const unfinished = livePlan?.tasks.filter((task) => !task.responderOwned &&
|
|
3350
3876
|
(task.state === "pending" || task.state === "in_progress"));
|
|
3351
|
-
const deferReport =
|
|
3877
|
+
const deferReport = shouldYieldForDeclaredResponderDependency(livePlan, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId);
|
|
3352
3878
|
if (livePlan &&
|
|
3353
3879
|
unfinished &&
|
|
3354
3880
|
unfinished.length > 0 &&
|
|
@@ -3370,9 +3896,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3370
3896
|
const remainingCriteria = [];
|
|
3371
3897
|
if (session.planApproved.value) {
|
|
3372
3898
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
3373
|
-
|
|
3374
|
-
|
|
3899
|
+
// Foreground work decides the turn outcome. Responder children run
|
|
3900
|
+
// concurrently by design and are reported separately.
|
|
3901
|
+
const unfinished = livePlan ? foregroundRemaining(livePlan) : [];
|
|
3902
|
+
const failedTasks = livePlan?.tasks.filter((task) => !task.responderOwned && task.state === "failed") ?? [];
|
|
3375
3903
|
remainingCriteria.push(...unfinished.map((task) => `[${task.id}] ${task.title}`), ...failedTasks.map((task) => `[${task.id}] retry failed task: ${task.title}`));
|
|
3904
|
+
const openResponderChildren = livePlan
|
|
3905
|
+
? responderOpenTasks(livePlan)
|
|
3906
|
+
: [];
|
|
3907
|
+
if (openResponderChildren.length > 0) {
|
|
3908
|
+
remainingCriteria.push(...openResponderChildren.map((task) => `[${task.id}] responder result awaiting analysis: ${task.title}`));
|
|
3909
|
+
}
|
|
3376
3910
|
if (failedTasks.length > 0)
|
|
3377
3911
|
outcomeStatus = "failed";
|
|
3378
3912
|
else if (unfinished.length > 0)
|
|
@@ -3514,7 +4048,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3514
4048
|
writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
|
|
3515
4049
|
}
|
|
3516
4050
|
if (historyNativeCalls.length) {
|
|
3517
|
-
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls);
|
|
4051
|
+
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock);
|
|
3518
4052
|
}
|
|
3519
4053
|
else {
|
|
3520
4054
|
const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
|
|
@@ -3563,7 +4097,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3563
4097
|
if (res.ok && res.call.name === "plan.create") {
|
|
3564
4098
|
planCreatedThisTurn = true;
|
|
3565
4099
|
}
|
|
3566
|
-
|
|
4100
|
+
if (!res.suppressedRepeat)
|
|
4101
|
+
productiveSteps += 1;
|
|
3567
4102
|
// E5: collapse identical large tool bodies within this turn to a pointer.
|
|
3568
4103
|
const deduped = dedupeToolContextOutput({
|
|
3569
4104
|
content: res.contextOutput,
|
|
@@ -3728,7 +4263,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3728
4263
|
}
|
|
3729
4264
|
// User Esc/Ctrl+C only — never cancel siblings because a delete failed
|
|
3730
4265
|
// or a confirm was declined; the model must see every tool result.
|
|
3731
|
-
if (res.
|
|
4266
|
+
if (res.aborted)
|
|
3732
4267
|
aborted = true;
|
|
3733
4268
|
};
|
|
3734
4269
|
// Multi-task sync guard: when one model message advances more than one
|
|
@@ -3749,7 +4284,24 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3749
4284
|
: parsed.taskId;
|
|
3750
4285
|
intents.push({ call: b.call, taskId: resolvedId, state: parsed.state });
|
|
3751
4286
|
}
|
|
3752
|
-
|
|
4287
|
+
const openIds = openingTaskIds(intents);
|
|
4288
|
+
if (openIds.length > 1) {
|
|
4289
|
+
// A multi-open is never confirmable; the store keeps at
|
|
4290
|
+
// most one active foreground task.
|
|
4291
|
+
session.pendingTaskBatch.value = undefined;
|
|
4292
|
+
const openDescriptors = openIds.map((taskId) => ({
|
|
4293
|
+
taskId,
|
|
4294
|
+
title: livePlanForBatch?.tasks.find((t) => t.id === taskId)?.title ?? "",
|
|
4295
|
+
targetState: "in_progress",
|
|
4296
|
+
}));
|
|
4297
|
+
batchReminderNote = buildMultiOpenRejection(openDescriptors);
|
|
4298
|
+
for (const intent of intents) {
|
|
4299
|
+
if (intent.state === "in_progress")
|
|
4300
|
+
batchRemindCalls.add(intent.call);
|
|
4301
|
+
}
|
|
4302
|
+
writeNotice("warn", multiOpenToast(openIds.length), chalk.yellow(` ⚠ ${multiOpenToast(openIds.length)}\n`));
|
|
4303
|
+
}
|
|
4304
|
+
else if (isSimultaneousTaskAdvance(intents)) {
|
|
3753
4305
|
const signature = batchUpdateSignature(intents);
|
|
3754
4306
|
if (session.pendingTaskBatch.value === signature) {
|
|
3755
4307
|
session.pendingTaskBatch.value = undefined;
|
|
@@ -3870,7 +4422,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3870
4422
|
return finishTurn("", productiveSteps, "partial", ["Approve or revise the draft plan before implementation."]);
|
|
3871
4423
|
}
|
|
3872
4424
|
if (aborted) {
|
|
3873
|
-
lastAnswer = "
|
|
4425
|
+
lastAnswer = "";
|
|
3874
4426
|
outcomeState.outcome.status = "aborted";
|
|
3875
4427
|
await saveOutcomeState(outcomeState);
|
|
3876
4428
|
moveTurn("aborted", "turn aborted");
|
|
@@ -3901,11 +4453,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3901
4453
|
}
|
|
3902
4454
|
catch (error) {
|
|
3903
4455
|
const isAbort = isAbortError(error, options.signal);
|
|
3904
|
-
const msg = isAbort ? "Aborted." : `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3905
4456
|
if (isAbort) {
|
|
3906
4457
|
writeAbort();
|
|
3907
|
-
return finishTurn(
|
|
4458
|
+
return finishTurn("", 0, "aborted");
|
|
3908
4459
|
}
|
|
4460
|
+
releaseUnreadResponderClaims();
|
|
4461
|
+
const msg = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3909
4462
|
if (options.onMessages) {
|
|
3910
4463
|
try {
|
|
3911
4464
|
options.onMessages(buildTurnHistory(liveMessages, msg));
|
|
@@ -3921,8 +4474,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3921
4474
|
throw error;
|
|
3922
4475
|
}
|
|
3923
4476
|
}
|
|
3924
|
-
/** Compatibility boundary for callers that
|
|
4477
|
+
/** Compatibility boundary for callers that consume visible assistant text. */
|
|
3925
4478
|
export async function runAgentLoop(prompt, options = {}) {
|
|
3926
|
-
return
|
|
4479
|
+
return (await runAgentTurn(prompt, options)).answer;
|
|
3927
4480
|
}
|
|
3928
4481
|
//# sourceMappingURL=runner.js.map
|