@pentoshi/clai 2.0.21 → 2.0.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.
Files changed (52) hide show
  1. package/README.md +4 -2
  2. package/dist/agent/loop-guard.d.ts +14 -0
  3. package/dist/agent/loop-guard.js +50 -0
  4. package/dist/agent/loop-guard.js.map +1 -1
  5. package/dist/agent/runner.d.ts +1 -0
  6. package/dist/agent/runner.js +192 -31
  7. package/dist/agent/runner.js.map +1 -1
  8. package/dist/commands/providers.js +2 -0
  9. package/dist/commands/providers.js.map +1 -1
  10. package/dist/commands/update.js +1 -1
  11. package/dist/index.js +1 -2
  12. package/dist/index.js.map +1 -1
  13. package/dist/llm/bynara.d.ts +2 -0
  14. package/dist/llm/bynara.js +80 -0
  15. package/dist/llm/bynara.js.map +1 -0
  16. package/dist/llm/capabilities.js +3 -0
  17. package/dist/llm/capabilities.js.map +1 -1
  18. package/dist/llm/http.d.ts +1 -1
  19. package/dist/llm/http.js +32 -24
  20. package/dist/llm/http.js.map +1 -1
  21. package/dist/llm/kimchi.js +28 -1
  22. package/dist/llm/kimchi.js.map +1 -1
  23. package/dist/llm/provider.d.ts +2 -0
  24. package/dist/llm/provider.js +27 -0
  25. package/dist/llm/provider.js.map +1 -1
  26. package/dist/llm/router.js +40 -18
  27. package/dist/llm/router.js.map +1 -1
  28. package/dist/prompts/index.d.ts +1 -1
  29. package/dist/prompts/index.js +6 -0
  30. package/dist/prompts/index.js.map +1 -1
  31. package/dist/repl.js +75 -3
  32. package/dist/repl.js.map +1 -1
  33. package/dist/store/config.js +1 -0
  34. package/dist/store/config.js.map +1 -1
  35. package/dist/store/keys.js +10 -5
  36. package/dist/store/keys.js.map +1 -1
  37. package/dist/tools/registry.d.ts +1 -0
  38. package/dist/tools/registry.js +11 -2
  39. package/dist/tools/registry.js.map +1 -1
  40. package/dist/tui/App.js +30 -5
  41. package/dist/tui/App.js.map +1 -1
  42. package/dist/tui/components/PickerPanel.js +7 -1
  43. package/dist/tui/components/PickerPanel.js.map +1 -1
  44. package/dist/tui/render-lines.js +78 -32
  45. package/dist/tui/render-lines.js.map +1 -1
  46. package/dist/types.d.ts +1 -1
  47. package/dist/types.js +1 -0
  48. package/dist/types.js.map +1 -1
  49. package/dist/ui/markdown.d.ts +1 -1
  50. package/dist/ui/markdown.js +16 -9
  51. package/dist/ui/markdown.js.map +1 -1
  52. package/package.json +1 -1
package/README.md CHANGED
@@ -106,7 +106,7 @@ classic REPL automatically.
106
106
 
107
107
  - **`/ask` mode** — Read-only. AI explains, gives commands & step-by-step guidance, but does NOT execute anything.
108
108
  - **`/agent` mode** — Agentic. AI plans, waits for approval, then executes shell commands, edits files, installs missing tools, parses output, and continues until the goal is met. Tasks run on an approve/refine/discard plan workflow (`/implement`, free-text to refine, `/discard` to cancel).
109
- - **9 LLM providers** — Groq, Google Gemini, OpenRouter, OpenAI, Anthropic, NVIDIA NIM, AgentRouter, Kimchi, AWS Mantle, and Ollama (local). All with streaming.
109
+ - **11 LLM providers** — Groq, Google Gemini, OpenRouter, OpenAI, Anthropic, NVIDIA NIM, AgentRouter, Kimchi, AWS Mantle, Ollama (local), and Bynara. All with streaming.
110
110
  - **10 built-in tools** — `shell.exec`, `fs.read`, `fs.write`, `fs.list`, `fs.search`, `pkg.install`, `net.scan`, `http.fetch`, `sysinfo`, `pentest.recon`.
111
111
  - **Smart safety gate** — Read-only commands auto-execute; mutating commands require confirmation; destructive patterns are blocked.
