pi-agent-browser-native 0.6.17 → 0.7.1

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 (49) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +55 -167
  3. package/dist/extensions/agent-browser/index.js +312 -223
  4. package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
  5. package/dist/extensions/agent-browser/lib/execution-cwd.js +4 -2
  6. package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
  7. package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
  8. package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
  9. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
  10. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +13 -1
  11. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +35 -20
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
  18. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
  19. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
  20. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
  21. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
  22. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
  23. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
  24. package/dist/extensions/agent-browser/lib/playbook.js +46 -25
  25. package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
  26. package/dist/extensions/agent-browser/lib/process.js +16 -13
  27. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
  28. package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
  29. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
  30. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
  31. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
  32. package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
  33. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
  34. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
  35. package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
  36. package/dist/extensions/agent-browser/lib/runtime.js +18 -6
  37. package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
  38. package/dist/extensions/agent-browser/lib/tool-surface.js +138 -0
  39. package/dist/extensions/agent-browser/script-worker.js +4 -1
  40. package/docs/ARCHITECTURE.md +46 -62
  41. package/docs/COMMAND_REFERENCE.md +77 -136
  42. package/docs/ELECTRON.md +51 -49
  43. package/docs/RELEASE.md +35 -21
  44. package/docs/REQUIREMENTS.md +18 -14
  45. package/docs/SUPPORT_MATRIX.md +33 -15
  46. package/docs/TOOL_CONTRACT.md +216 -405
  47. package/docs/platform-smoke.md +2 -2
  48. package/package.json +1 -1
  49. package/scripts/doctor.mjs +2 -2
@@ -2,7 +2,7 @@ import { writePersistentSessionArtifactFile, writeSecureTempFile, } from "../../
2
2
  import { buildEvictedSessionArtifactEntries } from "../artifact-manifest.js";
3
3
  import { countLines, truncateText } from "../text.js";
4
4
  import { applyArtifactManifest } from "./artifacts.js";
5
- import { getPresentationText } from "./content.js";
5
+ import { getPresentationText, projectAgentBrowserObservation } from "./content.js";
6
6
  import { redactModelFacingText, stringifyModelFacing } from "./common.js";
7
7
  const LARGE_OUTPUT_INLINE_MAX_CHARS = 8_000;
8
8
  const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
@@ -88,6 +88,46 @@ function buildSpillArtifactEntries(options) {
88
88
  ...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
89
89
  ];
90
90
  }
