@ixo/oracle-runtime-workers 0.2.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 (215) hide show
  1. package/README.md +110 -0
  2. package/package.json +77 -0
  3. package/src/attachments/classify.ts +114 -0
  4. package/src/attachments/content-blocks.ts +51 -0
  5. package/src/attachments/download.ts +235 -0
  6. package/src/attachments/extract.ts +241 -0
  7. package/src/attachments/index.ts +84 -0
  8. package/src/attachments/magic.ts +133 -0
  9. package/src/attachments/pipeline.ts +409 -0
  10. package/src/attachments/retention.ts +205 -0
  11. package/src/attachments/route.ts +41 -0
  12. package/src/attachments/sandbox-archive.ts +90 -0
  13. package/src/attachments/sanitize.ts +35 -0
  14. package/src/attachments/types.ts +86 -0
  15. package/src/attachments/view.ts +107 -0
  16. package/src/core/env.ts +200 -0
  17. package/src/core/index.ts +513 -0
  18. package/src/core/llm.ts +842 -0
  19. package/src/core/main-agent-types.ts +123 -0
  20. package/src/core/main-agent.ts +399 -0
  21. package/src/core/manifest.ts +365 -0
  22. package/src/core/meta-tools.ts +232 -0
  23. package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
  24. package/src/core/middlewares/capability-gate.ts +76 -0
  25. package/src/core/middlewares/index.ts +18 -0
  26. package/src/core/middlewares/page-context.ts +98 -0
  27. package/src/core/middlewares/safety-guardrail.ts +117 -0
  28. package/src/core/middlewares/summarization.ts +99 -0
  29. package/src/core/middlewares/tool-repetition-guard.ts +125 -0
  30. package/src/core/middlewares/tool-validation.ts +83 -0
  31. package/src/core/middlewares/work-status.ts +57 -0
  32. package/src/core/openrouter-pricing.ts +124 -0
  33. package/src/core/plugin-loader.ts +192 -0
  34. package/src/core/plugins/index.ts +6 -0
  35. package/src/core/plugins/skills/index.ts +8 -0
  36. package/src/core/plugins/skills/skills-tools.ts +356 -0
  37. package/src/core/plugins/skills/skills.plugin.ts +113 -0
  38. package/src/core/plugins/weather/index.ts +8 -0
  39. package/src/core/plugins/weather/weather-client.ts +153 -0
  40. package/src/core/plugins/weather/weather-middleware.ts +27 -0
  41. package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
  42. package/src/core/plugins/weather/weather-tools.ts +103 -0
  43. package/src/core/plugins/weather/weather.plugin.ts +227 -0
  44. package/src/core/prompt-composer.ts +469 -0
  45. package/src/core/registries.ts +642 -0
  46. package/src/core/runtime-context.ts +619 -0
  47. package/src/core/state.ts +108 -0
  48. package/src/core/sub-agent-fallback.ts +187 -0
  49. package/src/core/subagent-as-tool.ts +322 -0
  50. package/src/core/template.ts +245 -0
  51. package/src/core/utils.ts +246 -0
  52. package/src/core/wrap-plugin-tool.ts +100 -0
  53. package/src/do/ambient.ts +266 -0
  54. package/src/do/contracts.ts +562 -0
  55. package/src/do/debug-timers.ts +120 -0
  56. package/src/do/fresh-stub.ts +32 -0
  57. package/src/do/gateway-retry.ts +73 -0
  58. package/src/do/secrets-adapter.ts +32 -0
  59. package/src/do/size-alerts.ts +76 -0
  60. package/src/do/sse-stream.ts +472 -0
  61. package/src/do/transcript.ts +192 -0
  62. package/src/do/turn-body.ts +84 -0
  63. package/src/do/turn-metadata.ts +146 -0
  64. package/src/do/ucan-service.ts +339 -0
  65. package/src/do/user-oracle-do.ts +3035 -0
  66. package/src/gateway-worker.ts +82 -0
  67. package/src/index.ts +237 -0
  68. package/src/llm/byo-adapter.ts +59 -0
  69. package/src/llm/byo-catalog.ts +520 -0
  70. package/src/llm/byo-client.ts +280 -0
  71. package/src/llm/byo-routes.ts +255 -0
  72. package/src/llm/byo-service.ts +836 -0
  73. package/src/llm/chatgpt-oauth.ts +430 -0
  74. package/src/llm/provider-error.ts +283 -0
  75. package/src/matrix/gateway-do.ts +918 -0
  76. package/src/matrix/ingest.ts +209 -0
  77. package/src/matrix/replay-format.ts +34 -0
  78. package/src/matrix/reply-chain.ts +109 -0
  79. package/src/matrix/room-state-codec.ts +115 -0
  80. package/src/matrix/user-homeserver.ts +106 -0
  81. package/src/matrix/work-status.ts +294 -0
  82. package/src/memory/session-history-indexer.ts +328 -0
  83. package/src/owner-store/ixo-vfs-store.ts +570 -0
  84. package/src/owner-store/matrix-media-store.ts +136 -0
  85. package/src/owner-store/migrating-store.ts +185 -0
  86. package/src/owner-store/owner-copy-errors.ts +175 -0
  87. package/src/owner-store/retry.ts +45 -0
  88. package/src/owner-store/tus-upload.ts +333 -0
  89. package/src/owner-store/types.ts +0 -0
  90. package/src/plugin-api/define-plugin.ts +45 -0
  91. package/src/plugin-api/index.ts +56 -0
  92. package/src/plugin-api/oracle-plugin.ts +145 -0
  93. package/src/plugin-api/tool-helper.ts +82 -0
  94. package/src/plugin-api/types.ts +816 -0
  95. package/src/plugin-api/ucan-errors.ts +16 -0
  96. package/src/plugins/agui/agui-agent.ts +115 -0
  97. package/src/plugins/agui/agui.plugin.ts +148 -0
  98. package/src/plugins/agui/index.ts +2 -0
  99. package/src/plugins/attachments/attachments.plugin.ts +54 -0
  100. package/src/plugins/attachments/index.ts +6 -0
  101. package/src/plugins/attachments/view-attachment-tool.ts +91 -0
  102. package/src/plugins/base64.ts +26 -0
  103. package/src/plugins/composio/composio-tools.ts +365 -0
  104. package/src/plugins/composio/composio-ucan.ts +43 -0
  105. package/src/plugins/composio/composio.plugin.ts +214 -0
  106. package/src/plugins/composio/index.ts +15 -0
  107. package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
  108. package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
  109. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
  110. package/src/plugins/domain-indexer/index.ts +3 -0
  111. package/src/plugins/editor/blocknote-bridge.ts +120 -0
  112. package/src/plugins/editor/content-session.ts +331 -0
  113. package/src/plugins/editor/content-tools.ts +686 -0
  114. package/src/plugins/editor/document-model.ts +0 -0
  115. package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
  116. package/src/plugins/editor/editor-agent.ts +34 -0
  117. package/src/plugins/editor/editor-config.ts +64 -0
  118. package/src/plugins/editor/editor-mx.ts +128 -0
  119. package/src/plugins/editor/editor.plugin.ts +143 -0
  120. package/src/plugins/editor/failures.ts +154 -0
  121. package/src/plugins/editor/index.ts +31 -0
  122. package/src/plugins/editor/jsdom-shim.ts +55 -0
  123. package/src/plugins/editor/prompts.ts +59 -0
  124. package/src/plugins/editor/prop-policy.ts +147 -0
  125. package/src/plugins/editor/provider.ts +386 -0
  126. package/src/plugins/editor/room-membership.ts +95 -0
  127. package/src/plugins/editor/standalone-editor-tool.ts +200 -0
  128. package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
  129. package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
  130. package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
  131. package/src/plugins/firecrawl/index.ts +15 -0
  132. package/src/plugins/flows/action-metadata.ts +3505 -0
  133. package/src/plugins/flows/actions.ts +190 -0
  134. package/src/plugins/flows/edit.ts +314 -0
  135. package/src/plugins/flows/errors.ts +38 -0
  136. package/src/plugins/flows/explain.ts +43 -0
  137. package/src/plugins/flows/flow-doc.ts +111 -0
  138. package/src/plugins/flows/flows.plugin.ts +129 -0
  139. package/src/plugins/flows/forms.ts +330 -0
  140. package/src/plugins/flows/index.ts +2 -0
  141. package/src/plugins/flows/linkage.ts +147 -0
  142. package/src/plugins/flows/port-types.ts +53 -0
  143. package/src/plugins/flows/prompts.ts +65 -0
  144. package/src/plugins/flows/read.ts +392 -0
  145. package/src/plugins/flows/references.ts +66 -0
  146. package/src/plugins/flows/test-support.ts +165 -0
  147. package/src/plugins/flows/tools/authoring.ts +516 -0
  148. package/src/plugins/flows/tools/discovery.ts +119 -0
  149. package/src/plugins/flows/tools/forms.ts +142 -0
  150. package/src/plugins/flows/tools/inspect.ts +165 -0
  151. package/src/plugins/flows/tools/linkage.ts +117 -0
  152. package/src/plugins/flows/tools/settings.ts +184 -0
  153. package/src/plugins/flows/translator.ts +328 -0
  154. package/src/plugins/flows/types.ts +210 -0
  155. package/src/plugins/flows/ydoc-helpers.ts +543 -0
  156. package/src/plugins/index.ts +70 -0
  157. package/src/plugins/mcp-call-timeout.ts +38 -0
  158. package/src/plugins/mcp-tool-adapter.ts +87 -0
  159. package/src/plugins/memory/index.ts +17 -0
  160. package/src/plugins/memory/memory-tools.ts +463 -0
  161. package/src/plugins/memory/memory-ucan.ts +51 -0
  162. package/src/plugins/memory/memory.plugin.ts +122 -0
  163. package/src/plugins/memory/types.ts +147 -0
  164. package/src/plugins/portal/action-log.ts +34 -0
  165. package/src/plugins/portal/index.ts +2 -0
  166. package/src/plugins/portal/portal.plugin.ts +203 -0
  167. package/src/plugins/sandbox/index.ts +34 -0
  168. package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
  169. package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
  170. package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
  171. package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
  172. package/src/plugins/tasks/index.ts +17 -0
  173. package/src/plugins/tasks/manifest.ts +81 -0
  174. package/src/plugins/tasks/middleware.ts +219 -0
  175. package/src/plugins/tasks/tasks-tools.ts +414 -0
  176. package/src/plugins/tasks/tasks.plugin.ts +74 -0
  177. package/src/plugins/ucan-failure.ts +133 -0
  178. package/src/plugins/user-preferences/index.ts +22 -0
  179. package/src/plugins/user-preferences/schema.ts +31 -0
  180. package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
  181. package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
  182. package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
  183. package/src/plugins/vfs/index.ts +40 -0
  184. package/src/plugins/vfs/vfs-auth.ts +56 -0
  185. package/src/plugins/vfs/vfs-client.ts +655 -0
  186. package/src/plugins/vfs/vfs-content.ts +181 -0
  187. package/src/plugins/vfs/vfs-errors.ts +163 -0
  188. package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
  189. package/src/plugins/vfs/vfs-tools.ts +635 -0
  190. package/src/plugins/vfs/vfs.plugin.ts +201 -0
  191. package/src/realtime/frontend-call-registry.ts +163 -0
  192. package/src/realtime/realtime-endpoint.ts +619 -0
  193. package/src/realtime/session-socket-hub.ts +249 -0
  194. package/src/realtime/socket-io-codec.ts +187 -0
  195. package/src/secrets/jwe.ts +74 -0
  196. package/src/secrets/pin-cipher.ts +43 -0
  197. package/src/secrets/secrets-service.ts +319 -0
  198. package/src/shell/app.ts +564 -0
  199. package/src/shell/auth.ts +290 -0
  200. package/src/sqlite/blob-codec.ts +100 -0
  201. package/src/sqlite/blob-compactor.ts +146 -0
  202. package/src/sqlite/cache-config.ts +45 -0
  203. package/src/sqlite/database.ts +643 -0
  204. package/src/sqlite/do-vfs.ts +1485 -0
  205. package/src/sqlite/index.ts +61 -0
  206. package/src/sqlite/serialization.ts +370 -0
  207. package/src/sqlite/sessions-store.ts +311 -0
  208. package/src/sqlite/sqlite-saver.ts +919 -0
  209. package/src/sqlite/vacuum-policy.ts +77 -0
  210. package/src/sqlite/wa-sqlite-loader.ts +123 -0
  211. package/src/sqlite/wasm.d.ts +10 -0
  212. package/src/tasks/schedule.ts +174 -0
  213. package/src/tasks/scheduler.ts +753 -0
  214. package/src/tasks/spec.ts +125 -0
  215. package/src/tasks/store.ts +351 -0
