pi-agent-browser-native 0.8.3 → 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.
@@ -241,14 +241,7 @@ export async function readBrowserEntries(manager, physical = false) {
241
241
  const header = metadata.find(entry => entry.value.type === "session");
242
242
  if (header?.value.id !== (manager.getHeader()?.id ?? manager.getSessionId()))
243
243
  throw new Error("Pi session journal identity does not match the active session.");
244
- const publicManager = manager;
245
244
  const selected = physical ? metadata.filter(entry => entry.value.type !== "session") : branchEntries(metadata.filter(entry => entry.value.type !== "session"), leaf);
246
- if (publicManager.iterateEntryMetadata) {
247
- const native = new Map([...publicManager.iterateEntryMetadata(physical ? {} : { branchFrom: leaf })].map(entry => [entry.id, entry.parentId]));
248
- for (const entry of selected)
249
- if (typeof entry.value.id !== "string" || native.get(entry.value.id) !== entry.value.parentId)
250
- throw new Error("Published browser journal ancestry differs from the native selected boundary.");
251
- }
252
245
  const projected = [];
253
246
  const definitions = new Map();
254
247
  for (const entry of selected) {
@@ -285,9 +278,7 @@ export async function readBrowserEntries(manager, physical = false) {
285
278
  throw new Error(`Browser snapshot ${id} has no ancestral definition; inspect and take a fresh snapshot.`);
286
279
  // ponytail: a requested individual snapshot must fit its consumer's memory. Metadata
287
280
  // scans skip unlimited unrelated values; upgrade with native per-value paging if needed.
288
- const native = publicManager.getEntryMetadata && typeof entry.value.id === "string" ? manager.getEntry(entry.value.id) : undefined;
289
- const body = native && isRecord(native) && isRecord(native.data) ? { data: native.data }
290
- : await projectJson(readRange(file, entry), [["data", "snapshot"]], Infinity);
281
+ const body = await projectJson(readRange(file, entry), [["data", "snapshot"]], Infinity);
291
282
  const destination = projected.find(candidate => isRecord(candidate) && candidate.id === entry.value.id);
292
283
  if (!isRecord(destination) || !isRecord(destination.data) || !isRecord(body?.data))
293
284
  throw new Error("Winning browser snapshot could not be read.");
@@ -342,13 +333,22 @@ export function hasPublishedBrowserJournal(manager) {
342
333
  /** Capture before waiting: native selection precedes sequential session_tree handlers. */
343
334
  export function captureBrowserBranch(manager, isGenerationCurrent) {
344
335
  const sessionId = manager.getSessionId();
345
- const branch = { sessionId, anchorId: manager.getLeafId(), isCurrent() {
336
+ let anchorId = manager.getLeafId();
337
+ let validatedAnchor;
338
+ let validatedLeaf;
339
+ const branch = { sessionId, anchorId, isCurrent() {
346
340
  if (!isGenerationCurrent() || manager.getSessionId() !== sessionId)
347
341
  return false;
342
+ if (branch.anchorId !== anchorId) {
343
+ validatedAnchor = validatedLeaf = undefined;
344
+ anchorId = branch.anchorId;
345
+ }
348
346
  let leaf = manager.getLeafId();
349
347
  // Empty selection is not a universal ancestor of other independent roots.
350
348
  let current = branch.anchorId === null && leaf === null;
351
349
  const seen = new Set();
350
+ let selected;
351
+ let anchor;
352
352
  while (leaf !== null) {
353
353
  if (typeof leaf !== "string" || seen.has(leaf))
354
354
  throw new Error("Selected Pi journal ancestry is invalid or cyclic.");
@@ -356,10 +356,31 @@ export function captureBrowserBranch(manager, isGenerationCurrent) {
356
356
  const entry = manager.getEntry(leaf);
357
357
  if (!entry || entry.id !== leaf || entry.parentId !== null && typeof entry.parentId !== "string")
358
358
  throw new Error("Selected Pi journal ancestry is incomplete.");
359
- if (leaf === branch.anchorId)
359
+ selected ??= { entry, parentId: entry.parentId };
360
+ const cached = leaf === branch.anchorId ? validatedAnchor : leaf === validatedLeaf?.entry.id ? validatedLeaf : undefined;
361
+ if (cached) {
362
+ // Keep the native indexed lookup: it also refreshes fork journal identity.
363
+ if (cached.entry === entry && cached.parentId === entry.parentId) {
364
+ current = true;
365
+ break;
366
+ }
367
+ validatedAnchor = validatedLeaf = undefined;
368
+ }
369
+ if (leaf === branch.anchorId) {
360
370
  current = true;
371
+ anchor = { entry, parentId: entry.parentId };
372
+ }
361
373
  leaf = entry.parentId;
362
374
  }
375
+ // Native entries/parents are append-only. Cache only after validating through
376
+ // the root (or an already validated endpoint), never merely on leaf === anchor.
377
+ // ponytail: behind-host mutation of an old ancestor without native reseed or
378
+ // generation change is outside this contract; durable file proofs
379
+ // remain in replay/append. Upgrade if Pi exposes a separate ancestry revision.
380
+ if (current && selected) {
381
+ validatedAnchor = anchor ?? validatedAnchor;
382
+ validatedLeaf = selected;
383
+ }
363
384
  return current;
364
385
  } };
365
386
  return branch;
@@ -1,8 +1,6 @@
1
1
  import { readFileSync, statSync } from "node:fs";
2
2
  import { dirname, isAbsolute, join } from "node:path";
3
3
  import { isRecord } from "./parsing.js";
4
- import { getBrowserResultMessage } from "./browser-transcript.js";
5
- import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
6
4
  function isDirectoryOwner(source) {
7
5
  if (/^(?:npm:pi-change-working-dir|git:github\.com\/fitchmultz\/pi-change-working-dir(?:\.git)?)(?:@.+)?$/.test(source.source))
8
6
  return true;
@@ -50,17 +48,3 @@ export function getBrowserCwdError(cwd) {
50
48
  }
51
49
  return `Browser launch directory is unavailable: ${cwd}. Restore that directory, or explicitly use sessionMode: "fresh" without --session, or --config from the selected execution directory. The existing browser was left untouched.`;
52
50
  }
53
- export function restoreManagedSessionCwd(branch, sessionName, namespace, fallback) {
54
- const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
55
- for (const entry of [...branch].reverse()) {
56
- const message = getBrowserResultMessage(entry);
57
- if (!message)
58
- continue;
59
- const details = message.details;
60
- if (!isRecord(details) || typeof details.sessionName !== "string" || typeof details.managedSessionCwd !== "string" || !isAbsolute(details.managedSessionCwd))
61
- continue;
62
- if (getAgentBrowserSessionIdentityKey(details.sessionName, typeof details.namespace === "string" ? details.namespace : undefined) === key)
63
- return details.managedSessionCwd;
64
- }
65
- return fallback;
66
- }
@@ -0,0 +1,46 @@
1
+ import { JsonSchema } from "./json-schema.js";
2
+ import { isRecord } from "./parsing.js";
3
+ import { finalizeAgentBrowserFailure } from "./pi-tool-rendering.js";
4
+ import { isPlainTextInspectionArgs } from "./runtime.js";
5
+ import { projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./results/presentation/content.js";
6
+ export const AGENT_BROWSER_NAMESPACE = {
7
+ name: "browser",
8
+ description: "Browser commands, bounded browser code, advanced UI checks, Electron, and optional web search.",
9
+ instructions: "Use agent_browser for native commands and batch --bail for fixed sequences. Use agent_browser_code for a fresh bounded JS cell with a persistent browser. Enable advanced direct tools with agent_browser_tools before calling them. Check success and nextActions; structured imageObservations are capture metadata, not inline images. Optional web search is available only when configured. Full browser instructions remain in the agent_browser prompt section.",
10
+ };
11
+ export const AGENT_BROWSER_OUTPUT_SCHEMA = JsonSchema.Object({
12
+ success: JsonSchema.Boolean(),
13
+ resultCategory: JsonSchema.Union([JsonSchema.Literal("success"), JsonSchema.Literal("failure")]),
14
+ data: JsonSchema.Optional(JsonSchema.Unsafe({})),
15
+ error: JsonSchema.Optional(JsonSchema.Unsafe({})),
16
+ summary: JsonSchema.Optional(JsonSchema.String()),
17
+ nextActions: JsonSchema.Optional(JsonSchema.Array(JsonSchema.Object({}))),
18
+ }, { additionalProperties: true });
19
+ /** Final public boundary, after execution, persistence, and any requested output export. */
20
+ export async function finalizeAgentBrowserNativeResult(result, input) {
21
+ const finalized = finalizeAgentBrowserFailure(result, input);
22
+ // Native help/version stays plain text; the entrypoint intentionally skips observation rendering for it.
23
+ if (isRecord(input) && Array.isArray(input.args) && isPlainTextInspectionArgs(input.args))
24
+ return finalized;
25
+ const succeeded = finalized.isError !== true;
26
+ // The normal entrypoint supplies the exact bounded projection, including its complete spill.
27
+ if (isRecord(result.structuredContent) && result.structuredContent.success === succeeded)
28
+ return finalized;
29
+ const details = isRecord(result.details) ? result.details : {};
30
+ const observation = projectAgentBrowserObservation(details, succeeded);
31
+ if (JSON.stringify(observation).length > OBSERVATION_INLINE_MAX_CHARS) {
32
+ const { renderAgentBrowserObservation } = await import("./results/presentation/large-output.js");
33
+ const rendered = await renderAgentBrowserObservation({
34
+ content: finalized.content, details, succeeded,
35
+ json: isRecord(input) && ("code" in input || Array.isArray(input.args) && input.args.includes("--json")),
36
+ // The caller's exact visible output stays inline (for example formatted search results); only the structured field is bounded.
37
+ preserveContent: true,
38
+ });
39
+ const { artifactManifest, ...renderedResult } = rendered;
40
+ return finalizeAgentBrowserFailure({
41
+ ...finalized, ...renderedResult,
42
+ details: artifactManifest ? { ...details, artifactManifest } : details,
43
+ }, input);
44
+ }
45
+ return { ...finalized, structuredContent: JSON.parse(JSON.stringify(observation)) };
46
+ }
@@ -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";
@@ -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
  }
@@ -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
  }
@@ -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)) {
@@ -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);
@@ -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
  }