pi-agent-browser-native 0.8.3 → 0.9.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 (48) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +12 -11
  3. package/dist/extensions/agent-browser/index.js +96 -320
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +1 -1
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +3 -3
  6. package/dist/extensions/agent-browser/lib/browser-journal.js +33 -12
  7. package/dist/extensions/agent-browser/lib/browser-transcript.js +1 -1
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +0 -3
  9. package/dist/extensions/agent-browser/lib/config-policy.js +0 -13
  10. package/dist/extensions/agent-browser/lib/config.js +3 -48
  11. package/dist/extensions/agent-browser/lib/electron/launch.js +4 -4
  12. package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +2 -3
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +28 -6
  15. package/dist/extensions/agent-browser/lib/input-modes/types.js +2 -2
  16. package/dist/extensions/agent-browser/lib/managed-session-restore.js +1 -4
  17. package/dist/extensions/agent-browser/lib/native-output.js +46 -0
  18. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +0 -9
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-merge.js +22 -0
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +1 -1
  21. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +2 -2
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -1
  23. package/dist/extensions/agent-browser/lib/orchestration/execution-queue.js +86 -0
  24. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +31 -17
  25. package/dist/extensions/agent-browser/lib/process.js +0 -3
  26. package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -12
  27. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +2 -2
  28. package/dist/extensions/agent-browser/lib/results/network-routes.js +1 -1
  29. package/dist/extensions/agent-browser/lib/results/network.js +1 -1
  30. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +1 -1
  31. package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
  32. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +1 -1
  33. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +15 -8
  34. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +2 -2
  35. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +1 -1
  36. package/dist/extensions/agent-browser/lib/runtime.js +4 -2
  37. package/dist/extensions/agent-browser/lib/session-page-state.js +1 -1
  38. package/dist/extensions/agent-browser/lib/tool-surface.js +39 -11
  39. package/dist/extensions/agent-browser/lib/web-search.js +100 -79
  40. package/docs/ARCHITECTURE.md +17 -19
  41. package/docs/COMMAND_REFERENCE.md +6 -2
  42. package/docs/RELEASE.md +26 -20
  43. package/docs/REQUIREMENTS.md +5 -5
  44. package/docs/SESSION_STORAGE.md +4 -4
  45. package/docs/SUPPORT_MATRIX.md +15 -13
  46. package/docs/TOOL_CONTRACT.md +12 -8
  47. package/package.json +6 -6
  48. package/scripts/doctor.mjs +12 -4
@@ -1,5 +1,4 @@
1
1
  import { getKeybindings, Text, truncateToWidth } from "@earendil-works/pi-tui";
2
- import { BROWSER_RESULT_TOOLS } from "./browser-transcript.js";
3
2
  import { compileAgentBrowserElectron } from "./input-modes/electron.js";
4
3
  import { compileAgentBrowserQaPreset } from "./input-modes/job.js";
5
4
  import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup } from "./input-modes/lookups.js";
@@ -158,7 +157,7 @@ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
158
157
  }
159
158
  return text;
160
159
  }
