@opengeni/runtime 3.2.2 → 3.4.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 (69) hide show
  1. package/dist/agent-LJT5NPWT.js +9 -0
  2. package/dist/assets/codemode-client.json +1 -1
  3. package/dist/browser-current-tab.d.ts +46 -0
  4. package/dist/browser-model-text.d.ts +126 -0
  5. package/dist/{chunk-2AUDDDO2.js → chunk-3DFH3ZHU.js} +784 -121
  6. package/dist/chunk-3DFH3ZHU.js.map +1 -0
  7. package/dist/{chunk-HJSIPTFA.js → chunk-4XDJ4TJA.js} +2 -2
  8. package/dist/{chunk-XQ23VCZ4.js → chunk-6FQ6KEII.js} +3 -3
  9. package/dist/{chunk-7LJ7KDPI.js → chunk-ERD4ICVB.js} +4 -3
  10. package/dist/{chunk-462CH77G.js → chunk-ETTCGPBT.js} +400 -9
  11. package/dist/chunk-ETTCGPBT.js.map +1 -0
  12. package/dist/{chunk-7OUG7WBV.js → chunk-GWRBALGQ.js} +181 -13
  13. package/dist/chunk-GWRBALGQ.js.map +1 -0
  14. package/dist/{chunk-YG26XE7B.js → chunk-JYDG2A7H.js} +2 -2
  15. package/dist/{chunk-RSHQSLEJ.js → chunk-KIL4FGX5.js} +3 -3
  16. package/dist/{chunk-NWS4HPB7.js → chunk-MIAL5I4O.js} +2 -2
  17. package/dist/{chunk-Z5U6WE22.js → chunk-PRZPGRIK.js} +2 -2
  18. package/dist/{chunk-KCPW4K7L.js → chunk-PZQUUBEB.js} +2 -2
  19. package/dist/{chunk-5WJOPANS.js → chunk-Y5THZH5N.js} +7 -7
  20. package/dist/codex-tool-search.d.ts +9 -0
  21. package/dist/gmail-rest-mcp.js +8 -8
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.js +14 -12
  24. package/dist/interaction-tool-json-schema.d.ts +27 -0
  25. package/dist/{lazy-manifest-AJ6XPJ4F.js → lazy-manifest-BLZ3UZLX.js} +4 -4
  26. package/dist/lazy-tool-transport.d.ts +10 -0
  27. package/dist/{localSources-SSIEH5F5.js → localSources-I6SIQ6CX.js} +6 -6
  28. package/dist/model-call.js +3 -3
  29. package/dist/sandbox/browser-control-client.d.ts +87 -0
  30. package/dist/sandbox/browser-control-http.d.ts +33 -0
  31. package/dist/sandbox/index.d.ts +1 -1
  32. package/dist/sandbox/index.js +7 -7
  33. package/dist/sandbox/routing/backend-resolver.d.ts +1 -0
  34. package/dist/sandbox/selfhosted/session.d.ts +22 -0
  35. package/dist/slack-rest-mcp.js +8 -8
  36. package/dist/workspace-tool-gateway.js +12 -12
  37. package/package.json +14 -14
  38. package/src/browser-current-tab.ts +139 -0
  39. package/src/browser-model-text.ts +378 -0
  40. package/src/bundled_site_skills/opengeni-sites/SKILL.md +46 -12
  41. package/src/codex-tool-search.ts +53 -0
  42. package/src/index.ts +39 -1
  43. package/src/interaction-tool-json-schema.ts +281 -0
  44. package/src/interaction-tools.ts +425 -235
  45. package/src/lazy-tool-transport.ts +229 -10
  46. package/src/model-input.ts +12 -1
  47. package/src/sandbox/browser-control-client.ts +425 -0
  48. package/src/sandbox/browser-control-http.ts +32 -0
  49. package/src/sandbox/index.ts +4 -0
  50. package/src/sandbox/routing/backend-resolver.ts +2 -0
  51. package/src/sandbox/selfhosted/session.ts +154 -0
  52. package/dist/agent-CTPLA3HP.js +0 -9
  53. package/dist/browser-action-json-schema.d.ts +0 -4
  54. package/dist/chunk-2AUDDDO2.js.map +0 -1
  55. package/dist/chunk-462CH77G.js.map +0 -1
  56. package/dist/chunk-7OUG7WBV.js.map +0 -1
  57. package/src/browser-action-json-schema.ts +0 -10
  58. /package/dist/{agent-CTPLA3HP.js.map → agent-LJT5NPWT.js.map} +0 -0
  59. /package/dist/{chunk-HJSIPTFA.js.map → chunk-4XDJ4TJA.js.map} +0 -0
  60. /package/dist/{chunk-XQ23VCZ4.js.map → chunk-6FQ6KEII.js.map} +0 -0
  61. /package/dist/{chunk-7LJ7KDPI.js.map → chunk-ERD4ICVB.js.map} +0 -0
  62. /package/dist/{chunk-YG26XE7B.js.map → chunk-JYDG2A7H.js.map} +0 -0
  63. /package/dist/{chunk-RSHQSLEJ.js.map → chunk-KIL4FGX5.js.map} +0 -0
  64. /package/dist/{chunk-NWS4HPB7.js.map → chunk-MIAL5I4O.js.map} +0 -0
  65. /package/dist/{chunk-Z5U6WE22.js.map → chunk-PRZPGRIK.js.map} +0 -0
  66. /package/dist/{chunk-KCPW4K7L.js.map → chunk-PZQUUBEB.js.map} +0 -0
  67. /package/dist/{chunk-5WJOPANS.js.map → chunk-Y5THZH5N.js.map} +0 -0
  68. /package/dist/{lazy-manifest-AJ6XPJ4F.js.map → lazy-manifest-BLZ3UZLX.js.map} +0 -0
  69. /package/dist/{localSources-SSIEH5F5.js.map → localSources-I6SIQ6CX.js.map} +0 -0
@@ -90,8 +90,26 @@ import {
90
90
  type InteractionTransport,
91
91
  } from "@opengeni/sdk";
92
92
  import { z } from "zod";
93
- import { browserActionInputJsonSchema } from "./browser-action-json-schema";
93
+ import {
94
+ BrowserTargetResolutionError,
95
+ CurrentBrowserTab,
96
+ expandRefShorthand,
97
+ type ResolvedBrowserSession,
98
+ type ResolvedBrowserTarget,
99
+ } from "./browser-current-tab";
100
+ import {
101
+ describeInvalidToolArguments,
102
+ interactionToolInputJsonSchema,
103
+ withRefShorthandLocators,
104
+ } from "./interaction-tool-json-schema";
94
105
  import { guardedMcpFetch } from "./mcp-network";
106
+ import {
107
+ browserActionText,
108
+ browserObservationText,
109
+ browserOpenText,
110
+ browserReadText,
111
+ browserTabsText,
112
+ } from "./browser-model-text";
95
113
 
96
114
  export const INTERACTION_ATTEMPT_TOOL_NAMES = FIRST_PARTY_IN_PROCESS_TOOL_NAMES;
