@ixo/oracle-runtime-workers 0.38.0 → 0.40.1
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/package.json +5 -4
- package/src/action-interactions/routes.ts +78 -0
- package/src/action-interactions/store.ts +1038 -0
- package/src/channels/replies.ts +8 -4
- package/src/core/capability-expiry.ts +62 -0
- package/src/core/capability-router.ts +126 -95
- package/src/core/context-budget.ts +116 -4
- package/src/core/domain-context/document.ts +74 -2
- package/src/core/domain-context/index.ts +1 -0
- package/src/core/domain-context/resolver.ts +9 -6
- package/src/core/domain-context/turn-context.ts +62 -13
- package/src/core/domain-context/types.ts +17 -0
- package/src/core/env.ts +21 -2
- package/src/core/index.ts +4 -0
- package/src/core/llm.ts +29 -0
- package/src/core/main-agent-types.ts +45 -4
- package/src/core/main-agent.ts +210 -122
- package/src/core/manifest.ts +109 -24
- package/src/core/mcp-result.ts +56 -0
- package/src/core/meta-tools.ts +258 -110
- package/src/core/middlewares/capability-gate.ts +77 -28
- package/src/core/middlewares/context-guard.ts +154 -21
- package/src/core/middlewares/result-cap.ts +1 -1
- package/src/core/middlewares/summarization.ts +9 -0
- package/src/core/middlewares/tool-marks.ts +11 -2
- package/src/core/middlewares/tool-repetition-guard.ts +61 -1
- package/src/core/middlewares/turn-boundary.ts +22 -12
- package/src/core/operating-guides.ts +126 -0
- package/src/core/plugins/skills/skills-tools.ts +6 -31
- package/src/core/plugins/skills/skills.plugin.ts +4 -3
- package/src/core/plugins/weather/weather.plugin.ts +1 -0
- package/src/core/prompt-composer.ts +188 -111
- package/src/core/read-result-tool.ts +48 -14
- package/src/core/registries.ts +13 -5
- package/src/core/state.ts +4 -3
- package/src/core/sub-agent-fallback.ts +1 -0
- package/src/core/subagent-as-tool.ts +4 -1
- package/src/core/tool-access.ts +3 -32
- package/src/core/wrap-plugin-tool.ts +2 -1
- package/src/do/contracts.ts +45 -1
- package/src/do/transcript.ts +40 -11
- package/src/do/turn-metadata.ts +16 -4
- package/src/do/user-oracle-do.ts +358 -62
- package/src/index.ts +1 -0
- package/src/memory/session-history-indexer.ts +60 -14
- package/src/plugin-api/oracle-plugin.ts +4 -5
- package/src/plugin-api/types.ts +32 -3
- package/src/plugins/action-proposals/action-proposals.plugin.ts +7 -2
- package/src/plugins/agui/agui.plugin.ts +1 -0
- package/src/plugins/compact-schema.ts +80 -0
- package/src/plugins/composio/composio.plugin.ts +1 -0
- package/src/plugins/domain-indexer/domain-indexer.plugin.ts +4 -3
- package/src/plugins/editor/editor.plugin.ts +4 -0
- package/src/plugins/firecrawl/firecrawl-agent.ts +2 -0
- package/src/plugins/firecrawl/firecrawl.plugin.ts +1 -0
- package/src/plugins/flows/flows.plugin.ts +37 -6
- package/src/plugins/flows/prompts.ts +1 -1
- package/src/plugins/flows/tools/authoring.ts +5 -7
- package/src/plugins/flows/tools/discovery.ts +1 -4
- package/src/plugins/flows/tools/forms.ts +2 -8
- package/src/plugins/flows/tools/linkage.ts +1 -4
- package/src/plugins/flows/tools/settings.ts +1 -4
- package/src/plugins/flows/translator.ts +16 -5
- package/src/plugins/flows/types.ts +27 -48
- package/src/plugins/hub-actions/hub-actions.plugin.ts +30 -4
- package/src/plugins/index.ts +2 -2
- package/src/plugins/ixo-chain/ixo-chain.plugin.ts +29 -5
- package/src/plugins/ixo-chain/kinds/core.ts +2 -2
- package/src/plugins/ixo-chain/kinds/types.ts +34 -5
- package/src/plugins/ixo-chain/queries.ts +10 -1
- package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +1 -0
- package/src/plugins/matrix-group-chats/index.ts +1 -0
- package/src/plugins/memory/memory-tools.ts +4 -4
- package/src/plugins/memory/memory.plugin.ts +27 -1
- package/src/plugins/memory/oracle-document-tools.ts +24 -49
- package/src/plugins/pod-creator/pod-creator.plugin.ts +1 -0
- package/src/plugins/portal/portal.plugin.ts +3 -1
- package/src/plugins/sandbox/sandbox-write-blob.ts +3 -15
- package/src/plugins/sandbox/sandbox.plugin.ts +19 -2
- package/src/plugins/tasks/manifest.ts +1 -0
- package/src/plugins/tasks/tasks-tools.ts +34 -39
- package/src/plugins/user-preferences/user-preferences-tool.ts +11 -37
- package/src/plugins/user-preferences/user-preferences.plugin.ts +4 -0
- package/src/plugins/vfs/vfs-tools.ts +6 -6
- package/src/plugins/vfs/vfs.plugin.ts +4 -5
- package/src/plugins/yoma/yoma.plugin.ts +1 -0
- package/src/shell/app.ts +241 -6
- package/src/sqlite/serialization.ts +12 -0
- package/src/sqlite/sqlite-saver.ts +17 -0
- package/src/tasks/schedule-input.ts +5 -22
package/src/core/meta-tools.ts
CHANGED
|
@@ -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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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'
|
|
72
|
-
* of plugins the agent has explicitly
|
|
73
|
-
* with nothing by the turn's tool
|
|
74
|
-
* listed at all; one whose `requires`
|
|
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
|
-
(
|
|
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,
|
|
166
|
+
'One or more capability names to load, from the on-demand capability list.',
|
|
134
167
|
),
|
|
135
168
|
});
|
|
136
169
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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
|
|
174
|
+
interface LoadCapabilityResult extends Partial<
|
|
175
|
+
Pick<PluginManifest, 'summary' | 'whenToUse' | 'whenNotToUse' | 'examples'>
|
|
176
|
+
> {
|
|
177
|
+
name: string;
|
|
143
178
|
/**
|
|
144
|
-
* `true` when the plugin
|
|
145
|
-
*
|
|
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
|
-
/**
|
|
149
|
-
|
|
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
|
|
153
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
-
*
|
|
175
|
-
* - `silent` plugin →
|
|
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
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* with `alreadyAvailable: true
|
|
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
|
|
184
|
-
* (`access.withheldToolNames`). A plugin this call loads
|
|
185
|
-
*
|
|
186
|
-
*
|
|
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
|
|
210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
`Capability "${name}" does not exist.
|
|
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
|
-
|
|
224
|
-
|
|
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
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ToolMessage } from '@langchain/core/messages';
|
|
2
|
+
import { Command, ReducedValue, StateSchema } from '@langchain/langgraph';
|
|
2
3
|
import { type AgentMiddleware, createMiddleware } from 'langchain';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import type { Logger, PluginManifest } from '../../plugin-api/types';
|
|
5
6
|
import { describeRequirements, type CapabilityRequirement } from '../manifest';
|
|
7
|
+
import type { GateLoadResult } from '../meta-tools';
|
|
6
8
|
import { NOOP_LOGGER } from '../utils';
|
|
7
9
|
|
|
8
10
|
type Visibility = NonNullable<PluginManifest['visibility']>;
|
|
@@ -10,6 +12,43 @@ type Visibility = NonNullable<PluginManifest['visibility']>;
|
|
|
10
12
|
/** `response_metadata` key the gate sets on its refusals (never sent to a provider). */
|
|
11
13
|
const REFUSED = 'capabilityGateRefused';
|
|
12
14
|
|
|
15
|
+
const pluginSetUpdate = z
|
|
16
|
+
.union([z.array(z.string()), z.object({ replace: z.array(z.string()) })])
|
|
17
|
+
.optional();
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* What a write to a channel of plugin names does to it: a list adds to the set
|
|
21
|
+
* (`load_capability`, the editor context; two loads in one model step both
|
|
22
|
+
* land), `{ replace }` sets it (a turn that unloads idle plugins, a summary).
|
|
23
|
+
*/
|
|
24
|
+
function nextPluginSet(
|
|
25
|
+
current: string[] | undefined,
|
|
26
|
+
update: z.infer<typeof pluginSetUpdate>,
|
|
27
|
+
): string[] | undefined {
|
|
28
|
+
if (update === undefined) return current;
|
|
29
|
+
if (!Array.isArray(update)) return update.replace;
|
|
30
|
+
return Array.from(new Set([...(current ?? []), ...update]));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The graph channels of the loaded plugins, of the plugins whose usage the
|
|
35
|
+
* thread's messages carry, and of the plugins whose operating guides the
|
|
36
|
+
* system prompt carries (`operating-guides.ts`; last value wins). `createAgent`
|
|
37
|
+
* builds its channels from the middlewares' state schemas: a reducer declared
|
|
38
|
+
* anywhere else never runs.
|
|
39
|
+
*/
|
|
40
|
+
const capabilityGateState = new StateSchema({
|
|
41
|
+
loadedPlugins: new ReducedValue(z.array(z.string()).optional(), {
|
|
42
|
+
inputSchema: pluginSetUpdate,
|
|
43
|
+
reducer: nextPluginSet,
|
|
44
|
+
}),
|
|
45
|
+
capabilityUsage: new ReducedValue(z.array(z.string()).optional(), {
|
|
46
|
+
inputSchema: pluginSetUpdate,
|
|
47
|
+
reducer: nextPluginSet,
|
|
48
|
+
}),
|
|
49
|
+
promptGuides: z.array(z.string()).optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
13
52
|
/**
|
|
14
53
|
* A ToolMessage the gate produced instead of running the tool. The tool never
|
|
15
54
|
* ran, so it is not a failure: the repetition guard must let the same call
|
|
@@ -32,15 +71,10 @@ export interface CapabilityGateMiddlewareOptions {
|
|
|
32
71
|
* middleware does not need access to the registries at runtime.
|
|
33
72
|
*/
|
|
34
73
|
visibilityByToolName: Map<string, Visibility>;
|
|
35
|
-
/**
|
|
36
|
-
* Plugins the capability router preloaded for this turn. Their on-demand
|
|
37
|
-
* tools pass the gate as if loaded, without touching `state.loadedPlugins`.
|
|
38
|
-
*/
|
|
39
|
-
preloadedPlugins?: ReadonlySet<string>;
|
|
40
74
|
/**
|
|
41
75
|
* Plugin name → the `manifest.requires` capabilities the user's delegation
|
|
42
76
|
* does not grant. Such a plugin's tools are hidden and refused whatever
|
|
43
|
-
* their visibility, loaded or
|
|
77
|
+
* their visibility, loaded or not.
|
|
44
78
|
*/
|
|
45
79
|
unmetRequirements?: ReadonlyMap<string, readonly CapabilityRequirement[]>;
|
|
46
80
|
/**
|
|
@@ -50,6 +84,12 @@ export interface CapabilityGateMiddlewareOptions {
|
|
|
50
84
|
* cannot make it available.
|
|
51
85
|
*/
|
|
52
86
|
withheldToolNames?: ReadonlySet<string>;
|
|
87
|
+
/**
|
|
88
|
+
* What the model should read about `plugin` once a hidden tool call loaded
|
|
89
|
+
* it (`CapabilityLoader.describeForGate`): usage, tool names, its operating
|
|
90
|
+
* guide. Called once per such call.
|
|
91
|
+
*/
|
|
92
|
+
describeLoad: (plugin: string) => GateLoadResult;
|
|
53
93
|
/** Optional logger; defaults to a no-op. */
|
|
54
94
|
logger?: Logger;
|
|
55
95
|
}
|
|
@@ -74,13 +114,18 @@ export interface CapabilityGateMiddlewareOptions {
|
|
|
74
114
|
*
|
|
75
115
|
* This is capability discovery, not authorization: any non-silent plugin can
|
|
76
116
|
* be loaded with `load_capability`. What the gate guarantees is that an
|
|
77
|
-
* on-demand tool runs only after that load (or the
|
|
78
|
-
* visible in the thread.
|
|
117
|
+
* on-demand tool runs only after that load (or the router's, in the turn's
|
|
118
|
+
* graph input) is visible in the thread.
|
|
119
|
+
*
|
|
120
|
+
* A call to a hidden tool still never runs, whatever its effect: the model
|
|
121
|
+
* has not seen the tool's schema, so its arguments are a guess. Instead the
|
|
122
|
+
* gate loads the capability on that call, so the refusal costs one step, the
|
|
123
|
+
* call again with the schema now in view, not a `load_capability` step too.
|
|
79
124
|
*/
|
|
80
125
|
export const createCapabilityGateMiddleware = (
|
|
81
126
|
options: CapabilityGateMiddlewareOptions,
|
|
82
127
|
): AgentMiddleware => {
|
|
83
|
-
const { pluginByToolName, visibilityByToolName
|
|
128
|
+
const { pluginByToolName, visibilityByToolName } = options;
|
|
84
129
|
const logger = options.logger ?? NOOP_LOGGER;
|
|
85
130
|
|
|
86
131
|
/** What the user's authorization lacks for `toolName`'s plugin, or null. */
|
|
@@ -102,16 +147,12 @@ export const createCapabilityGateMiddleware = (
|
|
|
102
147
|
if (!plugin) return null;
|
|
103
148
|
const viz = visibilityByToolName.get(toolName) ?? 'on-demand';
|
|
104
149
|
if (viz === 'always' || viz === 'silent') return null;
|
|
105
|
-
|
|
106
|
-
return null;
|
|
107
|
-
return plugin;
|
|
150
|
+
return loaded.has(plugin) ? null : plugin;
|
|
108
151
|
};
|
|
109
152
|
|
|
110
153
|
return createMiddleware({
|
|
111
154
|
name: 'CapabilityGateMiddleware',
|
|
112
|
-
stateSchema:
|
|
113
|
-
loadedPlugins: z.array(z.string()).optional(),
|
|
114
|
-
}),
|
|
155
|
+
stateSchema: capabilityGateState,
|
|
115
156
|
// No `contextSchema`: the gate reads only `state.loadedPlugins`, and
|
|
116
157
|
// declaring one would make LangChain reject every invocation that omits
|
|
117
158
|
// `context` — the tool wrapper already carries the request context.
|
|
@@ -132,10 +173,7 @@ export const createCapabilityGateMiddleware = (
|
|
|
132
173
|
|
|
133
174
|
if (filtered.length !== request.tools.length) {
|
|
134
175
|
logger.log(
|
|
135
|
-
`[CapabilityGateMiddleware] exposed ${filtered.length}/${request.tools.length} tools; loadedPlugins=${Array.from(loaded).join(',') || '∅'}
|
|
136
|
-
(preloadedPlugins?.size
|
|
137
|
-
? ` preloadedPlugins=${Array.from(preloadedPlugins).join(',')}`
|
|
138
|
-
: ''),
|
|
176
|
+
`[CapabilityGateMiddleware] exposed ${filtered.length}/${request.tools.length} tools; loadedPlugins=${Array.from(loaded).join(',') || '∅'}`,
|
|
139
177
|
);
|
|
140
178
|
}
|
|
141
179
|
|
|
@@ -178,16 +216,27 @@ export const createCapabilityGateMiddleware = (
|
|
|
178
216
|
);
|
|
179
217
|
if (plugin === null) return handler(request);
|
|
180
218
|
logger.warn(
|
|
181
|
-
`[CapabilityGateMiddleware] refused a call to ${name}
|
|
219
|
+
`[CapabilityGateMiddleware] refused a call to ${name} and loaded capability '${plugin}'`,
|
|
182
220
|
);
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
221
|
+
const load = options.describeLoad(plugin);
|
|
222
|
+
return new Command({
|
|
223
|
+
update: {
|
|
224
|
+
loadedPlugins: [plugin],
|
|
225
|
+
capabilityUsage: [plugin],
|
|
226
|
+
messages: [
|
|
227
|
+
new ToolMessage({
|
|
228
|
+
content:
|
|
229
|
+
`Tool "${name}" was not run: it belongs to the "${plugin}" capability, which was not loaded. ` +
|
|
230
|
+
`It is loaded now, so the tool is in your tool list from your next step. If the user's request needs it, check the arguments against its schema and call it again.\n\n` +
|
|
231
|
+
load.text,
|
|
232
|
+
tool_call_id: request.toolCall.id ?? '',
|
|
233
|
+
name,
|
|
234
|
+
status: 'error',
|
|
235
|
+
additional_kwargs: load.additional_kwargs,
|
|
236
|
+
response_metadata: { [REFUSED]: true },
|
|
237
|
+
}),
|
|
238
|
+
],
|
|
239
|
+
},
|
|
191
240
|
});
|
|
192
241
|
},
|
|
193
242
|
});
|