pi-agent-browser-native 0.8.2 → 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.
Files changed (37) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +24 -15
  3. package/dist/extensions/agent-browser/index.js +496 -343
  4. package/dist/extensions/agent-browser/lib/browser-journal.js +439 -0
  5. package/dist/extensions/agent-browser/lib/browser-session-conversion.js +381 -0
  6. package/dist/extensions/agent-browser/lib/browser-transcript.js +119 -19
  7. package/dist/extensions/agent-browser/lib/execution-cwd.js +0 -16
  8. package/dist/extensions/agent-browser/lib/input-modes/job.js +49 -2
  9. package/dist/extensions/agent-browser/lib/input-modes/params.js +32 -35
  10. package/dist/extensions/agent-browser/lib/managed-session-policy-lock.js +25 -5
  11. package/dist/extensions/agent-browser/lib/managed-session-restore.js +20 -5
  12. package/dist/extensions/agent-browser/lib/native-output.js +46 -0
  13. package/dist/extensions/agent-browser/lib/orchestration/browser-run/managed-session-daemon-policy.js +97 -78
  14. package/dist/extensions/agent-browser/lib/orchestration/browser-run/prepare.js +23 -2
  15. package/dist/extensions/agent-browser/lib/orchestration/browser-run/process-output.js +52 -17
  16. package/dist/extensions/agent-browser/lib/orchestration/electron-host/index.js +1 -1
  17. package/dist/extensions/agent-browser/lib/orchestration/script-mode.js +13 -8
  18. package/dist/extensions/agent-browser/lib/pi-tool-rendering.js +30 -16
  19. package/dist/extensions/agent-browser/lib/playbook.js +9 -2
  20. package/dist/extensions/agent-browser/lib/prompt-policy.js +16 -26
  21. package/dist/extensions/agent-browser/lib/results/presentation/content.js +4 -0
  22. package/dist/extensions/agent-browser/lib/results/presentation/large-output.js +17 -9
  23. package/dist/extensions/agent-browser/lib/session-page-state.js +95 -157
  24. package/dist/extensions/agent-browser/lib/temp.js +14 -6
  25. package/dist/extensions/agent-browser/lib/tool-surface.js +50 -33
  26. package/dist/extensions/agent-browser/lib/web-search.js +85 -72
  27. package/docs/ARCHITECTURE.md +25 -25
  28. package/docs/COMMAND_REFERENCE.md +13 -9
  29. package/docs/RELEASE.md +25 -19
  30. package/docs/REQUIREMENTS.md +6 -6
  31. package/docs/SESSION_STORAGE.md +60 -0
  32. package/docs/SUPPORT_MATRIX.md +16 -11
  33. package/docs/TOOL_CONTRACT.md +26 -19
  34. package/package.json +9 -5
  35. package/scripts/build.mjs +4 -3
  36. package/scripts/convert-browser-session.mjs +22 -0
  37. package/scripts/doctor.mjs +2 -2
@@ -15,7 +15,7 @@ import { buildToolPresentation } from "../../results/presentation.js";
15
15
  import { compactLargePresentationOutput } from "../../results/presentation/large-output.js";
16
16
  import { extractEnvelopeErrorText, getAgentBrowserErrorText, parseAgentBrowserEnvelope } from "../../results/envelope.js";
17
17
  import { detectConfirmationRequired } from "../../results/confirmation.js";
18
- import { omitUpstreamLifecycle } from "../../results/presentation/common.js";
18
+ import { extractAgentBrowserLifecycle, omitUpstreamLifecycle } from "../../results/presentation/common.js";
19
19
  import { getClipboardWritePayloadCandidates, redactClipboardPermissionEcho, redactClipboardPermissionErrorValue } from "../../results/presentation/errors.js";
20
20
  import { shouldCaptureSemanticActionNavigationSummary } from "../../results/presentation/semantic-action.js";
21
21
  import { buildPageTransitionRefSnapshotInvalidation, commandExplicitlyTargetsAboutBlank, getCommandRefSnapshotInvalidation, deriveSessionTabTarget, extractLatestRefSnapshotStateFromBatchResults, extractRefSnapshotFromData, extractSessionTabTargetFromBatchResults, extractSessionTabTargetFromCommandData, isAboutBlankSessionTabTarget, normalizeSessionTabTarget, } from "../../session-page-state.js";
@@ -23,10 +23,10 @@ import { isRecord } from "../../parsing.js";
23
23
  import { buildReadConfirmationNextActions, nextReadConfirmation } from "../../read-confirmation.js";
24
24
  import { pruneOwnedManagedSessionRestoreSnapshots } from "../../managed-session-restore.js";
25
25
  import { isManagedSessionRestoreKey } from "../../managed-session-storage.js";
26
- import { createFreshSessionName, extractUpstreamCommandTokens, resolveManagedSessionState } from "../../runtime.js";
26
+ import { createFreshSessionName, extractUpstreamCommandTokens, redactInvocationArgs, resolveManagedSessionState } from "../../runtime.js";
27
27
  import { getUpstreamEffectiveBatchSteps } from "../batch-stdin.js";
