@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
@@ -1,3 +1,14 @@
1
+ import { taskCardSummary } from '../plugins/tasks/tasks-tools';
2
+ import {
3
+ ActionInteractionError,
4
+ type CheckClaimDispatch,
5
+ type CheckDispatch,
6
+ } from '@ixo/action-interactions';
7
+ import {
8
+ ActionInteractionStore,
9
+ interactionOutcome,
10
+ type InteractionCommand,
11
+ } from '../action-interactions/store';
1
12
  import {
2
13
  CHANNEL_DELIVERY_NOTICE_KWARG,
3
14
  channelDeliveryNote,
@@ -186,9 +197,15 @@ import {
186
197
  DEFAULT_MODEL_ID,
187
198
  } from '../core/llm';
188
199
  import { renderTurnTimeNote } from '../core/prompt-composer';
200
+ import type { RegisteredManifest } from '../core/registries';
201
+ import {
202
+ TURN_GUIDE_NOTE_KWARG,
203
+ USAGE_CARRIED_KWARG,
204
+ } from '../core/operating-guides';
189
205
  import { TURN_TIME_NOTE_KWARG, withTurnTimeNote } from '../core/turn-time-note';
190
206
  import {
191
207
  capabilityRouterMode,
208
+ preloadThreshold,
192
209
  createCapabilityRouter,
193
210
  shadowAgreement,
194
211
  type CapabilityRouter,
@@ -203,7 +220,6 @@ import type {
203
220
  RequestAdmissionResult,
204
221
  RequestDisposition,
205
222
  } from '../plugin-api/request-admission';
206
- import { bootHiddenPlugins } from '../core/tool-access';
207
223
  import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
208
224
  import type { AmbientServices, LlmAdapter } from '../core/runtime-context';
209
225
  import { chatGptBackendFromEnv } from '../llm/byo-client';
@@ -220,6 +236,8 @@ import { parseJwk } from '../secrets/jwe';
220
236
  import { WorkersSecretsService } from '../secrets/secrets-service';
221
237
  import { createSecretsAdapter } from './secrets-adapter';
222
238
  import {
239
+ EDITOR_PLUGIN_NAME,
240
+ editorContextActive,
223
241
  metadataBuildState,
224
242
  metadataGraphInput,
225
243
  parseTurnMetadata,
@@ -348,6 +366,7 @@ import {
348
366
  pageOfMessages,
349
367
  SessionHistoryIndexer,
350
368
  type HistoryEntry,
369
+ conversationReader,
351
370
  type HistoryMessage,
352
371
  type HistoryPage,
353
372
  type IndexableSession,
@@ -455,6 +474,7 @@ import {
455
474
  contentToText,
456
475
  isSummarizationMessage,
457
476
  pageThreadTranscript,
477
+ recentTurnTexts,
458
478
  TranscriptCursorError,
459
479
  transformTranscript,
460
480
  type TranscriptPageOptions,
@@ -571,6 +591,15 @@ type UserContextSettle = (
571
591
  signal: AbortSignal,
572
592
  ) => Promise<UserContextData | undefined>;
573
593
 
594
+ /**
595
+ * What the capability router sees of the thread: enough to route a follow-up
596
+ * (two exchanges, each turn cut short), read from a bounded number of rows so
597
+ * a long thread costs nothing extra.
598
+ */
599
+ const ROUTER_RECENT_TURNS = 4;
600
+ const ROUTER_RECENT_TURN_CHARS = 300;
601
+ const ROUTER_RECENT_ROWS = 12;
602
+
574
603
  /** How long a first turn waits on the user-context prefetch before saying so. */
575
604
  const USER_CONTEXT_PROGRESS_GRACE_MS = 300;
576
605
  const USER_CONTEXT_PROGRESS_STEP = 'Recalling your memories…';
@@ -717,6 +746,11 @@ const META_LEGACY_CLEARED = 'meta:legacyCleared';
717
746
  */
718
747
  const META_LEGACY_UNUSABLE = 'meta:legacyUnusable';
719
748
  const META_USER_DID = 'meta:userDid';
749
+ /**
750
+ * Set when an evicted object keeps an alarm only for action-card draft
751
+ * retention; a boot clears it. The alarm then prunes on the bare object.
752
+ */
753
+ const META_RETENTION_WAKE = 'meta:interactionRetentionWake';
720
754
 
721
755
  /** KV key of a session's context-guard counters (`GET /debug/context?session=`). */
722
756
  const contextStatsKey = (sessionId: string): string => `ctxstats:${sessionId}`;
@@ -1672,6 +1706,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1672
1706
  throw new Error(`Object bound to ${stored}, not ${userDid}`);
1673
1707
  if (!stored) await this.ctx.storage.put(META_USER_DID, userDid);
1674
1708
  this.userDid = userDid;
1709
+ await this.ctx.storage.delete(META_RETENTION_WAKE);
1675
1710
 
1676
1711
  const env = this.env;
1677
1712
  // The gateway lazily starts on first use; make sure it is up before the
@@ -2751,6 +2786,21 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2751
2786
  // exists once the object booted).
2752
2787
  const keepAliveAt = this.runs?.keepAliveDeadline(wakeAt) ?? null;
2753
2788
  const recoveryAt = this.runs?.nextRecoveryAt() ?? null;
2789
+ // An evicted object (no working copy, no housekeeping deadline) with
2790
+ // no socket is only woken for action-card draft retention: prune on
2791
+ // the bare object instead of importing the owner copy, and re-arm.
2792
+ if (
2793
+ !this.db &&
2794
+ nextPingAt === null &&
2795
+ keepAliveAt === null &&
2796
+ recoveryAt === null &&
2797
+ (await this.ctx.storage.get<boolean>(META_RETENTION_WAKE)) === true
2798
+ ) {
2799
+ const next = await this.bareInteractionRetention();
2800
+ if (next !== null) await this.requestAlarm(next);
2801
+ else await this.ctx.storage.delete(META_RETENTION_WAKE);
2802
+ return;
2803
+ }
2754
2804
  if (this.runs && recoveryAt !== null && recoveryAt <= wakeAt + 1000) {
2755
2805
  // A store error here must not reject the alarm: the tick below (or
2756
2806
  // the re-arm) still has to run.
@@ -2966,6 +3016,22 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2966
3016
  }
2967
3017
  }
2968
3018
 
3019
+ // Action-card drafts past retention: one bounded batch per tick, so a
3020
+ // user who stopped preparing cards still loses expired draft values.
3021
+ // Its follow-up is no reason to keep an idle copy either.
3022
+ if (this.userDid) {
3023
+ try {
3024
+ await new ActionInteractionStore(this.ctx.storage, {
3025
+ oracleDid: this.env.ORACLE_DID,
3026
+ userDid: this.userDid,
3027
+ }).prune();
3028
+ } catch (err) {
3029
+ console.warn(
3030
+ `[action-interactions] retention tick failed for ${this.userDid}: ${err instanceof Error ? err.message : String(err)}`,
3031
+ );
3032
+ }
3033
+ }
3034
+
2969
3035
  // Idle housekeeping: a user silent for five days loses the cached
2970
3036
  // pages — their file is safe upstream, and we stop paying to store a
2971
3037
  // copy. Never evict while tasks are scheduled: their runs ARE
@@ -2997,12 +3063,22 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2997
3063
  });
2998
3064
  if (outcome === 'evicted') {
2999
3065
  await this.ctx.storage.delete(META_HOUSEKEEPING_AT);
3066
+ // Action-card drafts outlive the working copy: their retention
3067
+ // still needs a wake (handled on the bare object, see alarm()).
3068
+ const retentionAt = await this.interactionRetentionAt();
3000
3069
  // An attached (idle) socket still needs its heartbeat rounds.
3001
- if (nextPingAt !== null) await this.requestAlarm(nextPingAt);
3070
+ if (nextPingAt !== null)
3071
+ await this.requestAlarm(
3072
+ Math.min(nextPingAt, retentionAt ?? Infinity),
3073
+ );
3002
3074
  // A request that waited on the wipe has booted the object afresh
3003
3075
  // and armed its own deadlines (a task Start's run): keep them.
3004
- else if (this.initPromise === null)
3005
- await this.ctx.storage.deleteAlarm();
3076
+ else if (this.initPromise === null) {
3077
+ if (retentionAt !== null) {
3078
+ await this.ctx.storage.put(META_RETENTION_WAKE, true);
3079
+ await this.requestAlarm(retentionAt);
3080
+ } else await this.ctx.storage.deleteAlarm();
3081
+ }
3006
3082
  return;
3007
3083
  }
3008
3084
  if (outcome === 'not-current') {
@@ -3178,6 +3254,162 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3178
3254
 
3179
3255
  // ── RPC surface (contracts.ts UserOracleObject) ───────────────────────
3180
3256
 
3257
+ private async interactionStore(
3258
+ identity: TurnIdentity,
3259
+ sessionId: string,
3260
+ options: { recordAccess?: boolean } = {},
3261
+ ) {
3262
+ await this.ready(identity, options);
3263
+ if (
3264
+ this.userDid !== identity.userDid ||
3265
+ !(await this.sessions?.getSession(sessionId))
3266
+ )
3267
+ throw new ActionInteractionError(404, 'not_found', 'Session not found');
3268
+ return new ActionInteractionStore(this.ctx.storage, {
3269
+ oracleDid: this.env.ORACLE_DID,
3270
+ userDid: identity.userDid,
3271
+ });
3272
+ }
3273
+
3274
+ async readActionInteractionTask(
3275
+ identity: TurnIdentity,
3276
+ sessionId: string,
3277
+ taskId: string,
3278
+ ) {
3279
+ return interactionOutcome(async () => {
3280
+ // The task scheduler needs a booted object, but an open task card polls every 15 s:
3281
+ // the poll is not activity, so it never keeps an idle object from being evicted.
3282
+ await this.interactionStore(identity, sessionId, {
3283
+ recordAccess: false,
3284
+ });
3285
+ const task = await this.taskScheduler?.surface.get(taskId);
3286
+ if (!task)
3287
+ throw new ActionInteractionError(404, 'not_found', 'Task not found');
3288
+ return JSON.stringify({ task: taskCardSummary(task) });
3289
+ });
3290
+ }
3291
+
3292
+ /**
3293
+ * The draft store without a boot: the object is addressed by the
3294
+ * caller's DID, and the stored binding is checked against it.
3295
+ */
3296
+ private async bareInteractionStore(identity: TurnIdentity) {
3297
+ const bound =
3298
+ this.userDid ?? (await this.ctx.storage.get<string>(META_USER_DID));
3299
+ if (bound && bound !== identity.userDid)
3300
+ throw new ActionInteractionError(404, 'not_found', 'Owner not found');
3301
+ return new ActionInteractionStore(this.ctx.storage, {
3302
+ oracleDid: this.env.ORACLE_DID,
3303
+ userDid: identity.userDid,
3304
+ });
3305
+ }
3306
+
3307
+ async checkDispatch(identity: TurnIdentity, input: CheckDispatch) {
3308
+ return interactionOutcome(async () => {
3309
+ // Runs before every bid, claim-block and approval effect: no boot, as in checkClaimDispatch.
3310
+ const store = await this.bareInteractionStore(identity);
3311
+ return JSON.stringify(await store.checkDispatch(input));
3312
+ });
3313
+ }
3314
+
3315
+ async checkClaimDispatch(
3316
+ identity: TurnIdentity,
3317
+ input: CheckClaimDispatch,
3318
+ ) {
3319
+ return interactionOutcome(async () => {
3320
+ // Every Portal claim calls this, Companion user or not, so it must
3321
+ // not boot the object (gateway, signer, SQLite, owner-copy import):
3322
+ // it only reads and writes a few keys. The object is addressed by
3323
+ // the caller's DID; the stored binding is checked without a boot.
3324
+ const store = await this.bareInteractionStore(identity);
3325
+ return JSON.stringify(await store.checkClaimDispatch(input));
3326
+ });
3327
+ }
3328
+
3329
+ /** When stored action-card drafts next need retention, if any are kept. */
3330
+ private async interactionRetentionAt(): Promise<number | null> {
3331
+ const userDid =
3332
+ this.userDid ?? (await this.ctx.storage.get<string>(META_USER_DID));
3333
+ if (!userDid) return null;
3334
+ return new ActionInteractionStore(this.ctx.storage, {
3335
+ oracleDid: this.env.ORACLE_DID,
3336
+ userDid,
3337
+ })
3338
+ .nextRetentionAt()
3339
+ .catch(() => null);
3340
+ }
3341
+
3342
+ /** One retention pass on an un-booted object; returns the next due time. */
3343
+ private async bareInteractionRetention(): Promise<number | null> {
3344
+ const userDid = await this.ctx.storage.get<string>(META_USER_DID);
3345
+ if (!userDid) return null;
3346
+ const store = new ActionInteractionStore(this.ctx.storage, {
3347
+ oracleDid: this.env.ORACLE_DID,
3348
+ userDid,
3349
+ });
3350
+ try {
3351
+ await store.prune();
3352
+ } catch (err) {
3353
+ console.warn(
3354
+ `[action-interactions] retention on an evicted object failed for ${userDid}: ${err instanceof Error ? err.message : String(err)}`,
3355
+ );
3356
+ return Date.now() + 60 * 60 * 1000;
3357
+ }
3358
+ const next = await store.nextRetentionAt().catch(() => null);
3359
+ // A backlog larger than one pass resumes a minute later.
3360
+ return next === null ? null : Math.max(next, Date.now() + 60_000);
3361
+ }
3362
+
3363
+ /** Drafts of a deleted chat, in bounded batches. */
3364
+ private async forgetInteractions(userDid: string, sessionId: string) {
3365
+ const store = new ActionInteractionStore(this.ctx.storage, {
3366
+ oracleDid: this.env.ORACLE_DID,
3367
+ userDid,
3368
+ });
3369
+ while ((await store.forgetSession(sessionId)) > 0);
3370
+ }
3371
+
3372
+ async actionInteraction(
3373
+ identity: TurnIdentity,
3374
+ command: InteractionCommand,
3375
+ ) {
3376
+ return interactionOutcome(async () => {
3377
+ const store = await this.interactionStore(
3378
+ identity,
3379
+ command.input.sessionId,
3380
+ );
3381
+ if (
3382
+ command.kind === 'prepare' &&
3383
+ command.input.owner.kind === 'task' &&
3384
+ !(await this.taskScheduler?.surface.get(command.input.owner.taskId))
3385
+ )
3386
+ throw new ActionInteractionError(404, 'not_found', 'Task not found');
3387
+ return JSON.stringify(await store.command(command));
3388
+ });
3389
+ }
3390
+
3391
+ async readActionInteraction(
3392
+ identity: TurnIdentity,
3393
+ sessionId: string,
3394
+ id?: string,
3395
+ references = false,
3396
+ ) {
3397
+ return interactionOutcome(async () => {
3398
+ // Card polls never boot the object or count as activity: they read a
3399
+ // few keys, so an open card neither imports an evicted owner copy nor
3400
+ // keeps an idle object alive. A deleted chat's drafts are deleted
3401
+ // with it, so no session lookup is needed to hide them.
3402
+ const store = await this.bareInteractionStore(identity);
3403
+ return JSON.stringify(
3404
+ await (id
3405
+ ? store.read(id, sessionId)
3406
+ : references
3407
+ ? store.listReferences(sessionId)
3408
+ : store.list(sessionId)),
3409
+ );
3410
+ });
3411
+ }
3412
+
3181
3413
  async createSession(
3182
3414
  identity: TurnIdentity,
3183
3415
  o: { roomId?: string; sessionId?: string; markerTxnId?: string } = {},
@@ -4094,6 +4326,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4094
4326
  });
4095
4327
  await this.ctx.storage.delete(contextStatsKey(sessionId));
4096
4328
  await this.feedbackMarkers!.forgetSession(sessionId);
4329
+ // The chat's action-card drafts go with it; a dispatched claim keeps
4330
+ // only the tombstones that stop a duplicate.
4331
+ await this.forgetInteractions(identity.userDid, sessionId).catch(
4332
+ (err: unknown) => {
4333
+ console.warn(
4334
+ `[action-interactions] could not delete the drafts of ${sessionId}; retention removes them later: ${err instanceof Error ? err.message : String(err)}`,
4335
+ );
4336
+ },
4337
+ );
4097
4338
  this.markDirty();
4098
4339
  }
4099
4340
  // A channel binding that pointed at the session opens a new one on
@@ -4787,7 +5028,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4787
5028
  !isAttachmentViewMessage(m) &&
4788
5029
  !isDeliveryNoticeMessage(m),
4789
5030
  )
