@lazyingart/agintiflow 0.1.0
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/AGENTS.md +41 -0
- package/LICENSE +201 -0
- package/README.md +294 -0
- package/bin/aginti-cli.js +7 -0
- package/docker/sandbox.Dockerfile +22 -0
- package/docs/npm-publishing.md +45 -0
- package/i18n/README.ar.md +51 -0
- package/i18n/README.de.md +51 -0
- package/i18n/README.es.md +51 -0
- package/i18n/README.fr.md +51 -0
- package/i18n/README.ja.md +51 -0
- package/i18n/README.ko.md +51 -0
- package/i18n/README.ru.md +51 -0
- package/i18n/README.vi.md +51 -0
- package/i18n/README.zh-Hans.md +51 -0
- package/i18n/README.zh-Hant.md +171 -0
- package/logos/banner-opaque.png +0 -0
- package/logos/logo.png +0 -0
- package/package.json +62 -0
- package/public/app.js +1060 -0
- package/public/index.html +198 -0
- package/public/styles.css +305 -0
- package/run.js +6 -0
- package/scripts/install-docker-ubuntu.sh +140 -0
- package/src/agent-runner.js +686 -0
- package/src/cli.js +181 -0
- package/src/command-policy.js +185 -0
- package/src/config.js +89 -0
- package/src/docker-sandbox.js +309 -0
- package/src/guardrails.js +122 -0
- package/src/model-client.js +212 -0
- package/src/model-routing.js +135 -0
- package/src/redaction.js +29 -0
- package/src/session-store.js +73 -0
- package/src/snapshot.js +55 -0
- package/src/tool-wrappers.js +193 -0
- package/src/web-db.js +158 -0
- package/web.js +530 -0
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import { exec as execCallback } from "node:child_process";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
import { chromium } from "playwright";
|
|
6
|
+
import { createClient, createPlan, requestNextStep } from "./model-client.js";
|
|
7
|
+
import { SessionStore } from "./session-store.js";
|
|
8
|
+
import { captureSnapshot } from "./snapshot.js";
|
|
9
|
+
import { checkToolUse } from "./guardrails.js";
|
|
10
|
+
import { ensureDockerSandboxReady, runDockerSandboxCommand } from "./docker-sandbox.js";
|
|
11
|
+
import { runAgentWrapper, wrapperStatusText } from "./tool-wrappers.js";
|
|
12
|
+
import { evaluateCommandPolicy } from "./command-policy.js";
|
|
13
|
+
import { redactSensitiveText, redactValue } from "./redaction.js";
|
|
14
|
+
|
|
15
|
+
const exec = promisify(execCallback);
|
|
16
|
+
const BROWSER_TOOLS = new Set(["open_url", "click", "type", "scroll", "press", "back"]);
|
|
17
|
+
|
|
18
|
+
function createInitialState(config, sessionId) {
|
|
19
|
+
const now = new Date().toISOString();
|
|
20
|
+
return {
|
|
21
|
+
sessionId,
|
|
22
|
+
createdAt: now,
|
|
23
|
+
updatedAt: now,
|
|
24
|
+
provider: config.provider,
|
|
25
|
+
model: config.model,
|
|
26
|
+
goal: config.goal,
|
|
27
|
+
startUrl: config.startUrl,
|
|
28
|
+
plan: "",
|
|
29
|
+
stepsCompleted: 0,
|
|
30
|
+
meta: {
|
|
31
|
+
lastUrl: "",
|
|
32
|
+
},
|
|
33
|
+
chat: [
|
|
34
|
+
{
|
|
35
|
+
role: "user",
|
|
36
|
+
content: config.goal,
|
|
37
|
+
at: now,
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
messages: [
|
|
41
|
+
{
|
|
42
|
+
role: "system",
|
|
43
|
+
content: [
|
|
44
|
+
"You are a careful browser and shell agent with a small tool surface.",
|
|
45
|
+
"Use only the provided tools.",
|
|
46
|
+
"If the shell tool can satisfy a local task, prefer it before opening a browser.",
|
|
47
|
+
"Do not open a browser page just because a start URL exists. Treat it as a suggestion only.",
|
|
48
|
+
"Only reference element ids from the latest browser snapshot.",
|
|
49
|
+
"Prefer short, deliberate actions over guessing.",
|
|
50
|
+
"Never navigate outside the allowed domains when an allowlist exists.",
|
|
51
|
+
"Avoid destructive actions, purchases, account changes, and sensitive workflows.",
|
|
52
|
+
config.allowShellTool
|
|
53
|
+
? config.useDockerSandbox
|
|
54
|
+
? `A shell command tool is available inside Docker sandbox mode ${config.sandboxMode}. Read-only commands have no network. Package installs or environment setup require approved package policy and Docker workspace-write mode.`
|
|
55
|
+
: "A read-only host shell command tool is available for short local inspection tasks."
|
|
56
|
+
: "No shell command tool is available.",
|
|
57
|
+
config.allowWrapperTools
|
|
58
|
+
? `External coding-agent wrappers are available as advisory tools only. Wrapper status: ${wrapperStatusText()}.`
|
|
59
|
+
: "External coding-agent wrappers are disabled.",
|
|
60
|
+
"When done, call finish with a concise result.",
|
|
61
|
+
].join(" "),
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
role: "user",
|
|
65
|
+
content: [
|
|
66
|
+
`Goal: ${config.goal}`,
|
|
67
|
+
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
68
|
+
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
69
|
+
config.allowShellTool
|
|
70
|
+
? config.useDockerSandbox
|
|
71
|
+
? `Shell working directory mounted into Docker: ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
72
|
+
: `Shell working directory: ${config.commandCwd}`
|
|
73
|
+
: "",
|
|
74
|
+
config.allowWrapperTools ? `Agent wrappers: ${wrapperStatusText()}` : "",
|
|
75
|
+
]
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.join("\n"),
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function createObservers(config) {
|
|
84
|
+
return {
|
|
85
|
+
log(message, data = {}) {
|
|
86
|
+
if (typeof config.onLog === "function") config.onLog(message, data);
|
|
87
|
+
},
|
|
88
|
+
event(type, data = {}) {
|
|
89
|
+
if (typeof config.onEvent === "function") config.onEvent(type, data);
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function createBrowserState() {
|
|
95
|
+
return {
|
|
96
|
+
browser: null,
|
|
97
|
+
context: null,
|
|
98
|
+
page: null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function ensureChatState(state) {
|
|
103
|
+
if (Array.isArray(state.chat)) return;
|
|
104
|
+
|
|
105
|
+
const chat = [];
|
|
106
|
+
if (state.goal) {
|
|
107
|
+
chat.push({
|
|
108
|
+
role: "user",
|
|
109
|
+
content: state.goal,
|
|
110
|
+
at: state.createdAt || new Date().toISOString(),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const finishTool = [...(state.messages || [])]
|
|
115
|
+
.reverse()
|
|
116
|
+
.find((message) => message.role === "tool" && typeof message.content === "string");
|
|
117
|
+
|
|
118
|
+
if (finishTool) {
|
|
119
|
+
try {
|
|
120
|
+
const parsed = JSON.parse(finishTool.content);
|
|
121
|
+
if (parsed.done && parsed.result) {
|
|
122
|
+
chat.push({
|
|
123
|
+
role: "assistant",
|
|
124
|
+
content: parsed.result,
|
|
125
|
+
at: state.updatedAt || new Date().toISOString(),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// Keep derived chat best-effort only.
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
state.chat = chat;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function appendChatEntry(state, role, content) {
|
|
137
|
+
ensureChatState(state);
|
|
138
|
+
state.chat.push({
|
|
139
|
+
role,
|
|
140
|
+
content,
|
|
141
|
+
at: new Date().toISOString(),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function applyContinuationPrompt(state, config, observers) {
|
|
146
|
+
if (!config.resume || !config.goal) return;
|
|
147
|
+
|
|
148
|
+
ensureChatState(state);
|
|
149
|
+
state.goal = config.goal;
|
|
150
|
+
state.provider = config.provider;
|
|
151
|
+
state.model = config.model;
|
|
152
|
+
state.startUrl = config.startUrl;
|
|
153
|
+
state.plan = "";
|
|
154
|
+
state.stepsCompleted = 0;
|
|
155
|
+
state.updatedAt = new Date().toISOString();
|
|
156
|
+
state.messages.push({
|
|
157
|
+
role: "user",
|
|
158
|
+
content: [
|
|
159
|
+
`Continue with this new request: ${config.goal}`,
|
|
160
|
+
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
161
|
+
config.allowedDomains.length > 0 ? `Allowed domains: ${config.allowedDomains.join(", ")}` : "",
|
|
162
|
+
config.allowShellTool
|
|
163
|
+
? config.useDockerSandbox
|
|
164
|
+
? `Shell working directory mounted into Docker: ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
165
|
+
: `Shell working directory: ${config.commandCwd}`
|
|
166
|
+
: "",
|
|
167
|
+
config.allowWrapperTools ? `Agent wrappers: ${wrapperStatusText()}` : "",
|
|
168
|
+
]
|
|
169
|
+
.filter(Boolean)
|
|
170
|
+
.join("\n"),
|
|
171
|
+
});
|
|
172
|
+
appendChatEntry(state, "user", config.goal);
|
|
173
|
+
observers.event("conversation.continued", {
|
|
174
|
+
sessionId: state.sessionId,
|
|
175
|
+
prompt: config.goal,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function saveBrowserState(browserState, store) {
|
|
180
|
+
if (browserState.context) {
|
|
181
|
+
await browserState.context.storageState({ path: store.storageStatePath });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function ensureBrowser(browserState, config, store, state, observers) {
|
|
186
|
+
if (browserState.page) return browserState;
|
|
187
|
+
|
|
188
|
+
observers.log("browser.starting", { headless: config.headless });
|
|
189
|
+
browserState.browser = await chromium.launch({ headless: config.headless });
|
|
190
|
+
browserState.context = await browserState.browser.newContext({
|
|
191
|
+
viewport: { width: 1440, height: 900 },
|
|
192
|
+
storageState: await fs
|
|
193
|
+
.access(store.storageStatePath)
|
|
194
|
+
.then(() => store.storageStatePath)
|
|
195
|
+
.catch(() => undefined),
|
|
196
|
+
});
|
|
197
|
+
browserState.page = await browserState.context.newPage();
|
|
198
|
+
|
|
199
|
+
if (state.meta.lastUrl) {
|
|
200
|
+
await browserState.page.goto(state.meta.lastUrl, { waitUntil: "domcontentloaded" }).catch(() => {});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return browserState;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function closeBrowser(browserState, store) {
|
|
207
|
+
await saveBrowserState(browserState, store).catch(() => {});
|
|
208
|
+
await browserState.context?.close().catch(() => {});
|
|
209
|
+
await browserState.browser?.close().catch(() => {});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function safeExecutionEnv() {
|
|
213
|
+
return {
|
|
214
|
+
PATH: process.env.PATH || "/usr/local/bin:/usr/bin:/bin",
|
|
215
|
+
HOME: process.env.HOME || "/tmp",
|
|
216
|
+
LANG: process.env.LANG || "C.UTF-8",
|
|
217
|
+
LC_ALL: process.env.LC_ALL || "C.UTF-8",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
async function runShellCommand(command, config, policy = evaluateCommandPolicy(command, config)) {
|
|
222
|
+
if (config.useDockerSandbox) {
|
|
223
|
+
return runDockerSandboxCommand(command, config, policy);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const result = await exec(command, {
|
|
227
|
+
cwd: config.commandCwd,
|
|
228
|
+
timeout: 5000,
|
|
229
|
+
maxBuffer: 200 * 1024,
|
|
230
|
+
shell: "/bin/bash",
|
|
231
|
+
env: safeExecutionEnv(),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
return {
|
|
235
|
+
stdout: redactSensitiveText(result.stdout).trim().slice(0, 8000),
|
|
236
|
+
stderr: redactSensitiveText(result.stderr).trim().slice(0, 4000),
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function captureSyntheticSnapshot(store, step, config) {
|
|
241
|
+
const snapshot = {
|
|
242
|
+
title: "No browser page open",
|
|
243
|
+
url: "",
|
|
244
|
+
pageText: [
|
|
245
|
+
"No browser page is currently open.",
|
|
246
|
+
config.startUrl ? `Suggested start URL: ${config.startUrl}` : "",
|
|
247
|
+
config.allowShellTool
|
|
248
|
+
? config.useDockerSandbox
|
|
249
|
+
? `Shell tool available in Docker with mounted workspace: ${config.commandCwd}. Sandbox mode: ${config.sandboxMode}. Package install policy: ${config.packageInstallPolicy}.`
|
|
250
|
+
: `Shell tool available in: ${config.commandCwd}`
|
|
251
|
+
: "Shell tool disabled.",
|
|
252
|
+
config.allowWrapperTools ? `Agent wrappers available: ${wrapperStatusText()}` : "Agent wrappers disabled.",
|
|
253
|
+
"Use open_url only if the task actually needs the web.",
|
|
254
|
+
]
|
|
255
|
+
.filter(Boolean)
|
|
256
|
+
.join(" "),
|
|
257
|
+
elements: [],
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
const snapshotPath = await store.saveSnapshot(step, snapshot);
|
|
261
|
+
return {
|
|
262
|
+
...snapshot,
|
|
263
|
+
screenshotPath: "",
|
|
264
|
+
snapshotPath,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function buildSnapshot(browserState, store, step, config) {
|
|
269
|
+
if (browserState.page) {
|
|
270
|
+
return captureSnapshot(browserState.page, store, step);
|
|
271
|
+
}
|
|
272
|
+
return captureSyntheticSnapshot(store, step, config);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
async function executeTool(browserState, toolCall, snapshot, config, store, observers, state) {
|
|
276
|
+
const args = JSON.parse(toolCall.function.arguments || "{}");
|
|
277
|
+
const safeArgs = redactValue(args);
|
|
278
|
+
const guard = checkToolUse({
|
|
279
|
+
toolName: toolCall.function.name,
|
|
280
|
+
args,
|
|
281
|
+
snapshot,
|
|
282
|
+
config,
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
if (!guard.allowed) {
|
|
286
|
+
await store.appendEvent("tool.blocked", {
|
|
287
|
+
toolName: toolCall.function.name,
|
|
288
|
+
args: safeArgs,
|
|
289
|
+
reason: guard.reason,
|
|
290
|
+
category: guard.category,
|
|
291
|
+
needsApproval: guard.needsApproval,
|
|
292
|
+
});
|
|
293
|
+
observers.event("tool.blocked", {
|
|
294
|
+
toolName: toolCall.function.name,
|
|
295
|
+
args: safeArgs,
|
|
296
|
+
reason: guard.reason,
|
|
297
|
+
category: guard.category,
|
|
298
|
+
needsApproval: guard.needsApproval,
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
ok: false,
|
|
302
|
+
blocked: true,
|
|
303
|
+
reason: guard.reason,
|
|
304
|
+
toolName: toolCall.function.name,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
await store.appendEvent("tool.started", {
|
|
309
|
+
toolName: toolCall.function.name,
|
|
310
|
+
args: safeArgs,
|
|
311
|
+
});
|
|
312
|
+
observers.event("tool.started", {
|
|
313
|
+
toolName: toolCall.function.name,
|
|
314
|
+
args: safeArgs,
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
if (BROWSER_TOOLS.has(toolCall.function.name)) {
|
|
319
|
+
await ensureBrowser(browserState, config, store, state, observers);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
switch (toolCall.function.name) {
|
|
323
|
+
case "open_url":
|
|
324
|
+
await browserState.page.goto(String(args.url), { waitUntil: "domcontentloaded" });
|
|
325
|
+
break;
|
|
326
|
+
case "click": {
|
|
327
|
+
const locator = browserState.page.locator(`[data-agent-id="${args.id}"]`).first();
|
|
328
|
+
await locator.scrollIntoViewIfNeeded();
|
|
329
|
+
await locator.click({ timeout: 5000 });
|
|
330
|
+
await browserState.page.waitForLoadState("domcontentloaded", { timeout: 2000 }).catch(() => {});
|
|
331
|
+
break;
|
|
332
|
+
}
|
|
333
|
+
case "type": {
|
|
334
|
+
const locator = browserState.page.locator(`[data-agent-id="${args.id}"]`).first();
|
|
335
|
+
await locator.scrollIntoViewIfNeeded();
|
|
336
|
+
await locator.fill(String(args.text));
|
|
337
|
+
if (args.pressEnter) {
|
|
338
|
+
await locator.press("Enter");
|
|
339
|
+
}
|
|
340
|
+
await browserState.page.waitForLoadState("domcontentloaded", { timeout: 2000 }).catch(() => {});
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
case "scroll": {
|
|
344
|
+
const amount = Number.isFinite(args.amount) ? Number(args.amount) : 700;
|
|
345
|
+
const dy = args.direction === "up" ? -Math.abs(amount) : Math.abs(amount);
|
|
346
|
+
await browserState.page.mouse.wheel(0, dy);
|
|
347
|
+
await browserState.page.waitForTimeout(300);
|
|
348
|
+
break;
|
|
349
|
+
}
|
|
350
|
+
case "press":
|
|
351
|
+
await browserState.page.keyboard.press(String(args.key));
|
|
352
|
+
await browserState.page.waitForTimeout(200);
|
|
353
|
+
break;
|
|
354
|
+
case "back":
|
|
355
|
+
await browserState.page.goBack({ waitUntil: "domcontentloaded" }).catch(() => {});
|
|
356
|
+
break;
|
|
357
|
+
case "wait":
|
|
358
|
+
if (browserState.page) {
|
|
359
|
+
await browserState.page.waitForTimeout(Number.isFinite(args.ms) ? Number(args.ms) : 1000);
|
|
360
|
+
} else {
|
|
361
|
+
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(args.ms) ? Number(args.ms) : 1000));
|
|
362
|
+
}
|
|
363
|
+
break;
|
|
364
|
+
case "run_command": {
|
|
365
|
+
const policy = evaluateCommandPolicy(String(args.command), config);
|
|
366
|
+
if (config.useDockerSandbox) {
|
|
367
|
+
await ensureDockerSandboxReady(config, observers);
|
|
368
|
+
}
|
|
369
|
+
const commandResult = await runShellCommand(String(args.command), config, policy);
|
|
370
|
+
const result = {
|
|
371
|
+
ok: true,
|
|
372
|
+
toolName: "run_command",
|
|
373
|
+
args: safeArgs,
|
|
374
|
+
sandbox: config.useDockerSandbox ? "docker" : "host",
|
|
375
|
+
commandPolicy: {
|
|
376
|
+
category: policy.category,
|
|
377
|
+
sandboxMode: policy.sandboxMode,
|
|
378
|
+
packageInstallPolicy: policy.packageInstallPolicy,
|
|
379
|
+
needsNetwork: Boolean(policy.needsNetwork),
|
|
380
|
+
writesWorkspace: Boolean(policy.writesWorkspace),
|
|
381
|
+
},
|
|
382
|
+
...commandResult,
|
|
383
|
+
};
|
|
384
|
+
await store.appendEvent("tool.completed", result);
|
|
385
|
+
observers.event("tool.completed", result);
|
|
386
|
+
return result;
|
|
387
|
+
}
|
|
388
|
+
case "delegate_agent": {
|
|
389
|
+
const wrapperResult = await runAgentWrapper(
|
|
390
|
+
{
|
|
391
|
+
wrapper: String(args.wrapper || ""),
|
|
392
|
+
prompt: String(args.prompt || ""),
|
|
393
|
+
},
|
|
394
|
+
config
|
|
395
|
+
);
|
|
396
|
+
const result = {
|
|
397
|
+
ok: Boolean(wrapperResult.ok),
|
|
398
|
+
toolName: "delegate_agent",
|
|
399
|
+
args: safeArgs,
|
|
400
|
+
...wrapperResult,
|
|
401
|
+
};
|
|
402
|
+
await store.appendEvent("tool.completed", result);
|
|
403
|
+
observers.event("tool.completed", result);
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
case "finish":
|
|
407
|
+
return { ok: true, done: true, result: String(args.result || ""), toolName: "finish" };
|
|
408
|
+
default:
|
|
409
|
+
throw new Error(`Unknown tool: ${toolCall.function.name}`);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
await saveBrowserState(browserState, store);
|
|
413
|
+
|
|
414
|
+
const result = {
|
|
415
|
+
ok: true,
|
|
416
|
+
toolName: toolCall.function.name,
|
|
417
|
+
args: safeArgs,
|
|
418
|
+
url: browserState.page?.url() || state.meta.lastUrl || "",
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
await store.appendEvent("tool.completed", result);
|
|
422
|
+
observers.event("tool.completed", result);
|
|
423
|
+
return result;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
const result = {
|
|
426
|
+
ok: false,
|
|
427
|
+
toolName: toolCall.function.name,
|
|
428
|
+
args: safeArgs,
|
|
429
|
+
error: redactSensitiveText(error instanceof Error ? error.message : String(error)),
|
|
430
|
+
};
|
|
431
|
+
await store.appendEvent("tool.failed", result);
|
|
432
|
+
observers.event("tool.failed", result);
|
|
433
|
+
return result;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export async function runAgent(config) {
|
|
438
|
+
if (!config.apiKey) {
|
|
439
|
+
throw new Error(`Missing API key for provider "${config.provider}".`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const sessionId = config.resume || config.sessionId || `web-agent-${crypto.randomUUID()}`;
|
|
443
|
+
const store = new SessionStore(config.sessionsDir, sessionId);
|
|
444
|
+
const client = createClient(config);
|
|
445
|
+
const observers = createObservers(config);
|
|
446
|
+
const browserState = createBrowserState();
|
|
447
|
+
|
|
448
|
+
await store.ensure();
|
|
449
|
+
|
|
450
|
+
let state = await store.loadState();
|
|
451
|
+
|
|
452
|
+
if (config.resume && !state) {
|
|
453
|
+
throw new Error(`No saved session found for "${config.resume}".`);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (!state) {
|
|
457
|
+
state = createInitialState(config, sessionId);
|
|
458
|
+
await store.appendEvent("session.created", {
|
|
459
|
+
sessionId,
|
|
460
|
+
provider: config.provider,
|
|
461
|
+
model: config.model,
|
|
462
|
+
routingMode: config.routingMode,
|
|
463
|
+
routeReason: config.routeReason,
|
|
464
|
+
goal: config.goal,
|
|
465
|
+
});
|
|
466
|
+
await store.saveState(state);
|
|
467
|
+
} else {
|
|
468
|
+
await store.appendEvent("session.resumed", { sessionId });
|
|
469
|
+
applyContinuationPrompt(state, config, observers);
|
|
470
|
+
await store.saveState(state);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
ensureChatState(state);
|
|
474
|
+
|
|
475
|
+
observers.log("session.ready", {
|
|
476
|
+
sessionId,
|
|
477
|
+
provider: config.provider,
|
|
478
|
+
model: config.model,
|
|
479
|
+
routingMode: config.routingMode,
|
|
480
|
+
routeReason: config.routeReason,
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
try {
|
|
484
|
+
if (!state.plan) {
|
|
485
|
+
const plan = await createPlan(client, config, state);
|
|
486
|
+
state.plan = plan;
|
|
487
|
+
state.messages.push({
|
|
488
|
+
role: "assistant",
|
|
489
|
+
content: `Execution plan:\n${plan}`,
|
|
490
|
+
});
|
|
491
|
+
await store.savePlan(plan);
|
|
492
|
+
await store.appendEvent("plan.created", { plan });
|
|
493
|
+
await store.saveState(state);
|
|
494
|
+
observers.event("plan.created", { plan });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
observers.log("session.context", {
|
|
498
|
+
sessionId,
|
|
499
|
+
provider: config.provider,
|
|
500
|
+
model: config.model,
|
|
501
|
+
routingMode: config.routingMode,
|
|
502
|
+
routeReason: config.routeReason,
|
|
503
|
+
commandCwd: config.commandCwd,
|
|
504
|
+
allowShellTool: config.allowShellTool,
|
|
505
|
+
allowWrapperTools: config.allowWrapperTools,
|
|
506
|
+
wrappers: config.allowWrapperTools ? wrapperStatusText() : "",
|
|
507
|
+
shellSandbox: config.useDockerSandbox ? "docker" : "host",
|
|
508
|
+
sandboxMode: config.sandboxMode,
|
|
509
|
+
packageInstallPolicy: config.packageInstallPolicy,
|
|
510
|
+
dockerSandboxImage: config.useDockerSandbox ? config.dockerSandboxImage : "",
|
|
511
|
+
startUrl: config.startUrl,
|
|
512
|
+
});
|
|
513
|
+
|
|
514
|
+
console.log(`Session: ${sessionId}`);
|
|
515
|
+
console.log(`Provider: ${config.provider}`);
|
|
516
|
+
console.log(`Model: ${config.model}`);
|
|
517
|
+
console.log(`Routing: ${config.routingMode} (${config.routeReason})`);
|
|
518
|
+
if (state.plan) {
|
|
519
|
+
console.log("\nPlan:");
|
|
520
|
+
console.log(state.plan);
|
|
521
|
+
console.log("");
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
for (let step = state.stepsCompleted + 1; step <= config.maxSteps; step += 1) {
|
|
525
|
+
const snapshot = await buildSnapshot(browserState, store, step, config);
|
|
526
|
+
state.meta.lastUrl = snapshot.url || state.meta.lastUrl;
|
|
527
|
+
await saveBrowserState(browserState, store).catch(() => {});
|
|
528
|
+
|
|
529
|
+
await store.appendEvent("snapshot.captured", {
|
|
530
|
+
step,
|
|
531
|
+
url: snapshot.url,
|
|
532
|
+
title: snapshot.title,
|
|
533
|
+
screenshotPath: snapshot.screenshotPath,
|
|
534
|
+
snapshotPath: snapshot.snapshotPath,
|
|
535
|
+
});
|
|
536
|
+
observers.event("snapshot.captured", {
|
|
537
|
+
step,
|
|
538
|
+
url: snapshot.url,
|
|
539
|
+
title: snapshot.title,
|
|
540
|
+
screenshotPath: snapshot.screenshotPath,
|
|
541
|
+
});
|
|
542
|
+
|
|
543
|
+
state.messages.push({
|
|
544
|
+
role: "user",
|
|
545
|
+
content: `Step ${step}/${config.maxSteps}. Latest runtime snapshot:\n${JSON.stringify({
|
|
546
|
+
title: snapshot.title,
|
|
547
|
+
url: snapshot.url,
|
|
548
|
+
pageText: snapshot.pageText,
|
|
549
|
+
elements: snapshot.elements,
|
|
550
|
+
browserOpen: Boolean(browserState.page),
|
|
551
|
+
shellToolAvailable: config.allowShellTool,
|
|
552
|
+
agentWrappersAvailable: config.allowWrapperTools,
|
|
553
|
+
agentWrappers: config.allowWrapperTools ? wrapperStatusText() : "",
|
|
554
|
+
shellSandbox: config.useDockerSandbox ? "docker" : "host",
|
|
555
|
+
sandboxMode: config.sandboxMode,
|
|
556
|
+
packageInstallPolicy: config.packageInstallPolicy,
|
|
557
|
+
commandCwd: config.commandCwd,
|
|
558
|
+
suggestedStartUrl: config.startUrl || "",
|
|
559
|
+
})}`,
|
|
560
|
+
});
|
|
561
|
+
|
|
562
|
+
const response = await requestNextStep(client, config, state.messages);
|
|
563
|
+
const assistantMessage = response.choices[0]?.message;
|
|
564
|
+
if (!assistantMessage) {
|
|
565
|
+
throw new Error("Model returned no assistant message.");
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
state.messages.push({
|
|
569
|
+
role: "assistant",
|
|
570
|
+
content: assistantMessage.content || "",
|
|
571
|
+
tool_calls: assistantMessage.tool_calls,
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
await store.appendEvent("model.responded", {
|
|
575
|
+
step,
|
|
576
|
+
content: assistantMessage.content || "",
|
|
577
|
+
toolCalls: (assistantMessage.tool_calls || []).map((call) => ({
|
|
578
|
+
id: call.id,
|
|
579
|
+
name: call.function.name,
|
|
580
|
+
arguments: redactSensitiveText(call.function.arguments),
|
|
581
|
+
})),
|
|
582
|
+
});
|
|
583
|
+
observers.event("model.responded", {
|
|
584
|
+
step,
|
|
585
|
+
content: assistantMessage.content || "",
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
const toolCalls = assistantMessage.tool_calls || [];
|
|
589
|
+
|
|
590
|
+
if (toolCalls.length === 0) {
|
|
591
|
+
const fallback = assistantMessage.content?.trim() || "No tool call returned.";
|
|
592
|
+
appendChatEntry(state, "assistant", fallback);
|
|
593
|
+
await store.appendEvent("session.finished", {
|
|
594
|
+
result: fallback,
|
|
595
|
+
mode: "assistant-content",
|
|
596
|
+
});
|
|
597
|
+
observers.event("session.finished", {
|
|
598
|
+
result: fallback,
|
|
599
|
+
sessionId,
|
|
600
|
+
});
|
|
601
|
+
console.log(fallback);
|
|
602
|
+
state.stepsCompleted = step;
|
|
603
|
+
state.updatedAt = new Date().toISOString();
|
|
604
|
+
await store.saveState(state);
|
|
605
|
+
return {
|
|
606
|
+
sessionId,
|
|
607
|
+
result: fallback,
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
for (const toolCall of toolCalls) {
|
|
612
|
+
const toolResult = await executeTool(browserState, toolCall, snapshot, config, store, observers, state);
|
|
613
|
+
state.messages.push({
|
|
614
|
+
role: "tool",
|
|
615
|
+
tool_call_id: toolCall.id,
|
|
616
|
+
content: JSON.stringify(toolResult),
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
if (toolResult.toolName === "run_command") {
|
|
620
|
+
observers.log("command.output", {
|
|
621
|
+
command: redactSensitiveText(toolResult.args.command),
|
|
622
|
+
stdout: toolResult.stdout,
|
|
623
|
+
stderr: toolResult.stderr,
|
|
624
|
+
commandPolicy: toolResult.commandPolicy,
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
if (toolResult.toolName === "delegate_agent") {
|
|
629
|
+
observers.log("wrapper.output", {
|
|
630
|
+
wrapper: toolResult.wrapper,
|
|
631
|
+
ok: toolResult.ok,
|
|
632
|
+
fallback: Boolean(toolResult.fallback),
|
|
633
|
+
stdout: toolResult.stdout,
|
|
634
|
+
stderr: toolResult.stderr,
|
|
635
|
+
error: toolResult.error,
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
if (toolResult.done) {
|
|
640
|
+
state.stepsCompleted = step;
|
|
641
|
+
state.updatedAt = new Date().toISOString();
|
|
642
|
+
state.meta.lastUrl = browserState.page?.url() || state.meta.lastUrl;
|
|
643
|
+
state.messages.push({
|
|
644
|
+
role: "assistant",
|
|
645
|
+
content: toolResult.result,
|
|
646
|
+
});
|
|
647
|
+
appendChatEntry(state, "assistant", toolResult.result);
|
|
648
|
+
await store.saveState(state);
|
|
649
|
+
await store.appendEvent("session.finished", {
|
|
650
|
+
result: toolResult.result,
|
|
651
|
+
mode: "finish-tool",
|
|
652
|
+
});
|
|
653
|
+
observers.event("session.finished", {
|
|
654
|
+
result: toolResult.result,
|
|
655
|
+
sessionId,
|
|
656
|
+
});
|
|
657
|
+
console.log(toolResult.result);
|
|
658
|
+
return {
|
|
659
|
+
sessionId,
|
|
660
|
+
result: toolResult.result,
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
state.stepsCompleted = step;
|
|
666
|
+
state.updatedAt = new Date().toISOString();
|
|
667
|
+
await store.saveState(state);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
await store.appendEvent("session.stopped", {
|
|
671
|
+
reason: "max_steps_reached",
|
|
672
|
+
maxSteps: config.maxSteps,
|
|
673
|
+
});
|
|
674
|
+
observers.event("session.stopped", {
|
|
675
|
+
reason: "max_steps_reached",
|
|
676
|
+
sessionId,
|
|
677
|
+
});
|
|
678
|
+
console.error(`Stopped after ${config.maxSteps} steps without finish().`);
|
|
679
|
+
return {
|
|
680
|
+
sessionId,
|
|
681
|
+
result: "",
|
|
682
|
+
};
|
|
683
|
+
} finally {
|
|
684
|
+
await closeBrowser(browserState, store);
|
|
685
|
+
}
|
|
686
|
+
}
|