@pentoshi/clai 2.0.18 → 2.0.20
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 +182 -28
- 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/os/permissions.d.ts +14 -0
- package/dist/os/permissions.js +76 -0
- package/dist/os/permissions.js.map +1 -0
- 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.js +25 -6
- package/dist/store/config.js.map +1 -1
- package/dist/store/history.js +80 -46
- package/dist/store/history.js.map +1 -1
- package/dist/store/keys.js +20 -6
- package/dist/store/keys.js.map +1 -1
- package/dist/store/logs.js +16 -6
- package/dist/store/logs.js.map +1 -1
- package/dist/store/plan.js +44 -21
- package/dist/store/plan.js.map +1 -1
- package/dist/store/scope.js +17 -5
- package/dist/store/scope.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,8 @@ 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 {
|
|
5
|
+
import { fixOwner, handlePermissionError } from "../os/permissions.js";
|
|
6
|
+
import { join, resolve } from "node:path";
|
|
6
7
|
import { streamWithProvider } from "../llm/router.js";
|
|
7
8
|
import { randomUUID } from "node:crypto";
|
|
8
9
|
import { jobManager } from "../tools/jobs.js";
|
|
@@ -26,6 +27,7 @@ import { analyzeTask } from "./task-analyzer.js";
|
|
|
26
27
|
import { LoopGuard } from "./loop-guard.js";
|
|
27
28
|
import { createPlan, loadPlan, savePlan, markTask, } from "../store/plan.js";
|
|
28
29
|
import { renderPlanChecklist, renderPlanSidePane } from "../ui/plan-pane.js";
|
|
30
|
+
import { pathInsideSandbox } from "../tools/fs.js";
|
|
29
31
|
/** Render the plan as a right-side pane on wide terminals, else inline. */
|
|
30
32
|
function renderPlanForTerminal(plan) {
|
|
31
33
|
const cols = process.stdout.columns ?? 0;
|
|
@@ -547,6 +549,25 @@ const LOCAL_RUNTIME_RE = /\b(?:current\s+(?:directory|dir|cwd|working\s+director
|
|
|
547
549
|
// under-budgeting silently truncates a half-built project.
|
|
548
550
|
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
551
|
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;
|
|
552
|
+
// Pentest / security keywords — these tasks are inherently multi-step and
|
|
553
|
+
// always deserve the full step budget, just like build tasks.
|
|
554
|
+
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;
|
|
555
|
+
/**
|
|
556
|
+
* Detect pentest/security tasks that need the full step budget.
|
|
557
|
+
* Mirrors looksLikeBuildTask but for security work.
|
|
558
|
+
*/
|
|
559
|
+
export function looksLikePentestTask(prompt, history) {
|
|
560
|
+
if (PENTEST_TASK_RE.test(prompt))
|
|
561
|
+
return true;
|
|
562
|
+
if (history && history.length > 0) {
|
|
563
|
+
const recent = history.slice(-6);
|
|
564
|
+
for (const msg of recent) {
|
|
565
|
+
if (msg.role === "user" && PENTEST_TASK_RE.test(msg.content))
|
|
566
|
+
return true;
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
550
571
|
// Short continuation prompts that, on their own, carry no build signal but
|
|
551
572
|
// clearly mean "keep going with what we were doing".
|
|
552
573
|
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 +773,58 @@ function restoreInteractiveStdin() {
|
|
|
752
773
|
/* ignore */
|
|
753
774
|
}
|
|
754
775
|
}
|
|
776
|
+
/**
|
|
777
|
+
* Build a rich summary when the agent stops (user declined to continue or
|
|
778
|
+
* maxIterations ceiling hit). Includes plan state, key findings, and clear
|
|
779
|
+
* resume instructions so a later "continue" can pick up exactly here.
|
|
780
|
+
*/
|
|
781
|
+
async function buildRichStopSummary(messages, session, steps) {
|
|
782
|
+
const plan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
783
|
+
const parts = [];
|
|
784
|
+
parts.push(`Session paused after ${steps} steps.\n`);
|
|
785
|
+
// Plan state
|
|
786
|
+
if (plan) {
|
|
787
|
+
parts.push("## Plan Status");
|
|
788
|
+
parts.push(`Goal: ${plan.goal}`);
|
|
789
|
+
for (const task of plan.tasks) {
|
|
790
|
+
const icon = task.state === "done"
|
|
791
|
+
? "✓"
|
|
792
|
+
: task.state === "in_progress"
|
|
793
|
+
? "▶"
|
|
794
|
+
: task.state === "failed"
|
|
795
|
+
? "✗"
|
|
796
|
+
: task.state === "skipped"
|
|
797
|
+
? "↷"
|
|
798
|
+
: "·";
|
|
799
|
+
parts.push(` ${icon} [${task.id}] (${task.state}) ${task.title}${task.note ? ` — ${task.note}` : ""}`);
|
|
800
|
+
}
|
|
801
|
+
const next = plan.tasks.find((t) => t.state === "pending" || t.state === "in_progress");
|
|
802
|
+
if (next) {
|
|
803
|
+
parts.push(`\nNext task to resume: ${next.id} — "${next.title}"`);
|
|
804
|
+
}
|
|
805
|
+
const doneCount = plan.tasks.filter((t) => t.state === "done").length;
|
|
806
|
+
parts.push(`\nProgress: ${doneCount}/${plan.tasks.length} tasks done.`);
|
|
807
|
+
}
|
|
808
|
+
// Key findings from tool results (last 20 tool messages)
|
|
809
|
+
const toolMsgs = messages.filter((m) => m.role === "tool").slice(-20);
|
|
810
|
+
if (toolMsgs.length > 0) {
|
|
811
|
+
parts.push("\n## Key Findings So Far");
|
|
812
|
+
for (const msg of toolMsgs) {
|
|
813
|
+
const firstLine = msg.content.split("\n")[0] ?? "";
|
|
814
|
+
// Extract tool name from the structured format "Tool <name> result ..."
|
|
815
|
+
const toolMatch = firstLine.match(/^Tool (\S+) result/);
|
|
816
|
+
if (toolMatch) {
|
|
817
|
+
parts.push(`- ${toolMatch[1]}: ${firstLine.slice(0, 150)}`);
|
|
818
|
+
}
|
|
819
|
+
else {
|
|
820
|
+
parts.push(`- ${firstLine.slice(0, 150)}`);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
parts.push("\n## To Resume");
|
|
825
|
+
parts.push('Type "continue" to pick up from where this session left off.');
|
|
826
|
+
return parts.join("\n");
|
|
827
|
+
}
|
|
755
828
|
/**
|
|
756
829
|
* Tools allowed while an UN-approved plan is active. Before the user runs
|
|
757
830
|
* /implement, the agent may only (re)create the plan and do read-only
|
|
@@ -793,11 +866,18 @@ async function saveToolOutput(call, output) {
|
|
|
793
866
|
if (!output.trim())
|
|
794
867
|
return undefined;
|
|
795
868
|
const dir = join(homedir(), ".clai", "outputs");
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
869
|
+
try {
|
|
870
|
+
await mkdir(dir, { recursive: true });
|
|
871
|
+
await fixOwner(dir);
|
|
872
|
+
const stamp = new Date().toISOString().replace(/[:.]/g, "-");
|
|
873
|
+
const path = join(dir, `${stamp}-${safeArtifactName(call.name)}.txt`);
|
|
874
|
+
await writeFile(path, `${output}\n`, "utf8");
|
|
875
|
+
await fixOwner(path);
|
|
876
|
+
return path;
|
|
877
|
+
}
|
|
878
|
+
catch (err) {
|
|
879
|
+
handlePermissionError(err);
|
|
880
|
+
}
|
|
801
881
|
}
|
|
802
882
|
function summarizeOutput(output, maxChars = 8_000) {
|
|
803
883
|
if (output.length <= maxChars)
|
|
@@ -876,6 +956,12 @@ const inquirerConfirmPort = {
|
|
|
876
956
|
default: false,
|
|
877
957
|
});
|
|
878
958
|
},
|
|
959
|
+
async confirmContinue(steps) {
|
|
960
|
+
return confirm({
|
|
961
|
+
message: chalk.yellow(` ${steps} steps reached — continue?`),
|
|
962
|
+
default: true,
|
|
963
|
+
});
|
|
964
|
+
},
|
|
879
965
|
};
|
|
880
966
|
async function ensurePentestAuthorization(call, autoConfirm, session, confirmPort) {
|
|
881
967
|
if (!isPentestToolCall(call))
|
|
@@ -1166,7 +1252,9 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1166
1252
|
// "now"/"latest" that trip the volatile-info regex; without this guard the
|
|
1167
1253
|
// agent burns its turn searching the date instead of writing files.
|
|
1168
1254
|
const buildLikeTurn = looksLikeBuildTask(prompt, options.history);
|
|
1255
|
+
const pentestLikeTurn = looksLikePentestTask(prompt, options.history);
|
|
1169
1256
|
const freshWebSearchRequired = !buildLikeTurn &&
|
|
1257
|
+
!pentestLikeTurn &&
|
|
1170
1258
|
toolNames.includes("web.search") &&
|
|
1171
1259
|
requiresFreshWebSearch(prompt);
|
|
1172
1260
|
const systemSections = [renderAgentSystemPrompt(toolNames.join(", "))];
|
|
@@ -1278,14 +1366,15 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1278
1366
|
const analysis = analyzeTask(prompt);
|
|
1279
1367
|
const hasHistory = (options.history?.length ?? 0) > 0;
|
|
1280
1368
|
const buildLike = buildLikeTurn;
|
|
1369
|
+
const pentestLike = looksLikePentestTask(prompt, options.history);
|
|
1281
1370
|
let stepBudget = analysis.complexity === "simple"
|
|
1282
1371
|
? 20
|
|
1283
1372
|
: analysis.complexity === "standard"
|
|
1284
1373
|
? 40
|
|
1285
1374
|
: maxSteps;
|
|
1286
|
-
if (buildLike) {
|
|
1287
|
-
// Scaffolding / multi-file work
|
|
1288
|
-
//
|
|
1375
|
+
if (buildLike || pentestLike) {
|
|
1376
|
+
// Scaffolding / multi-file work / pentest tasks need room.
|
|
1377
|
+
// Continuation prompts ("do it") inherit this too.
|
|
1289
1378
|
stepBudget = Math.max(stepBudget, maxSteps);
|
|
1290
1379
|
}
|
|
1291
1380
|
else if (hasHistory) {
|
|
@@ -1295,7 +1384,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1295
1384
|
}
|
|
1296
1385
|
// Hard ceiling on total loop iterations (productive + recovery) so a model
|
|
1297
1386
|
// stuck emitting only thinking or malformed calls can't loop indefinitely.
|
|
1298
|
-
|
|
1387
|
+
let maxIterations = stepBudget * 3;
|
|
1299
1388
|
let productiveSteps = 0;
|
|
1300
1389
|
let step = -1;
|
|
1301
1390
|
let nextToolEventId = 0;
|
|
@@ -1393,7 +1482,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1393
1482
|
if (needsPentestAuth) {
|
|
1394
1483
|
pentestJustConfirmed = true;
|
|
1395
1484
|
}
|
|
1396
|
-
|
|
1485
|
+
let forceManualConfirm = call.name === "fs.delete";
|
|
1486
|
+
if (call.name.startsWith("fs.") &&
|
|
1487
|
+
!isPreApprovalAllowedTool(call.name)) {
|
|
1488
|
+
const pathArg = typeof call.args.path === "string" ? call.args.path : undefined;
|
|
1489
|
+
if (pathArg) {
|
|
1490
|
+
const expandHomeLocal = (p) => p.startsWith("~/") || p.startsWith("~\\") ? join(homedir(), p.slice(2)) : p === "~" ? homedir() : p;
|
|
1491
|
+
const resolved = resolve(expandHomeLocal(pathArg));
|
|
1492
|
+
const mode = (call.name === "fs.read" || call.name === "fs.list" || call.name === "fs.search") ? "read" : "write";
|
|
1493
|
+
if (!pathInsideSandbox(resolved, mode)) {
|
|
1494
|
+
forceManualConfirm = true;
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
}
|
|
1397
1498
|
if (decision.level === "confirm" && !pentestJustConfirmed) {
|
|
1398
1499
|
const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session, confirmPort);
|
|
1399
1500
|
restoreInteractiveStdin();
|
|
@@ -1475,6 +1576,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1475
1576
|
return;
|
|
1476
1577
|
printLive(chunk);
|
|
1477
1578
|
},
|
|
1579
|
+
confirmed: true,
|
|
1478
1580
|
});
|
|
1479
1581
|
if (liveBytes > 0 || liveTruncatedNotified) {
|
|
1480
1582
|
writeToolOutput(toolEventId, "\n", "\n");
|
|
@@ -1555,8 +1657,59 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1555
1657
|
// `step` is the productive-step index (used for display + audit). It only
|
|
1556
1658
|
// advances when the previous iteration actually executed a tool.
|
|
1557
1659
|
step = productiveSteps;
|
|
1558
|
-
|
|
1559
|
-
|
|
1660
|
+
// ── Step budget gate: ask the user instead of hard-stopping ────────
|
|
1661
|
+
if (productiveSteps >= stepBudget) {
|
|
1662
|
+
const askContinue = confirmPort.confirmContinue ?? inquirerConfirmPort.confirmContinue;
|
|
1663
|
+
let shouldContinue = false;
|
|
1664
|
+
try {
|
|
1665
|
+
shouldContinue = await askContinue(productiveSteps);
|
|
1666
|
+
restoreInteractiveStdin();
|
|
1667
|
+
}
|
|
1668
|
+
catch {
|
|
1669
|
+
// Abort / non-interactive — treat as decline.
|
|
1670
|
+
shouldContinue = false;
|
|
1671
|
+
}
|
|
1672
|
+
if (shouldContinue) {
|
|
1673
|
+
// Extend the budget for another chunk of work.
|
|
1674
|
+
const extension = Math.max(40, maxSteps);
|
|
1675
|
+
stepBudget += extension;
|
|
1676
|
+
maxIterations = stepBudget * 3;
|
|
1677
|
+
// Compact older messages to free context space.
|
|
1678
|
+
const compacted = compactMessages(messages, { budgetTokens: 0, keepRecent: 12 });
|
|
1679
|
+
if (compacted.length < messages.length) {
|
|
1680
|
+
messages.splice(0, messages.length, ...compacted);
|
|
1681
|
+
loopGuard.resetReadOnly();
|
|
1682
|
+
await auditLog("agent.compact", {
|
|
1683
|
+
newLength: messages.length,
|
|
1684
|
+
estimatedTokens: estimateMessagesTokens(messages),
|
|
1685
|
+
reason: "step-budget-continue",
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
// Inject a progress summary so the model stays focused.
|
|
1689
|
+
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1690
|
+
let progressNote = "The step limit was reached and the user chose to continue. ";
|
|
1691
|
+
progressNote += "Review what you have accomplished so far and continue with the NEXT unfinished step. ";
|
|
1692
|
+
progressNote += "Do NOT repeat work already done. Do NOT re-fetch pages or re-run scans whose results you already have.";
|
|
1693
|
+
if (livePlan) {
|
|
1694
|
+
const doneTasks = livePlan.tasks.filter(t => t.state === "done");
|
|
1695
|
+
const pendingTasks = livePlan.tasks.filter(t => t.state === "pending" || t.state === "in_progress");
|
|
1696
|
+
progressNote += `\n\nPlan progress: ${doneTasks.length}/${livePlan.tasks.length} tasks done.`;
|
|
1697
|
+
if (pendingTasks.length > 0) {
|
|
1698
|
+
progressNote += ` Next: ${pendingTasks[0].id} — "${pendingTasks[0].title}".`;
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
messages.push({ role: "user", content: progressNote });
|
|
1702
|
+
writeNotice("info", `continuing — budget extended to ${stepBudget} steps`, chalk.dim(` ℹ continuing — budget extended to ${stepBudget} steps\n`));
|
|
1703
|
+
// Continue the loop — model doesn't know it paused.
|
|
1704
|
+
}
|
|
1705
|
+
else {
|
|
1706
|
+
// User declined — build a rich summary and return.
|
|
1707
|
+
const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
|
|
1708
|
+
writeAssistantMessage(richSummary);
|
|
1709
|
+
lastAnswer = richSummary;
|
|
1710
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1560
1713
|
options.signal?.throwIfAborted();
|
|
1561
1714
|
// `call` and `assistantText` are shared by both paths below: a fresh
|
|
1562
1715
|
// model round-trip, or draining a previously-queued tool call.
|
|
@@ -1678,24 +1831,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1678
1831
|
// answer and the user has to re-submit the same prompt.
|
|
1679
1832
|
if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
|
|
1680
1833
|
emptyVisibleRetries += 1;
|
|
1681
|
-
if (emptyVisibleRetries <=
|
|
1834
|
+
if (emptyVisibleRetries <= 3) {
|
|
1682
1835
|
writeThinkingBlock(assistantText.thinkContent);
|
|
1683
1836
|
writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
|
|
1684
1837
|
messages.push({
|
|
1685
1838
|
role: "assistant",
|
|
1686
1839
|
content: collapseRepeatedText(completion.text),
|
|
1687
1840
|
});
|
|
1841
|
+
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
1688
1842
|
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.";
|
|
1843
|
+
? "No visible output. Emit a ```tool block to call plan.create now. " +
|
|
1844
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response."
|
|
1845
|
+
: "No visible output. Emit a ```tool block or give your final answer. " +
|
|
1846
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response.";
|
|
1699
1847
|
messages.push(recoveryUserMessage(buildNudge));
|
|
1700
1848
|
continue;
|
|
1701
1849
|
}
|
|
@@ -2003,10 +2151,13 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
2003
2151
|
return finishTurn(lastAnswer, productiveSteps);
|
|
2004
2152
|
}
|
|
2005
2153
|
// Compact older messages when the running estimate exceeds budget
|
|
2006
|
-
if (estimateMessagesTokens(messages) >
|
|
2007
|
-
const compacted = compactMessages(messages);
|
|
2154
|
+
if (estimateMessagesTokens(messages) > 32_000) {
|
|
2155
|
+
const compacted = compactMessages(messages, { keepRecent: 12 });
|
|
2008
2156
|
if (compacted.length < messages.length) {
|
|
2009
2157
|
messages.splice(0, messages.length, ...compacted);
|
|
2158
|
+
// Reset read-only tool counters so the model can re-fetch data
|
|
2159
|
+
// whose results were compacted away.
|
|
2160
|
+
loopGuard.resetReadOnly();
|
|
2010
2161
|
await auditLog("agent.compact", {
|
|
2011
2162
|
newLength: messages.length,
|
|
2012
2163
|
estimatedTokens: estimateMessagesTokens(messages),
|
|
@@ -2015,8 +2166,11 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
2015
2166
|
}
|
|
2016
2167
|
}
|
|
2017
2168
|
}
|
|
2018
|
-
|
|
2019
|
-
|
|
2169
|
+
// maxIterations ceiling reached (safety net — normally the step budget
|
|
2170
|
+
// gate with user confirmation handles stopping gracefully).
|
|
2171
|
+
const richSummary = await buildRichStopSummary(messages, session, productiveSteps);
|
|
2172
|
+
writeAssistantMessage(richSummary);
|
|
2173
|
+
lastAnswer = richSummary;
|
|
2020
2174
|
return finishTurn(lastAnswer, productiveSteps);
|
|
2021
2175
|
}
|
|
2022
2176
|
catch (error) {
|