pi-agent-browser-native 0.8.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/README.md +11 -11
- package/dist/extensions/agent-browser/index.js +93 -213
- package/dist/extensions/agent-browser/lib/browser-journal.js +33 -12
- package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
- package/dist/extensions/agent-browser/lib/native-output.js +46 -0
- package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +30 -16
- package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -12
- package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
- package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +15 -8
- package/dist/extensions/agent-browser/lib/tool-surface.js +39 -11
- package/dist/extensions/agent-browser/lib/web-search.js +85 -64
- package/docs/ARCHITECTURE.md +13 -15
- package/docs/COMMAND_REFERENCE.md +4 -2
- package/docs/RELEASE.md +25 -19
- package/docs/REQUIREMENTS.md +5 -5
- package/docs/SESSION_STORAGE.md +4 -4
- package/docs/SUPPORT_MATRIX.md +12 -10
- package/docs/TOOL_CONTRACT.md +9 -5
- package/package.json +4 -4
- package/scripts/doctor.mjs +2 -2
|
@@ -241,14 +241,7 @@ export async function readBrowserEntries(manager, physical = false) {
|
|
|
241
241
|
const header = metadata.find(entry => entry.value.type === "session");
|
|
242
242
|
if (header?.value.id !== (manager.getHeader()?.id ?? manager.getSessionId()))
|
|
243
243
|
throw new Error("Pi session journal identity does not match the active session.");
|
|
244
|
-
const publicManager = manager;
|
|
245
244
|
const selected = physical ? metadata.filter(entry => entry.value.type !== "session") : branchEntries(metadata.filter(entry => entry.value.type !== "session"), leaf);
|
|
246
|
-
if (publicManager.iterateEntryMetadata) {
|
|
247
|
-
const native = new Map([...publicManager.iterateEntryMetadata(physical ? {} : { branchFrom: leaf })].map(entry => [entry.id, entry.parentId]));
|
|
248
|
-
for (const entry of selected)
|
|
249
|
-
if (typeof entry.value.id !== "string" || native.get(entry.value.id) !== entry.value.parentId)
|
|
250
|
-
throw new Error("Published browser journal ancestry differs from the native selected boundary.");
|
|
251
|
-
}
|
|
252
245
|
const projected = [];
|
|
253
246
|
const definitions = new Map();
|
|
254
247
|
for (const entry of selected) {
|
|
@@ -285,9 +278,7 @@ export async function readBrowserEntries(manager, physical = false) {
|
|
|
285
278
|
throw new Error(`Browser snapshot ${id} has no ancestral definition; inspect and take a fresh snapshot.`);
|
|
286
279
|
// ponytail: a requested individual snapshot must fit its consumer's memory. Metadata
|
|
287
280
|
// scans skip unlimited unrelated values; upgrade with native per-value paging if needed.
|
|
288
|
-
const
|
|
289
|
-
const body = native && isRecord(native) && isRecord(native.data) ? { data: native.data }
|
|
290
|
-
: await projectJson(readRange(file, entry), [["data", "snapshot"]], Infinity);
|
|
281
|
+
const body = await projectJson(readRange(file, entry), [["data", "snapshot"]], Infinity);
|
|
291
282
|
const destination = projected.find(candidate => isRecord(candidate) && candidate.id === entry.value.id);
|
|
292
283
|
if (!isRecord(destination) || !isRecord(destination.data) || !isRecord(body?.data))
|
|
293
284
|
throw new Error("Winning browser snapshot could not be read.");
|
|
@@ -342,13 +333,22 @@ export function hasPublishedBrowserJournal(manager) {
|
|
|
342
333
|
/** Capture before waiting: native selection precedes sequential session_tree handlers. */
|
|
343
334
|
export function captureBrowserBranch(manager, isGenerationCurrent) {
|
|
344
335
|
const sessionId = manager.getSessionId();
|
|
345
|
-
|
|
336
|
+
let anchorId = manager.getLeafId();
|
|
337
|
+
let validatedAnchor;
|
|
338
|
+
let validatedLeaf;
|
|
339
|
+
const branch = { sessionId, anchorId, isCurrent() {
|
|
346
340
|
if (!isGenerationCurrent() || manager.getSessionId() !== sessionId)
|
|
347
341
|
return false;
|
|
342
|
+
if (branch.anchorId !== anchorId) {
|
|
343
|
+
validatedAnchor = validatedLeaf = undefined;
|
|
344
|
+
anchorId = branch.anchorId;
|
|
345
|
+
}
|
|
348
346
|
let leaf = manager.getLeafId();
|
|
349
347
|
// Empty selection is not a universal ancestor of other independent roots.
|
|
350
348
|
let current = branch.anchorId === null && leaf === null;
|
|
351
349
|
const seen = new Set();
|
|
350
|
+
let selected;
|
|
351
|
+
let anchor;
|
|
352
352
|
while (leaf !== null) {
|
|
353
353
|
if (typeof leaf !== "string" || seen.has(leaf))
|
|
354
354
|
throw new Error("Selected Pi journal ancestry is invalid or cyclic.");
|
|
@@ -356,10 +356,31 @@ export function captureBrowserBranch(manager, isGenerationCurrent) {
|
|
|
356
356
|
const entry = manager.getEntry(leaf);
|
|
357
357
|
if (!entry || entry.id !== leaf || entry.parentId !== null && typeof entry.parentId !== "string")
|
|
358
358
|
throw new Error("Selected Pi journal ancestry is incomplete.");
|
|
359
|
-
|
|
359
|
+
selected ??= { entry, parentId: entry.parentId };
|
|
360
|
+
const cached = leaf === branch.anchorId ? validatedAnchor : leaf === validatedLeaf?.entry.id ? validatedLeaf : undefined;
|
|
361
|
+
if (cached) {
|
|
362
|
+
// Keep the native indexed lookup: it also refreshes fork journal identity.
|
|
363
|
+
if (cached.entry === entry && cached.parentId === entry.parentId) {
|
|
364
|
+
current = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
validatedAnchor = validatedLeaf = undefined;
|
|
368
|
+
}
|
|
369
|
+
if (leaf === branch.anchorId) {
|
|
360
370
|
current = true;
|
|
371
|
+
anchor = { entry, parentId: entry.parentId };
|
|
372
|
+
}
|
|
361
373
|
leaf = entry.parentId;
|
|
362
374
|
}
|
|
375
|
+
// Native entries/parents are append-only. Cache only after validating through
|
|
376
|
+
// the root (or an already validated endpoint), never merely on leaf === anchor.
|
|
377
|
+
// ponytail: behind-host mutation of an old ancestor without native reseed or
|
|
378
|
+
// generation change is outside this contract; durable file proofs
|
|
379
|
+
// remain in replay/append. Upgrade if Pi exposes a separate ancestry revision.
|
|
380
|
+
if (current && selected) {
|
|
381
|
+
validatedAnchor = anchor ?? validatedAnchor;
|
|
382
|
+
validatedLeaf = selected;
|
|
383
|
+
}
|
|
363
384
|
return current;
|
|
364
385
|
} };
|
|
365
386
|
return branch;
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join } from "node:path";
|
|
3
3
|
import { isRecord } from "./parsing.js";
|
|
4
|
-
import { getBrowserResultMessage } from "./browser-transcript.js";
|
|
5
|
-
import { getAgentBrowserSessionIdentityKey } from "./argv-grammar.js";
|
|
6
4
|
function isDirectoryOwner(source) {
|
|
7
5
|
if (/^(?:npm:pi-change-working-dir|git:github\.com\/fitchmultz\/pi-change-working-dir(?:\.git)?)(?:@.+)?$/.test(source.source))
|
|
8
6
|
return true;
|
|
@@ -50,17 +48,3 @@ export function getBrowserCwdError(cwd) {
|
|
|
50
48
|
}
|
|
51
49
|
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
50
|
}
|
|
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
|
-
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { JsonSchema } from "./json-schema.js";
|
|
2
|
+
import { isRecord } from "./parsing.js";
|
|
3
|
+
import { finalizeAgentBrowserFailure } from "./pi-tool-rendering.js";
|
|
4
|
+
import { isPlainTextInspectionArgs } from "./runtime.js";
|
|
5
|
+
import { projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./results/presentation/content.js";
|
|
6
|
+
export const AGENT_BROWSER_NAMESPACE = {
|
|
7
|
+
name: "browser",
|
|
8
|
+
description: "Browser commands, bounded browser code, advanced UI checks, Electron, and optional web search.",
|
|
9
|
+
instructions: "Use agent_browser for native commands and batch --bail for fixed sequences. Use agent_browser_code for a fresh bounded JS cell with a persistent browser. Enable advanced direct tools with agent_browser_tools before calling them. Check success and nextActions; structured imageObservations are capture metadata, not inline images. Optional web search is available only when configured. Full browser instructions remain in the agent_browser prompt section.",
|
|
10
|
+
};
|
|
11
|
+
export const AGENT_BROWSER_OUTPUT_SCHEMA = JsonSchema.Object({
|
|
12
|
+
success: JsonSchema.Boolean(),
|
|
13
|
+
resultCategory: JsonSchema.Union([JsonSchema.Literal("success"), JsonSchema.Literal("failure")]),
|
|
14
|
+
data: JsonSchema.Optional(JsonSchema.Unsafe({})),
|
|
15
|
+
error: JsonSchema.Optional(JsonSchema.Unsafe({})),
|
|
16
|
+
summary: JsonSchema.Optional(JsonSchema.String()),
|
|
17
|
+
nextActions: JsonSchema.Optional(JsonSchema.Array(JsonSchema.Object({}))),
|
|
18
|
+
}, { additionalProperties: true });
|
|
19
|
+
/** Final public boundary, after execution, persistence, and any requested output export. */
|
|
20
|
+
export async function finalizeAgentBrowserNativeResult(result, input) {
|
|
21
|
+
const finalized = finalizeAgentBrowserFailure(result, input);
|
|
22
|
+
// Native help/version stays plain text; the entrypoint intentionally skips observation rendering for it.
|
|
23
|
+
if (isRecord(input) && Array.isArray(input.args) && isPlainTextInspectionArgs(input.args))
|
|
24
|
+
return finalized;
|
|
25
|
+
const succeeded = finalized.isError !== true;
|
|
26
|
+
// The normal entrypoint supplies the exact bounded projection, including its complete spill.
|
|
27
|
+
if (isRecord(result.structuredContent) && result.structuredContent.success === succeeded)
|
|
28
|
+
return finalized;
|
|
29
|
+
const details = isRecord(result.details) ? result.details : {};
|
|
30
|
+
const observation = projectAgentBrowserObservation(details, succeeded);
|
|
31
|
+
if (JSON.stringify(observation).length > OBSERVATION_INLINE_MAX_CHARS) {
|
|
32
|
+
const { renderAgentBrowserObservation } = await import("./results/presentation/large-output.js");
|
|
33
|
+
const rendered = await renderAgentBrowserObservation({
|
|
34
|
+
content: finalized.content, details, succeeded,
|
|
35
|
+
json: isRecord(input) && ("code" in input || Array.isArray(input.args) && input.args.includes("--json")),
|
|
36
|
+
// The caller's exact visible output stays inline (for example formatted search results); only the structured field is bounded.
|
|
37
|
+
preserveContent: true,
|
|
38
|
+
});
|
|
39
|
+
const { artifactManifest, ...renderedResult } = rendered;
|
|
40
|
+
return finalizeAgentBrowserFailure({
|
|
41
|
+
...finalized, ...renderedResult,
|
|
42
|
+
details: artifactManifest ? { ...details, artifactManifest } : details,
|
|
43
|
+
}, input);
|
|
44
|
+
}
|
|
45
|
+
return { ...finalized, structuredContent: JSON.parse(JSON.stringify(observation)) };
|
|
46
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { getKeybindings, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
-
import { BROWSER_RESULT_TOOLS } from "./browser-transcript.js";
|
|
3
2
|
import { compileAgentBrowserElectron } from "./input-modes/electron.js";
|
|
4
3
|
import { compileAgentBrowserQaPreset } from "./input-modes/job.js";
|
|
5
4
|
import { compileAgentBrowserNetworkSourceLookup, compileAgentBrowserSourceLookup } from "./input-modes/lookups.js";
|
|
@@ -185,10 +184,10 @@ function formatModelVisibleFailureCategoryNotice(details) {
|
|
|
185
184
|
: undefined;
|
|
186
185
|
return `Result category: failure${failureCategory ? `; failureCategory: ${failureCategory}` : ""}; Pi tool isError: true.`;
|
|
187
186
|
}
|
|
188
|
-
function agentBrowserToolResultRequestedJson(
|
|
189
|
-
const details = isRecord(
|
|
187
|
+
function agentBrowserToolResultRequestedJson(result, input) {
|
|
188
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
190
189
|
const detailArgs = Array.isArray(details?.args) ? details.args : undefined;
|
|
191
|
-
const inputArgs = isRecord(
|
|
190
|
+
const inputArgs = isRecord(input) && Array.isArray(input.args) ? input.args : undefined;
|
|
192
191
|
return detailArgs?.includes("--json") === true || inputArgs?.includes("--json") === true;
|
|
193
192
|
}
|
|
194
193
|
function agentBrowserToolResultHasParseableJsonContent(content) {
|
|
@@ -219,26 +218,41 @@ function appendModelVisibleFailureCategoryNotice(content, notice) {
|
|
|
219
218
|
? { ...item, text: `${textItem.text}\n\n${notice}` }
|
|
220
219
|
: item);
|
|
221
220
|
}
|
|
222
|
-
export function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const
|
|
226
|
-
const
|
|
227
|
-
const content = notice ? appendModelVisibleFailureCategoryNotice(event.content, notice) : undefined;
|
|
228
|
-
const shouldMarkError = isRecord(event.details) && event.details.resultCategory === "failure" && event.isError !== true;
|
|
229
|
-
if (!shouldMarkError && !content)
|
|
230
|
-
return undefined;
|
|
221
|
+
export function finalizeAgentBrowserFailure(result, input) {
|
|
222
|
+
const failed = result.isError === true || (isRecord(result.details) && result.details.resultCategory === "failure");
|
|
223
|
+
const preservesParseableJson = (isRecord(input) && "code" in input || agentBrowserToolResultRequestedJson(result, input)) && agentBrowserToolResultHasParseableJsonContent(result.content);
|
|
224
|
+
const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(result.details);
|
|
225
|
+
const content = notice ? appendModelVisibleFailureCategoryNotice(result.content, notice) : undefined;
|
|
231
226
|
return {
|
|
232
|
-
...
|
|
233
|
-
|
|
227
|
+
...result,
|
|
228
|
+
content: content ?? result.content,
|
|
229
|
+
isError: failed,
|
|
234
230
|
};
|
|
235
231
|
}
|
|
236
232
|
export class AgentBrowserResultComponent {
|
|
237
233
|
expanded = false;
|
|
238
234
|
theme;
|
|
239
235
|
text = new Text("", 0, 0);
|
|
236
|
+
value;
|
|
237
|
+
formatKey;
|
|
238
|
+
setResult(result, options, theme, isError) {
|
|
239
|
+
const details = isRecord(result.details) ? result.details : undefined;
|
|
240
|
+
// Theme is a stable proxy in Pi; its resolved colors identify theme/terminal-color changes.
|
|
241
|
+
const key = [getPrimaryTextContent(result), details?.summary, details?.resultCategory, details?.failureCategory, options.isPartial, isError, theme, theme.colors, theme.fg];
|
|
242
|
+
if (!this.formatKey || key.some((value, index) => value !== this.formatKey[index])) {
|
|
243
|
+
this.formatKey = key;
|
|
244
|
+
this.setState(formatAgentBrowserRenderResult(result, options, theme, isError), options.expanded, theme);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
this.expanded = options.expanded;
|
|
248
|
+
this.theme = theme;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
240
251
|
setState(value, expanded, theme) {
|
|
241
|
-
this.
|
|
252
|
+
if (value !== this.value) {
|
|
253
|
+
this.text.setText(value);
|
|
254
|
+
this.value = value;
|
|
255
|
+
}
|
|
242
256
|
this.expanded = expanded;
|
|
243
257
|
this.theme = theme;
|
|
244
258
|
}
|
|
@@ -199,7 +199,7 @@ export function buildPromptPolicy(prompt) {
|
|
|
199
199
|
requestedArtifacts: extractPromptRequestedArtifacts(prompt),
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
|
-
function getMessageText(content) {
|
|
202
|
+
export function getMessageText(content) {
|
|
203
203
|
if (typeof content === "string")
|
|
204
204
|
return content;
|
|
205
205
|
if (!Array.isArray(content))
|
|
@@ -213,17 +213,21 @@ function getMessageText(content) {
|
|
|
213
213
|
.filter((text) => text.length > 0)
|
|
214
214
|
.join("\n");
|
|
215
215
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
216
|
+
/** Raw prompt intent survives compaction/context edits; only restoration walks ancestry. */
|
|
217
|
+
export function getLatestUserMessage(manager) {
|
|
218
|
+
let id = manager.getLeafId();
|
|
219
|
+
const seen = new Set();
|
|
220
|
+
while (id !== null) {
|
|
221
|
+
if (typeof id !== "string" || seen.has(id))
|
|
222
|
+
throw new Error("Latest user prompt ancestry is invalid or cyclic.");
|
|
223
|
+
seen.add(id);
|
|
224
|
+
const entry = manager.getEntry(id);
|
|
225
|
+
if (!entry || entry.id !== id || entry.parentId !== null && typeof entry.parentId !== "string") {
|
|
226
|
+
throw new Error("Latest user prompt ancestry is incomplete.");
|
|
225
227
|
}
|
|
226
|
-
|
|
228
|
+
if (entry.type === "message" && entry.message.role === "user")
|
|
229
|
+
return entry.message;
|
|
230
|
+
id = entry.parentId;
|
|
227
231
|
}
|
|
228
|
-
return
|
|
232
|
+
return undefined;
|
|
229
233
|
}
|
|
@@ -2,6 +2,7 @@ import { isRecord } from "../../parsing.js";
|
|
|
2
2
|
import { redactSensitiveValue } from "../../runtime.js";
|
|
3
3
|
import { omitUpstreamLifecycle } from "./common.js";
|
|
4
4
|
import { redactTimeoutPartialProgress } from "../../orchestration/browser-run/diagnostics.js";
|
|
5
|
+
export const OBSERVATION_INLINE_MAX_CHARS = 16_000;
|
|
5
6
|
export function isStringArray(value) {
|
|
6
7
|
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
7
8
|
}
|
|
@@ -40,6 +41,9 @@ export function projectAgentBrowserObservation(details, succeeded) {
|
|
|
40
41
|
}
|
|
41
42
|
if (details.inspection === true && details.data === undefined && typeof details.stdout === "string")
|
|
42
43
|
observation.data = details.stdout;
|
|
44
|
+
// Identical error/summary strings double-count large failure text against the observation bound; keep error.
|
|
45
|
+
if (typeof observation.error === "string" && observation.error === observation.summary)
|
|
46
|
+
observation.summary = undefined;
|
|
43
47
|
if (isRecord(observation.data))
|
|
44
48
|
observation.data = omitUpstreamLifecycle(observation.data);
|
|
45
49
|
if (Array.isArray(details.batchSteps)) {
|
|
@@ -2,7 +2,7 @@ import { writePersistentSessionArtifactFile, writeSecureTempFile, } from "../../
|
|
|
2
2
|
import { buildEvictedSessionArtifactEntries } from "../artifact-manifest.js";
|
|
3
3
|
import { countLines, truncateText } from "../text.js";
|
|
4
4
|
import { applyArtifactManifest } from "./artifacts.js";
|
|
5
|
-
import { getPresentationText, projectAgentBrowserObservation } from "./content.js";
|
|
5
|
+
import { getPresentationText, projectAgentBrowserObservation, OBSERVATION_INLINE_MAX_CHARS } from "./content.js";
|
|
6
6
|
import { redactModelFacingText, stringifyModelFacing } from "./common.js";
|
|
7
7
|
const LARGE_OUTPUT_INLINE_MAX_CHARS = 8_000;
|
|
8
8
|
const LARGE_OUTPUT_INLINE_MAX_LINES = 120;
|
|
@@ -88,16 +88,17 @@ function buildSpillArtifactEntries(options) {
|
|
|
88
88
|
...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
|
|
89
89
|
];
|
|
90
90
|
}
|
|
91
|
-
export
|
|
91
|
+
export { OBSERVATION_INLINE_MAX_CHARS } from "./content.js";
|
|
92
92
|
/** Bound the final model-visible result, after recovery/diagnostic assembly. Never truncate executable actions. */
|
|
93
93
|
export async function renderAgentBrowserObservation(options) {
|
|
94
94
|
const observation = projectAgentBrowserObservation(options.details, options.succeeded);
|
|
95
|
+
let structuredContent = observation;
|
|
95
96
|
const images = options.content.filter(part => part.type === "image");
|
|
96
97
|
const prose = options.content.filter(part => part.type === "text").map(part => part.text).join("\n\n");
|
|
97
98
|
const { data: _data, error: _error, summary: _summary, ...metadata } = observation;
|
|
98
99
|
let text = options.json ? JSON.stringify(observation, null, 2) : `${prose}\n\nObservation: ${JSON.stringify(metadata)}`;
|
|
99
100
|
let artifactManifest = options.details.artifactManifest;
|
|
100
|
-
if (text.length > OBSERVATION_INLINE_MAX_CHARS) {
|
|
101
|
+
if (text.length > OBSERVATION_INLINE_MAX_CHARS || JSON.stringify(observation).length > OBSERVATION_INLINE_MAX_CHARS) {
|
|
101
102
|
let spill;
|
|
102
103
|
let spillError;
|
|
103
104
|
try {
|
|
@@ -114,7 +115,8 @@ export async function renderAgentBrowserObservation(options) {
|
|
|
114
115
|
const compact = {
|
|
115
116
|
success: options.succeeded, resultCategory: observation.resultCategory,
|
|
116
117
|
failureCategory: observation.failureCategory, successCategory: observation.successCategory,
|
|
117
|
-
summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700)
|
|
118
|
+
summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700)
|
|
119
|
+
: typeof observation.error === "string" ? truncateText(observation.error, 700) : undefined,
|
|
118
120
|
compacted: true,
|
|
119
121
|
...(spill ? { observationPath: spill.path, retrieve: "Read observationPath for the complete redacted observation, including exact recovery actions and requested data." }
|
|
120
122
|
: { observationUnavailable: truncateText(redactModelFacingText(spillError ?? "Spill could not be written; request a smaller result."), 1_000) }),
|
|
@@ -123,11 +125,16 @@ export async function renderAgentBrowserObservation(options) {
|
|
|
123
125
|
if (observation[key] !== undefined && JSON.stringify({ ...compact, [key]: observation[key] }, null, 2).length <= OBSERVATION_INLINE_MAX_CHARS - 500)
|
|
124
126
|
compact[key] = observation[key];
|
|
125
127
|
}
|
|
126
|
-
|
|
127
|
-
if (
|
|
128
|
-
text =
|
|
128
|
+
structuredContent = compact;
|
|
129
|
+
if (options.preserveContent)
|
|
130
|
+
text = prose;
|
|
131
|
+
else {
|
|
132
|
+
text = JSON.stringify(compact, null, 2);
|
|
133
|
+
if (!options.json)
|
|
134
|
+
text = `Browser observation compacted.\n${text}`;
|
|
135
|
+
}
|
|
129
136
|
}
|
|
130
|
-
return { content: [{ type: "text", text }, ...images], artifactManifest };
|
|
137
|
+
return { content: [{ type: "text", text }, ...images], artifactManifest, structuredContent: JSON.parse(JSON.stringify(structuredContent)) };
|
|
131
138
|
}
|
|
132
139
|
export async function compactLargePresentationOutput(options) {
|
|
133
140
|
const text = getPresentationText(options.presentation);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Text } from "@earendil-works/pi-tui";
|
|
2
2
|
import { formatAgentBrowserRenderCall } from "./pi-tool-rendering.js";
|
|
3
|
+
import { JsonSchema } from "./json-schema.js";
|
|
4
|
+
import { AGENT_BROWSER_NAMESPACE, AGENT_BROWSER_OUTPUT_SCHEMA, finalizeAgentBrowserNativeResult } from "./native-output.js";
|
|
3
5
|
import { AGENT_BROWSER_PARAMS, AGENT_BROWSER_CODE_PARAMS, AGENT_BROWSER_ACTION_PARAMS, AGENT_BROWSER_QA_PARAMS, AGENT_BROWSER_ELECTRON_PARAMS, AGENT_BROWSER_SOURCE_PARAMS, AGENT_BROWSER_NETWORK_SOURCE_PARAMS, AGENT_BROWSER_TOOLS_PARAMS, } from "./input-modes/params.js";
|
|
4
6
|
export const AGENT_BROWSER_TOOL_INVENTORY = {
|
|
5
7
|
action: { name: "agent_browser_action", description: "Click, check, fill, or select a unique accessible target using stable locators or current refs." },
|
|
@@ -15,24 +17,28 @@ export const AGENT_BROWSER_INSTRUCTION_GROUP = {
|
|
|
15
17
|
const advancedNames = new Set(Object.values(AGENT_BROWSER_TOOL_INVENTORY).map(({ name }) => name));
|
|
16
18
|
/** Register once; advanced calls adapt only their input shape and reuse the ordinary executor. */
|
|
17
19
|
export function registerAgentBrowserToolSurface(pi, options) {
|
|
20
|
+
const execute = async (id, params, signal, onUpdate, ctx) => finalizeAgentBrowserNativeResult(await options.execute(id, params, signal, onUpdate, ctx), params);
|
|
21
|
+
const nativeOutput = { namespace: AGENT_BROWSER_NAMESPACE, outputSchema: AGENT_BROWSER_OUTPUT_SCHEMA };
|
|
18
22
|
pi.registerTool({
|
|
19
23
|
name: "agent_browser",
|
|
24
|
+
...nativeOutput,
|
|
20
25
|
label: "Agent Browser",
|
|
21
26
|
description: "Browse and interact through native agent-browser commands. One command in args; fixed sequences use batch --bail and JSON-array stdin. Use agent_browser_code for loops/branches and agent_browser_tools for advanced capabilities.",
|
|
22
27
|
promptSnippet: "Browse pages, inspect current refs, interact, and run native command batches.",
|
|
23
28
|
parameters: AGENT_BROWSER_PARAMS,
|
|
24
29
|
renderCall: options.renderCall,
|
|
25
30
|
renderResult: options.renderResult,
|
|
26
|
-
execute
|
|
31
|
+
execute,
|
|
27
32
|
executionMode: options.executionMode,
|
|
28
33
|
});
|
|
29
34
|
pi.registerTool({
|
|
30
35
|
name: "agent_browser_code",
|
|
36
|
+
...nativeOutput,
|
|
31
37
|
label: "Agent Browser Code",
|
|
32
38
|
description: "Run fresh JavaScript against a persistent browser. await browser({args,stdin?,timeoutMs?}) returns success/data/error/nextActions and imageObservations; emit(selected JSON) and emitImage(image handle) explicitly choose output. No host APIs or imports. Use native batch for fixed sequences.",
|
|
33
39
|
promptSnippet: "Branch, loop, and aggregate browser observations with explicit JSON/image output.",
|
|
34
40
|
parameters: AGENT_BROWSER_CODE_PARAMS,
|
|
35
|
-
execute: options.executeCode,
|
|
41
|
+
execute: async (id, params, signal, onUpdate, ctx) => finalizeAgentBrowserNativeResult(await options.executeCode(id, params, signal, onUpdate, ctx), params),
|
|
36
42
|
executionMode: options.executionMode,
|
|
37
43
|
renderCall(args, theme, context) {
|
|
38
44
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
@@ -43,56 +49,74 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
43
49
|
});
|
|
44
50
|
pi.registerTool({
|
|
45
51
|
...AGENT_BROWSER_TOOL_INVENTORY.action,
|
|
52
|
+
...nativeOutput,
|
|
53
|
+
defaultActive: false,
|
|
46
54
|
label: "Browser Action",
|
|
47
55
|
parameters: AGENT_BROWSER_ACTION_PARAMS,
|
|
48
56
|
renderResult: options.renderResult,
|
|
49
57
|
executionMode: options.executionMode,
|
|
50
58
|
execute(id, { outputPath, timeoutMs, ...semanticAction }, signal, onUpdate, ctx) {
|
|
51
|
-
return
|
|
59
|
+
return execute(id, { semanticAction, outputPath, timeoutMs }, signal, onUpdate, ctx);
|
|
52
60
|
},
|
|
53
61
|
});
|
|
54
62
|
pi.registerTool({
|
|
55
63
|
...AGENT_BROWSER_TOOL_INVENTORY.qa,
|
|
64
|
+
...nativeOutput,
|
|
65
|
+
defaultActive: false,
|
|
56
66
|
label: "Browser QA",
|
|
57
67
|
parameters: AGENT_BROWSER_QA_PARAMS,
|
|
58
68
|
renderResult: options.renderResult,
|
|
59
69
|
executionMode: options.executionMode,
|
|
60
70
|
execute(id, { outputPath, timeoutMs, sessionMode, ...qa }, signal, onUpdate, ctx) {
|
|
61
|
-
return
|
|
71
|
+
return execute(id, { qa, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
|
|
62
72
|
},
|
|
63
73
|
});
|
|
64
74
|
pi.registerTool({
|
|
65
75
|
...AGENT_BROWSER_TOOL_INVENTORY.electron,
|
|
76
|
+
...nativeOutput,
|
|
77
|
+
defaultActive: false,
|
|
66
78
|
label: "Browser Electron",
|
|
67
79
|
parameters: AGENT_BROWSER_ELECTRON_PARAMS,
|
|
68
80
|
renderResult: options.renderResult,
|
|
69
81
|
executionMode: options.executionMode,
|
|
70
82
|
execute(id, { outputPath, ...electron }, signal, onUpdate, ctx) {
|
|
71
|
-
return
|
|
83
|
+
return execute(id, { electron, outputPath }, signal, onUpdate, ctx);
|
|
72
84
|
},
|
|
73
85
|
});
|
|
74
86
|
pi.registerTool({
|
|
75
87
|
...AGENT_BROWSER_TOOL_INVENTORY.source,
|
|
88
|
+
...nativeOutput,
|
|
89
|
+
defaultActive: false,
|
|
76
90
|
label: "Browser Source",
|
|
77
91
|
parameters: AGENT_BROWSER_SOURCE_PARAMS,
|
|
78
92
|
renderResult: options.renderResult,
|
|
79
93
|
executionMode: options.executionMode,
|
|
80
94
|
execute(id, { outputPath, timeoutMs, sessionMode, ...sourceLookup }, signal, onUpdate, ctx) {
|
|
81
|
-
return
|
|
95
|
+
return execute(id, { sourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
|
|
82
96
|
},
|
|
83
97
|
});
|
|
84
98
|
pi.registerTool({
|
|
85
99
|
...AGENT_BROWSER_TOOL_INVENTORY.network,
|
|
100
|
+
...nativeOutput,
|
|
101
|
+
defaultActive: false,
|
|
86
102
|
label: "Browser Network Source",
|
|
87
103
|
parameters: AGENT_BROWSER_NETWORK_SOURCE_PARAMS,
|
|
88
104
|
renderResult: options.renderResult,
|
|
89
105
|
executionMode: options.executionMode,
|
|
90
106
|
execute(id, { outputPath, timeoutMs, sessionMode, ...networkSourceLookup }, signal, onUpdate, ctx) {
|
|
91
|
-
return
|
|
107
|
+
return execute(id, { networkSourceLookup, outputPath, timeoutMs, sessionMode }, signal, onUpdate, ctx);
|
|
92
108
|
},
|
|
93
109
|
});
|
|
94
110
|
pi.registerTool({
|
|
95
111
|
name: "agent_browser_tools",
|
|
112
|
+
namespace: AGENT_BROWSER_NAMESPACE,
|
|
113
|
+
outputSchema: JsonSchema.Object({
|
|
114
|
+
inventory: JsonSchema.Array(JsonSchema.Object({
|
|
115
|
+
key: JsonSchema.String(), name: JsonSchema.String(), description: JsonSchema.String(),
|
|
116
|
+
available: JsonSchema.Boolean(), active: JsonSchema.Boolean(),
|
|
117
|
+
})),
|
|
118
|
+
added: JsonSchema.Array(JsonSchema.String()),
|
|
119
|
+
}),
|
|
96
120
|
label: "Browser Tools",
|
|
97
121
|
description: "List or enable advanced browser tools: action → agent_browser_action; qa → agent_browser_qa; electron → agent_browser_electron; source → agent_browser_source; network → agent_browser_network_source. Omit enable for inventory. Activation only adds tools; it preserves other active tools.",
|
|
98
122
|
promptSnippet: "Discover and enable specialized browser actions, QA, Electron, and source tools.",
|
|
@@ -111,11 +135,13 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
111
135
|
return {
|
|
112
136
|
content: [{ type: "text", text: inventory.map((tool) => `${tool.key}: ${tool.name} (${!tool.available ? "unavailable in this Pi tool selection" : tool.active ? "active" : "inactive"}) — ${tool.description}`).join("\n") }],
|
|
113
137
|
details: { inventory, added: [...new Set(added)] },
|
|
138
|
+
structuredContent: { inventory, added: [...new Set(added)] },
|
|
114
139
|
};
|
|
115
140
|
},
|
|
116
141
|
});
|
|
117
142
|
pi.on("session_start", async (_event, ctx) => {
|
|
118
|
-
//
|
|
143
|
+
// ponytail: official 1.0 SDK supplies initialActiveToolNames on resume, bypassing
|
|
144
|
+
// native transcript restoration. Remove this additive fallback once the supported SDK restores it.
|
|
119
145
|
const argv = process.argv.slice(2);
|
|
120
146
|
const delimiter = argv.indexOf("--");
|
|
121
147
|
if ((delimiter < 0 ? argv : argv.slice(0, delimiter)).some((arg) => arg === "--tools" || arg === "-t"))
|
|
@@ -125,7 +151,7 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
125
151
|
if (!["agent_browser", "agent_browser_code", "agent_browser_tools", ...advancedNames].every(name => available.has(name)))
|
|
126
152
|
return;
|
|
127
153
|
// Native dynamic imports bypass Pi's host-package aliases in consumer installs.
|
|
128
|
-
// Only tool names are needed; replay
|
|
154
|
+
// Only tool names are needed; replay native deltas without requiring the optional pi-ai peer.
|
|
129
155
|
const restored = new Set();
|
|
130
156
|
for (const message of ctx.sessionManager.buildSessionProjection().messages) {
|
|
131
157
|
if (message.role !== "system")
|
|
@@ -135,7 +161,9 @@ export function registerAgentBrowserToolSurface(pi, options) {
|
|
|
135
161
|
for (const added of message.toolsAdded ?? [])
|
|
136
162
|
restored.add(added.name);
|
|
137
163
|
}
|
|
138
|
-
const active = pi.getActiveTools()
|
|
139
|
-
|
|
164
|
+
const active = new Set(pi.getActiveTools());
|
|
165
|
+
const added = [...restored].filter((name) => advancedNames.has(name) && available.has(name) && !active.has(name));
|
|
166
|
+
if (added.length > 0)
|
|
167
|
+
pi.setActiveTools([...active, ...added]);
|
|
140
168
|
});
|
|
141
169
|
}
|