@pentoshi/clai 3.11.6 → 3.11.8
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/finalize-gate.d.ts +57 -0
- package/dist/agent/finalize-gate.js +146 -0
- package/dist/agent/finalize-gate.js.map +1 -0
- package/dist/agent/interrupted-reasoning.d.ts +6 -0
- package/dist/agent/interrupted-reasoning.js +38 -0
- package/dist/agent/interrupted-reasoning.js.map +1 -0
- package/dist/agent/runner.js +148 -189
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/stream-recovery.d.ts +4 -1
- package/dist/agent/stream-recovery.js +20 -9
- package/dist/agent/stream-recovery.js.map +1 -1
- package/dist/llm/anthropic.js +16 -1
- package/dist/llm/anthropic.js.map +1 -1
- package/dist/llm/aws-mantle.js +14 -1
- package/dist/llm/aws-mantle.js.map +1 -1
- package/dist/llm/gemini.js +3 -1
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/http.d.ts +8 -2
- package/dist/llm/http.js +62 -21
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/modal.js +3 -3
- package/dist/llm/modal.js.map +1 -1
- package/dist/llm/nvidia.js +2 -3
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/ollama.js +3 -1
- package/dist/llm/ollama.js.map +1 -1
- package/dist/prompts/index.d.ts +7 -0
- package/dist/prompts/index.js +23 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/version.generated.d.ts +2 -2
- package/dist/version.generated.js +2 -2
- package/package.json +1 -1
package/dist/agent/runner.js
CHANGED
|
@@ -58,12 +58,13 @@ import { buildDurableEnvelope, WorkLedger, } from "./durable-envelope.js";
|
|
|
58
58
|
import { COMPACTION_SYSTEM_PROMPT, } from "./compaction-summary.js";
|
|
59
59
|
import { maybeAppendPlanModeReminder, PLAN_REMINDER_TOAST, } from "./plan-mode-reminders.js";
|
|
60
60
|
import { LoopGuard } from "./loop-guard.js";
|
|
61
|
+
import { appendInterruptedReasoning, interruptedReasoningBrief, isMeaningfulResumptionYield, } from "./interrupted-reasoning.js";
|
|
61
62
|
import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-attempt.js";
|
|
62
63
|
import { resolveRequestBudget } from "./request-budget.js";
|
|
63
64
|
import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
|
|
64
65
|
import { stat } from "node:fs/promises";
|
|
65
66
|
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,
|
|
67
|
+
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
67
68
|
import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
|
|
68
69
|
import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
|
|
69
70
|
import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
|
|
@@ -74,7 +75,8 @@ import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLoc
|
|
|
74
75
|
import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
|
|
75
76
|
import { buildContinueOrientation, looksLikeContinueOrResumePrompt, } from "./continue-orient.js";
|
|
76
77
|
import { detectPackageManager } from "./workspace-orient.js";
|
|
77
|
-
import { budgetRemaining, consumeBudget, createRecoveryBudgets,
|
|
78
|
+
import { budgetRemaining, consumeBudget, createRecoveryBudgets, } from "./must-continue.js";
|
|
79
|
+
import { chooseFinalizeRecovery } from "./finalize-gate.js";
|
|
78
80
|
import { outOfScopeToolMessage, scopeContextMessage } from "./scope-context.js";
|
|
79
81
|
import { EngagementPolicyEngine, actionFromUrl, engagementActionsForToolCall, evaluateEngagementAction, } from "../safety/engagement-policy.js";
|
|
80
82
|
import { patchPlanMeta } from "../store/plan.js";
|
|
@@ -169,6 +171,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
169
171
|
// at the top of every loop iteration.
|
|
170
172
|
let visibleCommitted = false;
|
|
171
173
|
let interruptedVisible = "";
|
|
174
|
+
let interruptedReasoning = "";
|
|
175
|
+
let lowYieldResumptions = 0;
|
|
172
176
|
const trimExactContinuationOverlap = (previous, current, minLength = 32) => {
|
|
173
177
|
if (previous.length > 0 && current.startsWith(previous)) {
|
|
174
178
|
return current.slice(previous.length);
|
|
@@ -540,6 +544,9 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
540
544
|
// Request, project, workspace, recovery, scope, and plan state are appended
|
|
541
545
|
// later as system-marked turns so a changing byte cannot invalidate the
|
|
542
546
|
// constitution (and, on Anthropic, the native tool schemas before it).
|
|
547
|
+
// The red-team methodology block is ~940 tokens on every request. Attach it
|
|
548
|
+
// only when this turn is actually a remote-security engagement.
|
|
549
|
+
const pentestPromptTurn = pentestLikeTurn || activePlan?.kind === "pentest";
|
|
543
550
|
const buildStableSystemContent = (native) => {
|
|
544
551
|
const reliability = getReliabilityPolicy();
|
|
545
552
|
const visionAvailable = modelSupportsVision(provider, model);
|
|
@@ -551,6 +558,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
551
558
|
imageView: visionAvailable,
|
|
552
559
|
// E6: slim native constitution when API tool schemas are attached.
|
|
553
560
|
...(native ? { slimNative: reliability.slimNativePrompt } : {}),
|
|
561
|
+
...(pentestPromptTurn ? { pentest: true } : {}),
|
|
554
562
|
});
|
|
555
563
|
};
|
|
556
564
|
const systemSections = [renderRequestEnvironmentContext()];
|
|
@@ -3095,6 +3103,30 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3095
3103
|
let accumulatedText = "";
|
|
3096
3104
|
const callIds = [];
|
|
3097
3105
|
let streamedCallsCount = 0;
|
|
3106
|
+
// A model can think silently for minutes. Without a heartbeat the UI
|
|
3107
|
+
// shows a frozen label and the turn looks hung, so surface elapsed time
|
|
3108
|
+
// and the current phase on a timer rather than only on token arrival.
|
|
3109
|
+
const streamStartedAt = Date.now();
|
|
3110
|
+
const streamPhase = () => {
|
|
3111
|
+
const seconds = Math.round((Date.now() - streamStartedAt) / 1000);
|
|
3112
|
+
const elapsed = seconds < 60
|
|
3113
|
+
? `${seconds}s`
|
|
3114
|
+
: `${Math.floor(seconds / 60)}m${String(seconds % 60).padStart(2, "0")}s`;
|
|
3115
|
+
if (generatedTokens > 0 && !inThinking) {
|
|
3116
|
+
return `generating response · ${generatedTokens} tokens · ${elapsed}`;
|
|
3117
|
+
}
|
|
3118
|
+
if (sawReasoning)
|
|
3119
|
+
return `thinking · ${elapsed}`;
|
|
3120
|
+
return `waiting for model · ${elapsed}`;
|
|
3121
|
+
};
|
|
3122
|
+
const heartbeat = setInterval(() => {
|
|
3123
|
+
const text = streamPhase();
|
|
3124
|
+
if (writesDirectly)
|
|
3125
|
+
spinner.setLabel(text);
|
|
3126
|
+
else
|
|
3127
|
+
emit({ type: "status", text });
|
|
3128
|
+
}, 10_000);
|
|
3129
|
+
heartbeat.unref?.();
|
|
3098
3130
|
const deferredToolCalls = [];
|
|
3099
3131
|
const deltaParser = writesDirectly
|
|
3100
3132
|
? undefined
|
|
@@ -3336,6 +3368,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3336
3368
|
resetStreamRecoveryState(recoveryState);
|
|
3337
3369
|
allowModelFallback = false;
|
|
3338
3370
|
preferModelFallback = false;
|
|
3371
|
+
lowYieldResumptions = 0;
|
|
3339
3372
|
}
|
|
3340
3373
|
catch (streamError) {
|
|
3341
3374
|
// User cancelled (double-Esc) — never try to recover, just stop.
|
|
@@ -3363,47 +3396,89 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3363
3396
|
// We only rethrow (stop the turn) in the worst case: every approach
|
|
3364
3397
|
// for that failure class is exhausted or the total budget is spent.
|
|
3365
3398
|
const failureKind = classifyStreamFailure(streamError);
|
|
3399
|
+
const partialStream = streamAlreadyEmitted(streamError) || accumulatedText.length > 0;
|
|
3400
|
+
const partial = rememberThinkingFromText(accumulatedText);
|
|
3401
|
+
const rawPartialVisible = partialStream
|
|
3402
|
+
? textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible)
|
|
3403
|
+
: "";
|
|
3404
|
+
const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
|
|
3405
|
+
const partialVisible = normalizedPartialVisible.trim();
|
|
3406
|
+
// A route that drops after a handful of characters is not making
|
|
3407
|
+
// progress, however many times it is retried. Only a substantial
|
|
3408
|
+
// yield unlocks the generous resumption budget; anything less is
|
|
3409
|
+
// charged to the failure class and escalates to another route.
|
|
3410
|
+
const meaningfulProgress = partialStream &&
|
|
3411
|
+
isMeaningfulResumptionYield(normalizedPartialVisible.length + partial.thinkContent.length);
|
|
3412
|
+
if (partialStream) {
|
|
3413
|
+
lowYieldResumptions = meaningfulProgress
|
|
3414
|
+
? 0
|
|
3415
|
+
: lowYieldResumptions + 1;
|
|
3416
|
+
}
|
|
3366
3417
|
const plan = planStreamRecovery({
|
|
3367
3418
|
kind: failureKind,
|
|
3368
3419
|
state: recoveryState,
|
|
3420
|
+
progressed: meaningfulProgress,
|
|
3369
3421
|
});
|
|
3370
|
-
const
|
|
3422
|
+
const terminalFailure = plan.action === "give-up";
|
|
3371
3423
|
let continuationNudge = "";
|
|
3372
3424
|
if (partialStream) {
|
|
3373
3425
|
spinner.stop();
|
|
3374
3426
|
deltaParser?.finish();
|
|
3375
|
-
const partial = rememberThinkingFromText(accumulatedText);
|
|
3376
3427
|
const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
|
|
3377
|
-
const rawPartialVisible = textBeforeToolCall(stripThinking(collapseRepeatedText(accumulatedText)).visible);
|
|
3378
|
-
const normalizedPartialVisible = trimExactContinuationOverlap(interruptedVisible, rawPartialVisible);
|
|
3379
|
-
const partialVisible = normalizedPartialVisible.trim();
|
|
3380
3428
|
if (partialVisible) {
|
|
3381
|
-
|
|
3382
|
-
|
|
3429
|
+
// Finalizing here would close the streaming card and split one
|
|
3430
|
+
// answer across a card per interruption. Keep it open and let
|
|
3431
|
+
// the single commit below paint the stitched text; only a
|
|
3432
|
+
// terminal failure has to flush it now.
|
|
3433
|
+
if (terminalFailure) {
|
|
3434
|
+
writeAssistantMessage(interruptedVisible + normalizedPartialVisible);
|
|
3435
|
+
}
|
|
3436
|
+
else {
|
|
3437
|
+
visibleCommitted = true;
|
|
3438
|
+
}
|
|
3439
|
+
messages.push({
|
|
3440
|
+
role: "assistant",
|
|
3441
|
+
content: sanitizeAssistantText(partialVisible),
|
|
3442
|
+
});
|
|
3383
3443
|
interruptedVisible += normalizedPartialVisible;
|
|
3384
3444
|
}
|
|
3385
|
-
else if (!writesDirectly) {
|
|
3445
|
+
else if (terminalFailure && !writesDirectly) {
|
|
3386
3446
|
emit({ type: "assistant-message", text: "" });
|
|
3387
3447
|
}
|
|
3388
3448
|
if (partial.hasThinking && !hasShownToolCall) {
|
|
3389
3449
|
writeThinkingBlock(partial.thinkContent);
|
|
3390
3450
|
}
|
|
3451
|
+
if (partial.hasThinking) {
|
|
3452
|
+
interruptedReasoning = appendInterruptedReasoning(interruptedReasoning, partial.thinkContent);
|
|
3453
|
+
}
|
|
3391
3454
|
for (const deferred of deferredToolCalls) {
|
|
3392
3455
|
if (!deferred.shown || deferred.call.name === "…")
|
|
3393
3456
|
continue;
|
|
3394
3457
|
writeToolBlocked(deferred.eventId, deferred.call.name, "Incomplete tool call discarded after the provider stream was interrupted.", chalk.yellow(" ⚠ incomplete tool call discarded after stream interruption\n"));
|
|
3395
3458
|
}
|
|
3396
|
-
continuationNudge =
|
|
3397
|
-
|
|
3398
|
-
|
|
3459
|
+
continuationNudge = [
|
|
3460
|
+
partialVisible
|
|
3461
|
+
? "The provider stream was interrupted after partial output. Continue from the exact stopping point without repeating prior text. Any incomplete tool call was discarded and must be reissued in full."
|
|
3462
|
+
: "The provider stream was interrupted before any answer was produced. Any incomplete tool call was discarded and must be reissued in full. Do not restart your analysis from the beginning.",
|
|
3463
|
+
interruptedReasoningBrief(interruptedReasoning),
|
|
3464
|
+
]
|
|
3465
|
+
.filter((part) => Boolean(part))
|
|
3466
|
+
.join("\n\n");
|
|
3467
|
+
const restartNotice = terminalFailure
|
|
3399
3468
|
? "partial response preserved before terminal provider failure"
|
|
3400
|
-
:
|
|
3469
|
+
: lowYieldResumptions > 1
|
|
3470
|
+
? `route is dropping after almost no output (${lowYieldResumptions} in a row) — switching model`
|
|
3471
|
+
: "partial response preserved — resuming from the interruption";
|
|
3401
3472
|
writeNotice("warn", restartNotice, chalk.yellow(` ⚠ ${restartNotice}\n`));
|
|
3402
3473
|
}
|
|
3403
|
-
if (
|
|
3474
|
+
if (terminalFailure) {
|
|
3404
3475
|
throw streamError;
|
|
3405
3476
|
}
|
|
3406
|
-
recordRecoveryAttempt(recoveryState, failureKind);
|
|
3477
|
+
recordRecoveryAttempt(recoveryState, failureKind, meaningfulProgress);
|
|
3478
|
+
if (lowYieldResumptions > 1) {
|
|
3479
|
+
allowModelFallback = true;
|
|
3480
|
+
preferModelFallback = true;
|
|
3481
|
+
}
|
|
3407
3482
|
if (plan.notice) {
|
|
3408
3483
|
writeNotice("warn", plan.notice, chalk.yellow(` ⚠ ${plan.notice}\n`));
|
|
3409
3484
|
}
|
|
@@ -3434,6 +3509,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3434
3509
|
}
|
|
3435
3510
|
finally {
|
|
3436
3511
|
// Always clear the spinner — abort, network error, or success.
|
|
3512
|
+
clearInterval(heartbeat);
|
|
3437
3513
|
spinner.stop();
|
|
3438
3514
|
}
|
|
3439
3515
|
if (responderDelivery) {
|
|
@@ -3487,7 +3563,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3487
3563
|
const commitAssistantRetry = (historyText) => {
|
|
3488
3564
|
const hasShownToolCall = deferredToolCalls.some((entry) => entry.shown);
|
|
3489
3565
|
if (!hasShownToolCall) {
|
|
3490
|
-
const displayText = textBeforeToolCall(collapseRepeatedText(
|
|
3566
|
+
const displayText = textBeforeToolCall(collapseRepeatedText(canonicalAssistantVisible)).trim();
|
|
3491
3567
|
if (displayText) {
|
|
3492
3568
|
writeAssistantMessage(displayText);
|
|
3493
3569
|
}
|
|
@@ -3503,6 +3579,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3503
3579
|
}
|
|
3504
3580
|
pushAssistantHistory(historyText);
|
|
3505
3581
|
interruptedVisible = "";
|
|
3582
|
+
interruptedReasoning = "";
|
|
3583
|
+
lowYieldResumptions = 0;
|
|
3506
3584
|
};
|
|
3507
3585
|
// Only emit a thinking-block event when the classic renderer is
|
|
3508
3586
|
// active (writesDirectly / no deltaParser). In TUI v2 the
|
|
@@ -3949,182 +4027,61 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3949
4027
|
"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."));
|
|
3950
4028
|
continue;
|
|
3951
4029
|
}
|
|
3952
|
-
const
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
3961
|
-
|
|
3962
|
-
|
|
3963
|
-
(
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
3969
|
-
|
|
3970
|
-
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
3989
|
-
|
|
3990
|
-
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
3997
|
-
}
|
|
3998
|
-
else if (budgetRemaining(recovery, "actionIntent")) {
|
|
3999
|
-
action = recoveryForNarration(toolsAttached, "generic");
|
|
4000
|
-
}
|
|
4001
|
-
if (action) {
|
|
4002
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4003
|
-
commitAssistantRetry(assistantText.visible);
|
|
4004
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4005
|
-
continue;
|
|
4006
|
-
}
|
|
4007
|
-
}
|
|
4008
|
-
if (freshWebSearchRequired &&
|
|
4009
|
-
!sawFreshWebSearch &&
|
|
4010
|
-
budgetRemaining(recovery, "freshnessUsed")) {
|
|
4011
|
-
const action = recoveryForFreshness(freshnessGuardMessage() +
|
|
4012
|
-
(toolsAttached
|
|
4013
|
-
? " Call the web_search tool now."
|
|
4014
|
-
: " Reply with ONLY a fenced ```tool block for web.search now."));
|
|
4015
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4016
|
-
commitAssistantRetry(assistantText.visible);
|
|
4017
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4018
|
-
continue;
|
|
4019
|
-
}
|
|
4020
|
-
if (isPlanMode &&
|
|
4021
|
-
!informationalQuery &&
|
|
4022
|
-
!idleOrSocialPrompt &&
|
|
4023
|
-
budgetRemaining(recovery, "forcePlan")) {
|
|
4024
|
-
const planAtEnd = await loadPlan(session.sessionId).catch(() => undefined);
|
|
4025
|
-
if (!planAtEnd && !sawPlanCreateOk) {
|
|
4026
|
-
const action = recoveryForMissingPlan(toolsAttached);
|
|
4027
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4028
|
-
commitAssistantRetry(assistantText.visible);
|
|
4029
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4030
|
-
continue;
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
if (buildLike &&
|
|
4034
|
-
!pentestLike &&
|
|
4035
|
-
!pentestSession &&
|
|
4036
|
-
session.planApproved.value &&
|
|
4037
|
-
featureAppAsk &&
|
|
4038
|
-
!sawFeatureImplWrite &&
|
|
4039
|
-
(sawScaffoldOk || sawLocalAppMaterialWork) &&
|
|
4040
|
-
productiveSteps > 0 &&
|
|
4041
|
-
budgetRemaining(recovery, "featureImpl")) {
|
|
4042
|
-
const action = recoveryForMissingFeature(getActiveProjectRoot());
|
|
4043
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4044
|
-
commitAssistantRetry(assistantText.visible);
|
|
4045
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4046
|
-
continue;
|
|
4047
|
-
}
|
|
4048
|
-
if (buildLike &&
|
|
4049
|
-
!pentestLike &&
|
|
4050
|
-
!pentestSession &&
|
|
4051
|
-
budgetRemaining(recovery, "runtimeVerify") &&
|
|
4052
|
-
(!featureAppAsk || sawFeatureImplWrite)) {
|
|
4053
|
-
const runtimePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
4054
|
-
// Durable plan evidence or multi-signal proof this turn is enough
|
|
4055
|
-
const planRuntimeOk = Boolean(runtimePlan && planHasVerifiedRuntime(runtimePlan));
|
|
4056
|
-
const sessionRuntimeOk = sawServerStart &&
|
|
4057
|
-
(sawServerTail || sawLocalHttpProbe || planRuntimeOk);
|
|
4058
|
-
if (!planRuntimeOk && !sessionRuntimeOk) {
|
|
4059
|
-
const codingPlanFinished = Boolean(runtimePlan &&
|
|
4060
|
-
session.planApproved.value &&
|
|
4061
|
-
runtimePlan.kind !== "pentest" &&
|
|
4062
|
-
runtimePlan.tasks.length > 0 &&
|
|
4063
|
-
runtimePlan.tasks.every((task) => task.state === "done" || task.state === "skipped"));
|
|
4064
|
-
const freestyleLocalAppDone = !session.planApproved.value &&
|
|
4065
|
-
sawLocalAppMaterialWork &&
|
|
4066
|
-
productiveSteps > 0 &&
|
|
4067
|
-
freestyleClaimsAppReady(cleaned) &&
|
|
4068
|
-
(getActiveProjectRoot() !== undefined ||
|
|
4069
|
-
/\b(?:npm|pnpm|yarn|bun)\s+run\s+dev\b/i.test(cleaned) ||
|
|
4070
|
-
/\bopen\s+http:\/\/localhost\b/i.test(cleaned));
|
|
4071
|
-
if (codingPlanFinished || freestyleLocalAppDone) {
|
|
4072
|
-
const action = recoveryForRuntimeVerify(getActiveProjectRoot());
|
|
4073
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4074
|
-
commitAssistantRetry(assistantText.visible);
|
|
4075
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4076
|
-
continue;
|
|
4030
|
+
const deferResponderReport = session.planApproved.value &&
|
|
4031
|
+
budgetRemaining(recovery, "prematureComplete")
|
|
4032
|
+
? shouldYieldForDeclaredResponderDependency(livePlanAtCompletion, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId)
|
|
4033
|
+
: false;
|
|
4034
|
+
const finalizeRecovery = chooseFinalizeRecovery({
|
|
4035
|
+
cleaned,
|
|
4036
|
+
recovery,
|
|
4037
|
+
toolsAttached,
|
|
4038
|
+
productiveSteps,
|
|
4039
|
+
planApproved: session.planApproved.value,
|
|
4040
|
+
planHasOpenWork: planHasOpenWorkNow,
|
|
4041
|
+
activePlanExists: Boolean(activePlan),
|
|
4042
|
+
wantsAction,
|
|
4043
|
+
narratedAction,
|
|
4044
|
+
narratedWebAction,
|
|
4045
|
+
isPlanMode,
|
|
4046
|
+
buildLikeTurn,
|
|
4047
|
+
pentestLikeTurn,
|
|
4048
|
+
buildLike,
|
|
4049
|
+
pentestLike,
|
|
4050
|
+
pentestSession,
|
|
4051
|
+
informationalQuery,
|
|
4052
|
+
idleOrSocialPrompt,
|
|
4053
|
+
freshWebSearchRequired,
|
|
4054
|
+
freshnessGuardText: freshWebSearchRequired
|
|
4055
|
+
? freshnessGuardMessage()
|
|
4056
|
+
: "",
|
|
4057
|
+
sawFreshWebSearch,
|
|
4058
|
+
sawPlanCreateOk,
|
|
4059
|
+
sawFeatureImplWrite,
|
|
4060
|
+
sawScaffoldOk,
|
|
4061
|
+
sawLocalAppMaterialWork,
|
|
4062
|
+
sawServerStart,
|
|
4063
|
+
sawServerTail,
|
|
4064
|
+
sawLocalHttpProbe,
|
|
4065
|
+
sawFailedLocalHttpProbe,
|
|
4066
|
+
sawActivePentestTest,
|
|
4067
|
+
sawSuccessfulMutation,
|
|
4068
|
+
featureAppAsk,
|
|
4069
|
+
projectRoot: getActiveProjectRoot(),
|
|
4070
|
+
plan: livePlanAtCompletion
|
|
4071
|
+
? {
|
|
4072
|
+
kind: livePlanAtCompletion.kind,
|
|
4073
|
+
hasVerifiedRuntime: planHasVerifiedRuntime(livePlanAtCompletion),
|
|
4074
|
+
tasks: livePlanAtCompletion.tasks,
|
|
4077
4075
|
}
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
sawFailedLocalHttpProbe &&
|
|
4084
|
-
!sawLocalHttpProbe &&
|
|
4085
|
-
budgetRemaining(recovery, "failedProbe") &&
|
|
4086
|
-
cleaned.trim().length > 0) {
|
|
4087
|
-
const action = recoveryForFailedProbe();
|
|
4088
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4089
|
-
commitAssistantRetry(assistantText.visible);
|
|
4090
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4091
|
-
continue;
|
|
4092
|
-
}
|
|
4093
|
-
if ((pentestLike || pentestSession) &&
|
|
4094
|
-
budgetRemaining(recovery, "shallowPentest") &&
|
|
4095
|
-
looksLikeShallowPentestReport(cleaned, {
|
|
4096
|
-
productiveSteps,
|
|
4097
|
-
sawActiveTest: sawActivePentestTest,
|
|
4098
|
-
})) {
|
|
4099
|
-
const action = recoveryForShallowPentest();
|
|
4100
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4076
|
+
: undefined,
|
|
4077
|
+
deferResponderReport,
|
|
4078
|
+
});
|
|
4079
|
+
if (finalizeRecovery) {
|
|
4080
|
+
consumeBudget(recovery, finalizeRecovery.budgetKey);
|
|
4101
4081
|
commitAssistantRetry(assistantText.visible);
|
|
4102
|
-
messages.push(recoveryUserMessage(
|
|
4082
|
+
messages.push(recoveryUserMessage(finalizeRecovery.message));
|
|
4103
4083
|
continue;
|
|
4104
4084
|
}
|
|
4105
|
-
if (session.planApproved.value &&
|
|
4106
|
-
budgetRemaining(recovery, "prematureComplete")) {
|
|
4107
|
-
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
4108
|
-
const unfinished = livePlan?.tasks.filter((task) => !task.responderOwned &&
|
|
4109
|
-
(task.state === "pending" || task.state === "in_progress"));
|
|
4110
|
-
const deferReport = shouldYieldForDeclaredResponderDependency(livePlan, jobManager.getRunningJobs(session.sessionId), jobManager.getPendingNotifications(session.sessionId), responderWakeNotificationId);
|
|
4111
|
-
if (livePlan &&
|
|
4112
|
-
unfinished &&
|
|
4113
|
-
unfinished.length > 0 &&
|
|
4114
|
-
!deferReport) {
|
|
4115
|
-
const next = unfinished[0];
|
|
4116
|
-
const action = recoveryForPrematureComplete({
|
|
4117
|
-
unfinished,
|
|
4118
|
-
next,
|
|
4119
|
-
pentest: livePlan.kind === "pentest" || pentestSession,
|
|
4120
|
-
errorFix: errorFixNarration,
|
|
4121
|
-
});
|
|
4122
|
-
consumeBudget(recovery, action.budgetKey);
|
|
4123
|
-
commitAssistantRetry(assistantText.visible);
|
|
4124
|
-
messages.push(recoveryUserMessage(action.message));
|
|
4125
|
-
continue;
|
|
4126
|
-
}
|
|
4127
|
-
}
|
|
4128
4085
|
let outcomeStatus = "succeeded";
|
|
4129
4086
|
const remainingCriteria = [];
|
|
4130
4087
|
if (session.planApproved.value) {
|
|
@@ -4186,6 +4143,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4186
4143
|
emit({ type: "assistant-message", text: "" });
|
|
4187
4144
|
}
|
|
4188
4145
|
interruptedVisible = "";
|
|
4146
|
+
interruptedReasoning = "";
|
|
4147
|
+
lowYieldResumptions = 0;
|
|
4189
4148
|
let bound = [];
|
|
4190
4149
|
if (nativeToolCalls.length) {
|
|
4191
4150
|
bound = nativeToolCalls.map((tc, index) => {
|