pi-agent-browser-native 0.6.16 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -1
- package/README.md +59 -165
- package/dist/extensions/agent-browser/index.js +634 -501
- package/dist/extensions/agent-browser/lib/browser-transcript.js +33 -0
- package/dist/extensions/agent-browser/lib/command-taxonomy.js +8 -4
- package/dist/extensions/agent-browser/lib/execution-cwd.js +66 -0
- package/dist/extensions/agent-browser/lib/input-modes/job.js +2 -213
- package/dist/extensions/agent-browser/lib/input-modes/lookups.js +1 -1
- 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/artifact-paths.js +27 -20
- package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +14 -2
- 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 +41 -24
- 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/operation-paths.js +76 -0
- 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 +47 -61
- package/docs/COMMAND_REFERENCE.md +78 -137
- 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 +218 -401
- package/package.json +4 -4
- package/scripts/doctor.mjs +2 -2
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isRecord } from "./parsing.js";
|
|
2
|
+
export const BROWSER_TRANSITION_ENTRY = "agent-browser-transition";
|
|
3
|
+
export const BROWSER_RESULT_TOOLS = new Set([
|
|
4
|
+
"agent_browser", "agent_browser_code", "agent_browser_action", "agent_browser_qa", "agent_browser_electron",
|
|
5
|
+
"agent_browser_source", "agent_browser_network_source",
|
|
6
|
+
]);
|
|
7
|
+
/** Direct tools and ordered code calls replay through the same browser state reducers. */
|
|
8
|
+
export function getBrowserResultMessage(entry) {
|
|
9
|
+
if (!isRecord(entry))
|
|
10
|
+
return undefined;
|
|
11
|
+
if (entry.type === "custom" && entry.customType === BROWSER_TRANSITION_ENTRY && isRecord(entry.data)) {
|
|
12
|
+
return isRecord(entry.data.details) && typeof entry.data.isError === "boolean" ? entry.data : undefined;
|
|
13
|
+
}
|
|
14
|
+
const message = entry.type === "message" && isRecord(entry.message) ? entry.message : undefined;
|
|
15
|
+
return message && typeof message.toolName === "string" && BROWSER_RESULT_TOOLS.has(message.toolName) ? message : undefined;
|
|
16
|
+
}
|
|
17
|
+
// Page data and rendered output are not state. Keep only fields consumed by the
|
|
18
|
+
// existing replay reducers; recording reservations retain their own native journal.
|
|
19
|
+
const TRANSITION_FIELDS = [
|
|
20
|
+
"args", "command", "subcommand", "sessionName", "namespace", "sessionMode", "usedImplicitSession",
|
|
21
|
+
"agentBrowserStarted", "resultCategory", "exitCode", "closeAllApplied", "attachedBrowserSession",
|
|
22
|
+
"sessionTabTarget", "sessionTabTargetUnknown", "sessionTabReopenPending", "refSnapshot", "refSnapshotInvalidation",
|
|
23
|
+
"readConfirmation", "compatibilityWorkaround", "managedSessionHeadedAutosaveDisabled", "managedSessionHeadedAutosaveInterval",
|
|
24
|
+
"managedSessionOutcome", "managedSessionCwd", "managedSessionRestoreDisabled", "artifactManifest", "electron", "compiledNetworkSourceLookup",
|
|
25
|
+
];
|
|
26
|
+
export function appendBrowserTransition(pi, toolCallId, details, isError) {
|
|
27
|
+
const state = Object.fromEntries(TRANSITION_FIELDS.filter(key => details[key] !== undefined).map(key => [key, details[key]]));
|
|
28
|
+
if (Array.isArray(details.batchSteps))
|
|
29
|
+
state.batchSteps = details.batchSteps.filter(isRecord).map(step => ({
|
|
30
|
+
command: step.command, success: step.success, lifecycle: step.lifecycle,
|
|
31
|
+
}));
|
|
32
|
+
pi.appendEntry(BROWSER_TRANSITION_ENTRY, { toolCallId, details: state, isError });
|
|
33
|
+
}
|
|
@@ -316,9 +316,9 @@ export function isSessionTabPinningExcludedCommand(command) {
|
|
|
316
316
|
export function isSessionTabPostCommandCorrectionExcludedCommand(command) {
|
|
317
317
|
return hasCommandCapability(command, "excludedFromPostCommandCorrection");
|
|
318
318
|
}
|
|
319
|
-
export function
|
|
319
|
+
export function getRecordCommandOperandIndices(tokens) {
|
|
320
320
|
if (tokens[0] !== "record" || !["start", "restart"].includes(tokens[1] ?? ""))
|
|
321
|
-
return
|
|
321
|
+
return [];
|
|
322
322
|
const operands = [];
|
|
323
323
|
for (let index = 2; index < tokens.length && operands.length < 2; index += 1) {
|
|
324
324
|
// Native validates the range; bare/non-numeric --fps keeps its old literal meaning.
|
|
@@ -329,9 +329,13 @@ export function getRecordCommandOperands(tokens) {
|
|
|
329
329
|
else if (tokens[index] === "--contact-sheet-threshold" && tokens[index + 1] !== undefined)
|
|
330
330
|
index += 1;
|
|
331
331
|
else
|
|
332
|
-
operands.push(
|
|
332
|
+
operands.push(index);
|
|
333
333
|
}
|
|
334
|
-
return operands.length > 0 ?
|
|
334
|
+
return operands.length > 0 ? operands : [2, 3];
|
|
335
|
+
}
|
|
336
|
+
export function getRecordCommandOperands(tokens) {
|
|
337
|
+
const [path, url] = getRecordCommandOperandIndices(tokens);
|
|
338
|
+
return path === undefined ? {} : { path: tokens[path], url: url === undefined ? undefined : tokens[url] };
|
|
335
339
|
}
|
|
336
340
|
/** Starts conservatively invalidate refs because older supported natives replace the page, even on failure. Restarts invalidate only when they have a URL. */
|
|
337
341
|
export function isRecordPageTransitionCommand(tokens) {
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFileSync, statSync } from "node:fs";
|
|
2
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
3
|
+
import { isRecord } from "./parsing.js";
|
|
4
|
+
import { getBrowserResultMessage } from "./browser-transcript.js";
|
|
5
|
+
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
6
|
+
function isDirectoryOwner(source) {
|
|
7
|
+
if (/^(?:npm:pi-change-working-dir|git:github\.com\/fitchmultz\/pi-change-working-dir(?:\.git)?)(?:@.+)?$/.test(source.source))
|
|
8
|
+
return true;
|
|
9
|
+
const directories = [source.baseDir, isAbsolute(source.path) ? dirname(source.path) : undefined];
|
|
10
|
+
return directories.some(directory => {
|
|
11
|
+
if (!directory || !isAbsolute(directory))
|
|
12
|
+
return false;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(join(directory, "package.json"), "utf8")).name === "pi-change-working-dir";
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/** The synchronous owner reply is captured before any browser policy, queue, or child await. */
|
|
22
|
+
export function resolveExecutionCwd(pi, ctx) {
|
|
23
|
+
const request = { sessionManager: ctx.sessionManager };
|
|
24
|
+
pi.events.emit("pi-change-working-dir:resolve-execution-cwd", request);
|
|
25
|
+
const result = request.result;
|
|
26
|
+
if (result !== undefined) {
|
|
27
|
+
const invalid = "pi-change-working-dir returned an invalid execution directory. Update the extension and restart Pi.";
|
|
28
|
+
if (!isRecord(result) || Array.isArray(result))
|
|
29
|
+
throw new Error(invalid);
|
|
30
|
+
if (result.error !== undefined)
|
|
31
|
+
throw new Error(typeof result.error === "string" && result.error.length > 0 ? result.error : invalid);
|
|
32
|
+
if (typeof result.cwd !== "string" || !isAbsolute(result.cwd) || result.cwd.includes("\0"))
|
|
33
|
+
throw new Error(invalid);
|
|
34
|
+
return result.cwd;
|
|
35
|
+
}
|
|
36
|
+
if (pi.getAllTools().some(tool => tool.name === "change_dir" && isDirectoryOwner(tool.sourceInfo))
|
|
37
|
+
|| pi.getCommands().some(command => /^cwd(?::\d+)?$/.test(command.name) && isDirectoryOwner(command.sourceInfo))) {
|
|
38
|
+
throw new Error("Update pi-change-working-dir and restart Pi to use agent_browser with the selected working directory.");
|
|
39
|
+
}
|
|
40
|
+
return ctx.cwd;
|
|
41
|
+
}
|
|
42
|
+
export function getBrowserCwdError(cwd) {
|
|
43
|
+
try {
|
|
44
|
+
if (statSync(cwd).isDirectory())
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
if (!["ENOENT", "ENOTDIR"].includes(error.code ?? ""))
|
|
49
|
+
throw error;
|
|
50
|
+
}
|
|
51
|
+
return `Browser launch directory is unavailable: ${cwd}. Restore that directory, or explicitly use sessionMode: "fresh" without --session, or --config from the selected execution directory. The existing browser was left untouched.`;
|
|
52
|
+
}
|
|
53
|
+
export function restoreManagedSessionCwd(branch, sessionName, namespace, fallback) {
|
|
54
|
+
const key = getAgentBrowserSessionIdentityKey(sessionName, namespace);
|
|
55
|
+
for (const entry of [...branch].reverse()) {
|
|
56
|
+
const message = getBrowserResultMessage(entry);
|
|
57
|
+
if (!message)
|
|
58
|
+
continue;
|
|
59
|
+
const details = message.details;
|
|
60
|
+
if (!isRecord(details) || typeof details.sessionName !== "string" || typeof details.managedSessionCwd !== "string" || !isAbsolute(details.managedSessionCwd))
|
|
61
|
+
continue;
|
|
62
|
+
if (getAgentBrowserSessionIdentityKey(details.sessionName, typeof details.namespace === "string" ? details.namespace : undefined) === key)
|
|
63
|
+
return details.managedSessionCwd;
|
|
64
|
+
}
|
|
65
|
+
return fallback;
|
|
66
|
+
}
|
|
@@ -1,218 +1,7 @@
|
|
|
1
1
|
import { isRecord } from "../parsing.js";
|
|
2
2
|
import { summarizeNetworkFailures } from "../results/network.js";
|
|
3
|
-
import { getBatchResultItems, getCommandNameFromBatchItem
|
|
4
|
-
import {
|
|
5
|
-
import { AGENT_BROWSER_JOB_STEP_ACTIONS, AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS, AGENT_BROWSER_QA_LOAD_STATES, } from "./types.js";
|
|
6
|
-
function getRequiredJobString(step, field, action) {
|
|
7
|
-
const value = step[field];
|
|
8
|
-
if (typeof value !== "string" || value.trim().length === 0) {
|
|
9
|
-
return { error: `job step ${action} requires a non-empty ${field} string.` };
|
|
10
|
-
}
|
|
11
|
-
return { value };
|
|
12
|
-
}
|
|
13
|
-
function compileJobClickOrFillStep(step, action) {
|
|
14
|
-
const hasSelector = typeof step.selector === "string" && step.selector.trim().length > 0;
|
|
15
|
-
const hasLocator = step.locator !== undefined || step.role !== undefined || step.name !== undefined || step.value !== undefined;
|
|
16
|
-
if (hasSelector && hasLocator) {
|
|
17
|
-
return { error: `job step ${action} must use either selector or semantic locator fields, not both.` };
|
|
18
|
-
}
|
|
19
|
-
if (hasSelector) {
|
|
20
|
-
if (action === "click")
|
|
21
|
-
return { args: ["click", step.selector] };
|
|
22
|
-
const text = getRequiredJobString(step, "text", action);
|
|
23
|
-
if (text.error)
|
|
24
|
-
return { error: text.error };
|
|
25
|
-
return { args: ["fill", step.selector, text.value] };
|
|
26
|
-
}
|
|
27
|
-
if (!hasLocator) {
|
|
28
|
-
return { error: `job step ${action} requires either a non-empty selector string or semantic locator fields.` };
|
|
29
|
-
}
|
|
30
|
-
const compiled = compileAgentBrowserSemanticAction({
|
|
31
|
-
action,
|
|
32
|
-
locator: step.locator,
|
|
33
|
-
name: step.name,
|
|
34
|
-
role: step.role,
|
|
35
|
-
text: step.text,
|
|
36
|
-
value: step.value,
|
|
37
|
-
});
|
|
38
|
-
if (compiled.error)
|
|
39
|
-
return { error: compiled.error.replaceAll("semanticAction", `job step ${action}`) };
|
|
40
|
-
return { args: compiled.compiled?.args };
|
|
41
|
-
}
|
|
42
|
-
function getUnsupportedJobStepField(step, allowedFields) {
|
|
43
|
-
return Object.keys(step).find((field) => !allowedFields.has(field));
|
|
44
|
-
}
|
|
45
|
-
function getUnsupportedJobStepFieldError(step, action, allowedFields) {
|
|
46
|
-
const unsupportedField = getUnsupportedJobStepField(step, allowedFields);
|
|
47
|
-
if (!unsupportedField)
|
|
48
|
-
return undefined;
|
|
49
|
-
const supportedFields = [...allowedFields].filter((field) => field !== "action");
|
|
50
|
-
const supportedText = supportedFields.length > 0 ? `supported fields are ${supportedFields.join(", ")}.` : "no additional fields are supported.";
|
|
51
|
-
return `job step ${action} does not support ${unsupportedField}; ${supportedText}`;
|
|
52
|
-
}
|
|
53
|
-
const JOB_STEP_ALLOWED_FIELDS = {
|
|
54
|
-
assertText: new Set(["action", "text"]),
|
|
55
|
-
assertUrl: new Set(["action", "url"]),
|
|
56
|
-
click: new Set(["action", "locator", "name", "role", "selector", "value"]),
|
|
57
|
-
fill: new Set(["action", "locator", "name", "role", "selector", "text", "value"]),
|
|
58
|
-
open: new Set(["action", "loadState", "url"]),
|
|
59
|
-
screenshot: new Set(["action", "path"]),
|
|
60
|
-
select: new Set(["action", "selector", "value", "values"]),
|
|
61
|
-
snapshot: new Set(["action"]),
|
|
62
|
-
type: new Set(["action", "delayMs", "press", "selector", "text"]),
|
|
63
|
-
wait: new Set(["action", "milliseconds"]),
|
|
64
|
-
waitForDownload: new Set(["action", "path"]),
|
|
65
|
-
};
|
|
66
|
-
function compileJobTypeSteps(step) {
|
|
67
|
-
const text = getRequiredJobString(step, "text", "type");
|
|
68
|
-
if (text.error)
|
|
69
|
-
return { error: text.error };
|
|
70
|
-
const selector = step.selector;
|
|
71
|
-
if (selector !== undefined && (typeof selector !== "string" || selector.trim().length === 0)) {
|
|
72
|
-
return { error: "job step type selector must be a non-empty string when provided." };
|
|
73
|
-
}
|
|
74
|
-
const delayMs = step.delayMs;
|
|
75
|
-
if (delayMs !== undefined && (typeof delayMs !== "number" || !Number.isInteger(delayMs) || delayMs <= 0)) {
|
|
76
|
-
return { error: "job step type delayMs must be a positive integer when provided." };
|
|
77
|
-
}
|
|
78
|
-
const press = step.press;
|
|
79
|
-
if (press !== undefined && (typeof press !== "string" || press.trim().length === 0)) {
|
|
80
|
-
return { error: "job step type press must be a non-empty key string when provided." };
|
|
81
|
-
}
|
|
82
|
-
const typedText = text.value;
|
|
83
|
-
const typedChars = Array.from(typedText);
|
|
84
|
-
if (typedChars.length === 0)
|
|
85
|
-
return { error: "job step type requires non-empty text." };
|
|
86
|
-
if (delayMs !== undefined && typedChars.length > AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS) {
|
|
87
|
-
return { error: `job step type delayMs supports at most ${AGENT_BROWSER_JOB_TYPE_DELAYED_TEXT_MAX_CHARACTERS} characters; split longer text into shorter calls or omit delayMs.` };
|
|
88
|
-
}
|
|
89
|
-
const compiledSteps = [];
|
|
90
|
-
if (delayMs === undefined) {
|
|
91
|
-
compiledSteps.push({ action: "type", args: typeof selector === "string" ? ["type", selector, typedText] : ["keyboard", "type", typedText] });
|
|
92
|
-
}
|
|
93
|
-
else {
|
|
94
|
-
if (typeof selector === "string")
|
|
95
|
-
compiledSteps.push({ action: "type", args: ["focus", selector], generatedFrom: "type.selector" });
|
|
96
|
-
for (const [index, char] of typedChars.entries()) {
|
|
97
|
-
compiledSteps.push({ action: "type", args: ["keyboard", "type", char], generatedFrom: "type.delayMs" });
|
|
98
|
-
if (index < typedChars.length - 1)
|
|
99
|
-
compiledSteps.push({ action: "wait", args: ["wait", String(delayMs)], generatedFrom: "type.delayMs" });
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (typeof press === "string")
|
|
103
|
-
compiledSteps.push({ action: "type", args: ["press", press], generatedFrom: "type.press" });
|
|
104
|
-
return { steps: compiledSteps };
|
|
105
|
-
}
|
|
106
|
-
function compileOpenJobStep(step, index) {
|
|
107
|
-
const result = getRequiredJobString(step, "url", "open");
|
|
108
|
-
if (result.error)
|
|
109
|
-
return { error: result.error };
|
|
110
|
-
const extraSteps = [];
|
|
111
|
-
if (step.loadState !== undefined) {
|
|
112
|
-
if (typeof step.loadState !== "string" || !AGENT_BROWSER_QA_LOAD_STATES.includes(step.loadState)) {
|
|
113
|
-
return { error: `job.steps[${index}].loadState must be one of: ${AGENT_BROWSER_QA_LOAD_STATES.join(", ")}.` };
|
|
114
|
-
}
|
|
115
|
-
extraSteps.push({ action: "wait", args: ["wait", "--load", step.loadState], generatedFrom: "open.loadState" });
|
|
116
|
-
}
|
|
117
|
-
return { args: ["open", result.value], extraSteps };
|
|
118
|
-
}
|
|
119
|
-
function compileClickJobStep(step) {
|
|
120
|
-
return compileJobClickOrFillStep(step, "click");
|
|
121
|
-
}
|
|
122
|
-
function compileFillJobStep(step) {
|
|
123
|
-
return compileJobClickOrFillStep(step, "fill");
|
|
124
|
-
}
|
|
125
|
-
function compileTypeJobStep(step) {
|
|
126
|
-
const result = compileJobTypeSteps(step);
|
|
127
|
-
if (result.error)
|
|
128
|
-
return { error: result.error };
|
|
129
|
-
const [firstStep, ...extraSteps] = result.steps;
|
|
130
|
-
return { args: firstStep.args, extraSteps, generatedFrom: firstStep.generatedFrom };
|
|
131
|
-
}
|
|
132
|
-
function compileSelectJobStep(step, index) {
|
|
133
|
-
const selector = getRequiredJobString(step, "selector", "select");
|
|
134
|
-
if (selector.error)
|
|
135
|
-
return { error: selector.error };
|
|
136
|
-
const values = getSelectValues(step, `job.steps[${index}]`);
|
|
137
|
-
if (values.error)
|
|
138
|
-
return { error: values.error };
|
|
139
|
-
return { args: ["select", selector.value, ...values.values] };
|
|
140
|
-
}
|
|
141
|
-
function compileWaitJobStep(step) {
|
|
142
|
-
const milliseconds = step.milliseconds;
|
|
143
|
-
if (typeof milliseconds !== "number" || !Number.isInteger(milliseconds) || milliseconds <= 0) {
|
|
144
|
-
return { error: "job step wait requires a positive integer milliseconds value." };
|
|
145
|
-
}
|
|
146
|
-
return { args: ["wait", String(milliseconds)] };
|
|
147
|
-
}
|
|
148
|
-
function compileAssertTextJobStep(step) {
|
|
149
|
-
const result = getRequiredJobString(step, "text", "assertText");
|
|
150
|
-
if (result.error)
|
|
151
|
-
return { error: result.error };
|
|
152
|
-
return { args: ["wait", "--text", result.value] };
|
|
153
|
-
}
|
|
154
|
-
function compileAssertUrlJobStep(step) {
|
|
155
|
-
const result = getRequiredJobString(step, "url", "assertUrl");
|
|
156
|
-
if (result.error)
|
|
157
|
-
return { error: result.error };
|
|
158
|
-
return { args: ["wait", "--url", result.value] };
|
|
159
|
-
}
|
|
160
|
-
function compilePathArtifactJobStep(step, action) {
|
|
161
|
-
const result = getRequiredJobString(step, "path", action);
|
|
162
|
-
if (result.error)
|
|
163
|
-
return { error: result.error };
|
|
164
|
-
return { args: action === "waitForDownload" ? ["wait", "--download", result.value] : ["screenshot", result.value] };
|
|
165
|
-
}
|
|
166
|
-
// ponytail: allowedFields for each action live in JOB_STEP_ALLOWED_FIELDS (same key
|
|
167
|
-
// alignment enforced by Record<AgentBrowserJobStepAction, …>), so the compiler map no
|
|
168
|
-
// longer mirrors that set per entry; the call site looks it up by action.
|
|
169
|
-
const JOB_STEP_COMPILERS = {
|
|
170
|
-
assertText: compileAssertTextJobStep,
|
|
171
|
-
assertUrl: compileAssertUrlJobStep,
|
|
172
|
-
click: compileClickJobStep,
|
|
173
|
-
fill: compileFillJobStep,
|
|
174
|
-
open: compileOpenJobStep,
|
|
175
|
-
screenshot: (step) => compilePathArtifactJobStep(step, "screenshot"),
|
|
176
|
-
select: compileSelectJobStep,
|
|
177
|
-
snapshot: () => ({ args: ["snapshot", "-i"] }),
|
|
178
|
-
type: compileTypeJobStep,
|
|
179
|
-
wait: compileWaitJobStep,
|
|
180
|
-
waitForDownload: (step) => compilePathArtifactJobStep(step, "waitForDownload"),
|
|
181
|
-
};
|
|
182
|
-
export function compileAgentBrowserJob(input) {
|
|
183
|
-
if (!isRecord(input)) {
|
|
184
|
-
return { error: "job must be an object." };
|
|
185
|
-
}
|
|
186
|
-
const rawFailFast = input.failFast;
|
|
187
|
-
if (rawFailFast !== undefined && typeof rawFailFast !== "boolean") {
|
|
188
|
-
return { error: "job.failFast must be a boolean when provided." };
|
|
189
|
-
}
|
|
190
|
-
const failFast = rawFailFast !== false;
|
|
191
|
-
const rawSteps = input.steps;
|
|
192
|
-
if (!Array.isArray(rawSteps) || rawSteps.length === 0) {
|
|
193
|
-
return { error: "job.steps must be a non-empty array." };
|
|
194
|
-
}
|
|
195
|
-
const steps = [];
|
|
196
|
-
for (const [index, rawStep] of rawSteps.entries()) {
|
|
197
|
-
if (!isRecord(rawStep)) {
|
|
198
|
-
return { error: `job.steps[${index}] must be an object.` };
|
|
199
|
-
}
|
|
200
|
-
const action = rawStep.action;
|
|
201
|
-
if (typeof action !== "string" || !AGENT_BROWSER_JOB_STEP_ACTIONS.includes(action)) {
|
|
202
|
-
return { error: `job.steps[${index}].action must be one of: ${AGENT_BROWSER_JOB_STEP_ACTIONS.join(", ")}.` };
|
|
203
|
-
}
|
|
204
|
-
const jobAction = action;
|
|
205
|
-
const compile = JOB_STEP_COMPILERS[jobAction];
|
|
206
|
-
const unsupportedFieldError = getUnsupportedJobStepFieldError(rawStep, jobAction, JOB_STEP_ALLOWED_FIELDS[jobAction]);
|
|
207
|
-
if (unsupportedFieldError)
|
|
208
|
-
return { error: `job.steps[${index}]: ${unsupportedFieldError}` };
|
|
209
|
-
const compiledStep = compile(rawStep, index);
|
|
210
|
-
if (compiledStep.error)
|
|
211
|
-
return { error: compiledStep.error.startsWith(`job.steps[${index}]`) ? compiledStep.error : `job.steps[${index}]: ${compiledStep.error}` };
|
|
212
|
-
steps.push({ action: jobAction, args: compiledStep.args, generatedFrom: compiledStep.generatedFrom }, ...(compiledStep.extraSteps ?? []));
|
|
213
|
-
}
|
|
214
|
-
return { compiled: { args: failFast ? ["batch", "--bail"] : ["batch"], failFast, stdin: JSON.stringify(steps.map((step) => step.args)), steps } };
|
|
215
|
-
}
|
|
3
|
+
import { getBatchResultItems, getCommandNameFromBatchItem } from "./shared.js";
|
|
4
|
+
import { AGENT_BROWSER_QA_LOAD_STATES, } from "./types.js";
|
|
216
5
|
function describeQaChecksRun(checks) {
|
|
217
6
|
const parts = [`load:${checks.loadState}`];
|
|
218
7
|
if (checks.expectedText.length > 0)
|
|
@@ -232,7 +232,7 @@ export async function analyzeSourceLookupResults(data, compiled, cwd, context) {
|
|
|
232
232
|
const electronContext = status === "no-candidates" ? context?.electronContext : undefined;
|
|
233
233
|
const workspaceRoot = context?.workspaceRoot ?? cwd;
|
|
234
234
|
if (electronContext) {
|
|
235
|
-
limitations.push(`Workspace source scan is limited to the
|
|
235
|
+
limitations.push(`Workspace source scan is limited to the captured execution directory: ${workspaceRoot}.`, "Packaged Electron app code may live inside installed app resources or app.asar outside the workspace; the wrapper does not unpack asar files or scan app bundle resources.");
|
|
236
236
|
}
|
|
237
237
|
return {
|
|
238
238
|
candidates,
|