@memberjunction/server 5.40.2 → 5.42.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 (129) hide show
  1. package/dist/agentSessions/HostInstance.d.ts +19 -0
  2. package/dist/agentSessions/HostInstance.d.ts.map +1 -0
  3. package/dist/agentSessions/HostInstance.js +48 -0
  4. package/dist/agentSessions/HostInstance.js.map +1 -0
  5. package/dist/agentSessions/SessionJanitor.d.ts +97 -0
  6. package/dist/agentSessions/SessionJanitor.d.ts.map +1 -0
  7. package/dist/agentSessions/SessionJanitor.js +222 -0
  8. package/dist/agentSessions/SessionJanitor.js.map +1 -0
  9. package/dist/agentSessions/SessionManager.d.ts +142 -0
  10. package/dist/agentSessions/SessionManager.d.ts.map +1 -0
  11. package/dist/agentSessions/SessionManager.js +308 -0
  12. package/dist/agentSessions/SessionManager.js.map +1 -0
  13. package/dist/agentSessions/index.d.ts +5 -0
  14. package/dist/agentSessions/index.d.ts.map +1 -0
  15. package/dist/agentSessions/index.js +31 -0
  16. package/dist/agentSessions/index.js.map +1 -0
  17. package/dist/agentSessions/remoteBrowserGoalEngine.d.ts +102 -0
  18. package/dist/agentSessions/remoteBrowserGoalEngine.d.ts.map +1 -0
  19. package/dist/agentSessions/remoteBrowserGoalEngine.js +164 -0
  20. package/dist/agentSessions/remoteBrowserGoalEngine.js.map +1 -0
  21. package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts +66 -0
  22. package/dist/agentSessions/remoteBrowserGoalRegistry.d.ts.map +1 -0
  23. package/dist/agentSessions/remoteBrowserGoalRegistry.js +86 -0
  24. package/dist/agentSessions/remoteBrowserGoalRegistry.js.map +1 -0
  25. package/dist/auth/initializeProviders.d.ts.map +1 -1
  26. package/dist/auth/initializeProviders.js +4 -2
  27. package/dist/auth/initializeProviders.js.map +1 -1
  28. package/dist/config.d.ts +7 -0
  29. package/dist/config.d.ts.map +1 -1
  30. package/dist/config.js +12 -2
  31. package/dist/config.js.map +1 -1
  32. package/dist/context.d.ts.map +1 -1
  33. package/dist/context.js +41 -7
  34. package/dist/context.js.map +1 -1
  35. package/dist/generated/generated.d.ts +1296 -49
  36. package/dist/generated/generated.d.ts.map +1 -1
  37. package/dist/generated/generated.js +50343 -43421
  38. package/dist/generated/generated.js.map +1 -1
  39. package/dist/generic/ResolverBase.js +1 -1
  40. package/dist/generic/ResolverBase.js.map +1 -1
  41. package/dist/generic/RunViewResolver.js +9 -10
  42. package/dist/generic/RunViewResolver.js.map +1 -1
  43. package/dist/index.d.ts +4 -0
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +127 -54
  46. package/dist/index.js.map +1 -1
  47. package/dist/integration/CustomColumnPromoter.d.ts +108 -0
  48. package/dist/integration/CustomColumnPromoter.d.ts.map +1 -0
  49. package/dist/integration/CustomColumnPromoter.js +508 -0
  50. package/dist/integration/CustomColumnPromoter.js.map +1 -0
  51. package/dist/logging/StartupLogger.d.ts +177 -0
  52. package/dist/logging/StartupLogger.d.ts.map +1 -0
  53. package/dist/logging/StartupLogger.js +354 -0
  54. package/dist/logging/StartupLogger.js.map +1 -0
  55. package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -1
  56. package/dist/logging/variablesLoggingMiddleware.js +21 -2
  57. package/dist/logging/variablesLoggingMiddleware.js.map +1 -1
  58. package/dist/resolvers/AgentSessionResolver.d.ts +42 -0
  59. package/dist/resolvers/AgentSessionResolver.d.ts.map +1 -0
  60. package/dist/resolvers/AgentSessionResolver.js +152 -0
  61. package/dist/resolvers/AgentSessionResolver.js.map +1 -0
  62. package/dist/resolvers/EntityPermissionResolver.d.ts +16 -0
  63. package/dist/resolvers/EntityPermissionResolver.d.ts.map +1 -0
  64. package/dist/resolvers/EntityPermissionResolver.js +95 -0
  65. package/dist/resolvers/EntityPermissionResolver.js.map +1 -0
  66. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +27 -0
  67. package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -0
  68. package/dist/resolvers/ExecuteRemoteOperationResolver.js +136 -0
  69. package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -0
  70. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +101 -1
  71. package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
  72. package/dist/resolvers/IntegrationDiscoveryResolver.js +544 -42
  73. package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
  74. package/dist/resolvers/RealtimeBridgeResolver.d.ts +121 -0
  75. package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -0
  76. package/dist/resolvers/RealtimeBridgeResolver.js +533 -0
  77. package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -0
  78. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +688 -0
  79. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -0
  80. package/dist/resolvers/RealtimeClientSessionResolver.js +1774 -0
  81. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -0
  82. package/dist/resolvers/RemoteBrowserActionResolver.d.ts +432 -0
  83. package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -0
  84. package/dist/resolvers/RemoteBrowserActionResolver.js +1108 -0
  85. package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -0
  86. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  87. package/dist/resolvers/RunAIAgentResolver.js +0 -7
  88. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  89. package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
  90. package/dist/rest/setupRESTEndpoints.js +5 -3
  91. package/dist/rest/setupRESTEndpoints.js.map +1 -1
  92. package/dist/services/ScheduledJobsService.d.ts.map +1 -1
  93. package/dist/services/ScheduledJobsService.js +6 -5
  94. package/dist/services/ScheduledJobsService.js.map +1 -1
  95. package/package.json +83 -74
  96. package/src/__tests__/RealtimeBridgeResolver.test.ts +100 -0
  97. package/src/__tests__/RealtimeClientSessionResolver.test.ts +2605 -0
  98. package/src/__tests__/RemoteBrowserAudioStream.test.ts +181 -0
  99. package/src/__tests__/RemoteBrowserGoalEngine.test.ts +144 -0
  100. package/src/__tests__/SessionJanitor.test.ts +234 -0
  101. package/src/__tests__/SessionManager.test.ts +465 -0
  102. package/src/__tests__/remoteBrowserGoalRegistry.test.ts +81 -0
  103. package/src/__tests__/subscriptionRedaction.test.ts +5 -0
  104. package/src/agentSessions/HostInstance.ts +53 -0
  105. package/src/agentSessions/SessionJanitor.ts +267 -0
  106. package/src/agentSessions/SessionManager.ts +446 -0
  107. package/src/agentSessions/index.ts +32 -0
  108. package/src/agentSessions/remoteBrowserGoalEngine.ts +191 -0
  109. package/src/agentSessions/remoteBrowserGoalRegistry.ts +122 -0
  110. package/src/auth/initializeProviders.ts +4 -2
  111. package/src/config.ts +13 -2
  112. package/src/context.ts +42 -7
  113. package/src/generated/generated.ts +33808 -29025
  114. package/src/generic/ResolverBase.ts +1 -1
  115. package/src/generic/RunViewResolver.ts +9 -9
  116. package/src/index.ts +128 -54
  117. package/src/integration/CustomColumnPromoter.ts +597 -0
  118. package/src/logging/StartupLogger.ts +441 -0
  119. package/src/logging/variablesLoggingMiddleware.ts +25 -5
  120. package/src/resolvers/AgentSessionResolver.ts +138 -0
  121. package/src/resolvers/EntityPermissionResolver.ts +73 -0
  122. package/src/resolvers/ExecuteRemoteOperationResolver.ts +108 -0
  123. package/src/resolvers/IntegrationDiscoveryResolver.ts +396 -37
  124. package/src/resolvers/RealtimeBridgeResolver.ts +450 -0
  125. package/src/resolvers/RealtimeClientSessionResolver.ts +2162 -0
  126. package/src/resolvers/RemoteBrowserActionResolver.ts +1122 -0
  127. package/src/resolvers/RunAIAgentResolver.ts +1 -9
  128. package/src/rest/setupRESTEndpoints.ts +5 -3
  129. package/src/services/ScheduledJobsService.ts +6 -5
