pi-agent-browser-native 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +24 -15
- package/dist/extensions/agent-browser/index.js +496 -343
- package/dist/extensions/agent-browser/lib/browser-journal.js +439 -0
- package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
- package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
- package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
- package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
- package/dist/extensions/agent-browser/lib/native-output.js +46 -0
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +30 -16
- package/dist/extensions/agent-browser/lib/playbook.js +9 -2
- package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -26
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +17 -9
- package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
- package/dist/extensions/agent-browser/lib/temp.js +14 -6
- package/dist/extensions/agent-browser/lib/tool-surface.js +50 -33
- package/dist/extensions/agent-browser/lib/web-search.js +85 -72
- package/docs/ARCHITECTURE.md +25 -25
- package/docs/COMMAND_REFERENCE.md +13 -9
- package/docs/RELEASE.md +25 -19
- package/docs/REQUIREMENTS.md +6 -6
- package/docs/SESSION_STORAGE.md +60 -0
- package/docs/SUPPORT_MATRIX.md +16 -11
- package/docs/TOOL_CONTRACT.md +26 -19
- package/package.json +9 -5
- package/scripts/build.mjs +4 -3
- package/scripts/convert-browser-session.mjs +22 -0
- package/scripts/doctor.mjs +2 -2
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isRecord } from "../parsing.js";
|
|
2
2
|
import { summarizeNetworkFailures } from "../results/network.js";
|
|
3
|
+
import { truncateText } from "../results/text.js";
|
|
3
4
|
import { getBatchResultItems, getCommandNameFromBatchItem } from "./shared.js";
|
|
4
5
|
import { AGENT_BROWSER_QA_LOAD_STATES, } from "./types.js";
|
|
5
6
|
function describeQaChecksRun(checks) {
|
|
@@ -61,15 +62,19 @@ export function buildQaCompactPassText(options) {
|
|
|
61
62
|
return lines.join("\n");
|
|
62
63
|
}
|
|
63
64
|
export function buildQaCompactFailureText(options) {
|
|
64
|
-
const lines = [options.
|
|
65
|
+
const lines = [options.causalError
|
|
66
|
+
? truncateText(options.causalError.replace(/\s+/g, " ").trim(), 700)
|
|
67
|
+
: options.qaPreset.summary];
|
|
65
68
|
const pageParts = [options.page?.title, options.page?.url].filter((part) => typeof part === "string" && part.length > 0);
|
|
66
69
|
if (pageParts.length > 0)
|
|
67
70
|
lines.push(`Page: ${pageParts.join(" — ")}`);
|
|
68
71
|
if (options.qaPreset.failedChecks.length > 0)
|
|
69
72
|
lines.push("Failed checks:", ...options.qaPreset.failedChecks.map((failure) => `- ${failure}`));
|
|
73
|
+
if (options.qaPreset.notRunChecks.length > 0)
|
|
74
|
+
lines.push("Not run:", ...options.qaPreset.notRunChecks.map((check) => `- ${check}`));
|
|
70
75
|
if (options.qaPreset.warnings.length > 0)
|
|
71
76
|
lines.push("Warnings:", ...options.qaPreset.warnings.map((warning) => `- ${warning}`));
|
|
72
|
-
lines.push(`
|
|
77
|
+
lines.push(`Execution: ${options.executedStepCount ?? "unknown"}/${options.plannedStepCount} batch steps`);
|
|
73
78
|
lines.push("Full diagnostic matrix: see details.qaPreset and details.batchSteps.");
|
|
74
79
|
return lines.join("\n");
|
|
75
80
|
}
|
|
@@ -182,11 +187,45 @@ export function analyzeQaPresetTimeout(compiled) {
|
|
|
182
187
|
const failedChecks = compiled.checks.expectedText.map((text) => `expected text was not verified before timeout: ${formatQaExpectedTextPreview(text)}`);
|
|
183
188
|
return {
|
|
184
189
|
failedChecks,
|
|
190
|
+
notRunChecks: [],
|
|
185
191
|
passed: false,
|
|
186
192
|
summary: `QA preset failed: ${failedChecks.join("; ")}.`,
|
|
187
193
|
warnings: ["The wrapper timed out before expected-text evidence could be verified; inspect timeoutPartialProgress and retry with a narrower readiness condition if the page was still loading."],
|
|
188
194
|
};
|
|
189
195
|
}
|
|
196
|
+
function describeNotRunQaChecks(compiled, executedStepCount) {
|
|
197
|
+
const checks = [];
|
|
198
|
+
let expectedTextIndex = 0;
|
|
199
|
+
for (const [index, step] of compiled.steps.entries()) {
|
|
200
|
+
let check;
|
|
201
|
+
if (step.action === "assertText") {
|
|
202
|
+
const expected = compiled.checks.expectedText[expectedTextIndex++];
|
|
203
|
+
if (expected)
|
|
204
|
+
check = `expected text: ${formatQaExpectedTextPreview(expected)}`;
|
|
205
|
+
}
|
|
206
|
+
else if (step.args[0] === "wait" && step.args[1] === "--load") {
|
|
207
|
+
check = `load state: ${step.args[2]}`;
|
|
208
|
+
}
|
|
209
|
+
else if (compiled.checks.expectedSelector && step.args[0] === "wait" && step.args[1] === compiled.checks.expectedSelector) {
|
|
210
|
+
check = `expected selector: ${formatQaExpectedTextPreview(compiled.checks.expectedSelector)}`;
|
|
211
|
+
}
|
|
212
|
+
else if (step.args[0] === "network" && !step.args.includes("--clear")) {
|
|
213
|
+
check = "network diagnostics";
|
|
214
|
+
}
|
|
215
|
+
else if (step.args[0] === "console" && !step.args.includes("--clear")) {
|
|
216
|
+
check = "console diagnostics";
|
|
217
|
+
}
|
|
218
|
+
else if (step.args[0] === "errors" && !step.args.includes("--clear") && step.generatedFrom !== "qa.errorBaselineAfterClear") {
|
|
219
|
+
check = "page error diagnostics";
|
|
220
|
+
}
|
|
221
|
+
else if (step.action === "screenshot") {
|
|
222
|
+
check = `screenshot: ${formatQaExpectedTextPreview(step.args[1] ?? "")}`;
|
|
223
|
+
}
|
|
224
|
+
if (index >= executedStepCount && check)
|
|
225
|
+
checks.push(check);
|
|
226
|
+
}
|
|
227
|
+
return checks;
|
|
228
|
+
}
|
|
190
229
|
export function analyzeQaPresetResults(data, compiled) {
|
|
191
230
|
const items = getBatchResultItems(data);
|
|
192
231
|
if (items.length === 0)
|
|
@@ -238,6 +277,8 @@ export function analyzeQaPresetResults(data, compiled) {
|
|
|
238
277
|
const expected = compiled.checks.expectedText[expectedTextIndex++];
|
|
239
278
|
if (!expected)
|
|
240
279
|
return;
|
|
280
|
+
if (!items[index])
|
|
281
|
+
return;
|
|
241
282
|
const visibleTextPassed = qaVisibleTextWaitPassed(items[index], step);
|
|
242
283
|
if (visibleTextPassed === true)
|
|
243
284
|
return;
|
|
@@ -246,10 +287,16 @@ export function analyzeQaPresetResults(data, compiled) {
|
|
|
246
287
|
failedChecks.push(`expected text not found: ${formatQaExpectedTextPreview(expected)}`);
|
|
247
288
|
});
|
|
248
289
|
}
|
|
290
|
+
const stoppedEarly = compiled && items.length < compiled.steps.length;
|
|
291
|
+
const failedFast = stoppedEarly && items.at(-1)?.success === false;
|
|
292
|
+
if (stoppedEarly && !failedFast)
|
|
293
|
+
failedChecks.push("QA execution could not be verified (incomplete batch results)");
|
|
249
294
|
const uniqueFailures = [...new Set(failedChecks)];
|
|
250
295
|
const uniqueWarnings = [...new Set(warnings)];
|
|
296
|
+
const notRunChecks = compiled && failedFast ? describeNotRunQaChecks(compiled, items.length) : [];
|
|
251
297
|
return {
|
|
252
298
|
failedChecks: uniqueFailures,
|
|
299
|
+
notRunChecks,
|
|
253
300
|
passed: uniqueFailures.length === 0,
|
|
254
301
|
summary: uniqueFailures.length === 0
|
|
255
302
|
? uniqueWarnings.length === 0 ? "QA preset passed." : `QA preset passed with warnings: ${uniqueWarnings.join("; ")}.`
|
|
@@ -79,41 +79,38 @@ export function createAgentBrowserQaParamsSchema(Type = JsonSchema, StringEnum =
|
|
|
79
79
|
export function createAgentBrowserElectronParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
|
|
80
80
|
const common = { outputPath: outputProperties(Type).outputPath };
|
|
81
81
|
const timeoutMs = Type.Optional(Type.Integer({ minimum: 1 }));
|
|
82
|
-
|
|
83
|
-
Type.
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
Type.
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
...common,
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
action: StringEnum(["probe"]),
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
...common,
|
|
115
|
-
}, { additionalProperties: false }),
|
|
116
|
-
]));
|
|
82
|
+
const list = {
|
|
83
|
+
query: Type.Optional(Type.String({ description: "Case-insensitive app filter.", minLength: 1 })),
|
|
84
|
+
maxResults: Type.Optional(Type.Integer({ description: `Result cap; default ${ELECTRON_DISCOVERY_DEFAULT_MAX_RESULTS}, clamped to ${ELECTRON_DISCOVERY_MAX_RESULTS}.`, minimum: 1 })),
|
|
85
|
+
};
|
|
86
|
+
const launch = {
|
|
87
|
+
appPath: Type.Optional(Type.String({ description: "macOS .app path.", minLength: 1 })),
|
|
88
|
+
appName: Type.Optional(Type.String({ description: "Name from agent_browser_electron list.", minLength: 1 })),
|
|
89
|
+
bundleId: Type.Optional(Type.String({ minLength: 1 })),
|
|
90
|
+
executablePath: Type.Optional(Type.String({ minLength: 1 })),
|
|
91
|
+
appArgs: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
92
|
+
handoff: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_HANDOFFS)),
|
|
93
|
+
targetType: Type.Optional(StringEnum(AGENT_BROWSER_ELECTRON_TARGET_TYPES)),
|
|
94
|
+
timeoutMs,
|
|
95
|
+
allow: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
96
|
+
deny: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
97
|
+
};
|
|
98
|
+
const probe = { launchId: Type.Optional(Type.String({ minLength: 1 })), timeoutMs };
|
|
99
|
+
const lifecycle = { ...probe, all: Type.Optional(Type.Literal(true)) };
|
|
100
|
+
// Strict providers need an object root; Pi needs root properties to normalize optional nulls.
|
|
101
|
+
// Each branch retains its original action-specific field boundary.
|
|
102
|
+
return Type.Unsafe(Type.Object({
|
|
103
|
+
action: StringEnum(["list", "launch", "status", "cleanup", "probe"]),
|
|
104
|
+
...list, ...launch, ...lifecycle, ...common,
|
|
105
|
+
}, {
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
anyOf: [
|
|
108
|
+
Type.Object({ action: StringEnum(["list"]), ...list, ...common }, { additionalProperties: false }),
|
|
109
|
+
Type.Object({ action: StringEnum(["launch"]), ...launch, ...common }, { additionalProperties: false }),
|
|
110
|
+
Type.Object({ action: StringEnum(["status", "cleanup"]), ...lifecycle, ...common }, { additionalProperties: false, not: { required: ["launchId", "all"] } }),
|
|
111
|
+
Type.Object({ action: StringEnum(["probe"]), ...probe, ...common }, { additionalProperties: false }),
|
|
112
|
+
],
|
|
113
|
+
}));
|
|
117
114
|
}
|
|
118
115
|
export function createAgentBrowserSourceParamsSchema(Type = JsonSchema, StringEnum = localStringEnum) {
|
|
119
116
|
return Type.Unsafe(Type.Object({
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { createHash, randomInt, randomUUID } from "node:crypto";
|
|
3
3
|
import { lstat, mkdir, readFile, readdir, realpath, rename, rm, writeFile } from "node:fs/promises";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { basename, dirname, join } from "node:path";
|
|
@@ -11,6 +11,9 @@ const POLICY_LOCK_RETRY_MS = 10;
|
|
|
11
11
|
const POLICY_LOCK_MAX_BYTES = 1_048_576; // One claim may cover an entire owned-browser cleanup set.
|
|
12
12
|
const LOCK_OWNER_FILE = "owner.json";
|
|
13
13
|
const LOCK_TICKET_FILE = "ticket.json";
|
|
14
|
+
const STALE_CLAIM_MIN_AGE_MS = 30 * 60 * 1_000;
|
|
15
|
+
const POLICY_GC_BATCH_SIZE = 8;
|
|
16
|
+
let lastPolicyGcName;
|
|
14
17
|
function getCoordinationDirectory(platform = process.platform) {
|
|
15
18
|
if (platform !== "win32") {
|
|
16
19
|
const uid = typeof process.getuid === "function" ? process.getuid() : undefined;
|
|
@@ -175,6 +178,7 @@ function parseOwner(content) {
|
|
|
175
178
|
&& Number.isSafeInteger(parsed.pid) && (parsed.pid ?? 0) > 0
|
|
176
179
|
&& typeof parsed.startIdentity === "string" && parsed.startIdentity.length > 0
|
|
177
180
|
&& typeof parsed.token === "string" && parsed.token.length > 0
|
|
181
|
+
&& !parsed.token.includes("/") && !parsed.token.includes("\\") && !parsed.token.includes("\0")
|
|
178
182
|
? parsed
|
|
179
183
|
: undefined;
|
|
180
184
|
}
|
|
@@ -323,14 +327,30 @@ async function cleanDeadPolicyArtifacts(directory, budget) {
|
|
|
323
327
|
catch {
|
|
324
328
|
return;
|
|
325
329
|
}
|
|
326
|
-
|
|
327
|
-
|
|
330
|
+
// ponytail: list/sort all names; stream directories if enumeration becomes the bottleneck.
|
|
331
|
+
const candidates = names.filter((name) => name.startsWith(".pi-agent-browser-policy-remove-")
|
|
332
|
+
|| /^\.pi-agent-browser-policy-[a-f0-9]{64}\.lock-v4\.(?:candidate|claim)-/.test(name)).sort();
|
|
333
|
+
if (candidates.length === 0)
|
|
334
|
+
return;
|
|
335
|
+
const start = lastPolicyGcName === undefined ? randomInt(candidates.length)
|
|
336
|
+
: Math.max(0, candidates.findIndex((name) => name > lastPolicyGcName));
|
|
337
|
+
const batch = Array.from({ length: Math.min(POLICY_GC_BATCH_SIZE, candidates.length) }, (_, offset) => candidates[(start + offset) % candidates.length]);
|
|
338
|
+
lastPolicyGcName = batch.at(-1);
|
|
339
|
+
for (const name of batch) {
|
|
328
340
|
if (budget.signal?.aborted || Date.now() >= (budget.deadline ?? Infinity))
|
|
329
341
|
return;
|
|
330
342
|
const path = join(directory, name);
|
|
343
|
+
const published = name.includes(".lock-v4.claim-");
|
|
344
|
+
if (published) {
|
|
345
|
+
const entry = await lstat(path).catch(() => undefined);
|
|
346
|
+
if (!entry || entry.mtimeMs >= Date.now() - STALE_CLAIM_MIN_AGE_MS)
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
331
349
|
const claim = await readClaim(path);
|
|
332
|
-
if (claim &&
|
|
333
|
-
|
|
350
|
+
if (!claim || (published && !name.endsWith(`.claim-${claim.owner.token}`)))
|
|
351
|
+
continue;
|
|
352
|
+
if (await ownerAlive(claim.owner, budget) === false)
|
|
353
|
+
await removeClaimOwnedBy(path, claim.owner.token).catch(() => false);
|
|
334
354
|
}
|
|
335
355
|
}
|
|
336
356
|
function claimPrecedes(left, right) {
|
|
@@ -4,6 +4,7 @@ import { canonicalizeAgentBrowserNamespace, extractExplicitNamespace, extractExp
|
|
|
4
4
|
import { hasLaunchScopedFlagToken, MANAGED_RESTORE_INCOMPATIBLE_BOOLEAN_ENVS, MANAGED_RESTORE_INCOMPATIBLE_ENVS, MANAGED_RESTORE_INCOMPATIBLE_FLAGS, } from "./launch-scoped-flags.js";
|
|
5
5
|
import { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope, getManagedSessionRestoreProtectedStorageEnv, hasManagedSessionRestoreProjectIdentity, resolveManagedSessionRestoreHome, } from "./managed-session-storage.js";
|
|
6
6
|
import { parseUserBatchStdin } from "./orchestration/batch-stdin.js";
|
|
7
|
+
import { isRecord } from "./parsing.js";
|
|
7
8
|
import { getAgentBrowserProcessEnvironment } from "./process-environment.js";
|
|
8
9
|
export { createManagedSessionRestoreKey, ensureManagedSessionRestoreStorageIsSecure, getManagedSessionRestoreScope } from "./managed-session-storage.js";
|
|
9
10
|
export { pruneOwnedManagedSessionRestoreSnapshots } from "./managed-session-snapshots.js";
|
|
@@ -39,10 +40,11 @@ export class ManagedSessionRestoreState {
|
|
|
39
40
|
this.#disabled.add(getAgentBrowserSessionIdentityKey(sessionName, namespace));
|
|
40
41
|
}
|
|
41
42
|
getDaemonRestoreKey(sessionName, namespace) {
|
|
42
|
-
return typeof sessionName === "string" ? this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace)) : undefined;
|
|
43
|
+
return typeof sessionName === "string" ? this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace))?.restoreKey : undefined;
|
|
43
44
|
}
|
|
44
45
|
hasDaemonRestoreKey(sessionName, namespace) {
|
|
45
|
-
return typeof sessionName === "string" && this.#daemonRestoreKeys.
|
|
46
|
+
return typeof sessionName === "string" && this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace))?.restored !== true
|
|
47
|
+
&& this.#daemonRestoreKeys.has(getAgentBrowserSessionIdentityKey(sessionName, namespace));
|
|
46
48
|
}
|
|
47
49
|
forgetDaemonRestoreKey(sessionName, namespace) {
|
|
48
50
|
if (sessionName)
|
|
@@ -51,9 +53,22 @@ export class ManagedSessionRestoreState {
|
|
|
51
53
|
isDisabled(sessionName, namespace) {
|
|
52
54
|
return typeof sessionName === "string" && this.#disabled.has(getAgentBrowserSessionIdentityKey(sessionName, namespace));
|
|
53
55
|
}
|
|
54
|
-
recordDaemonRestoreKey(sessionName, namespace, restoreKey) {
|
|
55
|
-
if (sessionName)
|
|
56
|
-
|
|
56
|
+
recordDaemonRestoreKey(sessionName, namespace, restoreKey, generation) {
|
|
57
|
+
if (sessionName) {
|
|
58
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
59
|
+
const previous = this.#daemonRestoreKeys.get(key);
|
|
60
|
+
this.#daemonRestoreKeys.set(key, { restoreKey, generation: generation ?? (previous?.restoreKey === restoreKey ? previous.generation : undefined) });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
getDaemonReceipt(sessionName, namespace) {
|
|
64
|
+
const receipt = sessionName ? this.#daemonRestoreKeys.get(getAgentBrowserSessionIdentityKey(sessionName, namespace)) : undefined;
|
|
65
|
+
return receipt?.generation ? { restoreKey: receipt.restoreKey, generation: receipt.generation } : undefined;
|
|
66
|
+
}
|
|
67
|
+
restoreDaemonReceipt(sessionName, namespace, receipt) {
|
|
68
|
+
if (isRecord(receipt) && typeof receipt.generation === "string" && receipt.generation.length > 0
|
|
69
|
+
&& (receipt.restoreKey === null || typeof receipt.restoreKey === "string")) {
|
|
70
|
+
this.#daemonRestoreKeys.set(getAgentBrowserSessionIdentityKey(sessionName, namespace), { restoreKey: receipt.restoreKey, generation: receipt.generation, restored: true });
|
|
71
|
+
}
|
|
57
72
|
}
|
|
58
73
|
replace(identities = [], options = {}) {
|
|
59
74
|
if (!options.preserveDaemonRestoreKeys)
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { JsonSchema } from "./json-schema.js";
|
|
2
|
+
import { isRecord } from "./parsing.js";
|
|
3
|
+
import { finalizeAgentBrowserFailure } from "./pi-tool-rendering.js";
|
|
4
|
+
import { isPlainTextInspectionArgs } from "./runtime.js";
|
|
5
|
+
import { projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./results/presentation/content.js";
|
|
6
|
+
export const AGENT_BROWSER_NAMESPACE = {
|
|
7
|
+
name: "browser",
|
|
8
|
+
description: "Browser commands, bounded browser code, advanced UI checks, Electron, and optional web search.",
|
|
9
|
+
instructions: "Use agent_browser for native commands and batch --bail for fixed sequences. Use agent_browser_code for a fresh bounded JS cell with a persistent browser. Enable advanced direct tools with agent_browser_tools before calling them. Check success and nextActions; structured imageObservations are capture metadata, not inline images. Optional web search is available only when configured. Full browser instructions remain in the agent_browser prompt section.",
|
|
10
|
+
};
|
|
11
|
+
export const AGENT_BROWSER_OUTPUT_SCHEMA = JsonSchema.Object({
|
|
12
|
+
success: JsonSchema.Boolean(),
|
|
13
|
+
resultCategory: JsonSchema.Union([JsonSchema.Literal("success"), JsonSchema.Literal("failure")]),
|
|
14
|
+
data: JsonSchema.Optional(JsonSchema.Unsafe({})),
|
|
15
|
+
error: JsonSchema.Optional(JsonSchema.Unsafe({})),
|
|
16
|
+
summary: JsonSchema.Optional(JsonSchema.String()),
|
|
17
|
+
nextActions: JsonSchema.Optional(JsonSchema.Array(JsonSchema.Object({}))),
|
|
18
|
+
}, { additionalProperties: true });
|
|
19
|
+
/** Final public boundary, after execution, persistence, and any requested output export. */
|
|
20
|
+
export async function finalizeAgentBrowserNativeResult(result, input) {
|
|
21
|
+
const finalized = finalizeAgentBrowserFailure(result, input);
|
|
22
|
+
// Native help/version stays plain text; the entrypoint intentionally skips observation rendering for it.
|
|
23
|
+
if (isRecord(input) && Array.isArray(input.args) && isPlainTextInspectionArgs(input.args))
|
|
24
|
+
return finalized;
|
|
25
|
+
const succeeded = finalized.isError !== true;
|
|
26
|
+
// The normal entrypoint supplies the exact bounded projection, including its complete spill.
|
|
27
|
+
if (isRecord(result.structuredContent) && result.structuredContent.success === succeeded)
|
|
28
|
+
return finalized;
|
|
29
|
+
const details = isRecord(result.details) ? result.details : {};
|
|
30
|
+
const observation = projectAgentBrowserObservation(details, succeeded);
|
|
31
|
+
if (JSON.stringify(observation).length > OBSERVATION_INLINE_MAX_CHARS) {
|
|
32
|
+
const { renderAgentBrowserObservation } = await import("./results/presentation/large-output.js");
|
|
33
|
+
const rendered = await renderAgentBrowserObservation({
|
|
34
|
+
content: finalized.content, details, succeeded,
|
|
35
|
+
json: isRecord(input) && ("code" in input || Array.isArray(input.args) && input.args.includes("--json")),
|
|
36
|
+
// The caller's exact visible output stays inline (for example formatted search results); only the structured field is bounded.
|
|
37
|
+
preserveContent: true,
|
|
38
|
+
});
|
|
39
|
+
const { artifactManifest, ...renderedResult } = rendered;
|
|
40
|
+
return finalizeAgentBrowserFailure({
|
|
41
|
+
...finalized, ...renderedResult,
|
|
42
|
+
details: artifactManifest ? { ...details, artifactManifest } : details,
|
|
43
|
+
}, input);
|
|
44
|
+
}
|
|
45
|
+
return { ...finalized, structuredContent: JSON.parse(JSON.stringify(observation)) };
|
|
46
|
+
}
|
package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js
CHANGED
|
@@ -5,7 +5,8 @@ import { acquireManagedSessionPolicyLock } from "../../managed-session-policy-lo
|
|
|
5
5
|
import { pruneOwnedManagedSessionRestoreSnapshots, resolveExplicitAutosaveInterval, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
|
|
6
6
|
import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
|
|
7
7
|
import { isRecord } from "../../parsing.js";
|
|
8
|
-
import {
|
|
8
|
+
import { readProcessStartIdentity } from "../../process-identity.js";
|
|
9
|
+
import { getAgentBrowserProcessEnvironment, withAgentBrowserProcessEnvironment } from "../../process-environment.js";
|
|
9
10
|
import { runAgentBrowserProcess, withAttachedBrowserSessionContext } from "../../process.js";
|
|
10
11
|
import { getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
11
12
|
import { redactInvocationArgs } from "../../runtime.js";
|
|
@@ -43,9 +44,16 @@ export async function inspectManagedSessionDaemon(options) {
|
|
|
43
44
|
return { status: "inactive" };
|
|
44
45
|
if (!isRecord(data.runtime))
|
|
45
46
|
return { status: "unknown" };
|
|
47
|
+
let generation;
|
|
48
|
+
if (options.includeGeneration && Number.isSafeInteger(data.runtime.backgroundPid) && Number(data.runtime.backgroundPid) > 0
|
|
49
|
+
&& typeof data.runtime.socketDir === "string" && data.runtime.browserLaunched !== false) {
|
|
50
|
+
const start = await readProcessStartIdentity(Number(data.runtime.backgroundPid), process.platform, { signal: options.signal, deadline: Date.now() + (options.timeoutMs ?? 5_000) });
|
|
51
|
+
if (start)
|
|
52
|
+
generation = JSON.stringify({ pid: data.runtime.backgroundPid, start, socketDir: data.runtime.socketDir });
|
|
53
|
+
}
|
|
46
54
|
if (typeof data.runtime.restoreKey === "string" && data.runtime.restoreKey.length > 0)
|
|
47
|
-
return { restoreKey: data.runtime.restoreKey, status: "active" };
|
|
48
|
-
return data.runtime.restoreKey === null ? { restoreKey: null, status: "active" } : { status: "unknown" };
|
|
55
|
+
return { restoreKey: data.runtime.restoreKey, status: "active", generation };
|
|
56
|
+
return data.runtime.restoreKey === null ? { restoreKey: null, status: "active", generation } : { status: "unknown" };
|
|
49
57
|
}
|
|
50
58
|
finally {
|
|
51
59
|
if (processResult.stdoutSpillPath)
|
|
@@ -86,8 +94,10 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
86
94
|
};
|
|
87
95
|
}
|
|
88
96
|
try {
|
|
97
|
+
const receipt = context.restoreState.getDaemonReceipt(context.sessionName, context.namespace);
|
|
89
98
|
const daemon = await inspectManagedSessionDaemon({
|
|
90
99
|
cwd: context.cwd,
|
|
100
|
+
includeGeneration: receipt !== undefined && !context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace),
|
|
91
101
|
headedManagedAutosaveInterval: context.headedManagedAutosaveInterval,
|
|
92
102
|
namespace: context.namespace,
|
|
93
103
|
sessionName: context.sessionName,
|
|
@@ -95,6 +105,13 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
95
105
|
});
|
|
96
106
|
if (daemon.status === "inactive")
|
|
97
107
|
context.restoreState.forgetDaemonRestoreKey(context.sessionName, context.namespace);
|
|
108
|
+
if (daemon.status === "active" && receipt && !context.restoreState.hasDaemonRestoreKey(context.sessionName, context.namespace)) {
|
|
109
|
+
if (daemon.generation === receipt.generation && daemon.restoreKey === receipt.restoreKey) {
|
|
110
|
+
context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey, daemon.generation);
|
|
111
|
+
}
|
|
112
|
+
else
|
|
113
|
+
context.restoreState.forgetDaemonRestoreKey(context.sessionName, context.namespace);
|
|
114
|
+
}
|
|
98
115
|
if (options.mode === "close") {
|
|
99
116
|
if (daemon.status === "active")
|
|
100
117
|
context.restoreState.recordDaemonRestoreKey(context.sessionName, context.namespace, daemon.restoreKey);
|
|
@@ -141,87 +158,89 @@ export async function acquireOwnedManagedSessionDaemonPolicy(options) {
|
|
|
141
158
|
}
|
|
142
159
|
}
|
|
143
160
|
export async function closeManagedSession(options) {
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
sessionName: options.sessionName,
|
|
157
|
-
signal: controller.signal,
|
|
158
|
-
timeoutMs: Math.min(options.timeoutMs, 1_000),
|
|
159
|
-
});
|
|
160
|
-
if (!policyLock) {
|
|
161
|
-
clearTimeout(timer);
|
|
162
|
-
return timeoutError ?? "Managed-session policy coordination is unavailable or busy; cleanup did not run. Retry after the current operation finishes or repair the private policy-lock directory.";
|
|
163
|
-
}
|
|
164
|
-
try {
|
|
165
|
-
phase = "daemon inspection";
|
|
166
|
-
phaseStartedAt = Date.now();
|
|
167
|
-
const daemon = await inspectManagedSessionDaemon({
|
|
168
|
-
cwd: options.cwd,
|
|
169
|
-
headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
|
|
161
|
+
return withAgentBrowserProcessEnvironment(options.socketDir ? { PI_AGENT_BROWSER_SOCKET_DIR: options.socketDir } : {}, async () => {
|
|
162
|
+
const controller = new AbortController();
|
|
163
|
+
let phase = "policy coordination";
|
|
164
|
+
let phaseStartedAt = Date.now();
|
|
165
|
+
let timeoutError;
|
|
166
|
+
const timer = setTimeout(() => {
|
|
167
|
+
timeoutError = `Managed-session cleanup timed out after ${options.timeoutMs} ms during ${phase} (${Date.now() - phaseStartedAt} ms in this phase).`;
|
|
168
|
+
controller.abort();
|
|
169
|
+
}, options.timeoutMs);
|
|
170
|
+
let stdoutSpillPath;
|
|
171
|
+
const closeArgs = [...(options.namespace !== undefined ? ["--namespace", options.namespace] : []), "--session", options.sessionName, "close"];
|
|
172
|
+
const policyLock = options.policyLock ?? await acquireManagedSessionPolicyLock({
|
|
170
173
|
namespace: options.namespace,
|
|
171
|
-
preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
|
|
172
174
|
sessionName: options.sessionName,
|
|
173
175
|
signal: controller.signal,
|
|
174
|
-
timeoutMs: Math.min(options.timeoutMs,
|
|
175
|
-
});
|
|
176
|
-
if (daemon.status === "active")
|
|
177
|
-
options.restoreState.recordDaemonRestoreKey(options.sessionName, options.namespace, daemon.restoreKey);
|
|
178
|
-
const daemonRestoreKey = options.restoreState.getDaemonRestoreKey(options.sessionName, options.namespace);
|
|
179
|
-
const ownedRestoreKey = !options.restoreState.isDisabled(options.sessionName, options.namespace)
|
|
180
|
-
&& isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
|
|
181
|
-
phase = "native close";
|
|
182
|
-
phaseStartedAt = Date.now();
|
|
183
|
-
const processResult = await runAgentBrowserProcess({
|
|
184
|
-
args: closeArgs,
|
|
185
|
-
cwd: options.cwd,
|
|
186
|
-
env: { AGENT_BROWSER_JSON: "1", ...getHeadedManagedAutosaveEnv(options.headedManagedAutosaveInterval) },
|
|
187
|
-
managedSessionRestoreState: options.restoreState,
|
|
188
|
-
ownedManagedSession: true,
|
|
189
|
-
preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
|
|
190
|
-
signal: controller.signal,
|
|
176
|
+
timeoutMs: Math.min(options.timeoutMs, 1_000),
|
|
191
177
|
});
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
178
|
+
if (!policyLock) {
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
return timeoutError ?? "Managed-session policy coordination is unavailable or busy; cleanup did not run. Retry after the current operation finishes or repair the private policy-lock directory.";
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
phase = "daemon inspection";
|
|
184
|
+
phaseStartedAt = Date.now();
|
|
185
|
+
const daemon = await inspectManagedSessionDaemon({
|
|
199
186
|
cwd: options.cwd,
|
|
187
|
+
headedManagedAutosaveInterval: options.headedManagedAutosaveInterval,
|
|
200
188
|
namespace: options.namespace,
|
|
201
|
-
|
|
202
|
-
|
|
189
|
+
preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
|
|
190
|
+
sessionName: options.sessionName,
|
|
191
|
+
signal: controller.signal,
|
|
192
|
+
timeoutMs: Math.min(options.timeoutMs, 2_000),
|
|
193
|
+
});
|
|
194
|
+
if (daemon.status === "active")
|
|
195
|
+
options.restoreState.recordDaemonRestoreKey(options.sessionName, options.namespace, daemon.restoreKey);
|
|
196
|
+
const daemonRestoreKey = options.restoreState.getDaemonRestoreKey(options.sessionName, options.namespace);
|
|
197
|
+
const ownedRestoreKey = !options.restoreState.isDisabled(options.sessionName, options.namespace)
|
|
198
|
+
&& isManagedSessionRestoreKey(daemonRestoreKey) ? daemonRestoreKey : null;
|
|
199
|
+
phase = "native close";
|
|
200
|
+
phaseStartedAt = Date.now();
|
|
201
|
+
const processResult = await runAgentBrowserProcess({
|
|
202
|
+
args: closeArgs,
|
|
203
|
+
cwd: options.cwd,
|
|
204
|
+
env: { AGENT_BROWSER_JSON: "1", ...getHeadedManagedAutosaveEnv(options.headedManagedAutosaveInterval) },
|
|
205
|
+
managedSessionRestoreState: options.restoreState,
|
|
206
|
+
ownedManagedSession: true,
|
|
207
|
+
preserveAttachedBrowserSession: options.preserveAttachedBrowserSession,
|
|
208
|
+
signal: controller.signal,
|
|
209
|
+
});
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
stdoutSpillPath = processResult.stdoutSpillPath;
|
|
212
|
+
if (!processResult.aborted && !processResult.spawnError && processResult.exitCode === 0) {
|
|
213
|
+
const parsed = await parseAgentBrowserEnvelope({ stdout: processResult.stdout, stdoutPath: processResult.stdoutSpillPath });
|
|
214
|
+
const data = parsed.envelope?.success === true && isRecord(parsed.envelope.data) ? parsed.envelope.data : undefined;
|
|
215
|
+
options.restoreState.clear(options.sessionName, options.namespace);
|
|
216
|
+
pruneOwnedManagedSessionRestoreSnapshots({
|
|
217
|
+
cwd: options.cwd,
|
|
218
|
+
namespace: options.namespace,
|
|
219
|
+
restoreKey: ownedRestoreKey,
|
|
220
|
+
statePath: typeof data?.statePath === "string" ? data.statePath : undefined,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return timeoutError ?? getAgentBrowserErrorText({
|
|
224
|
+
aborted: processResult.aborted,
|
|
225
|
+
command: "close",
|
|
226
|
+
effectiveArgs: redactInvocationArgs(closeArgs),
|
|
227
|
+
exitCode: processResult.exitCode,
|
|
228
|
+
plainTextInspection: false,
|
|
229
|
+
spawnError: processResult.spawnError,
|
|
230
|
+
stderr: processResult.stderr,
|
|
231
|
+
timedOut: processResult.timedOut,
|
|
232
|
+
timeoutMs: processResult.timeoutMs,
|
|
203
233
|
});
|
|
204
234
|
}
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
catch (error) {
|
|
218
|
-
return timeoutError ?? (error instanceof Error ? error.message : String(error));
|
|
219
|
-
}
|
|
220
|
-
finally {
|
|
221
|
-
clearTimeout(timer);
|
|
222
|
-
if (stdoutSpillPath)
|
|
223
|
-
await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
224
|
-
if (!options.policyLock)
|
|
225
|
-
await policyLock.release();
|
|
226
|
-
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
return timeoutError ?? (error instanceof Error ? error.message : String(error));
|
|
237
|
+
}
|
|
238
|
+
finally {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
if (stdoutSpillPath)
|
|
241
|
+
await rm(stdoutSpillPath, { force: true }).catch(() => undefined);
|
|
242
|
+
if (!options.policyLock)
|
|
243
|
+
await policyLock.release();
|
|
244
|
+
}
|
|
245
|
+
});
|
|
227
246
|
}
|
|
@@ -392,6 +392,7 @@ export async function prepareBrowserRun(options) {
|
|
|
392
392
|
}) };
|
|
393
393
|
}
|
|
394
394
|
electronLaunch = launchResult.value;
|
|
395
|
+
electronLaunch.record.ownerSessionId = options.ctx.sessionManager.getSessionId();
|
|
395
396
|
runtimeToolArgs = ["connect", electronLaunch.connectArg];
|
|
396
397
|
runtimeToolStdin = undefined;
|
|
397
398
|
}
|
|
@@ -570,7 +571,18 @@ export async function prepareBrowserRun(options) {
|
|
|
570
571
|
const sessionTabPinningReason = priorSessionPageState.pinningReason;
|
|
571
572
|
let priorRefSnapshotState = priorSessionPageState.refSnapshot;
|
|
572
573
|
let priorRefSnapshotInvalidation = priorSessionPageState.refSnapshotInvalidation;
|
|
573
|
-
|
|
574
|
+
let nativeGenerationChanged = false;
|
|
575
|
+
if (!browserIndependent && !isCloseCommand(executionPlan.commandInfo.command) && priorRefSnapshotState?.refIds.length && sessionStateKey && executionPlan.sessionName) {
|
|
576
|
+
const daemon = await inspectManagedSessionDaemon({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, includeGeneration: true, headedManagedAutosaveInterval: ownedManagedSession?.headedManagedAutosaveInterval ?? (ownedManagedSession?.headedManagedAutosaveDisabled ? "0" : undefined), timeoutMs: params.timeoutMs });
|
|
577
|
+
sessionPageState.bindSnapshotGeneration(sessionStateKey, daemon.status === "active" ? daemon.generation : undefined);
|
|
578
|
+
if (daemon.status !== "active" || daemon.generation === undefined || priorRefSnapshotState.generation !== daemon.generation) {
|
|
579
|
+
nativeGenerationChanged = daemon.status === "active" && daemon.generation !== undefined && priorRefSnapshotState.generation !== undefined && priorRefSnapshotState.generation !== daemon.generation;
|
|
580
|
+
priorRefSnapshotState = undefined;
|
|
581
|
+
priorRefSnapshotInvalidation = buildPageTransitionRefSnapshotInvalidation("The native browser generation changed or could not be verified. Take a new complete snapshot before using refs, even when the URL is unchanged.");
|
|
582
|
+
sessionPageState.applyRefSnapshotInvalidation({ invalidation: priorRefSnapshotInvalidation, sessionName: sessionStateKey, update: options.sessionPageStateUpdate });
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const coldManagedSession = !browserIndependent && (managedSessionDaemonInactive || nativeGenerationChanged || priorSessionPageState.tabReopenPending === true)
|
|
574
586
|
&& recordedOwnedSession !== undefined
|
|
575
587
|
&& sessionTabPinningReason === "restore"
|
|
576
588
|
&& ownedManagedSession?.restoreDecision === "enabled"
|
|
@@ -960,8 +972,17 @@ export async function prepareBrowserRun(options) {
|
|
|
960
972
|
signal,
|
|
961
973
|
usedImplicitSession: executionPlan.usedImplicitSession,
|
|
962
974
|
});
|
|
963
|
-
if (snapshotFilter)
|
|
975
|
+
if (snapshotFilter) {
|
|
976
|
+
if (sessionStateKey && executionPlan.sessionName) {
|
|
977
|
+
const daemon = await inspectManagedSessionDaemon({ cwd, namespace: executionPlan.namespace, sessionName: executionPlan.sessionName, signal, includeGeneration: true, headedManagedAutosaveInterval: ownedManagedSession?.headedManagedAutosaveInterval ?? (ownedManagedSession?.headedManagedAutosaveDisabled ? "0" : undefined), timeoutMs: params.timeoutMs });
|
|
978
|
+
sessionPageState.bindSnapshotGeneration(sessionStateKey, daemon.status === "active" ? daemon.generation : undefined);
|
|
979
|
+
if (ownedManagedSession && daemon.status === "active" && daemon.generation)
|
|
980
|
+
state.managedSessionRestoreState.recordDaemonRestoreKey(executionPlan.sessionName, executionPlan.namespace, daemon.restoreKey, daemon.generation);
|
|
981
|
+
if (isRecord(snapshotFilter.result.details))
|
|
982
|
+
snapshotFilter.result.details.refSnapshot = sessionPageState.get(sessionStateKey).refSnapshot;
|
|
983
|
+
}
|
|
964
984
|
return { kind: "early-result", statePatch: { ...statePatch, artifactManifest: snapshotFilter.artifactManifest ?? statePatch.artifactManifest }, result: snapshotFilter.result };
|
|
985
|
+
}
|
|
965
986
|
const networkRequestsPageFilter = await tryNetworkRequestsPageFilter({
|
|
966
987
|
commandTokens,
|
|
967
988
|
compatibilityWorkaround,
|