@opengeni/runtime 4.0.1 → 4.0.3-canary.36199476632001

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 (61) hide show
  1. package/dist/{agent-RJQLKFIW.js → agent-ME6HT4DH.js} +2 -2
  2. package/dist/assets/codemode-client.json +1 -1
  3. package/dist/browser-action-json-schema.d.ts +4 -0
  4. package/dist/{chunk-J5AR5XAJ.js → chunk-FAFZCSN7.js} +695 -210
  5. package/dist/chunk-FAFZCSN7.js.map +1 -0
  6. package/dist/{chunk-ZG2YH25H.js → chunk-JAYHA6BF.js} +2 -2
  7. package/dist/{chunk-UNICCRW5.js → chunk-LZIXCZUV.js} +6 -3
  8. package/dist/{chunk-UNICCRW5.js.map → chunk-LZIXCZUV.js.map} +1 -1
  9. package/dist/{chunk-J6J3VYFW.js → chunk-P4V7JGQ2.js} +1037 -105
  10. package/dist/chunk-P4V7JGQ2.js.map +1 -0
  11. package/dist/{chunk-SPT3PLMX.js → chunk-PRHMSM6C.js} +2 -2
  12. package/dist/codex-tool-search.d.ts +12 -0
  13. package/dist/context-compaction.d.ts +4 -2
  14. package/dist/index.d.ts +27 -8
  15. package/dist/index.js +23 -7
  16. package/dist/interaction-tools.d.ts +1 -1
  17. package/dist/{lazy-manifest-OEYEHNVY.js → lazy-manifest-GCGUFIWP.js} +3 -3
  18. package/dist/{localSources-TWHTEKGO.js → localSources-FDH65AFI.js} +4 -4
  19. package/dist/metrics.d.ts +17 -0
  20. package/dist/model-provider-routing.d.ts +14 -0
  21. package/dist/operational-instructions.d.ts +1 -1
  22. package/dist/provider-quota.d.ts +76 -0
  23. package/dist/sandbox/browser-control-client.d.ts +28 -1
  24. package/dist/sandbox/channel-a.d.ts +43 -0
  25. package/dist/sandbox/index.d.ts +3 -3
  26. package/dist/sandbox/index.js +10 -6
  27. package/dist/sandbox/providers/modal-command-router-wire.d.ts +8 -0
  28. package/dist/sandbox/providers/modal.d.ts +5 -6
  29. package/dist/workspace-tool-gateway.js +5 -5
  30. package/package.json +11 -11
  31. package/src/browser-action-json-schema.ts +10 -0
  32. package/src/bundled_artifact_skills/opengeni-documents/SKILL.md +4 -1
  33. package/src/bundled_artifact_skills/opengeni-documents/references/api.md +20 -0
  34. package/src/bundled_default_skills/opengeni-client/SKILL.md +4 -1
  35. package/src/bundled_default_skills/opengeni-client/references/discovery-and-autonomy.md +31 -0
  36. package/src/bundled_default_skills/opengeni-client/references/runtime-profile-and-verification.md +32 -0
  37. package/src/codex-tool-search.ts +54 -6
  38. package/src/context-compaction.ts +19 -3
  39. package/src/index.ts +285 -56
  40. package/src/interaction-tools.ts +656 -48
  41. package/src/lazy-tool-transport.ts +4 -0
  42. package/src/metrics.ts +26 -0
  43. package/src/model-context-inspector.ts +1 -0
  44. package/src/model-provider-client.ts +37 -16
  45. package/src/model-provider-routing.ts +32 -6
  46. package/src/operational-instructions.ts +1 -1
  47. package/src/provider-quota.ts +412 -0
  48. package/src/sandbox/browser-control-client.ts +241 -26
  49. package/src/sandbox/channel-a.ts +370 -0
  50. package/src/sandbox/index.ts +6 -1
  51. package/src/sandbox/providers/modal-command-control.ts +7 -1
  52. package/src/sandbox/providers/modal-command-router-wire.ts +47 -6
  53. package/src/sandbox/providers/modal.ts +16 -24
  54. package/src/sandbox/routing/routing-session.ts +29 -6
  55. package/dist/chunk-J5AR5XAJ.js.map +0 -1
  56. package/dist/chunk-J6J3VYFW.js.map +0 -1
  57. /package/dist/{agent-RJQLKFIW.js.map → agent-ME6HT4DH.js.map} +0 -0
  58. /package/dist/{chunk-ZG2YH25H.js.map → chunk-JAYHA6BF.js.map} +0 -0
  59. /package/dist/{chunk-SPT3PLMX.js.map → chunk-PRHMSM6C.js.map} +0 -0
  60. /package/dist/{lazy-manifest-OEYEHNVY.js.map → lazy-manifest-GCGUFIWP.js.map} +0 -0
  61. /package/dist/{localSources-TWHTEKGO.js.map → localSources-FDH65AFI.js.map} +0 -0
@@ -9,10 +9,15 @@ import {
9
9
  BrowserActionReceipt,
10
10
  BrowserClipboard,
11
11
  BrowserDiagnosticBatch,
12
+ BrowserDomReadResponse,
13
+ BrowserDomReadLocator,
14
+ BrowserDomReadSelector,
15
+ BrowserDomSafeAttribute,
12
16
  BrowserIdentity,
13
17
  BrowserIdentityListResponse,
14
18
  BrowserIdentityMutationResponse,
15
19
  BrowserObservation,
20
+ InteractionSemanticNode,
16
21
  BrowserRevisionListResponse,
17
22
  BrowserSession,
18
23
  BrowserSessionMutationResponse,
@@ -61,6 +66,7 @@ import type {
61
66
  } from "@opengeni/codemode";
62
67
  import { OpenGeniApiError, OpenGeniClient, type InteractionTransport } from "@opengeni/sdk";
63
68
  import { z } from "zod";
69
+ import { browserActionInputJsonSchema } from "./browser-action-json-schema";
64
70
  import { guardedMcpFetch } from "./mcp-network";
65
71
 
66
72
  export const INTERACTION_ATTEMPT_TOOL_NAMES = FIRST_PARTY_IN_PROCESS_TOOL_NAMES;
@@ -74,11 +80,200 @@ class InteractionExecutionResult<T> {
74
80
  ) {}
75
81
  }
