pi-agent-browser-native 0.6.17 → 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.
- package/CHANGELOG.md +13 -0
- package/README.md +55 -167
- package/dist/extensions/agent-browser/index.js +317 -223
- package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
- package/dist/extensions/agent-browser/lib/execution-cwd.js +4 -2
- package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
- package/dist/extensions/agent-browser/lib/input-modes/params.js +146 -142
- package/dist/extensions/agent-browser/lib/input-modes/script.js +64 -92
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +170 -24
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +13 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +26 -13
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +21 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +4 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +1 -3
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +35 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/recording-recovery.js +3 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/screenshot-observation.js +79 -0
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +3 -4
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +19 -52
- package/dist/extensions/agent-browser/lib/orchestration/native-session-defaults.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +3 -3
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +91 -278
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +9 -9
- package/dist/extensions/agent-browser/lib/playbook.js +46 -25
- package/dist/extensions/agent-browser/lib/process-identity.js +22 -6
- package/dist/extensions/agent-browser/lib/process.js +16 -13
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +2 -2
- package/dist/extensions/agent-browser/lib/results/next-actions.js +1 -2
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +25 -4
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +6 -2
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +2 -0
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +39 -0
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +3 -3
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +41 -1
- package/dist/extensions/agent-browser/lib/results/presentation.js +16 -11
- package/dist/extensions/agent-browser/lib/runtime.js +18 -6
- package/dist/extensions/agent-browser/lib/session-page-state.js +3 -4
- package/dist/extensions/agent-browser/lib/tool-surface.js +131 -0
- package/dist/extensions/agent-browser/script-worker.js +4 -1
- package/docs/ARCHITECTURE.md +46 -62
- package/docs/COMMAND_REFERENCE.md +77 -136
- package/docs/ELECTRON.md +51 -49
- package/docs/RELEASE.md +31 -15
- package/docs/REQUIREMENTS.md +18 -14
- package/docs/SUPPORT_MATRIX.md +33 -15
- package/docs/TOOL_CONTRACT.md +216 -405
- package/package.json +1 -1
- 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 {
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
:
|
|
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
|
-
:
|
|
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
|
|
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
|
-
:
|
|
153
|
-
? "
|
|
154
|
-
:
|
|
155
|
-
? "
|
|
156
|
-
:
|
|
157
|
-
? "
|
|
158
|
-
:
|
|
159
|
-
? "
|
|
160
|
-
:
|
|
161
|
-
? "
|
|
162
|
-
:
|
|
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 === "
|
|
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 {
|
|
2
|
-
import { AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES,
|
|
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 {
|
|
5
|
-
import {
|
|
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
|
-
|
|
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
|
|
20
|
-
if ((cleanup !== "active" && cleanup !== "closed" && cleanup !== "failed")
|
|
21
|
-
||
|
|
22
|
-
|| !
|
|
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:
|
|
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
|
-
|
|
39
|
-
const
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|
}
|