pi-agent-browser-native 0.3.0 → 0.6.5
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 +265 -0
- package/README.md +130 -54
- package/dist/extensions/agent-browser/index.js +781 -169
- package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
- package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
- package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
- package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
- package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
- package/dist/extensions/agent-browser/lib/config.js +1 -1
- package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
- package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
- package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
- package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
- package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
- package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
- package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
- package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
- package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
- package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
- package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
- package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
- package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
- package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
- package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
- package/dist/extensions/agent-browser/lib/playbook.js +29 -25
- package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
- package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
- package/dist/extensions/agent-browser/lib/process.js +130 -104
- package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
- package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
- package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
- package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
- package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
- package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
- package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
- package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
- package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
- package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
- package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
- package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
- package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
- package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
- package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
- package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
- package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
- package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
- package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
- package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
- package/dist/extensions/agent-browser/lib/runtime.js +186 -108
- package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
- package/dist/extensions/agent-browser/lib/temp.js +1 -2
- package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
- package/dist/extensions/agent-browser/lib/web-search.js +108 -24
- package/dist/extensions/agent-browser/script-worker.js +169 -0
- package/dist/scripts/agent-browser-target.mjs +21 -0
- package/docs/ARCHITECTURE.md +57 -34
- package/docs/COMMAND_REFERENCE.md +255 -68
- package/docs/ELECTRON.md +2 -2
- package/docs/RELEASE.md +12 -10
- package/docs/REQUIREMENTS.md +11 -8
- package/docs/SUPPORT_MATRIX.md +36 -24
- package/docs/TOOL_CONTRACT.md +169 -95
- package/package.json +3 -1
- package/platform-smoke.config.mjs +2 -2
- package/scripts/agent-browser-capability-baseline.mjs +87 -9
- package/scripts/agent-browser-target.mjs +21 -0
- package/scripts/build.mjs +41 -0
- package/scripts/config.mjs +1 -0
- package/scripts/doctor.mjs +16 -9
- package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
- package/scripts/platform-smoke/targets.mjs +12 -6
- package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
- package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
- package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
- package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { rm } from "node:fs/promises";
|
|
2
|
+
import { getAgentBrowserSessionIdentityKey, VALUE_FLAGS } from "../../argv-grammar.js";
|
|
2
3
|
import { runAgentBrowserProcess } from "../../process.js";
|
|
3
4
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
4
5
|
import { parseAgentBrowserEnvelope } from "../../results/envelope.js";
|
|
@@ -7,13 +8,11 @@ import { getSessionPageStateKey, isAboutBlankUrl, normalizeComparableUrl, normal
|
|
|
7
8
|
import { isCloseCommand, isElectronPostCommandHealthCommand, isNavigationObservableCommandName, isRefGuardedCommand, isRefInvalidatingBatchCommand, isSessionTabPinningExcludedCommand, isSessionTabPostCommandCorrectionExcludedCommand, } from "../../command-taxonomy.js";
|
|
8
9
|
import { chooseOpenResultTabCorrection } from "../../runtime.js";
|
|
9
10
|
import { isRecord } from "../../parsing.js";
|
|
10
|
-
import { parseUserBatchStdin } from "../batch-stdin.js";
|
|
11
|
+
import { getUpstreamEffectiveBatchSteps, parseUserBatchStdin } from "../batch-stdin.js";
|
|
11
12
|
export const NAVIGATION_SUMMARY_EVAL = `({ title: document.title, url: location.href })`;
|
|
12
13
|
export function applyBrowserRunStatePatch(state, patch) {
|
|
13
14
|
if (!patch)
|
|
14
15
|
return;
|
|
15
|
-
if (patch.allowedDomainsBySession)
|
|
16
|
-
state.allowedDomainsBySession = patch.allowedDomainsBySession;
|
|
17
16
|
if ("artifactManifest" in patch)
|
|
18
17
|
state.artifactManifest = patch.artifactManifest;
|
|
19
18
|
if (patch.freshSessionOrdinal !== undefined)
|
|
@@ -38,7 +37,7 @@ export function applyBrowserRunStatePatch(state, patch) {
|
|
|
38
37
|
export const getSessionContextKey = getSessionPageStateKey;
|
|
39
38
|
export function buildSessionDetailFields(sessionName, usedImplicitSession, namespace, managedSessionRestoreDisabled = false) {
|
|
40
39
|
return {
|
|
41
|
-
...(namespace ? { namespace } : {}),
|
|
40
|
+
...(namespace !== undefined ? { namespace } : {}),
|
|
42
41
|
...(sessionName
|
|
43
42
|
? {
|
|
44
43
|
sessionName,
|
|
@@ -213,10 +212,15 @@ export function getTraceOwnerGuardMessage(options) {
|
|
|
213
212
|
: `Wrapper believes tracing for session ${options.sessionName} is owned by ${activeOwner}; run ${activeOwner} stop instead of ${owner} stop.`;
|
|
214
213
|
}
|
|
215
214
|
export function updateTraceOwnerState(options) {
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
if (!options.sessionName || !options.succeeded)
|
|
216
|
+
return;
|
|
217
|
+
if (isCloseCommand(options.command)) {
|
|
218
|
+
options.traceOwners.delete(options.sessionName);
|
|
218
219
|
return;
|
|
219
220
|
}
|
|
221
|
+
const owner = getTraceOwner(options.command);
|
|
222
|
+
if (!owner)
|
|
223
|
+
return;
|
|
220
224
|
if (options.subcommand === "start") {
|
|
221
225
|
options.traceOwners.set(options.sessionName, owner);
|
|
222
226
|
}
|
|
@@ -243,14 +247,13 @@ export function extractNavigationSummaryFromData(data) {
|
|
|
243
247
|
const result = isRecord(data) && isRecord(data.result) ? data.result : data;
|
|
244
248
|
const title = extractStringResultField(result, "title");
|
|
245
249
|
const url = extractStringResultField(result, "url");
|
|
246
|
-
|
|
250
|
+
const urlChanged = isRecord(result) && typeof result.urlChanged === "boolean" ? result.urlChanged : undefined;
|
|
251
|
+
return title || url ? { title, url, urlChanged } : undefined;
|
|
247
252
|
}
|
|
248
|
-
export function shouldCaptureNavigationSummary(command, data) {
|
|
253
|
+
export function shouldCaptureNavigationSummary(command, data, subcommand) {
|
|
249
254
|
if (command === "eval")
|
|
250
255
|
return true;
|
|
251
|
-
|
|
252
|
-
return false;
|
|
253
|
-
return (isNavigationObservableCommandName(command) &&
|
|
256
|
+
return (isNavigationObservableCommandName(command, subcommand) &&
|
|
254
257
|
(!isRecord(data) || (typeof data.title !== "string" && typeof data.url !== "string")));
|
|
255
258
|
}
|
|
256
259
|
export function mergeNavigationSummaryIntoData(data, navigationSummary) {
|
|
@@ -269,29 +272,42 @@ function extractBatchResultCommand(item) {
|
|
|
269
272
|
return Array.isArray(item.command) ? item.command.filter((token) => typeof token === "string") : [];
|
|
270
273
|
}
|
|
271
274
|
export function getStaleRefArgs(commandTokens, stdin) {
|
|
272
|
-
if (commandTokens[0] !== "batch"
|
|
273
|
-
return commandTokens;
|
|
274
|
-
}
|
|
275
|
-
const parsed = parseUserBatchStdin(stdin);
|
|
276
|
-
if (parsed.error || parsed.steps === undefined) {
|
|
275
|
+
if (commandTokens[0] !== "batch") {
|
|
277
276
|
return commandTokens;
|
|
278
277
|
}
|
|
279
|
-
|
|
278
|
+
const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
|
|
279
|
+
return steps.length > 0 ? steps.flatMap((step) => step) : commandTokens;
|
|
280
|
+
}
|
|
281
|
+
// Selector-carrying flags whose values remain page-scoped refs; values of other value-taking flags
|
|
282
|
+
// (--text, --baseline, --name, and similar) are literal text or paths that may merely look like refs,
|
|
283
|
+
// per upstream parsing. Boolean flags such as --new-tab or --full do not consume the following token,
|
|
284
|
+
// so a ref after them is a genuine positional selector and must stay guarded.
|
|
285
|
+
const SELECTOR_FLAG_TOKENS = new Set(["--selector", "-s"]);
|
|
286
|
+
const SHORT_VALUE_FLAG_TOKENS = new Set(["-b", "-o", "-t"]);
|
|
287
|
+
function isNonSelectorValueFlagToken(token) {
|
|
288
|
+
if (token === undefined || SELECTOR_FLAG_TOKENS.has(token))
|
|
289
|
+
return false;
|
|
290
|
+
return VALUE_FLAGS.has(token) || SHORT_VALUE_FLAG_TOKENS.has(token);
|
|
280
291
|
}
|
|
281
292
|
function collectRefsFromTokens(tokens) {
|
|
282
|
-
|
|
293
|
+
const refs = [];
|
|
294
|
+
for (const [index, token] of tokens.entries()) {
|
|
295
|
+
if (!/^@e\d+\b/.test(token))
|
|
296
|
+
continue;
|
|
297
|
+
if (isNonSelectorValueFlagToken(index > 0 ? tokens[index - 1] : undefined))
|
|
298
|
+
continue;
|
|
299
|
+
refs.push(token.slice(1));
|
|
300
|
+
}
|
|
301
|
+
return refs;
|
|
283
302
|
}
|
|
284
303
|
export function getGuardedRefUsage(commandTokens, stdin, options = {}) {
|
|
285
304
|
const collectFromStep = (step) => isRefGuardedCommand(step[0]) ? collectRefsFromTokens(step) : [];
|
|
286
|
-
if (commandTokens[0] !== "batch"
|
|
287
|
-
return collectFromStep(commandTokens);
|
|
288
|
-
}
|
|
289
|
-
const parsed = parseUserBatchStdin(stdin);
|
|
290
|
-
if (parsed.error || parsed.steps === undefined) {
|
|
305
|
+
if (commandTokens[0] !== "batch") {
|
|
291
306
|
return collectFromStep(commandTokens);
|
|
292
307
|
}
|
|
308
|
+
const steps = getUpstreamEffectiveBatchSteps(commandTokens, stdin);
|
|
293
309
|
const refsBeforeInBatchSnapshot = [];
|
|
294
|
-
for (const step of
|
|
310
|
+
for (const step of steps) {
|
|
295
311
|
if (!options.includeRefsAfterBatchSnapshot && (step[0] ?? "") === "snapshot")
|
|
296
312
|
break;
|
|
297
313
|
refsBeforeInBatchSnapshot.push(...collectFromStep(step));
|
|
@@ -318,13 +334,10 @@ function isSafeSameSnapshotFormBatchStep(step, refSnapshot) {
|
|
|
318
334
|
return false;
|
|
319
335
|
}
|
|
320
336
|
function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
|
|
321
|
-
if (commandTokens[0] !== "batch"
|
|
322
|
-
return undefined;
|
|
323
|
-
const parsed = parseUserBatchStdin(stdin);
|
|
324
|
-
if (parsed.error || parsed.steps === undefined)
|
|
337
|
+
if (commandTokens[0] !== "batch")
|
|
325
338
|
return undefined;
|
|
326
339
|
let priorStepInvalidatesRefs = false;
|
|
327
|
-
for (const step of
|
|
340
|
+
for (const step of getUpstreamEffectiveBatchSteps(commandTokens, stdin)) {
|
|
328
341
|
if ((step[0] ?? "") === "snapshot") {
|
|
329
342
|
priorStepInvalidatesRefs = false;
|
|
330
343
|
}
|
|
@@ -332,7 +345,7 @@ function getBatchRefInvalidationMessage(commandTokens, stdin, refSnapshot) {
|
|
|
332
345
|
if (refIds.length > 0 && isRefGuardedCommand(step[0]) && priorStepInvalidatesRefs) {
|
|
333
346
|
return `Batch step ${step[0]} uses page-scoped ref ${refIds.map((refId) => `@${refId}`).join(", ")} after an earlier batch step can navigate or mutate the page. Split the batch, run snapshot -i after the page-changing step, then retry with current refs.`;
|
|
334
347
|
}
|
|
335
|
-
if (isRefInvalidatingBatchCommand(step
|
|
348
|
+
if (isRefInvalidatingBatchCommand(step) && !isSafeSameSnapshotFormBatchStep(step, refSnapshot)) {
|
|
336
349
|
priorStepInvalidatesRefs = true;
|
|
337
350
|
}
|
|
338
351
|
}
|
|
@@ -355,7 +368,9 @@ export function buildStaleRefPreflight(options) {
|
|
|
355
368
|
return undefined;
|
|
356
369
|
if (options.refSnapshotInvalidation) {
|
|
357
370
|
return {
|
|
358
|
-
message:
|
|
371
|
+
message: options.refSnapshotInvalidation.reason === "page-transition"
|
|
372
|
+
? `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used yet. ${options.refSnapshotInvalidation.summary}`
|
|
373
|
+
: `Ref ${usedRefIds.map((refId) => `@${refId}`).join(", ")} cannot be used because the latest snapshot for this session reported No active page. Run snapshot -i successfully before using page-scoped refs.`,
|
|
359
374
|
refIds: usedRefIds,
|
|
360
375
|
snapshotInvalidation: options.refSnapshotInvalidation,
|
|
361
376
|
};
|
|
@@ -401,11 +416,22 @@ export function shouldPinSessionTabForCommand(options) {
|
|
|
401
416
|
}
|
|
402
417
|
export function buildPinnedBatchPlan(options) {
|
|
403
418
|
if (options.command === "batch") {
|
|
419
|
+
const tabSelectionStep = ["tab", options.selectedTab];
|
|
420
|
+
// Upstream executes raw argument steps exclusively when any exist, so the
|
|
421
|
+
// pinned rewrite must dispatch those same steps rather than resurrecting
|
|
422
|
+
// caller stdin the guard never scanned.
|
|
423
|
+
const argumentSteps = getUpstreamEffectiveBatchSteps(options.commandTokens, undefined);
|
|
424
|
+
if (argumentSteps.length > 0) {
|
|
425
|
+
return {
|
|
426
|
+
includeNavigationSummary: false,
|
|
427
|
+
steps: [tabSelectionStep, ...argumentSteps],
|
|
428
|
+
unwrapMode: "user-batch",
|
|
429
|
+
};
|
|
430
|
+
}
|
|
404
431
|
const parsed = parseUserBatchStdin(options.stdin);
|
|
405
432
|
if (parsed.error) {
|
|
406
433
|
return { error: parsed.error };
|
|
407
434
|
}
|
|
408
|
-
const tabSelectionStep = ["tab", options.selectedTab];
|
|
409
435
|
return {
|
|
410
436
|
includeNavigationSummary: false,
|
|
411
437
|
steps: [tabSelectionStep, ...(parsed.steps ?? [])],
|
|
@@ -415,7 +441,7 @@ export function buildPinnedBatchPlan(options) {
|
|
|
415
441
|
if (options.commandTokens.length === 0) {
|
|
416
442
|
return undefined;
|
|
417
443
|
}
|
|
418
|
-
const includeNavigationSummary = isNavigationObservableCommandName(options.command);
|
|
444
|
+
const includeNavigationSummary = isNavigationObservableCommandName(options.command, options.commandTokens[1]);
|
|
419
445
|
const tabSelectionStep = ["tab", options.selectedTab];
|
|
420
446
|
const commandStep = options.commandTokens;
|
|
421
447
|
const navigationSummarySteps = includeNavigationSummary ? [["eval", NAVIGATION_SUMMARY_EVAL]] : [];
|
|
@@ -505,11 +531,10 @@ export function unwrapPinnedSessionBatchEnvelope(options) {
|
|
|
505
531
|
};
|
|
506
532
|
}
|
|
507
533
|
export async function runSessionCommandData(options) {
|
|
508
|
-
const {
|
|
534
|
+
const { args, cwd, namespace, pinNamespace, sessionName, signal, stdin, throwOnFailure, timeoutMs } = options;
|
|
509
535
|
if (!sessionName)
|
|
510
536
|
return undefined;
|
|
511
537
|
const processResult = await runAgentBrowserProcess({
|
|
512
|
-
allowManagedSessionTarget,
|
|
513
538
|
args: ["--json", ...(namespace !== undefined || pinNamespace ? ["--namespace", namespace ?? ""] : []), "--session", sessionName, ...args],
|
|
514
539
|
cwd,
|
|
515
540
|
signal,
|
|
@@ -613,10 +638,11 @@ export function electronTargetLabel(target) {
|
|
|
613
638
|
export function getActiveElectronRecords(records) {
|
|
614
639
|
return [...records.values()].filter((record) => record.cleanupState === "active" || record.cleanupState === "dead" || record.cleanupState === "partial" || record.cleanupState === "failed");
|
|
615
640
|
}
|
|
616
|
-
export function findElectronLaunchRecordForSession(sessionName, records) {
|
|
641
|
+
export function findElectronLaunchRecordForSession(sessionName, records, namespace) {
|
|
617
642
|
if (!sessionName)
|
|
618
643
|
return undefined;
|
|
619
|
-
|
|
644
|
+
const sessionKey = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
645
|
+
return getActiveElectronRecords(records).find((record) => record.sessionName && getAgentBrowserSessionIdentityKey(record.sessionName, record.namespace) === sessionKey);
|
|
620
646
|
}
|
|
621
647
|
function buildElectronReattachNextAction(record, liveTarget) {
|
|
622
648
|
const endpoint = liveTarget?.webSocketDebuggerUrl ?? record.webSocketDebuggerUrl ?? String(record.port);
|
|
@@ -2,7 +2,7 @@ import { cleanupElectronLaunchResources, inspectElectronLaunchStatus } from "../
|
|
|
2
2
|
import { discoverElectronApps } from "../../electron/discovery.js";
|
|
3
3
|
import { boundElectronProbeString } from "../../electron/cdp.js";
|
|
4
4
|
import { buildOwnedManagedSessionRestoreContext, withOwnedManagedSessionContext, } from "../../managed-session-restore.js";
|
|
5
|
-
import {
|
|
5
|
+
import { getPageTargetValidationError } from "../../page-target-validation.js";
|
|
6
6
|
import { isRecord } from "../../parsing.js";
|
|
7
7
|
import { withAttachedBrowserSessionContext } from "../../process.js";
|
|
8
8
|
import { buildAgentBrowserNextActions } from "../../results/action-recommendations.js";
|
|
@@ -114,6 +114,7 @@ function isElectronLaunchRecord(value) {
|
|
|
114
114
|
return false;
|
|
115
115
|
return value.version === 1 &&
|
|
116
116
|
value.launchedByWrapper === true &&
|
|
117
|
+
(value.namespace === undefined || typeof value.namespace === "string") &&
|
|
117
118
|
typeof value.launchId === "string" &&
|
|
118
119
|
typeof value.appName === "string" &&
|
|
119
120
|
typeof value.executablePath === "string" &&
|
|
@@ -133,13 +134,14 @@ export function restoreElectronLaunchRecordsFromBranch(branch) {
|
|
|
133
134
|
const electron = isRecord(details?.electron) ? details.electron : undefined;
|
|
134
135
|
if (!electron)
|
|
135
136
|
continue;
|
|
137
|
+
const namespace = typeof details?.namespace === "string" ? details.namespace : undefined;
|
|
136
138
|
const launch = isElectronLaunchRecord(electron.launch) ? electron.launch : undefined;
|
|
137
139
|
if (launch)
|
|
138
|
-
records.set(launch.launchId, launch);
|
|
140
|
+
records.set(launch.launchId, { ...launch, namespace: launch.namespace ?? namespace });
|
|
139
141
|
const cleanupRecords = isRecord(electron.cleanup) && Array.isArray(electron.cleanup.records) ? electron.cleanup.records : [];
|
|
140
142
|
for (const cleanupRecord of cleanupRecords) {
|
|
141
143
|
if (isElectronLaunchRecord(cleanupRecord))
|
|
142
|
-
records.set(cleanupRecord.launchId, cleanupRecord);
|
|
144
|
+
records.set(cleanupRecord.launchId, { ...cleanupRecord, namespace: cleanupRecord.namespace ?? namespace });
|
|
143
145
|
}
|
|
144
146
|
}
|
|
145
147
|
return records;
|
|
@@ -428,22 +430,22 @@ async function withOwnedElectronManagedSessionPolicy(options, run) {
|
|
|
428
430
|
}
|
|
429
431
|
async function collectOwnedElectronManagedSessionTarget(options) {
|
|
430
432
|
try {
|
|
431
|
-
|
|
432
|
-
allowManagedSessionTarget: true,
|
|
433
|
+
const target = await withOwnedElectronManagedSessionPolicy({ ...options, args: ["get", "url"] }, async () => await collectElectronManagedSessionTarget({
|
|
433
434
|
cwd: options.cwd,
|
|
434
435
|
namespace: options.namespace,
|
|
435
436
|
sessionName: options.sessionName,
|
|
436
437
|
signal: options.signal,
|
|
437
438
|
timeoutMs: options.timeoutMs,
|
|
438
439
|
})) ?? { sessionName: options.sessionName };
|
|
440
|
+
return { ...target, namespace: options.namespace };
|
|
439
441
|
}
|
|
440
442
|
catch (error) {
|
|
441
|
-
return { error: error instanceof Error ? error.message : String(error), sessionName: options.sessionName };
|
|
443
|
+
return { error: error instanceof Error ? error.message : String(error), namespace: options.namespace, sessionName: options.sessionName };
|
|
442
444
|
}
|
|
443
445
|
}
|
|
444
446
|
async function runElectronProbeCommandData(options) {
|
|
445
447
|
try {
|
|
446
|
-
return { data: await runSessionCommandData({ ...options,
|
|
448
|
+
return { data: await runSessionCommandData({ ...options, pinNamespace: true, throwOnFailure: true }) };
|
|
447
449
|
}
|
|
448
450
|
catch (error) {
|
|
449
451
|
return { error: error instanceof Error ? error.message : String(error) };
|
|
@@ -457,9 +459,6 @@ async function collectElectronProbe(options) {
|
|
|
457
459
|
const url = boundElectronProbeString(extractStringResultField(urlResult.data, "result") ?? extractStringResultField(urlResult.data, "url"), 300);
|
|
458
460
|
if (!url)
|
|
459
461
|
throw new Error("get url returned no active page URL.");
|
|
460
|
-
const fileAccessError = getManagedSessionStateAccessValidationError({ args: ["snapshot", "-i"], currentPageUrl: url, cwd: options.cwd });
|
|
461
|
-
if (fileAccessError)
|
|
462
|
-
throw new ElectronManagedSessionPolicyError(fileAccessError);
|
|
463
462
|
const titleResult = await runElectronProbeCommandData({ ...commandContext, args: ["get", "title"] });
|
|
464
463
|
const focusedResult = await runElectronProbeCommandData({ ...commandContext, args: ["eval", "--stdin"], stdin: ELECTRON_FOCUSED_ELEMENT_EVAL });
|
|
465
464
|
const tabsResult = await runElectronProbeCommandData({ ...commandContext, args: ["tab", "list"] });
|
|
@@ -600,10 +599,10 @@ function buildElectronProbeResult(options) {
|
|
|
600
599
|
export async function cleanupTrackedElectronHostLaunches(options) {
|
|
601
600
|
const results = [];
|
|
602
601
|
for (const record of options.records) {
|
|
603
|
-
const sessionKey = getSessionPageStateKey(record.sessionName) ?? record.sessionName;
|
|
602
|
+
const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
|
|
604
603
|
const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
|
|
605
604
|
const managedSessionCloseError = record.sessionName
|
|
606
|
-
? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
|
|
605
|
+
? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
|
|
607
606
|
: undefined;
|
|
608
607
|
const managedSessionStep = record.sessionName
|
|
609
608
|
? managedSessionCloseError
|
|
@@ -648,7 +647,7 @@ export async function cleanupActiveElectronHostLaunches(options) {
|
|
|
648
647
|
export async function handleElectronHostInput(options) {
|
|
649
648
|
const currentSessionKey = getSessionPageStateKey(options.managedSessionName, options.managedSessionNamespace) ?? options.managedSessionName;
|
|
650
649
|
const preserveAttachedBrowserSession = options.attachedSessionKeys.has(currentSessionKey)
|
|
651
|
-
|| [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName) ?? record.sessionName));
|
|
650
|
+
|| [...options.electronLaunchRecords.values()].some((record) => record.sessionName !== undefined && options.attachedSessionKeys.has(getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName));
|
|
652
651
|
return await withAttachedBrowserSessionContext(preserveAttachedBrowserSession, () => handleElectronHostInputInContext(options));
|
|
653
652
|
}
|
|
654
653
|
async function handleElectronHostInputInContext(options) {
|
|
@@ -671,11 +670,12 @@ async function handleElectronHostInputInContext(options) {
|
|
|
671
670
|
const managedSessions = await Promise.all(records
|
|
672
671
|
.filter((record) => typeof record.sessionName === "string")
|
|
673
672
|
.map((record) => {
|
|
674
|
-
const sessionKey = getSessionPageStateKey(record.sessionName) ?? record.sessionName;
|
|
673
|
+
const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
|
|
675
674
|
return collectOwnedElectronManagedSessionTarget({
|
|
676
675
|
cwd,
|
|
677
676
|
headedManagedAutosaveDisabled: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveDisabled,
|
|
678
677
|
headedManagedAutosaveInterval: ownedManagedSessions.get(sessionKey)?.headedManagedAutosaveInterval,
|
|
678
|
+
namespace: record.namespace,
|
|
679
679
|
restoreState: managedSessionRestoreState,
|
|
680
680
|
sessionName: record.sessionName,
|
|
681
681
|
signal,
|
|
@@ -684,7 +684,7 @@ async function handleElectronHostInputInContext(options) {
|
|
|
684
684
|
}));
|
|
685
685
|
const mismatches = managedSessions
|
|
686
686
|
.map((managedSession) => {
|
|
687
|
-
const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName);
|
|
687
|
+
const record = records.find((candidate) => candidate.sessionName === managedSession.sessionName && candidate.namespace === managedSession.namespace);
|
|
688
688
|
const status = record ? statuses.find((candidate) => candidate.launchId === record.launchId) : undefined;
|
|
689
689
|
return record && status ? buildElectronSessionMismatch({ managedSession, record, statusTargets: status.targets }) : undefined;
|
|
690
690
|
})
|
|
@@ -700,7 +700,7 @@ async function handleElectronHostInputInContext(options) {
|
|
|
700
700
|
if (compiledElectron?.action === "probe") {
|
|
701
701
|
const launchRecord = compiledElectron.launchId
|
|
702
702
|
? electronLaunchRecords.get(compiledElectron.launchId)
|
|
703
|
-
: findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords);
|
|
703
|
+
: findElectronLaunchRecordForSession(managedSessionName, electronLaunchRecords, managedSessionNamespace);
|
|
704
704
|
if (compiledElectron.launchId && !launchRecord) {
|
|
705
705
|
return buildElectronHostFailureResult({
|
|
706
706
|
compiledElectron: redactedCompiledElectron ?? compiledElectron,
|
|
@@ -732,17 +732,16 @@ async function handleElectronHostInputInContext(options) {
|
|
|
732
732
|
}
|
|
733
733
|
try {
|
|
734
734
|
const status = launchRecord ? await inspectElectronLaunchStatus(launchRecord) : undefined;
|
|
735
|
-
const probeNamespace = compiledElectron.launchId ?
|
|
735
|
+
const probeNamespace = compiledElectron.launchId ? launchRecord?.namespace : managedSessionNamespace;
|
|
736
736
|
const pageStateKey = getSessionPageStateKey(probeSessionName, probeNamespace) ?? probeSessionName;
|
|
737
737
|
const currentPageState = sessionPageState.get(pageStateKey);
|
|
738
|
-
const
|
|
738
|
+
const pageTargetError = getPageTargetValidationError({
|
|
739
739
|
args: ["snapshot", "-i"],
|
|
740
740
|
currentPageUrl: currentPageState.tabTarget?.url,
|
|
741
|
-
cwd,
|
|
742
741
|
pageUrlUnknown: currentPageState.tabTargetUnknown === true,
|
|
743
742
|
});
|
|
744
|
-
if (
|
|
745
|
-
throw new ElectronManagedSessionPolicyError(
|
|
743
|
+
if (pageTargetError)
|
|
744
|
+
throw new ElectronManagedSessionPolicyError(pageTargetError);
|
|
746
745
|
const managedSessionOwner = ownedManagedSessions.get(pageStateKey);
|
|
747
746
|
const headedManagedAutosaveDisabled = managedSessionOwner?.headedManagedAutosaveDisabled === true;
|
|
748
747
|
const headedManagedAutosaveInterval = managedSessionOwner?.headedManagedAutosaveInterval;
|
|
@@ -5,6 +5,7 @@ import { compileAgentBrowserElectron } from "../input-modes/electron.js";
|
|
|
5
5
|
import { compileAgentBrowserJob, compileAgentBrowserQaPreset } from "../input-modes/job.js";
|
|
6
6
|
import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup, redactNetworkSourceLookupArgs, redactNetworkSourceLookupUrl } from "../input-modes/lookups.js";
|
|
7
7
|
import { compileAgentBrowserSemanticAction } from "../input-modes/semantic-action.js";
|
|
8
|
+
import { AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS, compileAgentBrowserScript } from "../input-modes/script.js";
|
|
8
9
|
function redactCompiledElectron(compiled) {
|
|
9
10
|
if (!compiled)
|
|
10
11
|
return undefined;
|
|
@@ -71,6 +72,7 @@ export function resolveAgentBrowserInput(options) {
|
|
|
71
72
|
const sourceLookupResult = params.sourceLookup === undefined ? {} : compileAgentBrowserSourceLookup(params.sourceLookup);
|
|
72
73
|
const networkSourceLookupResult = params.networkSourceLookup === undefined ? {} : compileAgentBrowserNetworkSourceLookup(params.networkSourceLookup);
|
|
73
74
|
const electronResult = params.electron === undefined ? {} : compileAgentBrowserElectron(params.electron);
|
|
75
|
+
const scriptResult = params.script === undefined ? {} : compileAgentBrowserScript(params.script);
|
|
74
76
|
const hasExplicitArgs = Array.isArray(params.args);
|
|
75
77
|
const explicitInputModes = [
|
|
76
78
|
hasExplicitArgs,
|
|
@@ -80,19 +82,21 @@ export function resolveAgentBrowserInput(options) {
|
|
|
80
82
|
Boolean(sourceLookupResult.compiled),
|
|
81
83
|
Boolean(networkSourceLookupResult.compiled),
|
|
82
84
|
Boolean(electronResult.compiled),
|
|
85
|
+
Boolean(scriptResult.compiled),
|
|
83
86
|
].filter(Boolean).length;
|
|
84
87
|
const inputModeError = explicitInputModes !== 1
|
|
85
|
-
? "Provide exactly one of args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
|
|
88
|
+
? "Provide exactly one of script, args, semanticAction, job, qa, sourceLookup, networkSourceLookup, or electron."
|
|
86
89
|
: undefined;
|
|
87
90
|
const compiledSemanticAction = semanticActionResult.compiled;
|
|
88
91
|
const compiledQaPreset = qaResult.compiled;
|
|
89
92
|
const compiledSourceLookup = sourceLookupResult.compiled;
|
|
90
93
|
const compiledNetworkSourceLookup = networkSourceLookupResult.compiled;
|
|
91
94
|
const compiledElectron = electronResult.compiled;
|
|
95
|
+
const compiledScript = scriptResult.compiled;
|
|
92
96
|
const compiledJob = jobResult.compiled ?? compiledQaPreset;
|
|
93
97
|
const compiledGeneratedBatch = compiledNetworkSourceLookup ?? compiledSourceLookup ?? compiledJob;
|
|
94
98
|
const normalizedExplicitArgs = normalizeExplicitEvalStdinArgs(params.args ?? [], params.stdin);
|
|
95
|
-
const toolArgs = compiledElectron ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
|
|
99
|
+
const toolArgs = compiledElectron || compiledScript ? [] : compiledSemanticAction?.args ?? compiledGeneratedBatch?.args ?? normalizedExplicitArgs.args;
|
|
96
100
|
const toolStdin = compiledGeneratedBatch?.stdin ?? normalizedExplicitArgs.stdin;
|
|
97
101
|
const redactedArgs = redactInvocationArgs(toolArgs);
|
|
98
102
|
const generatedStdinError = params.stdin !== undefined
|
|
@@ -100,7 +104,9 @@ export function resolveAgentBrowserInput(options) {
|
|
|
100
104
|
? "Do not provide stdin with job, qa, sourceLookup, or networkSourceLookup; those modes generate their own batch stdin."
|
|
101
105
|
: compiledElectron
|
|
102
106
|
? "Do not provide stdin with electron; electron mode is host-only or manages its own input."
|
|
103
|
-
:
|
|
107
|
+
: compiledScript
|
|
108
|
+
? "Do not provide stdin with script; browser call stdin belongs inside browser({ args, stdin })."
|
|
109
|
+
: undefined
|
|
104
110
|
: undefined;
|
|
105
111
|
const outputPathError = params.outputPath !== undefined && (typeof params.outputPath !== "string" || params.outputPath.trim().length === 0)
|
|
106
112
|
? "outputPath must be a non-empty string when provided."
|
|
@@ -109,7 +115,12 @@ export function resolveAgentBrowserInput(options) {
|
|
|
109
115
|
? "timeoutMs must be a positive integer when provided."
|
|
110
116
|
: compiledElectron && params.timeoutMs !== undefined
|
|
111
117
|
? "Use electron.timeoutMs for electron actions; top-level timeoutMs applies only to browser CLI subprocess calls."
|
|
112
|
-
: undefined
|
|
118
|
+
: compiledScript && params.timeoutMs !== undefined && params.timeoutMs > AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS
|
|
119
|
+
? `script timeoutMs must be ${AGENT_BROWSER_SCRIPT_MAX_TIMEOUT_MS} or less.`
|
|
120
|
+
: undefined;
|
|
121
|
+
const scriptSessionModeError = compiledScript && params.sessionMode !== undefined
|
|
122
|
+
? "Do not provide sessionMode with script; script always uses its own isolated session."
|
|
123
|
+
: undefined;
|
|
113
124
|
const attachedQaSessionError = compiledQaPreset?.checks.attached
|
|
114
125
|
? params.sessionMode === "fresh"
|
|
115
126
|
? "qa.attached cannot be used with sessionMode=fresh; attach or launch a session first, then run qa.attached with the current session."
|
|
@@ -123,31 +134,35 @@ export function resolveAgentBrowserInput(options) {
|
|
|
123
134
|
?? sourceLookupResult.error
|
|
124
135
|
?? networkSourceLookupResult.error
|
|
125
136
|
?? electronResult.error
|
|
137
|
+
?? scriptResult.error
|
|
126
138
|
?? inputModeError
|
|
127
139
|
?? generatedStdinError
|
|
128
140
|
?? outputPathError
|
|
129
141
|
?? timeoutMsError
|
|
142
|
+
?? scriptSessionModeError
|
|
130
143
|
?? attachedQaSessionError
|
|
131
|
-
?? (compiledElectron ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
|
|
144
|
+
?? (compiledElectron || compiledScript ? undefined : validateToolArgs(toolArgs) ?? getBatchPreflightValidationError(toolArgs, toolStdin));
|
|
132
145
|
const redactedCompiledJob = redactCompiledJob(compiledJob);
|
|
133
146
|
const redactedCompiledSemanticAction = compiledSemanticAction
|
|
134
147
|
? { ...compiledSemanticAction, args: redactInvocationArgs(compiledSemanticAction.args) }
|
|
135
148
|
: undefined;
|
|
136
149
|
const attemptedKind = compiledElectron
|
|
137
150
|
? "electron"
|
|
138
|
-
:
|
|
139
|
-
? "
|
|
140
|
-
:
|
|
141
|
-
? "
|
|
142
|
-
:
|
|
143
|
-
? "
|
|
144
|
-
:
|
|
145
|
-
? "
|
|
146
|
-
:
|
|
147
|
-
? "
|
|
148
|
-
:
|
|
149
|
-
? "
|
|
150
|
-
:
|
|
151
|
+
: compiledScript
|
|
152
|
+
? "script"
|
|
153
|
+
: compiledNetworkSourceLookup
|
|
154
|
+
? "networkSourceLookup"
|
|
155
|
+
: compiledSourceLookup
|
|
156
|
+
? "sourceLookup"
|
|
157
|
+
: compiledQaPreset
|
|
158
|
+
? "qa"
|
|
159
|
+
: jobResult.compiled
|
|
160
|
+
? "job"
|
|
161
|
+
: compiledSemanticAction
|
|
162
|
+
? "semanticAction"
|
|
163
|
+
: hasExplicitArgs
|
|
164
|
+
? "args"
|
|
165
|
+
: undefined;
|
|
151
166
|
const redactedCompiledElectron = redactCompiledElectron(compiledElectron);
|
|
152
167
|
const redactedCompiledNetworkSourceLookup = redactCompiledNetworkSourceLookup(compiledNetworkSourceLookup);
|
|
153
168
|
const redactedCompiledQaPreset = compiledQaPreset && redactedCompiledJob ? { ...redactedCompiledJob, checks: compiledQaPreset.checks } : undefined;
|
|
@@ -178,6 +193,9 @@ export function resolveAgentBrowserInput(options) {
|
|
|
178
193
|
if (compiledElectron && redactedCompiledElectron) {
|
|
179
194
|
return { ...resolvedBase, compiledElectron, kind: "electron", redactedCompiledElectron, status: "valid" };
|
|
180
195
|
}
|
|
196
|
+
if (compiledScript) {
|
|
197
|
+
return { ...resolvedBase, compiledScript, kind: "script", status: "valid" };
|
|
198
|
+
}
|
|
181
199
|
if (compiledNetworkSourceLookup && redactedCompiledNetworkSourceLookup) {
|
|
182
200
|
return {
|
|
183
201
|
...resolvedBase,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { mkdir, realpath, stat, writeFile } from "node:fs/promises";
|
|
2
|
-
import { dirname, isAbsolute, resolve } from "node:path";
|
|
3
|
-
import { getAgentBrowserStoragePathValidationError } from "../managed-session-state-policy.js";
|
|
1
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, isAbsolute, resolve } from "node:path";
|
|
4
3
|
import { isRecord } from "../parsing.js";
|
|
4
|
+
import { isSessionArtifactManifest } from "../results/artifact-manifest.js";
|
|
5
5
|
export function normalizeRequestedOutputPath(path) {
|
|
6
6
|
return path.startsWith("@") ? path.slice(1) : path;
|
|
7
7
|
}
|
|
@@ -11,11 +11,41 @@ function getTextContent(result) {
|
|
|
11
11
|
.map((item) => item.text)
|
|
12
12
|
.join("\n\n") ?? "";
|
|
13
13
|
}
|
|
14
|
-
function
|
|
14
|
+
function getOutputSource(result) {
|
|
15
|
+
return isRecord(result.details) && result.details.data !== undefined ? "details.data" : "content.text";
|
|
16
|
+
}
|
|
17
|
+
async function readCompactedSpill(path, manifest) {
|
|
18
|
+
if (!path || !manifest?.entries.some((entry) => entry.kind === "spill"
|
|
19
|
+
&& (entry.path === path || entry.absolutePath === path)
|
|
20
|
+
&& (entry.storageScope === "persistent-session" || entry.storageScope === "process-temp")
|
|
21
|
+
&& (entry.retentionState === "live" || entry.retentionState === "ephemeral")))
|
|
22
|
+
throw new Error("Full compacted output is unavailable from the wrapper-managed spill; outputPath was not written.");
|
|
23
|
+
const text = await readFile(path, "utf8");
|
|
24
|
+
return extname(path) === ".json" ? JSON.parse(text) : text;
|
|
25
|
+
}
|
|
26
|
+
async function rehydrateCompactedData(data, details, manifest) {
|
|
27
|
+
if (isRecord(data) && data.compacted === true) {
|
|
28
|
+
return readCompactedSpill(typeof details.fullOutputPath === "string" ? details.fullOutputPath : undefined, manifest);
|
|
29
|
+
}
|
|
30
|
+
if (!Array.isArray(data))
|
|
31
|
+
return data;
|
|
32
|
+
const batchSteps = Array.isArray(details.batchSteps) ? details.batchSteps : [];
|
|
33
|
+
return Promise.all(data.map(async (row, index) => {
|
|
34
|
+
if (!isRecord(row) || !isRecord(row.result) || row.result.compacted !== true)
|
|
35
|
+
return row;
|
|
36
|
+
const step = isRecord(batchSteps[index]) ? batchSteps[index] : undefined;
|
|
37
|
+
const path = typeof step?.fullOutputPath === "string"
|
|
38
|
+
? step.fullOutputPath
|
|
39
|
+
: typeof row.result.fullOutputPath === "string" ? row.result.fullOutputPath : undefined;
|
|
40
|
+
return { ...row, result: await readCompactedSpill(path, manifest) };
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
async function getOutputPayload(result) {
|
|
15
44
|
const details = isRecord(result.details) ? result.details : undefined;
|
|
16
|
-
if (details
|
|
17
|
-
return { source: "
|
|
18
|
-
|
|
45
|
+
if (details?.data === undefined)
|
|
46
|
+
return { source: "content.text", value: getTextContent(result) };
|
|
47
|
+
const manifest = isSessionArtifactManifest(details.artifactManifest) ? details.artifactManifest : undefined;
|
|
48
|
+
return { source: "details.data", value: await rehydrateCompactedData(details.data, details, manifest) };
|
|
19
49
|
}
|
|
20
50
|
function serializeOutputPayload(value) {
|
|
21
51
|
return typeof value === "string" ? value : `${JSON.stringify(value, null, 2)}\n`;
|
|
@@ -28,9 +58,6 @@ function appendOutputFileNotice(result, message) {
|
|
|
28
58
|
}
|
|
29
59
|
return [{ type: "text", text: message }, ...content];
|
|
30
60
|
}
|
|
31
|
-
export function getAgentBrowserOutputPathValidationError(outputPath, cwd) {
|
|
32
|
-
return outputPath ? getAgentBrowserStoragePathValidationError(normalizeRequestedOutputPath(outputPath), cwd) : undefined;
|
|
33
|
-
}
|
|
34
61
|
function getArtifactPaths(result, cwd) {
|
|
35
62
|
const details = isRecord(result.details) ? result.details : undefined;
|
|
36
63
|
if (!details || !Array.isArray(details.artifacts))
|
|
@@ -61,24 +88,16 @@ async function pathsReferToSameFile(left, right) {
|
|
|
61
88
|
export async function applyAgentBrowserOutputPath(options) {
|
|
62
89
|
if (!options.outputPath)
|
|
63
90
|
return options.result;
|
|
64
|
-
const validationError = getAgentBrowserOutputPathValidationError(options.outputPath, options.cwd);
|
|
65
|
-
if (validationError) {
|
|
66
|
-
return {
|
|
67
|
-
content: [{ type: "text", text: validationError }],
|
|
68
|
-
details: { failureCategory: "validation-error", resultCategory: "failure", validationError },
|
|
69
|
-
isError: true,
|
|
70
|
-
};
|
|
71
|
-
}
|
|
72
91
|
if (options.result.isError || (isRecord(options.result.details) && options.result.details.resultCategory === "failure"))
|
|
73
92
|
return options.result;
|
|
74
93
|
const requestedPath = normalizeRequestedOutputPath(options.outputPath);
|
|
75
94
|
const absolutePath = isAbsolute(requestedPath) ? requestedPath : resolve(options.cwd, requestedPath);
|
|
76
|
-
const
|
|
95
|
+
const source = getOutputSource(options.result);
|
|
77
96
|
for (const artifactPath of getArtifactPaths(options.result, options.cwd)) {
|
|
78
97
|
if (!await pathsReferToSameFile(absolutePath, artifactPath))
|
|
79
98
|
continue;
|
|
80
99
|
const message = "outputPath resolves to the same file as a browser artifact destination; choose a separate outputPath or omit it. The browser artifact was preserved.";
|
|
81
|
-
const outputFile = { absolutePath, error: message, path: requestedPath, source
|
|
100
|
+
const outputFile = { absolutePath, error: message, path: requestedPath, source, status: "failed" };
|
|
82
101
|
const details = isRecord(options.result.details) ? { ...options.result.details } : {};
|
|
83
102
|
delete details.successCategory;
|
|
84
103
|
return {
|
|
@@ -89,6 +108,7 @@ export async function applyAgentBrowserOutputPath(options) {
|
|
|
89
108
|
};
|
|
90
109
|
}
|
|
91
110
|
try {
|
|
111
|
+
const payload = await getOutputPayload(options.result);
|
|
92
112
|
const serialized = serializeOutputPayload(payload.value);
|
|
93
113
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
94
114
|
await writeFile(absolutePath, serialized, "utf8");
|
|
@@ -103,7 +123,7 @@ export async function applyAgentBrowserOutputPath(options) {
|
|
|
103
123
|
}
|
|
104
124
|
catch (error) {
|
|
105
125
|
const message = error instanceof Error ? error.message : String(error);
|
|
106
|
-
const outputFile = { absolutePath, error: message, path: requestedPath, source
|
|
126
|
+
const outputFile = { absolutePath, error: message, path: requestedPath, source, status: "failed" };
|
|
107
127
|
const details = isRecord(options.result.details)
|
|
108
128
|
? (() => {
|
|
109
129
|
const rest = { ...options.result.details };
|