4790
- .map((m) => ({ type: m.type, content: visibleText(m) }));
5031
+ .map(conversationReader());
4791
5032
  }
4792
5033
 
4793
5034
  /**
@@ -4810,6 +5051,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4810
5051
  }
4811
5052
  const pageRows = Math.max(1, limit);
4812
5053
  const entries: HistoryEntry[] = [];
5054
+ const read = conversationReader();
4813
5055
  for (;;) {
4814
5056
  const rows = await saver.listThreadMessageRows(sessionId, {
4815
5057
  direction: 'newer',
@@ -4826,7 +5068,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4826
5068
  )
4827
5069
  continue;
4828
5070
  entries.push({
4829
- message: { type: m.type, content: visibleText(m) },
5071
+ message: read(m),
4830
5072
  cursor: row.messageId,
4831
5073
  });
4832
5074
  }
@@ -5021,6 +5263,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5021
5263
  prior: unknown;
5022
5264
  client: TurnRequest['client'];
5023
5265
  roomKind: TurnRequest['roomKind'];
5266
+ /** The provider's cache has expired, so the prompt may change. */
5267
+ coldTurn: boolean;
5024
5268
  }): UserContextSettle | null {
5025
5269
  // A first contact that fetches nothing still prints the one
5026
5270
  // first-contact line (see the settle step below), with its reason, so
@@ -5075,9 +5319,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5075
5319
  // room answer — but only one already in the cache: an upload that
5076
5320
  // just invalidated the room must not make the next message wait for
5077
5321
  // a gather. On a miss the thread keeps its context and the
5078
- // (single-flight) refresh warms the cache for a later turn.
5322
+ // (single-flight) refresh warms the cache for a later turn. The
5323
+ // answer sits in the system prompt, so it waits for a cold turn: a
5324
+ // change there re-reads the whole request at the uncached price.
5079
5325
  const warm =
5080
- input.client === 'channel'
5326
+ input.client === 'channel' && input.coldTurn
5081
5327
  ? fetcher.readCached(input.roomId)
5082
5328
  : undefined;
5083
5329
  keepGathering();
@@ -7178,13 +7424,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7178
7424
  sessionId: string,
7179
7425
  ): Promise<SessionContextStatus | null> {
7180
7426
  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
- : [];
7427
+ const context = await this.contextMessages(sessionId);
7188
7428
  const thread = await this.saver!.listThreadMessages(sessionId);
7189
7429
  const stats = await this.ctx.storage.get<SessionContextCounters>(
7190
7430
  contextStatsKey(sessionId),
@@ -7204,6 +7444,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7204
7444
  };
7205
7445
  }
