@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,642 @@
1
+ import type { z } from 'zod';
2
+ import type { OraclePlugin } from '../plugin-api/oracle-plugin';
3
+ import type {
4
+ AgentMiddleware,
5
+ PluginContext,
6
+ PluginManifest,
7
+ PluginSubAgent,
8
+ PluginTool,
9
+ RuntimeContext,
10
+ SharedAccessors,
11
+ } from '../plugin-api/types';
12
+ import {
13
+ mergeManifestOverride,
14
+ validateExamplesAgainstTools,
15
+ type PluginManifestOverride,
16
+ } from './manifest';
17
+ import { computeSubAgentToolName } from './subagent-as-tool';
18
+
19
+ /**
20
+ * The six registries the runtime composes. Instances are created per
21
+ * `createRuntimeCore()` call — never at module scope — so an isolate that
22
+ * boots two oracles (tests, or a multi-tenant shell) never shares caches.
23
+ */
24
+ export interface Registries {
25
+ tools: ToolRegistry;
26
+ subAgents: SubAgentRegistry;
27
+ middlewares: MiddlewareRegistry;
28
+ manifests: ManifestRegistry;
29
+ configSchema: ConfigSchemaRegistry;
30
+ sharedState: SharedStateRegistry;
31
+ }
32
+
33
+ /** Build a fresh, empty set of registries. */
34
+ export function createRegistries(): Registries {
35
+ return {
36
+ tools: new ToolRegistry(),
37
+ subAgents: new SubAgentRegistry(),
38
+ middlewares: new MiddlewareRegistry(),
39
+ manifests: new ManifestRegistry(),
40
+ configSchema: new ConfigSchemaRegistry(),
41
+ sharedState: new SharedStateRegistry(),
42
+ };
43
+ }
44
+
45
+ // ── Tool registry ───────────────────────────────────────────────────────────
46
+
47
+ /** Log prefix for the registry's own diagnostics. */
48
+ const LOG_PREFIX = '[tool-registry]';
49
+
50
+ /**
51
+ * Render collected tools as `plugin=[toolA, toolB]; other=[toolC]` — the
52
+ * diagnostic that answers "which plugin contributed what on this turn".
53
+ */
54
+ export function formatByPlugin(entries: readonly RegisteredTool[]): string {
55
+ const byPlugin = new Map<string, string[]>();
56
+ for (const { pluginName, tool } of entries) {
57
+ const names = byPlugin.get(pluginName);
58
+ if (names) names.push(tool.name);
59
+ else byPlugin.set(pluginName, [tool.name]);
60
+ }
61
+ return Array.from(
62
+ byPlugin,
63
+ ([name, tools]) => `${name}=[${tools.join(', ')}]`,
64
+ ).join('; ');
65
+ }
66
+
67
+ /** A collected tool tagged with the plugin that contributed it. */
68
+ export interface RegisteredTool {
69
+ pluginName: string;
70
+ tool: PluginTool;
71
+ /**
72
+ * Which hook produced it — `getTools` (boot, cached for the isolate) or
73
+ * `getRequestTools` (recomputed every turn).
74
+ */
75
+ origin: 'boot' | 'request';
76
+ }
77
+
78
+ /** Name/description slice of a collected tool, tagged with its plugin. */
79
+ export interface ToolSummary {
80
+ pluginName: string;
81
+ name: string;
82
+ description: string;
83
+ origin: 'boot' | 'request';
84
+ }
85
+
86
+ /**
87
+ * Stores plugins that contribute tools and resolves them lazily by invoking
88
+ * each plugin's `getTools(buildCtx)` once at collection time.
89
+ *
90
+ * Tools live in a flat namespace — duplicate names across plugins are a
91
+ * boot error caught by `assertNoCollisions()`.
92
+ *
93
+ * Boot-time outputs from `getTools(buildCtx)` are cached after the first
94
+ * `collectBoot(buildCtx)` call. The request hot path only re-runs each
95
+ * plugin's `getRequestTools(rtCtx)`.
96
+ *
97
+ * ## Cache safety / cross-user isolation
98
+ *
99
+ * `PluginContext` holds no user, session, or request data — only
100
+ * `{ config, identity, availablePlugins, logger }`, all oracle-scoped and
101
+ * stable for the isolate lifetime. Cached `PluginTool` objects are therefore
102
+ * safe to share between users: their handlers receive a fresh
103
+ * `RuntimeContext` from `wrapPluginTool` on every invocation.
104
+ */
105
+ export class ToolRegistry {
106
+ private readonly plugins: OraclePlugin[] = [];
107
+ private bootCache: RegisteredTool[] | null = null;
108
+ /**
109
+ * Metadata snapshot of the most recent `collect()`. Deliberately NOT the
110
+ * tool objects: request-time tools close over that request's
111
+ * `RuntimeContext`, and retaining them here would pin a full request
112
+ * graph in memory between turns.
113
+ */
114
+ private collectedMeta: ToolSummary[] | null = null;
115
+
116
+ /** Add a plugin whose `getTools` will be called at `collect()` time. */
117
+ register(plugin: OraclePlugin): void {
118
+ this.plugins.push(plugin);
119
+ this.bootCache = null;
120
+ this.collectedMeta = null;
121
+ }
122
+
123
+ /**
124
+ * Run every plugin's `getTools(buildCtx)` once and cache the result. Safe
125
+ * to call multiple times — subsequent calls return the cached list.
126
+ */
127
+ async collectBoot(buildCtx: PluginContext): Promise<RegisteredTool[]> {
128
+ if (this.bootCache !== null) return this.bootCache;
129
+ const out: RegisteredTool[] = [];
130
+ for (const plugin of this.plugins) {
131
+ if (!plugin.getTools) continue;
132
+ const tools = await plugin.getTools(buildCtx);
133
+ for (const tool of tools) {
134
+ out.push({ pluginName: plugin.name, tool, origin: 'boot' });
135
+ }
136
+ }
137
+ this.bootCache = out;
138
+ return out;
139
+ }
140
+
141
+ /**
142
+ * Run every plugin's `getRequestTools(rtCtx)`. Does NOT touch the boot
143
+ * cache.
144
+ *
145
+ * Hooks run concurrently — several of them open network connections, so
146
+ * serializing them puts every round-trip on the chat hot path
147
+ * back-to-back. Output order stays plugin-registration order regardless of
148
+ * which hook resolves first.
149
+ *
150
+ * Failures are isolated PER PLUGIN: a hook that rejects contributes zero
151
+ * tools and is logged as an error naming the plugin, while every other
152
+ * plugin's tools still resolve.
153
+ */
154
+ async collectRequest(rtCtx: RuntimeContext): Promise<RegisteredTool[]> {
155
+ const perPlugin = await Promise.all(
156
+ this.plugins.map(async (plugin): Promise<RegisteredTool[]> => {
157
+ if (!plugin.getRequestTools) return [];
158
+ try {
159
+ const requestTools = await plugin.getRequestTools(rtCtx);
160
+ return requestTools.map((tool) => ({
161
+ pluginName: plugin.name,
162
+ tool,
163
+ origin: 'request' as const,
164
+ }));
165
+ } catch (error) {
166
+ rtCtx.logger.error(
167
+ `${LOG_PREFIX} plugin "${plugin.name}" getRequestTools failed — it contributes NO tools this turn ` +
168
+ `(other plugins are unaffected): ${
169
+ error instanceof Error ? error.message : String(error)
170
+ }`,
171
+ );
172
+ return [];
173
+ }
174
+ }),
175
+ );
176
+ const out = perPlugin.flat();
177
+ rtCtx.logger.debug?.(
178
+ `${LOG_PREFIX} request tools by plugin: ${formatByPlugin(out) || '∅'}`,
179
+ );
180
+ return out;
181
+ }
182
+
183
+ /**
184
+ * Combined boot + request collection used by the main agent build. Uses
185
+ * the cached boot output when present so per-request rebuilds skip the
186
+ * `getTools` invocations that don't depend on runtime state.
187
+ */
188
+ async collect(
189
+ buildCtx: PluginContext,
190
+ rtCtx?: RuntimeContext,
191
+ ): Promise<RegisteredTool[]> {
192
+ const boot = await this.collectBoot(buildCtx);
193
+ const request = rtCtx ? await this.collectRequest(rtCtx) : [];
194
+ const out = [...boot, ...request];
195
+ this.collectedMeta = out.map(({ pluginName, tool, origin }) => ({
196
+ pluginName,
197
+ name: tool.name,
198
+ description: tool.description,
199
+ origin,
200
+ }));
201
+ return out;
202
+ }
203
+
204
+ /** Summaries source: the last full collection, else the boot cache. */
205
+ private summaries(): ToolSummary[] {
206
+ if (this.collectedMeta !== null) return this.collectedMeta;
207
+ return (this.bootCache ?? []).map(({ pluginName, tool, origin }) => ({
208
+ pluginName,
209
+ name: tool.name,
210
+ description: tool.description,
211
+ origin,
212
+ }));
213
+ }
214
+
215
+ /** The flat list of tool names produced by the most recent `collect()`. */
216
+ toolNames(): string[] {
217
+ return this.summaries().map((entry) => entry.name);
218
+ }
219
+
220
+ /**
221
+ * Tool names contributed by a given plugin in the most recent `collect()`
222
+ * (or `collectBoot()` if no full collection has happened yet).
223
+ */
224
+ toolNamesForPlugin(pluginName: string): string[] {
225
+ return this.summaries()
226
+ .filter((entry) => entry.pluginName === pluginName)
227
+ .map((entry) => entry.name);
228
+ }
229
+
230
+ /**
231
+ * Name/description of the tools a given plugin contributed in the most
232
+ * recent collection. Summaries, not the tool objects — see `collectedMeta`.
233
+ */
234
+ toolSummariesForPlugin(
235
+ pluginName: string,
236
+ ): Array<{ name: string; description: string }> {
237
+ return this.summaries()
238
+ .filter((entry) => entry.pluginName === pluginName)
239
+ .map(({ name, description }) => ({ name, description }));
240
+ }
241
+
242
+ /**
243
+ * Throw if two plugins contribute a tool with the same name. The error
244
+ * message names every colliding plugin pair so the boot log points at the
245
+ * actual conflict.
246
+ */
247
+ assertNoCollisions(): void {
248
+ if (this.collectedMeta === null && this.bootCache === null) {
249
+ throw new Error('ToolRegistry.assertNoCollisions called before collect');
250
+ }
251
+ const seen = new Map<string, string>();
252
+ const collisions: string[] = [];
253
+ for (const { pluginName, name } of this.summaries()) {
254
+ const prev = seen.get(name);
255
+ if (prev !== undefined && prev !== pluginName) {
256
+ collisions.push(
257
+ `Tool "${name}" registered by both "${prev}" and "${pluginName}"`,
258
+ );
259
+ } else if (prev === undefined) {
260
+ seen.set(name, pluginName);
261
+ }
262
+ }
263
+ if (collisions.length > 0) {
264
+ throw new Error(
265
+ `ToolRegistry: tool name collisions detected:\n - ${collisions.join('\n - ')}`,
266
+ );
267
+ }
268
+ }
269
+ }
270
+
271
+ // ── Sub-agent registry ──────────────────────────────────────────────────────
272
+
273
+ /** A collected sub-agent tagged with the plugin that contributed it. */
274
+ export interface RegisteredSubAgent {
275
+ pluginName: string;
276
+ subAgent: PluginSubAgent;
277
+ }
278
+
279
+ /**
280
+ * Stores plugins that contribute sub-agents and resolves them by invoking
281
+ * each plugin's `getSubAgents(buildCtx)` at collection time.
282
+ *
283
+ * Sub-agents are wrapped as tools by the runtime, so their names share the
284
+ * tool namespace. Duplicate sub-agent names across plugins are a boot error.
285
+ */
286
+ export class SubAgentRegistry {
287
+ private readonly plugins: OraclePlugin[] = [];
288
+ private bootCache: RegisteredSubAgent[] | null = null;
289
+ /** Names-only snapshot of the most recent `collect()` (see ToolRegistry). */
290
+ private collectedNames: Array<{
291
+ pluginName: string;
292
+ subAgentName: string;
293
+ }> | null = null;
294
+
295
+ /** Add a plugin whose `getSubAgents` will be called at `collect()` time. */
296
+ register(plugin: OraclePlugin): void {
297
+ this.plugins.push(plugin);
298
+ this.bootCache = null;
299
+ this.collectedNames = null;
300
+ }
301
+
302
+ /** Run every plugin's `getSubAgents(buildCtx)` once and cache the result. */
303
+ collectBoot(buildCtx: PluginContext): RegisteredSubAgent[] {
304
+ if (this.bootCache !== null) return this.bootCache;
305
+ const out: RegisteredSubAgent[] = [];
306
+ for (const plugin of this.plugins) {
307
+ if (!plugin.getSubAgents) continue;
308
+ const subAgents = plugin.getSubAgents(buildCtx);
309
+ for (const subAgent of subAgents) {
310
+ out.push({ pluginName: plugin.name, subAgent });
311
+ }
312
+ }
313
+ this.bootCache = out;
314
+ return out;
315
+ }
316
+
317
+ /**
318
+ * Run every plugin's `getRequestSubAgents(rtCtx)`. Does NOT touch the boot
319
+ * cache. Hooks run concurrently; output order stays registration order.
320
+ * Failures are isolated per plugin, mirroring `ToolRegistry.collectRequest`.
321
+ */
322
+ async collectRequest(rtCtx: RuntimeContext): Promise<RegisteredSubAgent[]> {
323
+ const perPlugin = await Promise.all(
324
+ this.plugins.map(async (plugin): Promise<RegisteredSubAgent[]> => {
325
+ if (!plugin.getRequestSubAgents) return [];
326
+ try {
327
+ const requestSubAgents = await plugin.getRequestSubAgents(rtCtx);
328
+ return requestSubAgents.map((subAgent) => ({
329
+ pluginName: plugin.name,
330
+ subAgent,
331
+ }));
332
+ } catch (error) {
333
+ rtCtx.logger.error(
334
+ `[subagent-registry] plugin "${plugin.name}" getRequestSubAgents failed — it contributes NO sub-agents this turn ` +
335
+ `(other plugins are unaffected): ${
336
+ error instanceof Error ? error.message : String(error)
337
+ }`,
338
+ );
339
+ return [];
340
+ }
341
+ }),
342
+ );
343
+ return perPlugin.flat();
344
+ }
345
+
346
+ /** Combined boot + request collection used by the main agent build. */
347
+ async collect(
348
+ buildCtx: PluginContext,
349
+ rtCtx?: RuntimeContext,
350
+ ): Promise<RegisteredSubAgent[]> {
351
+ const boot = this.collectBoot(buildCtx);
352
+ const request = rtCtx ? await this.collectRequest(rtCtx) : [];
353
+ const out = [...boot, ...request];
354
+ this.collectedNames = out.map(({ pluginName, subAgent }) => ({
355
+ pluginName,
356
+ subAgentName: subAgent.name,
357
+ }));
358
+ return out;
359
+ }
360
+
361
+ /** Names source: the last full collection, else the boot cache. */
362
+ private names(): Array<{ pluginName: string; subAgentName: string }> {
363
+ if (this.collectedNames !== null) return this.collectedNames;
364
+ return (this.bootCache ?? []).map(({ pluginName, subAgent }) => ({
365
+ pluginName,
366
+ subAgentName: subAgent.name,
367
+ }));
368
+ }
369
+
370
+ /**
371
+ * The *wrapped* tool names contributed by a given plugin in the most recent
372
+ * collection. Each entry passes through `computeSubAgentToolName` — the
373
+ * same transform `createSubagentAsTool` applies — so the names returned
374
+ * here match what the agent will actually see.
375
+ */
376
+ subAgentNamesForPlugin(pluginName: string): string[] {
377
+ return this.names()
378
+ .filter((entry) => entry.pluginName === pluginName)
379
+ .map((entry) => computeSubAgentToolName(entry.subAgentName));
380
+ }
381
+
382
+ /**
383
+ * Throw if two plugins contribute sub-agents with the same name. The error
384
+ * message names both plugin names so the boot log points at the conflict.
385
+ */
386
+ assertNoCollisions(): void {
387
+ if (this.collectedNames === null && this.bootCache === null) {
388
+ throw new Error(
389
+ 'SubAgentRegistry.assertNoCollisions called before collect',
390
+ );
391
+ }
392
+ const seen = new Map<string, string>();
393
+ const collisions: string[] = [];
394
+ for (const { pluginName, subAgentName } of this.names()) {
395
+ const prev = seen.get(subAgentName);
396
+ if (prev !== undefined && prev !== pluginName) {
397
+ collisions.push(
398
+ `Sub-agent "${subAgentName}" registered by both "${prev}" and "${pluginName}"`,
399
+ );
400
+ } else if (prev === undefined) {
401
+ seen.set(subAgentName, pluginName);
402
+ }
403
+ }
404
+ if (collisions.length > 0) {
405
+ throw new Error(
406
+ `SubAgentRegistry: sub-agent name collisions detected:\n - ${collisions.join('\n - ')}`,
407
+ );
408
+ }
409
+ }
410
+ }
411
+
412
+ // ── Middleware registry ─────────────────────────────────────────────────────
413
+
414
+ /** A collected middleware tagged with the plugin that contributed it. */
415
+ export interface RegisteredMiddleware {
416
+ pluginName: string;
417
+ middleware: AgentMiddleware;
418
+ }
419
+
420
+ /**
421
+ * Stores plugins that contribute middlewares and resolves them by invoking
422
+ * each plugin's `getMiddlewares(buildCtx)` at collection time.
423
+ *
424
+ * Order is preserved as registration order — the loader applies any
425
+ * dependency-driven topological reordering before registering plugins here.
426
+ *
427
+ * Middlewares are boot-time-only — there's no per-request hook. The
428
+ * registry caches the first `collect(buildCtx)` result.
429
+ */
430
+ export class MiddlewareRegistry {
431
+ private readonly plugins: OraclePlugin[] = [];
432
+ private bootCache: RegisteredMiddleware[] | null = null;
433
+
434
+ register(plugin: OraclePlugin): void {
435
+ this.plugins.push(plugin);
436
+ this.bootCache = null;
437
+ }
438
+
439
+ collect(buildCtx: PluginContext): RegisteredMiddleware[] {
440
+ if (this.bootCache !== null) return this.bootCache;
441
+ const out: RegisteredMiddleware[] = [];
442
+ for (const plugin of this.plugins) {
443
+ if (!plugin.getMiddlewares) continue;
444
+ const middlewares = plugin.getMiddlewares(buildCtx);
445
+ for (const middleware of middlewares) {
446
+ out.push({ pluginName: plugin.name, middleware });
447
+ }
448
+ }
449
+ this.bootCache = out;
450
+ return out;
451
+ }
452
+
453
+ /** Middlewares have no names, so no collision is possible. */
454
+ assertNoCollisions(): void {
455
+ // Intentional no-op — middleware order is established by the loader.
456
+ }
457
+ }
458
+
459
+ // ── Manifest registry ───────────────────────────────────────────────────────
460
+
461
+ /** A collected manifest tagged with the plugin that contributed it. */
462
+ export interface RegisteredManifest {
463
+ pluginName: string;
464
+ manifest: PluginManifest;
465
+ }
466
+
467
+ /** Result of cross-checking every plugin's example tools against a ToolRegistry. */
468
+ export interface ManifestCrossCheckResult {
469
+ errors: string[];
470
+ }
471
+
472
+ /**
473
+ * Stores plugin manifests and wires the cross-tool-reference check that can
474
+ * only run once tool registration is complete.
475
+ */
476
+ export class ManifestRegistry {
477
+ private readonly entries: RegisteredManifest[] = [];
478
+
479
+ /**
480
+ * Record a plugin's manifest. A fork-supplied `override` is merged shallowly
481
+ * over the plugin's own manifest, so every downstream reader sees the
482
+ * effective manifest.
483
+ */
484
+ register(plugin: OraclePlugin, override?: PluginManifestOverride): void {
485
+ this.entries.push({
486
+ pluginName: plugin.name,
487
+ manifest: mergeManifestOverride(plugin.manifest, override),
488
+ });
489
+ }
490
+
491
+ /** Registered manifests in registration order. */
492
+ collect(): RegisteredManifest[] {
493
+ return [...this.entries];
494
+ }
495
+
496
+ /**
497
+ * For each registered manifest, validate that every `examples[].tool`
498
+ * reference exists in the supplied registries. Sub-agents are first-class
499
+ * tools to the agent — they get wrapped as `call_<name>` — so the
500
+ * validator unions tool names with sub-agent wrapped names. Both
501
+ * registries MUST have been collected before calling this.
502
+ */
503
+ validateAgainstTools(
504
+ toolRegistry: ToolRegistry,
505
+ subAgentRegistry: SubAgentRegistry,
506
+ ): ManifestCrossCheckResult {
507
+ const errors: string[] = [];
508
+ for (const { pluginName, manifest } of this.entries) {
509
+ const toolNames = [
510
+ ...toolRegistry.toolNamesForPlugin(pluginName),
511
+ ...subAgentRegistry.subAgentNamesForPlugin(pluginName),
512
+ ];
513
+ const result = validateExamplesAgainstTools(
514
+ manifest,
515
+ toolNames,
516
+ pluginName,
517
+ );
518
+ errors.push(...result.errors);
519
+ }
520
+ return { errors };
521
+ }
522
+
523
+ /** Manifest title collisions are display-only, so this is a no-op. */
524
+ assertNoCollisions(): void {
525
+ // Intentional no-op.
526
+ }
527
+ }
528
+
529
+ // ── Config-schema registry ──────────────────────────────────────────────────
530
+
531
+ /** A collected configSchema tagged with the plugin that contributed it. */
532
+ export interface RegisteredConfigSchema {
533
+ pluginName: string;
534
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
535
+ schema: z.ZodObject<any>;
536
+ }
537
+
538
+ /**
539
+ * Stores plugin-owned `configSchema` objects in registration order. The
540
+ * actual Zod merge — collision detection, plugin-ownership tracking, env
541
+ * validation — lives in `env.ts` via `composeEnvSchema()`.
542
+ */
543
+ export class ConfigSchemaRegistry {
544
+ private readonly entries: RegisteredConfigSchema[] = [];
545
+
546
+ register(plugin: OraclePlugin): void {
547
+ if (!plugin.configSchema) return;
548
+ this.entries.push({
549
+ pluginName: plugin.name,
550
+ schema: plugin.configSchema,
551
+ });
552
+ }
553
+
554
+ collect(): RegisteredConfigSchema[] {
555
+ return [...this.entries];
556
+ }
557
+
558
+ /** Config keys may collide intentionally (later wins, with warning). */
559
+ assertNoCollisions(): void {
560
+ // intentional no-op
561
+ }
562
+ }
563
+
564
+ // ── Shared-state registry ───────────────────────────────────────────────────
565
+
566
+ /** A single read accessor, contributed by a plugin under a unique key. */
567
+ type SharedAccessorFn = (
568
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
569
+ state: any,
570
+ runCtx: RuntimeContext,
571
+ ) => unknown;
572
+
573
+ /** A collected accessor tagged with the plugin that contributed it. */
574
+ export interface RegisteredSharedAccessor {
575
+ pluginName: string;
576
+ key: string;
577
+ accessor: SharedAccessorFn;
578
+ }
579
+
580
+ /**
581
+ * Stores read-only accessors that plugins expose to other plugins via
582
+ * `runCtx.shared`. Key collisions across plugins are a boot error.
583
+ */
584
+ export class SharedStateRegistry {
585
+ private readonly entries: RegisteredSharedAccessor[] = [];
586
+
587
+ /** Record a plugin's shared accessors. Plugins without any are skipped. */
588
+ register(plugin: OraclePlugin): void {
589
+ if (!plugin.getSharedState) return;
590
+ const map = plugin.getSharedState();
591
+ for (const [key, accessor] of Object.entries(map)) {
592
+ this.entries.push({ pluginName: plugin.name, key, accessor });
593
+ }
594
+ }
595
+
596
+ /** Return the registered accessors in registration order. */
597
+ collect(): RegisteredSharedAccessor[] {
598
+ return [...this.entries];
599
+ }
600
+
601
+ /**
602
+ * Build a `SharedAccessors` snapshot from the supplied state and runtime
603
+ * context by invoking each registered accessor lazily as keys are read, so
604
+ * an accessor that throws does not break unrelated `ctx.shared.<key>` reads.
605
+ */
606
+ build(
607
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
608
+ state: any,
609
+ runCtx: RuntimeContext,
610
+ ): SharedAccessors {
611
+ const target: Record<string, unknown> = {};
612
+ for (const { key, accessor } of this.entries) {
613
+ Object.defineProperty(target, key, {
614
+ enumerable: true,
615
+ configurable: false,
616
+ get: () => accessor(state, runCtx),
617
+ });
618
+ }
619
+ return target;
620
+ }
621
+
622
+ /** Throw if two plugins contribute accessors under the same key. */
623
+ assertNoCollisions(): void {
624
+ const seen = new Map<string, string>();
625
+ const collisions: string[] = [];
626
+ for (const { pluginName, key } of this.entries) {
627
+ const prev = seen.get(key);
628
+ if (prev !== undefined && prev !== pluginName) {
629
+ collisions.push(
630
+ `Shared-state key "${key}" registered by both "${prev}" and "${pluginName}"`,
631
+ );
632
+ } else if (prev === undefined) {
633
+ seen.set(key, pluginName);
634
+ }
635
+ }
636
+ if (collisions.length > 0) {
637
+ throw new Error(
638
+ `SharedStateRegistry: shared-state key collisions detected:\n - ${collisions.join('\n - ')}`,
639
+ );
640
+ }
641
+ }
642
+ }