pi-agent-browser-native 0.6.16 → 0.7.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 (52) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/README.md +59 -165
  3. package/dist/extensions/agent-browser/index.js +634 -501
  4. package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
  5. package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
  6. package/dist/extensions/agent-browser/lib/execution-cwd.js +66 -0
  7. package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
  8. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
  9. package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
  10. package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
  11. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
  12. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +27 -20
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -2
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
  16. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
  17. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
  18. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +41 -24
  19. package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
  20. package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
  21. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
  22. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
  23. package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
  24. package/dist/extensions/agent-browser/lib/orchestration/operation-paths.js +76 -0
  25. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
  26. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
  27. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
  28. package/dist/extensions/agent-browser/lib/playbook.js +46 -25
  29. package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
  30. package/dist/extensions/agent-browser/lib/process.js +16 -13
  31. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
  32. package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
  33. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
  34. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
  35. package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
  36. package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
  37. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
  38. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
  39. package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
  40. package/dist/extensions/agent-browser/lib/runtime.js +18 -6
  41. package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
  42. package/dist/extensions/agent-browser/lib/tool-surface.js +131 -0
  43. package/dist/extensions/agent-browser/script-worker.js +4 -1
  44. package/docs/ARCHITECTURE.md +47 -61
  45. package/docs/COMMAND_REFERENCE.md +78 -137
  46. package/docs/ELECTRON.md +51 -49
  47. package/docs/RELEASE.md +31 -15
  48. package/docs/REQUIREMENTS.md +18 -14
  49. package/docs/SUPPORT_MATRIX.md +33 -15
  50. package/docs/TOOL_CONTRACT.md +218 -401
  51. package/package.json +4 -4
  52. package/scripts/doctor.mjs +2 -2
@@ -1,13 +1,12 @@
1
- import { open } from "node:fs/promises";
2
- import { AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES, AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES, AGENT_BROWSER_SCRIPT_SPILL_MAX_BYTES, createAgentBrowserScriptCloseArgs, isAgentBrowserScriptSessionName, validateAgentBrowserScriptBrowserParams, } from "../input-modes/script.js";
1
+ import { stat } from "node:fs/promises";
2
+ import { AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES, createAgentBrowserScriptCloseArgs, isAgentBrowserScriptSessionName, } from "../input-modes/script.js";
3
3
  import { isRecord } from "../parsing.js";
4
- import { parseCommandInfo, redactSensitiveText } from "../runtime.js";
5
- import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
4
+ import { redactSensitiveText } from "../runtime.js";
5
+ import { attachInlineImage } from "../results/presentation/artifacts.js";
6
+ import { projectAgentBrowserObservation } from "../results/presentation/content.js";
6
7
  import { redactPresentationData } from "../results/presentation/diagnostics.js";
7
- import { truncateText } from "../results/text.js";
8
8
  const SCRIPT_SESSION_ENTRY_TYPE = "agent-browser-script-session";