7206
7446
 
7447
+ /** The messages the session's model calls start from (a summary, then the kept tail, once condensed). */
7448
+ private async contextMessages(sessionId: string): Promise<BaseMessage[]> {
7449
+ const tuple = await this.saver!.getTuple({
7450
+ configurable: { thread_id: sessionId },
7451
+ });
7452
+ const channel: unknown = tuple?.checkpoint.channel_values.messages;
7453
+ return Array.isArray(channel) ? channel.filter(isBaseMessage) : [];
7454
+ }
7455
+
7207
7456
  /** Fold a guard event into the session's stored counters (ordered, never throws). */
7208
7457
  private recordContextEvent(
7209
7458
  sessionId: string,
@@ -7222,7 +7471,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7222
7471
  switch (event.kind) {
7223
7472
  case 'prune':
7224
7473
  next.prunes += 1;
7225
- if (event.stage !== 'soft') next.hardPrunes += 1;
7474
+ if (event.stage === 'hard' || event.stage === 'overflow')
7475
+ next.hardPrunes += 1;
7226
7476
  next.prunedResults += event.pruned;
7227
7477
  break;
7228
7478
  case 'overflow-retry':
@@ -7721,6 +7971,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7721
7971
  configurable: { thread_id: req.sessionId },
7722
7972
  });
