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.
Files changed (37) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +24 -15
  3. package/dist/extensions/agent-browser/index.js +496 -343
  4. package/dist/extensions/agent-browser/lib/browser-journal.js +439 -0
  5. package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
  7. package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
  8. package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
  10. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
  11. package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
  12. package/dist/extensions/agent-browser/lib/native-output.js +46 -0
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
  16. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
  17. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
  18. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +30 -16
  19. package/dist/extensions/agent-browser/lib/playbook.js +9 -2
  20. package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -26
  21. package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
  22. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +17 -9
  23. package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
  24. package/dist/extensions/agent-browser/lib/temp.js +14 -6
  25. package/dist/extensions/agent-browser/lib/tool-surface.js +50 -33
  26. package/dist/extensions/agent-browser/lib/web-search.js +85 -72
  27. package/docs/ARCHITECTURE.md +25 -25
  28. package/docs/COMMAND_REFERENCE.md +13 -9
  29. package/docs/RELEASE.md +25 -19
  30. package/docs/REQUIREMENTS.md +6 -6
  31. package/docs/SESSION_STORAGE.md +60 -0
  32. package/docs/SUPPORT_MATRIX.md +16 -11
  33. package/docs/TOOL_CONTRACT.md +26 -19
  34. package/package.json +9 -5
  35. package/scripts/build.mjs +4 -3
  36. package/scripts/convert-browser-session.mjs +22 -0
  37. 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 { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
- import { getBrowserResultMessage } from "./browser-transcript.js";
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 message = getBrowserResultMessage(entry);
366
- if (!message)
367
- continue;
368
- const details = isRecord(message.details) ? message.details : undefined;
369
- if (!details)
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
- else if (sessionKey && lifecycleReset) {
388
- restoredOrder += 1;
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
- const tabTarget = getRestoredSessionTabTarget(details, command, subcommand);
397
- const tabTargetUnknown = details.sessionTabTargetUnknown === true;
398
- const reopenPending = typeof details.sessionTabReopenPending === "boolean" ? details.sessionTabReopenPending : undefined;
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
- restoredOrder += 1;
404
- if (tabTargetUnknown) {
405
- state.refSnapshots.delete(sessionKey);
406
- if (refSnapshotInvalidation)
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 (tabTarget) {
415
- state.tabTargetUnknownOrders.delete(sessionKey);
416
- state.tabTargets.set(sessionKey, { order: restoredOrder, reopenPending: state.tabTargets.get(sessionKey)?.reopenPending, target: tabTarget });
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
- const currentTarget = state.tabTargets.get(sessionKey);
419
- if (currentTarget && reopenPending !== undefined)
420
- currentTarget.reopenPending = reopenPending;
421
- if (refSnapshotInvalidation) {
422
- state.refSnapshots.delete(sessionKey);
423
- state.refSnapshotInvalidations.set(sessionKey, { ...refSnapshotInvalidation, order: restoredOrder });
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 if (refSnapshot) {
426
- state.refSnapshotInvalidations.delete(sessionKey);
427
- state.refSnapshots.set(sessionKey, { ...refSnapshot, order: restoredOrder });
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
- state.updateOrder = Math.max(restoredOrder, getLatestTabTargetOrder(state.tabTargets, state.tabTargetUnknownOrders), getLatestRefStateOrder(state.refSnapshots, state.refSnapshotInvalidations));
431
- state.tabPinningReasons = new Map([...state.tabTargets.keys()].map((sessionName) => [sessionName, "restore"]));
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
- await Promise.all(entries
241
- .filter((entry) => entry.isDirectory() && entry.name.startsWith(TEMP_ROOT_PREFIX))
242
- .map(async (entry) => {
243
- const path = join(tmpdir(), entry.name);
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 AGENT_BROWSER_DISCOVERY_GROUP = {
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: options.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 options.execute(id, { semanticAction, outputPath, timeoutMs }, signal, onUpdate, ctx);
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 options.execute(id, { qa, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
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 options.execute(id, { electron, outputPath }, signal, onUpdate, ctx);
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 options.execute(id, { sourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
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 options.execute(id, { networkSourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
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
- // Pi owns selected tools and their durable system-message history; keep no parallel registry.
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
- // Keep Pi's replay helper behind the awaited restoration boundary, not factory registration.
146
- const { getCurrentSystemMessage } = await import("@earendil-works/pi-ai");
147
- const current = getCurrentSystemMessage(ctx.sessionManager.buildSessionProjection().messages);
148
- const restored = new Set(current?.toolsAdded?.map(({ name }) => name));
149
- const active = pi.getActiveTools().filter((name) => !advancedNames.has(name) || restored.has(name));
150
- pi.setActiveTools([...new Set([...active, ...[...restored].filter((name) => advancedNames.has(name) && available.has(name))])]);
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
  }