9
- const SCRIPT_BROWSER_SUMMARY_MAX_CHARS = 1_024;
10
- const SCRIPT_BROWSER_TEXT_MAX_CHARS = 8_192;
9
+ // Retire outstanding pre-0.7 isolated-session leases when an old transcript resumes.
11
10
  export function getScriptSessionLeasesFromBranch(branch) {
12
11
  const leases = new Map();
13
12
  for (const entry of branch) {
@@ -16,284 +15,98 @@ export function getScriptSessionLeasesFromBranch(branch) {
16
15
  const { cleanup, closeCommandArgs, launchAttempted, sessionName } = entry.data;
17
16
  if (!isAgentBrowserScriptSessionName(sessionName))
18
17
  continue;
19
- const expectedCloseCommandArgs = createAgentBrowserScriptCloseArgs(sessionName);
20
- if ((cleanup !== "active" && cleanup !== "closed" && cleanup !== "failed")
21
- || launchAttempted !== true
22
- || !Array.isArray(closeCommandArgs)
23
- || closeCommandArgs.length !== expectedCloseCommandArgs.length
24
- || !closeCommandArgs.every((token, index) => token === expectedCloseCommandArgs[index]))
18
+ const expected = createAgentBrowserScriptCloseArgs(sessionName);
19
+ if ((cleanup !== "active" && cleanup !== "closed" && cleanup !== "failed") || launchAttempted !== true
20
+ || !Array.isArray(closeCommandArgs) || closeCommandArgs.length !== expected.length
21
+ || !closeCommandArgs.every((token, index) => token === expected[index]))
25
22
  continue;
26
- leases.set(sessionName, { cleanup, closeCommandArgs: expectedCloseCommandArgs, launchAttempted: true, sessionName });
23
+ leases.set(sessionName, { cleanup, closeCommandArgs: expected, launchAttempted: true, sessionName });
27
24
  }
28
25
  return leases;
29
26
  }
30
27
  export function appendScriptSessionLease(pi, sessionName, cleanup) {
31
- pi.appendEntry(SCRIPT_SESSION_ENTRY_TYPE, {
32
- cleanup,
33
- closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName),
34
- launchAttempted: true,
35
- sessionName,
36
- });
28
+ pi.appendEntry(SCRIPT_SESSION_ENTRY_TYPE, { cleanup, closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName), launchAttempted: true, sessionName });
37
29
  }
