@lazyingart/agintiflow 0.8.4 → 0.8.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/docs/agent-runtime-pipe.md +4 -0
- package/package.json +1 -1
- package/public/app.js +55 -1
- package/public/index.html +4 -1
- package/public/styles.css +9 -0
- package/src/agent-runner.js +280 -25
- package/src/docker-sandbox.js +3 -1
- package/src/guardrails.js +21 -0
- package/src/interactive-cli.js +221 -75
- package/src/model-client.js +86 -16
- package/src/static-preview-server.js +105 -0
- package/src/workspace-tools.js +1 -1
- package/web.js +27 -0
package/src/agent-runner.js
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
|
-
import
|
|
3
|
+
import net from "node:net";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { exec as execCallback, spawn } from "node:child_process";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
7
|
import { promisify } from "node:util";
|
|
5
8
|
import { chromium } from "playwright";
|
|
6
9
|
import { createClient, createPlan, requestNextStep } from "./model-client.js";
|
|
@@ -11,13 +14,132 @@ import { ensureDockerSandboxReady, runDockerSandboxCommand } from "./docker-sand
|
|
|
11
14
|
import { normalizeWrapperName, runAgentWrapper, wrapperStatusText } from "./tool-wrappers.js";
|
|
12
15
|
import { evaluateCommandPolicy } from "./command-policy.js";
|
|
13
16
|
import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
14
|
-
import { executeWorkspaceTool, summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
|
|
17
|
+
import { executeWorkspaceTool, resolveWorkspacePath, summarizeWorkspaceTools, WORKSPACE_TOOL_NAMES } from "./workspace-tools.js";
|
|
15
18
|
import { normalizeCanvasPayload } from "./artifact-tunnel.js";
|
|
16
19
|
import { getTaskProfile } from "./task-profiles.js";
|
|
17
20
|
|
|
18
21
|
const exec = promisify(execCallback);
|
|
19
|
-
const BROWSER_TOOLS = new Set(["open_url", "click", "type", "scroll", "press", "back"]);
|
|
22
|
+
const BROWSER_TOOLS = new Set(["open_url", "open_workspace_file", "preview_workspace", "click", "type", "scroll", "press", "back"]);
|
|
20
23
|
const WORKSPACE_TOOLS = new Set(WORKSPACE_TOOL_NAMES);
|
|
24
|
+
const STATIC_PREVIEW_SERVER_PATH = fileURLToPath(new URL("./static-preview-server.js", import.meta.url));
|
|
25
|
+
const previewServers = new Map();
|
|
26
|
+
|
|
27
|
+
function sleep(ms) {
|
|
28
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function throwIfAborted(config) {
|
|
32
|
+
if (config.abortSignal?.aborted) {
|
|
33
|
+
const reason = config.abortSignal.reason;
|
|
34
|
+
const error = reason instanceof Error ? reason : new Error("Run interrupted by user.");
|
|
35
|
+
error.name = error.name || "AbortError";
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isAbortError(error, config = {}) {
|
|
41
|
+
return Boolean(
|
|
42
|
+
config.abortSignal?.aborted ||
|
|
43
|
+
error?.name === "AbortError" ||
|
|
44
|
+
error?.code === "ABORT_ERR" ||
|
|
45
|
+
/aborted|interrupted/i.test(String(error?.message || ""))
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function abortable(promise, signal) {
|
|
50
|
+
if (!signal) return promise;
|
|
51
|
+
if (signal.aborted) {
|
|
52
|
+
return Promise.reject(signal.reason instanceof Error ? signal.reason : new Error("Run interrupted by user."));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return new Promise((resolve, reject) => {
|
|
56
|
+
const onAbort = () => reject(signal.reason instanceof Error ? signal.reason : new Error("Run interrupted by user."));
|
|
57
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
58
|
+
promise.then(
|
|
59
|
+
(value) => {
|
|
60
|
+
signal.removeEventListener("abort", onAbort);
|
|
61
|
+
resolve(value);
|
|
62
|
+
},
|
|
63
|
+
(error) => {
|
|
64
|
+
signal.removeEventListener("abort", onAbort);
|
|
65
|
+
reject(error);
|
|
66
|
+
}
|
|
67
|
+
);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function isPortAvailable(port) {
|
|
72
|
+
return new Promise((resolve) => {
|
|
73
|
+
const server = net.createServer();
|
|
74
|
+
server.once("error", () => resolve(false));
|
|
75
|
+
server.once("listening", () => {
|
|
76
|
+
server.close(() => resolve(true));
|
|
77
|
+
});
|
|
78
|
+
server.listen(port, "127.0.0.1");
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function findAvailablePort(preferredPort = 8765) {
|
|
83
|
+
const preferred = Number(preferredPort);
|
|
84
|
+
const start = Number.isFinite(preferred) && preferred > 0 ? preferred : 8765;
|
|
85
|
+
for (let port = start; port < start + 80; port += 1) {
|
|
86
|
+
if (await isPortAvailable(port)) return port;
|
|
87
|
+
}
|
|
88
|
+
throw new Error(`No available preview port found near ${start}.`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function waitForPort(port, signal) {
|
|
92
|
+
for (let attempt = 0; attempt < 30; attempt += 1) {
|
|
93
|
+
if (signal?.aborted) throw signal.reason instanceof Error ? signal.reason : new Error("Preview interrupted.");
|
|
94
|
+
const connected = await new Promise((resolve) => {
|
|
95
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
96
|
+
socket.once("connect", () => {
|
|
97
|
+
socket.destroy();
|
|
98
|
+
resolve(true);
|
|
99
|
+
});
|
|
100
|
+
socket.once("error", () => {
|
|
101
|
+
socket.destroy();
|
|
102
|
+
resolve(false);
|
|
103
|
+
});
|
|
104
|
+
socket.setTimeout(500, () => {
|
|
105
|
+
socket.destroy();
|
|
106
|
+
resolve(false);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
if (connected) return;
|
|
110
|
+
await sleep(100);
|
|
111
|
+
}
|
|
112
|
+
throw new Error(`Preview server did not become ready on port ${port}.`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function startPreviewServer(root, preferredPort, signal) {
|
|
116
|
+
const key = path.resolve(root);
|
|
117
|
+
const existing = previewServers.get(key);
|
|
118
|
+
if (existing && existing.child.exitCode === null) {
|
|
119
|
+
return existing;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const port = await findAvailablePort(preferredPort);
|
|
123
|
+
const child = spawn(process.execPath, [STATIC_PREVIEW_SERVER_PATH, key, String(port)], {
|
|
124
|
+
detached: true,
|
|
125
|
+
stdio: "ignore",
|
|
126
|
+
});
|
|
127
|
+
child.unref();
|
|
128
|
+
const server = { root: key, port, child, url: `http://127.0.0.1:${port}/` };
|
|
129
|
+
previewServers.set(key, server);
|
|
130
|
+
await waitForPort(port, signal);
|
|
131
|
+
return server;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeUrlPath(relativePath) {
|
|
135
|
+
const normalized = String(relativePath || ".").replace(/\\/g, "/").replace(/^\/+/, "");
|
|
136
|
+
if (!normalized || normalized === ".") return "";
|
|
137
|
+
return normalized
|
|
138
|
+
.split("/")
|
|
139
|
+
.filter(Boolean)
|
|
140
|
+
.map((part) => encodeURIComponent(part))
|
|
141
|
+
.join("/");
|
|
142
|
+
}
|
|
21
143
|
|
|
22
144
|
function preserveAssistantMessage(message) {
|
|
23
145
|
const preserved = {
|
|
@@ -148,7 +270,7 @@ function createInitialState(config, sessionId) {
|
|
|
148
270
|
: "A host shell command tool is available under the configured trust policy."
|
|
149
271
|
: "No shell command tool is available.",
|
|
150
272
|
config.allowFileTools
|
|
151
|
-
? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, and
|
|
273
|
+
? `Workspace file tools are available in ${config.commandCwd}: list_files, read_file, search_files, write_file, apply_patch, open_workspace_file, and preview_workspace. Always use workspace-relative paths such as plot_fx.svg or docs/report.tex, never absolute host paths. Secret paths, .git internals, node_modules writes, and huge files are blocked. For generated local websites/pages, use open_workspace_file or preview_workspace instead of starting a localhost server inside Docker.`
|
|
152
274
|
: "No workspace file tools are available.",
|
|
153
275
|
config.allowWrapperTools
|
|
154
276
|
? `External coding-agent wrappers are available as advisory tools only. Use the selected wrapper only: ${normalizeWrapperName(config.preferredWrapper)}. Wrapper status: ${wrapperStatusText()}.`
|
|
@@ -159,6 +281,7 @@ function createInitialState(config, sessionId) {
|
|
|
159
281
|
"Work like a practical coding agent: inspect when useful, edit with file tools, run safe checks when they add confidence, and keep outputs inside the workspace.",
|
|
160
282
|
"Use the canvas tunnel for outputs the user would likely want to inspect visually, such as figures, PDFs, screenshots, images, important markdown, or generated files.",
|
|
161
283
|
"For environment or system-maintenance work, use the configured sandbox and package policy; Docker workspace mode is the preferred place for installs and toolchain setup.",
|
|
284
|
+
"If the user asks to open a generated local website or file, use open_workspace_file for a file or preview_workspace for a static site. Do not keep retrying the same localhost URL when a preview fails.",
|
|
162
285
|
"Docker language/toolchain installs should prefer /aginti-env or project files so they persist across runs; apt/apk changes are ephemeral unless the image is rebuilt.",
|
|
163
286
|
"If the run is close to the max-step limit, finish with the best complete artifact and honest limitations instead of starting a new approach.",
|
|
164
287
|
"When the requested outcome is complete and a useful check has passed or been honestly skipped, stop and call finish.",
|
|
@@ -176,7 +299,9 @@ function createInitialState(config, sessionId) {
|
|
|
176
299
|
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths, not absolute host temp paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
177
300
|
: `Shell working directory: ${config.commandCwd}`
|
|
178
301
|
: "",
|
|
179
|
-
config.allowFileTools
|
|
302
|
+
config.allowFileTools
|
|
303
|
+
? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. Local preview tools available: open_workspace_file and preview_workspace.`
|
|
304
|
+
: "",
|
|
180
305
|
config.allowWrapperTools
|
|
181
306
|
? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
182
307
|
: "",
|
|
@@ -204,6 +329,16 @@ function createObservers(config) {
|
|
|
204
329
|
};
|
|
205
330
|
}
|
|
206
331
|
|
|
332
|
+
function emitConsole(config, value = "", options = {}) {
|
|
333
|
+
if (typeof config.onConsole === "function") {
|
|
334
|
+
config.onConsole(String(value), options);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (options.error) console.error(value);
|
|
339
|
+
else console.log(value);
|
|
340
|
+
}
|
|
341
|
+
|
|
207
342
|
function createBrowserState() {
|
|
208
343
|
return {
|
|
209
344
|
browser: null,
|
|
@@ -278,7 +413,9 @@ function applyContinuationPrompt(state, config, observers) {
|
|
|
278
413
|
? `Shell working directory mounted into Docker as /workspace from ${config.commandCwd}. Use relative paths or /workspace paths. Persistent Docker env: /aginti-env, caches: /aginti-cache. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
279
414
|
: `Shell working directory: ${config.commandCwd}`
|
|
280
415
|
: "",
|
|
281
|
-
config.allowFileTools
|
|
416
|
+
config.allowFileTools
|
|
417
|
+
? `Workspace file tools enabled in: ${config.commandCwd}. Use workspace-relative paths. For generated local files/sites, use open_workspace_file or preview_workspace.`
|
|
418
|
+
: "",
|
|
282
419
|
config.allowWrapperTools
|
|
283
420
|
? `Agent wrappers: selected=${normalizeWrapperName(config.preferredWrapper)}; ${wrapperStatusText()}`
|
|
284
421
|
: "",
|
|
@@ -374,16 +511,68 @@ function sanitizeToolResult(result) {
|
|
|
374
511
|
return safeResult;
|
|
375
512
|
}
|
|
376
513
|
|
|
514
|
+
function stableStringify(value) {
|
|
515
|
+
if (Array.isArray(value)) return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
516
|
+
if (value && typeof value === "object") {
|
|
517
|
+
return `{${Object.keys(value)
|
|
518
|
+
.sort()
|
|
519
|
+
.map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`)
|
|
520
|
+
.join(",")}}`;
|
|
521
|
+
}
|
|
522
|
+
return JSON.stringify(value);
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function applyToolLoopGuard(state, toolResult, store, observers) {
|
|
526
|
+
if (!toolResult || toolResult.done || toolResult.ok !== false) return;
|
|
527
|
+
state.meta.toolLoop = state.meta.toolLoop || { recent: [], warned: [] };
|
|
528
|
+
const signature = `${toolResult.toolName}:${stableStringify(toolResult.args || {})}`;
|
|
529
|
+
const entry = {
|
|
530
|
+
signature,
|
|
531
|
+
toolName: toolResult.toolName,
|
|
532
|
+
ok: Boolean(toolResult.ok),
|
|
533
|
+
blocked: Boolean(toolResult.blocked),
|
|
534
|
+
error: toolResult.error || toolResult.reason || "",
|
|
535
|
+
at: new Date().toISOString(),
|
|
536
|
+
};
|
|
537
|
+
state.meta.toolLoop.recent.push(entry);
|
|
538
|
+
state.meta.toolLoop.recent = state.meta.toolLoop.recent.slice(-20);
|
|
539
|
+
|
|
540
|
+
const failures = state.meta.toolLoop.recent.filter((item) => item.signature === signature && item.ok === false).length;
|
|
541
|
+
if (failures < 2 || state.meta.toolLoop.warned.includes(signature)) return;
|
|
542
|
+
|
|
543
|
+
state.meta.toolLoop.warned.push(signature);
|
|
544
|
+
state.meta.toolLoop.warned = state.meta.toolLoop.warned.slice(-20);
|
|
545
|
+
const message = [
|
|
546
|
+
`Loop guard: ${toolResult.toolName} with the same arguments has failed or been blocked ${failures} times.`,
|
|
547
|
+
"Do not repeat that exact call.",
|
|
548
|
+
"If this is a local workspace preview, use open_workspace_file or preview_workspace instead of repeatedly starting localhost servers or opening the same URL.",
|
|
549
|
+
"If enough work is complete, call finish with the usable local path or preview URL.",
|
|
550
|
+
].join(" ");
|
|
551
|
+
state.messages.push({ role: "user", content: message });
|
|
552
|
+
await store.appendEvent("loop.guard", {
|
|
553
|
+
toolName: toolResult.toolName,
|
|
554
|
+
failures,
|
|
555
|
+
message,
|
|
556
|
+
});
|
|
557
|
+
observers.event("loop.guard", {
|
|
558
|
+
toolName: toolResult.toolName,
|
|
559
|
+
failures,
|
|
560
|
+
message,
|
|
561
|
+
});
|
|
562
|
+
}
|
|
563
|
+
|
|
377
564
|
async function runShellCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
378
565
|
try {
|
|
566
|
+
throwIfAborted(config);
|
|
379
567
|
const result = config.useDockerSandbox
|
|
380
|
-
? await runDockerSandboxCommand(command, config, policy)
|
|
568
|
+
? await runDockerSandboxCommand(command, config, policy, { signal: config.abortSignal })
|
|
381
569
|
: await exec(command, {
|
|
382
570
|
cwd: config.commandCwd,
|
|
383
571
|
timeout: 30000,
|
|
384
572
|
maxBuffer: 200 * 1024,
|
|
385
573
|
shell: "/bin/bash",
|
|
386
574
|
env: safeExecutionEnv(),
|
|
575
|
+
signal: config.abortSignal,
|
|
387
576
|
});
|
|
388
577
|
|
|
389
578
|
return {
|
|
@@ -393,6 +582,7 @@ async function runShellCommand(command, config, policy = evaluateCommandPolicy(c
|
|
|
393
582
|
stderr: redactSensitiveText(result.stderr).trim().slice(0, 4000),
|
|
394
583
|
};
|
|
395
584
|
} catch (error) {
|
|
585
|
+
if (isAbortError(error, config)) throw error;
|
|
396
586
|
return {
|
|
397
587
|
ok: false,
|
|
398
588
|
exitCode: Number.isInteger(error?.code) ? error.code : 1,
|
|
@@ -424,6 +614,7 @@ async function captureSyntheticSnapshot(store, step, config) {
|
|
|
424
614
|
"For draw/plot/graph/chart/diagram/figure requests, publish a canvas artifact proactively.",
|
|
425
615
|
"For LaTeX/PDF requests, publish the source and compiled PDF artifacts when available. Keep subfolder outputs beside their source and use pdflatex-compatible figure formats.",
|
|
426
616
|
"Use open_url only if the task actually needs the web.",
|
|
617
|
+
"For generated local HTML/SVG/PDF/site output, use open_workspace_file or preview_workspace instead of shelling a transient local server.",
|
|
427
618
|
]
|
|
428
619
|
.filter(Boolean)
|
|
429
620
|
.join(" "),
|
|
@@ -469,6 +660,7 @@ async function injectQueuedUserMessages(store, state, observers) {
|
|
|
469
660
|
}
|
|
470
661
|
|
|
471
662
|
async function executeTool(browserState, toolCall, snapshot, config, store, observers, state) {
|
|
663
|
+
throwIfAborted(config);
|
|
472
664
|
const args = JSON.parse(toolCall.function.arguments || "{}");
|
|
473
665
|
const safeArgs = sanitizeToolArgs(toolCall.function.name, args);
|
|
474
666
|
const guard = checkToolUse({
|
|
@@ -520,8 +712,45 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
520
712
|
|
|
521
713
|
switch (toolCall.function.name) {
|
|
522
714
|
case "open_url":
|
|
523
|
-
await browserState.page.goto(String(args.url), { waitUntil: "domcontentloaded" });
|
|
715
|
+
await abortable(browserState.page.goto(String(args.url), { waitUntil: "domcontentloaded" }), config.abortSignal);
|
|
524
716
|
break;
|
|
717
|
+
case "open_workspace_file": {
|
|
718
|
+
const target = resolveWorkspacePath(config, args.path || args.file || ".");
|
|
719
|
+
const stat = await fs.stat(target.absolutePath);
|
|
720
|
+
if (!stat.isFile()) throw new Error(`Workspace preview target is not a file: ${target.relativePath}`);
|
|
721
|
+
const fileUrl = pathToFileURL(target.absolutePath).href;
|
|
722
|
+
await abortable(browserState.page.goto(fileUrl, { waitUntil: "domcontentloaded" }), config.abortSignal);
|
|
723
|
+
const result = {
|
|
724
|
+
ok: true,
|
|
725
|
+
toolName: "open_workspace_file",
|
|
726
|
+
args: safeArgs,
|
|
727
|
+
path: target.relativePath,
|
|
728
|
+
url: browserState.page.url(),
|
|
729
|
+
};
|
|
730
|
+
await store.appendEvent("tool.completed", result);
|
|
731
|
+
observers.event("tool.completed", result);
|
|
732
|
+
return result;
|
|
733
|
+
}
|
|
734
|
+
case "preview_workspace": {
|
|
735
|
+
const target = resolveWorkspacePath(config, args.path || args.file || ".");
|
|
736
|
+
const stat = await fs.stat(target.absolutePath);
|
|
737
|
+
const server = await startPreviewServer(config.commandCwd, args.port || 8765, config.abortSignal);
|
|
738
|
+
const urlPath = stat.isDirectory() ? normalizeUrlPath(target.relativePath === "." ? "" : `${target.relativePath}/`) : normalizeUrlPath(target.relativePath);
|
|
739
|
+
const previewUrl = `${server.url}${urlPath}`;
|
|
740
|
+
await abortable(browserState.page.goto(previewUrl, { waitUntil: "domcontentloaded" }), config.abortSignal);
|
|
741
|
+
const result = {
|
|
742
|
+
ok: true,
|
|
743
|
+
toolName: "preview_workspace",
|
|
744
|
+
args: safeArgs,
|
|
745
|
+
path: target.relativePath,
|
|
746
|
+
url: browserState.page.url(),
|
|
747
|
+
port: server.port,
|
|
748
|
+
root: server.root,
|
|
749
|
+
};
|
|
750
|
+
await store.appendEvent("tool.completed", result);
|
|
751
|
+
observers.event("tool.completed", result);
|
|
752
|
+
return result;
|
|
753
|
+
}
|
|
525
754
|
case "click": {
|
|
526
755
|
const locator = browserState.page.locator(`[data-agent-id="${args.id}"]`).first();
|
|
527
756
|
await locator.scrollIntoViewIfNeeded();
|
|
@@ -714,6 +943,7 @@ async function executeTool(browserState, toolCall, snapshot, config, store, obse
|
|
|
714
943
|
observers.event("tool.completed", result);
|
|
715
944
|
return result;
|
|
716
945
|
} catch (error) {
|
|
946
|
+
if (isAbortError(error, config)) throw error;
|
|
717
947
|
const result = {
|
|
718
948
|
ok: false,
|
|
719
949
|
toolName: toolCall.function.name,
|
|
@@ -776,6 +1006,7 @@ export async function runAgent(config) {
|
|
|
776
1006
|
});
|
|
777
1007
|
|
|
778
1008
|
try {
|
|
1009
|
+
throwIfAborted(config);
|
|
779
1010
|
if (!state.plan) {
|
|
780
1011
|
const plan = await createPlan(client, config, state);
|
|
781
1012
|
state.plan = plan;
|
|
@@ -812,28 +1043,31 @@ export async function runAgent(config) {
|
|
|
812
1043
|
startUrl: config.startUrl,
|
|
813
1044
|
});
|
|
814
1045
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
1046
|
+
emitConsole(config, `Session: ${sessionId}`, { kind: "meta" });
|
|
1047
|
+
emitConsole(config, `Provider: ${config.provider}`, { kind: "meta" });
|
|
1048
|
+
emitConsole(config, `Model: ${config.model}`, { kind: "meta" });
|
|
1049
|
+
emitConsole(config, `Routing: ${config.routingMode} (${config.routeReason})`, { kind: "meta" });
|
|
1050
|
+
emitConsole(config, `Workspace: ${config.commandCwd}`, { kind: "meta" });
|
|
1051
|
+
emitConsole(config, `Sessions: ${config.sessionsDir}`, { kind: "meta" });
|
|
821
1052
|
if (config.useDockerSandbox) {
|
|
822
|
-
|
|
823
|
-
|
|
1053
|
+
emitConsole(
|
|
1054
|
+
config,
|
|
1055
|
+
`Docker: image=${config.dockerSandboxImage} mode=${config.sandboxMode} packagePolicy=${config.packageInstallPolicy}`,
|
|
1056
|
+
{ kind: "meta" }
|
|
824
1057
|
);
|
|
825
|
-
|
|
826
|
-
|
|
1058
|
+
emitConsole(config, `Docker workspace: /workspace -> ${config.commandCwd}`, { kind: "meta" });
|
|
1059
|
+
emitConsole(config, "Docker env: /aginti-env persistent toolchain; /aginti-cache persistent caches", { kind: "meta" });
|
|
827
1060
|
} else if (config.allowShellTool) {
|
|
828
|
-
|
|
1061
|
+
emitConsole(config, `Shell: host policy=${config.packageInstallPolicy}`, { kind: "meta" });
|
|
829
1062
|
}
|
|
830
1063
|
if (state.plan) {
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
1064
|
+
emitConsole(config, "\nPlan:", { kind: "heading" });
|
|
1065
|
+
emitConsole(config, state.plan, { kind: "plan", markdown: true });
|
|
1066
|
+
emitConsole(config, "", { kind: "meta" });
|
|
834
1067
|
}
|
|
835
1068
|
|
|
836
1069
|
for (let step = state.stepsCompleted + 1; step <= config.maxSteps; step += 1) {
|
|
1070
|
+
throwIfAborted(config);
|
|
837
1071
|
await injectQueuedUserMessages(store, state, observers);
|
|
838
1072
|
const snapshot = await buildSnapshot(browserState, store, step, config);
|
|
839
1073
|
state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
|
|
@@ -881,6 +1115,7 @@ export async function runAgent(config) {
|
|
|
881
1115
|
})}`,
|
|
882
1116
|
});
|
|
883
1117
|
|
|
1118
|
+
throwIfAborted(config);
|
|
884
1119
|
const response = await requestNextStep(client, config, state.messages);
|
|
885
1120
|
const assistantMessage = response.choices[0]?.message;
|
|
886
1121
|
if (!assistantMessage) {
|
|
@@ -916,7 +1151,7 @@ export async function runAgent(config) {
|
|
|
916
1151
|
result: fallback,
|
|
917
1152
|
sessionId,
|
|
918
1153
|
});
|
|
919
|
-
|
|
1154
|
+
emitConsole(config, fallback, { kind: "assistant", markdown: true });
|
|
920
1155
|
state.stepsCompleted = step;
|
|
921
1156
|
state.updatedAt = new Date().toISOString();
|
|
922
1157
|
await store.saveState(state);
|
|
@@ -927,12 +1162,14 @@ export async function runAgent(config) {
|
|
|
927
1162
|
}
|
|
928
1163
|
|
|
929
1164
|
for (const toolCall of toolCalls) {
|
|
1165
|
+
throwIfAborted(config);
|
|
930
1166
|
const toolResult = await executeTool(browserState, toolCall, snapshot, config, store, observers, state);
|
|
931
1167
|
state.messages.push({
|
|
932
1168
|
role: "tool",
|
|
933
1169
|
tool_call_id: toolCall.id,
|
|
934
1170
|
content: JSON.stringify(toolResult),
|
|
935
1171
|
});
|
|
1172
|
+
await applyToolLoopGuard(state, toolResult, store, observers);
|
|
936
1173
|
|
|
937
1174
|
if (toolResult.toolName === "run_command") {
|
|
938
1175
|
observers.log("command.output", {
|
|
@@ -984,7 +1221,7 @@ export async function runAgent(config) {
|
|
|
984
1221
|
result: toolResult.result,
|
|
985
1222
|
sessionId,
|
|
986
1223
|
});
|
|
987
|
-
|
|
1224
|
+
emitConsole(config, toolResult.result, { kind: "assistant", markdown: true });
|
|
988
1225
|
return {
|
|
989
1226
|
sessionId,
|
|
990
1227
|
result: toolResult.result,
|
|
@@ -1007,13 +1244,31 @@ export async function runAgent(config) {
|
|
|
1007
1244
|
reason: "max_steps_reached",
|
|
1008
1245
|
sessionId,
|
|
1009
1246
|
});
|
|
1010
|
-
|
|
1247
|
+
emitConsole(config, `Stopped after ${config.maxSteps} steps without finish().`, { kind: "error", error: true });
|
|
1011
1248
|
return {
|
|
1012
1249
|
sessionId,
|
|
1013
1250
|
result: "",
|
|
1014
1251
|
stopped: true,
|
|
1015
1252
|
reason: "max_steps_reached",
|
|
1016
1253
|
};
|
|
1254
|
+
} catch (error) {
|
|
1255
|
+
if (!isAbortError(error, config)) throw error;
|
|
1256
|
+
state.stepsCompleted = state.stepsCompleted || 0;
|
|
1257
|
+
state.updatedAt = new Date().toISOString();
|
|
1258
|
+
await store.saveState(state).catch(() => {});
|
|
1259
|
+
await store.appendEvent("session.stopped", {
|
|
1260
|
+
reason: "user_interrupt",
|
|
1261
|
+
});
|
|
1262
|
+
observers.event("session.stopped", {
|
|
1263
|
+
reason: "user_interrupt",
|
|
1264
|
+
sessionId,
|
|
1265
|
+
});
|
|
1266
|
+
return {
|
|
1267
|
+
sessionId,
|
|
1268
|
+
result: "",
|
|
1269
|
+
stopped: true,
|
|
1270
|
+
reason: "user_interrupt",
|
|
1271
|
+
};
|
|
1017
1272
|
} finally {
|
|
1018
1273
|
await closeBrowser(browserState, store);
|
|
1019
1274
|
}
|
package/src/docker-sandbox.js
CHANGED
|
@@ -37,6 +37,7 @@ async function execDocker(args, options = {}) {
|
|
|
37
37
|
const execOptions = {
|
|
38
38
|
timeout: options.timeout ?? 30000,
|
|
39
39
|
maxBuffer: options.maxBuffer ?? 200 * 1024,
|
|
40
|
+
signal: options.signal,
|
|
40
41
|
};
|
|
41
42
|
recordSandboxLog("docker.command", { args });
|
|
42
43
|
|
|
@@ -283,11 +284,12 @@ function dockerRunArgs(command, config, policy = evaluateCommandPolicy(command,
|
|
|
283
284
|
];
|
|
284
285
|
}
|
|
285
286
|
|
|
286
|
-
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
287
|
+
export async function runDockerSandboxCommand(command, config, policy = evaluateCommandPolicy(command, config), options = {}) {
|
|
287
288
|
const persistentDirs = await ensurePersistentDockerDirs(config);
|
|
288
289
|
const result = await execDocker(dockerRunArgs(command, config, policy, persistentDirs), {
|
|
289
290
|
timeout: policy.needsNetwork ? 120000 : policy.category === "toolchain" ? 90000 : 15000,
|
|
290
291
|
maxBuffer: 300 * 1024,
|
|
292
|
+
signal: options.signal,
|
|
291
293
|
});
|
|
292
294
|
|
|
293
295
|
const payload = {
|
package/src/guardrails.js
CHANGED
|
@@ -31,6 +31,12 @@ const DESTRUCTIVE_PROMPT_HINTS = [
|
|
|
31
31
|
"publish",
|
|
32
32
|
];
|
|
33
33
|
|
|
34
|
+
function isTransientDockerPreviewCommand(command) {
|
|
35
|
+
return /\bpython3?\s+-m\s+http\.server\b|\bnpx\s+(?:--yes\s+)?(?:serve|http-server)\b|\bnpm\s+exec\s+(?:serve|http-server)\b|\bphp\s+-S\s+127\.0\.0\.1:/i.test(
|
|
36
|
+
command
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
34
40
|
function normalizeDomain(hostname) {
|
|
35
41
|
return hostname.replace(/^www\./, "").toLowerCase();
|
|
36
42
|
}
|
|
@@ -66,6 +72,13 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
|
66
72
|
return { allowed: true };
|
|
67
73
|
}
|
|
68
74
|
|
|
75
|
+
if (toolName === "open_workspace_file" || toolName === "preview_workspace") {
|
|
76
|
+
if (!config.allowFileTools) {
|
|
77
|
+
return { allowed: false, reason: "Workspace preview tools require file tools to be enabled.", category: "workspace-tools" };
|
|
78
|
+
}
|
|
79
|
+
return checkWorkspaceToolUse("read_file", { path: args.path || args.file || "." }, config);
|
|
80
|
+
}
|
|
81
|
+
|
|
69
82
|
if (toolName === "click") {
|
|
70
83
|
const element = snapshot.elements.find((item) => item.id === String(args.id));
|
|
71
84
|
if (!element) return { allowed: false, reason: `Element ${args.id} is not in the latest snapshot.` };
|
|
@@ -96,6 +109,14 @@ export function checkToolUse({ toolName, args, snapshot, config }) {
|
|
|
96
109
|
|
|
97
110
|
if (toolName === "run_command") {
|
|
98
111
|
const command = String(args.command || "").trim();
|
|
112
|
+
if (config.useDockerSandbox && isTransientDockerPreviewCommand(command)) {
|
|
113
|
+
return {
|
|
114
|
+
allowed: false,
|
|
115
|
+
reason:
|
|
116
|
+
"Transient localhost preview servers inside Docker are not useful because command containers stop and ports are not published. Use preview_workspace/open_workspace_file, or switch to host mode for a persistent dev server.",
|
|
117
|
+
category: "preview-server",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
99
120
|
return evaluateCommandPolicy(command, config);
|
|
100
121
|
}
|
|
101
122
|
|