76
82
 
83
+ const BROWSER_AGENT_MAX_NODES = 60;
84
+ const BROWSER_AGENT_MAX_NODE_BYTES = 12_000;
85
+ const BROWSER_AGENT_MAX_FIELD_LENGTH = 180;
86
+ const BROWSER_READ_MAX_NODES = 80;
87
+ // A base64 image is journaled inside a 16 MiB Code Mode result. Leave room
88
+ // for JSON framing, metadata, and other content instead of failing after capture.
89
+ const BROWSER_TOOL_IMAGE_MAX_BYTES = 8 * 1024 * 1024;
90
+
91
+ const BrowserAgentNode = z
92
+ .object({
93
+ ref: z.string().max(512),
94
+ role: z.string().max(256),
95
+ depth: z.number().int().nonnegative(),
96
+ name: z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH).optional(),
97
+ description: z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH).optional(),
98
+ value: z
99
+ .union([
100
+ z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH),
101
+ z.object({ redacted: z.literal(true), reason: z.string().max(32) }).strict(),
102
+ ])
103
+ .optional(),
104
+ states: z.array(z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH)).max(6),
105
+ actions: z.array(z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH)).max(6),
106
+ })
107
+ .strict();
108
+ const BrowserAgentView = z
109
+ .object({
110
+ kind: z.literal("compact"),
111
+ sourceKind: z.enum(["snapshot", "diff", "none"]),
112
+ nodes: z.array(BrowserAgentNode).max(BROWSER_AGENT_MAX_NODES),
113
+ sourceNodeCount: z.number().int().nonnegative(),
114
+ omittedNodeCount: z.number().int().nonnegative(),
115
+ removedRefCount: z.number().int().nonnegative(),
116
+ clippedFieldCount: z.number().int().nonnegative(),
117
+ maxNodes: z.number().int().positive(),
118
+ maxNodeBytes: z.number().int().positive(),
119
+ })
120
+ .strict();
121
+ const CompactBrowserObservation = BrowserObservation.safeExtend({
122
+ semantic: z.null(),
123
+ agentView: BrowserAgentView,
124
+ });
125
+ const BrowserAgentObservation = z.union([BrowserObservation, CompactBrowserObservation]);
126
+ const BrowserAgentActionReceipt = BrowserActionReceipt.safeExtend({
127
+ observation: BrowserAgentObservation.nullable(),
128
+ });
129
+ const BrowserScreenshotInput = z
130
+ .object({
131
+ browserSessionId: z.string().uuid(),
132
+ targetId: z.string().min(1).max(512),
133
+ fullPage: z.boolean().optional(),
134
+ quality: z.number().int().min(1).max(100).optional(),
135
+ })
136
+ .strict();
137
+ const BrowserScreenshotOutput = z
138
+ .object({
139
+ kind: z.literal("browser_screenshot"),
140
+ browserSessionId: z.string().uuid(),
141
+ targetId: z.string(),
142
+ frameId: z.string(),
143
+ targetGeneration: z.string(),
144
+ documentGeneration: z.string(),
145
+ mediaType: z.enum(["image/jpeg", "image/png"]),
146
+ width: z.number().int().positive(),
147
+ height: z.number().int().positive(),
148
+ deviceScaleFactor: z.number().positive(),
149
+ scrollX: z.number(),
150
+ scrollY: z.number(),
151
+ capturedAt: z.string(),
152
+ fullPage: z.boolean(),
153
+ })
154
+ .strict();
155
+ const BrowserReadInput = z
156
+ .object({
157
+ browserSessionId: z.string().uuid(),
158
+ targetId: z.string().min(1).max(512),
159
+ mode: z.enum(["matches", "count", "subtree", "dom"]).optional(),
160
+ dom: z
161
+ .discriminatedUnion("kind", [
162
+ z
163
+ .object({
164
+ kind: z.literal("element"),
165
+ locator: BrowserDomReadLocator,
166
+ attributes: z.array(BrowserDomSafeAttribute).max(6).optional(),
167
+ maxChars: z.number().int().min(1).max(4_096).optional(),
168
+ })
169
+ .strict(),
170
+ z
171
+ .object({
172
+ kind: z.literal("count"),
173
+ selector: BrowserDomReadSelector,
174
+ })
175
+ .strict(),
176
+ ])
177
+ .optional(),
178
+ expectedTargetGeneration: z.string().min(1).max(256).optional(),
179
+ expectedDocumentGeneration: z.string().min(1).max(256).nullable().optional(),
180
+ expectedFrameId: z.string().min(1).max(256).nullable().optional(),
181
+ ref: z.string().min(1).max(512).optional(),
182
+ scopeRef: z.string().min(1).max(512).optional(),
183
+ role: z.string().min(1).max(256).optional(),
184
+ nameContains: z.string().min(1).max(512).optional(),
185
+ textContains: z.string().min(1).max(512).optional(),
186
+ state: z.string().min(1).max(128).optional(),
187
+ action: z.string().min(1).max(128).optional(),
188
+ limit: z.number().int().min(1).max(BROWSER_READ_MAX_NODES).optional(),
189
+ })
190
+ .strict()
191
+ .superRefine((value, context) => {
192
+ if (value.mode === "subtree" && !value.ref) {
193
+ context.addIssue({ code: "custom", path: ["ref"], message: "subtree requires a ref" });
194
+ }
195
+ if ((value.mode === "dom") !== (value.dom !== undefined)) {
196
+ context.addIssue({
197
+ code: "custom",
198
+ path: ["dom"],
199
+ message: "dom query is required only with mode=dom",
200
+ });
201
+ }
202
+ if (
203
+ value.mode === "dom" &&
204
+ [
205
+ value.ref,
206
+ value.scopeRef,
207
+ value.role,
208
+ value.nameContains,
209
+ value.textContains,
210
+ value.state,
211
+ value.action,
212
+ value.limit,
213
+ ].some((field) => field !== undefined)
214
+ ) {
215
+ context.addIssue({
216
+ code: "custom",
217
+ path: ["mode"],
218
+ message: "accessibility filters cannot be combined with a DOM query",
219
+ });
220
+ }
221
+ if (
222
+ value.mode !== "dom" &&
223
+ [
224
+ value.expectedTargetGeneration,
225
+ value.expectedDocumentGeneration,
226
+ value.expectedFrameId,
227
+ ].some((field) => field !== undefined)
228
+ ) {
229
+ context.addIssue({
230
+ code: "custom",
231
+ path: ["mode"],
232
+ message: "DOM generation fences require mode=dom",
233
+ });
234
+ }
235
+ });
236
+ const BrowserReadOutput = z
237
+ .object({
238
+ browserSessionId: z.string().uuid(),
239
+ targetId: z.string(),
240
+ observationId: z.string(),
241
+ targetGeneration: z.string(),
242
+ documentGeneration: z.string().nullable(),
243
+ frameId: z.string().nullable(),
244
+ source: z.literal("accessibility"),
245
+ mode: z.enum(["matches", "count", "subtree"]),
246
+ scopeFound: z.boolean(),
247
+ nodes: z.array(BrowserAgentNode).max(BROWSER_READ_MAX_NODES),
248
+ totalMatches: z.number().int().nonnegative(),
249
+ omittedMatches: z.number().int().nonnegative(),
250
+ clippedFieldCount: z.number().int().nonnegative(),
251
+ maxNodeBytes: z.number().int().positive(),
252
+ })
253
+ .strict();
254
+ const BrowserDomAgentReadOutput = BrowserDomReadResponse.safeExtend({
255
+ source: z.literal("dom"),
256
+ });
257
+ const BrowserReadToolOutput = z.discriminatedUnion("source", [
258
+ BrowserReadOutput,
259
+ BrowserDomAgentReadOutput,
260
+ ]);
261
+
262
+ type BrowserSemanticNodeValue = z.infer<typeof InteractionSemanticNode>;
263
+ type FlatBrowserNode = {
264
+ node: BrowserSemanticNodeValue;
265
+ depth: number;
266
+ index: number;
267
+ withinScope: boolean;
268
+ };
269
+
77
270
  const TOOL_PERMISSION = {
78
271
  interaction_discover: "sessions:read",
79
272
  browser_open: "sessions:control",
80
273
  browser_tabs: "sessions:control",
81
274
  browser_observe: "sessions:read",
275
+ browser_read: "sessions:read",
276
+ browser_screenshot: "sessions:read",
82
277
  browser_act: "sessions:control",
83
278
  browser_clipboard: "sessions:read",
84
279
  browser_debug: "sessions:read",
@@ -183,6 +378,8 @@ const BrowserObserveInput = z
183
378
  .object({
184
379
  browserSessionId: z.string().uuid(),
185
380
  targetId: z.string().min(1).max(512),
381
+ view: z.enum(["compact", "full"]).optional(),
382
+ includeScreenshot: z.boolean().optional(),
186
383
  })
187
384
  .strict();
188
385
  const BrowserActInput = z
@@ -192,6 +389,7 @@ const BrowserActInput = z
192
389
  expectedTargetGeneration: z.string().min(1).max(256).optional(),
193
390
  expectedDocumentGeneration: z.string().min(1).max(256).nullable().optional(),
194
391
  expectedFrameId: z.string().min(1).max(256).nullable().optional(),
392
+ view: z.enum(["compact", "full", "none"]).optional(),
195
393
  action: z.union([BrowserAction, BrowserActionBatch]),
196
394
  })
