pi-agent-browser-native 0.2.32 → 0.2.33

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 (62) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +27 -16
  3. package/docs/ARCHITECTURE.md +3 -2
  4. package/docs/COMMAND_REFERENCE.md +18 -10
  5. package/docs/ELECTRON.md +23 -4
  6. package/docs/RELEASE.md +4 -2
  7. package/docs/REQUIREMENTS.md +1 -1
  8. package/docs/SUPPORT_MATRIX.md +28 -16
  9. package/docs/TOOL_CONTRACT.md +29 -24
  10. package/extensions/agent-browser/index.ts +404 -4371
  11. package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
  12. package/extensions/agent-browser/lib/input-modes/job.ts +203 -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 +41 -0
  19. package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +696 -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 +711 -0
  23. package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +386 -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 +476 -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 +12 -11
  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 +154 -0
  48. package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
  49. package/extensions/agent-browser/lib/results/presentation.ts +87 -2399
  50. package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
  51. package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
  52. package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
  53. package/extensions/agent-browser/lib/results/shared.ts +17 -789
  54. package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
  55. package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
  56. package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
  57. package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
  58. package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
  59. package/extensions/agent-browser/lib/results/text.ts +40 -0
  60. package/extensions/agent-browser/lib/results.ts +16 -5
  61. package/extensions/agent-browser/lib/session-page-state.ts +486 -0
  62. package/package.json +2 -1
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Purpose: Build generic nextAction recommendations from result categories, artifacts, Electron lifecycle state, and recovery context.
3
+ * Responsibilities: Preserve stable action ids/order while keeping recommendation policy out of generic shared helpers.
4
+ * Scope: Generic result-level recommendations only; feature-specific diagnostics append their own actions in the extension entrypoint.
5
+ * Usage: Called by presentation and extension result assembly.
6
+ * Invariants/Assumptions: Action ids are public machine-readable contracts; preserve first-observed order.
7
+ */
8
+
9
+ import { isPendingRecordingArtifact } from "./artifact-state.js";
10
+ import type {
11
+ AgentBrowserFailureCategory,
12
+ AgentBrowserResultCategory,
13
+ AgentBrowserSuccessCategory,
14
+ FileArtifactMetadata,
15
+ } from "./contracts.js";
16
+ import { buildNextToolAction, type AgentBrowserNextAction } from "./next-actions.js";
17
+ import {
18
+ AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS,
19
+ buildRecoveryNextActions,
20
+ type AgentBrowserRecoveryContext,
21
+ } from "./recovery-actions.js";
22
+
23
+ function buildArtifactAction(path: string): AgentBrowserNextAction {
24
+ return {
25
+ artifactPath: path,
26
+ id: "use-saved-artifact",
27
+ reason: "Use the saved artifact path from the structured result instead of scraping it from text.",
28
+ safety: "Verify artifact metadata such as exists/status before treating the file as durable.",
29
+ tool: "agent_browser",
30
+ };
31
+ }
32
+
33
+ function buildArtifactVerificationAction(artifact: FileArtifactMetadata): AgentBrowserNextAction {
34
+ return {
35
+ artifactPath: artifact.path,
36
+ id: "verify-artifact-path",
37
+ reason: "The wrapper has artifact metadata but did not verify this file as present on disk.",
38
+ safety: "Check details.artifactVerification and the filesystem before treating the artifact as durable.",
39
+ tool: "agent_browser",
40
+ };
41
+ }
42
+
43
+ function buildElectronToolAction(options: {
44
+ action: "cleanup" | "probe" | "status";
45
+ id: string;
46
+ launchId: string;
47
+ reason: string;
48
+ safety?: string;
49
+ }): AgentBrowserNextAction {
50
+ return {
51
+ id: options.id,
52
+ params: { electron: { action: options.action, launchId: options.launchId } },
53
+ reason: options.reason,
54
+ ...(options.safety ? { safety: options.safety } : {}),
55
+ tool: "agent_browser",
56
+ };
57
+ }
58
+
59
+ const MUTATING_COMMANDS = new Set([
60
+ "back",
61
+ "check",
62
+ "click",
63
+ "dblclick",
64
+ "dialog",
65
+ "fill",
66
+ "forward",
67
+ "hover",
68
+ "press",
69
+ "pushstate",
70
+ "reload",
71
+ "scroll",
72
+ "scrollintoview",
73
+ "select",
74
+ "swipe",
75
+ "tap",
76
+ "type",
77
+ "uncheck",
78
+ ]);
79
+
80
+ function getDownloadRetryPath(args: string[] | undefined, fallback: string | undefined): string | undefined {
81
+ if (fallback) return fallback;
82
+ if (!args || args.length === 0) return undefined;
83
+ const downloadFlagIndex = args.indexOf("--download");
84
+ if (downloadFlagIndex >= 0) {
85
+ const candidate = args[downloadFlagIndex + 1];
86
+ return candidate && !candidate.startsWith("-") ? candidate : undefined;
87
+ }
88
+ const downloadCommandIndex = args.indexOf("download");
89
+ if (downloadCommandIndex >= 0 && args.length > downloadCommandIndex + 2) {
90
+ return args[args.length - 1];
91
+ }
92
+ return undefined;
93
+ }
94
+
95
+ export function buildAgentBrowserNextActions(options: {
96
+ artifacts?: FileArtifactMetadata[];
97
+ args?: string[];
98
+ command?: string;
99
+ confirmationId?: string;
100
+ electron?: {
101
+ launchId?: string;
102
+ sessionName?: string;
103
+ status?: "active" | "cleaned" | "dead" | "failed" | "partial" | "succeeded";
104
+ };
105
+ failureCategory?: AgentBrowserFailureCategory;
106
+ resultCategory: AgentBrowserResultCategory;
107
+ recovery?: AgentBrowserRecoveryContext;
108
+ savedFilePath?: string;
109
+ successCategory?: AgentBrowserSuccessCategory;
110
+ }): AgentBrowserNextAction[] | undefined {
111
+ const actions: AgentBrowserNextAction[] = [];
112
+ if (options.recovery) {
113
+ actions.push(...buildRecoveryNextActions(options.recovery));
114
+ }
115
+ if (options.electron?.launchId) {
116
+ const { launchId, sessionName, status } = options.electron;
117
+ if (options.resultCategory === "success" && status !== "cleaned") {
118
+ actions.push(
119
+ buildElectronToolAction({
120
+ action: "status",
121
+ id: "status-electron-launch",
122
+ launchId,
123
+ reason: "Check the wrapper-tracked Electron launch liveness and current CDP targets without mutating the app.",
124
+ }),
125
+ buildElectronToolAction({
126
+ action: "probe",
127
+ id: "probe-electron-launch",
128
+ launchId,
129
+ reason: "Probe the attached Electron managed session and carry the wrapper launchId for follow-up diagnostics.",
130
+ }),
131
+ buildElectronToolAction({
132
+ action: "cleanup",
133
+ id: "cleanup-electron-launch",
134
+ launchId,
135
+ reason: "Clean the wrapper-owned Electron process and isolated userDataDir when the run is complete.",
136
+ safety: "Only operates on the launchId created by electron.launch; explicit artifacts and manually launched apps remain host-owned.",
137
+ }),
138
+ );
139
+ if (sessionName) {
140
+ actions.push(
141
+ buildNextToolAction({
142
+ args: ["--session", sessionName, "tab", "list"],
143
+ id: "list-electron-tabs",
144
+ reason: "Inspect attached Electron page/webview targets before choosing the active tab.",
145
+ }),
146
+ buildNextToolAction({
147
+ args: ["--session", sessionName, "snapshot", "-i"],
148
+ id: "snapshot-electron-session",
149
+ reason: "Refresh interactive refs for the attached Electron session.",
150
+ safety: "Use current Electron refs only after a fresh snapshot for this session.",
151
+ }),
152
+ );
153
+ }
154
+ } else if (options.resultCategory === "failure" && options.failureCategory === "cleanup-failed") {
155
+ actions.push(
156
+ buildElectronToolAction({
157
+ action: "status",
158
+ id: "status-electron-launch",
159
+ launchId,
160
+ reason: "Inspect which wrapper-tracked Electron resources remain after partial cleanup.",
161
+ }),
162
+ buildElectronToolAction({
163
+ action: "cleanup",
164
+ id: "retry-electron-cleanup",
165
+ launchId,
166
+ reason: "Retry cleanup for the same wrapper-owned Electron launch after reviewing remaining resources.",
167
+ safety: "Only retry for the same launchId; do not use cleanup for manually launched Electron apps.",
168
+ }),
169
+ );
170
+ }
171
+ }
172
+ if (options.resultCategory === "success") {
173
+ if (options.command === "open") {
174
+ actions.push(buildNextToolAction({
175
+ args: ["snapshot", "-i"],
176
+ id: "inspect-opened-page",
177
+ reason: "Inspect the opened page before choosing interactive refs.",
178
+ }));
179
+ } else if (options.command && MUTATING_COMMANDS.has(options.command)) {
180
+ actions.push(buildNextToolAction({
181
+ args: ["snapshot", "-i"],
182
+ id: "inspect-after-mutation",
183
+ reason: "Refresh interactive refs after a browser mutation, navigation, scroll, or rerender.",
184
+ safety: "Do not reuse prior @refs until a fresh snapshot confirms they still exist.",
185
+ }));
186
+ }
187
+ const artifacts = options.artifacts ?? [];
188
+ const savedFileArtifact = options.savedFilePath ? artifacts.find((artifact) => artifact.path === options.savedFilePath) : undefined;
189
+ if (options.savedFilePath && savedFileArtifact?.exists !== false) {
190
+ actions.push(buildArtifactAction(options.savedFilePath));
191
+ }
192
+ for (const artifact of artifacts) {
193
+ if (isPendingRecordingArtifact(artifact)) {
194
+ continue;
195
+ }
196
+ if (artifact.exists === false) {
197
+ if (artifact.kind === "download") {
198
+ actions.push(buildNextToolAction({
199
+ args: ["wait", "--download", artifact.path],
200
+ id: "wait-for-download",
201
+ reason: "Upstream reported a download path, but the wrapper did not verify the file on disk.",
202
+ safety: "Use a bounded wait timeout that stays below the native wrapper IPC budget.",
203
+ }));
204
+ } else {
205
+ actions.push(buildArtifactVerificationAction(artifact));
206
+ }
207
+ continue;
208
+ }
209
+ if (artifact.path !== options.savedFilePath) {
210
+ actions.push(buildArtifactAction(artifact.path));
211
+ }
212
+ }
213
+ } else {
214
+ switch (options.failureCategory) {
215
+ case "confirmation-required":
216
+ if (options.confirmationId) {
217
+ actions.push(
218
+ buildNextToolAction({
219
+ args: ["confirm", options.confirmationId],
220
+ id: "approve-confirmation",
221
+ reason: "Approve the pending upstream confirmation when the requested action is safe.",
222
+ safety: "Only confirm after reviewing the guarded action shown in the result.",
223
+ }),
224
+ buildNextToolAction({
225
+ args: ["deny", options.confirmationId],
226
+ id: "deny-confirmation",
227
+ reason: "Deny the pending upstream confirmation when the guarded action is unsafe or unintended.",
228
+ }),
229
+ );
230
+ }
231
+ break;
232
+ case "stale-ref":
233
+ case "selector-not-found":
234
+ case "selector-unsupported":
235
+ actions.push(buildNextToolAction({
236
+ args: ["snapshot", "-i"],
237
+ id: "refresh-interactive-refs",
238
+ reason: "Get current interactive refs before retrying the element action.",
239
+ safety: "Prefer a current @ref or a stable find locator; do not retry stale refs blindly.",
240
+ }));
241
+ break;
242
+ case "download-not-verified":
243
+ {
244
+ const retryPath = getDownloadRetryPath(options.args, options.savedFilePath);
245
+ actions.push(buildNextToolAction({
246
+ args: retryPath ? ["wait", "--download", retryPath] : ["wait", "--download"],
247
+ id: "wait-for-download",
248
+ reason: "Wait for the browser download and let the wrapper verify saved-file metadata.",
249
+ safety: "Use a bounded wait timeout that stays below the native wrapper IPC budget.",
250
+ }));
251
+ }
252
+ break;
253
+ case "tab-drift":
254
+ if (options.recovery?.kind === "about-blank" || options.recovery?.kind === "tab-drift") {
255
+ break;
256
+ }
257
+ actions.push(
258
+ buildNextToolAction({
259
+ args: ["tab", "list"],
260
+ id: AGENT_BROWSER_RECOVERY_NEXT_ACTION_IDS.genericTabDriftListTabs,
261
+ reason: "Inspect available tabs before selecting the intended target.",
262
+ safety: "Read-only. Retry snapshot only after selecting or confirming the intended stable tab.",
263
+ }),
264
+ );
265
+ break;
266
+ }
267
+ }
268
+ return actions.length > 0 ? actions : undefined;
269
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Purpose: Own persistent session artifact manifest merge, retention, and validation logic.
3
+ * Responsibilities: Parse manifest bounds, recognize manifest entries, merge new artifact rows, and format retention summaries.
4
+ * Scope: Manifest accounting only; artifact detection and presentation live in presentation modules.
5
+ * Usage: Imported by presentation and snapshot artifact persistence paths.
6
+ * Invariants/Assumptions: Explicit-path artifacts are host-owned while persistent-session spill files are bounded by the manifest cap.
7
+ */
8
+
9
+ import type { SessionArtifactManifest, SessionArtifactManifestEntry } from "./contracts.js";
10
+
11
+ export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
12
+ export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
13
+ export const DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES = 100;
14
+
15
+ function parsePositiveSafeInteger(value: string | undefined): number | undefined {
16
+ if (value === undefined) return undefined;
17
+ const parsed = Number(value);
18
+ if (!Number.isSafeInteger(parsed) || parsed <= 0) return undefined;
19
+ return parsed;
20
+ }
21
+
22
+ export function getSessionArtifactManifestMaxEntries(env: NodeJS.ProcessEnv = process.env): number {
23
+ return parsePositiveSafeInteger(env[SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV]) ?? DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES;
24
+ }
25
+
26
+ function isRecord(value: unknown): value is Record<string, unknown> {
27
+ return typeof value === "object" && value !== null;
28
+ }
29
+
30
+ function isManifestEntry(value: unknown): value is SessionArtifactManifestEntry {
31
+ if (!isRecord(value)) return false;
32
+ if (typeof value.path !== "string" || value.path.trim().length === 0) return false;
33
+ if (typeof value.createdAtMs !== "number" || !Number.isFinite(value.createdAtMs)) return false;
34
+ if (!["evicted", "ephemeral", "live", "missing"].includes(String(value.retentionState))) return false;
35
+ if (!["explicit-path", "persistent-session", "process-temp"].includes(String(value.storageScope))) return false;
36
+ if (typeof value.kind !== "string" || value.kind.trim().length === 0) return false;
37
+ return true;
38
+ }
39
+
40
+ export function isSessionArtifactManifest(value: unknown): value is SessionArtifactManifest {
41
+ if (!isRecord(value)) return false;
42
+ if (value.version !== SESSION_ARTIFACT_MANIFEST_VERSION) return false;
43
+ if (!Array.isArray(value.entries) || !value.entries.every(isManifestEntry)) return false;
44
+ if (typeof value.updatedAtMs !== "number" || !Number.isFinite(value.updatedAtMs)) return false;
45
+ if (typeof value.maxEntries !== "number" || !Number.isSafeInteger(value.maxEntries) || value.maxEntries <= 0) return false;
46
+ if (typeof value.liveCount !== "number" || !Number.isSafeInteger(value.liveCount) || value.liveCount < 0) return false;
47
+ if (typeof value.evictedCount !== "number" || !Number.isSafeInteger(value.evictedCount) || value.evictedCount < 0) return false;
48
+ return true;
49
+ }
50
+
51
+ export function buildEvictedSessionArtifactEntries(
52
+ evictedArtifacts: Array<{ mtimeMs: number; path: string; sizeBytes: number }>,
53
+ nowMs: number,
54
+ ): SessionArtifactManifestEntry[] {
55
+ return evictedArtifacts.map((artifact) => ({
56
+ createdAtMs: artifact.mtimeMs,
57
+ evictedAtMs: nowMs,
58
+ kind: "spill",
59
+ path: artifact.path,
60
+ retentionState: "evicted",
61
+ sizeBytes: artifact.sizeBytes,
62
+ storageScope: "persistent-session",
63
+ }));
64
+ }
65
+
66
+ export function formatSessionArtifactRetentionSummary(manifest: SessionArtifactManifest): string {
67
+ const ephemeralCount = manifest.entries.filter((entry) => entry.retentionState === "ephemeral").length;
68
+ const missingCount = manifest.entries.filter((entry) => entry.retentionState === "missing").length;
69
+ const parts = [`${manifest.liveCount} live`, `${manifest.evictedCount} evicted`];
70
+ if (ephemeralCount > 0) parts.push(`${ephemeralCount} ephemeral`);
71
+ if (missingCount > 0) parts.push(`${missingCount} missing`);
72
+ return `Session artifacts: ${parts.join(", ")} (${manifest.entries.length}/${manifest.maxEntries} recent).`;
73
+ }
74
+
75
+ export function mergeSessionArtifactManifest(options: {
76
+ base?: SessionArtifactManifest;
77
+ entries?: SessionArtifactManifestEntry[];
78
+ nowMs?: number;
79
+ }): SessionArtifactManifest | undefined {
80
+ const nowMs = options.nowMs ?? Date.now();
81
+ const maxEntries = getSessionArtifactManifestMaxEntries();
82
+ const getEntryKey = (entry: SessionArtifactManifestEntry) =>
83
+ entry.storageScope === "explicit-path" && entry.absolutePath ? `${entry.storageScope}:${entry.absolutePath}` : `${entry.storageScope}:${entry.path}`;
84
+ const byPath = new Map<string, SessionArtifactManifestEntry>();
85
+ for (const entry of options.base?.entries ?? []) {
86
+ byPath.set(getEntryKey(entry), entry);
87
+ }
88
+ for (const entry of options.entries ?? []) {
89
+ const key = getEntryKey(entry);
90
+ const existing = byPath.get(key);
91
+ byPath.set(key, {
92
+ ...existing,
93
+ ...entry,
94
+ createdAtMs: existing?.createdAtMs ?? entry.createdAtMs,
95
+ evictedAtMs: entry.retentionState === "evicted" ? (entry.evictedAtMs ?? nowMs) : entry.evictedAtMs,
96
+ });
97
+ }
98
+ if (byPath.size === 0) return undefined;
99
+ const entries = [...byPath.values()]
100
+ .sort((left, right) => {
101
+ const leftTime = left.evictedAtMs ?? left.createdAtMs;
102
+ const rightTime = right.evictedAtMs ?? right.createdAtMs;
103
+ return rightTime - leftTime || left.path.localeCompare(right.path);
104
+ })
105
+ .slice(0, maxEntries);
106
+ return {
107
+ entries,
108
+ evictedCount: entries.filter((entry) => entry.retentionState === "evicted").length,
109
+ liveCount: entries.filter((entry) => entry.retentionState === "live").length,
110
+ maxEntries,
111
+ updatedAtMs: nowMs,
112
+ version: SESSION_ARTIFACT_MANIFEST_VERSION,
113
+ };
114
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Purpose: Centralize small artifact state predicates shared by result classifiers, recommendations, and presentation.
3
+ * Responsibilities: Identify pending recording artifacts whose output is not durable until record stop completes.
4
+ * Scope: Artifact predicates only; verification summaries, manifests, and user-facing formatting live in neighboring modules.
5
+ * Usage: Imported by categories, action recommendations, and presentation to avoid divergent artifact-state rules.
6
+ * Invariants/Assumptions: `record start` video artifacts are pending and should not be treated like verified saved files.
7
+ */
8
+
9
+ import type { FileArtifactMetadata } from "./contracts.js";
10
+
11
+ export function isPendingRecordingArtifact(artifact: FileArtifactMetadata): boolean {
12
+ return artifact.command === "record" && artifact.subcommand === "start" && artifact.kind === "video";
13
+ }
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Purpose: Classify successful and failed agent-browser outcomes into stable result categories.
3
+ * Responsibilities: Map artifacts, inspection calls, errors, refs, selectors, downloads, drift, and timeouts to small enums.
4
+ * Scope: Category policy only; next-action recommendations and presentation formatting live elsewhere.
5
+ * Usage: Called by presentation and extension result assembly before details are exposed to Pi.
6
+ * Invariants/Assumptions: Category strings are public machine-readable contracts covered by tests and docs.
7
+ */
8
+
9
+ import type {
10
+ AgentBrowserFailureCategory,
11
+ AgentBrowserResultCategoryDetails,
12
+ AgentBrowserSuccessCategory,
13
+ FileArtifactMetadata,
14
+ SavedFilePresentationDetails,
15
+ } from "./contracts.js";
16
+ import { isPendingRecordingArtifact } from "./artifact-state.js";
17
+
18
+ function hasUnverifiedFileArtifact(artifacts: FileArtifactMetadata[] | undefined): boolean {
19
+ return (artifacts ?? []).some((artifact) => !isPendingRecordingArtifact(artifact) && artifact.exists !== true);
20
+ }
21
+
22
+ export function classifyAgentBrowserSuccessCategory(options: {
23
+ artifacts?: FileArtifactMetadata[];
24
+ inspection?: boolean;
25
+ savedFile?: SavedFilePresentationDetails;
26
+ }): AgentBrowserSuccessCategory {
27
+ if (options.inspection) return "inspection";
28
+ if ((options.artifacts ?? []).length > 0) return hasUnverifiedFileArtifact(options.artifacts) ? "artifact-unverified" : "artifact-saved";
29
+ if (options.savedFile) return "artifact-saved";
30
+ return "completed";
31
+ }
32
+
33
+ export function classifyAgentBrowserFailureCategory(options: {
34
+ args?: string[];
35
+ command?: string;
36
+ confirmationRequired?: boolean;
37
+ errorText?: string;
38
+ parseError?: string;
39
+ spawnError?: string;
40
+ stderr?: string;
41
+ tabDrift?: boolean;
42
+ timedOut?: boolean;
43
+ validationError?: string;
44
+ }): AgentBrowserFailureCategory {
45
+ const text = [options.errorText, options.validationError, options.parseError, options.spawnError, options.stderr].filter(Boolean).join("\n");
46
+ const command = options.command ?? "";
47
+ const usedRef = options.args?.some((arg) => /^@e\d+\b/.test(arg)) ?? false;
48
+ if (options.confirmationRequired || /confirmation required|pending confirmation|requires confirmation/i.test(text)) return "confirmation-required";
49
+ if (options.timedOut || /timeout|timed out|watchdog|IPC read timeout|must stay under its 30s IPC read timeout/i.test(text)) return "timeout";
50
+ 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";
51
+ if (options.parseError || /invalid JSON|missing boolean success|success field must be boolean|returned no JSON output/i.test(text)) return "parse-failure";
52
+ if (/aborted/i.test(text)) return "aborted";
53
+ if (/policy[- ]blocked|blocked by caller policy|caller deny policy|caller allow policy/i.test(text)) return "policy-blocked";
54
+ if (/cleanup failed|cleanup.*partial|partial cleanup|remaining resources/i.test(text)) return "cleanup-failed";
55
+ 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";
56
+ 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";
57
+ if (usedRef && /could not locate element|element not found|no element/i.test(text)) return "stale-ref";
58
+ const mentionsPlaywrightSelectorDialect = /(?:\btext=|:has-text\(|\bgetByRole\b|\bgetByText\b)/i.test(text);
59
+ const reportsSelectorMatchFailure =
60
+ /\b(?:no elements? found|failed to find|could not find|unable to find)\b.*\b(?:selector|locator)\b/i.test(text) ||
61
+ /\b(?:selector|locator)\b.*\b(?:no elements? found|not found|missing|failed to find|could not find|unable to find)\b/i.test(text);
62
+ if (
63
+ /\b(?:unsupported|unknown|invalid)\s+(?:selector|locator)\b/i.test(text) ||
64
+ /\bfailed to parse selector\b/i.test(text) ||
65
+ /\bselector\b.*\b(?:parse|syntax|unsupported|invalid)\b/i.test(text) ||
66
+ (mentionsPlaywrightSelectorDialect && reportsSelectorMatchFailure)
67
+ ) {
68
+ return "selector-unsupported";
69
+ }
70
+ if (command === "find" && /could not locate element|element not found|no elements? found|unable to find/i.test(text)) return "selector-not-found";
71
+ if (reportsSelectorMatchFailure) return "selector-not-found";
72
+ if ((command === "download" || text.includes("wait --download") || /\bdownload\b/i.test(text)) && /missing|not verified|not found|failed|timeout|timed out/i.test(text)) {
73
+ return "download-not-verified";
74
+ }
75
+ if (options.validationError) return "validation-error";
76
+ return "upstream-error";
77
+ }
78
+
79
+
80
+ export function buildAgentBrowserResultCategoryDetails(options: {
81
+ artifacts?: FileArtifactMetadata[];
82
+ args?: string[];
83
+ command?: string;
84
+ confirmationRequired?: boolean;
85
+ errorText?: string;
86
+ failureCategory?: AgentBrowserFailureCategory;
87
+ inspection?: boolean;
88
+ parseError?: string;
89
+ savedFile?: SavedFilePresentationDetails;
90
+ spawnError?: string;
91
+ succeeded: boolean;
92
+ tabDrift?: boolean;
93
+ timedOut?: boolean;
94
+ validationError?: string;
95
+ }): AgentBrowserResultCategoryDetails {
96
+ if (options.succeeded) {
97
+ return {
98
+ resultCategory: "success",
99
+ successCategory: classifyAgentBrowserSuccessCategory(options),
100
+ };
101
+ }
102
+ return {
103
+ failureCategory: options.failureCategory ?? classifyAgentBrowserFailureCategory(options),
104
+ resultCategory: "failure",
105
+ };
106
+ }