112
112
  - **OS-aware & tool-frugal** — Picks the best approach for your OS, prefers tools already installed (installs only when nothing suitable exists), broadens its approach and escalates privileges as needed to finish the task.
@@ -122,7 +122,7 @@ classic REPL automatically.
122
122
 
123
123
  ## Provider Setup
124
124
 
125
- clai supports 10 LLM providers (8 with free tiers):
125
+ clai supports 11 LLM providers (9 with free tiers):
126
126
 
127
127
  | Provider | Default Model | Free? | API Key Prefix |
128
128
  |-------------|----------------------------------------------|-------|----------------|
@@ -136,6 +136,7 @@ clai supports 10 LLM providers (8 with free tiers):
136
136
  | Kimchi | `kimi-k2.6` | ✓ | (any) |
137
137
  | AWS Mantle | `anthropic.claude-haiku-4-5` | — | `sk-ant-` |
138
138
  | Ollama | `llama3.1:8b` | ✓ | (local URL) |
139
+ | Bynara | `mimo-v2.5-free` | ✓ | `sk_nry_` |
139
140
 
140
141
  ```sh
141
142
  # Store an API key
@@ -192,6 +193,7 @@ export OLLAMA_HOST=http://localhost:11434
192
193
  | `/set <provider> [key]` | Store API key (masked input if key omitted) |
193
194
  | `/unset <provider>` | Remove stored key |
194
195
  | `/keys` | List configured providers, masked |
196
+ | `/info [provider]` | Show details & usage plan for a provider |
195
197
  | `/variants [on|off|low|medium|high]` | Toggle model thinking/reasoning variants |
196
198
  | `/think` | Show hidden thinking from last response |
197
199
  | `/output [last|id|list]`| Toggle full saved tool output |
@@ -46,6 +46,20 @@ export declare class LoopGuard {
46
46
  * (e.g., command not found → retry → not found → ...).
47
47
  */
48
48
  hasRepeatedFailures(threshold?: number): boolean;
49
+ /**
50
+ * Count consecutive failures trailing the most recent attempts.
51
+ * Stops at the first success (or the start of the history).
52
+ */
53
+ consecutiveFailureCount(): number;
54
+ /**
55
+ * Returns a reflection prompt when recent failures suggest the agent may
56
+ * be stuck, or null if everything looks fine.
57
+ *
58
+ * Unlike hardcoded thresholds, this provides context for the MODEL to
59
+ * decide whether to continue (lengthy but progressing approach) or
60
+ * switch/stop (genuinely stuck approach).
61
+ */
62
+ getFailureReflection(): string | null;
49
63
  /**
50
64
  * Reset counters for read-only tools. Called after context compaction
51
65
  * so the model can re-fetch data whose results were compacted away.
@@ -118,6 +118,56 @@ export class LoopGuard {
118
118
  const recent = this.attempts.slice(-threshold);
119
119
  return recent.every((a) => !a.ok);
120
120
  }
121
+ /**
122
+ * Count consecutive failures trailing the most recent attempts.
123
+ * Stops at the first success (or the start of the history).
124
+ */
125
+ consecutiveFailureCount() {
126
+ let count = 0;
127
+ for (let i = this.attempts.length - 1; i >= 0; i--) {
128
+ if (!this.attempts[i].ok)
129
+ count++;
130
+ else
131
+ break;
132
+ }
133
+ return count;
134
+ }
135
+ /**
136
+ * Returns a reflection prompt when recent failures suggest the agent may
137
+ * be stuck, or null if everything looks fine.
138
+ *
139
+ * Unlike hardcoded thresholds, this provides context for the MODEL to
140
+ * decide whether to continue (lengthy but progressing approach) or
141
+ * switch/stop (genuinely stuck approach).
142
+ */
143
+ getFailureReflection() {
144
+ const consecutiveFailures = this.consecutiveFailureCount();
145
+ if (consecutiveFailures < 3)
146
+ return null;
147
+ // Build context: what tools failed and what they were trying
148
+ const recentFails = this.attempts.slice(-consecutiveFailures);
149
+ const toolSummary = recentFails
150
+ .map((a) => ` - ${a.callName}: ${a.canonicalSignature.slice(0, 120)}`)
151
+ .slice(-5) // Show last 5 max
152
+ .join("\n");
153
+ const severity = consecutiveFailures >= 6 ? "CRITICAL" : "WARNING";
154
+ return `⚠ APPROACH EVALUATION REQUIRED (${severity}) — ${consecutiveFailures} consecutive tool calls have FAILED.
155
+
156
+ Recent failures:
157
+ ${toolSummary}
158
+
159
+ You MUST now pause and evaluate:
160
+ 1. Are these failures all related to the SAME approach/method? If yes, this approach may not be viable.
161
+ 2. Is there a DIFFERENT approach that could work? (different tool, different protocol, different technique)
162
+ 3. Or is this a series of unrelated small issues that are being fixed incrementally?
163
+
164
+ DECIDE one of:
165
+ - CONTINUE: if you're making real progress and each failure teaches you something new
166
+ - SWITCH: describe the new approach before trying it
167
+ - STOP: if you've exhausted viable approaches — tell the user honestly what you tried and why it didn't work
168
+
169
+ Do NOT keep trying variations of the same failing approach without explicitly deciding to SWITCH or STOP first.`;
170
+ }
121
171
  /**
122
172
  * Reset counters for read-only tools. Called after context compaction
123
173
  * so the model can re-fetch data whose results were compacted away.
@@ -1 +1 @@
1
- {"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"loop-guard.js","sourceRoot":"","sources":["../../src/agent/loop-guard.ts"],"names":[],"mappings":"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"}
@@ -39,6 +39,7 @@ export interface AgentRunOptions {
39
39
  }) => Promise<string | undefined>) | undefined;
40
40
  session?: SessionPolicy | undefined;
41
41
  }
42
+ export declare function preprocessJson(raw: string): string;
42
43
  export interface ParseToolCallOptions {
43
44
  /**
44
45
  * When true, only formats that are explicitly tool-call delimited are
@@ -44,9 +44,58 @@ export function createSessionPolicy() {
44
44
  planApproved: { value: false },
45
45
  };
46
46
  }
47
+ export function preprocessJson(raw) {
48
+ let inString = false;
49
+ let escaped = false;
50
+ let result = "";
51
+ for (let i = 0; i < raw.length; i++) {
52
+ const char = raw[i];
53
+ if (char === '"' && !escaped) {
54
+ inString = !inString;
55
+ result += char;
56
+ }
57
+ else if (inString) {
58
+ if (char === '\n') {
59
+ result += '\\n';
60
+ }
61
+ else if (char === '\r') {
62
+ result += '\\r';
63
+ }
64
+ else if (char === '\t') {
65
+ result += '\\t';
66
+ }
67
+ else {
68
+ result += char;
69
+ }
70
+ }
71
+ else {
72
+ if (char === ',' && i + 1 < raw.length) {
73
+ let nextNonWs = "";
74
+ for (let j = i + 1; j < raw.length; j++) {
75
+ if (!/\s/.test(raw[j])) {
76
+ nextNonWs = raw[j];
77
+ break;
78
+ }
79
+ }
80
+ if (nextNonWs === '}' || nextNonWs === ']') {
81
+ continue;
82
+ }
83
+ }
84
+ result += char;
85
+ }
86
+ if (char === '\\' && inString) {
87
+ escaped = !escaped;
88
+ }
89
+ else {
90
+ escaped = false;
91
+ }
92
+ }
93
+ return result;
94
+ }
47
95
  function tryParseCall(raw) {
48
96
  try {
49
- const parsed = JSON.parse(raw.trim());
97
+ const preprocessed = preprocessJson(raw);
98
+ const parsed = JSON.parse(preprocessed.trim());
50
99
  if (typeof parsed.name === "string" &&
51
100
  parsed.args &&
52
101
  typeof parsed.args === "object") {
@@ -78,9 +127,84 @@ function parseKimiToolCall(text) {
78
127
  const name = match[1];
79
128
  return tryParseCall(JSON.stringify({ name, args: tryJson(match[2]) ?? {} }));
80
129
  }
130
+ function parseXmlToolCall(text) {
131
+ // Pattern 1:
132
+ // <tool_call>
133
+ // <name>tool.name</name>
134
+ // <args>{...}</args>
135
+ // </tool_call>
136
+ const xmlNameArgs = text.match(/<tool_call>[\s\S]*?<name>\s*([\w.]+?)\s*<\/name>\s*<args>\s*(\{[\s\S]*?\})\s*<\/args>[\s\S]*?<\/tool_call>/i);
137
+ if (xmlNameArgs?.[1] && xmlNameArgs?.[2]) {
138
+ try {
139
+ const args = JSON.parse(preprocessJson(xmlNameArgs[2]));
140
+ return {
141
+ name: xmlNameArgs[1],
142
+ args: args,
143
+ };
144
+ }
145
+ catch { }
146
+ }
147
+ // Pattern 1b (MiMo alternative):
148
+ // <tool_call>
149
+ // <tool_name>tool.name</tool_name>
150
+ // <parameters>{...}</parameters>
151
+ // </tool_call>
152
+ const xmlToolNameParams = text.match(/<tool_call>[\s\S]*?<tool_name>\s*([\w.]+?)\s*<\/tool_name>\s*<parameters>\s*(\{[\s\S]*?\})\s*<\/parameters>[\s\S]*?<\/tool_call>/i);
153
+ if (xmlToolNameParams?.[1] && xmlToolNameParams?.[2]) {
154
+ try {
155
+ const args = JSON.parse(preprocessJson(xmlToolNameParams[2]));
156
+ return {
157
+ name: xmlToolNameParams[1],
158
+ args: args,
159
+ };
160
+ }
161
+ catch { }
162
+ }
163
+ // Pattern 1c (MiMo function/parameter format):
164
+ // <tool_call>
165
+ // <function=tool.name>
166
+ // <parameter=name>value</parameter>
167
+ // </function>
168
+ // </tool_call>
169
+ const xmlFunctionBlock = text.match(/<tool_call>[\s\S]*?<function=([\w.]+?)>([\s\S]*?)<\/function>[\s\S]*?<\/tool_call>/i);
170
+ if (xmlFunctionBlock?.[1] && xmlFunctionBlock?.[2]) {
171
+ const name = xmlFunctionBlock[1];
172
+ const inner = xmlFunctionBlock[2];
173
+ const args = {};
174
+ const paramRegex = /<parameter=([\w.]+?)>([\s\S]*?)<\/parameter>/gi;
175
+ let paramMatch;
176
+ while ((paramMatch = paramRegex.exec(inner)) !== null) {
177
+ const paramName = paramMatch[1];
178
+ const paramValueStr = paramMatch[2].trim();
179
+ let paramValue = paramValueStr;
180
+ try {
181
+ if (/^(?:\[|\{|true|false|null|\d+(\.\d+)?$)/i.test(paramValueStr)) {
182
+ paramValue = JSON.parse(preprocessJson(paramValueStr));
183
+ }
184
+ }
185
+ catch { }
186
+ args[paramName] = paramValue;
187
+ }
188
+ return { name, args };
189
+ }
190
+ // Pattern 2:
191
+ // <tool_call>
192
+ // <tool>
193
+ // {"name": "...", "args": {...}}
194
+ // </tool_call>
195
+ // or simply <tool_call> {"name": "...", "args": {...}} </tool_call>
196
+ const xmlJson = text.match(/<tool_call>[\s\S]*?(?:<tool>)?\s*(\{[\s\S]*?\})\s*<\/tool_call>/i);
197
+ if (xmlJson?.[1]) {
198
+ const call = tryParseCall(xmlJson[1]);
199
+ if (call)
200
+ return call;
201
+ }
202
+ return undefined;
203
+ }
81
204
  function tryJson(raw) {
82
205
  try {
83
- const parsed = JSON.parse(raw);
206
+ const preprocessed = preprocessJson(raw);
207
+ const parsed = JSON.parse(preprocessed);
84
208
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
85
209
  return parsed;
86
210
  }
@@ -110,13 +234,10 @@ export function parseToolCall(text, options = {}) {
110
234
  if (call)
111
235
  return call;
112
236
  }
113
- // 2. <tool_call>...</tool_call>
114
- const xml = text.match(/<tool_call>([\s\S]*?)<\/tool_call>/i);
115
- if (xml?.[1]) {
116
- const call = tryParseCall(xml[1]);
117
- if (call)
118
- return call;
119
- }
237
+ // 2. <tool_call>...</tool_call> (XML formats)
238
+ const xmlCall = parseXmlToolCall(text);
239
+ if (xmlCall)
240
+ return xmlCall;
120
241
  // 3. Kimi/Moonshot sentinel format (used by kimi-k2 family on NIM).
121
242
  const kimi = parseKimiToolCall(text);
122
243
  if (kimi)
@@ -437,7 +558,7 @@ export function parseAllToolCalls(text) {
437
558
  }
438
559
  const xmlRe = /<tool_call>([\s\S]*?)<\/tool_call>/gi;
439
560
  while ((m = xmlRe.exec(text)) !== null) {
440
- const call = tryParseCall(m[1] ?? "");
561
+ const call = parseXmlToolCall(m[0]);
441
562
  if (call)
442
563
  found.push({ index: m.index, call });
443
564
  }
@@ -481,16 +602,16 @@ export function collapseRepeatedText(text) {
481
602
  /** Extract the text before the tool call block for display purposes */
482
603
  function textBeforeToolCall(text) {
483
604
  const patterns = [
484
- /```tool\s*\n?[\s\S]*?```/i,
485
- /<tool_call>[\s\S]*?<\/tool_call>/i,
605
+ /```tool\s*\n?[\s\S]*$/i,
606
+ /<tool_call>[\s\S]*$/i,
486
607
  // Kimi/Moonshot sentinel block — strip from the section opener
