@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.
- package/README.md +110 -0
- package/package.json +77 -0
- package/src/attachments/classify.ts +114 -0
- package/src/attachments/content-blocks.ts +51 -0
- package/src/attachments/download.ts +235 -0
- package/src/attachments/extract.ts +241 -0
- package/src/attachments/index.ts +84 -0
- package/src/attachments/magic.ts +133 -0
- package/src/attachments/pipeline.ts +409 -0
- package/src/attachments/retention.ts +205 -0
- package/src/attachments/route.ts +41 -0
- package/src/attachments/sandbox-archive.ts +90 -0
- package/src/attachments/sanitize.ts +35 -0
- package/src/attachments/types.ts +86 -0
- package/src/attachments/view.ts +107 -0
- package/src/core/env.ts +200 -0
- package/src/core/index.ts +513 -0
- package/src/core/llm.ts +842 -0
- package/src/core/main-agent-types.ts +123 -0
- package/src/core/main-agent.ts +399 -0
- package/src/core/manifest.ts +365 -0
- package/src/core/meta-tools.ts +232 -0
- package/src/core/middlewares/byo-history-sanitizer.ts +165 -0
- package/src/core/middlewares/capability-gate.ts +76 -0
- package/src/core/middlewares/index.ts +18 -0
- package/src/core/middlewares/page-context.ts +98 -0
- package/src/core/middlewares/safety-guardrail.ts +117 -0
- package/src/core/middlewares/summarization.ts +99 -0
- package/src/core/middlewares/tool-repetition-guard.ts +125 -0
- package/src/core/middlewares/tool-validation.ts +83 -0
- package/src/core/middlewares/work-status.ts +57 -0
- package/src/core/openrouter-pricing.ts +124 -0
- package/src/core/plugin-loader.ts +192 -0
- package/src/core/plugins/index.ts +6 -0
- package/src/core/plugins/skills/index.ts +8 -0
- package/src/core/plugins/skills/skills-tools.ts +356 -0
- package/src/core/plugins/skills/skills.plugin.ts +113 -0
- package/src/core/plugins/weather/index.ts +8 -0
- package/src/core/plugins/weather/weather-client.ts +153 -0
- package/src/core/plugins/weather/weather-middleware.ts +27 -0
- package/src/core/plugins/weather/weather-sub-agent.ts +65 -0
- package/src/core/plugins/weather/weather-tools.ts +103 -0
- package/src/core/plugins/weather/weather.plugin.ts +227 -0
- package/src/core/prompt-composer.ts +469 -0
- package/src/core/registries.ts +642 -0
- package/src/core/runtime-context.ts +619 -0
- package/src/core/state.ts +108 -0
- package/src/core/sub-agent-fallback.ts +187 -0
- package/src/core/subagent-as-tool.ts +322 -0
- package/src/core/template.ts +245 -0
- package/src/core/utils.ts +246 -0
- package/src/core/wrap-plugin-tool.ts +100 -0
- package/src/do/ambient.ts +266 -0
- package/src/do/contracts.ts +562 -0
- package/src/do/debug-timers.ts +120 -0
- package/src/do/fresh-stub.ts +32 -0
- package/src/do/gateway-retry.ts +73 -0
- package/src/do/secrets-adapter.ts +32 -0
- package/src/do/size-alerts.ts +76 -0
- package/src/do/sse-stream.ts +472 -0
- package/src/do/transcript.ts +192 -0
- package/src/do/turn-body.ts +84 -0
- package/src/do/turn-metadata.ts +146 -0
- package/src/do/ucan-service.ts +339 -0
- package/src/do/user-oracle-do.ts +3035 -0
- package/src/gateway-worker.ts +82 -0
- package/src/index.ts +237 -0
- package/src/llm/byo-adapter.ts +59 -0
- package/src/llm/byo-catalog.ts +520 -0
- package/src/llm/byo-client.ts +280 -0
- package/src/llm/byo-routes.ts +255 -0
- package/src/llm/byo-service.ts +836 -0
- package/src/llm/chatgpt-oauth.ts +430 -0
- package/src/llm/provider-error.ts +283 -0
- package/src/matrix/gateway-do.ts +918 -0
- package/src/matrix/ingest.ts +209 -0
- package/src/matrix/replay-format.ts +34 -0
- package/src/matrix/reply-chain.ts +109 -0
- package/src/matrix/room-state-codec.ts +115 -0
- package/src/matrix/user-homeserver.ts +106 -0
- package/src/matrix/work-status.ts +294 -0
- package/src/memory/session-history-indexer.ts +328 -0
- package/src/owner-store/ixo-vfs-store.ts +570 -0
- package/src/owner-store/matrix-media-store.ts +136 -0
- package/src/owner-store/migrating-store.ts +185 -0
- package/src/owner-store/owner-copy-errors.ts +175 -0
- package/src/owner-store/retry.ts +45 -0
- package/src/owner-store/tus-upload.ts +333 -0
- package/src/owner-store/types.ts +0 -0
- package/src/plugin-api/define-plugin.ts +45 -0
- package/src/plugin-api/index.ts +56 -0
- package/src/plugin-api/oracle-plugin.ts +145 -0
- package/src/plugin-api/tool-helper.ts +82 -0
- package/src/plugin-api/types.ts +816 -0
- package/src/plugin-api/ucan-errors.ts +16 -0
- package/src/plugins/agui/agui-agent.ts +115 -0
- package/src/plugins/agui/agui.plugin.ts +148 -0
- package/src/plugins/agui/index.ts +2 -0
- package/src/plugins/attachments/attachments.plugin.ts +54 -0
- package/src/plugins/attachments/index.ts +6 -0
- package/src/plugins/attachments/view-attachment-tool.ts +91 -0
- package/src/plugins/base64.ts +26 -0
- package/src/plugins/composio/composio-tools.ts +365 -0
- package/src/plugins/composio/composio-ucan.ts +43 -0
- package/src/plugins/composio/composio.plugin.ts +214 -0
- package/src/plugins/composio/index.ts +15 -0
- package/src/plugins/domain-indexer/domain-indexer-agent.ts +57 -0
- package/src/plugins/domain-indexer/domain-indexer-tools.ts +253 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +100 -0
- package/src/plugins/domain-indexer/index.ts +3 -0
- package/src/plugins/editor/blocknote-bridge.ts +120 -0
- package/src/plugins/editor/content-session.ts +331 -0
- package/src/plugins/editor/content-tools.ts +686 -0
- package/src/plugins/editor/document-model.ts +0 -0
- package/src/plugins/editor/editor-access-denied-tool.ts +58 -0
- package/src/plugins/editor/editor-agent.ts +34 -0
- package/src/plugins/editor/editor-config.ts +64 -0
- package/src/plugins/editor/editor-mx.ts +128 -0
- package/src/plugins/editor/editor.plugin.ts +143 -0
- package/src/plugins/editor/failures.ts +154 -0
- package/src/plugins/editor/index.ts +31 -0
- package/src/plugins/editor/jsdom-shim.ts +55 -0
- package/src/plugins/editor/prompts.ts +59 -0
- package/src/plugins/editor/prop-policy.ts +147 -0
- package/src/plugins/editor/provider.ts +386 -0
- package/src/plugins/editor/room-membership.ts +95 -0
- package/src/plugins/editor/standalone-editor-tool.ts +200 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +93 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +220 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +105 -0
- package/src/plugins/firecrawl/index.ts +15 -0
- package/src/plugins/flows/action-metadata.ts +3505 -0
- package/src/plugins/flows/actions.ts +190 -0
- package/src/plugins/flows/edit.ts +314 -0
- package/src/plugins/flows/errors.ts +38 -0
- package/src/plugins/flows/explain.ts +43 -0
- package/src/plugins/flows/flow-doc.ts +111 -0
- package/src/plugins/flows/flows.plugin.ts +129 -0
- package/src/plugins/flows/forms.ts +330 -0
- package/src/plugins/flows/index.ts +2 -0
- package/src/plugins/flows/linkage.ts +147 -0
- package/src/plugins/flows/port-types.ts +53 -0
- package/src/plugins/flows/prompts.ts +65 -0
- package/src/plugins/flows/read.ts +392 -0
- package/src/plugins/flows/references.ts +66 -0
- package/src/plugins/flows/test-support.ts +165 -0
- package/src/plugins/flows/tools/authoring.ts +516 -0
- package/src/plugins/flows/tools/discovery.ts +119 -0
- package/src/plugins/flows/tools/forms.ts +142 -0
- package/src/plugins/flows/tools/inspect.ts +165 -0
- package/src/plugins/flows/tools/linkage.ts +117 -0
- package/src/plugins/flows/tools/settings.ts +184 -0
- package/src/plugins/flows/translator.ts +328 -0
- package/src/plugins/flows/types.ts +210 -0
- package/src/plugins/flows/ydoc-helpers.ts +543 -0
- package/src/plugins/index.ts +70 -0
- package/src/plugins/mcp-call-timeout.ts +38 -0
- package/src/plugins/mcp-tool-adapter.ts +87 -0
- package/src/plugins/memory/index.ts +17 -0
- package/src/plugins/memory/memory-tools.ts +463 -0
- package/src/plugins/memory/memory-ucan.ts +51 -0
- package/src/plugins/memory/memory.plugin.ts +122 -0
- package/src/plugins/memory/types.ts +147 -0
- package/src/plugins/portal/action-log.ts +34 -0
- package/src/plugins/portal/index.ts +2 -0
- package/src/plugins/portal/portal.plugin.ts +203 -0
- package/src/plugins/sandbox/index.ts +34 -0
- package/src/plugins/sandbox/sandbox-bridge.ts +295 -0
- package/src/plugins/sandbox/sandbox-mcp.ts +127 -0
- package/src/plugins/sandbox/sandbox-write-blob.ts +103 -0
- package/src/plugins/sandbox/sandbox.plugin.ts +599 -0
- package/src/plugins/tasks/index.ts +17 -0
- package/src/plugins/tasks/manifest.ts +81 -0
- package/src/plugins/tasks/middleware.ts +219 -0
- package/src/plugins/tasks/tasks-tools.ts +414 -0
- package/src/plugins/tasks/tasks.plugin.ts +74 -0
- package/src/plugins/ucan-failure.ts +133 -0
- package/src/plugins/user-preferences/index.ts +22 -0
- package/src/plugins/user-preferences/schema.ts +31 -0
- package/src/plugins/user-preferences/user-preferences-store.ts +172 -0
- package/src/plugins/user-preferences/user-preferences-tool.ts +131 -0
- package/src/plugins/user-preferences/user-preferences.plugin.ts +137 -0
- package/src/plugins/vfs/index.ts +40 -0
- package/src/plugins/vfs/vfs-auth.ts +56 -0
- package/src/plugins/vfs/vfs-client.ts +655 -0
- package/src/plugins/vfs/vfs-content.ts +181 -0
- package/src/plugins/vfs/vfs-errors.ts +163 -0
- package/src/plugins/vfs/vfs-sandbox-tools.ts +272 -0
- package/src/plugins/vfs/vfs-tools.ts +635 -0
- package/src/plugins/vfs/vfs.plugin.ts +201 -0
- package/src/realtime/frontend-call-registry.ts +163 -0
- package/src/realtime/realtime-endpoint.ts +619 -0
- package/src/realtime/session-socket-hub.ts +249 -0
- package/src/realtime/socket-io-codec.ts +187 -0
- package/src/secrets/jwe.ts +74 -0
- package/src/secrets/pin-cipher.ts +43 -0
- package/src/secrets/secrets-service.ts +319 -0
- package/src/shell/app.ts +564 -0
- package/src/shell/auth.ts +290 -0
- package/src/sqlite/blob-codec.ts +100 -0
- package/src/sqlite/blob-compactor.ts +146 -0
- package/src/sqlite/cache-config.ts +45 -0
- package/src/sqlite/database.ts +643 -0
- package/src/sqlite/do-vfs.ts +1485 -0
- package/src/sqlite/index.ts +61 -0
- package/src/sqlite/serialization.ts +370 -0
- package/src/sqlite/sessions-store.ts +311 -0
- package/src/sqlite/sqlite-saver.ts +919 -0
- package/src/sqlite/vacuum-policy.ts +77 -0
- package/src/sqlite/wa-sqlite-loader.ts +123 -0
- package/src/sqlite/wasm.d.ts +10 -0
- package/src/tasks/schedule.ts +174 -0
- package/src/tasks/scheduler.ts +753 -0
- package/src/tasks/spec.ts +125 -0
- 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
|
+
}
|