@mercuryo-ai/agentbrowse 0.2.57 → 0.2.61
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 +76 -57
- package/dist/browser-session-state.d.ts +39 -0
- package/dist/browser-session-state.d.ts.map +1 -1
- package/dist/browser-session-state.js +63 -1
- package/dist/command-name.js +1 -1
- package/dist/commands/act.d.ts.map +1 -1
- package/dist/commands/act.js +540 -528
- package/dist/commands/action-executor-helpers.d.ts.map +1 -1
- package/dist/commands/action-executor-helpers.js +10 -8
- package/dist/commands/attach.d.ts.map +1 -1
- package/dist/commands/attach.js +5 -10
- package/dist/commands/browser-connection-failure.d.ts +9 -0
- package/dist/commands/browser-connection-failure.d.ts.map +1 -0
- package/dist/commands/browser-connection-failure.js +15 -0
- package/dist/commands/browser-status.d.ts.map +1 -1
- package/dist/commands/browser-status.js +26 -30
- package/dist/commands/click-activation-policy.d.ts.map +1 -1
- package/dist/commands/click-activation-policy.js +6 -2
- package/dist/commands/close.d.ts.map +1 -1
- package/dist/commands/close.js +5 -0
- package/dist/commands/extract.d.ts.map +1 -1
- package/dist/commands/extract.js +147 -144
- package/dist/commands/launch.d.ts +0 -1
- package/dist/commands/launch.d.ts.map +1 -1
- package/dist/commands/launch.js +13 -16
- package/dist/commands/navigate.d.ts.map +1 -1
- package/dist/commands/navigate.js +79 -73
- package/dist/commands/observe-inventory.d.ts +6 -1
- package/dist/commands/observe-inventory.d.ts.map +1 -1
- package/dist/commands/observe-inventory.js +331 -8
- package/dist/commands/observe-persistence.d.ts.map +1 -1
- package/dist/commands/observe-persistence.js +2 -0
- package/dist/commands/observe-projection.d.ts +3 -2
- package/dist/commands/observe-projection.d.ts.map +1 -1
- package/dist/commands/observe-projection.js +1 -0
- package/dist/commands/observe-protected.d.ts +3 -1
- package/dist/commands/observe-protected.d.ts.map +1 -1
- package/dist/commands/observe-protected.js +23 -1
- package/dist/commands/observe-semantics.d.ts.map +1 -1
- package/dist/commands/observe-semantics.js +70 -0
- package/dist/commands/observe.d.ts +1 -0
- package/dist/commands/observe.d.ts.map +1 -1
- package/dist/commands/observe.js +260 -270
- package/dist/commands/screenshot.d.ts.map +1 -1
- package/dist/commands/screenshot.js +50 -64
- package/dist/control-semantics.d.ts.map +1 -1
- package/dist/control-semantics.js +5 -0
- package/dist/date-value-normalization.d.ts +16 -0
- package/dist/date-value-normalization.d.ts.map +1 -0
- package/dist/date-value-normalization.js +117 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -24
- package/dist/library.d.ts +5 -1
- package/dist/library.d.ts.map +1 -1
- package/dist/library.js +4 -1
- package/dist/protected-fill.d.ts +3 -2
- package/dist/protected-fill.d.ts.map +1 -1
- package/dist/protected-fill.js +46 -7
- package/dist/runtime-protected-state.d.ts.map +1 -1
- package/dist/runtime-protected-state.js +8 -1
- package/dist/runtime-state.d.ts +11 -0
- package/dist/runtime-state.d.ts.map +1 -1
- package/dist/secrets/form-matcher.d.ts +1 -2
- package/dist/secrets/form-matcher.d.ts.map +1 -1
- package/dist/secrets/form-matcher.js +125 -119
- package/dist/secrets/matching-helpers.d.ts +13 -0
- package/dist/secrets/matching-helpers.d.ts.map +1 -0
- package/dist/secrets/matching-helpers.js +147 -0
- package/dist/secrets/observed-field-resolution.d.ts +43 -0
- package/dist/secrets/observed-field-resolution.d.ts.map +1 -0
- package/dist/secrets/observed-field-resolution.js +223 -0
- package/dist/secrets/protected-field-semantics.d.ts.map +1 -1
- package/dist/secrets/protected-field-semantics.js +3 -2
- package/dist/secrets/protected-fill.d.ts +3 -1
- package/dist/secrets/protected-fill.d.ts.map +1 -1
- package/dist/secrets/protected-fill.js +31 -0
- package/dist/secrets/protected-value-adapters.d.ts.map +1 -1
- package/dist/secrets/protected-value-adapters.js +14 -22
- package/dist/secrets/types.d.ts +3 -0
- package/dist/secrets/types.d.ts.map +1 -1
- package/dist/sticky-owner-host-entry.d.ts +2 -0
- package/dist/sticky-owner-host-entry.d.ts.map +1 -0
- package/dist/sticky-owner-host-entry.js +97 -0
- package/dist/sticky-owner.d.ts +15 -0
- package/dist/sticky-owner.d.ts.map +1 -0
- package/dist/sticky-owner.js +431 -0
- package/docs/README.md +15 -2
- package/docs/api-reference.md +13 -3
- package/docs/assistive-runtime.md +63 -7
- package/docs/configuration.md +48 -8
- package/docs/getting-started.md +42 -9
- package/docs/integration-checklist.md +8 -7
- package/docs/protected-fill.md +40 -7
- package/docs/testing.md +4 -3
- package/docs/troubleshooting.md +126 -36
- package/examples/README.md +9 -2
- package/package.json +8 -3
- package/dist/protected-fill-browser.d.ts +0 -22
- package/dist/protected-fill-browser.d.ts.map +0 -1
- package/dist/protected-fill-browser.js +0 -52
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { existsSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { chromium } from 'playwright-core';
|
|
8
|
+
import { buildStickyOwnerMetadata, isSessionAlive, resolveBrowserSessionId, } from './browser-session-state.js';
|
|
9
|
+
import { connectPlaywright, disconnectPlaywright } from './playwright-runtime.js';
|
|
10
|
+
const HOST_READY_TIMEOUT_MS = 5_000;
|
|
11
|
+
const HOST_READY_POLL_MS = 50;
|
|
12
|
+
const HOST_CONNECT_TIMEOUT_MS = 3_000;
|
|
13
|
+
const HOST_STOP_TIMEOUT_MS = 1_000;
|
|
14
|
+
const DEFAULT_STICKY_OWNER_TTL_MS = 30 * 60 * 1_000;
|
|
15
|
+
const STICKY_OWNER_HEARTBEAT_FLOOR_MS = 5_000;
|
|
16
|
+
const STICKY_OWNER_HEARTBEAT_CEILING_MS = 60_000;
|
|
17
|
+
const inProcessOwners = new WeakMap();
|
|
18
|
+
function resolveStickyOwnerBootstrapMode() {
|
|
19
|
+
const explicit = process.env.AGENTBROWSE_STICKY_OWNER_MODE?.trim().toLowerCase();
|
|
20
|
+
if (explicit === 'detached_process') {
|
|
21
|
+
return 'detached_process';
|
|
22
|
+
}
|
|
23
|
+
if (explicit === 'in_process') {
|
|
24
|
+
return 'in_process';
|
|
25
|
+
}
|
|
26
|
+
if (process.env.VITEST) {
|
|
27
|
+
return 'in_process';
|
|
28
|
+
}
|
|
29
|
+
return 'detached_process';
|
|
30
|
+
}
|
|
31
|
+
function createHostId() {
|
|
32
|
+
return `owner_${randomUUID().replace(/-/g, '')}`;
|
|
33
|
+
}
|
|
34
|
+
function resolveStickyOwnerTtlMs() {
|
|
35
|
+
const raw = process.env.AGENTBROWSE_STICKY_OWNER_TTL_MS?.trim();
|
|
36
|
+
if (!raw) {
|
|
37
|
+
return DEFAULT_STICKY_OWNER_TTL_MS;
|
|
38
|
+
}
|
|
39
|
+
const ttlMs = Number(raw);
|
|
40
|
+
if (!Number.isFinite(ttlMs) || ttlMs <= 0) {
|
|
41
|
+
return DEFAULT_STICKY_OWNER_TTL_MS;
|
|
42
|
+
}
|
|
43
|
+
return ttlMs;
|
|
44
|
+
}
|
|
45
|
+
function getStickyOwnerTouchPath(hostId) {
|
|
46
|
+
return join(tmpdir(), `agentbrowse-sticky-owner-${hostId}.touch`);
|
|
47
|
+
}
|
|
48
|
+
function buildInProcessStickyOwner(session, options = {}) {
|
|
49
|
+
return buildStickyOwnerMetadata({
|
|
50
|
+
hostId: options.hostId ?? session.stickyOwner?.hostId ?? createHostId(),
|
|
51
|
+
state: options.state ?? 'active',
|
|
52
|
+
startedAt: options.startedAt ?? session.stickyOwner?.startedAt ?? new Date().toISOString(),
|
|
53
|
+
browserSessionId: resolveBrowserSessionId(session),
|
|
54
|
+
transport: {
|
|
55
|
+
type: 'in_process',
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function readStickyOwnerLastUsedMs(stickyOwner) {
|
|
60
|
+
if (stickyOwner.touchPath) {
|
|
61
|
+
try {
|
|
62
|
+
if (existsSync(stickyOwner.touchPath)) {
|
|
63
|
+
return statSync(stickyOwner.touchPath).mtimeMs;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Fall through to metadata timestamps.
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const candidate = stickyOwner.lastUsedAt ?? stickyOwner.startedAt;
|
|
71
|
+
const parsed = Date.parse(candidate);
|
|
72
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
73
|
+
}
|
|
74
|
+
function isStickyOwnerExpired(stickyOwner) {
|
|
75
|
+
if (stickyOwner.transport.type !== 'playwright_bind') {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
const ttlMs = stickyOwner.ttlMs ?? DEFAULT_STICKY_OWNER_TTL_MS;
|
|
79
|
+
return Date.now() - readStickyOwnerLastUsedMs(stickyOwner) >= ttlMs;
|
|
80
|
+
}
|
|
81
|
+
function removeStickyOwnerTouchFile(stickyOwner) {
|
|
82
|
+
if (!stickyOwner?.touchPath) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
rmSync(stickyOwner.touchPath, { force: true });
|
|
86
|
+
}
|
|
87
|
+
function touchStickyOwnerLease(session) {
|
|
88
|
+
if (!session.stickyOwner || session.stickyOwner.transport.type !== 'playwright_bind') {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const touchedAt = new Date().toISOString();
|
|
92
|
+
if (session.stickyOwner.touchPath) {
|
|
93
|
+
writeFileSync(session.stickyOwner.touchPath, touchedAt);
|
|
94
|
+
}
|
|
95
|
+
session.stickyOwner = {
|
|
96
|
+
...session.stickyOwner,
|
|
97
|
+
lastUsedAt: touchedAt,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function startStickyOwnerLeaseHeartbeat(session) {
|
|
101
|
+
if (!session.stickyOwner || session.stickyOwner.transport.type !== 'playwright_bind') {
|
|
102
|
+
return () => undefined;
|
|
103
|
+
}
|
|
104
|
+
const ttlMs = session.stickyOwner.ttlMs ?? DEFAULT_STICKY_OWNER_TTL_MS;
|
|
105
|
+
const heartbeatMs = Math.max(STICKY_OWNER_HEARTBEAT_FLOOR_MS, Math.min(Math.floor(ttlMs / 3), STICKY_OWNER_HEARTBEAT_CEILING_MS));
|
|
106
|
+
const timer = setInterval(() => {
|
|
107
|
+
try {
|
|
108
|
+
touchStickyOwnerLease(session);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// Best-effort heartbeat; restore path will handle stale or dead owners later.
|
|
112
|
+
}
|
|
113
|
+
}, heartbeatMs);
|
|
114
|
+
timer.unref?.();
|
|
115
|
+
return () => clearInterval(timer);
|
|
116
|
+
}
|
|
117
|
+
function markStickyOwnerState(session, state) {
|
|
118
|
+
if (!session.stickyOwner) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
session.stickyOwner = {
|
|
122
|
+
...session.stickyOwner,
|
|
123
|
+
state,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function isProcessAlive(pid) {
|
|
127
|
+
if (typeof pid !== 'number' || !Number.isFinite(pid) || pid <= 0) {
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
process.kill(pid, 0);
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return false;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async function sleep(ms) {
|
|
139
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
async function waitForProcessExit(pid, timeoutMs) {
|
|
142
|
+
const deadline = Date.now() + timeoutMs;
|
|
143
|
+
while (Date.now() < deadline) {
|
|
144
|
+
if (!isProcessAlive(pid)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
await sleep(50);
|
|
148
|
+
}
|
|
149
|
+
return !isProcessAlive(pid);
|
|
150
|
+
}
|
|
151
|
+
async function canConnectToStickyOwnerEndpoint(endpoint) {
|
|
152
|
+
let browser = null;
|
|
153
|
+
try {
|
|
154
|
+
browser = await chromium.connect(endpoint, {
|
|
155
|
+
timeout: HOST_CONNECT_TIMEOUT_MS,
|
|
156
|
+
});
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
finally {
|
|
163
|
+
if (browser) {
|
|
164
|
+
await disconnectPlaywright(browser);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function getStickyOwnerHostEntryPath() {
|
|
169
|
+
const currentPath = fileURLToPath(import.meta.url);
|
|
170
|
+
const suffix = currentPath.endsWith('.ts') ? '.ts' : '.js';
|
|
171
|
+
return fileURLToPath(new URL(`./sticky-owner-host-entry${suffix}`, import.meta.url));
|
|
172
|
+
}
|
|
173
|
+
function getStickyOwnerHostSpawnArgs(entryPath, manifestPath, cdpUrl, hostId, browserSessionId, touchPath, ttlMs) {
|
|
174
|
+
const args = [
|
|
175
|
+
entryPath,
|
|
176
|
+
'--manifest',
|
|
177
|
+
manifestPath,
|
|
178
|
+
'--cdp-url',
|
|
179
|
+
cdpUrl,
|
|
180
|
+
'--host-id',
|
|
181
|
+
hostId,
|
|
182
|
+
'--browser-session-id',
|
|
183
|
+
browserSessionId,
|
|
184
|
+
'--touch-path',
|
|
185
|
+
touchPath,
|
|
186
|
+
'--ttl-ms',
|
|
187
|
+
String(ttlMs),
|
|
188
|
+
];
|
|
189
|
+
if (entryPath.endsWith('.ts')) {
|
|
190
|
+
return ['--import', 'tsx', ...args];
|
|
191
|
+
}
|
|
192
|
+
return args;
|
|
193
|
+
}
|
|
194
|
+
async function waitForStickyOwnerManifest(manifestPath) {
|
|
195
|
+
const deadline = Date.now() + HOST_READY_TIMEOUT_MS;
|
|
196
|
+
while (Date.now() < deadline) {
|
|
197
|
+
if (existsSync(manifestPath)) {
|
|
198
|
+
const payload = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
199
|
+
if (typeof payload.endpoint === 'string' &&
|
|
200
|
+
payload.endpoint.trim().length > 0 &&
|
|
201
|
+
typeof payload.pid === 'number' &&
|
|
202
|
+
Number.isFinite(payload.pid) &&
|
|
203
|
+
payload.pid > 0 &&
|
|
204
|
+
typeof payload.startedAt === 'string' &&
|
|
205
|
+
payload.startedAt.trim().length > 0) {
|
|
206
|
+
rmSync(manifestPath, { force: true });
|
|
207
|
+
return {
|
|
208
|
+
endpoint: payload.endpoint,
|
|
209
|
+
pid: payload.pid,
|
|
210
|
+
startedAt: payload.startedAt,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
await sleep(HOST_READY_POLL_MS);
|
|
215
|
+
}
|
|
216
|
+
throw new Error('sticky_owner_bootstrap_timeout');
|
|
217
|
+
}
|
|
218
|
+
async function bootstrapDetachedStickyOwner(session) {
|
|
219
|
+
const hostId = session.stickyOwner?.hostId ?? createHostId();
|
|
220
|
+
const browserSessionId = resolveBrowserSessionId(session);
|
|
221
|
+
const ttlMs = session.stickyOwner?.ttlMs ?? resolveStickyOwnerTtlMs();
|
|
222
|
+
const manifestPath = join(tmpdir(), `agentbrowse-sticky-owner-${hostId}.json`);
|
|
223
|
+
const touchPath = session.stickyOwner?.touchPath ?? getStickyOwnerTouchPath(hostId);
|
|
224
|
+
const touchedAt = new Date().toISOString();
|
|
225
|
+
writeFileSync(touchPath, touchedAt);
|
|
226
|
+
const entryPath = getStickyOwnerHostEntryPath();
|
|
227
|
+
const child = spawn(process.execPath, getStickyOwnerHostSpawnArgs(entryPath, manifestPath, session.cdpUrl, hostId, browserSessionId, touchPath, ttlMs), {
|
|
228
|
+
detached: true,
|
|
229
|
+
stdio: 'ignore',
|
|
230
|
+
});
|
|
231
|
+
child.unref();
|
|
232
|
+
try {
|
|
233
|
+
const ready = await waitForStickyOwnerManifest(manifestPath);
|
|
234
|
+
return buildStickyOwnerMetadata({
|
|
235
|
+
hostId,
|
|
236
|
+
state: 'active',
|
|
237
|
+
startedAt: ready.startedAt,
|
|
238
|
+
lastUsedAt: touchedAt,
|
|
239
|
+
ttlMs,
|
|
240
|
+
touchPath,
|
|
241
|
+
browserSessionId,
|
|
242
|
+
pid: ready.pid,
|
|
243
|
+
transport: {
|
|
244
|
+
type: 'playwright_bind',
|
|
245
|
+
endpoint: ready.endpoint,
|
|
246
|
+
mode: 'pipe',
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (isProcessAlive(child.pid)) {
|
|
252
|
+
process.kill(child.pid, 'SIGTERM');
|
|
253
|
+
await waitForProcessExit(child.pid, HOST_STOP_TIMEOUT_MS).catch(() => undefined);
|
|
254
|
+
}
|
|
255
|
+
rmSync(manifestPath, { force: true });
|
|
256
|
+
rmSync(touchPath, { force: true });
|
|
257
|
+
throw error;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
async function ensureInProcessOwner(session) {
|
|
261
|
+
const existing = inProcessOwners.get(session);
|
|
262
|
+
if (existing) {
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
const browser = await connectPlaywright(session.cdpUrl);
|
|
266
|
+
const owner = {
|
|
267
|
+
browser,
|
|
268
|
+
hostId: session.stickyOwner?.hostId ?? createHostId(),
|
|
269
|
+
startedAt: session.stickyOwner?.startedAt ?? new Date().toISOString(),
|
|
270
|
+
};
|
|
271
|
+
inProcessOwners.set(session, owner);
|
|
272
|
+
session.stickyOwner = buildInProcessStickyOwner(session, {
|
|
273
|
+
hostId: owner.hostId,
|
|
274
|
+
startedAt: owner.startedAt,
|
|
275
|
+
});
|
|
276
|
+
return owner;
|
|
277
|
+
}
|
|
278
|
+
export async function initializeBrowserSessionOwner(session) {
|
|
279
|
+
if (resolveStickyOwnerBootstrapMode() === 'in_process') {
|
|
280
|
+
session.stickyOwner = buildInProcessStickyOwner(session, {
|
|
281
|
+
state: 'active',
|
|
282
|
+
});
|
|
283
|
+
return session;
|
|
284
|
+
}
|
|
285
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
286
|
+
return session;
|
|
287
|
+
}
|
|
288
|
+
export async function terminateBrowserSessionOwner(session) {
|
|
289
|
+
if (!session?.stickyOwner) {
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
if (session.stickyOwner.transport.type === 'in_process') {
|
|
293
|
+
const owner = inProcessOwners.get(session);
|
|
294
|
+
inProcessOwners.delete(session);
|
|
295
|
+
if (owner) {
|
|
296
|
+
await disconnectPlaywright(owner.browser);
|
|
297
|
+
}
|
|
298
|
+
markStickyOwnerState(session, 'dead');
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const ownerPid = session.stickyOwner.pid;
|
|
302
|
+
if (isProcessAlive(ownerPid)) {
|
|
303
|
+
process.kill(ownerPid, 'SIGTERM');
|
|
304
|
+
const exited = await waitForProcessExit(ownerPid, HOST_STOP_TIMEOUT_MS);
|
|
305
|
+
if (!exited && isProcessAlive(ownerPid)) {
|
|
306
|
+
process.kill(ownerPid, 'SIGKILL');
|
|
307
|
+
await waitForProcessExit(ownerPid, HOST_STOP_TIMEOUT_MS).catch(() => undefined);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
removeStickyOwnerTouchFile(session.stickyOwner);
|
|
311
|
+
markStickyOwnerState(session, 'dead');
|
|
312
|
+
}
|
|
313
|
+
export async function restoreBrowserSessionOwner(session) {
|
|
314
|
+
const stickyOwner = session.stickyOwner;
|
|
315
|
+
if (!stickyOwner) {
|
|
316
|
+
if (resolveStickyOwnerBootstrapMode() === 'in_process') {
|
|
317
|
+
try {
|
|
318
|
+
await ensureInProcessOwner(session);
|
|
319
|
+
return {
|
|
320
|
+
success: true,
|
|
321
|
+
session,
|
|
322
|
+
restored: true,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return {
|
|
327
|
+
success: false,
|
|
328
|
+
reason: 'sticky_owner_unrecoverable',
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
if (!(await isSessionAlive(session))) {
|
|
333
|
+
return {
|
|
334
|
+
success: false,
|
|
335
|
+
reason: 'sticky_owner_unrecoverable',
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
try {
|
|
339
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
340
|
+
return {
|
|
341
|
+
success: true,
|
|
342
|
+
session,
|
|
343
|
+
restored: true,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return {
|
|
348
|
+
success: false,
|
|
349
|
+
reason: 'sticky_owner_unrecoverable',
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (stickyOwner.transport.type === 'in_process') {
|
|
354
|
+
try {
|
|
355
|
+
await ensureInProcessOwner(session);
|
|
356
|
+
return {
|
|
357
|
+
success: true,
|
|
358
|
+
session,
|
|
359
|
+
restored: false,
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
return {
|
|
364
|
+
success: false,
|
|
365
|
+
reason: 'sticky_owner_unrecoverable',
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (stickyOwner.state === 'active' &&
|
|
370
|
+
!isStickyOwnerExpired(stickyOwner) &&
|
|
371
|
+
isProcessAlive(stickyOwner.pid) &&
|
|
372
|
+
(await canConnectToStickyOwnerEndpoint(stickyOwner.transport.endpoint))) {
|
|
373
|
+
return {
|
|
374
|
+
success: true,
|
|
375
|
+
session,
|
|
376
|
+
restored: false,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
await terminateBrowserSessionOwner(session);
|
|
380
|
+
if (!(await isSessionAlive(session))) {
|
|
381
|
+
return {
|
|
382
|
+
success: false,
|
|
383
|
+
reason: 'sticky_owner_unrecoverable',
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
try {
|
|
387
|
+
markStickyOwnerState(session, 'recovering');
|
|
388
|
+
session.stickyOwner = await bootstrapDetachedStickyOwner(session);
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
session,
|
|
392
|
+
restored: true,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
session.stickyOwner = {
|
|
397
|
+
...stickyOwner,
|
|
398
|
+
state: 'dead',
|
|
399
|
+
};
|
|
400
|
+
return {
|
|
401
|
+
success: false,
|
|
402
|
+
reason: 'sticky_owner_unrecoverable',
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
export async function withStickyOwnerBrowser(session, operation) {
|
|
407
|
+
const stickyOwner = session.stickyOwner;
|
|
408
|
+
if (!stickyOwner || stickyOwner.transport.type === 'playwright_bind') {
|
|
409
|
+
const restored = await restoreBrowserSessionOwner(session);
|
|
410
|
+
if (!restored.success || !session.stickyOwner) {
|
|
411
|
+
throw new Error('sticky_owner_unrecoverable');
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if (session.stickyOwner?.transport.type === 'playwright_bind') {
|
|
415
|
+
touchStickyOwnerLease(session);
|
|
416
|
+
const browser = await chromium.connect(session.stickyOwner.transport.endpoint, {
|
|
417
|
+
timeout: HOST_CONNECT_TIMEOUT_MS,
|
|
418
|
+
});
|
|
419
|
+
const stopHeartbeat = startStickyOwnerLeaseHeartbeat(session);
|
|
420
|
+
try {
|
|
421
|
+
return await operation(browser);
|
|
422
|
+
}
|
|
423
|
+
finally {
|
|
424
|
+
stopHeartbeat();
|
|
425
|
+
touchStickyOwnerLease(session);
|
|
426
|
+
await disconnectPlaywright(browser);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const owner = await ensureInProcessOwner(session);
|
|
430
|
+
return operation(owner.browser);
|
|
431
|
+
}
|
package/docs/README.md
CHANGED
|
@@ -2,7 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
Public guides for `@mercuryo-ai/agentbrowse`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**Reading order for a new integration:**
|
|
6
|
+
|
|
7
|
+
1. [Package README](../README.md) — 5-minute overview and Quick Start.
|
|
8
|
+
2. [getting-started.md](./getting-started.md) — extended tutorial that
|
|
9
|
+
turns the Quick Start into a working model for every main API.
|
|
10
|
+
3. [api-reference.md](./api-reference.md) — lookup for types, result
|
|
11
|
+
shapes, and error codes (keep open while coding).
|
|
12
|
+
4. Only-when-needed: [configuration.md](./configuration.md),
|
|
13
|
+
[assistive-runtime.md](./assistive-runtime.md),
|
|
14
|
+
[protected-fill.md](./protected-fill.md).
|
|
15
|
+
5. Before shipping: [integration-checklist.md](./integration-checklist.md)
|
|
16
|
+
and [troubleshooting.md](./troubleshooting.md).
|
|
17
|
+
|
|
18
|
+
Full list:
|
|
6
19
|
|
|
7
20
|
- [getting-started.md](./getting-started.md)
|
|
8
21
|
The first guide to read after the package README. It explains the normal
|
|
@@ -18,7 +31,7 @@ Start here:
|
|
|
18
31
|
- [protected-fill.md](./protected-fill.md)
|
|
19
32
|
How protected fill works and when to use it instead of a normal fill action.
|
|
20
33
|
- [integration-checklist.md](./integration-checklist.md)
|
|
21
|
-
|
|
34
|
+
Checklist for packages and services that integrate AgentBrowse.
|
|
22
35
|
- [testing.md](./testing.md)
|
|
23
36
|
Stable testing helpers for packages that wrap AgentBrowse.
|
|
24
37
|
- [troubleshooting.md](./troubleshooting.md)
|
package/docs/api-reference.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# AgentBrowse API Reference
|
|
2
2
|
|
|
3
3
|
This page is the compact reference for the public `@mercuryo-ai/agentbrowse`
|
|
4
|
-
library
|
|
4
|
+
library API.
|
|
5
5
|
|
|
6
6
|
## Main Functions
|
|
7
7
|
|
|
@@ -113,6 +113,16 @@ branching:
|
|
|
113
113
|
|
|
114
114
|
These arrays back the exported `*ErrorCode` types.
|
|
115
115
|
|
|
116
|
+
## Error Classes
|
|
117
|
+
|
|
118
|
+
For code paths that want `instanceof` checks instead of string matching on
|
|
119
|
+
`error` codes:
|
|
120
|
+
|
|
121
|
+
- `AgentbrowseAssistiveRuntimeMissingError` — thrown when an assistive-only
|
|
122
|
+
command runs without an assistive runtime configured.
|
|
123
|
+
- `AssistiveStructuredOutputTruncatedError` — thrown when the assistive
|
|
124
|
+
runtime returns a structured output that was cut off mid-response.
|
|
125
|
+
|
|
116
126
|
## Core Result Shapes
|
|
117
127
|
|
|
118
128
|
All main commands use the same top-level pattern:
|
|
@@ -213,7 +223,7 @@ fields and does not treat the schema stringification format as part of the API.
|
|
|
213
223
|
|
|
214
224
|
## Assistive Runtime Types
|
|
215
225
|
|
|
216
|
-
The root package exports the
|
|
226
|
+
The root package exports the types that describe the assistive runtime:
|
|
217
227
|
|
|
218
228
|
- `AgentbrowseAssistiveChatCompletionRequest`
|
|
219
229
|
- `AgentbrowseAssistiveChatCompletionOptions`
|
|
@@ -229,7 +239,7 @@ Your adapter receives `args.options.messages`, optional
|
|
|
229
239
|
|
|
230
240
|
## Testing Subpath
|
|
231
241
|
|
|
232
|
-
The package publishes a dedicated testing
|
|
242
|
+
The package publishes a dedicated `/testing` subpath:
|
|
233
243
|
|
|
234
244
|
```ts
|
|
235
245
|
import {
|
|
@@ -18,15 +18,13 @@ Today, assistive behavior matters mainly for:
|
|
|
18
18
|
If you only need browser actions and normal page inspection, you can ignore
|
|
19
19
|
assistive runtime completely.
|
|
20
20
|
|
|
21
|
-
## The Runtime
|
|
21
|
+
## The Runtime Shape
|
|
22
22
|
|
|
23
23
|
AgentBrowse does not ship a built-in OpenAI or OpenRouter adapter.
|
|
24
24
|
|
|
25
25
|
Instead, you provide a small runtime object with one responsibility:
|
|
26
26
|
create a chat-completions client.
|
|
27
27
|
|
|
28
|
-
The shape is:
|
|
29
|
-
|
|
30
28
|
```ts
|
|
31
29
|
{
|
|
32
30
|
createLlmClient: () => ({
|
|
@@ -38,8 +36,66 @@ The shape is:
|
|
|
38
36
|
}
|
|
39
37
|
```
|
|
40
38
|
|
|
41
|
-
|
|
42
|
-
|
|
39
|
+
Any OpenAI-compatible chat-completions backend can work, as long as your
|
|
40
|
+
adapter returns the expected response shape.
|
|
41
|
+
|
|
42
|
+
### `options.response_model`
|
|
43
|
+
|
|
44
|
+
When AgentBrowse needs structured JSON (from `extract(...)` or certain
|
|
45
|
+
goal-driven `observe(...)` paths), it passes `response_model` describing
|
|
46
|
+
the expected output:
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
{
|
|
50
|
+
name: string; // short identifier for the schema (e.g. "checkout_total")
|
|
51
|
+
schema: ZodType; // a Zod schema AgentBrowse wants the LLM to conform to
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Your adapter is responsible for translating `response_model.schema` into
|
|
56
|
+
whatever structured-output mechanism your provider supports. For OpenAI,
|
|
57
|
+
this usually means:
|
|
58
|
+
|
|
59
|
+
1. Convert the Zod schema to JSON Schema (e.g. with
|
|
60
|
+
`@browserbasehq/stagehand`'s `toJsonSchema`, or your own helper).
|
|
61
|
+
2. Pass it as `response_format: { type: 'json_schema', json_schema: { ... } }`.
|
|
62
|
+
3. Parse `choices[0].message.content` and return `{ data, usage? }`.
|
|
63
|
+
|
|
64
|
+
If your provider does not support structured outputs, you can prompt the
|
|
65
|
+
model to return JSON and parse the result yourself — as long as the
|
|
66
|
+
returned `data` conforms to the schema, AgentBrowse does not care how it
|
|
67
|
+
was produced.
|
|
68
|
+
|
|
69
|
+
### `options.image`
|
|
70
|
+
|
|
71
|
+
For vision-assisted extraction, AgentBrowse passes a screenshot:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
{
|
|
75
|
+
buffer: Buffer; // raw image bytes
|
|
76
|
+
description?: string; // short hint about what's in the image
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Most adapters encode the buffer as a base64 data URL and inject it as a
|
|
81
|
+
user-turn content block. See the helper below.
|
|
82
|
+
|
|
83
|
+
### Errors
|
|
84
|
+
|
|
85
|
+
Your adapter throws on provider errors; AgentBrowse maps them to typed
|
|
86
|
+
failures on the calling command:
|
|
87
|
+
|
|
88
|
+
- HTTP errors (401/403/429/5xx): throw a normal `Error` with a descriptive
|
|
89
|
+
message. AgentBrowse surfaces it as a command failure.
|
|
90
|
+
- Missing/empty content: throw; do not return `{ data: null }`.
|
|
91
|
+
- Truncated structured output: AgentBrowse detects this and throws
|
|
92
|
+
`AssistiveStructuredOutputTruncatedError` — usually a signal to raise
|
|
93
|
+
`maxOutputTokens`.
|
|
94
|
+
|
|
95
|
+
The `AgentbrowseAssistiveLlmClient` type exported from the root package
|
|
96
|
+
describes the client object returned by `createLlmClient()`. Use it when you
|
|
97
|
+
want your adapter to conform to the expected shape without redefining it
|
|
98
|
+
by hand.
|
|
43
99
|
|
|
44
100
|
## Recommended Setup
|
|
45
101
|
|
|
@@ -203,5 +259,5 @@ import {
|
|
|
203
259
|
} from '@mercuryo-ai/agentbrowse/testing';
|
|
204
260
|
```
|
|
205
261
|
|
|
206
|
-
That helper installs a fetch-backed runtime with the same public
|
|
207
|
-
runtime
|
|
262
|
+
That helper installs a fetch-backed runtime with the same public
|
|
263
|
+
assistive-runtime shape used by the main package.
|
package/docs/configuration.md
CHANGED
|
@@ -15,6 +15,12 @@ Most applications can start with this mental model:
|
|
|
15
15
|
2. keep the returned `session` in memory
|
|
16
16
|
3. pass that `session` into later calls
|
|
17
17
|
|
|
18
|
+
Both bootstrap the same sticky-owner lifecycle. AgentBrowse may keep that
|
|
19
|
+
owner in-process or in an internal detached host, but consumers do not manage
|
|
20
|
+
that host directly. Detached hosts default to a 30 minute TTL and may be
|
|
21
|
+
recreated on the next browser command if the browser session itself is still
|
|
22
|
+
alive.
|
|
23
|
+
|
|
18
24
|
You only need more configuration when you want one of these:
|
|
19
25
|
|
|
20
26
|
- custom LLM integration
|
|
@@ -40,8 +46,12 @@ const attached = await attach(remoteCdpUrl, {
|
|
|
40
46
|
});
|
|
41
47
|
```
|
|
42
48
|
|
|
43
|
-
|
|
44
|
-
generic CDP-attached browser session.
|
|
49
|
+
The provider label is metadata only — AgentBrowse treats the connection as
|
|
50
|
+
a generic CDP-attached browser session regardless of the label.
|
|
51
|
+
|
|
52
|
+
`attach(...)` creates the same sticky-owner metadata as `launch(...)`. Later
|
|
53
|
+
browser commands reuse that owner and only attempt a fresh root attach again
|
|
54
|
+
as a repair path after owner loss.
|
|
45
55
|
|
|
46
56
|
## Client Configuration
|
|
47
57
|
|
|
@@ -68,19 +78,46 @@ configuration is the cleaner embedded pattern.
|
|
|
68
78
|
Persistence is optional. Use it when you want to restore a browser session
|
|
69
79
|
after a process restart.
|
|
70
80
|
|
|
81
|
+
Persisted session files store browser identity plus versioned sticky-owner
|
|
82
|
+
metadata. They do not serialize a live Playwright connection.
|
|
83
|
+
|
|
71
84
|
### Default Store
|
|
72
85
|
|
|
73
86
|
```ts
|
|
74
|
-
import { loadBrowserSession, saveBrowserSession } from '@mercuryo-ai/agentbrowse';
|
|
87
|
+
import { loadBrowserSession, saveBrowserSession, status } from '@mercuryo-ai/agentbrowse';
|
|
75
88
|
|
|
76
89
|
saveBrowserSession(session);
|
|
77
90
|
const restored = loadBrowserSession();
|
|
91
|
+
|
|
92
|
+
// `null` means there is no usable persisted session. That includes
|
|
93
|
+
// incompatible reconnect-era records and incomplete owner metadata.
|
|
94
|
+
if (restored) {
|
|
95
|
+
const check = await status(restored);
|
|
96
|
+
if (!check.alive) {
|
|
97
|
+
// The session is no longer reachable. Discard and relaunch or re-attach.
|
|
98
|
+
}
|
|
99
|
+
}
|
|
78
100
|
```
|
|
79
101
|
|
|
80
102
|
Default path:
|
|
81
103
|
|
|
82
104
|
`~/.agentbrowse/browse-session.json`
|
|
83
105
|
|
|
106
|
+
If the detached owner host is gone but the underlying browser session is still
|
|
107
|
+
alive, the first command after restore may repair ownership. If the browser is
|
|
108
|
+
gone, AgentBrowse fails closed and you should start a fresh session.
|
|
109
|
+
|
|
110
|
+
### Sticky Owner TTL
|
|
111
|
+
|
|
112
|
+
Detached sticky-owner hosts use a bounded lifetime by default:
|
|
113
|
+
|
|
114
|
+
- default TTL: `30` minutes
|
|
115
|
+
- env override: `AGENTBROWSE_STICKY_OWNER_TTL_MS=<milliseconds>`
|
|
116
|
+
|
|
117
|
+
This TTL is a resource guard for the detached owner host, not for the browser
|
|
118
|
+
session itself. If the TTL expires and the browser is still reachable, the next
|
|
119
|
+
browser command may bootstrap a fresh owner and continue.
|
|
120
|
+
|
|
84
121
|
### Custom Store
|
|
85
122
|
|
|
86
123
|
For embedded apps, prefer an explicit store root:
|
|
@@ -99,6 +136,10 @@ store.delete();
|
|
|
99
136
|
|
|
100
137
|
This avoids hidden machine-level coupling to `~/.agentbrowse`.
|
|
101
138
|
|
|
139
|
+
`store.load()` follows the same contract as `loadBrowserSession()`: it returns
|
|
140
|
+
`null` for missing files, incompatible old records, or unusable sticky-owner
|
|
141
|
+
metadata.
|
|
142
|
+
|
|
102
143
|
## Proxy Configuration
|
|
103
144
|
|
|
104
145
|
The clearest way to use a proxy is to pass it directly to `launch(...)`.
|
|
@@ -276,12 +317,11 @@ if your diagnostics implementation throws.
|
|
|
276
317
|
|
|
277
318
|
## Process-Global Convenience Helpers
|
|
278
319
|
|
|
279
|
-
AgentBrowse
|
|
320
|
+
For small scripts and quick experiments, AgentBrowse exposes process-global
|
|
321
|
+
helpers:
|
|
280
322
|
|
|
281
323
|
- `configureAgentbrowseAssistiveRuntime(...)`
|
|
282
324
|
- `configureAgentbrowseDiagnostics(...)`
|
|
283
325
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
For embedded production usage, client-scoped configuration is the better
|
|
287
|
-
default.
|
|
326
|
+
For embedded production usage, prefer client-scoped configuration through
|
|
327
|
+
`createAgentbrowseClient(...)`.
|