@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.
Files changed (90) hide show
  1. package/package.json +5 -4
  2. package/src/action-interactions/routes.ts +78 -0
  3. package/src/action-interactions/store.ts +1038 -0
  4. package/src/channels/replies.ts +8 -4
  5. package/src/core/capability-expiry.ts +62 -0
  6. package/src/core/capability-router.ts +126 -95
  7. package/src/core/context-budget.ts +116 -4
  8. package/src/core/domain-context/document.ts +74 -2
  9. package/src/core/domain-context/index.ts +1 -0
  10. package/src/core/domain-context/resolver.ts +9 -6
  11. package/src/core/domain-context/turn-context.ts +62 -13
  12. package/src/core/domain-context/types.ts +17 -0
  13. package/src/core/env.ts +21 -2
  14. package/src/core/index.ts +4 -0
  15. package/src/core/llm.ts +29 -0
  16. package/src/core/main-agent-types.ts +45 -4
  17. package/src/core/main-agent.ts +210 -122
  18. package/src/core/manifest.ts +109 -24
  19. package/src/core/mcp-result.ts +56 -0
  20. package/src/core/meta-tools.ts +258 -110
  21. package/src/core/middlewares/capability-gate.ts +77 -28
  22. package/src/core/middlewares/context-guard.ts +154 -21
  23. package/src/core/middlewares/result-cap.ts +1 -1
  24. package/src/core/middlewares/summarization.ts +9 -0
  25. package/src/core/middlewares/tool-marks.ts +11 -2
  26. package/src/core/middlewares/tool-repetition-guard.ts +61 -1
  27. package/src/core/middlewares/turn-boundary.ts +22 -12
  28. package/src/core/operating-guides.ts +126 -0
  29. package/src/core/plugins/skills/skills-tools.ts +6 -31
  30. package/src/core/plugins/skills/skills.plugin.ts +4 -3
  31. package/src/core/plugins/weather/weather.plugin.ts +1 -0
  32. package/src/core/prompt-composer.ts +188 -111
  33. package/src/core/read-result-tool.ts +48 -14
  34. package/src/core/registries.ts +13 -5
  35. package/src/core/state.ts +4 -3
  36. package/src/core/sub-agent-fallback.ts +1 -0
  37. package/src/core/subagent-as-tool.ts +4 -1
  38. package/src/core/tool-access.ts +3 -32
  39. package/src/core/wrap-plugin-tool.ts +2 -1
  40. package/src/do/contracts.ts +45 -1
  41. package/src/do/transcript.ts +40 -11
  42. package/src/do/turn-metadata.ts +16 -4
  43. package/src/do/user-oracle-do.ts +358 -62
  44. package/src/index.ts +1 -0
  45. package/src/memory/session-history-indexer.ts +60 -14
  46. package/src/plugin-api/oracle-plugin.ts +4 -5
  47. package/src/plugin-api/types.ts +32 -3
  48. package/src/plugins/action-proposals/action-proposals.plugin.ts +7 -2
  49. package/src/plugins/agui/agui.plugin.ts +1 -0
  50. package/src/plugins/compact-schema.ts +80 -0
  51. package/src/plugins/composio/composio.plugin.ts +1 -0
  52. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +4 -3
  53. package/src/plugins/editor/editor.plugin.ts +4 -0
  54. package/src/plugins/firecrawl/firecrawl-agent.ts +2 -0
  55. package/src/plugins/firecrawl/firecrawl.plugin.ts +1 -0
  56. package/src/plugins/flows/flows.plugin.ts +37 -6
  57. package/src/plugins/flows/prompts.ts +1 -1
  58. package/src/plugins/flows/tools/authoring.ts +5 -7
  59. package/src/plugins/flows/tools/discovery.ts +1 -4
  60. package/src/plugins/flows/tools/forms.ts +2 -8
  61. package/src/plugins/flows/tools/linkage.ts +1 -4
  62. package/src/plugins/flows/tools/settings.ts +1 -4
  63. package/src/plugins/flows/translator.ts +16 -5
  64. package/src/plugins/flows/types.ts +27 -48
  65. package/src/plugins/hub-actions/hub-actions.plugin.ts +30 -4
  66. package/src/plugins/index.ts +2 -2
  67. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +29 -5
  68. package/src/plugins/ixo-chain/kinds/core.ts +2 -2
  69. package/src/plugins/ixo-chain/kinds/types.ts +34 -5
  70. package/src/plugins/ixo-chain/queries.ts +10 -1
  71. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +1 -0
  72. package/src/plugins/matrix-group-chats/index.ts +1 -0
  73. package/src/plugins/memory/memory-tools.ts +4 -4
  74. package/src/plugins/memory/memory.plugin.ts +27 -1
  75. package/src/plugins/memory/oracle-document-tools.ts +24 -49
  76. package/src/plugins/pod-creator/pod-creator.plugin.ts +1 -0
  77. package/src/plugins/portal/portal.plugin.ts +3 -1
  78. package/src/plugins/sandbox/sandbox-write-blob.ts +3 -15
  79. package/src/plugins/sandbox/sandbox.plugin.ts +19 -2
  80. package/src/plugins/tasks/manifest.ts +1 -0
  81. package/src/plugins/tasks/tasks-tools.ts +34 -39
  82. package/src/plugins/user-preferences/user-preferences-tool.ts +11 -37
  83. package/src/plugins/user-preferences/user-preferences.plugin.ts +4 -0
  84. package/src/plugins/vfs/vfs-tools.ts +6 -6
  85. package/src/plugins/vfs/vfs.plugin.ts +4 -5
  86. package/src/plugins/yoma/yoma.plugin.ts +1 -0
  87. package/src/shell/app.ts +241 -6
  88. package/src/sqlite/serialization.ts +12 -0
  89. package/src/sqlite/sqlite-saver.ts +17 -0
  90. package/src/tasks/schedule-input.ts +5 -22