197
395
  .strict();
@@ -375,13 +573,18 @@ const ComputerObserveInput = z
375
573
  targetId: z.string().min(1).max(512),
376
574
  })
377
575
  .strict();
576
+ const ComputerObserveOutput = ComputerObservation.extend({
577
+ capturedFrame: z
578
+ .object({ width: z.number().int().positive(), height: z.number().int().positive() })
579
+ .strict(),
580
+ }).strict();
378
581
  const ComputerClipboardInput = z.object({ computerSessionId: z.string().uuid() }).strict();
379
582
  const ComputerActInput = z
380
583
  .object({
381
584
  computerSessionId: z.string().uuid(),
382
585
  targetId: z.string().min(1).max(512),
383
586
  expectedTargetGeneration: z.string().min(1).max(256).optional(),
384
- expectedObservationId: z.string().uuid().nullable().optional(),
587
+ expectedObservationId: ComputerObservation.shape.observationId.nullable().optional(),
385
588
  expectedFrameId: z.string().min(1).max(256).nullable().optional(),
386
589
  action: ComputerAction,
387
590
  })
@@ -437,7 +640,10 @@ export function createInteractionAttemptToolDefinitions(
437
640
  codemodePath: options.codemodePath,
438
641
  title: options.title,
439
642
  description: options.description,
440
- inputSchema: jsonSchema(options.input),
643
+ inputSchema:
644
+ options.name === "browser_act"
645
+ ? browserActionInputJsonSchema(options.input)
646
+ : jsonSchema(options.input),
441
647
  outputSchema: jsonSchema(options.output),
442
648
  annotations: {
443
649
  title: options.title,
@@ -510,7 +716,7 @@ export function createInteractionAttemptToolDefinitions(
510
716
  codemodePath: ["interaction", "browser", "open"],
511
717
  title: "Open or reuse browser",
512
718
  description:
513
- "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 an attached Chrome profile by passing placement={kind:'attached_device',deviceId}. This does not infer or attach the user's existing Chrome: for requests about 'my browser', 'my tabs', or current Chrome, call interaction_discover first and select an actual attachedBrowsers device; if none exists, explain that the Chrome extension must be connected instead of silently creating a blank managed browser. 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. Returns exact session and tab state.",
719
+ "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 an attached Chrome profile by passing placement={kind:'attached_device',deviceId}. This does not infer or attach the user's existing Chrome: for requests about 'my browser', 'my tabs', or current Chrome, call interaction_discover first and select an actual attachedBrowsers device; if none exists, explain that the Chrome extension must be connected instead of silently creating a blank managed browser. 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. Returns exact session and tab state.",
514
720
  input: BrowserOpenInput,
515
721
  output: BrowserOpenOutput,
516
722
  readOnly: false,
@@ -524,7 +730,7 @@ export function createInteractionAttemptToolDefinitions(
524
730
  codemodePath: ["interaction", "browser", "tabs"],
525
731
  title: "Manage browser tabs",
526
732
  description:
527
- "List, open, select, or close tabs in one exact BrowserSession. Returns the authoritative complete tab list after the operation.",
733
+ "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. Returns the authoritative complete tab list after the operation.",
528
734
  input: BrowserTabsInput,
529
735
  output: BrowserTargetListResponse,
530
736
  readOnly: false,
@@ -556,17 +762,161 @@ export function createInteractionAttemptToolDefinitions(
556
762
  codemodePath: ["interaction", "browser", "observe"],
557
763
  title: "Observe browser tab",
558
764
  description:
559
- "Read one tab's current URL/title, causal generations, compact semantic accessibility tree, dialog, and diagnostic counts without taking control.",
765
+ "Read one tab's URL, title, viewport, causal generations, and a bounded accessibility view. Compact view is the default: agentView lists selected refs and reports omitted nodes and clipped fields; it is not a complete page tree. 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.",
560
766
  input: BrowserObserveInput,
561
- output: BrowserObservation,
767
+ output: BrowserAgentObservation,
562
768
  readOnly: true,
563
769
  idempotent: true,
564
- execute: async (value) =>
565
- await input.transport.observeBrowserTarget(
770
+ execute: async (value) => {
771
+ let observation = await input.transport.observeBrowserTarget(
566
772
  input.workspaceId,
567
773
  value.browserSessionId,
568
774
  value.targetId,
569
- ),
775
+ );
776
+ if (!value.includeScreenshot)
777
+ return projectBrowserObservation(observation, value.view ?? "compact");
778
+ const frame = await input.transport.captureBrowserTarget(
779
+ input.workspaceId,
780
+ value.browserSessionId,
781
+ value.targetId,
782
+ );
783
+ if (
784
+ observation.target.targetGeneration !== frame.targetGeneration ||
785
+ observation.target.documentGeneration !== frame.documentGeneration
786
+ ) {
787
+ observation = await input.transport.observeBrowserTarget(
788
+ input.workspaceId,
789
+ value.browserSessionId,
790
+ value.targetId,
791
+ );
792
+ }
793
+ if (
794
+ observation.target.targetGeneration !== frame.targetGeneration ||
795
+ observation.target.documentGeneration !== frame.documentGeneration
796
+ ) {
797
+ throw new Error("browser target changed while its visual observation was captured");
798
+ }
799
+ return new InteractionExecutionResult(
800
+ projectBrowserObservation(observation, value.view ?? "compact"),
801
+ [browserToolImageContent(frame.data, frame.mediaType)],
802
+ );
803
+ },
804
+ });
805
+
806
+ add({
807
+ name: "browser_read",
808
+ codemodePath: ["interaction", "browser", "read"],
809
+ title: "Read focused browser content",
810
+ description:
811
+ "Search, count, or read a subtree from one tab's accessibility snapshot; filter by ref, role, name, accessibility text, state, or action, and scopeRef. AX mode refreshes the full tree internally and cannot return DOM attributes or editable input values. Use mode=dom with a ref, CSS, test-id, or placeholder locator for bounded element text, editable value, or allowlisted attributes, or a CSS selector for count. Content-matching role/label/text locators and CSS attribute/pseudo selectors are rejected for DOM reads to prevent secret-value probing. DOM reads use exact causal fences and only fixed, read-only browser-side projection; callers cannot supply JavaScript. Ref DOM locators refresh accessibility; CSS/test-id/placeholder locators avoid that scan.",
812
+ input: BrowserReadInput,
813
+ output: BrowserReadToolOutput,
814
+ readOnly: true,
815
+ idempotent: true,
816
+ execute: async (value) => {
817
+ if (value.mode === "dom") {
818
+ const state =
819
+ value.expectedTargetGeneration !== undefined &&
820
+ value.expectedDocumentGeneration !== undefined &&
821
+ value.expectedFrameId !== undefined
822
+ ? null
823
+ : await input.transport.getBrowserTargetState(
824
+ input.workspaceId,
825
+ value.browserSessionId,
826
+ value.targetId,
827
+ );
828
+ const fences = {
829
+ expectedTargetGeneration: value.expectedTargetGeneration ?? state!.targetGeneration,
830
+ expectedDocumentGeneration:
831
+ value.expectedDocumentGeneration === undefined
832
+ ? state!.documentGeneration
833
+ : value.expectedDocumentGeneration,
834
+ expectedFrameId:
835
+ value.expectedFrameId === undefined ? state!.frameId : value.expectedFrameId,
836
+ };
837
+ const documentGeneration = fences.expectedDocumentGeneration;
838
+ const frameId = fences.expectedFrameId;
839
+ if (documentGeneration === null || frameId === null) {
840
+ throw new OpenGeniApiError(409, "browser target has no inspectable document", {
841
+ code: "document_unavailable",
842
+ retryable: true,
843
+ });
844
+ }
845
+ const inspectedFences = {
846
+ expectedTargetGeneration: fences.expectedTargetGeneration,
847
+ expectedDocumentGeneration: documentGeneration,
848
+ expectedFrameId: frameId,
849
+ };
850
+ const dom = value.dom!;
851
+ const request =
852
+ dom.kind === "element"
853
+ ? {
854
+ ...inspectedFences,
855
+ kind: "element" as const,
856
+ locator: dom.locator,
857
+ ...(dom.attributes ? { attributes: dom.attributes } : {}),
858
+ maxChars: dom.maxChars ?? 4_096,
859
+ }
860
+ : { ...inspectedFences, kind: "count" as const, selector: dom.selector };
861
+ return {
862
+ ...(await input.transport.readBrowserDom(
863
+ input.workspaceId,
864
+ value.browserSessionId,
865
+ value.targetId,
866
+ request,
867
+ )),
868
+ source: "dom" as const,
869
+ };
870
+ }
871
+ return readBrowserObservation(
872
+ await input.transport.observeBrowserTarget(
873
+ input.workspaceId,
874
+ value.browserSessionId,
875
+ value.targetId,
876
+ ),
877
+ value,
878
+ );
879
+ },
880
+ });
881
+
882
+ add({
883
+ name: "browser_screenshot",
884
+ codemodePath: ["interaction", "browser", "screenshot"],
885
+ title: "Capture browser screenshot",
886
+ description:
887
+ "Capture a current browser tab screenshot without reading its accessibility tree. The image is returned as tool image content, with small structured frame metadata. Set fullPage=true to capture the whole scrollable page when supported; large pages may take longer or exceed capture bounds. Set quality lower to reduce JPEG bytes if a large capture exceeds the Code Mode image limit.",
888
+ input: BrowserScreenshotInput,
889
+ output: BrowserScreenshotOutput,
890
+ readOnly: true,
891
+ idempotent: true,
892
+ execute: async (value) => {
893
+ const frame = await input.transport.captureBrowserTarget(
894
+ input.workspaceId,
895
+ value.browserSessionId,
896
+ value.targetId,
897
+ {},
898
+ { fullPage: value.fullPage ?? false, ...(value.quality ? { quality: value.quality } : {}) },
899
+ );
900
+ return new InteractionExecutionResult(
901
+ {
902
+ kind: "browser_screenshot" as const,
903
+ browserSessionId: frame.browserSessionId,
904
+ targetId: frame.targetId,
905
+ frameId: frame.frameId,
906
+ targetGeneration: frame.targetGeneration,
907
+ documentGeneration: frame.documentGeneration,
908
+ mediaType: frame.mediaType,
909
+ width: frame.width,
910
+ height: frame.height,
911
+ deviceScaleFactor: frame.deviceScaleFactor,
912
+ scrollX: frame.scrollX,
913
+ scrollY: frame.scrollY,
914
+ capturedAt: frame.capturedAt,
915
+ fullPage: value.fullPage ?? false,
916
+ },
917
+ [browserToolImageContent(frame.data, frame.mediaType)],
918
+ );
919
+ },
570
920
  });
571
921
 
572
922
  add({
@@ -574,29 +924,42 @@ export function createInteractionAttemptToolDefinitions(
574
924
  codemodePath: ["interaction", "browser", "act"],
575
925
  title: "Act in browser tab",
576
926
  description:
577
- "Perform one semantic-first browser action or bounded batch, including setting a managed browser's web permission for this tab's exact current top-level origin. Omit generation fences to use a fresh observation automatically; provide them to require exact previously observed state. Returns the durable receipt and changed observation.",
927
+ "Perform one semantic-first browser action or bounded 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. 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.",
578
928
  input: BrowserActInput,
579
- output: BrowserActionReceipt,
929
+ output: BrowserAgentActionReceipt,
580
930
  readOnly: false,
581
931
  idempotent: true,
582
932
  execute: async (value, context) => {
583
- const current = await input.transport.observeBrowserTarget(
933
+ const current =
934
+ value.expectedTargetGeneration !== undefined &&
935
+ value.expectedDocumentGeneration !== undefined &&
936
+ value.expectedFrameId !== undefined
937
+ ? null
938
+ : await input.transport.getBrowserTargetState(
939
+ input.workspaceId,
940
+ value.browserSessionId,
941
+ value.targetId,
942
+ );
943
+ const receipt = await input.transport.actInBrowser(
584
944
  input.workspaceId,
585
945
  value.browserSessionId,
586
- value.targetId,
946
+ {
947
+ operationId: context.operationId,
948
+ targetId: value.targetId,
949
+ expectedTargetGeneration: value.expectedTargetGeneration ?? current!.targetGeneration,
950
+ expectedDocumentGeneration:
951
+ value.expectedDocumentGeneration === undefined
952
+ ? current!.documentGeneration
953
+ : value.expectedDocumentGeneration,
954
+ expectedFrameId:
955
+ value.expectedFrameId === undefined ? current!.frameId : value.expectedFrameId,
956
+ ...(value.view === "none" ? { observationMode: "none" as const } : {}),
957
+ action: value.action,
958
+ },
587
959
  );
588
- return await input.transport.actInBrowser(input.workspaceId, value.browserSessionId, {
589
- operationId: context.operationId,
590
- targetId: value.targetId,
591
- expectedTargetGeneration: value.expectedTargetGeneration ?? current.target.targetGeneration,
592
- expectedDocumentGeneration:
593
- value.expectedDocumentGeneration === undefined
594
- ? current.target.documentGeneration
595
- : value.expectedDocumentGeneration,
596
- expectedFrameId:
597
- value.expectedFrameId === undefined ? current.frameId : value.expectedFrameId,
598
- action: value.action,
599
- });
960
+ return receipt.observation && (value.view ?? "compact") === "compact"
961
+ ? { ...receipt, observation: projectBrowserObservation(receipt.observation, "compact") }
962
+ : receipt;
600
963
  },
601
964
  });
602
965
 
@@ -931,9 +1294,9 @@ export function createInteractionAttemptToolDefinitions(
931
1294
  codemodePath: ["interaction", "computer", "observe"],
932
1295
  title: "Observe app or window",
933
1296
  description:
934
- "Read one ComputerSession app/window/screen target, causal generation, semantic accessibility tree, focus, frame identity, and a bounded current screenshot without taking control.",
1297
+ "Read one ComputerSession app/window/screen target, causal generation, semantic accessibility tree, focus, frame identity, and a bounded current screenshot without taking control. capturedFrame gives the screenshot's exact pixel dimensions; use those pixels for pointer coordinates, not the target's native bounds or an image-preview size.",
935
1298
  input: ComputerObserveInput,
936
- output: ComputerObservation,
1299
+ output: ComputerObserveOutput,
937
1300
  readOnly: true,
938
1301
  idempotent: true,
939
1302
  execute: async (value) => {
@@ -957,13 +1320,20 @@ export function createInteractionAttemptToolDefinitions(
957
1320
  if (observation.target.targetGeneration !== frame.targetGeneration) {
958
1321
  throw new Error("computer target changed while its visual observation was captured");
959
1322
  }
960
- return new InteractionExecutionResult({ ...observation, frameId: frame.frameId }, [
1323
+ return new InteractionExecutionResult(
961
1324
  {
962
- type: "image",
963
- data: Buffer.from(frame.data).toString("base64"),
964
- mimeType: frame.mediaType,
1325
+ ...observation,
1326
+ frameId: frame.frameId,
1327
+ capturedFrame: { width: frame.width, height: frame.height },
965
1328
  },
966
- ]);
1329
+ [
1330
+ {
1331
+ type: "image",
1332
+ data: Buffer.from(frame.data).toString("base64"),
1333
+ mimeType: frame.mediaType,
1334
+ },
1335
+ ],
1336
+ );
967
1337
  },
968
1338
  });
969
1339
 
@@ -986,31 +1356,38 @@ export function createInteractionAttemptToolDefinitions(
986
1356
  codemodePath: ["interaction", "computer", "act"],
987
1357
  title: "Act in app or window",
988
1358
  description:
989
- "Perform one action in an exact ComputerSession target. Prefer semantic actions from computer_observe: on macOS they can invoke controls and set values without foregrounding the app. Pointer, keyboard, target focus, screen actions, and clipboard paste use the physical graphical seat and may change the user's foreground app; use them only when foreground control is explicitly intended. Omit fences to use a fresh observation automatically. Returns the durable causal receipt.",
1359
+ "Perform one action in an exact ComputerSession target. Prefer semantic actions from computer_observe: on macOS they can invoke controls and set values without foregrounding the app. Pointer, keyboard, target focus, screen actions, and clipboard paste use the physical graphical seat and may change the user's foreground app; use them only when foreground control is explicitly intended. For pointer actions, set action.frameId from the observed frame and use coordinates in the returned screenshot's pixels; do not scale them to target.bounds. A pixel frame fence applies only to pointer actions; semantic actions use the observation fence even if a frame ID is supplied. Supplying the target generation and relevant action fence preserves the exact observation without refreshing it. Omit fences to use a fresh observation automatically. Returns the durable causal receipt.",
990
1360
  input: ComputerActInput,
991
1361
  output: ComputerActionReceipt,
992
1362
  readOnly: false,
993
1363
  idempotent: true,
994
1364
  execute: async (value, context) => {
995
- const current = await input.transport.observeComputerTarget(
996
- input.workspaceId,
997
- value.computerSessionId,
998
- value.targetId,
999
- );
1365
+ const current =
1366
+ value.expectedTargetGeneration !== undefined &&
1367
+ (value.action.type === "pointer"
1368
+ ? (value.expectedFrameId ?? value.action.frameId) != null
1369
+ : value.expectedObservationId != null)
1370
+ ? null
1371
+ : await input.transport.observeComputerTarget(
1372
+ input.workspaceId,
1373
+ value.computerSessionId,
1374
+ value.targetId,
1375
+ );
1000
1376
  return await input.transport.actInComputer(input.workspaceId, value.computerSessionId, {
1001
1377
  operationId: context.operationId,
1002
1378
  targetId: value.targetId,
1003
- expectedTargetGeneration: value.expectedTargetGeneration ?? current.target.targetGeneration,
1379
+ expectedTargetGeneration:
1380
+ value.expectedTargetGeneration ?? current!.target.targetGeneration,
1004
1381
  expectedObservationId:
1005
1382
  value.expectedObservationId === undefined
1006
- ? current.observationId
1383
+ ? (current?.observationId ?? null)
1007
1384
  : value.expectedObservationId,
1008
1385
  expectedFrameId:
1009
- value.expectedFrameId === undefined
1010
- ? value.action.type === "pointer"
1386
+ value.action.type === "pointer"
1387
+ ? value.expectedFrameId === undefined
1011
1388
  ? value.action.frameId
1012
- : null
1013
- : value.expectedFrameId,
1389
+ : value.expectedFrameId
1390
+ : null,
1014
1391
  action: value.action,
1015
1392
  });
1016
1393
  },
@@ -1131,7 +1508,8 @@ async function openBrowser(
1131
1508
  if (value.browserSessionId) {
1132
1509
  session = await transport.getBrowserSession(workspaceId, value.browserSessionId);
1133
1510
  } else {
1134
- const listed = await transport.listBrowserSessions(workspaceId);
1511
+ const listed =
1512
+ value.mode === "new" ? { sessions: [] } : await transport.listBrowserSessions(workspaceId);
1135
1513
  // The agent-facing browser is human-capable by default. A headless session
1136
1514
  // is a deliberately narrower execution mode and must never be silently
1137
1515
  // reused for an omitted/default headed request: OAuth providers such as
@@ -1141,7 +1519,18 @@ async function openBrowser(
1141
1519
  value.mode === "new"
1142
1520
  ? null
1143
1521
  : newestRelevant(
1144
- listed.sessions.filter((candidate) => candidate.headless === requestedHeadless),
1522
+ listed.sessions.filter(
1523
+ (candidate) =>
1524
+ candidate.headless === requestedHeadless &&
1525
+ compatibleInteractionPlacement(candidate.placement, value.placement) &&
1526
+ (value.identityId === undefined || candidate.identityId === value.identityId) &&
1527
+ (value.baseRevisionId === undefined ||
1528
+ candidate.baseRevisionId === value.baseRevisionId) &&
1529
+ (value.networkRouteId === undefined ||
1530
+ candidate.networkRouteId === value.networkRouteId) &&
1531
+ (value.linkedComputerSessionId === undefined ||
1532
+ candidate.linkedComputerSessionId === value.linkedComputerSessionId),
1533
+ ),
1145
1534
  sourceSessionId,
1146
1535
  );
1147
1536
  if (reusable) {
@@ -1205,8 +1594,14 @@ async function openComputer(
1205
1594
  if (value.computerSessionId) {
1206
1595
  session = await transport.getComputerSession(workspaceId, value.computerSessionId);
1207
1596
  } else {
1208
- const listed = await transport.listComputerSessions(workspaceId);
1209
- const reusable = value.mode === "new" ? null : newestRelevant(listed.sessions, sourceSessionId);
1597
+ const listed =
1598
+ value.mode === "new" ? { sessions: [] } : await transport.listComputerSessions(workspaceId);
1599
+ const reusable = newestRelevant(
1600
+ listed.sessions.filter((candidate) =>
1601
+ compatibleInteractionPlacement(candidate.placement, value.placement),
1602
+ ),
1603
+ sourceSessionId,
1604
+ );
1210
1605
  session = reusable
1211
1606
  ? reusable
1212
1607
  : (
@@ -1227,6 +1622,16 @@ async function openComputer(
1227
1622
  };
1228
1623
  }
1229
1624
 
1625
+ function compatibleInteractionPlacement(
1626
+ candidate: z.infer<typeof InteractionPlacement>,
1627
+ requested: z.infer<typeof InteractionPlacement> | undefined,
1628
+ ): boolean {
1629
+ if (!requested) return candidate.kind !== "attached_device";
1630
+ return Object.entries(requested).every(
1631
+ ([key, value]) => (candidate as Record<string, unknown>)[key] === value,
1632
+ );
1633
+ }
1634
+
1230
1635
  function newestRelevant<
1231
1636
  T extends {
1232
1637
  lifecycle: string;
@@ -1270,6 +1675,195 @@ function assertInterventionResumeMatches(
1270
1675
  }
1271
1676
  }
1272
1677
 
1678
+ function flattenBrowserNodes(
1679
+ roots: readonly BrowserSemanticNodeValue[],
1680
+ scopeRef?: string,
1681
+ ): FlatBrowserNode[] {
1682
+ const flattened: FlatBrowserNode[] = [];
1683
+ const stack = [...roots].reverse().map((node) => ({ node, depth: 0, withinScope: !scopeRef }));
1684
+ while (stack.length > 0) {
1685
+ const next = stack.pop()!;
1686
+ const withinScope = next.withinScope || next.node.ref === scopeRef;
1687
+ flattened.push({
1688
+ node: next.node,
1689
+ depth: next.depth,
1690
+ index: flattened.length,
1691
+ withinScope,
1692
+ });
1693
+ for (const child of [...(next.node.children ?? [])].reverse()) {
1694
+ stack.push({ node: child, depth: next.depth + 1, withinScope });
1695
+ }
1696
+ }
1697
+ return flattened;
1698
+ }
1699
+
1700
+ function boundedAgentField(value: string, onClip: () => void): string {
1701
+ if (value.length <= BROWSER_AGENT_MAX_FIELD_LENGTH) return value;
1702
+ onClip();
1703
+ return `${value.slice(0, BROWSER_AGENT_MAX_FIELD_LENGTH - 1)}…`;
1704
+ }
1705
+
1706
+ function projectAgentNode(flat: FlatBrowserNode): {
1707
+ node: z.infer<typeof BrowserAgentNode>;
1708
+ clippedFieldCount: number;
1709
+ } {
1710
+ let clippedFieldCount = 0;
1711
+ const clip = () => {
1712
+ clippedFieldCount += 1;
1713
+ };
1714
+ const source = flat.node;
1715
+ const nodes = {
1716
+ ref: source.ref,
1717
+ role: source.role,
1718
+ depth: flat.depth,
1719
+ ...(source.name !== undefined ? { name: boundedAgentField(source.name, clip) } : {}),
1720
+ ...(source.description !== undefined
1721
+ ? { description: boundedAgentField(source.description, clip) }
1722
+ : {}),
1723
+ ...(source.value !== undefined
1724
+ ? {
1725
+ value:
1726
+ typeof source.value === "string" ? boundedAgentField(source.value, clip) : source.value,
1727
+ }
1728
+ : {}),
1729
+ states: source.states.slice(0, 6).map((state) => boundedAgentField(state, clip)),
1730
+ actions: source.actions.slice(0, 6).map((action) => boundedAgentField(action, clip)),
1731
+ };
1732
+ if (source.states.length > 6) clippedFieldCount += source.states.length - 6;
1733
+ if (source.actions.length > 6) clippedFieldCount += source.actions.length - 6;
1734
+ return { node: nodes, clippedFieldCount };
1735
+ }
1736
+
1737
+ function selectAgentNodes(
1738
+ flattened: readonly FlatBrowserNode[],
1739
+ maxNodes: number,
1740
+ maxBytes: number,
1741
+ prioritize = true,
1742
+ ): { nodes: z.infer<typeof BrowserAgentNode>[]; clippedFieldCount: number } {
1743
+ const priority = (flat: FlatBrowserNode): number => {
1744
+ const { node } = flat;
1745
+ if (node.actions.length > 0) return 0;
1746
+ if (["heading", "dialog", "alert", "status", "navigation", "main"].includes(node.role))
1747
+ return 1;
1748
+ if (["text", "paragraph", "listitem"].includes(node.role) && node.name) return 2;
1749
+ if (node.name || node.value || node.description) return 3;
1750
+ return 4;
1751
+ };
1752
+ const candidates = prioritize
1753
+ ? [...flattened].sort((a, b) => priority(a) - priority(b) || a.index - b.index)
1754
+ : flattened;
1755
+ const selected: Array<{ index: number; node: z.infer<typeof BrowserAgentNode> }> = [];
1756
+ let usedBytes = 0;
1757
+ let clippedFieldCount = 0;
1758
+ for (const flat of candidates) {
1759
+ if (selected.length >= maxNodes) break;
1760
+ const projected = projectAgentNode(flat);
1761
+ const bytes = Buffer.byteLength(JSON.stringify(projected.node), "utf8");
1762
+ if (usedBytes + bytes > maxBytes) continue;
1763
+ selected.push({ index: flat.index, node: projected.node });
1764
+ usedBytes += bytes;
1765
+ clippedFieldCount += projected.clippedFieldCount;
1766
+ }
1767
+ return {
1768
+ nodes: selected.sort((a, b) => a.index - b.index).map((entry) => entry.node),
1769
+ clippedFieldCount,
1770
+ };
1771
+ }
1772
+
1773
+ function projectBrowserObservation(
1774
+ observation: z.infer<typeof BrowserObservation>,
1775
+ view: "compact" | "full",
1776
+ ): z.infer<typeof BrowserAgentObservation> {
1777
+ if (view === "full") return observation;
1778
+ const semantic = observation.semantic;
1779
+ const roots =
1780
+ semantic?.kind === "snapshot"
1781
+ ? semantic.roots
1782
+ : semantic?.kind === "diff"
1783
+ ? semantic.changed
1784
+ : [];
1785
+ const flattened = flattenBrowserNodes(roots);
1786
+ const projected = selectAgentNodes(
1787
+ flattened,
1788
+ BROWSER_AGENT_MAX_NODES,
1789
+ BROWSER_AGENT_MAX_NODE_BYTES,
1790
+ );
1791
+ return {
1792
+ ...observation,
1793
+ semantic: null,
1794
+ agentView: {
1795
+ kind: "compact",
1796
+ sourceKind: semantic?.kind ?? "none",
1797
+ nodes: projected.nodes,
1798
+ sourceNodeCount: flattened.length,
1799
+ omittedNodeCount: flattened.length - projected.nodes.length,
1800
+ removedRefCount: semantic?.kind === "diff" ? semantic.removedRefs.length : 0,
1801
+ clippedFieldCount: projected.clippedFieldCount,
1802
+ maxNodes: BROWSER_AGENT_MAX_NODES,
1803
+ maxNodeBytes: BROWSER_AGENT_MAX_NODE_BYTES,
1804
+ },
1805
+ };
1806
+ }
1807
+
1808
+ function readBrowserObservation(
1809
+ observation: z.infer<typeof BrowserObservation>,
1810
+ input: z.output<typeof BrowserReadInput>,
1811
+ ): z.infer<typeof BrowserReadOutput> {
1812
+ if (observation.semantic?.kind !== "snapshot") {
1813
+ throw new Error("browser focused read requires a complete accessibility snapshot");
1814
+ }
1815
+ const mode = input.mode && input.mode !== "dom" ? input.mode : "matches";
1816
+ const scopeRef = mode === "subtree" ? input.ref : input.scopeRef;
1817
+ const flattened = flattenBrowserNodes(observation.semantic.roots, scopeRef);
1818
+ const scopeFound = !scopeRef || flattened.some((entry) => entry.node.ref === scopeRef);
1819
+ const includes = (source: string | undefined, needle: string | undefined): boolean =>
1820
+ needle === undefined || (source ?? "").toLowerCase().includes(needle.toLowerCase());
1821
+ const matches = flattened.filter(({ node, withinScope }) => {
1822
+ if (!withinScope) return false;
1823
+ if (mode !== "subtree" && input.ref && node.ref !== input.ref) return false;
1824
+ if (input.role && node.role.toLowerCase() !== input.role.toLowerCase()) return false;
1825
+ if (!includes(node.name, input.nameContains)) return false;
1826
+ if (
1827
+ input.textContains &&
1828
+ ![node.name, node.description, typeof node.value === "string" ? node.value : undefined].some(
1829
+ (candidate) => includes(candidate, input.textContains),
1830
+ )
1831
+ )
1832
+ return false;
1833
+ if (
1834
+ input.state &&
1835
+ !node.states.some((state) => state.toLowerCase() === input.state!.toLowerCase())
1836
+ )
1837
+ return false;
1838
+ if (
1839
+ input.action &&
1840
+ !node.actions.some((action) => action.toLowerCase() === input.action!.toLowerCase())
1841
+ )
1842
+ return false;
1843
+ return true;
1844
+ });
1845
+ const projected =
1846
+ mode === "count"
1847
+ ? { nodes: [] as z.infer<typeof BrowserAgentNode>[], clippedFieldCount: 0 }
1848
+ : selectAgentNodes(matches, input.limit ?? 40, BROWSER_AGENT_MAX_NODE_BYTES, false);
1849
+ return {
1850
+ browserSessionId: observation.browserSessionId,
1851
+ targetId: observation.target.id,
1852
+ observationId: observation.observationId,
1853
+ targetGeneration: observation.target.targetGeneration,
1854
+ documentGeneration: observation.target.documentGeneration,
1855
+ frameId: observation.frameId,
1856
+ source: "accessibility",
1857
+ mode,
1858
+ scopeFound,
1859
+ nodes: projected.nodes,
1860
+ totalMatches: matches.length,
1861
+ omittedMatches: matches.length - projected.nodes.length,
1862
+ clippedFieldCount: projected.clippedFieldCount,
1863
+ maxNodeBytes: BROWSER_AGENT_MAX_NODE_BYTES,
1864
+ };
1865
+ }
1866
+
1273
1867
  async function safeInteractionExecution<TInput extends z.ZodType, TOutput extends z.ZodType>(
1274
1868
  inputSchema: TInput,
1275
1869
  outputSchema: TOutput,
@@ -1311,6 +1905,20 @@ async function safeInteractionExecution<TInput extends z.ZodType, TOutput extend
1311
1905
  }
1312
1906
  }
1313
1907
 
1908
+ function browserToolImageContent(
1909
+ data: Uint8Array,
1910
+ mimeType: string,
1911
+ ): AttemptToolResultValue["content"][number] {
1912
+ if (data.byteLength > BROWSER_TOOL_IMAGE_MAX_BYTES) {
1913
+ throw new OpenGeniApiError(
1914
+ 413,
1915
+ "Browser screenshot exceeds the Code Mode image limit; capture the viewport or lower JPEG quality.",
1916
+ { code: "browser_screenshot_too_large", retryable: false },
1917
+ );
1918
+ }
1919
+ return { type: "image", data: Buffer.from(data).toString("base64"), mimeType };
1920
+ }
1921
+
1314
1922
  function interactionErrorResult(
1315
1923
  code: string,
1316
1924
  message: string,