91
+ export const OBSERVATION_INLINE_MAX_CHARS = 16_000;
92
+ /** Bound the final model-visible result, after recovery/diagnostic assembly. Never truncate executable actions. */
93
+ export async function renderAgentBrowserObservation(options) {
94
+ const observation = projectAgentBrowserObservation(options.details, options.succeeded);
95
+ const images = options.content.filter(part => part.type === "image");
96
+ const prose = options.content.filter(part => part.type === "text").map(part => part.text).join("\n\n");
97
+ const { data: _data, error: _error, summary: _summary, ...metadata } = observation;
98
+ let text = options.json ? JSON.stringify(observation, null, 2) : `${prose}\n\nObservation: ${JSON.stringify(metadata)}`;
99
+ let artifactManifest = options.details.artifactManifest;
100
+ if (text.length > OBSERVATION_INLINE_MAX_CHARS) {
101
+ let spill;
102
+ let spillError;
103
+ try {
104
+ spill = await writeLargeOutputSpillFile({ data: { ...observation, ...(!options.json ? { text: prose } : {}) }, persistentArtifactStore: options.persistentArtifactStore, text });
105
+ artifactManifest = applyArtifactManifest({ content: [], summary: "" }, artifactManifest, buildSpillArtifactEntries({
106
+ commandInfo: { command: typeof options.details.command === "string" ? options.details.command : undefined },
107
+ evictedArtifacts: spill.evictedArtifacts, path: spill.path, storageScope: spill.storageScope,
108
+ })).artifactManifest;
109
+ }
110
+ catch (error) {
111
+ spillError = error instanceof Error ? error.message : String(error);
112
+ }
113
+ const compact = {
114
+ success: options.succeeded, resultCategory: observation.resultCategory,
115
+ failureCategory: observation.failureCategory, successCategory: observation.successCategory,
116
+ summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700) : undefined,
117
+ compacted: true,
118
+ ...(spill ? { observationPath: spill.path, retrieve: "Read observationPath for the complete redacted observation, including exact recovery actions and requested data." }
119
+ : { observationUnavailable: truncateText(redactModelFacingText(spillError ?? "Spill could not be written; request a smaller result."), 1_000) }),
120
+ };
121
+ for (const key of ["sessionName", "namespace", "codeRun", "error", "failures", "nextActions", "artifactVerification", "imageObservations", "data", "fullOutputPath", "fullOutputPaths"]) {
122
+ if (observation[key] !== undefined && JSON.stringify({ ...compact, [key]: observation[key] }, null, 2).length <= OBSERVATION_INLINE_MAX_CHARS - 500)
123
+ compact[key] = observation[key];
124
+ }
125
+ text = JSON.stringify(compact, null, 2);
126
+ if (!options.json)
127
+ text = `Browser observation compacted.\n${text}`;
128
+ }
129
+ return { content: [{ type: "text", text }, ...images], artifactManifest };
130
+ }
91
131
  export async function compactLargePresentationOutput(options) {
92
132
  const text = getPresentationText(options.presentation);
93
133
  if (text.length === 0 || !shouldCompactLargeOutput(text)) {
@@ -1,3 +1,4 @@
1
+ import { getScreenshotCapture } from "../orchestration/browser-run/screenshot-observation.js";
1
2
  import { isRecord } from "../parsing.js";
2
3
  import { buildReadConfirmationNextActions, nextReadConfirmation } from "../read-confirmation.js";
3
4
  import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs } from "../runtime.js";
@@ -56,13 +57,9 @@ export async function buildToolPresentation(options) {
56
57
  const recordingBatch = commandInfo.command === "batch" && isAgentBrowserBatchResultArray(envelope?.data)
57
58
  && envelope.data.some((row) => row.command?.[0] === "record");
58
59
  if (errorText && !recordingCommand && !recordingBatch) {
59
- return buildErrorPresentation({
60
- args,
61
- commandInfo,
62
- errorText,
63
- presentationCommand: presentationCommandInfo.command,
64
- sessionName,
65
- });
60
+ return { ...buildErrorPresentation({ args, commandInfo, errorText, presentationCommand: presentationCommandInfo.command, sessionName }),
61
+ data: redactPresentationData(commandInfoWithTokens, envelope?.data),
62
+ };
66
63
  }
67
64
  let data = enrichStreamStatusData(commandInfoWithTokens, envelope?.data);
68
65
  if (commandInfo.command === "session" && commandInfo.subcommand === "info" && isRecord(data)) {
@@ -85,6 +82,7 @@ export async function buildToolPresentation(options) {
85
82
  artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs,
86
83
  artifactRequests: options.batchArtifactRequests,
87
84
  buildNestedToolPresentation: buildToolPresentation,
85
+ modelVisible: options.modelVisible,
88
86
  cwd,
89
87
  data,
90
88
  namespace,
@@ -95,14 +93,14 @@ export async function buildToolPresentation(options) {
95
93
  summary,
96
94
  });
97
95
  }
98
- else if (commandInfo.command === "snapshot" && isRecord(data)) {
96
+ else if (options.modelVisible !== false && commandInfo.command === "snapshot" && isRecord(data)) {
99
97
  presentation = await buildSnapshotPresentation(data, persistentArtifactStore, artifactManifest);
100
98
  }
101
99
  else {
102
100
  presentation = {
103
101
  artifactVerification,
104
102
  artifacts: artifacts.length > 0 ? artifacts : undefined,
105
- content: [{ type: "text", text: artifactText ?? formatPresentationContentText(commandInfoWithTokens, data, compiledSemanticAction) }],
103
+ content: options.modelVisible === false ? [] : [{ type: "text", text: artifactText ?? formatPresentationContentText(commandInfoWithTokens, data, compiledSemanticAction) }],
106
104
  data: presentationData,
107
105
  summary,
108
106
  };
@@ -143,8 +141,13 @@ export async function buildToolPresentation(options) {
143
141
  }
144
142
  const imagePath = artifactRequest?.absolutePath ?? extractImagePath(commandInfo, cwd, data)
145
143
  ?? (recordingCommand ? artifacts.find((artifact) => artifact.kind === "image" && artifact.status === "saved")?.absolutePath : undefined);
146
- const presentationWithImage = imagePath ? await attachInlineImage(presentation, imagePath) : presentation;
147
- const compactedPresentation = await compactLargePresentationOutput({
144
+ const attachImage = imagePath && !(isRecord(data) && data.changed === false) && !artifacts.some(artifact => artifact.absolutePath === imagePath && ["missing", "stale", "failed"].includes(artifact.status ?? ""));
145
+ const presentationWithImage = attachImage ? await attachInlineImage(presentation, imagePath, options.modelVisible) : presentation;
146
+ if (commandInfo.command === "screenshot" && presentationWithImage.imageObservations) {
147
+ for (const image of presentationWithImage.imageObservations)
148
+ image.capture = getScreenshotCapture(commandInfoWithTokens.commandTokens ?? []).kind;
149
+ }
150
+ const compactedPresentation = options.modelVisible === false ? presentationWithImage : await compactLargePresentationOutput({
148
151
  artifactManifest,
149
152
  commandInfo,
150
153
  data: presentationData,
@@ -241,5 +244,7 @@ export async function buildToolPresentation(options) {
241
244
  if (presentationWithManifest.pageChangeSummary?.observed === false && presentationCommandInfo.command !== "batch" && presentationWithManifest.content[0]?.type === "text") {
242
245
  presentationWithManifest.content[0] = { ...presentationWithManifest.content[0], text: `${presentationWithManifest.content[0].text}\n\nAction dispatched; application change unverified. Verify the expected URL, text, state, or external receipt before relying on it.` };
243
246
  }
247
+ if (options.modelVisible === false)
248
+ presentationWithManifest.content = [];
244
249
  return sanitizeModelFacingPresentation(presentationWithManifest);
245
250
  }
@@ -2,8 +2,10 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import { basename } from "node:path";
3
3
  import { extractUpstreamCommandTokens, findCommandStartIndex, parseArgvDescriptor, parseCommandInfo, } from "./argv-descriptor.js";
4
4
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
5
- import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, stripUpstreamGlobalFlags, } from "./argv-grammar.js";
5
+ import { getBrowserResultMessage } from "./browser-transcript.js";
6
+ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExplicitSessionName, getAgentBrowserSessionIdentityKey, getBooleanFlagValue, GLOBAL_BOOLEAN_FLAGS_WITH_OPTIONAL_VALUES, GLOBAL_VALUE_FLAGS_ALLOWING_DASH_VALUE, isAgentBrowserSessionIdentityKeyInNamespace, isUpstreamEnvFlagEnabled, PREVALIDATED_VALUE_FLAGS, projectUpstreamGlobalFlags, resolveAgentBrowserNamespace, scanUpstreamGlobalFlagOccurrences, stripUpstreamGlobalFlags, } from "./argv-grammar.js";
6
7
  import { needsManagedSession } from "./command-policy.js";
8
+ import { parseBatchCommandArgument } from "./orchestration/batch-stdin.js";
7
9
  import { isCloseAllCommand, isCloseCommand, isOpenNavigationCommand } from "./command-taxonomy.js";
8
10
  import { hasLaunchScopedFlagToken, LAUNCH_SCOPED_FLAG_DEFINITIONS, LAUNCH_SCOPED_FLAG_LABEL, } from "./launch-scoped-flags.js";
9
11
  import { MANAGED_SESSION_NAME_PREFIX, } from "./managed-session-restore.js";
@@ -356,6 +358,19 @@ export function redactInvocationArgs(args) {
356
358
  redacted[index] = "[REDACTED]";
357
359
  }
358
360
  }
361
+ const batch = projectUpstreamGlobalFlags(args);
362
+ if (batch.tokens[0] === "batch") {
363
+ for (let index = 1; index < batch.tokens.length; index++) {
364
+ if (batch.tokens[index] === "--bail")
365
+ continue;
366
+ const step = parseBatchCommandArgument(batch.tokens[index]).step;
367
+ if (!step)
368
+ continue;
369
+ const safe = redactInvocationArgs(step);
370
+ if (safe.some((token, offset) => token !== step[offset]))
371
+ redacted[batch.indices[index]] = safe.map(token => `'${token.replaceAll("'", "'\\''")}'`).join(" ");
372
+ }
373
+ }
359
374
  return redacted;
360
375
  }
361
376
  export function isPlainTextInspectionArgs(args) {
@@ -478,11 +493,8 @@ export function restoreManagedSessionStateFromBranch(branch, fallbackSessionName
478
493
  closedSessionName = sessionName;
479
494
  };
480
495
  for (const entry of branch) {
481
- if (!isRecord(entry) || entry.type !== "message") {
482
- continue;
483
- }
484
- const message = isRecord(entry.message) ? entry.message : undefined;
485
- if (!message || message.toolName !== "agent_browser") {
496
+ const message = getBrowserResultMessage(entry);
497
+ if (!message) {
486
498
  continue;
487
499
  }
488
500
  const details = isRecord(message.details) ? message.details : undefined;
@@ -1,6 +1,7 @@
1
1
  import { extractUpstreamCommandTokens } from "./argv-descriptor.js";
2
2
  import { getAgentBrowserSessionIdentityKey, isAgentBrowserSessionIdentityKeyInNamespace } from "./argv-grammar.js";
3
3
  import { batchHasSuccessfulCloseAll, getSuccessfulBatchCloseLifecycle } from "./batch-lifecycle.js";
4
+ import { getBrowserResultMessage } from "./browser-transcript.js";
4
5
  import { isCloseAllCommand, isCloseCommand, isReadOnlyDiagnosticSessionTargetCommand, isRecordPageTransitionCommand, isUnverifiedPageTransitionCommand, isWebMcpPageMutationCommand, isWindowOrDiffPageTransitionCommand } from "./command-taxonomy.js";
5
6
  import { isRecord } from "./parsing.js";
6
7
  import { findReadConfirmation as findPendingReadConfirmation, parseReadConfirmation } from "./read-confirmation.js";
@@ -351,10 +352,8 @@ export class SessionPageState {
351
352
  const state = new SessionPageState();
352
353
  let restoredOrder = 0;
353
354
  for (const entry of branch) {
354
- if (!isRecord(entry) || entry.type !== "message")
355
- continue;
356
- const message = isRecord(entry.message) ? entry.message : undefined;
357
- if (!message || message.toolName !== "agent_browser")
355
+ const message = getBrowserResultMessage(entry);
356
+ if (!message)
358
357
  continue;
359
358
  const details = isRecord(message.details) ? message.details : undefined;
360
359
  if (!details)
@@ -0,0 +1,138 @@
1
+ import { Text } from "@earendil-works/pi-tui";
2
+ import { formatAgentBrowserRenderCall } from "./pi-tool-rendering.js";
3
+ 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
+ export const AGENT_BROWSER_TOOL_INVENTORY = {
6
+ action: { name: "agent_browser_action", description: "Click, check, fill, or select a unique accessible target using stable locators or current refs." },
7
+ qa: { name: "agent_browser_qa", description: "Check a URL or attached page with visible assertions and a diagnostic pass/fail verdict." },
8
+ electron: { name: "agent_browser_electron", description: "Discover, launch, probe, and clean up owned Electron desktop applications." },
9
+ source: { name: "agent_browser_source", description: "Find experimental DOM/React/local-source candidates with a bounded workspace scan." },
10
+ network: { name: "agent_browser_network_source", description: "Correlate failed requests with initiator and local-source candidates." },
11
+ };
12
+ const advancedNames = new Set(Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name));
13
+ /** Register once; advanced calls adapt only their input shape and reuse the ordinary executor. */
14
+ export function registerAgentBrowserToolSurface(pi, options) {
15
+ pi.registerTool({
16
+ name: "agent_browser",
17
+ label: "Agent Browser",
18
+ 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.",
19
+ promptSnippet: "Browse pages, inspect current refs, interact, and run native command batches.",
20
+ promptGuidelines: options.promptGuidelines ?? buildToolPromptGuidelines({ includeWebSearch: false }),
21
+ parameters: AGENT_BROWSER_PARAMS,
22
+ renderCall: options.renderCall,
23
+ renderResult: options.renderResult,
24
+ execute: options.execute,
25
+ executionMode: options.executionMode,
26
+ });
27
+ pi.registerTool({
28
+ name: "agent_browser_code",
29
+ label: "Agent Browser Code",
30
+ 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.",
31
+ promptSnippet: "Branch, loop, and aggregate browser observations with explicit JSON/image output.",
32
+ parameters: AGENT_BROWSER_CODE_PARAMS,
33
+ constrainedSampling: { type: "json_schema", strict: "prefer" },
34
+ execute: options.executeCode,
35
+ executionMode: options.executionMode,
36
+ renderCall(args, theme, context) {
37
+ const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
38
+ text.setText(formatAgentBrowserRenderCall(args, theme, context.expanded));
39
+ return text;
40
+ },
41
+ renderResult: options.renderResult,
42
+ });
43
+ pi.registerTool({
44
+ ...AGENT_BROWSER_TOOL_INVENTORY.action,
45
+ label: "Browser Action",
46
+ promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.action],
47
+ parameters: AGENT_BROWSER_ACTION_PARAMS,
48
+ renderResult: options.renderResult,
49
+ executionMode: options.executionMode,
50
+ execute(id, { outputPath, timeoutMs, ...semanticAction }, signal, onUpdate, ctx) {
51
+ return options.execute(id, { semanticAction, outputPath, timeoutMs }, signal, onUpdate, ctx);
52
+ },
53
+ });
54
+ pi.registerTool({
55
+ ...AGENT_BROWSER_TOOL_INVENTORY.qa,
56
+ label: "Browser QA",
57
+ promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.qa],
58
+ parameters: AGENT_BROWSER_QA_PARAMS,
59
+ renderResult: options.renderResult,
60
+ executionMode: options.executionMode,
61
+ execute(id, { outputPath, timeoutMs, sessionMode, ...qa }, signal, onUpdate, ctx) {
62
+ return options.execute(id, { qa, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
63
+ },
64
+ });
65
+ pi.registerTool({
66
+ ...AGENT_BROWSER_TOOL_INVENTORY.electron,
67
+ label: "Browser Electron",
68
+ promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.electron],
69
+ parameters: AGENT_BROWSER_ELECTRON_PARAMS,
70
+ renderResult: options.renderResult,
71
+ executionMode: options.executionMode,
72
+ execute(id, { outputPath, ...electron }, signal, onUpdate, ctx) {
73
+ return options.execute(id, { electron, outputPath }, signal, onUpdate, ctx);
74
+ },
75
+ });
76
+ pi.registerTool({
77
+ ...AGENT_BROWSER_TOOL_INVENTORY.source,
78
+ label: "Browser Source",
79
+ promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.source],
80
+ parameters: AGENT_BROWSER_SOURCE_PARAMS,
81
+ renderResult: options.renderResult,
82
+ executionMode: options.executionMode,
83
+ execute(id, { outputPath, timeoutMs, sessionMode, ...sourceLookup }, signal, onUpdate, ctx) {
84
+ return options.execute(id, { sourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
85
+ },
86
+ });
87
+ pi.registerTool({
88
+ ...AGENT_BROWSER_TOOL_INVENTORY.network,
89
+ label: "Browser Network Source",
90
+ promptGuidelines: [...ADVANCED_TOOL_PROMPT_GUIDELINES.network],
91
+ parameters: AGENT_BROWSER_NETWORK_SOURCE_PARAMS,
92
+ renderResult: options.renderResult,
93
+ executionMode: options.executionMode,
94
+ execute(id, { outputPath, timeoutMs, sessionMode, ...networkSourceLookup }, signal, onUpdate, ctx) {
95
+ return options.execute(id, { networkSourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
96
+ },
97
+ });
98
+ pi.registerTool({
99
+ name: "agent_browser_tools",
100
+ label: "Browser Tools",
101
+ 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.",
102
+ promptSnippet: "Discover and enable specialized browser actions, QA, Electron, and source tools.",
103
+ parameters: AGENT_BROWSER_TOOLS_PARAMS,
104
+ async execute(_id, params) {
105
+ const available = new Set(pi.getAllTools().map(({ name }) => name));
106
+ const active = new Set(pi.getActiveTools());
107
+ const added = (params.enable ?? []).map((key) => AGENT_BROWSER_TOOL_INVENTORY[key].name)
108
+ .filter((name) => available.has(name) && !active.has(name));
109
+ if (added.length > 0)
110
+ pi.setActiveTools([...new Set([...active, ...added])]);
111
+ const current = new Set(pi.getActiveTools());
112
+ const inventory = Object.entries(AGENT_BROWSER_TOOL_INVENTORY).map(([key, tool]) => ({
113
+ key, ...tool, available: available.has(tool.name), active: current.has(tool.name),
114
+ }));
115
+ return {
116
+ 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") }],
117
+ details: { inventory, added: [...new Set(added)] },
118
+ };
119
+ },
120
+ });
121
+ pi.on("session_start", async (_event, ctx) => {
122
+ // Pi owns selected tools and their durable system-message history; keep no parallel registry.
123
+ const argv = process.argv.slice(2);
124
+ const delimiter = argv.indexOf("--");
125
+ if ((delimiter < 0 ? argv : argv.slice(0, delimiter)).some((arg) => arg === "--tools" || arg === "-t"))
126
+ return;
127
+ // A host-filtered catalog is an explicit selection, not our default surface.
128
+ const available = new Set(pi.getAllTools().map(({ name }) => name));
129
+ if (!["agent_browser", "agent_browser_code", "agent_browser_tools", ...advancedNames].every(name => available.has(name)))
130
+ return;
131
+ // Keep Pi's replay helper behind the awaited restoration boundary, not factory registration.
132
+ const { getCurrentSystemMessage } = await import("@earendil-works/pi-ai");
133
+ const current = getCurrentSystemMessage(ctx.sessionManager.buildSessionProjection().messages);
134
+ const restored = new Set(current?.toolsAdded?.map(({ name }) => name));
135
+ const active = pi.getActiveTools().filter((name) => !advancedNames.has(name) || restored.has(name));
136
+ pi.setActiveTools([...new Set([...active, ...[...restored].filter((name) => advancedNames.has(name) && available.has(name))])]);
137
+ });
138
+ }
@@ -56,11 +56,14 @@ runInContext("const " + stateName + " = (() => {\n" +
56
56
  " });\n" +
57
57
  " };\n" +
58
58
  " const emit = function emit(value) { sendValue({ type: 'emit', value }); };\n" +
59
+ " const emitImage = function emitImage(value) { sendValue({ type: 'image', value }); };\n" +
59
60
  " Object.setPrototypeOf(browser, null);\n" +
60
61
  " Object.setPrototypeOf(emit, null);\n" +
62
+ " Object.setPrototypeOf(emitImage, null);\n" +
61
63
  " Object.freeze(browser);\n" +
62
64
  " Object.freeze(emit);\n" +
63
- " Object.defineProperties(globalThis, { browser: { value: browser, writable: false, configurable: false }, emit: { value: emit, writable: false, configurable: false } });\n" +
65
+ " Object.freeze(emitImage);\n" +
66
+ " Object.defineProperties(globalThis, { browser: { value: browser, writable: false, configurable: false }, emit: { value: emit, writable: false, configurable: false }, emitImage: { value: emitImage, writable: false, configurable: false } });\n" +
64
67
  " const complete = (ok, value) => {\n" +
65
68
  " if (ok) {\n" +
66
69
  " try { sendValue(value === undefined ? { type: 'complete', hasValue: false } : { type: 'complete', hasValue: true, value }); }\n" +