pi-agent-browser-native 0.2.42 → 0.2.43
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 +21 -0
- package/README.md +8 -8
- package/docs/COMMAND_REFERENCE.md +9 -10
- package/docs/SUPPORT_MATRIX.md +6 -5
- package/docs/TOOL_CONTRACT.md +27 -24
- package/extensions/agent-browser/index.ts +71 -2
- package/extensions/agent-browser/lib/input-modes/params.ts +1 -1
- package/extensions/agent-browser/lib/input-modes/types.ts +1 -1
- package/extensions/agent-browser/lib/navigation-policy.ts +95 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.ts +2 -7
- package/extensions/agent-browser/lib/orchestration/browser-run/final-result.ts +1 -0
- package/extensions/agent-browser/lib/orchestration/browser-run/prepare.ts +2 -2
- package/extensions/agent-browser/lib/orchestration/browser-run/process-output.ts +103 -12
- package/extensions/agent-browser/lib/orchestration/browser-run/session-state.ts +20 -3
- package/extensions/agent-browser/lib/orchestration/browser-run/types.ts +6 -1
- package/extensions/agent-browser/lib/playbook.ts +4 -4
- package/extensions/agent-browser/lib/results/action-recommendations.ts +15 -0
- package/extensions/agent-browser/lib/results/contracts.ts +17 -0
- package/extensions/agent-browser/lib/results/network-routes.ts +80 -0
- package/extensions/agent-browser/lib/results/network.ts +10 -2
- package/extensions/agent-browser/lib/results/presentation/artifacts.ts +14 -0
- package/extensions/agent-browser/lib/results/presentation/batch.ts +36 -13
- package/extensions/agent-browser/lib/results/presentation/diagnostics.ts +154 -16
- package/extensions/agent-browser/lib/results/presentation/errors.ts +62 -2
- package/extensions/agent-browser/lib/results/presentation/semantic-action.ts +2 -4
- package/extensions/agent-browser/lib/results/presentation.ts +31 -1
- package/extensions/agent-browser/lib/results/selector-recovery.ts +11 -3
- package/extensions/agent-browser/lib/results/shared.ts +1 -0
- package/extensions/agent-browser/lib/results.ts +3 -0
- package/extensions/agent-browser/lib/runtime.ts +6 -0
- package/package.json +1 -1
|
@@ -11,15 +11,18 @@ import type {
|
|
|
11
11
|
AgentBrowserNextAction,
|
|
12
12
|
BatchFailurePresentationDetails,
|
|
13
13
|
BatchStepPresentationDetails,
|
|
14
|
+
NetworkRouteDiagnostic,
|
|
15
|
+
NetworkRouteRecord,
|
|
14
16
|
SessionArtifactManifest,
|
|
15
17
|
ToolPresentation,
|
|
16
18
|
} from "../contracts.js";
|
|
19
|
+
import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../network-routes.js";
|
|
17
20
|
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
18
21
|
import { stringifyModelFacing } from "./common.js";
|
|
19
22
|
import { buildArtifactVerificationSummary, classifyPresentationSuccessCategory, manifestHasNewNoticeWorthyEntries, type ArtifactRequestContext } from "./artifacts.js";
|
|
20
23
|
import { formatBatchStepCommand, getPresentationImages, getPresentationPaths, getPresentationText, isStringArray } from "./content.js";
|
|
21
24
|
import { buildPageChangeSummary } from "./navigation.js";
|
|
22
|
-
import { appendSelectorRecoveryHint } from "./errors.js";
|
|
25
|
+
import { appendSelectorRecoveryHint, getClipboardWritePayloadCandidates, redactClipboardPermissionErrorValue } from "./errors.js";
|
|
23
26
|
|
|
24
27
|
export interface BuildNestedToolPresentationOptions {
|
|
25
28
|
artifactManifest?: SessionArtifactManifest;
|
|
@@ -28,6 +31,7 @@ export interface BuildNestedToolPresentationOptions {
|
|
|
28
31
|
commandInfo: CommandInfo;
|
|
29
32
|
cwd: string;
|
|
30
33
|
envelope?: AgentBrowserEnvelope;
|
|
34
|
+
networkRouteDiagnostics?: NetworkRouteDiagnostic[];
|
|
31
35
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
32
36
|
sessionName?: string;
|
|
33
37
|
}
|
|
@@ -120,16 +124,19 @@ async function buildBatchStepPresentation(options: {
|
|
|
120
124
|
cwd: string;
|
|
121
125
|
index: number;
|
|
122
126
|
item: AgentBrowserBatchResult;
|
|
127
|
+
networkRoutes?: NetworkRouteRecord[];
|
|
123
128
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
124
129
|
sessionName?: string;
|
|
125
130
|
}): Promise<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> {
|
|
126
|
-
const { artifactManifest, artifactRequest, buildNestedToolPresentation, cwd, index, item, persistentArtifactStore, sessionName } = options;
|
|
131
|
+
const { artifactManifest, artifactRequest, buildNestedToolPresentation, cwd, index, item, networkRoutes, persistentArtifactStore, sessionName } = options;
|
|
127
132
|
const command = isStringArray(item.command) ? item.command : undefined;
|
|
128
133
|
const redactedCommand = command ? redactInvocationArgs(command) : undefined;
|
|
129
134
|
const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
|
|
130
135
|
|
|
131
136
|
if (item.success === false) {
|
|
132
|
-
const redactedErrorData =
|
|
137
|
+
const redactedErrorData = command?.[0] === "clipboard"
|
|
138
|
+
? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, item.error, getClipboardWritePayloadCandidates(command)))
|
|
139
|
+
: redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
|
|
133
140
|
const errorText = formatBatchStepError(redactedErrorData);
|
|
134
141
|
const failureCategory = classifyAgentBrowserFailureCategory({
|
|
135
142
|
args: command,
|
|
@@ -173,13 +180,18 @@ async function buildBatchStepPresentation(options: {
|
|
|
173
180
|
};
|
|
174
181
|
}
|
|
175
182
|
|
|
183
|
+
const commandInfo = parseCommandInfo(command ?? []);
|
|
184
|
+
const networkRouteDiagnostics = commandInfo.command === "network" && commandInfo.subcommand === "requests"
|
|
185
|
+
? buildNetworkRouteDiagnostics(item.result, networkRoutes)
|
|
186
|
+
: undefined;
|
|
176
187
|
const presentation = await buildNestedToolPresentation({
|
|
177
188
|
artifactManifest,
|
|
178
189
|
artifactRequest,
|
|
179
|
-
commandInfo
|
|
190
|
+
commandInfo,
|
|
180
191
|
cwd,
|
|
181
192
|
args: command,
|
|
182
193
|
envelope: { data: item.result, success: true },
|
|
194
|
+
networkRouteDiagnostics,
|
|
183
195
|
persistentArtifactStore,
|
|
184
196
|
sessionName,
|
|
185
197
|
});
|
|
@@ -192,17 +204,19 @@ async function buildBatchStepPresentation(options: {
|
|
|
192
204
|
secondaryPaths: presentation.imagePaths,
|
|
193
205
|
});
|
|
194
206
|
const text = getPresentationText(presentation) || presentation.summary;
|
|
207
|
+
const stepSucceeded = presentation.resultCategory !== "failure";
|
|
195
208
|
const nextActions = presentation.nextActions ?? buildAgentBrowserNextActions({
|
|
196
209
|
artifacts: presentation.artifacts,
|
|
197
210
|
args: command,
|
|
198
211
|
command: command?.[0],
|
|
199
|
-
|
|
212
|
+
failureCategory: presentation.failureCategory,
|
|
213
|
+
resultCategory: stepSucceeded ? "success" : "failure",
|
|
200
214
|
savedFilePath: presentation.savedFilePath,
|
|
201
215
|
successCategory: presentation.successCategory,
|
|
202
216
|
});
|
|
203
217
|
const pageChangeSummary = buildPageChangeSummary({
|
|
204
218
|
artifacts: presentation.artifacts,
|
|
205
|
-
commandInfo
|
|
219
|
+
commandInfo,
|
|
206
220
|
data: presentation.data,
|
|
207
221
|
nextActions,
|
|
208
222
|
savedFilePath: presentation.savedFilePath,
|
|
@@ -216,18 +230,20 @@ async function buildBatchStepPresentation(options: {
|
|
|
216
230
|
command: redactedCommand,
|
|
217
231
|
commandText,
|
|
218
232
|
data: presentation.data,
|
|
233
|
+
failureCategory: stepSucceeded ? undefined : presentation.failureCategory,
|
|
219
234
|
fullOutputPath: fullOutputPaths[0],
|
|
220
235
|
fullOutputPaths: fullOutputPaths.length > 0 ? fullOutputPaths : undefined,
|
|
221
236
|
imagePath: imagePaths[0],
|
|
222
237
|
imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
|
|
223
238
|
index,
|
|
239
|
+
networkRouteDiagnostics: presentation.networkRouteDiagnostics,
|
|
224
240
|
nextActions,
|
|
225
241
|
pageChangeSummary,
|
|
226
|
-
resultCategory: "success",
|
|
242
|
+
resultCategory: stepSucceeded ? "success" : "failure",
|
|
227
243
|
savedFile: presentation.savedFile,
|
|
228
244
|
savedFilePath: presentation.savedFilePath,
|
|
229
|
-
success:
|
|
230
|
-
successCategory: classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }),
|
|
245
|
+
success: stepSucceeded,
|
|
246
|
+
successCategory: stepSucceeded ? classifyPresentationSuccessCategory({ artifactVerification: presentation.artifactVerification, artifacts: presentation.artifacts, savedFile: presentation.savedFile }) : undefined,
|
|
231
247
|
summary: presentation.summary,
|
|
232
248
|
text,
|
|
233
249
|
},
|
|
@@ -241,14 +257,16 @@ export async function buildBatchPresentation(options: {
|
|
|
241
257
|
buildNestedToolPresentation: BuildNestedToolPresentation;
|
|
242
258
|
cwd: string;
|
|
243
259
|
data: AgentBrowserBatchResult[];
|
|
260
|
+
networkRoutes?: NetworkRouteRecord[];
|
|
244
261
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
245
262
|
sessionName?: string;
|
|
246
263
|
summary: string;
|
|
247
264
|
}): Promise<ToolPresentation> {
|
|
248
|
-
const { artifactRequests, buildNestedToolPresentation, cwd, data, persistentArtifactStore, sessionName, summary } = options;
|
|
265
|
+
const { artifactRequests, buildNestedToolPresentation, cwd, data, networkRoutes, persistentArtifactStore, sessionName, summary } = options;
|
|
249
266
|
const steps: Array<{ details: BatchStepPresentationDetails; presentation: ToolPresentation }> = [];
|
|
250
267
|
const protectedPersistentPaths: string[] = [];
|
|
251
268
|
let currentArtifactManifest = options.artifactManifest;
|
|
269
|
+
let currentNetworkRoutes = networkRoutes;
|
|
252
270
|
for (const [index, item] of data.entries()) {
|
|
253
271
|
const step = await buildBatchStepPresentation({
|
|
254
272
|
artifactManifest: currentArtifactManifest,
|
|
@@ -257,11 +275,13 @@ export async function buildBatchPresentation(options: {
|
|
|
257
275
|
cwd,
|
|
258
276
|
index,
|
|
259
277
|
item,
|
|
278
|
+
networkRoutes: currentNetworkRoutes,
|
|
260
279
|
persistentArtifactStore: persistentArtifactStore ? { ...persistentArtifactStore, protectedPaths: protectedPersistentPaths } : undefined,
|
|
261
280
|
sessionName,
|
|
262
281
|
});
|
|
263
282
|
steps.push(step);
|
|
264
283
|
currentArtifactManifest = step.presentation.artifactManifest ?? currentArtifactManifest;
|
|
284
|
+
currentNetworkRoutes = applyNetworkRouteRecords(currentNetworkRoutes, isStringArray(item.command) ? extractCommandTokens(item.command) : undefined, item.success !== false && step.details.success);
|
|
265
285
|
protectedPersistentPaths.push(
|
|
266
286
|
...getPresentationPaths({
|
|
267
287
|
primaryPath: step.presentation.fullOutputPath,
|
|
@@ -299,10 +319,13 @@ export async function buildBatchPresentation(options: {
|
|
|
299
319
|
return lines.join("\n");
|
|
300
320
|
})
|
|
301
321
|
.join("\n\n");
|
|
322
|
+
const batchSummary = batchFailure === undefined
|
|
323
|
+
? summary
|
|
324
|
+
: `Batch failed: ${batchFailure.successCount}/${batchFailure.totalCount} succeeded`;
|
|
302
325
|
const failureHeader = batchFailure === undefined
|
|
303
326
|
? undefined
|
|
304
327
|
: [
|
|
305
|
-
|
|
328
|
+
batchSummary,
|
|
306
329
|
`First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
|
|
307
330
|
batchFailure.failureCount > 1 ? `${batchFailure.failureCount} steps failed. See the per-step results below.` : "See the per-step results below.",
|
|
308
331
|
].join("\n");
|
|
@@ -321,7 +344,7 @@ export async function buildBatchPresentation(options: {
|
|
|
321
344
|
commandInfo: { command: "batch" },
|
|
322
345
|
data,
|
|
323
346
|
nextActions,
|
|
324
|
-
summary,
|
|
347
|
+
summary: batchSummary,
|
|
325
348
|
})
|
|
326
349
|
: changedSteps.length > 0
|
|
327
350
|
? {
|
|
@@ -350,6 +373,6 @@ export async function buildBatchPresentation(options: {
|
|
|
350
373
|
pageChangeSummary,
|
|
351
374
|
resultCategory: batchFailure ? "failure" : "success",
|
|
352
375
|
successCategory: batchFailure ? undefined : classifyPresentationSuccessCategory({ artifactVerification, artifacts }),
|
|
353
|
-
summary,
|
|
376
|
+
summary: batchSummary,
|
|
354
377
|
};
|
|
355
378
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
import { isRecord } from "../../parsing.js";
|
|
8
8
|
import { redactSensitiveText, redactSensitiveValue, type CommandInfo } from "../../runtime.js";
|
|
9
|
-
import type { AgentBrowserNextAction } from "../contracts.js";
|
|
10
|
-
import { classifyNetworkRequestFailure, summarizeNetworkFailures } from "../network.js";
|
|
9
|
+
import type { AgentBrowserNextAction, NetworkRouteDiagnostic } from "../contracts.js";
|
|
10
|
+
import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, summarizeNetworkFailures } from "../network.js";
|
|
11
11
|
import { withOptionalSessionArgs } from "../next-actions.js";
|
|
12
12
|
import { stringifyUnknown, truncateText } from "../text.js";
|
|
13
13
|
import {
|
|
@@ -29,10 +29,24 @@ const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
|
|
|
29
29
|
|
|
30
30
|
const NETWORK_ERROR_PREVIEW_MAX_CHARS = 220;
|
|
31
31
|
|
|
32
|
-
const NETWORK_NEXT_ACTION_LIMIT =
|
|
32
|
+
const NETWORK_NEXT_ACTION_LIMIT = 6;
|
|
33
33
|
|
|
34
34
|
const NETWORK_FILTER_MAX_CHARS = 160;
|
|
35
35
|
|
|
36
|
+
const STORAGE_VALUE_PREVIEW_MAX_CHARS = 160;
|
|
37
|
+
|
|
38
|
+
const STORAGE_SECRET_KEY_PATTERN = /(?:access(?:_|-)?token|account|api(?:_|-)?key|auth(?:orization)?|bearer|client(?:_|-)?secret|cookie|credential|csrf|email|id(?:_|-)?token|jwt|pass(?:word)?|private(?:_|-)?key|profile|refresh(?:_|-)?token|secret|session|sid|sig(?:nature)?|token|user(?:name)?|x(?:_|-)?api(?:_|-)?key|xsrf)/i;
|
|
39
|
+
|
|
40
|
+
const STORAGE_BENIGN_KEY_PATTERN = /^(?:color(?:scheme)?|debug|dev|experiment|feature(?:flag)?|flag|language|layout|locale|mode|onboarding|sort|tab|theme|timezone|tour|variant|view)$/i;
|
|
41
|
+
|
|
42
|
+
const STORAGE_TOKEN_VALUE_PATTERN = /(?:\bBearer\s+[A-Za-z0-9._~-]+|\bBasic\s+[A-Za-z0-9+/=]+|^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$|(?=.*[A-Za-z])(?=.*\d)[A-Za-z0-9_~+/=-]{32,})/;
|
|
43
|
+
|
|
44
|
+
const STORAGE_SECRET_VALUE_WORD_PATTERN = /(?:secret|token|password|passwd|bearer|credential|authorization|cookie|session[-_ ]?id)/i;
|
|
45
|
+
|
|
46
|
+
const STORAGE_EMAIL_VALUE_PATTERN = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
47
|
+
|
|
48
|
+
const STORAGE_IDENTITY_VALUE_PATTERN = /(?:^|[\s:=/_-])(?:account|profile|session|sid|user(?:id|name)?)(?:[\s:=/_-]|$)/i;
|
|
49
|
+
|
|
36
50
|
const NETWORK_FILTER_SENSITIVE_SEGMENT_TERMS = [
|
|
37
51
|
"apikey",
|
|
38
52
|
"api-key",
|
|
@@ -86,6 +100,15 @@ export function getTabSummary(data: Record<string, unknown>): string | undefined
|
|
|
86
100
|
}
|
|
87
101
|
|
|
88
102
|
export function getStreamSummary(data: Record<string, unknown>): string | undefined {
|
|
103
|
+
if (data.alreadyEnabled === true) {
|
|
104
|
+
const lines = ["Stream already enabled (idempotent no-op)."];
|
|
105
|
+
if (typeof data.port === "number") {
|
|
106
|
+
lines.push(`Port: ${data.port}`);
|
|
107
|
+
lines.push(`WebSocket URL: ${getStreamWebSocketUrl(data.port)}`);
|
|
108
|
+
}
|
|
109
|
+
lines.push("Run stream status for current connection details or stream disable when streaming is no longer needed.");
|
|
110
|
+
return lines.join("\n");
|
|
111
|
+
}
|
|
89
112
|
if (typeof data.enabled !== "boolean" || typeof data.connected !== "boolean") {
|
|
90
113
|
return undefined;
|
|
91
114
|
}
|
|
@@ -211,6 +234,7 @@ export function formatDiagnosticSummary(commandInfo: CommandInfo, data: Record<s
|
|
|
211
234
|
|
|
212
235
|
if (commandInfo.command === "stream") {
|
|
213
236
|
if (commandInfo.subcommand === "enable") {
|
|
237
|
+
if (data.alreadyEnabled === true) return "Stream already enabled";
|
|
214
238
|
const port = typeof data.port === "number" ? ` on port ${data.port}` : "";
|
|
215
239
|
return `Stream enabled${port}`;
|
|
216
240
|
}
|
|
@@ -416,14 +440,6 @@ function getNetworkRequestPathFilter(item: Record<string, unknown>): string | un
|
|
|
416
440
|
return getSafeNetworkActionValue(filter);
|
|
417
441
|
}
|
|
418
442
|
|
|
419
|
-
function isApiLikeNetworkRequest(item: Record<string, unknown>): boolean {
|
|
420
|
-
const method = (getStringField(item, "method") ?? "GET").toUpperCase();
|
|
421
|
-
const resourceType = (getStringField(item, "resourceType") ?? "").toLowerCase();
|
|
422
|
-
const mimeType = (getStringField(item, "mimeType") ?? "").toLowerCase();
|
|
423
|
-
const filter = getNetworkRequestPathFilter(item) ?? "";
|
|
424
|
-
return resourceType === "fetch" || resourceType === "xhr" || mimeType.includes("json") || /\/(?:api|graphql|rpc)(?:\/|$)/i.test(filter) || !["GET", "HEAD"].includes(method);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
443
|
function getNetworkRequestActionCandidate(item: Record<string, unknown>): NetworkRequestActionCandidate | undefined {
|
|
428
444
|
const requestId = getNetworkRequestId(item);
|
|
429
445
|
if (!requestId) return undefined;
|
|
@@ -458,7 +474,41 @@ function getNetworkRequestDetailActionId(candidate: NetworkRequestActionCandidat
|
|
|
458
474
|
return "inspect-network-request";
|
|
459
475
|
}
|
|
460
476
|
|
|
461
|
-
export function
|
|
477
|
+
export function formatNetworkRouteDiagnosticsText(diagnostics: NetworkRouteDiagnostic[] | undefined): string | undefined {
|
|
478
|
+
if (!diagnostics || diagnostics.length === 0) return undefined;
|
|
479
|
+
const lines = ["Network route diagnostics:"];
|
|
480
|
+
for (const diagnostic of diagnostics) {
|
|
481
|
+
const target = diagnostic.requestId ? `[${diagnostic.requestId}] ${diagnostic.requestUrl ?? "request"}` : diagnostic.requestUrl ?? "request";
|
|
482
|
+
lines.push(`- ${diagnostic.reason}: ${target} matched route ${diagnostic.routePattern} (${diagnostic.mode}).`);
|
|
483
|
+
}
|
|
484
|
+
lines.push("If this route is intended as a mock, verify the page origin/CORS headers and inspect the request before assuming the mock fulfilled normally.");
|
|
485
|
+
return lines.join("\n");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
export function buildNetworkRouteDiagnosticsNextActions(diagnostics: NetworkRouteDiagnostic[] | undefined, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
|
|
489
|
+
const diagnostic = diagnostics?.find((item) => item.requestId) ?? diagnostics?.[0];
|
|
490
|
+
if (!diagnostic) return undefined;
|
|
491
|
+
const actions: AgentBrowserNextAction[] = [];
|
|
492
|
+
if (diagnostic.requestId) {
|
|
493
|
+
actions.push({
|
|
494
|
+
id: "inspect-pending-routed-network-request",
|
|
495
|
+
params: { args: withOptionalSessionArgs(sessionName, ["network", "request", diagnostic.requestId]) },
|
|
496
|
+
reason: `Inspect the routed request ${diagnostic.requestId} before assuming the route mock fulfilled normally.`,
|
|
497
|
+
safety: "Read-only request diagnostic; look for pending state, CORS/preflight errors, response status, and headers.",
|
|
498
|
+
tool: "agent_browser",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
actions.push({
|
|
502
|
+
id: "start-network-har-capture-for-route-mock",
|
|
503
|
+
params: { args: withOptionalSessionArgs(sessionName, ["network", "har", "start"]) },
|
|
504
|
+
reason: "Capture a HAR before reproducing the route mock so pending/CORS behavior has request and response headers.",
|
|
505
|
+
safety: "HARs can contain URLs and headers; stop to an explicit path and avoid sharing sensitive captures.",
|
|
506
|
+
tool: "agent_browser",
|
|
507
|
+
});
|
|
508
|
+
return actions;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
export function buildNetworkRequestsNextActions(data: unknown, sessionName: string | undefined, routeDiagnostics?: NetworkRouteDiagnostic[]): AgentBrowserNextAction[] | undefined {
|
|
462
512
|
if (!isRecord(data)) return undefined;
|
|
463
513
|
const requests = getArrayField(data, "requests");
|
|
464
514
|
if (!requests) return undefined;
|
|
@@ -504,7 +554,27 @@ export function buildNetworkRequestsNextActions(data: unknown, sessionName: stri
|
|
|
504
554
|
safety: "HARs can contain URLs and headers; stop to an explicit path, inspect metadata, and avoid sharing sensitive captures.",
|
|
505
555
|
tool: "agent_browser",
|
|
506
556
|
});
|
|
507
|
-
return actions.slice(0, NETWORK_NEXT_ACTION_LIMIT);
|
|
557
|
+
return [...(buildNetworkRouteDiagnosticsNextActions(routeDiagnostics, sessionName) ?? []), ...actions].slice(0, NETWORK_NEXT_ACTION_LIMIT);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
export function buildStreamNextActions(commandInfo: CommandInfo, data: unknown, sessionName: string | undefined): AgentBrowserNextAction[] | undefined {
|
|
561
|
+
if (commandInfo.command !== "stream" || commandInfo.subcommand !== "enable" || !isRecord(data) || data.alreadyEnabled !== true) return undefined;
|
|
562
|
+
return [
|
|
563
|
+
{
|
|
564
|
+
id: "check-stream-status-after-noop",
|
|
565
|
+
params: { args: withOptionalSessionArgs(sessionName, ["stream", "status"]) },
|
|
566
|
+
reason: "Read current stream port and connection details after the idempotent enable no-op.",
|
|
567
|
+
safety: "Read-only stream diagnostic.",
|
|
568
|
+
tool: "agent_browser",
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
id: "disable-existing-stream-when-done",
|
|
572
|
+
params: { args: withOptionalSessionArgs(sessionName, ["stream", "disable"]) },
|
|
573
|
+
reason: "Disable the existing stream when it is no longer needed.",
|
|
574
|
+
safety: "Only run when no other workflow is relying on the current stream.",
|
|
575
|
+
tool: "agent_browser",
|
|
576
|
+
},
|
|
577
|
+
];
|
|
508
578
|
}
|
|
509
579
|
|
|
510
580
|
function formatConsoleText(data: Record<string, unknown>): string | undefined {
|
|
@@ -602,6 +672,74 @@ function formatCookiesText(data: Record<string, unknown>): string | undefined {
|
|
|
602
672
|
return undefined;
|
|
603
673
|
}
|
|
604
674
|
|
|
675
|
+
function valueContainsStorageSecret(value: unknown): boolean {
|
|
676
|
+
if (typeof value === "string") {
|
|
677
|
+
const trimmed = value.trim();
|
|
678
|
+
if (trimmed.length === 0) return false;
|
|
679
|
+
if (STORAGE_TOKEN_VALUE_PATTERN.test(trimmed) || STORAGE_SECRET_VALUE_WORD_PATTERN.test(trimmed) || STORAGE_EMAIL_VALUE_PATTERN.test(trimmed) || STORAGE_IDENTITY_VALUE_PATTERN.test(trimmed)) return true;
|
|
680
|
+
try {
|
|
681
|
+
const url = new URL(trimmed);
|
|
682
|
+
if (url.protocol === "http:" || url.protocol === "https:" || url.username || url.password || url.search) return true;
|
|
683
|
+
} catch {}
|
|
684
|
+
if (redactSensitiveText(trimmed) !== trimmed || redactModelFacingTextIfSensitive(trimmed) !== trimmed) return true;
|
|
685
|
+
try {
|
|
686
|
+
return valueContainsStorageSecret(JSON.parse(trimmed));
|
|
687
|
+
} catch {
|
|
688
|
+
return false;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (Array.isArray(value)) return value.some((item) => valueContainsStorageSecret(item));
|
|
692
|
+
if (!isRecord(value)) return false;
|
|
693
|
+
return Object.entries(value).some(([key, entryValue]) => STORAGE_SECRET_KEY_PATTERN.test(key) || valueContainsStorageSecret(entryValue));
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function shouldRevealStorageValue(key: string | undefined, value: unknown): boolean {
|
|
697
|
+
if (!key || STORAGE_SECRET_KEY_PATTERN.test(key) || !STORAGE_BENIGN_KEY_PATTERN.test(key)) return false;
|
|
698
|
+
if (valueContainsStorageSecret(value)) return false;
|
|
699
|
+
if (typeof value === "string") return value.length <= STORAGE_VALUE_PREVIEW_MAX_CHARS;
|
|
700
|
+
return value === null || typeof value === "number" || typeof value === "boolean";
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function formatStorageValue(key: string | undefined, value: unknown): string {
|
|
704
|
+
if (!shouldRevealStorageValue(key, value)) return "[REDACTED]";
|
|
705
|
+
if (typeof value === "string") return redactModelFacingText(value);
|
|
706
|
+
return stringifyModelFacing(value);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
function redactStorageEntryValue(item: Record<string, unknown>): Record<string, unknown> {
|
|
710
|
+
if (!Object.hasOwn(item, "value")) return redactStructuredPresentationValue(item) as Record<string, unknown>;
|
|
711
|
+
const key = getStringField(item, "key") ?? getStringField(item, "name");
|
|
712
|
+
const value = item.value;
|
|
713
|
+
if (shouldRevealStorageValue(key, value)) return redactStructuredPresentationValue(item) as Record<string, unknown>;
|
|
714
|
+
return {
|
|
715
|
+
...redactStructuredPresentationValue({ ...item, value: undefined }) as Record<string, unknown>,
|
|
716
|
+
value: "[REDACTED]",
|
|
717
|
+
valueRedacted: true,
|
|
718
|
+
valueRedactionReason: key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value",
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function redactStorageData(value: unknown): unknown {
|
|
723
|
+
if (Array.isArray(value)) return value.map((item) => redactStorageData(item));
|
|
724
|
+
if (!isRecord(value)) return redactStructuredPresentationValue(value);
|
|
725
|
+
const entries = Object.fromEntries(Object.entries(value).map(([key, entryValue]) => {
|
|
726
|
+
if ((key === "entries" || key === "items") && Array.isArray(entryValue)) return [key, entryValue.map((item) => isRecord(item) ? redactStorageEntryValue(item) : redactStructuredPresentationValue(item))];
|
|
727
|
+
if (key === "value") {
|
|
728
|
+
const itemKey = getStringField(value, "key") ?? getStringField(value, "name");
|
|
729
|
+
return [key, shouldRevealStorageValue(itemKey, entryValue) ? redactStructuredPresentationValue(entryValue) : "[REDACTED]"];
|
|
730
|
+
}
|
|
731
|
+
return [key, redactStructuredPresentationValue(entryValue)];
|
|
732
|
+
}));
|
|
733
|
+
if (Object.hasOwn(value, "value")) {
|
|
734
|
+
const key = getStringField(value, "key") ?? getStringField(value, "name");
|
|
735
|
+
if (!shouldRevealStorageValue(key, value.value)) {
|
|
736
|
+
entries.valueRedacted = true;
|
|
737
|
+
entries.valueRedactionReason = key && STORAGE_SECRET_KEY_PATTERN.test(key) ? "sensitive-key" : "sensitive-value";
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
return entries;
|
|
741
|
+
}
|
|
742
|
+
|
|
605
743
|
function formatStorageText(data: Record<string, unknown>): string | undefined {
|
|
606
744
|
const type = getStringField(data, "type") ?? getStringField(data, "storage") ?? "storage";
|
|
607
745
|
const entries = getArrayField(data, "entries") ?? getArrayField(data, "items");
|
|
@@ -612,12 +750,12 @@ function formatStorageText(data: Record<string, unknown>): string | undefined {
|
|
|
612
750
|
if (!isRecord(item)) return `${index + 1}. [REDACTED]`;
|
|
613
751
|
const rawKey = getStringField(item, "key") ?? getStringField(item, "name") ?? `(entry ${index + 1})`;
|
|
614
752
|
const key = redactModelFacingText(rawKey);
|
|
615
|
-
return Object.hasOwn(item, "value") ? `${key}:
|
|
753
|
+
return Object.hasOwn(item, "value") ? `${key}: ${formatStorageValue(rawKey, item.value)}` : key;
|
|
616
754
|
})
|
|
617
755
|
.join("\n");
|
|
618
756
|
}
|
|
619
757
|
const key = getStringField(data, "key");
|
|
620
|
-
if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}:
|
|
758
|
+
if (key && Object.hasOwn(data, "value")) return `${type} ${redactModelFacingText(key)}: ${formatStorageValue(key, data.value)}`;
|
|
621
759
|
if (key && data.set === true) return `${type} set: ${redactModelFacingText(key)}`;
|
|
622
760
|
if (data.cleared === true || data.clear === true) return `${type} cleared.`;
|
|
623
761
|
return undefined;
|
|
@@ -690,7 +828,7 @@ function redactStatefulValues(value: unknown, sensitiveKeys: Set<string>): unkno
|
|
|
690
828
|
|
|
691
829
|
export function redactPresentationData(commandInfo: CommandInfo, data: unknown): unknown {
|
|
692
830
|
if (commandInfo.command === "cookies") return redactStatefulValues(data, new Set(["value"]));
|
|
693
|
-
if (commandInfo.command === "storage") return
|
|
831
|
+
if (commandInfo.command === "storage") return redactStorageData(data);
|
|
694
832
|
return redactStructuredPresentationValue(data);
|
|
695
833
|
}
|
|
696
834
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isOpenNavigationCommand } from "../../command-taxonomy.js";
|
|
2
|
-
import type
|
|
2
|
+
import { redactSensitiveText, type CommandInfo } from "../../runtime.js";
|
|
3
3
|
import { buildBrowserProfileConfigRecovery } from "./browser-profile-recovery.js";
|
|
4
4
|
import { redactModelFacingText } from "./common.js";
|
|
5
5
|
import { buildAgentBrowserNextActions } from "../action-recommendations.js";
|
|
@@ -17,6 +17,17 @@ const SELECTOR_DIALECT_ERROR_HINT = [
|
|
|
17
17
|
"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.",
|
|
18
18
|
].join(" ");
|
|
19
19
|
|
|
20
|
+
const CLIPBOARD_PERMISSION_ERROR_HINT = [
|
|
21
|
+
"Agent-browser clipboard hint: Clipboard read/write access is environment-dependent and often fails in headless, managed, remote-profile, or file:// sessions.",
|
|
22
|
+
"If you see `NotAllowedError` or `permission denied`, treat it as a browser/OS permission limitation rather than proof that page state changed.",
|
|
23
|
+
"When possible, prefer page-native reads (`snapshot -i`, `get text`, `eval --stdin`) or direct input (`keyboard inserttext` / `keyboard type`) instead of relying on OS clipboard access.",
|
|
24
|
+
"If true clipboard access is required, retry in a browser/profile/session with explicit clipboard permission on a normal http(s) page.",
|
|
25
|
+
].join(" ");
|
|
26
|
+
|
|
27
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
28
|
+
return typeof value === "object" && value !== null;
|
|
29
|
+
}
|
|
30
|
+
|
|
20
31
|
function getSelectorRecoveryHint(errorText: string): string | undefined {
|
|
21
32
|
const normalized = errorText.trim();
|
|
22
33
|
if (normalized.length === 0) return undefined;
|
|
@@ -42,6 +53,51 @@ function getSelectorRecoveryHint(errorText: string): string | undefined {
|
|
|
42
53
|
return undefined;
|
|
43
54
|
}
|
|
44
55
|
|
|
56
|
+
function getClipboardPermissionHint(commandInfo: CommandInfo, errorText: string): string | undefined {
|
|
57
|
+
if (commandInfo.command !== "clipboard") return undefined;
|
|
58
|
+
if (!/\bNotAllowedError\b|\bclipboard\b.*\bpermission denied\b|\bpermission denied\b.*\bclipboard\b/i.test(errorText)) {
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
61
|
+
return CLIPBOARD_PERMISSION_ERROR_HINT;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function redactClipboardPermissionEcho(commandInfo: CommandInfo, errorText: string): string {
|
|
65
|
+
if (commandInfo.command !== "clipboard") return errorText;
|
|
66
|
+
return errorText
|
|
67
|
+
.replace(/(\b(?:read|write)\s+permission denied\b(?:\s+for)?\s+)([\s\S]+)$/gi, "$1[REDACTED]")
|
|
68
|
+
.replace(/(\bFailed to execute '[^']+' on 'Clipboard':\s*)([\s\S]+)$/gi, (match, prefix: string, suffix: string) => {
|
|
69
|
+
if (!/\bpermission denied\b/i.test(suffix)) return match;
|
|
70
|
+
return `${prefix}${suffix.replace(/(\bpermission denied\b(?:\s+for)?\s+)([\s\S]+)$/i, "$1[REDACTED]")}`;
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function getClipboardWritePayloadCandidates(commandTokens: readonly string[]): string[] {
|
|
75
|
+
if (commandTokens[0] !== "clipboard" || commandTokens[1] !== "write") return [];
|
|
76
|
+
const payloadTokens = commandTokens.slice(2).filter((value) => value.length > 0);
|
|
77
|
+
return [...new Set([...payloadTokens, payloadTokens.join(" ")].filter((value) => value.length > 0))];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function shouldRedactClipboardPayloadField(key: string, value: string, payloadCandidates: readonly string[]): boolean {
|
|
81
|
+
return payloadCandidates.some((candidate) => {
|
|
82
|
+
if (value === candidate) return true;
|
|
83
|
+
if (candidate.length < 8 || !/payload|clipboard|argument/i.test(key)) return false;
|
|
84
|
+
return value.includes(candidate);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function redactClipboardPermissionErrorValue(commandInfo: CommandInfo, value: unknown, payloadCandidates: readonly string[] = []): unknown {
|
|
89
|
+
if (commandInfo.command !== "clipboard") return value;
|
|
90
|
+
if (typeof value === "string") return payloadCandidates.includes(value) ? "[REDACTED]" : redactClipboardPermissionEcho(commandInfo, value);
|
|
91
|
+
if (Array.isArray(value)) return value.map((item) => redactClipboardPermissionErrorValue(commandInfo, item, payloadCandidates));
|
|
92
|
+
if (!isRecord(value)) return value;
|
|
93
|
+
return Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [
|
|
94
|
+
key,
|
|
95
|
+
typeof entryValue === "string" && shouldRedactClipboardPayloadField(key, entryValue, payloadCandidates)
|
|
96
|
+
? "[REDACTED]"
|
|
97
|
+
: redactClipboardPermissionErrorValue(commandInfo, entryValue, payloadCandidates),
|
|
98
|
+
]));
|
|
99
|
+
}
|
|
100
|
+
|
|
45
101
|
interface CommandSuggestion {
|
|
46
102
|
args?: string[];
|
|
47
103
|
description: string;
|
|
@@ -116,17 +172,21 @@ export function buildErrorPresentation(options: {
|
|
|
116
172
|
sessionName?: string;
|
|
117
173
|
}): ToolPresentation {
|
|
118
174
|
const { args, commandInfo, errorText, sessionName } = options;
|
|
119
|
-
const safeErrorText = redactModelFacingText(
|
|
175
|
+
const safeErrorText = redactModelFacingText(
|
|
176
|
+
redactSensitiveText(redactClipboardPermissionEcho(commandInfo, errorText)),
|
|
177
|
+
);
|
|
120
178
|
const selectorHintedErrorText = appendSelectorRecoveryHint(safeErrorText);
|
|
121
179
|
const unknownCommandSuggestions = getUnknownCommandSuggestions(commandInfo.command, safeErrorText);
|
|
122
180
|
const unknownCommandSuggestionText = formatUnknownCommandSuggestionText(unknownCommandSuggestions);
|
|
123
181
|
const browserProfileConfigRecovery = buildBrowserProfileConfigRecovery({ args, commandInfo, errorText: safeErrorText });
|
|
124
182
|
const localhostNavigationHint = getLocalhostNavigationHint(commandInfo, safeErrorText);
|
|
183
|
+
const clipboardPermissionHint = getClipboardPermissionHint(commandInfo, safeErrorText);
|
|
125
184
|
const hintedErrorParts = [
|
|
126
185
|
selectorHintedErrorText,
|
|
127
186
|
unknownCommandSuggestionText && !selectorHintedErrorText.includes("Agent-browser hint:") ? unknownCommandSuggestionText : undefined,
|
|
128
187
|
browserProfileConfigRecovery?.hint,
|
|
129
188
|
localhostNavigationHint,
|
|
189
|
+
clipboardPermissionHint,
|
|
130
190
|
].filter((part): part is string => Boolean(part));
|
|
131
191
|
const hintedErrorText = hintedErrorParts.join("\n\n");
|
|
132
192
|
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
@@ -18,9 +18,9 @@ import {
|
|
|
18
18
|
} from "./navigation.js";
|
|
19
19
|
import { redactModelFacingText } from "./common.js";
|
|
20
20
|
|
|
21
|
-
const SEMANTIC_NAVIGATION_PROBE_ACTIONS = new Set(["check", "click"
|
|
21
|
+
const SEMANTIC_NAVIGATION_PROBE_ACTIONS = new Set(["check", "click"]);
|
|
22
22
|
|
|
23
|
-
const SEMANTIC_PRESENTATION_ACTIONS = new Set(["check", "click", "fill", "select"
|
|
23
|
+
const SEMANTIC_PRESENTATION_ACTIONS = new Set(["check", "click", "fill", "select"]);
|
|
24
24
|
|
|
25
25
|
function getPageSummary(data: Record<string, unknown>): string | undefined {
|
|
26
26
|
const title = typeof data.title === "string" ? data.title : undefined;
|
|
@@ -55,8 +55,6 @@ export function formatSemanticActionCompactLine(compiled: CompiledAgentBrowserSe
|
|
|
55
55
|
return `Filled: ${target}`;
|
|
56
56
|
case "check":
|
|
57
57
|
return `Checked: ${target}`;
|
|
58
|
-
case "uncheck":
|
|
59
|
-
return `Unchecked: ${target}`;
|
|
60
58
|
case "select":
|
|
61
59
|
return `Selected: ${target}`;
|
|
62
60
|
default:
|
|
@@ -14,6 +14,7 @@ import { detectConfirmationRequired } from "./confirmation.js";
|
|
|
14
14
|
import type {
|
|
15
15
|
AgentBrowserEnvelope,
|
|
16
16
|
AgentBrowserNextAction,
|
|
17
|
+
NetworkRouteDiagnostic,
|
|
17
18
|
SessionArtifactManifest,
|
|
18
19
|
ToolPresentation,
|
|
19
20
|
} from "./contracts.js";
|
|
@@ -29,7 +30,9 @@ import {
|
|
|
29
30
|
extractImagePath,
|
|
30
31
|
formatArtifactMetadataLines,
|
|
31
32
|
formatArtifactSummary,
|
|
33
|
+
formatMissingArtifactFailureText,
|
|
32
34
|
getSavedFileDetails,
|
|
35
|
+
hasMissingFileArtifact,
|
|
33
36
|
isManifestFileArtifact,
|
|
34
37
|
type ArtifactRequestContext,
|
|
35
38
|
} from "./presentation/artifacts.js";
|
|
@@ -37,7 +40,9 @@ import { buildBatchPresentation, isAgentBrowserBatchResultArray } from "./presen
|
|
|
37
40
|
import { getPresentationPaths } from "./presentation/content.js";
|
|
38
41
|
import {
|
|
39
42
|
buildNetworkRequestsNextActions,
|
|
43
|
+
buildStreamNextActions,
|
|
40
44
|
enrichStreamStatusData,
|
|
45
|
+
formatNetworkRouteDiagnosticsText,
|
|
41
46
|
redactPresentationData,
|
|
42
47
|
} from "./presentation/diagnostics.js";
|
|
43
48
|
import { buildErrorPresentation } from "./presentation/errors.js";
|
|
@@ -71,6 +76,8 @@ export async function buildToolPresentation(options: {
|
|
|
71
76
|
cwd: string;
|
|
72
77
|
envelope?: AgentBrowserEnvelope;
|
|
73
78
|
errorText?: string;
|
|
79
|
+
networkRouteDiagnostics?: NetworkRouteDiagnostic[];
|
|
80
|
+
networkRoutes?: import("./contracts.js").NetworkRouteRecord[];
|
|
74
81
|
persistentArtifactStore?: PersistentSessionArtifactStore;
|
|
75
82
|
sessionName?: string;
|
|
76
83
|
}): Promise<ToolPresentation> {
|
|
@@ -83,6 +90,8 @@ export async function buildToolPresentation(options: {
|
|
|
83
90
|
cwd,
|
|
84
91
|
envelope,
|
|
85
92
|
errorText,
|
|
93
|
+
networkRouteDiagnostics,
|
|
94
|
+
networkRoutes,
|
|
86
95
|
persistentArtifactStore,
|
|
87
96
|
sessionName,
|
|
88
97
|
} = options;
|
|
@@ -108,6 +117,7 @@ export async function buildToolPresentation(options: {
|
|
|
108
117
|
buildNestedToolPresentation: buildToolPresentation,
|
|
109
118
|
cwd,
|
|
110
119
|
data,
|
|
120
|
+
networkRoutes,
|
|
111
121
|
persistentArtifactStore,
|
|
112
122
|
sessionName,
|
|
113
123
|
summary,
|
|
@@ -124,6 +134,11 @@ export async function buildToolPresentation(options: {
|
|
|
124
134
|
};
|
|
125
135
|
}
|
|
126
136
|
|
|
137
|
+
if (networkRouteDiagnostics && networkRouteDiagnostics.length > 0 && presentation.content[0]?.type === "text") {
|
|
138
|
+
const diagnosticText = formatNetworkRouteDiagnosticsText(networkRouteDiagnostics);
|
|
139
|
+
if (diagnosticText) presentation.content[0] = { ...presentation.content[0], text: `${diagnosticText}\n\n${presentation.content[0].text}` };
|
|
140
|
+
presentation.networkRouteDiagnostics = networkRouteDiagnostics;
|
|
141
|
+
}
|
|
127
142
|
if (artifacts.length > 0 && !presentation.artifacts) {
|
|
128
143
|
presentation.artifacts = artifacts;
|
|
129
144
|
}
|
|
@@ -161,6 +176,19 @@ export async function buildToolPresentation(options: {
|
|
|
161
176
|
) ?? presentationWithManifest.artifactVerification;
|
|
162
177
|
|
|
163
178
|
const confirmationRequired = detectConfirmationRequired(data);
|
|
179
|
+
const missingArtifactFailureText = formatMissingArtifactFailureText(presentationWithManifest.artifacts);
|
|
180
|
+
if (missingArtifactFailureText && hasMissingFileArtifact(presentationWithManifest.artifacts)) {
|
|
181
|
+
presentationWithManifest.resultCategory = "failure";
|
|
182
|
+
presentationWithManifest.failureCategory = "artifact-missing";
|
|
183
|
+
presentationWithManifest.successCategory = undefined;
|
|
184
|
+
presentationWithManifest.summary = missingArtifactFailureText;
|
|
185
|
+
if (presentationWithManifest.content[0]?.type === "text") {
|
|
186
|
+
presentationWithManifest.content[0] = { ...presentationWithManifest.content[0], text: `${missingArtifactFailureText}\n\n${presentationWithManifest.content[0].text}` };
|
|
187
|
+
} else {
|
|
188
|
+
presentationWithManifest.content.unshift({ type: "text", text: missingArtifactFailureText });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
164
192
|
if (!presentationWithManifest.resultCategory) {
|
|
165
193
|
const categoryDetails = buildAgentBrowserResultCategoryDetails({
|
|
166
194
|
artifacts: presentationWithManifest.artifacts,
|
|
@@ -199,12 +227,14 @@ export async function buildToolPresentation(options: {
|
|
|
199
227
|
successCategory: presentationWithManifest.successCategory,
|
|
200
228
|
});
|
|
201
229
|
const networkNextActions = commandInfo.command === "network" && commandInfo.subcommand === "requests" && presentationWithManifest.resultCategory === "success"
|
|
202
|
-
? buildNetworkRequestsNextActions(data, sessionName)
|
|
230
|
+
? buildNetworkRequestsNextActions(data, sessionName, presentationWithManifest.networkRouteDiagnostics)
|
|
203
231
|
: undefined;
|
|
232
|
+
const streamNextActions = presentationWithManifest.resultCategory === "success" ? buildStreamNextActions(commandInfo, data, sessionName) : undefined;
|
|
204
233
|
presentationWithManifest.nextActions = mergeNextActions(
|
|
205
234
|
presentationWithManifest.nextActions,
|
|
206
235
|
genericNextActions,
|
|
207
236
|
networkNextActions,
|
|
237
|
+
streamNextActions,
|
|
208
238
|
);
|
|
209
239
|
presentationWithManifest.pageChangeSummary = presentationWithManifest.pageChangeSummary ?? buildPageChangeSummary({
|
|
210
240
|
artifacts: presentationWithManifest.artifacts,
|