@@ -0,0 +1,1108 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ var __metadata = (this && this.__metadata) || function (k, v) {
8
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
9
+ };
10
+ var __param = (this && this.__param) || function (paramIndex, decorator) {
11
+ return function (target, key) { decorator(target, key, paramIndex); }
12
+ };
13
+ /**
14
+ * @fileoverview GraphQL resolvers for the **Remote Browser** native realtime channel, CLIENT-DIRECT
15
+ * topology.
16
+ *
17
+ * Realtime sessions are client-direct: the model talks to the browser, and the agent's browser-driving
18
+ * tools execute CLIENT-side (like the live Whiteboard). Each client-executed browser tool relays its
19
+ * intent to the server through {@link RemoteBrowserActionResolver.ExecuteRemoteBrowserAction}, which drives
20
+ * the server-side {@link RemoteBrowserEngine}'s live browser. A second query,
21
+ * {@link RemoteBrowserActionResolver.RemoteBrowserSnapshot}, returns the current screenshot + URL for the
22
+ * client's live view.
23
+ *
24
+ * Both operations are **ownership-gated** exactly like {@link import('./RealtimeClientSessionResolver.js').RealtimeClientSessionResolver}:
25
+ * the `AIAgentSession.UserID` must equal the calling user. The mutation lazily starts the browser on first
26
+ * use — so a realtime session that never touches the browser never launches Chrome — resolving the backend
27
+ * from the agent's `TypeConfiguration` (`{ remoteBrowser: { provider } }`), else the single Active provider.
28
+ *
29
+ * @module @memberjunction/server
30
+ */
31
+ import { Resolver, Mutation, Query, Arg, Ctx, Float, Int, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
32
+ import { LogError } from '@memberjunction/core';
33
+ import { UUIDsEqual } from '@memberjunction/global';
34
+ import { RemoteBrowserEngine } from '@memberjunction/remote-browser-server';
35
+ import { beginBrowserGoalStep, finalizeBrowserGoalStep, extractCoAgentRunID } from '../agentSessions/remoteBrowserGoalEngine.js';
36
+ import { RemoteBrowserGoalRegistry } from '../agentSessions/remoteBrowserGoalRegistry.js';
37
+ import { randomUUID } from 'node:crypto';
38
+ import { RemoteBrowserCapabilityNotSupportedError, } from '@memberjunction/remote-browser-base';
39
+ import { ResolverBase } from '../generic/ResolverBase.js';
40
+ import { GetReadWriteProvider } from '../util.js';
41
+ import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
42
+ import { AIEngine } from '@memberjunction/aiengine';
43
+ import { AIPromptRunner } from '@memberjunction/ai-prompts';
44
+ import { AIPromptParams } from '@memberjunction/ai-core-plus';
45
+ /** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
46
+ const SESSION_ENTITY = 'MJ: AI Agent Sessions';
47
+ /** Entity name for the agent whose `TypeConfiguration` carries the remote-browser provider preference. */
48
+ const AGENT_ENTITY = 'MJ: AI Agents';
49
+ /**
50
+ * Name of the governable vision AI Prompt that interprets a browser screenshot for a non-vision voice agent.
51
+ * Seeded as metadata (`metadata/prompts/.remote-browser-visual-interpreter-prompt.json`) and pinned to the
52
+ * `Gemini 3.1 Flash-Lite` model via its `MJ: AI Prompt Models` association — so the model is chosen by the
53
+ * prompt's own configuration, not hard-coded here.
54
+ */
55
+ const VISUAL_INTERPRETER_PROMPT_NAME = 'Remote Browser Visual Interpreter';
56
+ /**
57
+ * Result of {@link RemoteBrowserActionResolver.ExecuteRemoteBrowserAction} — the outcome of one browser
58
+ * action plus the resulting URL the client narrates / displays.
59
+ */
60
+ let RemoteBrowserActionResult = class RemoteBrowserActionResult {
61
+ };
62
+ __decorate([
63
+ Field(() => Boolean),
64
+ __metadata("design:type", Boolean)
65
+ ], RemoteBrowserActionResult.prototype, "Success", void 0);
66
+ __decorate([
67
+ Field(() => String, { nullable: true }),
68
+ __metadata("design:type", String)
69
+ ], RemoteBrowserActionResult.prototype, "CurrentUrl", void 0);
70
+ __decorate([
71
+ Field(() => String, { nullable: true }),
72
+ __metadata("design:type", String)
73
+ ], RemoteBrowserActionResult.prototype, "Detail", void 0);
74
+ RemoteBrowserActionResult = __decorate([
75
+ ObjectType()
76
+ ], RemoteBrowserActionResult);
77
+ export { RemoteBrowserActionResult };
78
+ /**
79
+ * Result of {@link RemoteBrowserActionResolver.ExecuteRemoteBrowserGoal} — the outcome of an autonomous,
80
+ * goal-driven browser run (computer-use loop or backend native AI).
81
+ */
82
+ let RemoteBrowserGoalResultType = class RemoteBrowserGoalResultType {
83
+ };
84
+ __decorate([
85
+ Field(() => Boolean),
86
+ __metadata("design:type", Boolean)
87
+ ], RemoteBrowserGoalResultType.prototype, "Success", void 0);
88
+ __decorate([
89
+ Field(() => String, { nullable: true }),
90
+ __metadata("design:type", String)
91
+ ], RemoteBrowserGoalResultType.prototype, "GoalRunID", void 0);
92
+ __decorate([
93
+ Field(() => String, { nullable: true }),
94
+ __metadata("design:type", String)
95
+ ], RemoteBrowserGoalResultType.prototype, "Strategy", void 0);
96
+ __decorate([
97
+ Field(() => String, { nullable: true }),
98
+ __metadata("design:type", String)
99
+ ], RemoteBrowserGoalResultType.prototype, "CurrentUrl", void 0);
100
+ __decorate([
101
+ Field(() => String, { nullable: true }),
102
+ __metadata("design:type", String)
103
+ ], RemoteBrowserGoalResultType.prototype, "Status", void 0);
104
+ __decorate([
105
+ Field(() => Int, { nullable: true }),
106
+ __metadata("design:type", Number)
107
+ ], RemoteBrowserGoalResultType.prototype, "StepCount", void 0);
108
+ __decorate([
109
+ Field(() => String, { nullable: true }),
110
+ __metadata("design:type", String)
111
+ ], RemoteBrowserGoalResultType.prototype, "Detail", void 0);
112
+ RemoteBrowserGoalResultType = __decorate([
113
+ ObjectType()
114
+ ], RemoteBrowserGoalResultType);
115
+ export { RemoteBrowserGoalResultType };
116
+ /**
117
+ * Result of {@link RemoteBrowserActionResolver.RemoteBrowserSnapshot} — the current viewport screenshot +
118
+ * URL for the client's live view. Both fields are null when the session holds no live browser.
119
+ */
120
+ let RemoteBrowserSnapshot = class RemoteBrowserSnapshot {
121
+ };
122
+ __decorate([
123
+ Field(() => String, { nullable: true }),
124
+ __metadata("design:type", String)
125
+ ], RemoteBrowserSnapshot.prototype, "ScreenshotBase64", void 0);
126
+ __decorate([
127
+ Field(() => String, { nullable: true }),
128
+ __metadata("design:type", String)
129
+ ], RemoteBrowserSnapshot.prototype, "CurrentUrl", void 0);
130
+ RemoteBrowserSnapshot = __decorate([
131
+ ObjectType()
132
+ ], RemoteBrowserSnapshot);
133
+ export { RemoteBrowserSnapshot };
134
+ /**
135
+ * Result of {@link RemoteBrowserActionResolver.StartRemoteBrowserScreencast} — whether the live CDP
136
+ * screencast started. When `false` the backend lacks the `ScreenStreaming` capability (or the start
137
+ * failed); the client keeps its 700ms snapshot poll as the fallback live view. When `true` the server
138
+ * is now PUSHING encoded frames on the user's push-status topic and the client paints them on a canvas.
139
+ */
140
+ let RemoteBrowserScreencastResult = class RemoteBrowserScreencastResult {
141
+ };
142
+ __decorate([
143
+ Field(() => Boolean),
144
+ __metadata("design:type", Boolean)
145
+ ], RemoteBrowserScreencastResult.prototype, "Streaming", void 0);
146
+ RemoteBrowserScreencastResult = __decorate([
147
+ ObjectType()
148
+ ], RemoteBrowserScreencastResult);
149
+ export { RemoteBrowserScreencastResult };
150
+ /**
151
+ * Result of {@link RemoteBrowserActionResolver.StartRemoteBrowserAudioStream} — whether the live tab-audio
152
+ * stream started. When `false` the backend lacks an audio-capture mechanism (v1 gates audio by backend
153
+ * implementation, not a metadata flag) or the start failed; the client simply plays no audio. When `true`
154
+ * the server is now PUSHING encoded audio chunks on the user's push-status topic and the client plays them.
155
+ */
156
+ let RemoteBrowserAudioStreamResult = class RemoteBrowserAudioStreamResult {
157
+ };
158
+ __decorate([
159
+ Field(() => Boolean),
160
+ __metadata("design:type", Boolean)
161
+ ], RemoteBrowserAudioStreamResult.prototype, "Streaming", void 0);
162
+ RemoteBrowserAudioStreamResult = __decorate([
163
+ ObjectType()
164
+ ], RemoteBrowserAudioStreamResult);
165
+ export { RemoteBrowserAudioStreamResult };
166
+ /**
167
+ * Result of {@link RemoteBrowserActionResolver.GetRemoteBrowserSelection} — the remote page's current text
168
+ * selection, the copy-out half of human clipboard support. `Text` is `''` when nothing is selected, no live
169
+ * browser exists, or the backend can't read the selection (the client then writes nothing to the clipboard).
170
+ */
171
+ let RemoteBrowserSelection = class RemoteBrowserSelection {
172
+ };
173
+ __decorate([
174
+ Field(() => String),
175
+ __metadata("design:type", String)
176
+ ], RemoteBrowserSelection.prototype, "Text", void 0);
177
+ RemoteBrowserSelection = __decorate([
178
+ ObjectType()
179
+ ], RemoteBrowserSelection);
180
+ export { RemoteBrowserSelection };
181
+ /**
182
+ * One UI element the visual interpreter localized in the screenshot — a label plus the pixel centroid the
183
+ * voice agent can feed straight into `browser_Click(x, y)`. Coordinates are in the SCREENSHOT's own pixel
184
+ * space (top-left origin), which equals the live browser viewport.
185
+ */
186
+ let RemoteBrowserInterpretedElement = class RemoteBrowserInterpretedElement {
187
+ };
188
+ __decorate([
189
+ Field(),
190
+ __metadata("design:type", String)
191
+ ], RemoteBrowserInterpretedElement.prototype, "Label", void 0);
192
+ __decorate([
193
+ Field(() => Float),
194
+ __metadata("design:type", Number)
195
+ ], RemoteBrowserInterpretedElement.prototype, "X", void 0);
196
+ __decorate([
197
+ Field(() => Float),
198
+ __metadata("design:type", Number)
199
+ ], RemoteBrowserInterpretedElement.prototype, "Y", void 0);
200
+ __decorate([
201
+ Field(() => Float),
202
+ __metadata("design:type", Number)
203
+ ], RemoteBrowserInterpretedElement.prototype, "Confidence", void 0);
204
+ RemoteBrowserInterpretedElement = __decorate([
205
+ ObjectType()
206
+ ], RemoteBrowserInterpretedElement);
207
+ export { RemoteBrowserInterpretedElement };
208
+ /**
209
+ * Result of {@link RemoteBrowserActionResolver.InterpretRemoteBrowserPage} — how a fast vision model "sees"
210
+ * the current browser viewport on behalf of a voice agent that cannot view images. `Description` is a concise
211
+ * text summary; `Elements` localizes any UI elements the request asked for (empty for a plain describe).
212
+ * `Detail` carries a non-fatal note (e.g. `'no live browser'` or a vision-error message) — every path is
213
+ * best-effort and never throws.
214
+ */
215
+ let RemoteBrowserInterpretation = class RemoteBrowserInterpretation {
216
+ };
217
+ __decorate([
218
+ Field(() => String, { nullable: true }),
219
+ __metadata("design:type", String)
220
+ ], RemoteBrowserInterpretation.prototype, "Description", void 0);
221
+ __decorate([
222
+ Field(() => [RemoteBrowserInterpretedElement]),
223
+ __metadata("design:type", Array)
224
+ ], RemoteBrowserInterpretation.prototype, "Elements", void 0);
225
+ __decorate([
226
+ Field(() => String, { nullable: true }),
227
+ __metadata("design:type", String)
228
+ ], RemoteBrowserInterpretation.prototype, "Detail", void 0);
229
+ RemoteBrowserInterpretation = __decorate([
230
+ ObjectType()
231
+ ], RemoteBrowserInterpretation);
232
+ export { RemoteBrowserInterpretation };
233
+ /**
234
+ * Resolver for the Remote Browser native realtime channel. A single {@link RemoteBrowserEngine} instance
235
+ * (the process-wide singleton) backs every request; ownership is enforced per call against the session's
236
+ * `UserID`.
237
+ */
238
+ let RemoteBrowserActionResolver = class RemoteBrowserActionResolver extends ResolverBase {
239
+ constructor() {
240
+ super(...arguments);
241
+ /**
242
+ * Agent-session ids whose live CDP screencast this resolver has already started. Keyed by
243
+ * `agentSessionID` so a re-issued {@link RemoteBrowserActionResolver.StartRemoteBrowserScreencast}
244
+ * (e.g. the surface re-binding after a tab collapse) is idempotent and never stacks two screencasts
245
+ * on the one session. Entries are removed by {@link RemoteBrowserActionResolver.StopRemoteBrowserScreencast}.
246
+ */
247
+ this.startedScreencasts = new Set();
248
+ /**
249
+ * Agent-session ids whose live tab-audio stream this resolver has already started. Keyed by
250
+ * `agentSessionID` so a re-issued {@link RemoteBrowserActionResolver.StartRemoteBrowserAudioStream}
251
+ * (the surface re-binding) is idempotent and never stacks two captures on the one session. Entries are
252
+ * removed by {@link RemoteBrowserActionResolver.StopRemoteBrowserAudioStream}.
253
+ */
254
+ this.startedAudioStreams = new Set();
255
+ }
256
+ /**
257
+ * Execute ONE browser action relayed from the client-direct realtime session, returning the outcome +
258
+ * resulting URL.
259
+ *
260
+ * Flow:
261
+ * 1. Ownership gate — the session's `UserID` must equal the caller's (throws otherwise).
262
+ * 2. Lazily start (or reuse) the session's browser via {@link RemoteBrowserEngine.StartSessionForAgentSession},
263
+ * resolving the backend from the agent's `TypeConfiguration` (`{ remoteBrowser: { provider } }`), else
264
+ * the single Active provider.
265
+ * 3. Build a strongly-typed {@link RemoteBrowserAction} from `kind` + the supplied fields.
266
+ * 4. Execute it against the live session and return the result.
267
+ *
268
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
269
+ * @param kind The action kind (`'navigate' | 'click' | 'type' | 'key' | 'scroll' | 'back' | 'forward' | 'wait'`).
270
+ * @returns The action result (success + resulting URL + detail).
271
+ */
272
+ async ExecuteRemoteBrowserAction(agentSessionID, kind, { userPayload, providers }, url, selector, x, y, text, key, deltaX, deltaY, ms) {
273
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
274
+ const session = await this.loadOwnedSession(agentSessionID, contextUser, provider);
275
+ const action = this.buildAction({ kind, url, selector, x, y, text, key, deltaX, deltaY, ms });
276
+ if (!action) {
277
+ return { Success: false, Detail: `Unknown or incomplete remote-browser action '${kind}'.` };
278
+ }
279
+ const providerName = await this.resolveProviderName(session, contextUser, provider);
280
+ try {
281
+ const liveSession = await RemoteBrowserEngine.Instance.StartSessionForAgentSession(agentSessionID, contextUser, providerName);
282
+ const result = await liveSession.ExecuteAction(action);
283
+ return { Success: result.Success, CurrentUrl: result.CurrentUrl, Detail: result.Detail };
284
+ }
285
+ catch (err) {
286
+ // Surface the real failure to BOTH the MJAPI terminal (for diagnosis) and the model (so it
287
+ // narrates the actual cause instead of the opaque client-side "no response from the server").
288
+ const message = err instanceof Error ? err.message : String(err);
289
+ LogError(`ExecuteRemoteBrowserAction failed (provider='${providerName}', kind='${kind}'): ${message}`);
290
+ return { Success: false, Detail: `Remote browser error (${providerName}): ${message}` };
291
+ }
292
+ }
293
+ /**
294
+ * Execute a high-level GOAL against the session's browser — the agent sets an intent ("log in and open
295
+ * the latest invoice") and the resolved control strategy (computer-use loop or backend native AI) plans
296
+ * + executes it autonomously, instead of relaying granular actions. Ownership-gated; lazily starts the
297
+ * browser. Returns the terminal outcome (this is a request/response mutation — for live progress
298
+ * narration in a server-bridged realtime session the broker calls {@link RemoteBrowserEngine.AchieveGoal}
299
+ * in-process with an `OnProgress` callback).
300
+ *
301
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
302
+ * @param goal The natural-language goal.
303
+ * @returns The goal outcome (success, strategy, status, step count, url, detail).
304
+ */
305
+ async ExecuteRemoteBrowserGoal(agentSessionID, goal, { userPayload, providers }, startUrl, maxSteps, preferredStrategy) {
306
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
307
+ const session = await this.loadOwnedSession(agentSessionID, contextUser, provider);
308
+ const providerName = await this.resolveProviderName(session, contextUser, provider);
309
+ // Observability: nest this goal's many prompt runs under ONE "Browser goal" step on the realtime
310
+ // co-agent run (when the session has one). Best-effort — a null step just means the goal runs unlinked.
311
+ const coAgentRunID = extractCoAgentRunID(session.Config_);
312
+ const goalStep = await beginBrowserGoalStep(provider, contextUser, coAgentRunID, goal);
313
+ // ASYNC START: a goal loop can run for minutes; do NOT hold this request open for it (browser
314
+ // fetch / proxy / janitor timeouts would kill the request while the loop runs on, and the agent
315
+ // would get "no response from the server" despite a successful run). Register the run, kick the
316
+ // loop off WITHOUT awaiting, and return a GoalRunID the client polls via GetRemoteBrowserGoalResult.
317
+ const goalRunID = randomUUID();
318
+ RemoteBrowserGoalRegistry.Instance.Begin(agentSessionID, goalRunID);
319
+ void RemoteBrowserEngine.Instance.AchieveGoal(agentSessionID, goal, {
320
+ ContextUser: contextUser,
321
+ ProviderName: providerName,
322
+ StartUrl: startUrl,
323
+ MaxSteps: maxSteps,
324
+ PreferredStrategy: preferredStrategy === 'NativeAI' || preferredStrategy === 'ComputerUse' ? preferredStrategy : undefined,
325
+ AgentRunID: coAgentRunID,
326
+ AgentRunStepID: goalStep?.ID,
327
+ })
328
+ .then(async (result) => {
329
+ await finalizeBrowserGoalStep(goalStep, result);
330
+ RemoteBrowserGoalRegistry.Instance.Complete(agentSessionID, goalRunID, result);
331
+ })
332
+ .catch(async (err) => {
333
+ const message = err instanceof Error ? err.message : String(err);
334
+ LogError(`ExecuteRemoteBrowserGoal failed (provider='${providerName}'): ${message}`);
335
+ const failure = { Success: false, Status: 'Error', Detail: `Remote browser error (${providerName}): ${message}` };
336
+ await finalizeBrowserGoalStep(goalStep, failure);
337
+ RemoteBrowserGoalRegistry.Instance.Complete(agentSessionID, goalRunID, failure);
338
+ });
339
+ return { Success: true, Status: 'Running', GoalRunID: goalRunID, Detail: 'Goal started.' };
340
+ }
341
+ /**
342
+ * Poll the outcome of a goal STARTED by {@link RemoteBrowserActionResolver.ExecuteRemoteBrowserGoal}.
343
+ * Returns `Status: 'Running'` while the loop is in flight, then the terminal outcome (success,
344
+ * strategy, status, step count, url, detail) once it finishes. Ownership-gated. A `Status: 'Unknown'`
345
+ * result means the run id is unrecognized — it expired (results are retained briefly) or never existed.
346
+ *
347
+ * @param agentSessionID The `AIAgentSession` id the goal runs against.
348
+ * @param goalRunID The handle returned by `ExecuteRemoteBrowserGoal`.
349
+ * @returns The current/terminal goal outcome.
350
+ */
351
+ async GetRemoteBrowserGoalResult(agentSessionID, goalRunID, { userPayload, providers }) {
352
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
353
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
354
+ const record = RemoteBrowserGoalRegistry.Instance.Get(agentSessionID, goalRunID);
355
+ if (!record) {
356
+ return { Success: false, Status: 'Unknown', GoalRunID: goalRunID, Detail: 'No such goal run (it may have expired).' };
357
+ }
358
+ if (record.Status === 'Running' || !record.Outcome) {
359
+ return { Success: false, Status: 'Running', GoalRunID: goalRunID };
360
+ }
361
+ return { ...record.Outcome, GoalRunID: goalRunID };
362
+ }
363
+ /**
364
+ * Return the current viewport screenshot + URL for the session's live browser — the client's live view.
365
+ * Ownership-gated. When the session holds no live browser (never started, or already torn down), the
366
+ * result's fields are null rather than an error.
367
+ *
368
+ * @param agentSessionID The `AIAgentSession` id.
369
+ * @returns The current screenshot + URL, or an empty snapshot when no live browser exists.
370
+ */
371
+ async RemoteBrowserSnapshot(agentSessionID, { userPayload, providers }) {
372
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
373
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
374
+ const liveSession = RemoteBrowserEngine.Instance.GetSessionForAgentSession(agentSessionID);
375
+ if (!liveSession) {
376
+ return {};
377
+ }
378
+ const screenshot = await liveSession.CaptureScreenshot();
379
+ return { ScreenshotBase64: screenshot, CurrentUrl: liveSession.GetCurrentUrl() };
380
+ }
381
+ /**
382
+ * VISION-QUERY path (separate from the navigate/click {@link RemoteBrowserActionResolver.ExecuteRemoteBrowserAction}
383
+ * path — this OBSERVES the page, it never drives it). Captures the session's current screenshot and runs
384
+ * a fast vision AI Prompt over it so a non-omnimodal voice agent (audio/text-only) can effectively SEE the
385
+ * page: a concise text description ("kinda see") and, when `query` names a target, the pixel centroid(s) the
386
+ * agent can then feed to `browser_Click(x, y)`.
387
+ *
388
+ * Flow:
389
+ * 1. Ownership gate — the session's `UserID` must equal the caller's (throws otherwise).
390
+ * 2. Get the LIVE session and `CaptureScreenshot()`. No live session / no screenshot → `{ Description: null,
391
+ * Elements: [], Detail: 'no live browser' }`.
392
+ * 3. Run the `Remote Browser Visual Interpreter` prompt with the screenshot as an `image_url` content block
393
+ * plus an instruction derived from `query` (empty/"describe" → describe; a named target → locate it).
394
+ * 4. Parse the strict-JSON result tolerantly and map it to {@link RemoteBrowserInterpretation}.
395
+ *
396
+ * Best-effort + tolerant by contract: any vision/prompt/parse failure is logged and returned as
397
+ * `{ Description: null, Elements: [], Detail: <message> }`. This mutation NEVER throws past the ownership gate.
398
+ *
399
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
400
+ * @param query Optional request — empty/"describe" for a page description, else a visual target to localize.
401
+ * @returns The interpretation (description + localized elements + optional detail note).
402
+ */
403
+ async InterpretRemoteBrowserPage(agentSessionID, { userPayload, providers }, query) {
404
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
405
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
406
+ const liveSession = RemoteBrowserEngine.Instance.GetSessionForAgentSession(agentSessionID);
407
+ if (!liveSession) {
408
+ return { Description: undefined, Elements: [], Detail: 'no live browser' };
409
+ }
410
+ try {
411
+ const screenshot = await liveSession.CaptureScreenshot();
412
+ if (!screenshot) {
413
+ return { Description: undefined, Elements: [], Detail: 'no live browser' };
414
+ }
415
+ return await this.runVisualInterpreter(screenshot, query, contextUser);
416
+ }
417
+ catch (err) {
418
+ const message = err instanceof Error ? err.message : String(err);
419
+ LogError(`InterpretRemoteBrowserPage failed for session ${agentSessionID}: ${message}`);
420
+ return { Description: undefined, Elements: [], Detail: message };
421
+ }
422
+ }
423
+ /**
424
+ * Starts a live CDP screencast on the session's browser and PUSHES each encoded frame to the calling
425
+ * user's push-status topic — replacing the client's 700ms snapshot poll with low-latency pushed frames.
426
+ * Ownership-gated. Idempotent: a re-call for an already-streaming session is a no-op that reports
427
+ * `Streaming: true`.
428
+ *
429
+ * Capability gating: {@link IRemoteBrowserSession.StartScreencast} throws
430
+ * {@link RemoteBrowserCapabilityNotSupportedError} on a backend without `ScreenStreaming` — caught here
431
+ * and reported as `Streaming: false`, leaving the client on its polling fallback. Any other failure is
432
+ * logged and likewise reported as `Streaming: false` (the poll keeps the view alive).
433
+ *
434
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
435
+ * @returns `{ Streaming: true }` when frames are now being pushed, else `{ Streaming: false }`.
436
+ */
437
+ async StartRemoteBrowserScreencast(agentSessionID, { userPayload, providers }, pubSub) {
438
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
439
+ const session = await this.loadOwnedSession(agentSessionID, contextUser, provider);
440
+ // Idempotent: a re-bind must not stack a second screencast on the one live browser.
441
+ if (this.startedScreencasts.has(agentSessionID)) {
442
+ return { Streaming: true };
443
+ }
444
+ const providerName = await this.resolveProviderName(session, contextUser, provider);
445
+ try {
446
+ const liveSession = await RemoteBrowserEngine.Instance.StartSessionForAgentSession(agentSessionID, contextUser, providerName);
447
+ await liveSession.StartScreencast((frame) => this.publishFrame(pubSub, userPayload, agentSessionID, frame));
448
+ this.startedScreencasts.add(agentSessionID);
449
+ return { Streaming: true };
450
+ }
451
+ catch (err) {
452
+ if (err instanceof RemoteBrowserCapabilityNotSupportedError) {
453
+ // Backend can't stream — the client keeps polling. Not an error condition.
454
+ return { Streaming: false };
455
+ }
456
+ const message = err instanceof Error ? err.message : String(err);
457
+ LogError(`StartRemoteBrowserScreencast failed (provider='${providerName}'): ${message}`);
458
+ return { Streaming: false };
459
+ }
460
+ }
461
+ /**
462
+ * Stops a screencast previously started by {@link RemoteBrowserActionResolver.StartRemoteBrowserScreencast}.
463
+ * Ownership-gated and best-effort: when no live browser exists, or `StopScreencast` rejects, the call
464
+ * still resolves `true` (the client's teardown should never depend on this succeeding).
465
+ *
466
+ * @param agentSessionID The `AIAgentSession` id.
467
+ * @returns `true` (always) once the stop has been attempted.
468
+ */
469
+ async StopRemoteBrowserScreencast(agentSessionID, { userPayload, providers }) {
470
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
471
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
472
+ this.startedScreencasts.delete(agentSessionID);
473
+ const liveSession = RemoteBrowserEngine.Instance.GetSessionForAgentSession(agentSessionID);
474
+ if (liveSession) {
475
+ try {
476
+ await liveSession.StopScreencast();
477
+ }
478
+ catch (err) {
479
+ // Best-effort: a backend without ScreenStreaming throws here too; teardown ignores it.
480
+ const message = err instanceof Error ? err.message : String(err);
481
+ LogError(`StopRemoteBrowserScreencast (best-effort) for session ${agentSessionID}: ${message}`);
482
+ }
483
+ }
484
+ return true;
485
+ }
486
+ /**
487
+ * Starts streaming the session browser's TAB AUDIO and PUSHES each encoded chunk to the calling user's
488
+ * push-status topic — so a co-agent demoing a video/audio site is HEARD, not just seen. Ownership-gated.
489
+ * Idempotent: a re-call for an already-streaming session is a no-op that reports `Streaming: true`.
490
+ *
491
+ * Capability gating (v1 = by backend implementation): {@link IRemoteBrowserSession.StartAudioStream}
492
+ * throws {@link RemoteBrowserCapabilityNotSupportedError} on a backend with no audio-capture mechanism —
493
+ * caught here and reported as `Streaming: false` (the client simply plays no audio). Any other failure is
494
+ * logged and likewise reported as `Streaming: false`.
495
+ *
496
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
497
+ * @returns `{ Streaming: true }` when audio chunks are now being pushed, else `{ Streaming: false }`.
498
+ */
499
+ async StartRemoteBrowserAudioStream(agentSessionID, { userPayload, providers }, pubSub) {
500
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
501
+ const session = await this.loadOwnedSession(agentSessionID, contextUser, provider);
502
+ // Idempotent: a re-bind must not stack a second audio capture on the one live browser.
503
+ if (this.startedAudioStreams.has(agentSessionID)) {
504
+ return { Streaming: true };
505
+ }
506
+ const providerName = await this.resolveProviderName(session, contextUser, provider);
507
+ try {
508
+ const liveSession = await RemoteBrowserEngine.Instance.StartSessionForAgentSession(agentSessionID, contextUser, providerName);
509
+ await liveSession.StartAudioStream((chunk) => this.publishAudioChunk(pubSub, userPayload, agentSessionID, chunk));
510
+ this.startedAudioStreams.add(agentSessionID);
511
+ return { Streaming: true };
512
+ }
513
+ catch (err) {
514
+ if (err instanceof RemoteBrowserCapabilityNotSupportedError) {
515
+ // Backend can't capture audio — the client plays no audio. Not an error condition.
516
+ return { Streaming: false };
517
+ }
518
+ const message = err instanceof Error ? err.message : String(err);
519
+ LogError(`StartRemoteBrowserAudioStream failed (provider='${providerName}'): ${message}`);
520
+ return { Streaming: false };
521
+ }
522
+ }
523
+ /**
524
+ * Stops a tab-audio stream previously started by {@link RemoteBrowserActionResolver.StartRemoteBrowserAudioStream}.
525
+ * Ownership-gated and best-effort: when no live browser exists, or `StopAudioStream` rejects, the call
526
+ * still resolves `true` (the client's teardown should never depend on this succeeding).
527
+ *
528
+ * @param agentSessionID The `AIAgentSession` id.
529
+ * @returns `true` (always) once the stop has been attempted.
530
+ */
531
+ async StopRemoteBrowserAudioStream(agentSessionID, { userPayload, providers }) {
532
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
533
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
534
+ this.startedAudioStreams.delete(agentSessionID);
535
+ const liveSession = RemoteBrowserEngine.Instance.GetSessionForAgentSession(agentSessionID);
536
+ if (liveSession) {
537
+ try {
538
+ await liveSession.StopAudioStream();
539
+ }
540
+ catch (err) {
541
+ // Best-effort: a backend without audio capture throws here too; teardown ignores it.
542
+ const message = err instanceof Error ? err.message : String(err);
543
+ LogError(`StopRemoteBrowserAudioStream (best-effort) for session ${agentSessionID}: ${message}`);
544
+ }
545
+ }
546
+ return true;
547
+ }
548
+ /**
549
+ * Relays ONE human-takeover input (pointer move/click or key press) from the user watching the live
550
+ * screencast into the session's server-hosted browser — the "grab the wheel" path. Ownership-gated.
551
+ * The surface captures the event on the live-view canvas, maps display→viewport coordinates, and calls
552
+ * this; the resolver builds a strongly-typed {@link RemoteBrowserHumanInput} and routes it over CDP via
553
+ * {@link IRemoteBrowserSession.RouteHumanInput}.
554
+ *
555
+ * Gracefully best-effort, never throws past the ownership gate:
556
+ * - No live browser for the session → `false`.
557
+ * - Unknown/incomplete input (`buildHumanInput` returns null) → `false`.
558
+ * - Backend lacks `HumanTakeover` ({@link RemoteBrowserCapabilityNotSupportedError}) → `false` (the
559
+ * live view stays view-only for that backend).
560
+ * - Any other failure → logged + `false`.
561
+ *
562
+ * NOTE: this prototype does NOT reject by control mode here — the engine's floor arbiter governs the
563
+ * agent⇄human floor, so we just relay. Finer floor / `AgentOnly` gating is a follow-up.
564
+ *
565
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
566
+ * @param kind The input kind (`'pointer-move' | 'pointer-click' | 'pointer-down' | 'pointer-up' | 'key' | 'text' | 'scroll'`).
567
+ * @param text The pasted text (the `'text'` paste-in kind only) — inserted into the page's focused element.
568
+ * @param modifiers Optional comma-separated modifier keys held during the input (`'Shift'`, `'Control'`,
569
+ * `'Alt'`, `'Meta'`) — carries Shift-click selection and Ctrl/Cmd+key chords faithfully.
570
+ * @returns `true` when the input was routed, else `false`.
571
+ */
572
+ async RelayRemoteBrowserHumanInput(agentSessionID, kind, { userPayload, providers }, x, y, button, key, text, deltaX, deltaY, modifiers) {
573
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
574
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
575
+ const liveSession = RemoteBrowserEngine.Instance.GetSessionForAgentSession(agentSessionID);
576
+ if (!liveSession) {
577
+ return false;
578
+ }
579
+ const input = this.buildHumanInput({ kind, x, y, button, key, text, deltaX, deltaY, modifiers });
580
+ if (!input) {
581
+ return false;
582
+ }
583
+ try {
584
+ liveSession.RouteHumanInput(input);
585
+ return true;
586
+ }
587
+ catch (err) {
588
+ if (err instanceof RemoteBrowserCapabilityNotSupportedError) {
589
+ // Backend can't take human input — the live view stays view-only. Not an error condition.
590
+ return false;
591
+ }
592
+ const message = err instanceof Error ? err.message : String(err);
593
+ LogError(`RelayRemoteBrowserHumanInput failed (kind='${kind}'): ${message}`);
594
+ return false;
595
+ }
596
+ }
597
+ /**
598
+ * Returns the remote page's CURRENT text selection — the copy-out half of human clipboard support. The
599
+ * viewer captures a local `copy` / Cmd+C, calls this to read what the human selected on the live page, and
600
+ * writes the result to the LOCAL clipboard (sidestepping the isolated remote clipboard, the mirror of the
601
+ * `'text'` paste-in path). Ownership-gated.
602
+ *
603
+ * Gracefully best-effort, never throws past the ownership gate (mirrors {@link RemoteBrowserSnapshot}):
604
+ * - No live browser for the session → `{ Text: '' }`.
605
+ * - Backend lacks `HumanTakeover` ({@link RemoteBrowserCapabilityNotSupportedError}) → `{ Text: '' }`.
606
+ * - Nothing selected / any other read failure → `{ Text: '' }` (logged).
607
+ *
608
+ * @param agentSessionID The `AIAgentSession` id the browser is bound to.
609
+ * @returns The selection text, or `{ Text: '' }` when none is readable.
610
+ */
611
+ async GetRemoteBrowserSelection(agentSessionID, { userPayload, providers }) {
612
+ const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
613
+ await this.loadOwnedSession(agentSessionID, contextUser, provider);
614
+ const liveSession = RemoteBrowserEngine.Instance.GetSessionForAgentSession(agentSessionID);
615
+ if (!liveSession) {
616
+ return { Text: '' };
617
+ }
618
+ try {
619
+ return { Text: await liveSession.GetSelectionText() };
620
+ }
621
+ catch (err) {
622
+ if (err instanceof RemoteBrowserCapabilityNotSupportedError) {
623
+ // Backend can't read the selection — copy-out is simply unavailable. Not an error condition.
624
+ return { Text: '' };
625
+ }
626
+ const message = err instanceof Error ? err.message : String(err);
627
+ LogError(`GetRemoteBrowserSelection failed for session ${agentSessionID}: ${message}`);
628
+ return { Text: '' };
629
+ }
630
+ }
631
+ // ----- internals -------------------------------------------------------------------------
632
+ /**
633
+ * Publishes one encoded screencast frame to the calling user's push-status topic, in the same envelope
634
+ * shape the conversations client already routes (mirrors `RealtimeClientSessionResolver`'s delegation
635
+ * progress publish). The client matches on `resolver` + `type`, then on `agentSessionID`, and paints
636
+ * `dataBase64` onto its canvas.
637
+ *
638
+ * @param pubSub The resolver-injected pub/sub engine.
639
+ * @param userPayload The calling user's payload (its `sessionId` scopes the topic to this browser).
640
+ * @param agentSessionID The `AIAgentSession` id the frame belongs to.
641
+ * @param frame The encoded viewport frame.
642
+ */
643
+ publishFrame(pubSub, userPayload, agentSessionID, frame) {
644
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
645
+ message: JSON.stringify({
646
+ resolver: 'RemoteBrowserActionResolver',
647
+ type: 'RemoteBrowserScreencastFrame',
648
+ agentSessionID,
649
+ dataBase64: frame.DataBase64,
650
+ width: frame.Width,
651
+ height: frame.Height,
652
+ seq: frame.SequenceNumber,
653
+ }),
654
+ sessionId: userPayload.sessionId,
655
+ });
656
+ }
657
+ /**
658
+ * Publishes one encoded tab-audio chunk to the calling user's push-status topic, in the same envelope
659
+ * shape the conversations client routes for screencast frames (distinguished by `type`). The client
660
+ * matches on `resolver` + `type`, then on `agentSessionID`, and feeds `dataBase64` to its audio player.
661
+ *
662
+ * @param pubSub The resolver-injected pub/sub engine.
663
+ * @param userPayload The calling user's payload (its `sessionId` scopes the topic to this browser).
664
+ * @param agentSessionID The `AIAgentSession` id the chunk belongs to.
665
+ * @param chunk The encoded audio chunk.
666
+ */
667
+ publishAudioChunk(pubSub, userPayload, agentSessionID, chunk) {
668
+ pubSub.publish(PUSH_STATUS_UPDATES_TOPIC, {
669
+ message: JSON.stringify({
670
+ resolver: 'RemoteBrowserActionResolver',
671
+ type: 'RemoteBrowserAudioChunk',
672
+ agentSessionID,
673
+ dataBase64: chunk.DataBase64,
674
+ codec: chunk.Codec,
675
+ sampleRate: chunk.SampleRate,
676
+ channels: chunk.Channels,
677
+ seq: chunk.SequenceNumber,
678
+ }),
679
+ sessionId: userPayload.sessionId,
680
+ });
681
+ }
682
+ /**
683
+ * Runs the governable `Remote Browser Visual Interpreter` AI Prompt over a screenshot + request, and maps
684
+ * its strict-JSON output to {@link RemoteBrowserInterpretation}. The model is chosen by the PROMPT's own
685
+ * `MJ: AI Prompt Models` association (pinned to Gemini 3.1 Flash-Lite in metadata) — not hard-coded here.
686
+ * Tolerant: a missing prompt, a failed run, or unparseable output all degrade to a best-effort result.
687
+ *
688
+ * @param screenshotBase64 The raw Base64 viewport screenshot (no `data:` prefix).
689
+ * @param query The agent's request — empty/"describe" for a description, else a target to localize.
690
+ * @param contextUser The owning user (server-side multi-user safety).
691
+ * @returns The mapped interpretation.
692
+ */
693
+ async runVisualInterpreter(screenshotBase64, query, contextUser) {
694
+ await AIEngine.Instance.Config(false, contextUser);
695
+ const promptEntity = AIEngine.Instance.Prompts.find((p) => p.Name?.trim().toLowerCase() === VISUAL_INTERPRETER_PROMPT_NAME.toLowerCase());
696
+ if (!promptEntity) {
697
+ const detail = `Visual interpreter prompt '${VISUAL_INTERPRETER_PROMPT_NAME}' not found.`;
698
+ LogError(detail);
699
+ return { Description: undefined, Elements: [], Detail: detail };
700
+ }
701
+ const instruction = this.buildInterpreterInstruction(query);
702
+ const params = new AIPromptParams();
703
+ params.prompt = promptEntity;
704
+ params.contextUser = contextUser;
705
+ // The template surfaces the request via a {{ query }} variable; the screenshot rides as an image block.
706
+ params.data = { query: instruction };
707
+ params.conversationMessages = [
708
+ {
709
+ role: 'user',
710
+ content: [
711
+ { type: 'text', content: instruction },
712
+ // Declare the explicit MIME so the modality check matches a concrete
713
+ // 'image/jpeg' driver capability (not just a wildcard probe).
714
+ { type: 'image_url', content: `data:image/jpeg;base64,${screenshotBase64}`, mimeType: 'image/jpeg' },
715
+ ],
716
+ },
717
+ ];
718
+ const runner = new AIPromptRunner();
719
+ const result = await runner.ExecutePrompt(params);
720
+ if (!result.success) {
721
+ const detail = result.errorMessage ?? 'Visual interpreter prompt failed.';
722
+ LogError(`InterpretRemoteBrowserPage: vision prompt failed: ${detail}`);
723
+ return { Description: undefined, Elements: [], Detail: detail };
724
+ }
725
+ return this.mapInterpreterResult(result.rawResult);
726
+ }
727
+ /**
728
+ * Builds the natural-language instruction handed to the visual interpreter from the raw `query`. An empty
729
+ * / blank / "describe" request asks for a concise page description; anything else asks to locate that target
730
+ * and return its pixel centroid.
731
+ *
732
+ * @param query The raw request from the agent.
733
+ * @returns The instruction string.
734
+ */
735
+ buildInterpreterInstruction(query) {
736
+ const trimmed = query?.trim();
737
+ if (!trimmed || trimmed.toLowerCase() === 'describe') {
738
+ return 'Describe concisely what is visible and actionable on this page. Return an empty elements array.';
739
+ }
740
+ return (`Find this UI element and report its approximate pixel centroid: "${trimmed}". ` +
741
+ 'Also give a one-sentence description of the page for context. ' +
742
+ 'If the element is not present, return an empty elements array.');
743
+ }
744
+ /**
745
+ * Tolerantly parses the interpreter's raw text output into a {@link RemoteBrowserInterpretation}. Strips
746
+ * code fences before parsing; on parse failure returns the raw text as `Description` with no elements.
747
+ *
748
+ * @param raw The prompt's raw text output (may be null/undefined/fenced JSON).
749
+ * @returns The mapped interpretation.
750
+ */
751
+ mapInterpreterResult(raw) {
752
+ const text = (raw ?? '').trim();
753
+ if (text.length === 0) {
754
+ return { Description: undefined, Elements: [], Detail: 'Empty interpreter response.' };
755
+ }
756
+ const stripped = this.stripCodeFences(text);
757
+ let payload;
758
+ try {
759
+ const parsed = JSON.parse(stripped);
760
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
761
+ return { Description: text, Elements: [], Detail: 'Interpreter output was not a JSON object.' };
762
+ }
763
+ payload = parsed;
764
+ }
765
+ catch {
766
+ // Not JSON — hand the raw text back as the description so the agent still gets something useful.
767
+ return { Description: text, Elements: [] };
768
+ }
769
+ const elements = Array.isArray(payload.elements)
770
+ ? payload.elements
771
+ .filter((e) => typeof e?.x === 'number' && typeof e?.y === 'number')
772
+ .map((e) => ({
773
+ Label: typeof e.label === 'string' ? e.label : '',
774
+ X: e.x,
775
+ Y: e.y,
776
+ Confidence: typeof e.confidence === 'number' ? e.confidence : 0,
777
+ }))
778
+ : [];
779
+ return {
780
+ Description: typeof payload.description === 'string' ? payload.description : undefined,
781
+ Elements: elements,
782
+ };
783
+ }
784
+ /**
785
+ * Strips a leading/trailing Markdown code fence (```json … ```) from a model response so the inner JSON can
786
+ * be parsed. Returns the input unchanged when no fence is present.
787
+ *
788
+ * @param text The possibly-fenced text.
789
+ * @returns The text with any surrounding fence removed.
790
+ */
791
+ stripCodeFences(text) {
792
+ const fence = /^```(?:json)?\s*([\s\S]*?)\s*```$/i;
793
+ const match = fence.exec(text.trim());
794
+ return match ? match[1].trim() : text;
795
+ }
796
+ /** Resolve the request user + read-write provider, throwing a clear error if unauthenticated. */
797
+ requireUserAndProvider(userPayload, providers) {
798
+ const contextUser = this.GetUserFromPayload(userPayload);
799
+ if (!contextUser) {
800
+ throw new Error('Not authenticated: no user context for remote-browser operation');
801
+ }
802
+ return { contextUser, provider: GetReadWriteProvider(providers) };
803
+ }
804
+ /**
805
+ * Loads the agent session and enforces inbound ownership: the session's `UserID` must equal the
806
+ * caller's. Throws when the session is missing or owned by another user.
807
+ *
808
+ * @param agentSessionID The `AIAgentSession` id.
809
+ * @returns The loaded, owned session entity.
810
+ */
811
+ async loadOwnedSession(agentSessionID, contextUser, provider) {
812
+ const session = await provider.GetEntityObject(SESSION_ENTITY, contextUser);
813
+ if (!(await session.Load(agentSessionID))) {
814
+ throw new Error(`Remote-browser session ${agentSessionID} not found.`);
815
+ }
816
+ if (!UUIDsEqual(session.UserID, contextUser.ID)) {
817
+ throw new Error(`Not authorized: remote-browser session ${agentSessionID} is not owned by you.`);
818
+ }
819
+ return session;
820
+ }
821
+ /**
822
+ * Resolves the remote-browser backend name for the session from the session's agent's
823
+ * `TypeConfiguration` JSON (`{ remoteBrowser: { provider } }`). Best-effort: a missing agent, absent
824
+ * config, or unparseable JSON all yield `undefined`, letting the engine fall back to the single Active
825
+ * provider.
826
+ *
827
+ * @param session The owned session entity (supplies the agent id).
828
+ * @returns The configured backend name, or `undefined` to let the engine auto-select.
829
+ */
830
+ async resolveProviderName(session, contextUser, provider) {
831
+ // The session's agent IS the co-agent (the realtime voice agent), and the interactive channels
832
+ // (Remote Browser, Whiteboard) are the CO-AGENT's abilities — so the remoteBrowser backend config
833
+ // lives on the co-agent's TypeConfiguration, not on the target agent it voices.
834
+ try {
835
+ const agent = await provider.GetEntityObject(AGENT_ENTITY, contextUser);
836
+ if (!(await agent.Load(session.AgentID))) {
837
+ return undefined;
838
+ }
839
+ const config = this.parseTypeConfiguration(agent.TypeConfiguration);
840
+ const name = config?.remoteBrowser?.provider?.trim();
841
+ return name && name.length > 0 ? name : undefined;
842
+ }
843
+ catch (error) {
844
+ LogError(`ExecuteRemoteBrowserAction: failed to read agent TypeConfiguration for session ${session.ID} — ` +
845
+ `falling back to the single Active provider: ${error instanceof Error ? error.message : String(error)}`);
846
+ return undefined;
847
+ }
848
+ }
849
+ /**
850
+ * Parses an agent's `TypeConfiguration` JSON into the {@link RemoteBrowserTypeConfiguration} slice this
851
+ * resolver reads. Returns `null` for null/blank/non-object/unparseable input.
852
+ *
853
+ * @param json The raw `TypeConfiguration` JSON, or null.
854
+ * @returns The parsed config slice, or `null`.
855
+ */
856
+ parseTypeConfiguration(json) {
857
+ if (!json || json.trim().length === 0) {
858
+ return null;
859
+ }
860
+ try {
861
+ const parsed = JSON.parse(json);
862
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
863
+ }
864
+ catch {
865
+ return null;
866
+ }
867
+ }
868
+ /**
869
+ * Builds a strongly-typed {@link RemoteBrowserAction} from the relayed `kind` + fields, validating the
870
+ * fields each kind requires. Returns `null` for an unknown kind or a kind missing its required field(s).
871
+ *
872
+ * @param input The relayed action kind + all optional fields.
873
+ * @returns The built action, or `null` when the kind is unknown / incomplete.
874
+ */
875
+ buildAction(input) {
876
+ switch (input.kind) {
877
+ case 'navigate':
878
+ return input.url ? { Kind: 'navigate', Url: input.url } : null;
879
+ case 'click':
880
+ if (input.selector || (typeof input.x === 'number' && typeof input.y === 'number')) {
881
+ return { Kind: 'click', Selector: input.selector, X: input.x, Y: input.y };
882
+ }
883
+ return null;
884
+ case 'type':
885
+ return typeof input.text === 'string' ? { Kind: 'type', Text: input.text, Selector: input.selector } : null;
886
+ case 'key':
887
+ return input.key ? { Kind: 'key', Key: input.key } : null;
888
+ case 'scroll':
889
+ if (input.selector || typeof input.deltaX === 'number' || typeof input.deltaY === 'number') {
890
+ return { Kind: 'scroll', DeltaX: input.deltaX, DeltaY: input.deltaY, Selector: input.selector };
891
+ }
892
+ return null;
893
+ case 'back':
894
+ return { Kind: 'back' };
895
+ case 'forward':
896
+ return { Kind: 'forward' };
897
+ case 'wait':
898
+ if (typeof input.ms === 'number' || input.selector) {
899
+ return { Kind: 'wait', Ms: input.ms, Selector: input.selector };
900
+ }
901
+ return null;
902
+ default:
903
+ return null;
904
+ }
905
+ }
906
+ /**
907
+ * Builds a strongly-typed {@link RemoteBrowserHumanInput} from the relayed `kind` + fields, validating
908
+ * each kind's required field(s). Returns `null` for an unknown kind or a kind missing its required
909
+ * field(s): pointer-move/click/down/up need finite `x`,`y`; key needs a non-empty `key`; text needs a
910
+ * non-empty `text`; scroll needs finite `x`,`y`,`deltaX`,`deltaY`. The `button` is clamped to the allowed
911
+ * union (`'left' | 'middle' | 'right'`), defaulting unknown/absent values to `'left'`. Held `modifiers`
912
+ * (e.g. Shift-click, Ctrl/Cmd+key) ride on pointer clicks/presses and key presses.
913
+ *
914
+ * @param input The relayed input kind + all optional fields.
915
+ * @returns The built human input, or `null` when the kind is unknown / incomplete.
916
+ */
917
+ buildHumanInput(input) {
918
+ const modifiers = this.parseModifiers(input.modifiers);
919
+ const hasXy = Number.isFinite(input.x) && Number.isFinite(input.y);
920
+ switch (input.kind) {
921
+ case 'pointer-move':
922
+ return hasXy ? { Kind: 'pointer-move', X: input.x, Y: input.y } : null;
923
+ case 'pointer-click':
924
+ return hasXy
925
+ ? {
926
+ Kind: 'pointer-click',
927
+ X: input.x,
928
+ Y: input.y,
929
+ Button: this.clampButton(input.button),
930
+ ...(modifiers.length ? { Modifiers: modifiers } : {}),
931
+ }
932
+ : null;
933
+ case 'pointer-down':
934
+ return hasXy
935
+ ? {
936
+ Kind: 'pointer-down',
937
+ X: input.x,
938
+ Y: input.y,
939
+ Button: this.clampButton(input.button),
940
+ ...(modifiers.length ? { Modifiers: modifiers } : {}),
941
+ }
942
+ : null;
943
+ case 'pointer-up':
944
+ return hasXy
945
+ ? {
946
+ Kind: 'pointer-up',
947
+ X: input.x,
948
+ Y: input.y,
949
+ Button: this.clampButton(input.button),
950
+ ...(modifiers.length ? { Modifiers: modifiers } : {}),
951
+ }
952
+ : null;
953
+ case 'key':
954
+ return input.key && input.key.length > 0 ? { Kind: 'key', Key: input.key, ...(modifiers.length ? { Modifiers: modifiers } : {}) } : null;
955
+ case 'text':
956
+ // Human paste — insert the relayed clipboard text into the focused element. Empty text is a no-op.
957
+ return typeof input.text === 'string' && input.text.length > 0 ? { Kind: 'text', Text: input.text } : null;
958
+ case 'scroll':
959
+ return hasXy && Number.isFinite(input.deltaX) && Number.isFinite(input.deltaY)
960
+ ? { Kind: 'scroll', X: input.x, Y: input.y, DeltaX: input.deltaX, DeltaY: input.deltaY }
961
+ : null;
962
+ default:
963
+ return null;
964
+ }
965
+ }
966
+ /** Clamps a relayed mouse-button string to the allowed union, defaulting to `'left'`. */
967
+ clampButton(button) {
968
+ return button === 'middle' || button === 'right' ? button : 'left';
969
+ }
970
+ /**
971
+ * Parses the relayed comma-separated `modifiers` string into the validated
972
+ * {@link RemoteBrowserModifierKey} list, dropping any unrecognized token. Returns an empty array for a
973
+ * null/blank input.
974
+ *
975
+ * @param modifiers The raw `'Shift,Control'`-style CSV, or undefined.
976
+ * @returns The validated modifier list (possibly empty).
977
+ */
978
+ parseModifiers(modifiers) {
979
+ if (!modifiers) {
980
+ return [];
981
+ }
982
+ const allowed = ['Shift', 'Control', 'Alt', 'Meta'];
983
+ return modifiers
984
+ .split(',')
985
+ .map((m) => m.trim())
986
+ .filter((m) => allowed.includes(m));
987
+ }
988
+ };
989
+ __decorate([
990
+ Mutation(() => RemoteBrowserActionResult),
991
+ __param(0, Arg('agentSessionID', () => String)),
992
+ __param(1, Arg('kind', () => String)),
993
+ __param(2, Ctx()),
994
+ __param(3, Arg('url', () => String, { nullable: true })),
995
+ __param(4, Arg('selector', () => String, { nullable: true })),
996
+ __param(5, Arg('x', () => Float, { nullable: true })),
997
+ __param(6, Arg('y', () => Float, { nullable: true })),
998
+ __param(7, Arg('text', () => String, { nullable: true })),
999
+ __param(8, Arg('key', () => String, { nullable: true })),
1000
+ __param(9, Arg('deltaX', () => Float, { nullable: true })),
1001
+ __param(10, Arg('deltaY', () => Float, { nullable: true })),
1002
+ __param(11, Arg('ms', () => Float, { nullable: true })),
1003
+ __metadata("design:type", Function),
1004
+ __metadata("design:paramtypes", [String, String, Object, String, String, Number, Number, String, String, Number, Number, Number]),
1005
+ __metadata("design:returntype", Promise)
1006
+ ], RemoteBrowserActionResolver.prototype, "ExecuteRemoteBrowserAction", null);
1007
+ __decorate([
1008
+ Mutation(() => RemoteBrowserGoalResultType),
1009
+ __param(0, Arg('agentSessionID', () => String)),
1010
+ __param(1, Arg('goal', () => String)),
1011
+ __param(2, Ctx()),
1012
+ __param(3, Arg('startUrl', () => String, { nullable: true })),
1013
+ __param(4, Arg('maxSteps', () => Int, { nullable: true })),
1014
+ __param(5, Arg('preferredStrategy', () => String, { nullable: true })),
1015
+ __metadata("design:type", Function),
1016
+ __metadata("design:paramtypes", [String, String, Object, String, Number, String]),
1017
+ __metadata("design:returntype", Promise)
1018
+ ], RemoteBrowserActionResolver.prototype, "ExecuteRemoteBrowserGoal", null);
1019
+ __decorate([
1020
+ Query(() => RemoteBrowserGoalResultType),
1021
+ __param(0, Arg('agentSessionID', () => String)),
1022
+ __param(1, Arg('goalRunID', () => String)),
1023
+ __param(2, Ctx()),
1024
+ __metadata("design:type", Function),
1025
+ __metadata("design:paramtypes", [String, String, Object]),
1026
+ __metadata("design:returntype", Promise)
1027
+ ], RemoteBrowserActionResolver.prototype, "GetRemoteBrowserGoalResult", null);
1028
+ __decorate([
1029
+ Query(() => RemoteBrowserSnapshot),
1030
+ __param(0, Arg('agentSessionID', () => String)),
1031
+ __param(1, Ctx()),
1032
+ __metadata("design:type", Function),
1033
+ __metadata("design:paramtypes", [String, Object]),
1034
+ __metadata("design:returntype", Promise)
1035
+ ], RemoteBrowserActionResolver.prototype, "RemoteBrowserSnapshot", null);
1036
+ __decorate([
1037
+ Mutation(() => RemoteBrowserInterpretation),
1038
+ __param(0, Arg('agentSessionID', () => String)),
1039
+ __param(1, Ctx()),
1040
+ __param(2, Arg('query', () => String, { nullable: true })),
1041
+ __metadata("design:type", Function),
1042
+ __metadata("design:paramtypes", [String, Object, String]),
1043
+ __metadata("design:returntype", Promise)
1044
+ ], RemoteBrowserActionResolver.prototype, "InterpretRemoteBrowserPage", null);
1045
+ __decorate([
1046
+ Mutation(() => RemoteBrowserScreencastResult),
1047
+ __param(0, Arg('agentSessionID', () => String)),
1048
+ __param(1, Ctx()),
1049
+ __param(2, PubSub()),
1050
+ __metadata("design:type", Function),
1051
+ __metadata("design:paramtypes", [String, Object, PubSubEngine]),
1052
+ __metadata("design:returntype", Promise)
1053
+ ], RemoteBrowserActionResolver.prototype, "StartRemoteBrowserScreencast", null);
1054
+ __decorate([
1055
+ Mutation(() => Boolean),
1056
+ __param(0, Arg('agentSessionID', () => String)),
1057
+ __param(1, Ctx()),
1058
+ __metadata("design:type", Function),
1059
+ __metadata("design:paramtypes", [String, Object]),
1060
+ __metadata("design:returntype", Promise)
1061
+ ], RemoteBrowserActionResolver.prototype, "StopRemoteBrowserScreencast", null);
1062
+ __decorate([
1063
+ Mutation(() => RemoteBrowserAudioStreamResult),
1064
+ __param(0, Arg('agentSessionID', () => String)),
1065
+ __param(1, Ctx()),
1066
+ __param(2, PubSub()),
1067
+ __metadata("design:type", Function),
1068
+ __metadata("design:paramtypes", [String, Object, PubSubEngine]),
1069
+ __metadata("design:returntype", Promise)
1070
+ ], RemoteBrowserActionResolver.prototype, "StartRemoteBrowserAudioStream", null);
1071
+ __decorate([
1072
+ Mutation(() => Boolean),
1073
+ __param(0, Arg('agentSessionID', () => String)),
1074
+ __param(1, Ctx()),
1075
+ __metadata("design:type", Function),
1076
+ __metadata("design:paramtypes", [String, Object]),
1077
+ __metadata("design:returntype", Promise)
1078
+ ], RemoteBrowserActionResolver.prototype, "StopRemoteBrowserAudioStream", null);
1079
+ __decorate([
1080
+ Mutation(() => Boolean),
1081
+ __param(0, Arg('agentSessionID', () => String)),
1082
+ __param(1, Arg('kind', () => String)),
1083
+ __param(2, Ctx()),
1084
+ __param(3, Arg('x', () => Float, { nullable: true })),
1085
+ __param(4, Arg('y', () => Float, { nullable: true })),
1086
+ __param(5, Arg('button', () => String, { nullable: true })),
1087
+ __param(6, Arg('key', () => String, { nullable: true })),
1088
+ __param(7, Arg('text', () => String, { nullable: true })),
1089
+ __param(8, Arg('deltaX', () => Float, { nullable: true })),
1090
+ __param(9, Arg('deltaY', () => Float, { nullable: true })),
1091
+ __param(10, Arg('modifiers', () => String, { nullable: true })),
1092
+ __metadata("design:type", Function),
1093
+ __metadata("design:paramtypes", [String, String, Object, Number, Number, String, String, String, Number, Number, String]),
1094
+ __metadata("design:returntype", Promise)
1095
+ ], RemoteBrowserActionResolver.prototype, "RelayRemoteBrowserHumanInput", null);
1096
+ __decorate([
1097
+ Query(() => RemoteBrowserSelection),
1098
+ __param(0, Arg('agentSessionID', () => String)),
1099
+ __param(1, Ctx()),
1100
+ __metadata("design:type", Function),
1101
+ __metadata("design:paramtypes", [String, Object]),
1102
+ __metadata("design:returntype", Promise)
1103
+ ], RemoteBrowserActionResolver.prototype, "GetRemoteBrowserSelection", null);
1104
+ RemoteBrowserActionResolver = __decorate([
1105
+ Resolver()
1106
+ ], RemoteBrowserActionResolver);
1107
+ export { RemoteBrowserActionResolver };
1108
+ //# sourceMappingURL=RemoteBrowserActionResolver.js.map