@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
@@ -17,17 +17,30 @@ import type {
17
17
  } from '../plugin-api/types';
18
18
  import type { MainAgentArgs, MainAgentBuildResult } from './main-agent-types';
19
19
  import {
20
+ effectiveToolVisibility,
20
21
  renderTier1,
21
22
  unmetRequirements as unmetManifestRequirements,
22
23
  type CapabilityRequirement,
24
+ type CatalogueEntry,
23
25
  type Tier1Entry,
24
26
  } from './manifest';
25
- import { buildMetaTools, META_TOOL_NAMES } from './meta-tools';
26
- import { buildReadResultTool, READ_RESULT_TOOL_NAME } from './read-result-tool';
27
+ import { idlePlugins } from './capability-expiry';
28
+ import {
29
+ buildMetaTools,
30
+ createCapabilityLoader,
31
+ META_TOOL_NAMES,
32
+ } from './meta-tools';
33
+ import {
34
+ buildReadResultTool,
35
+ READ_RESULT_TOOL_NAME,
36
+ readResultMaxChars,
37
+ } from './read-result-tool';
27
38
  import { describeBudget } from './context-budget';
39
+ import { planCapabilityContext, renderGuideNote } from './operating-guides';
28
40
  import { createContextGuardMiddleware } from './middlewares/context-guard';
29
41
  import { repetitionCapsFromEnv } from './middlewares/tool-repetition-guard';