161
- export function formatAgentBrowserRenderResult(result, options, theme, isError) {
160
+ function formatAgentBrowserRenderResult(result, options, theme, isError) {
162
161
  if (options.isPartial) {
163
162
  return theme.fg("warning", "Running agent-browser...");
164
163
  }
@@ -185,10 +184,10 @@ function formatModelVisibleFailureCategoryNotice(details) {
185
184
  : undefined;
186
185
  return `Result category: failure${failureCategory ? `; failureCategory: ${failureCategory}` : ""}; Pi tool isError: true.`;
187
186
  }
188
- function agentBrowserToolResultRequestedJson(event) {
189
- const details = isRecord(event.details) ? event.details : undefined;
187
+ function agentBrowserToolResultRequestedJson(result, input) {
188
+ const details = isRecord(result.details) ? result.details : undefined;
190
189
  const detailArgs = Array.isArray(details?.args) ? details.args : undefined;
191
- const inputArgs = isRecord(event.input) && Array.isArray(event.input.args) ? event.input.args : undefined;
190
+ const inputArgs = isRecord(input) && Array.isArray(input.args) ? input.args : undefined;
192
191
  return detailArgs?.includes("--json") === true || inputArgs?.includes("--json") === true;
193
192
  }
194
193
  function agentBrowserToolResultHasParseableJsonContent(content) {
@@ -219,26 +218,41 @@ function appendModelVisibleFailureCategoryNotice(content, notice) {
219
218
  ? { ...item, text: `${textItem.text}\n\n${notice}` }
220
219
  : item);
221
220
  }
222
- export function buildAgentBrowserToolResultPatch(event) {
223
- if (!BROWSER_RESULT_TOOLS.has(event.toolName))
224
- return undefined;
225
- const preservesParseableJson = (event.toolName === "agent_browser_code" || agentBrowserToolResultRequestedJson(event)) && agentBrowserToolResultHasParseableJsonContent(event.content);
226
- const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(event.details);
227
- const content = notice ? appendModelVisibleFailureCategoryNotice(event.content, notice) : undefined;
228
- const shouldMarkError = isRecord(event.details) && event.details.resultCategory === "failure" && event.isError !== true;
229
- if (!shouldMarkError && !content)
230
- return undefined;
221
+ export function finalizeAgentBrowserFailure(result, input) {
222
+ const failed = result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure");
223
+ const preservesParseableJson = (isRecord(input) && "code" in input || agentBrowserToolResultRequestedJson(result, input)) && agentBrowserToolResultHasParseableJsonContent(result.content);
224
+ const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(result.details);
225
+ const content = notice ? appendModelVisibleFailureCategoryNotice(result.content, notice) : undefined;
231
226
  return {
232
- ...(content ? { content } : {}),
233
- ...(shouldMarkError ? { isError: true } : {}),
227
+ ...result,
228
+ content: content ?? result.content,
229
+ isError: failed,
234
230
  };
235
231
  }
236
232
  export class AgentBrowserResultComponent {
237
233
  expanded = false;
238
234
  theme;
239
235
  text = new Text("", 0, 0);
236
+ value;
237
+ formatKey;
238
+ setResult(result, options, theme, isError) {
239
+ const details = isRecord(result.details) ? result.details : undefined;
240
+ // Theme is a stable proxy in Pi; its resolved colors identify theme/terminal-color changes.
241
+ const key = [getPrimaryTextContent(result), details?.summary, details?.resultCategory, details?.failureCategory, options.isPartial, isError, theme, theme.colors, theme.fg];
242
+ if (!this.formatKey || key.some((value, index) => value !== this.formatKey[index])) {
243
+ this.formatKey = key;
244
+ this.setState(formatAgentBrowserRenderResult(result, options, theme, isError), options.expanded, theme);
245
+ }
246
+ else {
247
+ this.expanded = options.expanded;
248
+ this.theme = theme;
249
+ }
250
+ }
240
251
  setState(value, expanded, theme) {
241
- this.text.setText(value);
252
+ if (value !== this.value) {
253
+ this.text.setText(value);
254
+ this.value = value;
255
+ }
242
256
  this.expanded = expanded;
243
257
  this.theme = theme;
244
258
  }
@@ -290,9 +290,6 @@ export async function getAgentBrowserSocketDirValidationError(socketDir, uid = t
290
290
  return `the directory could not be inspected (${error.code ?? "unknown error"})`;
291
291
  }
292
292
  }
293
- export async function ensureAgentBrowserSocketDir(socketDir, uid = typeof process.getuid === "function" ? process.getuid() : undefined) {
294
- return await getAgentBrowserSocketDirValidationError(socketDir, uid) === undefined;
295
- }
296
293
  export function getAgentBrowserSocketPathValidationError(options) {
297
294
  if ((options.platform ?? processPlatform) === "win32")
298
295
  return undefined;
@@ -199,7 +199,7 @@ export function buildPromptPolicy(prompt) {
199
199
  requestedArtifacts: extractPromptRequestedArtifacts(prompt),
200
200
  };
201
201
  }
202
- function getMessageText(content) {
202
+ export function getMessageText(content) {
203
203
  if (typeof content === "string")
204
204
  return content;
205
205
  if (!Array.isArray(content))
@@ -213,17 +213,21 @@ function getMessageText(content) {
213
213
  .filter((text) => text.length > 0)
214
214
  .join("\n");
215
215
  }
216
- export function getLatestUserPrompt(branch) {
217
- for (let index = branch.length - 1; index >= 0; index -= 1) {
218
- const entry = branch[index];
219
- if (typeof entry !== "object" || entry === null || !("type" in entry) || entry.type !== "message") {
220
- continue;
221
- }
222
- const message = "message" in entry ? entry.message : undefined;
223
- if (typeof message !== "object" || message === null || !("role" in message) || message.role !== "user") {
224
- continue;
216
+ /** Raw prompt intent survives compaction/context edits; only restoration walks ancestry. */
217
+ export function getLatestUserMessage(manager) {
218
+ let id = manager.getLeafId();
219
+ const seen = new Set();
220
+ while (id !== null) {
221
+ if (typeof id !== "string" || seen.has(id))
222
+ throw new Error("Latest user prompt ancestry is invalid or cyclic.");
223
+ seen.add(id);
224
+ const entry = manager.getEntry(id);
225
+ if (!entry || entry.id !== id || entry.parentId !== null && typeof entry.parentId !== "string") {
226
+ throw new Error("Latest user prompt ancestry is incomplete.");
225
227
  }
226
- return getMessageText("content" in message ? message.content : undefined);
228
+ if (entry.type === "message" && entry.message.role === "user")
229
+ return entry.message;
230
+ id = entry.parentId;
227
231
  }
228
- return "";
232
+ return undefined;
229
233
  }
@@ -7,8 +7,8 @@ export function isPendingRecordingArtifact(artifact) {
7
7
  return artifact.recordingState === "openRecording" || artifact.status === "pending"
8
8
  || (artifact.status === undefined && isPendingRecordingCommand(artifact.command, artifact.subcommand, artifact.kind));
9
9
  }
10
- export const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
11
- export const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
10
+ const SESSION_ARTIFACT_MANIFEST_VERSION = 1;
11
+ const SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES_ENV = "PI_AGENT_BROWSER_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES";
12
12
  export const DEFAULT_SESSION_ARTIFACT_MANIFEST_MAX_ENTRIES = 100;
13
13
  function parsePositiveSafeInteger(value) {
14
14
  if (value === undefined)
@@ -31,7 +31,7 @@ function getRouteDiagnosticReason(item, route) {
31
31
  return "unfulfilled-routed-request";
32
32
  return undefined;
33
33
  }
34
- export function getNetworkRouteMode(args) {
34
+ function getNetworkRouteMode(args) {
35
35
  if (args.includes("--abort"))
36
36
  return "abort";
37
37
  if (args.includes("--body"))
@@ -3,7 +3,7 @@ export function getStringRecordField(value, key) {
3
3
  const field = value[key];
4
4
  return typeof field === "string" && field.trim().length > 0 ? field.trim() : undefined;
5
5
  }
6
- export function getNetworkRequestUrlPath(url) {
6
+ function getNetworkRequestUrlPath(url) {
7
7
  if (!url)
8
8
  return undefined;
9
9
  try {
@@ -68,7 +68,7 @@ function getStatefulCommandSensitiveValues(command) {
68
68
  return [];
69
69
  const tokens = extractUpstreamCommandTokens(command);
70
70
  const values = [];
71
- if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3])
71
+ if (tokens[0] === "cookies" && tokens[1] === "set" && !tokens.slice(2).includes("--curl") && tokens[3])
72
72
  values.push(tokens[3]);
73
73
  if (tokens[0] === "storage" && ["local", "session"].includes(tokens[1] ?? "") && tokens[2] === "set" && tokens[4])
74
74
  values.push(tokens[4]);
@@ -2,6 +2,7 @@ import { isRecord } from "../../parsing.js";
2
2
  import { redactSensitiveValue } from "../../runtime.js";
3
3
  import { omitUpstreamLifecycle } from "./common.js";
4
4
  import { redactTimeoutPartialProgress } from "../../orchestration/browser-run/diagnostics.js";
5
+ export const OBSERVATION_INLINE_MAX_CHARS = 16_000;
5
6
  export function isStringArray(value) {
6
7
  return Array.isArray(value) && value.every((item) => typeof item === "string");
7
8
  }
@@ -40,6 +41,9 @@ export function projectAgentBrowserObservation(details, succeeded) {
40
41
  }
41
42
  if (details.inspection === true && details.data === undefined && typeof details.stdout === "string")
42
43
  observation.data = details.stdout;
44
+ // Identical error/summary strings double-count large failure text against the observation bound; keep error.
45
+ if (typeof observation.error === "string" && observation.error === observation.summary)
46
+ observation.summary = undefined;
43
47
  if (isRecord(observation.data))
44
48
  observation.data = omitUpstreamLifecycle(observation.data);
45
49
  if (Array.isArray(details.batchSteps)) {
@@ -507,7 +507,7 @@ export function formatNetworkRouteDiagnosticsText(diagnostics) {
507
507
  lines.push("If this route is intended as a mock, inspect the request/headers and treat failed, pending, or CORS-looking rows as unfulfilled until a mocked response is observed.");
508
508
  return lines.join("\n");
509
509
  }
510
- export function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
510
+ function buildNetworkRouteDiagnosticsNextActions(diagnostics, sessionName) {
511
511
  const diagnostic = diagnostics?.find((item) => item.requestId) ?? diagnostics?.[0];
512
512
  if (!diagnostic)
513
513
  return undefined;
@@ -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, projectAgentBrowserObservation } from "./content.js";
5
+ import { getPresentationText, projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } 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,16 +88,17 @@ function buildSpillArtifactEntries(options) {
88
88
  ...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
89
89
  ];
90
90
  }
91
- export const OBSERVATION_INLINE_MAX_CHARS = 16_000;
91
+ export { OBSERVATION_INLINE_MAX_CHARS } from "./content.js";
92
92
  /** Bound the final model-visible result, after recovery/diagnostic assembly. Never truncate executable actions. */
93
93
  export async function renderAgentBrowserObservation(options) {
94
94
  const observation = projectAgentBrowserObservation(options.details, options.succeeded);
95
+ let structuredContent = observation;
95
96
  const images = options.content.filter(part => part.type === "image");
96
97
  const prose = options.content.filter(part => part.type === "text").map(part => part.text).join("\n\n");
97
98
  const { data: _data, error: _error, summary: _summary, ...metadata } = observation;
98
99
  let text = options.json ? JSON.stringify(observation, null, 2) : `${prose}\n\nObservation: ${JSON.stringify(metadata)}`;
99
100
  let artifactManifest = options.details.artifactManifest;
100
- if (text.length > OBSERVATION_INLINE_MAX_CHARS) {
101
+ if (text.length > OBSERVATION_INLINE_MAX_CHARS || JSON.stringify(observation).length > OBSERVATION_INLINE_MAX_CHARS) {
101
102
  let spill;
102
103
  let spillError;
103
104
  try {
@@ -114,7 +115,8 @@ export async function renderAgentBrowserObservation(options) {
114
115
  const compact = {
115
116
  success: options.succeeded, resultCategory: observation.resultCategory,
116
117
  failureCategory: observation.failureCategory, successCategory: observation.successCategory,
117
- summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700) : undefined,
118
+ summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700)
119
+ : typeof observation.error === "string" ? truncateText(observation.error, 700) : undefined,
118
120
  compacted: true,
119
121
  ...(spill ? { observationPath: spill.path, retrieve: "Read observationPath for the complete redacted observation, including exact recovery actions and requested data." }
120
122
  : { observationUnavailable: truncateText(redactModelFacingText(spillError ?? "Spill could not be written; request a smaller result."), 1_000) }),
@@ -123,11 +125,16 @@ export async function renderAgentBrowserObservation(options) {
123
125
  if (observation[key] !== undefined && JSON.stringify({ ...compact, [key]: observation[key] }, null, 2).length <= OBSERVATION_INLINE_MAX_CHARS - 500)
124
126
  compact[key] = observation[key];
125
127
  }
126
- text = JSON.stringify(compact, null, 2);
127
- if (!options.json)
128
- text = `Browser observation compacted.\n${text}`;
128
+ structuredContent = compact;
129
+ if (options.preserveContent)
130
+ text = prose;
131
+ else {
132
+ text = JSON.stringify(compact, null, 2);
133
+ if (!options.json)
134
+ text = `Browser observation compacted.\n${text}`;
135
+ }
129
136
  }
130
- return { content: [{ type: "text", text }, ...images], artifactManifest };
137
+ return { content: [{ type: "text", text }, ...images], artifactManifest, structuredContent: JSON.parse(JSON.stringify(structuredContent)) };
131
138
  }
132
139
  export async function compactLargePresentationOutput(options) {
133
140
  const text = getPresentationText(options.presentation);
@@ -19,7 +19,7 @@ function formatSemanticActionTarget(compiled) {
19
19
  const target = `${locator} ${quotedValue}`;
20
20
  return name ? `${target} (name ${JSON.stringify(name)})` : target;
21
21
  }
22
- export function formatSemanticActionCompactLine(compiled) {
22
+ function formatSemanticActionCompactLine(compiled) {
23
23
  const target = formatSemanticActionTarget(compiled);
24
24
  switch (compiled.action) {
25
25
  case "click":
@@ -34,7 +34,7 @@ export function formatSemanticActionCompactLine(compiled) {
34
34
  return `${compiled.action}: ${target}`;
35
35
  }
36
36
  }
37
- export function resolveSemanticPresentationCommand(compiled) {
37
+ function resolveSemanticPresentationCommand(compiled) {
38
38
  if (!compiled || !SEMANTIC_PRESENTATION_ACTIONS.has(compiled.action))
39
39
  return undefined;
40
40
  if (compiled.action === "select")
@@ -256,6 +256,6 @@ export function formatRichInputRecoveryText(diagnostic) {
256
256
  `- ${diagnostic.inputMethodHint}`,
257
257
  ].join("\n");
258
258
  }
259
- export function normalizeSemanticActionAccessibleName(name) {
259
+ function normalizeSemanticActionAccessibleName(name) {
260
260
  return name.replace(/\s+/g, " ").trim().toLowerCase();
261
261
  }
@@ -254,7 +254,7 @@ function redactBearerCredentials(text) {
254
254
  return formatRedactedCredential(label, credential, trailing);
255
255
  });
256
256
  }
257
- export function isSensitiveFieldName(key) {
257
+ function isSensitiveFieldName(key) {
258
258
  SENSITIVE_FIELD_NAME_PATTERN.lastIndex = 0;
259
259
  return SENSITIVE_FIELD_NAME_PATTERN.test(key);
260
260
  }
@@ -347,7 +347,9 @@ export function redactInvocationArgs(args) {
347
347
  }
348
348
  }
349
349
  }
350
- if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set" && redacted[commandStartIndex + 3] !== undefined) {
350
+ if (commandStartIndex !== undefined && args[commandStartIndex] === "cookies" && args[commandStartIndex + 1] === "set"
351
+ && !extractUpstreamCommandTokens(args).slice(2).includes("--curl")
352
+ && redacted[commandStartIndex + 3] !== undefined) {
351
353
  redacted[commandStartIndex + 3] = "[REDACTED]";
352
354
  }
353
355
  if (commandStartIndex !== undefined
@@ -62,7 +62,7 @@ function extractStringResultField(data, fieldName) {
62
62
  const text = data[fieldName].trim();
63
63
  return text.length > 0 ? text : undefined;
64
64
  }
65
- export function extractSessionTabTargetFromData(data) {
65
+ function extractSessionTabTargetFromData(data) {
66
66
  const directTarget = normalizeSessionTabTarget({
67
67
  targetId: isRecord(data) && typeof data.targetId === "string" ? data.targetId : undefined,
68
68
  title: extractStringResultField(data, "title"),
@@ -1,5 +1,7 @@
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
6
  export const AGENT_BROWSER_TOOL_INVENTORY = {
5
7
  action: { name: "agent_browser_action", description: "Click, check, fill, or select a unique accessible target using stable locators or current refs." },
@@ -15,24 +17,28 @@ export const AGENT_BROWSER_INSTRUCTION_GROUP = {
15
17
  const advancedNames = new Set(Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name));
16
18
  /** Register once; advanced calls adapt only their input shape and reuse the ordinary executor. */
17
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 };
18
22
  pi.registerTool({
19
23
  name: "agent_browser",
24
+ ...nativeOutput,
20
25
  label: "Agent Browser",
21
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.",
22
27
  promptSnippet: "Browse pages, inspect current refs, interact, and run native command batches.",
23
28
  parameters: AGENT_BROWSER_PARAMS,
24
29
  renderCall: options.renderCall,
25
30
  renderResult: options.renderResult,
26
- execute: options.execute,
31
+ execute,
27
32
  executionMode: options.executionMode,
28
33
  });
29
34
  pi.registerTool({
30
35
  name: "agent_browser_code",
36
+ ...nativeOutput,
31
37
  label: "Agent Browser Code",
32
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.",
33
39
  promptSnippet: "Branch, loop, and aggregate browser observations with explicit JSON/image output.",
34
40
  parameters: AGENT_BROWSER_CODE_PARAMS,
35
- execute: options.executeCode,
41
+ execute: async (id, params, signal, onUpdate, ctx) => finalizeAgentBrowserNativeResult(await options.executeCode(id, params, signal, onUpdate, ctx), params),
36
42
  executionMode: options.executionMode,
37
43
  renderCall(args, theme, context) {
38
44
  const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
@@ -43,56 +49,74 @@ export function registerAgentBrowserToolSurface(pi, options) {
43
49
  });
44
50
  pi.registerTool({
45
51
  ...AGENT_BROWSER_TOOL_INVENTORY.action,
52
+ ...nativeOutput,
53
+ defaultActive: false,
46
54
  label: "Browser Action",
47
55
  parameters: AGENT_BROWSER_ACTION_PARAMS,
48
56
  renderResult: options.renderResult,
49
57
  executionMode: options.executionMode,
50
58
  execute(id, { outputPath, timeoutMs, ...semanticAction }, signal, onUpdate, ctx) {
51
- return options.execute(id, { semanticAction, outputPath, timeoutMs }, signal, onUpdate, ctx);
59
+ return execute(id, { semanticAction, outputPath, timeoutMs }, signal, onUpdate, ctx);
52
60
  },
53
61
  });
54
62
  pi.registerTool({
55
63
  ...AGENT_BROWSER_TOOL_INVENTORY.qa,
64
+ ...nativeOutput,
65
+ defaultActive: false,
56
66
  label: "Browser QA",
57
67
  parameters: AGENT_BROWSER_QA_PARAMS,
58
68
  renderResult: options.renderResult,
59
69
  executionMode: options.executionMode,
60
70
  execute(id, { outputPath, timeoutMs, sessionMode, ...qa }, signal, onUpdate, ctx) {
61
- return options.execute(id, { qa, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
71
+ return execute(id, { qa, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
62
72
  },
63
73
  });
64
74
  pi.registerTool({
65
75
  ...AGENT_BROWSER_TOOL_INVENTORY.electron,
76
+ ...nativeOutput,
77
+ defaultActive: false,
66
78
  label: "Browser Electron",
67
79
  parameters: AGENT_BROWSER_ELECTRON_PARAMS,
68
80
  renderResult: options.renderResult,
69
81
  executionMode: options.executionMode,
70
82
  execute(id, { outputPath, ...electron }, signal, onUpdate, ctx) {
71
- return options.execute(id, { electron, outputPath }, signal, onUpdate, ctx);
83
+ return execute(id, { electron, outputPath }, signal, onUpdate, ctx);
72
84
  },
73
85
  });
74
86
  pi.registerTool({
75
87
  ...AGENT_BROWSER_TOOL_INVENTORY.source,
88
+ ...nativeOutput,
89
+ defaultActive: false,
76
90
  label: "Browser Source",
77
91
  parameters: AGENT_BROWSER_SOURCE_PARAMS,
78
92
  renderResult: options.renderResult,
79
93
  executionMode: options.executionMode,
80
94
  execute(id, { outputPath, timeoutMs, sessionMode, ...sourceLookup }, signal, onUpdate, ctx) {
81
- return options.execute(id, { sourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
95
+ return execute(id, { sourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
82
96
  },
83
97
  });
84
98
  pi.registerTool({
85
99
  ...AGENT_BROWSER_TOOL_INVENTORY.network,
100
+ ...nativeOutput,
101
+ defaultActive: false,
86
102
  label: "Browser Network Source",
87
103
  parameters: AGENT_BROWSER_NETWORK_SOURCE_PARAMS,
88
104
  renderResult: options.renderResult,
89
105
  executionMode: options.executionMode,
90
106
  execute(id, { outputPath, timeoutMs, sessionMode, ...networkSourceLookup }, signal, onUpdate, ctx) {
91
- return options.execute(id, { networkSourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
107
+ return execute(id, { networkSourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
92
108
  },
93
109
  });
94
110
  pi.registerTool({
95
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
+ }),
96
120
  label: "Browser Tools",
97
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.",
98
122
  promptSnippet: "Discover and enable specialized browser actions, QA, Electron, and source tools.",
@@ -111,11 +135,13 @@ export function registerAgentBrowserToolSurface(pi, options) {
111
135
  return {
112
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") }],
113
137
  details: { inventory, added: [...new Set(added)] },
138
+ structuredContent: { inventory, added: [...new Set(added)] },
114
139
  };
115
140
  },
116
141
  });
117
142
  pi.on("session_start", async (_event, ctx) => {
118
- // 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.
119
145
  const argv = process.argv.slice(2);
120
146
  const delimiter = argv.indexOf("--");
121
147
  if ((delimiter < 0 ? argv : argv.slice(0, delimiter)).some((arg) => arg === "--tools" || arg === "-t"))
@@ -125,7 +151,7 @@ export function registerAgentBrowserToolSurface(pi, options) {
125
151
  if (!["agent_browser", "agent_browser_code", "agent_browser_tools", ...advancedNames].every(name => available.has(name)))
126
152
  return;
127
153
  // Native dynamic imports bypass Pi's host-package aliases in consumer installs.
128
- // Only tool names are needed; replay their native deltas without a host import.
154
+ // Only tool names are needed; replay native deltas without requiring the optional pi-ai peer.
129
155
  const restored = new Set();
130
156
  for (const message of ctx.sessionManager.buildSessionProjection().messages) {
131
157
  if (message.role !== "system")
@@ -135,7 +161,9 @@ export function registerAgentBrowserToolSurface(pi, options) {
135
161
  for (const added of message.toolsAdded ?? [])
136
162
  restored.add(added.name);
137
163
  }
138
- const active = pi.getActiveTools().filter((name) => !advancedNames.has(name) || restored.has(name));
139
- pi.setActiveTools([...new Set([...active, ...[...restored].filter((name) => advancedNames.has(name) && available.has(name))])]);
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]);
140
168
  });
141
169
  }