28
28
  import { closeManagedSession, inspectManagedSessionDaemon } from "./managed-session-daemon-policy.js";
29
- import { applyOpenResultTabCorrection, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, collectSessionTabTarget, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
29
+ import { applyOpenResultTabCorrection, applyBrowserRunStatePatch, buildAboutBlankRecoveryHint, buildAboutBlankWarning, buildElectronPostCommandHealthDiagnostic, buildElectronRefFreshnessDiagnostic, buildElectronSessionMismatch, buildManagedSessionOutcome, collectOpenResultTabCorrection, collectSessionTabSelection, collectSessionTabTarget, commandChoosesSessionTabTarget, extractNavigationSummaryFromData, extractStringResultField, findElectronLaunchRecordForSession, formatElectronPostCommandHealthText, formatElectronSessionMismatchText, getSessionContextKey, getStaleRefArgs, mergeNavigationSummaryIntoData, runSessionCommandData, shouldCaptureNavigationSummary, shouldCorrectSessionTabAfterCommand, shouldInspectElectronPostCommandHealth, updateTraceOwnerState, } from "./session-state.js";
30
30
  import { collectClickDispatchDiagnostic } from "./click-dispatch.js";
31
31
  import { buildScrollNoopDiagnostic, collectComboboxFocusDiagnostic, collectElectronBroadGetTextScopeDiagnostics, collectElectronHandoff, collectFillVerificationDiagnostic, collectNavigationSummary, collectOverlayBlockerDiagnostic, collectQaAttachedTarget, collectSnapshotOverlayBlockerDiagnostic, collectRecordingDependencyWarning, collectScrollPositionSnapshot, collectSelectorTextVisibilityDiagnostics, collectTimeoutPartialProgress, sleepMs, formatQaAttachedTargetText, getArtifactCleanupGuidance, getEvalResultWarning, getEvalStdinHint, getSourceLookupElectronContext, } from "./diagnostics.js";
32
32
  import { repairScreenshotData } from "./prepare.js";
@@ -617,7 +617,7 @@ export async function processBrowserOutput(input) {
617
617
  networkRoutesBySession.delete(replacedSessionStateKey ?? replacedManagedSessionName);
618
618
  sessionPageState.clearSession(replacedSessionStateKey ?? replacedManagedSessionName);
619
619
  const replacedSessionKey = replacedSessionStateKey ?? replacedManagedSessionName;
620
- const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, timeoutMs: implicitSessionCloseTimeoutMs });
620
+ const replacedCloseError = await closeManagedSession({ cwd: priorManagedSessionCwd, headedManagedAutosaveInterval: priorManagedSessionHeadedAutosaveInterval, namespace: priorManagedSessionNamespace, preserveAttachedBrowserSession: state.attachedSessionKeys.has(replacedSessionKey), restoreState: state.managedSessionRestoreState, sessionName: replacedManagedSessionName, socketDir: state.ownedManagedSessions.get(replacedSessionKey)?.socketDir, timeoutMs: implicitSessionCloseTimeoutMs });
621
621
  if (managedSessionOutcome) {
622
622
  managedSessionOutcome = {
623
623
  ...managedSessionOutcome,
@@ -709,6 +709,30 @@ export async function processBrowserOutput(input) {
709
709
  }
710
710
  if (plainTextUpgrade && errorText)
711
711
  presentationEnvelope = { ...presentationEnvelope, success: false, error: errorText };
712
+ const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
713
+ && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
714
+ const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
715
+ && resultRetainsPreparedManagedSession
716
+ && !(commandClosesSession && succeeded);
717
+ const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !(commandClosesSession && succeeded)
718
+ ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
719
+ : undefined;
720
+ // Commit native lifecycle facts to the execution state before rendering/export can fail.
721
+ applyBrowserRunStatePatch(state, { artifactManifest, freshSessionOrdinal, managedSessionActive, managedSessionCompatibilityWorkaround, managedSessionHeadedAutosaveDisabled, managedSessionHeadedAutosaveInterval, managedSessionCwd, managedSessionName, managedSessionNamespace, networkRoutesBySession });
722
+ state.observedBrowserEffects = {
723
+ args: prepared.redactedArgs, command: prepared.executionPlan.commandInfo.command, subcommand: prepared.executionPlan.commandInfo.subcommand,
724
+ sessionName: prepared.executionPlan.sessionName, namespace: prepared.executionPlan.namespace, sessionMode: prepared.sessionMode,
725
+ usedImplicitSession: prepared.executionPlan.usedImplicitSession, agentBrowserStarted: processResult.agentBrowserStarted,
726
+ nativeSucceeded: succeeded, managedSessionOutcome, managedSessionCwd: prepared.ownedManagedSessionContext?.cwd ?? managedSessionCwd, compatibilityWorkaround: prepared.compatibilityWorkaround,
727
+ managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval,
728
+ managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace),
729
+ closeAllApplied, readConfirmation: readConfirmationEvent ?? prepared.readConfirmation,
730
+ electron: electronLaunchRecord ? { launch: electronLaunchRecord, cleanup: electronFailedConnectCleanup } : undefined,
731
+ batchSteps: Array.isArray(presentationEnvelope?.data) ? presentationEnvelope.data.filter(isRecord).map(row => ({
732
+ command: Array.isArray(row.command) && row.command.every(token => typeof token === "string") ? redactInvocationArgs(row.command) : undefined,
733
+ success: row.success, lifecycle: extractAgentBrowserLifecycle(row.result),
734
+ })) : undefined,
735
+ };
712
736
  let presentation = plainTextInspection ? { artifacts: undefined, batchFailure: undefined, batchSteps: undefined, content: [{ type: "text", text: inspectionText ?? "" }], data: undefined, fullOutputPath: undefined, fullOutputPaths: undefined, imagePath: undefined, imagePaths: undefined, savedFile: undefined, savedFilePath: undefined, summary: `${prepared.redactedArgs.join(" ")} completed` } : recordingStopRecovery && !recordingStopRecovery.batch ? recordingStopRecovery.presentation : await buildToolPresentation({ modelVisible: input.modelVisible, args: prepared.redactedProcessArgs, artifactManifest, artifactMaxUpdatedAtMs: Date.now(), artifactMinUpdatedAtMs: input.artifactRunStartedAtMs, artifactRequest: screenshotArtifactRequest, batchArtifactRequests: batchScreenshotArtifactRequests, commandInfo: prepared.executionPlan.commandInfo, compiledSemanticAction: prepared.compiledSemanticAction, cwd: operationCwd, envelope: presentationEnvelope, errorText, namespace: prepared.executionPlan.namespace, networkRouteDiagnostics, networkRoutes: activeNetworkRoutes, persistentArtifactStore, previousRecordingContactSheetPath: sessionStateKey ? state.activeRecordingReservations?.get(sessionStateKey)?.contactSheetPath : undefined, piCleanupOwnership: sessionStateKey && (state.ownedManagedSessions.has(sessionStateKey) || prepared.executionPlan.managedSessionName !== undefined) ? "wrapper-managed" : "caller-owned", sessionName: prepared.executionPlan.sessionName });
713
737
  if (recordingStopRecovery)
714
738
  presentation = mergeRecordingRecoveryPresentation(presentation, recordingStopRecovery);
@@ -794,13 +818,14 @@ export async function processBrowserOutput(input) {
794
818
  presentation.failureCategory = "qa-failure";
795
819
  presentation.summary = qaPreset.summary;
796
820
  const compactText = buildQaCompactFailureText({
797
- batchStepCount: presentation.batchSteps?.length ?? prepared.compiledQaPreset.steps.length,
798
- checks: prepared.compiledQaPreset.checks,
821
+ causalError: presentation.batchFailure?.failedStep.text,
822
+ executedStepCount: presentation.batchSteps?.length,
799
823
  page: extractQaPageContext({
800
824
  attachedTarget: qaAttachedTarget,
801
825
  batchData: presentationEnvelope?.data,
802
826
  compiled: prepared.compiledQaPreset,
803
827
  }),
828
+ plannedStepCount: prepared.compiledQaPreset.steps.length,
804
829
  qaPreset,
805
830
  });
806
831
  const nonTextContent = presentation.content.filter((item) => item.type !== "text");
@@ -829,7 +854,12 @@ export async function processBrowserOutput(input) {
829
854
  const qaAttachedBannerText = [qaAttachedTargetText, qaAttachedDiagnosticsText].filter((part) => typeof part === "string" && part.length > 0).join("\n");
830
855
  const skipAttachedTargetBanner = qaPreset?.passed && prepared.compiledQaPreset?.checks.attached;
831
856
  if (!skipAttachedTargetBanner && qaAttachedBannerText && presentation.content[0]?.type === "text")
832
- presentation.content[0] = { ...presentation.content[0], text: `${qaAttachedBannerText}\n\n${presentation.content[0].text}` };
857
+ presentation.content[0] = {
858
+ ...presentation.content[0],
859
+ text: qaPreset?.passed === false
860
+ ? `${presentation.content[0].text}\n\n${qaAttachedBannerText}`
861
+ : `${qaAttachedBannerText}\n\n${presentation.content[0].text}`,
862
+ };
833
863
  else if (!skipAttachedTargetBanner && qaAttachedBannerText)
834
864
  presentation.content.unshift({ type: "text", text: qaAttachedBannerText });
835
865
  if (managedSessionOutcome && managedSessionOutcome.succeeded !== succeeded)
@@ -856,19 +886,24 @@ export async function processBrowserOutput(input) {
856
886
  currentRefSnapshot = finalRecoveryState.currentRefSnapshot;
857
887
  currentRefSnapshotInvalidation = finalRecoveryState.currentRefSnapshotInvalidation;
858
888
  const authoritativePageState = sessionStateKey ? sessionPageState.get(sessionStateKey) : undefined;
889
+ const newSnapshot = authoritativePageState?.refSnapshot && authoritativePageState.refSnapshot.snapshotId !== prepared.priorRefSnapshotState?.snapshotId;
890
+ const needsDaemonReceipt = prepared.ownedManagedSessionContext && !prepared.ownedManagedSessionContext.reuseOnly && !commandClosesSession && processResult.agentBrowserStarted
891
+ && (succeeded || managedSessionOutcome?.activeAfter === true && managedSessionOutcome.currentSessionName === prepared.executionPlan.sessionName)
892
+ && !state.managedSessionRestoreState.getDaemonReceipt(prepared.executionPlan.sessionName, prepared.executionPlan.namespace);
893
+ if (sessionStateKey && prepared.executionPlan.sessionName && (newSnapshot || needsDaemonReceipt)) {
894
+ const daemon = await inspectManagedSessionDaemon({ cwd, namespace: prepared.executionPlan.namespace, sessionName: prepared.executionPlan.sessionName, signal, includeGeneration: true, headedManagedAutosaveInterval: prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval ?? (prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled ? "0" : undefined), timeoutMs: prepared.processTimeoutMs });
895
+ if (newSnapshot) {
896
+ sessionPageState.bindSnapshotGeneration(sessionStateKey, daemon.status === "active" ? daemon.generation : undefined);
897
+ currentRefSnapshot = sessionPageState.get(sessionStateKey).refSnapshot;
898
+ }
899
+ if (prepared.ownedManagedSessionContext && daemon.status === "active" && daemon.generation) {
900
+ state.managedSessionRestoreState.recordDaemonRestoreKey(prepared.executionPlan.sessionName, prepared.executionPlan.namespace, daemon.restoreKey, daemon.generation);
901
+ }
902
+ }
859
903
  if (sessionStateKey)
860
904
  currentSessionTabTarget = authoritativePageState?.tabTarget;
861
905
  const currentSessionTabTargetUnknown = authoritativePageState?.tabTargetUnknown === true ? true : undefined;
862
- const resultRetainsPreparedManagedSession = !managedSessionOutcome || (managedSessionOutcome.activeAfter
863
- && managedSessionOutcome.attemptedSessionName === managedSessionOutcome.currentSessionName);
864
- const resultHeadedManagedAutosaveDisabled = prepared.ownedManagedSessionContext?.headedManagedAutosaveDisabled === true
865
- && !prepared.ownedManagedSessionContext.reuseOnly
866
- && resultRetainsPreparedManagedSession
867
- && !(commandClosesSession && succeeded);
868
- const resultHeadedManagedAutosaveInterval = resultRetainsPreparedManagedSession && !prepared.ownedManagedSessionContext?.reuseOnly && !(commandClosesSession && succeeded)
869
- ? prepared.ownedManagedSessionContext?.headedManagedAutosaveInterval
870
- : undefined;
871
- const result = buildFinalAgentBrowserToolResult({ modelVisible: input.modelVisible, aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
906
+ const result = buildFinalAgentBrowserToolResult({ modelVisible: input.modelVisible, aboutBlankSessionMismatch, artifactCleanup, categoryDetails: finalRecoveryState.categoryDetails, clickDispatchDiagnostic, commandTokens: prepared.commandTokens, comboboxFocusDiagnostic, compiledNetworkSourceLookup: prepared.compiledNetworkSourceLookup, compiledSemanticAction: prepared.compiledSemanticAction, compatibilityWorkaround: prepared.compatibilityWorkaround, currentRefSnapshot, currentRefSnapshotInvalidation, currentSessionTabTarget, currentSessionTabTargetUnknown, electronBroadGetTextScopeDiagnostics, electronFailedConnectCleanup, electronHandoff, electronLaunch: prepared.electronLaunch, electronLaunchRecord, electronLaunchRecords, electronPostCommandHealth, electronProfileIsolationDetails: input.electronProfileIsolationDetails, electronRefFreshnessDiagnostic, electronSessionMismatch, errorText, evalResultWarning, evalStdinHint, exactSensitiveValues: prepared.exactSensitiveValues, executionPlan: prepared.executionPlan, fillVerificationDiagnostic, headedLaunch: prepared.headedLaunch, inspectionText, preserveAttachedBrowserSession: input.preserveAttachedBrowserSession === true, providerLaunch: prepared.providerLaunch, managedSessionHeadedAutosaveDisabled: !prepared.ownedManagedSessionContext?.reuseOnly && resultHeadedManagedAutosaveDisabled || undefined, managedSessionHeadedAutosaveInterval: prepared.ownedManagedSessionContext?.reuseOnly ? undefined : resultHeadedManagedAutosaveInterval, managedSessionOutcome, managedSessionRestoreDisabled: state.managedSessionRestoreState.isDisabled(prepared.executionPlan.sessionName, prepared.executionPlan.namespace), navigationSummary, networkSourceLookup, noActivePageSnapshotFailure: finalRecoveryState.noActivePageSnapshotFailure, openResultTabCorrection, overlayBlockerDiagnostic, parseError, parseFailureOutput, parseSucceeded, plainTextInspection, presentation, presentationEnvelope, priorSessionTabTarget: prepared.priorSessionTabTarget, processResult, qaAttachedTarget, qaPreset, recordingDependencyWarning, redactedArgs: prepared.redactedArgs, redactedCompiledElectron: prepared.redactedCompiledElectron, redactedCompiledJob: prepared.redactedCompiledJob, redactedCompiledNetworkSourceLookup: prepared.redactedCompiledNetworkSourceLookup, redactedCompiledQaPreset: prepared.redactedCompiledQaPreset, redactedCompiledSemanticAction: prepared.redactedCompiledSemanticAction, redactedCompiledSourceLookup: prepared.redactedCompiledSourceLookup, redactedContent, redactedProcessArgs: prepared.redactedProcessArgs, redactedRecoveryHint: prepared.redactedRecoveryHint, resultArtifactManifest, richInputRecoveryDiagnostic: finalRecoveryState.richInputRecoveryDiagnostic, scrollNoopDiagnostic, selectorTextVisibilityDiagnostics, sessionMode: prepared.sessionMode, sessionTabCorrection, sourceLookup, succeeded, timeoutPartialProgress, unsettledWebMcpMutation, userRequestedJson: prepared.userRequestedJson, visibleRefFallbackDiagnostic: finalRecoveryState.visibleRefFallbackDiagnostic, visibleRefFallbackSessionName: finalRecoveryState.visibleRefFallbackSessionName });
872
907
  const resultWithCloseAll = closeAllApplied
873
908
  ? { ...result, details: { ...(isRecord(result.details) ? result.details : {}), closeAllApplied: true } }
874
909
  : result;
@@ -603,7 +603,7 @@ export async function cleanupTrackedElectronHostLaunches(options) {
603
603
  const sessionKey = getSessionPageStateKey(record.sessionName, record.namespace) ?? record.sessionName;
604
604
  const managedSessionOwner = sessionKey ? options.ownedManagedSessions.get(sessionKey) : undefined;
605
605
  const managedSessionCloseError = record.sessionName
606
- ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, timeoutMs: options.timeoutMs })
606
+ ? await closeManagedSession({ cwd: options.cwd, headedManagedAutosaveInterval: managedSessionOwner?.headedManagedAutosaveInterval, namespace: record.namespace, preserveAttachedBrowserSession: options.attachedSessionKeys.has(sessionKey ?? record.sessionName), restoreState: options.managedSessionRestoreState, sessionName: record.sessionName, socketDir: managedSessionOwner?.socketDir, timeoutMs: options.timeoutMs })
607
607
  : undefined;
608
608
  const managedSessionStep = record.sessionName
609
609
  ? managedSessionCloseError
@@ -1,18 +1,22 @@
1
1
  import { stat } from "node:fs/promises";
2
+ import { randomUUID } from "node:crypto";
2
3
  import { AGENT_BROWSER_SCRIPT_FINAL_OUTPUT_MAX_BYTES, createAgentBrowserScriptCloseArgs, isAgentBrowserScriptSessionName, } from "../input-modes/script.js";
3
4
  import { isRecord } from "../parsing.js";
5
+ import { appendBrowserTransition, getBrowserRecord } from "../browser-transcript.js";
4
6
  import { redactSensitiveText } from "../runtime.js";
5
7
  import { attachInlineImage } from "../results/presentation/artifacts.js";
6
8
  import { projectAgentBrowserObservation } from "../results/presentation/content.js";
7
9
  import { redactPresentationData } from "../results/presentation/diagnostics.js";
8
- const SCRIPT_SESSION_ENTRY_TYPE = "agent-browser-script-session";
9
- // Retire outstanding pre-0.7 isolated-session leases when an old transcript resumes.
10
- export function getScriptSessionLeasesFromBranch(branch) {
10
+ // The offline converter retains outstanding isolated-session cleanup facts in canonical events.
11
+ export function getScriptSessionLeasesFromBranch(branch, ownerSessionId) {
11
12
  const leases = new Map();
12
13
  for (const entry of branch) {
13
- if (!isRecord(entry) || entry.type !== "custom" || entry.customType !== SCRIPT_SESSION_ENTRY_TYPE || !isRecord(entry.data))
14
+ const lease = getBrowserRecord(entry)?.event.state.scriptLease;
15
+ if (!isRecord(lease))
14
16
  continue;
15
- const { cleanup, closeCommandArgs, launchAttempted, sessionName } = entry.data;
17
+ if (ownerSessionId !== undefined && lease.ownerSessionId !== ownerSessionId)
18
+ continue;
19
+ const { cleanup, closeCommandArgs, launchAttempted, sessionName } = lease;
16
20
  if (!isAgentBrowserScriptSessionName(sessionName))
17
21
  continue;
18
22
  const expected = createAgentBrowserScriptCloseArgs(sessionName);
@@ -20,12 +24,13 @@ export function getScriptSessionLeasesFromBranch(branch) {
20
24
  || !Array.isArray(closeCommandArgs) || closeCommandArgs.length !== expected.length
21
25
  || !closeCommandArgs.every((token, index) => token === expected[index]))
22
26
  continue;
23
- leases.set(sessionName, { cleanup, closeCommandArgs: expected, launchAttempted: true, sessionName });
27
+ leases.set(sessionName, { cleanup, closeCommandArgs: expected, launchAttempted: true, sessionName, ownerSessionId: typeof lease.ownerSessionId === "string" ? lease.ownerSessionId : undefined });
24
28
  }
25
29
  return leases;
26
30
  }
27
- export function appendScriptSessionLease(pi, sessionName, cleanup) {
28
- pi.appendEntry(SCRIPT_SESSION_ENTRY_TYPE, { cleanup, closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName), launchAttempted: true, sessionName });
31
+ export function appendScriptSessionLease(pi, sessionName, cleanup, ownerSessionId) {
32
+ appendBrowserTransition(pi, { event: { version: 1, phase: "state", operationId: randomUUID(), toolCallId: "cleanup", commandIndex: 0, isError: cleanup !== "closed",
33
+ state: { scriptLease: { cleanup, closeCommandArgs: createAgentBrowserScriptCloseArgs(sessionName), launchAttempted: true, sessionName, ownerSessionId } } } });
29
34
  }
30
35
  export function createBrowserCodeOutput() {
31
36
  const images = new Map();
@@ -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(event) {
189
- const details = isRecord(event.details) ? event.details : undefined;
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(event.input) && Array.isArray(event.input.args) ? event.input.args : undefined;
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 buildAgentBrowserToolResultPatch(event) {
223
- if (!BROWSER_RESULT_TOOLS.has(event.toolName))
224
- return undefined;
225
- const preservesParseableJson = (event.toolName === "agent_browser_code" || agentBrowserToolResultRequestedJson(event)) && agentBrowserToolResultHasParseableJsonContent(event.content);
226
- const notice = preservesParseableJson ? undefined : formatModelVisibleFailureCategoryNotice(event.details);
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
- ...(content ? { content } : {}),
233
- ...(shouldMarkError ? { isError: true } : {}),
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.text.setText(value);
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
  }
@@ -12,7 +12,7 @@ export const QUICK_START_GUIDELINES = [
12
12
  "Use { args: [\"batch\", \"--bail\"], stdin: \"[[\\\"fill\\\",\\\"@e1\\\",\\\"text\\\"],[\\\"click\\\",\\\"@e2\\\"]]\" } for same-snapshot form fields followed by submit. Split if a fill rerenders later fields. Add native condition waits after navigation-prone actions; a click alone does not prove the destination loaded. Avoid blind mutation retries after timeout.",
13
13
  "agent_browser_code takes { code, session?, namespace?, timeoutMs?, outputPath? }. JavaScript globals are fresh for every call; the selected browser persists and is shared with agent_browser. Use await browser({ args, stdin?, timeoutMs? }), inspect result.success, and emit selected JSON instead of whole envelopes. Use emitImage(result.imageObservations[0]) only when image inspection is needed; image handles belong to the current code call. No imports or host filesystem/network/process APIs are exposed.",
14
14
  "Example agent_browser_code: { code: 'const result = await browser({args:[\"get\",\"title\"]}); if (!result.success) { emit(result); } else { emit({title:result.data}); }' }. Native batch still avoids extra CLI processes for fixed segments inside code. Explicit session/namespace controls select the browser for this cell without changing the default browser.",
15
- "Call agent_browser_tools with no fields for inventory, or { enable: [\"action\", \"qa\", \"electron\", \"source\", \"network\"] } for advanced native tools. It adds agent_browser_action, agent_browser_qa, agent_browser_electron, agent_browser_source, and agent_browser_network_source without disabling other tools. Their specialized instructions appear when activated.",
15
+ "Call agent_browser_tools with no fields for inventory, or { enable: [\"action\", \"qa\", \"electron\", \"source\", \"network\"] } for advanced native tools. It adds agent_browser_action, agent_browser_qa, agent_browser_electron, agent_browser_source, and agent_browser_network_source without disabling other tools. The browser instructions cover these capabilities; activation reveals their selected tool declarations.",
16
16
  "Use read <url> for readable web text; get title/url, get text/html/value/count <selector>, or get attr <selector> <name> for targeted state. For eval --stdin, put JavaScript in stdin and return the desired value. outputPath saves result data and must differ from screenshot/download/recording paths. Use --json only when JSON text is needed.",
17
17
  "For artifacts, verify artifactVerification and artifacts before reporting success. A pending recording is not verified output; install ffmpeg before starting and pair start with stop. Native download owns the click and saves Blob/redirect downloads. Close keeps explicitly saved files. Save promptGuard-required paths before close. Screenshot pixels are not browser CSS coordinates: use imageObservations geometry (dimensions, viewport, crop/origin, scroll and DPR) before any visual-coordinate action; full-page and element captures are not current-viewport coordinates, and Pi resizing only maps sent pixels to original image pixels.",
18
18
  "Follow model-visible nextActions when present, including identity and stdin, instead of guessing commands. Treat native pending confirmations and unknown mutation outcomes as inspection boundaries. For dense snapshots, inspect Omitted high-value controls before reading a full spill.",
@@ -38,6 +38,13 @@ export const ADVANCED_TOOL_PROMPT_GUIDELINES = {
38
38
  ],
39
39
  };
40
40
  export const WEB_SEARCH_PROMPT_GUIDELINE = "Prefer agent_browser_web_search for current or external web facts and URL discovery over public search-engine forms that can hit anti-bot/CAPTCHA-gated pages. For research before implementation, pass searchType: deep-lite unless webSearch.defaultSearchType already does; omit it for everyday lookups so config/auto wins. Provider rank is not proof of authority: when correctness or version matters, prefer the vendor or project's primary current docs, inspect page-date and version clues, and constrain one follow-up after discovering the official domain (Exa includeDomains; Brave site: in query). Do not count URL aliases as independent sources. Use agent_browser after you have a target URL that needs interaction, screenshots, or DOM inspection.";
41
+ export const WEB_SEARCH_TOOL_PROMPT_GUIDELINES = [
42
+ WEB_SEARCH_PROMPT_GUIDELINE,
43
+ "agent_browser_web_search chooses Exa or Brave from configured keys; when both are available, Exa is preferred by default unless webSearch.preferredProvider says otherwise. Use provider only when the user/config calls for a specific provider.",
44
+ "Use Exa deep only when deep-lite may miss angles, and deep-reasoning only for exhaustive or still-thin research. Do not run parallel agent_browser_web_search calls; make one high-signal query, inspect its results, then at most one follow-up.",
45
+ "If agent_browser_web_search returns HTTP 429, stop searching and tell the user the API plan/rate limit needs time or a plan change.",
46
+ "After using agent_browser_web_search, cite result URLs in the final answer when web evidence informed the answer.",
47
+ ];
41
48
  export const SHARED_BROWSER_PLAYBOOK_GUIDELINES = [
42
49
  QUICK_START_GUIDELINES[0],
43
50
  QUICK_START_GUIDELINES[3],
@@ -96,7 +103,7 @@ export const WRAPPER_TAB_RECOVERY_BEHAVIOR = [
96
103
  "If a known session target unexpectedly reports about:blank, agent_browser best-effort re-selects the prior intended target when it still exists; if recovery fails, it records the observed about:blank target and reports exact recovery guidance instead of treating the prior page as active.",
97
104
  "If upstream reports tab_gone, the pinned bound tab is gone; use details.nextActions (tab list / tab new) instead of assuming another tab is yours.",
98
105
  ];
99
- /** Tier A: always-on tool promptGuidelines (keep small; Tier B lives in SHARED_BROWSER_PLAYBOOK_GUIDELINES and docs). */
106
+ /** Concise orientation included alongside the full operating playbook. */
100
107
  export const RUNTIME_PROMPT_GUIDELINES = [
101
108
  "Use agent_browser for one native command; batch --bail with JSON-array stdin for fixed sequences; agent_browser_code for loops/branches/aggregation. Return to the model at judgment boundaries. Use agent_browser_tools to discover/enable action, qa, electron, source, or network tools.",
102
109
  "agent_browser_code runs fresh JavaScript against the persistent browser: await browser({args, stdin?, timeoutMs?}), check result.success, emit(selected JSON), or emitImage(result.imageObservations[0]). No host APIs/imports; globals do not persist. Explicit session/namespace selects this cell's browser without changing the default.",
@@ -1,10 +1,3 @@
1
- const BROWSER_PROMPT_PATTERNS = [
2
- /\b(?:agent[_ -]?browser|browser automation|eval\s+--stdin|screenshot|snapshot|tab\s+list)\b/i,
3
- /\b(?:react\s+(?:tree|inspect|renders|suspense)|web\s+vitals|core\s+web\s+vitals|pushstate)\b/i,
4
- /\b(?:live\s+docs?|online\s+research|research\s+(?:online|the\s+web)|search\s+(?:online|the\s+web)|web\s+research)\b/i,
5
- /\bbrowser\b.*\b(?:automation|click|fill|navigate|open|page|screenshot|site|snapshot|tab|url|visit|web(?:site| page)?)\b/i,
6
- /\b(?:browse|click|fill|login|navigate|open|visit)\b.*\b(?:https?:\/\/\S+|page|site|tab|url|web(?:site| page)?)\b/i,
7
- ];
8
1
  const LEGACY_BASH_ALLOW_PATTERNS = [
9
2
  /\b(?:bash-oriented workflow|bash workflow)\b/i,
10
3
  /\b(?:use|via|through|with)\s+bash\b/i,
@@ -206,7 +199,7 @@ export function buildPromptPolicy(prompt) {
206
199
  requestedArtifacts: extractPromptRequestedArtifacts(prompt),
207
200
  };
208
201
  }
209
- function getMessageText(content) {
202
+ export function getMessageText(content) {
210
203
  if (typeof content === "string")
211
204
  return content;
212
205
  if (!Array.isArray(content))
@@ -220,24 +213,21 @@ function getMessageText(content) {
220
213
  .filter((text) => text.length > 0)
221
214
  .join("\n");
222
215
  }
223
- export function shouldAppendBrowserSystemPrompt(prompt) {
224
- const normalizedPrompt = prompt.trim();
225
- if (normalizedPrompt.length === 0) {
226
- return false;
227
- }
228
- return BROWSER_PROMPT_PATTERNS.some((pattern) => pattern.test(normalizedPrompt));
229
- }
230
- export function getLatestUserPrompt(branch) {
231
- for (let index = branch.length - 1; index >= 0; index -= 1) {
232
- const entry = branch[index];
233
- if (typeof entry !== "object" || entry === null || !("type" in entry) || entry.type !== "message") {
234
- continue;
235
- }
236
- const message = "message" in entry ? entry.message : undefined;
237
- if (typeof message !== "object" || message === null || !("role" in message) || message.role !== "user") {
238
- continue;
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.");
239
227
  }
240
- return getMessageText("content" in message ? message.content : undefined);
228
+ if (entry.type === "message" && entry.message.role === "user")
229
+ return entry.message;
230
+ id = entry.parentId;
241
231
  }
242
- return "";
232
+ return undefined;
243
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,20 +88,22 @@ function buildSpillArtifactEntries(options) {
88
88
  ...buildEvictedSessionArtifactEntries(options.evictedArtifacts, nowMs),
89
89
  ];
90
90
  }
91
- export const OBSERVATION_INLINE_MAX_CHARS = 16_000;
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 {
104
- spill = await writeLargeOutputSpillFile({ data: { ...observation, ...(!options.json ? { text: prose } : {}) }, persistentArtifactStore: options.persistentArtifactStore, text });
105
+ const write = () => writeLargeOutputSpillFile({ data: { ...observation, ...(!options.json ? { text: prose } : {}) }, persistentArtifactStore: options.persistentArtifactStore, text });
106
+ spill = await (options.withArtifactWrite ? options.withArtifactWrite(write) : write());
105
107
  artifactManifest = applyArtifactManifest({ content: [], summary: "" }, artifactManifest, buildSpillArtifactEntries({
106
108
  commandInfo: { command: typeof options.details.command === "string" ? options.details.command : undefined },
107
109
  evictedArtifacts: spill.evictedArtifacts, path: spill.path, storageScope: spill.storageScope,
@@ -113,7 +115,8 @@ export async function renderAgentBrowserObservation(options) {
113
115
  const compact = {
114
116
  success: options.succeeded, resultCategory: observation.resultCategory,
115
117
  failureCategory: observation.failureCategory, successCategory: observation.successCategory,
116
- summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700) : undefined,
118
+ summary: typeof observation.summary === "string" ? truncateText(observation.summary, 700)
119
+ : typeof observation.error === "string" ? truncateText(observation.error, 700) : undefined,
117
120
  compacted: true,
118
121
  ...(spill ? { observationPath: spill.path, retrieve: "Read observationPath for the complete redacted observation, including exact recovery actions and requested data." }
119
122
  : { observationUnavailable: truncateText(redactModelFacingText(spillError ?? "Spill could not be written; request a smaller result."), 1_000) }),
@@ -122,11 +125,16 @@ export async function renderAgentBrowserObservation(options) {
122
125
  if (observation[key] !== undefined && JSON.stringify({ ...compact, [key]: observation[key] }, null, 2).length <= OBSERVATION_INLINE_MAX_CHARS - 500)
123
126
  compact[key] = observation[key];
124
127
  }
125
- text = JSON.stringify(compact, null, 2);
126
- if (!options.json)
127
- text = `Browser observation compacted.\n${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
+ }
128
136
  }
129
- return { content: [{ type: "text", text }, ...images], artifactManifest };
137
+ return { content: [{ type: "text", text }, ...images], artifactManifest, structuredContent: JSON.parse(JSON.stringify(structuredContent)) };
130
138
  }
131
139
  export async function compactLargePresentationOutput(options) {
132
140
  const text = getPresentationText(options.presentation);