pi-agent-browser-native 0.2.32 → 0.2.34

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 (63) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +61 -20
  3. package/docs/ARCHITECTURE.md +9 -2
  4. package/docs/COMMAND_REFERENCE.md +45 -14
  5. package/docs/ELECTRON.md +23 -4
  6. package/docs/RELEASE.md +15 -5
  7. package/docs/REQUIREMENTS.md +1 -1
  8. package/docs/SUPPORT_MATRIX.md +36 -22
  9. package/docs/TOOL_CONTRACT.md +90 -31
  10. package/extensions/agent-browser/index.ts +407 -4373
  11. package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
  12. package/extensions/agent-browser/lib/input-modes/job.ts +265 -0
  13. package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
  14. package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
  15. package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
  16. package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
  17. package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
  18. package/extensions/agent-browser/lib/input-modes.ts +44 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +762 -0
  20. package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
  21. package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
  22. package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +736 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +413 -0
  24. package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
  25. package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +482 -0
  26. package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
  27. package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
  28. package/extensions/agent-browser/lib/playbook.ts +22 -20
  29. package/extensions/agent-browser/lib/process.ts +106 -4
  30. package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
  31. package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
  32. package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
  33. package/extensions/agent-browser/lib/results/categories.ts +106 -0
  34. package/extensions/agent-browser/lib/results/contracts.ts +220 -0
  35. package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
  36. package/extensions/agent-browser/lib/results/envelope.ts +2 -1
  37. package/extensions/agent-browser/lib/results/network.ts +64 -0
  38. package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
  39. package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
  40. package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
  41. package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
  42. package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
  43. package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
  44. package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
  45. package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
  46. package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
  47. package/extensions/agent-browser/lib/results/presentation/registry.ts +182 -0
  48. package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
  49. package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
  50. package/extensions/agent-browser/lib/results/presentation.ts +96 -2403
  51. package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
  52. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
  53. package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
  54. package/extensions/agent-browser/lib/results/shared.ts +17 -789
  55. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
  56. package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
  57. package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
  58. package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
  59. package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
  60. package/extensions/agent-browser/lib/results/text.ts +40 -0
  61. package/extensions/agent-browser/lib/results.ts +16 -5
  62. package/extensions/agent-browser/lib/session-page-state.ts +486 -0
  63. package/package.json +2 -1
@@ -1,792 +1,20 @@
1
1
  /**
2
- * Purpose: Share stable result-rendering types and small data-shaping helpers across the focused result modules.
3
- * Responsibilities: Define upstream envelope/presentation types, provide safe string utilities, and expose lightweight text helpers used by envelope parsing, snapshot compaction, and presentation rendering.
4
- * Scope: Shared result helpers only; higher-level parsing, snapshot compaction, and image attachment orchestration live in neighboring modules.
5
- * Usage: Imported by the focused result modules that back the public `lib/results.ts` facade.
6
- * Invariants/Assumptions: Helpers stay generic, side-effect free, and small enough to reuse without reintroducing a new god module.
2
+ * Purpose: Compatibility barrel for focused result modules.
3
+ * Responsibilities: Preserve the historical `./results/shared.js` import surface while delegating all logic to focused files.
4
+ * Scope: Re-exports only; do not add runtime policy here.
5
+ * Usage: Existing internal imports may keep using this path during migration, while new code should prefer focused modules.
6
+ * Invariants/Assumptions: This file intentionally contains no business logic so `shared` cannot grow back into a catch-all module.
7
7
  */
8
8
 