38
- async function readVerifiedScriptSpill(result) {
39
- const details = isRecord(result.details) ? result.details : undefined;
40
- const path = typeof details?.fullOutputPath === "string" ? details.fullOutputPath : undefined;
41
- const manifest = isSessionArtifactManifest(details?.artifactManifest) ? details.artifactManifest : undefined;
42
- if (!path || !manifest?.entries.some((entry) => entry.kind === "spill"
43
- && (entry.path === path || entry.absolutePath === path)
44
- && (entry.storageScope === "persistent-session" || entry.storageScope === "process-temp")
45
- && (entry.retentionState === "live" || entry.retentionState === "ephemeral")))
46
- return undefined;
47
- let handle;
48
- try {
49
- handle = await open(path, "r");
50
- const stats = await handle.stat();
51
- if (!stats.isFile() || stats.size < 0 || stats.size > AGENT_BROWSER_SCRIPT_SPILL_MAX_BYTES)
52
- return undefined;
53
- const buffer = Buffer.alloc(stats.size);
54
- let offset = 0;
55
- while (offset < buffer.length) {
56
- const { bytesRead } = await handle.read(buffer, offset, buffer.length - offset, offset);
57
- if (bytesRead === 0)
58
- break;
59
- offset += bytesRead;
60
- }
61
- const text = buffer.subarray(0, offset).toString("utf8");
62
- try {
63
- return JSON.parse(text);
64
- }
65
- catch {
66
- return undefined;
67
- }
68
- }
69
- catch {
70
- return undefined;
71
- }
72
- finally {
73
- await handle?.close().catch(() => undefined);
74
- }
75
- }
76
- function getToolResultText(result) {
77
- return result.content
78
- .filter((item) => item.type === "text")
79
- .map((item) => item.text)
80
- .join("\n\n");
81
- }
82
- function stripOwnedScriptSessionArgs(args, sessionName) {
83
- if (!isAgentBrowserScriptSessionName(sessionName))
84
- return args;
85
- if (args[0] === "--namespace" && args[1] === "" && args[2] === "--session" && args[3] === sessionName)
86
- return args.slice(4);
87
- return args[0] === "--session" && args[1] === sessionName ? args.slice(2) : args;
88
- }
89
- function getScriptCompatibleNextActions(value, sessionName) {
90
- if (!Array.isArray(value))
91
- return undefined;
92
- const actions = value.flatMap((action) => {
93
- if (!isRecord(action))
94
- return [];
95
- if (!isRecord(action.params))
96
- return typeof action.artifactPath === "string" ? [action] : [];
97
- if (Object.keys(action.params).some((key) => key !== "args" && key !== "stdin"))
98
- return [];
99
- if (!Array.isArray(action.params.args) || action.params.args.some((token) => typeof token !== "string"))
100
- return [];
101
- if (action.params.stdin !== undefined && typeof action.params.stdin !== "string")
102
- return [];
103
- const normalizedParams = {
104
- args: stripOwnedScriptSessionArgs(action.params.args, sessionName),
105
- ...(typeof action.params.stdin === "string" ? { stdin: action.params.stdin } : {}),
106
- };
107
- if (!validateAgentBrowserScriptBrowserParams(normalizedParams).params)
108
- return [];
109
- return [{ ...action, params: normalizedParams }];
110
- });
111
- return actions.length > 0 ? actions : undefined;
112
- }
113
- function scriptBrowserEnvelopeFitsIpc(envelope) {
114
- try {
115
- return Buffer.byteLength(JSON.stringify({ envelope, id: Number.MAX_SAFE_INTEGER, type: "response" }), "utf8") + 1 <= AGENT_BROWSER_SCRIPT_IPC_MESSAGE_MAX_BYTES;
116
- }
117
- catch {
118
- return false;
119
- }
120
- }
121
- function buildOversizedScriptBrowserEnvelope() {
122
- const text = "Browser result exceeds the script IPC response limit; narrow the inner browser call output.";
123
- return {
124
- data: null,
125
- details: { failureCategory: "upstream-error", resultCategory: "failure" },
126
- error: text,
127
- failureCategory: "upstream-error",
128
- ok: false,
129
- resultCategory: "failure",
130
- summary: text,
131
- text,
132
- };
133
- }
134
- export async function buildScriptBrowserEnvelope(result, args, scriptSessionName) {
135
- const details = isRecord(result.details) ? result.details : undefined;
136
- const fullData = await readVerifiedScriptSpill(result);
137
- const commandInfo = parseCommandInfo(args);
138
- const data = redactPresentationData(commandInfo, fullData ?? details?.data ?? null);
139
- const resultCategory = details?.resultCategory === "failure" || result.isError === true ? "failure" : "success";
140
- const text = truncateText(redactSensitiveText(getToolResultText(result)), SCRIPT_BROWSER_TEXT_MAX_CHARS);
141
- const summary = truncateText(redactSensitiveText(typeof details?.summary === "string" ? details.summary : text.split("\n", 1)[0] || "Browser call completed."), SCRIPT_BROWSER_SUMMARY_MAX_CHARS);
142
- const compatibleNextActions = getScriptCompatibleNextActions(details?.nextActions, scriptSessionName);
143
- const redactedNextActions = getScriptCompatibleNextActions(redactPresentationData(commandInfo, compatibleNextActions), undefined);
144
- const failureCategory = resultCategory === "failure" && typeof details?.failureCategory === "string"
145
- ? details.failureCategory
146
- : undefined;
147
- const successCategory = resultCategory === "success" && typeof details?.successCategory === "string"
148
- ? details.successCategory
149
- : undefined;
150
- const envelopeDetails = redactPresentationData(commandInfo, {
151
- artifactVerification: details?.artifactVerification,
152
- artifacts: details?.artifacts,
153
- failureCategory,
154
- pageChangeSummary: details?.pageChangeSummary,
155
- resultCategory,
156
- successCategory,
157
- });
158
- const envelope = {
159
- data,
160
- details: isRecord(envelopeDetails) ? envelopeDetails : { resultCategory },
161
- error: resultCategory === "failure" ? summary : undefined,
162
- failureCategory,
163
- nextActions: Array.isArray(redactedNextActions) ? redactedNextActions : undefined,
164
- ok: resultCategory === "success",
165
- resultCategory,
166
- successCategory,
167
- summary,
168
- text,
169
- };
170
- return scriptBrowserEnvelopeFitsIpc(envelope) ? envelope : buildOversizedScriptBrowserEnvelope();
171
- }
172
- function collectUniqueArtifacts(results) {
173
- const records = results.flatMap((result) => {
174
- const details = isRecord(result.details) ? result.details : undefined;
175
- return Array.isArray(details?.artifacts) ? details.artifacts.filter(isRecord) : [];
176
- });
177
- const unique = new Map(records.map((record) => [String(record.absolutePath ?? record.path ?? JSON.stringify(record)), record]));
178
- return unique.size > 0 ? [...unique.values()] : undefined;
179
- }
180
- function collectScriptArtifactVerification(results) {
181
- const entries = results.flatMap((result) => {
182
- const details = isRecord(result.details) ? result.details : undefined;
183
- const verification = isRecord(details?.artifactVerification) ? details.artifactVerification : undefined;
184
- return Array.isArray(verification?.artifacts) ? verification.artifacts.filter(isRecord) : [];
185
- });
186
- const unique = [...new Map(entries.map((entry) => [String(entry.absolutePath ?? entry.path ?? JSON.stringify(entry)), entry])).values()];
187
- if (unique.length === 0)
188
- return undefined;
189
- const count = (state) => unique.filter((entry) => entry.state === state).length;
190
- return {
191
- artifacts: unique,
192
- missingCount: count("missing"),
193
- pendingCount: count("pending"),
194
- unverifiedCount: count("unverified"),
195
- verified: unique.every((entry) => entry.state === "verified"),
196
- verifiedCount: count("verified"),
197
- };
198
- }
199
- function getLatestScriptResultDetail(results, key) {
200
- for (let index = results.length - 1; index >= 0; index -= 1) {
201
- const rawDetails = results[index]?.details;
202
- const details = isRecord(rawDetails) ? rawDetails : undefined;
203
- if (details?.[key] !== undefined)
204
- return details[key];
205
- }
206
- return undefined;
207
- }
208
- function buildScriptCloseNextAction(sessionName) {
30
+ export function createBrowserCodeOutput() {
31
+ const images = new Map();
32
+ const selected = new Map();
33
+ const receipts = new Map();
34
+ const fileArtifacts = new Map();
35
+ let selectedBytes = 0;
209
36
  return {
210
- id: "close-script-session-after-cleanup-failure",
211
- params: { args: createAgentBrowserScriptCloseArgs(sessionName) },
212
- reason: "Retry closing the isolated wrapper-owned script session after automatic cleanup failed.",
213
- safety: "Use these exact args; do not add profile, state, restore, namespace, or connection flags.",
214
- tool: "agent_browser",
215
- };
216
- }
217
- export function buildScriptToolResult(options) {
218
- let data;
219
- let serializedData;
220
- let outputError;
221
- try {
222
- data = redactPresentationData({ command: "script" }, options.run.data);
223
- serializedData = data === undefined ? undefined : JSON.stringify(data);
224
- if (data !== undefined && serializedData === undefined)
225
- throw new TypeError("Script output is not JSON-serializable.");
226
- if (serializedData !== undefined && Buffer.byteLength(serializedData, "utf8") > AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES) {
227
- throw new RangeError(`Redacted script output exceeds ${AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES} bytes.`);
228
- }
229
- }
230
- catch {
231
- data = undefined;
232
- serializedData = undefined;
233
- outputError = "Final script output could not be safely rendered as bounded JSON.";
234
- }
235
- const steps = options.run.steps.map((step) => ({ ...step, summary: redactSensitiveText(step.summary) }));
236
- const rejectedFailureCategory = steps.find((step) => step.failureCategory === "policy-blocked")?.failureCategory
237
- ?? (options.run.rejectedCallCount > 0 ? "validation-error" : undefined);
238
- const failureCategory = options.cleanupError
239
- ? "cleanup-failed"
240
- : outputError ? "validation-error" : options.run.failureCategory ?? rejectedFailureCategory;
241
- const failed = failureCategory !== undefined || !options.run.ok;
242
- const failedStepCount = steps.filter((step) => !step.ok).length;
243
- const failedCallCount = Math.max(0, failedStepCount - options.run.rejectedCallCount);
244
- const successfulCallCount = steps.filter((step) => step.ok).length;
245
- const cleanupSuffix = options.sessionName && !options.cleanupError ? " Isolated script session closed." : "";
246
- const summary = `${options.cleanupError
247
- ? `Script completed, but isolated session cleanup failed: ${redactSensitiveText(options.cleanupError)}`
248
- : outputError
249
- ? `Script failed: ${outputError}`
250
- : options.run.ok
251
- ? rejectedFailureCategory
252
- ? `Script failed: ${options.run.rejectedCallCount} browser call${options.run.rejectedCallCount === 1 ? " was" : "s were"} rejected before dispatch.`
253
- : failedCallCount > 0
254
- ? `Script completed (${options.run.callCount} browser call${options.run.callCount === 1 ? "" : "s"}; ${failedCallCount} returned failure for script handling).`
255
- : `Script completed (${options.run.callCount} browser call${options.run.callCount === 1 ? "" : "s"}).`
256
- : `Script failed: ${redactSensitiveText(options.run.error ?? "sandbox execution failed")}`}${cleanupSuffix}`;
257
- const dataText = serializedData === undefined ? "" : `\n\n${serializedData}`;
258
- const cleanupActionText = options.cleanupError && options.sessionName
259
- ? `\n\nNext action: ${JSON.stringify({ args: createAgentBrowserScriptCloseArgs(options.sessionName) })}`
260
- : "";
261
- const artifactManifest = getLatestScriptResultDetail(options.innerResults, "artifactManifest");
262
- const artifactRetentionSummary = getLatestScriptResultDetail(options.innerResults, "artifactRetentionSummary");
263
- const artifacts = collectUniqueArtifacts(options.innerResults);
264
- const artifactVerification = collectScriptArtifactVerification(options.innerResults);
265
- const nextActions = options.cleanupError && options.sessionName ? [buildScriptCloseNextAction(options.sessionName)] : undefined;
266
- return {
267
- content: [{ type: "text", text: `${summary}${dataText}${cleanupActionText}` }],
268
- details: {
269
- artifactManifest,
270
- artifactRetentionSummary,
271
- artifacts,
272
- artifactVerification,
273
- data,
274
- failureCategory,
275
- nextActions,
276
- resultCategory: failed ? "failure" : "success",
277
- scriptRun: {
278
- aborted: options.run.aborted,
279
- callCount: options.run.callCount,
280
- emitCount: options.run.emitCount,
281
- failedCallCount,
282
- preDispatchRejectedCallCount: options.run.rejectedCallCount,
283
- successfulCallCount,
284
- timedOut: options.run.timedOut,
285
- },
286
- scriptSession: options.sessionName ? {
287
- cleanup: options.cleanupError ? "failed" : "closed",
288
- closeCommandArgs: createAgentBrowserScriptCloseArgs(options.sessionName),
289
- ...(options.cleanupError ? { error: redactSensitiveText(options.cleanupError) } : {}),
290
- launchAttempted: true,
291
- sessionName: options.sessionName,
292
- } : undefined,
293
- scriptSteps: steps,
294
- successCategory: failed ? undefined : "completed",
295
- summary,
37
+ async observe(result) {
38
+ const details = isRecord(result.details) ? result.details : {};
39
+ const observation = projectAgentBrowserObservation(details, result.isError !== true);
40
+ for (const entry of observation.artifactVerification?.artifacts ?? [])
41
+ receipts.set(entry.absolutePath ?? entry.path, entry);
42
+ for (const artifact of observation.artifacts ?? [])
43
+ fileArtifacts.set(artifact.absolutePath, artifact);
44
+ if (observation.imageObservations) {
45
+ observation.imageObservations = await Promise.all(observation.imageObservations.map(async (image) => {
46
+ const file = await stat(image.path);
47
+ const id = `image-${images.size + 1}`;
48
+ images.set(id, { observation: image, size: file.size, mtime: file.mtimeMs });
49
+ return { ...image, id };
50
+ }));
51
+ }
52
+ return observation;
53
+ },
54
+ async emitImage(value) {
55
+ const id = isRecord(value) && typeof value.id === "string" ? value.id : undefined;
56
+ const image = id ? images.get(id) : undefined;
57
+ if (!id || !image)
58
+ throw new Error("emitImage expects an imageObservations handle returned by browser() in this code call.");
59
+ if (selected.has(id))
60
+ return;
61
+ if (selected.size >= 8 || selectedBytes + image.size > 20 * 1_024 * 1_024)
62
+ throw new Error("Selected images exceed the code output limit (8 images / 20 MiB). Emit fewer images.");
63
+ const file = await stat(image.observation.path);
64
+ if (!file.isFile() || file.size !== image.size || file.mtimeMs !== image.mtime)
65
+ throw new Error("The selected image changed since capture. Capture it again before emitting it.");
66
+ const presentation = await attachInlineImage({ content: [], summary: "Selected browser image" }, image.observation.path);
67
+ if (!presentation.content.some(item => item.type === "image")) {
68
+ throw new Error(presentation.content.filter(item => item.type === "text").map(item => item.text).join("\n") || "The selected image could not be attached.");
69
+ }
70
+ selected.set(id, presentation.content.filter(item => item.type === "image"));
71
+ selectedBytes += file.size;
72
+ },
73
+ async finish(run, sessionName, namespace) {
74
+ let data;
75
+ let outputError;
76
+ try {
77
+ data = redactPresentationData({ command: "code" }, run.data);
78
+ if (data !== undefined && Buffer.byteLength(JSON.stringify(data), "utf8") > AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES)
79
+ throw new Error("oversized");
80
+ }
81
+ catch {
82
+ data = undefined;
83
+ outputError = "Code output could not be rendered as bounded JSON.";
84
+ }
85
+ const failureCategory = outputError ? "validation-error" : run.failureCategory
86
+ ?? (run.rejectedCallCount > 0 ? "validation-error" : undefined);
87
+ const success = run.ok && failureCategory === undefined;
88
+ const artifacts = [...receipts.values()];
89
+ const count = (state) => artifacts.filter(entry => entry.state === state).length;
90
+ const artifactVerification = artifacts.length ? {
91
+ artifacts, missingCount: count("missing"), pendingCount: count("pending"), unverifiedCount: count("unverified"),
92
+ verifiedCount: count("verified"), verified: artifacts.every(entry => entry.state === "verified"),
93
+ } : undefined;
94
+ const observation = {
95
+ success, resultCategory: success ? "success" : "failure", failureCategory, data,
96
+ error: outputError ?? (run.error ? redactSensitiveText(run.error) : undefined),
97
+ sessionName, namespace,
98
+ codeRun: { callCount: run.callCount, emitCount: run.emitCount, failedCallCount: run.steps.filter(step => !step.ok).length, rejectedCallCount: run.rejectedCallCount, aborted: run.aborted, timedOut: run.timedOut },
99
+ failures: run.failures?.length ? run.failures : undefined,
100
+ artifactVerification,
101
+ artifacts: fileArtifacts.size ? [...fileArtifacts.values()] : undefined,
102
+ imageObservations: [...selected.keys()].map(id => ({ ...images.get(id).observation, id })),
103
+ };
104
+ const summary = success ? `Browser code completed (${run.callCount} calls).` : "Browser code failed.";
105
+ return {
106
+ content: [...selected.values()].flat(),
107
+ details: { ...observation, codeSteps: run.steps, summary },
108
+ isError: !success,
109
+ };
296
110
  },
297
- isError: failed,
298
111
  };
299
112
  }
@@ -1,6 +1,7 @@
1
1
  import { getKeybindings, Text, truncateToWidth } from "@earendil-works/pi-tui";
2
+ import { BROWSER_RESULT_TOOLS } from "./browser-transcript.js";
2
3
  import { compileAgentBrowserElectron } from "./input-modes/electron.js";
3
- import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "./input-modes/job.js";
4
+ import { compileAgentBrowserQaPreset } from "./input-modes/job.js";
4
5
  import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup } from "./input-modes/lookups.js";
5
6
  import { compileAgentBrowserSemanticAction } from "./input-modes/semantic-action.js";
6
7
  import { isRecord } from "./parsing.js";
@@ -95,14 +96,12 @@ function formatVisualTruncationNotice(remainingLines, totalLines, theme, width)
95
96
  return truncateToWidth(notice, Math.max(0, width));
96
97
  }
