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