7723
7973
  const priorState = existing?.checkpoint.channel_values ?? {};
7974
+ // A new thread, or quiet for longer than the provider's cache lives:
7975
+ // this turn pays a miss whatever it sends, so it is the one to change
7976
+ // the prefix on.
7977
+ const coldTurn =
7978
+ !existing ||
7979
+ turnAt.getTime() - Date.parse(existing.checkpoint.ts) >=
7980
+ contextBudget.cacheColdMs;
7724
7981
  // What the memory engine knows about the user, for the prompt's
7725
7982
  // context block: started now so the round-trip overlaps the rest of
7726
7983
  // the build, settled right before the agent is built.
@@ -7734,6 +7991,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7734
7991
  prior: priorState.userContext,
7735
7992
  client: req.client,
7736
7993
  roomKind: req.roomKind,
7994
+ coldTurn,
7737
7995
  });
7738
7996
 
7739
7997
  // A Matrix turn with no usable delegation: ask the web app to open its
@@ -7862,11 +8120,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7862
8120
  suppliedContextOnly ? {} : priorState,
7863
8121
  );
7864
8122
 
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
8123
  const priorLoaded: ReadonlySet<string> = new Set(
7871
8124
  priorMeta.loadedPlugins ?? [],
7872
8125
  );
@@ -7885,45 +8138,49 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
7885
8138
  client: req.client,
