@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,365 @@
1
+ import { z } from 'zod';
2
+ import type { ManifestExample, PluginManifest } from '../plugin-api/types';
3
+
4
+ // ── Schema ──────────────────────────────────────────────────────────────────
5
+
6
+ /** Few-shot example teaching the agent how to invoke the plugin. */
7
+ export const manifestExampleSchema: z.ZodType<ManifestExample> = z.object({
8
+ user: z.string(),
9
+ thought: z.string().optional(),
10
+ tool: z.string(),
11
+ args: z.record(z.string(), z.unknown()).optional(),
12
+ });
13
+
14
+ /** Allowed `category` values, kept aligned with the `PluginManifest` interface. */
15
+ export const manifestCategorySchema = z.enum([
16
+ 'data',
17
+ 'communication',
18
+ 'automation',
19
+ 'memory',
20
+ 'integration',
21
+ 'ui',
22
+ 'auth',
23
+ 'observability',
24
+ 'core',
25
+ ]);
26
+
27
+ /** Allowed `visibility` values. */
28
+ export const manifestVisibilitySchema = z.enum([
29
+ 'always',
30
+ 'on-demand',
31
+ 'silent',
32
+ ]);
33
+
34
+ /** Allowed `stability` values. */
35
+ export const manifestStabilitySchema = z.enum([
36
+ 'stable',
37
+ 'beta',
38
+ 'experimental',
39
+ ]);
40
+
41
+ /** Zod schema mirroring the `PluginManifest` TypeScript interface. */
42
+ export const pluginManifestSchema: z.ZodType<PluginManifest> = z.object({
43
+ title: z.string(),
44
+ summary: z.string(),
45
+ whenToUse: z.array(z.string()),
46
+ whenNotToUse: z.array(z.string()).optional(),
47
+ examples: z.array(manifestExampleSchema).optional(),
48
+ tags: z.array(z.string()).optional(),
49
+ category: manifestCategorySchema.optional(),
50
+ visibility: manifestVisibilitySchema.optional(),
51
+ stability: manifestStabilitySchema.optional(),
52
+ });
53
+
54
+ // ── Validator ───────────────────────────────────────────────────────────────
55
+
56
+ /** Result of validating a single plugin manifest. */
57
+ export interface ManifestValidationResult {
58
+ valid: boolean;
59
+ errors: string[];
60
+ warnings: string[];
61
+ }
62
+
63
+ /** Soft-cap thresholds — violations only emit warnings. */
64
+ const SOFT_LIMITS = {
65
+ summaryMaxChars: 120,
66
+ whenToUseMaxItems: 8,
67
+ whenToUseItemMaxChars: 100,
68
+ whenNotToUseMaxItems: 4,
69
+ whenNotToUseItemMaxChars: 80,
70
+ examplesMaxItems: 3,
71
+ } as const;
72
+
73
+ /** Format a per-plugin, per-field message prefix. */
74
+ function tag(pluginName: string, fieldPath: string): string {
75
+ return `[${pluginName}] ${fieldPath}:`;
76
+ }
77
+
78
+ /**
79
+ * Validate a plugin manifest at boot time.
80
+ *
81
+ * Hard rules push to `errors` and set `valid: false`.
82
+ * Soft rules push to `warnings` only — `valid` stays `true`.
83
+ *
84
+ * Cross-tool reference checking lives in `validateExamplesAgainstTools`
85
+ * because the registered tool list is only known once the plugin's
86
+ * `getTools()` has run.
87
+ */
88
+ export function validateManifest(
89
+ manifest: unknown,
90
+ pluginName: string,
91
+ ): ManifestValidationResult {
92
+ const errors: string[] = [];
93
+ const warnings: string[] = [];
94
+
95
+ const parsed = pluginManifestSchema.safeParse(manifest);
96
+ if (!parsed.success) {
97
+ for (const issue of parsed.error.issues) {
98
+ const path = issue.path.length > 0 ? issue.path.join('.') : '<root>';
99
+ errors.push(`${tag(pluginName, path)} ${issue.message}`);
100
+ }
101
+ return { valid: false, errors, warnings };
102
+ }
103
+
104
+ const m = parsed.data;
105
+
106
+ // Hard rule: summary must be non-empty.
107
+ if (m.summary.trim().length === 0) {
108
+ errors.push(`${tag(pluginName, 'summary')} must be non-empty.`);
109
+ }
110
+
111
+ // Hard rule: whenToUse must have ≥1 entry unless plugin is silent.
112
+ const visibility = m.visibility ?? 'on-demand';
113
+ if (visibility !== 'silent' && m.whenToUse.length < 1) {
114
+ errors.push(
115
+ `${tag(pluginName, 'whenToUse')} must contain at least one entry when visibility is '${visibility}'.`,
116
+ );
117
+ }
118
+
119
+ // Hard rule: tags, if present, must all be lowercase.
120
+ if (m.tags) {
121
+ m.tags.forEach((t, i) => {
122
+ if (t !== t.toLowerCase()) {
123
+ errors.push(
124
+ `${tag(pluginName, `tags[${i}]`)} must be lowercase (got "${t}").`,
125
+ );
126
+ }
127
+ });
128
+ }
129
+
130
+ // Soft rule: summary length cap.
131
+ if (m.summary.length > SOFT_LIMITS.summaryMaxChars) {
132
+ warnings.push(
133
+ `${tag(pluginName, 'summary')} is ${m.summary.length} chars (recommended ≤ ${SOFT_LIMITS.summaryMaxChars}).`,
134
+ );
135
+ }
136
+
137
+ // Soft rule: whenToUse caps.
138
+ if (m.whenToUse.length > SOFT_LIMITS.whenToUseMaxItems) {
139
+ warnings.push(
140
+ `${tag(pluginName, 'whenToUse')} has ${m.whenToUse.length} items (recommended ≤ ${SOFT_LIMITS.whenToUseMaxItems}).`,
141
+ );
142
+ }
143
+ m.whenToUse.forEach((line, i) => {
144
+ if (line.length > SOFT_LIMITS.whenToUseItemMaxChars) {
145
+ warnings.push(
146
+ `${tag(pluginName, `whenToUse[${i}]`)} is ${line.length} chars (recommended ≤ ${SOFT_LIMITS.whenToUseItemMaxChars}).`,
147
+ );
148
+ }
149
+ });
150
+
151
+ // Soft rule: whenNotToUse caps.
152
+ if (m.whenNotToUse) {
153
+ if (m.whenNotToUse.length > SOFT_LIMITS.whenNotToUseMaxItems) {
154
+ warnings.push(
155
+ `${tag(pluginName, 'whenNotToUse')} has ${m.whenNotToUse.length} items (recommended ≤ ${SOFT_LIMITS.whenNotToUseMaxItems}).`,
156
+ );
157
+ }
158
+ m.whenNotToUse.forEach((line, i) => {
159
+ if (line.length > SOFT_LIMITS.whenNotToUseItemMaxChars) {
160
+ warnings.push(
161
+ `${tag(pluginName, `whenNotToUse[${i}]`)} is ${line.length} chars (recommended ≤ ${SOFT_LIMITS.whenNotToUseItemMaxChars}).`,
162
+ );
163
+ }
164
+ });
165
+ }
166
+
167
+ // Soft rule: examples cap.
168
+ if (m.examples && m.examples.length > SOFT_LIMITS.examplesMaxItems) {
169
+ warnings.push(
170
+ `${tag(pluginName, 'examples')} has ${m.examples.length} items (recommended ≤ ${SOFT_LIMITS.examplesMaxItems}).`,
171
+ );
172
+ }
173
+
174
+ return { valid: errors.length === 0, errors, warnings };
175
+ }
176
+
177
+ /**
178
+ * Cross-check `examples[].tool` references against the set of tool names
179
+ * actually registered by this plugin. Run after the plugin's `getTools()`
180
+ * has been invoked.
181
+ *
182
+ * **Lenient for request-time plugins**: when `registeredToolNames` is empty,
183
+ * the plugin's tools come from `getRequestTools` — boot-time validation
184
+ * can't see them. We skip the cross-check in that case rather than
185
+ * false-flag every example. Plugins with boot-time tools still get the
186
+ * full check, so stale `examples[].tool` references are caught.
187
+ */
188
+ export function validateExamplesAgainstTools(
189
+ manifest: PluginManifest,
190
+ registeredToolNames: string[],
191
+ pluginName: string,
192
+ ): { errors: string[] } {
193
+ const errors: string[] = [];
194
+ if (!manifest.examples) return { errors };
195
+
196
+ if (registeredToolNames.length === 0) return { errors };
197
+
198
+ const known = new Set(registeredToolNames);
199
+ manifest.examples.forEach((ex, i) => {
200
+ if (!known.has(ex.tool)) {
201
+ errors.push(
202
+ `${tag(pluginName, `examples[${i}].tool`)} references unknown tool "${ex.tool}".`,
203
+ );
204
+ }
205
+ });
206
+
207
+ return { errors };
208
+ }
209
+
210
+ // ── Override merge ──────────────────────────────────────────────────────────
211
+
212
+ /**
213
+ * Fork-supplied overrides for a plugin's manifest. Merged shallowly over the
214
+ * plugin's own `manifest` at boot — keys present in the override win, absent
215
+ * keys keep the plugin default.
216
+ */
217
+ export type PluginManifestOverride = Partial<PluginManifest>;
218
+
219
+ /**
220
+ * Shallow-merge a manifest override onto a base manifest. Only keys the
221
+ * override actually sets take effect — an `undefined` value is treated as
222
+ * "leave the base alone" so a sparse override never blanks out a field.
223
+ *
224
+ * Returns the base manifest unchanged (same reference) when there is no
225
+ * override or the override is empty, so callers can cheaply skip work.
226
+ */
227
+ export function mergeManifestOverride(
228
+ base: PluginManifest,
229
+ override?: PluginManifestOverride,
230
+ ): PluginManifest {
231
+ if (!override) return base;
232
+
233
+ const defined = Object.fromEntries(
234
+ Object.entries(override).filter(([, value]) => value !== undefined),
235
+ );
236
+ if (Object.keys(defined).length === 0) return base;
237
+
238
+ return { ...base, ...defined };
239
+ }
240
+
241
+ // ── Tier-1 renderer ─────────────────────────────────────────────────────────
242
+
243
+ /** A plugin manifest paired with the name of the plugin that contributed it. */
244
+ export interface Tier1Entry {
245
+ pluginName: string;
246
+ manifest: PluginManifest;
247
+ }
248
+
249
+ export interface Tier1Input {
250
+ manifests: Tier1Entry[];
251
+ /** Soft budget in tokens. Default 5000. Exceeding it triggers a warning. */
252
+ tokenBudget?: number;
253
+ /**
254
+ * Override the token estimator (mostly for tests). Default: a chars/4
255
+ * heuristic — the Workers runtime carries no tokenizer vocabulary, and the
256
+ * budget is a soft operator hint where ±20% is immaterial.
257
+ */
258
+ estimateTokens?: (text: string) => number;
259
+ /**
260
+ * Whether the meta-tools are bound this turn. Default `true`. Pass `false`
261
+ * when they are not, so the block does not end by telling the model to
262
+ * call `list_capabilities` / `load_capability`.
263
+ */
264
+ capabilityDiscovery?: boolean;
265
+ }
266
+
267
+ export interface Tier1Output {
268
+ /** The composed prompt block (header + lines). Empty when no entries. */
269
+ block: string;
270
+ /** Token count of the body lines (header excluded). */
271
+ tokens: number;
272
+ /** Warnings to surface to the runtime logger. */
273
+ warnings: string[];
274
+ }
275
+
276
+ const DEFAULT_BUDGET = 5000;
277
+
278
+ const HEADER = '## Available Capabilities\n\n';
279
+ const FOOTER =
280
+ '\n\nFor more capabilities, call `list_capabilities()` to see what else is available,\nthen `load_capability({ names: ["cap1", "cap2"] })` to make their tools available in one call — pass all the capabilities you need at once rather than calling it multiple times.';
281
+
282
+ /**
283
+ * Chars/4 token estimate. Replaces the Node runtime's `js-tiktoken`
284
+ * (cl100k_base) so the Worker bundle does not carry a tokenizer vocabulary.
285
+ * Rounds up so an empty string still costs nothing and a one-char string
286
+ * costs one token.
287
+ */
288
+ export function estimateTokensApprox(text: string): number {
289
+ return Math.ceil(text.length / 4);
290
+ }
291
+
292
+ function formatExample(ex: ManifestExample): string {
293
+ const argSummary =
294
+ ex.args && Object.keys(ex.args).length > 0
295
+ ? `${ex.tool}(${JSON.stringify(ex.args)})`
296
+ : `${ex.tool}()`;
297
+ return `"${ex.user}" → ${argSummary}`;
298
+ }
299
+
300
+ function formatEntry(entry: Tier1Entry): string {
301
+ const { pluginName, manifest } = entry;
302
+ const lines: string[] = [`- **${pluginName}** — ${manifest.summary}`];
303
+
304
+ const whenToUse = manifest.whenToUse.slice(0, 2);
305
+ if (whenToUse.length > 0) {
306
+ lines.push(` - When to use: ${whenToUse.join('; ')}`);
307
+ }
308
+
309
+ if (manifest.whenNotToUse && manifest.whenNotToUse.length > 0) {
310
+ lines.push(` - Avoid for: ${manifest.whenNotToUse[0]}`);
311
+ }
312
+
313
+ const firstExample = manifest.examples?.[0];
314
+ if (firstExample) {
315
+ lines.push(` - Example: ${formatExample(firstExample)}`);
316
+ }
317
+
318
+ return lines.join('\n');
319
+ }
320
+
321
+ /**
322
+ * Compose the Tier-1 capability block from a list of plugin manifests.
323
+ *
324
+ * Only `visibility: 'always'` manifests are included, sorted alphabetically
325
+ * for prompt-caching determinism. Over-budget calls produce a warning naming
326
+ * the largest manifests so operators can mark them `'on-demand'` — degrading
327
+ * verbosity is an operator decision, not a runtime guess.
328
+ */
329
+ export function renderTier1(input: Tier1Input): Tier1Output {
330
+ const budget = input.tokenBudget ?? DEFAULT_BUDGET;
331
+ const estimate = input.estimateTokens ?? estimateTokensApprox;
332
+
333
+ const entries = input.manifests
334
+ .filter(({ manifest }) => manifest.visibility === 'always')
335
+ .slice()
336
+ .sort((a, b) => a.pluginName.localeCompare(b.pluginName));
337
+
338
+ if (entries.length === 0) {
339
+ return { block: '', tokens: 0, warnings: [] };
340
+ }
341
+
342
+ const renderedLines = entries.map(formatEntry);
343
+ const tokens = renderedLines.reduce((sum, line) => sum + estimate(line), 0);
344
+ const warnings: string[] = [];
345
+
346
+ if (tokens > budget) {
347
+ const largest = entries
348
+ .map((entry) => ({
349
+ name: entry.pluginName,
350
+ t: estimate(formatEntry(entry)),
351
+ }))
352
+ .sort((a, b) => b.t - a.t)
353
+ .slice(0, 3)
354
+ .map((e) => `${e.name} (~${e.t} tok)`)
355
+ .join(', ');
356
+ warnings.push(
357
+ `Tier-1 prompt is ${tokens} tokens (budget ${budget}). ` +
358
+ `Consider marking these on-demand: ${largest}.`,
359
+ );
360
+ }
361
+
362
+ const footer = input.capabilityDiscovery === false ? '' : FOOTER;
363
+ const block = `${HEADER}${renderedLines.join('\n\n')}${footer}`;
364
+ return { block, tokens, warnings };
365
+ }
@@ -0,0 +1,232 @@
1
+ import { z } from 'zod';
2
+ import { Command } from '@langchain/langgraph';
3
+ import { ToolMessage } from '@langchain/core/messages';
4
+ import { tool } from '../plugin-api/tool-helper';
5
+ import type { PluginManifest, PluginTool } from '../plugin-api/types';
6
+ import type { ManifestRegistry, ToolRegistry } from './registries';
7
+ import { acquireToolLock } from './utils';
8
+
9
+ // ── list_capabilities ───────────────────────────────────────────────────────
10
+
11
+ /**
12
+ * Schema accepted by `list_capabilities`.
13
+ *
14
+ * `includeOnDemand` defaults `true` because the agent normally wants to see
15
+ * everything it could load. `includeSilent` defaults `false` because silent
16
+ * plugins are not loadable and are kept out of the agent's view.
17
+ */
18
+ const listCapabilitiesSchema = z.object({
19
+ includeOnDemand: z.boolean().default(true),
20
+ includeSilent: z.boolean().default(false),
21
+ });
22
+
23
+ interface CapabilityListing {
24
+ name: string;
25
+ summary: string;
26
+ visibility: NonNullable<PluginManifest['visibility']>;
27
+ loaded: boolean;
28
+ category?: PluginManifest['category'];
29
+ tags: string[];
30
+ }
31
+
32
+ /**
33
+ * Build the `list_capabilities` meta-tool.
34
+ *
35
+ * Returns one entry per loaded plugin, with a `loaded` flag that combines
36
+ * the boot-time always-on plugins (`visibility === 'always'`) with the set
37
+ * of plugins the agent has explicitly loaded for this thread.
38
+ */
39
+ export function buildListCapabilitiesTool(
40
+ manifestRegistry: ManifestRegistry,
41
+ ): PluginTool {
42
+ return tool(
43
+ async (args, ctx) => {
44
+ const { includeOnDemand, includeSilent } =
45
+ listCapabilitiesSchema.parse(args);
46
+
47
+ const loadedSet = ctx.loadedPlugins ?? new Set<string>();
48
+
49
+ const out: CapabilityListing[] = [];
50
+ for (const { pluginName, manifest } of manifestRegistry.collect()) {
51
+ const visibility: NonNullable<PluginManifest['visibility']> =
52
+ manifest.visibility ?? 'on-demand';
53
+
54
+ if (visibility === 'silent' && !includeSilent) continue;
55
+ if (visibility === 'on-demand' && !includeOnDemand) continue;
56
+
57
+ out.push({
58
+ name: pluginName,
59
+ summary: manifest.summary,
60
+ visibility,
61
+ loaded: visibility === 'always' || loadedSet.has(pluginName),
62
+ category: manifest.category,
63
+ tags: manifest.tags ?? [],
64
+ });
65
+ }
66
+ ctx.logger.debug?.(
67
+ `[listCapabilities] includeOnDemand: ${includeOnDemand} | includeSilent: ${includeSilent} | loadedSet: ${Array.from(loadedSet).join(', ')} | results: ${out.length}`,
68
+ );
69
+
70
+ // Return a JSON string — LangChain's `tool()` mis-handles a raw array
71
+ // return (the `[content, artifact]` heuristic can drop the content).
72
+ return JSON.stringify(out);
73
+ },
74
+ {
75
+ name: 'list_capabilities',
76
+ description: 'List all available capabilities and their summaries.',
77
+ schema: listCapabilitiesSchema,
78
+ },
79
+ );
80
+ }
81
+
82
+ // ── load_capability ─────────────────────────────────────────────────────────
83
+
84
+ const loadCapabilitySchema = z.object({
85
+ names: z
86
+ .array(z.string())
87
+ .min(1)
88
+ .describe(
89
+ 'One or more capability names to load, as returned by list_capabilities.',
90
+ ),
91
+ });
92
+
93
+ interface ToolDetail {
94
+ name: string;
95
+ description: string;
96
+ }
97
+
98
+ interface LoadCapabilityResult extends PluginManifest {
99
+ /**
100
+ * `true` when the plugin was already loaded (or has `visibility: 'always'`),
101
+ * `false` when this call moved it into the loaded set.
102
+ */
103
+ alreadyAvailable: boolean;
104
+ /** One entry per tool the plugin contributes. */
105
+ tools: ToolDetail[];
106
+ }
107
+
108
+ /**
109
+ * Build the `load_capability` meta-tool.
110
+ *
111
+ * Accepts an array of plugin names so the agent can batch all needed
112
+ * capabilities into a single call. A per-session lock ensures the tool
113
+ * cannot be invoked in parallel — concurrent calls throw immediately.
114
+ *
115
+ * Behavior per name:
116
+ * - Unknown plugin → throws, instructing the agent to call
117
+ * `list_capabilities` first.
118
+ * - `silent` plugin → throws (silent plugins are not agent-loadable).
119
+ * - Plugin already loaded, or visibility is `always` → included in result
120
+ * with `alreadyAvailable: true` (no state change for that plugin).
121
+ * - Otherwise → added to the `loadedPlugins` state update.
122
+ *
123
+ * Return value:
124
+ * - If all requested plugins were already available: returns the result
125
+ * array directly (no state change).
126
+ * - If any are new: returns a LangGraph `Command` whose update appends
127
+ * all new plugins to `loadedPlugins` AND emits a `ToolMessage` carrying
128
+ * the full result array so the agent sees it on the same turn.
129
+ */
130
+ export function buildLoadCapabilityTool(
131
+ manifestRegistry: ManifestRegistry,
132
+ toolRegistry: ToolRegistry,
133
+ ): PluginTool {
134
+ return tool(
135
+ async (args, ctx) => {
136
+ const { names } = loadCapabilitySchema.parse(args);
137
+
138
+ const releaseLock = acquireToolLock(`${ctx.session.id}:load_capability`);
139
+ try {
140
+ const results: LoadCapabilityResult[] = [];
141
+ const newToLoad: string[] = [];
142
+
143
+ for (const name of names) {
144
+ const entry = manifestRegistry
145
+ .collect()
146
+ .find((m) => m.pluginName === name);
147
+
148
+ if (!entry) {
149
+ throw new Error(
150
+ `Capability "${name}" does not exist. Call list_capabilities first to discover available plugins.`,
151
+ );
152
+ }
153
+
154
+ if (entry.manifest.visibility === 'silent') {
155
+ throw new Error(
156
+ `Capability "${name}" is internal and cannot be loaded by the agent. Call list_capabilities first to discover loadable plugins.`,
157
+ );
158
+ }
159
+
160
+ const tools: ToolDetail[] = toolRegistry
161
+ .toolSummariesForPlugin(name)
162
+ .map((t) => ({
163
+ name: t.name,
164
+ description: t.description,
165
+ }));
166
+
167
+ const alreadyLoaded = ctx.loadedPlugins?.has(name) === true;
168
+ const alwaysVisible = entry.manifest.visibility === 'always';
169
+ const alreadyAvailable = alreadyLoaded || alwaysVisible;
170
+
171
+ results.push({ ...entry.manifest, alreadyAvailable, tools });
172
+
173
+ if (!alreadyAvailable) {
174
+ newToLoad.push(name);
175
+ }
176
+ }
177
+
178
+ if (newToLoad.length === 0) {
179
+ return results;
180
+ }
181
+
182
+ const update: Record<string, unknown> = {
183
+ loadedPlugins: newToLoad,
184
+ };
185
+ if (ctx.toolCallId) {
186
+ update.messages = [
187
+ new ToolMessage({
188
+ content: JSON.stringify(results),
189
+ tool_call_id: ctx.toolCallId,
190
+ }),
191
+ ];
192
+ }
193
+ return new Command({ update });
194
+ } finally {
195
+ releaseLock();
196
+ }
197
+ },
198
+ {
199
+ name: 'load_capability',
200
+ description:
201
+ "Load one or more capabilities for the rest of this conversation. Pass all capabilities you need in a single call — batching is preferred over multiple calls. The response is an array of plugin manifests plus tool lists; after this call, the new capabilities' tools are usable on the next model step.",
202
+ schema: loadCapabilitySchema,
203
+ },
204
+ );
205
+ }
206
+
207
+ // ── Bundle ──────────────────────────────────────────────────────────────────
208
+
209
+ /**
210
+ * Inputs for `buildMetaTools`. The runtime passes its already-collected
211
+ * registries; the meta-tools read manifests and tool descriptors from them.
212
+ */
213
+ export interface BuildMetaToolsOptions {
214
+ manifestRegistry: ManifestRegistry;
215
+ toolRegistry: ToolRegistry;
216
+ }
217
+
218
+ /**
219
+ * Build the two meta-tools the agent always has, regardless of which
220
+ * plugins are loaded:
221
+ *
222
+ * - `load_capability` — mark a plugin as loaded for this thread; returns
223
+ * the plugin's full manifest + tool list.
224
+ * - `list_capabilities` — list every visible plugin with status flags.
225
+ */
226
+ export function buildMetaTools(opts: BuildMetaToolsOptions): PluginTool[] {
227
+ const { manifestRegistry, toolRegistry } = opts;
228
+ return [
229
+ buildLoadCapabilityTool(manifestRegistry, toolRegistry),
230
+ buildListCapabilitiesTool(manifestRegistry),
231
+ ];
232
+ }