@pentoshi/clai 2.0.17 → 2.0.19
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/context-manager.js +24 -9
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/events.d.ts +1 -1
- package/dist/agent/loop-guard.d.ts +9 -0
- package/dist/agent/loop-guard.js +45 -3
- package/dist/agent/loop-guard.js.map +1 -1
- package/dist/agent/runner.d.ts +7 -0
- package/dist/agent/runner.js +169 -23
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/task-analyzer.d.ts +3 -3
- package/dist/agent/task-analyzer.js +16 -4
- package/dist/agent/task-analyzer.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/index.js +65 -4
- package/dist/index.js.map +1 -1
- package/dist/prompts/index.d.ts +1 -1
- package/dist/prompts/index.js +8 -1
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.js +17 -0
- package/dist/repl.js.map +1 -1
- package/dist/safety/classifier.js.map +1 -1
- package/dist/store/config.d.ts +2 -0
- package/dist/store/config.js +1 -0
- package/dist/store/config.js.map +1 -1
- package/dist/store/keys.js +3 -0
- package/dist/store/keys.js.map +1 -1
- package/dist/tools/fs.d.ts +33 -5
- package/dist/tools/fs.js +22 -19
- package/dist/tools/fs.js.map +1 -1
- package/dist/tools/registry.d.ts +1 -0
- package/dist/tools/registry.js +14 -12
- package/dist/tools/registry.js.map +1 -1
- package/dist/tui/App.js +3 -2
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/components/ConfirmModal.js +9 -2
- package/dist/tui/components/ConfirmModal.js.map +1 -1
- package/dist/tui/components/Pager.js +14 -1
- package/dist/tui/components/Pager.js.map +1 -1
- package/dist/tui/components/PickerPanel.d.ts +2 -1
- package/dist/tui/components/PickerPanel.js +4 -2
- package/dist/tui/components/PickerPanel.js.map +1 -1
- package/dist/tui/confirm.d.ts +1 -1
- package/dist/tui/confirm.js +6 -0
- package/dist/tui/confirm.js.map +1 -1
- package/dist/tui/render-lines.js +23 -9
- package/dist/tui/render-lines.js.map +1 -1
- package/dist/tui/state.d.ts +1 -1
- package/dist/tui/state.js +4 -1
- package/dist/tui/state.js.map +1 -1
- package/package.json +1 -1
|
@@ -16,8 +16,8 @@ export function estimateMessagesTokens(messages) {
|
|
|
16
16
|
}
|
|
17
17
|
return sum;
|
|
18
18
|
}
|
|
19
|
-
const DEFAULT_BUDGET_TOKENS =
|
|
20
|
-
const DEFAULT_KEEP_RECENT =
|
|
19
|
+
const DEFAULT_BUDGET_TOKENS = 32_000;
|
|
20
|
+
const DEFAULT_KEEP_RECENT = 12;
|
|
21
21
|
/**
|
|
22
22
|
* Replace older messages with a single condensed "memory" message while
|
|
23
23
|
* preserving the system prompt and the most recent N messages.
|
|
@@ -75,12 +75,27 @@ export function compactMessages(messages, options = {}) {
|
|
|
75
75
|
export async function compactMessagesWithSummary(messages, summarize, options = {}, sessionTranscript) {
|
|
76
76
|
const before = messages.length;
|
|
77
77
|
const beforeTokens = estimateMessagesTokens(messages);
|
|
78
|
-
const
|
|
78
|
+
const isForced = options.budgetTokens === 0;
|
|
79
|
+
let keepRecent = Math.max(2, options.keepRecent ?? DEFAULT_KEEP_RECENT);
|
|
79
80
|
const start = messages[0]?.role === "system" ? 1 : 0;
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
81
|
+
let tailStart = Math.max(start, messages.length - keepRecent);
|
|
82
|
+
let older = messages.slice(start, tailStart);
|
|
83
|
+
// If forced and the older slice would be empty, try keeping fewer recent
|
|
84
|
+
// messages (minimum 1) so we have something to compact (e.g. the first user prompt).
|
|
85
|
+
if (older.length === 0 && isForced && messages.length >= start + 2) {
|
|
86
|
+
keepRecent = 1;
|
|
87
|
+
tailStart = messages.length - 1;
|
|
88
|
+
older = messages.slice(start, tailStart);
|
|
89
|
+
}
|
|
90
|
+
if (older.length === 0 && !sessionTranscript?.trim()) {
|
|
91
|
+
return {
|
|
92
|
+
messages: [...messages],
|
|
93
|
+
before,
|
|
94
|
+
after: before,
|
|
95
|
+
beforeTokens,
|
|
96
|
+
afterTokens: beforeTokens,
|
|
97
|
+
summarized: false,
|
|
98
|
+
};
|
|
84
99
|
}
|
|
85
100
|
const messageTranscript = older
|
|
86
101
|
.map((message) => `${message.role.toUpperCase()}: ${redactSecrets(message.content)}`)
|
|
@@ -107,7 +122,7 @@ export async function compactMessagesWithSummary(messages, summarize, options =
|
|
|
107
122
|
...messages.slice(tailStart),
|
|
108
123
|
];
|
|
109
124
|
const afterTokens = estimateMessagesTokens(compacted);
|
|
110
|
-
if (afterTokens >= beforeTokens) {
|
|
125
|
+
if (afterTokens >= beforeTokens && !isForced) {
|
|
111
126
|
const deterministicCompacted = compactMessages(messages, { ...options, budgetTokens: 0 });
|
|
112
127
|
const detTokens = estimateMessagesTokens(deterministicCompacted);
|
|
113
128
|
if (detTokens < beforeTokens) {
|
|
@@ -144,7 +159,7 @@ export async function compactMessagesWithSummary(messages, summarize, options =
|
|
|
144
159
|
}
|
|
145
160
|
const compacted = compactMessages(messages, { ...options, budgetTokens: 0 });
|
|
146
161
|
const afterTokens = estimateMessagesTokens(compacted);
|
|
147
|
-
if (afterTokens >= beforeTokens) {
|
|
162
|
+
if (afterTokens >= beforeTokens && !isForced) {
|
|
148
163
|
return {
|
|
149
164
|
messages: [...messages],
|
|
150
165
|
before,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAEnD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;AACpC,CAAC;AAED,MAAM,UAAU,sBAAsB,CAAC,QAAuB;IAC5D,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE,CAAC;QAC/B,GAAG,IAAI,cAAc,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,gBAAgB;IAC9D,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAkBD,MAAM,qBAAqB,GAAG,MAAM,CAAC;AACrC,MAAM,mBAAmB,GAAG,
|
|
1
|
+
{"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/events.d.ts
CHANGED
|
@@ -31,6 +31,10 @@ export declare class LoopGuard {
|
|
|
31
31
|
* expected to fix the cause (install a tool, create a dir) and retry. Only
|
|
32
32
|
* calls that already SUCCEEDED are deduped, since re-running them wastes a
|
|
33
33
|
* step and risks an infinite summarize loop.
|
|
34
|
+
*
|
|
35
|
+
* Read-only tools (web.fetch, fs.read, etc.) get a higher threshold (3)
|
|
36
|
+
* because they may legitimately need re-calling after context compaction
|
|
37
|
+
* removes their results.
|
|
34
38
|
*/
|
|
35
39
|
shouldBlock(name: string, args: Record<string, unknown>): {
|
|
36
40
|
block: boolean;
|
|
@@ -42,6 +46,11 @@ export declare class LoopGuard {
|
|
|
42
46
|
* (e.g., command not found → retry → not found → ...).
|
|
43
47
|
*/
|
|
44
48
|
hasRepeatedFailures(threshold?: number): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Reset counters for read-only tools. Called after context compaction
|
|
51
|
+
* so the model can re-fetch data whose results were compacted away.
|
|
52
|
+
*/
|
|
53
|
+
resetReadOnly(): void;
|
|
45
54
|
/**
|
|
46
55
|
* Get the total number of recorded attempts.
|
|
47
56
|
*/
|
package/dist/agent/loop-guard.js
CHANGED
|
@@ -1,3 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-mutating tools that may legitimately need re-calling after context
|
|
3
|
+
* compaction removes their earlier results. These get a higher dedup
|
|
4
|
+
* threshold (3 vs 2 for write tools) and their counters can be reset when
|
|
5
|
+
* context is compacted.
|
|
6
|
+
*/
|
|
7
|
+
const READ_ONLY_TOOLS = new Set([
|
|
8
|
+
"web.fetch",
|
|
9
|
+
"http.fetch",
|
|
10
|
+
"web.search",
|
|
11
|
+
"dns.lookup",
|
|
12
|
+
"whois.lookup",
|
|
13
|
+
"fs.read",
|
|
14
|
+
"fs.list",
|
|
15
|
+
"fs.search",
|
|
16
|
+
"sysinfo",
|
|
17
|
+
"net.context",
|
|
18
|
+
"tool.check",
|
|
19
|
+
"image.ocr",
|
|
20
|
+
"pdf.read",
|
|
21
|
+
]);
|
|
1
22
|
/**
|
|
2
23
|
* Track and detect tool-call repetition patterns so the agent doesn't
|
|
3
24
|
* waste steps in loops.
|
|
@@ -47,6 +68,10 @@ export class LoopGuard {
|
|
|
47
68
|
* expected to fix the cause (install a tool, create a dir) and retry. Only
|
|
48
69
|
* calls that already SUCCEEDED are deduped, since re-running them wastes a
|
|
49
70
|
* step and risks an infinite summarize loop.
|
|
71
|
+
*
|
|
72
|
+
* Read-only tools (web.fetch, fs.read, etc.) get a higher threshold (3)
|
|
73
|
+
* because they may legitimately need re-calling after context compaction
|
|
74
|
+
* removes their results.
|
|
50
75
|
*/
|
|
51
76
|
shouldBlock(name, args) {
|
|
52
77
|
const sig = this.canonicalize(name, args);
|
|
@@ -60,7 +85,10 @@ export class LoopGuard {
|
|
|
60
85
|
// that just wrote a file to "use the results you already have" is
|
|
61
86
|
// nonsensical and has caused models to assume the whole task is done.
|
|
62
87
|
const isWrite = name === "fs.write" || name === "fs.writeMany" || name === "fs.edit";
|
|
63
|
-
|
|
88
|
+
// Read-only tools get a higher threshold — they may need re-calling
|
|
89
|
+
// after context compaction removes their earlier results.
|
|
90
|
+
const threshold = READ_ONLY_TOOLS.has(name) ? 3 : 2;
|
|
91
|
+
if (count < threshold) {
|
|
64
92
|
return {
|
|
65
93
|
block: false,
|
|
66
94
|
reason: isWrite
|
|
@@ -68,12 +96,12 @@ export class LoopGuard {
|
|
|
68
96
|
: `${name} has already been called with these arguments once and succeeded. Consider using the results you already have.`,
|
|
69
97
|
};
|
|
70
98
|
}
|
|
71
|
-
// count >=
|
|
99
|
+
// count >= threshold and at least one success: block
|
|
72
100
|
return {
|
|
73
101
|
block: true,
|
|
74
102
|
reason: isWrite
|
|
75
103
|
? `${name} was already called ${count} time(s) with the identical path and content. That file is already written. Continue with the remaining files/steps or give your final answer.`
|
|
76
|
-
: `${name} was already called ${count} time(s) with the same arguments.
|
|
104
|
+
: `${name} was already called ${count} time(s) with the same arguments. The data is already in your context — analyze what you have and move to the next step.`,
|
|
77
105
|
};
|
|
78
106
|
}
|
|
79
107
|
getAttemptCount(name, args) {
|
|
@@ -90,6 +118,20 @@ export class LoopGuard {
|
|
|
90
118
|
const recent = this.attempts.slice(-threshold);
|
|
91
119
|
return recent.every((a) => !a.ok);
|
|
92
120
|
}
|
|
121
|
+
/**
|
|
122
|
+
* Reset counters for read-only tools. Called after context compaction
|
|
123
|
+
* so the model can re-fetch data whose results were compacted away.
|
|
124
|
+
*/
|
|
125
|
+
resetReadOnly() {
|
|
126
|
+
for (const sig of [...this.signatureCount.keys()]) {
|
|
127
|
+
const name = sig.split("::")[0] ?? "";
|
|
128
|
+
if (READ_ONLY_TOOLS.has(name)) {
|
|
129
|
+
this.signatureCount.delete(sig);
|
|
130
|
+
this.signatureSuccess.delete(sig);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
this.attempts = this.attempts.filter((a) => !READ_ONLY_TOOLS.has(a.callName));
|
|
134
|
+
}
|
|
93
135
|
/**
|
|
94
136
|
* Get the total number of recorded attempts.
|
|
95
137
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"AAUA;;;GAGG;AACH,MAAM,OAAO,SAAS;IACZ,QAAQ,GAAkB,EAAE,CAAC;IAC7B,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC3C,gBAAgB,GAAG,IAAI,GAAG,EAAmB,CAAC;IAEtD;;;;OAIG;IACH,YAAY,CAAC,IAAY,EAAE,IAA6B;QACtD,MAAM,MAAM,GAA4B,EAAE,CAAC;QAC3C,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3C,IAAI,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC;YACtB,8CAA8C;YAC9C,IAAI,IAAI,KAAK,YAAY,IAAI,GAAG,KAAK,SAAS,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;gBAC5E,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;YAC5C,CAAC;YACD,MAAM,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACtB,CAAC;QACD,OAAO,GAAG,IAAI,KAAK,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;IAC9C,CAAC;IAED,aAAa,CACX,IAAY,EACZ,IAAY,EACZ,IAA6B,EAC7B,EAAW,EACX,QAA6B;QAE7B,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC1C,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,kBAAkB,EAAE,GAAG,EAAE,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;QACpF,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;QACtE,wEAAwE;QACxE,yEAAyE;QACzE,qEAAqE;QACrE,kBAAkB;QAClB,IAAI,EAAE;YAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;aACxC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IAClF,CAAC;IAED
|
|
1
|
+
{"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/runner.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export declare function createSessionPolicy(): SessionPolicy;
|
|
|
18
18
|
export interface ConfirmPort {
|
|
19
19
|
confirmTool(call: ToolCall): Promise<boolean>;
|
|
20
20
|
confirmPentest(): Promise<boolean>;
|
|
21
|
+
/** Ask the user whether to continue after hitting the step budget. */
|
|
22
|
+
confirmContinue?(steps: number): Promise<boolean>;
|
|
21
23
|
}
|
|
22
24
|
export interface AgentRunOptions {
|
|
23
25
|
provider?: ProviderId | undefined;
|
|
@@ -120,6 +122,11 @@ export declare function sameToolCall(a: ToolCall, b: ToolCall): boolean;
|
|
|
120
122
|
* copies and note the collapse so the meaning is preserved without the bulk.
|
|
121
123
|
*/
|
|
122
124
|
export declare function collapseRepeatedText(text: string): string;
|
|
125
|
+
/**
|
|
126
|
+
* Detect pentest/security tasks that need the full step budget.
|
|
127
|
+
* Mirrors looksLikeBuildTask but for security work.
|
|
128
|
+
*/
|
|
129
|
+
export declare function looksLikePentestTask(prompt: string, history?: ChatMessage[] | undefined): boolean;
|
|
123
130
|
/**
|
|
124
131
|
* Decide whether this turn should get the build workflow (explore → plan →
|
|
125
132
|
* implement) and a generous step budget. Looks at the current prompt first,
|
package/dist/agent/runner.js
CHANGED
|
@@ -2,7 +2,7 @@ import { confirm } from "@inquirer/prompts";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { homedir } from "node:os";
|
|
5
|
-
import { join } from "node:path";
|
|
5
|
+
import { join, resolve } from "node:path";
|
|
6
6
|
import { streamWithProvider } from "../llm/router.js";
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { jobManager } from "../tools/jobs.js";
|
|
@@ -26,6 +26,7 @@ import { analyzeTask } from "./task-analyzer.js";
|
|
|
26
26
|
import { LoopGuard } from "./loop-guard.js";
|
|
27
27
|
import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
|
|
28
28
|
import { renderPlanChecklist, renderPlanSidePane } from "../ui/plan-pane.js";
|
|
29
|
+
import { pathInsideSandbox } from "../tools/fs.js";
|
|
29
30
|
/** Render the plan as a right-side pane on wide terminals, else inline. */
|
|
30
31
|
function renderPlanForTerminal(plan) {
|
|
31
32
|
const cols = process.stdout.columns ?? 0;
|
|
@@ -547,6 +548,25 @@ const LOCAL_RUNTIME_RE = /\b(?:current\s+(?:directory|dir|cwd|working\s+director
|
|
|
547
548
|
// under-budgeting silently truncates a half-built project.
|
|
548
549
|
const BUILD_TASK_RE = /\b(?:build|create|scaffold|generate|make|set\s*up|setup|bootstrap|init(?:ialize)?|implement|add|write|develop|code|refactor|migrate|convert|wire\s*up|integrate)\b[\s\S]{0,80}\b(?:app|application|project|site|website|web\s*app|server|api|service|component|page|module|feature|cli|script|library|package|frontend|backend|fullstack|game|bot|dashboard|form|endpoint|database|schema|test|tests|suite)\b/i;
|
|
549
550
|
const BUILD_STACK_RE = /\b(?:react|next(?:\.?js)?|vue|svelte|angular|vite|webpack|express|fastify|nest(?:js)?|django|flask|fastapi|rails|laravel|spring|node(?:\.?js)?|typescript|tailwind|redux|prisma|mongoose|graphql|docker|kubernetes)\b/i;
|
|
551
|
+
// Pentest / security keywords — these tasks are inherently multi-step and
|
|
552
|
+
// always deserve the full step budget, just like build tasks.
|
|
553
|
+
const PENTEST_TASK_RE = /\b(?:pentest|pen[\s-]?test|penetration|security\s*(?:test|audit|scan|assess)|csrf|xss|sqli|sql[\s-]?inject|rce|lfi|rfi|ssrf|idor|xxe|brute[\s-]?force|enumerat|exploit|vulnerabilit|recon|bug[\s-]?bounty|ctf|capture[\s-]?the[\s-]?flag|red[\s-]?team|offensive|nmap|nikto|nuclei|ffuf|gobuster|sqlmap|hydra|metasploit)\b/i;
|
|
554
|
+
/**
|
|
555
|
+
* Detect pentest/security tasks that need the full step budget.
|
|
556
|
+
* Mirrors looksLikeBuildTask but for security work.
|
|
557
|
+
*/
|
|
558
|
+
export function looksLikePentestTask(prompt, history) {
|
|
559
|
+
if (PENTEST_TASK_RE.test(prompt))
|
|
560
|
+
return true;
|
|
561
|
+
if (history && history.length > 0) {
|
|
562
|
+
const recent = history.slice(-6);
|
|
563
|
+
for (const msg of recent) {
|
|
564
|
+
if (msg.role === "user" && PENTEST_TASK_RE.test(msg.content))
|
|
565
|
+
return true;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return false;
|
|
569
|
+
}
|
|
550
570
|
// Short continuation prompts that, on their own, carry no build signal but
|
|
551
571
|
// clearly mean "keep going with what we were doing".
|
|
552
572
|
const CONTINUATION_RE = /^(?:do\s+it|build\s+it|build\s+fully|build\s+it\s+fully|go\s+ahead|continue|proceed|keep\s+going|finish(?:\s+it)?|complete(?:\s+it)?|yes|ok(?:ay)?|make\s+it|run\s+it|next|on\s+your\s+own|build\s+(?:fully\s+)?on\s+your\s+own)\b/i;
|
|
@@ -752,6 +772,58 @@ function restoreInteractiveStdin() {
|
|
|
752
772
|
/* ignore */
|
|
753
773
|
}
|
|
754
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Build a rich summary when the agent stops (user declined to continue or
|
|
777
|
+
* maxIterations ceiling hit). Includes plan state, key findings, and clear
|
|
778
|
+
* resume instructions so a later "continue" can pick up exactly here.
|
|
779
|
+
*/
|
|
780
|
+
async function buildRichStopSummary(messages, session, steps) {
|
|
781
|
+
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
782
|
+
const parts = [];
|
|
783
|
+
parts.push(`Session paused after ${steps} steps.\n`);
|
|
784
|
+
// Plan state
|
|
785
|
+
if (plan) {
|
|
786
|
+
parts.push("## Plan Status");
|
|
787
|
+
parts.push(`Goal: ${plan.goal}`);
|
|
788
|
+
for (const task of plan.tasks) {
|
|
789
|
+
const icon = task.state === "done"
|
|
790
|
+
? "✓"
|
|
791
|
+
: task.state === "in_progress"
|
|
792
|
+
? "▶"
|
|
793
|
+
: task.state === "failed"
|
|
794
|
+
? "✗"
|
|
795
|
+
: task.state === "skipped"
|
|
796
|
+
? "↷"
|
|
797
|
+
: "·";
|
|
798
|
+
parts.push(` ${icon} [${task.id}] (${task.state}) ${task.title}${task.note ? ` — ${task.note}` : ""}`);
|
|
799
|
+
}
|
|
800
|
+
const next = plan.tasks.find((t) => t.state === "pending" || t.state === "in_progress");
|
|
801
|
+
if (next) {
|
|
802
|
+
parts.push(`\nNext task to resume: ${next.id} — "${next.title}"`);
|
|
803
|
+
}
|
|
804
|
+
const doneCount = plan.tasks.filter((t) => t.state === "done").length;
|
|
805
|
+
parts.push(`\nProgress: ${doneCount}/${plan.tasks.length} tasks done.`);
|
|
806
|
+
}
|
|
807
|
+
// Key findings from tool results (last 20 tool messages)
|
|
808
|
+
const toolMsgs = messages.filter((m) => m.role === "tool").slice(-20);
|
|
809
|
+
if (toolMsgs.length > 0) {
|
|
810
|
+
parts.push("\n## Key Findings So Far");
|
|
811
|
+
for (const msg of toolMsgs) {
|
|
812
|
+
const firstLine = msg.content.split("\n")[0] ?? "";
|
|
813
|
+
// Extract tool name from the structured format "Tool <name> result ..."
|
|
814
|
+
const toolMatch = firstLine.match(/^Tool (\S+) result/);
|
|
815
|
+
if (toolMatch) {
|
|
816
|
+
parts.push(`- ${toolMatch[1]}: ${firstLine.slice(0, 150)}`);
|
|
817
|
+
}
|
|
818
|
+
else {
|
|
819
|
+
parts.push(`- ${firstLine.slice(0, 150)}`);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
parts.push("\n## To Resume");
|
|
824
|
+
parts.push('Type "continue" to pick up from where this session left off.');
|
|
825
|
+
return parts.join("\n");
|
|
826
|
+
}
|
|
755
827
|
/**
|
|
756
828
|
* Tools allowed while an UN-approved plan is active. Before the user runs
|
|
757
829
|
* /implement, the agent may only (re)create the plan and do read-only
|
|
@@ -876,6 +948,12 @@ const inquirerConfirmPort = {
|
|
|
876
948
|
default: false,
|
|
877
949
|
});
|
|
878
950
|
},
|
|
951
|
+
async confirmContinue(steps) {
|
|
952
|
+
return confirm({
|
|
953
|
+
message: chalk.yellow(` ${steps} steps reached — continue?`),
|
|
954
|
+
default: true,
|
|
955
|
+
});
|
|
956
|
+
},
|
|
879
957
|
};
|
|
880
958
|
async function ensurePentestAuthorization(call, autoConfirm, session, confirmPort) {
|
|
881
959
|
if (!isPentestToolCall(call))
|
|
@@ -1166,7 +1244,9 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1166
1244
|
// "now"/"latest" that trip the volatile-info regex; without this guard the
|
|
1167
1245
|
// agent burns its turn searching the date instead of writing files.
|
|
1168
1246
|
const buildLikeTurn = looksLikeBuildTask(prompt, options.history);
|
|
1247
|
+
const pentestLikeTurn = looksLikePentestTask(prompt, options.history);
|
|
1169
1248
|
const freshWebSearchRequired = !buildLikeTurn &&
|
|
1249
|
+
!pentestLikeTurn &&
|
|
1170
1250
|
toolNames.includes("web.search") &&
|
|
1171
1251
|
requiresFreshWebSearch(prompt);
|
|
1172
1252
|
const systemSections = [renderAgentSystemPrompt(toolNames.join(", "))];
|
|
@@ -1278,14 +1358,15 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1278
1358
|
const analysis = analyzeTask(prompt);
|
|
1279
1359
|
const hasHistory = (options.history?.length ?? 0) > 0;
|
|
1280
1360
|
const buildLike = buildLikeTurn;
|
|
1361
|
+
const pentestLike = looksLikePentestTask(prompt, options.history);
|
|
1281
1362
|
let stepBudget = analysis.complexity === "simple"
|
|
1282
1363
|
? 20
|
|
1283
1364
|
: analysis.complexity === "standard"
|
|
1284
1365
|
? 40
|
|
1285
1366
|
: maxSteps;
|
|
1286
|
-
if (buildLike) {
|
|
1287
|
-
// Scaffolding / multi-file work
|
|
1288
|
-
//
|
|
1367
|
+
if (buildLike || pentestLike) {
|
|
1368
|
+
// Scaffolding / multi-file work / pentest tasks need room.
|
|
1369
|
+
// Continuation prompts ("do it") inherit this too.
|
|
1289
1370
|
stepBudget = Math.max(stepBudget, maxSteps);
|
|
1290
1371
|
}
|
|
1291
1372
|
else if (hasHistory) {
|
|
@@ -1295,7 +1376,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1295
1376
|
}
|
|
1296
1377
|
// Hard ceiling on total loop iterations (productive + recovery) so a model
|
|
1297
1378
|
// stuck emitting only thinking or malformed calls can't loop indefinitely.
|
|
1298
|
-
|
|
1379
|
+
let maxIterations = stepBudget * 3;
|
|
1299
1380
|
let productiveSteps = 0;
|
|
1300
1381
|
let step = -1;
|
|
1301
1382
|
let nextToolEventId = 0;
|
|
@@ -1393,7 +1474,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1393
1474
|
if (needsPentestAuth) {
|
|
1394
1475
|
pentestJustConfirmed = true;
|
|
1395
1476
|
}
|
|
1396
|
-
|
|
1477
|
+
let forceManualConfirm = call.name === "fs.delete";
|
|
1478
|
+
if (call.name.startsWith("fs.") &&
|
|
1479
|
+
!isPreApprovalAllowedTool(call.name)) {
|
|
1480
|
+
const pathArg = typeof call.args.path === "string" ? call.args.path : undefined;
|
|
1481
|
+
if (pathArg) {
|
|
1482
|
+
const expandHomeLocal = (p) => p.startsWith("~/") || p.startsWith("~\\") ? join(homedir(), p.slice(2)) : p === "~" ? homedir() : p;
|
|
1483
|
+
const resolved = resolve(expandHomeLocal(pathArg));
|
|
1484
|
+
const mode = (call.name === "fs.read" || call.name === "fs.list" || call.name === "fs.search") ? "read" : "write";
|
|
1485
|
+
if (!pathInsideSandbox(resolved, mode)) {
|
|
1486
|
+
forceManualConfirm = true;
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1397
1490
|
if (decision.level === "confirm" && !pentestJustConfirmed) {
|
|
1398
1491
|
const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session, confirmPort);
|
|
1399
1492
|
restoreInteractiveStdin();
|
|
@@ -1475,6 +1568,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1475
1568
|
return;
|
|
1476
1569
|
printLive(chunk);
|
|
1477
1570
|
},
|
|
1571
|
+
confirmed: true,
|
|
1478
1572
|
});
|
|
1479
1573
|
if (liveBytes > 0 || liveTruncatedNotified) {
|
|
1480
1574
|
writeToolOutput(toolEventId, "\n", "\n");
|
|
@@ -1555,8 +1649,59 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1555
1649
|
// `step` is the productive-step index (used for display + audit). It only
|
|
1556
1650
|
// advances when the previous iteration actually executed a tool.
|
|
1557
1651
|
step = productiveSteps;
|
|
1558
|
-
|
|
1559
|
-
|
|
1652
|
+
// ── Step budget gate: ask the user instead of hard-stopping ────────
|
|
1653
|
+
if (productiveSteps >= stepBudget) {
|
|
1654
|
+
const askContinue = confirmPort.confirmContinue ?? inquirerConfirmPort.confirmContinue;
|
|
1655
|
+
let shouldContinue = false;
|
|
1656
|
+
try {
|
|
1657
|
+
shouldContinue = await askContinue(productiveSteps);
|
|
1658
|
+
restoreInteractiveStdin();
|
|
1659
|
+
}
|
|
1660
|
+
catch {
|
|
1661
|
+
// Abort / non-interactive — treat as decline.
|
|
1662
|
+
shouldContinue = false;
|
|
1663
|
+
}
|
|
1664
|
+
if (shouldContinue) {
|
|
1665
|
+
// Extend the budget for another chunk of work.
|
|
1666
|
+
const extension = Math.max(40, maxSteps);
|
|
1667
|
+
stepBudget += extension;
|
|
1668
|
+
maxIterations = stepBudget * 3;
|
|
1669
|
+
// Compact older messages to free context space.
|
|
1670
|
+
const compacted = compactMessages(messages, { budgetTokens: 0, keepRecent: 12 });
|
|
1671
|
+
if (compacted.length < messages.length) {
|
|
1672
|
+
messages.splice(0, messages.length, ...compacted);
|
|
1673
|
+
loopGuard.resetReadOnly();
|
|
1674
|
+
await auditLog("agent.compact", {
|
|
1675
|
+
newLength: messages.length,
|
|
1676
|
+
estimatedTokens: estimateMessagesTokens(messages),
|
|
1677
|
+
reason: "step-budget-continue",
|
|
1678
|
+
});
|
|
1679
|
+
}
|
|
1680
|
+
// Inject a progress summary so the model stays focused.
|
|
1681
|
+
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1682
|
+
let progressNote = "The step limit was reached and the user chose to continue. ";
|
|
1683
|
+
progressNote += "Review what you have accomplished so far and continue with the NEXT unfinished step. ";
|
|
1684
|
+
progressNote += "Do NOT repeat work already done. Do NOT re-fetch pages or re-run scans whose results you already have.";
|
|
1685
|
+
if (livePlan) {
|
|
1686
|
+
const doneTasks = livePlan.tasks.filter(t => t.state === "done");
|
|
1687
|
+
const pendingTasks = livePlan.tasks.filter(t => t.state === "pending" || t.state === "in_progress");
|
|
1688
|
+
progressNote += `\n\nPlan progress: ${doneTasks.length}/${livePlan.tasks.length} tasks done.`;
|
|
1689
|
+
if (pendingTasks.length > 0) {
|
|
1690
|
+
progressNote += ` Next: ${pendingTasks[0].id} — "${pendingTasks[0].title}".`;
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
messages.push({ role: "user", content: progressNote });
|
|
1694
|
+
writeNotice("info", `continuing — budget extended to ${stepBudget} steps`, chalk.dim(` ℹ continuing — budget extended to ${stepBudget} steps\n`));
|
|
1695
|
+
// Continue the loop — model doesn't know it paused.
|
|
1696
|
+
}
|
|
1697
|
+
else {
|
|
1698
|
+
// User declined — build a rich summary and return.
|
|
1699
|
+
const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
|
|
1700
|
+
writeAssistantMessage(richSummary);
|
|
1701
|
+
lastAnswer = richSummary;
|
|
1702
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1560
1705
|
options.signal?.throwIfAborted();
|
|
1561
1706
|
// `call` and `assistantText` are shared by both paths below: a fresh
|
|
1562
1707
|
// model round-trip, or draining a previously-queued tool call.
|
|
@@ -1678,24 +1823,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1678
1823
|
// answer and the user has to re-submit the same prompt.
|
|
1679
1824
|
if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
|
|
1680
1825
|
emptyVisibleRetries += 1;
|
|
1681
|
-
if (emptyVisibleRetries <=
|
|
1826
|
+
if (emptyVisibleRetries <= 3) {
|
|
1682
1827
|
writeThinkingBlock(assistantText.thinkContent);
|
|
1683
1828
|
writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
|
|
1684
1829
|
messages.push({
|
|
1685
1830
|
role: "assistant",
|
|
1686
1831
|
content: collapseRepeatedText(completion.text),
|
|
1687
1832
|
});
|
|
1833
|
+
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
1688
1834
|
const buildNudge = buildLikeTurn && !activePlan
|
|
1689
|
-
? "
|
|
1690
|
-
"
|
|
1691
|
-
|
|
1692
|
-
"Do NOT
|
|
1693
|
-
"Your ONLY allowed action right now is plan.create (or read/list for exploration)."
|
|
1694
|
-
: "You only produced internal reasoning with no visible answer or tool call. " +
|
|
1695
|
-
"You MUST either call a tool using the ```tool format or provide your final answer. " +
|
|
1696
|
-
"Do NOT wrap your tool call inside considering or reasoning tags — put it in the VISIBLE response, not hidden. " +
|
|
1697
|
-
"If images are attached, inspect them directly for visual details (text, colors, layout, spacing, style) instead of using OCR unless explicitly needed. " +
|
|
1698
|
-
"Do NOT just think — take action NOW.";
|
|
1835
|
+
? "No visible output. Emit a ```tool block to call plan.create now. " +
|
|
1836
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response."
|
|
1837
|
+
: "No visible output. Emit a ```tool block or give your final answer. " +
|
|
1838
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response.";
|
|
1699
1839
|
messages.push(recoveryUserMessage(buildNudge));
|
|
1700
1840
|
continue;
|
|
1701
1841
|
}
|
|
@@ -2003,10 +2143,13 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
2003
2143
|
return finishTurn(lastAnswer, productiveSteps);
|
|
2004
2144
|
}
|
|
2005
2145
|
// Compact older messages when the running estimate exceeds budget
|
|
2006
|
-
if (estimateMessagesTokens(messages) >
|
|
2007
|
-
const compacted = compactMessages(messages);
|
|
2146
|
+
if (estimateMessagesTokens(messages) > 32_000) {
|
|
2147
|
+
const compacted = compactMessages(messages, { keepRecent: 12 });
|
|
2008
2148
|
if (compacted.length < messages.length) {
|
|
2009
2149
|
messages.splice(0, messages.length, ...compacted);
|
|
2150
|
+
// Reset read-only tool counters so the model can re-fetch data
|
|
2151
|
+
// whose results were compacted away.
|
|
2152
|
+
loopGuard.resetReadOnly();
|
|
2010
2153
|
await auditLog("agent.compact", {
|
|
2011
2154
|
newLength: messages.length,
|
|
2012
2155
|
estimatedTokens: estimateMessagesTokens(messages),
|
|
@@ -2015,8 +2158,11 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
2015
2158
|
}
|
|
2016
2159
|
}
|
|
2017
2160
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2161
|
+
// maxIterations ceiling reached (safety net — normally the step budget
|
|
2162
|
+
// gate with user confirmation handles stopping gracefully).
|
|
2163
|
+
const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
|
|
2164
|
+
writeAssistantMessage(richSummary);
|
|
2165
|
+
lastAnswer = richSummary;
|
|
2020
2166
|
return finishTurn(lastAnswer, productiveSteps);
|
|
2021
2167
|
}
|
|
2022
2168
|
catch (error) {
|