97
115
 
@@ -189,10 +207,22 @@ const CompactBrowserActionReceipt = z
189
207
  })
190
208
  .strict();
191
209
  const BrowserAgentActionReceipt = z.union([BrowserActionReceipt, CompactBrowserActionReceipt]);
210
+ /** Browser tools default to the browser and tab the agent used last. */
211
+ const CurrentBrowserSessionId = z
212
+ .string()
213
+ .uuid()
214
+ .optional()
215
+ .describe("Omit to use the browser you used last.");
216
+ const CurrentBrowserTargetId = z
217
+ .string()
218
+ .min(1)
219
+ .max(512)
220
+ .optional()
221
+ .describe("Omit to use the tab you used last.");
192
222
  const BrowserScreenshotInput = z
193
223
  .object({
194
- browserSessionId: z.string().uuid(),
195
- targetId: z.string().min(1).max(512),
224
+ browserSessionId: CurrentBrowserSessionId,
225
+ targetId: CurrentBrowserTargetId,
196
226
  fullPage: z.boolean().optional(),
197
227
  quality: z.number().int().min(1).max(100).optional(),
198
228
  })
@@ -217,8 +247,8 @@ const BrowserScreenshotOutput = z
217
247
  .strict();
218
248
  const BrowserReadInput = z
