pi-agent-browser-native 0.6.16 → 0.7.0
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 +18 -1
- package/README.md +59 -165
- package/dist/extensions/agent-browser/index.js +634 -501
- package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
- package/dist/extensions/agent-browser/lib/execution-cwd.js +66 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
- package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +27 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +41 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/operation-paths.js +76 -0
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
- package/dist/extensions/agent-browser/lib/playbook.js +46 -25
- package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
- package/dist/extensions/agent-browser/lib/process.js +16 -13
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
- package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
- package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
- package/dist/extensions/agent-browser/lib/runtime.js +18 -6
- package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
- package/dist/extensions/agent-browser/lib/tool-surface.js +131 -0
- package/dist/extensions/agent-browser/script-worker.js +4 -1
- package/docs/ARCHITECTURE.md +47 -61
- package/docs/COMMAND_REFERENCE.md +78 -137
- package/docs/ELECTRON.md +51 -49
- package/docs/RELEASE.md +31 -15
- package/docs/REQUIREMENTS.md +18 -14
- package/docs/SUPPORT_MATRIX.md +33 -15
- package/docs/TOOL_CONTRACT.md +218 -401
- package/package.json +4 -4
- package/scripts/doctor.mjs +2 -2
|
@@ -1,12 +1,14 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { tmpdir } from "node:os";
|
|
4
5
|
import { basename, dirname, join } from "node:path";
|
|
5
|
-
import { canonicalizeAgentBrowserNamespace,
|
|
6
|
+
import { canonicalizeAgentBrowserNamespace, foldAgentBrowserFilesystemIdentity } from "./argv-grammar.js";
|
|
7
|
+
import { getAgentBrowserSocketDirValidationError, resolveAgentBrowserSocketDir } from "./process.js";
|
|
6
8
|
import { processStartIdentitiesMatch, readProcessStartIdentity } from "./process-identity.js";
|
|
7
9
|
const POLICY_LOCK_WAIT_MS = 1_000;
|
|
8
10
|
const POLICY_LOCK_RETRY_MS = 10;
|
|
9
|
-
const POLICY_LOCK_MAX_BYTES =
|
|
11
|
+
const POLICY_LOCK_MAX_BYTES = 1_048_576; // One claim may cover an entire owned-browser cleanup set.
|
|
10
12
|
const LOCK_OWNER_FILE = "owner.json";
|
|
11
13
|
const LOCK_TICKET_FILE = "ticket.json";
|
|
12
14
|
function getCoordinationDirectory(platform = process.platform) {
|
|
@@ -43,19 +45,133 @@ async function ensureCoordinationDirectory(path, platform) {
|
|
|
43
45
|
return false;
|
|
44
46
|
}
|
|
45
47
|
}
|
|
46
|
-
function
|
|
47
|
-
const
|
|
48
|
-
|
|
48
|
+
export async function resolveBrowserExecutionIdentity(options) {
|
|
49
|
+
const socketDir = resolveAgentBrowserSocketDir(options);
|
|
50
|
+
const namespace = canonicalizeAgentBrowserNamespace(options.namespace);
|
|
51
|
+
let socketContext;
|
|
52
|
+
if (process.platform === "win32") {
|
|
53
|
+
// Native Windows can reach the same namespace/session TCP daemon from
|
|
54
|
+
// different storage roots. Do not split those callers into separate locks.
|
|
55
|
+
socketContext = `win32-native:${namespace ?? ""}`;
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (socketDir === undefined)
|
|
59
|
+
throw new Error("Browser execution coordination requires the native socket directory.");
|
|
60
|
+
const error = await getAgentBrowserSocketDirValidationError(socketDir);
|
|
61
|
+
if (error)
|
|
62
|
+
throw new Error(`Browser execution coordination cannot use socket storage: ${error}.`);
|
|
63
|
+
const root = await realpath(socketDir);
|
|
64
|
+
socketContext = namespace ? join(root, "namespaces", namespace, "run") : root;
|
|
65
|
+
}
|
|
66
|
+
return {
|
|
67
|
+
socketContext: foldAgentBrowserFilesystemIdentity(socketContext, process.platform),
|
|
68
|
+
...(options.sessionName !== undefined ? { sessionName: foldAgentBrowserFilesystemIdentity(options.sessionName, process.platform) } : {}),
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function getBrowserExecutionLockPath(identity) {
|
|
72
|
+
const digest = createHash("sha256").update(identity.socketContext).digest("hex");
|
|
73
|
+
return join(getCoordinationDirectory(), `.pi-agent-browser-policy-${digest}.lock-v4`);
|
|
74
|
+
}
|
|
75
|
+
function groupExecutionIdentities(identities) {
|
|
76
|
+
if (identities.length === 0)
|
|
77
|
+
throw new Error("Browser execution coordination requires at least one identity.");
|
|
78
|
+
const groups = new Map();
|
|
79
|
+
for (const { socketContext, sessionName } of identities) {
|
|
80
|
+
if (sessionName === undefined)
|
|
81
|
+
groups.set(socketContext, null);
|
|
82
|
+
else if (groups.get(socketContext) !== null) {
|
|
83
|
+
const sessions = groups.get(socketContext) ?? new Set();
|
|
84
|
+
sessions.add(sessionName);
|
|
85
|
+
groups.set(socketContext, sessions);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return [...groups.keys()].sort().map(socketContext => {
|
|
89
|
+
const sessions = groups.get(socketContext);
|
|
90
|
+
return { socketContext, sessionNames: sessions ? [...sessions].sort() : null };
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
const executionScope = new AsyncLocalStorage();
|
|
94
|
+
function scopeCovers(outer, inner) {
|
|
95
|
+
return inner.every(identity => outer.some(held => held.socketContext === identity.socketContext
|
|
96
|
+
&& (held.sessionNames === null || identity.sessionNames !== null && identity.sessionNames.every(session => held.sessionNames.includes(session)))));
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Hold across every helper and action (or a complete code cell), not each CLI call.
|
|
100
|
+
* Nested siblings are serialized; recursive inner calls and managed policy checks borrow
|
|
101
|
+
* the claims. Declare every old/new identity up front for replacement or multi-browser cleanup;
|
|
102
|
+
* omit sessionName for close --all. Upgrading mid-operation would allow a lock-order cycle.
|
|
103
|
+
* Sessions sharing a socket context use ONE set-valued claim: acquiring them separately
|
|
104
|
+
* could deadlock against a namespace-wide claimant between the first and second session.
|
|
105
|
+
* Cancellation never releases a still-running callback. The callback must pass its signal
|
|
106
|
+
* to subprocesses and await their termination; finally drains started children before release.
|
|
107
|
+
*/
|
|
108
|
+
export async function withBrowserExecutionLocks(options, run) {
|
|
109
|
+
if (!Number.isFinite(options.deadline))
|
|
110
|
+
throw new Error("Browser execution coordination requires a finite deadline.");
|
|
111
|
+
const identities = groupExecutionIdentities(options.identities);
|
|
112
|
+
const parent = executionScope.getStore();
|
|
113
|
+
if (parent && !parent.active)
|
|
114
|
+
throw new Error("Browser execution scope has already finished.");
|
|
115
|
+
if (parent && !scopeCovers(parent.identities, identities))
|
|
116
|
+
throw new Error("Cannot change or upgrade browser identity inside an execution lock; acquire every required identity before starting.");
|
|
117
|
+
const controller = new AbortController();
|
|
118
|
+
const signals = [controller.signal, options.signal, parent?.signal].filter((signal) => signal !== undefined);
|
|
119
|
+
const signal = AbortSignal.any(signals);
|
|
120
|
+
const expire = () => controller.abort(new DOMException("Browser execution deadline exceeded.", "TimeoutError"));
|
|
121
|
+
const remaining = options.deadline - Date.now();
|
|
122
|
+
const timer = setTimeout(expire, Math.max(0, Math.min(remaining, 2_147_483_647)));
|
|
123
|
+
const checkBudget = () => {
|
|
124
|
+
if (Date.now() >= options.deadline)
|
|
125
|
+
expire();
|
|
126
|
+
signal.throwIfAborted();
|
|
127
|
+
};
|
|
128
|
+
const locks = [];
|
|
129
|
+
try {
|
|
130
|
+
checkBudget();
|
|
131
|
+
if (!parent) {
|
|
132
|
+
for (const identity of identities) {
|
|
133
|
+
const lock = await acquireExecutionClaim({ identity, deadline: options.deadline, signal });
|
|
134
|
+
if (lock)
|
|
135
|
+
locks.push(lock);
|
|
136
|
+
checkBudget();
|
|
137
|
+
if (!lock)
|
|
138
|
+
throw new Error("Browser execution coordination is unavailable or busy; no browser command was run.");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const execute = async () => {
|
|
142
|
+
checkBudget();
|
|
143
|
+
if (options.waitOnly)
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
const scope = { identities: parent?.identities ?? identities, active: true, signal, children: Promise.resolve() };
|
|
146
|
+
try {
|
|
147
|
+
return await executionScope.run(scope, () => run(signal));
|
|
148
|
+
}
|
|
149
|
+
finally {
|
|
150
|
+
scope.active = false;
|
|
151
|
+
await scope.children;
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
if (!parent)
|
|
155
|
+
return await execute();
|
|
156
|
+
const result = parent.children.then(execute);
|
|
157
|
+
parent.children = result.then(() => undefined, () => undefined);
|
|
158
|
+
return await result;
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
clearTimeout(timer);
|
|
162
|
+
await Promise.all(locks.reverse().map(lock => lock.release()));
|
|
163
|
+
}
|
|
49
164
|
}
|
|
50
|
-
export function
|
|
51
|
-
return
|
|
165
|
+
export function withBrowserExecutionLock(options, run) {
|
|
166
|
+
return withBrowserExecutionLocks({ identities: [options.identity], signal: options.signal, deadline: options.deadline, waitOnly: options.waitOnly }, run);
|
|
52
167
|
}
|
|
53
168
|
function parseOwner(content) {
|
|
54
169
|
if (Buffer.byteLength(content) > POLICY_LOCK_MAX_BYTES)
|
|
55
170
|
return undefined;
|
|
56
171
|
try {
|
|
57
172
|
const parsed = JSON.parse(content);
|
|
58
|
-
return parsed.version ===
|
|
173
|
+
return parsed.version === 4
|
|
174
|
+
&& (parsed.sessionNames === null || Array.isArray(parsed.sessionNames) && parsed.sessionNames.length > 0 && parsed.sessionNames.every(session => typeof session === "string"))
|
|
59
175
|
&& Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0
|
|
60
176
|
&& typeof parsed.startIdentity === "string" && parsed.startIdentity.length > 0
|
|
61
177
|
&& typeof parsed.token === "string" && parsed.token.length > 0
|
|
@@ -71,7 +187,7 @@ function parseTicket(content, token) {
|
|
|
71
187
|
return undefined;
|
|
72
188
|
try {
|
|
73
189
|
const parsed = JSON.parse(content);
|
|
74
|
-
return parsed.version ===
|
|
190
|
+
return parsed.version === 4
|
|
75
191
|
&& parsed.token === token
|
|
76
192
|
&& Number.isSafeInteger(parsed.ticket)
|
|
77
193
|
&& (parsed.ticket ?? 0) > 0
|
|
@@ -150,7 +266,7 @@ async function readClaims(basePath) {
|
|
|
150
266
|
}
|
|
151
267
|
return claims;
|
|
152
268
|
}
|
|
153
|
-
async function ownerAlive(owner) {
|
|
269
|
+
async function ownerAlive(owner, budget) {
|
|
154
270
|
try {
|
|
155
271
|
process.kill(owner.pid, 0);
|
|
156
272
|
}
|
|
@@ -161,7 +277,7 @@ async function ownerAlive(owner) {
|
|
|
161
277
|
if (code !== "EPERM")
|
|
162
278
|
return undefined;
|
|
163
279
|
}
|
|
164
|
-
const current = await readProcessStartIdentity(owner.pid);
|
|
280
|
+
const current = await readProcessStartIdentity(owner.pid, process.platform, budget);
|
|
165
281
|
return current === undefined ? undefined : processStartIdentitiesMatch(owner.startIdentity, current);
|
|
166
282
|
}
|
|
167
283
|
async function removeClaimOwnedBy(path, token, deadline = 0) {
|
|
@@ -199,7 +315,7 @@ async function removeClaimOwnedBy(path, token, deadline = 0) {
|
|
|
199
315
|
await rm(movedPath, { force: true, recursive: true });
|
|
200
316
|
return true;
|
|
201
317
|
}
|
|
202
|
-
async function cleanDeadPolicyArtifacts(directory) {
|
|
318
|
+
async function cleanDeadPolicyArtifacts(directory, budget) {
|
|
203
319
|
let names;
|
|
204
320
|
try {
|
|
205
321
|
names = await readdir(directory);
|
|
@@ -208,10 +324,12 @@ async function cleanDeadPolicyArtifacts(directory) {
|
|
|
208
324
|
return;
|
|
209
325
|
}
|
|
210
326
|
for (const name of names.filter((candidate) => candidate.startsWith(".pi-agent-browser-policy-remove-")
|
|
211
|
-
|| candidate.includes(".lock-
|
|
327
|
+
|| candidate.includes(".lock-v4.candidate-"))) {
|
|
328
|
+
if (budget.signal?.aborted || Date.now() >= (budget.deadline ?? Infinity))
|
|
329
|
+
return;
|
|
212
330
|
const path = join(directory, name);
|
|
213
331
|
const claim = await readClaim(path);
|
|
214
|
-
if (claim && await ownerAlive(claim.owner) === false)
|
|
332
|
+
if (claim && await ownerAlive(claim.owner, budget) === false)
|
|
215
333
|
await rm(path, { force: true, recursive: true }).catch(() => undefined);
|
|
216
334
|
}
|
|
217
335
|
}
|
|
@@ -244,23 +362,48 @@ function waitForRetry(signal) {
|
|
|
244
362
|
export async function acquireManagedSessionPolicyLock(options) {
|
|
245
363
|
if (options.signal?.aborted)
|
|
246
364
|
return undefined;
|
|
365
|
+
let identity;
|
|
366
|
+
try {
|
|
367
|
+
identity = await resolveBrowserExecutionIdentity({ ...options, ownedManagedSession: true });
|
|
368
|
+
}
|
|
369
|
+
catch {
|
|
370
|
+
return undefined;
|
|
371
|
+
}
|
|
372
|
+
const scope = executionScope.getStore();
|
|
373
|
+
if (scope) {
|
|
374
|
+
if (!scope.active || !scopeCovers(scope.identities, groupExecutionIdentities([identity])))
|
|
375
|
+
return undefined;
|
|
376
|
+
return { release: async () => undefined };
|
|
377
|
+
}
|
|
378
|
+
return acquireExecutionClaim({ identity: groupExecutionIdentities([identity])[0], signal: options.signal, timeoutMs: options.timeoutMs });
|
|
379
|
+
}
|
|
380
|
+
async function acquireExecutionClaim(options) {
|
|
381
|
+
if (options.signal?.aborted)
|
|
382
|
+
return undefined;
|
|
383
|
+
const deadline = options.deadline ?? Date.now() + (options.timeoutMs ?? POLICY_LOCK_WAIT_MS);
|
|
384
|
+
// timeoutMs: 0 retains the existing single-attempt policy probe contract.
|
|
385
|
+
const budget = { signal: options.signal, deadline: options.timeoutMs === 0 ? undefined : deadline };
|
|
247
386
|
const platform = process.platform;
|
|
248
387
|
const directory = getCoordinationDirectory(platform);
|
|
249
388
|
if (!await ensureCoordinationDirectory(directory, platform))
|
|
250
389
|
return undefined;
|
|
251
|
-
const basePath =
|
|
390
|
+
const basePath = getBrowserExecutionLockPath(options.identity);
|
|
252
391
|
const token = randomUUID();
|
|
253
|
-
const startIdentity = await readProcessStartIdentity(process.pid);
|
|
392
|
+
const startIdentity = await readProcessStartIdentity(process.pid, process.platform, budget);
|
|
254
393
|
if (!startIdentity)
|
|
255
394
|
return undefined;
|
|
256
|
-
const owner = { pid: process.pid, startIdentity, token, version:
|
|
395
|
+
const owner = { pid: process.pid, startIdentity, token, sessionNames: options.identity.sessionNames, version: 4 };
|
|
396
|
+
const ownerContent = JSON.stringify(owner);
|
|
397
|
+
// Never publish metadata our reader cannot validate and ownership-safely remove.
|
|
398
|
+
if (Buffer.byteLength(ownerContent) > POLICY_LOCK_MAX_BYTES)
|
|
399
|
+
return undefined;
|
|
257
400
|
const candidatePath = `${basePath}.candidate-${token}`;
|
|
258
401
|
const claimPath = `${basePath}.claim-${token}`;
|
|
259
402
|
let claimPublished = false;
|
|
260
403
|
let lockAcquired = false;
|
|
261
404
|
try {
|
|
262
405
|
await mkdir(candidatePath, { mode: 0o700 });
|
|
263
|
-
await writeFile(join(candidatePath, LOCK_OWNER_FILE),
|
|
406
|
+
await writeFile(join(candidatePath, LOCK_OWNER_FILE), ownerContent, { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
264
407
|
await rename(candidatePath, claimPath);
|
|
265
408
|
claimPublished = true;
|
|
266
409
|
const initialClaims = await readClaims(basePath);
|
|
@@ -269,11 +412,10 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
269
412
|
const maxTicket = initialClaims.reduce((max, claim) => claim.ticket === null ? max : Math.max(max, claim.ticket), 0);
|
|
270
413
|
if (!Number.isSafeInteger(maxTicket + 1))
|
|
271
414
|
return undefined;
|
|
272
|
-
const ticket = { ticket: maxTicket + 1, token, version:
|
|
415
|
+
const ticket = { ticket: maxTicket + 1, token, version: 4 };
|
|
273
416
|
const ticketCandidatePath = join(claimPath, `.ticket-${token}.tmp`);
|
|
274
417
|
await writeFile(ticketCandidatePath, JSON.stringify(ticket), { encoding: "utf8", flag: "wx", mode: 0o600 });
|
|
275
418
|
await rename(ticketCandidatePath, join(claimPath, LOCK_TICKET_FILE));
|
|
276
|
-
const deadline = Date.now() + (options.timeoutMs ?? POLICY_LOCK_WAIT_MS);
|
|
277
419
|
while (!options.signal?.aborted) {
|
|
278
420
|
const claims = await readClaims(basePath);
|
|
279
421
|
if (!claims)
|
|
@@ -283,13 +425,15 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
283
425
|
return undefined;
|
|
284
426
|
let blocked = false;
|
|
285
427
|
for (const claim of claims) {
|
|
286
|
-
|
|
428
|
+
const disjoint = owner.sessionNames !== null && claim.owner.sessionNames !== null
|
|
429
|
+
&& !owner.sessionNames.some(session => claim.owner.sessionNames.includes(session));
|
|
430
|
+
if (claim.owner.token === token || disjoint || !claimPrecedes(claim, ownClaim))
|
|
287
431
|
continue;
|
|
288
432
|
// Choosing is transient: a later published ticket must not make
|
|
289
433
|
// its predecessor wait on it using an earlier null snapshot.
|
|
290
434
|
if (await hasPublishedLaterTicket(claim, ownClaim))
|
|
291
435
|
continue;
|
|
292
|
-
const alive = await ownerAlive(claim.owner);
|
|
436
|
+
const alive = await ownerAlive(claim.owner, budget);
|
|
293
437
|
if (alive === false) {
|
|
294
438
|
await removeClaimOwnedBy(claim.path, claim.owner.token);
|
|
295
439
|
continue;
|
|
@@ -311,7 +455,9 @@ export async function acquireManagedSessionPolicyLock(options) {
|
|
|
311
455
|
break;
|
|
312
456
|
}
|
|
313
457
|
if (!blocked) {
|
|
314
|
-
await cleanDeadPolicyArtifacts(directory);
|
|
458
|
+
await cleanDeadPolicyArtifacts(directory, budget);
|
|
459
|
+
if (options.signal?.aborted || (budget.deadline !== undefined && Date.now() >= budget.deadline))
|
|
460
|
+
return undefined;
|
|
315
461
|
lockAcquired = true;
|
|
316
462
|
return { release: async () => { await removeClaimOwnedBy(claimPath, token, Date.now() + POLICY_LOCK_WAIT_MS); } };
|
|
317
463
|
}
|
|
@@ -2,7 +2,7 @@ import { lstatSync, readlinkSync, realpathSync, statSync } from "node:fs";
|
|
|
2
2
|
import { basename, dirname, extname, join, resolve } from "node:path";
|
|
3
3
|
import { foldAgentBrowserFilesystemIdentity } from "../../argv-grammar.js";
|
|
4
4
|
import { parseWaitCommandTokens } from "../../argv-descriptor.js";
|
|
5
|
-
import { getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
5
|
+
import { getRecordCommandOperandIndices, getRecordCommandOperands } from "../../command-taxonomy.js";
|
|
6
6
|
const SCREENSHOT_IMAGE_EXTENSIONS = [".jpeg", ".jpg", ".png", ".webp"];
|
|
7
7
|
function isSingleScreenshotPathToken(token) {
|
|
8
8
|
const explicitlyRelative = token.startsWith("./") || token.startsWith("../");
|
|
@@ -38,20 +38,25 @@ export function getScreenshotPathTokenIndex(commandTokens) {
|
|
|
38
38
|
return undefined;
|
|
39
39
|
}
|
|
40
40
|
const DIFF_SCREENSHOT_VALUE_FLAGS = new Set(["-b", "--baseline", "-o", "--output", "-s", "--selector", "-t", "--threshold"]);
|
|
41
|
-
function
|
|
42
|
-
|
|
41
|
+
export function getDiffFilePathIndices(commandTokens) {
|
|
42
|
+
if (commandTokens[0] !== "diff" || !["snapshot", "screenshot"].includes(commandTokens[1]))
|
|
43
|
+
return {};
|
|
44
|
+
const valueFlags = commandTokens[1] === "screenshot" ? DIFF_SCREENSHOT_VALUE_FLAGS : new Set(["-b", "--baseline", "-s", "--selector", "-d", "--depth"]);
|
|
45
|
+
const paths = {};
|
|
43
46
|
for (let index = 2; index < commandTokens.length; index += 1) {
|
|
44
47
|
const token = commandTokens[index];
|
|
45
|
-
if (!
|
|
48
|
+
if (!valueFlags.has(token))
|
|
46
49
|
continue;
|
|
47
50
|
const value = commandTokens[index + 1];
|
|
48
51
|
if (value === undefined)
|
|
49
|
-
return
|
|
52
|
+
return {};
|
|
50
53
|
if (token === "-o" || token === "--output")
|
|
51
|
-
|
|
54
|
+
paths.output = index + 1;
|
|
55
|
+
if (token === "-b" || token === "--baseline")
|
|
56
|
+
paths.baseline = index + 1;
|
|
52
57
|
index += 1;
|
|
53
58
|
}
|
|
54
|
-
return
|
|
59
|
+
return paths;
|
|
55
60
|
}
|
|
56
61
|
function canonicalizeArtifactPath(absolutePath, platform, seenSymlinks) {
|
|
57
62
|
let cursor = absolutePath;
|
|
@@ -102,28 +107,30 @@ export function getRecordContactSheetDestination(commandTokens) {
|
|
|
102
107
|
const extension = extname(path);
|
|
103
108
|
return extension ? `${path.slice(0, -extension.length)}.contact-sheet.png` : undefined;
|
|
104
109
|
}
|
|
105
|
-
export function
|
|
110
|
+
export function getExplicitArtifactDestinationIndex(commandTokens) {
|
|
106
111
|
const command = commandTokens[0];
|
|
107
112
|
const subcommand = commandTokens[1];
|
|
108
|
-
if (command === "screenshot")
|
|
109
|
-
|
|
110
|
-
return index === undefined ? undefined : commandTokens[index];
|
|
111
|
-
}
|
|
113
|
+
if (command === "screenshot")
|
|
114
|
+
return getScreenshotPathTokenIndex(commandTokens);
|
|
112
115
|
if (command === "download")
|
|
113
|
-
return
|
|
116
|
+
return 2;
|
|
114
117
|
if (command === "pdf")
|
|
115
|
-
return
|
|
118
|
+
return 1;
|
|
116
119
|
if (command === "wait")
|
|
117
|
-
return parseWaitCommandTokens(commandTokens).
|
|
120
|
+
return parseWaitCommandTokens(commandTokens).downloadPathIndex;
|
|
118
121
|
if (command === "state" && subcommand === "save")
|
|
119
|
-
return
|
|
122
|
+
return 2;
|
|
120
123
|
if (command === "diff" && subcommand === "screenshot")
|
|
121
|
-
return
|
|
124
|
+
return getDiffFilePathIndices(commandTokens).output;
|
|
122
125
|
if (command === "network" && subcommand === "har" && commandTokens[2] === "stop")
|
|
123
|
-
return
|
|
126
|
+
return 3;
|
|
124
127
|
if ((command === "trace" || command === "profiler") && subcommand === "stop")
|
|
125
|
-
return
|
|
128
|
+
return 2;
|
|
126
129
|
if (command === "record")
|
|
127
|
-
return
|
|
130
|
+
return getRecordCommandOperandIndices(commandTokens)[0];
|
|
128
131
|
return undefined;
|
|
129
132
|
}
|
|
133
|
+
export function getExplicitArtifactDestination(commandTokens) {
|
|
134
|
+
const index = getExplicitArtifactDestinationIndex(commandTokens);
|
|
135
|
+
return index === undefined ? undefined : commandTokens[index];
|
|
136
|
+
}
|
|
@@ -432,7 +432,7 @@ export function buildSourceLookupElectronNextActions(sourceLookup) {
|
|
|
432
432
|
if (sessionName)
|
|
433
433
|
actions.push({ id: "snapshot-electron-session", params: { args: withOptionalSessionArgs(sessionName, ["snapshot", "-i"]) }, reason: "Refresh interactive refs in the attached Electron session before retrying source lookup with a narrower target.", safety: "Read-only snapshot; no app mutation.", tool: "agent_browser" });
|
|
434
434
|
if (launchId)
|
|
435
|
-
actions.push({ id: "probe-electron-launch", params: {
|
|
435
|
+
actions.push({ id: "probe-electron-launch", params: { action: "probe", launchId }, reason: "Collect bounded wrapper/session context for the packaged Electron launch after sourceLookup found no candidates.", safety: "Read-only probe of title, URL, focus, tabs, and compact snapshot metadata.", tool: "agent_browser_electron" });
|
|
436
436
|
if (sessionName)
|
|
437
437
|
actions.push({ id: "list-electron-tabs", params: { args: withOptionalSessionArgs(sessionName, ["tab", "list"]) }, reason: "Check current Electron tabs/targets before choosing a narrower selector or @ref.", safety: "Read-only tab listing.", tool: "agent_browser" });
|
|
438
438
|
return actions;
|
|
@@ -863,7 +863,7 @@ export async function collectTimeoutPartialProgress(options) {
|
|
|
863
863
|
if ((options.commandTokens[0] === "session" && options.commandTokens[1] === "info") || isBrowserIndependentRead(options.commandTokens, options.stdin))
|
|
864
864
|
return undefined;
|
|
865
865
|
const rawSteps = getTimeoutProgressSteps(options.compiledJob, options.commandTokens, options.stdin);
|
|
866
|
-
const artifacts = await collectTimeoutArtifactEvidence(options.cwd, rawSteps);
|
|
866
|
+
const artifacts = await collectTimeoutArtifactEvidence(options.operationCwd ?? options.cwd, rawSteps);
|
|
867
867
|
const urlData = await runSessionCommandData({ args: ["get", "url"], cwd: options.cwd, namespace: options.namespace, sessionName: options.sessionName });
|
|
868
868
|
const recoveredUrl = extractStringResultField(urlData, "result") ?? extractStringResultField(urlData, "url");
|
|
869
869
|
const titleData = recoveredUrl
|
|
@@ -901,6 +901,18 @@ function sanitizeCurrentPageUrlForTimeoutDiagnostic(url) {
|
|
|
901
901
|
return redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(url));
|
|
902
902
|
}
|
|
903
903
|
}
|
|
904
|
+
export function redactTimeoutPartialProgress(progress) {
|
|
905
|
+
const redact = (value) => {
|
|
906
|
+
if (typeof value === "string")
|
|
907
|
+
return /^https?:\/\//i.test(value) ? sanitizeCurrentPageUrlForTimeoutDiagnostic(value) : redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(value));
|
|
908
|
+
if (Array.isArray(value))
|
|
909
|
+
return value.map(redact);
|
|
910
|
+
if (isRecord(value))
|
|
911
|
+
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, redact(item)]));
|
|
912
|
+
return value;
|
|
913
|
+
};
|
|
914
|
+
return redact(progress);
|
|
915
|
+
}
|
|
904
916
|
export function formatTimeoutPartialProgressText(progress, pageTargetUnknown = false) {
|
|
905
917
|
const lines = [`Timeout partial progress: ${progress.summary}`];
|
|
906
918
|
const currentPageTitle = progress.currentPage?.title ? redactSensitivePathSegmentsForDiagnostic(redactSensitiveText(progress.currentPage.title)) : undefined;
|
|
@@ -4,6 +4,7 @@ import { redactNetworkSourceLookupSurface } from "../../input-modes/lookups.js";
|
|
|
4
4
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
5
5
|
import { buildAgentBrowserResultCategoryDetails } from "../../results/categories.js";
|
|
6
6
|
import { extractAgentBrowserLifecycle } from "../../results/presentation/common.js";
|
|
7
|
+
import { projectAgentBrowserObservation } from "../../results/presentation/content.js";
|
|
7
8
|
import { formatSessionArtifactRetentionSummary } from "../../results/artifact-manifest.js";
|
|
8
9
|
import { alignPageChangeSummaryNextActionIds, appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, isStandaloneSnapshotNextAction, withOptionalSessionArgs, } from "../../results/next-actions.js";
|
|
9
10
|
import { buildConnectedSessionNextActions, buildNoActivePageNextActions, buildPendingWebMcpNextActions, buildSessionAwareStaleRefNextActions, buildSessionTabRecoveryNextActions, } from "../../results/recovery-next-actions.js";
|
|
@@ -86,9 +87,7 @@ export function redactRecoveryHint(recoveryHint) {
|
|
|
86
87
|
}
|
|
87
88
|
export function buildJsonVisibleContent(options) {
|
|
88
89
|
const { error, presentation, succeeded, warnings } = options;
|
|
89
|
-
const payload =
|
|
90
|
-
if (isRecord(payload) && isRecord(payload.data) && isRecord(presentation.data) && typeof presentation.data.wsUrl === "string")
|
|
91
|
-
payload.data.wsUrl = presentation.data.wsUrl;
|
|
90
|
+
const payload = projectAgentBrowserObservation({ ...presentation, ...options.details, error, warnings: warnings && warnings.length > 0 ? warnings : undefined }, succeeded);
|
|
92
91
|
const images = presentation.content.filter((item) => item.type === "image");
|
|
93
92
|
return [{ type: "text", text: JSON.stringify(payload, null, 2) }, ...images];
|
|
94
93
|
}
|
|
@@ -175,8 +174,9 @@ export function buildRedactedPresentationContent(options) {
|
|
|
175
174
|
return contentWithSessionWarnings.map((item) => {
|
|
176
175
|
if (item.type !== "text")
|
|
177
176
|
return item;
|
|
178
|
-
|
|
179
|
-
|
|
177
|
+
if (userRequestedJson && !plainTextInspection)
|
|
178
|
+
return { ...item, text: JSON.stringify(redactExactSensitiveValue(JSON.parse(item.text), exactSensitiveValues), null, 2) };
|
|
179
|
+
return { ...item, text: redactSensitiveText(redactExactSensitiveText(item.text, exactSensitiveValues)) };
|
|
180
180
|
});
|
|
181
181
|
}
|
|
182
182
|
export async function prepareFinalResultRecoveryState(options) {
|
|
@@ -339,6 +339,10 @@ function buildResultNextActions(options) {
|
|
|
339
339
|
append(buildComboboxFocusNextActions(options.executionPlan.sessionName));
|
|
340
340
|
if (options.managedSessionOutcome)
|
|
341
341
|
appendUnique(buildManagedSessionFreshFailureNextActions(options.managedSessionOutcome));
|
|
342
|
+
if (options.currentSessionTabTargetUnknown && ["aborted", "parse-failure"].includes(options.categoryDetails.failureCategory ?? "")) {
|
|
343
|
+
nextActions = nextActions.filter(action => !isStandaloneSnapshotNextAction(action));
|
|
344
|
+
appendUnique([{ id: "verify-page-target-after-interruption", tool: "agent_browser", params: { args: withOptionalSessionArgs(options.executionPlan.sessionName, ["batch", "--bail"]), stdin: JSON.stringify([["get", "url"], ["snapshot", "-i"]]) }, reason: "Verify the current URL and inspect the page after an interrupted operation.", safety: "Read-only recovery. The mutation may already have happened; inspect before deciding whether to retry." }]);
|
|
345
|
+
}
|
|
342
346
|
if (options.categoryDetails.failureCategory === "timeout" && options.processResult.timedOut) {
|
|
343
347
|
if (options.currentSessionTabTargetUnknown)
|
|
344
348
|
nextActions = nextActions.filter((action) => !isStandaloneSnapshotNextAction(action));
|
|
@@ -354,14 +358,11 @@ function buildResultNextActions(options) {
|
|
|
354
358
|
export function formatAgentBrowserNextActionsText(nextActions) {
|
|
355
359
|
if (!nextActions || nextActions.length === 0)
|
|
356
360
|
return undefined;
|
|
357
|
-
const lines = nextActions.
|
|
358
|
-
const
|
|
359
|
-
? { ...action.params, ...(action.params.stdin !== undefined && action.params.stdin.length > 500 ? { stdin: "[omitted; use details.nextActions]" } : {}) }
|
|
360
|
-
: undefined;
|
|
361
|
-
const payload = action.artifactPath ? { artifactPath: action.artifactPath } : params;
|
|
361
|
+
const lines = nextActions.map((action) => {
|
|
362
|
+
const payload = action.artifactPath ? { artifactPath: action.artifactPath } : action.params;
|
|
362
363
|
return `- ${action.id}${payload ? ` ${redactSensitiveText(JSON.stringify(payload))}` : ""}: ${redactSensitiveText(action.reason)}`;
|
|
363
364
|
});
|
|
364
|
-
return ["Next actions:", ...lines
|
|
365
|
+
return ["Next actions:", ...lines].join("\n");
|
|
365
366
|
}
|
|
366
367
|
function formatFailureNextActionsText(options, nextActions) {
|
|
367
368
|
return options.categoryDetails.resultCategory === "failure" ? formatAgentBrowserNextActionsText(nextActions) : undefined;
|
|
@@ -438,6 +439,7 @@ function buildAgentBrowserResultDetails(options, nextActions) {
|
|
|
438
439
|
managedSessionOutcome: options.managedSessionOutcome,
|
|
439
440
|
imagePath: options.presentation.imagePath,
|
|
440
441
|
imagePaths: options.presentation.imagePaths,
|
|
442
|
+
imageObservations: options.presentation.imageObservations,
|
|
441
443
|
nextActions,
|
|
442
444
|
pageChangeSummary,
|
|
443
445
|
clickDispatch: options.clickDispatchDiagnostic,
|
|
@@ -485,6 +487,10 @@ export function buildFinalAgentBrowserToolResult(options) {
|
|
|
485
487
|
const lifecycle = extractAgentBrowserLifecycle(options.presentationEnvelope?.data);
|
|
486
488
|
const browserWindow = buildBrowserWindowStatus(options, lifecycle);
|
|
487
489
|
const details = buildAgentBrowserResultDetails(options, nextActions);
|
|
490
|
+
if (options.modelVisible === false) {
|
|
491
|
+
const result = { content: [], details: redactToolDetails(details, options.exactSensitiveValues), isError: !options.succeeded };
|
|
492
|
+
return options.compiledNetworkSourceLookup ? redactNetworkSourceLookupSurface(result) : result;
|
|
493
|
+
}
|
|
488
494
|
const visibleRefFallbackText = formatVisibleRefFallbackText(options.visibleRefFallbackDiagnostic);
|
|
489
495
|
const richInputRecoveryText = formatRichInputRecoveryText(options.richInputRecoveryDiagnostic);
|
|
490
496
|
const semanticActionCandidateText = nextActions ? formatSemanticActionCandidateText(nextActions) : undefined;
|
|
@@ -509,10 +515,17 @@ export function buildFinalAgentBrowserToolResult(options) {
|
|
|
509
515
|
const appendedDiagnosticText = redactSensitiveText(redactExactSensitiveText(rawAppendedDiagnosticText, options.exactSensitiveValues));
|
|
510
516
|
const shouldAppendDiagnosticText = appendedDiagnosticText.length > 0 && (!options.userRequestedJson || options.plainTextInspection);
|
|
511
517
|
let content = shouldAppendDiagnosticText && options.redactedContent[0]?.type === "text" ? [{ ...options.redactedContent[0], text: `${options.redactedContent[0].text}\n\n${appendedDiagnosticText}` }, ...options.redactedContent.slice(1)] : options.redactedContent;
|
|
512
|
-
if (options.electronLaunchRecord && options.succeeded && content[0]?.type === "text") {
|
|
518
|
+
if (options.electronLaunchRecord && options.succeeded && !options.userRequestedJson && content[0]?.type === "text") {
|
|
513
519
|
content = [{ ...content[0], text: redactSensitiveText(formatElectronLaunchText({ handoff: options.electronHandoff, record: options.electronLaunchRecord, targets: options.electronLaunch?.targets ?? [], upstreamText: content[0].text })) }, ...content.slice(1)];
|
|
514
520
|
}
|
|
515
|
-
const
|
|
521
|
+
const redactedDetails = redactToolDetails(details, options.exactSensitiveValues);
|
|
522
|
+
if (options.userRequestedJson && !options.plainTextInspection) {
|
|
523
|
+
const prior = options.redactedContent[0]?.type === "text" ? JSON.parse(options.redactedContent[0].text) : undefined;
|
|
524
|
+
if (Array.isArray(prior?.warnings))
|
|
525
|
+
redactedDetails.warnings = prior.warnings;
|
|
526
|
+
content = buildJsonVisibleContent({ error: redactedDetails.error, details: redactedDetails, presentation: options.presentation, succeeded: options.succeeded, warnings: prior?.warnings });
|
|
527
|
+
}
|
|
528
|
+
const result = { content, details: redactedDetails, isError: !options.succeeded };
|
|
516
529
|
return options.compiledNetworkSourceLookup ? redactNetworkSourceLookupSurface(result) : result;
|
|
517
530
|
}
|
|
518
531
|
export function isMissingAgentBrowserBinary(processResult) {
|
|
@@ -6,6 +6,7 @@ import { formatWebMcpCatalogUpdate } from "../../results/presentation/common.js"
|
|
|
6
6
|
import { withOwnedManagedSessionContext } from "../../managed-session-restore.js";
|
|
7
7
|
import { cleanupClickDispatchProbe } from "./click-dispatch.js";
|
|
8
8
|
import { applyBrowserRunStatePatch, getSessionContextKey } from "./session-state.js";
|
|
9
|
+
import { buildScreenshotGeometry, collectScreenshotSample } from "./screenshot-observation.js";
|
|
9
10
|
import { buildJsonVisibleContent, buildMissingBinaryFailureResult } from "./final-result.js";
|
|
10
11
|
import { prepareBrowserRun } from "./prepare.js";
|
|
11
12
|
import { processBrowserOutput } from "./process-output.js";
|
|
@@ -27,6 +28,12 @@ export async function runAgentBrowserTool(options) {
|
|
|
27
28
|
result.content.push({ type: "text", text: notice });
|
|
28
29
|
}
|
|
29
30
|
}
|
|
31
|
+
if (result.isError && details?.error === undefined) {
|
|
32
|
+
details = { ...details, error: details?.validationError ?? details?.summary ?? result.content.filter(part => part.type === "text").map(part => part.text).join("\n") };
|
|
33
|
+
result.details = details;
|
|
34
|
+
}
|
|
35
|
+
if (options.modelVisible === false)
|
|
36
|
+
result.content = [];
|
|
30
37
|
const sessionKey = getSessionContextKey(typeof details?.sessionName === "string" ? details.sessionName : undefined, typeof details?.namespace === "string" ? details.namespace : undefined);
|
|
31
38
|
const page = options.state.sessionPageState.get(sessionKey);
|
|
32
39
|
return page.tabReopenPending === undefined ? result : {
|
|
@@ -39,11 +46,12 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
39
46
|
applyBrowserRunStatePatch(options.state, preparedResult.kind === "ready" ? preparedResult.prepared.statePatch : preparedResult.statePatch);
|
|
40
47
|
if (preparedResult.kind === "early-result") {
|
|
41
48
|
const result = preparedResult.result;
|
|
42
|
-
if (options.input.toolArgs.includes("--json") && !isPlainTextInspectionArgs(options.input.toolArgs)) {
|
|
49
|
+
if (options.modelVisible !== false && options.input.toolArgs.includes("--json") && !isPlainTextInspectionArgs(options.input.toolArgs)) {
|
|
43
50
|
const details = isRecord(result.details) ? result.details : {};
|
|
44
51
|
const summary = result.content.filter(item => item.type === "text").map(item => item.text).join("\n");
|
|
45
52
|
result.content = buildJsonVisibleContent({
|
|
46
53
|
error: result.isError ? details.validationError ?? summary : null,
|
|
54
|
+
details,
|
|
47
55
|
presentation: { content: result.content, data: details.data, summary },
|
|
48
56
|
succeeded: result.isError !== true,
|
|
49
57
|
});
|
|
@@ -54,6 +62,11 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
54
62
|
const ownedManagedSession = prepared.ownedManagedSessionContext;
|
|
55
63
|
return await withOwnedManagedSessionContext(ownedManagedSession, async () => {
|
|
56
64
|
try {
|
|
65
|
+
const geometryOptions = { command: prepared.commandTokens, cwd: options.cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal: options.signal,
|
|
66
|
+
env: ownedManagedSession ? { AGENT_BROWSER_IDLE_TIMEOUT_MS: options.implicitSessionIdleTimeoutMs } : undefined };
|
|
67
|
+
// Do not start a cold browser through a probe or consume launch flags ahead of the requested capture.
|
|
68
|
+
const probeGeometry = prepared.commandTokens[0] === "screenshot" && !!prepared.priorSessionTabTarget && !prepared.priorSessionTabTargetUnknown && !prepared.executionPlan.startupScopedFlags?.length;
|
|
69
|
+
const before = probeGeometry ? await withChromeStartupArgs(prepared.chromeStartupArgs, () => collectScreenshotSample(geometryOptions)) : undefined;
|
|
57
70
|
const artifactRunStartedAtMs = Date.now();
|
|
58
71
|
const processResult = await withChromeStartupArgs(prepared.chromeStartupArgs, () => runAgentBrowserProcess({
|
|
59
72
|
args: prepared.processArgs,
|
|
@@ -70,6 +83,8 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
70
83
|
stdin: prepared.processStdin,
|
|
71
84
|
timeoutMs: prepared.processTimeoutMs,
|
|
72
85
|
}));
|
|
86
|
+
const after = probeGeometry && !processResult.aborted && !processResult.timedOut && processResult.exitCode === 0
|
|
87
|
+
? await withChromeStartupArgs(prepared.chromeStartupArgs, () => collectScreenshotSample(geometryOptions)) : undefined;
|
|
73
88
|
const missingBinaryResult = await buildMissingBinaryFailureResult({
|
|
74
89
|
compatibilityWorkaround: prepared.compatibilityWorkaround,
|
|
75
90
|
electronLaunch: prepared.electronLaunch,
|
|
@@ -88,6 +103,11 @@ async function runAgentBrowserToolInContext(options) {
|
|
|
88
103
|
return missingBinaryResult;
|
|
89
104
|
const output = await processBrowserOutput({ ...options, artifactRunStartedAtMs, prepared, processResult });
|
|
90
105
|
applyBrowserRunStatePatch(options.state, output.statePatch);
|
|
106
|
+
if (isRecord(output.result.details) && Array.isArray(output.result.details.imageObservations) && prepared.commandTokens[0] === "screenshot") {
|
|
107
|
+
output.result.details.imageObservations = redactSensitiveValue(output.result.details.imageObservations.map(image => ({
|
|
108
|
+
...image, geometry: buildScreenshotGeometry({ capture: image.capture, pixels: image.pixels, before, after }),
|
|
109
|
+
})));
|
|
110
|
+
}
|
|
91
111
|
return output.result;
|
|
92
112
|
}
|
|
93
113
|
finally {
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js
CHANGED
|
@@ -2,7 +2,7 @@ import { isRecord } from "../../../parsing.js";
|
|
|
2
2
|
import { buildAgentBrowserResultCategoryDetails } from "../../../results/categories.js";
|
|
3
3
|
import { buildSnapshotPresentation } from "../../../results/snapshot.js";
|
|
4
4
|
import { extractRefSnapshotFromData } from "../../../session-page-state.js";
|
|
5
|
-
import { redactSensitiveText } from "../../../runtime.js";
|
|
5
|
+
import { redactSensitiveText, redactSensitiveValue } from "../../../runtime.js";
|
|
6
6
|
import { collectScrollPositionSnapshot } from "../diagnostics.js";
|
|
7
7
|
import { buildSessionDetailFields, runSessionCommandData } from "../session-state.js";
|
|
8
8
|
function parseSnapshotFilterRequest(commandTokens) {
|
|
@@ -237,7 +237,9 @@ export async function trySnapshotFilter(options) {
|
|
|
237
237
|
const diff = request.diff ? buildSnapshotDiff(options.previousRefSnapshot, fullSnapshot) : undefined;
|
|
238
238
|
if (fullSnapshot)
|
|
239
239
|
options.sessionPageState.applyRefSnapshot({ sessionName: options.sessionStateKey ?? options.sessionName, snapshot: fullSnapshot, update: options.sessionPageStateUpdate });
|
|
240
|
-
const presentation =
|
|
240
|
+
const presentation = options.modelVisible === false
|
|
241
|
+
? { content: [], data: redactSensitiveValue(filtered.data), summary: "Snapshot" }
|
|
242
|
+
: await buildSnapshotPresentation(filtered.data, options.persistentArtifactStore, options.artifactManifest);
|
|
241
243
|
const summary = request.role || request.search
|
|
242
244
|
? `Snapshot filter: ${filtered.matchedRefs}/${filtered.totalRefs} direct refs matched${request.role ? ` role=${request.role}` : ""}${request.search ? ` search ${JSON.stringify(request.search)}` : ""}; ${filtered.visibleLines} surrounding snapshot line${filtered.visibleLines === 1 ? "" : "s"} shown.`
|
|
243
245
|
: request.diff
|
|
@@ -36,8 +36,6 @@ export function normalizeRunInput(input) {
|
|
|
36
36
|
switch (input.kind) {
|
|
37
37
|
case "electron":
|
|
38
38
|
return { ...base, compiledElectron: input.compiledElectron, redactedCompiledElectron: input.redactedCompiledElectron };
|
|
39
|
-
case "job":
|
|
40
|
-
return { ...base, compiledJob: input.compiledJob, redactedCompiledJob: input.redactedCompiledJob };
|
|
41
39
|
case "networkSourceLookup":
|
|
42
40
|
return { ...base, compiledNetworkSourceLookup: input.compiledNetworkSourceLookup, redactedCompiledNetworkSourceLookup: input.redactedCompiledNetworkSourceLookup };
|
|
43
41
|
case "qa":
|
|
@@ -46,7 +44,6 @@ export function normalizeRunInput(input) {
|
|
|
46
44
|
return { ...base, compiledSemanticAction: input.compiledSemanticAction, redactedCompiledSemanticAction: input.redactedCompiledSemanticAction };
|
|
47
45
|
case "sourceLookup":
|
|
48
46
|
return { ...base, compiledSourceLookup: input.compiledSourceLookup, redactedCompiledSourceLookup: input.redactedCompiledSourceLookup };
|
|
49
|
-
case "script":
|
|
50
47
|
case "args":
|
|
51
48
|
return base;
|
|
52
49
|
}
|
|
@@ -937,6 +934,7 @@ export async function prepareBrowserRun(options) {
|
|
|
937
934
|
}
|
|
938
935
|
const persistentArtifactStore = getPersistentSessionArtifactStore(options.ctx);
|
|
939
936
|
const snapshotFilter = await trySnapshotFilter({
|
|
937
|
+
modelVisible: options.modelVisible,
|
|
940
938
|
artifactManifest: state.artifactManifest,
|
|
941
939
|
commandTokens,
|
|
942
940
|
compatibilityWorkaround,
|