@ixo/oracle-runtime-workers 0.37.2 → 0.39.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 (111) hide show
  1. package/package.json +4 -4
  2. package/src/attachments/channel-links.ts +74 -7
  3. package/src/attachments/channel.ts +15 -4
  4. package/src/attachments/content-blocks.ts +19 -16
  5. package/src/attachments/download.ts +173 -50
  6. package/src/attachments/index.ts +6 -5
  7. package/src/attachments/pipeline.ts +117 -49
  8. package/src/attachments/resize.ts +142 -0
  9. package/src/attachments/retention.ts +26 -70
  10. package/src/attachments/room-media.ts +643 -0
  11. package/src/attachments/route.ts +47 -4
  12. package/src/attachments/sandbox-archive.ts +15 -4
  13. package/src/attachments/sanitize.ts +24 -13
  14. package/src/attachments/view.ts +82 -38
  15. package/src/channels/replies.ts +8 -4
  16. package/src/core/capability-expiry.ts +62 -0
  17. package/src/core/capability-router.ts +126 -95
  18. package/src/core/context-budget.ts +116 -4
  19. package/src/core/domain-context/document.ts +74 -2
  20. package/src/core/domain-context/index.ts +1 -0
  21. package/src/core/domain-context/resolver.ts +9 -6
  22. package/src/core/domain-context/turn-context.ts +62 -13
  23. package/src/core/domain-context/types.ts +17 -0
  24. package/src/core/env.ts +21 -2
  25. package/src/core/index.ts +4 -0
  26. package/src/core/llm.ts +29 -0
  27. package/src/core/main-agent-types.ts +53 -4
  28. package/src/core/main-agent.ts +228 -125
  29. package/src/core/manifest.ts +109 -24
  30. package/src/core/mcp-result.ts +56 -0
  31. package/src/core/meta-tools.ts +258 -110
  32. package/src/core/middlewares/attachment-recall.ts +502 -0
  33. package/src/core/middlewares/capability-gate.ts +77 -28
  34. package/src/core/middlewares/context-guard.ts +154 -21
  35. package/src/core/middlewares/index.ts +5 -0
  36. package/src/core/middlewares/result-cap.ts +1 -1
  37. package/src/core/middlewares/summarization.ts +9 -0
  38. package/src/core/middlewares/tool-marks.ts +11 -2
  39. package/src/core/middlewares/turn-boundary.ts +22 -12
  40. package/src/core/operating-guides.ts +126 -0
  41. package/src/core/plugins/skills/skills-tools.ts +6 -31
  42. package/src/core/plugins/skills/skills.plugin.ts +4 -3
  43. package/src/core/plugins/weather/weather.plugin.ts +1 -0
  44. package/src/core/prompt-composer.ts +188 -111
  45. package/src/core/read-result-tool.ts +48 -14
  46. package/src/core/registries.ts +13 -5
  47. package/src/core/state.ts +4 -3
  48. package/src/core/sub-agent-fallback.ts +1 -0
  49. package/src/core/subagent-as-tool.ts +4 -1
  50. package/src/core/tool-access.ts +3 -32
  51. package/src/core/wrap-plugin-tool.ts +2 -1
  52. package/src/do/contracts.ts +64 -24
  53. package/src/do/room-mirror.ts +144 -20
  54. package/src/do/transcript.ts +41 -12
  55. package/src/do/turn-attachments.ts +45 -6
  56. package/src/do/turn-metadata.ts +16 -4
  57. package/src/do/user-oracle-do.ts +639 -152
  58. package/src/index.ts +1 -0
  59. package/src/llm/provider-error.ts +5 -2
  60. package/src/llm/stream-liveness.ts +3 -2
  61. package/src/matrix/gateway-do.ts +164 -102
  62. package/src/matrix/ingest.ts +12 -3
  63. package/src/matrix/media-deadline.ts +5 -47
  64. package/src/memory/session-history-indexer.ts +60 -14
  65. package/src/plugin-api/oracle-plugin.ts +4 -5
  66. package/src/plugin-api/types.ts +32 -3
  67. package/src/plugins/action-proposals/action-proposals.plugin.ts +7 -2
  68. package/src/plugins/agui/agui.plugin.ts +1 -0
  69. package/src/plugins/attachments/attachments.plugin.ts +3 -3
  70. package/src/plugins/attachments/view-attachment-tool.ts +6 -4
  71. package/src/plugins/compact-schema.ts +80 -0
  72. package/src/plugins/composio/composio.plugin.ts +1 -0
  73. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +4 -3
  74. package/src/plugins/editor/editor.plugin.ts +4 -0
  75. package/src/plugins/firecrawl/firecrawl-agent.ts +2 -0
  76. package/src/plugins/firecrawl/firecrawl.plugin.ts +1 -0
  77. package/src/plugins/flows/flows.plugin.ts +37 -6
  78. package/src/plugins/flows/prompts.ts +1 -1
  79. package/src/plugins/flows/tools/authoring.ts +5 -7
  80. package/src/plugins/flows/tools/discovery.ts +1 -4
  81. package/src/plugins/flows/tools/forms.ts +2 -8
  82. package/src/plugins/flows/tools/linkage.ts +1 -4
  83. package/src/plugins/flows/tools/settings.ts +1 -4
  84. package/src/plugins/flows/translator.ts +16 -5
  85. package/src/plugins/flows/types.ts +27 -48
  86. package/src/plugins/hub-actions/hub-actions.plugin.ts +30 -4
  87. package/src/plugins/index.ts +2 -2
  88. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +29 -5
  89. package/src/plugins/ixo-chain/kinds/core.ts +2 -2
  90. package/src/plugins/ixo-chain/kinds/types.ts +34 -5
  91. package/src/plugins/ixo-chain/queries.ts +10 -1
  92. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +1 -0
  93. package/src/plugins/matrix-group-chats/index.ts +1 -0
  94. package/src/plugins/memory/memory-tools.ts +4 -4
  95. package/src/plugins/memory/memory.plugin.ts +27 -1
  96. package/src/plugins/memory/oracle-document-tools.ts +24 -49
  97. package/src/plugins/pod-creator/pod-creator.plugin.ts +1 -0
  98. package/src/plugins/portal/portal.plugin.ts +1 -0
  99. package/src/plugins/sandbox/sandbox-write-blob.ts +3 -15
  100. package/src/plugins/sandbox/sandbox.plugin.ts +19 -2
  101. package/src/plugins/tasks/manifest.ts +1 -0
  102. package/src/plugins/tasks/tasks-tools.ts +14 -19
  103. package/src/plugins/user-preferences/user-preferences-tool.ts +11 -37
  104. package/src/plugins/user-preferences/user-preferences.plugin.ts +4 -0
  105. package/src/plugins/vfs/vfs-tools.ts +6 -6
  106. package/src/plugins/vfs/vfs.plugin.ts +4 -5
  107. package/src/plugins/yoma/yoma.plugin.ts +1 -0
  108. package/src/sqlite/inline-payloads.ts +121 -0
  109. package/src/sqlite/serialization.ts +12 -0
  110. package/src/sqlite/sqlite-saver.ts +258 -42
  111. package/src/tasks/schedule-input.ts +5 -22