97
98
  function getStructuredModeInvocation(input) {
98
- if (typeof input.script === "string")
99
- return { mode: "script", rawArgs: [], scriptSource: input.script };
99
+ if (typeof input.code === "string")
100
+ return { mode: "code", rawArgs: [], scriptSource: input.code };
100
101
  if (Array.isArray(input.args))
101
102
  return { rawArgs: input.args.filter((value) => typeof value === "string") };
102
103
  if (input.semanticAction !== undefined)
103
104
  return { mode: "semanticAction", rawArgs: compileAgentBrowserSemanticAction(input.semanticAction).compiled?.args ?? [] };
104
- if (input.job !== undefined)
105
- return { mode: "job", rawArgs: compileAgentBrowserJob(input.job).compiled?.args ?? [] };
106
105
  if (input.qa !== undefined)
107
106
  return { mode: "qa", rawArgs: compileAgentBrowserQaPreset(input.qa).compiled?.args ?? [] };
108
107
  if (input.sourceLookup !== undefined)
@@ -137,9 +136,10 @@ export function formatAgentBrowserRenderCall(args, theme, expanded = false) {
137
136
  const invocationPreview = scriptSource === undefined
138
137
  ? formatInvocationPreview(rawArgs)
139
138
  : formatScriptSourceForDisplay(scriptSource, expanded);
140
- let text = theme.fg("toolTitle", theme.bold("agent_browser"));
139
+ let text = theme.fg("toolTitle", theme.bold(mode === "code" ? "agent_browser_code" : "agent_browser"));
141
140
  if (mode) {
142
- text += ` ${theme.fg("accent", mode)}`;
141
+ if (mode !== "code")
142
+ text += ` ${theme.fg("accent", mode)}`;
143
143
  if (scriptSource !== undefined && expanded) {
144
144
  text += `\n${theme.fg("dim", "Source:")}\n${theme.fg("accent", invocationPreview)}`;
145
145
  }
@@ -220,9 +220,9 @@ function appendModelVisibleFailureCategoryNotice(content, notice) {
220
220
  : item);
221
221
  }
222
222
  export function buildAgentBrowserToolResultPatch(event) {
223
- if (event.toolName !== "agent_browser")
223
+ if (!BROWSER_RESULT_TOOLS.has(event.toolName))
224
224
  return undefined;
225
- const preservesParseableJson = agentBrowserToolResultRequestedJson(event) && agentBrowserToolResultHasParseableJsonContent(event.content);
225
+ const preservesParseableJson = (event.toolName === "agent_browser_code" || agentBrowserToolResultRequestedJson(event)) && agentBrowserToolResultHasParseableJsonContent(event.content);
226
226
  const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(event.details);
227
227
  const content = notice ? appendModelVisibleFailureCategoryNotice(event.content, notice) : undefined;
228
228
  const shouldMarkError = isRecord(event.details) && event.details.resultCategory === "failure" && event.isError !== true;