219
249
  .object({
220
- browserSessionId: z.string().uuid(),
221
- targetId: z.string().min(1).max(512),
250
+ browserSessionId: CurrentBrowserSessionId,
251
+ targetId: CurrentBrowserTargetId,
222
252
  mode: z.enum(["matches", "count", "subtree", "dom"]).optional(),
223
253
  dom: z
224
254
  .discriminatedUnion("kind", [
@@ -478,25 +508,25 @@ const BrowserOpenOutput = z
478
508
  .strict();
479
509
 
480
510
  const BrowserTabsInput = z.discriminatedUnion("operation", [
481
- z.object({ operation: z.literal("list"), browserSessionId: z.string().uuid() }).strict(),
511
+ z.object({ operation: z.literal("list"), browserSessionId: CurrentBrowserSessionId }).strict(),
482
512
  z
483
513
  .object({
484
514
  operation: z.literal("open"),
485
- browserSessionId: z.string().uuid(),
515
+ browserSessionId: CurrentBrowserSessionId,
486
516
  url: z.string().url().max(16_384).optional(),
487
517
  })
488
518
  .strict(),
489
519
  z
490
520
  .object({
491
521
  operation: z.literal("select"),
492
- browserSessionId: z.string().uuid(),
522
+ browserSessionId: CurrentBrowserSessionId,
493
523
  targetId: z.string().min(1).max(512),
494
524
  })
495
525
  .strict(),
496
526
  z
497
527
  .object({
498
528
  operation: z.literal("close"),
499
- browserSessionId: z.string().uuid(),
529
+ browserSessionId: CurrentBrowserSessionId,
500
530
  targetId: z.string().min(1).max(512),
501
531
  })
502
532
  .strict(),
@@ -511,28 +541,28 @@ const BrowserTabsOutput = z
511
541
 
512
542
  const BrowserObserveInput = z
513
543
  .object({
514
- browserSessionId: z.string().uuid(),
515
- targetId: z.string().min(1).max(512),
544
+ browserSessionId: CurrentBrowserSessionId,
545
+ targetId: CurrentBrowserTargetId,
516
546
  view: z.enum(["compact", "full"]).optional(),
517
547
  includeScreenshot: z.boolean().optional(),
518
548
  })
519
549
  .strict();
520
550
  const BrowserActInput = z
521
551
  .object({
522
- browserSessionId: z.string().uuid(),
523
- targetId: z.string().min(1).max(512),
552
+ browserSessionId: CurrentBrowserSessionId,
553
+ targetId: CurrentBrowserTargetId,
524
554
  expectedTargetGeneration: z.string().min(1).max(256).optional(),
525
555
  expectedDocumentGeneration: z.string().min(1).max(256).nullable().optional(),
526
556
  expectedFrameId: z.string().min(1).max(256).nullable().optional(),
527
557
  view: z.enum(["compact", "full", "none"]).optional(),
528
- action: z.union([BrowserAction, BrowserActionBatch]),
558
+ action: z.preprocess(expandRefShorthand, z.union([BrowserAction, BrowserActionBatch])),
529
559
  })
530
560
  .strict();
531
561
  const BrowserClipboardInput = z.object({ browserSessionId: z.string().uuid() }).strict();
532
562
  const BrowserDebugInput = z
533
563
  .object({
534
- browserSessionId: z.string().uuid(),
535
- targetId: z.string().min(1).max(512),
564
+ browserSessionId: CurrentBrowserSessionId,
565
+ targetId: CurrentBrowserTargetId,
536
566
  kinds: z
537
567
  .array(z.enum(["console", "page_error", "failed_request", "download"]))
538
568
  .max(4)
@@ -759,16 +789,67 @@ export function createInteractionAttemptToolDefinitions(
759
789
  const selected = new Set(input.selectedTools ?? DEFAULT_FIRST_PARTY_MCP_TOOLS);
760
790
  const permissions = input.permissions ?? DEFAULT_FIRST_PARTY_MCP_PERMISSIONS;
761
791
  const definitions: AttemptToolDefinition[] = [];
792
+ const currentTab = new CurrentBrowserTab({
793
+ listLiveSessions: async () =>
794
+ filterDiscoveredSessions(
795
+ (
796
+ await input.transport.listBrowserSessions(input.workspaceId, {
797
+ associatedSessionId: input.sessionId,
798
+ includeTerminal: false,
799
+ })
800
+ ).sessions,
801
+ input.sessionId,
802
+ "current_session",
803
+ false,
804
+ ),
805
+ listTargets: async (browserSessionId) =>
806
+ (await input.transport.listBrowserTargets(input.workspaceId, browserSessionId)).targets,
807
+ });
808
+ /** Run one call on the requested or current tab and remember it on success. */
809
+ const onTab = async <T>(
810
+ value: { browserSessionId?: string | undefined; targetId?: string | undefined },
811
+ run: (tab: ResolvedBrowserTarget) => Promise<T>,
812
+ ): Promise<T> => {
813
+ const tab = await currentTab.target(value);
814
+ try {
815
+ const output = await run(tab);
816
+ currentTab.use(tab.browserSessionId, tab.targetId);
817
+ return output;
818
+ } catch (error) {
819
+ if (isGoneBrowserError(error)) currentTab.failed(tab);
820
+ throw error;
821
+ }
822
+ };
823
+ /** Run one call on the requested or current browser and remember it on success. */
824
+ const onBrowser = async <T>(
825
+ browserSessionId: string | undefined,
826
+ run: (browser: ResolvedBrowserSession) => Promise<T>,
827
+ ): Promise<T> => {
828
+ const browser = await currentTab.session(browserSessionId);
829
+ try {
830
+ const output = await run(browser);
831
+ currentTab.use(browser.browserSessionId);
832
+ return output;
833
+ } catch (error) {
834
+ if (isGoneBrowserError(error)) currentTab.failed(browser);
835
+ throw error;
836
+ }
837
+ };
762
838
  const add = <TInput extends z.ZodType, TOutput extends z.ZodType>(options: {
763
839
  name: InteractionAttemptToolName;
764
840
  codemodePath: readonly string[];
765
841
  title: string;
766
842
  description: string;
767
843
  input: TInput;
844
+ /** Adjusts the model's view of the input schema; validation is unchanged. */
845
+ modelInputSchema?: (schema: AttemptToolJsonSchema) => AttemptToolJsonSchema;
768
846
  output: TOutput;
769
847
  readOnly: boolean;
770
848
  idempotent: boolean;
771
849
  approval?: "none" | "human" | "policy";
850
+ /** The model's text view of a successful result. The complete typed value
851
+ * stays in structuredContent; without this the text is that value as JSON. */
852
+ modelText?: (output: z.output<TOutput>) => string;
772
853
  execute: (
773
854
  value: z.output<TInput>,
774
855
  context: AttemptToolExecutionContext,
@@ -786,10 +867,9 @@ export function createInteractionAttemptToolDefinitions(
786
867
  codemodePath: options.codemodePath,
787
868
  title: options.title,
788
869
  description: options.description,
789
- inputSchema:
790
- options.name === "browser_act"
791
- ? browserActionInputJsonSchema(options.input)
792
- : jsonSchema(options.input),
870
+ inputSchema: (options.modelInputSchema ?? ((schema) => schema))(
871
+ interactionToolInputJsonSchema(options.input),
872
+ ),
793
873
  outputSchema: jsonSchema(options.output),
794
874
  annotations: {
795
875
  title: options.title,
@@ -808,6 +888,7 @@ export function createInteractionAttemptToolDefinitions(
808
888
  context,
809
889
  options.execute,
810
890
  options.readOnly,
891
+ options.modelText,
811
892
  ),
812
893
  });
813
894
  };
@@ -885,13 +966,28 @@ export function createInteractionAttemptToolDefinitions(
885
966
  name: "browser_open",
886
967
  codemodePath: ["interaction", "browser", "open"],
887
968
  title: "Open or reuse browser",
888
- description: `Open a managed BrowserSession on the current agent placement, reuse a relevant compatible live session by default, attach to an explicit workspace BrowserSession, or open the user's own Chrome with placement={kind:'attached_device'}. Use that directly for requests about 'my browser', 'my tabs', or current Chrome: it opens the only Chrome profile connected through the Opengeni Browser extension, or else the most recently active one (reported as attachedProfile, the rest as otherAttachedProfiles). Add deviceId (from interaction_discover scope=attached_browsers or otherAttachedProfiles) only when the user wants a different profile. Omitting placement never uses the user's Chrome. If no profile is connected, explain that the Chrome extension must be connected instead of silently creating a blank managed browser (it needs a Connected Machine, then the Opengeni Browser extension: ${OPENGENI_BROWSER_EXTENSION_URL}). BrowserSessions persist across tool calls; shell-launched browser daemons do not survive remote-command cleanup. Never switch to the user's attached Chrome as a fallback for a failed managed browser unless the user requested that profile. A new attached session opens a dedicated tab. Managed Chromium defaults to headed so OAuth and later human interaction use a supported browser; request headless=true only for agent-only work that will not require sign-in or human control. Operator-enabled disposable sandbox verification may explicitly request storageMode=ephemeral_context with headless=true: no saved identity, route, Computer, checkpoint or resume; process loss ends the session. Default private_profile storage is unchanged. Returns exact session and tab state, plus observation: a compact snapshot of the opened (or selected) tab when one is available. With initialUrl, targets holds only the opened tab and otherTabCount counts the rest (browser_tabs list shows them). Reuse its refs and generations for the first action instead of observing again.`,
969
+ description: `Open a managed BrowserSession on the current agent placement, reuse a relevant compatible live session by default, attach to an explicit workspace BrowserSession, or open the user's own Chrome with placement={kind:'attached_device'}. Use that directly for requests about 'my browser', 'my tabs', or current Chrome: it opens the only Chrome profile connected through the Opengeni Browser extension, or else the most recently active one (reported as attachedProfile, the rest as otherAttachedProfiles). Add deviceId (from interaction_discover scope=attached_browsers or otherAttachedProfiles) only when the user wants a different profile. Omitting placement never uses the user's Chrome. If no profile is connected, explain that the Chrome extension must be connected instead of silently creating a blank managed browser (it needs a Connected Machine, then the Opengeni Browser extension: ${OPENGENI_BROWSER_EXTENSION_URL}). BrowserSessions persist across tool calls; shell-launched browser daemons do not survive remote-command cleanup. Never switch to the user's attached Chrome as a fallback for a failed managed browser unless the user requested that profile. A new attached session opens a dedicated tab. Managed Chromium defaults to headed so OAuth and later human interaction use a supported browser; request headless=true only for agent-only work that will not require sign-in or human control. Operator-enabled disposable sandbox verification may explicitly request storageMode=ephemeral_context with headless=true: no saved identity, route, Computer, checkpoint or resume; process loss ends the session. Default private_profile storage is unchanged. Returns the session, its tabs and an outline of the opened (or selected) tab when one is available, in the browser_observe format. With initialUrl, only the opened tab is listed and the others are counted (browser_tabs list shows them). Act on the outline's elements directly instead of observing again. Call it first for any browser task; browser_observe, browser_act, browser_screenshot and browser_tabs then work on the session it returns.`,
889
970
  input: BrowserOpenInput,
890
971
  output: BrowserOpenOutput,
891
972
  readOnly: false,
892
973
  idempotent: true,
893
- execute: async (value, context) =>
894
- await openBrowser(input.transport, input.workspaceId, input.sessionId, value, context),
974
+ modelText: browserOpenText,
975
+ execute: async (value, context) => {
976
+ const opened = await openBrowser(
977
+ input.transport,
978
+ input.workspaceId,
979
+ input.sessionId,
980
+ value,
981
+ context,
982
+ );
983
+ currentTab.use(
984
+ opened.session.id,
985
+ opened.observation?.target.id ??
986
+ opened.targets.find((target) => target.selected)?.id ??
987
+ null,
988
+ );
989
+ return opened;
990
+ },
895
991
  });
896
992
 
897
993
  add({
@@ -899,45 +995,46 @@ export function createInteractionAttemptToolDefinitions(
899
995
  codemodePath: ["interaction", "browser", "tabs"],
900
996
  title: "Manage browser tabs",
901
997
  description:
902
- "List, open, logically select, or close tabs in one exact BrowserSession. Selection changes the BrowserSession's default target, not the visible desktop tab. New attached-Chrome tabs open in the background. Use browser_act activate only when foregrounding the owned tab is explicitly intended. Closing a tab does not release the browser process; use browser_lifecycle for session cleanup. Returns the authoritative complete tab list after the operation. Open/select also return mutationObservation, the operation's compact page snapshot. Reuse its refs and generations for immediate actions; refresh after later page changes.",
998
+ "List, open, logically select, or close tabs in one exact BrowserSession. Selection changes the BrowserSession's default target, not the visible desktop tab. New attached-Chrome tabs open in the background. Use browser_act activate only when foregrounding the owned tab is explicitly intended. Closing a tab does not release the browser process; use browser_lifecycle for session cleanup. Returns the authoritative complete tab list after the operation. Open/select also return the page outline. Act on its elements directly; observe again after later page changes.",
903
999
  input: BrowserTabsInput,
904
1000
  output: BrowserTabsOutput,
905
1001
  readOnly: false,
906
1002
  idempotent: false,
907
- execute: async (value) => {
908
- let mutationObservation: z.infer<typeof BrowserObservation> | undefined;
909
- if (value.operation === "open") {
910
- mutationObservation = await input.transport.openBrowserTarget(
911
- input.workspaceId,
912
- value.browserSessionId,
913
- { ...(value.url ? { url: value.url } : {}) },
914
- );
915
- } else if (value.operation === "select") {
916
- mutationObservation = await input.transport.selectBrowserTarget(
917
- input.workspaceId,
918
- value.browserSessionId,
919
- value.targetId,
920
- );
921
- } else if (value.operation === "close") {
922
- return agentTargetList(
923
- await input.transport.closeBrowserTarget(
1003
+ modelText: browserTabsText,
1004
+ execute: async (value) =>
1005
+ await onBrowser(value.browserSessionId, async ({ browserSessionId }) => {
1006
+ let mutationObservation: z.infer<typeof BrowserObservation> | undefined;
1007
+ if (value.operation === "open") {
1008
+ mutationObservation = await input.transport.openBrowserTarget(
1009
+ input.workspaceId,
1010
+ browserSessionId,
1011
+ { ...(value.url ? { url: value.url } : {}) },
1012
+ );
1013
+ } else if (value.operation === "select") {
1014
+ mutationObservation = await input.transport.selectBrowserTarget(
924
1015
  input.workspaceId,
925
- value.browserSessionId,
1016
+ browserSessionId,
926
1017
  value.targetId,
927
- ),
928
- );
929
- }
930
- const tabs = await input.transport.listBrowserTargets(
931
- input.workspaceId,
932
- value.browserSessionId,
933
- );
934
- return mutationObservation
935
- ? {
1018
+ );
1019
+ } else if (value.operation === "close") {
1020
+ const closed = await input.transport.closeBrowserTarget(
1021
+ input.workspaceId,
1022
+ browserSessionId,
1023
+ value.targetId,
1024
+ );
1025
+ currentTab.forgetTarget(browserSessionId, value.targetId);
1026
+ return agentTargetList(closed);
1027
+ }
1028
+ const tabs = await input.transport.listBrowserTargets(input.workspaceId, browserSessionId);
1029
+ if (mutationObservation) {
1030
+ currentTab.use(browserSessionId, mutationObservation.target.id);
1031
+ return {
936
1032
  ...agentTargetList(tabs),
937
1033
  mutationObservation: projectBrowserObservation(mutationObservation, "compact"),
938
- }
939
- : agentTargetList(tabs);
940
- },
1034
+ };
1035
+ }
1036
+ return agentTargetList(tabs);
1037
+ }),
941
1038
  });
942
1039
 
943
1040
  add({
@@ -945,56 +1042,54 @@ export function createInteractionAttemptToolDefinitions(
945
1042
  codemodePath: ["interaction", "browser", "observe"],
946
1043
  title: "Observe browser tab",
947
1044
  description:
948
- "Read one tab's URL, title, viewport, causal generations, and a bounded accessibility view. Compact view is the default: agentView lists selected refs, and omittedNodeCount or clippedFieldCount appear only when something was left out; it is not a complete page tree. Empty and default fields are omitted. Set view=full for the complete accessibility snapshot. Set includeScreenshot=true to add a current still image as tool image content; browser_screenshot is faster when only pixels are needed.",
1045
+ "Read one tab's URL, title, viewport and accessibility outline: one line per element, role \"name\" [ref] value and states, indented by nesting. Locate an element by role and name, or by ref ({kind:'ref',ref}); refs are listed on controls and other actionable elements, not on text and layout lines. The default view is selective and says how many elements it left out; it is not a complete page tree. Set view=full for every element with its ref. Set includeScreenshot=true to add a current still image as tool image content; browser_screenshot is faster when only pixels are needed.",
949
1046
  input: BrowserObserveInput,
950
1047
  output: BrowserAgentObservation,
951
1048
  readOnly: true,
952
1049
  idempotent: true,
953
- execute: async (value) => {
954
- const observationRequest = input.transport.observeBrowserTarget(
955
- input.workspaceId,
956
- value.browserSessionId,
957
- value.targetId,
958
- );
959
- if (!value.includeScreenshot)
960
- return projectBrowserObservation(await observationRequest, value.view ?? "compact");
961
- // These are independent reads. Settle both before returning an error so
962
- // the tool never leaves an outstanding capture behind after a failed read.
963
- const [observed, captured] = await Promise.allSettled([
964
- observationRequest,
965
- Promise.resolve().then(() =>
966
- input.transport.captureBrowserTarget(
967
- input.workspaceId,
968
- value.browserSessionId,
969
- value.targetId,
970
- ),
971
- ),
972
- ]);
973
- if (observed.status === "rejected") throw observed.reason;
974
- if (captured.status === "rejected") throw captured.reason;
975
- let observation = observed.value;
976
- const frame = captured.value;
977
- if (
978
- observation.target.targetGeneration !== frame.targetGeneration ||
979
- observation.target.documentGeneration !== frame.documentGeneration
980
- ) {
981
- observation = await input.transport.observeBrowserTarget(
1050
+ modelText: agentObservationText,
1051
+ execute: async (value) =>
1052
+ await onTab(value, async ({ browserSessionId, targetId }) => {
1053
+ const observationRequest = input.transport.observeBrowserTarget(
982
1054
  input.workspaceId,
983
- value.browserSessionId,
984
- value.targetId,
1055
+ browserSessionId,
1056
+ targetId,
985
1057
  );
986
- }
987
- if (
988
- observation.target.targetGeneration !== frame.targetGeneration ||
989
- observation.target.documentGeneration !== frame.documentGeneration
990
- ) {
991
- throw new Error("browser target changed while its visual observation was captured");
992
- }
993
- return new InteractionExecutionResult(
994
- projectBrowserObservation(observation, value.view ?? "compact"),
995
- [browserToolImageContent(frame.data, frame.mediaType)],
996
- );
997
- },
1058
+ if (!value.includeScreenshot)
1059
+ return projectBrowserObservation(await observationRequest, value.view ?? "compact");
1060
+ // These are independent reads. Settle both before returning an error so
1061
+ // the tool never leaves an outstanding capture behind after a failed read.
1062
+ const [observed, captured] = await Promise.allSettled([
1063
+ observationRequest,
1064
+ Promise.resolve().then(() =>
1065
+ input.transport.captureBrowserTarget(input.workspaceId, browserSessionId, targetId),
1066
+ ),
1067
+ ]);
1068
+ if (observed.status === "rejected") throw observed.reason;
1069
+ if (captured.status === "rejected") throw captured.reason;
1070
+ let observation = observed.value;
1071
+ const frame = captured.value;
1072
+ if (
1073
+ observation.target.targetGeneration !== frame.targetGeneration ||
1074
+ observation.target.documentGeneration !== frame.documentGeneration
1075
+ ) {
1076
+ observation = await input.transport.observeBrowserTarget(
1077
+ input.workspaceId,
1078
+ browserSessionId,
1079
+ targetId,
1080
+ );
1081
+ }
1082
+ if (
1083
+ observation.target.targetGeneration !== frame.targetGeneration ||
1084
+ observation.target.documentGeneration !== frame.documentGeneration
1085
+ ) {
1086
+ throw new Error("browser target changed while its visual observation was captured");
1087
+ }
1088
+ return new InteractionExecutionResult(
1089
+ projectBrowserObservation(observation, value.view ?? "compact"),
1090
+ [browserToolImageContent(frame.data, frame.mediaType)],
1091
+ );
1092
+ }),
998
1093
  });
999
1094
 
1000
1095
  add({
@@ -1007,70 +1102,69 @@ export function createInteractionAttemptToolDefinitions(
1007
1102
  output: BrowserReadToolOutput,
1008
1103
  readOnly: true,
1009
1104
  idempotent: true,
1010
- execute: async (value) => {
1011
- if (value.mode === "dom") {
1012
- const state =
1013
- value.expectedTargetGeneration !== undefined &&
1014
- value.expectedDocumentGeneration !== undefined &&
1015
- value.expectedFrameId !== undefined
1016
- ? null
1017
- : await input.transport.getBrowserTargetState(
1018
- input.workspaceId,
1019
- value.browserSessionId,
1020
- value.targetId,
1021
- );
1022
- const fences = {
1023
- expectedTargetGeneration: value.expectedTargetGeneration ?? state!.targetGeneration,
1024
- expectedDocumentGeneration:
1025
- value.expectedDocumentGeneration === undefined
1026
- ? state!.documentGeneration
1027
- : value.expectedDocumentGeneration,
1028
- expectedFrameId:
1029
- value.expectedFrameId === undefined ? state!.frameId : value.expectedFrameId,
1030
- };
1031
- const documentGeneration = fences.expectedDocumentGeneration;
1032
- const frameId = fences.expectedFrameId;
1033
- if (documentGeneration === null || frameId === null) {
1034
- throw new OpenGeniApiError(409, "browser target has no inspectable document", {
1035
- code: "document_unavailable",
1036
- retryable: true,
1037
- });
1105
+ modelText: (output) =>
1106
+ output.source === "accessibility" ? browserReadText(output) : JSON.stringify(output),
1107
+ execute: async (value) =>
1108
+ await onTab(value, async ({ browserSessionId, targetId }) => {
1109
+ if (value.mode === "dom") {
1110
+ const state =
1111
+ value.expectedTargetGeneration !== undefined &&
1112
+ value.expectedDocumentGeneration !== undefined &&
1113
+ value.expectedFrameId !== undefined
1114
+ ? null
1115
+ : await input.transport.getBrowserTargetState(
1116
+ input.workspaceId,
1117
+ browserSessionId,
1118
+ targetId,
1119
+ );
1120
+ const fences = {
1121
+ expectedTargetGeneration: value.expectedTargetGeneration ?? state!.targetGeneration,
1122
+ expectedDocumentGeneration:
1123
+ value.expectedDocumentGeneration === undefined
1124
+ ? state!.documentGeneration
1125
+ : value.expectedDocumentGeneration,
1126
+ expectedFrameId:
1127
+ value.expectedFrameId === undefined ? state!.frameId : value.expectedFrameId,
1128
+ };
1129
+ const documentGeneration = fences.expectedDocumentGeneration;
1130
+ const frameId = fences.expectedFrameId;
1131
+ if (documentGeneration === null || frameId === null) {
1132
+ throw new OpenGeniApiError(409, "browser target has no inspectable document", {
1133
+ code: "document_unavailable",
1134
+ retryable: true,
1135
+ });
1136
+ }
1137
+ const inspectedFences = {
1138
+ expectedTargetGeneration: fences.expectedTargetGeneration,
1139
+ expectedDocumentGeneration: documentGeneration,
1140
+ expectedFrameId: frameId,
1141
+ };
1142
+ const dom = value.dom!;
1143
+ const request =
1144
+ dom.kind === "element"
1145
+ ? {
1146
+ ...inspectedFences,
1147
+ kind: "element" as const,
1148
+ locator: dom.locator,
1149
+ ...(dom.attributes ? { attributes: dom.attributes } : {}),
1150
+ maxChars: dom.maxChars ?? 4_096,
1151
+ }
1152
+ : { ...inspectedFences, kind: "count" as const, selector: dom.selector };
1153
+ return {
1154
+ ...(await input.transport.readBrowserDom(
1155
+ input.workspaceId,
1156
+ browserSessionId,
1157
+ targetId,
1158
+ request,
1159
+ )),
1160
+ source: "dom" as const,
1161
+ };
1038
1162
  }
1039
- const inspectedFences = {
1040
- expectedTargetGeneration: fences.expectedTargetGeneration,
1041
- expectedDocumentGeneration: documentGeneration,
1042
- expectedFrameId: frameId,
1043
- };
1044
- const dom = value.dom!;
1045
- const request =
1046
- dom.kind === "element"
1047
- ? {
1048
- ...inspectedFences,
1049
- kind: "element" as const,
1050
- locator: dom.locator,
1051
- ...(dom.attributes ? { attributes: dom.attributes } : {}),
1052
- maxChars: dom.maxChars ?? 4_096,
1053
- }
1054
- : { ...inspectedFences, kind: "count" as const, selector: dom.selector };
1055
- return {
1056
- ...(await input.transport.readBrowserDom(
1057
- input.workspaceId,
1058
- value.browserSessionId,
1059
- value.targetId,
1060
- request,
1061
- )),
1062
- source: "dom" as const,
1063
- };
1064
- }
1065
- return readBrowserObservation(
1066
- await input.transport.observeBrowserTarget(
1067
- input.workspaceId,
1068
- value.browserSessionId,
1069
- value.targetId,
1070
- ),
1071
- value,
1072
- );
1073
- },
1163
+ return readBrowserObservation(
1164
+ await input.transport.observeBrowserTarget(input.workspaceId, browserSessionId, targetId),
1165
+ value,
1166
+ );
1167
+ }),
1074
1168
  });
1075
1169
 
1076
1170
  add({
@@ -1083,34 +1177,38 @@ export function createInteractionAttemptToolDefinitions(
1083
1177
  output: BrowserScreenshotOutput,
1084
1178
  readOnly: true,
1085
1179
  idempotent: true,
1086
- execute: async (value) => {
1087
- const frame = await input.transport.captureBrowserTarget(
1088
- input.workspaceId,
1089
- value.browserSessionId,
1090
- value.targetId,
1091
- {},
1092
- { fullPage: value.fullPage ?? false, ...(value.quality ? { quality: value.quality } : {}) },
1093
- );
1094
- return new InteractionExecutionResult(
1095
- {
1096
- kind: "browser_screenshot" as const,
1097
- browserSessionId: frame.browserSessionId,
1098
- targetId: frame.targetId,
1099
- frameId: frame.frameId,
1100
- targetGeneration: frame.targetGeneration,
1101
- documentGeneration: frame.documentGeneration,
1102
- mediaType: frame.mediaType,
1103
- width: frame.width,
1104
- height: frame.height,
1105
- deviceScaleFactor: frame.deviceScaleFactor,
1106
- scrollX: frame.scrollX,
1107
- scrollY: frame.scrollY,
1108
- capturedAt: frame.capturedAt,
1109
- fullPage: value.fullPage ?? false,
1110
- },
1111
- [browserToolImageContent(frame.data, frame.mediaType)],
1112
- );
1113
- },
1180
+ execute: async (value) =>
1181
+ await onTab(value, async ({ browserSessionId, targetId }) => {
1182
+ const frame = await input.transport.captureBrowserTarget(
1183
+ input.workspaceId,
1184
+ browserSessionId,
1185
+ targetId,
1186
+ {},
1187
+ {
1188
+ fullPage: value.fullPage ?? false,
1189
+ ...(value.quality ? { quality: value.quality } : {}),
1190
+ },
1191
+ );
1192
+ return new InteractionExecutionResult(
1193
+ {
1194
+ kind: "browser_screenshot" as const,
1195
+ browserSessionId: frame.browserSessionId,
1196
+ targetId: frame.targetId,
1197
+ frameId: frame.frameId,
1198
+ targetGeneration: frame.targetGeneration,
1199
+ documentGeneration: frame.documentGeneration,
1200
+ mediaType: frame.mediaType,
1201
+ width: frame.width,
1202
+ height: frame.height,
1203
+ deviceScaleFactor: frame.deviceScaleFactor,
1204
+ scrollX: frame.scrollX,
1205
+ scrollY: frame.scrollY,
1206
+ capturedAt: frame.capturedAt,
1207
+ fullPage: value.fullPage ?? false,
1208
+ },
1209
+ [browserToolImageContent(frame.data, frame.mediaType)],
1210
+ );
1211
+ }),
1114
1212
  });
1115
1213
 
1116
1214
  add({
@@ -1118,20 +1216,20 @@ export function createInteractionAttemptToolDefinitions(
1118
1216
  codemodePath: ["interaction", "browser", "act"],
1119
1217
  title: "Act in browser tab",
1120
1218
  description:
1121
- "Perform one semantic-first browser action or bounded batch. Prefer action={type:'batch',actions:[...],fenceEachAction:true} for predictable edits in the same observed document, such as filling known form fields before one authorized submit. Pause for new page state, a dialog, or a decision instead of batching steps that depend on an unseen result. Reuse a page observation, when present, from browser_open, tab open/select or a prior action; observe only when none is present or it lacks needed content. Observe again after page changes or when needed content was omitted. If an action reports outcome_unknown, inspect the outcome and do not replay the batch. Use viewport to set page width, height, desktop/mobile layout and touch emulation, then check the measured viewport in the returned observation; emulation does not prove physical mobile-browser behavior. Use history back/forward for tab navigation; keypress shortcuts are page input and may not navigate browser history. A press key is one character or a key name (Enter, Tab, Escape, Backspace, Delete, Insert, Home, End, PageUp, PageDown, ArrowUp/Down/Left/Right, Space, F1-F24), optionally prefixed by Ctrl+, Alt+, Shift+, Meta+ or Mod+. The explicit activate action foregrounds the target in the user's desktop browser; use only when that is intended. Permission actions set a managed browser's web permission for this tab's exact current top-level origin. Omit generation fences to fetch fresh generation metadata without scanning accessibility; supplying all three fences skips that state read while preserving controller validation. The default receipt includes a compact accessibility view with explicit omissions; set view=full for the complete observation, or view=none when no post-action tree is needed. An upload action takes workspace File IDs; the artifact.artifactId returned by sandbox_file_publish is one. Text in fill, type and clipboard write may contain {{secret:…}} references; the value (or the current TOTP code for /otp) is typed at dispatch and never shown to you. Use type with mode:'keys' for split one-time-code boxes or inputs that ignore pasted text.",
1219
+ "Perform one semantic-first browser action or bounded batch. Prefer action={type:'batch',actions:[...],fenceEachAction:true} for predictable edits in the same observed document, such as filling known form fields before one authorized submit. Pause for new page state, a dialog, or a decision instead of batching steps that depend on an unseen result. Reuse a page observation, when present, from browser_open, tab open/select or a prior action; observe only when none is present or it lacks needed content. Observe again after page changes or when needed content was omitted. If an action reports outcome_unknown, inspect the outcome and do not replay the batch. Use viewport to set page width, height, desktop/mobile layout and touch emulation, then check the measured viewport in the returned observation; emulation does not prove physical mobile-browser behavior. Use history back/forward for tab navigation; keypress shortcuts are page input and may not navigate browser history. A press key is one character or a key name (Enter, Tab, Escape, Backspace, Delete, Insert, Home, End, PageUp, PageDown, ArrowUp/Down/Left/Right, Space, F1-F24), optionally prefixed by Ctrl+, Alt+, Shift+, Meta+ or Mod+. The explicit activate action foregrounds the target in the user's desktop browser; use only when that is intended. Permission actions set a managed browser's web permission for this tab's exact current top-level origin. The default receipt includes the page outline after the action, in the browser_observe format; set view=full for every element, or view=none when no post-action outline is needed. An upload action takes workspace File IDs; the artifact.artifactId returned by sandbox_file_publish is one. Text in fill, type and clipboard write may contain {{secret:…}} references; the value (or the current TOTP code for /otp) is typed at dispatch and never shown to you. Use type with mode:'keys' for split one-time-code boxes or inputs that ignore pasted text.",
1122
1220
  input: BrowserActInput,
1221
+ modelInputSchema: withRefShorthandLocators,
1123
1222
  output: BrowserAgentActionReceipt,
1124
1223
  readOnly: false,
1125
1224
  idempotent: true,
1126
- execute: async (value, context) => {
1127
- // Omitted fences are resolved by the API from the live target in the same
1128
- // request, so acting on the current document costs one round trip.
1129
- const receipt = await input.transport.actInBrowser(
1130
- input.workspaceId,
1131
- value.browserSessionId,
1132
- {
1225
+ modelText: agentActionText,
1226
+ execute: async (value, context) =>
1227
+ await onTab(value, async ({ browserSessionId, targetId }) => {
1228
+ // Omitted fences are resolved by the API from the live target in the same
1229
+ // request, so acting on the current document costs one round trip.
1230
+ const receipt = await input.transport.actInBrowser(input.workspaceId, browserSessionId, {
1133
1231
  operationId: context.operationId,
1134
- targetId: value.targetId,
1232
+ targetId,
1135
1233
  ...(value.expectedTargetGeneration === undefined
1136
1234
  ? {}
1137
1235
  : { expectedTargetGeneration: value.expectedTargetGeneration }),
@@ -1143,10 +1241,9 @@ export function createInteractionAttemptToolDefinitions(
1143
1241
  : { expectedFrameId: value.expectedFrameId }),
1144
1242
  ...(value.view === "none" ? { observationMode: "none" as const } : {}),
1145
1243
  action: value.action,
1146
- },
1147
- );
1148
- return value.view === "full" ? receipt : compactActionReceipt(receipt);
1149
- },
1244
+ });
1245
+ return value.view === "full" ? receipt : compactActionReceipt(receipt);
1246
+ }),
1150
1247
  });
1151
1248
 
1152
1249
  add({
@@ -1174,15 +1271,19 @@ export function createInteractionAttemptToolDefinitions(
1174
1271
  readOnly: true,
1175
1272
  idempotent: true,
1176
1273
  execute: async (value) =>
1177
- await input.transport.listBrowserDiagnostics(
1178
- input.workspaceId,
1179
- value.browserSessionId,
1180
- value.targetId,
1181
- {
1182
- ...(value.kinds ? { kinds: value.kinds } : {}),
1183
- ...(value.after !== undefined ? { after: value.after } : {}),
1184
- ...(value.limit !== undefined ? { limit: value.limit } : {}),
1185
- },
1274
+ await onTab(
1275
+ value,
1276
+ async ({ browserSessionId, targetId }) =>
1277
+ await input.transport.listBrowserDiagnostics(
1278
+ input.workspaceId,
1279
+ browserSessionId,
1280
+ targetId,
1281
+ {
1282
+ ...(value.kinds ? { kinds: value.kinds } : {}),
1283
+ ...(value.after !== undefined ? { after: value.after } : {}),
1284
+ ...(value.limit !== undefined ? { limit: value.limit } : {}),
1285
+ },
1286
+ ),
1186
1287
  ),
1187
1288
  });
1188
1289
 
@@ -1503,6 +1604,7 @@ export function createInteractionAttemptToolDefinitions(
1503
1604
  request,
1504
1605
  );
1505
1606
  }
1607
+ currentTab.forgetSession(value.browserSessionId);
1506
1608
  return await input.transport.endBrowserSession(
1507
1609
  input.workspaceId,
1508
1610
  value.browserSessionId,
@@ -1516,7 +1618,7 @@ export function createInteractionAttemptToolDefinitions(
1516
1618
  codemodePath: ["interaction", "computer", "open"],
1517
1619
  title: "Open or reuse computer",
1518
1620
  description:
1519
- "Open a ComputerSession on the current agent placement, reuse a relevant live session by default, or attach to an explicit workspace ComputerSession. Returns exact apps/windows/screens.",
1621
+ "Open a ComputerSession on the current agent placement, reuse a relevant live session by default, or attach to an explicit workspace ComputerSession. Returns exact apps/windows/screens. Call it first for any computer task; computer_observe and computer_act then work on the session it returns.",
1520
1622
  input: ComputerOpenInput,
1521
1623
  output: ComputerOpenOutput,
1522
1624
  readOnly: false,
@@ -1912,6 +2014,9 @@ async function openBrowser(
1912
2014
  ): Promise<z.input<typeof BrowserOpenOutput>> {
1913
2015
  let session: z.infer<typeof BrowserSession>;
1914
2016
  let created = false;
2017
+ // A create that launched the browser in this call returns its tabs and the
2018
+ // opened tab's observation, which replaces the two reads below.
2019
+ let initialView: Awaited<ReturnType<InteractionTransport["createBrowserSession"]>>["initialView"];
1915
2020
  let profileChoice: AttachedProfileChoice | undefined;
1916
2021
  if (value.browserSessionId) {
1917
2022
  session = await transport.getBrowserSession(workspaceId, value.browserSessionId);
@@ -1967,23 +2072,23 @@ async function openBrowser(
1967
2072
  const placement = profileChoice
1968
2073
  ? { kind: "attached_device" as const, deviceId: profileChoice.chosen.id }
1969
2074
  : (value.placement as z.infer<typeof InteractionPlacement> | undefined);
1970
- session = (
1971
- await transport.createBrowserSession(workspaceId, {
1972
- operationId: context.operationId,
1973
- sessionId: sourceSessionId,
1974
- ...(value.name ? { name: value.name } : {}),
1975
- ...(value.initialUrl ? { initialUrl: value.initialUrl } : {}),
1976
- headless: requestedHeadless,
1977
- ...(value.storageMode ? { storageMode: value.storageMode } : {}),
1978
- ...(placement ? { placement } : {}),
1979
- ...(value.identityId ? { identityId: value.identityId } : {}),
1980
- ...(value.baseRevisionId ? { baseRevisionId: value.baseRevisionId } : {}),
1981
- ...(value.networkRouteId ? { networkRouteId: value.networkRouteId } : {}),
1982
- ...(value.linkedComputerSessionId
1983
- ? { linkedComputerSessionId: value.linkedComputerSessionId }
1984
- : {}),
1985
- })
1986
- ).session;
2075
+ const createdResponse = await transport.createBrowserSession(workspaceId, {
2076
+ operationId: context.operationId,
2077
+ sessionId: sourceSessionId,
2078
+ ...(value.name ? { name: value.name } : {}),
2079
+ ...(value.initialUrl ? { initialUrl: value.initialUrl } : {}),
2080
+ headless: requestedHeadless,
2081
+ ...(value.storageMode ? { storageMode: value.storageMode } : {}),
2082
+ ...(placement ? { placement } : {}),
2083
+ ...(value.identityId ? { identityId: value.identityId } : {}),
2084
+ ...(value.baseRevisionId ? { baseRevisionId: value.baseRevisionId } : {}),
2085
+ ...(value.networkRouteId ? { networkRouteId: value.networkRouteId } : {}),
2086
+ ...(value.linkedComputerSessionId
2087
+ ? { linkedComputerSessionId: value.linkedComputerSessionId }
2088
+ : {}),
2089
+ });
2090
+ session = createdResponse.session;
2091
+ initialView = createdResponse.initialView;
1987
2092
  }
1988
2093
  }
1989
2094
  if (session.lifecycle === "suspended") {
@@ -2002,7 +2107,8 @@ async function openBrowser(
2002
2107
  }
2003
2108
  : {};
2004
2109
  if (session.lifecycle !== "active") return { session, targets: [], ...chosenProfile };
2005
- let targets = (await transport.listBrowserTargets(workspaceId, session.id)).targets;
2110
+ let targets =
2111
+ initialView?.targets ?? (await transport.listBrowserTargets(workspaceId, session.id)).targets;
2006
2112
  if (
2007
2113
  value.initialUrl &&
2008
2114
  !created &&
@@ -2031,9 +2137,11 @@ async function openBrowser(
2031
2137
  targets = inventory.targets;
2032
2138
  }
2033
2139
  const primary = openedBrowserTarget(targets, value.initialUrl);
2034
- const observation = primary
2035
- ? await observeOpenedBrowserTarget(transport, workspaceId, session.id, primary)
2036
- : undefined;
2140
+ const observation = !primary
2141
+ ? undefined
2142
+ : initialView?.observation.target.id === primary.id
2143
+ ? projectBrowserObservation(initialView.observation, "compact")
2144
+ : await observeOpenedBrowserTarget(transport, workspaceId, session.id, primary);
2037
2145
  // A browser opened on a URL is about that tab. A person's own Chrome profile
2038
2146
  // can hold dozens of unrelated tabs; listing them all costs context on every
2039
2147
  // later step, so only the opened tab is returned with a count of the rest
@@ -2574,6 +2682,79 @@ function readBrowserObservation(
2574
2682
  };
2575
2683
  }
2576
2684
 
2685
+ /** A tool's model text, or its JSON when it has none. Rendering is a pure view
2686
+ * of a value that already passed its output schema; should it ever fail, the
2687
+ * model still gets the complete value rather than an error for a call that
2688
+ * succeeded. */
2689
+ function resultModelText<T>(output: T, modelText: ((output: T) => string) | undefined): string {
2690
+ if (modelText) {
2691
+ try {
2692
+ return modelText(output);
2693
+ } catch {
2694
+ // Fall through to the complete value.
2695
+ }
2696
+ }
2697
+ return JSON.stringify(output);
2698
+ }
2699
+
2700
+ /** The text view of a complete observation (view=full): every node, every ref. */
2701
+ function fullObservationText(observation: z.infer<typeof BrowserObservation>): string {
2702
+ const semantic = observation.semantic;
2703
+ const roots =
2704
+ semantic?.kind === "snapshot"
2705
+ ? semantic.roots
2706
+ : semantic?.kind === "diff"
2707
+ ? semantic.changed
2708
+ : [];
2709
+ return browserObservationText(
2710
+ {
2711
+ target: observation.target,
2712
+ viewport: observation.viewport,
2713
+ focusedRef: observation.focusedRef,
2714
+ changedRegions: observation.changedRegions,
2715
+ diagnostics: observation.diagnostics,
2716
+ dialog: observation.dialog,
2717
+ screenshot: observation.screenshot ?? undefined,
2718
+ agentView: {
2719
+ sourceKind: semantic?.kind ?? "none",
2720
+ nodes: flattenBrowserNodes(roots).map(({ node, depth }) => ({
2721
+ ref: node.ref,
2722
+ role: node.role,
2723
+ depth,
2724
+ ...(node.name !== undefined ? { name: node.name } : {}),
2725
+ ...(node.description !== undefined ? { description: node.description } : {}),
2726
+ ...(node.value !== undefined ? { value: node.value } : {}),
2727
+ states: node.states,
2728
+ actions: node.actions,
2729
+ })),
2730
+ ...(semantic?.kind === "diff" && semantic.removedRefs.length > 0
2731
+ ? { removedRefCount: semantic.removedRefs.length }
2732
+ : {}),
2733
+ },
2734
+ },
2735
+ { allRefs: true },
2736
+ );
2737
+ }
2738
+
2739
+ function agentObservationText(observation: z.infer<typeof BrowserAgentObservation>): string {
2740
+ return "agentView" in observation
2741
+ ? browserObservationText(observation)
2742
+ : fullObservationText(observation);
2743
+ }
2744
+
2745
+ function agentActionText(receipt: z.infer<typeof BrowserAgentActionReceipt>): string {
2746
+ if ("protocolVersion" in receipt) {
2747
+ const text = browserActionText({
2748
+ targetId: receipt.targetId,
2749
+ state: receipt.state,
2750
+ error: receipt.error,
2751
+ ...(receipt.targets ? { targets: receipt.targets } : {}),
2752
+ });
2753
+ return receipt.observation ? `${text}\n${fullObservationText(receipt.observation)}` : text;
2754
+ }
2755
+ return browserActionText(receipt);
2756
+ }
2757
+
2577
2758
  async function safeInteractionExecution<TInput extends z.ZodType, TOutput extends z.ZodType>(
2578
2759
  inputSchema: TInput,
2579
2760
  outputSchema: TOutput,
@@ -2584,6 +2765,7 @@ async function safeInteractionExecution<TInput extends z.ZodType, TOutput extend
2584
2765
  context: AttemptToolExecutionContext,
2585
2766
  ) => Promise<z.input<TOutput> | InteractionExecutionResult<z.input<TOutput>>>,
2586
2767
  readOnly: boolean,
2768
+ modelText?: (output: z.output<TOutput>) => string,
2587
2769
  ): Promise<AttemptToolResultValue> {
2588
2770
  try {
2589
2771
  const value = inputSchema.parse(raw);
@@ -2593,7 +2775,7 @@ async function safeInteractionExecution<TInput extends z.ZodType, TOutput extend
2593
2775
  );
2594
2776
  const result = {
2595
2777
  content: [
2596
- { type: "text" as const, text: JSON.stringify(structuredContent) },
2778
+ { type: "text" as const, text: resultModelText(structuredContent, modelText) },
2597
2779
  ...(executed instanceof InteractionExecutionResult ? executed.additionalContent : []),
2598
2780
  ],
2599
2781
  structuredContent: structuredContent as NonNullable<
@@ -2603,7 +2785,10 @@ async function safeInteractionExecution<TInput extends z.ZodType, TOutput extend
2603
2785
  return result;
2604
2786
  } catch (error) {
2605
2787
  if (error instanceof z.ZodError) {
2606
- return interactionErrorResult("invalid_arguments", "Interaction tool arguments are invalid.");
2788
+ return interactionErrorResult("invalid_arguments", describeInvalidToolArguments(error));
2789
+ }
2790
+ if (error instanceof BrowserTargetResolutionError) {
2791
+ return interactionErrorResult(error.code, error.message);
2607
2792
  }
2608
2793
  // A failed read is still a useful tool result. Preserve the API's public
2609
2794
  // explanation; mutations and uncertain outcomes retain their failure path.
@@ -2709,6 +2894,11 @@ function browserToolImageContent(
2709
2894
  return { type: "image", data: Buffer.from(data).toString("base64"), mimeType };
2710
2895
  }
2711
2896
 
2897
+ /** The remembered browser or tab no longer exists. */
2898
+ function isGoneBrowserError(error: unknown): boolean {
2899
+ return error instanceof OpenGeniApiError && (error.status === 404 || error.status === 410);
2900
+ }
2901
+
2712
2902
  function interactionErrorResult(
2713
2903
  code: string,
2714
2904
  message: string,