9
- export interface AgentBrowserEnvelope {
10
- data?: unknown;
11
- error?: unknown;
12
- success: boolean;
13
- }
14
-
15
- export interface AgentBrowserBatchResult {
16
- command?: string[];
17
- error?: unknown;
18
- result?: unknown;
19
- success?: boolean;
20
- }
21
-
22
- export type AgentBrowserResultCategory = "failure" | "success";
23
-
24
- export type AgentBrowserSuccessCategory = "artifact-saved" | "artifact-unverified" | "completed" | "inspection";
25
-
26
- export type AgentBrowserFailureCategory =
27
- | "aborted"
28
- | "cleanup-failed"
29
- | "confirmation-required"
30
- | "download-not-verified"
31
- | "missing-binary"
32
- | "parse-failure"
33
- | "policy-blocked"
34
- | "qa-failure"
35
- | "selector-not-found"
36
- | "selector-unsupported"
37
- | "stale-ref"
38
- | "tab-drift"
39
- | "timeout"
40
- | "upstream-error"
41
- | "validation-error";
42
-
43
- export interface AgentBrowserResultCategoryDetails {
44
- failureCategory?: AgentBrowserFailureCategory;
45
- resultCategory: AgentBrowserResultCategory;
46
- successCategory?: AgentBrowserSuccessCategory;
47
- }
48
-
49
- export interface AgentBrowserPageChangeSummary {
50
- artifactCount?: number;
51
- changeType: "artifact" | "confirmation" | "mutation" | "navigation";
52
- command?: string;
53
- nextActionIds?: string[];
54
- savedFilePath?: string;
55
- summary: string;
56
- title?: string;
57
- url?: string;
58
- }
59
-
60
- export interface AgentBrowserNextAction {
61
- artifactPath?: string;
62
- id: string;
63
- params?: {
64
- args?: string[];
65
- electron?: {
66
- action: "cleanup" | "list" | "launch" | "probe" | "status";
67
- all?: boolean;
68
- handoff?: "connect" | "snapshot" | "tabs";
69
- launchId?: string;
70
- };
71
- networkSourceLookup?: {
72
- filter?: string;
73
- requestId?: string;
74
- session?: string;
75
- url?: string;
76
- };
77
- sessionMode?: "auto" | "fresh";
78
- stdin?: string;
79
- };
80
- reason: string;
81
- safety?: string;
82
- tool: "agent_browser";
83
- }
84
-
85
- export type FileArtifactKind = "download" | "file" | "har" | "image" | "pdf" | "profile" | "trace" | "video";
86
-
87
- export type FileArtifactStatus = "missing" | "repaired-from-temp" | "saved" | "upstream-temp-only";
88
-
89
- export interface FileArtifactMetadata {
90
- absolutePath: string;
91
- artifactType?: FileArtifactKind;
92
- command?: string;
93
- cwd?: string;
94
- exists?: boolean;
95
- extension?: string;
96
- kind: FileArtifactKind;
97
- mediaType?: string;
98
- path: string;
99
- requestedPath?: string;
100
- session?: string;
101
- sizeBytes?: number;
102
- status?: FileArtifactStatus;
103
- subcommand?: string;
104
- tempPath?: string;
105
- }
106
-
107
- export type ArtifactVerificationState = "missing" | "pending" | "unverified" | "verified";
108
-
109
- export interface ArtifactVerificationEntry {
110
- absolutePath?: string;
111
- exists?: boolean;
112
- kind: FileArtifactKind | "spill";
113
- limitation?: string;
114
- mediaType?: string;
115
- path: string;
116
- requestedPath?: string;
117
- retentionState?: ArtifactRetentionState;
118
- sizeBytes?: number;
119
- state: ArtifactVerificationState;
120
- status?: FileArtifactStatus;
121
- storageScope?: ArtifactStorageScope;
122
- }
123
-
124
- export interface ArtifactVerificationSummary {
125
- artifacts: ArtifactVerificationEntry[];
126
- missingCount: number;
127
- pendingCount: number;
128
- unverifiedCount: number;
129
- verified: boolean;
130
- verifiedCount: number;
131
- }
132
-
133
- export interface SavedFilePresentationDetails {
134
- command: "download" | "pdf" | "wait";
135
- kind: "download" | "pdf";
136
- metadata?: Record<string, unknown>;
137
- path: string;
138
- subcommand?: string;
139
- }
140
-
141
- export type ArtifactRetentionState = "evicted" | "ephemeral" | "live" | "missing";
142
-
143
- export type ArtifactStorageScope = "explicit-path" | "persistent-session" | "process-temp";
144
-
145
- export interface SessionArtifactManifestEntry {
146
- absolutePath?: string;
147
- command?: string;
148
- createdAtMs: number;
149
- cwd?: string;
150
- evictedAtMs?: number;
151
- exists?: boolean;
152
- extension?: string;
153
- kind: FileArtifactKind | "spill";
154
- mediaType?: string;
155
- path: string;
156
- requestedPath?: string;
157
- retentionState: ArtifactRetentionState;
158
- session?: string;
159
- sizeBytes?: number;
160
- storageScope: ArtifactStorageScope;
161
- subcommand?: string;
162
- }
163
-
164
- export interface SessionArtifactManifest {
165
- entries: SessionArtifactManifestEntry[];
166
- evictedCount: number;
167
- liveCount: number;
168
- maxEntries: number;
169
- updatedAtMs: number;
170
- version: 1;
171
- }
172
-
173
- export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
174
- export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
175
- export const DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES = 100;
176
-
177
- function parsePositiveSafeInteger(value: string | undefined): number | undefined {
178
- if (value === undefined) return undefined;
179
- const parsed = Number(value);
180
- if (!Number.isSafeInteger(parsed) || parsed <= 0) return undefined;
181
- return parsed;
182
- }
183
-
184
- export function getSessionArtifactManifestMaxEntries(env: NodeJS.ProcessEnv = process.env): number {
185
- return parsePositiveSafeInteger(env[SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV]) ?? DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES;
186
- }
187
-
188
- function isRecord(value: unknown): value is Record<string, unknown> {
189
- return typeof value === "object" && value !== null;
190
- }
191
-
192
- function isManifestEntry(value: unknown): value is SessionArtifactManifestEntry {
193
- if (!isRecord(value)) return false;
194
- if (typeof value.path !== "string" || value.path.trim().length === 0) return false;
195
- if (typeof value.createdAtMs !== "number" || !Number.isFinite(value.createdAtMs)) return false;
196
- if (!["evicted", "ephemeral", "live", "missing"].includes(String(value.retentionState))) return false;
197
- if (!["explicit-path", "persistent-session", "process-temp"].includes(String(value.storageScope))) return false;
198
- if (typeof value.kind !== "string" || value.kind.trim().length === 0) return false;
199
- return true;
200
- }
201
-
202
- export function isSessionArtifactManifest(value: unknown): value is SessionArtifactManifest {
203
- if (!isRecord(value)) return false;
204
- if (value.version !== SESSION_ARTIFACT_MANIFEST_VERSION) return false;
205
- if (!Array.isArray(value.entries) || !value.entries.every(isManifestEntry)) return false;
206
- if (typeof value.updatedAtMs !== "number" || !Number.isFinite(value.updatedAtMs)) return false;
207
- if (typeof value.maxEntries !== "number" || !Number.isSafeInteger(value.maxEntries) || value.maxEntries <= 0) return false;
208
- if (typeof value.liveCount !== "number" || !Number.isSafeInteger(value.liveCount) || value.liveCount < 0) return false;
209
- if (typeof value.evictedCount !== "number" || !Number.isSafeInteger(value.evictedCount) || value.evictedCount < 0) return false;
210
- return true;
211
- }
212
-
213
- export function buildEvictedSessionArtifactEntries(
214
- evictedArtifacts: Array<{ mtimeMs: number; path: string; sizeBytes: number }>,
215
- nowMs: number,
216
- ): SessionArtifactManifestEntry[] {
217
- return evictedArtifacts.map((artifact) => ({
218
- createdAtMs: artifact.mtimeMs,
219
- evictedAtMs: nowMs,
220
- kind: "spill",
221
- path: artifact.path,
222
- retentionState: "evicted",
223
- sizeBytes: artifact.sizeBytes,
224
- storageScope: "persistent-session",
225
- }));
226
- }
227
-
228
- export function formatSessionArtifactRetentionSummary(manifest: SessionArtifactManifest): string {
229
- const ephemeralCount = manifest.entries.filter((entry) => entry.retentionState === "ephemeral").length;
230
- const missingCount = manifest.entries.filter((entry) => entry.retentionState === "missing").length;
231
- const parts = [`${manifest.liveCount} live`, `${manifest.evictedCount} evicted`];
232
- if (ephemeralCount > 0) parts.push(`${ephemeralCount} ephemeral`);
233
- if (missingCount > 0) parts.push(`${missingCount} missing`);
234
- return `Session artifacts: ${parts.join(", ")} (${manifest.entries.length}/${manifest.maxEntries} recent).`;
235
- }
236
-
237
- export function mergeSessionArtifactManifest(options: {
238
- base?: SessionArtifactManifest;
239
- entries?: SessionArtifactManifestEntry[];
240
- nowMs?: number;
241
- }): SessionArtifactManifest | undefined {
242
- const nowMs = options.nowMs ?? Date.now();
243
- const maxEntries = getSessionArtifactManifestMaxEntries();
244
- const getEntryKey = (entry: SessionArtifactManifestEntry) =>
245
- entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
246
- const byPath = new Map<string, SessionArtifactManifestEntry>();
247
- for (const entry of options.base?.entries ?? []) {
248
- byPath.set(getEntryKey(entry), entry);
249
- }
250
- for (const entry of options.entries ?? []) {
251
- const key = getEntryKey(entry);
252
- const existing = byPath.get(key);
253
- byPath.set(key, {
254
- ...existing,
255
- ...entry,
256
- createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
257
- evictedAtMs: entry.retentionState === "evicted" ? (entry.evictedAtMs ?? nowMs) : entry.evictedAtMs,
258
- });
259
- }
260
- if (byPath.size === 0) return undefined;
261
- const entries = [...byPath.values()]
262
- .sort((left, right) => {
263
- const leftTime = left.evictedAtMs ?? left.createdAtMs;
264
- const rightTime = right.evictedAtMs ?? right.createdAtMs;
265
- return rightTime - leftTime || left.path.localeCompare(right.path);
266
- })
267
- .slice(0, maxEntries);
268
- return {
269
- entries,
270
- evictedCount: entries.filter((entry) => entry.retentionState === "evicted").length,
271
- liveCount: entries.filter((entry) => entry.retentionState === "live").length,
272
- maxEntries,
273
- updatedAtMs: nowMs,
274
- version: SESSION_ARTIFACT_MANIFEST_VERSION,
275
- };
276
- }
277
-
278
- export interface BatchStepPresentationDetails {
279
- artifactVerification?: ArtifactVerificationSummary;
280
- artifacts?: FileArtifactMetadata[];
281
- command?: string[];
282
- commandText: string;
283
- data?: unknown;
284
- failureCategory?: AgentBrowserFailureCategory;
285
- nextActions?: AgentBrowserNextAction[];
286
- pageChangeSummary?: AgentBrowserPageChangeSummary;
287
- fullOutputPath?: string;
288
- fullOutputPaths?: string[];
289
- imagePath?: string;
290
- imagePaths?: string[];
291
- index: number;
292
- resultCategory: AgentBrowserResultCategory;
293
- savedFile?: SavedFilePresentationDetails;
294
- savedFilePath?: string;
295
- success: boolean;
296
- successCategory?: AgentBrowserSuccessCategory;
297
- summary: string;
298
- text: string;
299
- }
300
-
301
- export interface BatchFailurePresentationDetails {
302
- failedStep: BatchStepPresentationDetails;
303
- failureCount: number;
304
- successCount: number;
305
- totalCount: number;
306
- }
307
-
308
- export interface ToolPresentation {
309
- artifactManifest?: SessionArtifactManifest;
310
- artifactRetentionSummary?: string;
311
- artifactVerification?: ArtifactVerificationSummary;
312
- artifacts?: FileArtifactMetadata[];
313
- batchFailure?: BatchFailurePresentationDetails;
314
- batchSteps?: BatchStepPresentationDetails[];
315
- content: Array<{ text: string; type: "text" } | { data: string; mimeType: string; type: "image" }>;
316
- data?: unknown;
317
- failureCategory?: AgentBrowserFailureCategory;
318
- fullOutputPath?: string;
319
- fullOutputPaths?: string[];
320
- imagePath?: string;
321
- imagePaths?: string[];
322
- nextActions?: AgentBrowserNextAction[];
323
- pageChangeSummary?: AgentBrowserPageChangeSummary;
324
- resultCategory?: AgentBrowserResultCategory;
325
- savedFile?: SavedFilePresentationDetails;
326
- savedFilePath?: string;
327
- successCategory?: AgentBrowserSuccessCategory;
328
- summary: string;
329
- }
330
-
331
- function isPendingFileArtifact(artifact: FileArtifactMetadata): boolean {
332
- return artifact.command === "record" && artifact.subcommand === "start" && artifact.kind === "video";
333
- }
334
-
335
- function hasUnverifiedFileArtifact(artifacts: FileArtifactMetadata[] | undefined): boolean {
336
- return (artifacts ?? []).some((artifact) => !isPendingFileArtifact(artifact) && artifact.exists !== true);
337
- }
338
-
339
- export function classifyAgentBrowserSuccessCategory(options: {
340
- artifacts?: FileArtifactMetadata[];
341
- inspection?: boolean;
342
- savedFile?: SavedFilePresentationDetails;
343
- }): AgentBrowserSuccessCategory {
344
- if (options.inspection) return "inspection";
345
- if ((options.artifacts ?? []).length > 0) return hasUnverifiedFileArtifact(options.artifacts) ? "artifact-unverified" : "artifact-saved";
346
- if (options.savedFile) return "artifact-saved";
347
- return "completed";
348
- }
349
-
350
- export function classifyAgentBrowserFailureCategory(options: {
351
- args?: string[];
352
- command?: string;
353
- confirmationRequired?: boolean;
354
- errorText?: string;
355
- parseError?: string;
356
- spawnError?: string;
357
- stderr?: string;
358
- tabDrift?: boolean;
359
- timedOut?: boolean;
360
- validationError?: string;
361
- }): AgentBrowserFailureCategory {
362
- const text = [options.errorText, options.validationError, options.parseError, options.spawnError, options.stderr].filter(Boolean).join("\n");
363
- const command = options.command ?? "";
364
- const usedRef = options.args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
365
- if (options.confirmationRequired || /confirmation required|pending confirmation|requires confirmation/i.test(text)) return "confirmation-required";
366
- if (options.timedOut || /timeout|timed out|watchdog|IPC read timeout|must stay under its 30s IPC read timeout/i.test(text)) return "timeout";
367
- if (/ENOENT|not found on PATH|could not find.*agent-browser|agent-browser is required but was not found/i.test(text)) return "missing-binary";
368
- if (options.parseError || /invalid JSON|missing boolean success|success field must be boolean|returned no JSON output/i.test(text)) return "parse-failure";
369
- if (/aborted/i.test(text)) return "aborted";
370
- if (/policy[- ]blocked|blocked by caller policy|caller deny policy|caller allow policy/i.test(text)) return "policy-blocked";
371
- if (/cleanup failed|cleanup.*partial|partial cleanup|remaining resources/i.test(text)) return "cleanup-failed";
372
- if (options.tabDrift || /could not re-select the intended tab|about:blank|selected tab looks wrong|tab drift|tab.*wrong/i.test(text)) return "tab-drift";
373
- if (/\bUnknown ref\b|\bstale ref\b|@ref may be stale|\bref\b.*\b(?:not found|missing|expired)\b/i.test(text)) return "stale-ref";
374
- if (usedRef && /could not locate element|element not found|no element/i.test(text)) return "stale-ref";
375
- const mentionsPlaywrightSelectorDialect = /(?:\btext=|:has-text\(|\bgetByRole\b|\bgetByText\b)/i.test(text);
376
- const reportsSelectorMatchFailure =
377
- /\b(?:no elements? found|failed to find|could not find|unable to find)\b.*\b(?:selector|locator)\b/i.test(text) ||
378
- /\b(?:selector|locator)\b.*\b(?:no elements? found|not found|missing|failed to find|could not find|unable to find)\b/i.test(text);
379
- if (
380
- /\b(?:unsupported|unknown|invalid)\s+(?:selector|locator)\b/i.test(text) ||
381
- /\bfailed to parse selector\b/i.test(text) ||
382
- /\bselector\b.*\b(?:parse|syntax|unsupported|invalid)\b/i.test(text) ||
383
- (mentionsPlaywrightSelectorDialect && reportsSelectorMatchFailure)
384
- ) {
385
- return "selector-unsupported";
386
- }
387
- if (command === "find" && /could not locate element|element not found|no elements? found|unable to find/i.test(text)) return "selector-not-found";
388
- if (reportsSelectorMatchFailure) return "selector-not-found";
389
- if ((command === "download" || text.includes("wait --download") || /\bdownload\b/i.test(text)) && /missing|not verified|not found|failed|timeout|timed out/i.test(text)) {
390
- return "download-not-verified";
391
- }
392
- if (options.validationError) return "validation-error";
393
- return "upstream-error";
394
- }
395
-
396
- function buildNextToolAction(options: {
397
- args: string[];
398
- id: string;
399
- reason: string;
400
- safety?: string;
401
- sessionMode?: "auto" | "fresh";
402
- stdin?: string;
403
- }): AgentBrowserNextAction {
404
- return {
405
- id: options.id,
406
- params: {
407
- args: options.args,
408
- ...(options.sessionMode ? { sessionMode: options.sessionMode } : {}),
409
- ...(options.stdin ? { stdin: options.stdin } : {}),
410
- },
411
- reason: options.reason,
412
- ...(options.safety ? { safety: options.safety } : {}),
413
- tool: "agent_browser",
414
- };
415
- }
416
-
417
- function buildArtifactAction(path: string): AgentBrowserNextAction {
418
- return {
419
- artifactPath: path,
420
- id: "use-saved-artifact",
421
- reason: "Use the saved artifact path from the structured result instead of scraping it from text.",
422
- safety: "Verify artifact metadata such as exists/status before treating the file as durable.",
423
- tool: "agent_browser",
424
- };
425
- }
426
-
427
- function buildArtifactVerificationAction(artifact: FileArtifactMetadata): AgentBrowserNextAction {
428
- return {
429
- artifactPath: artifact.path,
430
- id: "verify-artifact-path",
431
- reason: "The wrapper has artifact metadata but did not verify this file as present on disk.",
432
- safety: "Check details.artifactVerification and the filesystem before treating the artifact as durable.",
433
- tool: "agent_browser",
434
- };
435
- }
436
-
437
- function buildElectronToolAction(options: {
438
- action: "cleanup" | "probe" | "status";
439
- id: string;
440
- launchId: string;
441
- reason: string;
442
- safety?: string;
443
- }): AgentBrowserNextAction {
444
- return {
445
- id: options.id,
446
- params: { electron: { action: options.action, launchId: options.launchId } },
447
- reason: options.reason,
448
- ...(options.safety ? { safety: options.safety } : {}),
449
- tool: "agent_browser",
450
- };
451
- }
452
-
453
- const MUTATING_COMMANDS = new Set([
454
- "back",
455
- "check",
456
- "click",
457
- "dblclick",
458
- "dialog",
459
- "fill",
460
- "forward",
461
- "hover",
462
- "press",
463
- "pushstate",
464
- "reload",
465
- "scroll",
466
- "scrollintoview",
467
- "select",
468
- "swipe",
469
- "tap",
470
- "type",
471
- "uncheck",
472
- ]);
473
-
474
- function getDownloadRetryPath(args: string[] | undefined, fallback: string | undefined): string | undefined {
475
- if (fallback) return fallback;
476
- if (!args || args.length === 0) return undefined;
477
- const downloadFlagIndex = args.indexOf("--download");
478
- if (downloadFlagIndex >= 0) {
479
- const candidate = args[downloadFlagIndex + 1];
480
- return candidate && !candidate.startsWith("-") ? candidate : undefined;
481
- }
482
- const downloadCommandIndex = args.indexOf("download");
483
- if (downloadCommandIndex >= 0 && args.length > downloadCommandIndex + 2) {
484
- return args[args.length - 1];
485
- }
486
- return undefined;
487
- }
488
-
489
- export function buildAgentBrowserNextActions(options: {
490
- artifacts?: FileArtifactMetadata[];
491
- args?: string[];
492
- command?: string;
493
- confirmationId?: string;
494
- electron?: {
495
- launchId?: string;
496
- sessionName?: string;
497
- status?: "active" | "cleaned" | "dead" | "failed" | "partial" | "succeeded";
498
- };
499
- failureCategory?: AgentBrowserFailureCategory;
500
- resultCategory: AgentBrowserResultCategory;
501
- savedFilePath?: string;
502
- successCategory?: AgentBrowserSuccessCategory;
503
- }): AgentBrowserNextAction[] | undefined {
504
- const actions: AgentBrowserNextAction[] = [];
505
- if (options.electron?.launchId) {
506
- const { launchId, sessionName, status } = options.electron;
507
- if (options.resultCategory === "success" && status !== "cleaned") {
508
- actions.push(
509
- buildElectronToolAction({
510
- action: "status",
511
- id: "status-electron-launch",
512
- launchId,
513
- reason: "Check the wrapper-tracked Electron launch liveness and current CDP targets without mutating the app.",
514
- }),
515
- buildElectronToolAction({
516
- action: "probe",
517
- id: "probe-electron-launch",
518
- launchId,
519
- reason: "Probe the attached Electron managed session and carry the wrapper launchId for follow-up diagnostics.",
520
- }),
521
- buildElectronToolAction({
522
- action: "cleanup",
523
- id: "cleanup-electron-launch",
524
- launchId,
525
- reason: "Clean the wrapper-owned Electron process and isolated userDataDir when the run is complete.",
526
- safety: "Only operates on the launchId created by electron.launch; explicit artifacts and manually launched apps remain host-owned.",
527
- }),
528
- );
529
- if (sessionName) {
530
- actions.push(
531
- buildNextToolAction({
532
- args: ["--session", sessionName, "tab", "list"],
533
- id: "list-electron-tabs",
534
- reason: "Inspect attached Electron page/webview targets before choosing the active tab.",
535
- }),
536
- buildNextToolAction({
537
- args: ["--session", sessionName, "snapshot", "-i"],
538
- id: "snapshot-electron-session",
539
- reason: "Refresh interactive refs for the attached Electron session.",
540
- safety: "Use current Electron refs only after a fresh snapshot for this session.",
541
- }),
542
- );
543
- }
544
- } else if (options.resultCategory === "failure" && options.failureCategory === "cleanup-failed") {
545
- actions.push(
546
- buildElectronToolAction({
547
- action: "status",
548
- id: "status-electron-launch",
549
- launchId,
550
- reason: "Inspect which wrapper-tracked Electron resources remain after partial cleanup.",
551
- }),
552
- buildElectronToolAction({
553
- action: "cleanup",
554
- id: "retry-electron-cleanup",
555
- launchId,
556
- reason: "Retry cleanup for the same wrapper-owned Electron launch after reviewing remaining resources.",
557
- safety: "Only retry for the same launchId; do not use cleanup for manually launched Electron apps.",
558
- }),
559
- );
560
- }
561
- }
562
- if (options.resultCategory === "success") {
563
- if (options.command === "open") {
564
- actions.push(buildNextToolAction({
565
- args: ["snapshot", "-i"],
566
- id: "inspect-opened-page",
567
- reason: "Inspect the opened page before choosing interactive refs.",
568
- }));
569
- } else if (options.command && MUTATING_COMMANDS.has(options.command)) {
570
- actions.push(buildNextToolAction({
571
- args: ["snapshot", "-i"],
572
- id: "inspect-after-mutation",
573
- reason: "Refresh interactive refs after a browser mutation, navigation, scroll, or rerender.",
574
- safety: "Do not reuse prior @refs until a fresh snapshot confirms they still exist.",
575
- }));
576
- }
577
- const artifacts = options.artifacts ?? [];
578
- const savedFileArtifact = options.savedFilePath ? artifacts.find((artifact) => artifact.path === options.savedFilePath) : undefined;
579
- if (options.savedFilePath && savedFileArtifact?.exists !== false) {
580
- actions.push(buildArtifactAction(options.savedFilePath));
581
- }
582
- for (const artifact of artifacts) {
583
- if (isPendingFileArtifact(artifact)) {
584
- continue;
585
- }
586
- if (artifact.exists === false) {
587
- if (artifact.kind === "download") {
588
- actions.push(buildNextToolAction({
589
- args: ["wait", "--download", artifact.path],
590
- id: "wait-for-download",
591
- reason: "Upstream reported a download path, but the wrapper did not verify the file on disk.",
592
- safety: "Use a bounded wait timeout that stays below the native wrapper IPC budget.",
593
- }));
594
- } else {
595
- actions.push(buildArtifactVerificationAction(artifact));
596
- }
597
- continue;
598
- }
599
- if (artifact.path !== options.savedFilePath) {
600
- actions.push(buildArtifactAction(artifact.path));
601
- }
602
- }
603
- } else {
604
- switch (options.failureCategory) {
605
- case "confirmation-required":
606
- if (options.confirmationId) {
607
- actions.push(
608
- buildNextToolAction({
609
- args: ["confirm", options.confirmationId],
610
- id: "approve-confirmation",
611
- reason: "Approve the pending upstream confirmation when the requested action is safe.",
612
- safety: "Only confirm after reviewing the guarded action shown in the result.",
613
- }),
614
- buildNextToolAction({
615
- args: ["deny", options.confirmationId],
616
- id: "deny-confirmation",
617
- reason: "Deny the pending upstream confirmation when the guarded action is unsafe or unintended.",
618
- }),
619
- );
620
- }
621
- break;
622
- case "stale-ref":
623
- case "selector-not-found":
624
- case "selector-unsupported":
625
- actions.push(buildNextToolAction({
626
- args: ["snapshot", "-i"],
627
- id: "refresh-interactive-refs",
628
- reason: "Get current interactive refs before retrying the element action.",
629
- safety: "Prefer a current @ref or a stable find locator; do not retry stale refs blindly.",
630
- }));
631
- break;
632
- case "download-not-verified":
633
- {
634
- const retryPath = getDownloadRetryPath(options.args, options.savedFilePath);
635
- actions.push(buildNextToolAction({
636
- args: retryPath ? ["wait", "--download", retryPath] : ["wait", "--download"],
637
- id: "wait-for-download",
638
- reason: "Wait for the browser download and let the wrapper verify saved-file metadata.",
639
- safety: "Use a bounded wait timeout that stays below the native wrapper IPC budget.",
640
- }));
641
- }
642
- break;
643
- case "tab-drift":
644
- actions.push(
645
- buildNextToolAction({
646
- args: ["tab", "list"],
647
- id: "list-tabs-for-recovery",
648
- reason: "Inspect available tabs before selecting the intended target.",
649
- }),
650
- buildNextToolAction({
651
- args: ["snapshot", "-i"],
652
- id: "inspect-current-tab",
653
- reason: "Inspect the currently selected tab after tab recovery.",
654
- }),
655
- );
656
- break;
657
- }
658
- }
659
- return actions.length > 0 ? actions : undefined;
660
- }
661
-
662
- export function buildAgentBrowserResultCategoryDetails(options: {
663
- artifacts?: FileArtifactMetadata[];
664
- args?: string[];
665
- command?: string;
666
- confirmationRequired?: boolean;
667
- errorText?: string;
668
- failureCategory?: AgentBrowserFailureCategory;
669
- inspection?: boolean;
670
- parseError?: string;
671
- savedFile?: SavedFilePresentationDetails;
672
- spawnError?: string;
673
- succeeded: boolean;
674
- tabDrift?: boolean;
675
- timedOut?: boolean;
676
- validationError?: string;
677
- }): AgentBrowserResultCategoryDetails {
678
- if (options.succeeded) {
679
- return {
680
- resultCategory: "success",
681
- successCategory: classifyAgentBrowserSuccessCategory(options),
682
- };
683
- }
684
- return {
685
- failureCategory: options.failureCategory ?? classifyAgentBrowserFailureCategory(options),
686
- resultCategory: "failure",
687
- };
688
- }
689
-
690
- export type NetworkFailureImpact = "actionable" | "benign";
691
-
692
- export interface NetworkFailureClassification {
693
- impact: NetworkFailureImpact;
694
- reason: string;
695
- resourceType?: string;
696
- status?: number;
697
- url?: string;
698
- }
699
-
700
- export interface NetworkFailureSummary {
701
- actionableCount: number;
702
- benignCount: number;
703
- failures: NetworkFailureClassification[];
704
- totalCount: number;
705
- }
706
-
707
- function getStringRecordField(value: Record<string, unknown>, key: string): string | undefined {
708
- const field = value[key];
709
- return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
710
- }
711
-
712
- function getNetworkRequestUrlPath(url: string | undefined): string | undefined {
713
- if (!url) return undefined;
714
- try {
715
- return new URL(url).pathname;
716
- } catch {
717
- const withoutQuery = url.split(/[?#]/, 1)[0];
718
- return withoutQuery.length > 0 ? withoutQuery : undefined;
719
- }
720
- }
721
-
722
- function isFailedNetworkRequest(request: Record<string, unknown>): boolean {
723
- return (typeof request.status === "number" && request.status >= 400) || request.failed === true || typeof request.error === "string";
724
- }
725
-
726
- function isBenignAssetFailure(request: Record<string, unknown>, url: string | undefined, resourceType: string | undefined): boolean {
727
- const path = getNetworkRequestUrlPath(url);
728
- if (!path) return false;
729
- const normalizedResourceType = resourceType?.toLowerCase();
730
- return /(?:^|\/)(?:favicon(?:[-.\w]*)?\.(?:ico|png|svg)|apple-touch-icon(?:[-.\w]*)?\.png)$/i.test(path)
731
- && (request.status === 404 || request.failed === true || typeof request.error === "string")
732
- && (!normalizedResourceType || ["image", "img", "other"].includes(normalizedResourceType) || normalizedResourceType.startsWith("image/"));
733
- }
734
-
735
- export function classifyNetworkRequestFailure(request: Record<string, unknown>): NetworkFailureClassification | undefined {
736
- if (!isFailedNetworkRequest(request)) return undefined;
737
- const url = getStringRecordField(request, "url");
738
- const resourceType = getStringRecordField(request, "resourceType") ?? getStringRecordField(request, "mimeType");
739
- const status = typeof request.status === "number" ? request.status : undefined;
740
- if (isBenignAssetFailure(request, url, resourceType)) {
741
- return { impact: "benign", reason: "low-impact browser icon asset", resourceType, status, url };
742
- }
743
- return { impact: "actionable", reason: "document, script, API, or non-benign request failure", resourceType, status, url };
744
- }
745
-
746
- export function summarizeNetworkFailures(requests: unknown[]): NetworkFailureSummary {
747
- const failures = requests.flatMap((request) => {
748
- if (!isRecord(request)) return [];
749
- const classification = classifyNetworkRequestFailure(request);
750
- return classification ? [classification] : [];
751
- });
752
- const benignCount = failures.filter((failure) => failure.impact === "benign").length;
753
- return {
754
- actionableCount: failures.length - benignCount,
755
- benignCount,
756
- failures,
757
- totalCount: failures.length,
758
- };
759
- }
760
-
761
- export function stringifyUnknown(value: unknown): string {
762
- if (typeof value === "string") return value;
763
- if (typeof value === "number" || typeof value === "boolean") return String(value);
764
- if (value === null || value === undefined) return "";
765
- try {
766
- return JSON.stringify(value, null, 2);
767
- } catch {
768
- return String(value);
769
- }
770
- }
771
-
772
- export function countLines(text: string): number {
773
- return text.length === 0 ? 0 : text.split("\n").length;
774
- }
775
-
776
- export function normalizeWhitespace(text: string): string {
777
- return text.replace(/\s+/g, " ").trim();
778
- }
779
-
780
- export function truncateText(text: string, maxChars: number): string {
781
- if (text.length <= maxChars) return text;
782
- return `${text.slice(0, Math.max(1, maxChars - 1))}…`;
783
- }
784
-
785
- export function compareRefIds(left: string, right: string): number {
786
- const leftMatch = left.match(/^(?:[a-zA-Z]+)?(\d+)$/);
787
- const rightMatch = right.match(/^(?:[a-zA-Z]+)?(\d+)$/);
788
- if (leftMatch && rightMatch) {
789
- return Number(leftMatch[1]) - Number(rightMatch[1]);
790
- }
791
- return left.localeCompare(right);
792
- }
9
+ export * from "./contracts.js";
10
+ export * from "./categories.js";
11
+ export * from "./action-recommendations.js";
12
+ export * from "./artifact-manifest.js";
13
+ export * from "./artifact-state.js";
14
+ export * from "./editable-ref-evidence.js";
15
+ export * from "./network.js";
16
+ export * from "./next-actions.js";
17
+ export * from "./recovery-actions.js";
18
+ export * from "./recovery-next-actions.js";
19
+ export * from "./selector-recovery.js";
20
+ export * from "./text.js";