@ixo/oracle-runtime-workers 0.38.0 → 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 (86) hide show
  1. package/package.json +2 -2
  2. package/src/channels/replies.ts +8 -4
  3. package/src/core/capability-expiry.ts +62 -0
  4. package/src/core/capability-router.ts +126 -95
  5. package/src/core/context-budget.ts +116 -4
  6. package/src/core/domain-context/document.ts +74 -2
  7. package/src/core/domain-context/index.ts +1 -0
  8. package/src/core/domain-context/resolver.ts +9 -6
  9. package/src/core/domain-context/turn-context.ts +62 -13
  10. package/src/core/domain-context/types.ts +17 -0
  11. package/src/core/env.ts +21 -2
  12. package/src/core/index.ts +4 -0
  13. package/src/core/llm.ts +29 -0
  14. package/src/core/main-agent-types.ts +45 -4
  15. package/src/core/main-agent.ts +210 -122
  16. package/src/core/manifest.ts +109 -24
  17. package/src/core/mcp-result.ts +56 -0
  18. package/src/core/meta-tools.ts +258 -110
  19. package/src/core/middlewares/capability-gate.ts +77 -28
  20. package/src/core/middlewares/context-guard.ts +154 -21
  21. package/src/core/middlewares/result-cap.ts +1 -1
  22. package/src/core/middlewares/summarization.ts +9 -0
  23. package/src/core/middlewares/tool-marks.ts +11 -2
  24. package/src/core/middlewares/turn-boundary.ts +22 -12
  25. package/src/core/operating-guides.ts +126 -0
  26. package/src/core/plugins/skills/skills-tools.ts +6 -31
  27. package/src/core/plugins/skills/skills.plugin.ts +4 -3
  28. package/src/core/plugins/weather/weather.plugin.ts +1 -0
  29. package/src/core/prompt-composer.ts +188 -111
  30. package/src/core/read-result-tool.ts +48 -14
  31. package/src/core/registries.ts +13 -5
  32. package/src/core/state.ts +4 -3
  33. package/src/core/sub-agent-fallback.ts +1 -0
  34. package/src/core/subagent-as-tool.ts +4 -1
  35. package/src/core/tool-access.ts +3 -32
  36. package/src/core/wrap-plugin-tool.ts +2 -1
  37. package/src/do/contracts.ts +13 -1
  38. package/src/do/transcript.ts +40 -11
  39. package/src/do/turn-metadata.ts +16 -4
  40. package/src/do/user-oracle-do.ts +132 -59
  41. package/src/index.ts +1 -0
  42. package/src/memory/session-history-indexer.ts +60 -14
  43. package/src/plugin-api/oracle-plugin.ts +4 -5
  44. package/src/plugin-api/types.ts +32 -3
  45. package/src/plugins/action-proposals/action-proposals.plugin.ts +7 -2
  46. package/src/plugins/agui/agui.plugin.ts +1 -0
  47. package/src/plugins/compact-schema.ts +80 -0
  48. package/src/plugins/composio/composio.plugin.ts +1 -0
  49. package/src/plugins/domain-indexer/domain-indexer.plugin.ts +4 -3
  50. package/src/plugins/editor/editor.plugin.ts +4 -0
  51. package/src/plugins/firecrawl/firecrawl-agent.ts +2 -0
  52. package/src/plugins/firecrawl/firecrawl.plugin.ts +1 -0
  53. package/src/plugins/flows/flows.plugin.ts +37 -6
  54. package/src/plugins/flows/prompts.ts +1 -1
  55. package/src/plugins/flows/tools/authoring.ts +5 -7
  56. package/src/plugins/flows/tools/discovery.ts +1 -4
  57. package/src/plugins/flows/tools/forms.ts +2 -8
  58. package/src/plugins/flows/tools/linkage.ts +1 -4
  59. package/src/plugins/flows/tools/settings.ts +1 -4
  60. package/src/plugins/flows/translator.ts +16 -5
  61. package/src/plugins/flows/types.ts +27 -48
  62. package/src/plugins/hub-actions/hub-actions.plugin.ts +30 -4
  63. package/src/plugins/index.ts +2 -2
  64. package/src/plugins/ixo-chain/ixo-chain.plugin.ts +29 -5
  65. package/src/plugins/ixo-chain/kinds/core.ts +2 -2
  66. package/src/plugins/ixo-chain/kinds/types.ts +34 -5
  67. package/src/plugins/ixo-chain/queries.ts +10 -1
  68. package/src/plugins/ixo-transaction/ixo-transaction.plugin.ts +1 -0
  69. package/src/plugins/matrix-group-chats/index.ts +1 -0
  70. package/src/plugins/memory/memory-tools.ts +4 -4
  71. package/src/plugins/memory/memory.plugin.ts +27 -1
  72. package/src/plugins/memory/oracle-document-tools.ts +24 -49
  73. package/src/plugins/pod-creator/pod-creator.plugin.ts +1 -0
  74. package/src/plugins/portal/portal.plugin.ts +1 -0
  75. package/src/plugins/sandbox/sandbox-write-blob.ts +3 -15
  76. package/src/plugins/sandbox/sandbox.plugin.ts +19 -2
  77. package/src/plugins/tasks/manifest.ts +1 -0
  78. package/src/plugins/tasks/tasks-tools.ts +14 -19
  79. package/src/plugins/user-preferences/user-preferences-tool.ts +11 -37
  80. package/src/plugins/user-preferences/user-preferences.plugin.ts +4 -0
  81. package/src/plugins/vfs/vfs-tools.ts +6 -6
  82. package/src/plugins/vfs/vfs.plugin.ts +4 -5
  83. package/src/plugins/yoma/yoma.plugin.ts +1 -0
  84. package/src/sqlite/serialization.ts +12 -0
  85. package/src/sqlite/sqlite-saver.ts +17 -0
  86. package/src/tasks/schedule-input.ts +5 -22