487
608
  // (or the first call opener if the section header is missing).
488
609
  /<\|tool_calls_section_begin\|>[\s\S]*$/i,
489
610
  /<\|tool_call_begin\|>[\s\S]*$/i,
490
- /#{1,3}\s*tool\s*\n\s*\{[\s\S]*\}/i,
491
- /\*\*tool\*\*\s*\n\s*\{[\s\S]*\}/i,
492
- /```\w*\s*\n?\{[\s\S]*?"name"[\s\S]*?\}[\s\S]*?```/,
493
- /\{"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[\s\S]*?\}\s*\}\s*$/,
611
+ /#{1,3}\s*tool\s*\n\s*\{[\s\S]*$/i,
612
+ /\*\*tool\*\*\s*\n\s*\{[\s\S]*$/i,
613
+ /```\w*\s*\n?\{[\s\S]*?"name"[\s\S]*$/i,
614
+ /\{"name"\s*:\s*"[^"]+"\s*,\s*"args"\s*:\s*\{[\s\S]*$/i,
494
615
  ];
495
616
  for (const pattern of patterns) {
496
617
  const idx = text.search(pattern);
@@ -638,7 +759,7 @@ export function looksLikeBuildTask(prompt, history) {
638
759
  }
639
760
  // Matrix of action-verb narration: the model says it is *about to* do
640
761
  // something but hasn't. Used to detect "narrate, don't act" stalls.
641
- const ACTION_NARRATION_RE = /\b(?:let me|let's|i'?ll|i will|i'?m going to|i am going to|i need to|i should|i'?m about to|going to|now i'?ll|first[,]?\s*i'?ll)\s+(?:now\s+|first\s+|quickly\s+|just\s+|go\s+ahead\s+and\s+)?(?:explore|list|read|fetch|browse|check|inspect|examine|look|create|run|start|write|build|add|scaffold|set\s*up|setup|install|initialize|init|generate|make|review|open|find|search|verify|update|edit|modify|fix|implement)\b/i;
762
+ const ACTION_NARRATION_RE = /\b(?:let me|let's|i'?ll|i will|i'?m going to|i am going to|i need to|i should|i'?m about to|going to|now i'?ll|first[,]?\s*i'?ll)\s+(?:now\s+|first\s+|quickly\s+|just\s+|go\s+ahead\s+and\s+)?(?:explore|list|read|fetch|browse|check|inspect|examine|look|create|run|start|write|build|add|scaffold|set\s*up|setup|install|initialize|init|generate|make|review|open|find|search|verify|update|edit|modify|fix|implement|gather|assess|scan|audit)\b/i;
642
763
  /**
643
764
  * Detect a message that narrates an *upcoming* action ("let me explore the
644
765
  * directory", "I'll create the components") rather than an actual answer or
@@ -1567,6 +1688,12 @@ export async function runAgentLoop(prompt, options = {}) {
1567
1688
  artifactPath: "",
1568
1689
  };
1569
1690
  jobManager.registerJob(jobId, backgroundJob, toolAc);
1691
+ const TOOL_STALL_WARNING_MS = 60_000; // 1 minute
1692
+ const stallTimer = setTimeout(() => {
1693
+ if (!toolAc.signal.aborted) {
1694
+ writeNotice("info", `${call.name} has been running for >60s — still waiting (ESC to abort)`, chalk.yellow(` ⏳ ${call.name} still running — ESC to abort\n`));
1695
+ }
1696
+ }, TOOL_STALL_WARNING_MS);
1570
1697
  try {
1571
1698
  result = await runToolCall(call, {
1572
1699
  signal: toolAc.signal,
@@ -1577,6 +1704,7 @@ export async function runAgentLoop(prompt, options = {}) {
1577
1704
  printLive(chunk);
1578
1705
  },
1579
1706
  confirmed: true,
1707
+ userPrompt: prompt,
1580
1708
  });
1581
1709
  if (liveBytes > 0 || liveTruncatedNotified) {
1582
1710
  writeToolOutput(toolEventId, "\n", "\n");
@@ -1593,6 +1721,7 @@ export async function runAgentLoop(prompt, options = {}) {
1593
1721
  result = { ok: false, output: `Tool error: ${errMsg}`, exitCode: 1 };
1594
1722
  }
1595
1723
  finally {
1724
+ clearTimeout(stallTimer);
1596
1725
  parentSignal.removeEventListener("abort", onParentAbort);
1597
1726
  }
1598
1727
  const output = result.output.trim();
@@ -1622,6 +1751,18 @@ export async function runAgentLoop(prompt, options = {}) {
1622
1751
  output: result.output.slice(0, 4_000),
1623
1752
  });
1624
1753
  loopGuard.recordAttempt(step, call.name, call.args, result.ok, result.exitCode);
1754
+ // Inject approach evaluation when consecutive failures are detected.
1755
+ // Lets the MODEL decide (with full context) whether to continue a
1756
+ // legitimately long approach, switch, or stop — instead of a
1757
+ // hardcoded kill threshold.
1758
+ if (!result.ok) {
1759
+ const reflection = loopGuard.getFailureReflection();
1760
+ if (reflection) {
1761
+ messages.push({ role: "system", content: reflection });
1762
+ const failCount = loopGuard.consecutiveFailureCount();
1763
+ writeNotice("warn", `${failCount} consecutive failures — model evaluating approach`, chalk.yellow(` ⚠ ${failCount} consecutive failures — evaluating approach\n`));
1764
+ }
1765
+ }
1625
1766
  const statusIcon = result.ok ? chalk.green(" ✓") : chalk.red(" ✗");
1626
1767
  writeToolOutput(toolEventId, result.ok ? "ok\n" : "failed\n", statusIcon + "\n");
1627
1768
  if (output) {
@@ -1824,16 +1965,24 @@ export async function runAgentLoop(prompt, options = {}) {
1824
1965
  writeNotice("info", "recovered tool call from thinking content", chalk.dim(" ℹ recovered tool call from thinking content\n"));
1825
1966
  }
1826
1967
  }
1827
- // ── Thinking-only recovery ────────────────────────────────────────
1828
- // Some models (eg gpt-oss-20b on NVIDIA NIM) occasionally spend their
1829
- // entire budget on hidden <think> reasoning and emit no visible text
1830
- // or tool call. Without this guard the agent silently returns an empty
1831
- // answer and the user has to re-submit the same prompt.
1832
- if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
1968
+ // ── Empty-response recovery ───────────────────────────────────────
1969
+ // Some models occasionally return an empty completion: a reasoning
1970
+ // model that spent its whole budget on hidden &lt;think&gt; reasoning and emitted
1971
+ // no visible text, OR (more perniciously) a gateway hiccup that
1972
+ // streamed [DONE] with no content deltas at all. Without this guard
1973
+ // the agent silently ends the turn with no answer, no warning, and no
1974
+ // error — the user just sees the spinner stop. Catch BOTH cases
1975
+ // (thinking-only AND truly empty) and nudge the model to retry.
1976
+ if (!assistantText.visible.trim() && !call) {
1833
1977
  emptyVisibleRetries += 1;
1834
1978
  if (emptyVisibleRetries <= 3) {
1835
- writeThinkingBlock(assistantText.thinkContent);
1836
- writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
1979
+ if (assistantText.hasThinking) {
1980
+ writeThinkingBlock(assistantText.thinkContent);
1981
+ writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
1982
+ }
1983
+ else {
1984
+ writeNotice("warn", "model returned an empty response — nudging it to answer", chalk.yellow(" ⚠ model returned an empty response — nudging it to answer\n"));
1985
+ }
1837
1986
  messages.push({
1838
1987
  role: "assistant",
1839
1988
  content: collapseRepeatedText(completion.text),
@@ -1847,8 +1996,10 @@ export async function runAgentLoop(prompt, options = {}) {
1847
1996
  messages.push(recoveryUserMessage(buildNudge));
1848
1997
  continue;
1849
1998
  }
1850
- // Exhausted retries — fall through to the normal empty-answer path
1851
- // which will print a warning and return.
1999
+ // Exhausted retries — surface a clear notice instead of ending the
2000
+ // turn silently with no answer (which left the user staring at a
2001
+ // stopped spinner with no clue what happened).
2002
+ writeNotice("warn", "model returned an empty response after retries — no answer produced", chalk.yellow(" ⚠ model returned an empty response after retries — no answer produced\n"));
1852
2003
  }
1853
2004
  else {
1854
2005
  // Reset the counter on any successful visible output or recovered call.
@@ -1979,6 +2130,7 @@ export async function runAgentLoop(prompt, options = {}) {
1979
2130
  // Nudge it to emit a real tool call, with a concrete example.
1980
2131
  const narratedAction = looksLikeActionNarration(cleaned);
1981
2132
  const wantsAction = buildLikeTurn ||
2133
+ pentestLikeTurn ||
1982
2134
  (activePlan && session.planApproved.value) ||
1983
2135
  freshWebSearchRequired ||
1984
2136
  narratedAction;
@@ -1996,6 +2148,13 @@ export async function runAgentLoop(prompt, options = {}) {
1996
2148
  "You wrote a message but emitted NO ```tool block, so NOTHING ran. Do NOT narrate what you will do — DO it. Emit the next tool call now (task.update / fs.writeMany / shell.exec) in a single ```tool block.";
1997
2149
  writeNotice("warn", "described an action but emitted no tool call — nudging it to run one", chalk.yellow(" ⚠ described an action but emitted no tool call — nudging it to run one\n"));
1998
2150
  }
2151
+ else if (pentestLikeTurn) {
2152
+ nudge =
2153
+ "You described what you will do but emitted NO ```tool block, so NOTHING actually happened — narration is not action. Emit a real tool call NOW (e.g. net.scan / sysinfo / shell.exec). For example, to scan local network or read system settings:\n" +
2154
+ '```tool\n{"name":"sysinfo","args":{}}\n```\n' +
2155
+ "Every turn MUST contain a ```tool block until the task is done.";
2156
+ writeNotice("warn", "described a security/pentest action but emitted no tool call — nudging it to run one", chalk.yellow(" ⚠ described a security/pentest action but emitted no tool call — nudging it to run one\n"));
2157
+ }
1999
2158
  else if (!buildLikeTurn) {
2000
2159
  nudge =
2001
2160
  "You wrote that you would fetch/search/read something but emitted NO ```tool block, so NOTHING ran. Do NOT narrate the next browsing step — DO it. Emit exactly one valid ```tool block now. If you know the exact page, use:\n" +
@@ -2107,14 +2266,16 @@ export async function runAgentLoop(prompt, options = {}) {
2107
2266
  if (assistantText.hasThinking) {
2108
2267
  writeThinkingBlock(assistantText.thinkContent);
2109
2268
  }
2110
- messages.push({
2111
- role: "assistant",
2112
- content: collapseRepeatedText(assistantText.visible),
2113
- });
2114
2269
  let allCalls = parseAllToolCalls(assistantText.visible || assistantText.thinkContent);
2115
2270
  if (allCalls.length === 0 && call) {
2116
2271
  allCalls = [call];
2117
2272
  }
2273
+ const standardizedContent = (beforeTool ? beforeTool.trim() + "\n\n" : "") +
2274
+ allCalls.map(c => `\`\`\`tool\n${JSON.stringify(c)}\n\`\`\``).join("\n\n");
2275
+ messages.push({
2276
+ role: "assistant",
2277
+ content: standardizedContent,
2278
+ });
2118
2279
  if (allCalls.length > 1) {
2119
2280
  writeNotice("info", `${allCalls.length} tool calls in this message — running them in parallel`, chalk.dim(` ℹ ${allCalls.length} tool calls in this message — running them in parallel\n`));
2120
2281
  }