@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
@@ -1,5 +1,9 @@
1
1
  import { z } from 'zod';
2
- import type { ManifestExample, PluginManifest } from '../plugin-api/types';
2
+ import type {
3
+ ManifestExample,
4
+ PluginManifest,
5
+ PluginTool,
6
+ } from '../plugin-api/types';
3
7
 
4
8
  // ── Schema ──────────────────────────────────────────────────────────────────
5
9
 
@@ -48,6 +52,9 @@ export const pluginManifestSchema: z.ZodType<PluginManifest> = z.object({
48
52
  tags: z.array(z.string()).optional(),
49
53
  category: manifestCategorySchema.optional(),
50
54
  visibility: manifestVisibilitySchema.optional(),
55
+ brief: z.string().optional(),
56
+ onDemandTools: z.array(z.string().min(1)).optional(),
57
+ routable: z.boolean().optional(),
51
58
  stability: manifestStabilitySchema.optional(),
52
59
  requires: z
53
60
  .array(z.object({ resource: z.string().min(1), action: z.string().min(1) }))
@@ -69,6 +76,19 @@ export function unmetRequirements(
69
76
  );
70
77
  }
71
78
 
79
+ /**
80
+ * A tool's effective visibility: its own `visibility`, else the manifest's
81
+ * `onDemandTools`, else the plugin's visibility (`on-demand` by default).
82
+ */
83
+ export function effectiveToolVisibility(
84
+ tool: Pick<PluginTool, 'name' | 'visibility'>,
85
+ manifest: PluginManifest | undefined,
86
+ ): NonNullable<PluginManifest['visibility']> {
87
+ if (tool.visibility) return tool.visibility;
88
+ if (manifest?.onDemandTools?.includes(tool.name)) return 'on-demand';
89
+ return manifest?.visibility ?? 'on-demand';
90
+ }
91
+
72
92
  /** `can` on `with` pairs for a message: "`*` on `ixo:filesystem`, …". */
73
93
  export function describeRequirements(
74
94
  requirements: readonly CapabilityRequirement[],
@@ -90,6 +110,7 @@ export interface ManifestValidationResult {
90
110
  /** Soft-cap thresholds — violations only emit warnings. */
91
111
  const SOFT_LIMITS = {
92
112
  summaryMaxChars: 120,
113
+ briefMaxChars: 60,
93
114
  whenToUseMaxItems: 8,
94
115
  whenToUseItemMaxChars: 100,
95
116
  whenNotToUseMaxItems: 4,
@@ -161,6 +182,13 @@ export function validateManifest(
161
182
  );
162
183
  }
163
184
 
185
+ // Soft rule: brief length cap.
186
+ if (m.brief && m.brief.length > SOFT_LIMITS.briefMaxChars) {
187
+ warnings.push(
188
+ `${tag(pluginName, 'brief')} is ${m.brief.length} chars (recommended ≤ ${SOFT_LIMITS.briefMaxChars}).`,
189
+ );
190
+ }
191
+
164
192
  // Soft rule: whenToUse caps.
165
193
  if (m.whenToUse.length > SOFT_LIMITS.whenToUseMaxItems) {
166
194
  warnings.push(
@@ -202,9 +230,9 @@ export function validateManifest(
202
230
  }
203
231
 
204
232
  /**
205
- * Cross-check `examples[].tool` references against the set of tool names
206
- * actually registered by this plugin. Run after the plugin's `getTools()`
207
- * has been invoked.
233
+ * Cross-check `examples[].tool` and `onDemandTools` references against the
234
+ * set of tool names actually registered by this plugin. Run after the
235
+ * plugin's `getTools()` has been invoked.
208
236
  *
209
237
  * **Lenient for request-time plugins**: when `registeredToolNames` is empty,
210
238
  * the plugin's tools come from `getRequestTools` — boot-time validation
@@ -218,18 +246,23 @@ export function validateExamplesAgainstTools(
218
246
  pluginName: string,
219
247
  ): { errors: string[] } {
220
248
  const errors: string[] = [];
221
- if (!manifest.examples) return { errors };
222
-
223
249
  if (registeredToolNames.length === 0) return { errors };
224
250
 
225
251
  const known = new Set(registeredToolNames);
226
- manifest.examples.forEach((ex, i) => {
252
+ manifest.examples?.forEach((ex, i) => {
227
253
  if (!known.has(ex.tool)) {
228
254
  errors.push(
229
255
  `${tag(pluginName, `examples[${i}].tool`)} references unknown tool "${ex.tool}".`,
230
256
  );
231
257
  }
232
258
  });
259
+ manifest.onDemandTools?.forEach((name, i) => {
260
+ if (!known.has(name)) {
261
+ errors.push(
262
+ `${tag(pluginName, `onDemandTools[${i}]`)} references unknown tool "${name}".`,
263
+ );
264
+ }
265
+ });
233
266
 
234
267
  return { errors };
235
268
  }
@@ -273,8 +306,20 @@ export interface Tier1Entry {
273
306
  manifest: PluginManifest;
274
307
  }
275
308
 
309
+ /** One line of the on-demand catalogue. */
310
+ export interface CatalogueEntry extends Tier1Entry {
311
+ /** The user's authorization lacks what the plugin requires. */
312
+ unavailable?: boolean;
313
+ }
314
+
276
315
  export interface Tier1Input {
277
316
  manifests: Tier1Entry[];
317
+ /**
318
+ * Capabilities with something left to load this turn. Rendered whether or
319
+ * not the thread already loaded them, so the block does not change when a
320
+ * plugin is loaded and stays inside the provider's prompt cache.
321
+ */
322
+ catalogue?: CatalogueEntry[];
278
323
  /** Soft budget in tokens. Default 5000. Exceeding it triggers a warning. */
279
324
  tokenBudget?: number;
280
325
  /**
@@ -283,12 +328,6 @@ export interface Tier1Input {
283
328
  * budget is a soft operator hint where ±20% is immaterial.
284
329
  */
285
330
  estimateTokens?: (text: string) => number;
286
- /**
287
- * Whether the meta-tools are bound this turn. Default `true`. Pass `false`
288
- * when they are not, so the block does not end by telling the model to
289
- * call `list_capabilities` / `load_capability`.
290
- */
291
- capabilityDiscovery?: boolean;
292
331
  }
293
332
 
294
333
  export interface Tier1Output {
@@ -303,8 +342,10 @@ export interface Tier1Output {
303
342
  const DEFAULT_BUDGET = 5000;
304
343
 
305
344
  const HEADER = '## Available Capabilities\n\n';
306
- const FOOTER =
307
- '\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.';
345
+ const CATALOGUE_HEADER =
346
+ '## On-demand capabilities\n\nNot in your tool list until loaded. Call `load_capability({ names: [...] })` once with all you need, right from this list (no `list_capabilities` needed); the tools join your list on the next step. Anything already in your tool list needs no loading.\n\n';
347
+ /** Catalogue line of a plugin that declares no `brief`. */
348
+ const CLIPPED_SUMMARY_CHARS = 60;
308
349
 
309
350
  /**
310
351
  * Chars/4 token estimate. Replaces the Node runtime's `js-tiktoken`
@@ -331,6 +372,37 @@ function formatExample(ex: ManifestExample): string {
331
372
  return `"${ex.user}" → ${argSummary}`;
332
373
  }
333
374
 
375
+ /** The manifest's `brief`, or its summary clipped at a word boundary. */
376
+ function catalogueText(manifest: PluginManifest): string {
377
+ if (manifest.brief) return manifest.brief;
378
+ const summary = manifest.summary.trim();
379
+ if (summary.length <= CLIPPED_SUMMARY_CHARS) return summary;
380
+ const clipped = summary.slice(0, CLIPPED_SUMMARY_CHARS);
381
+ const boundary = clipped.lastIndexOf(' ');
382
+ return `${boundary > 0 ? clipped.slice(0, boundary) : clipped}…`;
383
+ }
384
+
385
+ function formatCatalogueLine({
386
+ pluginName,
387
+ manifest,
388
+ unavailable,
389
+ }: CatalogueEntry): string {
390
+ const line = `- ${pluginName}: ${catalogueText(manifest)}`;
391
+ return unavailable ? `${line} (not authorized by this user)` : line;
392
+ }
393
+
394
+ /**
395
+ * The first example the prompt may teach: one naming a tool still behind
396
+ * `load_capability` would invite a call the capability gate refuses.
397
+ */
398
+ function firstBoundExample(
399
+ manifest: PluginManifest,
400
+ ): ManifestExample | undefined {
401
+ return manifest.examples?.find(
402
+ (example) => !manifest.onDemandTools?.includes(example.tool),
403
+ );
404
+ }
405
+
334
406
  function formatEntry(entry: Tier1Entry): string {
335
407
  const { pluginName, manifest } = entry;
336
408
  const lines: string[] = [`- **${pluginName}** — ${manifest.summary}`];
@@ -344,7 +416,7 @@ function formatEntry(entry: Tier1Entry): string {
344
416
  lines.push(` - Avoid for: ${manifest.whenNotToUse[0]}`);
345
417
  }
346
418
 
347
- const firstExample = manifest.examples?.[0];
419
+ const firstExample = firstBoundExample(manifest);
348
420
  if (firstExample) {
349
421
  lines.push(` - Example: ${formatExample(firstExample)}`);
350
422
  }
@@ -355,10 +427,11 @@ function formatEntry(entry: Tier1Entry): string {
355
427
  /**
356
428
  * Compose the Tier-1 capability block from a list of plugin manifests.
357
429
  *
358
- * Only `visibility: 'always'` manifests are included, sorted alphabetically
359
- * for prompt-caching determinism. Over-budget calls produce a warning naming
360
- * the largest manifests so operators can mark them `'on-demand'` — degrading
361
- * verbosity is an operator decision, not a runtime guess.
430
+ * Full entries for the `visibility: 'always'` manifests, then one line per
431
+ * `catalogue` entry, each list sorted alphabetically for prompt-caching
432
+ * determinism. Over-budget calls produce a warning naming the largest
433
+ * manifests so operators can mark them `'on-demand'` — degrading verbosity
434
+ * is an operator decision, not a runtime guess.
362
435
  */
363
436
  export function renderTier1(input: Tier1Input): Tier1Output {
364
437
  const budget = input.tokenBudget ?? DEFAULT_BUDGET;
@@ -369,12 +442,20 @@ export function renderTier1(input: Tier1Input): Tier1Output {
369
442
  .slice()
370
443
  .sort((a, b) => a.pluginName.localeCompare(b.pluginName));
371
444
 
372
- if (entries.length === 0) {
445
+ const catalogue = (input.catalogue ?? [])
446
+ .slice()
447
+ .sort((a, b) => a.pluginName.localeCompare(b.pluginName))
448
+ .map(formatCatalogueLine);
449
+
450
+ if (entries.length === 0 && catalogue.length === 0) {
373
451
  return { block: '', tokens: 0, warnings: [] };
374
452
  }
375
453
 
376
454
  const renderedLines = entries.map(formatEntry);
377
- const tokens = renderedLines.reduce((sum, line) => sum + estimate(line), 0);
455
+ const tokens = [...renderedLines, ...catalogue].reduce(
456
+ (sum, line) => sum + estimate(line),
457
+ 0,
458
+ );
378
459
  const warnings: string[] = [];
379
460
 
380
461
  if (tokens > budget) {
@@ -393,7 +474,11 @@ export function renderTier1(input: Tier1Input): Tier1Output {
393
474
  );
394
475
  }
395
476
 
396
- const footer = input.capabilityDiscovery === false ? '' : FOOTER;
397
- const block = `${HEADER}${renderedLines.join('\n\n')}${footer}`;
477
+ const block = [
478
+ entries.length > 0 ? `${HEADER}${renderedLines.join('\n\n')}` : '',
479
+ catalogue.length > 0 ? `${CATALOGUE_HEADER}${catalogue.join('\n')}` : '',
480
+ ]
481
+ .filter((section) => section !== '')
482
+ .join('\n\n');
398
483
  return { block, tokens, warnings };
399
484
  }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * `@langchain/mcp-adapters` answers a tool that returns `structuredContent`
3
+ * with one block holding the text and the structured value side by side, and
4
+ * a tool's non-string result is stringified for the model. A server's text
5
+ * block is normally the JSON of its structured content (the MCP specification
6
+ * asks for that), so the model reads the whole result twice, the first copy
7
+ * with every quote escaped.
8
+ */
9
+
10
+ function isRecord(value: unknown): value is Record<string, unknown> {
11
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
12
+ }
13
+
14
+ /** Whether two JSON values are equal, whatever the order of their keys. */
15
+ function sameJson(a: unknown, b: unknown): boolean {
16
+ if (Array.isArray(a))
17
+ return (
18
+ Array.isArray(b) &&
19
+ a.length === b.length &&
20
+ a.every((item, index) => sameJson(item, b[index]))
21
+ );
22
+ if (isRecord(a)) {
23
+ if (!isRecord(b)) return false;
24
+ const keys = Object.keys(a);
25
+ return (
26
+ keys.length === Object.keys(b).length &&
27
+ keys.every((key) => key in b && sameJson(a[key], b[key]))
28
+ );
29
+ }
30
+ return a === b;
31
+ }
32
+
33
+ function parsesTo(text: string, value: unknown): boolean {
34
+ try {
35
+ return sameJson(JSON.parse(text), value);
36
+ } catch {
37
+ return false;
38
+ }
39
+ }
40
+
41
+ /**
42
+ * A tool's output without a structured copy of its text. Only an exact copy
43
+ * goes: a server whose text says something else (a summary beside the data)
44
+ * keeps both, and a block with `meta` keeps it. Anything else passes through.
45
+ */
46
+ export function withoutStructuredCopy(output: unknown): unknown {
47
+ if (
48
+ !isRecord(output) ||
49
+ output.type !== 'text' ||
50
+ typeof output.text !== 'string' ||
51
+ !parsesTo(output.text, output.structuredContent)
52
+ )
53
+ return output;
54
+ const { structuredContent: _copy, ...block } = output;
55
+ return 'meta' in block ? block : block.text;
56
+ }