@@ -6,13 +6,8 @@ import type {
6
6
  PluginTool,
7
7
  } from '../plugin-api/types';
8
8
  import type { CapabilityRequirement } from './manifest';
9
- import type {
10
- RegisteredSubAgent,
11
- SubAgentRegistry,
12
- ToolRegistry,
13
- } from './registries';
9
+ import type { RegisteredSubAgent } from './registries';
14
10
  import { computeSubAgentToolName } from './subagent-as-tool';
15
- import { NOOP_LOGGER } from './utils';
16
11
 
17
12
  /** `ctx.ucan.hasCapability` bound to the turn's delegation. */
18
13
  export type HasCapability = (resource: string, action: string) => boolean;
@@ -53,8 +48,8 @@ export interface TurnToolAccess<
53
48
  /**
54
49
  * Plugins that contributed tools or sub-agents of which none is left.
55
50
  * Treated like a plugin whose `requires` are unmet (left out of the
56
- * prompt, refused by `load_capability`, never preloaded) except that they
57
- * are hidden outright: `list_capabilities` does not list them.
51
+ * prompt, refused by `load_capability`, not offered to the router) except
52
+ * that they are hidden outright: `list_capabilities` does not list them.
58
53
  */
59
54
  hiddenPlugins: ReadonlySet<string>;
60
55
  }
@@ -145,30 +140,6 @@ export function resolveTurnToolAccess<
145
140
  return { tools, subAgents, withheldToolNames, hiddenPlugins };
146
141
  }
147
142
 
148
- /**
149
- * `hiddenPlugins` from the boot-time contributions alone, for the
150
- * capability router, which predicts before the turn's request-time tools are
151
- * collected. A plugin whose request-time tools would make it usable may be
152
- * left out here (the router does not offer it; `load_capability` still
153
- * can); one whose request-time tools are all withheld may be offered, and
154
- * the turn's build voids that preload (`createMainAgent`).
155
- */
156
- export async function bootHiddenPlugins(input: {
157
- registries: { tools: ToolRegistry; subAgents: SubAgentRegistry };
158
- buildCtx: PluginContext;
159
- has: HasCapability;
160
- }): Promise<ReadonlySet<string>> {
161
- const { registries, buildCtx, has } = input;
162
- return resolveTurnToolAccess({
163
- tools: await registries.tools.collectBoot(buildCtx),
164
- subAgents: registries.subAgents.collectBoot(buildCtx),
165
- buildCtx,
166
- has,
167
- // A failing sub-agent is reported by the turn's build, once.
168
- logger: NOOP_LOGGER,
169
- }).hiddenPlugins;
170
- }
171
-
172
143
  /**
173
144
  * The manifest as this turn may show it: `examples` naming a withheld tool
174
145
  * removed, so neither the Tier-1 prompt nor `load_capability` teaches the
@@ -1,5 +1,6 @@
1
1
  import { ToolMessage } from '@langchain/core/messages';
2
2
  import { Command } from '@langchain/langgraph';
3
+ import { withoutStructuredCopy } from './mcp-result';
3
4
  import { capToolResult, type ResultCapConfig } from './middlewares/result-cap';
4
5
  import { tool } from '@langchain/core/tools';
5
6
  import type { StructuredTool } from 'langchain';
@@ -125,7 +126,7 @@ export function wrapPluginTool(
125
126
  );
126
127
  if (pluginName !== undefined)
127
128
  requireToolPlane(ctx, pluginName, pluginTool);
128
- const output = await pluginTool.handler(args, ctx);
129
+ const output = withoutStructuredCopy(await pluginTool.handler(args, ctx));
129
130
  // A tool's own message (a result that says the tool did not run) is
130
131
  // returned whole, never capped into text.
131
132
  if (
@@ -271,6 +271,8 @@ export interface OracleWorkerEnv {
271
271
  LLM_STREAM_IDLE_TIMEOUT_MS?: string;
272
272
  LLM_BYO_STREAM_IDLE_TIMEOUT_MS?: string;
273
273
  LLM_STREAM_RETRIES?: string;
274
+ /** Most tokens one platform model call may generate, reasoning included (default 16384; 0 = the model's limit). */
275
+ LLM_MAX_OUTPUT_TOKENS?: string;
274
276
  /** LangGraph steps one turn may take before `GraphRecursionError` (default 600; Node hard-codes 200). */
