@ap3x/browser-agent 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/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/agent/agent.d.ts +160 -0
- package/dist/agent/agent.d.ts.map +1 -0
- package/dist/agent/backend.d.ts +38 -0
- package/dist/agent/backend.d.ts.map +1 -0
- package/dist/agent/captcha-watchdog.d.ts +31 -0
- package/dist/agent/captcha-watchdog.d.ts.map +1 -0
- package/dist/agent/context-builder.d.ts +24 -0
- package/dist/agent/context-builder.d.ts.map +1 -0
- package/dist/agent/event-stream.d.ts +104 -0
- package/dist/agent/event-stream.d.ts.map +1 -0
- package/dist/agent/executor.d.ts +37 -0
- package/dist/agent/executor.d.ts.map +1 -0
- package/dist/agent/gif.d.ts +23 -0
- package/dist/agent/gif.d.ts.map +1 -0
- package/dist/agent/history.d.ts +89 -0
- package/dist/agent/history.d.ts.map +1 -0
- package/dist/agent/judge.d.ts +36 -0
- package/dist/agent/judge.d.ts.map +1 -0
- package/dist/agent/message-manager.d.ts +94 -0
- package/dist/agent/message-manager.d.ts.map +1 -0
- package/dist/agent/output-schema.d.ts +26 -0
- package/dist/agent/output-schema.d.ts.map +1 -0
- package/dist/agent/png-decode.d.ts +9 -0
- package/dist/agent/png-decode.d.ts.map +1 -0
- package/dist/agent/rematch.d.ts +26 -0
- package/dist/agent/rematch.d.ts.map +1 -0
- package/dist/agent/replay.d.ts +155 -0
- package/dist/agent/replay.d.ts.map +1 -0
- package/dist/agent/step-loop.d.ts +124 -0
- package/dist/agent/step-loop.d.ts.map +1 -0
- package/dist/agent/variable-detector.d.ts +19 -0
- package/dist/agent/variable-detector.d.ts.map +1 -0
- package/dist/agent/video.d.ts +12 -0
- package/dist/agent/video.d.ts.map +1 -0
- package/dist/chunk-32HNRAQS.js +82 -0
- package/dist/chunk-UZALZUEE.js +2907 -0
- package/dist/filesystem/external-read.d.ts +18 -0
- package/dist/filesystem/external-read.d.ts.map +1 -0
- package/dist/filesystem/file-system.d.ts +61 -0
- package/dist/filesystem/file-system.d.ts.map +1 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1070 -0
- package/dist/mcp/bin.d.ts +3 -0
- package/dist/mcp/bin.d.ts.map +1 -0
- package/dist/mcp/bin.js +19 -0
- package/dist/mcp/client.d.ts +45 -0
- package/dist/mcp/client.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +3 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/log.d.ts +6 -0
- package/dist/mcp/log.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +36 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/observability/logger.d.ts +25 -0
- package/dist/observability/logger.d.ts.map +1 -0
- package/dist/testing.d.ts +37 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +18 -0
- package/dist/tools/actions.d.ts +9 -0
- package/dist/tools/actions.d.ts.map +1 -0
- package/dist/tools/registry.d.ts +125 -0
- package/dist/tools/registry.d.ts.map +1 -0
- package/dist/tools/sensitive.d.ts +59 -0
- package/dist/tools/sensitive.d.ts.map +1 -0
- package/dist/tools/types.d.ts +117 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/usage/ledger.d.ts +27 -0
- package/dist/usage/ledger.d.ts.map +1 -0
- package/package.json +50 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1070 @@
|
|
|
1
|
+
import {
|
|
2
|
+
agentStep,
|
|
3
|
+
collectingLogger,
|
|
4
|
+
emptyLlmResponse,
|
|
5
|
+
invalidLlmResponse,
|
|
6
|
+
scriptedLlm,
|
|
7
|
+
transportErrorResponse,
|
|
8
|
+
truncatedLlmResponse
|
|
9
|
+
} from "./chunk-32HNRAQS.js";
|
|
10
|
+
import {
|
|
11
|
+
Agent,
|
|
12
|
+
AgentEventStream,
|
|
13
|
+
AgentHistoryList,
|
|
14
|
+
BUILTIN_ACTIONS,
|
|
15
|
+
COMPACT_EVERY_N_STEPS,
|
|
16
|
+
COMPACT_TRIGGER_CHARS,
|
|
17
|
+
CaptchaWatchdog,
|
|
18
|
+
FileParseError,
|
|
19
|
+
FileSystem,
|
|
20
|
+
JudgementResultSchema,
|
|
21
|
+
LoopDetector,
|
|
22
|
+
MessageManager,
|
|
23
|
+
Registry,
|
|
24
|
+
StepLoop,
|
|
25
|
+
UsageLedger,
|
|
26
|
+
anySignal,
|
|
27
|
+
applicablePlaceholders,
|
|
28
|
+
assembleNarration,
|
|
29
|
+
budgetNudge,
|
|
30
|
+
buildAgentOutputSchema,
|
|
31
|
+
createBrowserMcpServer,
|
|
32
|
+
createDefaultRegistry,
|
|
33
|
+
createLogger,
|
|
34
|
+
defineAction,
|
|
35
|
+
detectCaptcha,
|
|
36
|
+
doneResult,
|
|
37
|
+
encodeFramesToGif,
|
|
38
|
+
generateRunGif,
|
|
39
|
+
generateRunVideoGif,
|
|
40
|
+
isDoneResult,
|
|
41
|
+
judgeRun,
|
|
42
|
+
logStderr,
|
|
43
|
+
maskOutbound,
|
|
44
|
+
maskSecretMap,
|
|
45
|
+
multiAct,
|
|
46
|
+
parseAction,
|
|
47
|
+
perceive,
|
|
48
|
+
readExternalFile,
|
|
49
|
+
registerBuiltins,
|
|
50
|
+
resolveSecretsForUrl,
|
|
51
|
+
resultsAreDone,
|
|
52
|
+
runBrowserMcpServer,
|
|
53
|
+
sanitizeFilename,
|
|
54
|
+
sensitiveDataDescription,
|
|
55
|
+
silentLogger,
|
|
56
|
+
stepDurationSeconds,
|
|
57
|
+
stepResult,
|
|
58
|
+
substituteSecrets,
|
|
59
|
+
toTypedAction,
|
|
60
|
+
totp
|
|
61
|
+
} from "./chunk-UZALZUEE.js";
|
|
62
|
+
|
|
63
|
+
// src/agent/backend.ts
|
|
64
|
+
var EMPTY_USAGE = {
|
|
65
|
+
input: 0,
|
|
66
|
+
output: 0,
|
|
67
|
+
cacheRead: 0,
|
|
68
|
+
cacheWrite: 0,
|
|
69
|
+
totalTokens: 0,
|
|
70
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }
|
|
71
|
+
};
|
|
72
|
+
function mapUsage(summary) {
|
|
73
|
+
if (!summary) return EMPTY_USAGE;
|
|
74
|
+
let input = 0;
|
|
75
|
+
let output = 0;
|
|
76
|
+
let cacheRead = 0;
|
|
77
|
+
let cacheWrite = 0;
|
|
78
|
+
for (const model of summary.perModel) {
|
|
79
|
+
input += model.input;
|
|
80
|
+
output += model.output;
|
|
81
|
+
cacheRead += model.cacheRead;
|
|
82
|
+
cacheWrite += model.cacheWrite;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
input,
|
|
86
|
+
output,
|
|
87
|
+
cacheRead,
|
|
88
|
+
cacheWrite,
|
|
89
|
+
totalTokens: summary.totalTokens,
|
|
90
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: summary.totalCost }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function toTaskText(task) {
|
|
94
|
+
if (typeof task === "string") return task;
|
|
95
|
+
const parts = [];
|
|
96
|
+
for (const message of task) {
|
|
97
|
+
const content = message.content;
|
|
98
|
+
if (typeof content === "string") {
|
|
99
|
+
parts.push(content);
|
|
100
|
+
} else if (Array.isArray(content)) {
|
|
101
|
+
for (const block of content) {
|
|
102
|
+
if (block && typeof block === "object" && "text" in block) {
|
|
103
|
+
parts.push(String(block.text));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return parts.join("\n");
|
|
109
|
+
}
|
|
110
|
+
function toToolResult(event) {
|
|
111
|
+
const { result } = event;
|
|
112
|
+
const text = result.error ?? result.extractedContent ?? result.longTermMemory ?? "";
|
|
113
|
+
return {
|
|
114
|
+
role: "toolResult",
|
|
115
|
+
toolCallId: `${event.step}:${event.index}`,
|
|
116
|
+
toolName: event.name,
|
|
117
|
+
content: text ? [{ type: "text", text }] : [],
|
|
118
|
+
isError: result.error !== void 0,
|
|
119
|
+
timestamp: Date.now()
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
function bridgeEvent(event) {
|
|
123
|
+
switch (event.type) {
|
|
124
|
+
case "run-started":
|
|
125
|
+
return { type: "agent_start" };
|
|
126
|
+
case "step-started":
|
|
127
|
+
return { type: "turn_start" };
|
|
128
|
+
case "action-started":
|
|
129
|
+
return {
|
|
130
|
+
type: "tool_execution_start",
|
|
131
|
+
toolCallId: `${event.step}:${event.index}`,
|
|
132
|
+
toolName: event.name,
|
|
133
|
+
args: event.params
|
|
134
|
+
};
|
|
135
|
+
case "action-finished":
|
|
136
|
+
return {
|
|
137
|
+
type: "tool_execution_end",
|
|
138
|
+
toolCallId: `${event.step}:${event.index}`,
|
|
139
|
+
toolName: event.name,
|
|
140
|
+
result: toToolResult(event),
|
|
141
|
+
isError: event.result.error !== void 0
|
|
142
|
+
};
|
|
143
|
+
case "done":
|
|
144
|
+
case "error":
|
|
145
|
+
return { type: "agent_end", messages: [] };
|
|
146
|
+
case "model-thought":
|
|
147
|
+
return void 0;
|
|
148
|
+
case "step-finished":
|
|
149
|
+
return void 0;
|
|
150
|
+
case "paused":
|
|
151
|
+
case "resumed":
|
|
152
|
+
return void 0;
|
|
153
|
+
case "captcha-detected":
|
|
154
|
+
return void 0;
|
|
155
|
+
default:
|
|
156
|
+
return event;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
var BrowserAgentBackend = class {
|
|
160
|
+
source;
|
|
161
|
+
model;
|
|
162
|
+
listeners = /* @__PURE__ */ new Set();
|
|
163
|
+
active;
|
|
164
|
+
constructor(source) {
|
|
165
|
+
this.source = source;
|
|
166
|
+
this.model = source instanceof Agent ? source.currentModel : source.llm.model;
|
|
167
|
+
}
|
|
168
|
+
agentFor(task) {
|
|
169
|
+
return this.source instanceof Agent ? this.source : new Agent(task, this.source);
|
|
170
|
+
}
|
|
171
|
+
async run(task, opts) {
|
|
172
|
+
const taskText = toTaskText(task);
|
|
173
|
+
const agent = this.agentFor(taskText);
|
|
174
|
+
this.active = agent;
|
|
175
|
+
const pump = this.listeners.size > 0 ? this.pumpEvents(agent) : void 0;
|
|
176
|
+
try {
|
|
177
|
+
const history = await agent.run({ signal: opts?.signal });
|
|
178
|
+
await pump;
|
|
179
|
+
return this.toRunResult(taskText, history, opts?.signal?.aborted === true);
|
|
180
|
+
} finally {
|
|
181
|
+
this.active = void 0;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
/** Drain the O2 stream, translating each event to a subscribed agent-core listener. */
|
|
185
|
+
async pumpEvents(agent) {
|
|
186
|
+
for await (const event of agent.events()) {
|
|
187
|
+
const mapped = bridgeEvent(event);
|
|
188
|
+
if (!mapped) continue;
|
|
189
|
+
for (const listener of this.listeners) await listener(mapped);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
toRunResult(task, history, aborted) {
|
|
193
|
+
const finalText = history.finalResult() ?? "";
|
|
194
|
+
const usage = mapUsage(history.usage);
|
|
195
|
+
const stopReason = stopReasonFor(history, aborted);
|
|
196
|
+
const failed = stopReason !== "stop";
|
|
197
|
+
return {
|
|
198
|
+
messages: buildMessages(task, finalText, this.model, usage, stopReason),
|
|
199
|
+
finalText,
|
|
200
|
+
usage,
|
|
201
|
+
stopReason,
|
|
202
|
+
errorMessage: failed ? finalText || `run stopped: ${stopReason}` : void 0
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
subscribe(listener) {
|
|
206
|
+
this.listeners.add(listener);
|
|
207
|
+
return () => {
|
|
208
|
+
this.listeners.delete(listener);
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
abort() {
|
|
212
|
+
this.active?.stop();
|
|
213
|
+
}
|
|
214
|
+
supports(capability) {
|
|
215
|
+
return capability === "subscribe" || capability === "abort";
|
|
216
|
+
}
|
|
217
|
+
getCurrentModel() {
|
|
218
|
+
return this.model;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
function stopReasonFor(history, aborted) {
|
|
222
|
+
if (aborted || history.terminationReason === "aborted") return "aborted";
|
|
223
|
+
if (!history.isDone()) return "error";
|
|
224
|
+
if (history.terminationReason === "max_steps" || history.terminationReason === "run_timeout")
|
|
225
|
+
return "length";
|
|
226
|
+
return "stop";
|
|
227
|
+
}
|
|
228
|
+
function buildMessages(task, finalText, model, usage, stopReason) {
|
|
229
|
+
const now = Date.now();
|
|
230
|
+
return [
|
|
231
|
+
{ role: "user", content: task, timestamp: now },
|
|
232
|
+
{
|
|
233
|
+
role: "assistant",
|
|
234
|
+
content: finalText ? [{ type: "text", text: finalText }] : [],
|
|
235
|
+
api: model.api,
|
|
236
|
+
provider: model.provider,
|
|
237
|
+
model: model.id,
|
|
238
|
+
usage,
|
|
239
|
+
stopReason,
|
|
240
|
+
timestamp: now
|
|
241
|
+
}
|
|
242
|
+
];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/mcp/client.ts
|
|
246
|
+
import { AgentMCPConnectionError } from "@ap3x/agent-core";
|
|
247
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
248
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
249
|
+
import { Type } from "@sinclair/typebox";
|
|
250
|
+
var UNCONVERTIBLE_KEYS = ["$ref", "allOf", "anyOf", "oneOf", "not"];
|
|
251
|
+
function leafSchema(prop) {
|
|
252
|
+
const type = typeof prop === "object" && prop !== null ? prop.type : void 0;
|
|
253
|
+
switch (type) {
|
|
254
|
+
case "string":
|
|
255
|
+
return Type.String();
|
|
256
|
+
case "number":
|
|
257
|
+
return Type.Number();
|
|
258
|
+
case "integer":
|
|
259
|
+
return Type.Integer();
|
|
260
|
+
case "boolean":
|
|
261
|
+
return Type.Boolean();
|
|
262
|
+
case "array":
|
|
263
|
+
return Type.Array(Type.Unknown());
|
|
264
|
+
case "object":
|
|
265
|
+
return Type.Object({}, { additionalProperties: true });
|
|
266
|
+
default:
|
|
267
|
+
return Type.Unknown();
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
function synthesizeParamSchema(inputSchema) {
|
|
271
|
+
if (typeof inputSchema !== "object" || inputSchema === null) return void 0;
|
|
272
|
+
const schema = inputSchema;
|
|
273
|
+
if (schema.type !== "object") return void 0;
|
|
274
|
+
if (UNCONVERTIBLE_KEYS.some((key) => key in schema)) return void 0;
|
|
275
|
+
const properties = typeof schema.properties === "object" && schema.properties !== null ? schema.properties : {};
|
|
276
|
+
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
277
|
+
const shape = {};
|
|
278
|
+
for (const [key, prop] of Object.entries(properties)) {
|
|
279
|
+
const leaf = leafSchema(prop);
|
|
280
|
+
shape[key] = required.includes(key) ? leaf : Type.Optional(leaf);
|
|
281
|
+
}
|
|
282
|
+
return Type.Object(shape, { additionalProperties: schema.additionalProperties === true });
|
|
283
|
+
}
|
|
284
|
+
function flattenContent(content) {
|
|
285
|
+
const parts = Array.isArray(content) ? content : [];
|
|
286
|
+
const texts = [];
|
|
287
|
+
const images = [];
|
|
288
|
+
for (const part of parts) {
|
|
289
|
+
if (typeof part !== "object" || part === null) continue;
|
|
290
|
+
const p = part;
|
|
291
|
+
if (p.type === "text" && typeof p.text === "string") texts.push(p.text);
|
|
292
|
+
else if (p.type === "image" && typeof p.data === "string")
|
|
293
|
+
images.push({ name: "image", data: p.data });
|
|
294
|
+
}
|
|
295
|
+
return { text: texts.join("\n"), images };
|
|
296
|
+
}
|
|
297
|
+
function mcpToolAction(client, toolName, description, schema, prefix) {
|
|
298
|
+
return defineAction({
|
|
299
|
+
name: `${prefix}${toolName}`,
|
|
300
|
+
description,
|
|
301
|
+
schema,
|
|
302
|
+
async execute(params, _ctx) {
|
|
303
|
+
try {
|
|
304
|
+
const result = await client.callTool({
|
|
305
|
+
name: toolName,
|
|
306
|
+
arguments: params ?? {}
|
|
307
|
+
});
|
|
308
|
+
const { text, images } = flattenContent(result.content);
|
|
309
|
+
if (result.isError === true) {
|
|
310
|
+
return stepResult({ error: text || `MCP tool "${toolName}" reported an error` });
|
|
311
|
+
}
|
|
312
|
+
return stepResult({
|
|
313
|
+
extractedContent: text,
|
|
314
|
+
includeExtractedContentOnlyOnce: true,
|
|
315
|
+
longTermMemory: `Used MCP tool "${toolName}"`,
|
|
316
|
+
images: images.length > 0 ? images : void 0
|
|
317
|
+
});
|
|
318
|
+
} catch (error) {
|
|
319
|
+
return stepResult({
|
|
320
|
+
error: `MCP tool "${toolName}" failed: ${error instanceof Error ? error.message : String(error)}`
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
async function importMcpTools(client, options = {}) {
|
|
327
|
+
const prefix = options.prefix ?? "";
|
|
328
|
+
const { tools } = await client.listTools();
|
|
329
|
+
const actions = [];
|
|
330
|
+
for (const tool of tools) {
|
|
331
|
+
if (options.toolFilter && !options.toolFilter(tool.name)) continue;
|
|
332
|
+
const schema = synthesizeParamSchema(tool.inputSchema);
|
|
333
|
+
if (!schema) {
|
|
334
|
+
logStderr(`skipping MCP tool "${tool.name}": input schema could not be converted`);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
actions.push(
|
|
338
|
+
mcpToolAction(
|
|
339
|
+
client,
|
|
340
|
+
tool.name,
|
|
341
|
+
tool.description ?? `MCP tool "${tool.name}"`,
|
|
342
|
+
schema,
|
|
343
|
+
prefix
|
|
344
|
+
)
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
return actions;
|
|
348
|
+
}
|
|
349
|
+
async function connectMcpClient(connection) {
|
|
350
|
+
const client = new Client(
|
|
351
|
+
{
|
|
352
|
+
name: connection.clientInfo?.name ?? "ap3x-browser-agent",
|
|
353
|
+
version: connection.clientInfo?.version ?? "0.1.0"
|
|
354
|
+
},
|
|
355
|
+
{ capabilities: {} }
|
|
356
|
+
);
|
|
357
|
+
const transport = new StdioClientTransport({
|
|
358
|
+
command: connection.command,
|
|
359
|
+
args: connection.args,
|
|
360
|
+
env: connection.env
|
|
361
|
+
});
|
|
362
|
+
try {
|
|
363
|
+
await client.connect(transport);
|
|
364
|
+
} catch (error) {
|
|
365
|
+
throw new AgentMCPConnectionError(
|
|
366
|
+
`Failed to connect to MCP server "${connection.command}": ${error instanceof Error ? error.message : String(error)}`,
|
|
367
|
+
error
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
return client;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/agent/rematch.ts
|
|
374
|
+
import {
|
|
375
|
+
MatchLevel,
|
|
376
|
+
elementHash,
|
|
377
|
+
xpath as nodeXpath,
|
|
378
|
+
parentBranchHash,
|
|
379
|
+
stableHash
|
|
380
|
+
} from "@ap3x/browser";
|
|
381
|
+
var UNIQUE_ATTRIBUTES = ["name", "id", "aria-label"];
|
|
382
|
+
var DIAGNOSTIC_CANDIDATE_LIMIT = 5;
|
|
383
|
+
function findBy(selectorMap, predicate) {
|
|
384
|
+
for (const entry of selectorMap) {
|
|
385
|
+
if (predicate(entry[1])) return entry;
|
|
386
|
+
}
|
|
387
|
+
return void 0;
|
|
388
|
+
}
|
|
389
|
+
function candidateLabel(node) {
|
|
390
|
+
const label = node.axNode?.name ?? node.attributes["aria-label"] ?? node.attributes.name ?? node.attributes.id ?? "";
|
|
391
|
+
return `[${node.backendNodeId}]<${node.nodeName.toLowerCase()}>${label ? ` "${label}"` : ""}`;
|
|
392
|
+
}
|
|
393
|
+
function diagnosticFor(recorded, selectorMap) {
|
|
394
|
+
const tag = recorded.nodeName.toLowerCase();
|
|
395
|
+
const sought = recorded.axName ?? recorded.attributes["aria-label"] ?? recorded.attributes.name ?? recorded.attributes.id ?? recorded.xpath;
|
|
396
|
+
const similar = [...selectorMap.values()].filter((node) => node.nodeName.toLowerCase() === tag).slice(0, DIAGNOSTIC_CANDIDATE_LIMIT).map(candidateLabel);
|
|
397
|
+
const similarText = similar.length > 0 ? `Similar <${tag}> elements present: ${similar.join(", ")}.` : `No <${tag}> elements found on the current page.`;
|
|
398
|
+
return `Could not re-match recorded element <${tag}> "${sought}" (xpath: ${recorded.xpath}). ${similarText}`;
|
|
399
|
+
}
|
|
400
|
+
function rematchElement(recorded, selectorMap) {
|
|
401
|
+
const exact = findBy(selectorMap, (node) => elementHash(node) === recorded.elementHash);
|
|
402
|
+
if (exact) return { level: MatchLevel.Exact, backendNodeId: exact[0], node: exact[1] };
|
|
403
|
+
const stable = findBy(selectorMap, (node) => stableHash(node) === recorded.stableHash);
|
|
404
|
+
if (stable) return { level: MatchLevel.Stable, backendNodeId: stable[0], node: stable[1] };
|
|
405
|
+
const xp = findBy(selectorMap, (node) => nodeXpath(node) === recorded.xpath);
|
|
406
|
+
if (xp) return { level: MatchLevel.Xpath, backendNodeId: xp[0], node: xp[1] };
|
|
407
|
+
const recordedTag = recorded.nodeName.toLowerCase();
|
|
408
|
+
if (recorded.axName) {
|
|
409
|
+
const byAx = findBy(
|
|
410
|
+
selectorMap,
|
|
411
|
+
(node) => node.nodeName.toLowerCase() === recordedTag && node.axNode?.name === recorded.axName
|
|
412
|
+
);
|
|
413
|
+
if (byAx) return { level: MatchLevel.AxName, backendNodeId: byAx[0], node: byAx[1] };
|
|
414
|
+
}
|
|
415
|
+
for (const attr of UNIQUE_ATTRIBUTES) {
|
|
416
|
+
const wanted = recorded.attributes[attr];
|
|
417
|
+
if (!wanted) continue;
|
|
418
|
+
const matches = [];
|
|
419
|
+
for (const entry of selectorMap) {
|
|
420
|
+
if (entry[1].attributes[attr] === wanted && entry[1].nodeName.toLowerCase() === recordedTag)
|
|
421
|
+
matches.push(entry);
|
|
422
|
+
}
|
|
423
|
+
if (matches.length === 1) {
|
|
424
|
+
const [backendNodeId, node] = matches[0];
|
|
425
|
+
return { level: MatchLevel.Attribute, backendNodeId, node };
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (recorded.parentBranchHash) {
|
|
429
|
+
const structuralMatches = [];
|
|
430
|
+
for (const entry of selectorMap) {
|
|
431
|
+
if (parentBranchHash(entry[1]) === recorded.parentBranchHash) structuralMatches.push(entry);
|
|
432
|
+
}
|
|
433
|
+
if (structuralMatches.length === 1) {
|
|
434
|
+
const [backendNodeId, node] = structuralMatches[0];
|
|
435
|
+
return { level: MatchLevel.Structural, backendNodeId, node };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return { level: void 0, diagnostic: diagnosticFor(recorded, selectorMap) };
|
|
439
|
+
}
|
|
440
|
+
function looksLikeMenuOpener(el) {
|
|
441
|
+
const attrs = el.attributes;
|
|
442
|
+
if (attrs["aria-haspopup"] !== void 0 || attrs["aria-expanded"] !== void 0) return true;
|
|
443
|
+
const cls = (attrs.class ?? "").toLowerCase();
|
|
444
|
+
return cls.includes("menu") || cls.includes("dropdown");
|
|
445
|
+
}
|
|
446
|
+
function looksLikeMenuItem(el) {
|
|
447
|
+
const role = el.attributes.role?.toLowerCase();
|
|
448
|
+
if (role === "menuitem" || role === "option") return true;
|
|
449
|
+
const cls = (el.attributes.class ?? "").toLowerCase();
|
|
450
|
+
return cls.includes("menu-item") || cls.includes("dropdown-item");
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// src/agent/variable-detector.ts
|
|
454
|
+
var HTML5_TYPE_NAMES = {
|
|
455
|
+
email: "email",
|
|
456
|
+
tel: "phone",
|
|
457
|
+
date: "date",
|
|
458
|
+
number: "number",
|
|
459
|
+
url: "url"
|
|
460
|
+
};
|
|
461
|
+
var KEYWORD_MAP = [
|
|
462
|
+
["billing_address", "address"],
|
|
463
|
+
["billing-address", "address"],
|
|
464
|
+
["shipping_address", "address"],
|
|
465
|
+
["shipping-address", "address"],
|
|
466
|
+
["address", "address"],
|
|
467
|
+
["comment", "comment"],
|
|
468
|
+
["email", "email"],
|
|
469
|
+
["phone", "phone"],
|
|
470
|
+
["first_name", "first_name"],
|
|
471
|
+
["firstname", "first_name"],
|
|
472
|
+
["last_name", "last_name"],
|
|
473
|
+
["lastname", "last_name"],
|
|
474
|
+
["full_name", "full_name"],
|
|
475
|
+
["fullname", "full_name"],
|
|
476
|
+
["city", "city"],
|
|
477
|
+
["state", "state"],
|
|
478
|
+
["country", "country"],
|
|
479
|
+
["postal", "zip"],
|
|
480
|
+
["zip", "zip"],
|
|
481
|
+
["company", "company"],
|
|
482
|
+
["date", "date"],
|
|
483
|
+
["name", "full_name"]
|
|
484
|
+
];
|
|
485
|
+
var EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
486
|
+
var DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
487
|
+
var CAPITALIZED_WORDS_RE = /^[A-Z][a-zA-Z]{1,29}(\s[A-Z][a-zA-Z]{1,29})?$/;
|
|
488
|
+
function digitsOnly(value) {
|
|
489
|
+
return value.replace(/\D/g, "");
|
|
490
|
+
}
|
|
491
|
+
function attributeBasedName(el) {
|
|
492
|
+
if (!el) return void 0;
|
|
493
|
+
const type = el.attributes.type?.toLowerCase();
|
|
494
|
+
if (type && HTML5_TYPE_NAMES[type]) return HTML5_TYPE_NAMES[type];
|
|
495
|
+
const haystacks = [
|
|
496
|
+
el.attributes.id,
|
|
497
|
+
el.attributes.name,
|
|
498
|
+
el.attributes.placeholder,
|
|
499
|
+
el.attributes["aria-label"]
|
|
500
|
+
].filter((v) => !!v).map((v) => v.toLowerCase());
|
|
501
|
+
for (const [keyword, varName] of KEYWORD_MAP) {
|
|
502
|
+
if (haystacks.some((h) => h.includes(keyword))) return varName;
|
|
503
|
+
}
|
|
504
|
+
return void 0;
|
|
505
|
+
}
|
|
506
|
+
function patternBasedName(value) {
|
|
507
|
+
const trimmed = value.trim();
|
|
508
|
+
if (!trimmed) return void 0;
|
|
509
|
+
if (EMAIL_RE.test(trimmed)) return "email";
|
|
510
|
+
if (digitsOnly(trimmed).length >= 10) return "phone";
|
|
511
|
+
if (DATE_RE.test(trimmed)) return "date";
|
|
512
|
+
if (CAPITALIZED_WORDS_RE.test(trimmed)) return trimmed.includes(" ") ? "full_name" : "first_name";
|
|
513
|
+
if (/^\d{1,9}$/.test(trimmed)) return "number";
|
|
514
|
+
return void 0;
|
|
515
|
+
}
|
|
516
|
+
function detectVariables(history) {
|
|
517
|
+
const detected = [];
|
|
518
|
+
const nameForValue = /* @__PURE__ */ new Map();
|
|
519
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
520
|
+
const nameFor = (base, value) => {
|
|
521
|
+
const existing = nameForValue.get(value);
|
|
522
|
+
if (existing) return existing;
|
|
523
|
+
let candidate = base;
|
|
524
|
+
let n = 2;
|
|
525
|
+
while (usedNames.has(candidate)) {
|
|
526
|
+
candidate = `${base}_${n}`;
|
|
527
|
+
n += 1;
|
|
528
|
+
}
|
|
529
|
+
usedNames.add(candidate);
|
|
530
|
+
nameForValue.set(value, candidate);
|
|
531
|
+
return candidate;
|
|
532
|
+
};
|
|
533
|
+
history.history.forEach((step, stepIndex) => {
|
|
534
|
+
const actions = step.modelOutput?.action ?? [];
|
|
535
|
+
actions.forEach((raw, actionIndex) => {
|
|
536
|
+
const parsed = parseAction(raw);
|
|
537
|
+
if (!parsed) return;
|
|
538
|
+
const param = typeof parsed.params.text === "string" ? "text" : typeof parsed.params.query === "string" ? "query" : void 0;
|
|
539
|
+
if (!param) return;
|
|
540
|
+
const value = parsed.params[param];
|
|
541
|
+
if (!value) return;
|
|
542
|
+
const el = step.state.interactedElements?.[actionIndex];
|
|
543
|
+
const base = attributeBasedName(el) ?? patternBasedName(value);
|
|
544
|
+
if (!base) return;
|
|
545
|
+
detected.push({ name: nameFor(base, value), value, stepIndex, actionIndex, param });
|
|
546
|
+
});
|
|
547
|
+
});
|
|
548
|
+
return detected;
|
|
549
|
+
}
|
|
550
|
+
function substituteVariables(history, detected, values) {
|
|
551
|
+
const copy = history.toJSON();
|
|
552
|
+
const clone = structuredClone(copy);
|
|
553
|
+
for (const variable of detected) {
|
|
554
|
+
const replacement = values[variable.name];
|
|
555
|
+
if (replacement === void 0) continue;
|
|
556
|
+
const step = clone.history[variable.stepIndex];
|
|
557
|
+
const action = step?.modelOutput?.action[variable.actionIndex];
|
|
558
|
+
if (!action) continue;
|
|
559
|
+
const parsed = parseAction(action);
|
|
560
|
+
if (!parsed) continue;
|
|
561
|
+
const current = parsed.params[variable.param];
|
|
562
|
+
if (typeof current !== "string") continue;
|
|
563
|
+
parsed.params[variable.param] = current.split(variable.value).join(replacement);
|
|
564
|
+
}
|
|
565
|
+
return AgentHistoryList.fromJSON(clone);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/agent/replay.ts
|
|
569
|
+
import { readFile, writeFile } from "fs/promises";
|
|
570
|
+
import {
|
|
571
|
+
complete,
|
|
572
|
+
generateObject
|
|
573
|
+
} from "@ap3x/ai";
|
|
574
|
+
import { Type as Type2 } from "@sinclair/typebox";
|
|
575
|
+
import { Value } from "@sinclair/typebox/value";
|
|
576
|
+
var DEFAULT_MAX_RETRIES = 3;
|
|
577
|
+
var DEFAULT_RETRY_BASE_MS = 5e3;
|
|
578
|
+
var DEFAULT_RETRY_CAP_MS = 3e4;
|
|
579
|
+
var DEFAULT_STEP_DELAY_MS = 2e3;
|
|
580
|
+
var DEFAULT_MAX_STEP_INTERVAL_MS = 45e3;
|
|
581
|
+
var MENU_REOPEN_SETTLE_MS = 500;
|
|
582
|
+
var DEFAULT_STEP_TIMEOUT_MS = 18e4;
|
|
583
|
+
function realSleep(ms) {
|
|
584
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
585
|
+
}
|
|
586
|
+
function abortableSleep(ms, sleep, signal) {
|
|
587
|
+
if (!signal) return sleep(ms);
|
|
588
|
+
if (signal.aborted) return Promise.reject(new DOMException("Aborted", "AbortError"));
|
|
589
|
+
return new Promise((resolve, reject) => {
|
|
590
|
+
const onAbort = () => reject(new DOMException("Aborted", "AbortError"));
|
|
591
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
592
|
+
sleep(ms).then(
|
|
593
|
+
() => {
|
|
594
|
+
signal.removeEventListener("abort", onAbort);
|
|
595
|
+
resolve();
|
|
596
|
+
},
|
|
597
|
+
(error) => {
|
|
598
|
+
signal.removeEventListener("abort", onAbort);
|
|
599
|
+
reject(error);
|
|
600
|
+
}
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
var StepMetadataSchema = Type2.Object({
|
|
605
|
+
stepStartTime: Type2.Number(),
|
|
606
|
+
stepEndTime: Type2.Number(),
|
|
607
|
+
stepNumber: Type2.Number(),
|
|
608
|
+
stepInterval: Type2.Optional(Type2.Number())
|
|
609
|
+
});
|
|
610
|
+
var ActionResultSchema = Type2.Object({
|
|
611
|
+
isDone: Type2.Optional(Type2.Boolean()),
|
|
612
|
+
success: Type2.Optional(Type2.Boolean()),
|
|
613
|
+
error: Type2.Optional(Type2.String()),
|
|
614
|
+
extractedContent: Type2.Optional(Type2.String()),
|
|
615
|
+
includeExtractedContentOnlyOnce: Type2.Optional(Type2.Boolean()),
|
|
616
|
+
longTermMemory: Type2.Optional(Type2.String()),
|
|
617
|
+
attachments: Type2.Optional(Type2.Array(Type2.String())),
|
|
618
|
+
images: Type2.Optional(Type2.Array(Type2.Object({ name: Type2.String(), data: Type2.String() }))),
|
|
619
|
+
metadata: Type2.Optional(Type2.Record(Type2.String(), Type2.Unknown())),
|
|
620
|
+
includeInMemory: Type2.Optional(Type2.Boolean())
|
|
621
|
+
});
|
|
622
|
+
var InteractedElementSchema = Type2.Object({
|
|
623
|
+
nodeId: Type2.Number(),
|
|
624
|
+
backendNodeId: Type2.Number(),
|
|
625
|
+
frameId: Type2.Optional(Type2.String()),
|
|
626
|
+
nodeType: Type2.Number(),
|
|
627
|
+
nodeValue: Type2.String(),
|
|
628
|
+
nodeName: Type2.String(),
|
|
629
|
+
attributes: Type2.Record(Type2.String(), Type2.String()),
|
|
630
|
+
bounds: Type2.Optional(
|
|
631
|
+
Type2.Object({
|
|
632
|
+
x: Type2.Number(),
|
|
633
|
+
y: Type2.Number(),
|
|
634
|
+
width: Type2.Number(),
|
|
635
|
+
height: Type2.Number()
|
|
636
|
+
})
|
|
637
|
+
),
|
|
638
|
+
xpath: Type2.String(),
|
|
639
|
+
elementHash: Type2.String(),
|
|
640
|
+
stableHash: Type2.String(),
|
|
641
|
+
axName: Type2.Optional(Type2.String()),
|
|
642
|
+
// Optional: absent on history files recorded before W13c (structural rematch
|
|
643
|
+
// level has nothing to fall back to for those — the cascade just skips it).
|
|
644
|
+
parentBranchHash: Type2.Optional(Type2.String())
|
|
645
|
+
});
|
|
646
|
+
var BrowserStateHistorySchema = Type2.Object({
|
|
647
|
+
url: Type2.String(),
|
|
648
|
+
title: Type2.String(),
|
|
649
|
+
tabs: Type2.Array(
|
|
650
|
+
Type2.Object({
|
|
651
|
+
id: Type2.String(),
|
|
652
|
+
url: Type2.String(),
|
|
653
|
+
title: Type2.String(),
|
|
654
|
+
parentId: Type2.Optional(Type2.String())
|
|
655
|
+
})
|
|
656
|
+
),
|
|
657
|
+
screenshot: Type2.Optional(Type2.String()),
|
|
658
|
+
// `undefined` array entries (non-indexed actions: scroll/navigate/wait/done/…)
|
|
659
|
+
// serialize through `JSON.stringify` as `null` — `Type.Optional` inside
|
|
660
|
+
// `Type.Array` does NOT accept that (Optional only applies to object
|
|
661
|
+
// properties), so a real save→load round-trip must accept `null` explicitly.
|
|
662
|
+
// `AgentHistoryList.fromJSON` normalizes `null` back to `undefined` on load.
|
|
663
|
+
interactedElements: Type2.Optional(Type2.Array(Type2.Union([InteractedElementSchema, Type2.Null()])))
|
|
664
|
+
});
|
|
665
|
+
var AgentHistoryItemSchema = Type2.Object({
|
|
666
|
+
modelOutput: Type2.Optional(
|
|
667
|
+
Type2.Object({
|
|
668
|
+
thinking: Type2.Optional(Type2.String()),
|
|
669
|
+
evaluation_previous_goal: Type2.Optional(Type2.String()),
|
|
670
|
+
memory: Type2.Optional(Type2.String()),
|
|
671
|
+
next_goal: Type2.Optional(Type2.String()),
|
|
672
|
+
action: Type2.Array(Type2.Record(Type2.String(), Type2.Unknown()))
|
|
673
|
+
})
|
|
674
|
+
),
|
|
675
|
+
result: Type2.Array(ActionResultSchema),
|
|
676
|
+
state: BrowserStateHistorySchema,
|
|
677
|
+
metadata: Type2.Optional(StepMetadataSchema),
|
|
678
|
+
stateMessage: Type2.Optional(Type2.String())
|
|
679
|
+
});
|
|
680
|
+
var HistoryFileSchema = Type2.Object({
|
|
681
|
+
version: Type2.Literal(1),
|
|
682
|
+
history: Type2.Array(AgentHistoryItemSchema),
|
|
683
|
+
terminationReason: Type2.Optional(Type2.String())
|
|
684
|
+
});
|
|
685
|
+
function firstSchemaError(value) {
|
|
686
|
+
const [error] = [...Value.Errors(HistoryFileSchema, value)];
|
|
687
|
+
return error ? `${error.path || "/"}: ${error.message}` : "does not match the history-file schema";
|
|
688
|
+
}
|
|
689
|
+
async function saveHistoryToFile(history, filePath) {
|
|
690
|
+
await writeFile(filePath, JSON.stringify(history.toJSON(), null, 2), "utf8");
|
|
691
|
+
}
|
|
692
|
+
async function loadHistoryFromFile(filePath) {
|
|
693
|
+
const raw = await readFile(filePath, "utf8");
|
|
694
|
+
let parsed;
|
|
695
|
+
try {
|
|
696
|
+
parsed = JSON.parse(raw);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
throw new Error(
|
|
699
|
+
`History file "${filePath}" is not valid JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
700
|
+
);
|
|
701
|
+
}
|
|
702
|
+
if (!Value.Check(HistoryFileSchema, parsed)) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`History file "${filePath}" failed schema validation: ${firstSchemaError(parsed)}`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
const file = parsed;
|
|
708
|
+
file.history.forEach((item, i) => {
|
|
709
|
+
const stepNumber = item.metadata?.stepNumber ?? i + 1;
|
|
710
|
+
for (const action of item.modelOutput?.action ?? []) {
|
|
711
|
+
if (!parseAction(action)) {
|
|
712
|
+
throw new Error(
|
|
713
|
+
`History file "${filePath}" step ${stepNumber} has a malformed action (expected exactly one key): ${JSON.stringify(action)}`
|
|
714
|
+
);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
return AgentHistoryList.fromJSON(file);
|
|
719
|
+
}
|
|
720
|
+
var RerunSummarySchema = Type2.Object({
|
|
721
|
+
summary: Type2.String(),
|
|
722
|
+
success: Type2.Boolean(),
|
|
723
|
+
completionStatus: Type2.Union([
|
|
724
|
+
Type2.Literal("full"),
|
|
725
|
+
Type2.Literal("partial"),
|
|
726
|
+
Type2.Literal("failed")
|
|
727
|
+
])
|
|
728
|
+
});
|
|
729
|
+
function nonLlmSummary(succeeded, total) {
|
|
730
|
+
const completionStatus = total === 0 || succeeded === total ? "full" : succeeded === 0 ? "failed" : "partial";
|
|
731
|
+
return {
|
|
732
|
+
summary: `${succeeded}/${total} steps succeeded.`,
|
|
733
|
+
success: total > 0 && succeeded === total,
|
|
734
|
+
completionStatus
|
|
735
|
+
};
|
|
736
|
+
}
|
|
737
|
+
async function buildRerunSummary(task, steps, succeeded, total, model, signal, streamOptions) {
|
|
738
|
+
const fallback = nonLlmSummary(succeeded, total);
|
|
739
|
+
if (!model) return { ...fallback, summarySource: "fallback" };
|
|
740
|
+
const statLines = steps.map((s) => `Step ${s.stepNumber}: ${s.outcome}${s.error ? ` (${s.error})` : ""}`).join("\n");
|
|
741
|
+
const prompt = [
|
|
742
|
+
"You are summarizing a REPLAY of a previously recorded browser-agent run.",
|
|
743
|
+
`Original task: ${task}`,
|
|
744
|
+
`Result: ${succeeded}/${total} steps replayed successfully.`,
|
|
745
|
+
"Per-step outcomes:",
|
|
746
|
+
statLines,
|
|
747
|
+
"Write a short summary of what happened, whether the replay succeeded overall, and a completion status."
|
|
748
|
+
].join("\n\n");
|
|
749
|
+
const messages = [{ role: "user", content: prompt, timestamp: Date.now() }];
|
|
750
|
+
try {
|
|
751
|
+
const { object } = await generateObject(model, messages, RerunSummarySchema, {
|
|
752
|
+
...streamOptions,
|
|
753
|
+
signal
|
|
754
|
+
});
|
|
755
|
+
return { ...object, summarySource: "llm" };
|
|
756
|
+
} catch {
|
|
757
|
+
try {
|
|
758
|
+
const response = await complete(model, { messages }, { ...streamOptions, signal });
|
|
759
|
+
const text = response.content.filter((b) => b.type === "text").map((b) => b.text).join("").trim();
|
|
760
|
+
if (text)
|
|
761
|
+
return {
|
|
762
|
+
summary: text,
|
|
763
|
+
success: fallback.success,
|
|
764
|
+
completionStatus: fallback.completionStatus,
|
|
765
|
+
summarySource: "plain-text"
|
|
766
|
+
};
|
|
767
|
+
} catch {
|
|
768
|
+
}
|
|
769
|
+
return { ...fallback, summarySource: "fallback" };
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
var ReplayStepError = class extends Error {
|
|
773
|
+
constructor(message, results, matchLevels) {
|
|
774
|
+
super(message);
|
|
775
|
+
this.results = results;
|
|
776
|
+
this.matchLevels = matchLevels;
|
|
777
|
+
this.name = "ReplayStepError";
|
|
778
|
+
}
|
|
779
|
+
results;
|
|
780
|
+
matchLevels;
|
|
781
|
+
};
|
|
782
|
+
function buildCtx(deps, pageUrl, signal) {
|
|
783
|
+
return {
|
|
784
|
+
session: deps.session,
|
|
785
|
+
driver: deps.driver,
|
|
786
|
+
pageUrl,
|
|
787
|
+
fileSystem: deps.fileSystem,
|
|
788
|
+
extractionLlm: deps.extractionLlm,
|
|
789
|
+
availableFilePaths: deps.availableFilePaths ?? [],
|
|
790
|
+
hasSensitiveData: false,
|
|
791
|
+
signal
|
|
792
|
+
};
|
|
793
|
+
}
|
|
794
|
+
function stepIdentity(item) {
|
|
795
|
+
const actions = item.modelOutput?.action ?? [];
|
|
796
|
+
if (actions.length !== 1) return void 0;
|
|
797
|
+
const parsed = parseAction(actions[0]);
|
|
798
|
+
if (!parsed) return void 0;
|
|
799
|
+
const element = item.state.interactedElements?.[0];
|
|
800
|
+
if (!element) return void 0;
|
|
801
|
+
return { actionName: parsed.name, element };
|
|
802
|
+
}
|
|
803
|
+
function sameElement(a, b) {
|
|
804
|
+
return a.elementHash === b.elementHash || a.stableHash === b.stableHash || a.xpath === b.xpath;
|
|
805
|
+
}
|
|
806
|
+
async function replayStepOnce(item, deps, previousOpener, signal, sleep, reopenState) {
|
|
807
|
+
const actions = item.modelOutput?.action ?? [];
|
|
808
|
+
const state = await deps.driver.getState();
|
|
809
|
+
const matchLevels = [];
|
|
810
|
+
const results = [];
|
|
811
|
+
let queued = [];
|
|
812
|
+
const ctx = buildCtx(deps, state.url, signal);
|
|
813
|
+
const flush = async () => {
|
|
814
|
+
if (queued.length === 0) return;
|
|
815
|
+
const batch = queued;
|
|
816
|
+
queued = [];
|
|
817
|
+
const batchResults = await multiAct({
|
|
818
|
+
actions: batch,
|
|
819
|
+
registry: deps.registry,
|
|
820
|
+
ctx,
|
|
821
|
+
session: deps.session,
|
|
822
|
+
signal
|
|
823
|
+
});
|
|
824
|
+
results.push(...batchResults);
|
|
825
|
+
const failed = batchResults.find((r) => r.error !== void 0);
|
|
826
|
+
if (failed) throw new ReplayStepError(failed.error, results, matchLevels);
|
|
827
|
+
};
|
|
828
|
+
for (let i = 0; i < actions.length; i++) {
|
|
829
|
+
const raw = actions[i];
|
|
830
|
+
const parsed = parseAction(raw);
|
|
831
|
+
if (!parsed) {
|
|
832
|
+
matchLevels.push(void 0);
|
|
833
|
+
continue;
|
|
834
|
+
}
|
|
835
|
+
if (parsed.name === "extract_content") {
|
|
836
|
+
await flush();
|
|
837
|
+
const result = await deps.registry.execute("extract_content", parsed.params, ctx);
|
|
838
|
+
results.push(result);
|
|
839
|
+
matchLevels.push(void 0);
|
|
840
|
+
if (result.error !== void 0) throw new ReplayStepError(result.error, results, matchLevels);
|
|
841
|
+
continue;
|
|
842
|
+
}
|
|
843
|
+
const recorded = item.state.interactedElements?.[i];
|
|
844
|
+
if (recorded && typeof parsed.params.index === "number") {
|
|
845
|
+
let match = rematchElement(recorded, state.selectorMap);
|
|
846
|
+
if (!match.level && !reopenState.done && previousOpener && looksLikeMenuItem(recorded)) {
|
|
847
|
+
reopenState.done = true;
|
|
848
|
+
const openerMatch = rematchElement(previousOpener.recorded, state.selectorMap);
|
|
849
|
+
if (openerMatch.level) {
|
|
850
|
+
await deps.registry.execute(
|
|
851
|
+
"click",
|
|
852
|
+
{ ...previousOpener.params, index: openerMatch.backendNodeId },
|
|
853
|
+
ctx
|
|
854
|
+
);
|
|
855
|
+
await abortableSleep(MENU_REOPEN_SETTLE_MS, sleep, signal);
|
|
856
|
+
const fresh = await deps.driver.getState();
|
|
857
|
+
match = rematchElement(recorded, fresh.selectorMap);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (!match.level) {
|
|
861
|
+
matchLevels.push(void 0);
|
|
862
|
+
throw new ReplayStepError(
|
|
863
|
+
match.diagnostic,
|
|
864
|
+
[...results, stepResult({ error: match.diagnostic })],
|
|
865
|
+
matchLevels
|
|
866
|
+
);
|
|
867
|
+
}
|
|
868
|
+
matchLevels.push(match.level);
|
|
869
|
+
queued.push({ ...raw, [parsed.name]: { ...parsed.params, index: match.backendNodeId } });
|
|
870
|
+
} else {
|
|
871
|
+
matchLevels.push(void 0);
|
|
872
|
+
queued.push(raw);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
await flush();
|
|
876
|
+
return { matchLevels, results };
|
|
877
|
+
}
|
|
878
|
+
function backoffMs(attempt, base, cap) {
|
|
879
|
+
return Math.min(base * 2 ** (attempt - 1), cap);
|
|
880
|
+
}
|
|
881
|
+
async function rerunHistory(history, deps, options = {}) {
|
|
882
|
+
const maxRetries = options.maxRetries ?? DEFAULT_MAX_RETRIES;
|
|
883
|
+
const retryBaseMs = options.retryBaseMs ?? DEFAULT_RETRY_BASE_MS;
|
|
884
|
+
const retryCapMs = options.retryCapMs ?? DEFAULT_RETRY_CAP_MS;
|
|
885
|
+
const defaultStepDelayMs = options.defaultStepDelayMs ?? DEFAULT_STEP_DELAY_MS;
|
|
886
|
+
const maxStepIntervalMs = options.maxStepIntervalMs ?? DEFAULT_MAX_STEP_INTERVAL_MS;
|
|
887
|
+
const stepTimeoutMs = options.stepTimeoutMs ?? DEFAULT_STEP_TIMEOUT_MS;
|
|
888
|
+
const skipFailures = options.skipFailures ?? false;
|
|
889
|
+
const sleep = options.sleep ?? realSleep;
|
|
890
|
+
const signal = options.signal;
|
|
891
|
+
const log = deps.logger ?? silentLogger;
|
|
892
|
+
const steps = [];
|
|
893
|
+
let lastSuccessful;
|
|
894
|
+
let previousOpener;
|
|
895
|
+
for (let i = 0; i < history.history.length; i++) {
|
|
896
|
+
const item = history.history[i];
|
|
897
|
+
const stepNumber = item.metadata?.stepNumber ?? i + 1;
|
|
898
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
899
|
+
if (skipFailures && item.result.some((r) => r.error !== void 0)) {
|
|
900
|
+
steps.push({ stepNumber, outcome: "skipped-original-error", matchLevels: [], attempts: 0 });
|
|
901
|
+
continue;
|
|
902
|
+
}
|
|
903
|
+
const actions = item.modelOutput?.action ?? [];
|
|
904
|
+
if (actions.length === 0) {
|
|
905
|
+
steps.push({ stepNumber, outcome: "skipped-no-actions", matchLevels: [], attempts: 0 });
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
const identity = stepIdentity(item);
|
|
909
|
+
if (identity && lastSuccessful && identity.actionName === lastSuccessful.actionName && sameElement(identity.element, lastSuccessful.element)) {
|
|
910
|
+
steps.push({ stepNumber, outcome: "skipped-redundant", matchLevels: [], attempts: 0 });
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
if (i > 0) {
|
|
914
|
+
const savedMs = (item.metadata?.stepInterval ?? 0) * 1e3;
|
|
915
|
+
await abortableSleep(
|
|
916
|
+
savedMs > 0 ? Math.min(savedMs, maxStepIntervalMs) : defaultStepDelayMs,
|
|
917
|
+
sleep,
|
|
918
|
+
signal
|
|
919
|
+
);
|
|
920
|
+
}
|
|
921
|
+
let attempt = 0;
|
|
922
|
+
let outcome;
|
|
923
|
+
const reopenState = { done: false };
|
|
924
|
+
while (!outcome) {
|
|
925
|
+
const attemptSignal = anySignal(signal, AbortSignal.timeout(stepTimeoutMs));
|
|
926
|
+
try {
|
|
927
|
+
const { matchLevels, results } = await replayStepOnce(
|
|
928
|
+
item,
|
|
929
|
+
deps,
|
|
930
|
+
previousOpener,
|
|
931
|
+
attemptSignal,
|
|
932
|
+
sleep,
|
|
933
|
+
reopenState
|
|
934
|
+
);
|
|
935
|
+
outcome = { stepNumber, outcome: "replayed", matchLevels, results, attempts: attempt + 1 };
|
|
936
|
+
lastSuccessful = identity;
|
|
937
|
+
const openerCandidate = item.state.interactedElements?.[0];
|
|
938
|
+
previousOpener = actions.length === 1 && openerCandidate && looksLikeMenuOpener(openerCandidate) ? {
|
|
939
|
+
params: parseAction(actions[0])?.params ?? {},
|
|
940
|
+
recorded: openerCandidate
|
|
941
|
+
} : void 0;
|
|
942
|
+
} catch (error) {
|
|
943
|
+
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
|
|
944
|
+
const stepError = attemptSignal?.aborted && !(error instanceof ReplayStepError) ? new ReplayStepError(
|
|
945
|
+
`step ${stepNumber} attempt timed out after ${stepTimeoutMs}ms`,
|
|
946
|
+
[],
|
|
947
|
+
[]
|
|
948
|
+
) : error;
|
|
949
|
+
if (!(stepError instanceof ReplayStepError)) throw stepError;
|
|
950
|
+
attempt += 1;
|
|
951
|
+
log.warn("replay step failed", { step: stepNumber, attempt, error: stepError.message });
|
|
952
|
+
if (attempt >= maxRetries) {
|
|
953
|
+
lastSuccessful = void 0;
|
|
954
|
+
const message = `step ${stepNumber} failed after ${attempt} attempt(s): ${stepError.message}`;
|
|
955
|
+
if (!skipFailures) {
|
|
956
|
+
throw new Error(`Replay aborted: ${message}`);
|
|
957
|
+
}
|
|
958
|
+
outcome = {
|
|
959
|
+
stepNumber,
|
|
960
|
+
outcome: "failed",
|
|
961
|
+
matchLevels: stepError.matchLevels,
|
|
962
|
+
results: stepError.results,
|
|
963
|
+
error: stepError.message,
|
|
964
|
+
attempts: attempt
|
|
965
|
+
};
|
|
966
|
+
} else {
|
|
967
|
+
await abortableSleep(backoffMs(attempt, retryBaseMs, retryCapMs), sleep, signal);
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
steps.push(outcome);
|
|
972
|
+
}
|
|
973
|
+
const succeededSteps = steps.filter((s) => s.outcome === "replayed").length;
|
|
974
|
+
const totalSteps = steps.length;
|
|
975
|
+
const { summary, success, completionStatus, summarySource } = await buildRerunSummary(
|
|
976
|
+
deps.task ?? "",
|
|
977
|
+
steps,
|
|
978
|
+
succeededSteps,
|
|
979
|
+
totalSteps,
|
|
980
|
+
deps.model,
|
|
981
|
+
signal,
|
|
982
|
+
deps.streamOptions
|
|
983
|
+
);
|
|
984
|
+
return { steps, succeededSteps, totalSteps, summary, success, completionStatus, summarySource };
|
|
985
|
+
}
|
|
986
|
+
async function loadAndRerun(filePath, deps, options = {}) {
|
|
987
|
+
const loaded = await loadHistoryFromFile(filePath);
|
|
988
|
+
const detected = detectVariables(loaded);
|
|
989
|
+
const history = options.variables ? substituteVariables(loaded, detected, options.variables) : loaded;
|
|
990
|
+
const report = await rerunHistory(history, deps, options);
|
|
991
|
+
return { ...report, detectedVariables: detected };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// src/index.ts
|
|
995
|
+
var PACKAGE = "@ap3x/browser-agent";
|
|
996
|
+
var VERSION = "0.1.0";
|
|
997
|
+
export {
|
|
998
|
+
Agent,
|
|
999
|
+
AgentEventStream,
|
|
1000
|
+
AgentHistoryList,
|
|
1001
|
+
BUILTIN_ACTIONS,
|
|
1002
|
+
BrowserAgentBackend,
|
|
1003
|
+
COMPACT_EVERY_N_STEPS,
|
|
1004
|
+
COMPACT_TRIGGER_CHARS,
|
|
1005
|
+
CaptchaWatchdog,
|
|
1006
|
+
FileParseError,
|
|
1007
|
+
FileSystem,
|
|
1008
|
+
HistoryFileSchema,
|
|
1009
|
+
JudgementResultSchema,
|
|
1010
|
+
LoopDetector,
|
|
1011
|
+
MessageManager,
|
|
1012
|
+
PACKAGE,
|
|
1013
|
+
Registry,
|
|
1014
|
+
StepLoop,
|
|
1015
|
+
UsageLedger,
|
|
1016
|
+
VERSION,
|
|
1017
|
+
agentStep,
|
|
1018
|
+
anySignal,
|
|
1019
|
+
applicablePlaceholders,
|
|
1020
|
+
assembleNarration,
|
|
1021
|
+
budgetNudge,
|
|
1022
|
+
buildAgentOutputSchema,
|
|
1023
|
+
collectingLogger,
|
|
1024
|
+
connectMcpClient,
|
|
1025
|
+
createBrowserMcpServer,
|
|
1026
|
+
createDefaultRegistry,
|
|
1027
|
+
createLogger,
|
|
1028
|
+
defineAction,
|
|
1029
|
+
detectCaptcha,
|
|
1030
|
+
detectVariables,
|
|
1031
|
+
doneResult,
|
|
1032
|
+
emptyLlmResponse,
|
|
1033
|
+
encodeFramesToGif,
|
|
1034
|
+
generateRunGif,
|
|
1035
|
+
generateRunVideoGif,
|
|
1036
|
+
importMcpTools,
|
|
1037
|
+
invalidLlmResponse,
|
|
1038
|
+
isDoneResult,
|
|
1039
|
+
judgeRun,
|
|
1040
|
+
loadAndRerun,
|
|
1041
|
+
loadHistoryFromFile,
|
|
1042
|
+
looksLikeMenuItem,
|
|
1043
|
+
looksLikeMenuOpener,
|
|
1044
|
+
maskOutbound,
|
|
1045
|
+
maskSecretMap,
|
|
1046
|
+
multiAct,
|
|
1047
|
+
parseAction,
|
|
1048
|
+
perceive,
|
|
1049
|
+
readExternalFile,
|
|
1050
|
+
registerBuiltins,
|
|
1051
|
+
rematchElement,
|
|
1052
|
+
rerunHistory,
|
|
1053
|
+
resolveSecretsForUrl,
|
|
1054
|
+
resultsAreDone,
|
|
1055
|
+
runBrowserMcpServer,
|
|
1056
|
+
sanitizeFilename,
|
|
1057
|
+
saveHistoryToFile,
|
|
1058
|
+
scriptedLlm,
|
|
1059
|
+
sensitiveDataDescription,
|
|
1060
|
+
silentLogger,
|
|
1061
|
+
stepDurationSeconds,
|
|
1062
|
+
stepResult,
|
|
1063
|
+
substituteSecrets,
|
|
1064
|
+
substituteVariables,
|
|
1065
|
+
synthesizeParamSchema,
|
|
1066
|
+
toTypedAction,
|
|
1067
|
+
totp,
|
|
1068
|
+
transportErrorResponse,
|
|
1069
|
+
truncatedLlmResponse
|
|
1070
|
+
};
|