@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.
Files changed (100) hide show
  1. package/README.md +76 -57
  2. package/dist/browser-session-state.d.ts +39 -0
  3. package/dist/browser-session-state.d.ts.map +1 -1
  4. package/dist/browser-session-state.js +63 -1
  5. package/dist/command-name.js +1 -1
  6. package/dist/commands/act.d.ts.map +1 -1
  7. package/dist/commands/act.js +540 -528
  8. package/dist/commands/action-executor-helpers.d.ts.map +1 -1
  9. package/dist/commands/action-executor-helpers.js +10 -8
  10. package/dist/commands/attach.d.ts.map +1 -1
  11. package/dist/commands/attach.js +5 -10
  12. package/dist/commands/browser-connection-failure.d.ts +9 -0
  13. package/dist/commands/browser-connection-failure.d.ts.map +1 -0
  14. package/dist/commands/browser-connection-failure.js +15 -0
  15. package/dist/commands/browser-status.d.ts.map +1 -1
  16. package/dist/commands/browser-status.js +26 -30
  17. package/dist/commands/click-activation-policy.d.ts.map +1 -1
  18. package/dist/commands/click-activation-policy.js +6 -2
  19. package/dist/commands/close.d.ts.map +1 -1
  20. package/dist/commands/close.js +5 -0
  21. package/dist/commands/extract.d.ts.map +1 -1
  22. package/dist/commands/extract.js +147 -144
  23. package/dist/commands/launch.d.ts +0 -1
  24. package/dist/commands/launch.d.ts.map +1 -1
  25. package/dist/commands/launch.js +13 -16
  26. package/dist/commands/navigate.d.ts.map +1 -1
  27. package/dist/commands/navigate.js +79 -73
  28. package/dist/commands/observe-inventory.d.ts +6 -1
  29. package/dist/commands/observe-inventory.d.ts.map +1 -1
  30. package/dist/commands/observe-inventory.js +331 -8
  31. package/dist/commands/observe-persistence.d.ts.map +1 -1
  32. package/dist/commands/observe-persistence.js +2 -0
  33. package/dist/commands/observe-projection.d.ts +3 -2
  34. package/dist/commands/observe-projection.d.ts.map +1 -1
  35. package/dist/commands/observe-projection.js +1 -0
  36. package/dist/commands/observe-protected.d.ts +3 -1
  37. package/dist/commands/observe-protected.d.ts.map +1 -1
  38. package/dist/commands/observe-protected.js +23 -1
  39. package/dist/commands/observe-semantics.d.ts.map +1 -1
  40. package/dist/commands/observe-semantics.js +70 -0
  41. package/dist/commands/observe.d.ts +1 -0
  42. package/dist/commands/observe.d.ts.map +1 -1
  43. package/dist/commands/observe.js +260 -270
  44. package/dist/commands/screenshot.d.ts.map +1 -1
  45. package/dist/commands/screenshot.js +50 -64
  46. package/dist/control-semantics.d.ts.map +1 -1
  47. package/dist/control-semantics.js +5 -0
  48. package/dist/date-value-normalization.d.ts +16 -0
  49. package/dist/date-value-normalization.d.ts.map +1 -0
  50. package/dist/date-value-normalization.js +117 -0
  51. package/dist/index.d.ts.map +1 -1
  52. package/dist/index.js +5 -24
  53. package/dist/library.d.ts +5 -1
  54. package/dist/library.d.ts.map +1 -1
  55. package/dist/library.js +4 -1
  56. package/dist/protected-fill.d.ts +3 -2
  57. package/dist/protected-fill.d.ts.map +1 -1
  58. package/dist/protected-fill.js +46 -7
  59. package/dist/runtime-protected-state.d.ts.map +1 -1
  60. package/dist/runtime-protected-state.js +8 -1
  61. package/dist/runtime-state.d.ts +11 -0
  62. package/dist/runtime-state.d.ts.map +1 -1
  63. package/dist/secrets/form-matcher.d.ts +1 -2
  64. package/dist/secrets/form-matcher.d.ts.map +1 -1
  65. package/dist/secrets/form-matcher.js +125 -119
  66. package/dist/secrets/matching-helpers.d.ts +13 -0
  67. package/dist/secrets/matching-helpers.d.ts.map +1 -0
  68. package/dist/secrets/matching-helpers.js +147 -0
  69. package/dist/secrets/observed-field-resolution.d.ts +43 -0
  70. package/dist/secrets/observed-field-resolution.d.ts.map +1 -0
  71. package/dist/secrets/observed-field-resolution.js +223 -0
  72. package/dist/secrets/protected-field-semantics.d.ts.map +1 -1
  73. package/dist/secrets/protected-field-semantics.js +3 -2
  74. package/dist/secrets/protected-fill.d.ts +3 -1
  75. package/dist/secrets/protected-fill.d.ts.map +1 -1
  76. package/dist/secrets/protected-fill.js +31 -0
  77. package/dist/secrets/protected-value-adapters.d.ts.map +1 -1
  78. package/dist/secrets/protected-value-adapters.js +14 -22
  79. package/dist/secrets/types.d.ts +3 -0
  80. package/dist/secrets/types.d.ts.map +1 -1
  81. package/dist/sticky-owner-host-entry.d.ts +2 -0
  82. package/dist/sticky-owner-host-entry.d.ts.map +1 -0
  83. package/dist/sticky-owner-host-entry.js +97 -0
  84. package/dist/sticky-owner.d.ts +15 -0
  85. package/dist/sticky-owner.d.ts.map +1 -0
  86. package/dist/sticky-owner.js +431 -0
  87. package/docs/README.md +15 -2
  88. package/docs/api-reference.md +13 -3
  89. package/docs/assistive-runtime.md +63 -7
  90. package/docs/configuration.md +48 -8
  91. package/docs/getting-started.md +42 -9
  92. package/docs/integration-checklist.md +8 -7
  93. package/docs/protected-fill.md +40 -7
  94. package/docs/testing.md +4 -3
  95. package/docs/troubleshooting.md +126 -36
  96. package/examples/README.md +9 -2
  97. package/package.json +8 -3
  98. package/dist/protected-fill-browser.d.ts +0 -22
  99. package/dist/protected-fill-browser.d.ts.map +0 -1
  100. 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
- Start here:
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
- Current contract checklist for packages and services that integrate AgentBrowse.
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)
@@ -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 surface.
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 current assistive runtime contract:
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 surface:
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 Contract
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
- That means any OpenAI-compatible chat-completions backend can work, as long as
42
- your adapter returns the expected response shape.
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 assistive
207
- runtime contract used by the main package.
262
+ That helper installs a fetch-backed runtime with the same public
263
+ assistive-runtime shape used by the main package.
@@ -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
- That provider label is metadata only. AgentBrowse still treats this as a
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 still exposes process-global convenience functions such as:
320
+ For small scripts and quick experiments, AgentBrowse exposes process-global
321
+ helpers:
280
322
 
281
323
  - `configureAgentbrowseAssistiveRuntime(...)`
282
324
  - `configureAgentbrowseDiagnostics(...)`
283
325
 
284
- These are useful for small scripts and quick experiments.
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(...)`.