@pentoshi/clai 4.1.0 → 4.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -0
- package/dist/agent/compaction-executor.d.ts +68 -0
- package/dist/agent/compaction-executor.js +343 -0
- package/dist/agent/compaction-executor.js.map +1 -0
- package/dist/agent/context-breakdown.js +2 -0
- package/dist/agent/context-breakdown.js.map +1 -1
- package/dist/agent/context-manager.d.ts +23 -9
- package/dist/agent/context-manager.js +72 -43
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +3 -0
- package/dist/agent/loop-guard.d.ts +2 -0
- package/dist/agent/loop-guard.js +48 -3
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/prompt-composer.js +6 -6
- package/dist/agent/prompt-composer.js.map +1 -1
- package/dist/agent/reliability-policy.d.ts +1 -0
- package/dist/agent/reliability-policy.js +6 -3
- package/dist/agent/reliability-policy.js.map +1 -1
- package/dist/agent/request-accounting.d.ts +97 -0
- package/dist/agent/request-accounting.js +207 -0
- package/dist/agent/request-accounting.js.map +1 -0
- package/dist/agent/request-budget.d.ts +2 -2
- package/dist/agent/request-budget.js +2 -4
- package/dist/agent/request-budget.js.map +1 -1
- package/dist/agent/runner.d.ts +8 -2
- package/dist/agent/runner.js +409 -178
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/session-title.d.ts +0 -26
- package/dist/agent/session-title.js +0 -80
- package/dist/agent/session-title.js.map +1 -1
- package/dist/agent/tool-history.d.ts +2 -2
- package/dist/agent/tool-history.js +19 -6
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/app/adapters/agent-event-adapter.js +19 -0
- package/dist/app/adapters/agent-event-adapter.js.map +1 -1
- package/dist/app/adapters/current-agent-adapter.js +4 -0
- package/dist/app/adapters/current-agent-adapter.js.map +1 -1
- package/dist/app/commands/catalog.js +24 -0
- package/dist/app/commands/catalog.js.map +1 -1
- package/dist/app/controllers/cancel-coordinator.d.ts +51 -0
- package/dist/app/controllers/cancel-coordinator.js +48 -0
- package/dist/app/controllers/cancel-coordinator.js.map +1 -0
- package/dist/app/controllers/session-compact-helper.d.ts +19 -2
- package/dist/app/controllers/session-compact-helper.js +100 -127
- package/dist/app/controllers/session-compact-helper.js.map +1 -1
- package/dist/app/controllers/session-context-usage.d.ts +32 -15
- package/dist/app/controllers/session-context-usage.js +186 -51
- package/dist/app/controllers/session-context-usage.js.map +1 -1
- package/dist/app/controllers/session-controller.d.ts +20 -11
- package/dist/app/controllers/session-controller.js +111 -90
- package/dist/app/controllers/session-controller.js.map +1 -1
- package/dist/app/controllers/session-naming.d.ts +31 -0
- package/dist/app/controllers/session-naming.js +173 -0
- package/dist/app/controllers/session-naming.js.map +1 -0
- package/dist/app/controllers/session-persistence.d.ts +2 -2
- package/dist/app/controllers/session-persistence.js +3 -6
- package/dist/app/controllers/session-persistence.js.map +1 -1
- package/dist/app/controllers/session-turn-request.d.ts +2 -1
- package/dist/app/controllers/session-turn-request.js +3 -0
- package/dist/app/controllers/session-turn-request.js.map +1 -1
- package/dist/app/controllers/turn-controller.d.ts +2 -1
- package/dist/app/controllers/turn-controller.js +1 -0
- package/dist/app/controllers/turn-controller.js.map +1 -1
- package/dist/app/events/app-event.d.ts +9 -0
- package/dist/app/events/app-event.js.map +1 -1
- package/dist/app/ports/agent-port.d.ts +8 -1
- package/dist/classic/app/ClassicApp.js +2 -1
- package/dist/classic/app/ClassicApp.js.map +1 -1
- package/dist/classic/app/app-wiring.js +1 -2
- package/dist/classic/app/app-wiring.js.map +1 -1
- package/dist/classic/app/wiring-interactions.js +2 -7
- package/dist/classic/app/wiring-interactions.js.map +1 -1
- package/dist/classic/blocks/tool-lines.js +2 -1
- package/dist/classic/blocks/tool-lines.js.map +1 -1
- package/dist/classic/input/cancel-ladder.d.ts +2 -19
- package/dist/classic/input/cancel-ladder.js +13 -20
- package/dist/classic/input/cancel-ladder.js.map +1 -1
- package/dist/commands/providers.js +2 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/llm/adapters/anthropic-tools.d.ts +31 -14
- package/dist/llm/adapters/anthropic-tools.js +124 -29
- package/dist/llm/adapters/anthropic-tools.js.map +1 -1
- package/dist/llm/adapters/gemini-tools.d.ts +16 -4
- package/dist/llm/adapters/gemini-tools.js +117 -16
- package/dist/llm/adapters/gemini-tools.js.map +1 -1
- package/dist/llm/adapters/openai-tools.d.ts +13 -2
- package/dist/llm/adapters/openai-tools.js +58 -11
- package/dist/llm/adapters/openai-tools.js.map +1 -1
- package/dist/llm/agentrouter.js +68 -45
- package/dist/llm/agentrouter.js.map +1 -1
- package/dist/llm/anthropic.js +69 -36
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +70 -32
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/bynara.js +26 -69
- package/dist/llm/bynara.js.map +1 -1
- package/dist/llm/capabilities.d.ts +2 -1
- package/dist/llm/capabilities.js +47 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/context-snapshot.d.ts +100 -0
- package/dist/llm/context-snapshot.js +277 -0
- package/dist/llm/context-snapshot.js.map +1 -0
- package/dist/llm/context-windows.d.ts +10 -0
- package/dist/llm/context-windows.js +129 -0
- package/dist/llm/context-windows.js.map +1 -0
- package/dist/llm/custom-provider-profile.d.ts +75 -0
- package/dist/llm/custom-provider-profile.js +558 -0
- package/dist/llm/custom-provider-profile.js.map +1 -0
- package/dist/llm/custom-providers.d.ts +13 -1
- package/dist/llm/custom-providers.js +77 -25
- package/dist/llm/custom-providers.js.map +1 -1
- package/dist/llm/effort-fallback.d.ts +44 -0
- package/dist/llm/effort-fallback.js +101 -0
- package/dist/llm/effort-fallback.js.map +1 -0
- package/dist/llm/fireworks.js +4 -0
- package/dist/llm/fireworks.js.map +1 -1
- package/dist/llm/free.js +4 -0
- package/dist/llm/free.js.map +1 -1
- package/dist/llm/gemini.d.ts +1 -1
- package/dist/llm/gemini.js +89 -51
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.js +4 -0
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/hetzner.js +4 -0
- package/dist/llm/hetzner.js.map +1 -1
- package/dist/llm/http.d.ts +49 -10
- package/dist/llm/http.js +417 -153
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/kimchi.js +4 -0
- package/dist/llm/kimchi.js.map +1 -1
- package/dist/llm/lightning.js +4 -0
- package/dist/llm/lightning.js.map +1 -1
- package/dist/llm/meta.d.ts +2 -0
- package/dist/llm/meta.js +811 -655
- package/dist/llm/meta.js.map +1 -1
- package/dist/llm/modal.js +4 -0
- package/dist/llm/modal.js.map +1 -1
- package/dist/llm/model-families.d.ts +9 -0
- package/dist/llm/model-families.js +39 -0
- package/dist/llm/model-families.js.map +1 -0
- package/dist/llm/nvidia.js +4 -0
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +44 -40
- package/dist/llm/ollama.js.map +1 -1
- package/dist/llm/openai.js +4 -0
- package/dist/llm/openai.js.map +1 -1
- package/dist/llm/openrouter.js +4 -0
- package/dist/llm/openrouter.js.map +1 -1
- package/dist/llm/operation-ledger.d.ts +60 -0
- package/dist/llm/operation-ledger.js +136 -0
- package/dist/llm/operation-ledger.js.map +1 -0
- package/dist/llm/operation-usage.d.ts +32 -0
- package/dist/llm/operation-usage.js +172 -0
- package/dist/llm/operation-usage.js.map +1 -0
- package/dist/llm/orcarouter.d.ts +7 -0
- package/dist/llm/orcarouter.js +166 -0
- package/dist/llm/orcarouter.js.map +1 -0
- package/dist/llm/provider-profile-layers.d.ts +7 -0
- package/dist/llm/provider-profile-layers.js +961 -0
- package/dist/llm/provider-profile-layers.js.map +1 -0
- package/dist/llm/provider-profile.d.ts +181 -0
- package/dist/llm/provider-profile.js +329 -0
- package/dist/llm/provider-profile.js.map +1 -0
- package/dist/llm/provider-profiles.d.ts +13 -0
- package/dist/llm/provider-profiles.js +86 -0
- package/dist/llm/provider-profiles.js.map +1 -0
- package/dist/llm/provider.d.ts +2 -0
- package/dist/llm/provider.js +67 -0
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/qwen-cloud.js +4 -0
- package/dist/llm/qwen-cloud.js.map +1 -1
- package/dist/llm/reasoning-artifacts.d.ts +101 -0
- package/dist/llm/reasoning-artifacts.js +440 -0
- package/dist/llm/reasoning-artifacts.js.map +1 -0
- package/dist/llm/request-fingerprint.d.ts +10 -0
- package/dist/llm/request-fingerprint.js +313 -0
- package/dist/llm/request-fingerprint.js.map +1 -0
- package/dist/llm/request-plan.d.ts +119 -0
- package/dist/llm/request-plan.js +279 -0
- package/dist/llm/request-plan.js.map +1 -0
- package/dist/llm/router.d.ts +19 -1
- package/dist/llm/router.js +447 -75
- package/dist/llm/router.js.map +1 -1
- package/dist/llm/stream-events.d.ts +47 -0
- package/dist/llm/stream-events.js +55 -0
- package/dist/llm/stream-events.js.map +1 -0
- package/dist/llm/stream-terminal.d.ts +30 -0
- package/dist/llm/stream-terminal.js +51 -0
- package/dist/llm/stream-terminal.js.map +1 -0
- package/dist/llm/token-estimate-calibration.d.ts +23 -0
- package/dist/llm/token-estimate-calibration.js +140 -0
- package/dist/llm/token-estimate-calibration.js.map +1 -0
- package/dist/llm/token-usage.d.ts +35 -5
- package/dist/llm/token-usage.js +212 -162
- package/dist/llm/token-usage.js.map +1 -1
- package/dist/llm/tokenrouter.js +8 -2
- package/dist/llm/tokenrouter.js.map +1 -1
- package/dist/modes/ask.js +11 -5
- package/dist/modes/ask.js.map +1 -1
- package/dist/noninteractive/start-noninteractive.js +31 -2
- package/dist/noninteractive/start-noninteractive.js.map +1 -1
- package/dist/noninteractive/stream-blocks.js +15 -4
- package/dist/noninteractive/stream-blocks.js.map +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/system.agent.md +6 -0
- package/dist/store/config.js +5 -1
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.d.ts +2 -0
- package/dist/store/history.js +19 -10
- package/dist/store/history.js.map +1 -1
- package/dist/store/keys.js +11 -1
- package/dist/store/keys.js.map +1 -1
- package/dist/store/plan.js +20 -14
- package/dist/store/plan.js.map +1 -1
- package/dist/tools/elevated-shell.d.ts +1 -1
- package/dist/tools/elevated-shell.js +24 -72
- package/dist/tools/elevated-shell.js.map +1 -1
- package/dist/tools/nmap-runner.js +30 -36
- package/dist/tools/nmap-runner.js.map +1 -1
- package/dist/tools/sudo-session.d.ts +83 -0
- package/dist/tools/sudo-session.js +124 -0
- package/dist/tools/sudo-session.js.map +1 -0
- package/dist/tui-v2/app/App.js +9 -24
- package/dist/tui-v2/app/App.js.map +1 -1
- package/dist/tui-v2/components/status/context-limit-chip.js +2 -2
- package/dist/tui-v2/components/status/context-limit-chip.js.map +1 -1
- package/dist/tui-v2/components/transcript/intro-card.js +2 -1
- package/dist/tui-v2/components/transcript/intro-card.js.map +1 -1
- package/dist/tui-v2/composer/composer-editor.js +2 -1
- package/dist/tui-v2/composer/composer-editor.js.map +1 -1
- package/dist/types.d.ts +156 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- package/dist/ui-core/bootstrap/composition-root.d.ts +2 -0
- package/dist/ui-core/bootstrap/composition-root.js +26 -3
- package/dist/ui-core/bootstrap/composition-root.js.map +1 -1
- package/dist/ui-core/commands/picker-commands.js +11 -5
- package/dist/ui-core/commands/picker-commands.js.map +1 -1
- package/dist/ui-core/commands/session-commands.js +41 -5
- package/dist/ui-core/commands/session-commands.js.map +1 -1
- package/dist/ui-core/plan/plan-lifecycle.js +3 -2
- package/dist/ui-core/plan/plan-lifecycle.js.map +1 -1
- package/dist/ui-core/rendering/context-limit.js +4 -1
- package/dist/ui-core/rendering/context-limit.js.map +1 -1
- package/dist/ui-core/rendering/duration.d.ts +2 -1
- package/dist/ui-core/rendering/duration.js +13 -5
- package/dist/ui-core/rendering/duration.js.map +1 -1
- package/dist/ui-core/rendering/intro-header.js +2 -2
- package/dist/ui-core/rendering/intro-header.js.map +1 -1
- package/dist/ui-core/rendering/theme.d.ts +1 -0
- package/dist/ui-core/rendering/theme.js +2 -0
- package/dist/ui-core/rendering/theme.js.map +1 -1
- package/dist/ui-core/state/transcript-hydrate.js +3 -3
- package/dist/ui-core/state/transcript-hydrate.js.map +1 -1
- package/dist/ui-core/state/transcript-reducer.d.ts +5 -0
- package/dist/ui-core/state/transcript-reducer.js +76 -3
- package/dist/ui-core/state/transcript-reducer.js.map +1 -1
- package/dist/ui-core/state/transcript-store.d.ts +10 -1
- package/dist/ui-core/state/transcript-store.js +22 -1
- package/dist/ui-core/state/transcript-store.js.map +1 -1
- package/dist/ui-core/state/transcript-types.d.ts +2 -0
- package/dist/ui-core/state/transcript-types.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +6 -1
package/dist/agent/runner.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
1
|
import { join } from "node:path";
|
|
3
2
|
import { streamWithProvider } from "../llm/router.js";
|
|
3
|
+
import { operationUsageFromError } from "../llm/operation-ledger.js";
|
|
4
|
+
import { contextAttemptFromOperationUsage } from "../llm/context-snapshot.js";
|
|
4
5
|
import { modelContextWindow } from "../llm/token-usage.js";
|
|
5
6
|
import { streamAlreadyEmitted } from "../llm/stream-progress.js";
|
|
6
7
|
import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, createStreamRecoveryState, resetStreamRecoveryState, } from "./stream-recovery.js";
|
|
@@ -41,21 +42,26 @@ import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, }
|
|
|
41
42
|
import { getToolDefinitions, getCompactToolDefinitions, RUNNER_META_TOOL_NAMES, } from "../tools/definitions.js";
|
|
42
43
|
import { elidedStubReuseMessage, findElidedStubArg, } from "./message-slim.js";
|
|
43
44
|
import { appendAssistantWithTools, ensureUniqueToolCallIds, toolCallIdsInHistory, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
|
|
45
|
+
import { legacyReasoningBlockFromArtifacts, reasoningArtifactsForPersistence, } from "../llm/reasoning-artifacts.js";
|
|
44
46
|
import { compactMessagesWithSummary, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
|
|
45
47
|
import { buildContextBreakdown, contextBreakdownAuditPayload, describeDominantContextBlock, toolSchemaHash, } from "./context-breakdown.js";
|
|
48
|
+
import { recordRequestTokenObservation } from "../llm/token-estimate-calibration.js";
|
|
49
|
+
import { accountAssembledRequest, RequestOverLimitError, } from "./request-accounting.js";
|
|
46
50
|
import { autoCompactTriggerTokens, dedupeToolContextOutput, freeTierGuardNotices, getReliabilityPolicy, resolveStepMaxTokens, } from "./reliability-policy.js";
|
|
47
51
|
import { auditLog } from "../store/logs.js";
|
|
48
52
|
import { loadProjectContext } from "../store/project.js";
|
|
49
53
|
import { loadScope, isScopeActive } from "../store/scope.js";
|
|
50
54
|
import { ensureProviderConfigured } from "../commands/providers.js";
|
|
51
|
-
import { createThinkingStreamParser,
|
|
52
|
-
import { hasReasoningMarker
|
|
55
|
+
import { createThinkingStreamParser, rememberThinking, stripThinking, } from "../ui/thinking.js";
|
|
56
|
+
import { hasReasoningMarker } from "../llm/reasoning-marker.js";
|
|
53
57
|
import { safeCwd } from "../os/cwd.js";
|
|
54
58
|
import { analyzeTask, formatTaskAnalysisHint, isNarrowExplicitNmapOperation, } from "./task-analyzer.js";
|
|
55
59
|
import { computeMaxIterations, computeStepBudget } from "./step-budget.js";
|
|
56
60
|
import { isScratchOnlyWrite } from "./scratch-write.js";
|
|
57
61
|
import { buildDurableEnvelope, WorkLedger, } from "./durable-envelope.js";
|
|
58
|
-
import {
|
|
62
|
+
import { buildDirectCompactionPrompt, compactionSinglePassInputBudget, COMPACTION_SYSTEM_PROMPT, COMPACTION_MAX_COMPLETION_TOKENS, COMPACTION_MAP_MAX_COMPLETION_TOKENS, } from "./compaction-summary.js";
|
|
63
|
+
import { executeCompactionSummary, planCompactionReplay, } from "./compaction-executor.js";
|
|
64
|
+
import { isOperationPolicyError, OperationLedger, singleAdmissionOperationPolicy, } from "../llm/operation-ledger.js";
|
|
59
65
|
import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
|
|
60
66
|
import { LoopGuard } from "./loop-guard.js";
|
|
61
67
|
import { appendInterruptedReasoning, interruptedReasoningBrief, isMeaningfulResumptionYield, } from "./interrupted-reasoning.js";
|
|
@@ -63,7 +69,7 @@ import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-att
|
|
|
63
69
|
import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
|
|
64
70
|
import { stat } from "node:fs/promises";
|
|
65
71
|
import { isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools/fs.js";
|
|
66
|
-
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, localHttpProbeIsFailure, localHttpProbeIsSuccess, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective,
|
|
72
|
+
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, localHttpProbeIsFailure, localHttpProbeIsSuccess, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
67
73
|
import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
|
|
68
74
|
import { saveToolOutput, formatToolContext, } from "./tool-output-formatting.js";
|
|
69
75
|
import { codingSessionFromContext, isProtocolPlaceholderOutput, } from "./progress-pause-policy.js";
|
|
@@ -91,9 +97,6 @@ import { createTurnOutcome, normalizeTurnOutcomeInput, renderTurnOutcome, } from
|
|
|
91
97
|
import { beginEngagementAction, finishEngagementAction, recordEngagementCheckpoint, reconcileEngagementJob, openEngagement, saveEngagement, } from "../store/engagement.js";
|
|
92
98
|
export * from "./tool-call-parser.js";
|
|
93
99
|
export { createSessionPolicy, isPreApprovalAllowedTool, isPlanApprovedByStatus, planHasOpenWork, shouldEnableImageOcr, } from "./session-policy.js";
|
|
94
|
-
export function styleToolChatter(call, text) {
|
|
95
|
-
return shouldDimToolChatter(call) ? chalk.dim(text) : text;
|
|
96
|
-
}
|
|
97
100
|
/**
|
|
98
101
|
* A foreground task waits for a responder child only when the plan
|
|
99
102
|
* Declares that dependency. Report titles carry no scheduling meaning: any
|
|
@@ -160,7 +163,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
160
163
|
? options.mode
|
|
161
164
|
: "agent";
|
|
162
165
|
const isPlanMode = agentMode === "plan";
|
|
163
|
-
const writesDirectly = !options.onEvent;
|
|
164
166
|
const emit = (event) => options.onEvent?.(event);
|
|
165
167
|
// Whether the CURRENT model iteration has already committed its visible
|
|
166
168
|
// prose to the transcript with an `assistant-message` event. The recovery
|
|
@@ -246,15 +248,13 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
246
248
|
...(options?.replace ? { replace: true } : {}),
|
|
247
249
|
});
|
|
248
250
|
};
|
|
249
|
-
const writeToolCall = (id, call
|
|
251
|
+
const writeToolCall = (id, call) => {
|
|
250
252
|
emit({
|
|
251
253
|
type: "tool-call",
|
|
252
254
|
id,
|
|
253
255
|
name: call.name,
|
|
254
256
|
argsDisplay: formatToolArgs(call),
|
|
255
257
|
});
|
|
256
|
-
if (writesDirectly)
|
|
257
|
-
process.stdout.write(rendered);
|
|
258
258
|
};
|
|
259
259
|
const writePlanUpdate = (plan) => {
|
|
260
260
|
emit({ type: "plan-update", plan });
|
|
@@ -305,6 +305,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
305
305
|
summary,
|
|
306
306
|
beforeTokens,
|
|
307
307
|
afterTokens,
|
|
308
|
+
contextScope: "assembled-request",
|
|
308
309
|
});
|
|
309
310
|
};
|
|
310
311
|
const writeCompactionFailed = (id, message, retainedTokens) => {
|
|
@@ -835,22 +836,39 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
835
836
|
}
|
|
836
837
|
return text;
|
|
837
838
|
};
|
|
838
|
-
const pushAssistantHistory = (content) => {
|
|
839
|
+
const pushAssistantHistory = (content, reasoning) => {
|
|
839
840
|
const cleaned = sanitizeAssistantText(hasReasoningMarker(content) ? stripThinking(content).visible : content);
|
|
840
841
|
if (!visibleCommitted) {
|
|
841
842
|
const prose = recoveryProse(cleaned);
|
|
842
843
|
if (prose)
|
|
843
844
|
writeAssistantMessage(prose);
|
|
844
845
|
}
|
|
846
|
+
const persistedArtifacts = reasoningArtifactsForPersistence({
|
|
847
|
+
artifacts: reasoning?.reasoningArtifacts,
|
|
848
|
+
hasToolCalls: false,
|
|
849
|
+
});
|
|
850
|
+
const reasoningBlock = persistedArtifacts
|
|
851
|
+
? legacyReasoningBlockFromArtifacts(persistedArtifacts)
|
|
852
|
+
: reasoning?.reasoningArtifacts
|
|
853
|
+
? undefined
|
|
854
|
+
: reasoning?.reasoningBlock;
|
|
845
855
|
messages.push({
|
|
846
856
|
role: "assistant",
|
|
847
857
|
content: cleaned.trim()
|
|
848
858
|
? cleaned
|
|
849
859
|
: "[No visible assistant response was produced.]",
|
|
860
|
+
...(reasoningBlock?.text || reasoningBlock?.items?.length
|
|
861
|
+
? { reasoningBlock }
|
|
862
|
+
: {}),
|
|
863
|
+
...(persistedArtifacts ? { reasoningArtifacts: persistedArtifacts } : {}),
|
|
850
864
|
});
|
|
851
865
|
};
|
|
852
866
|
const loopGuard = new LoopGuard();
|
|
853
867
|
let lastExactPromptTokens = 0;
|
|
868
|
+
// Uncalibrated estimate for the request currently in flight. Paired with the
|
|
869
|
+
// provider's reported prompt size below so the estimator learns this route's
|
|
870
|
+
// bias instead of permanently over-reporting it.
|
|
871
|
+
let dispatchedRawRequestTokens = 0;
|
|
854
872
|
let consecutiveSynthesizedRounds = 0;
|
|
855
873
|
const engagementPolicy = new EngagementPolicyEngine();
|
|
856
874
|
const probeStateKey = (call) => {
|
|
@@ -1137,6 +1155,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1137
1155
|
let stepMaxTokens = 0;
|
|
1138
1156
|
let nextToolEventId = 0;
|
|
1139
1157
|
const alreadyPrintedIds = new Set();
|
|
1158
|
+
const executedWireOccurrences = new Map();
|
|
1140
1159
|
const promptMutex = {
|
|
1141
1160
|
promise: Promise.resolve(),
|
|
1142
1161
|
async acquire() {
|
|
@@ -1198,8 +1217,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1198
1217
|
let call = normalizeToolCall(rawCall);
|
|
1199
1218
|
const emitVisibleSyntheticReceipt = (result, summary) => {
|
|
1200
1219
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1201
|
-
writeToolCall(toolEventId, call
|
|
1202
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1220
|
+
writeToolCall(toolEventId, call);
|
|
1203
1221
|
alreadyPrintedIds.add(toolEventId);
|
|
1204
1222
|
}
|
|
1205
1223
|
emit({ type: "tool-start", id: toolEventId });
|
|
@@ -1267,8 +1285,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1267
1285
|
// no execution, until the model re-issues the identical batch to confirm.
|
|
1268
1286
|
if (call.name === "task.update" && batchRemindCalls.has(rawCall)) {
|
|
1269
1287
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1270
|
-
writeToolCall(toolEventId, call
|
|
1271
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1288
|
+
writeToolCall(toolEventId, call);
|
|
1272
1289
|
alreadyPrintedIds.add(toolEventId);
|
|
1273
1290
|
}
|
|
1274
1291
|
const result = { ok: false, output: batchReminderNote, exitCode: 1 };
|
|
@@ -1385,8 +1402,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1385
1402
|
unreadResponderNotificationIds.delete(responderWakeNotificationId);
|
|
1386
1403
|
}
|
|
1387
1404
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1388
|
-
writeToolCall(toolEventId, call
|
|
1389
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1405
|
+
writeToolCall(toolEventId, call);
|
|
1390
1406
|
alreadyPrintedIds.add(toolEventId);
|
|
1391
1407
|
}
|
|
1392
1408
|
const result = {
|
|
@@ -1446,8 +1462,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1446
1462
|
if (!gate.ok) {
|
|
1447
1463
|
writeNotice("warn", gate.reason);
|
|
1448
1464
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1449
|
-
writeToolCall(toolEventId, call
|
|
1450
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1465
|
+
writeToolCall(toolEventId, call);
|
|
1451
1466
|
alreadyPrintedIds.add(toolEventId);
|
|
1452
1467
|
}
|
|
1453
1468
|
const result = {
|
|
@@ -1523,8 +1538,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1523
1538
|
pendingSessionStatePlan = planResult.plan;
|
|
1524
1539
|
}
|
|
1525
1540
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1526
|
-
writeToolCall(toolEventId, call
|
|
1527
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1541
|
+
writeToolCall(toolEventId, call);
|
|
1528
1542
|
alreadyPrintedIds.add(toolEventId);
|
|
1529
1543
|
}
|
|
1530
1544
|
if (planResult.reminder && planResult.toast) {
|
|
@@ -1541,8 +1555,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1541
1555
|
pendingSessionStatePlan = null;
|
|
1542
1556
|
removePlanContextMessage(messages);
|
|
1543
1557
|
emit({ type: "plan-cleared", sessionId: session.sessionId });
|
|
1544
|
-
if (writesDirectly)
|
|
1545
|
-
process.stdout.write(planResult.display);
|
|
1546
1558
|
}
|
|
1547
1559
|
const result = { ok: planResult.ok, output: planResult.modelNote };
|
|
1548
1560
|
emitToolResult(toolEventId, result, planResult.modelNote);
|
|
@@ -1588,8 +1600,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1588
1600
|
`Accept the plan (y/i or /implement) to switch to agent and execute.`;
|
|
1589
1601
|
writeNotice("warn", reason);
|
|
1590
1602
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1591
|
-
writeToolCall(toolEventId, call
|
|
1592
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1603
|
+
writeToolCall(toolEventId, call);
|
|
1593
1604
|
alreadyPrintedIds.add(toolEventId);
|
|
1594
1605
|
}
|
|
1595
1606
|
const result = { ok: false, output: reason, exitCode: 1 };
|
|
@@ -1692,8 +1703,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1692
1703
|
: `Scaffold was not run: the existing target${target ? ` at ${target}` : ""} is incomplete. Inspect and repair it before completing the scaffold task; do not retry the scaffolder into this non-empty directory.`;
|
|
1693
1704
|
writeNotice("info", message);
|
|
1694
1705
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1695
|
-
writeToolCall(toolEventId, call
|
|
1696
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1706
|
+
writeToolCall(toolEventId, call);
|
|
1697
1707
|
alreadyPrintedIds.add(toolEventId);
|
|
1698
1708
|
}
|
|
1699
1709
|
const result = { ok: true, output: message, exitCode: 0 };
|
|
@@ -1708,8 +1718,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1708
1718
|
}
|
|
1709
1719
|
}
|
|
1710
1720
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1711
|
-
writeToolCall(toolEventId, call
|
|
1712
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
1721
|
+
writeToolCall(toolEventId, call);
|
|
1713
1722
|
alreadyPrintedIds.add(toolEventId);
|
|
1714
1723
|
}
|
|
1715
1724
|
const scopeTarget = safeScopeTargetForToolCall(call);
|
|
@@ -2678,6 +2687,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2678
2687
|
let lastCompactionMsgCount = 0;
|
|
2679
2688
|
const compactionAttempts = new CompactionAttemptLedger();
|
|
2680
2689
|
let activeCompactionId;
|
|
2690
|
+
let activeCompactionLedger;
|
|
2691
|
+
/**
|
|
2692
|
+
* The last successful main request exactly as dispatched. A compaction
|
|
2693
|
+
* that replays it (plus the messages appended since) keeps the entire
|
|
2694
|
+
* prior prompt as a strict prefix, so APC providers serve the compaction
|
|
2695
|
+
* request from cache instead of re-billing the whole context.
|
|
2696
|
+
*/
|
|
2697
|
+
let lastSuccessfulRequestSnapshot = options.previousSuccessfulRequest;
|
|
2698
|
+
/**
|
|
2699
|
+
* Per-attempt replay decision made by maybeAutoCompact and read by
|
|
2700
|
+
* summarizeForCompaction. When undefined the legacy transcript-rendered
|
|
2701
|
+
* requests are used (no snapshot yet, or the replay would not fit).
|
|
2702
|
+
*/
|
|
2703
|
+
let compactionReplaySnapshot;
|
|
2681
2704
|
/** E5: identical tool bodies within this turn → pointer instead of re-append. */
|
|
2682
2705
|
const toolResultHashes = new Map();
|
|
2683
2706
|
/** E4: consecutive free-tier stream failures this turn. */
|
|
@@ -2697,109 +2720,59 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2697
2720
|
const compactionTools = sourceMessages
|
|
2698
2721
|
? selectToolDefs(nativeToolsActive, useCompactSystemPrompt)
|
|
2699
2722
|
: undefined;
|
|
2700
|
-
|
|
2723
|
+
// Cache-preserving replay: resend the last successful request verbatim
|
|
2724
|
+
// (same provider, model, tools, sampling and reasoning settings) with
|
|
2725
|
+
// only the new tail and the compaction instruction appended. Anything
|
|
2726
|
+
// else — a different system prompt, dropped tool schemas, a re-rendered
|
|
2727
|
+
// transcript — changes the first bytes of the prompt and throws away the
|
|
2728
|
+
// whole cached prefix.
|
|
2729
|
+
const replay = compactionReplaySnapshot;
|
|
2730
|
+
return executeCompactionSummary({
|
|
2701
2731
|
provider,
|
|
2702
2732
|
model,
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
...sourceMessages,
|
|
2706
|
-
{ role: "user", content: summaryPrompt },
|
|
2707
|
-
]
|
|
2708
|
-
: [
|
|
2709
|
-
{ role: "system", content: COMPACTION_SYSTEM_PROMPT },
|
|
2710
|
-
{ role: "user", content: summaryPrompt },
|
|
2711
|
-
],
|
|
2712
|
-
temperature: 0.1,
|
|
2733
|
+
systemContent: COMPACTION_SYSTEM_PROMPT,
|
|
2734
|
+
prompt: summaryPrompt,
|
|
2713
2735
|
maxTokens,
|
|
2714
|
-
thinking: { enabled: false, effort: "none" },
|
|
2715
2736
|
signal: options.signal,
|
|
2716
|
-
|
|
2717
|
-
...(compactionTools?.length
|
|
2737
|
+
...(replay
|
|
2718
2738
|
? {
|
|
2719
|
-
|
|
2720
|
-
|
|
2739
|
+
baseRequest: replay,
|
|
2740
|
+
history: messages,
|
|
2741
|
+
...(currentContextLimitTokens() !== undefined
|
|
2742
|
+
? { contextLimitTokens: currentContextLimitTokens() }
|
|
2743
|
+
: {}),
|
|
2721
2744
|
}
|
|
2745
|
+
: {
|
|
2746
|
+
...(sourceMessages ? { sourceMessages } : {}),
|
|
2747
|
+
...(compactionTools?.length ? { tools: compactionTools } : {}),
|
|
2748
|
+
}),
|
|
2749
|
+
...(activeCompactionLedger
|
|
2750
|
+
? { operation: activeCompactionLedger }
|
|
2722
2751
|
: {}),
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
writeCompactionDelta(compactionId, text);
|
|
2731
|
-
}, undefined, { remember: false });
|
|
2732
|
-
const result = await streamWithProvider(attemptRequest, (token) => parser.push(token), { onStatus: () => undefined, maxRetries: 0 });
|
|
2733
|
-
parser.finish();
|
|
2734
|
-
return result;
|
|
2735
|
-
};
|
|
2736
|
-
const first = await runAttempt(request);
|
|
2737
|
-
let visible = normalizeCompactionSummary(stripThinking(first.text).visible);
|
|
2738
|
-
let retryReason;
|
|
2739
|
-
if (isCompactionCompletionTruncated(first, maxTokens)) {
|
|
2740
|
-
retryReason = "truncated";
|
|
2741
|
-
}
|
|
2742
|
-
else if (!visible) {
|
|
2743
|
-
retryReason = "reasoning-only";
|
|
2744
|
-
}
|
|
2745
|
-
else if (looksLikeTranscriptReplay(visible)) {
|
|
2746
|
-
retryReason = "replayed";
|
|
2747
|
-
}
|
|
2748
|
-
else if (looksLikeIncompleteCompactionSummary(visible)) {
|
|
2749
|
-
retryReason = "incomplete";
|
|
2750
|
-
}
|
|
2751
|
-
if (retryReason) {
|
|
2752
|
-
const retry = await runAttempt({
|
|
2753
|
-
...request,
|
|
2754
|
-
messages: sourceMessages
|
|
2755
|
-
? [
|
|
2756
|
-
...sourceMessages,
|
|
2757
|
-
{
|
|
2758
|
-
role: "user",
|
|
2759
|
-
content: buildCompactionRetryPrompt(summaryPrompt, retryReason),
|
|
2760
|
-
},
|
|
2761
|
-
]
|
|
2762
|
-
: [
|
|
2763
|
-
{
|
|
2764
|
-
role: "system",
|
|
2765
|
-
content: `${COMPACTION_SYSTEM_PROMPT}\nReturn only a complete continuation-memory summary. Do not include analysis, reasoning, or <think> tags.`,
|
|
2766
|
-
},
|
|
2767
|
-
{
|
|
2768
|
-
role: "user",
|
|
2769
|
-
content: buildCompactionRetryPrompt(summaryPrompt, retryReason),
|
|
2770
|
-
},
|
|
2771
|
-
],
|
|
2772
|
-
temperature: 0,
|
|
2773
|
-
maxTokens,
|
|
2774
|
-
thinking: { enabled: false, effort: "none" },
|
|
2775
|
-
allowModelFallback: true,
|
|
2776
|
-
}, true);
|
|
2777
|
-
if (isCompactionCompletionTruncated(retry, maxTokens)) {
|
|
2778
|
-
throw new Error("compaction failed: model hit the summary output limit twice — original context retained");
|
|
2779
|
-
}
|
|
2780
|
-
visible = normalizeCompactionSummary(stripThinking(retry.text).visible);
|
|
2781
|
-
if (!visible) {
|
|
2782
|
-
throw new Error("compaction failed: model returned an empty summary");
|
|
2783
|
-
}
|
|
2784
|
-
if (looksLikeTranscriptReplay(visible)) {
|
|
2785
|
-
throw new Error("compaction failed: model replayed the transcript twice — original context retained");
|
|
2786
|
-
}
|
|
2787
|
-
if (looksLikeIncompleteCompactionSummary(visible)) {
|
|
2788
|
-
throw new Error("compaction failed: model returned an incomplete summary twice — original context retained");
|
|
2789
|
-
}
|
|
2790
|
-
}
|
|
2791
|
-
return visible;
|
|
2752
|
+
qualityRetry: false,
|
|
2753
|
+
retryOnServerError: true,
|
|
2754
|
+
stream: true,
|
|
2755
|
+
onToken: compactionId
|
|
2756
|
+
? (text, replace) => writeCompactionDelta(compactionId, text, replace)
|
|
2757
|
+
: undefined,
|
|
2758
|
+
});
|
|
2792
2759
|
};
|
|
2793
2760
|
/**
|
|
2794
|
-
* Estimate the complete next model request
|
|
2795
|
-
*
|
|
2796
|
-
*
|
|
2797
|
-
* the
|
|
2761
|
+
* Estimate the complete next model request through the one serialized-
|
|
2762
|
+
* request accounting service, including attached native-tool schemas and
|
|
2763
|
+
* reasoning replay payloads. The same service owns the final pre-dispatch
|
|
2764
|
+
* fit check, so the trigger path and the dispatch path cannot disagree.
|
|
2798
2765
|
*/
|
|
2799
2766
|
const estimateNextRequestTokens = (contextMessages) => {
|
|
2800
2767
|
const { native } = resolveNativeTools(provider, model);
|
|
2801
2768
|
const nextTools = selectToolDefs(native, useCompactSystemPrompt);
|
|
2802
|
-
return
|
|
2769
|
+
return accountAssembledRequest({
|
|
2770
|
+
provider,
|
|
2771
|
+
model,
|
|
2772
|
+
messages: contextMessages,
|
|
2773
|
+
stream: true,
|
|
2774
|
+
...(nextTools?.length ? { tools: nextTools } : {}),
|
|
2775
|
+
}).accounting.requestTokens;
|
|
2803
2776
|
};
|
|
2804
2777
|
/**
|
|
2805
2778
|
* Canonical state that must survive compaction verbatim. Built from the
|
|
@@ -2851,7 +2824,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2851
2824
|
});
|
|
2852
2825
|
}
|
|
2853
2826
|
async function maybeAutoCompact(reason, force = false) {
|
|
2854
|
-
const
|
|
2827
|
+
const beforeRequestTokens = estimateNextRequestTokens(messages);
|
|
2828
|
+
const beforeTokens = Math.max(beforeRequestTokens, lastExactPromptTokens);
|
|
2855
2829
|
const contextLimitTokens = currentContextLimitTokens();
|
|
2856
2830
|
const compactTrigger = autoCompactTriggerTokens(getReliabilityPolicy(), {
|
|
2857
2831
|
provider,
|
|
@@ -2879,6 +2853,34 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2879
2853
|
return;
|
|
2880
2854
|
const compactionId = `compact-${randomUUID().slice(0, 12)}`;
|
|
2881
2855
|
activeCompactionId = compactionId;
|
|
2856
|
+
// Admissions: the pinned dispatch plus the executor's bounded error
|
|
2857
|
+
// retry. Still a hard cap — every admission re-sends the full prompt.
|
|
2858
|
+
const compactionLedger = new OperationLedger(singleAdmissionOperationPolicy("compaction", 3));
|
|
2859
|
+
activeCompactionLedger = compactionLedger;
|
|
2860
|
+
// Plan the cache-preserving replay up front: resend the last successful
|
|
2861
|
+
// request with the tail + instruction appended. When it fits, the direct
|
|
2862
|
+
// single pass is forced (the raw estimate gate would otherwise reject a
|
|
2863
|
+
// request that fits fine); when it does not, compaction falls back to
|
|
2864
|
+
// the legacy transcript-rendered requests so it still succeeds.
|
|
2865
|
+
const replaySnapshot = lastSuccessfulRequestSnapshot;
|
|
2866
|
+
const replayPlan = replaySnapshot
|
|
2867
|
+
? planCompactionReplay({
|
|
2868
|
+
baseRequest: replaySnapshot,
|
|
2869
|
+
history: messages,
|
|
2870
|
+
prompt: buildDirectCompactionPrompt({
|
|
2871
|
+
...(durableEnvelope ? { durableState: durableEnvelope } : {}),
|
|
2872
|
+
}),
|
|
2873
|
+
maxTokens: COMPACTION_MAX_COMPLETION_TOKENS,
|
|
2874
|
+
...(contextLimitTokens !== undefined
|
|
2875
|
+
? { contextLimitTokens }
|
|
2876
|
+
: { contextLimitTokens: modelContextWindow(model, provider) }),
|
|
2877
|
+
stream: true,
|
|
2878
|
+
})
|
|
2879
|
+
: undefined;
|
|
2880
|
+
compactionReplaySnapshot =
|
|
2881
|
+
replayPlan && !replayPlan.accounting.overLimit
|
|
2882
|
+
? replaySnapshot
|
|
2883
|
+
: undefined;
|
|
2882
2884
|
writeCompactionStarted(compactionId, beforeTokens);
|
|
2883
2885
|
try {
|
|
2884
2886
|
const compactionTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
|
|
@@ -2886,6 +2888,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2886
2888
|
const result = await compactMessagesWithSummary(messages, summarizeForCompaction, {
|
|
2887
2889
|
budgetTokens: 0,
|
|
2888
2890
|
keepRecent: AUTO_COMPACT_KEEP_RECENT,
|
|
2891
|
+
singleAdmission: true,
|
|
2892
|
+
...(compactionReplaySnapshot
|
|
2893
|
+
? { forceDirectSinglePass: true }
|
|
2894
|
+
: {}),
|
|
2889
2895
|
singlePassInputBudgetTokens: Math.max(0, compactionSinglePassInputBudget(contextLimitTokens ?? modelContextWindow(model, provider)) - compactionSchemaTokens),
|
|
2890
2896
|
...(durableEnvelope ? { durableEnvelope } : {}),
|
|
2891
2897
|
});
|
|
@@ -2917,18 +2923,48 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2917
2923
|
writeCompactionFailed(compactionId, `Summary remained over the context limit; largest block: ${dominant}.`, beforeTokens);
|
|
2918
2924
|
return;
|
|
2919
2925
|
}
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
loopGuard.resetReadOnly();
|
|
2923
|
-
lastExactPromptTokens = 0;
|
|
2924
|
-
// Token stats use the same complete request estimate as the trigger.
|
|
2925
|
-
const compactedTokens = estimateNextRequestTokens(messages);
|
|
2926
|
-
// Re-inject the live plan so the model keeps full plan awareness even
|
|
2927
|
-
// after older turns (which carried the plan context) were summarized.
|
|
2926
|
+
// Re-inject the live plan on a candidate copy first so the commit is
|
|
2927
|
+
// validated against the complete next request, not the bare summary.
|
|
2928
2928
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
2929
|
+
const candidateMessages = [...result.messages];
|
|
2929
2930
|
if (livePlan) {
|
|
2930
|
-
upsertPlanContextMessage(
|
|
2931
|
+
upsertPlanContextMessage(candidateMessages, planContextMessage(livePlan, session.planApproved.value));
|
|
2932
|
+
}
|
|
2933
|
+
if (contextLimitTokens !== undefined) {
|
|
2934
|
+
const finalFit = accountAssembledRequest({
|
|
2935
|
+
provider,
|
|
2936
|
+
model,
|
|
2937
|
+
messages: candidateMessages,
|
|
2938
|
+
stream: true,
|
|
2939
|
+
...(selectToolDefs(nativeToolsActive, useCompactSystemPrompt)?.length
|
|
2940
|
+
? {
|
|
2941
|
+
tools: selectToolDefs(nativeToolsActive, useCompactSystemPrompt),
|
|
2942
|
+
}
|
|
2943
|
+
: {}),
|
|
2944
|
+
contextLimitTokens,
|
|
2945
|
+
});
|
|
2946
|
+
if (finalFit.accounting.overLimit) {
|
|
2947
|
+
const dominant = describeDominantContextBlock(candidateMessages);
|
|
2948
|
+
compactionAttempts.recordFailure(attemptKey);
|
|
2949
|
+
await auditLog("agent.compact.overflow", {
|
|
2950
|
+
reason,
|
|
2951
|
+
candidateTokens: finalFit.accounting.requestTokens,
|
|
2952
|
+
safeLimit: finalFit.accounting.limit.effectiveSafeTokens,
|
|
2953
|
+
trigger: compactTrigger,
|
|
2954
|
+
dominant,
|
|
2955
|
+
});
|
|
2956
|
+
writeNotice("warn", `compacted request would still exceed the effective safe context limit (~${finalFit.accounting.requestTokens.toLocaleString()} > ~${(finalFit.accounting.limit.effectiveSafeTokens ?? 0).toLocaleString()} tokens) — largest block: ${dominant}; run /compact or trim large outputs`);
|
|
2957
|
+
writeCompactionFailed(compactionId, `Compacted request would not fit the effective safe context limit; largest block: ${dominant}.`, beforeTokens);
|
|
2958
|
+
return;
|
|
2959
|
+
}
|
|
2931
2960
|
}
|
|
2961
|
+
messages.splice(0, messages.length, ...candidateMessages);
|
|
2962
|
+
compactionAttempts.recordSuccess(attemptKey);
|
|
2963
|
+
loopGuard.resetReadOnly();
|
|
2964
|
+
lastExactPromptTokens = 0;
|
|
2965
|
+
// The snapshot predates the rewrite: replaying it would resurrect the
|
|
2966
|
+
// pre-compaction history. The next successful request re-seeds it.
|
|
2967
|
+
lastSuccessfulRequestSnapshot = undefined;
|
|
2932
2968
|
// Re-inject live SESSION STATE after compaction (older flags survive).
|
|
2933
2969
|
refreshSessionState(livePlan);
|
|
2934
2970
|
lastCompactionMsgCount = messages.length;
|
|
@@ -2938,6 +2974,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2938
2974
|
newLength: messages.length,
|
|
2939
2975
|
estimatedTokens: afterTokens,
|
|
2940
2976
|
reason,
|
|
2977
|
+
strategy: result.strategy ?? "single",
|
|
2978
|
+
compactionAdmissions: compactionLedger.snapshot().attempts.length,
|
|
2941
2979
|
});
|
|
2942
2980
|
const insertedSummary = messages.find((m) => isCompactionMemoryMessage(m))?.content ?? "";
|
|
2943
2981
|
const summaryText = insertedSummary.startsWith(`${PLAN_IMPLEMENT_MEMORY_PREFIX}\n\n`)
|
|
@@ -2950,11 +2988,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2950
2988
|
// Report the final assembled request, including live plan and session
|
|
2951
2989
|
// state reinjection, so the card and the next provider request agree.
|
|
2952
2990
|
writeCompactionCompleted(compactionId, summaryText, beforeTokens, afterTokens);
|
|
2953
|
-
writeNotice("info", `context auto-compacted to fit the window (~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens)`);
|
|
2991
|
+
writeNotice("info", `context auto-compacted to fit the window (~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens)${result.strategy === "emergency_prefix_slice" ? " — oldest slice only (lower confidence); run /compact for a full summary" : ""}`);
|
|
2954
2992
|
}
|
|
2955
2993
|
catch (error) {
|
|
2956
2994
|
const message = error instanceof Error ? error.message : String(error);
|
|
2957
|
-
writeCompactionFailed(compactionId, /aborted/i.test(message)
|
|
2995
|
+
writeCompactionFailed(compactionId, /aborted/i.test(message)
|
|
2996
|
+
? "Compaction was cancelled."
|
|
2997
|
+
: isOperationPolicyError(error)
|
|
2998
|
+
? "Compaction is limited to one pinned request (plus its bounded retry) and none completed; the original context was retained."
|
|
2999
|
+
: message, beforeTokens);
|
|
2958
3000
|
if (error instanceof Error &&
|
|
2959
3001
|
(error.name === "AbortError" || error.message.includes("aborted"))) {
|
|
2960
3002
|
throw error;
|
|
@@ -2965,6 +3007,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2965
3007
|
finally {
|
|
2966
3008
|
if (activeCompactionId === compactionId)
|
|
2967
3009
|
activeCompactionId = undefined;
|
|
3010
|
+
if (activeCompactionLedger === compactionLedger) {
|
|
3011
|
+
activeCompactionLedger = undefined;
|
|
3012
|
+
}
|
|
3013
|
+
compactionReplaySnapshot = undefined;
|
|
2968
3014
|
}
|
|
2969
3015
|
}
|
|
2970
3016
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
@@ -2997,6 +3043,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2997
3043
|
let emittedThinkingStatus = false;
|
|
2998
3044
|
let generatedTokens = 0;
|
|
2999
3045
|
let accumulatedText = "";
|
|
3046
|
+
let streamedReasoningText = "";
|
|
3047
|
+
let typedReasoningOpen = false;
|
|
3000
3048
|
const callIds = [];
|
|
3001
3049
|
let streamedCallsCount = 0;
|
|
3002
3050
|
// A model can think silently for minutes. Without a heartbeat the UI
|
|
@@ -3044,11 +3092,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3044
3092
|
const turnTools = selectToolDefs(nativeToolsActive, useCompactSystemPrompt);
|
|
3045
3093
|
toolsAttached = Boolean(turnTools?.length);
|
|
3046
3094
|
const contextBreakdown = buildContextBreakdown(messages, toolsAttached ? turnTools : undefined);
|
|
3047
|
-
emit({
|
|
3048
|
-
type: "context-estimate",
|
|
3049
|
-
estimatedTokens: contextBreakdown.estimatedTotalTokens,
|
|
3050
|
-
model,
|
|
3051
|
-
});
|
|
3052
3095
|
// E4: advisory only — never blocks free-tier users.
|
|
3053
3096
|
if (!freeTierLargeContextWarned) {
|
|
3054
3097
|
const notices = freeTierGuardNotices({
|
|
@@ -3084,6 +3127,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3084
3127
|
toolsAttached,
|
|
3085
3128
|
recoveryNudge: retryWithoutThinking,
|
|
3086
3129
|
truncationDepth: truncatedBudgetRounds,
|
|
3130
|
+
thinkingEnabled: Boolean(config.thinking?.enabled) && !retryWithoutThinking,
|
|
3087
3131
|
}),
|
|
3088
3132
|
});
|
|
3089
3133
|
// Resume / mid-turn abort can leave orphan tool rows or a user
|
|
@@ -3099,8 +3143,44 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3099
3143
|
toolsAttached,
|
|
3100
3144
|
recoveryNudge: retryWithoutThinking,
|
|
3101
3145
|
truncationDepth: truncatedBudgetRounds,
|
|
3146
|
+
thinkingEnabled: Boolean(config.thinking?.enabled) && !retryWithoutThinking,
|
|
3102
3147
|
});
|
|
3103
3148
|
try {
|
|
3149
|
+
// MR-007: the fit verdict is taken on the final assembled request —
|
|
3150
|
+
// after protocol repair and every live-state reinjection — and a
|
|
3151
|
+
// request that cannot fit the effective safe limit never dispatches.
|
|
3152
|
+
const finalAccounting = accountAssembledRequest({
|
|
3153
|
+
provider,
|
|
3154
|
+
model,
|
|
3155
|
+
messages,
|
|
3156
|
+
stream: true,
|
|
3157
|
+
...(toolsAttached && turnTools?.length
|
|
3158
|
+
? { tools: turnTools, toolChoice: "auto", parallelToolCalls: true }
|
|
3159
|
+
: {}),
|
|
3160
|
+
...(contextLimitTokens !== undefined ? { contextLimitTokens } : {}),
|
|
3161
|
+
}).accounting;
|
|
3162
|
+
dispatchedRawRequestTokens = finalAccounting.rawRequestTokens;
|
|
3163
|
+
// The chip reports the request that is actually about to be sent, from
|
|
3164
|
+
// the same accounting the fit gate and the compaction card use, so the
|
|
3165
|
+
// three can never disagree.
|
|
3166
|
+
emit({
|
|
3167
|
+
type: "context-estimate",
|
|
3168
|
+
estimatedTokens: finalAccounting.requestTokens,
|
|
3169
|
+
model,
|
|
3170
|
+
});
|
|
3171
|
+
if (finalAccounting.overLimit) {
|
|
3172
|
+
await auditLog("agent.request.over-limit-blocked", {
|
|
3173
|
+
provider,
|
|
3174
|
+
model,
|
|
3175
|
+
estimatedTokens: finalAccounting.requestTokens,
|
|
3176
|
+
effectiveSafeTokens: finalAccounting.limit.effectiveSafeTokens,
|
|
3177
|
+
limitSource: finalAccounting.limit.source,
|
|
3178
|
+
reservedOutputTokens: finalAccounting.limit.reservedOutputTokens,
|
|
3179
|
+
safetyMarginTokens: finalAccounting.limit.safetyMarginTokens,
|
|
3180
|
+
});
|
|
3181
|
+
writeNotice("warn", `estimated request (~${finalAccounting.requestTokens.toLocaleString()} tokens) exceeds the model's safe context window (~${finalAccounting.limit.effectiveSafeTokens?.toLocaleString()} tokens) — run /compact, trim large outputs, or raise the session context limit`);
|
|
3182
|
+
throw new RequestOverLimitError(`estimated request (~${finalAccounting.requestTokens.toLocaleString()} tokens) exceeds the effective safe context limit (~${finalAccounting.limit.effectiveSafeTokens?.toLocaleString()} tokens); dispatch blocked`);
|
|
3183
|
+
}
|
|
3104
3184
|
if (responderDelivery &&
|
|
3105
3185
|
!jobManager.markDeliveryStarted(responderDelivery.id, session.sessionId)) {
|
|
3106
3186
|
jobManager.releaseResponderNotificationClaim(responderDelivery.id);
|
|
@@ -3155,35 +3235,59 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3155
3235
|
deferredToolCalls.push({
|
|
3156
3236
|
eventId,
|
|
3157
3237
|
call,
|
|
3158
|
-
rendered: "",
|
|
3159
3238
|
shown: true,
|
|
3160
3239
|
});
|
|
3161
|
-
writeToolCall(eventId, call
|
|
3162
|
-
chalk.gray(` ${formatToolArgs(call)}`)) + "\n");
|
|
3240
|
+
writeToolCall(eventId, call);
|
|
3163
3241
|
emit({ type: "status", text: call.name });
|
|
3164
3242
|
streamedCallsCount += 1;
|
|
3165
3243
|
}
|
|
3166
3244
|
}
|
|
3167
3245
|
}
|
|
3246
|
+
if (typedReasoningOpen) {
|
|
3247
|
+
typedReasoningOpen = false;
|
|
3248
|
+
inThinking = false;
|
|
3249
|
+
generatedTokens = 0;
|
|
3250
|
+
}
|
|
3168
3251
|
if (!sawReasoning &&
|
|
3169
|
-
(
|
|
3170
|
-
/^\s*<think(?:ing)?\b/i.test(accumulatedText))) {
|
|
3252
|
+
/^\s*<think(?:ing)?\b/i.test(accumulatedText)) {
|
|
3171
3253
|
sawReasoning = true;
|
|
3172
3254
|
inThinking = true;
|
|
3173
3255
|
emit({ type: "status", text: "thinking" });
|
|
3174
3256
|
}
|
|
3175
|
-
if (
|
|
3176
|
-
(inThinking && /<\/think(?:ing)?>/i.test(token))) {
|
|
3257
|
+
if (inThinking && /<\/think(?:ing)?>/i.test(token)) {
|
|
3177
3258
|
inThinking = false;
|
|
3178
3259
|
generatedTokens = 0;
|
|
3179
3260
|
}
|
|
3180
|
-
},
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3261
|
+
}, {
|
|
3262
|
+
onStatus: (status) => {
|
|
3263
|
+
writeStatus(status);
|
|
3264
|
+
// Toast only on key *switch* after a failure — never on sticky
|
|
3265
|
+
// "using" or retry countdown ticks (those stay in composer status).
|
|
3266
|
+
if (/^switching /i.test(status.trim())) {
|
|
3267
|
+
writeNotice("warn", status.trim());
|
|
3268
|
+
}
|
|
3269
|
+
},
|
|
3270
|
+
onStreamEvent: (event) => {
|
|
3271
|
+
if (event.type !== "reasoning_delta")
|
|
3272
|
+
return;
|
|
3273
|
+
streamedReasoningText += event.text;
|
|
3274
|
+
typedReasoningOpen = true;
|
|
3275
|
+
sawReasoning = true;
|
|
3276
|
+
inThinking = true;
|
|
3277
|
+
generatedTokens += 1;
|
|
3278
|
+
if (!emittedThinkingStatus) {
|
|
3279
|
+
emittedThinkingStatus = true;
|
|
3280
|
+
emit({ type: "status", text: "thinking" });
|
|
3281
|
+
}
|
|
3282
|
+
emit({ type: "thinking-delta", text: event.text });
|
|
3283
|
+
},
|
|
3284
|
+
// Capture every successful dispatch locally as well: an
|
|
3285
|
+
// auto-compaction later in this turn replays the exact request
|
|
3286
|
+
// so the whole prior prompt stays a cached prefix.
|
|
3287
|
+
onSuccessfulRequest: (snapshot) => {
|
|
3288
|
+
lastSuccessfulRequestSnapshot = snapshot;
|
|
3289
|
+
options.onSuccessfulRequest?.(snapshot);
|
|
3290
|
+
},
|
|
3187
3291
|
});
|
|
3188
3292
|
freeTierConsecutiveFailures = 0;
|
|
3189
3293
|
// Stream succeeded → the failure episode is over. Reset the recovery
|
|
@@ -3198,6 +3302,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3198
3302
|
// User cancelled (double-Esc) — never try to recover, just stop.
|
|
3199
3303
|
if (options.signal?.aborted)
|
|
3200
3304
|
throw streamError;
|
|
3305
|
+
// A blocked over-limit request is a policy stop, not a route
|
|
3306
|
+
// failure — retrying it would just re-bill the same doomed prefix.
|
|
3307
|
+
if (streamError instanceof RequestOverLimitError)
|
|
3308
|
+
throw streamError;
|
|
3201
3309
|
// E4: track free-tier failures for advisory notices (never blocks).
|
|
3202
3310
|
freeTierConsecutiveFailures += 1;
|
|
3203
3311
|
if (!freeTierAdvisoryShown) {
|
|
@@ -3220,8 +3328,32 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3220
3328
|
// We only rethrow (stop the turn) in the worst case: every approach
|
|
3221
3329
|
// for that failure class is exhausted or the total budget is spent.
|
|
3222
3330
|
const failureKind = classifyStreamFailure(streamError);
|
|
3331
|
+
const failedOperationUsage = operationUsageFromError(streamError);
|
|
3332
|
+
const failedUsage = failedOperationUsage?.aggregate.usage;
|
|
3333
|
+
const failedAttempt = failedOperationUsage?.attempts.at(-1);
|
|
3334
|
+
if (failedUsage && failedAttempt) {
|
|
3335
|
+
emit({
|
|
3336
|
+
type: "token-usage",
|
|
3337
|
+
usage: failedUsage,
|
|
3338
|
+
model: failedAttempt.model,
|
|
3339
|
+
provider: failedAttempt.provider,
|
|
3340
|
+
});
|
|
3341
|
+
}
|
|
3223
3342
|
const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
|
|
3224
|
-
const
|
|
3343
|
+
const partialSplit = stripThinking(accumulatedText);
|
|
3344
|
+
const partialThinkContent = [
|
|
3345
|
+
streamedReasoningText.trim(),
|
|
3346
|
+
partialSplit.thinkContent,
|
|
3347
|
+
]
|
|
3348
|
+
.filter(Boolean)
|
|
3349
|
+
.join("\n\n");
|
|
3350
|
+
if (partialThinkContent)
|
|
3351
|
+
rememberThinking(partialThinkContent);
|
|
3352
|
+
const partial = {
|
|
3353
|
+
visible: partialSplit.visible,
|
|
3354
|
+
hasThinking: partialThinkContent.length > 0,
|
|
3355
|
+
thinkContent: partialThinkContent,
|
|
3356
|
+
};
|
|
3225
3357
|
const rawPartialVisible = partialStream
|
|
3226
3358
|
? textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible)
|
|
3227
3359
|
: "";
|
|
@@ -3346,12 +3478,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3346
3478
|
if (completion.usage) {
|
|
3347
3479
|
if (completion.usage.exact && completion.usage.promptTokens > 0) {
|
|
3348
3480
|
lastExactPromptTokens = completion.usage.promptTokens;
|
|
3481
|
+
recordRequestTokenObservation({
|
|
3482
|
+
provider: completion.provider,
|
|
3483
|
+
model: completion.model,
|
|
3484
|
+
estimatedRequestTokens: dispatchedRawRequestTokens,
|
|
3485
|
+
actualPromptTokens: completion.usage.promptTokens,
|
|
3486
|
+
});
|
|
3349
3487
|
}
|
|
3488
|
+
const attempt = contextAttemptFromOperationUsage(completion.operationUsage);
|
|
3350
3489
|
emit({
|
|
3351
3490
|
type: "token-usage",
|
|
3352
3491
|
usage: completion.usage,
|
|
3353
3492
|
model: completion.model,
|
|
3354
3493
|
provider: completion.provider,
|
|
3494
|
+
...(attempt.kind === "generation" ? { attempt } : {}),
|
|
3355
3495
|
});
|
|
3356
3496
|
// Cache telemetry: without read/create counts there is no way to tell
|
|
3357
3497
|
// whether the stable prefix is actually being reused.
|
|
@@ -3378,7 +3518,21 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3378
3518
|
// fallback dropped tools, treat as text mode for this turn's parse.
|
|
3379
3519
|
const usedNativeProtocol = Boolean(completion.toolCalls?.length) ||
|
|
3380
3520
|
(toolsAttached && !isTextOnlyModel(provider, model));
|
|
3381
|
-
const
|
|
3521
|
+
const completionSplit = stripThinking(completion.text);
|
|
3522
|
+
const completionThinkContent = [
|
|
3523
|
+
streamedReasoningText.trim() ||
|
|
3524
|
+
(completion.reasoningBlock?.text ?? "").trim(),
|
|
3525
|
+
completionSplit.thinkContent,
|
|
3526
|
+
]
|
|
3527
|
+
.filter(Boolean)
|
|
3528
|
+
.join("\n\n");
|
|
3529
|
+
if (completionThinkContent)
|
|
3530
|
+
rememberThinking(completionThinkContent);
|
|
3531
|
+
const assistantTextResult = {
|
|
3532
|
+
visible: completionSplit.visible,
|
|
3533
|
+
hasThinking: completionThinkContent.length > 0,
|
|
3534
|
+
thinkContent: completionThinkContent,
|
|
3535
|
+
};
|
|
3382
3536
|
const continuedVisible = trimExactContinuationOverlap(interruptedVisible, assistantTextResult.visible);
|
|
3383
3537
|
canonicalAssistantVisible = interruptedVisible + continuedVisible;
|
|
3384
3538
|
assistantText = {
|
|
@@ -3402,7 +3556,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3402
3556
|
});
|
|
3403
3557
|
}
|
|
3404
3558
|
}
|
|
3405
|
-
pushAssistantHistory(historyText);
|
|
3559
|
+
pushAssistantHistory(historyText, completion);
|
|
3406
3560
|
interruptedVisible = "";
|
|
3407
3561
|
interruptedReasoning = "";
|
|
3408
3562
|
lowYieldResumptions = 0;
|
|
@@ -3425,7 +3579,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3425
3579
|
deferredToolCalls.push({
|
|
3426
3580
|
eventId,
|
|
3427
3581
|
call: normalized,
|
|
3428
|
-
rendered: "",
|
|
3429
3582
|
shown: false,
|
|
3430
3583
|
});
|
|
3431
3584
|
}
|
|
@@ -3449,7 +3602,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3449
3602
|
const entry = {
|
|
3450
3603
|
eventId,
|
|
3451
3604
|
call: normalized,
|
|
3452
|
-
rendered: "",
|
|
3453
3605
|
shown: existing?.shown ?? false,
|
|
3454
3606
|
};
|
|
3455
3607
|
if (existing)
|
|
@@ -3524,7 +3676,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3524
3676
|
appendAssistantWithTools(messages, assistantText.visible, salvageHistoryCalls, completion.reasoningBlock ??
|
|
3525
3677
|
(assistantText.hasThinking && assistantText.thinkContent
|
|
3526
3678
|
? { text: assistantText.thinkContent }
|
|
3527
|
-
: undefined));
|
|
3679
|
+
: undefined), completion.reasoningArtifacts);
|
|
3528
3680
|
for (const tc of salvageHistoryCalls) {
|
|
3529
3681
|
appendToolResult(messages, tc.id, tc.id === salvagedCallId
|
|
3530
3682
|
? `Tool ${tc.name} result (exit=0, ok=true):\nSalvaged partial write: ${lineCount} lines to ${salvaged.path}`
|
|
@@ -3607,6 +3759,26 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3607
3759
|
"Finish briefly: emit the next tool call, or the final answer if the task is complete."));
|
|
3608
3760
|
continue;
|
|
3609
3761
|
}
|
|
3762
|
+
if (hitOutputLimit &&
|
|
3763
|
+
!truncatedRoundText.trim() &&
|
|
3764
|
+
assistantText.hasThinking &&
|
|
3765
|
+
truncatedBudgetRounds < 4) {
|
|
3766
|
+
truncatedBudgetRounds += 1;
|
|
3767
|
+
interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, assistantText.thinkContent);
|
|
3768
|
+
const preservedBudgetReasoning = interruptedReasoning;
|
|
3769
|
+
writeNotice("warn", "reasoning used the whole output budget — preserving it and widening the budget");
|
|
3770
|
+
commitAssistantRetry(assistantText.visible);
|
|
3771
|
+
interruptedReasoning = preservedBudgetReasoning;
|
|
3772
|
+
messages.push(recoveryUserMessage([
|
|
3773
|
+
"Your previous response spent the entire output budget on reasoning and was cut off before any visible answer. " +
|
|
3774
|
+
"Do not restart the analysis — your conclusions so far are preserved below. " +
|
|
3775
|
+
"Wrap up the reasoning now and emit the next tool call or the final answer directly.",
|
|
3776
|
+
interruptedReasoningBrief(interruptedReasoning),
|
|
3777
|
+
]
|
|
3778
|
+
.filter((part) => Boolean(part))
|
|
3779
|
+
.join("\n\n")));
|
|
3780
|
+
continue;
|
|
3781
|
+
}
|
|
3610
3782
|
const incompleteNativeStream = nativeToolCalls.length === 0 &&
|
|
3611
3783
|
streamedNativeCallNames.size > 0;
|
|
3612
3784
|
if (incompleteNativeStream) {
|
|
@@ -3622,16 +3794,22 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3622
3794
|
emptyVisibleRetries += 1;
|
|
3623
3795
|
if (emptyVisibleRetries <= 3) {
|
|
3624
3796
|
if (assistantText.hasThinking) {
|
|
3625
|
-
writeNotice("warn", "model produced only thinking — nudging it to
|
|
3797
|
+
writeNotice("warn", "model produced only thinking — preserving the reasoning and nudging it to act");
|
|
3626
3798
|
}
|
|
3627
3799
|
else {
|
|
3628
3800
|
writeNotice("warn", "model returned an empty response — nudging it to answer");
|
|
3629
3801
|
}
|
|
3630
|
-
if (assistantText.hasThinking)
|
|
3802
|
+
if (assistantText.hasThinking) {
|
|
3803
|
+
interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, assistantText.thinkContent);
|
|
3804
|
+
}
|
|
3805
|
+
const preservedReasoning = interruptedReasoning;
|
|
3806
|
+
if (assistantText.hasThinking && emptyVisibleRetries >= 2) {
|
|
3631
3807
|
retryWithoutThinking = true;
|
|
3808
|
+
}
|
|
3632
3809
|
commitAssistantRetry(assistantText.visible);
|
|
3810
|
+
interruptedReasoning = preservedReasoning;
|
|
3633
3811
|
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
3634
|
-
const
|
|
3812
|
+
const baseNudge = incompleteNativeStream
|
|
3635
3813
|
? "Your native tool call was incomplete, so nothing ran. Use exactly one complete fenced ```tool block now; do not repeat the incomplete native call."
|
|
3636
3814
|
: isPlanMode && !activePlan
|
|
3637
3815
|
? toolsAttached
|
|
@@ -3642,6 +3820,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3642
3820
|
? "No visible output. " + toolNudge(true)
|
|
3643
3821
|
: "No visible output. Emit a ```tool block or give your final answer. " +
|
|
3644
3822
|
"Do NOT hide tool calls in <think> tags — put them in the visible response.";
|
|
3823
|
+
const reasoningBrief = assistantText.hasThinking
|
|
3824
|
+
? interruptedReasoningBrief(interruptedReasoning)
|
|
3825
|
+
: undefined;
|
|
3826
|
+
const buildNudge = reasoningBrief
|
|
3827
|
+
? "Your previous response contained only reasoning and no visible answer or tool call. " +
|
|
3828
|
+
"Do not restart the analysis — your reasoning so far is preserved below. " +
|
|
3829
|
+
"Build on it and act now: emit the next tool call or the final answer.\n\n" +
|
|
3830
|
+
reasoningBrief +
|
|
3831
|
+
"\n\n" +
|
|
3832
|
+
baseNudge
|
|
3833
|
+
: baseNudge;
|
|
3645
3834
|
messages.push(recoveryUserMessage(buildNudge));
|
|
3646
3835
|
continue;
|
|
3647
3836
|
}
|
|
@@ -3653,6 +3842,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3653
3842
|
emptyVisibleRetries = 0;
|
|
3654
3843
|
truncatedBudgetRounds = 0;
|
|
3655
3844
|
retryWithoutThinking = false;
|
|
3845
|
+
interruptedReasoning = "";
|
|
3656
3846
|
}
|
|
3657
3847
|
let bareArgsOnly = false;
|
|
3658
3848
|
recoveredFromBareJson = false;
|
|
@@ -4010,7 +4200,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4010
4200
|
},
|
|
4011
4201
|
}
|
|
4012
4202
|
: normalizeToolCall({ name: tc.name, args: tc.args });
|
|
4013
|
-
return { index, id: tc.id, call, native: tc };
|
|
4203
|
+
return { index, id: tc.id, call, native: tc, wireId: tc.id };
|
|
4014
4204
|
});
|
|
4015
4205
|
}
|
|
4016
4206
|
else {
|
|
@@ -4129,8 +4319,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4129
4319
|
for (const { b, resultReason, result } of suppressedResults) {
|
|
4130
4320
|
const queued = deferredToolCalls[b.index];
|
|
4131
4321
|
const eventId = queued?.eventId ?? `tool-${++nextToolEventId}`;
|
|
4132
|
-
writeToolCall(eventId, b.call
|
|
4133
|
-
chalk.gray(` ${formatToolArgs(b.call)}`)) + "\n");
|
|
4322
|
+
writeToolCall(eventId, b.call);
|
|
4134
4323
|
alreadyPrintedIds.add(eventId);
|
|
4135
4324
|
emit({ type: "tool-start", id: eventId });
|
|
4136
4325
|
const output = resultReason.endsWith("\n")
|
|
@@ -4150,7 +4339,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4150
4339
|
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock ??
|
|
4151
4340
|
(assistantText.hasThinking && assistantText.thinkContent
|
|
4152
4341
|
? { text: assistantText.thinkContent }
|
|
4153
|
-
: undefined));
|
|
4342
|
+
: undefined), completion.reasoningArtifacts);
|
|
4154
4343
|
for (const { b, resultReason } of suppressedResults) {
|
|
4155
4344
|
appendToolResult(messages, b.id, deniedContent(b, resultReason), b.call.name, false);
|
|
4156
4345
|
}
|
|
@@ -4160,7 +4349,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4160
4349
|
bound
|
|
4161
4350
|
.map((b) => `\`\`\`tool\n${JSON.stringify(b.call)}\n\`\`\``)
|
|
4162
4351
|
.join("\n\n");
|
|
4163
|
-
pushAssistantHistory(standardizedContent);
|
|
4352
|
+
pushAssistantHistory(standardizedContent, completion);
|
|
4164
4353
|
for (const { b, resultReason } of suppressedResults) {
|
|
4165
4354
|
messages.push({ role: "tool", content: deniedContent(b, resultReason) });
|
|
4166
4355
|
}
|
|
@@ -4213,31 +4402,24 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4213
4402
|
formatToolArgs(finalCall) !== formatToolArgs(deferred.call));
|
|
4214
4403
|
if (stale) {
|
|
4215
4404
|
deferred.call = finalCall;
|
|
4216
|
-
const refreshedLine = chalk.cyan(` ▶ ${finalCall.name}`) +
|
|
4217
|
-
chalk.gray(` ${formatToolArgs(finalCall)}`);
|
|
4218
|
-
deferred.rendered =
|
|
4219
|
-
styleToolChatter(finalCall, refreshedLine) + "\n";
|
|
4220
4405
|
}
|
|
4221
|
-
if (!deferred.shown) {
|
|
4222
|
-
writeToolCall(deferred.eventId, deferred.call
|
|
4406
|
+
if (!deferred.shown || stale) {
|
|
4407
|
+
writeToolCall(deferred.eventId, deferred.call);
|
|
4223
4408
|
deferred.shown = true;
|
|
4224
4409
|
}
|
|
4225
|
-
else if (stale) {
|
|
4226
|
-
writeToolCall(deferred.eventId, deferred.call, "");
|
|
4227
|
-
}
|
|
4228
4410
|
}
|
|
4229
4411
|
if (historyNativeCalls.length) {
|
|
4230
4412
|
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock ??
|
|
4231
4413
|
(assistantText.hasThinking && assistantText.thinkContent
|
|
4232
4414
|
? { text: assistantText.thinkContent }
|
|
4233
|
-
: undefined));
|
|
4415
|
+
: undefined), completion.reasoningArtifacts);
|
|
4234
4416
|
}
|
|
4235
4417
|
else {
|
|
4236
4418
|
const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
|
|
4237
4419
|
allCalls
|
|
4238
4420
|
.map((c) => `\`\`\`tool\n${JSON.stringify(c)}\n\`\`\``)
|
|
4239
4421
|
.join("\n\n");
|
|
4240
|
-
pushAssistantHistory(standardizedContent);
|
|
4422
|
+
pushAssistantHistory(standardizedContent, completion);
|
|
4241
4423
|
}
|
|
4242
4424
|
const scopeForBatch = await loadScope().catch(() => undefined);
|
|
4243
4425
|
const isParallelSafe = (c) => {
|
|
@@ -4543,6 +4725,43 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4543
4725
|
session.pendingTaskBatch.value = undefined;
|
|
4544
4726
|
}
|
|
4545
4727
|
}
|
|
4728
|
+
const replayExecutedOccurrence = (bc, uiId) => {
|
|
4729
|
+
const prior = bc.wireId
|
|
4730
|
+
? executedWireOccurrences.get(bc.wireId)
|
|
4731
|
+
: undefined;
|
|
4732
|
+
if (!prior)
|
|
4733
|
+
return undefined;
|
|
4734
|
+
const notice = "This exact provider tool call already executed this turn. " +
|
|
4735
|
+
"The earlier result is replayed below; the tool did not run again.";
|
|
4736
|
+
const output = `${notice}\n\n${prior.contextOutput}`;
|
|
4737
|
+
const result = { ...prior.result, output, suppressedRepeat: true };
|
|
4738
|
+
if (!alreadyPrintedIds.has(uiId)) {
|
|
4739
|
+
writeToolCall(uiId, bc.call);
|
|
4740
|
+
alreadyPrintedIds.add(uiId);
|
|
4741
|
+
emit({ type: "tool-start", id: uiId });
|
|
4742
|
+
}
|
|
4743
|
+
writeToolOutput(uiId, output.endsWith("\n") ? output : `${output}\n`);
|
|
4744
|
+
emitToolResult(uiId, result, output);
|
|
4745
|
+
return {
|
|
4746
|
+
call: bc.call,
|
|
4747
|
+
result,
|
|
4748
|
+
contextOutput: output,
|
|
4749
|
+
ok: prior.ok,
|
|
4750
|
+
suppressedRepeat: true,
|
|
4751
|
+
};
|
|
4752
|
+
};
|
|
4753
|
+
const rememberExecutedOccurrence = (bc, res) => {
|
|
4754
|
+
if (!bc.wireId || executedWireOccurrences.has(bc.wireId))
|
|
4755
|
+
return;
|
|
4756
|
+
if (!res.ok || res.aborted || res.suppressedRepeat)
|
|
4757
|
+
return;
|
|
4758
|
+
executedWireOccurrences.set(bc.wireId, {
|
|
4759
|
+
call: res.call,
|
|
4760
|
+
result: res.result,
|
|
4761
|
+
contextOutput: res.contextOutput,
|
|
4762
|
+
ok: res.ok,
|
|
4763
|
+
});
|
|
4764
|
+
};
|
|
4546
4765
|
const groups = groupToolCallsForExecution(allCalls, isParallelSafe, PARALLEL_LIMIT);
|
|
4547
4766
|
for (const group of groups) {
|
|
4548
4767
|
if (aborted || awaitingPlanApproval)
|
|
@@ -4556,8 +4775,14 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4556
4775
|
callIds[bc.index] = `tool-${++nextToolEventId}`;
|
|
4557
4776
|
}
|
|
4558
4777
|
const id = callIds[bc.index];
|
|
4778
|
+
const replayed = replayExecutedOccurrence(bc, id);
|
|
4779
|
+
if (replayed) {
|
|
4780
|
+
recordResult(bc, replayed);
|
|
4781
|
+
continue;
|
|
4782
|
+
}
|
|
4559
4783
|
const res = await executeSingleTool(call, id, options.signal || new AbortController().signal);
|
|
4560
4784
|
recordResult(bc, res);
|
|
4785
|
+
rememberExecutedOccurrence(bc, res);
|
|
4561
4786
|
}
|
|
4562
4787
|
else {
|
|
4563
4788
|
// Concurrent group — BoundCall via Map; record in document order.
|
|
@@ -4573,9 +4798,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4573
4798
|
groupBound.push(bc);
|
|
4574
4799
|
uiIds.push(callIds[bc.index]);
|
|
4575
4800
|
}
|
|
4576
|
-
const results = await Promise.all(groupBound.map((bc, k) =>
|
|
4801
|
+
const results = await Promise.all(groupBound.map((bc, k) => {
|
|
4802
|
+
const replayed = replayExecutedOccurrence(bc, uiIds[k]);
|
|
4803
|
+
if (replayed)
|
|
4804
|
+
return replayed;
|
|
4805
|
+
return executeSingleTool(bc.call, uiIds[k], options.signal || new AbortController().signal);
|
|
4806
|
+
}));
|
|
4577
4807
|
for (let k = 0; k < results.length; k += 1) {
|
|
4578
4808
|
recordResult(groupBound[k], results[k]);
|
|
4809
|
+
rememberExecutedOccurrence(groupBound[k], results[k]);
|
|
4579
4810
|
}
|
|
4580
4811
|
}
|
|
4581
4812
|
}
|