@@ -5,11 +5,13 @@ import { tool } from '../plugin-api/tool-helper';
5
5
  import type { PluginManifest, PluginTool } from '../plugin-api/types';
6
6
  import {
7
7
  describeRequirements,
8
+ effectiveToolVisibility,
8
9
  unmetRequirements,
9
10
  type CapabilityRequirement,
10
11
  } from './manifest';
11
12
  import type { ManifestRegistry, ToolSummarySource } from './registries';
12
13
  import { withoutWithheldExamples, type TurnToolAccess } from './tool-access';
14
+ import { USAGE_CARRIED_KWARG } from './operating-guides';
13
15
  import { acquireToolLock } from './utils';
14
16
 
15
17
  export const LOAD_CAPABILITY_TOOL_NAME = 'load_capability';
@@ -27,14 +29,43 @@ export const META_TOOL_NAMES: readonly string[] = [
27
29
  export type MetaToolAccess = Pick<
28
30
  TurnToolAccess<never>,
29
31
  'withheldToolNames' | 'hiddenPlugins'
30
- > & {
31
- /**
32
- * Plugins available this turn only through the capability router's
33
- * preload: usable, but their operating guide is not in the system prompt,
34
- * so `load_capability` still returns it.
35
- */
36
- preloadedOnly?: ReadonlySet<string>;
37
- };
32
+ >;
33
+
34
+ // ── Shared loading rules ────────────────────────────────────────────────────
35
+
36
+ /** Whether `manifest`'s plugin still has a tool behind `load_capability`. */
37
+ function hasGatedTools(
38
+ pluginName: string,
39
+ manifest: PluginManifest,
40
+ toolSource: ToolSummarySource,
41
+ access: MetaToolAccess,
42
+ ): boolean {
43
+ return toolSource
44
+ .toolSummariesForPlugin(pluginName)
45
+ .some(
46
+ (tool) =>
47
+ !access.withheldToolNames.has(tool.name) &&
48
+ effectiveToolVisibility(tool, manifest) === 'on-demand',
49
+ );
50
+ }
51
+
52
+ /**
53
+ * Whether the plugin has nothing left to load: the thread loaded it, or it is
54
+ * `always` visible with no tool behind `load_capability`.
55
+ */
56
+ function fullyBound(
57
+ pluginName: string,
58
+ manifest: PluginManifest,
59
+ loaded: ReadonlySet<string> | undefined,
60
+ toolSource: ToolSummarySource,
61
+ access: MetaToolAccess,
62
+ ): boolean {
63
+ return (
64
+ loaded?.has(pluginName) === true ||
65
+ (manifest.visibility === 'always' &&
66
+ !hasGatedTools(pluginName, manifest, toolSource, access))
67
+ );
68
+ }
38
69
 
39
70
  // ── list_capabilities ───────────────────────────────────────────────────────
40
71
 
@@ -68,13 +99,15 @@ interface CapabilityListing {
68
99
  * Build the `list_capabilities` meta-tool.
69
100
  *
70
101
  * Returns one entry per loaded plugin, with a `loaded` flag that combines
71
- * the boot-time always-on plugins (`visibility === 'always'`) with the set
72
- * of plugins the agent has explicitly loaded for this thread. A plugin left
73
- * with nothing by the turn's tool planes (`access.hiddenPlugins`) is not
74
- * listed at all; one whose `requires` are unmet is listed as `unavailable`.
102
+ * the boot-time always-on plugins (`visibility === 'always'`, with no tool
103
+ * behind `load_capability`) with the set of plugins the agent has explicitly
104
+ * loaded for this thread. A plugin left with nothing by the turn's tool
105
+ * planes (`access.hiddenPlugins`) is not listed at all; one whose `requires`
106
+ * are unmet is listed as `unavailable`.
75
107
  */
76
108
  export function buildListCapabilitiesTool(
77
109
  manifestRegistry: ManifestRegistry,
110
+ toolSource: ToolSummarySource,
78
111
  access: MetaToolAccess,
79
112
  ): PluginTool {
80
113
  return tool(
@@ -101,7 +134,7 @@ export function buildListCapabilitiesTool(
101
134
  visibility,
102
135
  loaded:
103
136
  missing.length === 0 &&
104
- (visibility === 'always' || loadedSet.has(pluginName)),
137
+ fullyBound(pluginName, manifest, loadedSet, toolSource, access),
105
138
  category: manifest.category,
106
139
  tags: manifest.tags ?? [],
107
140
  ...(missing.length > 0 ? { unavailable: { missing } } : {}),
@@ -130,37 +163,154 @@ const loadCapabilitySchema = z.object({
130
163
  .array(z.string())
131
164
  .min(1)
132
165
  .describe(
133
- 'One or more capability names to load, as returned by list_capabilities.',
166
+ 'One or more capability names to load, from the on-demand capability list.',
134
167
  ),
135
168
  });
136
169
 
137
- interface ToolDetail {
138
- name: string;
139
- description: string;
140
- }
170
+ /** The note a no-op load carries, in place of the plugin's manifest. */
171
+ const ALREADY_BOUND_NOTE =
172
+ 'Nothing to load: these tools are already in your tool list. Call them directly.';
141
173
 
142
- interface LoadCapabilityResult extends PluginManifest {
174
+ interface LoadCapabilityResult extends Partial<
175
+ Pick<PluginManifest, 'summary' | 'whenToUse' | 'whenNotToUse' | 'examples'>
176
+ > {
177
+ name: string;
143
178
  /**
144
- * `true` when the plugin was already loaded (or has `visibility: 'always'`),
145
- * `false` when this call moved it into the loaded set.
179
+ * `true` when the plugin had nothing left to load, `false` when this call
180
+ * moved it into the loaded set (or refused it).
146
181
  */
147
182
  alreadyAvailable: boolean;
148
- /** One entry per tool the plugin contributes. */
149
- tools: ToolDetail[];
183
+ /**
184
+ * The plugin's tool names. Their definitions reach the model in its tool
185
+ * list on the next step, so repeating the descriptions here would pay for
186
+ * them twice, and for the rest of the conversation.
187
+ */
188
+ tools: string[];
189
+ /** Present on a no-op load: says there is nothing to do. */
190
+ note?: string;
150
191
  /**
151
192
  * The plugin's working rules (`OraclePlugin.operatingGuide`), present when
152
- * this call loaded it or the router only preloaded it, once per turn. A
153
- * plugin loaded on the thread (or `always` visible) has it in the system
154
- * prompt, so it is not repeated.
193
+ * this call loaded it, once per turn, unless the system prompt carries them
194
+ * (`operating-guides.ts`).
155
195
  */
156
196
  operatingGuide?: string;
157
197
  /**
158
- * Present when the plugin was NOT loaded because the user's authorization
159
- * does not grant what it requires (`manifest.requires`).
198
+ * Present when the plugin was NOT loaded: it does not exist, or the user's
199
+ * authorization does not grant what it requires (`manifest.requires`, the
200
+ * `missing` list).
160
201
  */
161
202
  refused?: { missing: CapabilityRequirement[]; reason: string };
162
203
  }
163
204
 
205
+ /**
206
+ * What a load tells the model, shared by `load_capability` and the capability
207
+ * gate's load on a hidden tool call. Built per turn: it remembers what the
208
+ * turn already loaded, which the tool context cannot show (a state update
209
+ * made earlier in the turn is not in it).
210
+ */
211
+ export interface CapabilityLoader {
212
+ /** The plugin's tools the turn does not withhold. */
213
+ toolNames(pluginName: string): string[];
214
+ /**
215
+ * What the model needs to use the plugin: how to use it, the tool names,
216
+ * and its operating guide (once per turn) unless the prompt carries it.
217
+ * Records the plugin as loaded this turn.
218
+ */
219
+ describe(
220
+ pluginName: string,
221
+ manifest: PluginManifest,
222
+ guideInPrompt: boolean,
223
+ ): LoadCapabilityResult;
224
+ /** Whether this turn already loaded the plugin. */
225
+ loadedThisTurn(pluginName: string): boolean;
226
+ /** The gate's load: `describe` as a tool result. */
227
+ describeForGate(pluginName: string): GateLoadResult;
228
+ }
229
+
230
+ /** What the capability gate's refusal adds to its tool message. */
231
+ export interface GateLoadResult {
232
+ text: string;
233
+ additional_kwargs: Record<string, unknown>;
234
+ }
235
+
236
+ /** What marks a message as carrying the usage of `plugins`. */
237
+ function usageMark(plugins: readonly string[]): Record<string, unknown> {
238
+ return plugins.length > 0 ? { [USAGE_CARRIED_KWARG]: plugins } : {};
239
+ }
240
+
241
+ export function createCapabilityLoader(
242
+ manifestRegistry: ManifestRegistry,
243
+ toolSource: ToolSummarySource,
244
+ access: MetaToolAccess,
245
+ ): CapabilityLoader {
246
+ const loadedThisTurn = new Set<string>();
247
+ const guidesReturned = new Set<string>();
248
+
249
+ const toolNames = (pluginName: string): string[] =>
250
+ toolSource
251
+ .toolSummariesForPlugin(pluginName)
252
+ .filter((tool) => !access.withheldToolNames.has(tool.name))
253
+ .map((tool) => tool.name);
254
+
255
+ const describe: CapabilityLoader['describe'] = (
256
+ pluginName,
257
+ manifest,
258
+ guideInPrompt,
259
+ ) => {
260
+ loadedThisTurn.add(pluginName);
261
+ const { summary, whenToUse, whenNotToUse, examples } =
262
+ withoutWithheldExamples(manifest, access.withheldToolNames);
263
+ const guide =
264
+ guideInPrompt ||
265
+ manifest.visibility === 'always' ||
266
+ guidesReturned.has(pluginName)
267
+ ? undefined
268
+ : manifestRegistry.operatingGuide(pluginName);
269
+ if (guide) guidesReturned.add(pluginName);
270
+ return {
271
+ name: pluginName,
272
+ alreadyAvailable: false,
273
+ tools: toolNames(pluginName),
274
+ summary,
275
+ whenToUse,
276
+ whenNotToUse,
277
+ examples,
278
+ ...(guide ? { operatingGuide: guide } : {}),
279
+ };
280
+ };
281
+
282
+ return {
283
+ toolNames,
284
+ describe,
285
+ loadedThisTurn: (pluginName) => loadedThisTurn.has(pluginName),
286
+ describeForGate: (pluginName) => {
287
+ const entry = manifestRegistry
288
+ .collect()
289
+ .find((m) => m.pluginName === pluginName);
290
+ if (!entry) return { text: '', additional_kwargs: {} };
291
+ return {
292
+ text: JSON.stringify(describe(pluginName, entry.manifest, false)),
293
+ additional_kwargs: usageMark([pluginName]),
294
+ };
295
+ },
296
+ };
297
+ }
298
+
299
+ /** The names the error for an unknown capability offers. */
300
+ function loadableNames(
301
+ manifestRegistry: ManifestRegistry,
302
+ access: MetaToolAccess,
303
+ ): string[] {
304
+ return manifestRegistry
305
+ .collect()
306
+ .filter(
307
+ ({ pluginName, manifest }) =>
308
+ manifest.visibility !== 'silent' &&
309
+ !access.hiddenPlugins.has(pluginName),
310
+ )
311
+ .map(({ pluginName }) => pluginName);
312
+ }
313
+
164
314
  /**
165
315
  * Build the `load_capability` meta-tool.
166
316
  *
@@ -168,22 +318,25 @@ interface LoadCapabilityResult extends PluginManifest {
168
318
  * capabilities into a single call. A per-session lock ensures the tool
169
319
  * cannot be invoked in parallel — concurrent calls throw immediately.
170
320
  *
171
- * Behavior per name:
321
+ * Every name is judged before any is loaded, and a name that cannot be loaded
322
+ * never stops the rest of the batch. Behavior per name:
172
323
  * - Unknown plugin, or one the turn's tool planes left with nothing
173
324
  * (`access.hiddenPlugins`, answered exactly like an unknown one) →
174
- * throws, instructing the agent to call `list_capabilities` first.
175
- * - `silent` plugin → throws (silent plugins are not agent-loadable).
325
+ * `refused`, naming the capabilities that can be loaded.
326
+ * - `silent` plugin → `refused` (silent plugins are not agent-loadable).
176
327
  * - Plugin whose `requires` the user's delegation does not grant →
177
- * included in the result with `refused` (what is missing) and not
178
- * loaded; the rest of the batch is unaffected.
179
- * - Plugin already loaded, or visibility is `always` → included in result
180
- * with `alreadyAvailable: true` (no state change for that plugin).
328
+ * `refused`, with what is missing.
329
+ * - Plugin with nothing left to load: loaded on the thread or earlier this
330
+ * turn, or `always` visible with no tool behind `load_capability` → a
331
+ * no-op entry with `alreadyAvailable: true`, its tool names and a note
332
+ * saying so (no state change for that plugin). An `always` plugin with
333
+ * `on-demand` tools does load, once.
181
334
  * - Otherwise → added to the `loadedPlugins` state update.
182
335
  *
183
- * The tool list and `examples` leave out the tools the turn withholds
184
- * (`access.withheldToolNames`). A plugin this call loads, or one the router
185
- * only preloaded, comes with its `operatingGuide` when it has one — once per
186
- * turn; a refused one never does.
336
+ * The tool names and `examples` leave out the tools the turn withholds
337
+ * (`access.withheldToolNames`). A plugin this call loads comes with its usage
338
+ * and its `operatingGuide` when it has one, once per turn, and the result is
339
+ * marked as carrying them; a refused one never does.
187
340
  *
188
341
  * Return value:
189
342
  * - If all requested plugins were already available: returns the result
@@ -196,89 +349,80 @@ export function buildLoadCapabilityTool(
196
349
  manifestRegistry: ManifestRegistry,
197
350
  toolSource: ToolSummarySource,
198
351
  access: MetaToolAccess,
352
+ loader: CapabilityLoader = createCapabilityLoader(
353
+ manifestRegistry,
354
+ toolSource,
355
+ access,
356
+ ),
199
357
  ): PluginTool {
200
- // Guides this tool already returned: the tool is built per turn, and the
201
- // context it reads does not see a load made earlier in the same turn.
202
- const guidesReturned = new Set<string>();
203
358
  return tool(
204
359
  async (args, ctx) => {
205
360
  const { names } = loadCapabilitySchema.parse(args);
206
361
 
207
362
  const releaseLock = acquireToolLock(`${ctx.session.id}:load_capability`);
208
363
  try {
209
- const results: LoadCapabilityResult[] = [];
210
- const newToLoad: string[] = [];
364
+ const refused = (
365
+ name: string,
366
+ reason: string,
367
+ missing: CapabilityRequirement[] = [],
368
+ ): LoadCapabilityResult => ({
369
+ name,
370
+ alreadyAvailable: false,
371
+ tools: [],
372
+ refused: { missing, reason },
373
+ });
374
+ const nothingToLoad = (name: string): LoadCapabilityResult => ({
375
+ name,
376
+ alreadyAvailable: true,
377
+ tools: loader.toolNames(name),
378
+ note: ALREADY_BOUND_NOTE,
379
+ });
211
380
 
212
- for (const name of names) {
381
+ // Judged first: `describe` records the load, so it runs only for what
382
+ // this call really loads.
383
+ const judged = names.map((name) => {
213
384
  const entry = manifestRegistry
214
385
  .collect()
215
386
  .find((m) => m.pluginName === name);
216
-
217
- if (!entry || access.hiddenPlugins.has(name)) {
218
- throw new Error(
219
- `Capability "${name}" does not exist. Call list_capabilities first to discover available plugins.`,
387
+ if (!entry || access.hiddenPlugins.has(name))
388
+ return refused(
389
+ name,
390
+ `Capability "${name}" does not exist. Loadable capabilities: ${loadableNames(manifestRegistry, access).join(', ')}.`,
220
391
  );
221
- }
222
-
223
- if (entry.manifest.visibility === 'silent') {
224
- throw new Error(
225
- `Capability "${name}" is internal and cannot be loaded by the agent. Call list_capabilities first to discover loadable plugins.`,
392
+ if (entry.manifest.visibility === 'silent')
393
+ return refused(
394
+ name,
395
+ `Capability "${name}" is internal and cannot be loaded by the agent. Loadable capabilities: ${loadableNames(manifestRegistry, access).join(', ')}.`,
226
396
  );
227
- }
228
-
229
397
  const missing = unmetRequirements(
230
398
  entry.manifest,
231
399
  ctx.ucan.hasCapability,
232
400
  );
233
- if (missing.length > 0) {
234
- results.push({
235
- ...withoutWithheldExamples(
236
- entry.manifest,
237
- access.withheldToolNames,
238
- ),
239
- alreadyAvailable: false,
240
- tools: [],
241
- refused: {
242
- missing,
243
- reason: `The user's authorization for this oracle does not grant ${describeRequirements(missing)}, which "${name}" requires, so it was not loaded. Tell the user; they can re-authorize the oracle with that permission and ask again.`,
244
- },
245
- });
246
- continue;
247
- }
248
-
249
- const tools: ToolDetail[] = toolSource
250
- .toolSummariesForPlugin(name)
251
- .filter((t) => !access.withheldToolNames.has(t.name))
252
- .map((t) => ({
253
- name: t.name,
254
- description: t.description,
255
- }));
256
-
257
- const alreadyLoaded = ctx.loadedPlugins?.has(name) === true;
258
- const alwaysVisible = entry.manifest.visibility === 'always';
259
- const alreadyAvailable = alreadyLoaded || alwaysVisible;
260
-
261
- const guideInPrompt =
262
- alreadyAvailable && access.preloadedOnly?.has(name) !== true;
263
- const guide =
264
- guideInPrompt || guidesReturned.has(name)
265
- ? undefined
266
- : manifestRegistry.operatingGuide(name);
267
- if (guide) guidesReturned.add(name);
268
- results.push({
269
- ...withoutWithheldExamples(
270
- entry.manifest,
271
- access.withheldToolNames,
272
- ),
273
- alreadyAvailable,
274
- tools,
275
- ...(guide ? { operatingGuide: guide } : {}),
276
- });
277
-
278
- if (!alreadyAvailable) {
279
- newToLoad.push(name);
280
- }
281
- }
401
+ if (missing.length > 0)
402
+ return refused(
403
+ name,
404
+ `The user's authorization for this oracle does not grant ${describeRequirements(missing)}, which "${name}" requires, so it was not loaded. Tell the user; they can re-authorize the oracle with that permission and ask again.`,
405
+ missing,
406
+ );
407
+ return fullyBound(
408
+ name,
409
+ entry.manifest,
410
+ ctx.loadedPlugins,
411
+ toolSource,
412
+ access,
413
+ )
414
+ ? nothingToLoad(name)
415
+ : entry.manifest;
416
+ });
417
+
418
+ const newToLoad: string[] = [];
419
+ const results = judged.map((verdict, index) => {
420
+ if ('alreadyAvailable' in verdict) return verdict;
421
+ const name = names[index] ?? '';
422
+ if (loader.loadedThisTurn(name)) return nothingToLoad(name);
423
+ newToLoad.push(name);
424
+ return loader.describe(name, verdict, false);
425
+ });
282
426
 
283
427
  // Text, never the bare array: LangChain would pass an array of
284
428
  // objects through as the message's content blocks.
@@ -290,10 +434,12 @@ export function buildLoadCapabilityTool(
290
434
  loadedPlugins: newToLoad,
291
435
  };
292
436
  if (ctx.toolCallId) {
437
+ update.capabilityUsage = newToLoad;
293
438
  update.messages = [
294
439
  new ToolMessage({
295
440
  content: JSON.stringify(results),
296
441
  tool_call_id: ctx.toolCallId,
442
+ additional_kwargs: usageMark(newToLoad),
297
443
  }),
298
444
  ];
299
445
  }
@@ -305,7 +451,7 @@ export function buildLoadCapabilityTool(
305
451
  {
306
452
  name: LOAD_CAPABILITY_TOOL_NAME,
307
453
  description:
308
- "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.",
454
+ 'Load capabilities that are not in your tool list yet. Pass all you need in one call. Returns how to use each and its tool names; the tools are usable on your next step. A capability left unused for a while is unloaded; load it again when you need it. Never needed for a tool already in your tool list.',
309
455
  schema: loadCapabilitySchema,
310
456
  },
311
457
  );
@@ -323,6 +469,8 @@ export interface BuildMetaToolsOptions {
323
469
  toolRegistry: ToolSummarySource;
324
470
  /** The turn's tool-plane result (`resolveTurnToolAccess`). */
325
471
  toolAccess: MetaToolAccess;
472
+ /** The turn's loader, when the capability gate shares it. */
473
+ loader?: CapabilityLoader;
326
474
  }
327
475
 
328
476
  /**
@@ -330,13 +478,13 @@ export interface BuildMetaToolsOptions {
330
478
  * plugins are loaded:
331
479
  *
332
480
  * - `load_capability` — mark a plugin as loaded for this thread; returns
333
- * the plugin's full manifest + tool list.
481
+ * how to use it + its tool names.
334
482
  * - `list_capabilities` — list every visible plugin with status flags.
335
483
  */
336
484
  export function buildMetaTools(opts: BuildMetaToolsOptions): PluginTool[] {
337
- const { manifestRegistry, toolRegistry, toolAccess } = opts;
485
+ const { manifestRegistry, toolRegistry, toolAccess, loader } = opts;
338
486
  return [
339
- buildLoadCapabilityTool(manifestRegistry, toolRegistry, toolAccess),
340
- buildListCapabilitiesTool(manifestRegistry, toolAccess),
487
+ buildLoadCapabilityTool(manifestRegistry, toolRegistry, toolAccess, loader),
488
+ buildListCapabilitiesTool(manifestRegistry, toolRegistry, toolAccess),
341
489
  ];
342
490
  }