@pentoshi/clai 2.0.42 → 2.0.43
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.d.ts +5 -5
- package/dist/agent/context-manager.js +34 -10
- package/dist/agent/context-manager.js.map +1 -1
- package/dist/agent/runner.js +117 -21
- package/dist/agent/runner.js.map +1 -1
- package/dist/agent/tool-call-parser.d.ts +16 -0
- package/dist/agent/tool-call-parser.js +100 -4
- package/dist/agent/tool-call-parser.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/prompts/index.d.ts +2 -2
- package/dist/prompts/index.js +12 -5
- package/dist/prompts/index.js.map +1 -1
- package/dist/repl.js +19 -2
- package/dist/repl.js.map +1 -1
- package/dist/tools/registry.js +2 -2
- package/dist/tools/registry.js.map +1 -1
- package/dist/tui/App.js +7 -2
- package/dist/tui/App.js.map +1 -1
- package/dist/tui/render-lines.js +20 -5
- package/dist/tui/render-lines.js.map +1 -1
- package/dist/ui/intro-card.js +47 -8
- package/dist/ui/intro-card.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import type { ChatMessage } from "../types.js";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* over-
|
|
7
|
-
* to a provider context-window error.
|
|
3
|
+
* Per-char token estimator. Real tokenization varies by provider, but for
|
|
4
|
+
* budgeting a chars/3.3 heuristic is close enough for mixed text/code/JSON
|
|
5
|
+
* (which tokenizes less efficiently than pure English prose). We
|
|
6
|
+
* deliberately over-estimate — better to compact one turn too early than to
|
|
7
|
+
* lose state to a provider context-window error.
|
|
8
8
|
*/
|
|
9
9
|
export declare function estimateTokens(text: string): number;
|
|
10
10
|
export declare function estimateMessagesTokens(messages: ChatMessage[]): number;
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
import { redactSecrets } from "../llm/provider.js";
|
|
2
|
+
import { stripThinking } from "../ui/thinking.js";
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* over-
|
|
7
|
-
* to a provider context-window error.
|
|
4
|
+
* Per-char token estimator. Real tokenization varies by provider, but for
|
|
5
|
+
* budgeting a chars/3.3 heuristic is close enough for mixed text/code/JSON
|
|
6
|
+
* (which tokenizes less efficiently than pure English prose). We
|
|
7
|
+
* deliberately over-estimate — better to compact one turn too early than to
|
|
8
|
+
* lose state to a provider context-window error.
|
|
8
9
|
*/
|
|
9
10
|
export function estimateTokens(text) {
|
|
10
|
-
return Math.ceil(text.length /
|
|
11
|
+
return Math.ceil(text.length / 3.3);
|
|
11
12
|
}
|
|
12
13
|
export function estimateMessagesTokens(messages) {
|
|
13
14
|
let sum = 0;
|
|
14
15
|
for (const message of messages) {
|
|
15
16
|
sum += estimateTokens(message.content) + 4; // role overhead
|
|
17
|
+
// Images contribute tokens too — a typical image is ~1k tokens.
|
|
18
|
+
if (message.images) {
|
|
19
|
+
sum += message.images.length * 1000;
|
|
20
|
+
}
|
|
16
21
|
}
|
|
17
22
|
return sum;
|
|
18
23
|
}
|
|
19
24
|
const DEFAULT_BUDGET_TOKENS = 32_000;
|
|
20
|
-
const DEFAULT_KEEP_RECENT =
|
|
25
|
+
const DEFAULT_KEEP_RECENT = 6;
|
|
21
26
|
/**
|
|
22
27
|
* Content prefixes that mark a `role:"system"` message as compacted session
|
|
23
28
|
* memory (vs. the main system prompt or transient injected guidance). Exported
|
|
@@ -116,7 +121,15 @@ export async function compactMessagesWithSummary(messages, summarize, options =
|
|
|
116
121
|
};
|
|
117
122
|
}
|
|
118
123
|
const messageTranscript = older
|
|
119
|
-
.map((message) =>
|
|
124
|
+
.map((message) => {
|
|
125
|
+
let content = redactSecrets(message.content);
|
|
126
|
+
// Strip <think> tags from assistant messages so thinking content
|
|
127
|
+
// never leaks into the compaction summary or model context.
|
|
128
|
+
if (message.role === "assistant") {
|
|
129
|
+
content = stripThinking(content).visible;
|
|
130
|
+
}
|
|
131
|
+
return `${message.role.toUpperCase()}: ${content}`;
|
|
132
|
+
})
|
|
120
133
|
.join("\n\n");
|
|
121
134
|
const transcript = sessionTranscript?.trim()
|
|
122
135
|
? redactSecrets(sessionTranscript.trim())
|
|
@@ -131,14 +144,25 @@ export async function compactMessagesWithSummary(messages, summarize, options =
|
|
|
131
144
|
].join("\n");
|
|
132
145
|
// The summary is the only path. Any failure propagates to the caller —
|
|
133
146
|
// there is deliberately NO deterministic fallback.
|
|
134
|
-
|
|
147
|
+
// Strip <think> tags from the summary — the summarizer model may itself
|
|
148
|
+
// produce reasoning tags that would leak into the compacted context.
|
|
149
|
+
const rawSummary = redactSecrets((await summarize(prompt)).trim());
|
|
150
|
+
const summary = stripThinking(rawSummary).visible.trim();
|
|
135
151
|
if (!summary)
|
|
136
152
|
throw new Error("compaction failed: model returned an empty summary");
|
|
137
153
|
const head = start === 1 ? [messages[0]] : [];
|
|
154
|
+
// Strip <think> tags from tail messages so thinking content never
|
|
155
|
+
// survives compaction into the model's context.
|
|
156
|
+
const tail = messages.slice(tailStart).map((msg) => {
|
|
157
|
+
if (msg.role === "assistant" && /<think/i.test(msg.content)) {
|
|
158
|
+
return { ...msg, content: stripThinking(msg.content).visible };
|
|
159
|
+
}
|
|
160
|
+
return msg;
|
|
161
|
+
});
|
|
138
162
|
const compacted = [
|
|
139
163
|
...head,
|
|
140
164
|
{ role: "system", content: `${COMPACTION_MEMORY_PREFIX}\n\n${summary}` },
|
|
141
|
-
...
|
|
165
|
+
...tail,
|
|
142
166
|
];
|
|
143
167
|
const afterTokens = estimateMessagesTokens(compacted);
|
|
144
168
|
return {
|
|
@@ -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;
|
|
1
|
+
{"version":3,"file":"context-manager.js","sourceRoot":"","sources":["../../src/agent/context-manager.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/runner.js
CHANGED
|
@@ -15,15 +15,15 @@ import { auditLog } from "../store/logs.js";
|
|
|
15
15
|
import { loadProjectContext } from "../store/project.js";
|
|
16
16
|
import { loadScope, isScopeActive, targetInScope } from "../store/scope.js";
|
|
17
17
|
import { ensureProviderConfigured } from "../commands/providers.js";
|
|
18
|
-
import { createThinkingStreamParser, rememberThinkingFromText, renderThinkingSummary, } from "../ui/thinking.js";
|
|
18
|
+
import { createThinkingStreamParser, rememberThinkingFromText, renderThinkingSummary, stripThinking, } from "../ui/thinking.js";
|
|
19
19
|
import { renderMarkdown, indentAndWrapText } from "../ui/markdown.js";
|
|
20
20
|
import { startThinkingSpinner } from "../ui/spinner.js";
|
|
21
21
|
import { safeCwd } from "../os/cwd.js";
|
|
22
22
|
import { analyzeTask } from "./task-analyzer.js";
|
|
23
23
|
import { LoopGuard } from "./loop-guard.js";
|
|
24
24
|
import { loadPlan } from "../store/plan.js";
|
|
25
|
-
import { pathInsideSandbox } from "../tools/fs.js";
|
|
26
|
-
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeActionNarration, looksLikePlanNarration, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, pentestWorkflowDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
25
|
+
import { pathInsideSandbox, fsWrite } from "../tools/fs.js";
|
|
26
|
+
import { stripSentinelTokens, parseToolCall, recognizeBareToolJson, looksLikeTruncatedToolCall, salvageTruncatedWrite, countToolFences, parseAllToolCalls, groupToolCallsForExecution, buildTurnHistory, collapseRepeatedText, textBeforeToolCall, formatToolArgs, looksLikePentestTask, looksLikeBuildTask, looksLikeInformationalQuery, looksLikeActionNarration, looksLikePlanNarration, requiresFreshWebSearch, freshnessGuardMessage, buildWorkflowDirective, pentestWorkflowDirective, shouldDimToolChatter, looksLikePromptLeak, } from "./tool-call-parser.js";
|
|
27
27
|
import { createSessionPolicy, isPreApprovalAllowedTool, isPlanApprovedByStatus, planHasOpenWork, isAbortError, shouldEnableImageOcr, } from "./session-policy.js";
|
|
28
28
|
import { saveToolOutput, summarizeOutput, formatToolContext, } from "./tool-output-formatting.js";
|
|
29
29
|
import { planContextMessage, handlePlanTool, } from "./plan-tool.js";
|
|
@@ -810,8 +810,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
810
810
|
// what is done, and what remains. The estimate is chars/4; the budget is
|
|
811
811
|
// deliberately conservative so we compact a little early rather than hit a
|
|
812
812
|
// provider context-window error mid-task.
|
|
813
|
-
const AUTO_COMPACT_TOKEN_BUDGET =
|
|
814
|
-
const AUTO_COMPACT_KEEP_RECENT =
|
|
813
|
+
const AUTO_COMPACT_TOKEN_BUDGET = 200_000;
|
|
814
|
+
const AUTO_COMPACT_KEEP_RECENT = 6;
|
|
815
815
|
let lastCompactionMsgCount = 0;
|
|
816
816
|
const summarizeForCompaction = async (summaryPrompt) => {
|
|
817
817
|
const response = await completeWithProvider({
|
|
@@ -856,10 +856,13 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
856
856
|
});
|
|
857
857
|
}
|
|
858
858
|
lastCompactionMsgCount = messages.length;
|
|
859
|
-
|
|
859
|
+
// Report the compacted token count BEFORE plan re-injection so the
|
|
860
|
+
// compaction stats accurately show the reduction. Then report the
|
|
861
|
+
// final count (which is what the model actually receives) separately.
|
|
862
|
+
const compactedTokens = estimateMessagesTokens(messages);
|
|
860
863
|
await auditLog("agent.compact", {
|
|
861
864
|
newLength: messages.length,
|
|
862
|
-
estimatedTokens:
|
|
865
|
+
estimatedTokens: compactedTokens,
|
|
863
866
|
reason,
|
|
864
867
|
});
|
|
865
868
|
// Extract the inserted compaction memory so we can surface the
|
|
@@ -871,8 +874,12 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
871
874
|
const summaryText = insertedSummary.startsWith(`${COMPACTION_MEMORY_PREFIX}\n\n`)
|
|
872
875
|
? insertedText(insertedSummary, `${COMPACTION_MEMORY_PREFIX}\n\n`)
|
|
873
876
|
: insertedText(insertedSummary, COMPACTION_MEMORY_PREFIX);
|
|
877
|
+
const afterTokens = estimateMessagesTokens(messages);
|
|
874
878
|
writeCompacted(summaryText, beforeTokens, afterTokens);
|
|
875
|
-
|
|
879
|
+
const planNote = afterTokens > compactedTokens
|
|
880
|
+
? ` (compacted to ~${compactedTokens.toLocaleString()}, +plan → ~${afterTokens.toLocaleString()})`
|
|
881
|
+
: "";
|
|
882
|
+
writeNotice("info", `context auto-compacted to fit the window (~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens${planNote})`, chalk.dim(` ℹ context auto-compacted (~${beforeTokens.toLocaleString()} → ~${afterTokens.toLocaleString()} tokens${planNote})\n`));
|
|
876
883
|
}
|
|
877
884
|
catch (error) {
|
|
878
885
|
if (error instanceof Error &&
|
|
@@ -975,6 +982,10 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
975
982
|
let accumulatedText = "";
|
|
976
983
|
const callIds = [];
|
|
977
984
|
let streamedCallsCount = 0;
|
|
985
|
+
// Deferred tool-call events: collect tool calls parsed from the stream
|
|
986
|
+
// and emit them AFTER thinking + assistant text, so the display order
|
|
987
|
+
// is correct: thinking → model text → tool-call cards.
|
|
988
|
+
const deferredToolCalls = [];
|
|
978
989
|
const deltaParser = writesDirectly
|
|
979
990
|
? undefined
|
|
980
991
|
: createThinkingStreamParser((text) => emit({ type: "assistant-delta", text }), (text) => {
|
|
@@ -1020,7 +1031,17 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1020
1031
|
callIds.push(eventId);
|
|
1021
1032
|
alreadyPrintedIds.add(eventId);
|
|
1022
1033
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) + chalk.gray(` ${formatToolArgs(call)}`);
|
|
1023
|
-
writeToolCall
|
|
1034
|
+
// Defer the writeToolCall emission — collect it so we can
|
|
1035
|
+
// emit after thinking + assistant text for correct order.
|
|
1036
|
+
deferredToolCalls.push({
|
|
1037
|
+
eventId,
|
|
1038
|
+
call,
|
|
1039
|
+
rendered: styleToolChatter(call, toolCallLine) + "\n",
|
|
1040
|
+
});
|
|
1041
|
+
// Still update spinner label for user feedback during streaming.
|
|
1042
|
+
if (!writesDirectly) {
|
|
1043
|
+
emit({ type: "status", text: call.name });
|
|
1044
|
+
}
|
|
1024
1045
|
streamedCallsCount += 1;
|
|
1025
1046
|
}
|
|
1026
1047
|
if (writesDirectly) {
|
|
@@ -1129,7 +1150,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1129
1150
|
}
|
|
1130
1151
|
messages.push({
|
|
1131
1152
|
role: "assistant",
|
|
1132
|
-
content: collapseRepeatedText(completion.text),
|
|
1153
|
+
content: stripThinking(collapseRepeatedText(completion.text)).visible,
|
|
1133
1154
|
});
|
|
1134
1155
|
// Keep nudges SHORT — cheap models lose the key instruction in long text.
|
|
1135
1156
|
const buildNudge = buildLikeTurn && !activePlan
|
|
@@ -1221,24 +1242,62 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1221
1242
|
continue;
|
|
1222
1243
|
}
|
|
1223
1244
|
// Detect a tool call that opened but was cut off by the token limit
|
|
1224
|
-
// (most common with
|
|
1225
|
-
//
|
|
1226
|
-
//
|
|
1245
|
+
// (most common with large fs.write/fs.writeMany for reports).
|
|
1246
|
+
// Instead of asking the model to retry (which will just truncate
|
|
1247
|
+
// again at the same limit), we SALVAGE the partial content from the
|
|
1248
|
+
// truncated JSON and write it, then tell the model to CONTINUE with
|
|
1249
|
+
// fs.append from where it was cut off.
|
|
1227
1250
|
if (looksLikeTruncatedToolCall(assistantText.visible)) {
|
|
1228
1251
|
truncatedToolRetries += 1;
|
|
1252
|
+
// Try to salvage a partial fs.write / fs.append from the truncated JSON.
|
|
1253
|
+
// The pattern is: {"name":"fs.write","args":{"path":"...","content":"...
|
|
1254
|
+
// We extract the path and whatever content was produced before truncation.
|
|
1255
|
+
const salvaged = salvageTruncatedWrite(assistantText.visible);
|
|
1256
|
+
if (salvaged && truncatedToolRetries <= 5) {
|
|
1257
|
+
// Write the salvaged partial content
|
|
1258
|
+
try {
|
|
1259
|
+
const writeResult = await fsWrite(salvaged.path, salvaged.content, {
|
|
1260
|
+
confirmed: true,
|
|
1261
|
+
});
|
|
1262
|
+
if (writeResult.ok) {
|
|
1263
|
+
const lineCount = salvaged.content.split("\n").length;
|
|
1264
|
+
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
|
+
messages.push({
|
|
1266
|
+
role: "assistant",
|
|
1267
|
+
content: stripThinking(assistantText.visible).visible,
|
|
1268
|
+
});
|
|
1269
|
+
messages.push({
|
|
1270
|
+
role: "user",
|
|
1271
|
+
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}. ` +
|
|
1272
|
+
`The file now exists with content up to: "${salvaged.lastLine}"\n\n` +
|
|
1273
|
+
`CONTINUE writing the rest of the content using fs.append:\n` +
|
|
1274
|
+
'```tool\n{"name":"fs.append","args":{"path":"' + salvaged.path + '","content":"...remaining content..."}}\n```\n' +
|
|
1275
|
+
`Write the NEXT section of content starting from where it was cut off. ` +
|
|
1276
|
+
`Keep each fs.append call to ~100 lines max so it fits in the output window. ` +
|
|
1277
|
+
`Use multiple fs.append calls if needed. Do NOT re-write content that was already saved.`,
|
|
1278
|
+
});
|
|
1279
|
+
continue;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
catch {
|
|
1283
|
+
// Salvage failed — fall through to standard retry
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1229
1286
|
if (truncatedToolRetries <= 3) {
|
|
1230
1287
|
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"));
|
|
1231
1288
|
messages.push({
|
|
1232
1289
|
role: "assistant",
|
|
1233
|
-
content: assistantText.visible,
|
|
1290
|
+
content: stripThinking(assistantText.visible).visible,
|
|
1234
1291
|
});
|
|
1235
1292
|
messages.push({
|
|
1236
1293
|
role: "user",
|
|
1237
1294
|
content: "Your previous tool call was cut off before it finished — the JSON was incomplete, so NOTHING ran. " +
|
|
1238
|
-
"
|
|
1239
|
-
"
|
|
1240
|
-
"
|
|
1241
|
-
"
|
|
1295
|
+
"Your output token limit is ~32k tokens. For LARGE files (reports, docs, long code), you MUST write in chunks:\n" +
|
|
1296
|
+
"1. Use fs.write to create the file with the FIRST ~100 lines\n" +
|
|
1297
|
+
"2. Use fs.append to add the NEXT ~100 lines\n" +
|
|
1298
|
+
"3. Repeat fs.append for each remaining section\n" +
|
|
1299
|
+
"Keep your reasoning SHORT — emit the ```tool block as early as possible to maximize content space. " +
|
|
1300
|
+
"Do NOT try to write the entire file in one call. Do NOT claim any file was written until a tool call actually succeeds.",
|
|
1242
1301
|
});
|
|
1243
1302
|
continue;
|
|
1244
1303
|
}
|
|
@@ -1256,19 +1315,51 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1256
1315
|
const hasFencedCallShape = countToolFences(assistantText.visible) > 0 &&
|
|
1257
1316
|
/```tool\s*\n[\s\S]*?"(?:name|args)"\s*:/i.test(assistantText.visible);
|
|
1258
1317
|
if (hasFencedCallShape) {
|
|
1318
|
+
// Before treating as a generic malformed fence, check if this is
|
|
1319
|
+
// actually a truncated write call that should be salvaged.
|
|
1320
|
+
const salvaged = salvageTruncatedWrite(assistantText.visible);
|
|
1321
|
+
if (salvaged) {
|
|
1322
|
+
try {
|
|
1323
|
+
const writeResult = await fsWrite(salvaged.path, salvaged.content, {
|
|
1324
|
+
confirmed: true,
|
|
1325
|
+
});
|
|
1326
|
+
if (writeResult.ok) {
|
|
1327
|
+
const lineCount = salvaged.content.split("\n").length;
|
|
1328
|
+
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
|
+
messages.push({
|
|
1330
|
+
role: "assistant",
|
|
1331
|
+
content: stripThinking(assistantText.visible).visible,
|
|
1332
|
+
});
|
|
1333
|
+
messages.push({
|
|
1334
|
+
role: "user",
|
|
1335
|
+
content: `The system extracted and wrote ${lineCount} lines to ${salvaged.path} from your malformed tool call. ` +
|
|
1336
|
+
`The file content ends at: "${salvaged.lastLine}"\n\n` +
|
|
1337
|
+
`If the file is complete, proceed with the next step. ` +
|
|
1338
|
+
`If more content is needed, use fs.append to add the remaining sections (~100 lines per call).`,
|
|
1339
|
+
});
|
|
1340
|
+
continue;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
catch {
|
|
1344
|
+
// Salvage failed — fall through to standard malformed retry
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1259
1347
|
malformedFenceRetries += 1;
|
|
1260
1348
|
if (malformedFenceRetries <= 3) {
|
|
1261
1349
|
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"));
|
|
1262
1350
|
messages.push({
|
|
1263
1351
|
role: "assistant",
|
|
1264
|
-
content: assistantText.visible,
|
|
1352
|
+
content: stripThinking(assistantText.visible).visible,
|
|
1265
1353
|
});
|
|
1266
1354
|
messages.push({
|
|
1267
1355
|
role: "user",
|
|
1268
1356
|
content: "Your previous message contained a ```tool block, but its JSON was INVALID, so NOTHING ran. " +
|
|
1269
|
-
"Common causes: an extra or missing `}` / `]`,
|
|
1357
|
+
"Common causes: unescaped newlines or quotes inside a string value, an extra or missing `}` / `]`, or content too large for the output window. " +
|
|
1270
1358
|
'Re-emit ONE valid ```tool block of the exact form {"name":"<tool>","args":{...}} with balanced braces. ' +
|
|
1271
|
-
"
|
|
1359
|
+
"IMPORTANT: For large file content (reports, docs), write in chunks:\n" +
|
|
1360
|
+
"1. fs.write with the FIRST ~100 lines only\n" +
|
|
1361
|
+
"2. fs.append for each subsequent ~100-line section\n" +
|
|
1362
|
+
"Keep reasoning SHORT to maximize output space for the tool call JSON. " +
|
|
1272
1363
|
"Do NOT claim any file was written until a tool call actually succeeds.",
|
|
1273
1364
|
});
|
|
1274
1365
|
continue;
|
|
@@ -1473,6 +1564,11 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1473
1564
|
if (beforeTool) {
|
|
1474
1565
|
writeAssistantMessage(beforeTool);
|
|
1475
1566
|
}
|
|
1567
|
+
// Now emit deferred tool-call events (collected during streaming)
|
|
1568
|
+
// so the display order is: thinking → text → tool-call cards.
|
|
1569
|
+
for (const deferred of deferredToolCalls) {
|
|
1570
|
+
writeToolCall(deferred.eventId, deferred.call, deferred.rendered);
|
|
1571
|
+
}
|
|
1476
1572
|
let allCalls = parseAllToolCalls(assistantText.visible || assistantText.thinkContent);
|
|
1477
1573
|
if (allCalls.length === 0 && call) {
|
|
1478
1574
|
allCalls = [call];
|