@pentoshi/clai 2.0.52 → 2.0.54
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/README.md +6 -3
- package/dist/agent/runner.js +100 -69
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tool-call-parser.js +6 -1
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/commands/providers.js +2 -0
- package/dist/commands/providers.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/llm/capabilities.js +5 -2
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/gemini.js +74 -22
- package/dist/llm/gemini.js.map +1 -1
- package/dist/llm/groq.d.ts +1 -0
- package/dist/llm/groq.js +14 -0
- package/dist/llm/groq.js.map +1 -1
- package/dist/llm/http.d.ts +10 -2
- package/dist/llm/http.js +107 -45
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/nvidia.js +5 -1
- package/dist/llm/nvidia.js.map +1 -1
- package/dist/llm/provider.js +7 -1
- package/dist/llm/provider.js.map +1 -1
- package/dist/llm/qwen-cloud.d.ts +2 -0
- package/dist/llm/qwen-cloud.js +81 -0
- package/dist/llm/qwen-cloud.js.map +1 -0
- package/dist/llm/router.js +9 -3
- package/dist/llm/router.js.map +1 -1
- package/dist/prompts/index.d.ts +6 -0
- package/dist/prompts/index.js +49 -0
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl/slash-commands.js +7 -0
- package/dist/repl/slash-commands.js.map +1 -1
- package/dist/repl.js +35 -34
- package/dist/repl.js.map +1 -1
- package/dist/store/config.js +1 -0
- package/dist/store/config.js.map +1 -1
- package/dist/tui/App.js +126 -71
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/hooks/useAgentRunner.d.ts +3 -1
- package/dist/tui/hooks/useAgentRunner.js +8 -3
- package/dist/tui/hooks/useAgentRunner.js.map +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.js +1 -0
- package/dist/types.js.map +1 -1
- 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
|
-
- **
|
|
109
|
+
- **12 LLM providers** — Groq, Google Gemini, OpenRouter, OpenAI, Anthropic, NVIDIA NIM, AgentRouter, Kimchi, AWS Mantle, Ollama (local), Bynara, and Qwen Cloud. 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
|
|
125
|
+
clai supports 12 LLM providers (9 with free tiers):
|
|
126
126
|
|
|
127
127
|
| Provider | Default Model | Free? | API Key Prefix |
|
|
128
128
|
|-------------|----------------------------------------------|-------|----------------|
|
|
@@ -137,6 +137,7 @@ clai supports 11 LLM providers (9 with free tiers):
|
|
|
137
137
|
| AWS Mantle | `anthropic.claude-haiku-4-5` | — | `sk-ant-` |
|
|
138
138
|
| Ollama | `llama3.1:8b` | ✓ | (local URL) |
|
|
139
139
|
| Bynara | `mimo-v2.5-free` | ✓ | `sk_nry_` |
|
|
140
|
+
| Qwen Cloud | `qwen3.7-plus` | — | `sk-` |
|
|
140
141
|
|
|
141
142
|
```sh
|
|
142
143
|
# Store an API key
|
|
@@ -178,6 +179,7 @@ export NVIDIA_API_KEY=nvapi-...
|
|
|
178
179
|
export CASTAI_API_KEY=...
|
|
179
180
|
export ANTHROPIC_WORKSPACE_ID=default # optional, for AWS Mantle
|
|
180
181
|
export OLLAMA_HOST=http://localhost:11434
|
|
182
|
+
export DASHSCOPE_API_KEY=sk-...
|
|
181
183
|
```
|
|
182
184
|
|
|
183
185
|
## REPL Commands
|
|
@@ -434,7 +436,8 @@ clai/
|
|
|
434
436
|
│ │ ├─ openai.ts # OpenAI provider (streaming)
|
|
435
437
|
│ │ ├─ anthropic.ts # Anthropic provider (streaming)
|
|
436
438
|
│ │ ├─ nvidia.ts # NVIDIA NIM provider (streaming)
|
|
437
|
-
│ │
|
|
439
|
+
│ │ ├─ openrouter.ts # OpenRouter provider (streaming)
|
|
440
|
+
│ │ └─ qwen-cloud.ts # Qwen Cloud provider (streaming + live models)
|
|
438
441
|
│ ├─ tools/
|
|
439
442
|
│ │ ├─ registry.ts # Tool dispatch table
|
|
440
443
|
│ │ ├─ shell.ts # shell.exec via execa
|
package/dist/agent/runner.js
CHANGED
|
@@ -4,13 +4,14 @@ import { join, relative, resolve } from "node:path";
|
|
|
4
4
|
import { streamWithProvider, completeWithProvider } from "../llm/router.js";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { jobManager } from "../tools/jobs.js";
|
|
7
|
-
import { renderAgentSystemPrompt, scratchDirFor, } from "../prompts/index.js";
|
|
7
|
+
import { renderAgentSystemPrompt, renderCompactAgentSystemPrompt, scratchDirFor, } from "../prompts/index.js";
|
|
8
8
|
import { getConfig } from "../store/config.js";
|
|
9
|
+
import { groqInputTokenBudget } from "../llm/groq.js";
|
|
9
10
|
import { classifyToolCall, isPentestToolCall, scopeHint, scopeTargetForToolCall, } from "../safety/classifier.js";
|
|
10
11
|
import { availableToolNames, normalizeToolCall, runToolCall, BATCH_SAFE_TOOLS, } from "../tools/registry.js";
|
|
11
12
|
import { looksInteractiveStdin } from "../tools/shell.js";
|
|
12
13
|
import { formatViewportHint, registerViewport } from "../ui/output-pane.js";
|
|
13
|
-
import { compactMessagesWithSummary, estimateMessagesTokens, COMPACTION_MEMORY_PREFIX, } from "./context-manager.js";
|
|
14
|
+
import { compactMessagesWithSummary, estimateTokens, estimateMessagesTokens, COMPACTION_MEMORY_PREFIX, } from "./context-manager.js";
|
|
14
15
|
import { auditLog } from "../store/logs.js";
|
|
15
16
|
import { loadProjectContext } from "../store/project.js";
|
|
16
17
|
import { loadScope, isScopeActive, targetInScope } from "../store/scope.js";
|
|
@@ -248,16 +249,26 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
248
249
|
!pentestLikeTurn &&
|
|
249
250
|
toolNames.includes("web.search") &&
|
|
250
251
|
requiresFreshWebSearch(prompt);
|
|
251
|
-
|
|
252
|
+
let provider = options.provider ?? config.defaultProvider;
|
|
253
|
+
await ensureProviderConfigured(provider);
|
|
254
|
+
let model = options.model ?? config.defaultModel;
|
|
255
|
+
// Some Groq free-tier models have a per-request/per-minute input budget
|
|
256
|
+
// below the normal agent prompt alone. Select a purpose-built compact
|
|
257
|
+
// instruction set before the request is made, rather than treating the
|
|
258
|
+
// provider's 413 as a context-window failure after the fact.
|
|
259
|
+
const inputTokenBudget = provider === "groq" ? groqInputTokenBudget(model) : undefined;
|
|
260
|
+
const useCompactSystemPrompt = inputTokenBudget !== undefined;
|
|
261
|
+
const systemSections = [
|
|
262
|
+
(useCompactSystemPrompt
|
|
263
|
+
? renderCompactAgentSystemPrompt
|
|
264
|
+
: renderAgentSystemPrompt)(toolNames.join(", ")),
|
|
265
|
+
];
|
|
252
266
|
if (projectContext) {
|
|
253
267
|
systemSections.push(`Project context from .clai/context.md:\n${projectContext}`);
|
|
254
268
|
}
|
|
255
269
|
if (freshWebSearchRequired) {
|
|
256
270
|
systemSections.push(freshnessGuardMessage());
|
|
257
271
|
}
|
|
258
|
-
let provider = options.provider ?? config.defaultProvider;
|
|
259
|
-
await ensureProviderConfigured(provider);
|
|
260
|
-
let model = options.model ?? config.defaultModel;
|
|
261
272
|
let lastAnswer = "";
|
|
262
273
|
const session = options.session ?? createSessionPolicy();
|
|
263
274
|
// Active plan context
|
|
@@ -296,7 +307,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
296
307
|
if (pentestLikeTurn && !activePlan && !informationalQuery) {
|
|
297
308
|
systemSections.push(pentestWorkflowDirective());
|
|
298
309
|
}
|
|
299
|
-
const
|
|
310
|
+
const renderedSystemPrompt = systemSections.join("\n\n");
|
|
311
|
+
// Reserve most of a constrained model's input budget for the user message,
|
|
312
|
+
// recent conversation, tool results, and provider framing. Dynamic project
|
|
313
|
+
// context or a saved plan must not silently grow the compact base prompt
|
|
314
|
+
// back above the model's TPM ceiling.
|
|
315
|
+
const maxSystemTokens = inputTokenBudget
|
|
316
|
+
? Math.min(2_000, Math.floor(inputTokenBudget * 0.4))
|
|
317
|
+
: undefined;
|
|
318
|
+
const systemTruncationNote = "\n\n[Additional system context omitted to fit the provider input budget.]";
|
|
319
|
+
const fullSystemPrompt = maxSystemTokens !== undefined &&
|
|
320
|
+
estimateTokens(renderedSystemPrompt) > maxSystemTokens
|
|
321
|
+
? renderedSystemPrompt.slice(0, Math.max(0, Math.floor(maxSystemTokens * 3.3) - systemTruncationNote.length)) + systemTruncationNote
|
|
322
|
+
: renderedSystemPrompt;
|
|
300
323
|
const userMessage = { role: "user", content: prompt };
|
|
301
324
|
if (options.images && options.images.length > 0) {
|
|
302
325
|
userMessage.images = options.images;
|
|
@@ -317,6 +340,20 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
317
340
|
}
|
|
318
341
|
return message;
|
|
319
342
|
};
|
|
343
|
+
// Every provider must receive a syntactically valid assistant turn between
|
|
344
|
+
// the original user prompt and a recovery nudge. In particular, Gemini
|
|
345
|
+
// serializes an empty assistant message as an empty `model` text part,
|
|
346
|
+
// which can cause every retry to return empty as well. Keep hidden thinking
|
|
347
|
+
// out of history, but record a compact non-empty sentinel when there was no
|
|
348
|
+
// visible output.
|
|
349
|
+
const pushAssistantHistory = (content) => {
|
|
350
|
+
messages.push({
|
|
351
|
+
role: "assistant",
|
|
352
|
+
content: content.trim()
|
|
353
|
+
? content
|
|
354
|
+
: "[No visible assistant response was produced.]",
|
|
355
|
+
});
|
|
356
|
+
};
|
|
320
357
|
// Track recent tool calls to detect models stuck in a loop calling the
|
|
321
358
|
// same tool with the same arguments over and over (e.g. pentest.recon
|
|
322
359
|
// called 3× on the same target without summarizing).
|
|
@@ -324,6 +361,10 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
324
361
|
// Track consecutive thinking-only responses so we can nudge the model
|
|
325
362
|
// to actually act instead of silently returning an empty answer.
|
|
326
363
|
let emptyVisibleRetries = 0;
|
|
364
|
+
// A model that spent an entire completion in hidden reasoning gets one
|
|
365
|
+
// visible-output retry with provider thinking disabled. This is per-turn
|
|
366
|
+
// only: a subsequent successful response restores the configured setting.
|
|
367
|
+
let retryWithoutThinking = false;
|
|
327
368
|
// Track tool calls truncated by the token limit so we can ask the model
|
|
328
369
|
// to retry in smaller pieces instead of leaking broken JSON as an answer.
|
|
329
370
|
let truncatedToolRetries = 0;
|
|
@@ -689,12 +730,22 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
689
730
|
artifactPath: "",
|
|
690
731
|
};
|
|
691
732
|
jobManager.registerJob(jobId, backgroundJob, toolAc);
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
733
|
+
// Long-lived commands should use shell.start/background jobs. Reset this
|
|
734
|
+
// watchdog whenever a blocking tool emits output so only a genuinely
|
|
735
|
+
// stalled operation is cancelled.
|
|
736
|
+
const TOOL_STALL_ABORT_MS = 60_000; // 1 minute
|
|
737
|
+
let stallTimer;
|
|
738
|
+
const resetStallTimer = () => {
|
|
739
|
+
if (stallTimer)
|
|
740
|
+
clearTimeout(stallTimer);
|
|
741
|
+
stallTimer = setTimeout(() => {
|
|
742
|
+
if (!toolAc.signal.aborted) {
|
|
743
|
+
writeNotice("warn", `${call.name} has been running for >60s — cancelling stalled tool`, chalk.yellow(` ⏳ ${call.name} stalled for >60s — cancelling\n`));
|
|
744
|
+
toolAc.abort();
|
|
745
|
+
}
|
|
746
|
+
}, TOOL_STALL_ABORT_MS);
|
|
747
|
+
};
|
|
748
|
+
resetStallTimer();
|
|
698
749
|
try {
|
|
699
750
|
result = await runToolCall(call, {
|
|
700
751
|
signal: toolAc.signal,
|
|
@@ -702,6 +753,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
702
753
|
onOutput: (chunk) => {
|
|
703
754
|
if (toolAc.signal.aborted)
|
|
704
755
|
return;
|
|
756
|
+
resetStallTimer();
|
|
705
757
|
printLive(chunk);
|
|
706
758
|
},
|
|
707
759
|
confirmed: true,
|
|
@@ -728,7 +780,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
728
780
|
result = { ok: false, output: `Tool error: ${errMsg}`, exitCode: 1 };
|
|
729
781
|
}
|
|
730
782
|
finally {
|
|
731
|
-
|
|
783
|
+
if (stallTimer)
|
|
784
|
+
clearTimeout(stallTimer);
|
|
732
785
|
parentSignal.removeEventListener("abort", onParentAbort);
|
|
733
786
|
}
|
|
734
787
|
const output = result.output.trim();
|
|
@@ -1000,8 +1053,15 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1000
1053
|
completion = await streamWithProvider({
|
|
1001
1054
|
provider,
|
|
1002
1055
|
model,
|
|
1056
|
+
// The selected model is the first choice, but an agent must not
|
|
1057
|
+
// end a turn with no answer if it only emits reasoning. Honor
|
|
1058
|
+
// the user's providerFallback setting for that recovery path.
|
|
1059
|
+
allowModelFallback: true,
|
|
1003
1060
|
messages,
|
|
1004
|
-
temperature
|
|
1061
|
+
// MiniMax M3 degenerates at the generic agent temperature. The
|
|
1062
|
+
// HTTP layer also applies its `top_p` override for both the
|
|
1063
|
+
// NVIDIA long ID and Kimchi's short `minimax-m3` ID.
|
|
1064
|
+
temperature: /minimax-m3/i.test(model) ? 1.0 : 0.2,
|
|
1005
1065
|
// Reasoning models can spend a lot on hidden thinking; give
|
|
1006
1066
|
// them headroom so the visible answer / tool call isn't
|
|
1007
1067
|
// truncated to silence. The non-thinking budget must be large
|
|
@@ -1015,7 +1075,9 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1015
1075
|
// substantial source file; providers with a lower limit clamp it.
|
|
1016
1076
|
maxTokens: 32_768,
|
|
1017
1077
|
signal: options.signal,
|
|
1018
|
-
thinking:
|
|
1078
|
+
thinking: retryWithoutThinking
|
|
1079
|
+
? { ...config.thinking, enabled: false }
|
|
1080
|
+
: config.thinking,
|
|
1019
1081
|
}, (token) => {
|
|
1020
1082
|
deltaParser?.push(token);
|
|
1021
1083
|
generatedTokens += 1;
|
|
@@ -1148,16 +1210,17 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1148
1210
|
else {
|
|
1149
1211
|
writeNotice("warn", "model returned an empty response — nudging it to answer", chalk.yellow(" ⚠ model returned an empty response — nudging it to answer\n"));
|
|
1150
1212
|
}
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
});
|
|
1213
|
+
if (assistantText.hasThinking)
|
|
1214
|
+
retryWithoutThinking = true;
|
|
1215
|
+
pushAssistantHistory(stripThinking(collapseRepeatedText(completion.text)).visible);
|
|
1155
1216
|
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
1156
|
-
const buildNudge =
|
|
1157
|
-
? "No visible output.
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1217
|
+
const buildNudge = freshWebSearchRequired && !sawFreshWebSearch
|
|
1218
|
+
? "No visible output. This is current or scheduled information: emit exactly one valid ```tool block for web.search now. Do NOT answer from memory or hide the tool call in <think> tags."
|
|
1219
|
+
: buildLikeTurn && !activePlan
|
|
1220
|
+
? "No visible output. Emit a ```tool block to call plan.create now. " +
|
|
1221
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response."
|
|
1222
|
+
: "No visible output. Emit a ```tool block or give your final answer. " +
|
|
1223
|
+
"Do NOT hide tool calls in <think> tags — put them in the visible response.";
|
|
1161
1224
|
messages.push(recoveryUserMessage(buildNudge));
|
|
1162
1225
|
continue;
|
|
1163
1226
|
}
|
|
@@ -1169,6 +1232,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1169
1232
|
else {
|
|
1170
1233
|
// Reset the counter on any successful visible output or recovered call.
|
|
1171
1234
|
emptyVisibleRetries = 0;
|
|
1235
|
+
retryWithoutThinking = false;
|
|
1172
1236
|
}
|
|
1173
1237
|
// `call` was already extracted above (from visible text or thinking content).
|
|
1174
1238
|
// Recovery: the model meant to call a tool but emitted a bare JSON object
|
|
@@ -1206,10 +1270,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1206
1270
|
bareToolJsonRetries += 1;
|
|
1207
1271
|
if (bareToolJsonRetries <= 3) {
|
|
1208
1272
|
writeNotice("warn", "tool call missing its name/fence — asking the model to re-emit a proper ```tool block", chalk.yellow(" ⚠ tool call missing its name/fence — asking the model to re-emit a proper ```tool block\n"));
|
|
1209
|
-
|
|
1210
|
-
role: "assistant",
|
|
1211
|
-
content: assistantText.visible,
|
|
1212
|
-
});
|
|
1273
|
+
pushAssistantHistory(assistantText.visible);
|
|
1213
1274
|
messages.push(recoveryUserMessage(buildLikeTurn && !activePlan
|
|
1214
1275
|
? "Your previous message was a bare JSON args object with no tool name and no ```tool fence, so NOTHING ran. " +
|
|
1215
1276
|
"This is a BUILD/SCAFFOLD task with NO plan yet. " +
|
|
@@ -1231,10 +1292,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1231
1292
|
// model to retry the tool call in a clean JSON format.
|
|
1232
1293
|
if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>/i.test(assistantText.visible)) {
|
|
1233
1294
|
writeNotice("warn", "tool call was malformed or cut off — asking the model to retry in JSON form", chalk.yellow(" ⚠ tool call was malformed or cut off — asking the model to retry in JSON form\n"));
|
|
1234
|
-
|
|
1235
|
-
role: "assistant",
|
|
1236
|
-
content: assistantText.visible,
|
|
1237
|
-
});
|
|
1295
|
+
pushAssistantHistory(assistantText.visible);
|
|
1238
1296
|
messages.push(recoveryUserMessage("Your previous tool call was malformed or truncated. " +
|
|
1239
1297
|
"Reply with ONLY a fenced ```tool block containing valid JSON " +
|
|
1240
1298
|
'of the form `{"name": "<tool>", "args": { ... }}`. ' +
|
|
@@ -1262,10 +1320,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1262
1320
|
if (writeResult.ok) {
|
|
1263
1321
|
const lineCount = salvaged.content.split("\n").length;
|
|
1264
1322
|
writeNotice("info", `tool call was truncated — salvaged ${lineCount} lines and wrote to ${salvaged.path}`, chalk.cyan(` ℹ tool call was truncated — salvaged ${lineCount} lines to ${salvaged.path}\n`));
|
|
1265
|
-
|
|
1266
|
-
role: "assistant",
|
|
1267
|
-
content: stripThinking(assistantText.visible).visible,
|
|
1268
|
-
});
|
|
1323
|
+
pushAssistantHistory(stripThinking(assistantText.visible).visible);
|
|
1269
1324
|
messages.push({
|
|
1270
1325
|
role: "user",
|
|
1271
1326
|
content: `Your fs.write tool call was cut off at the token limit, but the system salvaged the partial content and wrote ${lineCount} lines to ${salvaged.path}. ` +
|
|
@@ -1285,10 +1340,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1285
1340
|
}
|
|
1286
1341
|
if (truncatedToolRetries <= 3) {
|
|
1287
1342
|
writeNotice("warn", "tool call was cut off (output too long) — asking the model to retry safely", chalk.yellow(" ⚠ tool call was cut off (output too long) — asking the model to retry safely\n"));
|
|
1288
|
-
|
|
1289
|
-
role: "assistant",
|
|
1290
|
-
content: stripThinking(assistantText.visible).visible,
|
|
1291
|
-
});
|
|
1343
|
+
pushAssistantHistory(stripThinking(assistantText.visible).visible);
|
|
1292
1344
|
messages.push({
|
|
1293
1345
|
role: "user",
|
|
1294
1346
|
content: "Your previous tool call was cut off before it finished — the JSON was incomplete, so NOTHING ran. " +
|
|
@@ -1326,10 +1378,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1326
1378
|
if (writeResult.ok) {
|
|
1327
1379
|
const lineCount = salvaged.content.split("\n").length;
|
|
1328
1380
|
writeNotice("info", `malformed tool call salvaged — wrote ${lineCount} lines to ${salvaged.path}`, chalk.cyan(` ℹ malformed tool call salvaged — wrote ${lineCount} lines to ${salvaged.path}\n`));
|
|
1329
|
-
|
|
1330
|
-
role: "assistant",
|
|
1331
|
-
content: stripThinking(assistantText.visible).visible,
|
|
1332
|
-
});
|
|
1381
|
+
pushAssistantHistory(stripThinking(assistantText.visible).visible);
|
|
1333
1382
|
messages.push({
|
|
1334
1383
|
role: "user",
|
|
1335
1384
|
content: `The system extracted and wrote ${lineCount} lines to ${salvaged.path} from your malformed tool call. ` +
|
|
@@ -1347,10 +1396,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1347
1396
|
malformedFenceRetries += 1;
|
|
1348
1397
|
if (malformedFenceRetries <= 3) {
|
|
1349
1398
|
writeNotice("warn", "tool block present but its JSON didn't parse — asking the model to re-emit valid JSON", chalk.yellow(" ⚠ tool block present but its JSON didn't parse — asking the model to re-emit valid JSON\n"));
|
|
1350
|
-
|
|
1351
|
-
role: "assistant",
|
|
1352
|
-
content: stripThinking(assistantText.visible).visible,
|
|
1353
|
-
});
|
|
1399
|
+
pushAssistantHistory(stripThinking(assistantText.visible).visible);
|
|
1354
1400
|
messages.push({
|
|
1355
1401
|
role: "user",
|
|
1356
1402
|
content: "Your previous message contained a ```tool block, but its JSON was INVALID, so NOTHING ran. " +
|
|
@@ -1441,10 +1487,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1441
1487
|
}
|
|
1442
1488
|
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"));
|
|
1443
1489
|
}
|
|
1444
|
-
|
|
1445
|
-
role: "assistant",
|
|
1446
|
-
content: assistantText.visible,
|
|
1447
|
-
});
|
|
1490
|
+
pushAssistantHistory(assistantText.visible);
|
|
1448
1491
|
messages.push(recoveryUserMessage(nudge));
|
|
1449
1492
|
continue;
|
|
1450
1493
|
}
|
|
@@ -1453,10 +1496,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1453
1496
|
!freshnessRetryUsed) {
|
|
1454
1497
|
freshnessRetryUsed = true;
|
|
1455
1498
|
writeNotice("info", "current-info question detected — searching the web before answering", chalk.dim(" ℹ current-info question detected — searching the web before answering\n"));
|
|
1456
|
-
|
|
1457
|
-
role: "assistant",
|
|
1458
|
-
content: assistantText.visible,
|
|
1459
|
-
});
|
|
1499
|
+
pushAssistantHistory(assistantText.visible);
|
|
1460
1500
|
messages.push({
|
|
1461
1501
|
role: "user",
|
|
1462
1502
|
content: freshnessGuardMessage() +
|
|
@@ -1477,10 +1517,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1477
1517
|
runtimePlan.tasks.every((task) => task.state === "done" || task.state === "skipped"));
|
|
1478
1518
|
if (tasksFinished) {
|
|
1479
1519
|
runtimeVerificationRetries += 1;
|
|
1480
|
-
|
|
1481
|
-
role: "assistant",
|
|
1482
|
-
content: assistantText.visible,
|
|
1483
|
-
});
|
|
1520
|
+
pushAssistantHistory(assistantText.visible);
|
|
1484
1521
|
const missing = [
|
|
1485
1522
|
!sawServerStart ? "shell.start" : "",
|
|
1486
1523
|
!sawServerTail ? "shell.tail" : "",
|
|
@@ -1508,10 +1545,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1508
1545
|
prematureCompletionRetries += 1;
|
|
1509
1546
|
const next = unfinished[0];
|
|
1510
1547
|
writeNotice("warn", `${unfinished.length} plan task(s) still unfinished — not accepting a "done" claim; resuming execution`, chalk.yellow(` ⚠ ${unfinished.length} plan task(s) still unfinished — not accepting a "done" claim; resuming execution\n`));
|
|
1511
|
-
|
|
1512
|
-
role: "assistant",
|
|
1513
|
-
content: assistantText.visible,
|
|
1514
|
-
});
|
|
1548
|
+
pushAssistantHistory(assistantText.visible);
|
|
1515
1549
|
let instruction = `Resume now with the NEXT task ${next.id} ("${next.title}"): `;
|
|
1516
1550
|
if (next.state === "pending") {
|
|
1517
1551
|
instruction += `call task.update {taskId:"${next.id}", state:"in_progress"}, then do the real work with a tool call (fs.writeMany / shell.exec / shell.start), VERIFY it, and mark it done. `;
|
|
@@ -1583,10 +1617,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1583
1617
|
allCalls
|
|
1584
1618
|
.map((c) => `\`\`\`tool\n${JSON.stringify(c)}\n\`\`\``)
|
|
1585
1619
|
.join("\n\n");
|
|
1586
|
-
|
|
1587
|
-
role: "assistant",
|
|
1588
|
-
content: standardizedContent,
|
|
1589
|
-
});
|
|
1620
|
+
pushAssistantHistory(standardizedContent);
|
|
1590
1621
|
if (allCalls.length > 1) {
|
|
1591
1622
|
writeNotice("info", `${allCalls.length} tool calls in this message — running scoped (independent read-only lookups in parallel, everything else in order)`, chalk.dim(` ℹ ${allCalls.length} tool calls — read-only lookups in parallel, the rest in order\n`));
|
|
1592
1623
|
}
|