275
277
  TURN_RECURSION_LIMIT?: string;
276
278
  /** Turn budget: tool attempts (default 120) and deadline in ms (600000). */
@@ -295,7 +297,12 @@ export interface OracleWorkerEnv {
295
297
  MODEL_CONTEXT_TOKENS?: string;
296
298
  MODEL_CONTEXT_OVERRIDES?: string;
297
299
  CONTEXT_SUMMARIZE_FRACTION?: string;
300
+ CONTEXT_SUMMARIZE_MAX_TOKENS?: string;
298
301
  CONTEXT_PRUNE_FRACTION?: string;
302
+ CONTEXT_PRUNE_MAX_TOKENS?: string;
303
+ CONTEXT_STALE_RESULT_TURNS?: string;
304
+ CONTEXT_CACHE_COLD_MS?: string;
305
+ CONTEXT_PLUGIN_IDLE_TURNS?: string;
299
306
  CONTEXT_RESULT_CAP_FRACTION?: string;
300
307
  CONTEXT_RESULT_CAP_MAX_CHARS?: string;
301
308
  CONTEXT_REQUEST_FRACTION?: string;
@@ -327,9 +334,14 @@ export interface OracleWorkerEnv {
327
334
  /**
328
335
  * Capability router (needs a `DECISION_PROVIDER`). `off` (default) never
329
336
  * evaluates; `shadow` evaluates and logs the verdict without preloading;
330
- * `on` preloads the predicted on-demand plugin's tools for the turn.
337
+ * `on` loads the predicted plugins on the thread before the first model
338
+ * call.
331
339
  */
332
340
  CAPABILITY_ROUTER?: CapabilityRouterMode;
341
+ /** The score a preload needs on a warm turn (default 0.8; above 1, never). */
342
+ CAPABILITY_ROUTER_WARM_THRESHOLD?: string;
343
+ /** The score a preload needs on a cold turn (default 0.7). */
344
+ CAPABILITY_ROUTER_COLD_THRESHOLD?: string;
333
345
  /** Applies exclusively to authenticated channel admission; Portal/Matrix turns never route. */
334
346
  CHANNEL_SEMANTIC_ROUTING?: 'off' | 'shadow' | 'on';
335
347
  CHANNEL_SEMANTIC_ROUTING_BINDINGS?: string;
@@ -140,6 +140,45 @@ export function visibleText(message: BaseMessage): string {
140
140
  return contentToText(visibleContent(message));
141
141
  }
142
142
 
143
+ /**
144
+ * Whether a stored message is one the user sees in the transcript. System and
145
+ * tool messages never are, nor a failed reply, nor anything the runtime wrote
146
+ * for the model about an attachment: the extracted text of a file (an AI
147
+ * message with an `attachment`) and `view_attachment` re-attaching a payload,
148
+ * nor a chat gateway's delivery notice.
149
+ */
150
+ export function visibleToUser(message: BaseMessage): boolean {
151
+ if (message.type === 'system' || message.type === 'tool') return false;
152
+ const kw = (message.additional_kwargs ?? {}) as Record<string, unknown>;
153
+ if (kw.isError) return false;
154
+ if (message.type === 'ai' && Boolean(kw.attachment)) return false;
155
+ // A chat gateway's delivery notice: the model's, never a user message.
156
+ return !isAttachmentViewMessage(message) && !isDeliveryNoticeMessage(message);
157
+ }
158
+
159
+ /**
160
+ * The latest turns as the user saw them, oldest first and each as
161
+ * `user: …` or `assistant: …` cut to `maxChars`, from messages listed newest
162
+ * first. Only what `visibleToUser` lets through, without summaries and rows
163
+ * with no visible text.
164
+ */
165
+ export function recentTurnTexts(
166
+ newestFirst: readonly BaseMessage[],
167
+ limit: number,
168
+ maxChars: number,
169
+ ): string[] {
170
+ const turns: string[] = [];
171
+ for (const message of newestFirst) {
172
+ if (turns.length === limit) break;
173
+ if (isSummarizationMessage(message) || !visibleToUser(message)) continue;
174
+ const text = visibleText(message).trim();
175
+ if (!text) continue;
176
+ const speaker = message.type === 'human' ? 'user' : 'assistant';
177
+ turns.push(`${speaker}: ${text.slice(0, maxChars)}`);
178
+ }
179
+ return turns.reverse();
180
+ }
181
+
143
182
  /**
144
183
  * The summarization middleware's bookkeeping message — never shown to users.
145
184
  * LangChain 1.4 writes it as a HUMAN message tagged
@@ -171,18 +210,8 @@ export async function transformTranscript(
171
210
  for (const message of messages) {
172
211
  const toolMsg = message.type === 'tool' ? (message as ToolMessage) : null;
173
212
  const kw = (message.additional_kwargs ?? {}) as Record<string, unknown>;
174
- const isExtractionContext = message.type === 'ai' && Boolean(kw.attachment);
175
213
 
176
- if (
177
- message.type !== 'system' &&
178
- message.type !== 'tool' &&
179
- !kw.isError &&
180
- !isExtractionContext &&
181
- // `view_attachment` re-attaching an offloaded payload — model-facing.
182
- !isAttachmentViewMessage(message) &&
183
- // A chat gateway's delivery notice — the model's, never a user message.
184
- !isDeliveryNoticeMessage(message)
185
- ) {
214
+ if (visibleToUser(message)) {
186
215
  const reasoning = reasoningToText(kw.reasoning);
187
216
  const attachments =
188
217
  message.type === 'human'
@@ -62,6 +62,16 @@ export interface PriorMetadataState {
62
62
  sessionRunId?: string;
63
63
  currentEntityDid?: string;
64
64
  loadedPlugins?: string[];
65
+ /** The plugins whose operating guides the prompt carried (`operating-guides.ts`). */
66
+ promptGuides?: string[];
67
+ /** The plugins whose usage the thread's messages carry (`operating-guides.ts`). */
68
+ capabilityUsage?: string[];
69
+ }
70
+
71
+ function strings(value: unknown): string[] | undefined {
72
+ return Array.isArray(value)
73
+ ? value.filter((entry): entry is string => typeof entry === 'string')
74
+ : undefined;
65
75
  }
66
76
 
67
77
  function priorString(
@@ -75,15 +85,17 @@ function priorString(
75
85
  export function priorMetadataState(
76
86
  prior: Record<string, unknown>,
77
87
  ): PriorMetadataState {
78
- const loaded = prior['loadedPlugins'];
88
+ const loaded = strings(prior['loadedPlugins']);
89
+ const promptGuides = strings(prior['promptGuides']);
90
+ const capabilityUsage = strings(prior['capabilityUsage']);
79
91
  return {
80
92
  editorRoomId: priorString(prior, 'editorRoomId'),
81
93
  spaceId: priorString(prior, 'spaceId'),
82
94
  sessionRunId: priorString(prior, 'sessionRunId'),
83
95
  currentEntityDid: priorString(prior, 'currentEntityDid'),
84
- ...(Array.isArray(loaded)
85
- ? { loadedPlugins: loaded.filter((p) => typeof p === 'string') }
86
- : {}),
96
+ ...(loaded ? { loadedPlugins: loaded } : {}),
97
+ ...(promptGuides ? { promptGuides } : {}),
98
+ ...(capabilityUsage ? { capabilityUsage } : {}),
87
99
  };
88
100
  }
89
101
 
@@ -186,9 +186,15 @@ import {
186
186
  DEFAULT_MODEL_ID,
187
187
  } from '../core/llm';
188
188
  import { renderTurnTimeNote } from '../core/prompt-composer';
189
+ import type { RegisteredManifest } from '../core/registries';
190
+ import {
191
+ TURN_GUIDE_NOTE_KWARG,
192
+ USAGE_CARRIED_KWARG,
193
+ } from '../core/operating-guides';
189
194
  import { TURN_TIME_NOTE_KWARG, withTurnTimeNote } from '../core/turn-time-note';
190
195
  import {
191
196
  capabilityRouterMode,
197
+ preloadThreshold,
192
198
  createCapabilityRouter,
193
199
  shadowAgreement,
194
200
  type CapabilityRouter,
@@ -203,7 +209,6 @@ import type {
203
209
  RequestAdmissionResult,
204
210
  RequestDisposition,
205
211
  } from '../plugin-api/request-admission';
206
- import { bootHiddenPlugins } from '../core/tool-access';
207
212
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
208
213
  import type { AmbientServices, LlmAdapter } from '../core/runtime-context';
209
214
  import { chatGptBackendFromEnv } from '../llm/byo-client';
@@ -220,6 +225,8 @@ import { parseJwk } from '../secrets/jwe';
220
225
  import { WorkersSecretsService } from '../secrets/secrets-service';
221
226
  import { createSecretsAdapter } from './secrets-adapter';
222
227
  import {
228
+ EDITOR_PLUGIN_NAME,
229
+ editorContextActive,
223
230
  metadataBuildState,
224
231
  metadataGraphInput,
225
232
  parseTurnMetadata,
@@ -348,6 +355,7 @@ import {
348
355
  pageOfMessages,
349
356
  SessionHistoryIndexer,
350
357
  type HistoryEntry,
358
+ conversationReader,
351
359
  type HistoryMessage,
352
360
  type HistoryPage,
353
361
  type IndexableSession,
@@ -455,6 +463,7 @@ import {
455
463
  contentToText,
456
464
  isSummarizationMessage,
457
465
  pageThreadTranscript,
466
+ recentTurnTexts,
458
467
  TranscriptCursorError,
459
468
  transformTranscript,
460
469
  type TranscriptPageOptions,
@@ -571,6 +580,15 @@ type UserContextSettle = (
571
580
  signal: AbortSignal,
572
581
  ) => Promise<UserContextData | undefined>;
573
582
 
583
+ /**
584
+ * What the capability router sees of the thread: enough to route a follow-up
585
+ * (two exchanges, each turn cut short), read from a bounded number of rows so
586
+ * a long thread costs nothing extra.
587
+ */
588
+ const ROUTER_RECENT_TURNS = 4;
589
+ const ROUTER_RECENT_TURN_CHARS = 300;
590
+ const ROUTER_RECENT_ROWS = 12;
591
+
574
592
  /** How long a first turn waits on the user-context prefetch before saying so. */
575
593
  const USER_CONTEXT_PROGRESS_GRACE_MS = 300;
576
594
  const USER_CONTEXT_PROGRESS_STEP = 'Recalling your memories…';
@@ -4787,7 +4805,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4787
4805
  !isAttachmentViewMessage(m) &&
4788
4806
  !isDeliveryNoticeMessage(m),
4789
4807
  )
4790
- .map((m) => ({ type: m.type, content: visibleText(m) }));
4808
+ .map(conversationReader());
4791
4809
  }
4792
4810
 
4793
4811
  /**
@@ -4810,6 +4828,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4810
4828
  }
4811
4829
  const pageRows = Math.max(1, limit);
4812
4830
  const entries: HistoryEntry[] = [];
4831
+ const read = conversationReader();
4813
4832
  for (;;) {
4814
4833
  const rows = await saver.listThreadMessageRows(sessionId, {
4815
4834
  direction: 'newer',
@@ -4826,7 +4845,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4826
4845
  )
4827
4846
  continue;
4828
4847
  entries.push({
4829
- message: { type: m.type, content: visibleText(m) },
4848
+ message: read(m),
4830
4849
  cursor: row.messageId,
4831
4850
  });
4832
4851
  }
@@ -5021,6 +5040,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5021
5040
  prior: unknown;
5022
5041
  client: TurnRequest['client'];
5023
5042
  roomKind: TurnRequest['roomKind'];
5043
+ /** The provider's cache has expired, so the prompt may change. */
5044
+ coldTurn: boolean;
5024
5045
  }): UserContextSettle | null {
5025
5046
  // A first contact that fetches nothing still prints the one
5026
5047
  // first-contact line (see the settle step below), with its reason, so
@@ -5075,9 +5096,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5075
5096
  // room answer — but only one already in the cache: an upload that
5076
5097
  // just invalidated the room must not make the next message wait for
5077
5098
  // a gather. On a miss the thread keeps its context and the
5078
- // (single-flight) refresh warms the cache for a later turn.
5099
+ // (single-flight) refresh warms the cache for a later turn. The
5100
+ // answer sits in the system prompt, so it waits for a cold turn: a
5101
+ // change there re-reads the whole request at the uncached price.
5079
5102
  const warm =
5080
- input.client === 'channel'
5103
+ input.client === 'channel' && input.coldTurn
5081
5104
  ? fetcher.readCached(input.roomId)
5082
5105
  : undefined;
5083
5106
  keepGathering();
@@ -7178,13 +7201,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7178
7201
  sessionId: string,
7179
7202
  ): Promise<SessionContextStatus | null> {
7180
7203
  if (!(await this.sessions!.getSession(sessionId))) return null;
7181
- const tuple = await this.saver!.getTuple({
7182
- configurable: { thread_id: sessionId },
7183
- });
7184
- const channel: unknown = tuple?.checkpoint.channel_values.messages;
7185
- const context: BaseMessage[] = Array.isArray(channel)
7186
- ? channel.filter(isBaseMessage)
7187
- : [];
7204
+ const context = await this.contextMessages(sessionId);
7188
7205
  const thread = await this.saver!.listThreadMessages(sessionId);
7189
7206
  const stats = await this.ctx.storage.get<SessionContextCounters>(
7190
7207
  contextStatsKey(sessionId),
@@ -7204,6 +7221,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7204
7221
  };
7205
7222
  }
7206
7223
 
7224
+ /** The messages the session's model calls start from (a summary, then the kept tail, once condensed). */
7225
+ private async contextMessages(sessionId: string): Promise<BaseMessage[]> {
7226
+ const tuple = await this.saver!.getTuple({
7227
+ configurable: { thread_id: sessionId },
7228
+ });
7229
+ const channel: unknown = tuple?.checkpoint.channel_values.messages;
7230
+ return Array.isArray(channel) ? channel.filter(isBaseMessage) : [];
7231
+ }
7232
+
7207
7233
  /** Fold a guard event into the session's stored counters (ordered, never throws). */
7208
7234
  private recordContextEvent(
7209
7235
  sessionId: string,
@@ -7222,7 +7248,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7222
7248
  switch (event.kind) {
7223
7249
  case 'prune':
7224
7250
  next.prunes += 1;
7225
- if (event.stage !== 'soft') next.hardPrunes += 1;
7251
+ if (event.stage === 'hard' || event.stage === 'overflow')
7252
+ next.hardPrunes += 1;
7226
7253
  next.prunedResults += event.pruned;
7227
7254
  break;
7228
7255
  case 'overflow-retry':
@@ -7721,6 +7748,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7721
7748
  configurable: { thread_id: req.sessionId },
7722
7749
  });
7723
7750
  const priorState = existing?.checkpoint.channel_values ?? {};
7751
+ // A new thread, or quiet for longer than the provider's cache lives:
7752
+ // this turn pays a miss whatever it sends, so it is the one to change
7753
+ // the prefix on.
7754
+ const coldTurn =
7755
+ !existing ||
7756
+ turnAt.getTime() - Date.parse(existing.checkpoint.ts) >=
7757
+ contextBudget.cacheColdMs;
7724
7758
  // What the memory engine knows about the user, for the prompt's
7725
7759
  // context block: started now so the round-trip overlaps the rest of
7726
7760
  // the build, settled right before the agent is built.
@@ -7734,6 +7768,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7734
7768
  prior: priorState.userContext,
7735
7769
  client: req.client,
7736
7770
  roomKind: req.roomKind,
7771
+ coldTurn,
7737
7772
  });
7738
7773
 
7739
7774
  // A Matrix turn with no usable delegation: ask the web app to open its
@@ -7862,11 +7897,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7862
7897
  suppliedContextOnly ? {} : priorState,
7863
7898
  );
