@pentoshi/clai 3.8.21 → 3.8.23
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/confirm-port.d.ts +5 -2
- package/dist/agent/confirm-port.js +11 -5
- package/dist/agent/confirm-port.js.map +1 -1
- package/dist/agent/loop-guard.d.ts +6 -1
- package/dist/agent/loop-guard.js +17 -1
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/plan-decision.js +7 -3
- package/dist/agent/plan-decision.js.map +1 -1
- package/dist/agent/plan-tool.js +63 -20
- package/dist/agent/plan-tool.js.map +1 -1
- package/dist/agent/progress-pause-policy.d.ts +21 -0
- package/dist/agent/progress-pause-policy.js +30 -0
- package/dist/agent/progress-pause-policy.js.map +1 -0
- package/dist/agent/runner.js +166 -33
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/stop-summary.js +3 -1
- package/dist/agent/stop-summary.js.map +1 -1
- package/dist/agent/tool-history.d.ts +13 -1
- package/dist/agent/tool-history.js +34 -15
- package/dist/agent/tool-history.js.map +1 -1
- package/dist/agent/tool-output-formatting.js +16 -0
- package/dist/agent/tool-output-formatting.js.map +1 -1
- package/dist/app/ports/confirm-port.d.ts +1 -1
- package/dist/prompts/embedded.js +1 -1
- package/dist/prompts/embedded.js.map +1 -1
- package/dist/prompts/index.js +6 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/system.agent.md +2 -0
- package/dist/tools/web/search.js +22 -3
- package/dist/tools/web/search.js.map +1 -1
- package/dist/tui-v2/bootstrap/overlay-ports.js +7 -2
- package/dist/tui-v2/bootstrap/overlay-ports.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
|
@@ -13,7 +13,7 @@ import { groqInputTokenBudget } from "../llm/groq.js";
|
|
|
13
13
|
import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall, } from "../safety/classifier.js";
|
|
14
14
|
import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
|
|
15
15
|
import { getToolDefinitions, getCompactToolDefinitions, PLAN_TOOL_NAMES, } from "../tools/definitions.js";
|
|
16
|
-
import { appendAssistantWithTools, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
|
|
16
|
+
import { appendAssistantWithTools, ensureUniqueToolCallIds, appendToolResult, assertValidToolProtocol, fillMissingToolResults, repairToolProtocol, } from "./tool-history.js";
|
|
17
17
|
import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
|
|
18
18
|
import { compactMessagesWithSummary, estimateMessagesTokens, shouldApplyAutoCompact, COMPACTION_MEMORY_PREFIX, PLAN_IMPLEMENT_MEMORY_PREFIX, isCompactionMemoryMessage, } from "./context-manager.js";
|
|
19
19
|
import { buildContextBreakdown, contextBreakdownAuditPayload, } from "./context-breakdown.js";
|
|
@@ -37,6 +37,7 @@ import { fsWrite, isOutsideWorkingDirectory, resolveFsToolPath, } from "../tools
|
|
|
37
37
|
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, salvageTruncatedWriteFromNative, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeIdleOrSocialPrompt, looksLikeActionNarration, looksLikeWebActionNarration, looksLikePlanNarration, looksLikeErrorDiagnosisWithFixIntent, localHttpProbeIsFailure, localHttpProbeIsSuccess, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, narrowNmapOperationDirective, pentestWorkflowDirective, pentestNoLocalServerDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
38
38
|
import { createSessionPolicy, isPreApprovalAllowedTool, isPlanModeAllowedShellCommand, isPlanModeAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
|
|
39
39
|
import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
|
|
40
|
+
import { codingSessionFromContext, isProtocolPlaceholderOutput, progressPauseMode, } from "./progress-pause-policy.js";
|
|
40
41
|
import { renderPlanForTerminal, planContextMessage, handlePlanTool, resolvePlanTaskId, } from "./plan-tool.js";
|
|
41
42
|
import { absorbLooseWorkIntoLedger, applyDestinationCwd, canMarkTaskDone, hasLocalRuntimeProof, hasRemoteWorkProof, isDevServerCall, isEvidenceWorkTool, isFeatureImplementationCall, isPackageInstallCommand, isPlanPreflightTool, isPortListeningOutput, isReadOnlyReconTool, isRemoteActiveTestCall, isRemoteObservationTask, isRemoteReconToolCall, isRuntimeObservationTask, isScaffoldCreateCommand, isServerReadyOutput, ledgerFromTaskEvidence, pickPendingTaskForToolCall, recordTaskWorkSuccess, resolveUserDestinationHint, taskEvidenceFromLedger, TOOL_ABORT_GRACE_MS, toolHardBudgetMs, toolStallBudgetMs, userAskedForFeatureApp, } from "./task-evidence.js";
|
|
42
43
|
import { buildSessionStateBlock, inferNextHint, upsertSessionStateMessage, } from "./session-state.js";
|
|
@@ -793,6 +794,15 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
793
794
|
const hasHistory = (options.history?.length ?? 0) > 0;
|
|
794
795
|
const buildLike = buildLikeTurn;
|
|
795
796
|
const pentestLike = looksLikePentestTask(prompt, options.history);
|
|
797
|
+
/** Coding/build sessions never hard-pause mid-turn for the progress governor. */
|
|
798
|
+
let codingSession = codingSessionFromContext({
|
|
799
|
+
buildLike,
|
|
800
|
+
planKind: activePlan?.kind,
|
|
801
|
+
});
|
|
802
|
+
let pauseMode = progressPauseMode({
|
|
803
|
+
codingSession,
|
|
804
|
+
autoConfirm: Boolean(options.autoConfirm),
|
|
805
|
+
});
|
|
796
806
|
const continueExistingOutcome = /^(?:continue|resume|proceed|keep\s+going|finish|next)\b/i.test(prompt.trim()) ||
|
|
797
807
|
Boolean(activePlan && !isPlanTerminal(activePlan));
|
|
798
808
|
const outcomeState = await openOutcomeState({
|
|
@@ -911,13 +921,40 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
911
921
|
environmentChanged: retryEnvironmentChanged,
|
|
912
922
|
...(retryReason ? { retryReason } : {}),
|
|
913
923
|
});
|
|
924
|
+
// Reuse prior SUCCESS instead of re-running or returning ok=false.
|
|
925
|
+
// Models often thrash "tools failed after resume" and re-call the same
|
|
926
|
+
// tool.check/fs.list; answering with a fake failure made the thrash worse.
|
|
927
|
+
const cachedSuccess = loopGuard.getCachedSuccessOutput(call.name, call.args);
|
|
928
|
+
if (cachedSuccess !== undefined && loopGuard.hasSucceeded(call.name, call.args)) {
|
|
929
|
+
const reuseNote = loopCheck.reason ??
|
|
930
|
+
`${call.name} already succeeded with identical arguments earlier this turn — reusing that result. Do not re-call it; proceed to the next step.`;
|
|
931
|
+
writeNotice("info", reuseNote, chalk.dim(` ℹ ${reuseNote}\n`));
|
|
932
|
+
const body = `Prior successful ${call.name} result (reused; identical args — tools are working):\n` +
|
|
933
|
+
cachedSuccess;
|
|
934
|
+
const result = { ok: true, output: body, exitCode: 0 };
|
|
935
|
+
emitToolResult(toolEventId, result, reuseNote);
|
|
936
|
+
return {
|
|
937
|
+
ok: true,
|
|
938
|
+
call,
|
|
939
|
+
result,
|
|
940
|
+
contextOutput: body,
|
|
941
|
+
};
|
|
942
|
+
}
|
|
914
943
|
if (loopCheck.block) {
|
|
915
944
|
const reason = loopCheck.reason ??
|
|
916
945
|
`${call.name} was already called with the same arguments. Use the prior result and choose a different next step.`;
|
|
917
946
|
writeNotice("warn", reason, chalk.yellow(` ⚠ ${reason}\n`));
|
|
918
|
-
|
|
947
|
+
// Prefer ok=true when we previously succeeded but have no cache body,
|
|
948
|
+
// so the model does not treat loop-guard as a tool outage.
|
|
949
|
+
const softOk = loopGuard.hasSucceeded(call.name, call.args);
|
|
950
|
+
const result = {
|
|
951
|
+
ok: softOk,
|
|
952
|
+
output: reason,
|
|
953
|
+
exitCode: softOk ? 0 : 1,
|
|
954
|
+
};
|
|
955
|
+
emitToolResult(toolEventId, result, reason);
|
|
919
956
|
return {
|
|
920
|
-
ok:
|
|
957
|
+
ok: softOk,
|
|
921
958
|
call,
|
|
922
959
|
result,
|
|
923
960
|
contextOutput: reason,
|
|
@@ -1767,30 +1804,55 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
1767
1804
|
((call.name === "shell.exec" || call.name === "shell.start") &&
|
|
1768
1805
|
isPackageInstallCommand(String(call.args.command ?? "")));
|
|
1769
1806
|
}
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1807
|
+
// Protocol-repair placeholders are not live work — never let them
|
|
1808
|
+
// accumulate into a mid-turn pause (they used to look like failed tools).
|
|
1809
|
+
if (!isProtocolPlaceholderOutput(result.output)) {
|
|
1810
|
+
const governed = governProgress(governorState, "activity", {
|
|
1811
|
+
evidenceDelta: newEvidence.length,
|
|
1812
|
+
hypothesisDelta,
|
|
1813
|
+
repetitionScore: loopGuard.getAttemptCount(call.name, call.args) > 1 ? 1 : 0,
|
|
1814
|
+
policy: {
|
|
1815
|
+
resourceEnvelope: Math.max(12, maxSteps),
|
|
1816
|
+
// Coding builds get a much higher ceiling; never use the tight
|
|
1817
|
+
// default that stopped multi-file scaffolds after a handful of steps.
|
|
1818
|
+
emergencyCeiling: codingSession
|
|
1819
|
+
? Math.max(200, maxSteps * 5)
|
|
1820
|
+
: Math.max(70, maxSteps * 3),
|
|
1821
|
+
reflectionAfterNoDelta: codingSession ? 5 : 3,
|
|
1822
|
+
pauseAfterNoDelta: codingSession ? 24 : 6,
|
|
1823
|
+
repetitionThreshold: 0.8,
|
|
1824
|
+
},
|
|
1787
1825
|
});
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1826
|
+
governorState = governed.state;
|
|
1827
|
+
if (governed.recommendation === "reflect") {
|
|
1828
|
+
deferredPostToolMessages.push({
|
|
1829
|
+
role: "system",
|
|
1830
|
+
content: `PROGRESS GOVERNOR: ${governed.reason}. Reassess the current premise and choose the next action that can produce criterion-linked evidence.` +
|
|
1831
|
+
(codingSession
|
|
1832
|
+
? " Keep working — coding builds do not stop for a continue prompt."
|
|
1833
|
+
: ""),
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
else if (governed.recommendation === "paused_budget") {
|
|
1837
|
+
if (pauseMode === "never") {
|
|
1838
|
+
// Soft reset so we do not re-trip every subsequent tool.
|
|
1839
|
+
governorState = {
|
|
1840
|
+
...governed.state,
|
|
1841
|
+
consecutiveNoDelta: 0,
|
|
1842
|
+
};
|
|
1843
|
+
deferredPostToolMessages.push({
|
|
1844
|
+
role: "system",
|
|
1845
|
+
content: `PROGRESS GOVERNOR (soft, coding build): ${governed.reason}. ` +
|
|
1846
|
+
"Change approach if stuck, but keep implementing — do not stop for user confirmation.",
|
|
1847
|
+
});
|
|
1848
|
+
}
|
|
1849
|
+
else {
|
|
1850
|
+
governorPauseReason = governed.reason;
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1791
1853
|
}
|
|
1792
1854
|
await saveOutcomeState(outcomeState);
|
|
1793
|
-
loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode);
|
|
1855
|
+
loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode, result.output);
|
|
1794
1856
|
// Evidence for verify-before-done: only successful real work counts.
|
|
1795
1857
|
if (result.ok && isEvidenceWorkTool(call.name)) {
|
|
1796
1858
|
const liveAfter = await loadPlan(session.sessionId).catch(() => undefined);
|
|
@@ -2033,14 +2095,53 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
2033
2095
|
// advances when the previous iteration actually executed a tool.
|
|
2034
2096
|
step = productiveSteps;
|
|
2035
2097
|
if (governorPauseReason) {
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2098
|
+
// Non-coding: always ask continue/stop. Coding never sets this reason
|
|
2099
|
+
// (pauseMode === "never"), but guard anyway.
|
|
2100
|
+
if (pauseMode === "never") {
|
|
2101
|
+
governorPauseReason = undefined;
|
|
2102
|
+
governorState = {
|
|
2103
|
+
...governorState,
|
|
2104
|
+
consecutiveNoDelta: 0,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
else {
|
|
2108
|
+
const confirmPort = options.confirm;
|
|
2109
|
+
let keepGoing = false;
|
|
2110
|
+
if (confirmPort?.confirmContinue) {
|
|
2111
|
+
try {
|
|
2112
|
+
keepGoing = await confirmPort.confirmContinue(productiveSteps, governorPauseReason);
|
|
2113
|
+
}
|
|
2114
|
+
catch {
|
|
2115
|
+
keepGoing = false;
|
|
2116
|
+
}
|
|
2117
|
+
finally {
|
|
2118
|
+
restoreInteractiveStdin();
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
if (keepGoing) {
|
|
2122
|
+
writeNotice("info", "continuing after progress pause", chalk.dim(` ℹ continuing after pause (${governorPauseReason}) — change approach if stuck\n`));
|
|
2123
|
+
deferredPostToolMessages.push({
|
|
2124
|
+
role: "system",
|
|
2125
|
+
content: `User chose CONTINUE after progress pause (${governorPauseReason}). ` +
|
|
2126
|
+
"Do not repeat the same failing step; change approach and produce new evidence.",
|
|
2127
|
+
});
|
|
2128
|
+
governorPauseReason = undefined;
|
|
2129
|
+
governorState = {
|
|
2130
|
+
...governorState,
|
|
2131
|
+
consecutiveNoDelta: 0,
|
|
2132
|
+
};
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
|
|
2136
|
+
outcomeState.outcome.status = "paused_budget";
|
|
2137
|
+
await saveOutcomeState(outcomeState);
|
|
2138
|
+
moveTurn("paused_budget", governorPauseReason);
|
|
2139
|
+
lastAnswer = richSummary;
|
|
2140
|
+
return finishTurn(lastAnswer, productiveSteps, "paused_budget", outcomeState.outcome.criteria
|
|
2141
|
+
.filter((criterion) => criterion.required && criterion.status !== "proven")
|
|
2142
|
+
.map((criterion) => criterion.statement), governorPauseReason);
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2044
2145
|
}
|
|
2045
2146
|
options.signal?.throwIfAborted();
|
|
2046
2147
|
let call;
|
|
@@ -3000,12 +3101,29 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3000
3101
|
// Preserve model/document order. In particular, never move a later
|
|
3001
3102
|
// in_progress transition ahead of the preceding work or done receipt;
|
|
3002
3103
|
// doing so inverts dependency order and desynchronizes the task pane.
|
|
3104
|
+
// Re-id empty/duplicate native ids BEFORE building toRun/history so
|
|
3105
|
+
// assistant toolCalls and role:tool results always share the same id.
|
|
3106
|
+
// Mismatched ids make results look orphaned → repair injects placeholders
|
|
3107
|
+
// → model thrash-retries tools that already succeeded in the UI.
|
|
3108
|
+
const historyNativeCalls = ensureUniqueToolCallIds(bound.map((b) => b.native));
|
|
3109
|
+
for (let i = 0; i < bound.length; i++) {
|
|
3110
|
+
const fixed = historyNativeCalls[i];
|
|
3111
|
+
bound[i] = {
|
|
3112
|
+
...bound[i],
|
|
3113
|
+
id: fixed.id,
|
|
3114
|
+
native: fixed,
|
|
3115
|
+
};
|
|
3116
|
+
}
|
|
3117
|
+
// Re-bind toRun to the rewritten bound entries (by original call identity).
|
|
3118
|
+
toRun = toRun.map((old) => {
|
|
3119
|
+
const match = bound.find((b) => b.call === old.call);
|
|
3120
|
+
return match ?? old;
|
|
3121
|
+
});
|
|
3003
3122
|
// Re-index toRun positions for UI callIds[] (0..n-1 this turn).
|
|
3004
3123
|
toRun = toRun.map((b, index) => ({ ...b, index }));
|
|
3005
3124
|
const allCalls = toRun.map((b) => b.call);
|
|
3006
3125
|
/** Stable call→Bound map (object identity; no indexOf for result ids). */
|
|
3007
3126
|
const callToBound = new Map(toRun.map((b) => [b.call, b]));
|
|
3008
|
-
const historyNativeCalls = bound.map((b) => b.native);
|
|
3009
3127
|
const runIds = new Set(toRun.map((b) => b.id));
|
|
3010
3128
|
// Notice BEFORE tool cards so the transcript reads:
|
|
3011
3129
|
// thinking → response → "N tool calls…" → tool cards (not tools then info).
|
|
@@ -3216,6 +3334,19 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3216
3334
|
else {
|
|
3217
3335
|
session.planApproved.value = true;
|
|
3218
3336
|
}
|
|
3337
|
+
// Re-derive pause policy from the new plan kind (coding builds
|
|
3338
|
+
// must not hard-pause even if the free-text prompt was generic).
|
|
3339
|
+
const kindArg = typeof res.call.args.kind === "string"
|
|
3340
|
+
? res.call.args.kind
|
|
3341
|
+
: undefined;
|
|
3342
|
+
codingSession = codingSessionFromContext({
|
|
3343
|
+
buildLike,
|
|
3344
|
+
planKind: kindArg,
|
|
3345
|
+
});
|
|
3346
|
+
pauseMode = progressPauseMode({
|
|
3347
|
+
codingSession,
|
|
3348
|
+
autoConfirm: Boolean(options.autoConfirm),
|
|
3349
|
+
});
|
|
3219
3350
|
}
|
|
3220
3351
|
// User Esc/Ctrl+C only — never cancel siblings because a delete failed
|
|
3221
3352
|
// or a confirm was declined; the model must see every tool result.
|
|
@@ -3335,6 +3466,8 @@ export async function runAgentTurn(prompt, options = {}) {
|
|
|
3335
3466
|
}
|
|
3336
3467
|
}
|
|
3337
3468
|
}
|
|
3469
|
+
// Hard iteration ceiling (hundreds of steps) — rare. Mid-turn governor
|
|
3470
|
+
// pauses already confirm for non-coding; coding never hard-pauses there.
|
|
3338
3471
|
const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
|
|
3339
3472
|
lastAnswer = richSummary;
|
|
3340
3473
|
outcomeState.outcome.status = "paused_budget";
|