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
@@ -3,10 +3,9 @@ import { normalizeUrlLessOpen } from "./batch-stdin.js";
3
3
  import { isPlainTextInspectionArgs, validateToolArgs, redactInvocationArgs, redactSensitiveText } from "../runtime.js";
4
4
  import { buildAgentBrowserResultCategoryDetails } from "../results/categories.js";
5
5
  import { compileAgentBrowserElectron } from "../input-modes/electron.js";
6
- import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "../input-modes/job.js";
6
+ import { compileAgentBrowserQaPreset } from "../input-modes/job.js";
7
7
  import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup, redactNetworkSourceLookupArgs, redactNetworkSourceLookupUrl } from "../input-modes/lookups.js";
8
8
  import { compileAgentBrowserSemanticAction } from "../input-modes/semantic-action.js";
9
- import { AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS, compileAgentBrowserScript } from "../input-modes/script.js";
10
9
  function redactCompiledElectron(compiled) {
11
10
  if (!compiled)
12
11
  return undefined;
@@ -68,46 +67,39 @@ function normalizeExplicitEvalStdinArgs(args, stdin) {
68
67
  export function resolveAgentBrowserInput(options) {
69
68
  const { getBatchPreflightValidationError, params } = options;
70
69
  const semanticActionResult = params.semanticAction === undefined ? {} : compileAgentBrowserSemanticAction(params.semanticAction);
71
- const jobResult = params.job === undefined ? {} : compileAgentBrowserJob(params.job);
72
70
  const qaResult = params.qa === undefined ? {} : compileAgentBrowserQaPreset(params.qa);
73
71
  const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
74
72
  const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
75
73
  const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
76
- const scriptResult = params.script === undefined ? {} : compileAgentBrowserScript(params.script);
77
74
  const hasExplicitArgs = Array.isArray(params.args);
78
75
  const explicitInputModes = [
79
76
  hasExplicitArgs,
80
77
  Boolean(semanticActionResult.compiled),
81
- Boolean(jobResult.compiled),
82
78
  Boolean(qaResult.compiled),
83
79
  Boolean(sourceLookupResult.compiled),
84
80
  Boolean(networkSourceLookupResult.compiled),
85
81
  Boolean(electronResult.compiled),
86
- Boolean(scriptResult.compiled),
87
82
  ].filter(Boolean).length;
88
83
  const inputModeError = explicitInputModes !== 1
89
- ? "Provide exactly one of script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
84
+ ? "Provide exactly one of args, semanticAction, qa, sourceLookup, networkSourceLookup, or electron."
90
85
  : undefined;
91
86
  const compiledSemanticAction = semanticActionResult.compiled;
92
87
  const compiledQaPreset = qaResult.compiled;
93
88
  const compiledSourceLookup = sourceLookupResult.compiled;
94
89
  const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
95
90
  const compiledElectron = electronResult.compiled;
96
- const compiledScript = scriptResult.compiled;
97
- const compiledJob = jobResult.compiled ?? compiledQaPreset;
91
+ const compiledJob = compiledQaPreset;
98
92
  const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
99
93
  const normalizedExplicitArgs = normalizeExplicitEvalStdinArgs(params.args ?? [], params.stdin);
100
- const toolArgs = compiledElectron || compiledScript ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
94
+ const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
101
95
  const toolStdin = compiledGeneratedBatch?.stdin ?? normalizedExplicitArgs.stdin;
102
96
  const redactedArgs = redactInvocationArgs(toolArgs);
103
97
  const generatedStdinError = params.stdin !== undefined
104
98
  ? compiledGeneratedBatch
105
- ? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
99
+ ? "Do not provide stdin with qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
106
100
  : compiledElectron
107
101
  ? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
108
- : compiledScript
109
- ? "Do not provide stdin with script; browser call stdin belongs inside browser({ args, stdin })."
110
- : undefined
102
+ : undefined
111
103
  : undefined;
112
104
  const outputPathError = params.outputPath !== undefined && (typeof params.outputPath !== "string" || params.outputPath.trim().length === 0)
113
105
  ? "outputPath must be a non-empty string when provided."
@@ -118,52 +110,40 @@ export function resolveAgentBrowserInput(options) {
118
110
  ? compiledElectron.action === "list"
119
111
  ? "electron.list has no configurable timeout; remove top-level timeoutMs."
120
112
  : "Use electron.timeoutMs for this action; top-level timeoutMs applies only to browser CLI subprocess calls."
121
- : compiledScript && params.timeoutMs !== undefined && params.timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS
122
- ? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
123
- : undefined;
124
- const scriptSessionModeError = compiledScript && params.sessionMode !== undefined
125
- ? "Do not provide sessionMode with script; script always uses its own isolated session."
126
- : undefined;
113
+ : undefined;
127
114
  const attachedQaSessionError = compiledQaPreset?.checks.attached
128
115
  ? params.sessionMode === "fresh"
129
116
  ? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
130
117
  : undefined
131
118
  : undefined;
132
119
  const validationError = semanticActionResult.error
133
- ?? jobResult.error
134
120
  ?? qaResult.error
135
121
  ?? sourceLookupResult.error
136
122
  ?? networkSourceLookupResult.error
137
123
  ?? electronResult.error
138
- ?? scriptResult.error
139
124
  ?? inputModeError
140
125
  ?? generatedStdinError
141
126
  ?? outputPathError
142
127
  ?? timeoutMsError
143
- ?? scriptSessionModeError
144
128
  ?? attachedQaSessionError
145
- ?? (compiledElectron || compiledScript ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
129
+ ?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
146
130
  const redactedCompiledJob = redactCompiledJob(compiledJob);
147
131
  const redactedCompiledSemanticAction = compiledSemanticAction
148
132
  ? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
149
133
  : undefined;
150
134
  const attemptedKind = compiledElectron
151
135
  ? "electron"
152
- : compiledScript
153
- ? "script"
154
- : compiledNetworkSourceLookup
155
- ? "networkSourceLookup"
156
- : compiledSourceLookup
157
- ? "sourceLookup"
158
- : compiledQaPreset
159
- ? "qa"
160
- : jobResult.compiled
161
- ? "job"
162
- : compiledSemanticAction
163
- ? "semanticAction"
164
- : hasExplicitArgs
165
- ? "args"
166
- : undefined;
136
+ : compiledNetworkSourceLookup
137
+ ? "networkSourceLookup"
138
+ : compiledSourceLookup
139
+ ? "sourceLookup"
140
+ : compiledQaPreset
141
+ ? "qa"
142
+ : compiledSemanticAction
143
+ ? "semanticAction"
144
+ : hasExplicitArgs
145
+ ? "args"
146
+ : undefined;
167
147
  const redactedCompiledElectron = redactCompiledElectron(compiledElectron);
168
148
  const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
169
149
  const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
@@ -195,9 +175,6 @@ export function resolveAgentBrowserInput(options) {
195
175
  if (compiledElectron && redactedCompiledElectron) {
196
176
  return { ...resolvedBase, compiledElectron, kind: "electron", redactedCompiledElectron, status: "valid" };
197
177
  }
198
- if (compiledScript) {
199
- return { ...resolvedBase, compiledScript, kind: "script", status: "valid" };
200
- }
201
178
  if (compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookup) {
202
179
  return {
203
180
  ...resolvedBase,
@@ -230,16 +207,6 @@ export function resolveAgentBrowserInput(options) {
230
207
  status: "valid",
231
208
  };
232
209
  }
233
- if (jobResult.compiled && redactedCompiledJob) {
234
- return {
235
- ...resolvedBase,
236
- compiledGeneratedBatch: jobResult.compiled,
237
- compiledJob: jobResult.compiled,
238
- kind: "job",
239
- redactedCompiledJob,
240
- status: "valid",
241
- };
242
- }
243
210
  if (compiledSemanticAction && redactedCompiledSemanticAction) {
244
211
  return { ...resolvedBase, compiledSemanticAction, kind: "semanticAction", redactedCompiledSemanticAction, status: "valid" };
245
212
  }
@@ -75,7 +75,7 @@ async function readNativeIdentity(path, cwd, signal, rootFallback, browserComman
75
75
  export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
76
76
  if (input.kind === "electron" && input.compiledElectron.action === "launch")
77
77
  return withAgentBrowserProcessEnvironment({ AGENT_BROWSER_SESSION: undefined }, () => run(input));
78
- if (input.kind === "script" || input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
78
+ if (input.kind === "electron" || isPlainTextInspectionArgs(input.toolArgs))
79
79
  return run(input);
80
80
  const env = getAgentBrowserProcessEnvironment();
81
81
  const configArg = scanUpstreamGlobalFlagOccurrences(input.toolArgs, "--config")[0];
@@ -127,8 +127,8 @@ export async function withNativeSessionDefaults(input, cwd, signal, run, root) {
127
127
  ...(configPath !== undefined ? { AGENT_BROWSER_CONFIG: resolve(cwd, configPath) } : {}),
128
128
  ...(session !== undefined ? { AGENT_BROWSER_SESSION: session } : {}),
129
129
  ...(namespace !== undefined ? { AGENT_BROWSER_NAMESPACE: namespace } : {}),
130
- }, () => run({ ...input, toolArgs: args, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun) => {
131
- const daemon = await inspectManagedSessionDaemon({ cwd, signal, sessionName: rootDefault,
130
+ }, () => run({ ...input, toolArgs: args, chromeStartupArgs, persistentChromeArgs, configuredChromeLaunch }, !rootDefault ? undefined : async (browserRun, launchSignal = signal) => {
131
+ const daemon = await inspectManagedSessionDaemon({ cwd, signal: launchSignal, sessionName: rootDefault,
132
132
  namespace: scanUpstreamGlobalFlagOccurrences(args, "--namespace").at(-1)?.value ?? namespace, timeoutMs: 5_000 });
133
133
  if (daemon.status === "missing-binary")
134
134
  return {
@@ -138,7 +138,7 @@ export async function applyAgentBrowserOutputPath(options) {
138
138
  return {
139
139
  ...options.result,
140
140
  content: appendOutputFileNotice(options.result, `Output file rejected: ${message}`, true),
141
- details: { ...details, failureCategory: "validation-error", outputFile, resultCategory: "failure" },
141
+ details: { ...details, error: message, summary: "Output file rejected.", failureCategory: "validation-error", outputFile, resultCategory: "failure" },
142
142
  isError: true,
143
143
  };
144
144
  }
@@ -163,9 +163,9 @@ export async function applyAgentBrowserOutputPath(options) {
163
163
  ? (() => {
164
164
  const rest = { ...options.result.details };
165
165
  delete rest.successCategory;
166
- return { ...rest, failureCategory: rest.failureCategory ?? "upstream-error", outputFile, resultCategory: "failure" };
166
+ return { ...rest, error: message, summary: "Output file failed.", failureCategory: rest.failureCategory ?? "upstream-error", outputFile, resultCategory: "failure" };
167
167
  })()
168
- : { failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
168
+ : { error: message, summary: "Output file failed.", failureCategory: "upstream-error", outputFile, resultCategory: "failure" };
169
169
  return {
170
170
  ...options.result,
171
171
  content: appendOutputFileNotice(options.result, `Output file failed: ${requestedPath} (${message}).`, true),
@@ -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
  }