@pentoshi/clai 1.2.5 → 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 +12 -4
- package/dist/agent/runner.js +236 -83
- 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/prompts/index.d.ts +1 -1
- package/dist/prompts/index.js +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";
|
|
@@ -525,31 +525,60 @@ const INCOMPLETE_RE = /\b(?:not\s+complete|incomplete|isn'?t\s+(?:done|complete|
|
|
|
525
525
|
// would otherwise trip the volatile-info freshness guard and divert the run
|
|
526
526
|
// into a pointless web.search instead of executing the plan.
|
|
527
527
|
const PLAN_EXECUTION_RE = /\b(?:approve the plan|execute it (?:now|task by task)|task by task|execute the plan|implement the plan)\b/i;
|
|
528
|
+
// Informational / comparison / explanation intent. These questions want an
|
|
529
|
+
// ANSWER, not a build — even when they mention a framework or an install
|
|
530
|
+
// step (e.g. "compare installation steps in react vite", "how do I set up
|
|
531
|
+
// tailwind", "tailwind 3 vs 4"). They must NOT trigger the explore→plan
|
|
532
|
+
// build workflow.
|
|
533
|
+
const INFORMATIONAL_SIGNAL_RE = /\b(?:compare|comparison|contrast|differ(?:ence|ences|s)?|pros\s+and\s+cons|trade-?offs?|versus|vs\.?|cheat\s*sheet|explain|describe|summari[sz]e|overview)\b/i;
|
|
534
|
+
const INTERROGATIVE_LEAD_RE = /^(?:what|which|why|how|when|who|where|is|are|do|does|did|can|could|should|would|will)\b/i;
|
|
528
535
|
/**
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
|
|
536
|
+
* Does a single message imply an actual build/scaffold task (as opposed to a
|
|
537
|
+
* question about one)? Comparison/explanation signals and plain questions are
|
|
538
|
+
* treated as informational and return false even when they name a stack.
|
|
539
|
+
*/
|
|
540
|
+
function messageImpliesBuild(text) {
|
|
541
|
+
if (!text)
|
|
542
|
+
return false;
|
|
543
|
+
if (INFORMATIONAL_SIGNAL_RE.test(text))
|
|
544
|
+
return false;
|
|
545
|
+
// Explicit "build/create/scaffold … <thing>" is always a build.
|
|
546
|
+
if (BUILD_TASK_RE.test(text))
|
|
547
|
+
return true;
|
|
548
|
+
// A bare question (interrogative lead or trailing "?") that merely mentions
|
|
549
|
+
// a stack is informational, not a build.
|
|
550
|
+
if (text.endsWith("?") || INTERROGATIVE_LEAD_RE.test(text))
|
|
551
|
+
return false;
|
|
552
|
+
return BUILD_STACK_RE.test(text);
|
|
553
|
+
}
|
|
554
|
+
/**
|
|
555
|
+
* Decide whether this turn should get the build workflow (explore → plan →
|
|
556
|
+
* implement) and a generous step budget. Looks at the current prompt first,
|
|
557
|
+
* then falls back to recent USER turns so a terse follow-up inherits an
|
|
558
|
+
* ongoing build — but NOT the agent's own (possibly mistaken) plan narration.
|
|
533
559
|
*/
|
|
534
560
|
export function looksLikeBuildTask(prompt, history) {
|
|
535
561
|
const text = prompt.replace(/\s+/g, " ").trim();
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
CONTINUATION_RE.test(text) ||
|
|
562
|
+
// Continuation / "not done yet" / plan-execution always count as build.
|
|
563
|
+
if (CONTINUATION_RE.test(text) ||
|
|
539
564
|
INCOMPLETE_RE.test(text) ||
|
|
540
565
|
PLAN_EXECUTION_RE.test(text)) {
|
|
541
566
|
return true;
|
|
542
567
|
}
|
|
543
|
-
|
|
544
|
-
|
|
568
|
+
if (messageImpliesBuild(text)) {
|
|
569
|
+
return true;
|
|
570
|
+
}
|
|
571
|
+
// Inspect recent USER turns only: if the user was already building
|
|
572
|
+
// something, treat a terse follow-up as part of that build. (Assistant
|
|
573
|
+
// turns are excluded so a misfired plan can't keep re-triggering build.)
|
|
545
574
|
if (history && history.length > 0) {
|
|
546
575
|
const recent = history.slice(-6);
|
|
547
576
|
for (const msg of recent) {
|
|
548
|
-
if (msg.role !== "user"
|
|
577
|
+
if (msg.role !== "user")
|
|
549
578
|
continue;
|
|
550
|
-
|
|
551
|
-
if (BUILD_TASK_RE.test(h) || BUILD_STACK_RE.test(h))
|
|
579
|
+
if (messageImpliesBuild(msg.content.replace(/\s+/g, " ").trim())) {
|
|
552
580
|
return true;
|
|
581
|
+
}
|
|
553
582
|
}
|
|
554
583
|
}
|
|
555
584
|
return false;
|
|
@@ -711,7 +740,7 @@ const PRE_APPROVAL_ALLOWED_TOOLS = new Set([
|
|
|
711
740
|
export function isPreApprovalAllowedTool(name) {
|
|
712
741
|
return PRE_APPROVAL_ALLOWED_TOOLS.has(name);
|
|
713
742
|
}
|
|
714
|
-
function styleToolChatter(call, text) {
|
|
743
|
+
export function styleToolChatter(call, text) {
|
|
715
744
|
return shouldDimToolChatter(call) ? chalk.dim(text) : text;
|
|
716
745
|
}
|
|
717
746
|
function isAbortError(error, signal) {
|
|
@@ -790,7 +819,21 @@ function formatToolContext(call, result) {
|
|
|
790
819
|
: "";
|
|
791
820
|
return `${summary.text}${saved}`.trim();
|
|
792
821
|
}
|
|
793
|
-
|
|
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) {
|
|
794
837
|
if (!isPentestToolCall(call))
|
|
795
838
|
return true;
|
|
796
839
|
// Persistent auth (via `clai authorize-pentest AGREE`) wins.
|
|
@@ -805,16 +848,13 @@ async function ensurePentestAuthorization(call, autoConfirm, session) {
|
|
|
805
848
|
session.pentestAuthorized.value = true;
|
|
806
849
|
return true;
|
|
807
850
|
}
|
|
808
|
-
const ok = await
|
|
809
|
-
message: chalk.red("clai only assists with security testing on systems you own or have written permission to test. Confirm for this session?"),
|
|
810
|
-
default: false,
|
|
811
|
-
});
|
|
851
|
+
const ok = await confirmPort.confirmPentest();
|
|
812
852
|
if (!ok)
|
|
813
853
|
return false;
|
|
814
854
|
session.pentestAuthorized.value = true;
|
|
815
855
|
return true;
|
|
816
856
|
}
|
|
817
|
-
async function confirmToolExecution(call, autoConfirm, session) {
|
|
857
|
+
async function confirmToolExecution(call, autoConfirm, session, confirmPort) {
|
|
818
858
|
const config = getConfig();
|
|
819
859
|
if (autoConfirm)
|
|
820
860
|
return true;
|
|
@@ -825,10 +865,7 @@ async function confirmToolExecution(call, autoConfirm, session) {
|
|
|
825
865
|
// set so authorizations never leak across processes.
|
|
826
866
|
if (config.allowAlwaysTools.includes(call.name))
|
|
827
867
|
return true;
|
|
828
|
-
return
|
|
829
|
-
message: chalk.yellow(` run ${call.name}: ${formatToolArgs(call)}?`),
|
|
830
|
-
default: true,
|
|
831
|
-
});
|
|
868
|
+
return confirmPort.confirmTool(call);
|
|
832
869
|
}
|
|
833
870
|
/** Build the system-context block describing the session's active plan. */
|
|
834
871
|
function planContextMessage(plan, approved) {
|
|
@@ -921,6 +958,7 @@ async function handlePlanTool(call, session, ctx) {
|
|
|
921
958
|
return {
|
|
922
959
|
handled: true,
|
|
923
960
|
ok: true,
|
|
961
|
+
plan,
|
|
924
962
|
display,
|
|
925
963
|
modelNote: `Plan saved with ${plan.tasks.length} task(s). STOP here and wait — produce NO other tool calls now. ` +
|
|
926
964
|
"Do NOT start executing tasks until the user approves with /implement. " +
|
|
@@ -979,6 +1017,7 @@ async function handlePlanTool(call, session, ctx) {
|
|
|
979
1017
|
return {
|
|
980
1018
|
handled: true,
|
|
981
1019
|
ok: true,
|
|
1020
|
+
plan,
|
|
982
1021
|
display: checklist + "\n",
|
|
983
1022
|
modelNote: allDone
|
|
984
1023
|
? "Task updated. ALL tasks are now finished. Verify the result and give your final summary."
|
|
@@ -986,8 +1025,89 @@ async function handlePlanTool(call, session, ctx) {
|
|
|
986
1025
|
};
|
|
987
1026
|
}
|
|
988
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 });
|
|
989
1108
|
const config = getConfig();
|
|
990
1109
|
const maxSteps = options.maxSteps ?? 70;
|
|
1110
|
+
const confirmPort = options.confirm ?? inquirerConfirmPort;
|
|
991
1111
|
const projectContext = await loadProjectContext();
|
|
992
1112
|
const toolNames = availableToolNames();
|
|
993
1113
|
// Build / scaffold / continuation turns must NEVER be diverted into a
|
|
@@ -1128,6 +1248,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1128
1248
|
const maxIterations = stepBudget * 3;
|
|
1129
1249
|
let productiveSteps = 0;
|
|
1130
1250
|
let step = -1;
|
|
1251
|
+
let nextToolEventId = 0;
|
|
1131
1252
|
for (let iteration = 0; iteration < maxIterations; iteration += 1) {
|
|
1132
1253
|
// `step` is the productive-step index (used for display + audit). It only
|
|
1133
1254
|
// advances when the previous iteration actually executed a tool.
|
|
@@ -1146,16 +1267,33 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1146
1267
|
// when the batch was parsed.
|
|
1147
1268
|
call = pendingCalls.shift();
|
|
1148
1269
|
assistantText = { visible: "", thinkContent: "", hasThinking: false };
|
|
1149
|
-
|
|
1270
|
+
const batchStatus = ` ↳ continuing batch (${pendingCalls.length} more queued)\n`;
|
|
1271
|
+
writeStatus(batchStatus, chalk.dim(batchStatus));
|
|
1150
1272
|
}
|
|
1151
1273
|
else {
|
|
1152
1274
|
// Buffer LLM output so tool JSON and hidden thinking are not printed raw.
|
|
1153
1275
|
// Status messages (rate-limit retries, fallback hints) still surface live.
|
|
1154
1276
|
// A spinner gives the user feedback during long thinking phases on
|
|
1155
1277
|
// models like glm-5.1 / deepseek-v4-flash that stream reasoning first.
|
|
1156
|
-
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
|
+
}
|
|
1157
1285
|
let sawReasoning = false;
|
|
1158
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
|
+
});
|
|
1159
1297
|
let completion;
|
|
1160
1298
|
try {
|
|
1161
1299
|
completion = await streamWithProvider({
|
|
@@ -1174,6 +1312,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1174
1312
|
signal: options.signal,
|
|
1175
1313
|
thinking: config.thinking,
|
|
1176
1314
|
}, (token) => {
|
|
1315
|
+
deltaParser?.push(token);
|
|
1177
1316
|
// Heuristic: <think>… markers and reasoning_content tokens flow
|
|
1178
1317
|
// through onToken. Surface activity in the spinner so the screen
|
|
1179
1318
|
// is never empty for minutes.
|
|
@@ -1181,6 +1320,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1181
1320
|
sawReasoning = true;
|
|
1182
1321
|
inThinking = true;
|
|
1183
1322
|
spinner.setLabel("thinking");
|
|
1323
|
+
if (!writesDirectly)
|
|
1324
|
+
emit({ type: "status", text: "thinking" });
|
|
1184
1325
|
}
|
|
1185
1326
|
if (/<\/think>/i.test(token)) {
|
|
1186
1327
|
inThinking = false;
|
|
@@ -1201,7 +1342,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1201
1342
|
}
|
|
1202
1343
|
}, (status) => {
|
|
1203
1344
|
spinner.stop();
|
|
1204
|
-
|
|
1345
|
+
writeStatus(status, chalk.dim(status));
|
|
1205
1346
|
});
|
|
1206
1347
|
}
|
|
1207
1348
|
finally {
|
|
@@ -1210,6 +1351,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1210
1351
|
}
|
|
1211
1352
|
provider = completion.provider;
|
|
1212
1353
|
model = completion.model;
|
|
1354
|
+
deltaParser?.finish();
|
|
1213
1355
|
const assistantTextResult = rememberThinkingFromText(completion.text);
|
|
1214
1356
|
assistantText = assistantTextResult;
|
|
1215
1357
|
// Try visible text first, then thinking content — some models (e.g. glm-5.1)
|
|
@@ -1224,7 +1366,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1224
1366
|
strict: getConfig().parserStrict,
|
|
1225
1367
|
});
|
|
1226
1368
|
if (call) {
|
|
1227
|
-
|
|
1369
|
+
writeNotice("info", "recovered tool call from thinking content", chalk.dim(" ℹ recovered tool call from thinking content\n"));
|
|
1228
1370
|
}
|
|
1229
1371
|
}
|
|
1230
1372
|
// ── Thinking-only recovery ────────────────────────────────────────
|
|
@@ -1235,8 +1377,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1235
1377
|
if (!assistantText.visible.trim() && !call && assistantText.hasThinking) {
|
|
1236
1378
|
emptyVisibleRetries += 1;
|
|
1237
1379
|
if (emptyVisibleRetries <= 2) {
|
|
1238
|
-
|
|
1239
|
-
|
|
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"));
|
|
1240
1382
|
messages.push({
|
|
1241
1383
|
role: "assistant",
|
|
1242
1384
|
content: collapseRepeatedText(completion.text),
|
|
@@ -1275,7 +1417,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1275
1417
|
if (bare?.call) {
|
|
1276
1418
|
call = bare.call;
|
|
1277
1419
|
recoveredFromBareJson = true;
|
|
1278
|
-
|
|
1420
|
+
writeNotice("info", "recovered an unfenced tool call from bare JSON", chalk.dim(" ℹ recovered an unfenced tool call from bare JSON\n"));
|
|
1279
1421
|
}
|
|
1280
1422
|
else if (bare?.argsOnly) {
|
|
1281
1423
|
bareArgsOnly = true;
|
|
@@ -1287,7 +1429,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1287
1429
|
if (bareThink?.call) {
|
|
1288
1430
|
call = bareThink.call;
|
|
1289
1431
|
recoveredFromBareJson = true;
|
|
1290
|
-
|
|
1432
|
+
writeNotice("info", "recovered an unfenced tool call from thinking content", chalk.dim(" ℹ recovered an unfenced tool call from thinking content\n"));
|
|
1291
1433
|
}
|
|
1292
1434
|
else if (bareThink?.argsOnly) {
|
|
1293
1435
|
bareArgsOnly = true;
|
|
@@ -1297,7 +1439,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1297
1439
|
if (bareArgsOnly) {
|
|
1298
1440
|
bareToolJsonRetries += 1;
|
|
1299
1441
|
if (bareToolJsonRetries <= 3) {
|
|
1300
|
-
|
|
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"));
|
|
1301
1443
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1302
1444
|
messages.push(recoveryUserMessage(buildLikeTurn && !activePlan
|
|
1303
1445
|
? "Your previous message was a bare JSON args object with no tool name and no ```tool fence, so NOTHING ran. " +
|
|
@@ -1319,7 +1461,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1319
1461
|
// raw tokens looks like a crash to the user — instead, ask the
|
|
1320
1462
|
// model to retry the tool call in a clean JSON format.
|
|
1321
1463
|
if (/<\|tool_call(?:s_section)?_begin\|>|<\|tool_call_argument_begin\|>/i.test(assistantText.visible)) {
|
|
1322
|
-
|
|
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"));
|
|
1323
1465
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1324
1466
|
messages.push(recoveryUserMessage("Your previous tool call was malformed or truncated. " +
|
|
1325
1467
|
"Reply with ONLY a fenced ```tool block containing valid JSON " +
|
|
@@ -1334,7 +1476,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1334
1476
|
if (looksLikeTruncatedToolCall(assistantText.visible)) {
|
|
1335
1477
|
truncatedToolRetries += 1;
|
|
1336
1478
|
if (truncatedToolRetries <= 3) {
|
|
1337
|
-
|
|
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"));
|
|
1338
1480
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1339
1481
|
messages.push({
|
|
1340
1482
|
role: "user",
|
|
@@ -1361,7 +1503,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1361
1503
|
if (hasFencedCallShape) {
|
|
1362
1504
|
malformedFenceRetries += 1;
|
|
1363
1505
|
if (malformedFenceRetries <= 3) {
|
|
1364
|
-
|
|
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"));
|
|
1365
1507
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1366
1508
|
messages.push({
|
|
1367
1509
|
role: "user",
|
|
@@ -1398,7 +1540,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1398
1540
|
if (activePlan && session.planApproved.value) {
|
|
1399
1541
|
nudge =
|
|
1400
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.";
|
|
1401
|
-
|
|
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"));
|
|
1402
1544
|
}
|
|
1403
1545
|
else if (planNarrated || productiveSteps > 0) {
|
|
1404
1546
|
// It explored and/or wrote a plan as prose but never called
|
|
@@ -1408,14 +1550,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1408
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" +
|
|
1409
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' +
|
|
1410
1552
|
"Do not describe the plan again in prose — just emit the plan.create tool block.";
|
|
1411
|
-
|
|
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"));
|
|
1412
1554
|
}
|
|
1413
1555
|
else {
|
|
1414
1556
|
nudge =
|
|
1415
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" +
|
|
1416
1558
|
'```tool\n{"name":"fs.list","args":{"path":"."}}\n```\n' +
|
|
1417
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.";
|
|
1418
|
-
|
|
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"));
|
|
1419
1561
|
}
|
|
1420
1562
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1421
1563
|
messages.push(recoveryUserMessage(nudge));
|
|
@@ -1423,7 +1565,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1423
1565
|
}
|
|
1424
1566
|
if (freshWebSearchRequired && !sawFreshWebSearch && !freshnessRetryUsed) {
|
|
1425
1567
|
freshnessRetryUsed = true;
|
|
1426
|
-
|
|
1568
|
+
writeNotice("info", "current-info question detected — searching the web before answering", chalk.dim(" ℹ current-info question detected — searching the web before answering\n"));
|
|
1427
1569
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1428
1570
|
messages.push({
|
|
1429
1571
|
role: "user",
|
|
@@ -1444,7 +1586,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1444
1586
|
if (livePlan && unfinished && unfinished.length > 0) {
|
|
1445
1587
|
prematureCompletionRetries += 1;
|
|
1446
1588
|
const next = unfinished[0];
|
|
1447
|
-
|
|
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`));
|
|
1448
1590
|
messages.push({ role: "assistant", content: assistantText.visible });
|
|
1449
1591
|
messages.push({
|
|
1450
1592
|
role: "user",
|
|
@@ -1464,10 +1606,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1464
1606
|
// unchallenged — append an explicit, honest status so the user knows the
|
|
1465
1607
|
// build did not actually complete.
|
|
1466
1608
|
let completionWarning = "";
|
|
1609
|
+
let completionWarningText = "";
|
|
1467
1610
|
if (session.planApproved.value) {
|
|
1468
1611
|
const livePlan = await loadPlan(session.sessionId).catch(() => undefined);
|
|
1469
1612
|
const unfinished = livePlan?.tasks.filter((t) => t.state === "pending" || t.state === "in_progress");
|
|
1470
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.";
|
|
1471
1617
|
completionWarning =
|
|
1472
1618
|
chalk.yellow(`\n ⚠ ${unfinished.length} of ${livePlan.tasks.length} plan task(s) are NOT actually complete:\n`) +
|
|
1473
1619
|
unfinished
|
|
@@ -1477,19 +1623,17 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1477
1623
|
}
|
|
1478
1624
|
}
|
|
1479
1625
|
if (cleaned) {
|
|
1480
|
-
|
|
1481
|
-
if (!cleaned.endsWith("\n"))
|
|
1482
|
-
process.stdout.write("\n");
|
|
1626
|
+
writeAssistantMessage(cleaned);
|
|
1483
1627
|
}
|
|
1484
1628
|
if (completionWarning) {
|
|
1485
|
-
|
|
1629
|
+
writeNotice("warn", completionWarningText, completionWarning);
|
|
1486
1630
|
}
|
|
1487
1631
|
if (assistantText.hasThinking) {
|
|
1488
|
-
|
|
1632
|
+
writeThinkingBlock(assistantText.thinkContent);
|
|
1489
1633
|
}
|
|
1490
1634
|
await auditLog("agent.final", { provider, model, steps: step + 1 });
|
|
1491
1635
|
lastAnswer = cleaned;
|
|
1492
|
-
return lastAnswer;
|
|
1636
|
+
return finishTurn(lastAnswer, step + 1);
|
|
1493
1637
|
}
|
|
1494
1638
|
// A valid primary tool call exists for this fresh model turn. Show any
|
|
1495
1639
|
// prose / thinking that preceded it, record the assistant message ONCE,
|
|
@@ -1499,10 +1643,10 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1499
1643
|
? ""
|
|
1500
1644
|
: textBeforeToolCall(assistantText.visible);
|
|
1501
1645
|
if (beforeTool) {
|
|
1502
|
-
|
|
1646
|
+
writeAssistantMessage(beforeTool);
|
|
1503
1647
|
}
|
|
1504
1648
|
if (assistantText.hasThinking) {
|
|
1505
|
-
|
|
1649
|
+
writeThinkingBlock(assistantText.thinkContent);
|
|
1506
1650
|
}
|
|
1507
1651
|
messages.push({
|
|
1508
1652
|
role: "assistant",
|
|
@@ -1514,7 +1658,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1514
1658
|
allCalls[0] &&
|
|
1515
1659
|
sameToolCall(allCalls[0], call)) {
|
|
1516
1660
|
pendingCalls = allCalls.slice(1);
|
|
1517
|
-
|
|
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`));
|
|
1518
1662
|
}
|
|
1519
1663
|
}
|
|
1520
1664
|
}
|
|
@@ -1527,6 +1671,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1527
1671
|
// runs and is safety-classified as the shell command it really is —
|
|
1528
1672
|
// instead of dead-ending on "Unknown tool: sed".
|
|
1529
1673
|
call = normalizeToolCall(call);
|
|
1674
|
+
const toolEventId = `tool-${++nextToolEventId}`;
|
|
1530
1675
|
// ── Duplicate-call detection ──────────────────────────────────────────
|
|
1531
1676
|
// If the model calls the exact same tool with the exact same args
|
|
1532
1677
|
// repeatedly, it's stuck in a loop. Inject a corrective message
|
|
@@ -1536,7 +1681,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1536
1681
|
const isWrite = call.name === "fs.write" ||
|
|
1537
1682
|
call.name === "fs.writeMany" ||
|
|
1538
1683
|
call.name === "fs.edit";
|
|
1539
|
-
|
|
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`));
|
|
1540
1686
|
// A repeat means this batch went off the rails — drop any queued calls
|
|
1541
1687
|
// and let the model react. The assistant message was already recorded.
|
|
1542
1688
|
pendingCalls = [];
|
|
@@ -1552,7 +1698,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1552
1698
|
continue;
|
|
1553
1699
|
}
|
|
1554
1700
|
if (loopCheck.reason) {
|
|
1555
|
-
|
|
1701
|
+
writeNotice("info", loopCheck.reason, chalk.dim(` ℹ ${loopCheck.reason}\n`));
|
|
1556
1702
|
}
|
|
1557
1703
|
// ── Plan / task tools (session-scoped, handled inline) ─────────────
|
|
1558
1704
|
// These don't go through the generic registry because they need the
|
|
@@ -1565,7 +1711,9 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1565
1711
|
if (planResult.handled) {
|
|
1566
1712
|
productiveSteps += 1;
|
|
1567
1713
|
loopGuard.recordAttempt(step, call.name, call.args, planResult.ok, 0);
|
|
1568
|
-
|
|
1714
|
+
if (planResult.plan) {
|
|
1715
|
+
writePlanUpdate(planResult.plan, planResult.display);
|
|
1716
|
+
}
|
|
1569
1717
|
// plan.create means "STOP and wait for /implement" — abandon any
|
|
1570
1718
|
// other calls the model batched alongside it.
|
|
1571
1719
|
if (call.name === "plan.create")
|
|
@@ -1596,7 +1744,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1596
1744
|
if (activePlan &&
|
|
1597
1745
|
!session.planApproved.value &&
|
|
1598
1746
|
!isPreApprovalAllowedTool(call.name)) {
|
|
1599
|
-
|
|
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`));
|
|
1600
1749
|
pendingCalls = [];
|
|
1601
1750
|
messages.push({
|
|
1602
1751
|
role: "user",
|
|
@@ -1613,23 +1762,23 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1613
1762
|
}
|
|
1614
1763
|
// Show tool call
|
|
1615
1764
|
const toolCallLine = chalk.cyan(` ▶ ${call.name}`) + chalk.gray(` ${formatToolArgs(call)}`);
|
|
1616
|
-
|
|
1765
|
+
writeToolCall(toolEventId, call, styleToolChatter(call, toolCallLine) + "\n");
|
|
1617
1766
|
const scopeTarget = scopeTargetForToolCall(call);
|
|
1618
1767
|
if (scopeTarget &&
|
|
1619
1768
|
(!isScopeActive(scope) || !targetInScope(scopeTarget, scope))) {
|
|
1620
|
-
|
|
1769
|
+
writeNotice("info", `scope optional: ${scopeHint(scopeTarget)}`, chalk.dim(` scope optional: ${scopeHint(scopeTarget)}\n`));
|
|
1621
1770
|
}
|
|
1622
1771
|
if (decision.level === "block") {
|
|
1623
|
-
|
|
1772
|
+
writeToolBlocked(toolEventId, call.name, decision.reason, chalk.red(` ✗ blocked: ${decision.reason}`) + "\n");
|
|
1624
1773
|
lastAnswer = `Blocked: ${call.name} — ${decision.reason}`;
|
|
1625
|
-
return lastAnswer;
|
|
1774
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1626
1775
|
}
|
|
1627
1776
|
// Pentest authorization — if user confirms this, skip the per-tool confirm
|
|
1628
1777
|
let pentestJustConfirmed = false;
|
|
1629
1778
|
const needsPentestAuth = isPentestToolCall(call) &&
|
|
1630
1779
|
!getConfig().pentestAuthorized &&
|
|
1631
1780
|
!session.pentestAuthorized.value;
|
|
1632
|
-
const authorized = await ensurePentestAuthorization(call, Boolean(options.autoConfirm), session);
|
|
1781
|
+
const authorized = await ensurePentestAuthorization(call, Boolean(options.autoConfirm), session, confirmPort);
|
|
1633
1782
|
// inquirer's confirm() creates its own readline interface which resets
|
|
1634
1783
|
// raw mode AND pauses stdin when it finishes. Re-assert raw mode and
|
|
1635
1784
|
// resume stdin so the outer keypress handler (ESC/Ctrl+C abort, Ctrl+O
|
|
@@ -1637,8 +1786,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1637
1786
|
restoreInteractiveStdin();
|
|
1638
1787
|
if (!authorized) {
|
|
1639
1788
|
lastAnswer = "Pentest authorization not confirmed.";
|
|
1640
|
-
|
|
1641
|
-
return lastAnswer;
|
|
1789
|
+
writeToolBlocked(toolEventId, call.name, lastAnswer, chalk.red(` ✗ ${lastAnswer}`) + "\n");
|
|
1790
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1642
1791
|
}
|
|
1643
1792
|
if (needsPentestAuth) {
|
|
1644
1793
|
pentestJustConfirmed = true;
|
|
@@ -1647,14 +1796,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1647
1796
|
// fs.delete and shell deletions NEVER auto-confirm even with -y flag.
|
|
1648
1797
|
const forceManualConfirm = call.name === "fs.delete";
|
|
1649
1798
|
if (decision.level === "confirm" && !pentestJustConfirmed) {
|
|
1650
|
-
const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session);
|
|
1799
|
+
const ok = await confirmToolExecution(call, forceManualConfirm ? false : Boolean(options.autoConfirm), session, confirmPort);
|
|
1651
1800
|
// Re-assert raw mode and resume stdin after inquirer's confirm()
|
|
1652
1801
|
// (see restoreInteractiveStdin / the comment above).
|
|
1653
1802
|
restoreInteractiveStdin();
|
|
1654
1803
|
if (!ok) {
|
|
1655
1804
|
lastAnswer = "Cancelled.";
|
|
1656
|
-
|
|
1657
|
-
return lastAnswer;
|
|
1805
|
+
writeNotice("warn", "cancelled", chalk.yellow(` ✗ cancelled`) + "\n");
|
|
1806
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1658
1807
|
}
|
|
1659
1808
|
}
|
|
1660
1809
|
// Execute tool
|
|
@@ -1668,7 +1817,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1668
1817
|
typeof call.args.command === "string" &&
|
|
1669
1818
|
looksInteractiveStdin(call.args.command);
|
|
1670
1819
|
if (interactiveCommand && process.stdin.isTTY) {
|
|
1671
|
-
|
|
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"));
|
|
1672
1821
|
}
|
|
1673
1822
|
let result;
|
|
1674
1823
|
let liveBytes = 0;
|
|
@@ -1692,8 +1841,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1692
1841
|
if (liveBytes >= liveCap) {
|
|
1693
1842
|
if (!liveTruncatedNotified) {
|
|
1694
1843
|
liveTruncatedNotified = true;
|
|
1695
|
-
|
|
1696
|
-
|
|
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"));
|
|
1697
1846
|
lastProgressAt = Date.now();
|
|
1698
1847
|
}
|
|
1699
1848
|
// After truncation, show a dot every 5 seconds so the user knows
|
|
@@ -1701,7 +1850,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1701
1850
|
const now = Date.now();
|
|
1702
1851
|
if (now - lastProgressAt > 5_000) {
|
|
1703
1852
|
lastProgressAt = now;
|
|
1704
|
-
|
|
1853
|
+
writeToolOutput(toolEventId, ".", chalk.dim("."));
|
|
1705
1854
|
}
|
|
1706
1855
|
return;
|
|
1707
1856
|
}
|
|
@@ -1714,7 +1863,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1714
1863
|
// Skip the dim wrapper for interactive commands so a sudo password
|
|
1715
1864
|
// prompt is rendered at full brightness; everything else stays dim
|
|
1716
1865
|
// so tool chatter is visually distinct from the model's prose.
|
|
1717
|
-
|
|
1866
|
+
writeToolOutput(toolEventId, slice, shouldDimLive ? chalk.dim(body) : body);
|
|
1718
1867
|
};
|
|
1719
1868
|
try {
|
|
1720
1869
|
result = await runToolCall(call, {
|
|
@@ -1726,13 +1875,14 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1726
1875
|
},
|
|
1727
1876
|
});
|
|
1728
1877
|
// Newline separator if live output or progress dots didn't already end with one.
|
|
1729
|
-
if (liveBytes > 0 || liveTruncatedNotified)
|
|
1730
|
-
|
|
1878
|
+
if (liveBytes > 0 || liveTruncatedNotified) {
|
|
1879
|
+
writeToolOutput(toolEventId, "\n", "\n");
|
|
1880
|
+
}
|
|
1731
1881
|
}
|
|
1732
1882
|
catch (toolError) {
|
|
1733
1883
|
if (isAbortError(toolError, options.signal)) {
|
|
1734
1884
|
lastAnswer = "Aborted.";
|
|
1735
|
-
|
|
1885
|
+
writeAbort();
|
|
1736
1886
|
return lastAnswer;
|
|
1737
1887
|
}
|
|
1738
1888
|
const errMsg = toolError instanceof Error ? toolError.message : String(toolError);
|
|
@@ -1742,7 +1892,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1742
1892
|
// from the same message so the model sees the failure and decides what to
|
|
1743
1893
|
// do next instead of blindly running steps that depended on it.
|
|
1744
1894
|
if (!result.ok && pendingCalls.length > 0) {
|
|
1745
|
-
|
|
1895
|
+
const cancelledQueuedStatus = ` ↳ ${pendingCalls.length} queued call(s) cancelled because this step failed\n`;
|
|
1896
|
+
writeStatus(cancelledQueuedStatus, chalk.dim(cancelledQueuedStatus));
|
|
1746
1897
|
pendingCalls = [];
|
|
1747
1898
|
}
|
|
1748
1899
|
const output = result.output.trim();
|
|
@@ -1759,6 +1910,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1759
1910
|
outputPath: savedOutputPath,
|
|
1760
1911
|
truncated: result.truncated ?? Boolean(savedOutputPath),
|
|
1761
1912
|
};
|
|
1913
|
+
const contextOutput = formatToolContext(call, resultWithArtifact);
|
|
1914
|
+
emitToolResult(toolEventId, resultWithArtifact, contextOutput, savedOutputPath);
|
|
1762
1915
|
options.onToolResult?.(call, resultWithArtifact);
|
|
1763
1916
|
await auditLog("tool.result", {
|
|
1764
1917
|
call,
|
|
@@ -1785,7 +1938,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1785
1938
|
? "tesseract"
|
|
1786
1939
|
: undefined;
|
|
1787
1940
|
if (cmdName) {
|
|
1788
|
-
|
|
1941
|
+
writeNotice("warn", `${cmdName} not found — asking model to install and retry`, chalk.yellow(` ⚠ ${cmdName} not found — asking model to install and retry\n`));
|
|
1789
1942
|
messages.push({
|
|
1790
1943
|
role: "tool",
|
|
1791
1944
|
content: `Tool failed: "${cmdName}" is not installed.\n` +
|
|
@@ -1803,7 +1956,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1803
1956
|
// -S / askpass / piping a password.
|
|
1804
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;
|
|
1805
1958
|
if (!result.ok && SUDO_NEEDS_TTY_RE.test(output)) {
|
|
1806
|
-
|
|
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"));
|
|
1807
1960
|
messages.push({
|
|
1808
1961
|
role: "tool",
|
|
1809
1962
|
content: "Tool failed: sudo could not read a password.\n" +
|
|
@@ -1816,7 +1969,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1816
1969
|
}
|
|
1817
1970
|
// Print tool result
|
|
1818
1971
|
const statusIcon = result.ok ? chalk.green(" ✓") : chalk.red(" ✗");
|
|
1819
|
-
|
|
1972
|
+
writeToolOutput(toolEventId, result.ok ? "ok\n" : "failed\n", statusIcon + "\n");
|
|
1820
1973
|
if (output) {
|
|
1821
1974
|
const displaySummary = summarizeOutput(output, displayMax);
|
|
1822
1975
|
const displayText = displaySummary.truncated
|
|
@@ -1826,20 +1979,19 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1826
1979
|
// the same bytes. Just note where the full output lives if it was saved.
|
|
1827
1980
|
if (liveBytes > 0) {
|
|
1828
1981
|
if (savedOutputPath) {
|
|
1829
|
-
|
|
1982
|
+
writeNotice("info", `full output saved to ${savedOutputPath}`, chalk.dim(` full output saved to ${savedOutputPath}\n`));
|
|
1830
1983
|
}
|
|
1831
1984
|
}
|
|
1832
1985
|
else {
|
|
1833
1986
|
const renderedOutput = indentAndWrapText(displayText);
|
|
1834
|
-
|
|
1987
|
+
writeToolOutput(toolEventId, displayText, styleToolChatter(call, renderedOutput) + "\n");
|
|
1835
1988
|
}
|
|
1836
1989
|
}
|
|
1837
1990
|
if (isAbortError(undefined, options.signal)) {
|
|
1838
1991
|
lastAnswer = "Aborted.";
|
|
1839
|
-
|
|
1992
|
+
writeAbort();
|
|
1840
1993
|
return lastAnswer;
|
|
1841
1994
|
}
|
|
1842
|
-
const contextOutput = formatToolContext(call, resultWithArtifact);
|
|
1843
1995
|
// Register a collapse/expand viewport so the user can pull the full raw
|
|
1844
1996
|
// output back with Ctrl+O or `/output last` after the AI summary lands.
|
|
1845
1997
|
if (output) {
|
|
@@ -1853,7 +2005,8 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1853
2005
|
// (large output saved to disk). Avoid spamming the hint for
|
|
1854
2006
|
// every tiny tool call — the user can always use /output last.
|
|
1855
2007
|
if (savedOutputPath) {
|
|
1856
|
-
|
|
2008
|
+
const viewportHint = `${formatViewportHint(viewport)}\n`;
|
|
2009
|
+
writeStatus(viewportHint, viewportHint);
|
|
1857
2010
|
}
|
|
1858
2011
|
}
|
|
1859
2012
|
messages.push({
|
|
@@ -1874,7 +2027,7 @@ export async function runAgentLoop(prompt, options = {}) {
|
|
|
1874
2027
|
}
|
|
1875
2028
|
}
|
|
1876
2029
|
lastAnswer = `Stopped after ${productiveSteps} steps.`;
|
|
1877
|
-
|
|
1878
|
-
return lastAnswer;
|
|
2030
|
+
writeNotice("warn", lastAnswer, " " + chalk.yellow(lastAnswer) + "\n");
|
|
2031
|
+
return finishTurn(lastAnswer, productiveSteps);
|
|
1879
2032
|
}
|
|
1880
2033
|
//# sourceMappingURL=runner.js.map
|