pi-agent-browser-native 0.3.0 → 0.6.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +265 -0
  2. package/README.md +130 -54
  3. package/dist/extensions/agent-browser/index.js +781 -169
  4. package/dist/extensions/agent-browser/lib/argv-descriptor.js +35 -3
  5. package/dist/extensions/agent-browser/lib/argv-grammar.js +50 -2
  6. package/dist/extensions/agent-browser/lib/batch-lifecycle.js +71 -0
  7. package/dist/extensions/agent-browser/lib/command-policy.js +5 -8
  8. package/dist/extensions/agent-browser/lib/command-taxonomy.js +53 -12
  9. package/dist/extensions/agent-browser/lib/config-policy.js +25 -1
  10. package/dist/extensions/agent-browser/lib/config.js +1 -1
  11. package/dist/extensions/agent-browser/lib/input-modes/job.js +61 -13
  12. package/dist/extensions/agent-browser/lib/input-modes/lookups.js +2 -2
  13. package/dist/extensions/agent-browser/lib/input-modes/params.js +23 -24
  14. package/dist/extensions/agent-browser/lib/input-modes/script.js +462 -0
  15. package/dist/extensions/agent-browser/lib/input-modes/semantic-action.js +51 -12
  16. package/dist/extensions/agent-browser/lib/launch-scoped-flags.js +26 -4
  17. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +6 -139
  18. package/dist/extensions/agent-browser/lib/managed-session-restore.js +26 -116
  19. package/dist/extensions/agent-browser/lib/managed-session-snapshots.js +2 -4
  20. package/dist/extensions/agent-browser/lib/managed-session-storage.js +54 -25
  21. package/dist/extensions/agent-browser/lib/orchestration/batch-stdin.js +26 -5
  22. package/dist/extensions/agent-browser/lib/orchestration/browser-run/artifact-paths.js +110 -30
  23. package/dist/extensions/agent-browser/lib/orchestration/browser-run/click-dispatch.js +2 -1
  24. package/dist/extensions/agent-browser/lib/orchestration/browser-run/diagnostics.js +54 -48
  25. package/dist/extensions/agent-browser/lib/orchestration/browser-run/final-result.js +71 -5
  26. package/dist/extensions/agent-browser/lib/orchestration/browser-run/index.js +2 -1
  27. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +6 -7
  28. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/snapshot-filter.js +119 -2
  29. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare/wait-timeouts.js +7 -6
  30. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +152 -64
  31. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +244 -102
  32. package/dist/extensions/agent-browser/lib/orchestration/browser-run/session-state.js +63 -37
  33. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +20 -21
  34. package/dist/extensions/agent-browser/lib/orchestration/input-plan.js +36 -18
  35. package/dist/extensions/agent-browser/lib/orchestration/output-file.js +41 -21
  36. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +299 -0
  37. package/dist/extensions/agent-browser/lib/page-target-validation.js +270 -0
  38. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +32 -10
  39. package/dist/extensions/agent-browser/lib/playbook.js +29 -25
  40. package/dist/extensions/agent-browser/lib/process-environment.js +14 -0
  41. package/dist/extensions/agent-browser/lib/process-identity.js +5 -12
  42. package/dist/extensions/agent-browser/lib/process.js +130 -104
  43. package/dist/extensions/agent-browser/lib/recording-reservations.js +116 -0
  44. package/dist/extensions/agent-browser/lib/results/action-recommendations.js +63 -6
  45. package/dist/extensions/agent-browser/lib/results/artifact-manifest.js +62 -4
  46. package/dist/extensions/agent-browser/lib/results/categories.js +6 -1
  47. package/dist/extensions/agent-browser/lib/results/next-actions.js +19 -5
  48. package/dist/extensions/agent-browser/lib/results/presentation/artifacts.js +85 -38
  49. package/dist/extensions/agent-browser/lib/results/presentation/batch.js +86 -18
  50. package/dist/extensions/agent-browser/lib/results/presentation/common.js +38 -2
  51. package/dist/extensions/agent-browser/lib/results/presentation/diagnostics.js +18 -17
  52. package/dist/extensions/agent-browser/lib/results/presentation/errors.js +2 -1
  53. package/dist/extensions/agent-browser/lib/results/presentation/navigation.js +38 -20
  54. package/dist/extensions/agent-browser/lib/results/presentation/registry.js +60 -15
  55. package/dist/extensions/agent-browser/lib/results/presentation/semantic-action.js +1 -10
  56. package/dist/extensions/agent-browser/lib/results/presentation.js +36 -6
  57. package/dist/extensions/agent-browser/lib/results/recovery-actions.js +3 -1
  58. package/dist/extensions/agent-browser/lib/results/recovery-next-actions.js +9 -0
  59. package/dist/extensions/agent-browser/lib/results/selector-recovery.js +54 -11
  60. package/dist/extensions/agent-browser/lib/results/snapshot-high-value-controls.js +13 -7
  61. package/dist/extensions/agent-browser/lib/results/snapshot-spill.js +2 -1
  62. package/dist/extensions/agent-browser/lib/results/snapshot.js +4 -4
  63. package/dist/extensions/agent-browser/lib/runtime.js +186 -108
  64. package/dist/extensions/agent-browser/lib/session-page-state.js +71 -10
  65. package/dist/extensions/agent-browser/lib/temp.js +1 -2
  66. package/dist/extensions/agent-browser/lib/upstream-version.js +14 -0
  67. package/dist/extensions/agent-browser/lib/web-search.js +108 -24
  68. package/dist/extensions/agent-browser/script-worker.js +169 -0
  69. package/dist/scripts/agent-browser-target.mjs +21 -0
  70. package/docs/ARCHITECTURE.md +57 -34
  71. package/docs/COMMAND_REFERENCE.md +255 -68
  72. package/docs/ELECTRON.md +2 -2
  73. package/docs/RELEASE.md +12 -10
  74. package/docs/REQUIREMENTS.md +11 -8
  75. package/docs/SUPPORT_MATRIX.md +36 -24
  76. package/docs/TOOL_CONTRACT.md +169 -95
  77. package/package.json +3 -1
  78. package/platform-smoke.config.mjs +2 -2
  79. package/scripts/agent-browser-capability-baseline.mjs +87 -9
  80. package/scripts/agent-browser-target.mjs +21 -0
  81. package/scripts/build.mjs +41 -0
  82. package/scripts/config.mjs +1 -0
  83. package/scripts/doctor.mjs +16 -9
  84. package/scripts/platform-smoke/browser-dogfood-windows.ps1 +9 -3
  85. package/scripts/platform-smoke/targets.mjs +12 -6
  86. package/dist/extensions/agent-browser/lib/managed-session-capabilities.js +0 -20
  87. package/dist/extensions/agent-browser/lib/managed-session-state-policy.js +0 -583
  88. package/dist/extensions/agent-browser/lib/navigation-policy.js +0 -78
  89. package/dist/extensions/agent-browser/lib/results/presentation/managed-list-filter.js +0 -37