@@ -17,14 +17,26 @@ 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 {
@@ -63,7 +75,6 @@ import {
63
75
  dropShadowingRequestEntries,
64
76
  formatByPlugin,
65
77
  turnToolSummaries,
66
- type ManifestRegistry,
67
78
  } from './registries';
68
79
  import {
69
80
  buildPluginContext,
@@ -90,12 +101,6 @@ import { ixoAddressOf, renderUserIdentity } from './user-identity';
90
101
 
91
102
  const PLUGIN_LOGGER_COMPONENT = 'main-agent';
92
103
 
93
- const DEFAULT_OPERATIONAL_MODE = [
94
- '**General conversation mode**',
95
- '',
96
- 'Default to conversation mode, using available capabilities for recall, search, and task delegation.',
97
- ].join('\n');
98
-
99
104
  type CollectedTool = { pluginName: string; tool: PluginTool };
100
105
 
101
106
  /** The validated env values the user-identity section names. */
@@ -105,31 +110,16 @@ const identityConfigSchema = z.object({
105
110
  ORACLE_ENTITY_DID: z.string().min(1).optional(),
106
111
  });
107
112
 
108
- /** Map plugin name → effective `manifest.visibility` (default `on-demand`). */
109
- function visibilityIndex(
110
- manifests: ManifestRegistry,
111
- ): Map<string, NonNullable<PluginManifest['visibility']>> {
112
- const out = new Map<string, NonNullable<PluginManifest['visibility']>>();
113
- for (const { pluginName, manifest } of manifests.collect()) {
114
- out.set(pluginName, manifest.visibility ?? 'on-demand');
115
- }
116
- return out;
117
- }
118
-
119
- /**
120
- * Filter collected tools by their effective visibility — per-tool override
121
- * wins; otherwise the plugin's manifest visibility decides.
122
- */
113
+ /** Filter collected tools by their effective visibility. */
123
114
  function selectByVisibility(
124
115
  tools: CollectedTool[],
125
- manifestViz: Map<string, NonNullable<PluginManifest['visibility']>>,
116
+ manifestOf: Map<string, PluginManifest>,
126
117
  visibility: NonNullable<PluginManifest['visibility']>,
127
118
  ): CollectedTool[] {
128
- return tools.filter(({ pluginName, tool }) => {
129
- const effective =
130
- tool.visibility ?? manifestViz.get(pluginName) ?? 'on-demand';
131
- return effective === visibility;
132
- });
119
+ return tools.filter(
120
+ ({ pluginName, tool }) =>
121
+ effectiveToolVisibility(tool, manifestOf.get(pluginName)) === visibility,
122
+ );
133
123
  }
134
124
 
135
125
  /**
@@ -173,7 +163,7 @@ export async function createMainAgent(
173
163
  ambient,
174
164
  state,
175
165
  availablePlugins,
176
- preloadedPlugins,
166
+ routeCapabilities,
177
167
  checkpointer,
178
168
  abortSignal,
179
169
  byoProvider,
@@ -249,6 +239,7 @@ export async function createMainAgent(
249
239
  toolSurfaces: new Map(),
250
240
  subAgentToolNames: new Set(),
251
241
  context: { user: requestCtx.user, session: requestCtx.session },
242
+ stateUpdate: {},
252
243
  };
253
244
  }
254
245
 
@@ -267,14 +258,12 @@ export async function createMainAgent(
267
258
  state.loadedPlugins,
268
259
  );
269
260
 
270
- // What the turn treats as loaded: the thread's checkpointed plugins plus the
271
- // router's one-turn preload. Only the RuntimeContext and the gate see this
272
- // union; the graph state keeps the checkpointed list alone.
273
- const threadLoaded: ReadonlySet<string> = new Set(state.loadedPlugins ?? []);
274
- const loadedSet = new Set<string>([
275
- ...threadLoaded,
276
- ...(preloadedPlugins ?? []),
277
- ]);
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);
278
267
  // Carry the prior request state (browserTools, agActions, …) into the
279
268
  // per-request RuntimeContext and the tool-wrapper closures, but NOT the
280
269
  // message history: keeping the full `messages` array here would pin the
@@ -411,22 +400,6 @@ export async function createMainAgent(
411
400
  const missing = unmetManifestRequirements(manifest, hasCapability);
412
401
  if (missing.length > 0) unmetRequirements.set(pluginName, missing);
413
402
  }
414
- // The router predicts before the tools are collected (see
415
- // `routableCandidates`): its preload of a plugin left with nothing, or of
416
- // one whose requirements the delegation does not meet, is void, for the
417
- // gate and for what handlers see as loaded.
418
- const turnPreloads = preloadedPlugins
419
- ? new Set(
420
- [...preloadedPlugins].filter(
421
- (pluginName) =>
422
- !toolAccess.hiddenPlugins.has(pluginName) &&
423
- !unmetRequirements.has(pluginName),
424
- ),
425
- )
426
- : undefined;
427
- for (const pluginName of preloadedPlugins ?? [])
428
- if (!turnPreloads?.has(pluginName) && !threadLoaded.has(pluginName))
429
- loadedSet.delete(pluginName);
430
403
 
431
404
  // The per-turn tool-surface line. Request tools are named in full (there
432
405
  // are only a handful and they are the ones that vary turn to turn); the
@@ -440,7 +413,9 @@ export async function createMainAgent(
440
413
  ambient.logger.debug?.(
441
414
  `[main-agent] full tool surface: ${formatByPlugin(allTools) || '∅'}`,
442
415
  );
443
- const manifestViz = visibilityIndex(registries.manifests);
416
+ const manifestOf = new Map(
417
+ manifestEntries.map(({ pluginName, manifest }) => [pluginName, manifest]),
418
+ );
444
419
  if (unmetRequirements.size > 0)
445
420
  ambient.logger.log(
446
421
  `[main-agent] not usable by ${requestCtx.user.did} (authorization lacks what they require): ${[...unmetRequirements.keys()].join(', ')}`,
@@ -452,27 +427,152 @@ export async function createMainAgent(
452
427
  ]),
453
428
  );
454
429
 
455
- 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');
456
562
  // Bind ALL on-demand tools at compile time — gating happens per model call
457
563
  // in `CapabilityGateMiddleware` based on the live `loadedPlugins` state.
458
564
  // This lets `load_capability` take effect on the very next LLM call inside
459
565
  // the same run, instead of waiting for the next request to rebuild.
460
- const onDemandTools = selectByVisibility(allTools, manifestViz, 'on-demand');
461
- const silentTools = selectByVisibility(allTools, manifestViz, 'silent');
566
+ const onDemandTools = selectByVisibility(allTools, manifestOf, 'on-demand');
567
+ const silentTools = selectByVisibility(allTools, manifestOf, 'silent');
462
568
 
463
569
  // ── 4. Wrap tools (meta + plugin) so handlers receive a RuntimeContext ──
464
570
  const metaTools = [
465
571
  ...buildMetaTools({
466
572
  manifestRegistry: registries.manifests,
467
- // This turn's own collection (the shared registry holds no request
468
- // tools), already cut to what the delegation reaches.
469
- toolRegistry: turnToolSummaries(toolAccess.tools),
470
- toolAccess: {
471
- ...toolAccess,
472
- preloadedOnly: new Set(
473
- [...(turnPreloads ?? [])].filter((name) => !threadLoaded.has(name)),
474
- ),
475
- },
573
+ toolRegistry: turnSummaries,
574
+ toolAccess,
575
+ loader: capabilityLoader,
476
576
  }),
477
577
  // Pages through tool results the result cap saved whole (result-cap.ts);
478
578
  // its chunks stay well under the cap so a page is never capped itself.
@@ -480,12 +580,8 @@ export async function createMainAgent(
480
580
  ? [
481
581
  buildReadResultTool({
482
582
  read: hooks.readResult,
483
- maxChars: Math.min(
484
- 16_000,
485
- Math.max(
486
- 512,
487
- Math.floor((contextBudget?.resultCapChars ?? 32_000) / 2),
488
- ),
583
+ maxChars: readResultMaxChars(
584
+ contextBudget?.resultCapChars ?? 32_000,
489
585
  ),
490
586
  }),
491
587
  ]
@@ -648,29 +744,6 @@ export async function createMainAgent(
648
744
  ...domainTools,
649
745
  ];
650
746
 
651
- // Lookups used by `CapabilityGateMiddleware` to gate on-demand plugins
652
- // and sub-agents per model call. Meta-tools omitted from the map are
653
- // pass-through.
654
- const pluginByToolName = new Map<string, string>();
655
- const visibilityByToolName = new Map<
656
- string,
657
- NonNullable<PluginManifest['visibility']>
658
- >();
659
- for (const { pluginName, tool } of allTools) {
660
- pluginByToolName.set(tool.name, pluginName);
661
- const effective =
662
- tool.visibility ?? manifestViz.get(pluginName) ?? 'on-demand';
663
- visibilityByToolName.set(tool.name, effective);
664
- }
665
- for (const { pluginName, subAgent } of subAgentEntries) {
666
- const toolName = computeSubAgentToolName(subAgent.name);
667
- pluginByToolName.set(toolName, pluginName);
668
- visibilityByToolName.set(
669
- toolName,
670
- manifestViz.get(pluginName) ?? 'on-demand',
671
- );
672
- }
673
-
674
747
  // ── 6. Middleware stack — always-on + plugin contributions ──────────────
675
748
  const pluginMiddlewares = [
676
749
  ...registries.middlewares.collect(buildCtx),
@@ -757,9 +830,9 @@ export async function createMainAgent(
757
830
  createCapabilityGateMiddleware({
758
831
  pluginByToolName,
759
832
  visibilityByToolName,
760
- preloadedPlugins: turnPreloads,
761
833
  unmetRequirements,
762
834
  withheldToolNames: toolAccess.withheldToolNames,
835
+ describeLoad: capabilityLoader.describeForGate,
763
836
  logger: ambient.logger,
764
837
  }),
765
838
  userContextStateMiddleware(),
@@ -842,6 +915,13 @@ export async function createMainAgent(
842
915
  ? { onOverflow: hooks.onContextOverflow }
843
916
  : {}),
844
917
  ...(hooks?.onContextEvent ? { onEvent: hooks.onContextEvent } : {}),
918
+ ...(hooks?.readResult
919
+ ? {
920
+ readResultChars: readResultMaxChars(
921
+ contextBudget.resultCapChars,
922
+ ),
923
+ }
924
+ : {}),
845
925
  logger: ambient.logger,
846
926
  }),
847
927
  ]
@@ -882,7 +962,7 @@ export async function createMainAgent(
882
962
  pluginName,
883
963
  manifest: withoutWithheldExamples(manifest, toolAccess.withheldToolNames),
884
964
  }));
885
- const tier1 = renderTier1({ manifests: eagerEntries });
965
+ const tier1 = renderTier1({ manifests: eagerEntries, catalogue });
886
966
  for (const warning of tier1.warnings) ambient.logger.warn(warning);
887
967
 
888
968
  const customInstructions = identity.prompt?.customInstructions?.trim() ?? '';
@@ -899,35 +979,15 @@ export async function createMainAgent(
899
979
  origin === 'request' && pluginName === PORTAL_CAPABILITY,
900
980
  )
901
981
  .map(({ tool }) => tool.name),
902
- bound:
903
- manifestViz.get(PORTAL_CAPABILITY) === 'always' ||
904
- loadedSet.has(PORTAL_CAPABILITY),
982
+ bound: manifestOf.get(PORTAL_CAPABILITY)?.visibility === 'always',
905
983
  };
906
984
 
907
- // Operating guides of the plugins in use when the turn starts — loaded on
908
- // the thread or `always` visible — and usable by this delegation. A router
909
- // preload lasts one turn: its guide would enter the prompt and leave it
910
- // again on the next turn, so it comes with `load_capability` instead, as
911
- // does the guide of a plugin loaded mid-turn (here from the next turn on).
912
- const operatingGuides = registries.manifests
913
- .operatingGuides()
914
- .filter(({ pluginName }) => {
915
- const visibility = manifestViz.get(pluginName) ?? 'on-demand';
916
- return (
917
- visibility !== 'silent' &&
918
- (visibility === 'always' || threadLoaded.has(pluginName)) &&
919
- !unmetRequirements.has(pluginName) &&
920
- !toolAccess.hiddenPlugins.has(pluginName)
921
- );
922
- })
923
- .map(({ guide }) => guide);
924
-
925
985
  const composedPrompt = await composePrompt({
926
986
  identity,
927
987
  capabilityBlock: tier1.block,
928
988
  browserTools,
929
989
  customInstructions,
930
- operationalMode: hooks?.operationalMode ?? DEFAULT_OPERATIONAL_MODE,
990
+ operationalMode: hooks?.operationalMode ?? '',
931
991
  userPreferencesContext: formatUserPreferences(state.userPreferences),
932
992
  userContext: state.userContext,
933
993
  // The day only: the prompt must stay byte-identical across the turns
@@ -948,7 +1008,7 @@ export async function createMainAgent(
948
1008
  tool.name === SEND_FILE_TOOL && !unmetRequirements.has(pluginName),
949
1009
  ),
950
1010
  ),
951
- operatingGuides,
1011
+ operatingGuides: capabilityPlan.inPrompt.map(({ guide }) => guide),
952
1012
  });
953
1013
  // The domain block goes last: it frames its documents as retrieved data
954
1014
  // that never outranks the instructions above it.
@@ -992,6 +1052,34 @@ export async function createMainAgent(
992
1052
  toolSurfaces,
993
1053
  subAgentToolNames: new Set(subAgentTools.map((t) => t.name)),
994
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
+ : {}),
995
1083
  ...(domain
996
1084
  ? {
997
1085
  domainContext: {