pi-agent-browser-native 0.8.2 → 0.9.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 +35 -0
- package/README.md +24 -15
- package/dist/extensions/agent-browser/index.js +496 -343
- package/dist/extensions/agent-browser/lib/browser-journal.js +439 -0
- package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
- package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
- package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
- package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
- package/dist/extensions/agent-browser/lib/native-output.js +46 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +30 -16
- package/dist/extensions/agent-browser/lib/playbook.js +9 -2
- package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -26
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +17 -9
- package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
- package/dist/extensions/agent-browser/lib/temp.js +14 -6
- package/dist/extensions/agent-browser/lib/tool-surface.js +50 -33
- package/dist/extensions/agent-browser/lib/web-search.js +85 -72
- package/docs/ARCHITECTURE.md +25 -25
- package/docs/COMMAND_REFERENCE.md +13 -9
- package/docs/RELEASE.md +25 -19
- package/docs/REQUIREMENTS.md +6 -6
- package/docs/SESSION_STORAGE.md +60 -0
- package/docs/SUPPORT_MATRIX.md +16 -11
- package/docs/TOOL_CONTRACT.md +26 -19
- package/package.json +9 -5
- package/scripts/build.mjs +4 -3
- package/scripts/convert-browser-session.mjs +22 -0
- package/scripts/doctor.mjs +2 -2
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
1
2
|
import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
|
|
2
3
|
import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
|
|
4
|
+
import { getBrowserRecord, snapshotFromDefinition } from "./browser-transcript.js";
|
|
5
|
+
import { isCloseCommand, isOpenNavigationCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
|
|
6
6
|
import { isRecord } from "./parsing.js";
|
|
7
7
|
import { findReadConfirmation as findPendingReadConfirmation, parseReadConfirmation } from "./read-confirmation.js";
|
|
8
8
|
import { getEditableRefEvidence } from "./results/editable-ref-evidence.js";
|
|
@@ -148,43 +148,6 @@ export function deriveSessionTabTarget(options) {
|
|
|
148
148
|
return observedTarget ?? options.previousTarget;
|
|
149
149
|
return undefined;
|
|
150
150
|
}
|
|
151
|
-
function batchContainsOnlyReadOnlyDiagnosticTargets(data) {
|
|
152
|
-
if (!Array.isArray(data) || data.length === 0) {
|
|
153
|
-
return false;
|
|
154
|
-
}
|
|
155
|
-
return data.every((item) => {
|
|
156
|
-
if (!isRecord(item))
|
|
157
|
-
return false;
|
|
158
|
-
const [command, subcommand] = extractBatchResultCommand(item);
|
|
159
|
-
return isReadOnlyDiagnosticSessionTargetCommand(command, subcommand);
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
function getRestoredSessionTabTarget(details, command, subcommand) {
|
|
163
|
-
if (isReadOnlyDiagnosticSessionTargetCommand(command, subcommand)) {
|
|
164
|
-
return undefined;
|
|
165
|
-
}
|
|
166
|
-
const storedTarget = isRecord(details.sessionTabTarget)
|
|
167
|
-
? normalizeSessionTabTarget({
|
|
168
|
-
targetId: typeof details.sessionTabTarget.targetId === "string" ? details.sessionTabTarget.targetId : undefined,
|
|
169
|
-
title: typeof details.sessionTabTarget.title === "string" ? details.sessionTabTarget.title : undefined,
|
|
170
|
-
url: typeof details.sessionTabTarget.url === "string" ? details.sessionTabTarget.url : undefined,
|
|
171
|
-
})
|
|
172
|
-
: undefined;
|
|
173
|
-
if (command !== "batch") {
|
|
174
|
-
return storedTarget;
|
|
175
|
-
}
|
|
176
|
-
if (isRecord(details.compiledNetworkSourceLookup) || batchContainsOnlyReadOnlyDiagnosticTargets(details.data)) {
|
|
177
|
-
return undefined;
|
|
178
|
-
}
|
|
179
|
-
// The stored target includes post-batch live verification and native tab identity.
|
|
180
|
-
if (storedTarget || details.sessionTabTargetUnknown === true)
|
|
181
|
-
return storedTarget;
|
|
182
|
-
const batchTarget = extractSessionTabTargetFromBatchResults(details.data);
|
|
183
|
-
if (batchTarget) {
|
|
184
|
-
return batchTarget;
|
|
185
|
-
}
|
|
186
|
-
return storedTarget;
|
|
187
|
-
}
|
|
188
151
|
function extractRefSnapshotRefs(data) {
|
|
189
152
|
if (!isRecord(data) || !isRecord(data.refs))
|
|
190
153
|
return undefined;
|
|
@@ -278,62 +241,6 @@ export function extractLatestRefSnapshotStateFromBatchResults(data) {
|
|
|
278
241
|
}
|
|
279
242
|
return latestState;
|
|
280
243
|
}
|
|
281
|
-
function getRestoredRefSnapshotInvalidation(details, command) {
|
|
282
|
-
const invalidation = isRecord(details.refSnapshotInvalidation) ? details.refSnapshotInvalidation : undefined;
|
|
283
|
-
if (invalidation?.reason === "no-active-page")
|
|
284
|
-
return buildNoActivePageRefSnapshotInvalidation();
|
|
285
|
-
if (invalidation?.reason === "page-transition")
|
|
286
|
-
return buildPageTransitionRefSnapshotInvalidation(typeof invalidation.summary === "string" ? invalidation.summary : undefined);
|
|
287
|
-
const errorText = typeof details.error === "string"
|
|
288
|
-
? details.error
|
|
289
|
-
: typeof details.summary === "string"
|
|
290
|
-
? details.summary
|
|
291
|
-
: undefined;
|
|
292
|
-
return isNoActivePageSnapshotFailure(command, errorText) ? buildNoActivePageRefSnapshotInvalidation() : undefined;
|
|
293
|
-
}
|
|
294
|
-
function getRestoredRefSnapshot(details) {
|
|
295
|
-
const refSnapshot = isRecord(details.refSnapshot) ? details.refSnapshot : undefined;
|
|
296
|
-
if (!refSnapshot || !Array.isArray(refSnapshot.refIds))
|
|
297
|
-
return undefined;
|
|
298
|
-
const refIds = refSnapshot.refIds.filter((refId) => typeof refId === "string" && /^e\d+$/.test(refId));
|
|
299
|
-
const refRecord = isRecord(refSnapshot.refs) ? refSnapshot.refs : undefined;
|
|
300
|
-
const refEntries = refRecord
|
|
301
|
-
? Object.fromEntries(refIds.flatMap((refId) => {
|
|
302
|
-
const entry = refRecord[refId];
|
|
303
|
-
if (!isRecord(entry) || typeof entry.name !== "string" || typeof entry.role !== "string")
|
|
304
|
-
return [];
|
|
305
|
-
const isContentEditable = typeof entry.isContentEditable === "boolean" ? entry.isContentEditable : undefined;
|
|
306
|
-
const isEditable = typeof entry.isEditable === "boolean" ? entry.isEditable : undefined;
|
|
307
|
-
return [[refId, { ...(isContentEditable !== undefined ? { isContentEditable } : {}), ...(isEditable !== undefined ? { isEditable } : {}), name: entry.name, role: entry.role }]];
|
|
308
|
-
}))
|
|
309
|
-
: undefined;
|
|
310
|
-
return {
|
|
311
|
-
refIds,
|
|
312
|
-
...(refEntries && Object.keys(refEntries).length > 0 ? { refs: refEntries } : {}),
|
|
313
|
-
target: isRecord(refSnapshot.target)
|
|
314
|
-
? normalizeSessionTabTarget({
|
|
315
|
-
title: typeof refSnapshot.target.title === "string" ? refSnapshot.target.title : undefined,
|
|
316
|
-
url: typeof refSnapshot.target.url === "string" ? refSnapshot.target.url : undefined,
|
|
317
|
-
})
|
|
318
|
-
: undefined,
|
|
319
|
-
};
|
|
320
|
-
}
|
|
321
|
-
function getLatestTabTargetOrder(targets, unknownTargets) {
|
|
322
|
-
let latestOrder = 0;
|
|
323
|
-
for (const target of targets.values())
|
|
324
|
-
latestOrder = Math.max(latestOrder, target.order);
|
|
325
|
-
for (const order of unknownTargets.values())
|
|
326
|
-
latestOrder = Math.max(latestOrder, order);
|
|
327
|
-
return latestOrder;
|
|
328
|
-
}
|
|
329
|
-
function getLatestRefStateOrder(snapshots, invalidations) {
|
|
330
|
-
let latestOrder = 0;
|
|
331
|
-
for (const snapshot of snapshots.values())
|
|
332
|
-
latestOrder = Math.max(latestOrder, snapshot.order);
|
|
333
|
-
for (const invalidation of invalidations.values())
|
|
334
|
-
latestOrder = Math.max(latestOrder, invalidation.order);
|
|
335
|
-
return latestOrder;
|
|
336
|
-
}
|
|
337
244
|
function shouldApplyTabTargetUpdate(current, unknownOrder, updateOrder) {
|
|
338
245
|
return updateOrder >= Math.max(current?.order ?? 0, unknownOrder ?? 0);
|
|
339
246
|
}
|
|
@@ -342,7 +249,7 @@ function shouldApplyRefStateUpdate(options) {
|
|
|
342
249
|
return options.updateOrder >= currentOrder;
|
|
343
250
|
}
|
|
344
251
|
function stripRefSnapshotOrder(snapshot) {
|
|
345
|
-
return snapshot ? { refIds: snapshot.refIds, ...(snapshot.refs ? { refs: snapshot.refs } : {}), target: snapshot.target } : undefined;
|
|
252
|
+
return snapshot ? { ...(snapshot.snapshotId ? { snapshotId: snapshot.snapshotId } : {}), ...(snapshot.generation ? { generation: snapshot.generation } : {}), refIds: snapshot.refIds, ...(snapshot.refs ? { refs: snapshot.refs } : {}), target: snapshot.target } : undefined;
|
|
346
253
|
}
|
|
347
254
|
function stripRefSnapshotInvalidationOrder(invalidation) {
|
|
348
255
|
return invalidation ? { reason: invalidation.reason, summary: invalidation.summary } : undefined;
|
|
@@ -358,78 +265,96 @@ export class SessionPageState {
|
|
|
358
265
|
tabTargetUnknownOrders = new Map();
|
|
359
266
|
tabTargets = new Map();
|
|
360
267
|
updateOrder = 0;
|
|
268
|
+
nativeGenerations = new Map();
|
|
269
|
+
pending = new Map();
|
|
361
270
|
static fromBranch(branch) {
|
|
362
271
|
const state = new SessionPageState();
|
|
363
|
-
let restoredOrder = 0;
|
|
364
272
|
for (const entry of branch) {
|
|
365
|
-
const
|
|
366
|
-
if (
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
273
|
+
const record = getBrowserRecord(entry);
|
|
274
|
+
if (record)
|
|
275
|
+
state.applyBrowserRecord(record);
|
|
276
|
+
}
|
|
277
|
+
for (const key of state.tabTargets.keys())
|
|
278
|
+
state.tabPinningReasons.set(key, "restore");
|
|
279
|
+
return state;
|
|
280
|
+
}
|
|
281
|
+
/** A command operates on its admitted state while the committed view is unavailable. */
|
|
282
|
+
fork() {
|
|
283
|
+
const state = new SessionPageState();
|
|
284
|
+
state.readConfirmations = new Map(this.readConfirmations);
|
|
285
|
+
state.refSnapshotInvalidations = new Map(this.refSnapshotInvalidations);
|
|
286
|
+
state.refSnapshots = new Map(this.refSnapshots);
|
|
287
|
+
state.tabPinningReasons = new Map(this.tabPinningReasons);
|
|
288
|
+
state.tabTargetUnknownOrders = new Map(this.tabTargetUnknownOrders);
|
|
289
|
+
state.tabTargets = new Map([...this.tabTargets].map(([key, value]) => [key, { ...value }]));
|
|
290
|
+
state.pending = new Map(this.pending);
|
|
291
|
+
state.nativeGenerations = new Map(this.nativeGenerations);
|
|
292
|
+
state.updateOrder = this.updateOrder;
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
views() {
|
|
296
|
+
return new Map([...new Set([...this.tabTargets.keys(), ...this.tabTargetUnknownOrders.keys(), ...this.refSnapshots.keys(), ...this.refSnapshotInvalidations.keys()])].map(key => [key, this.get(key)]));
|
|
297
|
+
}
|
|
298
|
+
/** The same reducer commits live observations and replays selected journal envelopes. */
|
|
299
|
+
applyBrowserRecord({ event, snapshot }) {
|
|
300
|
+
const update = this.beginUpdate();
|
|
301
|
+
const confirmation = parseReadConfirmation(event.state.readConfirmation);
|
|
302
|
+
for (const page of event.pages ?? []) {
|
|
303
|
+
const key = page.key;
|
|
304
|
+
if (event.phase === "begin") {
|
|
305
|
+
this.pending.set(key, { operationId: event.operationId, snapshot: this.refSnapshots.get(key) ?? this.pending.get(key)?.snapshot });
|
|
306
|
+
this.markTabTargetUnknown({ sessionName: key, update });
|
|
307
|
+
this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("This browser operation has no persisted finish. Inspect the current URL and take a fresh snapshot before using refs; changes may already have happened.") });
|
|
370
308
|
continue;
|
|
371
|
-
const sessionName = typeof details.sessionName === "string" ? details.sessionName : undefined;
|
|
372
|
-
const namespace = typeof details.namespace === "string" ? details.namespace : undefined;
|
|
373
|
-
const sessionKey = getSessionPageStateKey(sessionName, namespace);
|
|
374
|
-
const args = Array.isArray(details.args) && details.args.every((arg) => typeof arg === "string") ? details.args : [];
|
|
375
|
-
const commandTokens = extractUpstreamCommandTokens(args);
|
|
376
|
-
const command = typeof details.command === "string" ? details.command : commandTokens[0];
|
|
377
|
-
const subcommand = typeof details.subcommand === "string" ? details.subcommand : commandTokens[1];
|
|
378
|
-
const batchCloseLifecycle = getSuccessfulBatchCloseLifecycle(details.batchSteps);
|
|
379
|
-
const closeAllApplied = details.closeAllApplied === true
|
|
380
|
-
|| (message.isError !== true && isCloseAllCommand(commandTokens))
|
|
381
|
-
|| batchHasSuccessfulCloseAll(details.batchSteps);
|
|
382
|
-
const lifecycleReset = (isCloseCommand(command) && message.isError !== true) || batchCloseLifecycle !== undefined;
|
|
383
|
-
if (closeAllApplied) {
|
|
384
|
-
restoredOrder += 1;
|
|
385
|
-
state.clearNamespace(namespace);
|
|
386
309
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
state.clearSession(sessionKey);
|
|
390
|
-
}
|
|
391
|
-
const readConfirmation = parseReadConfirmation(details.readConfirmation);
|
|
392
|
-
if (readConfirmation)
|
|
393
|
-
state.applyReadConfirmation(readConfirmation, ++restoredOrder);
|
|
394
|
-
if (!sessionKey || ((closeAllApplied || lifecycleReset) && (isCloseCommand(command) || batchCloseLifecycle?.endsClosed === true)))
|
|
310
|
+
if (page.clear) {
|
|
311
|
+
this.clearSession(key);
|
|
395
312
|
continue;
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
399
|
-
const refSnapshotInvalidation = getRestoredRefSnapshotInvalidation(details, command);
|
|
400
|
-
const refSnapshot = refSnapshotInvalidation ? undefined : getRestoredRefSnapshot(details);
|
|
401
|
-
if (!tabTarget && !tabTargetUnknown && !refSnapshotInvalidation && !refSnapshot && reopenPending === undefined)
|
|
313
|
+
}
|
|
314
|
+
const pending = this.pending.get(key);
|
|
315
|
+
if (event.phase === "finish" && pending && pending.operationId !== event.operationId)
|
|
402
316
|
continue;
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
|
|
408
|
-
else
|
|
409
|
-
state.refSnapshotInvalidations.delete(sessionKey);
|
|
410
|
-
state.tabTargets.delete(sessionKey);
|
|
411
|
-
state.tabTargetUnknownOrders.set(sessionKey, restoredOrder);
|
|
317
|
+
if (page.unknown || page.refs.kind === "unknown") {
|
|
318
|
+
this.markTabTargetUnknown({ sessionName: key, update });
|
|
319
|
+
this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: page.refs.kind === "unknown" && page.refs.invalidation
|
|
320
|
+
? page.refs.invalidation : buildPageTransitionRefSnapshotInvalidation("The browser target or operation outcome is unknown. Verify the current URL and take a fresh snapshot before using refs.") });
|
|
412
321
|
continue;
|
|
413
322
|
}
|
|
414
|
-
if (
|
|
415
|
-
|
|
416
|
-
|
|
323
|
+
if (page.target)
|
|
324
|
+
this.applyTabTarget({ sessionName: key, target: page.target, update });
|
|
325
|
+
else {
|
|
326
|
+
this.tabTargets.delete(key);
|
|
327
|
+
this.tabTargetUnknownOrders.delete(key);
|
|
328
|
+
}
|
|
329
|
+
if (page.reopenPending !== undefined)
|
|
330
|
+
this.setTabReopenPending({ pending: page.reopenPending, sessionName: key, update });
|
|
331
|
+
if (page.pinningReason)
|
|
332
|
+
this.markPinning(key, page.pinningReason);
|
|
333
|
+
else
|
|
334
|
+
this.tabPinningReasons.delete(key);
|
|
335
|
+
if (page.refs.kind === "replace") {
|
|
336
|
+
const definition = snapshot?.id === page.refs.snapshotId && snapshot.refs ? snapshotFromDefinition(snapshot)
|
|
337
|
+
: { snapshotId: page.refs.snapshotId, refIds: [] };
|
|
338
|
+
this.applyRefSnapshot({ sessionName: key, snapshot: definition, fallbackTarget: page.target, update });
|
|
417
339
|
}
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
340
|
+
else if (page.refs.kind === "reuse") {
|
|
341
|
+
const candidate = pending?.snapshot ?? this.refSnapshots.get(key);
|
|
342
|
+
if (candidate?.snapshotId === page.refs.snapshotId)
|
|
343
|
+
this.applyRefSnapshot({ sessionName: key, snapshot: candidate, fallbackTarget: page.target, update });
|
|
344
|
+
else
|
|
345
|
+
this.applyRefSnapshotInvalidation({ sessionName: key, update, invalidation: buildPageTransitionRefSnapshotInvalidation("The ancestral snapshot definition is unavailable. Take a new complete snapshot before using refs.") });
|
|
424
346
|
}
|
|
425
|
-
else
|
|
426
|
-
|
|
427
|
-
|
|
347
|
+
else {
|
|
348
|
+
this.refSnapshots.delete(key);
|
|
349
|
+
if (page.refs.invalidation)
|
|
350
|
+
this.applyRefSnapshotInvalidation({ sessionName: key, invalidation: page.refs.invalidation, update });
|
|
351
|
+
else
|
|
352
|
+
this.refSnapshotInvalidations.delete(key);
|
|
428
353
|
}
|
|
354
|
+
this.pending.delete(key);
|
|
429
355
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
return state;
|
|
356
|
+
if (confirmation)
|
|
357
|
+
this.applyReadConfirmation(confirmation, update);
|
|
433
358
|
}
|
|
434
359
|
beginUpdate() {
|
|
435
360
|
this.updateOrder += 1;
|
|
@@ -437,6 +362,8 @@ export class SessionPageState {
|
|
|
437
362
|
}
|
|
438
363
|
reset() {
|
|
439
364
|
this.readConfirmations.clear();
|
|
365
|
+
this.pending.clear();
|
|
366
|
+
this.nativeGenerations.clear();
|
|
440
367
|
this.refSnapshotInvalidations = new Map();
|
|
441
368
|
this.refSnapshots = new Map();
|
|
442
369
|
this.tabPinningReasons = new Map();
|
|
@@ -490,11 +417,20 @@ export class SessionPageState {
|
|
|
490
417
|
})) {
|
|
491
418
|
return { ...this.get(options.sessionName), applied: false, stale: true };
|
|
492
419
|
}
|
|
493
|
-
const snapshot = { ...options.snapshot, target: options.snapshot.target ?? options.fallbackTarget };
|
|
420
|
+
const snapshot = { ...options.snapshot, generation: options.snapshot.generation ?? this.nativeGenerations.get(options.sessionName), snapshotId: options.snapshot.snapshotId ?? randomUUID(), target: options.snapshot.target ?? options.fallbackTarget };
|
|
494
421
|
this.refSnapshotInvalidations.delete(options.sessionName);
|
|
495
422
|
this.refSnapshots.set(options.sessionName, { ...snapshot, order: options.update });
|
|
496
423
|
return { ...this.get(options.sessionName), applied: true };
|
|
497
424
|
}
|
|
425
|
+
bindSnapshotGeneration(sessionName, generation) {
|
|
426
|
+
if (generation)
|
|
427
|
+
this.nativeGenerations.set(sessionName, generation);
|
|
428
|
+
else
|
|
429
|
+
this.nativeGenerations.delete(sessionName);
|
|
430
|
+
const snapshot = this.refSnapshots.get(sessionName);
|
|
431
|
+
if (snapshot)
|
|
432
|
+
this.refSnapshots.set(sessionName, { ...snapshot, generation });
|
|
433
|
+
}
|
|
498
434
|
applyRefSnapshotInvalidation(options) {
|
|
499
435
|
if (!shouldApplyRefStateUpdate({
|
|
500
436
|
currentInvalidation: this.refSnapshotInvalidations.get(options.sessionName),
|
|
@@ -519,6 +455,8 @@ export class SessionPageState {
|
|
|
519
455
|
return { ...this.get(options.sessionName), applied: true };
|
|
520
456
|
}
|
|
521
457
|
clearSession(sessionName) {
|
|
458
|
+
this.pending.delete(sessionName);
|
|
459
|
+
this.nativeGenerations.delete(sessionName);
|
|
522
460
|
this.readConfirmations.delete(sessionName);
|
|
523
461
|
this.refSnapshotInvalidations.delete(sessionName);
|
|
524
462
|
this.refSnapshots.delete(sessionName);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
1
|
+
import { randomBytes, randomInt } from "node:crypto";
|
|
2
2
|
import { existsSync, readdirSync, rmSync } from "node:fs";
|
|
3
3
|
import { chmod, mkdir, mkdtemp, open, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
@@ -10,6 +10,8 @@ const TEMP_ROOT_MARKER_FILE_NAME = ".pi-agent-browser-owner.json";
|
|
|
10
10
|
const TEMP_ROOT_MARKER_KIND = "pi-agent-browser-temp-root";
|
|
11
11
|
const TEMP_ROOT_MARKER_VERSION = 2;
|
|
12
12
|
const STALE_TEMP_ROOT_MAX_AGE_MS = 24 * 60 * 60 * 1_000;
|
|
13
|
+
const STALE_TEMP_ROOT_BATCH_SIZE = 8;
|
|
14
|
+
let lastTempGcName;
|
|
13
15
|
const TEMP_ROOT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_TEMP_ROOT_MAX_BYTES";
|
|
14
16
|
const DEFAULT_TEMP_ROOT_MAX_BYTES = 32 * 1_024 * 1_024;
|
|
15
17
|
const SESSION_ARTIFACT_MAX_BYTES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MAX_BYTES";
|
|
@@ -237,10 +239,17 @@ async function pruneStaleTempRoots(currentTempRoot) {
|
|
|
237
239
|
const entries = await readdir(tmpdir(), { withFileTypes: true }).catch(() => []);
|
|
238
240
|
const cutoffTime = Date.now() - STALE_TEMP_ROOT_MAX_AGE_MS;
|
|
239
241
|
const currentUid = getCurrentProcessUid();
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
.map(
|
|
243
|
-
|
|
242
|
+
// ponytail: list/sort all names; stream directories if enumeration becomes the bottleneck.
|
|
243
|
+
const candidates = entries.filter((entry) => entry.isDirectory() && entry.name.startsWith(TEMP_ROOT_PREFIX))
|
|
244
|
+
.map((entry) => entry.name).sort();
|
|
245
|
+
if (candidates.length === 0)
|
|
246
|
+
return;
|
|
247
|
+
const start = lastTempGcName === undefined ? randomInt(candidates.length)
|
|
248
|
+
: Math.max(0, candidates.findIndex((name) => name > lastTempGcName));
|
|
249
|
+
const batch = Array.from({ length: Math.min(STALE_TEMP_ROOT_BATCH_SIZE, candidates.length) }, (_, offset) => candidates[(start + offset) % candidates.length]);
|
|
250
|
+
lastTempGcName = batch.at(-1);
|
|
251
|
+
await Promise.all(batch.map(async (name) => {
|
|
252
|
+
const path = join(tmpdir(), name);
|
|
244
253
|
if (path === currentTempRoot)
|
|
245
254
|
return;
|
|
246
255
|
const ownershipMarker = await readTempRootOwnershipMarker(path);
|
|
@@ -400,7 +409,6 @@ async function prunePersistentSessionArtifactsToBudget(sessionArtifactDir, addit
|
|
|
400
409
|
async function getSessionTempRoot() {
|
|
401
410
|
if (!sessionTempRootPromise) {
|
|
402
411
|
sessionTempRootPromise = (async () => {
|
|
403
|
-
await pruneStaleTempRoots(undefined);
|
|
404
412
|
const tempRoot = await mkdtemp(join(tmpdir(), TEMP_ROOT_PREFIX));
|
|
405
413
|
await chmod(tempRoot, 0o700).catch(() => undefined);
|
|
406
414
|
await writeSecureTempRootOwnershipMarker(tempRoot);
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { formatAgentBrowserRenderCall } from "./pi-tool-rendering.js";
|
|
3
|
+
import { JsonSchema } from "./json-schema.js";
|
|
4
|
+
import { AGENT_BROWSER_NAMESPACE, AGENT_BROWSER_OUTPUT_SCHEMA, finalizeAgentBrowserNativeResult } from "./native-output.js";
|
|
3
5
|
import { AGENT_BROWSER_PARAMS, AGENT_BROWSER_CODE_PARAMS, AGENT_BROWSER_ACTION_PARAMS, AGENT_BROWSER_QA_PARAMS, AGENT_BROWSER_ELECTRON_PARAMS, AGENT_BROWSER_SOURCE_PARAMS, AGENT_BROWSER_NETWORK_SOURCE_PARAMS, AGENT_BROWSER_TOOLS_PARAMS, } from "./input-modes/params.js";
|
|
4
|
-
import { ADVANCED_TOOL_PROMPT_GUIDELINES, buildToolPromptGuidelines } from "./playbook.js";
|
|
5
6
|
export const AGENT_BROWSER_TOOL_INVENTORY = {
|
|
6
7
|
action: { name: "agent_browser_action", description: "Click, check, fill, or select a unique accessible target using stable locators or current refs." },
|
|
7
8
|
qa: { name: "agent_browser_qa", description: "Check a URL or attached page with visible assertions and a diagnostic pass/fail verdict." },
|
|
@@ -9,37 +10,35 @@ export const AGENT_BROWSER_TOOL_INVENTORY = {
|
|
|
9
10
|
source: { name: "agent_browser_source", description: "Find experimental DOM/React/local-source candidates with a bounded workspace scan." },
|
|
10
11
|
network: { name: "agent_browser_network_source", description: "Correlate failed requests with initiator and local-source candidates." },
|
|
11
12
|
};
|
|
12
|
-
export const
|
|
13
|
+
export const AGENT_BROWSER_INSTRUCTION_GROUP = {
|
|
13
14
|
name: "browser",
|
|
14
15
|
description: "Search the web, browse and interact with pages, verify browser UI, and inspect Electron apps.",
|
|
15
|
-
sections: ["agent_browser"],
|
|
16
16
|
};
|
|
17
|
-
const entryDiscovery = { discovery: { group: AGENT_BROWSER_DISCOVERY_GROUP, role: "entry" } };
|
|
18
|
-
const advancedDiscovery = { discovery: { group: AGENT_BROWSER_DISCOVERY_GROUP, role: "advanced" } };
|
|
19
17
|
const advancedNames = new Set(Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name));
|
|
20
18
|
/** Register once; advanced calls adapt only their input shape and reuse the ordinary executor. */
|
|
21
19
|
export function registerAgentBrowserToolSurface(pi, options) {
|
|
20
|
+
const execute = async (id, params, signal, onUpdate, ctx) => finalizeAgentBrowserNativeResult(await options.execute(id, params, signal, onUpdate, ctx), params);
|
|
21
|
+
const nativeOutput = { namespace: AGENT_BROWSER_NAMESPACE, outputSchema: AGENT_BROWSER_OUTPUT_SCHEMA };
|
|
22
22
|
pi.registerTool({
|
|
23
|
-
...entryDiscovery,
|
|
24
23
|
name: "agent_browser",
|
|
24
|
+
...nativeOutput,
|
|
25
25
|
label: "Agent Browser",
|
|
26
26
|
description: "Browse and interact through native agent-browser commands. One command in args; fixed sequences use batch --bail and JSON-array stdin. Use agent_browser_code for loops/branches and agent_browser_tools for advanced capabilities.",
|
|
27
27
|
promptSnippet: "Browse pages, inspect current refs, interact, and run native command batches.",
|
|
28
|
-
promptGuidelines: options.promptGuidelines ?? buildToolPromptGuidelines({ includeWebSearch: false }),
|
|
29
28
|
parameters: AGENT_BROWSER_PARAMS,
|
|
30
29
|
renderCall: options.renderCall,
|
|
31
30
|
renderResult: options.renderResult,
|
|
32
|
-
execute
|
|
31
|
+
execute,
|
|
33
32
|
executionMode: options.executionMode,
|
|
34
33
|
});
|
|
35
34
|
pi.registerTool({
|
|
36
|
-
...entryDiscovery,
|
|
37
35
|
name: "agent_browser_code",
|
|
36
|
+
...nativeOutput,
|
|
38
37
|
label: "Agent Browser Code",
|
|
39
38
|
description: "Run fresh JavaScript against a persistent browser. await browser({args,stdin?,timeoutMs?}) returns success/data/error/nextActions and imageObservations; emit(selected JSON) and emitImage(image handle) explicitly choose output. No host APIs or imports. Use native batch for fixed sequences.",
|
|
40
39
|
promptSnippet: "Branch, loop, and aggregate browser observations with explicit JSON/image output.",
|
|
41
40
|
parameters: AGENT_BROWSER_CODE_PARAMS,
|
|
42
|
-
execute: options.executeCode,
|
|
41
|
+
execute: async (id, params, signal, onUpdate, ctx) => finalizeAgentBrowserNativeResult(await options.executeCode(id, params, signal, onUpdate, ctx), params),
|
|
43
42
|
executionMode: options.executionMode,
|
|
44
43
|
renderCall(args, theme, context) {
|
|
45
44
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
@@ -49,68 +48,75 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
49
48
|
renderResult: options.renderResult,
|
|
50
49
|
});
|
|
51
50
|
pi.registerTool({
|
|
52
|
-
...advancedDiscovery,
|
|
53
51
|
...AGENT_BROWSER_TOOL_INVENTORY.action,
|
|
52
|
+
...nativeOutput,
|
|
53
|
+
defaultActive: false,
|
|
54
54
|
label: "Browser Action",
|
|
55
|
-
promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.action],
|
|
56
55
|
parameters: AGENT_BROWSER_ACTION_PARAMS,
|
|
57
56
|
renderResult: options.renderResult,
|
|
58
57
|
executionMode: options.executionMode,
|
|
59
58
|
execute(id, { outputPath, timeoutMs, ...semanticAction }, signal, onUpdate, ctx) {
|
|
60
|
-
return
|
|
59
|
+
return execute(id, { semanticAction, outputPath, timeoutMs }, signal, onUpdate, ctx);
|
|
61
60
|
},
|
|
62
61
|
});
|
|
63
62
|
pi.registerTool({
|
|
64
|
-
...advancedDiscovery,
|
|
65
63
|
...AGENT_BROWSER_TOOL_INVENTORY.qa,
|
|
64
|
+
...nativeOutput,
|
|
65
|
+
defaultActive: false,
|
|
66
66
|
label: "Browser QA",
|
|
67
|
-
promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.qa],
|
|
68
67
|
parameters: AGENT_BROWSER_QA_PARAMS,
|
|
69
68
|
renderResult: options.renderResult,
|
|
70
69
|
executionMode: options.executionMode,
|
|
71
70
|
execute(id, { outputPath, timeoutMs, sessionMode, ...qa }, signal, onUpdate, ctx) {
|
|
72
|
-
return
|
|
71
|
+
return execute(id, { qa, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
|
|
73
72
|
},
|
|
74
73
|
});
|
|
75
74
|
pi.registerTool({
|
|
76
|
-
...advancedDiscovery,
|
|
77
75
|
...AGENT_BROWSER_TOOL_INVENTORY.electron,
|
|
76
|
+
...nativeOutput,
|
|
77
|
+
defaultActive: false,
|
|
78
78
|
label: "Browser Electron",
|
|
79
|
-
promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.electron],
|
|
80
79
|
parameters: AGENT_BROWSER_ELECTRON_PARAMS,
|
|
81
80
|
renderResult: options.renderResult,
|
|
82
81
|
executionMode: options.executionMode,
|
|
83
82
|
execute(id, { outputPath, ...electron }, signal, onUpdate, ctx) {
|
|
84
|
-
return
|
|
83
|
+
return execute(id, { electron, outputPath }, signal, onUpdate, ctx);
|
|
85
84
|
},
|
|
86
85
|
});
|
|
87
86
|
pi.registerTool({
|
|
88
|
-
...advancedDiscovery,
|
|
89
87
|
...AGENT_BROWSER_TOOL_INVENTORY.source,
|
|
88
|
+
...nativeOutput,
|
|
89
|
+
defaultActive: false,
|
|
90
90
|
label: "Browser Source",
|
|
91
|
-
promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.source],
|
|
92
91
|
parameters: AGENT_BROWSER_SOURCE_PARAMS,
|
|
93
92
|
renderResult: options.renderResult,
|
|
94
93
|
executionMode: options.executionMode,
|
|
95
94
|
execute(id, { outputPath, timeoutMs, sessionMode, ...sourceLookup }, signal, onUpdate, ctx) {
|
|
96
|
-
return
|
|
95
|
+
return execute(id, { sourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
|
|
97
96
|
},
|
|
98
97
|
});
|
|
99
98
|
pi.registerTool({
|
|
100
|
-
...advancedDiscovery,
|
|
101
99
|
...AGENT_BROWSER_TOOL_INVENTORY.network,
|
|
100
|
+
...nativeOutput,
|
|
101
|
+
defaultActive: false,
|
|
102
102
|
label: "Browser Network Source",
|
|
103
|
-
promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.network],
|
|
104
103
|
parameters: AGENT_BROWSER_NETWORK_SOURCE_PARAMS,
|
|
105
104
|
renderResult: options.renderResult,
|
|
106
105
|
executionMode: options.executionMode,
|
|
107
106
|
execute(id, { outputPath, timeoutMs, sessionMode, ...networkSourceLookup }, signal, onUpdate, ctx) {
|
|
108
|
-
return
|
|
107
|
+
return execute(id, { networkSourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
|
|
109
108
|
},
|
|
110
109
|
});
|
|
111
110
|
pi.registerTool({
|
|
112
|
-
...entryDiscovery,
|
|
113
111
|
name: "agent_browser_tools",
|
|
112
|
+
namespace: AGENT_BROWSER_NAMESPACE,
|
|
113
|
+
outputSchema: JsonSchema.Object({
|
|
114
|
+
inventory: JsonSchema.Array(JsonSchema.Object({
|
|
115
|
+
key: JsonSchema.String(), name: JsonSchema.String(), description: JsonSchema.String(),
|
|
116
|
+
available: JsonSchema.Boolean(), active: JsonSchema.Boolean(),
|
|
117
|
+
})),
|
|
118
|
+
added: JsonSchema.Array(JsonSchema.String()),
|
|
119
|
+
}),
|
|
114
120
|
label: "Browser Tools",
|
|
115
121
|
description: "List or enable advanced browser tools: action → agent_browser_action; qa → agent_browser_qa; electron → agent_browser_electron; source → agent_browser_source; network → agent_browser_network_source. Omit enable for inventory. Activation only adds tools; it preserves other active tools.",
|
|
116
122
|
promptSnippet: "Discover and enable specialized browser actions, QA, Electron, and source tools.",
|
|
@@ -129,11 +135,13 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
129
135
|
return {
|
|
130
136
|
content: [{ type: "text", text: inventory.map((tool) => `${tool.key}: ${tool.name} (${!tool.available ? "unavailable in this Pi tool selection" : tool.active ? "active" : "inactive"}) — ${tool.description}`).join("\n") }],
|
|
131
137
|
details: { inventory, added: [...new Set(added)] },
|
|
138
|
+
structuredContent: { inventory, added: [...new Set(added)] },
|
|
132
139
|
};
|
|
133
140
|
},
|
|
134
141
|
});
|
|
135
142
|
pi.on("session_start", async (_event, ctx) => {
|
|
136
|
-
//
|
|
143
|
+
// ponytail: official 1.0 SDK supplies initialActiveToolNames on resume, bypassing
|
|
144
|
+
// native transcript restoration. Remove this additive fallback once the supported SDK restores it.
|
|
137
145
|
const argv = process.argv.slice(2);
|
|
138
146
|
const delimiter = argv.indexOf("--");
|
|
139
147
|
if ((delimiter < 0 ? argv : argv.slice(0, delimiter)).some((arg) => arg === "--tools" || arg === "-t"))
|
|
@@ -142,11 +150,20 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
142
150
|
const available = new Set(pi.getAllTools().map(({ name }) => name));
|
|
143
151
|
if (!["agent_browser", "agent_browser_code", "agent_browser_tools", ...advancedNames].every(name => available.has(name)))
|
|
144
152
|
return;
|
|
145
|
-
//
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
// Native dynamic imports bypass Pi's host-package aliases in consumer installs.
|
|
154
|
+
// Only tool names are needed; replay native deltas without requiring the optional pi-ai peer.
|
|
155
|
+
const restored = new Set();
|
|
156
|
+
for (const message of ctx.sessionManager.buildSessionProjection().messages) {
|
|
157
|
+
if (message.role !== "system")
|
|
158
|
+
continue;
|
|
159
|
+
for (const removed of message.toolsRemoved ?? [])
|
|
160
|
+
restored.delete(removed.name);
|
|
161
|
+
for (const added of message.toolsAdded ?? [])
|
|
162
|
+
restored.add(added.name);
|
|
163
|
+
}
|
|
164
|
+
const active = new Set(pi.getActiveTools());
|
|
165
|
+
const added = [...restored].filter((name) => advancedNames.has(name) && available.has(name) && !active.has(name));
|
|
166
|
+
if (added.length > 0)
|
|
167
|
+
pi.setActiveTools([...active, ...added]);
|
|
151
168
|
});
|
|
152
169
|
}
|