7886
8139
  env: langsmithEnvFromWorkerEnv(this.env),
7887
8140
  });
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,
8141
+ // Capability router: from what is left to load this turn (the build
8142
+ // works that out from the turn's tools), the plugins this message needs.
8143
+ // `on` is awaited in the build, ahead of the first model call, and the
8144
+ // plugins load on the thread like any other; `shadow` returns at once
8145
+ // and logs its verdict when it lands. A resumed attempt keeps what the
8146
+ // run started with.
8147
+ const router = this.capabilityRouter;
8148
+ const routeCapabilities =
8149
+ !suppliedContextOnly && !run.resumed && router
8150
+ ? (candidates: readonly RegisteredManifest[]) =>
8151
+ router({
8152
+ mode: capabilityRouterMode(core.validatedEnv.CAPABILITY_ROUTER),
8153
+ candidates,
8154
+ ...preloadThreshold(coldTurn, core.validatedEnv),
8155
+ text: body.message,
8156
+ recentTurns: async () =>
8157
+ recentTurnTexts(
8158
+ await saver.listRecentThreadMessages(
8159
+ req.sessionId,
8160
+ ROUTER_RECENT_ROWS,
7903
8161
  ),
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,
8162
+ ROUTER_RECENT_TURNS,
8163
+ ROUTER_RECENT_TURN_CHARS,
8164
+ ),
8165
+ requestId: req.requestId,
8166
+ signal: abortController.signal,
8167
+ trace: {
8168
+ ...(tracing.callbacks && { callbacks: tracing.callbacks }),
8169
+ // `thread_id` matches the graph run's, so LangSmith's thread
8170
+ // view groups the router span with the turn.
8171
+ metadata: {
8172
+ ...tracing.metadata,
8173
+ thread_id: req.sessionId,
8174
+ request_id: req.requestId,
8175
+ },
7918
8176
  },
7919
- },
7920
- onShadowVerdict: (wouldPreload) =>
7921
- this.shadowRoutes.set(req.sessionId, {
7922
- requestId: req.requestId,
7923
- priorLoaded,
7924
- wouldPreload,
7925
- }),
7926
- })
8177
+ onShadowVerdict: (wouldPreload) =>
8178
+ this.shadowRoutes.set(req.sessionId, {
8179
+ requestId: req.requestId,
8180
+ priorLoaded,
8181
+ wouldPreload,
8182
+ }),
8183
+ })
7927
8184
  : undefined;