30
42
  import {
43
+ createAttachmentRecallMiddleware,
31
44
  createByoHistorySanitizerMiddleware,
32
45
  createCapabilityGateMiddleware,
33
46
  createDanglingToolCallRepairMiddleware,
@@ -62,7 +75,6 @@ import {
62
75
  dropShadowingRequestEntries,
63
76
  formatByPlugin,
64
77
  turnToolSummaries,
65
- type ManifestRegistry,
66
78
  } from './registries';
67
79
  import {
68
80
  buildPluginContext,
@@ -89,12 +101,6 @@ import { ixoAddressOf, renderUserIdentity } from './user-identity';
89
101
 
90
102
  const PLUGIN_LOGGER_COMPONENT = 'main-agent';
91
103
 
92
- const DEFAULT_OPERATIONAL_MODE = [
93
- '**General conversation mode**',
94
- '',
95
- 'Default to conversation mode, using available capabilities for recall, search, and task delegation.',
96
- ].join('\n');
97
-
98
104
  type CollectedTool = { pluginName: string; tool: PluginTool };
99
105
 
100
106
  /** The validated env values the user-identity section names. */
@@ -104,31 +110,16 @@ const identityConfigSchema = z.object({
104
110
  ORACLE_ENTITY_DID: z.string().min(1).optional(),
105
111
  });
106
112
 
107
- /** Map plugin name → effective `manifest.visibility` (default `on-demand`). */
108
- function visibilityIndex(
109
- manifests: ManifestRegistry,
110
- ): Map<string, NonNullable<PluginManifest['visibility']>> {
111
- const out = new Map<string, NonNullable<PluginManifest['visibility']>>();
112
- for (const { pluginName, manifest } of manifests.collect()) {
113
- out.set(pluginName, manifest.visibility ?? 'on-demand');
114
- }
115
- return out;
116
- }
117
-
118
- /**
119
- * Filter collected tools by their effective visibility — per-tool override
120
- * wins; otherwise the plugin's manifest visibility decides.
121
- */
113
+ /** Filter collected tools by their effective visibility. */
122
114
  function selectByVisibility(
123
115
  tools: CollectedTool[],
124
- manifestViz: Map<string, NonNullable<PluginManifest['visibility']>>,
116
+ manifestOf: Map<string, PluginManifest>,
125
117
  visibility: NonNullable<PluginManifest['visibility']>,
126
118
  ): CollectedTool[] {
127
- return tools.filter(({ pluginName, tool }) => {
128
- const effective =
129
- tool.visibility ?? manifestViz.get(pluginName) ?? 'on-demand';
130
- return effective === visibility;
131
- });
119
+ return tools.filter(
120
+ ({ pluginName, tool }) =>
121
+ effectiveToolVisibility(tool, manifestOf.get(pluginName)) === visibility,
122
+ );
132
123
  }
133
124
 
134
125
  /**
@@ -172,7 +163,7 @@ export async function createMainAgent(
172
163
  ambient,
173
164
  state,
174
165
  availablePlugins,
175
- preloadedPlugins,
166
+ routeCapabilities,
176
167
  checkpointer,
177
168
  abortSignal,
178
169
  byoProvider,
@@ -248,6 +239,7 @@ export async function createMainAgent(
248
239
  toolSurfaces: new Map(),
249
240
  subAgentToolNames: new Set(),
250
241
  context: { user: requestCtx.user, session: requestCtx.session },
242
+ stateUpdate: {},
251
243
  };
252
244
  }
253
245
 
@@ -266,14 +258,12 @@ export async function createMainAgent(
266
258
  state.loadedPlugins,
267
259
  );
268
260
 
269
- // What the turn treats as loaded: the thread's checkpointed plugins plus the
270
- // router's one-turn preload. Only the RuntimeContext and the gate see this
271
- // union; the graph state keeps the checkpointed list alone.
272
- const threadLoaded: ReadonlySet<string> = new Set(state.loadedPlugins ?? []);
273
- const loadedSet = new Set<string>([
274
- ...threadLoaded,
275
- ...(preloadedPlugins ?? []),
276
- ]);
261
+ // What the thread loaded when the turn starts. Tools and sub-agents are
262
+ // collected first: what is left to load depends on them. A cold turn then
263
+ // drops plugins from this set and the router adds to it, below; it is one
264
+ // set from there on, also what tool handlers see as loaded.
265
+ const priorLoaded: ReadonlySet<string> = new Set(state.loadedPlugins ?? []);
266
+ const loadedSet = new Set<string>(priorLoaded);
277
267
  // Carry the prior request state (browserTools, agActions, …) into the
278
268
  // per-request RuntimeContext and the tool-wrapper closures, but NOT the
279
269
  // message history: keeping the full `messages` array here would pin the
@@ -410,22 +400,6 @@ export async function createMainAgent(
410
400
  const missing = unmetManifestRequirements(manifest, hasCapability);
411
401
  if (missing.length > 0) unmetRequirements.set(pluginName, missing);
412
402
  }
413
- // The router predicts before the tools are collected (see
414
- // `routableCandidates`): its preload of a plugin left with nothing, or of
415
- // one whose requirements the delegation does not meet, is void, for the
416
- // gate and for what handlers see as loaded.
417
- const turnPreloads = preloadedPlugins
418
- ? new Set(
419
- [...preloadedPlugins].filter(
420
- (pluginName) =>
421
- !toolAccess.hiddenPlugins.has(pluginName) &&
422
- !unmetRequirements.has(pluginName),
423
- ),
424
- )
425
- : undefined;
426
- for (const pluginName of preloadedPlugins ?? [])
427
- if (!turnPreloads?.has(pluginName) && !threadLoaded.has(pluginName))
428
- loadedSet.delete(pluginName);
429
403
 
430
404
  // The per-turn tool-surface line. Request tools are named in full (there
431
405
  // are only a handful and they are the ones that vary turn to turn); the
@@ -439,7 +413,9 @@ export async function createMainAgent(
439
413
  ambient.logger.debug?.(
440
414
  `[main-agent] full tool surface: ${formatByPlugin(allTools) || '∅'}`,
441
415
  );
442
- const manifestViz = visibilityIndex(registries.manifests);
416
+ const manifestOf = new Map(
417
+ manifestEntries.map(({ pluginName, manifest }) => [pluginName, manifest]),
418
+ );
443
419
  if (unmetRequirements.size > 0)
444
420
  ambient.logger.log(
445
421
  `[main-agent] not usable by ${requestCtx.user.did} (authorization lacks what they require): ${[...unmetRequirements.keys()].join(', ')}`,
@@ -451,27 +427,152 @@ export async function createMainAgent(
451
427
  ]),
452
428
  );
453
429
 
454
- const eagerTools = selectByVisibility(allTools, manifestViz, 'always');
430
+ // Lookups used by `CapabilityGateMiddleware` to gate on-demand plugins
431
+ // and sub-agents per model call. Meta-tools omitted from the map are
432
+ // pass-through.
433
+ const pluginByToolName = new Map<string, string>();
434
+ const visibilityByToolName = new Map<
435
+ string,
436
+ NonNullable<PluginManifest['visibility']>
437
+ >();
438
+ for (const { pluginName, tool } of allTools) {
439
+ pluginByToolName.set(tool.name, pluginName);
440
+ visibilityByToolName.set(
441
+ tool.name,
442
+ effectiveToolVisibility(tool, manifestOf.get(pluginName)),
443
+ );
444
+ }
445
+ for (const { pluginName, subAgent } of subAgentEntries) {
446
+ const toolName = computeSubAgentToolName(subAgent.name);
447
+ pluginByToolName.set(toolName, pluginName);
448
+ visibilityByToolName.set(
449
+ toolName,
450
+ effectiveToolVisibility({ name: toolName }, manifestOf.get(pluginName)),
451
+ );
452
+ }
453
+
454
+ // This turn's own collection (the shared registry holds no request tools),
455
+ // already cut to what the delegation reaches. A sub-agent counts: to the
456
+ // model its `call_*` tool is what a plugin like firecrawl offers.
457
+ const turnSummaries = turnToolSummaries([
458
+ ...toolAccess.tools,
459
+ ...subAgentEntries.map(({ pluginName, subAgent }) => ({
460
+ pluginName,
461
+ tool: {
462
+ name: computeSubAgentToolName(subAgent.name),
463
+ description: subAgent.description,
464
+ },
465
+ })),
466
+ ]);
467
+ // Shared by `load_capability` and the capability gate's load on a hidden
468
+ // call, so one turn reads a plugin's guide once whichever loaded it.
469
+ const capabilityLoader = createCapabilityLoader(
470
+ registries.manifests,
471
+ turnSummaries,
472
+ toolAccess,
473
+ );
474
+
475
+ // A cold turn drops the capabilities the thread has moved on from, from the
476
+ // gate, the prompt and what handlers see as loaded. The caller writes the
477
+ // same change to the graph (capability-expiry.ts).
478
+ const expiredPlugins = args.capabilityExpiry
479
+ ? idlePlugins({
480
+ messages: args.capabilityExpiry.messages,
481
+ loaded: args.capabilityExpiry.loaded,
482
+ pluginOf: (toolName) => pluginByToolName.get(toolName),
483
+ maxIdleTurns: args.capabilityExpiry.maxIdleTurns,
484
+ })
485
+ : [];
486
+ for (const pluginName of expiredPlugins) loadedSet.delete(pluginName);
487
+ if (expiredPlugins.length > 0)
488
+ ambient.logger.log(
489
+ `[main-agent] unloaded idle capabilities: ${expiredPlugins.join(', ')}`,
490
+ );
491
+
492
+ // One line per capability with something left to load this turn: named by
493
+ // the turn's own tools, so a plugin with nothing to offer this turn (no
494
+ // browser tools declared, no editor open) stays out; one the user's
495
+ // authorization lacks stays in, and `load_capability` says what is missing.
496
+ const gatedPlugins = new Set(
497
+ [...visibilityByToolName]
498
+ .filter(([, visibility]) => visibility === 'on-demand')
499
+ .flatMap(([toolName]) => pluginByToolName.get(toolName) ?? []),
500
+ );
501
+ const catalogue: CatalogueEntry[] = manifestEntries
502
+ .filter(
503
+ ({ pluginName, manifest }) =>
504
+ manifest.visibility !== 'silent' &&
505
+ !toolAccess.hiddenPlugins.has(pluginName) &&
506
+ (gatedPlugins.has(pluginName) || unmetRequirements.has(pluginName)),
507
+ )
508
+ .map((entry) => ({
509
+ ...entry,
510
+ unavailable: unmetRequirements.has(entry.pluginName),
511
+ }));
512
+
513
+ // The router chooses from what a `load_capability` could load right now, so
514
+ // what it returns needs no further check. A preload is such a load, made
515
+ // before the first model call: the tool list changes once, then stays.
516
+ const routable = catalogue.filter(
517
+ ({ pluginName, manifest, unavailable }) =>
518
+ manifest.routable !== false && !unavailable && !loadedSet.has(pluginName),
519
+ );
520
+ const routed =
521
+ routable.length > 0 ? await routeCapabilities?.(routable) : undefined;
522
+ const preloaded = routable
523
+ .map(({ pluginName }) => pluginName)
524
+ .filter((pluginName) => routed?.has(pluginName));
525
+ for (const pluginName of preloaded) loadedSet.add(pluginName);
526
+
527
+ // Where each plugin's guide and usage go this turn. A plugin the model loads
528
+ // during the turn gets both with the load result, whatever is decided here.
529
+ const usablePlugin = (pluginName: string): boolean =>
530
+ manifestOf.get(pluginName) !== undefined &&
531
+ manifestOf.get(pluginName)?.visibility !== 'silent' &&
532
+ !unmetRequirements.has(pluginName) &&
533
+ !toolAccess.hiddenPlugins.has(pluginName);
534
+ const capabilityPlan = planCapabilityContext({
535
+ guides: registries.manifests
536
+ .operatingGuides()
537
+ .filter(({ pluginName }) => usablePlugin(pluginName)),
538
+ loaded: loadedSet,
539
+ priorLoaded,
540
+ usable: usablePlugin,
541
+ alwaysVisible: (pluginName) =>
542
+ manifestOf.get(pluginName)?.visibility === 'always',
543
+ ...(args.promptCache ? { cache: args.promptCache } : {}),
544
+ });
545
+ const guidesInPrompt = new Set(
546
+ capabilityPlan.inPrompt.map(({ pluginName }) => pluginName),
547
+ );
548
+ const notedLoads = capabilityPlan.onMessage.flatMap((pluginName) => {
549
+ const manifest = manifestOf.get(pluginName);
550
+ return manifest
551
+ ? [
552
+ capabilityLoader.describe(
553
+ pluginName,
554
+ manifest,
555
+ guidesInPrompt.has(pluginName),
556
+ ),
557
+ ]
558
+ : [];
559
+ });
560
+
561
+ const eagerTools = selectByVisibility(allTools, manifestOf, 'always');
455
562
  // Bind ALL on-demand tools at compile time — gating happens per model call
456
563
  // in `CapabilityGateMiddleware` based on the live `loadedPlugins` state.
457
564
  // This lets `load_capability` take effect on the very next LLM call inside
458
565
  // the same run, instead of waiting for the next request to rebuild.
459
- const onDemandTools = selectByVisibility(allTools, manifestViz, 'on-demand');
460
- const silentTools = selectByVisibility(allTools, manifestViz, 'silent');
566
+ const onDemandTools = selectByVisibility(allTools, manifestOf, 'on-demand');
567
+ const silentTools = selectByVisibility(allTools, manifestOf, 'silent');
461
568
 
462
569
  // ── 4. Wrap tools (meta + plugin) so handlers receive a RuntimeContext ──
463
570
  const metaTools = [
464
571
  ...buildMetaTools({
465
572
  manifestRegistry: registries.manifests,
466
- // This turn's own collection (the shared registry holds no request
467
- // tools), already cut to what the delegation reaches.
468
- toolRegistry: turnToolSummaries(toolAccess.tools),
469
- toolAccess: {
470
- ...toolAccess,
471
- preloadedOnly: new Set(
472
- [...(turnPreloads ?? [])].filter((name) => !threadLoaded.has(name)),
473
- ),
474
- },
573
+ toolRegistry: turnSummaries,
574
+ toolAccess,
575
+ loader: capabilityLoader,
475
576
  }),
476
577
  // Pages through tool results the result cap saved whole (result-cap.ts);
477
578
  // its chunks stay well under the cap so a page is never capped itself.
@@ -479,12 +580,8 @@ export async function createMainAgent(
479
580
  ? [
480
581
  buildReadResultTool({
481
582
  read: hooks.readResult,
482
- maxChars: Math.min(
483
- 16_000,
484
- Math.max(
485
- 512,
486
- Math.floor((contextBudget?.resultCapChars ?? 32_000) / 2),
487
- ),
583
+ maxChars: readResultMaxChars(
584
+ contextBudget?.resultCapChars ?? 32_000,
488
585
  ),
489
586
  }),
490
587
  ]
@@ -647,29 +744,6 @@ export async function createMainAgent(
647
744
  ...domainTools,
648
745
  ];
649
746
 
650
- // Lookups used by `CapabilityGateMiddleware` to gate on-demand plugins
651
- // and sub-agents per model call. Meta-tools omitted from the map are
652
- // pass-through.
653
- const pluginByToolName = new Map<string, string>();
654
- const visibilityByToolName = new Map<
655
- string,
656
- NonNullable<PluginManifest['visibility']>
657
- >();
658
- for (const { pluginName, tool } of allTools) {
659
- pluginByToolName.set(tool.name, pluginName);
660
- const effective =
661
- tool.visibility ?? manifestViz.get(pluginName) ?? 'on-demand';
662
- visibilityByToolName.set(tool.name, effective);
663
- }
664
- for (const { pluginName, subAgent } of subAgentEntries) {
665
- const toolName = computeSubAgentToolName(subAgent.name);
666
- pluginByToolName.set(toolName, pluginName);
667
- visibilityByToolName.set(
668
- toolName,
669
- manifestViz.get(pluginName) ?? 'on-demand',
670
- );
671
- }
672
-
673
747
  // ── 6. Middleware stack — always-on + plugin contributions ──────────────
674
748
  const pluginMiddlewares = [
675
749
  ...registries.middlewares.collect(buildCtx),
@@ -756,9 +830,9 @@ export async function createMainAgent(
756
830
  createCapabilityGateMiddleware({
757
831
  pluginByToolName,
758
832
  visibilityByToolName,
759
- preloadedPlugins: turnPreloads,
760
833
  unmetRequirements,
761
834
  withheldToolNames: toolAccess.withheldToolNames,
835
+ describeLoad: capabilityLoader.describeForGate,
762
836
  logger: ambient.logger,
763
837
  }),
764
838
  userContextStateMiddleware(),
@@ -829,9 +903,10 @@ export async function createMainAgent(
829
903
  ...(returnDirectNames.size > 0
830
904
  ? [createReturnDirectFirstMiddleware(returnDirectNames)]
831
905
  : []),
832
- // Innermost: sees the request exactly as it goes to the provider. Prunes
833
- // old tool results under pressure, refuses what cannot fit, and learns
834
- // the window from a provider overflow (context-guard.ts).
906
+ // Innermost but one: sees the request as it goes to the provider, except
907
+ // the images the recall middleware below adds inside it. Prunes old tool
908
+ // results under pressure, refuses what cannot fit, and learns the window
909
+ // from a provider overflow (context-guard.ts).
835
910
  ...(contextBudget
836
911
  ? [
837
912
  createContextGuardMiddleware({
@@ -840,6 +915,26 @@ export async function createMainAgent(
840
915
  ? { onOverflow: hooks.onContextOverflow }
841
916
  : {}),
842
917
  ...(hooks?.onContextEvent ? { onEvent: hooks.onContextEvent } : {}),
918
+ ...(hooks?.readResult
919
+ ? {
920
+ readResultChars: readResultMaxChars(
921
+ contextBudget.resultCapChars,
922
+ ),
923
+ }
924
+ : {}),
925
+ logger: ambient.logger,
926
+ }),
927
+ ]
928
+ : []),
929
+ // Inside the guard: recalled images (attachment-recall.ts) reach the
930
+ // provider but are not counted as chars/4 text, which their base64 is
931
+ // not (they are bounded by the recall's own caps instead); graph state
932
+ // never holds them. A provider refusal of a request carrying them is
933
+ // retried here once without them, before the guard sees the error.
934
+ ...(hooks?.attachmentRecall
935
+ ? [
936
+ createAttachmentRecallMiddleware({
937
+ ...hooks.attachmentRecall,
843
938
  logger: ambient.logger,
844
939
  }),
845
940
  ]
@@ -867,7 +962,7 @@ export async function createMainAgent(
867
962
  pluginName,
868
963
  manifest: withoutWithheldExamples(manifest, toolAccess.withheldToolNames),
869
964
  }));
870
- const tier1 = renderTier1({ manifests: eagerEntries });
965
+ const tier1 = renderTier1({ manifests: eagerEntries, catalogue });
871
966
  for (const warning of tier1.warnings) ambient.logger.warn(warning);
872
967
 
873
968
  const customInstructions = identity.prompt?.customInstructions?.trim() ?? '';
@@ -884,35 +979,15 @@ export async function createMainAgent(
884
979
  origin === 'request' && pluginName === PORTAL_CAPABILITY,
885
980
  )
886
981
  .map(({ tool }) => tool.name),
887
- bound:
888
- manifestViz.get(PORTAL_CAPABILITY) === 'always' ||
889
- loadedSet.has(PORTAL_CAPABILITY),
982
+ bound: manifestOf.get(PORTAL_CAPABILITY)?.visibility === 'always',
890
983
  };
891
984
 
892
- // Operating guides of the plugins in use when the turn starts — loaded on
893
- // the thread or `always` visible — and usable by this delegation. A router
894
- // preload lasts one turn: its guide would enter the prompt and leave it
895
- // again on the next turn, so it comes with `load_capability` instead, as
896
- // does the guide of a plugin loaded mid-turn (here from the next turn on).
897
- const operatingGuides = registries.manifests
898
- .operatingGuides()
899
- .filter(({ pluginName }) => {
900
- const visibility = manifestViz.get(pluginName) ?? 'on-demand';
901
- return (
902
- visibility !== 'silent' &&
903
- (visibility === 'always' || threadLoaded.has(pluginName)) &&
904
- !unmetRequirements.has(pluginName) &&
905
- !toolAccess.hiddenPlugins.has(pluginName)
906
- );
907
- })
908
- .map(({ guide }) => guide);
909
-
910
985
  const composedPrompt = await composePrompt({
911
986
  identity,
912
987
  capabilityBlock: tier1.block,
913
988
  browserTools,
914
989
  customInstructions,
915
- operationalMode: hooks?.operationalMode ?? DEFAULT_OPERATIONAL_MODE,
990
+ operationalMode: hooks?.operationalMode ?? '',
916
991
  userPreferencesContext: formatUserPreferences(state.userPreferences),
917
992
  userContext: state.userContext,
918
993
  // The day only: the prompt must stay byte-identical across the turns
@@ -933,7 +1008,7 @@ export async function createMainAgent(
933
1008
  tool.name === SEND_FILE_TOOL && !unmetRequirements.has(pluginName),
934
1009
  ),
935
1010
  ),
936
- operatingGuides,
1011
+ operatingGuides: capabilityPlan.inPrompt.map(({ guide }) => guide),
937
1012
  });
938
1013
  // The domain block goes last: it frames its documents as retrieved data
939
1014
  // that never outranks the instructions above it.
@@ -977,6 +1052,34 @@ export async function createMainAgent(
977
1052
  toolSurfaces,
978
1053
  subAgentToolNames: new Set(subAgentTools.map((t) => t.name)),
979
1054
  context: runConfig.context,
1055
+ stateUpdate: {
1056
+ // Every loaded plugin, so a write here also carries what the caller's
1057
+ // own write to the channel carries (the editor context).
1058
+ ...(expiredPlugins.length > 0
1059
+ ? { loadedPlugins: { replace: [...loadedSet] } }
1060
+ : preloaded.length > 0
1061
+ ? { loadedPlugins: [...loadedSet] }
1062
+ : {}),
1063
+ ...(capabilityPlan.promptGuides
1064
+ ? { promptGuides: capabilityPlan.promptGuides }
1065
+ : {}),
1066
+ // A channel's first value is stored as written, past its reducer.
1067
+ ...(capabilityPlan.capabilityUsage
1068
+ ? {
1069
+ capabilityUsage: args.promptCache?.capabilityUsage
1070
+ ? { replace: capabilityPlan.capabilityUsage }
1071
+ : capabilityPlan.capabilityUsage,
1072
+ }
1073
+ : {}),
1074
+ },
1075
+ ...(notedLoads.length > 0
1076
+ ? {
1077
+ guideNote: {
1078
+ plugins: notedLoads.map(({ name }) => name),
1079
+ text: renderGuideNote(notedLoads),
1080
+ },
1081
+ }
1082
+ : {}),
980
1083
  ...(domain
981
1084
  ? {
982
1085
  domainContext: {