@@ -0,0 +1,201 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ UCAN_STORE_DEFAULT_URLS,
4
+ VFS_DEFAULT_BASE_URLS,
5
+ } from '../../owner-store/ixo-vfs-store';
6
+ import { OraclePlugin } from '../../plugin-api/oracle-plugin';
7
+ import type {
8
+ PluginManifest,
9
+ PluginTool,
10
+ RuntimeContext,
11
+ } from '../../plugin-api/types';
12
+ import type { SandboxMcpClientFactory } from '../sandbox/sandbox.plugin';
13
+ import { createVfsSandboxTools } from './vfs-sandbox-tools';
14
+ import { createVfsTools } from './vfs-tools';
15
+
16
+ /** Optional tuning env this plugin owns (the worker URLs come from NETWORK). */
17
+ const configSchema = z.object({
18
+ VFS_MAX_READ_LINES: z.coerce.number().int().positive().default(2000),
19
+ VFS_REQUEST_TIMEOUT_MS: z.coerce.number().int().positive().default(20000),
20
+ });
21
+
22
+ /**
23
+ * Sibling env the VFS plugin reads but does not own. `NETWORK` (owned by the
24
+ * base runtime schema) selects the worker URLs; `VFS_BASE_URL` /
25
+ * `UCAN_STORE_URL` (also base-schema keys, used by the VFS owner store) act
26
+ * as per-deployment overrides; `SANDBOX_MCP_URL` (owned by the sandbox
27
+ * plugin), when set, adds the two sandbox↔files bridge tools so a file can
28
+ * move between the two without its bytes passing through the LLM.
29
+ */
30
+ const siblingEnvSchema = z.object({
31
+ NETWORK: z.enum(['mainnet', 'testnet', 'devnet']).optional(),
32
+ VFS_BASE_URL: z.string().url().optional(),
33
+ UCAN_STORE_URL: z.string().url().optional(),
34
+ SANDBOX_MCP_URL: z.string().url().optional(),
35
+ });
36
+
37
+ /** Runtime VFS config the client + tools consume. URLs derived from NETWORK. */
38
+ export interface VfsConfig {
39
+ VFS_BASE_URL: string;
40
+ UCAN_STORE_URL: string;
41
+ VFS_MAX_READ_LINES: number;
42
+ VFS_REQUEST_TIMEOUT_MS: number;
43
+ }
44
+
45
+ const manifest: PluginManifest = {
46
+ title: 'Files',
47
+ summary:
48
+ "The user's Virtual Filesystem (VFS) — their persistent, secure, governed filesystem: the canonical home for their real documents, notes, datasets, and artifacts, in folders, searchable and versioned linked to their domain. Reusable context that lives across sessions — not scratch files, not chat attachments, not the web. Securely stored and access-controlled (managed encryption); authorized IXO services can read content to power search, previews, and these tools, so it is NOT end-to-end encrypted — never describe it as zero-knowledge or unreadable by IXO.",
49
+ category: 'data',
50
+ // On-demand with two exceptions: `vfs_search` and `vfs_read` carry per-tool
51
+ // `visibility: 'always'` so recall ("what did my notes say…") works with no
52
+ // load step. The write/organise/share surface (and the sandbox bridges)
53
+ // loads via `load_capability(['vfs'])` — keeping ~11 tool schemas out of
54
+ // every unrelated chat turn.
55
+ visibility: 'on-demand',
56
+ whenToUse: [
57
+ "The user refers to a document, note, file, folder, or something they 'saved', 'uploaded', or 'shared with you'.",
58
+ 'You need to create, update, or organise a file for the user (draft, report, notes, export) that should persist.',
59
+ 'You need to find or quote something from the user\'s files ("what did my notes say about X", "find the contract").',
60
+ 'You can reuse a file the user already has instead of asking them to paste or re-upload it.',
61
+ 'The user asks you to share a file or make it downloadable.',
62
+ "Persist something the sandbox produced (a report, export, chart, or dataset under /workspace/…) into the user's files so it survives the session — use `sandbox_to_vfs`.",
63
+ "Feed one of the user's files into the sandbox so `sandbox_run` code can process it — use `vfs_to_sandbox` (writes under /workspace/data/).",
64
+ 'Remember you act ONLY within the files and rights the user granted you. If a file action reports you have no access (or only read-only), relay the short grant steps the tool returns — with your agent DID — so the user can authorize you, then retry. Never claim a file is missing when the real issue is access.',
65
+ ],
66
+ whenNotToUse: [
67
+ "General knowledge or web questions — the files are the user's private content, not a knowledge base of the world.",
68
+ "Content the user pasted directly into chat (act on that inline; don't write it to a file unless asked).",
69
+ "Temporary/scratch or intermediate files produced during a code or compute run — those belong to the sandbox, not the user's filesystem.",
70
+ 'Another plugin owns the data (flows, skills, memory) — use that plugin.',
71
+ ],
72
+ examples: [
73
+ {
74
+ user: 'What did my project notes say about the launch date?',
75
+ thought: "Search the user's files, then read the relevant lines.",
76
+ tool: 'vfs_search',
77
+ args: { q: 'launch date', path: '/' },
78
+ },
79
+ {
80
+ user: 'Save this summary as notes/meeting-2026-07.md',
81
+ thought: 'Create the file at that path.',
82
+ tool: 'vfs_write',
83
+ args: { path: '/notes/meeting-2026-07.md' },
84
+ },
85
+ {
86
+ user: 'In todo.md change "Draft" to "Final"',
87
+ thought: 'Exact-string edit.',
88
+ tool: 'vfs_edit',
89
+ args: { path: '/todo.md', oldString: 'Draft', newString: 'Final' },
90
+ },
91
+ {
92
+ user: 'Share my resume so I can send a link',
93
+ thought: 'Publish it and return the link.',
94
+ tool: 'vfs_share',
95
+ args: { path: '/resume.pdf', public: true },
96
+ },
97
+ ],
98
+ };
99
+
100
+ /** Optional dependency injection — primarily for tests. */
101
+ export interface VfsPluginOptions {
102
+ /** Injected transport for the VFS client. Defaults to global `fetch`. */
103
+ fetchImpl?: typeof fetch;
104
+ /** Retry backoff forwarded to the VFS client. Tests pass `0`. */
105
+ retryDelayMs?: number;
106
+ /** Injected sandbox MCP-client factory for the two bridge tools. */
107
+ sandboxMcpClientFactory?: SandboxMcpClientFactory;
108
+ }
109
+
110
+ /**
111
+ * VFS plugin.
112
+ *
113
+ * Contributes ten file tools (`vfs_search`, `vfs_grep`, `vfs_glob`,
114
+ * `vfs_list`, `vfs_read`, `vfs_write`, `vfs_edit`, `vfs_move`, `vfs_delete`,
115
+ * `vfs_share`) that read, create, edit, search, organise, and share a user's
116
+ * files on the IXO Virtual Filesystem. The oracle acts as the user, inside the
117
+ * folder the user delegated to it; each tool resolves a fresh single-use UCAN
118
+ * bearer per call.
119
+ *
120
+ * When the sandbox is also configured (`SANDBOX_MCP_URL`, a sibling env owned
121
+ * by the sandbox plugin), two extra bridge tools are added — `sandbox_to_vfs`
122
+ * and `vfs_to_sandbox` — that move a file between the user's sandbox and their
123
+ * files entirely server-side, so the bytes never pass through the LLM.
124
+ *
125
+ * Tools are request-time (`getRequestTools`) because auth needs the live
126
+ * `rtCtx.user.did` and the user's deposited delegation. With no signing key,
127
+ * or invalid config, the plugin contributes nothing and the agent simply
128
+ * doesn't see the filesystem that turn. Always on — no `autoDetect`.
129
+ */
130
+ export class VfsPlugin extends OraclePlugin {
131
+ readonly name = 'vfs';
132
+
133
+ readonly version = '1.0.0';
134
+
135
+ readonly manifest = manifest;
136
+
137
+ override readonly configSchema = configSchema;
138
+
139
+ private readonly options: VfsPluginOptions;
140
+
141
+ constructor(options: VfsPluginOptions = {}) {
142
+ super();
143
+ this.options = options;
144
+ }
145
+
146
+ override getRequestTools(rtCtx: RuntimeContext): PluginTool[] {
147
+ if (!rtCtx.ucan.hasSigningKey()) {
148
+ rtCtx.logger.warn('[vfs] skipping — oracle has no UCAN signing key.');
149
+ return [];
150
+ }
151
+
152
+ const tuning = configSchema.safeParse(rtCtx.config);
153
+ const siblings = siblingEnvSchema.safeParse(rtCtx.config);
154
+ const network = (siblings.success && siblings.data.NETWORK) || 'mainnet';
155
+ const cfg: VfsConfig = {
156
+ VFS_BASE_URL:
157
+ (siblings.success && siblings.data.VFS_BASE_URL) ||
158
+ VFS_DEFAULT_BASE_URLS[network] ||
159
+ // Unreachable: `network` is one of the three keys the map declares.
160
+ 'https://vfs.ixo.earth',
161
+ UCAN_STORE_URL:
162
+ (siblings.success && siblings.data.UCAN_STORE_URL) ||
163
+ UCAN_STORE_DEFAULT_URLS[network] ||
164
+ 'https://store.ucan.ixo.earth',
165
+ VFS_MAX_READ_LINES: tuning.success
166
+ ? tuning.data.VFS_MAX_READ_LINES
167
+ : 2000,
168
+ VFS_REQUEST_TIMEOUT_MS: tuning.success
169
+ ? tuning.data.VFS_REQUEST_TIMEOUT_MS
170
+ : 20000,
171
+ };
172
+
173
+ const tools = createVfsTools({
174
+ cfg,
175
+ fetchImpl: this.options.fetchImpl,
176
+ retryDelayMs: this.options.retryDelayMs,
177
+ });
178
+
179
+ // When the sandbox is configured, add the two sandbox↔files bridge tools.
180
+ // The sandbox MCP client is built lazily inside each handler, so adding
181
+ // them costs nothing until one is actually called.
182
+ const sandboxMcpUrl = siblings.success
183
+ ? siblings.data.SANDBOX_MCP_URL
184
+ : undefined;
185
+ if (sandboxMcpUrl) {
186
+ tools.push(
187
+ ...createVfsSandboxTools(
188
+ {
189
+ vfsCfg: cfg,
190
+ sandboxMcpUrl,
191
+ vfsFetchImpl: this.options.fetchImpl,
192
+ vfsRetryDelayMs: this.options.retryDelayMs,
193
+ },
194
+ this.options.sandboxMcpClientFactory,
195
+ ),
196
+ );
197
+ }
198
+
199
+ return tools;
200
+ }
201
+ }
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Pending frontend (browser-tool / AG-UI action) calls awaiting a result
3
+ * from the user's browser — the Workers counterpart of the Node runtime's
4
+ * `callFrontendTool`, which parks a promise on `rootEventEmitter` until the
5
+ * socket delivers `browser_tool_result` / `action_call_result` for the same
6
+ * `toolCallId`.
7
+ *
8
+ * Settlement rules are Node's, verbatim:
9
+ * - `error` set → reject(Error(error))
10
+ * - AG-UI and `result.success === false` → reject(Error(result.error ?? 'Action failed'))
11
+ * - otherwise → resolve(result)
12
+ * - no result within `timeoutMs` → reject("<kind> timeout after <ms>ms: <tool>")
13
+ *
14
+ * Purely in-memory: a pending call does not survive an object restart, and
15
+ * neither does the turn that made it.
16
+ */
17
+
18
+ export type FrontendCallKind = 'browser' | 'agui';
19
+
20
+ export interface FrontendCallResult {
21
+ toolCallId: string;
22
+ result?: unknown;
23
+ error?: string;
24
+ }
25
+
26
+ export interface PendingFrontendCall {
27
+ kind: FrontendCallKind;
28
+ toolCallId: string;
29
+ toolName: string;
30
+ sessionId: string;
31
+ startedAt: number;
32
+ }
33
+
34
+ interface Pending extends PendingFrontendCall {
35
+ resolve: (value: unknown) => void;
36
+ reject: (error: Error) => void;
37
+ timer: ReturnType<typeof setTimeout>;
38
+ }
39
+
40
+ const KIND_LABEL: Record<FrontendCallKind, string> = {
41
+ browser: 'Browser tool',
42
+ agui: 'AG-UI action',
43
+ };
44
+
45
+ function isRecord(value: unknown): value is Record<string, unknown> {
46
+ return typeof value === 'object' && value !== null;
47
+ }
48
+
49
+ function readSuccessFlag(result: unknown): {
50
+ success?: boolean;
51
+ error?: string;
52
+ } {
53
+ if (!isRecord(result)) return {};
54
+ const record = result;
55
+ return {
56
+ ...(typeof record.success === 'boolean' ? { success: record.success } : {}),
57
+ ...(typeof record.error === 'string' ? { error: record.error } : {}),
58
+ };
59
+ }
60
+
61
+ export class FrontendCallRegistry {
62
+ private readonly pending = new Map<string, Pending>();
63
+
64
+ private key(kind: FrontendCallKind, toolCallId: string): string {
65
+ return `${kind}:${toolCallId}`;
66
+ }
67
+
68
+ /**
69
+ * Park a call until `settle` delivers its result or `timeoutMs` elapses.
70
+ * `signal` (the turn's abort signal) rejects early so an aborted turn does
71
+ * not keep a browser waiting.
72
+ */
73
+ wait(
74
+ call: Omit<PendingFrontendCall, 'startedAt'>,
75
+ opts: { timeoutMs: number; signal?: AbortSignal },
76
+ ): Promise<unknown> {
77
+ const key = this.key(call.kind, call.toolCallId);
78
+ const existing = this.pending.get(key);
79
+ if (existing) {
80
+ this.finish(key);
81
+ existing.reject(
82
+ new Error(`${KIND_LABEL[call.kind]} ${call.toolCallId} was superseded`),
83
+ );
84
+ }
85
+ return new Promise<unknown>((resolve, reject) => {
86
+ const timer = setTimeout(() => {
87
+ this.finish(key);
88
+ reject(
89
+ new Error(
90
+ `${KIND_LABEL[call.kind]} timeout after ${opts.timeoutMs}ms: ${call.toolName}`,
91
+ ),
92
+ );
93
+ }, opts.timeoutMs);
94
+ const entry: Pending = {
95
+ ...call,
96
+ startedAt: Date.now(),
97
+ resolve,
98
+ reject,
99
+ timer,
100
+ };
101
+ this.pending.set(key, entry);
102
+ if (opts.signal) {
103
+ const onAbort = () => {
104
+ if (this.pending.get(key) !== entry) return;
105
+ this.finish(key);
106
+ reject(
107
+ new Error(`${KIND_LABEL[call.kind]} ${call.toolName} aborted`),
108
+ );
109
+ };
110
+ if (opts.signal.aborted) onAbort();
111
+ else opts.signal.addEventListener('abort', onAbort, { once: true });
112
+ }
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Deliver a result from the socket. Returns false when nothing was waiting
118
+ * for that id (late, duplicate or unknown result — logged by the caller).
119
+ */
120
+ settle(kind: FrontendCallKind, data: FrontendCallResult): boolean {
121
+ const key = this.key(kind, data.toolCallId);
122
+ const entry = this.pending.get(key);
123
+ if (!entry) return false;
124
+ this.finish(key);
125
+ if (data.error) {
126
+ entry.reject(new Error(data.error));
127
+ return true;
128
+ }
129
+ if (kind === 'agui') {
130
+ const flags = readSuccessFlag(data.result);
131
+ if (flags.success === false) {
132
+ entry.reject(new Error(flags.error || 'Action failed'));
133
+ return true;
134
+ }
135
+ }
136
+ entry.resolve(data.result);
137
+ return true;
138
+ }
139
+
140
+ /** Pending calls, oldest first (diagnostics / status). */
141
+ list(): PendingFrontendCall[] {
142
+ return [...this.pending.values()]
143
+ .map(({ kind, toolCallId, toolName, sessionId, startedAt }) => ({
144
+ kind,
145
+ toolCallId,
146
+ toolName,
147
+ sessionId,
148
+ startedAt,
149
+ }))
150
+ .sort((a, b) => a.startedAt - b.startedAt);
151
+ }
152
+
153
+ get size(): number {
154
+ return this.pending.size;
155
+ }
156
+
157
+ private finish(key: string): void {
158
+ const entry = this.pending.get(key);
159
+ if (!entry) return;
160
+ clearTimeout(entry.timer);
161
+ this.pending.delete(key);
162
+ }
163
+ }