7864
7899
 
7865
- // Capability router: predict the on-demand plugin this message needs
7866
- // and preload it for THIS turn only. `on` is awaited here, ahead of the
7867
- // first model call; `shadow` returns at once and logs its verdict when
7868
- // it lands. The preload never enters `stateInput` — the checkpointed
7869
- // `loadedPlugins` channel is written by `load_capability` alone.
7870
7900
  const priorLoaded: ReadonlySet<string> = new Set(
7871
7901
  priorMeta.loadedPlugins ?? [],
7872
7902
  );
@@ -7885,45 +7915,49 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7885
7915
  client: req.client,
7886
7916
  env: langsmithEnvFromWorkerEnv(this.env),
7887
7917
  });
7888
- const preloadedPlugins =
7889
- !suppliedContextOnly && this.capabilityRouter
7890
- ? await this.capabilityRouter({
7891
- mode: capabilityRouterMode(core.validatedEnv.CAPABILITY_ROUTER),
7892
- manifests: core.registries.manifests.collect(),
7893
- loaded: priorLoaded,
7894
- hidden: () =>
7895
- bootHiddenPlugins({
7896
- registries: core.registries,
7897
- buildCtx: core.buildCtx(),
7898
- has: (resource, action) =>
7899
- ambient.ucan.hasCapability(
7900
- ucanDelegation,
7901
- resource,
7902
- action,
7918
+ // Capability router: from what is left to load this turn (the build
7919
+ // works that out from the turn's tools), the plugins this message needs.
7920
+ // `on` is awaited in the build, ahead of the first model call, and the
7921
+ // plugins load on the thread like any other; `shadow` returns at once
7922
+ // and logs its verdict when it lands. A resumed attempt keeps what the
7923
+ // run started with.
7924
+ const router = this.capabilityRouter;
7925
+ const routeCapabilities =
7926
+ !suppliedContextOnly && !run.resumed && router
7927
+ ? (candidates: readonly RegisteredManifest[]) =>
7928
+ router({
7929
+ mode: capabilityRouterMode(core.validatedEnv.CAPABILITY_ROUTER),
7930
+ candidates,
7931
+ ...preloadThreshold(coldTurn, core.validatedEnv),
7932
+ text: body.message,
7933
+ recentTurns: async () =>
7934
+ recentTurnTexts(
7935
+ await saver.listRecentThreadMessages(
7936
+ req.sessionId,
7937
+ ROUTER_RECENT_ROWS,
7903
7938
  ),
7904
- }),
7905
- hasCapability: (resource, action) =>
7906
- ambient.ucan.hasCapability(ucanDelegation, resource, action),
7907
- text: body.message,
7908
- requestId: req.requestId,
7909
- signal: abortController.signal,
7910
- trace: {
7911
- ...(tracing.callbacks && { callbacks: tracing.callbacks }),
7912
- // `thread_id` matches the graph run's, so LangSmith's thread
7913
- // view groups the router span with the turn.
7914
- metadata: {
7915
- ...tracing.metadata,
7916
- thread_id: req.sessionId,
7917
- request_id: req.requestId,
7939
+ ROUTER_RECENT_TURNS,
7940
+ ROUTER_RECENT_TURN_CHARS,
7941
+ ),
7942
+ requestId: req.requestId,
7943
+ signal: abortController.signal,
7944
+ trace: {
7945
+ ...(tracing.callbacks && { callbacks: tracing.callbacks }),
7946
+ // `thread_id` matches the graph run's, so LangSmith's thread
7947
+ // view groups the router span with the turn.
7948
+ metadata: {
7949
+ ...tracing.metadata,
7950
+ thread_id: req.sessionId,
7951
+ request_id: req.requestId,
7952
+ },
7918
7953
  },
7919
- },
7920
- onShadowVerdict: (wouldPreload) =>
7921
- this.shadowRoutes.set(req.sessionId, {
7922
- requestId: req.requestId,
7923
- priorLoaded,
7924
- wouldPreload,
7925
- }),
7926
- })
7954
+ onShadowVerdict: (wouldPreload) =>
7955
+ this.shadowRoutes.set(req.sessionId, {
7956
+ requestId: req.requestId,
7957
+ priorLoaded,
7958
+ wouldPreload,
7959
+ }),
7960
+ })
7927
7961
  : undefined;
7928
7962
 
7929
7963
  // Host page-context / safety-guardrail hooks, resolved against this
@@ -8094,13 +8128,44 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8094
8128
  )
8095
8129
  : undefined;
8096
8130
 
8131
+ // On a cold turn the thread's history shows which capabilities it has
8132
+ // moved on from. The messages are read only then, and only for a
8133
+ // thread with something to unload; the editor stays while its context
8134
+ // is active, and a resumed attempt keeps what the run started with.
8135
+ const unloadable = (priorMeta.loadedPlugins ?? []).filter(
8136
+ (plugin) =>
8137
+ plugin !== EDITOR_PLUGIN_NAME ||
8138
+ !editorContextActive(meta, priorMeta),
8139
+ );
8140
+ const capabilityExpiry =
8141
+ coldTurn &&
8142
+ !run.resumed &&
8143
+ contextBudget.pluginIdleTurns > 0 &&
8144
+ unloadable.length > 0
8145
+ ? {
8146
+ loaded: unloadable,
8147
+ maxIdleTurns: contextBudget.pluginIdleTurns,
8148
+ messages: await this.contextMessages(req.sessionId),
8149
+ }
8150
+ : undefined;
8151
+
8097
8152
  const built = await createMainAgent({
8098
8153
  executionProfile: req.executionProfile,
8154
+ ...(capabilityExpiry ? { capabilityExpiry } : {}),
8099
8155
  registries: core.registries,
8100
8156
  identity: core.identity,
8101
8157
  config: core.validatedEnv,
8102
8158
  availablePlugins: core.availablePlugins,
8103
- preloadedPlugins,
8159
+ ...(routeCapabilities ? { routeCapabilities } : {}),
8160
+ promptCache: {
8161
+ cold: coldTurn && !run.resumed,
8162
+ ...(priorMeta.promptGuides
8163
+ ? { promptGuides: priorMeta.promptGuides }
8164
+ : {}),
8165
+ ...(priorMeta.capabilityUsage
8166
+ ? { capabilityUsage: priorMeta.capabilityUsage }
8167
+ : {}),
8168
+ },
8104
8169
  byoProvider: byoTurn?.provider,
8105
8170
  contextBudget,
8106
8171
  turnBudget: budget,
@@ -8224,7 +8289,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8224
8289
  checkpointer: saver,
8225
8290
  abortSignal: abortController.signal,
8226
8291
  });
8227
- const { agent, context } = built;
8292
+ const { agent, context, guideNote } = built;
8228
8293
  for (const [name, effect] of built.toolEffects)
8229
8294
  toolEffects.set(name, effect);
8230
8295
  for (const [name, surface] of built.toolSurfaces)
@@ -8277,9 +8342,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8277
8342
  const referenced = replyNote
8278
8343
  ? withTurnTimeNote(noticed, replyNote)
8279
8344
  : noticed;
8280
- const content = timeNote
8281
- ? withTurnTimeNote(referenced, timeNote)
8345
+ const guided = guideNote
8346
+ ? withTurnTimeNote(referenced, guideNote.text)
8282
8347
  : referenced;
8348
+ const content = timeNote ? withTurnTimeNote(guided, timeNote) : guided;
8283
8349
  const human = new HumanMessage({
8284
8350
  content,
8285
8351
  additional_kwargs: {
@@ -8289,6 +8355,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8289
8355
  ...(noticeNote
8290
8356
  ? { [CHANNEL_DELIVERY_NOTICE_KWARG]: noticeNote }
8291
8357
  : {}),
8358
+ ...(guideNote
8359
+ ? {
8360
+ [TURN_GUIDE_NOTE_KWARG]: guideNote.text,
8361
+ [USAGE_CARRIED_KWARG]: guideNote.plugins,
8362
+ }
8363
+ : {}),
8292
8364
  oracleName: core.identity.name,
8293
8365
  msgFromMatrixRoom: req.client === 'matrix',
8294
8366
  ...speakerKwargs,
@@ -8319,6 +8391,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8319
8391
  // every slot present; later turns write nothing and keep it.
8320
8392
  ...(userContext ? { userContext } : {}),
8321
8393
  ...metadataGraphInput(meta, priorMeta),
8394
+ ...built.stateUpdate,
8322
8395
  ...surface.input,
8323
8396
  };
8324
8397
  const config = {
package/src/index.ts CHANGED
@@ -60,6 +60,7 @@ export {
60
60
  } from './do/user-oracle-do';
61
61
  export type {
62
62
  DocumentRequest,
63
+ DomainBriefDetail,
63
64
  DomainCapsuleInspection,
64
65
  DomainContextOptions,
65
66
  DomainContextPins,