@pentoshi/clai 1.2.6 → 1.2.7
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/classic-renderer.d.ts +5 -0
- package/dist/agent/classic-renderer.js +124 -0
- package/dist/agent/classic-renderer.js.map +1 -0
- package/dist/agent/events.d.ts +62 -0
- package/dist/agent/events.js +2 -0
- package/dist/agent/events.js.map +1 -0
- package/dist/agent/runner.d.ts +8 -0
- package/dist/agent/runner.js +195 -71
- package/dist/agent/runner.js.map +1 -1
- package/dist/commands/update.js +1 -1
- package/dist/index.js +32 -0
- package/dist/index.js.map +1 -1
- package/dist/llm/capabilities.js +1 -0
- package/dist/llm/capabilities.js.map +1 -1
- package/dist/llm/http.js +19 -11
- package/dist/llm/http.js.map +1 -1
- package/dist/llm/openai.js +10 -1
- package/dist/llm/openai.js.map +1 -1
- package/dist/modes/agent.d.ts +4 -1
- package/dist/modes/agent.js.map +1 -1
- package/dist/repl.js +57 -15
- package/dist/repl.js.map +1 -1
- package/dist/tui/App.d.ts +8 -0
- package/dist/tui/App.js +273 -0
- package/dist/tui/App.js.map +1 -0
- package/dist/tui/can-use-tui.d.ts +20 -0
- package/dist/tui/can-use-tui.js +28 -0
- package/dist/tui/can-use-tui.js.map +1 -0
- package/dist/tui/components/Composer.d.ts +6 -0
- package/dist/tui/components/Composer.js +122 -0
- package/dist/tui/components/Composer.js.map +1 -0
- package/dist/tui/components/ConfirmModal.d.ts +6 -0
- package/dist/tui/components/ConfirmModal.js +16 -0
- package/dist/tui/components/ConfirmModal.js.map +1 -0
- package/dist/tui/components/Header.d.ts +7 -0
- package/dist/tui/components/Header.js +6 -0
- package/dist/tui/components/Header.js.map +1 -0
- package/dist/tui/components/JobsPanel.d.ts +11 -0
- package/dist/tui/components/JobsPanel.js +55 -0
- package/dist/tui/components/JobsPanel.js.map +1 -0
- package/dist/tui/components/Pager.d.ts +13 -0
- package/dist/tui/components/Pager.js +46 -0
- package/dist/tui/components/Pager.js.map +1 -0
- package/dist/tui/components/StatusLine.d.ts +7 -0
- package/dist/tui/components/StatusLine.js +25 -0
- package/dist/tui/components/StatusLine.js.map +1 -0
- package/dist/tui/components/items.d.ts +5 -0
- package/dist/tui/components/items.js +57 -0
- package/dist/tui/components/items.js.map +1 -0
- package/dist/tui/confirm.d.ts +16 -0
- package/dist/tui/confirm.js +41 -0
- package/dist/tui/confirm.js.map +1 -0
- package/dist/tui/hooks/useAgentRunner.d.ts +36 -0
- package/dist/tui/hooks/useAgentRunner.js +90 -0
- package/dist/tui/hooks/useAgentRunner.js.map +1 -0
- package/dist/tui/hooks/useJobs.d.ts +7 -0
- package/dist/tui/hooks/useJobs.js +39 -0
- package/dist/tui/hooks/useJobs.js.map +1 -0
- package/dist/tui/hooks/useSpinner.d.ts +5 -0
- package/dist/tui/hooks/useSpinner.js +19 -0
- package/dist/tui/hooks/useSpinner.js.map +1 -0
- package/dist/tui/index.d.ts +8 -0
- package/dist/tui/index.js +22 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/state.d.ts +101 -0
- package/dist/tui/state.js +323 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/types.d.ts +1 -1
- package/package.json +5 -2
package/dist/agent/runner.js
CHANGED
|
@@ -16,7 +16,7 @@ import { auditLog } from "../store/logs.js";
|
|
|
16
16
|
import { loadProjectContext } from "../store/project.js";
|
|
17
17
|
import { loadScope, isScopeActive, targetInScope } from "../store/scope.js";
|
|
18
18
|
import { ensureProviderConfigured } from "../commands/providers.js";
|
|
19
|
-
import { rememberThinkingFromText, renderThinkingSummary, } from "../ui/thinking.js";
|
|
19
|
+
import { createThinkingStreamParser, rememberThinkingFromText, renderThinkingSummary, } from "../ui/thinking.js";
|
|
20
20
|
import { renderMarkdown, indentAndWrapText } from "../ui/markdown.js";
|
|
21
21
|
import { startThinkingSpinner } from "../ui/spinner.js";
|
|
22
22
|
import { safeCwd } from "../os/cwd.js";
|
|
@@ -740,7 +740,7 @@ const PRE_APPROVAL_ALLOWED_TOOLS = new Set([
|
|
|
740
740
|
export function isPreApprovalAllowedTool(name) {
|
|
741
741
|
return PRE_APPROVAL_ALLOWED_TOOLS.has(name);
|
|
742
742
|
}
|
|
743
|
-
function styleToolChatter(call, text) {
|
|
743
|
+
export function styleToolChatter(call, text) {
|
|
744
744
|
return shouldDimToolChatter(call) ? chalk.dim(text) : text;
|
|
745
745
|
}
|
|
746
746
|
function isAbortError(error, signal) {
|
|
@@ -819,7 +819,21 @@ function formatToolContext(call, result) {
|
|
|
819
819
|
: "";
|
|
820
820
|
return `${summary.text}${saved}`.trim();
|
|
821
821
|
}
|
|
822
|
-
|
|
822
|
+
const inquirerConfirmPort = {
|
|
823
|
+
async confirmTool(call) {
|
|
824
|
+
return confirm({
|
|
825
|
+
message: chalk.yellow(` run ${call.name}: ${formatToolArgs(call)}?`),
|
|
826
|
+
default: true,
|
|
827
|
+
});
|
|
828
|
+
},
|
|
829
|
+
async confirmPentest() {
|
|
830
|
+
return confirm({
|
|
831
|
+
message: chalk.red("clai only assists with security testing on systems you own or have written permission to test. Confirm for this session?"),
|
|
832
|
+
default: false,
|
|
833
|
+
});
|
|
834
|
+
},
|
|
835
|
+
};
|
|
836
|
+
async function ensurePentestAuthorization(call, autoConfirm, session, confirmPort) {
|
|
823
837
|
if (!isPentestToolCall(call))
|
|
824
838
|
return true;
|
|
825
839
|
// Persistent auth (via `clai authorize-pentest AGREE`) wins.
|
|
@@ -834,16 +848,13 @@ async function ensurePentestAuthorization(call, autoConfirm, session) {
|
|
|
834
848
|
session.pentestAuthorized.value = true;
|
|
835
849
|
return true;
|
|
836
850
|
}
|
|
837
|
-
const ok = await
|
|
838
|
-
message: chalk.red("clai only assists with security testing on systems you own or have written permission to test. Confirm for this session?"),
|
|
839
|
-
default: false,
|
|
840
|
-
});
|
|
851
|
+
const ok = await confirmPort.confirmPentest();
|
|
841
852
|
if (!ok)
|
|
842
853
|
return false;
|
|
843
854
|
session.pentestAuthorized.value = true;
|
|
844
855
|
return true;
|
|
845
856
|
}
|
|
846
|
-
async function confirmToolExecution(call, autoConfirm, session) {
|
|
857
|
+
async function confirmToolExecution(call, autoConfirm, session, confirmPort) {
|
|
847
858
|
const config = getConfig();
|
|
848
859
|
if (autoConfirm)
|
|
849
860
|
return true;
|
|
@@ -854,10 +865,7 @@ async function confirmToolExecution(call, autoConfirm, session) {
|
|
|
854
865
|
// set so authorizations never leak across processes.
|
|
855
866
|
if (config.allowAlwaysTools.includes(call.name))
|
|
856
867
|
return true;
|
|
857
|
-
return
|
|
858
|
-
message: chalk.yellow(` run ${call.name}: ${formatToolArgs(call)}?`),
|
|
859
|
-
default: true,
|
|
860
|
-
});
|
|
868
|
+
return confirmPort.confirmTool(call);
|
|
861
869
|
}
|
|
862
870
|
/** Build the system-context block describing the session's active plan. */
|
|
863
871
|
function planContextMessage(plan, approved) {
|
|
@@ -950,6 +958,7 @@ async function handlePlanTool(call, session, ctx) {
|
|
|
950
958
|
return {
|
|
951
959
|
handled: true,
|
|
952
960
|
ok: true,
|
|
961
|
+
plan,
|
|
953
962
|
display,
|
|
954
963
|
modelNote: `Plan saved with ${plan.tasks.length} task(s). STOP here and wait — produce NO other tool calls now. ` +
|
|
955
964
|
"Do NOT start executing tasks until the user approves with /implement. " +
|
|
@@ -1008,6 +1017,7 @@ async function handlePlanTool(call, session, ctx) {
|
|
|
1008
1017
|
return {
|
|
1009
1018
|
handled: true,
|
|
1010
1019
|
ok: true,
|
|
1020
|
+
plan,
|
|
1011
1021
|
display: checklist + "\n",
|
|
1012
1022
|
modelNote: allDone
|
|
1013
1023
|
? "Task updated. ALL tasks are now finished. Verify the result and give your final summary."
|
|
@@ -1015,8 +1025,89 @@ async function handlePlanTool(call, session, ctx) {
|
|
|
1015
1025
|
};
|
|
1016
1026
|
}
|
|
1017
1027
|
export async function runAgentLoop(prompt, options = {}) {
|
|
1028
|
+
const writesDirectly = !options.onEvent;
|
|
1029
|
+
const emit = (event) => options.onEvent?.(event);
|
|
1030
|
+
const noopSpinner = {
|
|
1031
|
+
setLabel: () => { },
|
|
1032
|
+
bumpReasoning: () => { },
|
|
1033
|
+
pushPreview: () => { },
|
|
1034
|
+
stop: () => { },
|
|
1035
|
+
};
|
|
1036
|
+
const writeStatus = (text, rendered = chalk.dim(text)) => {
|
|
1037
|
+
emit({ type: "status", text });
|
|
1038
|
+
if (writesDirectly)
|
|
1039
|
+
process.stdout.write(rendered);
|
|
1040
|
+
};
|
|
1041
|
+
const writeNotice = (level, text, rendered) => {
|
|
1042
|
+
emit({ type: "notice", level, text });
|
|
1043
|
+
if (writesDirectly)
|
|
1044
|
+
process.stdout.write(rendered);
|
|
1045
|
+
};
|
|
1046
|
+
const writeAssistantMessage = (text) => {
|
|
1047
|
+
emit({ type: "assistant-message", text });
|
|
1048
|
+
const rendered = renderMarkdown(text);
|
|
1049
|
+
if (writesDirectly) {
|
|
1050
|
+
process.stdout.write(text.endsWith("\n") ? rendered : `${rendered}\n`);
|
|
1051
|
+
}
|
|
1052
|
+
};
|
|
1053
|
+
const writeThinkingBlock = (content) => {
|
|
1054
|
+
emit({ type: "thinking-block", content });
|
|
1055
|
+
if (writesDirectly)
|
|
1056
|
+
process.stdout.write(`${renderThinkingSummary(content)}\n`);
|
|
1057
|
+
};
|
|
1058
|
+
const writeToolOutput = (id, chunk, rendered) => {
|
|
1059
|
+
emit({ type: "tool-output", id, chunk });
|
|
1060
|
+
if (writesDirectly)
|
|
1061
|
+
process.stdout.write(rendered);
|
|
1062
|
+
};
|
|
1063
|
+
const writeToolCall = (id, call, rendered) => {
|
|
1064
|
+
emit({
|
|
1065
|
+
type: "tool-call",
|
|
1066
|
+
id,
|
|
1067
|
+
name: call.name,
|
|
1068
|
+
argsDisplay: formatToolArgs(call),
|
|
1069
|
+
});
|
|
1070
|
+
if (writesDirectly)
|
|
1071
|
+
process.stdout.write(rendered);
|
|
1072
|
+
};
|
|
1073
|
+
const writePlanUpdate = (plan, rendered) => {
|
|
1074
|
+
emit({ type: "plan-update", plan });
|
|
1075
|
+
if (writesDirectly)
|
|
1076
|
+
process.stdout.write(rendered);
|
|
1077
|
+
};
|
|
1078
|
+
const writeToolBlocked = (id, name, reason, rendered) => {
|
|
1079
|
+
emit({ type: "tool-blocked", id, name, reason });
|
|
1080
|
+
if (writesDirectly)
|
|
1081
|
+
process.stdout.write(rendered);
|
|
1082
|
+
};
|
|
1083
|
+
const writeAbort = () => {
|
|
1084
|
+
emit({ type: "turn-aborted" });
|
|
1085
|
+
if (writesDirectly)
|
|
1086
|
+
process.stdout.write(chalk.yellow(" ⏹ Aborted.\n"));
|
|
1087
|
+
};
|
|
1088
|
+
const emitToolResult = (id, result, summary, artifactPath) => {
|
|
1089
|
+
const event = {
|
|
1090
|
+
type: "tool-result",
|
|
1091
|
+
id,
|
|
1092
|
+
ok: result.ok,
|
|
1093
|
+
summary,
|
|
1094
|
+
};
|
|
1095
|
+
if (typeof result.exitCode === "number") {
|
|
1096
|
+
event.exitCode = result.exitCode;
|
|
1097
|
+
}
|
|
1098
|
+
if (artifactPath) {
|
|
1099
|
+
event.artifactPath = artifactPath;
|
|
1100
|
+
}
|
|
1101
|
+
emit(event);
|
|
1102
|
+
};
|
|
1103
|
+
const finishTurn = (answer, steps) => {
|
|
1104
|
+
emit({ type: "turn-end", finalAnswer: answer, steps });
|
|
1105
|
+
return answer;
|
|
1106
|
+
};
|
|
1107
|
+
emit({ type: "turn-start", prompt });
|
|
1018
1108
|
const config = getConfig();
|
|
1019
1109
|
const maxSteps = options.maxSteps ?? 70;
|
|
1110
|
+
const confirmPort = options.confirm ?? inquirerConfirmPort;
|
|
1020
1111
|
const projectContext = await loadProjectContext();
|
|
1021
1112
|
const toolNames = availableToolNames();
|
|
1022
1113
|
// Build / scaffold / continuation turns must NEVER be diverted into a
|
|
@@ -1157,6 +1248,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1157
1248
|
const maxIterations = stepBudget * 3;
|
|
1158
1249
|
let productiveSteps = 0;
|
|
1159
1250
|
let step = -1;
|
|
1251
|
+
let nextToolEventId = 0;
|
|
1160
1252
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
1161
1253
|
// `step` is the productive-step index (used for display + audit). It only
|
|
1162
1254
|
// advances when the previous iteration actually executed a tool.
|
|
@@ -1175,16 +1267,33 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1175
1267
|
// when the batch was parsed.
|
|
1176
1268
|
call = pendingCalls.shift();
|
|
1177
1269
|
assistantText = { visible: "", thinkContent: "", hasThinking: false };
|
|
1178
|
-
|
|
1270
|
+
const batchStatus = ` ↳ continuing batch (${pendingCalls.length} more queued)\n`;
|
|
1271
|
+
writeStatus(batchStatus, chalk.dim(batchStatus));
|
|
1179
1272
|
}
|
|
1180
1273
|
else {
|
|
1181
1274
|
// Buffer LLM output so tool JSON and hidden thinking are not printed raw.
|
|
1182
1275
|
// Status messages (rate-limit retries, fallback hints) still surface live.
|
|
1183
1276
|
// A spinner gives the user feedback during long thinking phases on
|
|
1184
1277
|
// models like glm-5.1 / deepseek-v4-flash that stream reasoning first.
|
|
1185
|
-
const
|
|
1278
|
+
const streamLabel = step === 0 ? "waiting for model" : `step ${step + 1}`;
|
|
1279
|
+
const spinner = writesDirectly
|
|
1280
|
+
? startThinkingSpinner(streamLabel, options.signal)
|
|
1281
|
+
: noopSpinner;
|
|
1282
|
+
if (!writesDirectly) {
|
|
1283
|
+
emit({ type: "status", text: streamLabel });
|
|
1284
|
+
}
|
|
1186
1285
|
let sawReasoning = false;
|
|
1187
1286
|
let inThinking = false;
|
|
1287
|
+
let emittedThinkingStatus = false;
|
|
1288
|
+
const deltaParser = writesDirectly
|
|
1289
|
+
? undefined
|
|
1290
|
+
: createThinkingStreamParser((text) => emit({ type: "assistant-delta", text }), (text) => {
|
|
1291
|
+
if (!emittedThinkingStatus) {
|
|
1292
|
+
emittedThinkingStatus = true;
|
|
1293
|
+
emit({ type: "status", text: "thinking" });
|
|
1294
|
+
}
|
|
1295
|
+
emit({ type: "thinking-delta", text });
|
|
1296
|
+
});
|
|
1188
1297
|
let completion;
|
|
1189
1298
|
try {
|
|
1190
1299
|
completion = await streamWithProvider({
|
|
@@ -1203,6 +1312,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1203
1312
|
signal: options.signal,
|
|
1204
1313
|
thinking: config.thinking,
|
|
1205
1314
|
}, (token) => {
|
|
1315
|
+
deltaParser?.push(token);
|
|
1206
1316
|
// Heuristic: <think>… markers and reasoning_content tokens flow
|
|
1207
1317
|
// through onToken. Surface activity in the spinner so the screen
|
|
1208
1318
|
// is never empty for minutes.
|
|
@@ -1210,6 +1320,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1210
1320
|
sawReasoning = true;
|
|
1211
1321
|
inThinking = true;
|
|
1212
1322
|
spinner.setLabel("thinking");
|
|
1323
|
+
if (!writesDirectly)
|
|
1324
|
+
emit({ type: "status", text: "thinking" });
|
|
1213
1325
|
}
|
|
1214
1326
|
if (/<\/think>/i.test(token)) {
|
|
1215
1327
|
inThinking = false;
|
|
@@ -1230,7 +1342,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1230
1342
|
}
|
|
1231
1343
|
}, (status) => {
|
|
1232
1344
|
spinner.stop();
|
|
1233
|
-
|
|
1345
|
+
writeStatus(status, chalk.dim(status));
|
|
1234
1346
|
});
|
|
1235
1347
|
}
|
|
1236
1348
|
finally {
|
|
@@ -1239,6 +1351,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1239
1351
|
}
|
|
1240
1352
|
provider = completion.provider;
|
|
1241
1353
|
model = completion.model;
|
|
1354
|
+
deltaParser?.finish();
|
|
1242
1355
|
const assistantTextResult = rememberThinkingFromText(completion.text);
|
|
1243
1356
|
assistantText = assistantTextResult;
|
|
1244
1357
|
// Try visible text first, then thinking content — some models (e.g. glm-5.1)
|
|
@@ -1253,7 +1366,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1253
1366
|
strict: getConfig().parserStrict,
|
|
1254
1367
|
});
|
|
1255
1368
|
if (call) {
|
|
1256
|
-
|
|
1369
|
+
writeNotice("info", "recovered tool call from thinking content", chalk.dim(" ℹ recovered tool call from thinking content\n"));
|
|
1257
1370
|
}
|
|
1258
1371
|
}
|
|
1259
1372
|
// ── Thinking-only recovery ────────────────────────────────────────
|
|
@@ -1264,8 +1377,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1264
1377
|
if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
|
|
1265
1378
|
emptyVisibleRetries += 1;
|
|
1266
1379
|
if (emptyVisibleRetries <= 2) {
|
|
1267
|
-
|
|
1268
|
-
|
|
1380
|
+
writeThinkingBlock(assistantText.thinkContent);
|
|
1381
|
+
writeNotice("warn", "model produced only thinking — nudging it to take action", chalk.yellow(" ⚠ model produced only thinking — nudging it to take action\n"));
|
|
1269
1382
|
messages.push({
|
|
1270
1383
|
role: "assistant",
|
|
1271
1384
|
content: collapseRepeatedText(completion.text),
|
|
@@ -1304,7 +1417,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1304
1417
|
if (bare?.call) {
|
|
1305
1418
|
call = bare.call;
|
|
1306
1419
|
recoveredFromBareJson = true;
|
|
1307
|
-
|
|
1420
|
+
writeNotice("info", "recovered an unfenced tool call from bare JSON", chalk.dim(" ℹ recovered an unfenced tool call from bare JSON\n"));
|
|
1308
1421
|
}
|
|
1309
1422
|
else if (bare?.argsOnly) {
|
|
1310
1423
|
bareArgsOnly = true;
|
|
@@ -1316,7 +1429,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1316
1429
|
if (bareThink?.call) {
|
|
1317
1430
|
call = bareThink.call;
|
|
1318
1431
|
recoveredFromBareJson = true;
|
|
1319
|
-
|
|
1432
|
+
writeNotice("info", "recovered an unfenced tool call from thinking content", chalk.dim(" ℹ recovered an unfenced tool call from thinking content\n"));
|
|
1320
1433
|
}
|
|
1321
1434
|
else if (bareThink?.argsOnly) {
|
|
1322
1435
|
bareArgsOnly = true;
|
|
@@ -1326,7 +1439,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1326
1439
|
if (bareArgsOnly) {
|
|
1327
1440
|
bareToolJsonRetries += 1;
|
|
1328
1441
|
if (bareToolJsonRetries <= 3) {
|
|
1329
|
-
|
|
1442
|
+
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"));
|
|
1330
1443
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1331
1444
|
messages.push(recoveryUserMessage(buildLikeTurn && !activePlan
|
|
1332
1445
|
? "Your previous message was a bare JSON args object with no tool name and no ```tool fence, so NOTHING ran. " +
|
|
@@ -1348,7 +1461,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1348
1461
|
// raw tokens looks like a crash to the user — instead, ask the
|
|
1349
1462
|
// model to retry the tool call in a clean JSON format.
|
|
1350
1463
|
if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>/i.test(assistantText.visible)) {
|
|
1351
|
-
|
|
1464
|
+
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"));
|
|
1352
1465
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1353
1466
|
messages.push(recoveryUserMessage("Your previous tool call was malformed or truncated. " +
|
|
1354
1467
|
"Reply with ONLY a fenced ```tool block containing valid JSON " +
|
|
@@ -1363,7 +1476,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1363
1476
|
if (looksLikeTruncatedToolCall(assistantText.visible)) {
|
|
1364
1477
|
truncatedToolRetries += 1;
|
|
1365
1478
|
if (truncatedToolRetries <= 3) {
|
|
1366
|
-
|
|
1479
|
+
writeNotice("warn", "tool call was cut off (output too long) — asking the model to retry in smaller pieces", chalk.yellow(" ⚠ tool call was cut off (output too long) — asking the model to retry in smaller pieces\n"));
|
|
1367
1480
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1368
1481
|
messages.push({
|
|
1369
1482
|
role: "user",
|
|
@@ -1390,7 +1503,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1390
1503
|
if (hasFencedCallShape) {
|
|
1391
1504
|
malformedFenceRetries += 1;
|
|
1392
1505
|
if (malformedFenceRetries <= 3) {
|
|
1393
|
-
|
|
1506
|
+
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"));
|
|
1394
1507
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1395
1508
|
messages.push({
|
|
1396
1509
|
role: "user",
|
|
@@ -1427,7 +1540,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1427
1540
|
if (activePlan && session.planApproved.value) {
|
|
1428
1541
|
nudge =
|
|
1429
1542
|
"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.";
|
|
1430
|
-
|
|
1543
|
+
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"));
|
|
1431
1544
|
}
|
|
1432
1545
|
else if (planNarrated || productiveSteps > 0) {
|
|
1433
1546
|
// It explored and/or wrote a plan as prose but never called
|
|
@@ -1437,14 +1550,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1437
1550
|
"You wrote the plan as PROSE but did NOT call plan.create, so no plan was saved and the user cannot /implement it. Emit it as a real tool call NOW — exactly one ```tool block:\n" +
|
|
1438
1551
|
'```tool\n{"name":"plan.create","args":{"goal":"<short goal>","detail":"<stack/approach and how you\'ll verify>","tasks":["task 1","task 2","task 3"],"kind":"coding"}}\n```\n' +
|
|
1439
1552
|
"Do not describe the plan again in prose — just emit the plan.create tool block.";
|
|
1440
|
-
|
|
1553
|
+
writeNotice("warn", "plan was written as text, not created — nudging it to call plan.create", chalk.yellow(" ⚠ plan was written as text, not created — nudging it to call plan.create\n"));
|
|
1441
1554
|
}
|
|
1442
1555
|
else {
|
|
1443
1556
|
nudge =
|
|
1444
1557
|
"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. For this build task, explore first like this:\n" +
|
|
1445
1558
|
'```tool\n{"name":"fs.list","args":{"path":"."}}\n```\n' +
|
|
1446
1559
|
"Then read key files, and once you understand the directory, call plan.create. Every turn MUST contain a ```tool block until the task is done.";
|
|
1447
|
-
|
|
1560
|
+
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"));
|
|
1448
1561
|
}
|
|
1449
1562
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1450
1563
|
messages.push(recoveryUserMessage(nudge));
|
|
@@ -1452,7 +1565,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1452
1565
|
}
|
|
1453
1566
|
if (freshWebSearchRequired && !sawFreshWebSearch && !freshnessRetryUsed) {
|
|
1454
1567
|
freshnessRetryUsed = true;
|
|
1455
|
-
|
|
1568
|
+
writeNotice("info", "current-info question detected — searching the web before answering", chalk.dim(" ℹ current-info question detected — searching the web before answering\n"));
|
|
1456
1569
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1457
1570
|
messages.push({
|
|
1458
1571
|
role: "user",
|
|
@@ -1473,7 +1586,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1473
1586
|
if (livePlan && unfinished && unfinished.length > 0) {
|
|
1474
1587
|
prematureCompletionRetries += 1;
|
|
1475
1588
|
const next = unfinished[0];
|
|
1476
|
-
|
|
1589
|
+
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`));
|
|
1477
1590
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1478
1591
|
messages.push({
|
|
1479
1592
|
role: "user",
|
|
@@ -1493,10 +1606,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1493
1606
|
// unchallenged — append an explicit, honest status so the user knows the
|
|
1494
1607
|
// build did not actually complete.
|
|
1495
1608
|
let completionWarning = "";
|
|
1609
|
+
let completionWarningText = "";
|
|
1496
1610
|
if (session.planApproved.value) {
|
|
1497
1611
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1498
1612
|
const unfinished = livePlan?.tasks.filter((t) => t.state === "pending" || t.state === "in_progress");
|
|
1499
1613
|
if (livePlan && unfinished && unfinished.length > 0) {
|
|
1614
|
+
completionWarningText =
|
|
1615
|
+
`${unfinished.length} of ${livePlan.tasks.length} plan task(s) are NOT actually complete. ` +
|
|
1616
|
+
"The summary above may overstate progress.";
|
|
1500
1617
|
completionWarning =
|
|
1501
1618
|
chalk.yellow(`\n ⚠ ${unfinished.length} of ${livePlan.tasks.length} plan task(s) are NOT actually complete:\n`) +
|
|
1502
1619
|
unfinished
|
|
@@ -1506,19 +1623,17 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1506
1623
|
}
|
|
1507
1624
|
}
|
|
1508
1625
|
if (cleaned) {
|
|
1509
|
-
|
|
1510
|
-
if (!cleaned.endsWith("\n"))
|
|
1511
|
-
process.stdout.write("\n");
|
|
1626
|
+
writeAssistantMessage(cleaned);
|
|
1512
1627
|
}
|
|
1513
1628
|
if (completionWarning) {
|
|
1514
|
-
|
|
1629
|
+
writeNotice("warn", completionWarningText, completionWarning);
|
|
1515
1630
|
}
|
|
1516
1631
|
if (assistantText.hasThinking) {
|
|
1517
|
-
|
|
1632
|
+
writeThinkingBlock(assistantText.thinkContent);
|
|
1518
1633
|
}
|
|
1519
1634
|
await auditLog("agent.final", { provider, model, steps: step + 1 });
|
|
1520
1635
|
lastAnswer = cleaned;
|
|
1521
|
-
return lastAnswer;
|
|
1636
|
+
return finishTurn(lastAnswer, step + 1);
|
|
1522
1637
|
}
|
|
1523
1638
|
// A valid primary tool call exists for this fresh model turn. Show any
|
|
1524
1639
|
// prose / thinking that preceded it, record the assistant message ONCE,
|
|
@@ -1528,10 +1643,10 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1528
1643
|
? ""
|
|
1529
1644
|
: textBeforeToolCall(assistantText.visible);
|
|
1530
1645
|
if (beforeTool) {
|
|
1531
|
-
|
|
1646
|
+
writeAssistantMessage(beforeTool);
|
|
1532
1647
|
}
|
|
1533
1648
|
if (assistantText.hasThinking) {
|
|
1534
|
-
|
|
1649
|
+
writeThinkingBlock(assistantText.thinkContent);
|
|
1535
1650
|
}
|
|
1536
1651
|
messages.push({
|
|
1537
1652
|
role: "assistant",
|
|
@@ -1543,7 +1658,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1543
1658
|
allCalls[0] &&
|
|
1544
1659
|
sameToolCall(allCalls[0], call)) {
|
|
1545
1660
|
pendingCalls = allCalls.slice(1);
|
|
1546
|
-
|
|
1661
|
+
writeNotice("info", `${allCalls.length} tool calls in this message — running them in order`, chalk.dim(` ℹ ${allCalls.length} tool calls in this message — running them in order\n`));
|
|
1547
1662
|
}
|
|
1548
1663
|
}
|
|
1549
1664
|
}
|
|
@@ -1556,6 +1671,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1556
1671
|
// runs and is safety-classified as the shell command it really is —
|
|
1557
1672
|
// instead of dead-ending on "Unknown tool: sed".
|
|
1558
1673
|
call = normalizeToolCall(call);
|
|
1674
|
+
const toolEventId = `tool-${++nextToolEventId}`;
|
|
1559
1675
|
// ── Duplicate-call detection ──────────────────────────────────────────
|
|
1560
1676
|
// If the model calls the exact same tool with the exact same args
|
|
1561
1677
|
// repeatedly, it's stuck in a loop. Inject a corrective message
|
|
@@ -1565,7 +1681,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1565
1681
|
const isWrite = call.name === "fs.write" ||
|
|
1566
1682
|
call.name === "fs.writeMany" ||
|
|
1567
1683
|
call.name === "fs.edit";
|
|
1568
|
-
|
|
1684
|
+
const reason = `${call.name} was already called with the same arguments — ${isWrite ? "moving on" : "forcing summary"}`;
|
|
1685
|
+
writeNotice("warn", reason, chalk.yellow(` ⚠ ${call.name} was already called with the same arguments — ${isWrite ? "moving on" : "forcing summary"}\n`));
|
|
1569
1686
|
// A repeat means this batch went off the rails — drop any queued calls
|
|
1570
1687
|
// and let the model react. The assistant message was already recorded.
|
|
1571
1688
|
pendingCalls = [];
|
|
@@ -1581,7 +1698,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1581
1698
|
continue;
|
|
1582
1699
|
}
|
|
1583
1700
|
if (loopCheck.reason) {
|
|
1584
|
-
|
|
1701
|
+
writeNotice("info", loopCheck.reason, chalk.dim(` ℹ ${loopCheck.reason}\n`));
|
|
1585
1702
|
}
|
|
1586
1703
|
// ── Plan / task tools (session-scoped, handled inline) ─────────────
|
|
1587
1704
|
// These don't go through the generic registry because they need the
|
|
@@ -1594,7 +1711,9 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1594
1711
|
if (planResult.handled) {
|
|
1595
1712
|
productiveSteps += 1;
|
|
1596
1713
|
loopGuard.recordAttempt(step, call.name, call.args, planResult.ok, 0);
|
|
1597
|
-
|
|
1714
|
+
if (planResult.plan) {
|
|
1715
|
+
writePlanUpdate(planResult.plan, planResult.display);
|
|
1716
|
+
}
|
|
1598
1717
|
// plan.create means "STOP and wait for /implement" — abandon any
|
|
1599
1718
|
// other calls the model batched alongside it.
|
|
1600
1719
|
if (call.name === "plan.create")
|
|
@@ -1625,7 +1744,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1625
1744
|
if (activePlan &&
|
|
1626
1745
|
!session.planApproved.value &&
|
|
1627
1746
|
!isPreApprovalAllowedTool(call.name)) {
|
|
1628
|
-
|
|
1747
|
+
const reason = `plan awaiting approval — ${call.name} is blocked until you /implement (or /discard)`;
|
|
1748
|
+
writeNotice("warn", reason, chalk.yellow(` ⚠ plan awaiting approval — ${call.name} is blocked until you /implement (or /discard)\n`));
|
|
1629
1749
|
pendingCalls = [];
|
|
1630
1750
|
messages.push({
|
|
1631
1751
|
role: "user",
|
|
@@ -1642,23 +1762,23 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1642
1762
|
}
|
|
1643
1763
|
// Show tool call
|
|
1644
1764
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) + chalk.gray(` ${formatToolArgs(call)}`);
|
|
1645
|
-
|
|
1765
|
+
writeToolCall(toolEventId, call, styleToolChatter(call, toolCallLine) + "\n");
|
|
1646
1766
|
const scopeTarget = scopeTargetForToolCall(call);
|
|
1647
1767
|
if (scopeTarget &&
|
|
1648
1768
|
(!isScopeActive(scope) || !targetInScope(scopeTarget, scope))) {
|
|
1649
|
-
|
|
1769
|
+
writeNotice("info", `scope optional: ${scopeHint(scopeTarget)}`, chalk.dim(` scope optional: ${scopeHint(scopeTarget)}\n`));
|
|
1650
1770
|
}
|
|
1651
1771
|
if (decision.level === "block") {
|
|
1652
|
-
|
|
1772
|
+
writeToolBlocked(toolEventId, call.name, decision.reason, chalk.red(` ✗ blocked: ${decision.reason}`) + "\n");
|
|
1653
1773
|
lastAnswer = `Blocked: ${call.name} — ${decision.reason}`;
|
|
1654
|
-
return lastAnswer;
|
|
1774
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1655
1775
|
}
|
|
1656
1776
|
// Pentest authorization — if user confirms this, skip the per-tool confirm
|
|
1657
1777
|
let pentestJustConfirmed = false;
|
|
1658
1778
|
const needsPentestAuth = isPentestToolCall(call) &&
|
|
1659
1779
|
!getConfig().pentestAuthorized &&
|
|
1660
1780
|
!session.pentestAuthorized.value;
|
|
1661
|
-
const authorized = await ensurePentestAuthorization(call, Boolean(options.autoConfirm), session);
|
|
1781
|
+
const authorized = await ensurePentestAuthorization(call, Boolean(options.autoConfirm), session, confirmPort);
|
|
1662
1782
|
// inquirer's confirm() creates its own readline interface which resets
|
|
1663
1783
|
// raw mode AND pauses stdin when it finishes. Re-assert raw mode and
|
|
1664
1784
|
// resume stdin so the outer keypress handler (ESC/Ctrl+C abort, Ctrl+O
|
|
@@ -1666,8 +1786,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1666
1786
|
restoreInteractiveStdin();
|
|
1667
1787
|
if (!authorized) {
|
|
1668
1788
|
lastAnswer = "Pentest authorization not confirmed.";
|
|
1669
|
-
|
|
1670
|
-
return lastAnswer;
|
|
1789
|
+
writeToolBlocked(toolEventId, call.name, lastAnswer, chalk.red(` ✗ ${lastAnswer}`) + "\n");
|
|
1790
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1671
1791
|
}
|
|
1672
1792
|
if (needsPentestAuth) {
|
|
1673
1793
|
pentestJustConfirmed = true;
|
|
@@ -1676,14 +1796,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1676
1796
|
// fs.delete and shell deletions NEVER auto-confirm even with -y flag.
|
|
1677
1797
|
const forceManualConfirm = call.name === "fs.delete";
|
|
1678
1798
|
if (decision.level === "confirm" && !pentestJustConfirmed) {
|
|
1679
|
-
const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session);
|
|
1799
|
+
const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session, confirmPort);
|
|
1680
1800
|
// Re-assert raw mode and resume stdin after inquirer's confirm()
|
|
1681
1801
|
// (see restoreInteractiveStdin / the comment above).
|
|
1682
1802
|
restoreInteractiveStdin();
|
|
1683
1803
|
if (!ok) {
|
|
1684
1804
|
lastAnswer = "Cancelled.";
|
|
1685
|
-
|
|
1686
|
-
return lastAnswer;
|
|
1805
|
+
writeNotice("warn", "cancelled", chalk.yellow(` ✗ cancelled`) + "\n");
|
|
1806
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1687
1807
|
}
|
|
1688
1808
|
}
|
|
1689
1809
|
// Execute tool
|
|
@@ -1697,7 +1817,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1697
1817
|
typeof call.args.command === "string" &&
|
|
1698
1818
|
looksInteractiveStdin(call.args.command);
|
|
1699
1819
|
if (interactiveCommand && process.stdin.isTTY) {
|
|
1700
|
-
|
|
1820
|
+
writeNotice("warn", "this command may prompt for a password — type it when asked", chalk.yellow(" ⚠ this command may prompt for a password — type it when asked\n"));
|
|
1701
1821
|
}
|
|
1702
1822
|
let result;
|
|
1703
1823
|
let liveBytes = 0;
|
|
@@ -1721,8 +1841,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1721
1841
|
if (liveBytes >= liveCap) {
|
|
1722
1842
|
if (!liveTruncatedNotified) {
|
|
1723
1843
|
liveTruncatedNotified = true;
|
|
1724
|
-
|
|
1725
|
-
|
|
1844
|
+
writeNotice("info", "live preview truncated, full output saved", chalk.dim("\n … live preview truncated, full output saved\n"));
|
|
1845
|
+
writeNotice("info", "tool still running — ESC or Ctrl+C to abort", chalk.dim(" (tool still running — ESC or Ctrl+C to abort)\n"));
|
|
1726
1846
|
lastProgressAt = Date.now();
|
|
1727
1847
|
}
|
|
1728
1848
|
// After truncation, show a dot every 5 seconds so the user knows
|
|
@@ -1730,7 +1850,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1730
1850
|
const now = Date.now();
|
|
1731
1851
|
if (now - lastProgressAt > 5_000) {
|
|
1732
1852
|
lastProgressAt = now;
|
|
1733
|
-
|
|
1853
|
+
writeToolOutput(toolEventId, ".", chalk.dim("."));
|
|
1734
1854
|
}
|
|
1735
1855
|
return;
|
|
1736
1856
|
}
|
|
@@ -1743,7 +1863,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1743
1863
|
// Skip the dim wrapper for interactive commands so a sudo password
|
|
1744
1864
|
// prompt is rendered at full brightness; everything else stays dim
|
|
1745
1865
|
// so tool chatter is visually distinct from the model's prose.
|
|
1746
|
-
|
|
1866
|
+
writeToolOutput(toolEventId, slice, shouldDimLive ? chalk.dim(body) : body);
|
|
1747
1867
|
};
|
|
1748
1868
|
try {
|
|
1749
1869
|
result = await runToolCall(call, {
|
|
@@ -1755,13 +1875,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1755
1875
|
},
|
|
1756
1876
|
});
|
|
1757
1877
|
// Newline separator if live output or progress dots didn't already end with one.
|
|
1758
|
-
if (liveBytes > 0 || liveTruncatedNotified)
|
|
1759
|
-
|
|
1878
|
+
if (liveBytes > 0 || liveTruncatedNotified) {
|
|
1879
|
+
writeToolOutput(toolEventId, "\n", "\n");
|
|
1880
|
+
}
|
|
1760
1881
|
}
|
|
1761
1882
|
catch (toolError) {
|
|
1762
1883
|
if (isAbortError(toolError, options.signal)) {
|
|
1763
1884
|
lastAnswer = "Aborted.";
|
|
1764
|
-
|
|
1885
|
+
writeAbort();
|
|
1765
1886
|
return lastAnswer;
|
|
1766
1887
|
}
|
|
1767
1888
|
const errMsg = toolError instanceof Error ? toolError.message : String(toolError);
|
|
@@ -1771,7 +1892,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1771
1892
|
// from the same message so the model sees the failure and decides what to
|
|
1772
1893
|
// do next instead of blindly running steps that depended on it.
|
|
1773
1894
|
if (!result.ok && pendingCalls.length > 0) {
|
|
1774
|
-
|
|
1895
|
+
const cancelledQueuedStatus = ` ↳ ${pendingCalls.length} queued call(s) cancelled because this step failed\n`;
|
|
1896
|
+
writeStatus(cancelledQueuedStatus, chalk.dim(cancelledQueuedStatus));
|
|
1775
1897
|
pendingCalls = [];
|
|
1776
1898
|
}
|
|
1777
1899
|
const output = result.output.trim();
|
|
@@ -1788,6 +1910,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1788
1910
|
outputPath: savedOutputPath,
|
|
1789
1911
|
truncated: result.truncated ?? Boolean(savedOutputPath),
|
|
1790
1912
|
};
|
|
1913
|
+
const contextOutput = formatToolContext(call, resultWithArtifact);
|
|
1914
|
+
emitToolResult(toolEventId, resultWithArtifact, contextOutput, savedOutputPath);
|
|
1791
1915
|
options.onToolResult?.(call, resultWithArtifact);
|
|
1792
1916
|
await auditLog("tool.result", {
|
|
1793
1917
|
call,
|
|
@@ -1814,7 +1938,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1814
1938
|
? "tesseract"
|
|
1815
1939
|
: undefined;
|
|
1816
1940
|
if (cmdName) {
|
|
1817
|
-
|
|
1941
|
+
writeNotice("warn", `${cmdName} not found — asking model to install and retry`, chalk.yellow(` ⚠ ${cmdName} not found — asking model to install and retry\n`));
|
|
1818
1942
|
messages.push({
|
|
1819
1943
|
role: "tool",
|
|
1820
1944
|
content: `Tool failed: "${cmdName}" is not installed.\n` +
|
|
@@ -1832,7 +1956,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1832
1956
|
// -S / askpass / piping a password.
|
|
1833
1957
|
const SUDO_NEEDS_TTY_RE = /sudo:\s+a terminal is required to read the password|sudo:\s+a password is required|no askpass program|sudo: \d+ incorrect password attempts|sudo:\s+(?:no tty present|sorry, you must have a tty)/i;
|
|
1834
1958
|
if (!result.ok && SUDO_NEEDS_TTY_RE.test(output)) {
|
|
1835
|
-
|
|
1959
|
+
writeNotice("warn", "sudo needs an interactive terminal — asking the model to retry without -S/askpass", chalk.yellow(" ⚠ sudo needs an interactive terminal — asking the model to retry without -S/askpass\n"));
|
|
1836
1960
|
messages.push({
|
|
1837
1961
|
role: "tool",
|
|
1838
1962
|
content: "Tool failed: sudo could not read a password.\n" +
|
|
@@ -1845,7 +1969,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1845
1969
|
}
|
|
1846
1970
|
// Print tool result
|
|
1847
1971
|
const statusIcon = result.ok ? chalk.green(" ✓") : chalk.red(" ✗");
|
|
1848
|
-
|
|
1972
|
+
writeToolOutput(toolEventId, result.ok ? "ok\n" : "failed\n", statusIcon + "\n");
|
|
1849
1973
|
if (output) {
|
|
1850
1974
|
const displaySummary = summarizeOutput(output, displayMax);
|
|
1851
1975
|
const displayText = displaySummary.truncated
|
|
@@ -1855,20 +1979,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1855
1979
|
// the same bytes. Just note where the full output lives if it was saved.
|
|
1856
1980
|
if (liveBytes > 0) {
|
|
1857
1981
|
if (savedOutputPath) {
|
|
1858
|
-
|
|
1982
|
+
writeNotice("info", `full output saved to ${savedOutputPath}`, chalk.dim(` full output saved to ${savedOutputPath}\n`));
|
|
1859
1983
|
}
|
|
1860
1984
|
}
|
|
1861
1985
|
else {
|
|
1862
1986
|
const renderedOutput = indentAndWrapText(displayText);
|
|
1863
|
-
|
|
1987
|
+
writeToolOutput(toolEventId, displayText, styleToolChatter(call, renderedOutput) + "\n");
|
|
1864
1988
|
}
|
|
1865
1989
|
}
|
|
1866
1990
|
if (isAbortError(undefined, options.signal)) {
|
|
1867
1991
|
lastAnswer = "Aborted.";
|
|
1868
|
-
|
|
1992
|
+
writeAbort();
|
|
1869
1993
|
return lastAnswer;
|
|
1870
1994
|
}
|
|
1871
|
-
const contextOutput = formatToolContext(call, resultWithArtifact);
|
|
1872
1995
|
// Register a collapse/expand viewport so the user can pull the full raw
|
|
1873
1996
|
// output back with Ctrl+O or `/output last` after the AI summary lands.
|
|
1874
1997
|
if (output) {
|
|
@@ -1882,7 +2005,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1882
2005
|
// (large output saved to disk). Avoid spamming the hint for
|
|
1883
2006
|
// every tiny tool call — the user can always use /output last.
|
|
1884
2007
|
if (savedOutputPath) {
|
|
1885
|
-
|
|
2008
|
+
const viewportHint = `${formatViewportHint(viewport)}\n`;
|
|
2009
|
+
writeStatus(viewportHint, viewportHint);
|
|
1886
2010
|
}
|
|
1887
2011
|
}
|
|
1888
2012
|
messages.push({
|
|
@@ -1903,7 +2027,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1903
2027
|
}
|
|
1904
2028
|
}
|
|
1905
2029
|
lastAnswer = `Stopped after ${productiveSteps} steps.`;
|
|
1906
|
-
|
|
1907
|
-
return lastAnswer;
|
|
2030
|
+
writeNotice("warn", lastAnswer, " " + chalk.yellow(lastAnswer) + "\n");
|
|
2031
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1908
2032
|
}
|
|
1909
2033
|
//# sourceMappingURL=runner.js.map
|