@pentoshi/clai 3.11.29 → 3.11.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/compaction-summary.js +1 -1
- package/dist/agent/compaction-summary.js.map +1 -1
- package/dist/agent/continue-orient.js +1 -1
- package/dist/agent/continue-orient.js.map +1 -1
- package/dist/agent/finalize-gate.d.ts +0 -3
- package/dist/agent/finalize-gate.js +2 -10
- package/dist/agent/finalize-gate.js.map +1 -1
- package/dist/agent/loop-guard.d.ts +12 -3
- package/dist/agent/loop-guard.js +237 -41
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/must-continue.d.ts +1 -3
- package/dist/agent/must-continue.js +0 -16
- package/dist/agent/must-continue.js.map +1 -1
- package/dist/agent/plan-tool.js +1 -1
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/runner.js +73 -139
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/task-analyzer.js +1 -1
- package/dist/agent/task-analyzer.js.map +1 -1
- package/dist/agent/tool-call-parser.d.ts +2 -4
- package/dist/agent/tool-call-parser.js +109 -59
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/agent/workspace-orient.js +1 -1
- package/dist/agent/workspace-orient.js.map +1 -1
- package/dist/llm/capabilities.d.ts +2 -2
- package/dist/llm/capabilities.js +9 -11
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/http.js +3 -1
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/router.js +9 -5
- package/dist/llm/router.js.map +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/index.js +13 -6
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +6 -4
- package/dist/tui-v2/rendering/incremental-strip.js +1 -1
- package/dist/tui-v2/rendering/incremental-strip.js.map +1 -1
- package/dist/tui-v2/rendering/strip-tool-surfaces.js +10 -0
- package/dist/tui-v2/rendering/strip-tool-surfaces.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
|
@@ -6,7 +6,7 @@ import { classifyStreamFailure, planStreamRecovery, recordRecoveryAttempt, creat
|
|
|
6
6
|
import { modelSupportsVision, resolveToolDialect } from "../llm/capabilities.js";
|
|
7
7
|
import { syntheticToolCallId, isTextOnlyModel, markTextOnlyModel, fromWireName, } from "../llm/tool-protocol.js";
|
|
8
8
|
import { sanitizeAssistantText } from "../ui/ansi-box.js";
|
|
9
|
-
import { randomUUID } from "node:crypto";
|
|
9
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
10
10
|
import { jobManager, } from "../tools/jobs.js";
|
|
11
11
|
import { isResponderResultLedgerMessage, responderContextMessage, upsertResponderContextMessage, upsertResponderResultLedger, } from "./responder-context.js";
|
|
12
12
|
import { agentModeDirective, planModeDirective, renderAgentSystemPrompt, renderCompactAgentSystemPrompt, renderRequestEnvironmentContext, scratchDirFor, toolNudge, } from "../prompts/index.js";
|
|
@@ -63,14 +63,14 @@ import { CompactionAttemptLedger, compactionAttemptKey, } from "./compaction-att
|
|
|
63
63
|
import { loadPlan, mutatePlan, markTask, appendPlanTask, readyPlanTasks, foregroundRemaining, responderOpenTasks, isPlanTerminal, isPlanSuccessful, } from "../store/plan.js";
|
|
64
64
|
import { stat } from "node:fs/promises";
|
|
65
65
|
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,
|
|
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, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
67
67
|
import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
|
|
68
68
|
import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
|
|
69
69
|
import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
|
|
70
70
|
import { renderPlanForTerminal, planContextMessage, upsertPlanContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
|
|
71
71
|
import { readDeclaredParentTaskId, resolveResponderParent, isExplicitResponderDelegation, delegationTaskTitle, } from "./responder-parent.js";
|
|
72
72
|
import { readTaskUpdateArgs, distinctAdvancingTaskIds, isSimultaneousTaskAdvance, batchUpdateSignature, buildMultiUpdateReminder, buildMultiOpenRejection, multiUpdateToast, multiOpenToast, openingTaskIds, } from "./task-sync.js";
|
|
73
|
-
import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall,
|
|
73
|
+
import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteReconToolCall, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
|
|
74
74
|
import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
|
|
75
75
|
import { buildContinueOrientation, looksLikeContinueOrResumePrompt, } from "./continue-orient.js";
|
|
76
76
|
import { detectPackageManager } from "./workspace-orient.js";
|
|
@@ -86,7 +86,7 @@ import { buildRichStopSummary } from "./stop-summary.js";
|
|
|
86
86
|
import { composeAgentSystemPrompt } from "./prompt-composer.js";
|
|
87
87
|
import { createGovernorState, governProgress, } from "./evidence-governor.js";
|
|
88
88
|
import { createTurnState, transitionTurn, } from "./turn-state.js";
|
|
89
|
-
import { deriveOutcomeStatus, inferOutcomeKind, openOutcomeState, recordAnswerEvidence, recordFailedHypothesis, recordToolEvidence, saveOutcomeState, validateCriterionEvidence, } from "./outcomes.js";
|
|
89
|
+
import { deriveOutcomeStatus, inferOutcomeKind, openOutcomeState, recordAnswerEvidence, recordFailedHypothesis, recordToolEvidence, completedOperationObservationDigest, saveOutcomeState, validateCriterionEvidence, } from "./outcomes.js";
|
|
90
90
|
import { createTurnOutcome, normalizeTurnOutcomeInput, renderTurnOutcome, } from "./turn-outcome.js";
|
|
91
91
|
import { beginEngagementAction, finishEngagementAction, recordEngagementCheckpoint, reconcileEngagementJob, openEngagement, saveEngagement, } from "../store/engagement.js";
|
|
92
92
|
export * from "./tool-call-parser.js";
|
|
@@ -442,19 +442,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
442
442
|
// brand-new plan (the exact failure where "what do u know till now"
|
|
443
443
|
// triggered explore→plan and created an unrelated "Enhance clai" plan).
|
|
444
444
|
const informationalQuery = looksLikeInformationalQuery(prompt);
|
|
445
|
-
// Greetings / thanks / short acks must never force tools
|
|
446
|
-
// freshness retries — a false "act don't narrate" path burned tokens on
|
|
447
|
-
// web.search recovery loops after a simple "hi".
|
|
445
|
+
// Greetings / thanks / short acks must never force tools or plans.
|
|
448
446
|
const idleOrSocialPrompt = looksLikeIdleOrSocialPrompt(prompt);
|
|
449
447
|
suppressOutcomeDiagnostics =
|
|
450
448
|
informationalQuery ||
|
|
451
449
|
idleOrSocialPrompt ||
|
|
452
450
|
looksLikeContinueOrResumePrompt(prompt);
|
|
453
|
-
const freshWebSearchRequired = !buildLikeTurn &&
|
|
454
|
-
!pentestLikeTurn &&
|
|
455
|
-
!idleOrSocialPrompt &&
|
|
456
|
-
toolNames.includes("web.search") &&
|
|
457
|
-
requiresFreshWebSearch(prompt);
|
|
458
451
|
let provider = initialProvider;
|
|
459
452
|
await ensureProviderConfigured(provider);
|
|
460
453
|
let model = initialModel;
|
|
@@ -608,9 +601,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
608
601
|
orientInput.candidateProject = candidate;
|
|
609
602
|
systemSections.push(buildWorkspaceOrientation(orientInput));
|
|
610
603
|
}
|
|
611
|
-
if (freshWebSearchRequired) {
|
|
612
|
-
systemSections.push(freshnessGuardMessage());
|
|
613
|
-
}
|
|
614
604
|
// The live plan is mutable state: it is injected once as a keyed request
|
|
615
605
|
// suffix (upsertPlanContextMessage) instead of being frozen into the stable
|
|
616
606
|
// system prefix, so the model never sees a stale and a fresh plan together.
|
|
@@ -719,7 +709,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
719
709
|
content.startsWith("ACTIVE PROJECT ROOT:") ||
|
|
720
710
|
content.startsWith("USER DESTINATION:") ||
|
|
721
711
|
content.startsWith("WORKSPACE STATUS") ||
|
|
722
|
-
content.startsWith("Freshness guard for this turn:") ||
|
|
723
712
|
content.includes("MODE") ||
|
|
724
713
|
content.includes("OUTCOME"),
|
|
725
714
|
}));
|
|
@@ -856,6 +845,17 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
856
845
|
}
|
|
857
846
|
return message;
|
|
858
847
|
};
|
|
848
|
+
const upsertActionCycleRecovery = (content) => {
|
|
849
|
+
const prefix = "[ACTION CYCLE RECOVERY] ";
|
|
850
|
+
for (let index = messages.length - 1; index >= 0; index -= 1) {
|
|
851
|
+
const message = messages[index];
|
|
852
|
+
if (message.role === "user" && message.internal && message.content.startsWith(prefix)) {
|
|
853
|
+
messages.splice(index, 1);
|
|
854
|
+
break;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
messages.push(recoveryUserMessage(prefix + content));
|
|
858
|
+
};
|
|
859
859
|
const recoveryProse = (content) => {
|
|
860
860
|
const text = textBeforeToolCall(stripSentinelTokens(content)).trim();
|
|
861
861
|
if (!text ||
|
|
@@ -928,7 +928,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
928
928
|
// (e.g. malformed extra/missing braces that are NOT simple truncation). We
|
|
929
929
|
// retry instead of leaking the raw block as the final answer.
|
|
930
930
|
let malformedFenceRetries = 0;
|
|
931
|
-
let sawFreshWebSearch = false;
|
|
932
931
|
const recovery = createRecoveryBudgets();
|
|
933
932
|
let sawServerStart = false;
|
|
934
933
|
let sawPlanCreateOk = false;
|
|
@@ -1000,70 +999,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1000
999
|
remoteWorkVerified: planHasVerifiedRemoteWork(plan),
|
|
1001
1000
|
});
|
|
1002
1001
|
};
|
|
1003
|
-
const reconcileOpenTaskBeforeFinalizing = async () => {
|
|
1004
|
-
let plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1005
|
-
const open = plan?.tasks.find((task) => task.state === "in_progress" && !task.responderOwned);
|
|
1006
|
-
if (!plan || !open)
|
|
1007
|
-
return plan;
|
|
1008
|
-
const gate = completionGateForTask(plan, open.id);
|
|
1009
|
-
if (!gate.ok)
|
|
1010
|
-
return plan;
|
|
1011
|
-
const reconciledTaskIds = [open.id];
|
|
1012
|
-
const working = plan;
|
|
1013
|
-
markTask(working, open.id, "done", "Completion reconciled from verified task evidence.");
|
|
1014
|
-
while (true) {
|
|
1015
|
-
const observation = readyPlanTasks(working).find((task) => isRuntimeObservationTask(task.title) ||
|
|
1016
|
-
(working.kind === "pentest" && isRemoteObservationTask(task.title)));
|
|
1017
|
-
if (!observation)
|
|
1018
|
-
break;
|
|
1019
|
-
const observationGate = completionGateForTask(working, observation.id);
|
|
1020
|
-
if (!observationGate.ok)
|
|
1021
|
-
break;
|
|
1022
|
-
markTask(working, observation.id, "done", working.kind === "pentest"
|
|
1023
|
-
? "Satisfied by verified remote evidence from the preceding task."
|
|
1024
|
-
: "Satisfied by the verified runtime evidence from the preceding task.");
|
|
1025
|
-
reconciledTaskIds.push(observation.id);
|
|
1026
|
-
}
|
|
1027
|
-
if (working.status === "draft" || working.status === "approved") {
|
|
1028
|
-
working.status = "in_progress";
|
|
1029
|
-
}
|
|
1030
|
-
if (isPlanTerminal(working)) {
|
|
1031
|
-
working.status = isPlanSuccessful(working) ? "completed" : "abandoned";
|
|
1032
|
-
}
|
|
1033
|
-
// Replay the same reconciliation as a reducer so a concurrent
|
|
1034
|
-
// responder settlement is preserved instead of overwritten.
|
|
1035
|
-
const reconciledNotes = new Map(reconciledTaskIds.map((id) => [
|
|
1036
|
-
id,
|
|
1037
|
-
working.tasks.find((task) => task.id === id)?.note,
|
|
1038
|
-
]));
|
|
1039
|
-
const reconcileResult = await mutatePlan(session.sessionId, (draft) => {
|
|
1040
|
-
let changed = false;
|
|
1041
|
-
for (const [id, note] of reconciledNotes) {
|
|
1042
|
-
const task = draft.tasks.find((candidate) => candidate.id === id);
|
|
1043
|
-
if (!task || task.state === "done")
|
|
1044
|
-
continue;
|
|
1045
|
-
task.state = "done";
|
|
1046
|
-
if (note !== undefined)
|
|
1047
|
-
task.note = note;
|
|
1048
|
-
changed = true;
|
|
1049
|
-
}
|
|
1050
|
-
if (draft.status === "draft" || draft.status === "approved") {
|
|
1051
|
-
draft.status = "in_progress";
|
|
1052
|
-
changed = true;
|
|
1053
|
-
}
|
|
1054
|
-
if (isPlanTerminal(draft)) {
|
|
1055
|
-
draft.status = isPlanSuccessful(draft) ? "completed" : "abandoned";
|
|
1056
|
-
changed = true;
|
|
1057
|
-
}
|
|
1058
|
-
return changed;
|
|
1059
|
-
}).catch(() => undefined);
|
|
1060
|
-
if (reconcileResult?.ok && reconcileResult.plan)
|
|
1061
|
-
plan = reconcileResult.plan;
|
|
1062
|
-
writePlanUpdate(plan, renderPlanForTerminal(plan) + "\n");
|
|
1063
|
-
writeNotice("info", `reconciled ${reconciledTaskIds.map((id) => `[${id}]`).join(", ")} from verified evidence`, chalk.dim(` ℹ reconciled ${reconciledTaskIds.map((id) => `[${id}]`).join(", ")} from verified evidence — no duplicate verification\n`));
|
|
1064
|
-
taskWorkLedger = null;
|
|
1065
|
-
return plan;
|
|
1066
|
-
};
|
|
1067
1002
|
async function persistProjectRootOnPlan(root) {
|
|
1068
1003
|
const pm = detectPackageManager(root);
|
|
1069
1004
|
// Metadata patches go through the transactional boundary so a
|
|
@@ -1376,8 +1311,14 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1376
1311
|
...(retryReason ? { retryReason } : {}),
|
|
1377
1312
|
});
|
|
1378
1313
|
if (loopCheck.block) {
|
|
1379
|
-
const
|
|
1314
|
+
const baseReason = loopCheck.reason ??
|
|
1380
1315
|
`${call.name} previously failed with identical arguments. Change the command/args and retry.`;
|
|
1316
|
+
const priorObservation = loopCheck.kind === "unchanged-success"
|
|
1317
|
+
? loopGuard.getPriorObservation(call.name, call.args)
|
|
1318
|
+
: undefined;
|
|
1319
|
+
const reason = priorObservation
|
|
1320
|
+
? `${baseReason}\n\nPrior successful result (reuse this; it is the result of the requested call):\n${priorObservation}`
|
|
1321
|
+
: baseReason;
|
|
1381
1322
|
if (loopCheck.kind === "unchanged-success") {
|
|
1382
1323
|
const result = { ok: true, output: reason, exitCode: 0 };
|
|
1383
1324
|
emitVisibleSyntheticReceipt(result, reason);
|
|
@@ -1789,9 +1730,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1789
1730
|
};
|
|
1790
1731
|
}
|
|
1791
1732
|
}
|
|
1792
|
-
if (call.name === "web.search") {
|
|
1793
|
-
sawFreshWebSearch = true;
|
|
1794
|
-
}
|
|
1795
1733
|
if (!alreadyPrintedIds.has(toolEventId)) {
|
|
1796
1734
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) + chalk.gray(` ${formatToolArgs(call)}`);
|
|
1797
1735
|
writeToolCall(toolEventId, call, styleToolChatter(call, toolCallLine) + "\n");
|
|
@@ -3257,9 +3195,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3257
3195
|
...(toolsAttached
|
|
3258
3196
|
? {
|
|
3259
3197
|
tools: turnTools,
|
|
3260
|
-
toolChoice:
|
|
3261
|
-
? { type: "function", name: "web.search" }
|
|
3262
|
-
: "auto",
|
|
3198
|
+
toolChoice: "auto",
|
|
3263
3199
|
parallelToolCalls: true,
|
|
3264
3200
|
// P2-3: emit tool cards as soon as the function name arrives.
|
|
3265
3201
|
onToolCallDelta: (delta) => {
|
|
@@ -3692,12 +3628,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3692
3628
|
}
|
|
3693
3629
|
if (nativeToolCalls.length) {
|
|
3694
3630
|
const first = nativeToolCalls[0];
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3631
|
+
call = first.args?._parseError
|
|
3632
|
+
? {
|
|
3633
|
+
name: first.name || "unknown",
|
|
3634
|
+
args: {
|
|
3635
|
+
__nativeParseError: true,
|
|
3636
|
+
_raw: first.args._raw,
|
|
3637
|
+
},
|
|
3638
|
+
}
|
|
3639
|
+
: normalizeToolCall({ name: first.name, args: first.args });
|
|
3701
3640
|
}
|
|
3702
3641
|
else {
|
|
3703
3642
|
call = parseToolCall(assistantText.visible, {
|
|
@@ -3804,19 +3743,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3804
3743
|
retryWithoutThinking = true;
|
|
3805
3744
|
commitAssistantRetry(assistantText.visible);
|
|
3806
3745
|
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
3807
|
-
const buildNudge =
|
|
3746
|
+
const buildNudge = isPlanMode && !activePlan
|
|
3808
3747
|
? toolsAttached
|
|
3809
|
-
? "No visible output.
|
|
3810
|
-
: "No visible output.
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
: toolsAttached
|
|
3817
|
-
? "No visible output. " + toolNudge(true)
|
|
3818
|
-
: "No visible output. Emit a ```tool block or give your final answer. " +
|
|
3819
|
-
"Do NOT hide tool calls in <think> tags — put them in the visible response.";
|
|
3748
|
+
? "No visible output. In plan mode: gather context or call plan.create when ready (do not only describe the plan)."
|
|
3749
|
+
: "No visible output. In plan mode: emit a ```tool block for research/recon or plan.create. " +
|
|
3750
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response."
|
|
3751
|
+
: toolsAttached
|
|
3752
|
+
? "No visible output. " + toolNudge(true)
|
|
3753
|
+
: "No visible output. Emit a ```tool block or give your final answer. " +
|
|
3754
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response.";
|
|
3820
3755
|
messages.push(recoveryUserMessage(buildNudge));
|
|
3821
3756
|
continue;
|
|
3822
3757
|
}
|
|
@@ -3884,7 +3819,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3884
3819
|
}
|
|
3885
3820
|
// Exhausted retries — fall through to the normal answer path.
|
|
3886
3821
|
}
|
|
3887
|
-
if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin
|
|
3822
|
+
if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>|<[||]DSML[||](?:tool_calls|invoke|parameter)\b/i.test(assistantText.visible)) {
|
|
3888
3823
|
writeNotice("warn", "tool call was malformed or cut off — asking the model to retry in JSON form", chalk.yellow(" ⚠ tool call was malformed or cut off — asking the model to retry in JSON form\n"));
|
|
3889
3824
|
commitAssistantRetry(assistantText.visible);
|
|
3890
3825
|
messages.push(recoveryUserMessage(toolsAttached
|
|
@@ -4012,15 +3947,12 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4012
3947
|
const cleaned = collapseRepeatedText(stripSentinelTokens(canonicalAssistantVisible));
|
|
4013
3948
|
const narratedAction = looksLikeActionNarration(cleaned);
|
|
4014
3949
|
const narratedWebAction = looksLikeWebActionNarration(cleaned);
|
|
4015
|
-
const
|
|
4016
|
-
const livePlanAtCompletion = reconciledPlanAtCompletion ??
|
|
4017
|
-
(await loadPlan(session.sessionId).catch(() => undefined));
|
|
3950
|
+
const livePlanAtCompletion = await loadPlan(session.sessionId).catch(() => undefined);
|
|
4018
3951
|
const planStatusAtCompletion = livePlanAtCompletion?.status ?? activePlan?.status;
|
|
4019
3952
|
const completedPlanDuringThisTurn = activePlan?.status !== "completed" &&
|
|
4020
3953
|
planStatusAtCompletion === "completed";
|
|
4021
3954
|
const planHasOpenWorkNow = planHasOpenWork(planStatusAtCompletion);
|
|
4022
|
-
const userExpectsWork =
|
|
4023
|
-
(planHasOpenWorkNow && session.planApproved.value) ||
|
|
3955
|
+
const userExpectsWork = (planHasOpenWorkNow && session.planApproved.value) ||
|
|
4024
3956
|
(!informationalQuery &&
|
|
4025
3957
|
!idleOrSocialPrompt &&
|
|
4026
3958
|
(buildLikeTurn || pentestLikeTurn));
|
|
@@ -4088,11 +4020,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4088
4020
|
pentestSession,
|
|
4089
4021
|
informationalQuery,
|
|
4090
4022
|
idleOrSocialPrompt,
|
|
4091
|
-
freshWebSearchRequired,
|
|
4092
|
-
freshnessGuardText: freshWebSearchRequired
|
|
4093
|
-
? freshnessGuardMessage()
|
|
4094
|
-
: "",
|
|
4095
|
-
sawFreshWebSearch,
|
|
4096
4023
|
sawPlanCreateOk,
|
|
4097
4024
|
sawFeatureImplWrite,
|
|
4098
4025
|
sawScaffoldOk,
|
|
@@ -4291,11 +4218,20 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4291
4218
|
writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
|
|
4292
4219
|
const suppressedResults = bound.map((b) => {
|
|
4293
4220
|
const duplicate = runIds.has(b.id);
|
|
4294
|
-
const
|
|
4221
|
+
const priorObservation = duplicate
|
|
4222
|
+
? loopGuard.getPriorObservation(b.call.name, b.call.args)
|
|
4223
|
+
: undefined;
|
|
4224
|
+
const resultReason = duplicate
|
|
4225
|
+
? reason +
|
|
4226
|
+
(priorObservation
|
|
4227
|
+
? `\n\nPrior successful result for ${b.call.name}:\n${priorObservation}`
|
|
4228
|
+
: "")
|
|
4229
|
+
: deferReason;
|
|
4295
4230
|
const result = {
|
|
4296
|
-
ok:
|
|
4231
|
+
ok: duplicate,
|
|
4297
4232
|
output: resultReason,
|
|
4298
|
-
exitCode: duplicate ?
|
|
4233
|
+
exitCode: duplicate ? 0 : 130,
|
|
4234
|
+
...(duplicate ? { suppressedRepeat: true } : {}),
|
|
4299
4235
|
};
|
|
4300
4236
|
return { b, resultReason, result };
|
|
4301
4237
|
});
|
|
@@ -4316,22 +4252,6 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4316
4252
|
writeToolOutput(eventId, output, chalk.dim(` ${output}`));
|
|
4317
4253
|
emitToolResult(eventId, result, resultReason);
|
|
4318
4254
|
}
|
|
4319
|
-
if (historyNativeCalls.length) {
|
|
4320
|
-
appendAssistantWithTools(messages, beforeTool ?? "", historyNativeCalls, completion.reasoningBlock ??
|
|
4321
|
-
(assistantText.hasThinking && assistantText.thinkContent
|
|
4322
|
-
? { text: assistantText.thinkContent }
|
|
4323
|
-
: undefined));
|
|
4324
|
-
for (const { b, resultReason, result } of suppressedResults) {
|
|
4325
|
-
appendToolResult(messages, b.id, `Tool ${b.call.name} result (exit=${result.exitCode}, ok=false):\n${resultReason}`, b.call.name, false);
|
|
4326
|
-
}
|
|
4327
|
-
}
|
|
4328
|
-
else {
|
|
4329
|
-
const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
|
|
4330
|
-
allCalls
|
|
4331
|
-
.map((candidate) => `\`\`\`tool\n${JSON.stringify(candidate)}\n\`\`\``)
|
|
4332
|
-
.join("\n\n");
|
|
4333
|
-
pushAssistantHistory(standardizedContent);
|
|
4334
|
-
}
|
|
4335
4255
|
if (sequenceDecision.terminal) {
|
|
4336
4256
|
const remainingCriteria = unreadResponderNotificationIds.size > 0
|
|
4337
4257
|
? ["Analyze and acknowledge the delivered Responder result without repeating completed foreground work."]
|
|
@@ -4341,10 +4261,10 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4341
4261
|
moveTurn("partial", "repeated identical action sequence");
|
|
4342
4262
|
return finishTurn("Stopped an identical action cycle before it could execute again.", productiveSteps, "partial", remainingCriteria, "The model repeated an identical action sequence without a new premise or state change.");
|
|
4343
4263
|
}
|
|
4344
|
-
|
|
4264
|
+
upsertActionCycleRecovery(reason +
|
|
4345
4265
|
(unreadResponderNotificationIds.size > 0
|
|
4346
4266
|
? " A delivered Responder result is still unread: analyze the available result, gather only genuinely necessary bounded evidence, then call job.read before returning to foreground work."
|
|
4347
|
-
: " Reassess
|
|
4267
|
+
: " The original successful tool result remains in context. Reassess that evidence and either finish or select a materially different action; do not replay completed work."));
|
|
4348
4268
|
continue;
|
|
4349
4269
|
}
|
|
4350
4270
|
if (sequenceDecision.warn && sequenceDecision.warnMessage) {
|
|
@@ -4408,6 +4328,7 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4408
4328
|
let planCreatedThisTurn = Boolean(activePlan && activePlan.tasks.length > 0);
|
|
4409
4329
|
let actionSequenceExecuted = 0;
|
|
4410
4330
|
let actionSequenceEligible = allCalls.length > 0;
|
|
4331
|
+
const actionSequenceOutcomes = new Map();
|
|
4411
4332
|
/**
|
|
4412
4333
|
* Record a tool result into history. Failures / user declines are
|
|
4413
4334
|
* always returned to the model — we never cancel later siblings or
|
|
@@ -4418,6 +4339,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4418
4339
|
consecutiveModelOnlyRounds = 0;
|
|
4419
4340
|
recordedNativeIds.add(boundCall.id);
|
|
4420
4341
|
actionSequenceExecuted += 1;
|
|
4342
|
+
const sequenceObservation = res.suppressedRepeat
|
|
4343
|
+
? loopGuard.getPriorObservation(res.call.name, res.call.args) ??
|
|
4344
|
+
res.contextOutput
|
|
4345
|
+
: res.result.output ?? res.contextOutput;
|
|
4346
|
+
actionSequenceOutcomes.set(boundCall.id, JSON.stringify({
|
|
4347
|
+
ok: res.ok,
|
|
4348
|
+
exitCode: res.result.exitCode ?? null,
|
|
4349
|
+
digest: completedOperationObservationDigest(res.call.name, sequenceObservation),
|
|
4350
|
+
}));
|
|
4421
4351
|
// A policy-suppressed call is deterministic: replaying it verbatim
|
|
4422
4352
|
// returns the identical receipt. It must therefore keep the sequence
|
|
4423
4353
|
// eligible, otherwise the tool-level suppression and the sequence
|
|
@@ -4757,12 +4687,16 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
4757
4687
|
}
|
|
4758
4688
|
fillMissingToolResults(messages, historyNativeCalls, "Cancelled — not executed this turn.");
|
|
4759
4689
|
}
|
|
4690
|
+
const actionSequenceOutcome = createHash("sha256")
|
|
4691
|
+
.update(JSON.stringify(bound.map((entry) => actionSequenceOutcomes.get(entry.id) ?? null)))
|
|
4692
|
+
.digest("hex")
|
|
4693
|
+
.slice(0, 24);
|
|
4760
4694
|
loopGuard.completeActionSequence(actionSequenceCalls, actionSequenceEligible &&
|
|
4761
4695
|
toRun.length === bound.length &&
|
|
4762
4696
|
actionSequenceExecuted === allCalls.length &&
|
|
4763
4697
|
!aborted &&
|
|
4764
4698
|
!awaitingPlanApproval &&
|
|
4765
|
-
!governorPauseReason);
|
|
4699
|
+
!governorPauseReason, actionSequenceOutcome);
|
|
4766
4700
|
// Keep ledger system rows outside the native assistant→tool group so
|
|
4767
4701
|
// protocol repair preserves the real successful job.read body.
|
|
4768
4702
|
for (const notification of deferredResponderLedgerNotifications.splice(0)) {
|