@pentoshi/clai 3.9.2 → 3.9.3
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 +14 -1
- package/dist/agent/runner.js +759 -211
- 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-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/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 +17 -17
- package/dist/app/controllers/session-controller.js +122 -124
- 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/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/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-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 +61 -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 +162 -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/jobs.d.ts +78 -4
- package/dist/tools/jobs.js +542 -115
- 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 +154 -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/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 +29 -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, } 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,7 @@ 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 = informationalQuery || idleOrSocialPrompt;
|
|
326
384
|
const freshWebSearchRequired = !buildLikeTurn &&
|
|
327
385
|
!pentestLikeTurn &&
|
|
328
386
|
!idleOrSocialPrompt &&
|
|
@@ -348,7 +406,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
348
406
|
const base = compact
|
|
349
407
|
? getCompactToolDefinitions()
|
|
350
408
|
: getToolDefinitions();
|
|
351
|
-
const allow = new Set([...toolNames, ...
|
|
409
|
+
const allow = new Set([...toolNames, ...RUNNER_META_TOOL_NAMES]);
|
|
352
410
|
return base.filter((d) => allow.has(d.name));
|
|
353
411
|
};
|
|
354
412
|
let lastAnswer = "";
|
|
@@ -469,9 +527,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
469
527
|
return sections.join("\n\n");
|
|
470
528
|
};
|
|
471
529
|
const systemSections = [buildSystemContent(nativeToolsActive)];
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
530
|
+
// The live plan is mutable state: it is injected once as a keyed request
|
|
531
|
+
// suffix (upsertPlanContextMessage) instead of being frozen into the stable
|
|
532
|
+
// system prefix, so the model never sees a stale and a fresh plan together.
|
|
475
533
|
// Soft mid-work recovery (any domain): re-attach to jobs / open tasks /
|
|
476
534
|
// last tools after interrupt or "continue" — not a hard gate.
|
|
477
535
|
if (!informationalQuery && !idleOrSocialPrompt) {
|
|
@@ -483,6 +541,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
483
541
|
recentJobs: jobManager.getRecentJobs(12, session.sessionId),
|
|
484
542
|
informationalQuery,
|
|
485
543
|
idleOrSocial: idleOrSocialPrompt,
|
|
544
|
+
...(options.previousTurn ? { previousTurn: options.previousTurn } : {}),
|
|
486
545
|
});
|
|
487
546
|
if (continueBrief) {
|
|
488
547
|
systemSections.push(continueBrief);
|
|
@@ -585,7 +644,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
585
644
|
if (!has("plan")) {
|
|
586
645
|
sections.push({
|
|
587
646
|
kind: "plan",
|
|
588
|
-
content: "
|
|
647
|
+
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
648
|
mandatory: true,
|
|
590
649
|
});
|
|
591
650
|
}
|
|
@@ -636,13 +695,31 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
636
695
|
userMessage,
|
|
637
696
|
];
|
|
638
697
|
liveMessages = messages;
|
|
639
|
-
|
|
698
|
+
if (activePlan) {
|
|
699
|
+
upsertPlanContextMessage(messages, planContextMessage(activePlan, session.planApproved.value));
|
|
700
|
+
}
|
|
701
|
+
const responderWakeTurn = options.displayPrompt === null &&
|
|
702
|
+
prompt.startsWith("Responder result arrived");
|
|
640
703
|
const responderWakeNotificationId = responderWakeTurn
|
|
641
704
|
? /^notification=(.+)$/m.exec(prompt)?.[1]?.trim()
|
|
642
705
|
: undefined;
|
|
643
|
-
const
|
|
644
|
-
|
|
645
|
-
|
|
706
|
+
const responderWakeJobId = responderWakeTurn
|
|
707
|
+
? /^job=(.+)$/m.exec(prompt)?.[1]?.trim()
|
|
708
|
+
: undefined;
|
|
709
|
+
const responderWakeResultRevision = responderWakeTurn
|
|
710
|
+
? Number(/^resultRevision=(\d+)$/m.exec(prompt)?.[1]) || undefined
|
|
711
|
+
: undefined;
|
|
712
|
+
const matchesWakeRevision = (notification) => responderWakeResultRevision === undefined ||
|
|
713
|
+
(notification.resultRevision ?? 1) === responderWakeResultRevision;
|
|
714
|
+
const wakeNotification = responderWakeNotificationId
|
|
715
|
+
? jobManager
|
|
716
|
+
.getPendingNotifications(session.sessionId)
|
|
717
|
+
.find((notification) => notification.id === responderWakeNotificationId &&
|
|
718
|
+
(!responderWakeJobId || notification.jobId === responderWakeJobId) &&
|
|
719
|
+
matchesWakeRevision(notification))
|
|
720
|
+
: undefined;
|
|
721
|
+
if (wakeNotification) {
|
|
722
|
+
unreadResponderNotificationIds.add(wakeNotification.id);
|
|
646
723
|
}
|
|
647
724
|
const refreshResponderInbox = () => {
|
|
648
725
|
const running = jobManager
|
|
@@ -653,6 +730,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
653
730
|
.getPendingNotifications(session.sessionId)
|
|
654
731
|
.filter((notification) => notification.responder &&
|
|
655
732
|
unreadResponderNotificationIds.has(notification.id) &&
|
|
733
|
+
matchesWakeRevision(notification) &&
|
|
656
734
|
!notification.readAt &&
|
|
657
735
|
!notification.analyzedAt &&
|
|
658
736
|
!notification.archivedAt)
|
|
@@ -715,6 +793,33 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
715
793
|
};
|
|
716
794
|
const loopGuard = new LoopGuard();
|
|
717
795
|
const engagementPolicy = new EngagementPolicyEngine();
|
|
796
|
+
const probeStateKey = (call) => {
|
|
797
|
+
const project = (job) => job
|
|
798
|
+
? [
|
|
799
|
+
job.id,
|
|
800
|
+
job.status,
|
|
801
|
+
job.exitCode ?? null,
|
|
802
|
+
job.signal ?? null,
|
|
803
|
+
job.stdoutArtifact,
|
|
804
|
+
job.artifacts.stdout.bytes,
|
|
805
|
+
job.artifacts.stdout.sha256,
|
|
806
|
+
job.stderrArtifact,
|
|
807
|
+
job.artifacts.stderr.bytes,
|
|
808
|
+
job.artifacts.stderr.sha256,
|
|
809
|
+
]
|
|
810
|
+
: undefined;
|
|
811
|
+
if (call.name === "shell.tail" && typeof call.args.id === "string") {
|
|
812
|
+
const projected = project(jobManager.getJob(call.args.id));
|
|
813
|
+
return projected ? JSON.stringify(projected) : undefined;
|
|
814
|
+
}
|
|
815
|
+
if (call.name === "shell.jobs") {
|
|
816
|
+
return JSON.stringify(jobManager
|
|
817
|
+
.getRecentJobs(100, session.sessionId)
|
|
818
|
+
.map((job) => project(job))
|
|
819
|
+
.filter(Boolean));
|
|
820
|
+
}
|
|
821
|
+
return undefined;
|
|
822
|
+
};
|
|
718
823
|
// Track consecutive thinking-only responses so we can nudge the model
|
|
719
824
|
// to actually act instead of silently returning an empty answer.
|
|
720
825
|
let emptyVisibleRetries = 0;
|
|
@@ -807,7 +912,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
807
912
|
});
|
|
808
913
|
};
|
|
809
914
|
const reconcileOpenTaskBeforeFinalizing = async () => {
|
|
810
|
-
|
|
915
|
+
let plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
811
916
|
const open = plan?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
|
|
812
917
|
if (!plan || !open)
|
|
813
918
|
return plan;
|
|
@@ -815,42 +920,80 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
815
920
|
if (!gate.ok)
|
|
816
921
|
return plan;
|
|
817
922
|
const reconciledTaskIds = [open.id];
|
|
818
|
-
|
|
923
|
+
const working = plan;
|
|
924
|
+
markTask(working, open.id, "done", "Completion reconciled from verified task evidence.");
|
|
819
925
|
while (true) {
|
|
820
|
-
const observation = readyPlanTasks(
|
|
821
|
-
(
|
|
926
|
+
const observation = readyPlanTasks(working).find((task) => isRuntimeObservationTask(task.title) ||
|
|
927
|
+
(working.kind === "pentest" && isRemoteObservationTask(task.title)));
|
|
822
928
|
if (!observation)
|
|
823
929
|
break;
|
|
824
|
-
const observationGate = completionGateForTask(
|
|
930
|
+
const observationGate = completionGateForTask(working, observation.id);
|
|
825
931
|
if (!observationGate.ok)
|
|
826
932
|
break;
|
|
827
|
-
markTask(
|
|
933
|
+
markTask(working, observation.id, "done", working.kind === "pentest"
|
|
828
934
|
? "Satisfied by verified remote evidence from the preceding task."
|
|
829
935
|
: "Satisfied by the verified runtime evidence from the preceding task.");
|
|
830
936
|
reconciledTaskIds.push(observation.id);
|
|
831
937
|
}
|
|
832
|
-
if (
|
|
833
|
-
|
|
938
|
+
if (working.status === "draft" || working.status === "approved") {
|
|
939
|
+
working.status = "in_progress";
|
|
834
940
|
}
|
|
835
|
-
if (isPlanTerminal(
|
|
836
|
-
|
|
941
|
+
if (isPlanTerminal(working)) {
|
|
942
|
+
working.status = isPlanSuccessful(working) ? "completed" : "abandoned";
|
|
837
943
|
}
|
|
838
|
-
|
|
944
|
+
// Replay the same reconciliation as a reducer so a concurrent
|
|
945
|
+
// responder settlement is preserved instead of overwritten.
|
|
946
|
+
const reconciledNotes = new Map(reconciledTaskIds.map((id) => [
|
|
947
|
+
id,
|
|
948
|
+
working.tasks.find((task) => task.id === id)?.note,
|
|
949
|
+
]));
|
|
950
|
+
const reconcileResult = await mutatePlan(session.sessionId, (draft) => {
|
|
951
|
+
let changed = false;
|
|
952
|
+
for (const [id, note] of reconciledNotes) {
|
|
953
|
+
const task = draft.tasks.find((candidate) => candidate.id === id);
|
|
954
|
+
if (!task || task.state === "done")
|
|
955
|
+
continue;
|
|
956
|
+
task.state = "done";
|
|
957
|
+
if (note !== undefined)
|
|
958
|
+
task.note = note;
|
|
959
|
+
changed = true;
|
|
960
|
+
}
|
|
961
|
+
if (draft.status === "draft" || draft.status === "approved") {
|
|
962
|
+
draft.status = "in_progress";
|
|
963
|
+
changed = true;
|
|
964
|
+
}
|
|
965
|
+
if (isPlanTerminal(draft)) {
|
|
966
|
+
draft.status = isPlanSuccessful(draft) ? "completed" : "abandoned";
|
|
967
|
+
changed = true;
|
|
968
|
+
}
|
|
969
|
+
return changed;
|
|
970
|
+
}).catch(() => undefined);
|
|
971
|
+
if (reconcileResult?.ok && reconcileResult.plan)
|
|
972
|
+
plan = reconcileResult.plan;
|
|
839
973
|
writePlanUpdate(plan, renderPlanForTerminal(plan) + "\n");
|
|
840
974
|
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
975
|
taskWorkLedger = null;
|
|
842
976
|
return plan;
|
|
843
977
|
};
|
|
844
978
|
async function persistProjectRootOnPlan(root) {
|
|
845
|
-
const live = await loadPlan(session.sessionId).catch(() => undefined);
|
|
846
|
-
if (!live)
|
|
847
|
-
return;
|
|
848
979
|
const pm = detectPackageManager(root);
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
980
|
+
// Metadata patches go through the transactional boundary so a
|
|
981
|
+
// concurrent task transition or responder settlement is not clobbered.
|
|
982
|
+
await mutatePlan(session.sessionId, (draft) => {
|
|
983
|
+
patchPlanMeta(draft, {
|
|
984
|
+
projectRoot: root,
|
|
985
|
+
...(pm ? { packageManager: pm } : {}),
|
|
986
|
+
});
|
|
987
|
+
}).catch(() => undefined);
|
|
988
|
+
}
|
|
989
|
+
/** Persist task evidence without rewriting the whole plan. */
|
|
990
|
+
async function persistTaskEvidence(taskId, evidence) {
|
|
991
|
+
await mutatePlan(session.sessionId, (draft) => {
|
|
992
|
+
const task = draft.tasks.find((candidate) => candidate.id === taskId);
|
|
993
|
+
if (!task)
|
|
994
|
+
return false;
|
|
995
|
+
task.evidence = evidence;
|
|
996
|
+
}).catch(() => undefined);
|
|
854
997
|
}
|
|
855
998
|
refreshSessionState = (plan) => {
|
|
856
999
|
if (idleOrSocialPrompt || informationalQuery)
|
|
@@ -901,6 +1044,11 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
901
1044
|
: undefined,
|
|
902
1045
|
};
|
|
903
1046
|
snap.nextHint = inferNextHint(snap);
|
|
1047
|
+
// One live plan copy, refreshed at the same protocol-safe points as
|
|
1048
|
+
// SESSION STATE so advancing tasks are never contradicted by a stale copy.
|
|
1049
|
+
if (p) {
|
|
1050
|
+
upsertPlanContextMessage(messages, planContextMessage(p, session.planApproved.value));
|
|
1051
|
+
}
|
|
904
1052
|
upsertSessionStateMessage(messages, buildSessionStateBlock(snap));
|
|
905
1053
|
};
|
|
906
1054
|
refreshSessionState(activePlan);
|
|
@@ -937,7 +1085,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
937
1085
|
kind: inferOutcomeKind({ userIntent: prompt, buildLike, pentestLike }),
|
|
938
1086
|
continueExisting: continueExistingOutcome,
|
|
939
1087
|
});
|
|
1088
|
+
loopGuard.restoreCompletedOperations(outcomeState.completedOperations ?? []);
|
|
940
1089
|
await saveOutcomeState(outcomeState);
|
|
1090
|
+
// Canonical mutation/artifact ledger feeding the durable compaction envelope.
|
|
1091
|
+
const workLedger = new WorkLedger();
|
|
941
1092
|
let governorState = createGovernorState();
|
|
942
1093
|
let governorPauseReason;
|
|
943
1094
|
let turnState = createTurnState();
|
|
@@ -992,10 +1143,54 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
992
1143
|
return release;
|
|
993
1144
|
},
|
|
994
1145
|
};
|
|
1146
|
+
/**
|
|
1147
|
+
* Apply a salvaged partial write through the NORMAL tool path so the
|
|
1148
|
+
* classifier, scope/engagement gates, confirmation prompt, and receipts
|
|
1149
|
+
* all apply exactly as they would for a model-emitted call. Salvage must
|
|
1150
|
+
* never mutate a file with `confirmed: true`, and an `fs.append` that was
|
|
1151
|
+
* cut off must stay an append (with its precondition) instead of becoming
|
|
1152
|
+
* a full overwrite.
|
|
1153
|
+
*/
|
|
1154
|
+
async function applySalvagedWrite(salvaged) {
|
|
1155
|
+
const args = {
|
|
1156
|
+
path: salvaged.path,
|
|
1157
|
+
content: salvaged.content,
|
|
1158
|
+
};
|
|
1159
|
+
if (salvaged.operation === "append") {
|
|
1160
|
+
args.position = "end";
|
|
1161
|
+
if (typeof salvaged.expectedPriorBytes === "number") {
|
|
1162
|
+
args.expectedPriorBytes = salvaged.expectedPriorBytes;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
const call = {
|
|
1166
|
+
name: salvaged.operation === "append" ? "fs.append" : "fs.write",
|
|
1167
|
+
args,
|
|
1168
|
+
};
|
|
1169
|
+
const eventId = `tool-${++nextToolEventId}`;
|
|
1170
|
+
const res = await executeSingleTool(call, eventId, options.signal || new AbortController().signal);
|
|
1171
|
+
const ok = res.ok && res.result.ok;
|
|
1172
|
+
let bytesOnDisk = Buffer.byteLength(salvaged.content, "utf8");
|
|
1173
|
+
if (ok) {
|
|
1174
|
+
try {
|
|
1175
|
+
const stats = await stat(resolveFsToolPath(salvaged.path));
|
|
1176
|
+
bytesOnDisk = stats.size;
|
|
1177
|
+
}
|
|
1178
|
+
catch {
|
|
1179
|
+
// Keep the content-length estimate when the file cannot be stat'ed.
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return {
|
|
1183
|
+
ok,
|
|
1184
|
+
cancelled: Boolean(res.blockOrCancel),
|
|
1185
|
+
output: res.result.output,
|
|
1186
|
+
bytesOnDisk,
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
995
1189
|
async function executeSingleTool(rawCall, toolEventId, parentSignal) {
|
|
996
1190
|
const scratchDir = scratchDirFor(safeCwd());
|
|
997
1191
|
let call = normalizeToolCall(rawCall);
|
|
998
1192
|
let dispatchedTaskId;
|
|
1193
|
+
let delegation;
|
|
999
1194
|
let engagementLease;
|
|
1000
1195
|
let engagementGraph;
|
|
1001
1196
|
let engagementRecord;
|
|
@@ -1016,7 +1211,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1016
1211
|
return { ok: true, call, result, contextOutput: recoveryText };
|
|
1017
1212
|
}
|
|
1018
1213
|
if (narrowNmapOperation) {
|
|
1019
|
-
const allowed = new Set([
|
|
1214
|
+
const allowed = new Set([
|
|
1215
|
+
"net.scan",
|
|
1216
|
+
"shell.tail",
|
|
1217
|
+
"shell.jobs",
|
|
1218
|
+
"job.read",
|
|
1219
|
+
"task.read",
|
|
1220
|
+
]);
|
|
1020
1221
|
if (!allowed.has(call.name)) {
|
|
1021
1222
|
const reason = `Narrow nmap request: ${call.name} was not run because the user requested only one nmap operation. ` +
|
|
1022
1223
|
`Call net.scan with the requested target/options; do not create a plan or add DNS, WHOIS, HTTP, recon, or vulnerability steps.`;
|
|
@@ -1056,17 +1257,27 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1056
1257
|
detail: String(retryReasonRaw.detail ?? ""),
|
|
1057
1258
|
}
|
|
1058
1259
|
: undefined;
|
|
1059
|
-
|
|
1060
|
-
// Successful re-calls always run for real — no "already succeeded /
|
|
1061
|
-
// use prior results" nags (those caused model abnormalities).
|
|
1260
|
+
const currentProbeState = probeStateKey(call);
|
|
1062
1261
|
const loopCheck = loopGuard.shouldBlock(call.name, call.args, {
|
|
1063
1262
|
dependenciesChanged: retryDependenciesChanged,
|
|
1064
1263
|
environmentChanged: retryEnvironmentChanged,
|
|
1264
|
+
...(currentProbeState ? { stateKey: currentProbeState } : {}),
|
|
1065
1265
|
...(retryReason ? { retryReason } : {}),
|
|
1066
1266
|
});
|
|
1067
1267
|
if (loopCheck.block) {
|
|
1068
1268
|
const reason = loopCheck.reason ??
|
|
1069
1269
|
`${call.name} previously failed with identical arguments. Change the command/args and retry.`;
|
|
1270
|
+
if (loopCheck.kind === "unchanged-success") {
|
|
1271
|
+
const result = { ok: true, output: reason, exitCode: 0 };
|
|
1272
|
+
emitToolResult(toolEventId, result, reason);
|
|
1273
|
+
return {
|
|
1274
|
+
ok: true,
|
|
1275
|
+
call,
|
|
1276
|
+
result,
|
|
1277
|
+
contextOutput: reason,
|
|
1278
|
+
suppressedRepeat: true,
|
|
1279
|
+
};
|
|
1280
|
+
}
|
|
1070
1281
|
writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
|
|
1071
1282
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1072
1283
|
emitToolResult(toolEventId, result, reason);
|
|
@@ -1080,29 +1291,63 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1080
1291
|
if (call.name === "plan.create" ||
|
|
1081
1292
|
call.name === "task.add" ||
|
|
1082
1293
|
call.name === "task.move" ||
|
|
1294
|
+
call.name === "job.read" ||
|
|
1083
1295
|
call.name === "task.read" ||
|
|
1084
1296
|
call.name === "task.update") {
|
|
1085
|
-
if (call.name === "task.read") {
|
|
1086
|
-
const
|
|
1297
|
+
if (call.name === "job.read" || call.name === "task.read") {
|
|
1298
|
+
const requestedNotificationId = typeof call.args.notificationId === "string"
|
|
1087
1299
|
? call.args.notificationId.trim()
|
|
1088
1300
|
: "";
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1301
|
+
const requestedJobId = typeof call.args.jobId === "string" ? call.args.jobId.trim() : "";
|
|
1302
|
+
const pending = jobManager.getPendingNotifications(session.sessionId);
|
|
1303
|
+
const eligible = responderWakeTurn
|
|
1304
|
+
? pending.filter(matchesWakeRevision)
|
|
1305
|
+
: pending;
|
|
1306
|
+
const byNotification = requestedNotificationId
|
|
1307
|
+
? eligible.find((candidate) => candidate.id === requestedNotificationId)
|
|
1308
|
+
: undefined;
|
|
1309
|
+
const byJob = requestedJobId
|
|
1310
|
+
? eligible.find((candidate) => candidate.jobId === requestedJobId)
|
|
1311
|
+
: undefined;
|
|
1312
|
+
const identifiersConflict = Boolean((byNotification && requestedJobId && byNotification.jobId !== requestedJobId) ||
|
|
1313
|
+
(byJob && requestedNotificationId && byJob.id !== requestedNotificationId));
|
|
1314
|
+
const notification = identifiersConflict
|
|
1315
|
+
? undefined
|
|
1316
|
+
: (byNotification ?? byJob);
|
|
1317
|
+
const visible = Boolean(notification && unreadResponderNotificationIds.has(notification.id));
|
|
1318
|
+
const wakeIdentityMatches = Boolean(responderWakeTurn &&
|
|
1319
|
+
(requestedNotificationId || requestedJobId) &&
|
|
1320
|
+
(!requestedNotificationId ||
|
|
1321
|
+
requestedNotificationId === responderWakeNotificationId) &&
|
|
1322
|
+
(!requestedJobId || requestedJobId === responderWakeJobId));
|
|
1323
|
+
const staleWakeSettled = wakeIdentityMatches && !identifiersConflict && !notification;
|
|
1324
|
+
const persistedRead = Boolean(notification &&
|
|
1094
1325
|
visible &&
|
|
1095
|
-
jobManager.markRead(
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1326
|
+
jobManager.markRead(notification.id, session.sessionId));
|
|
1327
|
+
const marked = persistedRead || staleWakeSettled;
|
|
1328
|
+
const identifier = requestedJobId || requestedNotificationId;
|
|
1329
|
+
const revisionLabel = responderWakeResultRevision
|
|
1330
|
+
? ` revision ${responderWakeResultRevision}`
|
|
1331
|
+
: "";
|
|
1332
|
+
const output = persistedRead
|
|
1333
|
+
? `Responder job ${notification.jobId} (${notification.id}) marked delivered and read after model analysis.`
|
|
1334
|
+
: staleWakeSettled
|
|
1335
|
+
? `Responder result ${identifier}${revisionLabel} was already settled or discarded; the stale wake is acknowledged idempotently.`
|
|
1336
|
+
: !requestedNotificationId && !requestedJobId
|
|
1337
|
+
? `${call.name} failed: jobId or notificationId is required.`
|
|
1338
|
+
: identifiersConflict
|
|
1339
|
+
? `${call.name} failed: jobId and notificationId refer to different Responder results.`
|
|
1340
|
+
: !notification
|
|
1341
|
+
? `${call.name} failed: Responder result ${identifier} is unavailable, consumed, or archived.`
|
|
1342
|
+
: !visible
|
|
1343
|
+
? `${call.name} failed: Responder result ${identifier} was not delivered to this model turn. Analyze a delivered result before marking it read.`
|
|
1344
|
+
: `${call.name} failed: read state for Responder result ${identifier} could not be persisted.`;
|
|
1345
|
+
if (persistedRead && notification) {
|
|
1104
1346
|
upsertResponderResultLedger(messages, notification);
|
|
1105
|
-
unreadResponderNotificationIds.delete(
|
|
1347
|
+
unreadResponderNotificationIds.delete(notification.id);
|
|
1348
|
+
}
|
|
1349
|
+
else if (staleWakeSettled && responderWakeNotificationId) {
|
|
1350
|
+
unreadResponderNotificationIds.delete(responderWakeNotificationId);
|
|
1106
1351
|
}
|
|
1107
1352
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1108
1353
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) +
|
|
@@ -1219,7 +1464,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1219
1464
|
taskWorkLedger = led;
|
|
1220
1465
|
if (planResult.plan && led && led.successWorkCount > 0 && persisted) {
|
|
1221
1466
|
persisted.evidence = taskEvidenceFromLedger(led);
|
|
1222
|
-
await
|
|
1467
|
+
await persistTaskEvidence(persisted.id, persisted.evidence);
|
|
1223
1468
|
}
|
|
1224
1469
|
}
|
|
1225
1470
|
else if (stateRaw === "done" && resolved) {
|
|
@@ -1228,7 +1473,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1228
1473
|
const t = planResult.plan.tasks.find((x) => x.id === resolved);
|
|
1229
1474
|
if (t) {
|
|
1230
1475
|
t.evidence = taskEvidenceFromLedger(taskWorkLedger);
|
|
1231
|
-
await
|
|
1476
|
+
await persistTaskEvidence(t.id, t.evidence);
|
|
1232
1477
|
}
|
|
1233
1478
|
}
|
|
1234
1479
|
taskWorkLedger = null;
|
|
@@ -1357,7 +1602,18 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1357
1602
|
livePlanForGate.status === "approved") {
|
|
1358
1603
|
livePlanForGate.status = "in_progress";
|
|
1359
1604
|
}
|
|
1360
|
-
|
|
1605
|
+
// Opening a task is a transition applied by
|
|
1606
|
+
// the reducer, which also enforces the single-active invariant.
|
|
1607
|
+
await mutatePlan(session.sessionId, (draft) => {
|
|
1608
|
+
const target = draft.tasks.find((candidate) => candidate.id === nextPending.id);
|
|
1609
|
+
if (!target || target.state === "in_progress")
|
|
1610
|
+
return false;
|
|
1611
|
+
target.state = "in_progress";
|
|
1612
|
+
if (draft.status === "draft" || draft.status === "approved") {
|
|
1613
|
+
draft.status = "in_progress";
|
|
1614
|
+
}
|
|
1615
|
+
return true;
|
|
1616
|
+
}).catch(() => undefined);
|
|
1361
1617
|
// Preserve evidence already credited to this task (e.g. pentest
|
|
1362
1618
|
// recon that ran before the task was formally opened).
|
|
1363
1619
|
if (!taskWorkLedger ||
|
|
@@ -1415,22 +1671,29 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1415
1671
|
alreadyPrintedIds.add(toolEventId);
|
|
1416
1672
|
}
|
|
1417
1673
|
const scopeTarget = safeScopeTargetForToolCall(call);
|
|
1418
|
-
const
|
|
1419
|
-
?
|
|
1420
|
-
:
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1674
|
+
const engagementActions = pentestSession || isPentestToolCall(call) || Boolean(scope)
|
|
1675
|
+
? safeEngagementActionsForToolCall(call)
|
|
1676
|
+
: [];
|
|
1677
|
+
// The primary action carries the URL/port/path detail used for leases and
|
|
1678
|
+
// network-hop authorization; every action (one per named target) must pass
|
|
1679
|
+
// the scope check below before the tool runs.
|
|
1680
|
+
const engagementAction = engagementActions[0];
|
|
1681
|
+
let engagementDecision;
|
|
1682
|
+
for (const action of engagementActions) {
|
|
1683
|
+
const decisionForAction = evaluateEngagementAction(scope, action);
|
|
1684
|
+
if (!decisionForAction)
|
|
1685
|
+
continue;
|
|
1686
|
+
if (action === engagementAction)
|
|
1687
|
+
engagementDecision = decisionForAction;
|
|
1425
1688
|
if (scope) {
|
|
1426
1689
|
engagementGraph = await openEngagement(scope);
|
|
1427
1690
|
engagementRecord = beginEngagementAction(engagementGraph, {
|
|
1428
1691
|
tool: call.name,
|
|
1429
|
-
target:
|
|
1430
|
-
phase:
|
|
1431
|
-
capability:
|
|
1432
|
-
authorized:
|
|
1433
|
-
reason:
|
|
1692
|
+
target: decisionForAction.normalizedTarget || action.target,
|
|
1693
|
+
phase: decisionForAction.phase,
|
|
1694
|
+
capability: decisionForAction.capability,
|
|
1695
|
+
authorized: decisionForAction.allowed,
|
|
1696
|
+
reason: decisionForAction.reason,
|
|
1434
1697
|
});
|
|
1435
1698
|
await saveEngagement(engagementGraph);
|
|
1436
1699
|
}
|
|
@@ -1438,16 +1701,22 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1438
1701
|
...(engagementGraph ? { engagementId: engagementGraph.id } : {}),
|
|
1439
1702
|
...(engagementRecord ? { actionId: engagementRecord.id } : {}),
|
|
1440
1703
|
tool: call.name,
|
|
1441
|
-
target:
|
|
1442
|
-
phase:
|
|
1443
|
-
capability:
|
|
1444
|
-
allowed:
|
|
1445
|
-
reason:
|
|
1704
|
+
target: decisionForAction.normalizedTarget,
|
|
1705
|
+
phase: decisionForAction.phase,
|
|
1706
|
+
capability: decisionForAction.capability,
|
|
1707
|
+
allowed: decisionForAction.allowed,
|
|
1708
|
+
reason: decisionForAction.reason,
|
|
1446
1709
|
});
|
|
1447
|
-
if (!
|
|
1448
|
-
const target =
|
|
1449
|
-
|
|
1450
|
-
|
|
1710
|
+
if (!decisionForAction.allowed) {
|
|
1711
|
+
const target = decisionForAction.normalizedTarget ||
|
|
1712
|
+
action.target ||
|
|
1713
|
+
scopeTarget ||
|
|
1714
|
+
"requested target";
|
|
1715
|
+
const reason = outOfScopeToolMessage({
|
|
1716
|
+
target,
|
|
1717
|
+
reason: decisionForAction.reason,
|
|
1718
|
+
allowed: scope?.authorizedTargets,
|
|
1719
|
+
});
|
|
1451
1720
|
writeToolBlocked(toolEventId, call.name, reason, chalk.red(` ✗ ${reason}\n`));
|
|
1452
1721
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1453
1722
|
emitToolResult(toolEventId, result, reason);
|
|
@@ -1554,6 +1823,53 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1554
1823
|
const candidate = pickPendingTaskForToolCall(readyPlanTasks(planAtDispatch), call, planAtDispatch.tasks.map((task) => task.title));
|
|
1555
1824
|
dispatchedTaskId = candidate?.id;
|
|
1556
1825
|
}
|
|
1826
|
+
// An explicitly declared responder parent wins over inference.
|
|
1827
|
+
const declaredParent = readDeclaredParentTaskId(call);
|
|
1828
|
+
if (declaredParent) {
|
|
1829
|
+
const resolvedParent = resolveResponderParent({
|
|
1830
|
+
plan: planAtDispatch,
|
|
1831
|
+
declared: declaredParent,
|
|
1832
|
+
activeForegroundTaskIds: dispatchedTaskId ? [dispatchedTaskId] : [],
|
|
1833
|
+
});
|
|
1834
|
+
if (!resolvedParent.ok) {
|
|
1835
|
+
const reason = `${call.name} failed: ${resolvedParent.reason}`;
|
|
1836
|
+
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1837
|
+
emitToolResult(toolEventId, result, reason);
|
|
1838
|
+
return { ok: false, call, result, contextOutput: reason };
|
|
1839
|
+
}
|
|
1840
|
+
dispatchedTaskId = resolvedParent.taskId ?? dispatchedTaskId;
|
|
1841
|
+
}
|
|
1842
|
+
// For an explicit delegation, create the child subtask before the
|
|
1843
|
+
// Process starts and launch the job already bound to it. The job therefore
|
|
1844
|
+
// Never carries the foreground parent in `taskId`, and settlement has a
|
|
1845
|
+
// Durable row to advance even if this turn dies right after spawn.
|
|
1846
|
+
if (isExplicitResponderDelegation(call) && planAtDispatch) {
|
|
1847
|
+
delegation = { id: `dg-${randomUUID().slice(0, 8)}` };
|
|
1848
|
+
const created = await mutatePlan(session.sessionId, (draft) => {
|
|
1849
|
+
const parentExists = !!dispatchedTaskId &&
|
|
1850
|
+
draft.tasks.some((task) => task.id === dispatchedTaskId);
|
|
1851
|
+
const child = appendPlanTask(draft, {
|
|
1852
|
+
title: delegationTaskTitle(call),
|
|
1853
|
+
state: "in_progress",
|
|
1854
|
+
note: `delegation=${delegation.id} awaiting launch`,
|
|
1855
|
+
dependencies: [],
|
|
1856
|
+
resourceLocks: [],
|
|
1857
|
+
...(parentExists ? { parentTaskId: dispatchedTaskId } : {}),
|
|
1858
|
+
responderOwned: true,
|
|
1859
|
+
delegationId: delegation.id,
|
|
1860
|
+
});
|
|
1861
|
+
delegation.taskId = child.id;
|
|
1862
|
+
return true;
|
|
1863
|
+
}).catch(() => undefined);
|
|
1864
|
+
if (!created?.ok || !delegation.taskId) {
|
|
1865
|
+
delegation = undefined;
|
|
1866
|
+
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"));
|
|
1867
|
+
}
|
|
1868
|
+
else if (created.plan) {
|
|
1869
|
+
pendingSessionStatePlan = created.plan;
|
|
1870
|
+
writePlanUpdate(created.plan, renderPlanForTerminal(created.plan) + "\n");
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1557
1873
|
if (dispatchedTaskId &&
|
|
1558
1874
|
(!taskWorkLedger || taskWorkLedger.taskId !== dispatchedTaskId)) {
|
|
1559
1875
|
const dispatchedTask = planAtDispatch?.tasks.find((task) => task.id === dispatchedTaskId);
|
|
@@ -1562,7 +1878,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1562
1878
|
if (engagementAction) {
|
|
1563
1879
|
engagementLease = engagementPolicy.acquire(scope, engagementAction);
|
|
1564
1880
|
if (!engagementLease.decision.allowed) {
|
|
1565
|
-
const
|
|
1881
|
+
const target = engagementLease.decision.normalizedTarget || engagementAction.target;
|
|
1882
|
+
const reason = outOfScopeToolMessage({
|
|
1883
|
+
target,
|
|
1884
|
+
reason: engagementLease.decision.reason,
|
|
1885
|
+
allowed: scope?.authorizedTargets,
|
|
1886
|
+
});
|
|
1566
1887
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
1567
1888
|
emitToolResult(toolEventId, result, reason);
|
|
1568
1889
|
return { ok: false, call, result, contextOutput: reason };
|
|
@@ -1664,7 +1985,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1664
1985
|
confirmed: true,
|
|
1665
1986
|
userPrompt: prompt,
|
|
1666
1987
|
sessionId: session.sessionId,
|
|
1667
|
-
...(
|
|
1988
|
+
...(delegation?.taskId ? { taskId: delegation.taskId } : {}),
|
|
1989
|
+
...(delegation ? { delegationId: delegation.id } : {}),
|
|
1990
|
+
...(dispatchedTaskId ? { parentTaskId: dispatchedTaskId } : {}),
|
|
1668
1991
|
wakeOnCompletion: true,
|
|
1669
1992
|
monitor: {
|
|
1670
1993
|
toolName: call.name,
|
|
@@ -1775,13 +2098,18 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1775
2098
|
// User Esc/Ctrl+C: force-settle may resolve with a cancel result
|
|
1776
2099
|
// instead of throwing — still end the turn as aborted.
|
|
1777
2100
|
if (parentSignal.aborted && !stalledByWatchdog && !hardTimedOut) {
|
|
1778
|
-
|
|
2101
|
+
const result = {
|
|
2102
|
+
ok: false,
|
|
2103
|
+
output: "Cancelled by user.",
|
|
2104
|
+
exitCode: 130,
|
|
2105
|
+
};
|
|
2106
|
+
emitToolResult(toolEventId, result, result.output);
|
|
1779
2107
|
return {
|
|
1780
2108
|
ok: false,
|
|
1781
2109
|
call,
|
|
1782
|
-
result
|
|
1783
|
-
contextOutput:
|
|
1784
|
-
|
|
2110
|
+
result,
|
|
2111
|
+
contextOutput: result.output,
|
|
2112
|
+
aborted: true,
|
|
1785
2113
|
};
|
|
1786
2114
|
}
|
|
1787
2115
|
if (liveBytes > 0) {
|
|
@@ -1793,13 +2121,18 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1793
2121
|
jobManager.updateJobStatus(jobId, "failed", 1);
|
|
1794
2122
|
if (isAbortError(toolError, toolAc.signal) || forceSettled) {
|
|
1795
2123
|
if (parentSignal.aborted && !stalledByWatchdog && !hardTimedOut) {
|
|
1796
|
-
|
|
2124
|
+
const result = {
|
|
2125
|
+
ok: false,
|
|
2126
|
+
output: "Cancelled by user.",
|
|
2127
|
+
exitCode: 130,
|
|
2128
|
+
};
|
|
2129
|
+
emitToolResult(toolEventId, result, result.output);
|
|
1797
2130
|
return {
|
|
1798
2131
|
ok: false,
|
|
1799
2132
|
call,
|
|
1800
|
-
result
|
|
1801
|
-
contextOutput:
|
|
1802
|
-
|
|
2133
|
+
result,
|
|
2134
|
+
contextOutput: result.output,
|
|
2135
|
+
aborted: true,
|
|
1803
2136
|
};
|
|
1804
2137
|
}
|
|
1805
2138
|
result = {
|
|
@@ -1903,6 +2236,27 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1903
2236
|
writeNotice("info", `project root → ${fromScaffold}`, chalk.dim(` ℹ project root set to ${fromScaffold}\n`));
|
|
1904
2237
|
}
|
|
1905
2238
|
}
|
|
2239
|
+
if (delegation?.taskId && !result.backgroundJob) {
|
|
2240
|
+
// The delegation never became a durable job: settle its child instead of
|
|
2241
|
+
// Leaving a permanently yellow subtask behind.
|
|
2242
|
+
const delegationId = delegation.id;
|
|
2243
|
+
const settledState = result.ok ? "skipped" : "failed";
|
|
2244
|
+
const settlement = await mutatePlan(session.sessionId, (draft) => {
|
|
2245
|
+
const child = draft.tasks.find((task) => task.delegationId === delegationId);
|
|
2246
|
+
if (!child)
|
|
2247
|
+
return false;
|
|
2248
|
+
child.state = settledState;
|
|
2249
|
+
child.note = result.ok
|
|
2250
|
+
? `delegation=${delegationId} ran in the foreground; no durable job was created`
|
|
2251
|
+
: `delegation=${delegationId} failed to launch`;
|
|
2252
|
+
return true;
|
|
2253
|
+
}).catch(() => undefined);
|
|
2254
|
+
if (settlement?.ok && settlement.plan) {
|
|
2255
|
+
pendingSessionStatePlan = settlement.plan;
|
|
2256
|
+
writePlanUpdate(settlement.plan, renderPlanForTerminal(settlement.plan) + "\n");
|
|
2257
|
+
}
|
|
2258
|
+
delegation = undefined;
|
|
2259
|
+
}
|
|
1906
2260
|
if (result.backgroundJob) {
|
|
1907
2261
|
const durableJob = jobManager.getJob(result.backgroundJob.id);
|
|
1908
2262
|
// Responder linkage is opt-in: only jobs launched with responder:true
|
|
@@ -1910,9 +2264,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1910
2264
|
// Plain background jobs stay pollable (shell.jobs/shell.tail) as before.
|
|
1911
2265
|
if (durableJob?.responder) {
|
|
1912
2266
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1913
|
-
let linkedTaskId =
|
|
2267
|
+
let linkedTaskId = delegation?.taskId;
|
|
1914
2268
|
let linkedParentTaskId;
|
|
1915
2269
|
let responderTaskId;
|
|
2270
|
+
let responderChildId;
|
|
1916
2271
|
if (durableJob && livePlan) {
|
|
1917
2272
|
const existing = livePlan.tasks.find((task) => task.jobId === durableJob.id);
|
|
1918
2273
|
const parentTaskId = dispatchedTaskId && livePlan.tasks.some((task) => task.id === dispatchedTaskId)
|
|
@@ -1927,46 +2282,64 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1927
2282
|
: "in_progress";
|
|
1928
2283
|
const note = `job=${durableJob.id} pid=${durableJob.pid ?? "?"} status=${durableJob.status} ` +
|
|
1929
2284
|
`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
|
-
|
|
2285
|
+
const responderTitle = `Responder · ${durableJob.name ?? durableJob.commandDisplay.slice(0, 96)}`;
|
|
2286
|
+
// Upsert the child by delegation/job identity
|
|
2287
|
+
// inside the transactional boundary. A concurrent settlement that
|
|
2288
|
+
// already turned the child green is therefore never reverted, and the
|
|
2289
|
+
// child is never written as the foreground parent.
|
|
2290
|
+
const upsert = await mutatePlan(session.sessionId, (draft) => {
|
|
2291
|
+
const target = (durableJob.delegationId
|
|
2292
|
+
? draft.tasks.find((task) => task.delegationId === durableJob.delegationId)
|
|
2293
|
+
: undefined) ??
|
|
2294
|
+
draft.tasks.find((task) => task.jobId === durableJob.id);
|
|
2295
|
+
const child = target ??
|
|
2296
|
+
appendPlanTask(draft, {
|
|
2297
|
+
title: responderTitle,
|
|
2298
|
+
state: terminalState,
|
|
2299
|
+
note,
|
|
2300
|
+
dependencies: [],
|
|
2301
|
+
resourceLocks: [],
|
|
2302
|
+
parentTaskId,
|
|
2303
|
+
jobId: durableJob.id,
|
|
2304
|
+
processId: durableJob.pid,
|
|
2305
|
+
responderOwned: true,
|
|
2306
|
+
...(durableJob.delegationId
|
|
2307
|
+
? { delegationId: durableJob.delegationId }
|
|
2308
|
+
: {}),
|
|
2309
|
+
});
|
|
2310
|
+
// Never regress a child that process settlement already finished.
|
|
2311
|
+
const settledTerminal = child.state === "done" || child.state === "failed";
|
|
2312
|
+
if (!settledTerminal) {
|
|
2313
|
+
child.state = terminalState;
|
|
2314
|
+
child.note = note;
|
|
2315
|
+
}
|
|
2316
|
+
child.jobId = durableJob.id;
|
|
2317
|
+
child.processId = durableJob.pid;
|
|
2318
|
+
child.responderOwned = true;
|
|
2319
|
+
if (durableJob.delegationId) {
|
|
2320
|
+
child.delegationId = durableJob.delegationId;
|
|
2321
|
+
}
|
|
2322
|
+
if (parentTaskId)
|
|
2323
|
+
child.parentTaskId = parentTaskId;
|
|
2324
|
+
if (isPlanTerminal(draft)) {
|
|
2325
|
+
draft.status = isPlanSuccessful(draft) ? "completed" : "abandoned";
|
|
2326
|
+
}
|
|
2327
|
+
else if (draft.status !== "draft") {
|
|
2328
|
+
draft.status = "in_progress";
|
|
2329
|
+
}
|
|
2330
|
+
responderChildId = child.id;
|
|
2331
|
+
return true;
|
|
2332
|
+
}).catch(() => undefined);
|
|
2333
|
+
if (!upsert?.ok || !responderChildId) {
|
|
1962
2334
|
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
2335
|
}
|
|
1964
|
-
|
|
1965
|
-
linkedTaskId =
|
|
2336
|
+
else {
|
|
2337
|
+
linkedTaskId = responderChildId;
|
|
1966
2338
|
linkedParentTaskId = parentTaskId;
|
|
1967
|
-
responderTaskId =
|
|
1968
|
-
pendingSessionStatePlan = livePlan;
|
|
1969
|
-
|
|
2339
|
+
responderTaskId = responderChildId;
|
|
2340
|
+
pendingSessionStatePlan = upsert.plan ?? livePlan;
|
|
2341
|
+
const rendered = upsert.plan ?? livePlan;
|
|
2342
|
+
writePlanUpdate(rendered, renderPlanForTerminal(rendered) + "\n");
|
|
1970
2343
|
}
|
|
1971
2344
|
}
|
|
1972
2345
|
if (durableJob) {
|
|
@@ -2051,13 +2424,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2051
2424
|
}
|
|
2052
2425
|
await saveEngagement(engagementGraph);
|
|
2053
2426
|
}
|
|
2427
|
+
workLedger.recordToolCall(call, result.ok, savedOutputPath);
|
|
2428
|
+
const completedProbeState = probeStateKey(call);
|
|
2054
2429
|
const newEvidence = recordToolEvidence(outcomeState, {
|
|
2055
2430
|
tool: call.name,
|
|
2056
2431
|
callId: toolEventId,
|
|
2057
2432
|
ok: result.ok,
|
|
2433
|
+
...(result.exitCode !== undefined ? { exitCode: result.exitCode } : {}),
|
|
2058
2434
|
output: result.output,
|
|
2059
2435
|
...(savedOutputPath ? { artifact: savedOutputPath } : {}),
|
|
2060
2436
|
...(dispatchedTaskId ? { taskId: dispatchedTaskId } : {}),
|
|
2437
|
+
...(completedProbeState ? { stateKey: completedProbeState } : {}),
|
|
2061
2438
|
args: call.args,
|
|
2062
2439
|
});
|
|
2063
2440
|
let hypothesisDelta = 0;
|
|
@@ -2130,7 +2507,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2130
2507
|
}
|
|
2131
2508
|
}
|
|
2132
2509
|
await saveOutcomeState(outcomeState);
|
|
2133
|
-
loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode, result.output);
|
|
2510
|
+
loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode, result.output, completedProbeState ? { stateKey: completedProbeState } : undefined);
|
|
2134
2511
|
// Evidence for verify-before-done: only successful real work counts.
|
|
2135
2512
|
if (result.ok && isEvidenceWorkTool(call.name)) {
|
|
2136
2513
|
const liveAfter = await loadPlan(session.sessionId).catch(() => undefined);
|
|
@@ -2202,7 +2579,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2202
2579
|
taskWorkLedger.successWorkCount < absorbed.successWorkCount) {
|
|
2203
2580
|
taskWorkLedger = absorbed;
|
|
2204
2581
|
}
|
|
2205
|
-
await
|
|
2582
|
+
await persistTaskEvidence(task.id, task.evidence);
|
|
2206
2583
|
}
|
|
2207
2584
|
}
|
|
2208
2585
|
}
|
|
@@ -2211,7 +2588,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2211
2588
|
const task = liveAfter.tasks.find((candidate) => candidate.id === creditId);
|
|
2212
2589
|
if (task) {
|
|
2213
2590
|
task.evidence = taskEvidenceFromLedger(taskWorkLedger);
|
|
2214
|
-
await
|
|
2591
|
+
await persistTaskEvidence(task.id, task.evidence);
|
|
2215
2592
|
}
|
|
2216
2593
|
}
|
|
2217
2594
|
// Do NOT refreshSessionState here. executeSingleTool often finishes
|
|
@@ -2279,6 +2656,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2279
2656
|
// Align with /compact default: small recency + dense memory (not keepRecent=6 fat tails).
|
|
2280
2657
|
const AUTO_COMPACT_KEEP_RECENT = 2;
|
|
2281
2658
|
let lastCompactionMsgCount = 0;
|
|
2659
|
+
const compactionAttempts = new CompactionAttemptLedger();
|
|
2660
|
+
let activeCompactionId;
|
|
2282
2661
|
/** E5: identical tool bodies within this turn → pointer instead of re-append. */
|
|
2283
2662
|
const toolResultHashes = new Map();
|
|
2284
2663
|
/** E4: consecutive free-tier stream failures this turn. */
|
|
@@ -2288,8 +2667,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2288
2667
|
// once per turn — the recovery planner already narrates each retry, so
|
|
2289
2668
|
// repeating this on every failure just adds noise.
|
|
2290
2669
|
let freeTierAdvisoryShown = false;
|
|
2291
|
-
const summarizeForCompaction = async (summaryPrompt) => {
|
|
2292
|
-
const
|
|
2670
|
+
const summarizeForCompaction = async (summaryPrompt, stage) => {
|
|
2671
|
+
const streamFinalSummary = stage?.phase !== "map";
|
|
2672
|
+
const compactionId = streamFinalSummary ? activeCompactionId : undefined;
|
|
2673
|
+
const deltaParser = compactionId
|
|
2674
|
+
? createThinkingStreamParser((text) => writeCompactionDelta(compactionId, text), undefined, { remember: false })
|
|
2675
|
+
: undefined;
|
|
2676
|
+
const request = {
|
|
2293
2677
|
provider,
|
|
2294
2678
|
model,
|
|
2295
2679
|
messages: [
|
|
@@ -2297,11 +2681,32 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2297
2681
|
{ role: "user", content: summaryPrompt },
|
|
2298
2682
|
],
|
|
2299
2683
|
temperature: 0.1,
|
|
2300
|
-
// Prefer dense memory; enough room for findings without forcing a tiny stub.
|
|
2301
2684
|
maxTokens: 2_048,
|
|
2302
2685
|
signal: options.signal,
|
|
2686
|
+
};
|
|
2687
|
+
const response = await streamWithProvider(request, (token) => deltaParser?.push(token), { onStatus: () => undefined });
|
|
2688
|
+
deltaParser?.finish();
|
|
2689
|
+
const parsed = stripThinking(response.text);
|
|
2690
|
+
if (parsed.visible.trim() || !parsed.hasThinking)
|
|
2691
|
+
return response.text;
|
|
2692
|
+
const retry = await completeWithProvider({
|
|
2693
|
+
...request,
|
|
2694
|
+
messages: [
|
|
2695
|
+
{
|
|
2696
|
+
role: "system",
|
|
2697
|
+
content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only the continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
|
|
2698
|
+
},
|
|
2699
|
+
{ role: "user", content: summaryPrompt },
|
|
2700
|
+
],
|
|
2701
|
+
temperature: 0,
|
|
2702
|
+
maxTokens: 8_192,
|
|
2703
|
+
thinking: { enabled: false, effort: "none" },
|
|
2303
2704
|
});
|
|
2304
|
-
|
|
2705
|
+
const retryVisible = stripThinking(retry.text).visible.trim();
|
|
2706
|
+
if (retryVisible && compactionId) {
|
|
2707
|
+
writeCompactionDelta(compactionId, retryVisible);
|
|
2708
|
+
}
|
|
2709
|
+
return retry.text;
|
|
2305
2710
|
};
|
|
2306
2711
|
/**
|
|
2307
2712
|
* Estimate the complete next model request, including attached native-tool
|
|
@@ -2314,20 +2719,85 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2314
2719
|
const nextTools = selectToolDefs(native, useCompactSystemPrompt);
|
|
2315
2720
|
return buildContextBreakdown(contextMessages, nextTools).estimatedTotalTokens;
|
|
2316
2721
|
};
|
|
2722
|
+
/**
|
|
2723
|
+
* Canonical state that must survive compaction verbatim. Built from the
|
|
2724
|
+
* plan store, outcome contract, responder ledger and mutation ledger — never
|
|
2725
|
+
* from the narrative summary.
|
|
2726
|
+
*/
|
|
2727
|
+
async function buildTurnDurableEnvelope() {
|
|
2728
|
+
const plan = (await loadPlan(session.sessionId).catch(() => undefined)) ?? undefined;
|
|
2729
|
+
const root = getActiveProjectRoot() ?? plan?.meta?.projectRoot;
|
|
2730
|
+
const consumed = [];
|
|
2731
|
+
for (const message of messages) {
|
|
2732
|
+
if (!isResponderResultLedgerMessage(message))
|
|
2733
|
+
continue;
|
|
2734
|
+
for (const line of message.content.split("\n")) {
|
|
2735
|
+
const match = /notification=(\S+)/.exec(line);
|
|
2736
|
+
if (match?.[1])
|
|
2737
|
+
consumed.push(match[1]);
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
const unread = jobManager
|
|
2741
|
+
.getPendingNotifications(session.sessionId)
|
|
2742
|
+
.map((notification) => notification.id);
|
|
2743
|
+
const toEnvelopeJob = (job) => ({
|
|
2744
|
+
id: job.id,
|
|
2745
|
+
status: job.status,
|
|
2746
|
+
command: job.commandDisplay || job.command,
|
|
2747
|
+
...(job.taskId ? { taskId: job.taskId } : {}),
|
|
2748
|
+
...(job.stdoutArtifact ? { artifact: job.stdoutArtifact } : {}),
|
|
2749
|
+
});
|
|
2750
|
+
const liveJobs = jobManager
|
|
2751
|
+
.getRunningJobs(session.sessionId)
|
|
2752
|
+
.map(toEnvelopeJob);
|
|
2753
|
+
const liveIds = new Set(liveJobs.map((job) => job.id));
|
|
2754
|
+
const finishedJobs = jobManager
|
|
2755
|
+
.getRecentJobs(12, session.sessionId)
|
|
2756
|
+
.filter((job) => !liveIds.has(job.id))
|
|
2757
|
+
.map(toEnvelopeJob);
|
|
2758
|
+
return buildDurableEnvelope({
|
|
2759
|
+
...(plan ? { plan } : {}),
|
|
2760
|
+
outcome: outcomeState,
|
|
2761
|
+
ledger: workLedger,
|
|
2762
|
+
...(root ? { projectRoot: root } : {}),
|
|
2763
|
+
...(root
|
|
2764
|
+
? { packageManager: plan?.meta?.packageManager ?? detectPackageManager(root) }
|
|
2765
|
+
: {}),
|
|
2766
|
+
responder: { unread, consumed: [...new Set(consumed)] },
|
|
2767
|
+
...(liveJobs.length > 0 ? { liveJobs } : {}),
|
|
2768
|
+
...(finishedJobs.length > 0 ? { finishedJobs } : {}),
|
|
2769
|
+
});
|
|
2770
|
+
}
|
|
2317
2771
|
async function maybeAutoCompact(reason, force = false) {
|
|
2318
2772
|
const beforeTokens = estimateNextRequestTokens(messages);
|
|
2319
|
-
|
|
2320
|
-
const compactTrigger =
|
|
2773
|
+
const budget = resolveRequestBudget({ provider, model });
|
|
2774
|
+
const compactTrigger = budget.effectiveTrigger;
|
|
2321
2775
|
if (!force && beforeTokens < compactTrigger)
|
|
2322
2776
|
return;
|
|
2777
|
+
// Structural eligibility only: there must be closed history to summarize.
|
|
2323
2778
|
if (messages.length <= AUTO_COMPACT_KEEP_RECENT + 2)
|
|
2324
2779
|
return;
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2780
|
+
const durableEnvelope = await buildTurnDurableEnvelope();
|
|
2781
|
+
const attemptKey = compactionAttemptKey({
|
|
2782
|
+
messages,
|
|
2783
|
+
provider,
|
|
2784
|
+
model,
|
|
2785
|
+
dialect: toolDialect,
|
|
2786
|
+
triggerTokens: compactTrigger,
|
|
2787
|
+
schemaHash: toolSchemaHash(selectToolDefs(nativeToolsActive, useCompactSystemPrompt)),
|
|
2788
|
+
...(durableEnvelope ? { durableEnvelope } : {}),
|
|
2789
|
+
});
|
|
2790
|
+
if (!force && compactionAttempts.isSuppressed(attemptKey))
|
|
2328
2791
|
return;
|
|
2792
|
+
const compactionId = `compact-${randomUUID().slice(0, 12)}`;
|
|
2793
|
+
activeCompactionId = compactionId;
|
|
2794
|
+
writeCompactionStarted(compactionId, beforeTokens);
|
|
2329
2795
|
try {
|
|
2330
|
-
const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
|
|
2796
|
+
const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
|
|
2797
|
+
budgetTokens: 0,
|
|
2798
|
+
keepRecent: AUTO_COMPACT_KEEP_RECENT,
|
|
2799
|
+
...(durableEnvelope ? { durableEnvelope } : {}),
|
|
2800
|
+
});
|
|
2331
2801
|
const summaryBody = result.messages.find((m) => isCompactionMemoryMessage(m))?.content ??
|
|
2332
2802
|
"";
|
|
2333
2803
|
if (!shouldApplyAutoCompact({
|
|
@@ -2337,9 +2807,27 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2337
2807
|
afterTokens: result.afterTokens,
|
|
2338
2808
|
afterMessages: result.messages,
|
|
2339
2809
|
})) {
|
|
2810
|
+
writeCompactionFailed(compactionId, "The generated summary was not accepted; the original context was retained.", beforeTokens);
|
|
2811
|
+
return;
|
|
2812
|
+
}
|
|
2813
|
+
// otherwise the oversized request is sent anyway and corrective
|
|
2814
|
+
// compaction is suppressed as "already compacted".
|
|
2815
|
+
const candidateTokens = estimateNextRequestTokens(result.messages);
|
|
2816
|
+
if (!force && candidateTokens >= compactTrigger) {
|
|
2817
|
+
const dominant = describeDominantContextBlock(result.messages);
|
|
2818
|
+
compactionAttempts.recordFailure(attemptKey);
|
|
2819
|
+
await auditLog("agent.compact.overflow", {
|
|
2820
|
+
reason,
|
|
2821
|
+
candidateTokens,
|
|
2822
|
+
trigger: compactTrigger,
|
|
2823
|
+
dominant,
|
|
2824
|
+
});
|
|
2825
|
+
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`));
|
|
2826
|
+
writeCompactionFailed(compactionId, `Summary remained over the context limit; largest block: ${dominant}.`, beforeTokens);
|
|
2340
2827
|
return;
|
|
2341
2828
|
}
|
|
2342
2829
|
messages.splice(0, messages.length, ...result.messages);
|
|
2830
|
+
compactionAttempts.recordSuccess(attemptKey);
|
|
2343
2831
|
loopGuard.resetReadOnly();
|
|
2344
2832
|
// Token stats use the same complete request estimate as the trigger.
|
|
2345
2833
|
const compactedTokens = estimateNextRequestTokens(messages);
|
|
@@ -2347,10 +2835,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2347
2835
|
// after older turns (which carried the plan context) were summarized.
|
|
2348
2836
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
2349
2837
|
if (livePlan) {
|
|
2350
|
-
messages.
|
|
2351
|
-
role: "system",
|
|
2352
|
-
content: planContextMessage(livePlan, session.planApproved.value),
|
|
2353
|
-
});
|
|
2838
|
+
upsertPlanContextMessage(messages, planContextMessage(livePlan, session.planApproved.value));
|
|
2354
2839
|
}
|
|
2355
2840
|
// Re-inject live SESSION STATE after compaction (older flags survive).
|
|
2356
2841
|
refreshSessionState(livePlan);
|
|
@@ -2371,22 +2856,25 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2371
2856
|
? PLAN_IMPLEMENT_MEMORY_PREFIX
|
|
2372
2857
|
: COMPACTION_MEMORY_PREFIX);
|
|
2373
2858
|
// Card shows pre/post of the summarization; plan re-injection is noted.
|
|
2374
|
-
|
|
2859
|
+
writeCompactionCompleted(compactionId, summaryText, beforeTokens, compactedTokens);
|
|
2375
2860
|
const planNote = afterTokens > compactedTokens
|
|
2376
2861
|
? ` (compacted to ~${compactedTokens.toLocaleString()}, +plan → ~${afterTokens.toLocaleString()})`
|
|
2377
2862
|
: "";
|
|
2378
2863
|
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
2864
|
}
|
|
2380
2865
|
catch (error) {
|
|
2866
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2867
|
+
writeCompactionFailed(compactionId, /aborted/i.test(message) ? "Compaction was cancelled." : message, beforeTokens);
|
|
2381
2868
|
if (error instanceof Error &&
|
|
2382
2869
|
(error.name === "AbortError" || error.message.includes("aborted"))) {
|
|
2383
2870
|
throw error;
|
|
2384
2871
|
}
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2872
|
+
compactionAttempts.recordFailure(attemptKey);
|
|
2873
|
+
await auditLog("agent.compact.failed", { reason: message });
|
|
2874
|
+
}
|
|
2875
|
+
finally {
|
|
2876
|
+
if (activeCompactionId === compactionId)
|
|
2877
|
+
activeCompactionId = undefined;
|
|
2390
2878
|
}
|
|
2391
2879
|
}
|
|
2392
2880
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
@@ -2549,16 +3037,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2549
3037
|
});
|
|
2550
3038
|
try {
|
|
2551
3039
|
if (responderDelivery &&
|
|
2552
|
-
!jobManager.
|
|
3040
|
+
!jobManager.markDeliveryStarted(responderDelivery.id, session.sessionId)) {
|
|
2553
3041
|
jobManager.releaseResponderNotificationClaim(responderDelivery.id);
|
|
2554
|
-
throw new Error(`failed to
|
|
3042
|
+
throw new Error(`failed to record responder delivery attempt ${responderDelivery.id}`);
|
|
2555
3043
|
}
|
|
2556
3044
|
completion = await streamWithProvider({
|
|
2557
3045
|
provider,
|
|
2558
3046
|
model,
|
|
2559
3047
|
allowModelFallback,
|
|
2560
3048
|
messages,
|
|
2561
|
-
|
|
3049
|
+
// Sampling is provider/model policy (llm/sampling.ts).
|
|
3050
|
+
// Sending a fixed 0.2 here overrode it for every model.
|
|
2562
3051
|
maxTokens: stepMaxTokens,
|
|
2563
3052
|
signal: options.signal,
|
|
2564
3053
|
thinking: retryWithoutThinking
|
|
@@ -2737,6 +3226,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2737
3226
|
throw streamError;
|
|
2738
3227
|
}
|
|
2739
3228
|
recordRecoveryAttempt(recoveryState, failureKind);
|
|
3229
|
+
// The router refuses transparent retries after emission,
|
|
3230
|
+
// but the recovery ladder may still re-run the step. Say so, since
|
|
3231
|
+
// the visible answer restarts from scratch.
|
|
3232
|
+
if (streamAlreadyEmitted(streamError)) {
|
|
3233
|
+
const restartNotice = "partial answer discarded after a mid-stream failure — the reply restarts below";
|
|
3234
|
+
writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
|
|
3235
|
+
}
|
|
2740
3236
|
if (plan.notice) {
|
|
2741
3237
|
writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
|
|
2742
3238
|
}
|
|
@@ -2764,6 +3260,14 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2764
3260
|
// Always clear the spinner — abort, network error, or success.
|
|
2765
3261
|
spinner.stop();
|
|
2766
3262
|
}
|
|
3263
|
+
if (responderDelivery) {
|
|
3264
|
+
// The result text is now part of this turn, so consumption is durable.
|
|
3265
|
+
// A stream that aborted or threw above never reaches this point and the
|
|
3266
|
+
// receipt stays deliverable.
|
|
3267
|
+
if (!jobManager.markDelivered(responderDelivery.id, session.sessionId)) {
|
|
3268
|
+
jobManager.releaseResponderNotificationClaim(responderDelivery.id);
|
|
3269
|
+
}
|
|
3270
|
+
}
|
|
2767
3271
|
provider = completion.provider;
|
|
2768
3272
|
model = completion.model;
|
|
2769
3273
|
if (completion.usage) {
|
|
@@ -2772,6 +3276,22 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2772
3276
|
usage: completion.usage,
|
|
2773
3277
|
model: completion.model,
|
|
2774
3278
|
});
|
|
3279
|
+
// Cache telemetry: without read/create counts there is no way to tell
|
|
3280
|
+
// whether the stable prefix is actually being reused.
|
|
3281
|
+
const cacheRead = completion.usage.cachedPromptTokens ?? 0;
|
|
3282
|
+
const cacheCreated = completion.usage.cacheCreationTokens ?? 0;
|
|
3283
|
+
if (cacheRead > 0 || cacheCreated > 0) {
|
|
3284
|
+
await auditLog("agent.prompt.cache", {
|
|
3285
|
+
provider: completion.provider,
|
|
3286
|
+
model: completion.model,
|
|
3287
|
+
promptTokens: completion.usage.promptTokens,
|
|
3288
|
+
cacheReadTokens: cacheRead,
|
|
3289
|
+
cacheCreationTokens: cacheCreated,
|
|
3290
|
+
hitRatio: completion.usage.promptTokens > 0
|
|
3291
|
+
? Number((cacheRead / completion.usage.promptTokens).toFixed(3))
|
|
3292
|
+
: 0,
|
|
3293
|
+
});
|
|
3294
|
+
}
|
|
2775
3295
|
}
|
|
2776
3296
|
deltaParser?.finish();
|
|
2777
3297
|
// Sticky text-only may have flipped dialect during stream retry.
|
|
@@ -2903,25 +3423,28 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2903
3423
|
truncatedToolRetries += 1;
|
|
2904
3424
|
if (truncatedToolRetries <= 5) {
|
|
2905
3425
|
try {
|
|
2906
|
-
const writeResult = await
|
|
3426
|
+
const writeResult = await applySalvagedWrite(salvaged);
|
|
2907
3427
|
if (writeResult.ok) {
|
|
2908
3428
|
const lineCount = salvaged.content.split("\n").length;
|
|
2909
3429
|
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
3430
|
// Pair assistant tool_calls with synthetic results so the
|
|
2911
3431
|
// next turn is not orphaned, then nudge for append.
|
|
2912
|
-
appendAssistantWithTools(messages, assistantText.visible, nativeToolCalls);
|
|
3432
|
+
appendAssistantWithTools(messages, assistantText.visible, nativeToolCalls, completion.reasoningBlock);
|
|
2913
3433
|
for (const tc of nativeToolCalls) {
|
|
2914
3434
|
appendToolResult(messages, tc.id, tc.id === writeTc.id
|
|
2915
3435
|
? `Tool ${tc.name} result (exit=0, ok=true):\nSalvaged partial write: ${lineCount} lines to ${salvaged.path}`
|
|
2916
3436
|
: `Tool ${tc.name} result (exit=1, ok=false):\nCancelled — sibling write was truncated and salvaged.`, tc.name, tc.id === writeTc.id);
|
|
2917
3437
|
}
|
|
2918
|
-
const priorBytes =
|
|
3438
|
+
const priorBytes = writeResult.bytesOnDisk;
|
|
3439
|
+
const salvagedToolName = salvaged.operation === "append"
|
|
3440
|
+
? "fs.append"
|
|
3441
|
+
: "fs.write";
|
|
2919
3442
|
const appendNudge = toolsAttached
|
|
2920
|
-
? `Your ${
|
|
3443
|
+
? `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
3444
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
2922
3445
|
`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
3446
|
`Do not re-read the full file; do not re-send content already saved. Use the platform tool interface — no markdown fences.`
|
|
2924
|
-
: `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}. ` +
|
|
2925
3448
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
2926
3449
|
`CONTINUE with ONE large fs.append of the remaining content:\n` +
|
|
2927
3450
|
'```tool\n{"name":"fs.append","args":{"path":' +
|
|
@@ -3055,23 +3578,23 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3055
3578
|
truncatedToolRetries += 1;
|
|
3056
3579
|
const salvaged = salvageTruncatedWrite(assistantText.visible);
|
|
3057
3580
|
if (salvaged && truncatedToolRetries <= 5) {
|
|
3058
|
-
// Write the salvaged partial content
|
|
3581
|
+
// Write the salvaged partial content through the normal
|
|
3582
|
+
// authorization path (classifier + confirmation + receipt).
|
|
3059
3583
|
try {
|
|
3060
|
-
const writeResult = await
|
|
3061
|
-
confirmed: true,
|
|
3062
|
-
});
|
|
3584
|
+
const writeResult = await applySalvagedWrite(salvaged);
|
|
3063
3585
|
if (writeResult.ok) {
|
|
3064
3586
|
const lineCount = salvaged.content.split("\n").length;
|
|
3065
3587
|
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
3588
|
pushAssistantHistory(stripThinking(assistantText.visible).visible);
|
|
3067
|
-
const priorBytes =
|
|
3589
|
+
const priorBytes = writeResult.bytesOnDisk;
|
|
3590
|
+
const salvagedToolName = salvaged.operation === "append" ? "fs.append" : "fs.write";
|
|
3068
3591
|
messages.push({
|
|
3069
3592
|
role: "user",
|
|
3070
3593
|
content: toolsAttached
|
|
3071
|
-
? `Your
|
|
3594
|
+
? `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
3595
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
3073
3596
|
`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
|
|
3597
|
+
: `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
3598
|
`The file ends with: ${JSON.stringify(salvaged.lastLine)}\n\n` +
|
|
3076
3599
|
`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
3600
|
'```tool\n{"name":"fs.append","args":{"path":' +
|
|
@@ -3120,9 +3643,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3120
3643
|
const salvaged = salvageTruncatedWrite(assistantText.visible);
|
|
3121
3644
|
if (salvaged) {
|
|
3122
3645
|
try {
|
|
3123
|
-
const writeResult = await
|
|
3124
|
-
confirmed: true,
|
|
3125
|
-
});
|
|
3646
|
+
const writeResult = await applySalvagedWrite(salvaged);
|
|
3126
3647
|
if (writeResult.ok) {
|
|
3127
3648
|
const lineCount = salvaged.content.split("\n").length;
|
|
3128
3649
|
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 +3690,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3169
3690
|
const unread = [...unreadResponderNotificationIds];
|
|
3170
3691
|
pushAssistantHistory(assistantText.visible);
|
|
3171
3692
|
messages.push(recoveryUserMessage(`You have ${unread.length} delivered Responder result(s) that remain unread: ${unread.join(", ")}. ` +
|
|
3172
|
-
"If analysis is incomplete, call the
|
|
3693
|
+
"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
3694
|
continue;
|
|
3174
3695
|
}
|
|
3175
3696
|
const narratedAction = looksLikeActionNarration(cleaned);
|
|
@@ -3348,7 +3869,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3348
3869
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
3349
3870
|
const unfinished = livePlan?.tasks.filter((task) => !task.responderOwned &&
|
|
3350
3871
|
(task.state === "pending" || task.state === "in_progress"));
|
|
3351
|
-
const deferReport =
|
|
3872
|
+
const deferReport = shouldYieldForDeclaredResponderDependency(livePlan, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId);
|
|
3352
3873
|
if (livePlan &&
|
|
3353
3874
|
unfinished &&
|
|
3354
3875
|
unfinished.length > 0 &&
|
|
@@ -3370,9 +3891,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3370
3891
|
const remainingCriteria = [];
|
|
3371
3892
|
if (session.planApproved.value) {
|
|
3372
3893
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
3373
|
-
|
|
3374
|
-
|
|
3894
|
+
// Foreground work decides the turn outcome. Responder children run
|
|
3895
|
+
// concurrently by design and are reported separately.
|
|
3896
|
+
const unfinished = livePlan ? foregroundRemaining(livePlan) : [];
|
|
3897
|
+
const failedTasks = livePlan?.tasks.filter((task) => !task.responderOwned && task.state === "failed") ?? [];
|
|
3375
3898
|
remainingCriteria.push(...unfinished.map((task) => `[${task.id}] ${task.title}`), ...failedTasks.map((task) => `[${task.id}] retry failed task: ${task.title}`));
|
|
3899
|
+
const openResponderChildren = livePlan
|
|
3900
|
+
? responderOpenTasks(livePlan)
|
|
3901
|
+
: [];
|
|
3902
|
+
if (openResponderChildren.length > 0) {
|
|
3903
|
+
remainingCriteria.push(...openResponderChildren.map((task) => `[${task.id}] responder result awaiting analysis: ${task.title}`));
|
|
3904
|
+
}
|
|
3376
3905
|
if (failedTasks.length > 0)
|
|
3377
3906
|
outcomeStatus = "failed";
|
|
3378
3907
|
else if (unfinished.length > 0)
|
|
@@ -3514,7 +4043,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3514
4043
|
writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
|
|
3515
4044
|
}
|
|
3516
4045
|
if (historyNativeCalls.length) {
|
|
3517
|
-
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls);
|
|
4046
|
+
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock);
|
|
3518
4047
|
}
|
|
3519
4048
|
else {
|
|
3520
4049
|
const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
|
|
@@ -3563,7 +4092,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3563
4092
|
if (res.ok && res.call.name === "plan.create") {
|
|
3564
4093
|
planCreatedThisTurn = true;
|
|
3565
4094
|
}
|
|
3566
|
-
|
|
4095
|
+
if (!res.suppressedRepeat)
|
|
4096
|
+
productiveSteps += 1;
|
|
3567
4097
|
// E5: collapse identical large tool bodies within this turn to a pointer.
|
|
3568
4098
|
const deduped = dedupeToolContextOutput({
|
|
3569
4099
|
content: res.contextOutput,
|
|
@@ -3728,7 +4258,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3728
4258
|
}
|
|
3729
4259
|
// User Esc/Ctrl+C only — never cancel siblings because a delete failed
|
|
3730
4260
|
// or a confirm was declined; the model must see every tool result.
|
|
3731
|
-
if (res.
|
|
4261
|
+
if (res.aborted)
|
|
3732
4262
|
aborted = true;
|
|
3733
4263
|
};
|
|
3734
4264
|
// Multi-task sync guard: when one model message advances more than one
|
|
@@ -3749,7 +4279,24 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3749
4279
|
: parsed.taskId;
|
|
3750
4280
|
intents.push({ call: b.call, taskId: resolvedId, state: parsed.state });
|
|
3751
4281
|
}
|
|
3752
|
-
|
|
4282
|
+
const openIds = openingTaskIds(intents);
|
|
4283
|
+
if (openIds.length > 1) {
|
|
4284
|
+
// A multi-open is never confirmable; the store keeps at
|
|
4285
|
+
// most one active foreground task.
|
|
4286
|
+
session.pendingTaskBatch.value = undefined;
|
|
4287
|
+
const openDescriptors = openIds.map((taskId) => ({
|
|
4288
|
+
taskId,
|
|
4289
|
+
title: livePlanForBatch?.tasks.find((t) => t.id === taskId)?.title ?? "",
|
|
4290
|
+
targetState: "in_progress",
|
|
4291
|
+
}));
|
|
4292
|
+
batchReminderNote = buildMultiOpenRejection(openDescriptors);
|
|
4293
|
+
for (const intent of intents) {
|
|
4294
|
+
if (intent.state === "in_progress")
|
|
4295
|
+
batchRemindCalls.add(intent.call);
|
|
4296
|
+
}
|
|
4297
|
+
writeNotice("warn", multiOpenToast(openIds.length), chalk.yellow(` ⚠ ${multiOpenToast(openIds.length)}\n`));
|
|
4298
|
+
}
|
|
4299
|
+
else if (isSimultaneousTaskAdvance(intents)) {
|
|
3753
4300
|
const signature = batchUpdateSignature(intents);
|
|
3754
4301
|
if (session.pendingTaskBatch.value === signature) {
|
|
3755
4302
|
session.pendingTaskBatch.value = undefined;
|
|
@@ -3870,7 +4417,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3870
4417
|
return finishTurn("", productiveSteps, "partial", ["Approve or revise the draft plan before implementation."]);
|
|
3871
4418
|
}
|
|
3872
4419
|
if (aborted) {
|
|
3873
|
-
lastAnswer = "
|
|
4420
|
+
lastAnswer = "";
|
|
3874
4421
|
outcomeState.outcome.status = "aborted";
|
|
3875
4422
|
await saveOutcomeState(outcomeState);
|
|
3876
4423
|
moveTurn("aborted", "turn aborted");
|
|
@@ -3901,11 +4448,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3901
4448
|
}
|
|
3902
4449
|
catch (error) {
|
|
3903
4450
|
const isAbort = isAbortError(error, options.signal);
|
|
3904
|
-
const msg = isAbort ? "Aborted." : `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3905
4451
|
if (isAbort) {
|
|
3906
4452
|
writeAbort();
|
|
3907
|
-
return finishTurn(
|
|
4453
|
+
return finishTurn("", 0, "aborted");
|
|
3908
4454
|
}
|
|
4455
|
+
releaseUnreadResponderClaims();
|
|
4456
|
+
const msg = `Error: ${error instanceof Error ? error.message : String(error)}`;
|
|
3909
4457
|
if (options.onMessages) {
|
|
3910
4458
|
try {
|
|
3911
4459
|
options.onMessages(buildTurnHistory(liveMessages, msg));
|