pi-agent-browser-native 0.2.32 → 0.2.34
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 +36 -0
- package/README.md +61 -20
- package/docs/ARCHITECTURE.md +9 -2
- package/docs/COMMAND_REFERENCE.md +45 -14
- package/docs/ELECTRON.md +23 -4
- package/docs/RELEASE.md +15 -5
- package/docs/REQUIREMENTS.md +1 -1
- package/docs/SUPPORT_MATRIX.md +36 -22
- package/docs/TOOL_CONTRACT.md +90 -31
- package/extensions/agent-browser/index.ts +407 -4373
- package/extensions/agent-browser/lib/input-modes/electron.ts +170 -0
- package/extensions/agent-browser/lib/input-modes/job.ts +265 -0
- package/extensions/agent-browser/lib/input-modes/lookups.ts +447 -0
- package/extensions/agent-browser/lib/input-modes/params.ts +188 -0
- package/extensions/agent-browser/lib/input-modes/semantic-action.ts +107 -0
- package/extensions/agent-browser/lib/input-modes/shared.ts +46 -0
- package/extensions/agent-browser/lib/input-modes/types.ts +221 -0
- package/extensions/agent-browser/lib/input-modes.ts +44 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +762 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +450 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/index.ts +46 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +736 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +413 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +868 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +482 -0
- package/extensions/agent-browser/lib/orchestration/browser-run.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/input-plan.ts +338 -0
- package/extensions/agent-browser/lib/playbook.ts +22 -20
- package/extensions/agent-browser/lib/process.ts +106 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +269 -0
- package/extensions/agent-browser/lib/results/artifact-manifest.ts +114 -0
- package/extensions/agent-browser/lib/results/artifact-state.ts +13 -0
- package/extensions/agent-browser/lib/results/categories.ts +106 -0
- package/extensions/agent-browser/lib/results/contracts.ts +220 -0
- package/extensions/agent-browser/lib/results/editable-ref-evidence.ts +72 -0
- package/extensions/agent-browser/lib/results/envelope.ts +2 -1
- package/extensions/agent-browser/lib/results/network.ts +64 -0
- package/extensions/agent-browser/lib/results/next-actions.ts +117 -0
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +506 -0
- package/extensions/agent-browser/lib/results/presentation/batch.ts +355 -0
- package/extensions/agent-browser/lib/results/presentation/common.ts +53 -0
- package/extensions/agent-browser/lib/results/presentation/content.ts +36 -0
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +730 -0
- package/extensions/agent-browser/lib/results/presentation/errors.ts +125 -0
- package/extensions/agent-browser/lib/results/presentation/large-output.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/navigation.ts +216 -0
- package/extensions/agent-browser/lib/results/presentation/registry.ts +182 -0
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +133 -0
- package/extensions/agent-browser/lib/results/presentation/skills.ts +143 -0
- package/extensions/agent-browser/lib/results/presentation.ts +96 -2403
- package/extensions/agent-browser/lib/results/recovery-actions.ts +139 -0
- package/extensions/agent-browser/lib/results/recovery-next-actions.ts +71 -0
- package/extensions/agent-browser/lib/results/selector-recovery.ts +312 -0
- package/extensions/agent-browser/lib/results/shared.ts +17 -789
- package/extensions/agent-browser/lib/results/snapshot-high-value-controls.ts +262 -0
- package/extensions/agent-browser/lib/results/snapshot-refs.ts +100 -0
- package/extensions/agent-browser/lib/results/snapshot-segments.ts +366 -0
- package/extensions/agent-browser/lib/results/snapshot-spill.ts +63 -0
- package/extensions/agent-browser/lib/results/snapshot.ts +37 -489
- package/extensions/agent-browser/lib/results/text.ts +40 -0
- package/extensions/agent-browser/lib/results.ts +16 -5
- package/extensions/agent-browser/lib/session-page-state.ts +486 -0
- package/package.json +2 -1
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { CommandInfo } from "../../runtime.js";
|
|
2
|
+
import { redactModelFacingText } from "./common.js";
|
|
3
|
+
import { buildAgentBrowserNextActions } from "../action-recommendations.js";
|
|
4
|
+
import { buildAgentBrowserResultCategoryDetails } from "../categories.js";
|
|
5
|
+
import type { AgentBrowserNextAction, ToolPresentation } from "../contracts.js";
|
|
6
|
+
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
7
|
+
|
|
8
|
+
const STALE_REF_ERROR_HINT = [
|
|
9
|
+
"Agent-browser hint: This ref may be stale after navigation, scrolling, or re-rendering.",
|
|
10
|
+
"Run `snapshot -i` again and retry with a current `@e…` ref; for less ref churn, use `find role|text|label|placeholder|alt|title|testid ...` or `scrollintoview` before interacting with off-screen elements.",
|
|
11
|
+
].join(" ");
|
|
12
|
+
|
|
13
|
+
const SELECTOR_DIALECT_ERROR_HINT = [
|
|
14
|
+
"Agent-browser hint: This selector may use an unsupported selector dialect.",
|
|
15
|
+
"Prefer refs from `snapshot -i`, or use supported `find role|text|label|placeholder|alt|title|testid ...` locators; use `scrollintoview` before interacting with off-screen elements.",
|
|
16
|
+
].join(" ");
|
|
17
|
+
|
|
18
|
+
function getSelectorRecoveryHint(errorText: string): string | undefined {
|
|
19
|
+
const normalized = errorText.trim();
|
|
20
|
+
if (normalized.length === 0) return undefined;
|
|
21
|
+
|
|
22
|
+
if (/\bUnknown ref\b|\bstale ref\b|\bref\b.*\b(?:not found|missing|expired)\b/i.test(normalized)) {
|
|
23
|
+
return STALE_REF_ERROR_HINT;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const mentionsPlaywrightSelectorDialect = /(?:\btext=|:has-text\(|\bgetByRole\b|\bgetByText\b)/i.test(normalized);
|
|
27
|
+
const reportsSelectorMatchFailure =
|
|
28
|
+
/\b(?:no elements? found|failed to find|could not find|unable to find)\b.*\b(?:selector|locator)\b/i.test(normalized) ||
|
|
29
|
+
/\b(?:selector|locator)\b.*\b(?:no elements? found|not found|missing|failed to find|could not find|unable to find)\b/i.test(normalized);
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
/\b(?:unsupported|unknown|invalid)\s+(?:selector|locator)\b/i.test(normalized) ||
|
|
33
|
+
/\bfailed to parse selector\b/i.test(normalized) ||
|
|
34
|
+
/\bselector\b.*\b(?:parse|syntax|unsupported|invalid)\b/i.test(normalized) ||
|
|
35
|
+
(mentionsPlaywrightSelectorDialect && reportsSelectorMatchFailure)
|
|
36
|
+
) {
|
|
37
|
+
return SELECTOR_DIALECT_ERROR_HINT;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return undefined;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface CommandSuggestion {
|
|
44
|
+
args?: string[];
|
|
45
|
+
description: string;
|
|
46
|
+
id?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const UNKNOWN_COMMAND_SUGGESTIONS: Record<string, CommandSuggestion[]> = {
|
|
50
|
+
attr: [{ description: "Use `get attr <selector> <name>` to read an attribute from a selector or current `@ref`." }],
|
|
51
|
+
count: [{ description: "Use `get count <selector>` to count matching elements." }],
|
|
52
|
+
html: [{ description: "Use `get html <selector>` to read element HTML, or `get html` for the page when upstream supports it." }],
|
|
53
|
+
text: [{ description: "Use `get text <selector>` to read text from a selector or current `@ref`; run `snapshot -i` first when you need a safe `@ref`." }],
|
|
54
|
+
title: [{ args: ["get", "title"], description: "Use `get title` to read the current page title.", id: "use-get-title" }],
|
|
55
|
+
url: [{ args: ["get", "url"], description: "Use `get url` to read the current page URL.", id: "use-get-url" }],
|
|
56
|
+
value: [{ description: "Use `get value <selector>` to read form control value from a selector or current `@ref`." }],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function getUnknownCommandSuggestions(command: string | undefined, errorText: string): CommandSuggestion[] {
|
|
60
|
+
if (!command) return [];
|
|
61
|
+
const normalizedCommand = command.trim().toLowerCase();
|
|
62
|
+
if (!/\bunknown\s+command\b|\bunknown\s+subcommand\b|\bunrecognized\s+command\b/i.test(errorText)) return [];
|
|
63
|
+
return UNKNOWN_COMMAND_SUGGESTIONS[normalizedCommand] ?? [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function formatUnknownCommandSuggestionText(suggestions: CommandSuggestion[]): string | undefined {
|
|
67
|
+
if (suggestions.length === 0) return undefined;
|
|
68
|
+
return ["Agent-browser hint: This looks like a getter shortcut, but upstream getter commands are grouped under `get`.", ...suggestions.map((suggestion) => suggestion.description)].join(" ");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function buildUnknownCommandSuggestionActions(suggestions: CommandSuggestion[], sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
|
|
72
|
+
const actions = suggestions
|
|
73
|
+
.filter((suggestion): suggestion is CommandSuggestion & { args: string[]; id: string } => suggestion.args !== undefined && suggestion.id !== undefined)
|
|
74
|
+
.map((suggestion) => ({
|
|
75
|
+
id: suggestion.id,
|
|
76
|
+
params: { args: withOptionalSessionArgs(sessionName, suggestion.args) },
|
|
77
|
+
reason: suggestion.description,
|
|
78
|
+
safety: "Read-only getter command; safe to retry when you intended to inspect page state.",
|
|
79
|
+
tool: "agent_browser" as const,
|
|
80
|
+
}));
|
|
81
|
+
return actions.length > 0 ? actions : undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function appendSelectorRecoveryHint(errorText: string): string {
|
|
85
|
+
const hint = getSelectorRecoveryHint(errorText);
|
|
86
|
+
if (!hint || errorText.includes("Agent-browser hint:")) return errorText;
|
|
87
|
+
return `${errorText}\n\n${hint}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function buildErrorPresentation(options: {
|
|
91
|
+
args?: string[];
|
|
92
|
+
commandInfo: CommandInfo;
|
|
93
|
+
errorText: string;
|
|
94
|
+
sessionName?: string;
|
|
95
|
+
}): ToolPresentation {
|
|
96
|
+
const { args, commandInfo, errorText, sessionName } = options;
|
|
97
|
+
const safeErrorText = redactModelFacingText(errorText);
|
|
98
|
+
const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
|
|
99
|
+
const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
|
|
100
|
+
const unknownCommandSuggestionText = formatUnknownCommandSuggestionText(unknownCommandSuggestions);
|
|
101
|
+
const hintedErrorText = unknownCommandSuggestionText && !selectorHintedErrorText.includes("Agent-browser hint:")
|
|
102
|
+
? `${selectorHintedErrorText}\n\n${unknownCommandSuggestionText}`
|
|
103
|
+
: selectorHintedErrorText;
|
|
104
|
+
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
105
|
+
args: [commandInfo.command, commandInfo.subcommand].filter((item): item is string => item !== undefined),
|
|
106
|
+
command: commandInfo.command,
|
|
107
|
+
errorText: hintedErrorText,
|
|
108
|
+
succeeded: false,
|
|
109
|
+
});
|
|
110
|
+
const nextActions = [
|
|
111
|
+
...(buildUnknownCommandSuggestionActions(unknownCommandSuggestions, sessionName) ?? []),
|
|
112
|
+
...(buildAgentBrowserNextActions({
|
|
113
|
+
args,
|
|
114
|
+
command: commandInfo.command,
|
|
115
|
+
failureCategory: categoryDetails.failureCategory,
|
|
116
|
+
resultCategory: "failure",
|
|
117
|
+
}) ?? []),
|
|
118
|
+
];
|
|
119
|
+
return {
|
|
120
|
+
...categoryDetails,
|
|
121
|
+
content: [{ type: "text", text: hintedErrorText }],
|
|
122
|
+
nextActions: nextActions.length > 0 ? nextActions : undefined,
|
|
123
|
+
summary: hintedErrorText,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Compact oversized model-facing tool output into bounded previews and spill artifacts.
|
|
3
|
+
* Responsibilities: Write full output to persistent/session temp storage, update artifact manifests, and preserve safe previews.
|
|
4
|
+
* Scope: Large-output compaction only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CommandInfo } from "../../runtime.js";
|
|
8
|
+
import {
|
|
9
|
+
type PersistentSessionArtifactEviction,
|
|
10
|
+
type PersistentSessionArtifactStore,
|
|
11
|
+
writePersistentSessionArtifactFile,
|
|
12
|
+
writeSecureTempFile,
|
|
13
|
+
} from "../../temp.js";
|
|
14
|
+
import { buildEvictedSessionArtifactEntries } from "../artifact-manifest.js";
|
|
15
|
+
import type { ArtifactStorageScope, SessionArtifactManifest, SessionArtifactManifestEntry, ToolPresentation } from "../contracts.js";
|
|
16
|
+
import { countLines, truncateText } from "../text.js";
|
|
17
|
+
import { applyArtifactManifest } from "./artifacts.js";
|
|
18
|
+
import { getPresentationText } from "./content.js";
|
|
19
|
+
import { redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
20
|
+
|
|
21
|
+
const LARGE_OUTPUT_INLINE_MAX_CHARS = 8_000;
|
|
22
|
+
|
|
23
|
+
const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
|
|
24
|
+
|
|
25
|
+
const LARGE_OUTPUT_PREVIEW_MAX_CHARS = 2_500;
|
|
26
|
+
|
|
27
|
+
const LARGE_OUTPUT_PREVIEW_MAX_LINES = 40;
|
|
28
|
+
|
|
29
|
+
const LARGE_OUTPUT_FILE_PREFIX = "pi-agent-browser-output";
|
|
30
|
+
|
|
31
|
+
function shouldCompactLargeOutput(text: string): boolean {
|
|
32
|
+
return text.length > LARGE_OUTPUT_INLINE_MAX_CHARS || countLines(text) > LARGE_OUTPUT_INLINE_MAX_LINES;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildLargeOutputPreview(text: string): { omittedLineCount: number; previewText: string } {
|
|
36
|
+
const lines = text.split("\n");
|
|
37
|
+
const previewLines: string[] = [];
|
|
38
|
+
let previewChars = 0;
|
|
39
|
+
for (const line of lines) {
|
|
40
|
+
if (previewLines.length >= LARGE_OUTPUT_PREVIEW_MAX_LINES || previewChars >= LARGE_OUTPUT_PREVIEW_MAX_CHARS) {
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
const remainingChars = LARGE_OUTPUT_PREVIEW_MAX_CHARS - previewChars;
|
|
44
|
+
const previewLine = truncateText(line, Math.max(40, remainingChars));
|
|
45
|
+
previewLines.push(previewLine);
|
|
46
|
+
previewChars += previewLine.length + 1;
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
omittedLineCount: Math.max(0, lines.length - previewLines.length),
|
|
50
|
+
previewText: previewLines.join("\n"),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
interface LargeOutputSpillWriteResult {
|
|
55
|
+
evictedArtifacts: PersistentSessionArtifactEviction[];
|
|
56
|
+
path: string;
|
|
57
|
+
storageScope: ArtifactStorageScope;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function writeLargeOutputSpillFile(options: {
|
|
61
|
+
data: unknown;
|
|
62
|
+
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
63
|
+
text: string;
|
|
64
|
+
}): Promise<LargeOutputSpillWriteResult> {
|
|
65
|
+
const payload =
|
|
66
|
+
typeof options.data === "string"
|
|
67
|
+
? redactModelFacingText(options.data)
|
|
68
|
+
: typeof options.data === "number" || typeof options.data === "boolean"
|
|
69
|
+
? String(options.data)
|
|
70
|
+
: options.data === undefined
|
|
71
|
+
? redactModelFacingText(options.text)
|
|
72
|
+
: stringifyModelFacing(options.data);
|
|
73
|
+
const isStructuredPayload = typeof options.data !== "string" && typeof options.data !== "number" && typeof options.data !== "boolean";
|
|
74
|
+
const fileOptions = {
|
|
75
|
+
content: payload,
|
|
76
|
+
prefix: LARGE_OUTPUT_FILE_PREFIX,
|
|
77
|
+
suffix: isStructuredPayload ? ".json" : ".txt",
|
|
78
|
+
};
|
|
79
|
+
if (options.persistentArtifactStore) {
|
|
80
|
+
const result = await writePersistentSessionArtifactFile({ ...fileOptions, store: options.persistentArtifactStore });
|
|
81
|
+
return { ...result, storageScope: "persistent-session" };
|
|
82
|
+
}
|
|
83
|
+
return { evictedArtifacts: [], path: await writeSecureTempFile(fileOptions), storageScope: "process-temp" };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function buildSpillArtifactEntries(options: {
|
|
87
|
+
commandInfo: CommandInfo;
|
|
88
|
+
evictedArtifacts: PersistentSessionArtifactEviction[];
|
|
89
|
+
path: string;
|
|
90
|
+
storageScope: ArtifactStorageScope;
|
|
91
|
+
}): SessionArtifactManifestEntry[] {
|
|
92
|
+
const nowMs = Date.now();
|
|
93
|
+
return [
|
|
94
|
+
{
|
|
95
|
+
command: options.commandInfo.command,
|
|
96
|
+
createdAtMs: nowMs,
|
|
97
|
+
kind: "spill",
|
|
98
|
+
path: options.path,
|
|
99
|
+
retentionState: options.storageScope === "persistent-session" ? "live" : "ephemeral",
|
|
100
|
+
storageScope: options.storageScope,
|
|
101
|
+
subcommand: options.commandInfo.subcommand,
|
|
102
|
+
},
|
|
103
|
+
...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
|
|
104
|
+
];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function compactLargePresentationOutput(options: {
|
|
108
|
+
artifactManifest?: SessionArtifactManifest;
|
|
109
|
+
commandInfo: CommandInfo;
|
|
110
|
+
data: unknown;
|
|
111
|
+
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
112
|
+
presentation: ToolPresentation;
|
|
113
|
+
}): Promise<ToolPresentation> {
|
|
114
|
+
const text = getPresentationText(options.presentation);
|
|
115
|
+
if (text.length === 0 || !shouldCompactLargeOutput(text)) {
|
|
116
|
+
return options.presentation;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
let fullOutputPath: string | undefined;
|
|
120
|
+
let spill: LargeOutputSpillWriteResult | undefined;
|
|
121
|
+
let spillErrorText: string | undefined;
|
|
122
|
+
try {
|
|
123
|
+
spill = await writeLargeOutputSpillFile({
|
|
124
|
+
data: options.data,
|
|
125
|
+
persistentArtifactStore: options.persistentArtifactStore,
|
|
126
|
+
text,
|
|
127
|
+
});
|
|
128
|
+
fullOutputPath = spill.path;
|
|
129
|
+
} catch (error) {
|
|
130
|
+
spillErrorText = error instanceof Error ? error.message : String(error);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const { omittedLineCount, previewText } = buildLargeOutputPreview(text);
|
|
134
|
+
const commandLabel = options.commandInfo.command ?? "agent-browser";
|
|
135
|
+
const lines = [
|
|
136
|
+
`Large ${commandLabel} output compacted.`,
|
|
137
|
+
"",
|
|
138
|
+
"Preview:",
|
|
139
|
+
previewText,
|
|
140
|
+
];
|
|
141
|
+
if (omittedLineCount > 0) {
|
|
142
|
+
lines.push(`- ... (${omittedLineCount} additional lines omitted)`);
|
|
143
|
+
}
|
|
144
|
+
lines.push(
|
|
145
|
+
"",
|
|
146
|
+
fullOutputPath
|
|
147
|
+
? `Full output path: ${fullOutputPath}`
|
|
148
|
+
: `Full output unavailable: ${spillErrorText ?? "spill file could not be created."}`,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const firstTextIndex = options.presentation.content.findIndex((part) => part.type === "text");
|
|
152
|
+
const compactedText = lines.join("\n");
|
|
153
|
+
if (firstTextIndex >= 0) {
|
|
154
|
+
options.presentation.content[firstTextIndex] = { type: "text", text: compactedText };
|
|
155
|
+
} else {
|
|
156
|
+
options.presentation.content.unshift({ type: "text", text: compactedText });
|
|
157
|
+
}
|
|
158
|
+
options.presentation.data = {
|
|
159
|
+
compacted: true,
|
|
160
|
+
fullOutputPath,
|
|
161
|
+
outputCharCount: text.length,
|
|
162
|
+
outputLineCount: countLines(text),
|
|
163
|
+
previewCharCount: previewText.length,
|
|
164
|
+
previewLineCount: countLines(previewText),
|
|
165
|
+
spillError: spillErrorText,
|
|
166
|
+
};
|
|
167
|
+
options.presentation.fullOutputPath = fullOutputPath;
|
|
168
|
+
options.presentation.summary = `${options.presentation.summary} (compact)`;
|
|
169
|
+
if (fullOutputPath && spill) {
|
|
170
|
+
return applyArtifactManifest(
|
|
171
|
+
options.presentation,
|
|
172
|
+
options.presentation.artifactManifest ?? options.artifactManifest,
|
|
173
|
+
buildSpillArtifactEntries({
|
|
174
|
+
commandInfo: options.commandInfo,
|
|
175
|
+
evictedArtifacts: spill.evictedArtifacts,
|
|
176
|
+
path: fullOutputPath,
|
|
177
|
+
storageScope: spill.storageScope,
|
|
178
|
+
}),
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return options.presentation;
|
|
182
|
+
}
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Purpose: Format scalar extraction results, navigation summaries, and page-change summaries.
|
|
3
|
+
* Responsibilities: Keep navigation/extraction presentation separate from core tool result orchestration.
|
|
4
|
+
* Scope: Navigation and get/eval extraction formatting only.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { isRecord } from "../../parsing.js";
|
|
8
|
+
import type { CommandInfo } from "../../runtime.js";
|
|
9
|
+
import { detectConfirmationRequired } from "../confirmation.js";
|
|
10
|
+
import type { AgentBrowserPageChangeSummary, FileArtifactMetadata } from "../contracts.js";
|
|
11
|
+
import { redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
12
|
+
|
|
13
|
+
const NAVIGATION_SUMMARY_COMMANDS = new Set(["back", "click", "dblclick", "forward", "reload"]);
|
|
14
|
+
|
|
15
|
+
const PAGE_CHANGE_SUMMARY_COMMANDS = new Set([
|
|
16
|
+
"back",
|
|
17
|
+
"check",
|
|
18
|
+
"click",
|
|
19
|
+
"dblclick",
|
|
20
|
+
"dialog",
|
|
21
|
+
"download",
|
|
22
|
+
"fill",
|
|
23
|
+
"forward",
|
|
24
|
+
"goto",
|
|
25
|
+
"hover",
|
|
26
|
+
"navigate",
|
|
27
|
+
"open",
|
|
28
|
+
"pdf",
|
|
29
|
+
"press",
|
|
30
|
+
"pushstate",
|
|
31
|
+
"reload",
|
|
32
|
+
"screenshot",
|
|
33
|
+
"scroll",
|
|
34
|
+
"scrollintoview",
|
|
35
|
+
"select",
|
|
36
|
+
"swipe",
|
|
37
|
+
"tap",
|
|
38
|
+
"type",
|
|
39
|
+
"uncheck",
|
|
40
|
+
]);
|
|
41
|
+
|
|
42
|
+
const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
|
|
43
|
+
|
|
44
|
+
interface NavigationSummary {
|
|
45
|
+
title?: string;
|
|
46
|
+
url?: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function getScalarExtractionResult(data: Record<string, unknown>): string | undefined {
|
|
50
|
+
const { result } = data;
|
|
51
|
+
if (typeof result === "string") {
|
|
52
|
+
return result.trim().length > 0 ? result : undefined;
|
|
53
|
+
}
|
|
54
|
+
if (typeof result === "number" || typeof result === "boolean") {
|
|
55
|
+
return String(result);
|
|
56
|
+
}
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function getExtractionOrigin(data: Record<string, unknown>): string | undefined {
|
|
61
|
+
if (typeof data.origin === "string" && data.origin.trim().length > 0) {
|
|
62
|
+
return data.origin.trim();
|
|
63
|
+
}
|
|
64
|
+
if (typeof data.url === "string" && data.url.trim().length > 0) {
|
|
65
|
+
return data.url.trim();
|
|
66
|
+
}
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function formatGetSummaryLabel(subcommand: string | undefined): string {
|
|
71
|
+
if (!subcommand) {
|
|
72
|
+
return "Get result";
|
|
73
|
+
}
|
|
74
|
+
if (subcommand.toLowerCase() === "url") {
|
|
75
|
+
return "URL";
|
|
76
|
+
}
|
|
77
|
+
return `${subcommand.slice(0, 1).toUpperCase()}${subcommand.slice(1)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function formatExtractionSummary(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
81
|
+
const scalarResult = getScalarExtractionResult(data);
|
|
82
|
+
if (!scalarResult) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const safeScalarResult = redactModelFacingText(scalarResult);
|
|
86
|
+
const firstResultLine = safeScalarResult.split("\n", 1)[0] ?? safeScalarResult;
|
|
87
|
+
if (commandInfo.command === "get") {
|
|
88
|
+
return `${formatGetSummaryLabel(commandInfo.subcommand)}: ${firstResultLine}`;
|
|
89
|
+
}
|
|
90
|
+
if (commandInfo.command === "eval") {
|
|
91
|
+
return `Eval result: ${firstResultLine}`;
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function formatExtractionText(commandInfo: CommandInfo, data: Record<string, unknown>): string | undefined {
|
|
97
|
+
if (commandInfo.command !== "get" && commandInfo.command !== "eval") {
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
const scalarResult = getScalarExtractionResult(data);
|
|
101
|
+
if (!scalarResult) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
const origin = getExtractionOrigin(data);
|
|
105
|
+
const safeScalarResult = redactModelFacingText(scalarResult);
|
|
106
|
+
const safeOrigin = origin ? redactModelFacingText(origin) : undefined;
|
|
107
|
+
return safeOrigin && safeOrigin !== safeScalarResult ? `${safeScalarResult}\n\nOrigin: ${safeOrigin}` : safeScalarResult;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function isNavigationObservableCommand(command: string | undefined): boolean {
|
|
111
|
+
return command !== undefined && NAVIGATION_SUMMARY_COMMANDS.has(command);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isNavigationSummary(value: unknown): value is NavigationSummary {
|
|
115
|
+
return isRecord(value) && (typeof value.title === "string" || typeof value.url === "string");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export function getNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
|
|
119
|
+
const candidate = data[NAVIGATION_SUMMARY_FIELD];
|
|
120
|
+
return isNavigationSummary(candidate) ? candidate : undefined;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getTopLevelNavigationSummary(data: Record<string, unknown>): NavigationSummary | undefined {
|
|
124
|
+
return isNavigationSummary(data)
|
|
125
|
+
? {
|
|
126
|
+
title: typeof data.title === "string" ? data.title : undefined,
|
|
127
|
+
url: typeof data.url === "string" ? data.url : undefined,
|
|
128
|
+
}
|
|
129
|
+
: undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function getNormalizedNavigationSummary(summary: NavigationSummary | undefined): { title?: string; url?: string } | undefined {
|
|
133
|
+
const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
|
|
134
|
+
const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
|
|
135
|
+
return title || url ? { title, url } : undefined;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function formatNavigationSummary(summary: NavigationSummary): string | undefined {
|
|
139
|
+
const normalized = getNormalizedNavigationSummary(summary);
|
|
140
|
+
if (!normalized) return undefined;
|
|
141
|
+
if (normalized.title && normalized.url) return `${normalized.title}\n${normalized.url}`;
|
|
142
|
+
return normalized.title ?? normalized.url;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function isPageChangeSummaryCommand(command: string | undefined): boolean {
|
|
146
|
+
return command !== undefined && PAGE_CHANGE_SUMMARY_COMMANDS.has(command);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function buildPageChangeSummary(options: {
|
|
150
|
+
artifacts?: FileArtifactMetadata[];
|
|
151
|
+
commandInfo: CommandInfo;
|
|
152
|
+
data: unknown;
|
|
153
|
+
nextActions?: Array<{ id: string }>;
|
|
154
|
+
savedFilePath?: string;
|
|
155
|
+
summary: string;
|
|
156
|
+
}): AgentBrowserPageChangeSummary | undefined {
|
|
157
|
+
const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
|
|
158
|
+
const artifactCount = artifacts?.length ?? 0;
|
|
159
|
+
const navigation = isRecord(data)
|
|
160
|
+
? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
|
|
161
|
+
: undefined;
|
|
162
|
+
const confirmationRequired = detectConfirmationRequired(data) !== undefined;
|
|
163
|
+
if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
const changeType: AgentBrowserPageChangeSummary["changeType"] = savedFilePath || artifactCount > 0
|
|
167
|
+
? "artifact"
|
|
168
|
+
: navigation
|
|
169
|
+
? "navigation"
|
|
170
|
+
: confirmationRequired
|
|
171
|
+
? "confirmation"
|
|
172
|
+
: "mutation";
|
|
173
|
+
const parts = [commandInfo.command ?? "agent-browser", changeType];
|
|
174
|
+
if (navigation?.title) parts.push(navigation.title);
|
|
175
|
+
if (navigation?.url) parts.push(navigation.url);
|
|
176
|
+
if (savedFilePath) parts.push(savedFilePath);
|
|
177
|
+
else if (artifactCount > 0) parts.push(`${artifactCount} artifact${artifactCount === 1 ? "" : "s"}`);
|
|
178
|
+
return {
|
|
179
|
+
...(artifactCount > 0 ? { artifactCount } : {}),
|
|
180
|
+
changeType,
|
|
181
|
+
...(commandInfo.command ? { command: commandInfo.command } : {}),
|
|
182
|
+
...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
|
|
183
|
+
...(savedFilePath ? { savedFilePath } : {}),
|
|
184
|
+
summary: parts.join(" → "),
|
|
185
|
+
...(navigation?.title ? { title: navigation.title } : {}),
|
|
186
|
+
...(navigation?.url ? { url: navigation.url } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function stripNavigationSummary(data: Record<string, unknown>): Record<string, unknown> {
|
|
191
|
+
const { [NAVIGATION_SUMMARY_FIELD]: _navigationSummary, ...rest } = data;
|
|
192
|
+
return rest;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function formatNavigationActionResult(data: Record<string, unknown>): string | undefined {
|
|
196
|
+
const actionData = stripNavigationSummary(data);
|
|
197
|
+
const lines: string[] = [];
|
|
198
|
+
if (typeof actionData.clicked === "string" || typeof actionData.clicked === "boolean") {
|
|
199
|
+
lines.push(`Clicked: ${String(actionData.clicked)}`);
|
|
200
|
+
}
|
|
201
|
+
if (typeof actionData.href === "string") {
|
|
202
|
+
lines.push(`Href: ${redactModelFacingText(actionData.href)}`);
|
|
203
|
+
}
|
|
204
|
+
if (typeof actionData.navigated === "boolean") {
|
|
205
|
+
lines.push(`Navigated: ${actionData.navigated}`);
|
|
206
|
+
}
|
|
207
|
+
if (lines.length > 0) {
|
|
208
|
+
return lines.join("\n");
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const actionText = stringifyModelFacing(actionData).trim();
|
|
212
|
+
if (actionText.length === 0 || actionText === "{}") {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
return actionText;
|
|
216
|
+
}
|