7928
8185
 
7929
8186
  // Host page-context / safety-guardrail hooks, resolved against this
@@ -8094,13 +8351,44 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8094
8351
  )
8095
8352
  : undefined;
8096
8353
 
8354
+ // On a cold turn the thread's history shows which capabilities it has
8355
+ // moved on from. The messages are read only then, and only for a
8356
+ // thread with something to unload; the editor stays while its context
8357
+ // is active, and a resumed attempt keeps what the run started with.
8358
+ const unloadable = (priorMeta.loadedPlugins ?? []).filter(
8359
+ (plugin) =>
8360
+ plugin !== EDITOR_PLUGIN_NAME ||
8361
+ !editorContextActive(meta, priorMeta),
8362
+ );
8363
+ const capabilityExpiry =
8364
+ coldTurn &&
8365
+ !run.resumed &&
8366
+ contextBudget.pluginIdleTurns > 0 &&
8367
+ unloadable.length > 0
8368
+ ? {
8369
+ loaded: unloadable,
8370
+ maxIdleTurns: contextBudget.pluginIdleTurns,
8371
+ messages: await this.contextMessages(req.sessionId),
8372
+ }
8373
+ : undefined;
8374
+
8097
8375
  const built = await createMainAgent({
8098
8376
  executionProfile: req.executionProfile,
8377
+ ...(capabilityExpiry ? { capabilityExpiry } : {}),
8099
8378
  registries: core.registries,
8100
8379
  identity: core.identity,
8101
8380
  config: core.validatedEnv,
8102
8381
  availablePlugins: core.availablePlugins,
8103
- preloadedPlugins,
8382
+ ...(routeCapabilities ? { routeCapabilities } : {}),
8383
+ promptCache: {
8384
+ cold: coldTurn && !run.resumed,
8385
+ ...(priorMeta.promptGuides
8386
+ ? { promptGuides: priorMeta.promptGuides }
8387
+ : {}),
8388
+ ...(priorMeta.capabilityUsage
8389
+ ? { capabilityUsage: priorMeta.capabilityUsage }
8390
+ : {}),
8391
+ },
8104
8392
  byoProvider: byoTurn?.provider,
8105
8393
  contextBudget,
8106
8394
  turnBudget: budget,
@@ -8224,7 +8512,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8224
8512
  checkpointer: saver,
8225
8513
  abortSignal: abortController.signal,
8226
8514
  });
