@opengeni/runtime 3.0.2-canary.38894290271001 → 3.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/codemode-client.json +1 -1
- package/dist/{chunk-EQQ3YOKR.js → chunk-7OUG7WBV.js} +165 -121
- package/dist/chunk-7OUG7WBV.js.map +1 -0
- package/dist/{chunk-JDNBDSQV.js → chunk-TE3JIUL6.js} +330 -76
- package/dist/chunk-TE3JIUL6.js.map +1 -0
- package/dist/context-compaction.d.ts +6 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -2
- package/dist/interaction-tools.d.ts +11 -1
- package/dist/model-call.js +1 -1
- package/dist/model-image-sizing.d.ts +10 -4
- package/dist/workspace-tool-gateway.js +2 -2
- package/package.json +14 -14
- package/src/bundled_site_skills/opengeni-sites/SKILL.md +41 -0
- package/src/codex-tool-search.ts +48 -0
- package/src/context-compaction.ts +51 -31
- package/src/history-sanitizer.ts +3 -0
- package/src/index.ts +26 -16
- package/src/interaction-tools.ts +478 -68
- package/src/model-image-sizing.ts +30 -5
- package/dist/chunk-EQQ3YOKR.js.map +0 -1
- package/dist/chunk-JDNBDSQV.js.map +0 -1
package/src/interaction-tools.ts
CHANGED
|
@@ -7,8 +7,7 @@ import {
|
|
|
7
7
|
} from "@opengeni/contracts";
|
|
8
8
|
import { boundComputerNativeReceipt } from "@opengeni/interaction";
|
|
9
9
|
import {
|
|
10
|
-
|
|
11
|
-
AttachedBrowserDevice,
|
|
10
|
+
AttachedBrowserDeviceListResponse,
|
|
12
11
|
AuthRun,
|
|
13
12
|
AuthRunListResponse,
|
|
14
13
|
AuthRunMutationResponse,
|
|
@@ -29,6 +28,13 @@ import {
|
|
|
29
28
|
BrowserIdentityListResponse,
|
|
30
29
|
BrowserIdentityMutationResponse,
|
|
31
30
|
BrowserObservation,
|
|
31
|
+
BrowserActionResolvedTarget,
|
|
32
|
+
BrowserDialog,
|
|
33
|
+
InteractionDiagnosticSummary,
|
|
34
|
+
InteractionError,
|
|
35
|
+
InteractionOperationState,
|
|
36
|
+
InteractionRect,
|
|
37
|
+
RetainedArtifactReferenceSchema,
|
|
32
38
|
InteractionSemanticNode,
|
|
33
39
|
BrowserRevisionListResponse,
|
|
34
40
|
BrowserSession,
|
|
@@ -119,31 +125,70 @@ const BrowserAgentNode = z
|
|
|
119
125
|
z.object({ redacted: z.literal(true), reason: z.string().max(32) }).strict(),
|
|
120
126
|
])
|
|
121
127
|
.optional(),
|
|
122
|
-
|
|
123
|
-
|
|
128
|
+
// Omitted when empty: most nodes have no states or actions, and repeating
|
|
129
|
+
// empty arrays on every node only costs the model tokens.
|
|
130
|
+
states: z.array(z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH)).min(1).max(6).optional(),
|
|
131
|
+
actions: z.array(z.string().max(BROWSER_AGENT_MAX_FIELD_LENGTH)).min(1).max(6).optional(),
|
|
124
132
|
})
|
|
125
133
|
.strict();
|
|
134
|
+
/** The model-facing view keeps only what a next call can use: what is on the
|
|
135
|
+
* page, the refs to act on, the fences an action may pass, and explicit budget
|
|
136
|
+
* omissions. Zero counts and constant limits are left out. */
|
|
126
137
|
const BrowserAgentView = z
|
|
127
138
|
.object({
|
|
128
|
-
kind: z.literal("compact"),
|
|
129
139
|
sourceKind: z.enum(["snapshot", "diff", "none"]),
|
|
130
140
|
nodes: z.array(BrowserAgentNode).max(BROWSER_AGENT_MAX_NODES),
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
141
|
+
omittedNodeCount: z.number().int().positive().optional(),
|
|
142
|
+
removedRefCount: z.number().int().positive().optional(),
|
|
143
|
+
clippedFieldCount: z.number().int().positive().optional(),
|
|
144
|
+
})
|
|
145
|
+
.strict();
|
|
146
|
+
/** A tab as the model needs it: identity, page, selection and the generations
|
|
147
|
+
* an action or human request may pass as fences. Timestamps stay out. */
|
|
148
|
+
const AgentBrowserTarget = z
|
|
149
|
+
.object({
|
|
150
|
+
id: z.string().max(512),
|
|
151
|
+
kind: z.enum(["page", "popup", "background_page", "worker"]),
|
|
152
|
+
title: z.string().max(4_096),
|
|
153
|
+
url: z.string().max(16_384),
|
|
154
|
+
selected: z.boolean(),
|
|
155
|
+
controllerGeneration: z.string().max(256),
|
|
156
|
+
targetGeneration: z.string().max(256),
|
|
157
|
+
documentGeneration: z.string().max(256).nullable(),
|
|
158
|
+
})
|
|
159
|
+
.strict();
|
|
160
|
+
const CompactBrowserObservation = z
|
|
161
|
+
.object({
|
|
162
|
+
target: AgentBrowserTarget,
|
|
163
|
+
frameId: z.string().max(256).optional(),
|
|
164
|
+
viewport: z
|
|
165
|
+
.object({
|
|
166
|
+
width: z.number().int().positive(),
|
|
167
|
+
height: z.number().int().positive(),
|
|
168
|
+
deviceScaleFactor: z.number().positive().optional(),
|
|
169
|
+
maxTouchPoints: z.number().int().positive().optional(),
|
|
170
|
+
})
|
|
171
|
+
.strict()
|
|
172
|
+
.optional(),
|
|
173
|
+
focusedRef: z.string().max(512).optional(),
|
|
174
|
+
changedRegions: z.array(InteractionRect).min(1).optional(),
|
|
175
|
+
diagnostics: InteractionDiagnosticSummary.optional(),
|
|
176
|
+
dialog: BrowserDialog.optional(),
|
|
177
|
+
screenshot: RetainedArtifactReferenceSchema.optional(),
|
|
178
|
+
agentView: BrowserAgentView,
|
|
137
179
|
})
|
|
138
180
|
.strict();
|
|
139
|
-
const CompactBrowserObservation = BrowserObservation.safeExtend({
|
|
140
|
-
semantic: z.null(),
|
|
141
|
-
agentView: BrowserAgentView,
|
|
142
|
-
});
|
|
143
181
|
const BrowserAgentObservation = z.union([BrowserObservation, CompactBrowserObservation]);
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
|
|
182
|
+
const CompactBrowserActionReceipt = z
|
|
183
|
+
.object({
|
|
184
|
+
targetId: z.string().max(512),
|
|
185
|
+
state: InteractionOperationState,
|
|
186
|
+
error: InteractionError.optional(),
|
|
187
|
+
targets: z.array(BrowserActionResolvedTarget).optional(),
|
|
188
|
+
observation: CompactBrowserObservation.optional(),
|
|
189
|
+
})
|
|
190
|
+
.strict();
|
|
191
|
+
const BrowserAgentActionReceipt = z.union([BrowserActionReceipt, CompactBrowserActionReceipt]);
|
|
147
192
|
const BrowserScreenshotInput = z
|
|
148
193
|
.object({
|
|
149
194
|
browserSessionId: z.string().uuid(),
|
|
@@ -283,6 +328,7 @@ type FlatBrowserNode = {
|
|
|
283
328
|
depth: number;
|
|
284
329
|
index: number;
|
|
285
330
|
withinScope: boolean;
|
|
331
|
+
parentName?: string;
|
|
286
332
|
};
|
|
287
333
|
|
|
288
334
|
const TOOL_PERMISSION = {
|
|
@@ -318,6 +364,28 @@ const DiscoveryInput = z
|
|
|
318
364
|
includeDisconnectedDevices: z.boolean().optional(),
|
|
319
365
|
})
|
|
320
366
|
.strict();
|
|
367
|
+
/** An attached Chrome profile as the model needs it to choose one: identity,
|
|
368
|
+
* labels, state and any capability it lacks. Enrollment, generation and
|
|
369
|
+
* timestamp bookkeeping stay out. */
|
|
370
|
+
const AgentAttachedBrowser = z
|
|
371
|
+
.object({
|
|
372
|
+
id: z.string().uuid(),
|
|
373
|
+
name: z.string().max(200),
|
|
374
|
+
profileLabel: z.string().max(200).optional(),
|
|
375
|
+
browserName: z.string().max(100),
|
|
376
|
+
platform: z.enum(["linux", "macos", "windows"]),
|
|
377
|
+
state: z.enum(["connected", "disconnected"]),
|
|
378
|
+
tabCount: z.number().int().nonnegative(),
|
|
379
|
+
unsupported: z.array(z.string().max(64)).min(1).optional(),
|
|
380
|
+
})
|
|
381
|
+
.strict();
|
|
382
|
+
/** An online machine that can host the extension but has no attached profile. */
|
|
383
|
+
const AgentAttachedBrowserBridge = z
|
|
384
|
+
.object({
|
|
385
|
+
enrollmentId: z.string().uuid(),
|
|
386
|
+
connectedProfileCount: z.number().int().nonnegative(),
|
|
387
|
+
})
|
|
388
|
+
.strict();
|
|
321
389
|
const DiscoveryOutput = z
|
|
322
390
|
.object({
|
|
323
391
|
browserRevision: z.number().int().nonnegative(),
|
|
@@ -327,8 +395,23 @@ const DiscoveryOutput = z
|
|
|
327
395
|
browsers: z.array(BrowserSession),
|
|
328
396
|
computers: z.array(ComputerSession),
|
|
329
397
|
identities: z.array(BrowserIdentity),
|
|
330
|
-
attachedBrowserBridges: z.array(
|
|
331
|
-
attachedBrowsers: z.array(
|
|
398
|
+
attachedBrowserBridges: z.array(AgentAttachedBrowserBridge),
|
|
399
|
+
attachedBrowsers: z.array(AgentAttachedBrowser),
|
|
400
|
+
})
|
|
401
|
+
.strict();
|
|
402
|
+
|
|
403
|
+
/** browser_open may name the person's connected Chrome without a profile id;
|
|
404
|
+
* the tool then resolves it (see chooseAttachedProfile). */
|
|
405
|
+
const BrowserOpenPlacement = z.union([
|
|
406
|
+
InteractionPlacement,
|
|
407
|
+
z.object({ kind: z.literal("attached_device") }).strict(),
|
|
408
|
+
]);
|
|
409
|
+
/** The Chrome profile browser_open chose when the request named none. */
|
|
410
|
+
const AgentAttachedProfile = z
|
|
411
|
+
.object({
|
|
412
|
+
id: z.string().uuid(),
|
|
413
|
+
name: z.string(),
|
|
414
|
+
profileLabel: z.string().optional(),
|
|
332
415
|
})
|
|
333
416
|
.strict();
|
|
334
417
|
|
|
@@ -340,7 +423,7 @@ const BrowserOpenInput = z
|
|
|
340
423
|
initialUrl: z.string().url().max(16_384).optional(),
|
|
341
424
|
headless: z.boolean().optional(),
|
|
342
425
|
storageMode: BrowserStorageMode.optional(),
|
|
343
|
-
placement:
|
|
426
|
+
placement: BrowserOpenPlacement.optional(),
|
|
344
427
|
identityId: z.string().uuid().optional(),
|
|
345
428
|
baseRevisionId: z.string().uuid().optional(),
|
|
346
429
|
networkRouteId: z.string().uuid().optional(),
|
|
@@ -384,7 +467,14 @@ const BrowserOpenInput = z
|
|
|
384
467
|
}
|
|
385
468
|
});
|
|
386
469
|
const BrowserOpenOutput = z
|
|
387
|
-
.object({
|
|
470
|
+
.object({
|
|
471
|
+
session: BrowserSession,
|
|
472
|
+
targets: z.array(AgentBrowserTarget),
|
|
473
|
+
otherTabCount: z.number().int().nonnegative().optional(),
|
|
474
|
+
observation: CompactBrowserObservation.optional(),
|
|
475
|
+
attachedProfile: AgentAttachedProfile.optional(),
|
|
476
|
+
otherAttachedProfiles: z.array(AgentAttachedProfile).optional(),
|
|
477
|
+
})
|
|
388
478
|
.strict();
|
|
389
479
|
|
|
390
480
|
const BrowserTabsInput = z.discriminatedUnion("operation", [
|
|
@@ -411,9 +501,13 @@ const BrowserTabsInput = z.discriminatedUnion("operation", [
|
|
|
411
501
|
})
|
|
412
502
|
.strict(),
|
|
413
503
|
]);
|
|
414
|
-
const BrowserTabsOutput =
|
|
415
|
-
|
|
416
|
-
|
|
504
|
+
const BrowserTabsOutput = z
|
|
505
|
+
.object({
|
|
506
|
+
browserSessionId: z.string().uuid(),
|
|
507
|
+
targets: z.array(AgentBrowserTarget),
|
|
508
|
+
mutationObservation: CompactBrowserObservation.optional(),
|
|
509
|
+
})
|
|
510
|
+
.strict();
|
|
417
511
|
|
|
418
512
|
const BrowserObserveInput = z
|
|
419
513
|
.object({
|
|
@@ -722,7 +816,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
722
816
|
name: "interaction_discover",
|
|
723
817
|
codemodePath: ["interaction", "discover"],
|
|
724
818
|
title: "Discover browsers and computers",
|
|
725
|
-
description: `Discover browsers and computers.
|
|
819
|
+
description: `Discover browsers and computers. To use the user's existing/current/personal Chrome, call browser_open with placement={kind:'attached_device'} directly; use scope=attached_browsers only to pick a specific Chrome profile or to see why none is connected. That scope reads only Chrome profiles and extension bridges, avoiding unrelated sessions and saved identities. The default current_session scope lists this agent session's BrowserSessions and ComputerSessions. Use scope=workspace only for reusable identities or peer/child resources; that inventory can be large. An attachedBrowserBridge only means the machine is ready for the extension; only attachedBrowsers are real user Chrome profiles/tabs. If none exist, the person needs a Connected Machine first (post the Connected Machine card with capability_authorization_request api:connected-machine) and then the Opengeni Browser Chrome extension: ${OPENGENI_BROWSER_EXTENSION_URL}. Leave includeTerminal=false unless ended history is specifically required.`,
|
|
726
820
|
input: DiscoveryInput,
|
|
727
821
|
output: DiscoveryOutput,
|
|
728
822
|
readOnly: true,
|
|
@@ -743,8 +837,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
743
837
|
browsers: [],
|
|
744
838
|
computers: [],
|
|
745
839
|
identities: [],
|
|
746
|
-
|
|
747
|
-
attachedBrowsers: attached.devices,
|
|
840
|
+
...agentAttachedInventory(attached),
|
|
748
841
|
};
|
|
749
842
|
}
|
|
750
843
|
// Narrow in SQL: a workspace can retain thousands of ended sessions, and
|
|
@@ -781,8 +874,9 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
781
874
|
value.includeTerminal ?? false,
|
|
782
875
|
),
|
|
783
876
|
identities: scope === "workspace" ? identities.identities : [],
|
|
784
|
-
|
|
785
|
-
|
|
877
|
+
...(scope === "workspace"
|
|
878
|
+
? agentAttachedInventory(attached)
|
|
879
|
+
: { attachedBrowserBridges: [], attachedBrowsers: [] }),
|
|
786
880
|
};
|
|
787
881
|
},
|
|
788
882
|
});
|
|
@@ -791,7 +885,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
791
885
|
name: "browser_open",
|
|
792
886
|
codemodePath: ["interaction", "browser", "open"],
|
|
793
887
|
title: "Open or reuse browser",
|
|
794
|
-
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
|
|
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.`,
|
|
795
889
|
input: BrowserOpenInput,
|
|
796
890
|
output: BrowserOpenOutput,
|
|
797
891
|
readOnly: false,
|
|
@@ -825,10 +919,12 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
825
919
|
value.targetId,
|
|
826
920
|
);
|
|
827
921
|
} else if (value.operation === "close") {
|
|
828
|
-
return
|
|
829
|
-
input.
|
|
830
|
-
|
|
831
|
-
|
|
922
|
+
return agentTargetList(
|
|
923
|
+
await input.transport.closeBrowserTarget(
|
|
924
|
+
input.workspaceId,
|
|
925
|
+
value.browserSessionId,
|
|
926
|
+
value.targetId,
|
|
927
|
+
),
|
|
832
928
|
);
|
|
833
929
|
}
|
|
834
930
|
const tabs = await input.transport.listBrowserTargets(
|
|
@@ -837,10 +933,10 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
837
933
|
);
|
|
838
934
|
return mutationObservation
|
|
839
935
|
? {
|
|
840
|
-
...tabs,
|
|
936
|
+
...agentTargetList(tabs),
|
|
841
937
|
mutationObservation: projectBrowserObservation(mutationObservation, "compact"),
|
|
842
938
|
}
|
|
843
|
-
: tabs;
|
|
939
|
+
: agentTargetList(tabs);
|
|
844
940
|
},
|
|
845
941
|
});
|
|
846
942
|
|
|
@@ -849,7 +945,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
849
945
|
codemodePath: ["interaction", "browser", "observe"],
|
|
850
946
|
title: "Observe browser tab",
|
|
851
947
|
description:
|
|
852
|
-
"Read one tab's URL, title, viewport, causal generations, and a bounded accessibility view. Compact view is the default: agentView lists selected refs and
|
|
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.",
|
|
853
949
|
input: BrowserObserveInput,
|
|
854
950
|
output: BrowserAgentObservation,
|
|
855
951
|
readOnly: true,
|
|
@@ -1022,7 +1118,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
1022
1118
|
codemodePath: ["interaction", "browser", "act"],
|
|
1023
1119
|
title: "Act in browser tab",
|
|
1024
1120
|
description:
|
|
1025
|
-
"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 tab open/select or a prior action;
|
|
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.",
|
|
1026
1122
|
input: BrowserActInput,
|
|
1027
1123
|
output: BrowserAgentActionReceipt,
|
|
1028
1124
|
readOnly: false,
|
|
@@ -1049,9 +1145,7 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
1049
1145
|
action: value.action,
|
|
1050
1146
|
},
|
|
1051
1147
|
);
|
|
1052
|
-
return
|
|
1053
|
-
? { ...receipt, observation: projectBrowserObservation(receipt.observation, "compact") }
|
|
1054
|
-
: receipt;
|
|
1148
|
+
return value.view === "full" ? receipt : compactActionReceipt(receipt);
|
|
1055
1149
|
},
|
|
1056
1150
|
});
|
|
1057
1151
|
|
|
@@ -1659,12 +1753,66 @@ export function createInteractionAttemptToolDefinitions(
|
|
|
1659
1753
|
return definitions;
|
|
1660
1754
|
}
|
|
1661
1755
|
|
|
1662
|
-
/**
|
|
1663
|
-
|
|
1756
|
+
/**
|
|
1757
|
+
* One top-level object schema for every platform variant of a native CUA tool.
|
|
1758
|
+
* Model providers require a plain object at the top level of a function schema
|
|
1759
|
+
* (a top-level anyOf is rejected or ignored, so the model loses every argument
|
|
1760
|
+
* type). Properties are the union across platforms; a property whose platform
|
|
1761
|
+
* schemas differ in more than wording keeps those alternatives at property
|
|
1762
|
+
* level. Only properties every platform requires stay required. The session's
|
|
1763
|
+
* CUA worker still validates arguments against its own exact platform schema.
|
|
1764
|
+
*/
|
|
1765
|
+
export function nativeCuaSchema(variants: Record<string, unknown>[]): AttemptToolJsonSchema {
|
|
1664
1766
|
const unique = [...new Map(variants.map((schema) => [JSON.stringify(schema), schema])).values()];
|
|
1665
|
-
return
|
|
1666
|
-
|
|
1667
|
-
)
|
|
1767
|
+
if (unique.length === 1) return unique[0] as AttemptToolJsonSchema;
|
|
1768
|
+
const properties: Record<string, unknown> = {};
|
|
1769
|
+
const names = [...new Set(unique.flatMap((schema) => Object.keys(objectProperties(schema))))];
|
|
1770
|
+
for (const name of names) {
|
|
1771
|
+
const alternatives = unique
|
|
1772
|
+
.map((schema) => objectProperties(schema)[name])
|
|
1773
|
+
.filter((value): value is Record<string, unknown> => isJsonObject(value));
|
|
1774
|
+
// The first platform's wording wins for constraint-identical schemas.
|
|
1775
|
+
const distinct = new Map<string, Record<string, unknown>>();
|
|
1776
|
+
for (const schema of alternatives) {
|
|
1777
|
+
const key = JSON.stringify(withoutWording(schema));
|
|
1778
|
+
if (!distinct.has(key)) distinct.set(key, schema);
|
|
1779
|
+
}
|
|
1780
|
+
const options = [...distinct.values()];
|
|
1781
|
+
properties[name] = options.length === 1 ? options[0] : { anyOf: options };
|
|
1782
|
+
}
|
|
1783
|
+
const required = names.filter((name) =>
|
|
1784
|
+
unique.every((schema) =>
|
|
1785
|
+
Array.isArray(schema.required) ? schema.required.includes(name) : false,
|
|
1786
|
+
),
|
|
1787
|
+
);
|
|
1788
|
+
return {
|
|
1789
|
+
type: "object",
|
|
1790
|
+
properties,
|
|
1791
|
+
...(required.length > 0 ? { required } : {}),
|
|
1792
|
+
...(unique.every((schema) => schema.additionalProperties === false)
|
|
1793
|
+
? { additionalProperties: false }
|
|
1794
|
+
: {}),
|
|
1795
|
+
} as AttemptToolJsonSchema;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
function objectProperties(schema: Record<string, unknown>): Record<string, unknown> {
|
|
1799
|
+
return isJsonObject(schema.properties) ? schema.properties : {};
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function isJsonObject(value: unknown): value is Record<string, unknown> {
|
|
1803
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
/** Compare platform property schemas by constraint, not by explanatory text. */
|
|
1807
|
+
function withoutWording(value: unknown): unknown {
|
|
1808
|
+
if (Array.isArray(value)) return value.map(withoutWording);
|
|
1809
|
+
if (!isJsonObject(value)) return value;
|
|
1810
|
+
return Object.fromEntries(
|
|
1811
|
+
Object.entries(value)
|
|
1812
|
+
.filter(([key]) => key !== "description" && key !== "title" && key !== "default")
|
|
1813
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
1814
|
+
.map(([key, entry]) => [key, withoutWording(entry)]),
|
|
1815
|
+
);
|
|
1668
1816
|
}
|
|
1669
1817
|
|
|
1670
1818
|
function filterDiscoveredSessions<
|
|
@@ -1764,18 +1912,28 @@ async function openBrowser(
|
|
|
1764
1912
|
): Promise<z.input<typeof BrowserOpenOutput>> {
|
|
1765
1913
|
let session: z.infer<typeof BrowserSession>;
|
|
1766
1914
|
let created = false;
|
|
1915
|
+
let profileChoice: AttachedProfileChoice | undefined;
|
|
1767
1916
|
if (value.browserSessionId) {
|
|
1768
1917
|
session = await transport.getBrowserSession(workspaceId, value.browserSessionId);
|
|
1769
1918
|
if (value.storageMode && browserSessionStorageMode(session) !== value.storageMode)
|
|
1770
1919
|
throw new Error("existing BrowserSession storage mode does not match request");
|
|
1771
1920
|
} else {
|
|
1772
|
-
|
|
1921
|
+
// placement={kind:'attached_device'} without a deviceId means "the person's
|
|
1922
|
+
// own Chrome". Read the profile inventory alongside the reuse lookup so the
|
|
1923
|
+
// resolution adds no sequential round trip.
|
|
1924
|
+
const resolveProfile =
|
|
1925
|
+
value.placement?.kind === "attached_device" && !("deviceId" in value.placement);
|
|
1926
|
+
const [listed, attachedInventory] = await Promise.all([
|
|
1773
1927
|
value.mode === "new"
|
|
1774
1928
|
? { sessions: [] }
|
|
1775
|
-
:
|
|
1929
|
+
: transport.listBrowserSessions(workspaceId, {
|
|
1776
1930
|
associatedSessionId: sourceSessionId,
|
|
1777
1931
|
includeTerminal: false,
|
|
1778
|
-
})
|
|
1932
|
+
}),
|
|
1933
|
+
resolveProfile
|
|
1934
|
+
? transport.listAttachedBrowsers(workspaceId, { includeDisconnected: false })
|
|
1935
|
+
: null,
|
|
1936
|
+
]);
|
|
1779
1937
|
// The agent-facing browser is human-capable by default. A headless session
|
|
1780
1938
|
// is a deliberately narrower execution mode and must never be silently
|
|
1781
1939
|
// reused for an omitted/default headed request: OAuth providers such as
|
|
@@ -1804,6 +1962,11 @@ async function openBrowser(
|
|
|
1804
1962
|
session = reusable;
|
|
1805
1963
|
} else {
|
|
1806
1964
|
created = true;
|
|
1965
|
+
if (attachedInventory) profileChoice = chooseAttachedProfile(attachedInventory);
|
|
1966
|
+
// Without a deviceId, chooseAttachedProfile has resolved one (or thrown).
|
|
1967
|
+
const placement = profileChoice
|
|
1968
|
+
? { kind: "attached_device" as const, deviceId: profileChoice.chosen.id }
|
|
1969
|
+
: (value.placement as z.infer<typeof InteractionPlacement> | undefined);
|
|
1807
1970
|
session = (
|
|
1808
1971
|
await transport.createBrowserSession(workspaceId, {
|
|
1809
1972
|
operationId: context.operationId,
|
|
@@ -1812,7 +1975,7 @@ async function openBrowser(
|
|
|
1812
1975
|
...(value.initialUrl ? { initialUrl: value.initialUrl } : {}),
|
|
1813
1976
|
headless: requestedHeadless,
|
|
1814
1977
|
...(value.storageMode ? { storageMode: value.storageMode } : {}),
|
|
1815
|
-
...(
|
|
1978
|
+
...(placement ? { placement } : {}),
|
|
1816
1979
|
...(value.identityId ? { identityId: value.identityId } : {}),
|
|
1817
1980
|
...(value.baseRevisionId ? { baseRevisionId: value.baseRevisionId } : {}),
|
|
1818
1981
|
...(value.networkRouteId ? { networkRouteId: value.networkRouteId } : {}),
|
|
@@ -1830,7 +1993,15 @@ async function openBrowser(
|
|
|
1830
1993
|
})
|
|
1831
1994
|
).session;
|
|
1832
1995
|
}
|
|
1833
|
-
|
|
1996
|
+
const chosenProfile = profileChoice
|
|
1997
|
+
? {
|
|
1998
|
+
attachedProfile: agentAttachedProfile(profileChoice.chosen),
|
|
1999
|
+
...(profileChoice.others.length > 0
|
|
2000
|
+
? { otherAttachedProfiles: profileChoice.others.map(agentAttachedProfile) }
|
|
2001
|
+
: {}),
|
|
2002
|
+
}
|
|
2003
|
+
: {};
|
|
2004
|
+
if (session.lifecycle !== "active") return { session, targets: [], ...chosenProfile };
|
|
1834
2005
|
let targets = (await transport.listBrowserTargets(workspaceId, session.id)).targets;
|
|
1835
2006
|
if (
|
|
1836
2007
|
value.initialUrl &&
|
|
@@ -1859,7 +2030,101 @@ async function openBrowser(
|
|
|
1859
2030
|
}
|
|
1860
2031
|
targets = inventory.targets;
|
|
1861
2032
|
}
|
|
1862
|
-
|
|
2033
|
+
const primary = openedBrowserTarget(targets, value.initialUrl);
|
|
2034
|
+
const observation = primary
|
|
2035
|
+
? await observeOpenedBrowserTarget(transport, workspaceId, session.id, primary)
|
|
2036
|
+
: undefined;
|
|
2037
|
+
// A browser opened on a URL is about that tab. A person's own Chrome profile
|
|
2038
|
+
// can hold dozens of unrelated tabs; listing them all costs context on every
|
|
2039
|
+
// later step, so only the opened tab is returned with a count of the rest
|
|
2040
|
+
// (browser_tabs list shows them).
|
|
2041
|
+
const opened =
|
|
2042
|
+
value.initialUrl && primary && sameBrowserUrl(primary.url, value.initialUrl)
|
|
2043
|
+
? primary
|
|
2044
|
+
: undefined;
|
|
2045
|
+
const shown = opened && targets.length > 1 ? [opened] : targets;
|
|
2046
|
+
return {
|
|
2047
|
+
session,
|
|
2048
|
+
targets: shown.map(agentBrowserTarget),
|
|
2049
|
+
...(shown.length < targets.length ? { otherTabCount: targets.length - shown.length } : {}),
|
|
2050
|
+
...(observation ? { observation } : {}),
|
|
2051
|
+
...chosenProfile,
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
|
|
2055
|
+
type AttachedProfileChoice = {
|
|
2056
|
+
chosen: z.infer<typeof AttachedBrowserDeviceListResponse>["devices"][number];
|
|
2057
|
+
others: z.infer<typeof AttachedBrowserDeviceListResponse>["devices"];
|
|
2058
|
+
};
|
|
2059
|
+
|
|
2060
|
+
/** The person's own Chrome when browser_open names no profile: the only
|
|
2061
|
+
* connected profile, else the one whose tabs changed most recently (a device's
|
|
2062
|
+
* updatedAt moves with its tab inventory, not with heartbeats). */
|
|
2063
|
+
function chooseAttachedProfile(
|
|
2064
|
+
inventory: z.infer<typeof AttachedBrowserDeviceListResponse>,
|
|
2065
|
+
): AttachedProfileChoice {
|
|
2066
|
+
const usable = inventory.devices
|
|
2067
|
+
.filter((device) => device.state === "connected" && device.capabilities.debuggerAttachment)
|
|
2068
|
+
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
2069
|
+
const [chosen, ...others] = usable;
|
|
2070
|
+
if (!chosen) {
|
|
2071
|
+
throw new OpenGeniApiError(
|
|
2072
|
+
409,
|
|
2073
|
+
`No Chrome profile is connected through the Opengeni Browser extension. The person needs a Connected Machine, then the extension: ${OPENGENI_BROWSER_EXTENSION_URL}`,
|
|
2074
|
+
{ code: "attached_browser_unavailable", retryable: false },
|
|
2075
|
+
);
|
|
2076
|
+
}
|
|
2077
|
+
return { chosen, others };
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
function agentAttachedProfile(
|
|
2081
|
+
device: AttachedProfileChoice["chosen"],
|
|
2082
|
+
): z.infer<typeof AgentAttachedProfile> {
|
|
2083
|
+
return {
|
|
2084
|
+
id: device.id,
|
|
2085
|
+
name: device.name,
|
|
2086
|
+
...(device.profileLabel && device.profileLabel !== device.name
|
|
2087
|
+
? { profileLabel: device.profileLabel }
|
|
2088
|
+
: {}),
|
|
2089
|
+
};
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
/**
|
|
2093
|
+
* The tab the agent most likely acts on next: the tab showing the requested
|
|
2094
|
+
* URL, else the selected page.
|
|
2095
|
+
*/
|
|
2096
|
+
function openedBrowserTarget(
|
|
2097
|
+
targets: z.infer<typeof BrowserTarget>[],
|
|
2098
|
+
initialUrl: string | undefined,
|
|
2099
|
+
): z.infer<typeof BrowserTarget> | undefined {
|
|
2100
|
+
const pages = targets.filter((target) => target.kind === "page");
|
|
2101
|
+
return (
|
|
2102
|
+
(initialUrl ? pages.find((target) => sameBrowserUrl(target.url, initialUrl)) : undefined) ??
|
|
2103
|
+
pages.find((target) => target.selected) ??
|
|
2104
|
+
(pages.length === 1 ? pages[0] : undefined)
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
/**
|
|
2109
|
+
* A compact observation of the opened tab. Opening a browser is almost always
|
|
2110
|
+
* followed by reading it, so this saves that model round trip. It is a
|
|
2111
|
+
* read-only convenience: if it fails, the open still succeeds and the agent can
|
|
2112
|
+
* observe explicitly.
|
|
2113
|
+
*/
|
|
2114
|
+
async function observeOpenedBrowserTarget(
|
|
2115
|
+
transport: InteractionTransport,
|
|
2116
|
+
workspaceId: string,
|
|
2117
|
+
browserSessionId: string,
|
|
2118
|
+
primary: z.infer<typeof BrowserTarget>,
|
|
2119
|
+
): Promise<z.infer<typeof CompactBrowserObservation> | undefined> {
|
|
2120
|
+
try {
|
|
2121
|
+
return projectBrowserObservation(
|
|
2122
|
+
await transport.observeBrowserTarget(workspaceId, browserSessionId, primary.id),
|
|
2123
|
+
"compact",
|
|
2124
|
+
);
|
|
2125
|
+
} catch {
|
|
2126
|
+
return undefined;
|
|
2127
|
+
}
|
|
1863
2128
|
}
|
|
1864
2129
|
|
|
1865
2130
|
function sameBrowserUrl(left: string, right: string): boolean {
|
|
@@ -1916,7 +2181,7 @@ async function openComputer(
|
|
|
1916
2181
|
|
|
1917
2182
|
function compatibleInteractionPlacement(
|
|
1918
2183
|
candidate: z.infer<typeof InteractionPlacement>,
|
|
1919
|
-
requested: z.infer<typeof
|
|
2184
|
+
requested: z.infer<typeof BrowserOpenPlacement> | undefined,
|
|
1920
2185
|
): boolean {
|
|
1921
2186
|
if (!requested) return candidate.kind !== "attached_device";
|
|
1922
2187
|
return Object.entries(requested).every(
|
|
@@ -1972,7 +2237,12 @@ function flattenBrowserNodes(
|
|
|
1972
2237
|
scopeRef?: string,
|
|
1973
2238
|
): FlatBrowserNode[] {
|
|
1974
2239
|
const flattened: FlatBrowserNode[] = [];
|
|
1975
|
-
const stack
|
|
2240
|
+
const stack: Array<{
|
|
2241
|
+
node: BrowserSemanticNodeValue;
|
|
2242
|
+
depth: number;
|
|
2243
|
+
withinScope: boolean;
|
|
2244
|
+
parentName?: string;
|
|
2245
|
+
}> = [...roots].reverse().map((node) => ({ node, depth: 0, withinScope: !scopeRef }));
|
|
1976
2246
|
while (stack.length > 0) {
|
|
1977
2247
|
const next = stack.pop()!;
|
|
1978
2248
|
const withinScope = next.withinScope || next.node.ref === scopeRef;
|
|
@@ -1981,14 +2251,53 @@ function flattenBrowserNodes(
|
|
|
1981
2251
|
depth: next.depth,
|
|
1982
2252
|
index: flattened.length,
|
|
1983
2253
|
withinScope,
|
|
2254
|
+
...(next.parentName !== undefined ? { parentName: next.parentName } : {}),
|
|
1984
2255
|
});
|
|
1985
2256
|
for (const child of [...(next.node.children ?? [])].reverse()) {
|
|
1986
|
-
stack.push({
|
|
2257
|
+
stack.push({
|
|
2258
|
+
node: child,
|
|
2259
|
+
depth: next.depth + 1,
|
|
2260
|
+
withinScope,
|
|
2261
|
+
...(next.node.name !== undefined ? { parentName: next.node.name } : {}),
|
|
2262
|
+
});
|
|
1987
2263
|
}
|
|
1988
2264
|
}
|
|
1989
2265
|
return flattened;
|
|
1990
2266
|
}
|
|
1991
2267
|
|
|
2268
|
+
/** Container roles that help the model orient even without a name. */
|
|
2269
|
+
const BROWSER_AGENT_STRUCTURAL_ROLES = new Set([
|
|
2270
|
+
"dialog",
|
|
2271
|
+
"alertdialog",
|
|
2272
|
+
"alert",
|
|
2273
|
+
"status",
|
|
2274
|
+
"navigation",
|
|
2275
|
+
"main",
|
|
2276
|
+
"form",
|
|
2277
|
+
"table",
|
|
2278
|
+
"grid",
|
|
2279
|
+
"list",
|
|
2280
|
+
"listbox",
|
|
2281
|
+
"menu",
|
|
2282
|
+
"menubar",
|
|
2283
|
+
"tablist",
|
|
2284
|
+
"radiogroup",
|
|
2285
|
+
]);
|
|
2286
|
+
|
|
2287
|
+
/** Nodes that add nothing to the compact view: static text repeating its
|
|
2288
|
+
* parent's accessible name, and unnamed, stateless, non-actionable containers.
|
|
2289
|
+
* Their children keep their own depth, so the outline stays readable. */
|
|
2290
|
+
function redundantAgentNode(flat: FlatBrowserNode): boolean {
|
|
2291
|
+
const { node } = flat;
|
|
2292
|
+
if (node.actions.length > 0 || node.states.length > 0) return false;
|
|
2293
|
+
const hasValue = node.value !== undefined && node.value !== null && node.value !== "";
|
|
2294
|
+
if (node.role === "text" || node.role === "StaticText") {
|
|
2295
|
+
return !hasValue && !node.description && (!node.name || node.name === flat.parentName);
|
|
2296
|
+
}
|
|
2297
|
+
if (BROWSER_AGENT_STRUCTURAL_ROLES.has(node.role)) return false;
|
|
2298
|
+
return !node.name && !node.description && !hasValue;
|
|
2299
|
+
}
|
|
2300
|
+
|
|
1992
2301
|
function boundedAgentField(value: string, onClip: () => void): string {
|
|
1993
2302
|
if (value.length <= BROWSER_AGENT_MAX_FIELD_LENGTH) return value;
|
|
1994
2303
|
onClip();
|
|
@@ -2018,8 +2327,12 @@ function projectAgentNode(flat: FlatBrowserNode): {
|
|
|
2018
2327
|
typeof source.value === "string" ? boundedAgentField(source.value, clip) : source.value,
|
|
2019
2328
|
}
|
|
2020
2329
|
: {}),
|
|
2021
|
-
|
|
2022
|
-
|
|
2330
|
+
...(source.states.length > 0
|
|
2331
|
+
? { states: source.states.slice(0, 6).map((state) => boundedAgentField(state, clip)) }
|
|
2332
|
+
: {}),
|
|
2333
|
+
...(source.actions.length > 0
|
|
2334
|
+
? { actions: source.actions.slice(0, 6).map((action) => boundedAgentField(action, clip)) }
|
|
2335
|
+
: {}),
|
|
2023
2336
|
};
|
|
2024
2337
|
if (source.states.length > 6) clippedFieldCount += source.states.length - 6;
|
|
2025
2338
|
if (source.actions.length > 6) clippedFieldCount += source.actions.length - 6;
|
|
@@ -2082,29 +2395,126 @@ function projectBrowserObservation(
|
|
|
2082
2395
|
: semantic?.kind === "diff"
|
|
2083
2396
|
? semantic.changed
|
|
2084
2397
|
: [];
|
|
2085
|
-
const flattened = flattenBrowserNodes(roots);
|
|
2398
|
+
const flattened = flattenBrowserNodes(roots).filter((flat) => !redundantAgentNode(flat));
|
|
2086
2399
|
const projected = selectAgentNodes(
|
|
2087
2400
|
flattened,
|
|
2088
2401
|
BROWSER_AGENT_MAX_NODES,
|
|
2089
2402
|
BROWSER_AGENT_MAX_NODE_BYTES,
|
|
2090
2403
|
);
|
|
2404
|
+
const omittedNodeCount = flattened.length - projected.nodes.length;
|
|
2405
|
+
const removedRefCount = semantic?.kind === "diff" ? semantic.removedRefs.length : 0;
|
|
2406
|
+
const viewport = observation.viewport;
|
|
2407
|
+
const diagnostics = observation.diagnostics;
|
|
2408
|
+
const hasDiagnostics =
|
|
2409
|
+
diagnostics.consoleErrorCount > 0 ||
|
|
2410
|
+
diagnostics.failedRequestCount > 0 ||
|
|
2411
|
+
diagnostics.downloadCount > 0 ||
|
|
2412
|
+
diagnostics.pageErrorCount > 0;
|
|
2091
2413
|
return {
|
|
2092
|
-
|
|
2093
|
-
|
|
2414
|
+
target: agentBrowserTarget(observation.target),
|
|
2415
|
+
...(observation.frameId ? { frameId: observation.frameId } : {}),
|
|
2416
|
+
...(viewport
|
|
2417
|
+
? {
|
|
2418
|
+
viewport: {
|
|
2419
|
+
width: viewport.width,
|
|
2420
|
+
height: viewport.height,
|
|
2421
|
+
...(viewport.deviceScaleFactor !== 1
|
|
2422
|
+
? { deviceScaleFactor: viewport.deviceScaleFactor }
|
|
2423
|
+
: {}),
|
|
2424
|
+
...(viewport.maxTouchPoints > 0 ? { maxTouchPoints: viewport.maxTouchPoints } : {}),
|
|
2425
|
+
},
|
|
2426
|
+
}
|
|
2427
|
+
: {}),
|
|
2428
|
+
...(observation.focusedRef ? { focusedRef: observation.focusedRef } : {}),
|
|
2429
|
+
...(observation.changedRegions.length > 0
|
|
2430
|
+
? { changedRegions: observation.changedRegions }
|
|
2431
|
+
: {}),
|
|
2432
|
+
...(hasDiagnostics ? { diagnostics } : {}),
|
|
2433
|
+
...(observation.dialog ? { dialog: observation.dialog } : {}),
|
|
2434
|
+
...(observation.screenshot ? { screenshot: observation.screenshot } : {}),
|
|
2094
2435
|
agentView: {
|
|
2095
|
-
kind: "compact",
|
|
2096
2436
|
sourceKind: semantic?.kind ?? "none",
|
|
2097
2437
|
nodes: projected.nodes,
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
|
|
2103
|
-
maxNodeBytes: BROWSER_AGENT_MAX_NODE_BYTES,
|
|
2438
|
+
...(omittedNodeCount > 0 ? { omittedNodeCount } : {}),
|
|
2439
|
+
...(removedRefCount > 0 ? { removedRefCount } : {}),
|
|
2440
|
+
...(projected.clippedFieldCount > 0
|
|
2441
|
+
? { clippedFieldCount: projected.clippedFieldCount }
|
|
2442
|
+
: {}),
|
|
2104
2443
|
},
|
|
2105
2444
|
};
|
|
2106
2445
|
}
|
|
2107
2446
|
|
|
2447
|
+
function agentBrowserTarget(
|
|
2448
|
+
target: z.infer<typeof BrowserTarget>,
|
|
2449
|
+
): z.infer<typeof AgentBrowserTarget> {
|
|
2450
|
+
return {
|
|
2451
|
+
id: target.id,
|
|
2452
|
+
kind: target.kind,
|
|
2453
|
+
title: target.title,
|
|
2454
|
+
url: target.url,
|
|
2455
|
+
selected: target.selected,
|
|
2456
|
+
controllerGeneration: target.controllerGeneration,
|
|
2457
|
+
targetGeneration: target.targetGeneration,
|
|
2458
|
+
documentGeneration: target.documentGeneration,
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
/** Attached profiles are what the model selects. Bridges only explain why
|
|
2463
|
+
* none is available, so they are listed (online only) when no profile is. */
|
|
2464
|
+
function agentAttachedInventory(inventory: z.infer<typeof AttachedBrowserDeviceListResponse>): {
|
|
2465
|
+
attachedBrowserBridges: z.infer<typeof AgentAttachedBrowserBridge>[];
|
|
2466
|
+
attachedBrowsers: z.infer<typeof AgentAttachedBrowser>[];
|
|
2467
|
+
} {
|
|
2468
|
+
const attachedBrowsers = inventory.devices.map((device) => {
|
|
2469
|
+
const unsupported = Object.entries(device.capabilities)
|
|
2470
|
+
.filter(([, supported]) => !supported)
|
|
2471
|
+
.map(([capability]) => capability);
|
|
2472
|
+
return {
|
|
2473
|
+
id: device.id,
|
|
2474
|
+
name: device.name,
|
|
2475
|
+
...(device.profileLabel ? { profileLabel: device.profileLabel } : {}),
|
|
2476
|
+
browserName: device.browserName,
|
|
2477
|
+
platform: device.platform,
|
|
2478
|
+
state: device.state,
|
|
2479
|
+
tabCount: device.tabCount,
|
|
2480
|
+
...(unsupported.length > 0 ? { unsupported } : {}),
|
|
2481
|
+
};
|
|
2482
|
+
});
|
|
2483
|
+
const anyConnected = inventory.devices.some((device) => device.state === "connected");
|
|
2484
|
+
const attachedBrowserBridges = anyConnected
|
|
2485
|
+
? []
|
|
2486
|
+
: inventory.bridges
|
|
2487
|
+
.filter((bridge) => bridge.state === "online")
|
|
2488
|
+
.map((bridge) => ({
|
|
2489
|
+
enrollmentId: bridge.enrollmentId,
|
|
2490
|
+
connectedProfileCount: bridge.connectedProfileCount,
|
|
2491
|
+
}));
|
|
2492
|
+
return { attachedBrowserBridges, attachedBrowsers };
|
|
2493
|
+
}
|
|
2494
|
+
|
|
2495
|
+
function agentTargetList(
|
|
2496
|
+
list: z.infer<typeof BrowserTargetListResponse>,
|
|
2497
|
+
): z.infer<typeof BrowserTabsOutput> {
|
|
2498
|
+
return {
|
|
2499
|
+
browserSessionId: list.browserSessionId,
|
|
2500
|
+
targets: list.targets.map(agentBrowserTarget),
|
|
2501
|
+
};
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
function compactActionReceipt(
|
|
2505
|
+
receipt: z.infer<typeof BrowserActionReceipt>,
|
|
2506
|
+
): z.infer<typeof CompactBrowserActionReceipt> {
|
|
2507
|
+
return {
|
|
2508
|
+
targetId: receipt.targetId,
|
|
2509
|
+
state: receipt.state,
|
|
2510
|
+
...(receipt.error ? { error: receipt.error } : {}),
|
|
2511
|
+
...(receipt.targets && receipt.targets.length > 0 ? { targets: receipt.targets } : {}),
|
|
2512
|
+
...(receipt.observation
|
|
2513
|
+
? { observation: projectBrowserObservation(receipt.observation, "compact") }
|
|
2514
|
+
: {}),
|
|
2515
|
+
};
|
|
2516
|
+
}
|
|
2517
|
+
|
|
2108
2518
|
function readBrowserObservation(
|
|
2109
2519
|
observation: z.infer<typeof BrowserObservation>,
|
|
2110
2520
|
input: z.output<typeof BrowserReadInput>,
|