@@ -1,12 +1,15 @@
1
+ import { isCloseCommand } from "../../command-taxonomy.js";
1
2
  import { isRecord } from "../../parsing.js";
2
- import { extractCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
+ import { getAgentBrowserSessionIdentityKey } from "../../argv-grammar.js";
4
+ import { extractUpstreamCommandTokens, parseCommandInfo, redactInvocationArgs, redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
5
  import { buildAgentBrowserNextActions } from "../action-recommendations.js";
4
- import { formatSessionArtifactRetentionSummary } from "../artifact-manifest.js";
6
+ import { buildPendingWebMcpNextActions } from "../recovery-next-actions.js";
7
+ import { formatSessionArtifactRetentionSummary, isPendingRecordingArtifact } from "../artifact-manifest.js";
5
8
  import { classifyAgentBrowserFailureCategory } from "../categories.js";
6
9
  import { detectConfirmationRequired } from "../confirmation.js";
7
10
  import { applyNetworkRouteRecords, buildNetworkRouteDiagnostics } from "../network-routes.js";
8
- import { applyNamespaceToNextActions, withOptionalSessionArgs } from "../next-actions.js";
9
- import { stringifyModelFacing } from "./common.js";
11
+ import { appendUniqueAgentBrowserNextActions, applyNamespaceToNextActions, withOptionalSessionArgs } from "../next-actions.js";
12
+ import { extractAgentBrowserLifecycle, stringifyModelFacing } from "./common.js";
10
13
  import { buildArtifactVerificationSummary, classifyPresentationSuccessCategory, manifestHasNewNoticeWorthyEntries } from "./artifacts.js";
11
14
  import { formatBatchStepCommand, getPresentationImages, getPresentationPaths, getPresentationText, isStringArray } from "./content.js";
12
15
  import { buildPageChangeSummary } from "./navigation.js";
@@ -62,7 +65,7 @@ function hasModelFacingArgRedaction(args) {
62
65
  function getStatefulCommandSensitiveValues(command) {
63
66
  if (!command)
64
67
  return [];
65
- const tokens = extractCommandTokens(command);
68
+ const tokens = extractUpstreamCommandTokens(command);
66
69
  const values = [];
67
70
  if (tokens[0] === "cookies" && tokens[1] === "set" && tokens[3])
68
71
  values.push(tokens[3]);
@@ -92,6 +95,11 @@ function redactExactValues(value, sensitiveValues) {
92
95
  return value;
93
96
  return redactSensitiveValue(Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, redactExactValues(entryValue, sensitiveValues)])));
94
97
  }
98
+ export function redactBatchStepErrorData(command, value) {
99
+ return command?.[0] === "clipboard"
100
+ ? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, value, getClipboardWritePayloadCandidates(command)))
101
+ : redactExactValues(value, getStatefulCommandSensitiveValues(command));
102
+ }
95
103
  function getTypedTextLength(command) {
96
104
  return command?.[0] === "keyboard" && command[1] === "type" && typeof command[2] === "string"
97
105
  ? Array.from(command[2]).length
@@ -181,14 +189,13 @@ function formatBatchStepsText(steps) {
181
189
  return lines.join("\n\n");
182
190
  }
183
191
  async function buildBatchStepPresentation(options) {
184
- const { artifactManifest, artifactRequest, buildNestedToolPresentation, cwd, index, item, namespace, networkRoutes, persistentArtifactStore, sessionName } = options;
192
+ const { artifactManifest, artifactMaxUpdatedAtMs, artifactMinUpdatedAtMs, artifactRequest, buildNestedToolPresentation, cwd, index, item, namespace, networkRoutes, persistentArtifactStore, sessionName } = options;
185
193
  const command = isStringArray(item.command) ? item.command : undefined;
186
194
  const redactedCommand = command ? redactInvocationArgs(command) : undefined;
187
195
  const commandText = formatBatchStepCommand(hasModelFacingArgRedaction(redactedCommand) ? redactedCommand : command, index);
196
+ const lifecycle = extractAgentBrowserLifecycle(item.result);
188
197
  if (item.success === false) {
189
- const redactedErrorData = command?.[0] === "clipboard"
190
- ? redactSensitiveValue(redactClipboardPermissionErrorValue({ command: "clipboard", subcommand: command[1] }, item.error, getClipboardWritePayloadCandidates(command)))
191
- : redactExactValues(item.error, getStatefulCommandSensitiveValues(command));
198
+ const redactedErrorData = redactBatchStepErrorData(command, item.error);
192
199
  const errorText = formatBatchStepError(redactedErrorData);
193
200
  const failureCategory = classifyAgentBrowserFailureCategory({
194
201
  args: command,
@@ -196,13 +203,15 @@ async function buildBatchStepPresentation(options) {
196
203
  errorText,
197
204
  });
198
205
  const confirmationRequired = detectConfirmationRequired(item.error);
199
- const nextActions = applyNamespaceToNextActions(mergePresentationNextActions(buildAgentBrowserNextActions({
206
+ const nextActions = applyNamespaceToNextActions(mergePresentationNextActions(isWaitTextAssertionCommand(command) ? [buildWaitTextAssertionFailureNextAction(sessionName)] : undefined, buildAgentBrowserNextActions({
200
207
  args: command,
201
208
  command: command?.[0],
202
209
  confirmationId: confirmationRequired?.id,
203
210
  failureCategory,
204
211
  resultCategory: "failure",
205
- }), isWaitTextAssertionCommand(command) ? [buildWaitTextAssertionFailureNextAction(sessionName)] : undefined), namespace);
212
+ sessionName,
213
+ subcommand: command?.[1],
214
+ })), namespace);
206
215
  const presentation = {
207
216
  content: [{ type: "text", text: errorText }],
208
217
  failureCategory,
@@ -219,6 +228,7 @@ async function buildBatchStepPresentation(options) {
219
228
  data: redactedErrorData,
220
229
  failureCategory,
221
230
  index,
231
+ lifecycle,
222
232
  nextActions,
223
233
  resultCategory: "failure",
224
234
  success: false,
@@ -235,12 +245,15 @@ async function buildBatchStepPresentation(options) {
235
245
  : undefined;
236
246
  const presentation = await buildNestedToolPresentation({
237
247
  artifactManifest,
248
+ artifactMaxUpdatedAtMs,
249
+ artifactMinUpdatedAtMs,
238
250
  artifactRequest,
239
251
  commandInfo: commandInfoWithTokens,
240
252
  cwd,
241
253
  args: command,
242
254
  envelope: { data: item.result, success: true },
243
255
  networkRouteDiagnostics,
256
+ namespace,
244
257
  persistentArtifactStore,
245
258
  sessionName,
246
259
  });
@@ -254,13 +267,20 @@ async function buildBatchStepPresentation(options) {
254
267
  });
255
268
  const text = getPresentationText(presentation) || presentation.summary;
256
269
  const stepSucceeded = presentation.resultCategory !== "failure";
257
- const nextActions = applyNamespaceToNextActions(presentation.nextActions ?? buildAgentBrowserNextActions({
270
+ const pendingWebMcpMutation = stepSucceeded
271
+ && commandInfo.command === "webmcp"
272
+ && ["invoke", "result"].includes(commandInfo.subcommand ?? "")
273
+ && isRecord(presentation.data)
274
+ && presentation.data.status === "pending";
275
+ const nextActions = applyNamespaceToNextActions(pendingWebMcpMutation ? buildPendingWebMcpNextActions(sessionName) : presentation.nextActions ?? buildAgentBrowserNextActions({
258
276
  artifacts: presentation.artifacts,
259
277
  args: command,
260
278
  command: command?.[0],
261
279
  failureCategory: presentation.failureCategory,
262
280
  resultCategory: stepSucceeded ? "success" : "failure",
263
281
  savedFilePath: presentation.savedFilePath,
282
+ sessionName,
283
+ subcommand: command?.[1],
264
284
  successCategory: presentation.successCategory,
265
285
  }), namespace);
266
286
  const pageChangeSummary = buildPageChangeSummary({
@@ -284,6 +304,7 @@ async function buildBatchStepPresentation(options) {
284
304
  imagePath: imagePaths[0],
285
305
  imagePaths: imagePaths.length > 0 ? imagePaths : undefined,
286
306
  index,
307
+ lifecycle,
287
308
  networkRouteDiagnostics: presentation.networkRouteDiagnostics,
288
309
  nextActions,
289
310
  pageChangeSummary,
@@ -298,6 +319,42 @@ async function buildBatchStepPresentation(options) {
298
319
  presentation,
299
320
  };
300
321
  }
322
+ function abandonedRecordingArtifact(artifact) {
323
+ const { recordingState: _recordingState, willExistOnStop: _willExistOnStop, ...terminal } = artifact;
324
+ return { ...terminal, exists: false, status: "missing", subcommand: "close-abandoned" };
325
+ }
326
+ function coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace) {
327
+ const artifacts = [];
328
+ const pendingIndexesBySession = new Map();
329
+ const removedPendingIndexes = new Set();
330
+ for (const step of steps) {
331
+ for (const artifact of step.presentation.artifacts ?? []) {
332
+ const index = artifacts.push(artifact) - 1;
333
+ if (artifact.command !== "record" || artifact.kind !== "video")
334
+ continue;
335
+ const session = artifact.session ? getAgentBrowserSessionIdentityKey(artifact.session, artifact.namespace) : "";
336
+ if (isPendingRecordingArtifact(artifact)) {
337
+ const pendingIndexes = pendingIndexesBySession.get(session) ?? [];
338
+ pendingIndexes.push(index);
339
+ pendingIndexesBySession.set(session, pendingIndexes);
340
+ continue;
341
+ }
342
+ const pendingIndex = pendingIndexesBySession.get(session)?.pop();
343
+ if (pendingIndex !== undefined)
344
+ removedPendingIndexes.add(pendingIndex);
345
+ }
346
+ const command = step.details.command;
347
+ if (step.details.success !== true || !command || !isCloseCommand(extractUpstreamCommandTokens(command)[0]))
348
+ continue;
349
+ const session = sessionName ? getAgentBrowserSessionIdentityKey(sessionName, namespace) : "";
350
+ for (const pendingIndex of pendingIndexesBySession.get(session) ?? []) {
351
+ const pending = artifacts[pendingIndex];
352
+ if (pending && !removedPendingIndexes.has(pendingIndex))
353
+ artifacts[pendingIndex] = abandonedRecordingArtifact(pending);
354
+ }
355
+ }
356
+ return artifacts.filter((_, index) => !removedPendingIndexes.has(index));
357
+ }
301
358
  export async function buildBatchPresentation(options) {
302
359
  const { artifactRequests, buildNestedToolPresentation, cwd, data, namespace, networkRoutes, persistentArtifactStore, sessionName, summary } = options;
303
360
  const steps = [];
@@ -307,6 +364,8 @@ export async function buildBatchPresentation(options) {
307
364
  for (const [index, item] of data.entries()) {
308
365
  const step = await buildBatchStepPresentation({
309
366
  artifactManifest: currentArtifactManifest,
367
+ artifactMaxUpdatedAtMs: options.artifactMaxUpdatedAtMs,
368
+ artifactMinUpdatedAtMs: options.artifactMinUpdatedAtMs,
310
369
  artifactRequest: artifactRequests?.[index],
311
370
  buildNestedToolPresentation,
312
371
  cwd,
@@ -319,7 +378,7 @@ export async function buildBatchPresentation(options) {
319
378
  });
320
379
  steps.push(step);
321
380
  currentArtifactManifest = step.presentation.artifactManifest ?? currentArtifactManifest;
322
- currentNetworkRoutes = applyNetworkRouteRecords(currentNetworkRoutes, isStringArray(item.command) ? extractCommandTokens(item.command) : undefined, item.success !== false && step.details.success);
381
+ currentNetworkRoutes = applyNetworkRouteRecords(currentNetworkRoutes, isStringArray(item.command) ? extractUpstreamCommandTokens(item.command) : undefined, item.success !== false && step.details.success);
323
382
  protectedPersistentPaths.push(...getPresentationPaths({
324
383
  primaryPath: step.presentation.fullOutputPath,
325
384
  secondaryPaths: step.presentation.fullOutputPaths,
@@ -327,7 +386,7 @@ export async function buildBatchPresentation(options) {
327
386
  }
328
387
  const batchFailure = getBatchFailureDetails(steps);
329
388
  const images = steps.flatMap((step) => getPresentationImages(step.presentation));
330
- const artifacts = steps.flatMap((step) => step.presentation.artifacts ?? []);
389
+ const artifacts = coalesceTerminalBatchRecordingArtifacts(steps, sessionName, namespace);
331
390
  const artifactVerification = buildArtifactVerificationSummary(artifacts);
332
391
  const fullOutputPaths = steps.flatMap((step) => getPresentationPaths({
333
392
  primaryPath: step.presentation.fullOutputPath,
@@ -340,6 +399,10 @@ export async function buildBatchPresentation(options) {
340
399
  const redactedBatchData = steps.map(({ details }) => (details.success
341
400
  ? { command: details.command, result: details.data, success: true }
342
401
  : { command: details.command, error: details.text, success: false }));
402
+ const unverifiedMutationCount = steps.filter((step) => step.details.pageChangeSummary?.changeType === "mutation" && step.details.pageChangeSummary.observed === false).length;
403
+ const mutationEvidenceText = unverifiedMutationCount > 0
404
+ ? `Mutation evidence: ${unverifiedMutationCount} action result${unverifiedMutationCount === 1 ? " proves" : "s prove"} dispatch only, not application state change. Use explicit later assertions or external receipts as postconditions; fixed waits are not postconditions.`
405
+ : undefined;
343
406
  const stepText = formatBatchStepsText(steps);
344
407
  const batchSummary = batchFailure === undefined
345
408
  ? summary
@@ -351,15 +414,17 @@ export async function buildBatchPresentation(options) {
351
414
  `First failing step: ${batchFailure.failedStep.index + 1} — ${batchFailure.failedStep.commandText}`,
352
415
  batchFailure.failureCount > 1 ? `${batchFailure.failureCount} steps failed. See the per-step results below.` : "See the per-step results below.",
353
416
  ].join("\n");
354
- const text = failureHeader ? `${failureHeader}\n\n${stepText}` : stepText;
417
+ const text = [failureHeader, mutationEvidenceText, stepText].filter((line) => line !== undefined).join("\n\n");
355
418
  const artifactRetentionSummary = currentArtifactManifest ? formatSessionArtifactRetentionSummary(currentArtifactManifest) : undefined;
356
419
  const contentText = artifactRetentionSummary && manifestHasNewNoticeWorthyEntries(options.artifactManifest, currentArtifactManifest)
357
420
  ? `${text}\n\n${artifactRetentionSummary}`
358
421
  : text;
422
+ const artifactLifecycleActions = applyNamespaceToNextActions(buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: batchFailure ? "failure" : "success", sessionName }), namespace);
359
423
  const nextActions = batchFailure
360
- ? batchFailure.failedStep.nextActions
361
- : buildAgentBrowserNextActions({ artifacts, command: "batch", resultCategory: "success" });
424
+ ? appendUniqueAgentBrowserNextActions([...(batchFailure.failedStep.nextActions ?? [])], artifactLifecycleActions)
425
+ : artifactLifecycleActions;
362
426
  const changedSteps = steps.map((step) => step.details).filter((details) => details.pageChangeSummary !== undefined);
427
+ const observedChangeCount = changedSteps.filter((details) => details.pageChangeSummary?.observed === true).length;
363
428
  const pageChangeSummary = artifacts.length > 0
364
429
  ? buildPageChangeSummary({
365
430
  artifacts,
@@ -373,7 +438,10 @@ export async function buildBatchPresentation(options) {
373
438
  changeType: "mutation",
374
439
  command: "batch",
375
440
  nextActionIds: nextActions?.map((action) => action.id),
376
- summary: `batch mutation → ${changedSteps.length} changed step${changedSteps.length === 1 ? "" : "s"}`,
441
+ observed: observedChangeCount > 0,
442
+ summary: observedChangeCount > 0
443
+ ? `batch → ${observedChangeCount} observed change${observedChangeCount === 1 ? "" : "s"}${unverifiedMutationCount > 0 ? `; ${unverifiedMutationCount} dispatched action${unverifiedMutationCount === 1 ? "" : "s"} unverified` : ""}`
444
+ : `batch → ${unverifiedMutationCount} action${unverifiedMutationCount === 1 ? "" : "s"} dispatched → application change unverified`,
377
445
  }
378
446
  : undefined;
379
447
  return {
@@ -1,6 +1,7 @@
1
- import { containsManagedSessionRestoreKey } from "../../managed-session-capabilities.js";
1
+ import { isRecord } from "../../parsing.js";
2
2
  import { redactSensitiveText, redactSensitiveValue } from "../../runtime.js";
3
3
  import { stringifyUnknown, truncateText } from "../text.js";
4
+ const UNTITLED_PAGE_SUMMARY = "(untitled page)";
4
5
  export function stringifyModelFacing(value) {
5
6
  return stringifyUnknown(redactSensitiveValue(value));
6
7
  }
@@ -23,7 +24,7 @@ export function redactModelFacingText(text) {
23
24
  return redactSensitiveText(text);
24
25
  }
25
26
  export function redactModelFacingTextIfSensitive(text) {
26
- return containsManagedSessionRestoreKey(text) || /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
27
+ return /(?:@|\b(?:access[_-]?key|api[_-]?key|auth|authorization|basic|bearer|connection[_-]?string|cookie|database[_-]?url|db[_-]?url|mongo(?:db)?[_-]?uri|pass(?:word)?|private[_-]?key|redis[_-]?url|secret|session[_-]?id|token)\b)/i.test(text)
27
28
  ? redactModelFacingText(text)
28
29
  : text;
29
30
  }
@@ -34,6 +35,41 @@ export function getStringField(data, key) {
34
35
  const value = data[key];
35
36
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
36
37
  }
38
+ // `lifecycle` is upstream launch/reuse bookkeeping, never page content, so it must not be the
39
+ // answer an agent reads when a command has no dedicated presenter.
40
+ export function extractAgentBrowserLifecycle(result) {
41
+ if (Array.isArray(result)) {
42
+ let latest;
43
+ for (const row of result) {
44
+ const nested = isRecord(row) ? row.result ?? row.data ?? row : row;
45
+ latest = extractAgentBrowserLifecycle(nested) ?? latest;
46
+ }
47
+ return latest;
48
+ }
49
+ if (!isRecord(result))
50
+ return undefined;
51
+ if (isRecord(result.lifecycle) && isRecord(result.lifecycle.effectiveLaunch)) {
52
+ const browserLaunched = result.lifecycle.effectiveLaunch.browserLaunched;
53
+ if (typeof browserLaunched === "boolean")
54
+ return { effectiveLaunch: { browserLaunched } };
55
+ }
56
+ return extractAgentBrowserLifecycle(result.result ?? result.data);
57
+ }
58
+ export function omitUpstreamLifecycle(data) {
59
+ const { lifecycle: _lifecycle, ...rest } = data;
60
+ return rest;
61
+ }
62
+ export function getPageSummary(data) {
63
+ const title = typeof data.title === "string" ? data.title : undefined;
64
+ const url = typeof data.url === "string" ? data.url : undefined;
65
+ if (title === undefined && url === undefined)
66
+ return undefined;
67
+ if (title && url)
68
+ return `${title}\n${url}`;
69
+ if (url)
70
+ return url;
71
+ return title || UNTITLED_PAGE_SUMMARY;
72
+ }
37
73
  export function formatCount(count, singular, plural = `${singular}s`) {
38
74
  return `${count} ${count === 1 ? singular : plural}`;
39
75
  }
@@ -4,7 +4,6 @@ import { classifyNetworkRequestFailure, isApiLikeNetworkRequest, isNetworkArtifa
4
4
  import { withOptionalSessionArgs } from "../next-actions.js";
5
5
  import { stringifyUnknown, truncateText } from "../text.js";
6
6
  import { firstLine, formatCount, getArrayField, getStringField, parseJsonPreviewString, redactModelFacingText, redactModelFacingTextIfSensitive, stringifyModelFacing, } from "./common.js";
7
- import { filterCallerOwnedSessionListItems, filterCallerOwnedStateListItems, filterManagedSessionListRows, filterManagedStateListRows, } from "./managed-list-filter.js";
8
7
  const DIAGNOSTIC_REQUEST_PREVIEW_LIMIT = 40;
9
8
  const DIAGNOSTIC_LOG_PREVIEW_LIMIT = 80;
10
9
  const NETWORK_BODY_PREVIEW_MAX_CHARS = 280;
@@ -61,7 +60,9 @@ export function getTabSummary(data) {
61
60
  ? String(tab.index)
62
61
  : String(index);
63
62
  const labelText = label && label !== tabSelector ? ` label=${redactModelFacingText(label)}` : "";
64
- return `${marker} [${tabSelector}]${labelText} ${title} ${url}`;
63
+ const targetId = typeof tab.targetId === "string" && tab.targetId.trim().length > 0 ? tab.targetId.trim() : undefined;
64
+ const targetText = targetId && targetId !== tabSelector ? ` target=${redactModelFacingText(targetId)}` : "";
65
+ return `${marker} [${tabSelector}]${labelText}${targetText} ${title} — ${url}`;
65
66
  });
66
67
  return lines.join("\n");
67
68
  }
@@ -110,7 +111,7 @@ export function formatDiagnosticSummary(commandInfo, data) {
110
111
  if (commandInfo.command === "session") {
111
112
  const sessions = getArrayField(data, "sessions");
112
113
  if (sessions)
113
- return `Sessions: ${filterCallerOwnedSessionListItems(sessions).length}`;
114
+ return `Sessions: ${sessions.length}`;
114
115
  const session = getStringField(data, "session");
115
116
  if (session)
116
117
  return `Session: ${session}`;
@@ -168,10 +169,8 @@ export function formatDiagnosticSummary(commandInfo, data) {
168
169
  }
169
170
  if (commandInfo.command === "state") {
170
171
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
171
- if (states) {
172
- const visibleStates = commandInfo.subcommand === "list" ? filterCallerOwnedStateListItems(states) : states;
173
- return `States: ${visibleStates.length}`;
174
- }
172
+ if (states)
173
+ return `States: ${states.length}`;
175
174
  if (commandInfo.subcommand === "load")
176
175
  return undefined;
177
176
  const stateName = getStringField(data, "name") ?? getStringField(data, "file") ?? getStringField(data, "filename") ?? getStringField(data, "path") ?? commandInfo.subcommand;
@@ -259,10 +258,9 @@ export function formatDiagnosticSummary(commandInfo, data) {
259
258
  function formatSessionText(data) {
260
259
  const sessions = getArrayField(data, "sessions");
261
260
  if (sessions) {
262
- const visibleSessions = filterCallerOwnedSessionListItems(sessions);
263
- if (visibleSessions.length === 0)
264
- return sessions.length === 0 ? "No active sessions." : "No caller-owned active sessions.";
265
- return visibleSessions
261
+ if (sessions.length === 0)
262
+ return "No active sessions.";
263
+ return sessions
266
264
  .map((item, index) => {
267
265
  if (!isRecord(item))
268
266
  return `${index + 1}. ${stringifyModelFacing(item)}`;
@@ -348,6 +346,8 @@ function formatNetworkRequestLine(item, index) {
348
346
  }
349
347
  function formatNetworkRequestsText(data, commandInfo) {
350
348
  const requests = getArrayField(data, "requests");
349
+ if (isClearDiagnosticCommand(commandInfo) && data.cleared === true && !requests)
350
+ return "Network request buffer cleared.";
351
351
  if (!requests)
352
352
  return undefined;
353
353
  if (isClearDiagnosticCommand(commandInfo)) {
@@ -593,6 +593,8 @@ export function buildStreamNextActions(commandInfo, data, sessionName) {
593
593
  }
594
594
  function formatConsoleText(data, commandInfo) {
595
595
  const messages = getArrayField(data, "messages");
596
+ if (isClearDiagnosticCommand(commandInfo) && data.cleared === true && !messages)
597
+ return "Console buffer cleared.";
596
598
  if (!messages)
597
599
  return undefined;
598
600
  if (isClearDiagnosticCommand(commandInfo)) {
@@ -926,10 +928,9 @@ function formatStateText(data, subcommand) {
926
928
  }
927
929
  const states = getArrayField(data, "states") ?? getArrayField(data, "files");
928
930
  if (states) {
929
- const visibleStates = filterCallerOwnedStateListItems(states);
930
- if (visibleStates.length === 0)
931
- return "No caller-owned saved states.";
932
- return visibleStates
931
+ if (states.length === 0)
932
+ return "No saved states.";
933
+ return states
933
934
  .map((item, index) => {
934
935
  if (!isRecord(item))
935
936
  return `${index + 1}. ${redactModelFacingTextIfSensitive(stringifyModelFacing(item))}`;
@@ -973,9 +974,9 @@ export function redactPresentationData(commandInfo, data) {
973
974
  if (commandInfo.command === "storage")
974
975
  return redactStorageData(data);
975
976
  if (commandInfo.command === "session" && commandInfo.subcommand === "list")
976
- return redactStructuredPresentationValue(filterManagedSessionListRows(data));
977
+ return redactStructuredPresentationValue(data);
977
978
  if (commandInfo.command === "state" && commandInfo.subcommand === "list")
978
- return redactStructuredPresentationValue(filterManagedStateListRows(data));
979
+ return redactStructuredPresentationValue(data);
979
980
  if (commandInfo.command === "state" && commandInfo.subcommand === "show")
980
981
  return redactStatefulValues(data, new Set(["value"]));
981
982
  return redactStructuredPresentationValue(data);
@@ -189,11 +189,12 @@ export function buildErrorPresentation(options) {
189
189
  const nextActions = [
190
190
  ...(buildUnknownCommandSuggestionActions(unknownCommandSuggestions, sessionName) ?? []),
191
191
  ...(browserProfileConfigRecovery?.actions ?? []),
192
- ...(buildAgentBrowserNextActions({
192
+ ...(browserProfileConfigRecovery ? [] : buildAgentBrowserNextActions({
193
193
  args,
194
194
  command: commandInfo.command,
195
195
  failureCategory: categoryDetails.failureCategory,
196
196
  resultCategory: "failure",
197
+ sessionName,
197
198
  }) ?? []),
198
199
  ];
199
200
  return {
@@ -1,16 +1,31 @@
1
- import { isNavigationObservableCommandName, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
1
+ import { isNavigationObservableCommandName, isOpenNavigationCommand, isPageChangeSummaryCommand } from "../../command-taxonomy.js";
2
2
  import { isRecord } from "../../parsing.js";
3
3
  import { detectConfirmationRequired } from "../confirmation.js";
4
- import { redactModelFacingText, stringifyModelFacing } from "./common.js";
4
+ import { omitUpstreamLifecycle, redactModelFacingText, stringifyModelFacing } from "./common.js";
5
5
  const NAVIGATION_SUMMARY_FIELD = "navigationSummary";
6
- function getScalarExtractionResult(data) {
7
- const { result } = data;
8
- if (typeof result === "string") {
9
- return result.trim().length > 0 ? result : undefined;
10
- }
11
- if (typeof result === "number" || typeof result === "boolean") {
6
+ const GET_RESULT_FIELDS = {
7
+ attr: "value",
8
+ count: "count",
9
+ html: "html",
10
+ text: "text",
11
+ title: "title",
12
+ url: "url",
13
+ value: "value",
14
+ };
15
+ function getScalarExtractionResult(commandInfo, data) {
16
+ const fallbackField = commandInfo.command === "get" && commandInfo.subcommand ? GET_RESULT_FIELDS[commandInfo.subcommand] ?? "" : "";
17
+ const resultField = Object.hasOwn(data, "result") ? "result" : fallbackField.length > 0 && Object.hasOwn(data, fallbackField) ? fallbackField : undefined;
18
+ if (resultField === undefined)
19
+ return undefined;
20
+ const result = data[resultField];
21
+ if (typeof result === "string")
22
+ return result.trim().length > 0 ? result : "(empty string)";
23
+ if (typeof result === "number" || typeof result === "boolean")
12
24
  return String(result);
13
- }
25
+ if (result === null || result === undefined)
26
+ return "null";
27
+ if (typeof result === "object")
28
+ return JSON.stringify(result);
14
29
  return undefined;
15
30
  }
16
31
  function getExtractionOrigin(data) {
@@ -32,7 +47,7 @@ function formatGetSummaryLabel(subcommand) {
32
47
  return `${subcommand.slice(0, 1).toUpperCase()}${subcommand.slice(1)}`;
33
48
  }
34
49
  export function formatExtractionSummary(commandInfo, data) {
35
- const scalarResult = getScalarExtractionResult(data);
50
+ const scalarResult = getScalarExtractionResult(commandInfo, data);
36
51
  if (!scalarResult) {
37
52
  return undefined;
38
53
  }
@@ -50,7 +65,7 @@ export function formatExtractionText(commandInfo, data) {
50
65
  if (commandInfo.command !== "get" && commandInfo.command !== "eval") {
51
66
  return undefined;
52
67
  }
53
- const scalarResult = getScalarExtractionResult(data);
68
+ const scalarResult = getScalarExtractionResult(commandInfo, data);
54
69
  if (!scalarResult) {
55
70
  return undefined;
56
71
  }
@@ -59,8 +74,8 @@ export function formatExtractionText(commandInfo, data) {
59
74
  const safeOrigin = origin ? redactModelFacingText(origin) : undefined;
60
75
  return safeOrigin && safeOrigin !== safeScalarResult ? `${safeScalarResult}\n\nOrigin: ${safeOrigin}` : safeScalarResult;
61
76
  }
62
- export function isNavigationObservableCommand(command) {
63
- return isNavigationObservableCommandName(command);
77
+ export function isNavigationObservableCommand(command, subcommand) {
78
+ return isNavigationObservableCommandName(command, subcommand);
64
79
  }
65
80
  function isNavigationSummary(value) {
66
81
  return isRecord(value) && (typeof value.title === "string" || typeof value.url === "string");
@@ -80,7 +95,7 @@ function getTopLevelNavigationSummary(data) {
80
95
  function getNormalizedNavigationSummary(summary) {
81
96
  const title = typeof summary?.title === "string" && summary.title.trim().length > 0 ? summary.title.trim() : undefined;
82
97
  const url = typeof summary?.url === "string" && summary.url.trim().length > 0 ? summary.url.trim() : undefined;
83
- return title || url ? { title, url } : undefined;
98
+ return title || url ? { title, url, ...(typeof summary?.urlChanged === "boolean" ? { urlChanged: summary.urlChanged } : {}) } : undefined;
84
99
  }
85
100
  export function formatNavigationSummary(summary) {
86
101
  const normalized = getNormalizedNavigationSummary(summary);
@@ -94,20 +109,22 @@ export function buildPageChangeSummary(options) {
94
109
  const { artifacts, commandInfo, data, nextActions, savedFilePath } = options;
95
110
  const artifactCount = artifacts?.length ?? 0;
96
111
  const navigation = isRecord(data)
97
- ? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command) ? getTopLevelNavigationSummary(data) : undefined))
112
+ ? getNormalizedNavigationSummary(getNavigationSummary(data) ?? (isPageChangeSummaryCommand(commandInfo.command, commandInfo.subcommand) ? getTopLevelNavigationSummary(data) : undefined))
98
113
  : undefined;
99
114
  const confirmationRequired = detectConfirmationRequired(data) !== undefined;
100
- if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command)) {
115
+ if (!navigation && !confirmationRequired && artifactCount === 0 && !savedFilePath && !isPageChangeSummaryCommand(commandInfo.command, commandInfo.subcommand)) {
101
116
  return undefined;
102
117
  }
118
+ const navigationObserved = navigation && (navigation.urlChanged === true || isOpenNavigationCommand(commandInfo.command) || ["back", "forward", "pushstate", "reload"].includes(commandInfo.command ?? ""));
103
119
  const changeType = savedFilePath || artifactCount > 0
104
120
  ? "artifact"
105
- : navigation
121
+ : navigationObserved
106
122
  ? "navigation"
107
123
  : confirmationRequired
108
124
  ? "confirmation"
109
125
  : "mutation";
110
- const parts = [commandInfo.command ?? "agent-browser", changeType];
126
+ const observed = changeType !== "mutation";
127
+ const parts = [commandInfo.command ?? "agent-browser", observed ? changeType : "action dispatched"];
111
128
  if (navigation?.title)
112
129
  parts.push(navigation.title);
113
130
  if (navigation?.url)
@@ -121,8 +138,9 @@ export function buildPageChangeSummary(options) {
121
138
  changeType,
122
139
  ...(commandInfo.command ? { command: commandInfo.command } : {}),
123
140
  ...(nextActions ? { nextActionIds: nextActions.map((action) => action.id) } : {}),
141
+ observed,
124
142
  ...(savedFilePath ? { savedFilePath } : {}),
125
- summary: parts.join(" → "),
143
+ summary: `${parts.join(" → ")}${observed ? "" : " → application change unverified"}`,
126
144
  ...(navigation?.title ? { title: navigation.title } : {}),
127
145
  ...(navigation?.url ? { url: navigation.url } : {}),
128
146
  };
@@ -132,7 +150,7 @@ function stripNavigationSummary(data) {
132
150
  return rest;
133
151
  }
134
152
  export function formatNavigationActionResult(data) {
135
- const actionData = stripNavigationSummary(data);
153
+ const actionData = omitUpstreamLifecycle(stripNavigationSummary(data));
136
154
  const lines = [];
137
155
  if (typeof actionData.clicked === "string" || typeof actionData.clicked === "boolean") {
138
156
  lines.push(`Clicked: ${String(actionData.clicked)}`);