8227
- const { agent, context } = built;
8515
+ const { agent, context, guideNote } = built;
8228
8516
  for (const [name, effect] of built.toolEffects)
8229
8517
  toolEffects.set(name, effect);
8230
8518
  for (const [name, surface] of built.toolSurfaces)
@@ -8277,9 +8565,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8277
8565
  const referenced = replyNote
8278
8566
  ? withTurnTimeNote(noticed, replyNote)
8279
8567
  : noticed;
8280
- const content = timeNote
8281
- ? withTurnTimeNote(referenced, timeNote)
8568
+ const guided = guideNote
8569
+ ? withTurnTimeNote(referenced, guideNote.text)
8282
8570
  : referenced;
8571
+ const content = timeNote ? withTurnTimeNote(guided, timeNote) : guided;
8283
8572
  const human = new HumanMessage({
8284
8573
  content,
8285
8574
  additional_kwargs: {
@@ -8289,6 +8578,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8289
8578
  ...(noticeNote
8290
8579
  ? { [CHANNEL_DELIVERY_NOTICE_KWARG]: noticeNote }
8291
8580
  : {}),
8581
+ ...(guideNote
8582
+ ? {
8583
+ [TURN_GUIDE_NOTE_KWARG]: guideNote.text,
8584
+ [USAGE_CARRIED_KWARG]: guideNote.plugins,
8585
+ }
8586
+ : {}),
8292
8587
  oracleName: core.identity.name,
8293
8588
  msgFromMatrixRoom: req.client === 'matrix',
8294
8589
  ...speakerKwargs,
@@ -8319,6 +8614,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
8319
8614
  // every slot present; later turns write nothing and keep it.
8320
8615
  ...(userContext ? { userContext } : {}),
8321
8616
  ...metadataGraphInput(meta, priorMeta),
8617
+ ...built.stateUpdate,
8322
8618
  ...surface.input,
8323
8619
  };
8324
8620
  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,