@lazyingart/agintiflow 0.20.118 → 0.20.119
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/package.json +1 -1
- package/scripts/smoke-coding-tools.js +8 -0
- package/scripts/smoke-toolchain-docker.js +19 -0
- package/src/agent-runner.js +123 -18
- package/src/docker-sandbox.js +29 -2
- package/src/interactive-cli.js +44 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyingart/agintiflow",
|
|
3
|
-
"version": "0.20.
|
|
3
|
+
"version": "0.20.119",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AgInTiFlow is a project-aware agent workspace for hybrid wet-dry R&D, hardware-aware intelligence, software automation, and industrial workflows.",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
runAgent,
|
|
10
10
|
sanitizeToolResult,
|
|
11
11
|
shouldShortCircuitToolBatch,
|
|
12
|
+
shellDiagnosticHint,
|
|
12
13
|
skippedAfterBlockedToolResult,
|
|
13
14
|
} from "../src/agent-runner.js";
|
|
14
15
|
import { formatBehaviorContractForPrompt } from "../src/behavior-contract.js";
|
|
@@ -410,6 +411,13 @@ try {
|
|
|
410
411
|
quotedDangerSearchPolicy.category !== "destructive",
|
|
411
412
|
"quoted destructive strings in grep pattern should not classify as a destructive command"
|
|
412
413
|
);
|
|
414
|
+
const grepCountHint = shellDiagnosticHint("grep -c 'Fatal\\|Emergency' article.log && echo done", {
|
|
415
|
+
ok: false,
|
|
416
|
+
exitCode: 1,
|
|
417
|
+
stdout: "0\n",
|
|
418
|
+
stderr: "",
|
|
419
|
+
});
|
|
420
|
+
assert(grepCountHint.includes("grep -c exits 1"), "failed grep-count validation should explain clean zero-match exit behavior");
|
|
413
421
|
const actualDangerAfterQuotePolicy = evaluateCommandPolicy('echo "rm -rf is text" && rm -rf reports', dockerWorkspacePolicy);
|
|
414
422
|
assert(!actualDangerAfterQuotePolicy.allowed, "actual destructive command after quoted text should still be blocked");
|
|
415
423
|
assert(actualDangerAfterQuotePolicy.category === "destructive", "actual destructive command after quoted text was not classified as destructive");
|
|
@@ -65,6 +65,25 @@ try {
|
|
|
65
65
|
const outsideRead = await runToolchainCommand(`cat ${path.join(outsideData, "source-note.txt")}`, config);
|
|
66
66
|
assert(outsideRead.stdout.includes("READ_ONLY_HOST_MOUNT_OK"), "Docker normal mode could not read the configured outside data root");
|
|
67
67
|
|
|
68
|
+
const abortController = new AbortController();
|
|
69
|
+
const abortCommand = `python3 -c 'import time; time.sleep(20)'`;
|
|
70
|
+
const abortPolicy = evaluateCommandPolicy(abortCommand, { ...config, packageInstallPolicy: "allow" });
|
|
71
|
+
assert(abortPolicy.allowed, "long-running Docker command should be allowed for interrupt smoke");
|
|
72
|
+
const abortStartedAt = Date.now();
|
|
73
|
+
const abortRun = runDockerSandboxCommand(abortCommand, { ...config, packageInstallPolicy: "allow" }, abortPolicy, {
|
|
74
|
+
signal: abortController.signal,
|
|
75
|
+
});
|
|
76
|
+
setTimeout(() => abortController.abort(new Error("smoke interrupt")), 500);
|
|
77
|
+
let interrupted = false;
|
|
78
|
+
try {
|
|
79
|
+
await abortRun;
|
|
80
|
+
} catch (error) {
|
|
81
|
+
interrupted = /abort|interrupt|cancel/i.test(String(error?.name || "")) || /abort|interrupt|cancel/i.test(String(error?.message || ""));
|
|
82
|
+
}
|
|
83
|
+
const abortElapsed = Date.now() - abortStartedAt;
|
|
84
|
+
assert(interrupted, "Docker command did not reject as interrupted after AbortController abort");
|
|
85
|
+
assert(abortElapsed < 8000, `Docker interrupt took too long: ${abortElapsed}ms`);
|
|
86
|
+
|
|
68
87
|
await fs.writeFile(
|
|
69
88
|
path.join(workspace, "plot_fx.py"),
|
|
70
89
|
[
|
package/src/agent-runner.js
CHANGED
|
@@ -2,9 +2,8 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import {
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
7
|
-
import { promisify } from "node:util";
|
|
8
7
|
import { chromium } from "playwright";
|
|
9
8
|
import { createClient, createPlan, requestNextStep } from "./model-client.js";
|
|
10
9
|
import { SessionStore } from "./session-store.js";
|
|
@@ -54,7 +53,6 @@ import {
|
|
|
54
53
|
serializeStepBudgetState,
|
|
55
54
|
} from "./step-budget-controller.js";
|
|
56
55
|
|
|
57
|
-
const exec = promisify(execCallback);
|
|
58
56
|
const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
|
|
59
57
|
const WORKSPACE_TOOLS = new Set(WORKSPACE_TOOL_NAMES);
|
|
60
58
|
const STATIC_PREVIEW_SERVER_PATH = fileURLToPath(new URL("./static-preview-server.js", import.meta.url));
|
|
@@ -585,6 +583,7 @@ async function createInitialState(config, sessionId) {
|
|
|
585
583
|
: "No shell command tool is available.",
|
|
586
584
|
`Permission contract: permission mode is ${config.permissionMode || "normal"}. Safe mode asks before workspace writes/setup. Normal mode allows current-project writes, read-only inspection of visible host paths, and approved Docker setup, but outside-workspace writes and host-system changes require approval. Danger mode is trusted host/full-access mode. Do not bypass blockers by retrying variants. If a tool result includes permissionAdvice or suggestedCommand, stop, explain the blocker, copy the exact suggestedCommand when giving a rerun path, and ask the user to approve/rerun that mode or choose a safer workspace-relative path. Never invent legacy AgInTi syntax such as \`aginti run --sandbox host\`; use the exact flags from permissionAdvice.`,
|
|
587
585
|
"If an operation fails but a directory, artifact, or file already exists, treat it as pre-existing unless you have evidence this run created or updated it. Verify expected outputs before claiming success.",
|
|
586
|
+
"For validation/evidence commands, remember that grep exits 1 on zero matches. If zero matches is the expected clean result, use `grep -c PATTERN file || true`, split evidence checks into independent commands, or use awk/python so a clean zero count does not stop an `&&` chain.",
|
|
588
587
|
config.allowShellTool
|
|
589
588
|
? "Host tmux tools are available for long-running terminals: list sessions, capture panes, send safe keys/text, and start detached sessions. Prefer these tools for monitoring long installs/tests/dev servers without blocking; capture before sending input and never send secrets or sudo passwords. Do not start or install tmux inside Docker run_command containers because those containers are short-lived. In Docker sandbox mode, tmux start/send commands must stay workspace-write-bound; prefer run_command for read-only host absolute path inspection through read-only mounts, and ask for --sandbox-mode host for trusted whole-host write/system work."
|
|
590
589
|
+ " For one-shot tmux commands, redirect stdout/stderr and exit status to a durable workspace log or keep the pane alive for capture; if capture fails because the session ended, do not infer output or exit status."
|
|
@@ -1006,6 +1005,7 @@ async function applyContinuationPrompt(state, config, observers) {
|
|
|
1006
1005
|
temporalContext,
|
|
1007
1006
|
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
1008
1007
|
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
1008
|
+
"Validation reminder: grep exits 1 on zero matches. For clean-zero checks, guard `grep -c` with `|| true` or split evidence commands so the validation can continue.",
|
|
1009
1009
|
config.allowShellTool
|
|
1010
1010
|
? config.useDockerSandbox
|
|
1011
1011
|
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths for outputs/writes; common host data roots are read-only at original absolute paths for inspection. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
@@ -1076,6 +1076,112 @@ function safeExecutionEnv() {
|
|
|
1076
1076
|
};
|
|
1077
1077
|
}
|
|
1078
1078
|
|
|
1079
|
+
function trimOutput(value = "", limit = 8000) {
|
|
1080
|
+
const text = redactSensitiveText(String(value || ""));
|
|
1081
|
+
return text.trim().slice(0, limit);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
function killChildTree(child, signal = "SIGTERM") {
|
|
1085
|
+
if (!child || child.killed) return;
|
|
1086
|
+
try {
|
|
1087
|
+
if (process.platform === "win32") child.kill(signal);
|
|
1088
|
+
else process.kill(-child.pid, signal);
|
|
1089
|
+
} catch {
|
|
1090
|
+
try {
|
|
1091
|
+
child.kill(signal);
|
|
1092
|
+
} catch {
|
|
1093
|
+
// Best effort; process may already be gone.
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
function runHostShellCommand(command, config) {
|
|
1099
|
+
return new Promise((resolve, reject) => {
|
|
1100
|
+
const shell = hostShellOption();
|
|
1101
|
+
const child = spawn(String(command || ""), {
|
|
1102
|
+
cwd: config.commandCwd,
|
|
1103
|
+
env: safeExecutionEnv(),
|
|
1104
|
+
shell,
|
|
1105
|
+
detached: process.platform !== "win32",
|
|
1106
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1107
|
+
});
|
|
1108
|
+
let stdout = "";
|
|
1109
|
+
let stderr = "";
|
|
1110
|
+
let settled = false;
|
|
1111
|
+
let timedOut = false;
|
|
1112
|
+
const timeoutMs = Number(config.shellTimeoutMs || process.env.AGINTI_SHELL_TIMEOUT_MS || 30000);
|
|
1113
|
+
const maxStdout = 220 * 1024;
|
|
1114
|
+
const maxStderr = 120 * 1024;
|
|
1115
|
+
|
|
1116
|
+
const settle = (callback) => {
|
|
1117
|
+
if (settled) return;
|
|
1118
|
+
settled = true;
|
|
1119
|
+
if (timer) clearTimeout(timer);
|
|
1120
|
+
if (config.abortSignal && onAbort) config.abortSignal.removeEventListener("abort", onAbort);
|
|
1121
|
+
callback();
|
|
1122
|
+
};
|
|
1123
|
+
const onAbort = () => {
|
|
1124
|
+
killChildTree(child, "SIGTERM");
|
|
1125
|
+
setTimeout(() => killChildTree(child, "SIGKILL"), 1200).unref?.();
|
|
1126
|
+
const error = new Error("Run interrupted by user.");
|
|
1127
|
+
error.name = "AbortError";
|
|
1128
|
+
error.code = "ABORT_ERR";
|
|
1129
|
+
settle(() => reject(error));
|
|
1130
|
+
};
|
|
1131
|
+
const timer =
|
|
1132
|
+
Number.isFinite(timeoutMs) && timeoutMs > 0
|
|
1133
|
+
? setTimeout(() => {
|
|
1134
|
+
timedOut = true;
|
|
1135
|
+
killChildTree(child, "SIGTERM");
|
|
1136
|
+
setTimeout(() => killChildTree(child, "SIGKILL"), 1200).unref?.();
|
|
1137
|
+
}, timeoutMs)
|
|
1138
|
+
: null;
|
|
1139
|
+
timer?.unref?.();
|
|
1140
|
+
|
|
1141
|
+
if (config.abortSignal?.aborted) return onAbort();
|
|
1142
|
+
if (config.abortSignal) config.abortSignal.addEventListener("abort", onAbort, { once: true });
|
|
1143
|
+
|
|
1144
|
+
child.stdout?.on("data", (chunk) => {
|
|
1145
|
+
if (stdout.length < maxStdout) stdout += chunk.toString();
|
|
1146
|
+
});
|
|
1147
|
+
child.stderr?.on("data", (chunk) => {
|
|
1148
|
+
if (stderr.length < maxStderr) stderr += chunk.toString();
|
|
1149
|
+
});
|
|
1150
|
+
child.on("error", (error) => {
|
|
1151
|
+
settle(() =>
|
|
1152
|
+
resolve({
|
|
1153
|
+
ok: false,
|
|
1154
|
+
exitCode: 1,
|
|
1155
|
+
stdout,
|
|
1156
|
+
stderr: `${stderr}${stderr ? "\n" : ""}${error instanceof Error ? error.message : String(error)}`,
|
|
1157
|
+
})
|
|
1158
|
+
);
|
|
1159
|
+
});
|
|
1160
|
+
child.on("close", (code, signalName) => {
|
|
1161
|
+
settle(() =>
|
|
1162
|
+
resolve({
|
|
1163
|
+
ok: !timedOut && Number(code || 0) === 0,
|
|
1164
|
+
exitCode: timedOut ? 124 : Number.isInteger(code) ? code : signalName ? 130 : 1,
|
|
1165
|
+
stdout,
|
|
1166
|
+
stderr: `${stderr}${timedOut ? `\nCommand timed out after ${timeoutMs}ms.` : ""}`,
|
|
1167
|
+
})
|
|
1168
|
+
);
|
|
1169
|
+
});
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
export function shellDiagnosticHint(command = "", result = {}) {
|
|
1174
|
+
if (result?.ok !== false) return "";
|
|
1175
|
+
const text = String(command || "");
|
|
1176
|
+
if (/\bgrep\b[\s\S]*\s-c\b|\bgrep\s+-c\b/.test(text)) {
|
|
1177
|
+
return "grep -c exits 1 when it finds zero matches even though it prints 0. For count-only validation, use `grep -c PATTERN file || true`, split evidence commands, or use awk/python when zero matches is the expected clean state.";
|
|
1178
|
+
}
|
|
1179
|
+
if (/\bgrep\b/.test(text) && /&&/.test(text)) {
|
|
1180
|
+
return "grep exits 1 when it finds no matches, so an `&&` evidence chain can stop early. If no matches is acceptable, guard that grep with `|| true` or run independent validation commands.";
|
|
1181
|
+
}
|
|
1182
|
+
return "";
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1079
1185
|
function hashForLog(value) {
|
|
1080
1186
|
return crypto.createHash("sha256").update(String(value ?? "")).digest("hex");
|
|
1081
1187
|
}
|
|
@@ -1261,29 +1367,26 @@ async function runShellCommand(command, config, policy = evaluateCommandPolicy(c
|
|
|
1261
1367
|
throwIfAborted(config);
|
|
1262
1368
|
const result = config.useDockerSandbox
|
|
1263
1369
|
? await runDockerSandboxCommand(command, config, policy, { signal: config.abortSignal })
|
|
1264
|
-
: await
|
|
1265
|
-
|
|
1266
|
-
timeout: 30000,
|
|
1267
|
-
maxBuffer: 200 * 1024,
|
|
1268
|
-
shell: hostShellOption(),
|
|
1269
|
-
env: safeExecutionEnv(),
|
|
1270
|
-
signal: config.abortSignal,
|
|
1271
|
-
});
|
|
1370
|
+
: await runHostShellCommand(command, config);
|
|
1371
|
+
const diagnosticHint = shellDiagnosticHint(command, result);
|
|
1272
1372
|
|
|
1273
1373
|
return {
|
|
1274
|
-
ok:
|
|
1275
|
-
exitCode: 0,
|
|
1276
|
-
stdout:
|
|
1277
|
-
stderr:
|
|
1374
|
+
ok: result.ok !== false,
|
|
1375
|
+
exitCode: Number.isInteger(result.exitCode) ? result.exitCode : 0,
|
|
1376
|
+
stdout: trimOutput(result.stdout, 8000),
|
|
1377
|
+
stderr: trimOutput(result.stderr, 4000),
|
|
1378
|
+
...(diagnosticHint ? { diagnosticHint } : {}),
|
|
1278
1379
|
};
|
|
1279
1380
|
} catch (error) {
|
|
1280
1381
|
if (isAbortError(error, config)) throw error;
|
|
1281
|
-
|
|
1382
|
+
const failedResult = {
|
|
1282
1383
|
ok: false,
|
|
1283
1384
|
exitCode: Number.isInteger(error?.code) ? error.code : 1,
|
|
1284
|
-
stdout:
|
|
1285
|
-
stderr:
|
|
1385
|
+
stdout: trimOutput(error?.stdout || "", 8000),
|
|
1386
|
+
stderr: trimOutput(error?.stderr || error?.message || "", 4000),
|
|
1286
1387
|
};
|
|
1388
|
+
const diagnosticHint = shellDiagnosticHint(command, failedResult);
|
|
1389
|
+
return diagnosticHint ? { ...failedResult, diagnosticHint } : failedResult;
|
|
1287
1390
|
}
|
|
1288
1391
|
}
|
|
1289
1392
|
|
|
@@ -1295,6 +1398,7 @@ async function captureSyntheticSnapshot(store, step, config) {
|
|
|
1295
1398
|
pageText: [
|
|
1296
1399
|
"No browser page is currently open.",
|
|
1297
1400
|
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
1401
|
+
"Validation reminder: grep exits 1 on zero matches; guard expected clean-zero grep checks or split evidence commands.",
|
|
1298
1402
|
config.allowShellTool
|
|
1299
1403
|
? config.useDockerSandbox
|
|
1300
1404
|
? `Shell tool available in Docker with mounted workspace /workspace from ${config.commandCwd}. Use relative paths or /workspace paths for outputs/writes; common host data roots are read-only at original absolute paths for inspection. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
@@ -2377,6 +2481,7 @@ export async function runAgent(config) {
|
|
|
2377
2481
|
command: redactSensitiveText(toolResult.args?.command || ""),
|
|
2378
2482
|
stdout: toolResult.stdout || "",
|
|
2379
2483
|
stderr: toolResult.stderr || "",
|
|
2484
|
+
diagnosticHint: toolResult.diagnosticHint || "",
|
|
2380
2485
|
commandPolicy: toolResult.commandPolicy,
|
|
2381
2486
|
blocked: Boolean(toolResult.blocked),
|
|
2382
2487
|
error: toolResult.error || toolResult.reason || "",
|
package/src/docker-sandbox.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fsSync, { constants as fsConstants } from "node:fs";
|
|
2
3
|
import fs from "node:fs/promises";
|
|
3
4
|
import os from "node:os";
|
|
@@ -63,6 +64,23 @@ async function execDocker(args, options = {}) {
|
|
|
63
64
|
signal: options.signal,
|
|
64
65
|
};
|
|
65
66
|
recordSandboxLog("docker.command", { args });
|
|
67
|
+
const containerName = options.containerName || "";
|
|
68
|
+
let abortHandler = null;
|
|
69
|
+
if (options.signal && containerName) {
|
|
70
|
+
abortHandler = () => {
|
|
71
|
+
void execFile("docker", ["kill", containerName], {
|
|
72
|
+
timeout: 8000,
|
|
73
|
+
maxBuffer: 32 * 1024,
|
|
74
|
+
}).catch((error) => {
|
|
75
|
+
recordSandboxLog("docker.kill.failed", {
|
|
76
|
+
containerName,
|
|
77
|
+
error: error instanceof Error ? error.message : String(error),
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
if (options.signal.aborted) abortHandler();
|
|
82
|
+
else options.signal.addEventListener("abort", abortHandler, { once: true });
|
|
83
|
+
}
|
|
66
84
|
|
|
67
85
|
try {
|
|
68
86
|
const result = await execFile("docker", args, execOptions);
|
|
@@ -81,6 +99,8 @@ async function execDocker(args, options = {}) {
|
|
|
81
99
|
stdout: redactSensitiveText(result.stdout),
|
|
82
100
|
stderr: redactSensitiveText(result.stderr),
|
|
83
101
|
};
|
|
102
|
+
} finally {
|
|
103
|
+
if (options.signal && abortHandler) options.signal.removeEventListener("abort", abortHandler);
|
|
84
104
|
}
|
|
85
105
|
}
|
|
86
106
|
|
|
@@ -282,7 +302,11 @@ function dockerCommand(command, policy) {
|
|
|
282
302
|
return [...envLines, dockerUserCommand(command, policy)].join("\n");
|
|
283
303
|
}
|
|
284
304
|
|
|
285
|
-
function
|
|
305
|
+
function dockerContainerName() {
|
|
306
|
+
return `aginti-${process.pid}-${Date.now()}-${crypto.randomBytes(4).toString("hex")}`.slice(0, 63);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command, config), persistentDirs = persistentDockerDirs(config), options = {}) {
|
|
286
310
|
const uid = typeof process.getuid === "function" ? String(process.getuid()) : "";
|
|
287
311
|
const gid = typeof process.getgid === "function" ? String(process.getgid()) : "";
|
|
288
312
|
const userArgs = uid && gid && !policy.requiresDockerRoot ? ["--user", `${uid}:${gid}`] : [];
|
|
@@ -299,6 +323,7 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
299
323
|
return [
|
|
300
324
|
"run",
|
|
301
325
|
"--rm",
|
|
326
|
+
...(options.containerName ? ["--name", options.containerName] : []),
|
|
302
327
|
"--network",
|
|
303
328
|
networkMode,
|
|
304
329
|
"--cap-drop",
|
|
@@ -337,10 +362,12 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
337
362
|
|
|
338
363
|
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config), options = {}) {
|
|
339
364
|
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
340
|
-
const
|
|
365
|
+
const containerName = dockerContainerName();
|
|
366
|
+
const result = await execDocker(dockerRunArgs(command, config, policy, persistentDirs, { containerName }), {
|
|
341
367
|
timeout: dockerExecTimeoutMs(policy),
|
|
342
368
|
maxBuffer: 300 * 1024,
|
|
343
369
|
signal: options.signal,
|
|
370
|
+
containerName,
|
|
344
371
|
});
|
|
345
372
|
|
|
346
373
|
const payload = {
|
package/src/interactive-cli.js
CHANGED
|
@@ -720,6 +720,9 @@ function printCommandOutputLog(data = {}) {
|
|
|
720
720
|
if (advice.suggestedCommand) outputLine(`${color(" | ", ansi.red)} rerun: ${compactLine(advice.suggestedCommand, 120)}`);
|
|
721
721
|
if (advice.trustedHostCommand) outputLine(`${color(" | ", ansi.red)} host: ${compactLine(advice.trustedHostCommand, 120)}`);
|
|
722
722
|
}
|
|
723
|
+
if (data.diagnosticHint) {
|
|
724
|
+
outputLine(`${label("hint", ansi.yellow)} ${compactLine(data.diagnosticHint, 112)}`);
|
|
725
|
+
}
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
function printHeading(text) {
|
|
@@ -1675,7 +1678,8 @@ class LiveRunInput {
|
|
|
1675
1678
|
|
|
1676
1679
|
handleKey(str = "", key = {}) {
|
|
1677
1680
|
if (key.ctrl && key.name === "c") {
|
|
1678
|
-
this.
|
|
1681
|
+
this.setStatus("stopping · ctrl-c");
|
|
1682
|
+
this.controller.abort(createAbortError("Interrupted by ctrl-c."));
|
|
1679
1683
|
return;
|
|
1680
1684
|
}
|
|
1681
1685
|
if (key.name === "escape") {
|
|
@@ -1686,7 +1690,8 @@ class LiveRunInput {
|
|
|
1686
1690
|
this.redraw();
|
|
1687
1691
|
return;
|
|
1688
1692
|
}
|
|
1689
|
-
this.
|
|
1693
|
+
this.setStatus("stopping · escape");
|
|
1694
|
+
this.controller.abort(createAbortError("Interrupted by escape."));
|
|
1690
1695
|
return;
|
|
1691
1696
|
}
|
|
1692
1697
|
if (key.meta && key.name === "up") {
|
|
@@ -1784,7 +1789,9 @@ function printStatus(state) {
|
|
|
1784
1789
|
printSystemLine(`project=${process.cwd()}`);
|
|
1785
1790
|
printSystemLine(`cwd=${state.commandCwd || process.cwd()}`);
|
|
1786
1791
|
printSystemLine(`session=${state.sessionId || "new"}`);
|
|
1787
|
-
|
|
1792
|
+
const progress =
|
|
1793
|
+
state.currentStep && state.currentMaxSteps ? ` progress=${state.currentStep}/${state.currentMaxSteps}` : state.currentStep ? ` progress=${state.currentStep}` : "";
|
|
1794
|
+
printSystemLine(`status=${state.status || "idle"}${progress}${state.activeGoal ? ` workingOn=${state.activeGoal}` : ""}`);
|
|
1788
1795
|
printSystemLine(`language=${state.language || cliLanguage} (${languageLabel(state.language || cliLanguage)})`);
|
|
1789
1796
|
if (state.lastEvent) printSystemLine(`last=${state.lastEvent}`);
|
|
1790
1797
|
printSystemLine(`provider=${state.provider || "auto"} routing=${state.routingMode} model=${state.model || "auto"}`);
|
|
@@ -1954,9 +1961,26 @@ async function printResumeHistory(state, { limit = 0 } = {}) {
|
|
|
1954
1961
|
for (const entry of shown) printHistoryEntry(entry);
|
|
1955
1962
|
}
|
|
1956
1963
|
|
|
1957
|
-
function
|
|
1964
|
+
function toolStatusDetails(data = {}) {
|
|
1965
|
+
const tool = data.toolName || "unknown";
|
|
1966
|
+
const args = data.args || {};
|
|
1967
|
+
if (tool === "run_command" && args.command) return `${tool}: ${compactLine(args.command, 58)}`;
|
|
1968
|
+
if ((tool === "write_file" || tool === "apply_patch" || tool === "read_file" || tool === "open_workspace_file") && args.path) {
|
|
1969
|
+
return `${tool}: ${compactLine(args.path, 58)}`;
|
|
1970
|
+
}
|
|
1971
|
+
if ((tool === "open_url" || tool === "web_research" || tool === "web_search") && (args.url || args.query || args.q)) {
|
|
1972
|
+
return `${tool}: ${compactLine(args.url || args.query || args.q, 58)}`;
|
|
1973
|
+
}
|
|
1974
|
+
return tool;
|
|
1975
|
+
}
|
|
1976
|
+
|
|
1977
|
+
function printStatusEvent(state, label, details = "", meta = {}) {
|
|
1958
1978
|
const safeDetails = compactLine(details, 72);
|
|
1959
|
-
state.
|
|
1979
|
+
if (meta.step) state.currentStep = meta.step;
|
|
1980
|
+
if (meta.maxSteps) state.currentMaxSteps = meta.maxSteps;
|
|
1981
|
+
const progress =
|
|
1982
|
+
state.currentStep && state.currentMaxSteps ? `step ${state.currentStep}/${state.currentMaxSteps} · ` : state.currentStep ? `step ${state.currentStep} · ` : "";
|
|
1983
|
+
state.lastEvent = `${progress}${safeDetails ? `${label}: ${safeDetails}` : label}`;
|
|
1960
1984
|
const statusText = `${state.status || "running"} · ${state.lastEvent}`;
|
|
1961
1985
|
if (activeRunInput) {
|
|
1962
1986
|
activeRunInput.setStatus(statusText);
|
|
@@ -1979,7 +2003,7 @@ function attachRunInterrupts(controller) {
|
|
|
1979
2003
|
if (controller.signal.aborted) return;
|
|
1980
2004
|
const reason = isEscape ? "escape" : "ctrl-c";
|
|
1981
2005
|
printSystemLine(`status=stopping reason=${reason}`);
|
|
1982
|
-
controller.abort(
|
|
2006
|
+
controller.abort(createAbortError(`Interrupted by ${reason}.`));
|
|
1983
2007
|
};
|
|
1984
2008
|
input.on("keypress", handler);
|
|
1985
2009
|
return () => {
|
|
@@ -3613,12 +3637,18 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3613
3637
|
onEvent: (type, data = {}) => {
|
|
3614
3638
|
if (type === "plan.created") {
|
|
3615
3639
|
printStatusEvent(state, "planned");
|
|
3640
|
+
} else if (type === "budget.initialized") {
|
|
3641
|
+
state.currentMaxSteps = data.currentMaxSteps || data.initialMaxSteps || state.maxSteps;
|
|
3642
|
+
printStatusEvent(state, "budget", `${state.currentMaxSteps} steps`);
|
|
3616
3643
|
} else if (type === "model.requested") {
|
|
3617
|
-
printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}
|
|
3644
|
+
printStatusEvent(state, "model_wait", `${data.provider || "model"}/${data.model || ""}`, {
|
|
3645
|
+
step: data.step,
|
|
3646
|
+
maxSteps: state.currentMaxSteps,
|
|
3647
|
+
});
|
|
3618
3648
|
} else if (type === "tool.started") {
|
|
3619
|
-
printStatusEvent(state, "tool", data
|
|
3620
|
-
} else if (type === "tool.completed") {
|
|
3621
|
-
printStatusEvent(state, "
|
|
3649
|
+
printStatusEvent(state, "tool", toolStatusDetails(data));
|
|
3650
|
+
} else if (type === "tool.completed" || type === "tool.failed") {
|
|
3651
|
+
printStatusEvent(state, type === "tool.failed" || data.ok === false ? "tool_failed" : "tool_done", toolStatusDetails(data));
|
|
3622
3652
|
} else if (type === "file.changed") {
|
|
3623
3653
|
printWorkspaceChange(data);
|
|
3624
3654
|
} else if (type === "tool.blocked") {
|
|
@@ -3643,7 +3673,10 @@ async function runPrompt(prompt, state, packageDir, { approvalDepth = 0 } = {})
|
|
|
3643
3673
|
} else if (type === "session.stopped") {
|
|
3644
3674
|
printStatusEvent(state, "stopped", data.reason || "");
|
|
3645
3675
|
} else if (type === "model.responded") {
|
|
3646
|
-
printStatusEvent(state, "model_responded", data.content ? data.content.slice(0, 80).replace(/\s+/g, " ") : ""
|
|
3676
|
+
printStatusEvent(state, "model_responded", data.content ? data.content.slice(0, 80).replace(/\s+/g, " ") : "", {
|
|
3677
|
+
step: data.step,
|
|
3678
|
+
maxSteps: state.currentMaxSteps,
|
|
3679
|
+
});
|
|
3647
3680
|
}
|
|
3648
3681
|
},
|
|
3649
3682
|
});
|