@ixo/oracle-runtime-workers 0.15.0 → 0.17.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 (44) hide show
  1. package/README.md +4 -1
  2. package/package.json +4 -2
  3. package/src/channels/auth.ts +54 -11
  4. package/src/channels/contract.ts +122 -13
  5. package/src/channels/turns.ts +12 -2
  6. package/src/core/budgeted-llm.ts +115 -6
  7. package/src/core/context-budget.ts +24 -26
  8. package/src/core/context-window.ts +39 -12
  9. package/src/core/domain-context/document.ts +100 -0
  10. package/src/core/domain-context/findings.ts +25 -0
  11. package/src/core/domain-context/index.ts +28 -0
  12. package/src/core/domain-context/integrity.ts +60 -0
  13. package/src/core/domain-context/lru.ts +63 -0
  14. package/src/core/domain-context/resolver.ts +651 -0
  15. package/src/core/domain-context/transport.ts +340 -0
  16. package/src/core/domain-context/turn-context.ts +443 -0
  17. package/src/core/domain-context/types.ts +119 -0
  18. package/src/core/domain-context/validator.ts +41 -0
  19. package/src/core/env.ts +60 -6
  20. package/src/core/index.ts +2 -0
  21. package/src/core/llm.ts +225 -4
  22. package/src/core/main-agent-types.ts +28 -0
  23. package/src/core/main-agent.ts +61 -4
  24. package/src/core/middlewares/tool-execution.ts +19 -2
  25. package/src/core/registries.ts +14 -3
  26. package/src/core/state.ts +2 -1
  27. package/src/core/sub-agent-fallback.ts +14 -2
  28. package/src/core/turn-budget.ts +19 -22
  29. package/src/do/contracts.ts +23 -2
  30. package/src/do/domain-context-store.ts +123 -0
  31. package/src/do/run-request.ts +6 -0
  32. package/src/do/sse-stream.ts +1 -1
  33. package/src/do/transcript.ts +9 -0
  34. package/src/do/turn-body.ts +2 -1
  35. package/src/do/turn-metadata.ts +13 -6
  36. package/src/do/user-oracle-do.ts +237 -60
  37. package/src/index.ts +16 -0
  38. package/src/llm/byo-adapter.ts +6 -0
  39. package/src/llm/byo-catalog.ts +7 -4
  40. package/src/llm/byo-client.ts +42 -7
  41. package/src/llm/provider-error.ts +21 -3
  42. package/src/llm/stream-liveness.ts +498 -0
  43. package/src/plugins/firecrawl/firecrawl-tools.ts +6 -0
  44. package/src/shell/app.ts +41 -8
@@ -4,6 +4,7 @@ import {
4
4
  ChannelError,
5
5
  channelRequestHash,
6
6
  channelOrigin,
7
+ delegationRequiredError,
7
8
  requireChannelDelegation,
8
9
  type ChannelTurnInput,
9
10
  type ChannelTurnOutcome,
@@ -68,10 +69,10 @@ import {
68
69
  langsmithEnvFromWorkerEnv,
69
70
  llmEnvFromWorkerEnv,
70
71
  resolveLangsmithTracing,
72
+ streamLivenessFromEnv,
71
73
  type LangsmithTracingDecision,
72
74
  type OpenRouterLlmAdapter,
73
75
  DEFAULT_MODEL_ID,
74
- OPENROUTER_MODEL_MAP,
75
76
  } from '../core/llm';
76
77
  import { renderTurnTimeNote } from '../core/prompt-composer';
77
78
  import { TURN_TIME_NOTE_KWARG, withTurnTimeNote } from '../core/turn-time-note';
@@ -96,7 +97,7 @@ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
96
97
  import type { AmbientServices, LlmAdapter } from '../core/runtime-context';
97
98
  import { chatGptBackendFromEnv } from '../llm/byo-client';
98
99
  import { createByoLlmAdapter } from '../llm/byo-adapter';
99
- import { byoModelForRole, isByoModelId } from '../llm/byo-catalog';
100
+ import { isByoModelId } from '../llm/byo-catalog';
100
101
  import { handleByoRequest } from '../llm/byo-routes';
101
102
  import {
102
103
  WorkersByoService,
@@ -191,6 +192,7 @@ import {
191
192
  type TurnIdentity,
192
193
  type TurnRequest,
193
194
  type TurnResult,
195
+ type DomainContextRouterUpdate,
194
196
  type MemorySchemaDebug,
195
197
  TURN_INTERRUPTED_MARKER,
196
198
  type JsonString,
@@ -274,6 +276,14 @@ import {
274
276
  } from '../core/middlewares/context-guard';
275
277
  import { fetchOpenRouterContextLengths } from '../core/openrouter-pricing';
276
278
  import { ResultStore, resultStoreKnobs } from './result-store';
279
+ import {
280
+ DomainContextResolver,
281
+ type DomainContextOptions,
282
+ type DomainContextPins,
283
+ type DomainContextProvenance,
284
+ } from '../core/domain-context';
285
+ import { DomainContextStore } from './domain-context-store';
286
+ import { EVENT_NAMES } from '../core/runtime-context';
277
287
  import { artifactStorageConfig } from '../artifacts/config';
278
288
  import {
279
289
  ArtifactStore,
@@ -311,7 +321,11 @@ import type {
311
321
  FeedbackSettlement,
312
322
  FeedbackTarget,
313
323
  } from '../feedback/contract';
314
- import { resolveTurnDelegation, WorkersUcanService } from './ucan-service';
324
+ import {
325
+ readDelegation,
326
+ resolveTurnDelegation,
327
+ WorkersUcanService,
328
+ } from './ucan-service';
315
329
  import type { UcanDelegation } from '../plugin-api/types';
316
330
 
317
331
  /** What one attempt's agent build reads from the request body. */
@@ -328,6 +342,8 @@ interface TurnBuildRun {
328
342
  resumed: boolean;
329
343
  /** The reply text the user already received (resumed attempts). */
330
344
  continuation: string | null;
345
+ /** Domain anchors the run started with (resumed attempts keep them). */
346
+ domainPins?: DomainContextPins;
331
347
  }
332
348
 
333
349
  /**
@@ -531,6 +547,11 @@ export interface OracleWorkerHooks {
531
547
  export interface UserOracleDOOptions {
532
548
  core: (env: OracleWorkerEnv) => RuntimeCore;
533
549
  hooks?: OracleWorkerHooks;
550
+ /**
551
+ * Observe-only domain context (docs/configuration.md). Absent or `off`:
552
+ * no lookups, no prompt block, no tools.
553
+ */
554
+ domainContext?: DomainContextOptions;
534
555
  }
535
556
 
536
557
  interface StoredDelegation {
@@ -540,6 +561,17 @@ interface StoredDelegation {
540
561
  at: number;
541
562
  }
542
563
 
564
+ /** The user's delegation as the object holds it in memory. */
565
+ interface CachedDelegation {
566
+ raw: string;
567
+ /**
568
+ * Unix seconds: the delegation's effective expiry. Taken from where the
569
+ * delegation arrived (the shell's validation, the deposit record) or read
570
+ * off the token itself; absent only when the token cannot be parsed.
571
+ */
572
+ expiration?: number;
573
+ }
574
+
543
575
  interface FlushResult {
544
576
  uploaded: boolean;
545
577
  /** Raw file size (skips) or bytes sent upstream (uploads). */
@@ -603,7 +635,11 @@ function lastAiText(messages: BaseMessage[]): string {
603
635
  }
604
636
 
605
637
  /** Id of the final assistant message (what `POST /messages` reports as `message.id`). */
606
- /** `turn_runs.usage` for `/debug/runs`; a row written before the column existed, or an unreadable one, is null. */
638
+ /**
639
+ * `turn_runs.usage` for `/debug/runs`; a row written before the column
640
+ * existed, or an unreadable one, is null. A `limits.tokens` a row from an
641
+ * older runtime still carries is ignored: tokens are no longer limited.
642
+ */
607
643
  function parseUsage(raw: string | null): TurnUsage | null {
608
644
  if (!raw) return null;
609
645
  try {
@@ -626,7 +662,6 @@ function parseUsage(raw: string | null): TurnUsage | null {
626
662
  toolAttempts: num('toolAttempts'),
627
663
  elapsedMs: num('elapsedMs'),
628
664
  limits: {
629
- tokens: limit('tokens'),
630
665
  tools: limit('tools'),
631
666
  durationMs: limit('durationMs'),
632
667
  },
@@ -637,7 +672,6 @@ function parseUsage(raw: string | null): TurnUsage | null {
637
672
  usage.modelCalls === undefined ||
638
673
  usage.toolAttempts === undefined ||
639
674
  usage.elapsedMs === undefined ||
640
- usage.limits.tokens === undefined ||
641
675
  usage.limits.tools === undefined ||
642
676
  usage.limits.durationMs === undefined
643
677
  )
@@ -649,7 +683,6 @@ function parseUsage(raw: string | null): TurnUsage | null {
649
683
  toolAttempts: usage.toolAttempts,
650
684
  elapsedMs: usage.elapsedMs,
651
685
  limits: {
652
- tokens: usage.limits.tokens,
653
686
  tools: usage.limits.tools,
654
687
  durationMs: usage.limits.durationMs,
655
688
  },
@@ -725,6 +758,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
725
758
  * bounded lanes. Lives as long as the object.
726
759
  */
727
760
  private readonly toolScheduler = new ToolScheduler();
761
+ /**
762
+ * Domain-context anchors and documents, cached for every session of this
763
+ * object; `refresh_domain_context` invalidates entries here.
764
+ */
765
+ private readonly domainResolver = new DomainContextResolver();
766
+ /** Per-turn domain-context provenance (`domain_context_runs`). */
767
+ private domainContextStore: DomainContextStore | null = null;
728
768
  private runs: RunCoordinator | null = null;
729
769
  private readonly runConfig: RunDurabilityConfig = runDurabilityConfig(
730
770
  this.env,
@@ -761,7 +801,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
761
801
  * events of one request (a soft then a hard prune) never race.
762
802
  */
763
803
  private contextStatsWrites: Promise<void> = Promise.resolve();
764
- private readonly delegations = new Map<string, { raw: string }>();
804
+ private readonly delegations = new Map<string, CachedDelegation>();
765
805
  /** Epoch-ms until which a room-state delegation lookup is not retried. */
766
806
  private delegationMissUntil = 0;
767
807
  private dirty = false;
@@ -977,9 +1017,18 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
977
1017
  this.delegations.get(identity.userDid)?.raw ??
978
1018
  (await this.ctx.storage.get<StoredDelegation>(META_DELEGATION))?.raw;
979
1019
  delegationReplaced = known !== identity.ucanDelegation;
980
- this.delegations.set(identity.userDid, {
981
- raw: identity.ucanDelegation,
982
- });
1020
+ const cached = this.delegations.get(identity.userDid);
1021
+ if (
1022
+ cached?.raw !== identity.ucanDelegation ||
1023
+ typeof cached.expiration !== 'number'
1024
+ )
1025
+ this.delegations.set(
1026
+ identity.userDid,
1027
+ await this.delegationRecord(
1028
+ identity.ucanDelegation,
1029
+ identity.ucanDelegationExpiration,
1030
+ ),
1031
+ );
983
1032
  // Remember a new one for turns that arrive without the header.
984
1033
  if (delegationReplaced)
985
1034
  await this.ctx.storage.put(META_DELEGATION, {
@@ -1125,7 +1174,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1125
1174
  const cached =
1126
1175
  await this.ctx.storage.get<StoredDelegation>(META_DELEGATION);
1127
1176
  if (live(cached)) {
1128
- this.delegations.set(userDid, { raw: cached.raw });
1177
+ this.delegations.set(
1178
+ userDid,
1179
+ await this.delegationRecord(cached.raw, cached.expiration),
1180
+ );
1129
1181
  return;
1130
1182
  }
1131
1183
  if (Date.now() < this.delegationMissUntil) return;
@@ -1156,7 +1208,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1156
1208
  }
1157
1209
  : null;
1158
1210
  if (live(deposited)) {
1159
- this.delegations.set(userDid, { raw: deposited.raw });
1211
+ this.delegations.set(
1212
+ userDid,
1213
+ await this.delegationRecord(deposited.raw, deposited.expiration),
1214
+ );
1160
1215
  await this.ctx.storage.put(META_DELEGATION, deposited);
1161
1216
  console.log(
1162
1217
  `[user-do] delegation for ${userDid} loaded from the deposited room state (header-less turn)`,
@@ -1171,6 +1226,31 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1171
1226
  this.delegationMissUntil = Date.now() + DELEGATION_MISS_TTL_MS;
1172
1227
  }
1173
1228
 
1229
+ /**
1230
+ * The in-memory record of a delegation: the expiry where it arrived
1231
+ * stated one (the shell validated the token and read it off the chain),
1232
+ * else the token's own effective expiry along its proof chain. A token
1233
+ * that cannot be parsed keeps no expiry; channel turns refuse it.
1234
+ */
1235
+ private async delegationRecord(
1236
+ raw: string,
1237
+ expiration: number | undefined,
1238
+ ): Promise<CachedDelegation> {
1239
+ if (typeof expiration === 'number' && Number.isFinite(expiration))
1240
+ return { raw, expiration };
1241
+ try {
1242
+ const grant = await readDelegation(raw);
1243
+ return typeof grant.expiration === 'number'
1244
+ ? { raw, expiration: grant.expiration }
1245
+ : { raw };
1246
+ } catch (err) {
1247
+ console.warn(
1248
+ `[user-do] could not read the expiry of a delegation: ${err instanceof Error ? err.message : String(err)}`,
1249
+ );
1250
+ return { raw };
1251
+ }
1252
+ }
1253
+
1174
1254
  private async boot(userDid: string): Promise<void> {
1175
1255
  const stored = await this.ctx.storage.get<string>(META_USER_DID);
1176
1256
  if (stored && stored !== userDid)
@@ -1585,6 +1665,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1585
1665
  });
1586
1666
  await this.resultStore.setup();
1587
1667
  this.attachmentTextCache = new AttachmentTextCacheStore(liveDb, console);
1668
+ // Constructing it touches no SQL. Only an observe turn records (and so
1669
+ // creates `domain_context_runs`); a session delete never creates it but
1670
+ // still removes rows an earlier observe deployment left behind.
1671
+ this.domainContextStore = new DomainContextStore(liveDb, console);
1588
1672
  // The store exists whenever the bucket is bound, so artefacts made
1589
1673
  // earlier stay readable, revocable and deleted with their session after
1590
1674
  // the public origin is removed; only new ones need it.
@@ -1616,7 +1700,12 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1616
1700
  this.channelTurns = new ChannelTurns(liveDb, {
1617
1701
  createSession: async (identity, markerTxnId) => {
1618
1702
  const room = await this.gateway.resolveUserRoom(identity.userDid);
1619
- if (!room) throw new ChannelError(409, 'Companion room is not ready');
1703
+ if (!room)
1704
+ throw new ChannelError(
1705
+ 428,
1706
+ 'Companion room is not ready',
1707
+ 'room_not_ready',
1708
+ );
1620
1709
  return (
1621
1710
  await this.createSession(identity, {
1622
1711
  roomId: room.roomId,
@@ -1633,7 +1722,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1633
1722
  !room ||
1634
1723
  session.roomId !== room.roomId
1635
1724
  )
1636
- throw new ChannelError(404, 'Companion session not found');
1725
+ throw new ChannelError(
1726
+ 404,
1727
+ 'Companion session not found',
1728
+ 'session_not_found',
1729
+ );
1637
1730
  const encryption = await this.gateway.getRoomStateEvent(
1638
1731
  room.roomId,
1639
1732
  'm.room.encryption',
@@ -1645,11 +1738,17 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1645
1738
  !('algorithm' in parsed) ||
1646
1739
  parsed.algorithm !== 'm.megolm.v1.aes-sha2'
1647
1740
  )
1648
- throw new ChannelError(409, 'Companion room must be encrypted');
1741
+ throw new ChannelError(
1742
+ 409,
1743
+ 'Companion room must be encrypted',
1744
+ 'room_not_encrypted',
1745
+ );
1649
1746
  },
1747
+ // Checked again as the run starts: admission may have waited.
1650
1748
  requireDelegation: async (identity) =>
1651
1749
  requireChannelDelegation(
1652
- !!this.delegations.get(identity.userDid)?.raw,
1750
+ this.delegations.get(identity.userDid),
1751
+ Math.floor(Date.now() / 1000),
1653
1752
  ),
1654
1753
  getRun: (runId) => this.runStore!.get(runId),
1655
1754
  getPlan: (runId) => this.runStore!.getPlan(runId),
@@ -1823,6 +1922,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
1823
1922
  this.resultStore = null;
1824
1923
  this.artifacts = null;
1825
1924
  this.attachmentTextCache = null;
1925
+ this.domainContextStore = null;
1826
1926
  // Its store is bound to the database just dropped: a caller that sees
1827
1927
  // no scheduler boots again instead of reading a closed connection.
1828
1928
  this.taskScheduler = null;
@@ -2558,16 +2658,52 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2558
2658
  throw new ChannelError(
2559
2659
  403,
2560
2660
  'Channel grants cannot become tool authority',
2661
+ 'channel_grant_not_tool_authority',
2561
2662
  );
2562
2663
  // The shell (the only caller) checked the binding just before.
2563
- await this.ready(identity);
2664
+ // The delegation is checked BEFORE the object boots: a cold boot
2665
+ // loads the owner copy under it, and a user without a usable one
2666
+ // must get `delegation_required`, not a boot failure. Hydration
2667
+ // reads the cached copy, then the deposited room state (throttled
2668
+ // after a miss); an in-memory one that no longer passes is
2669
+ // re-read too, in case a newer deposit did not reach this object.
2670
+ const nowSeconds = Math.floor(Date.now() / 1000);
2671
+ try {
2672
+ requireChannelDelegation(
2673
+ this.delegations.get(identity.userDid),
2674
+ nowSeconds,
2675
+ );
2676
+ } catch {
2677
+ await this.hydrateDelegation(identity.userDid);
2678
+ requireChannelDelegation(
2679
+ this.delegations.get(identity.userDid),
2680
+ nowSeconds,
2681
+ );
2682
+ }
2683
+ try {
2684
+ await this.ready(identity);
2685
+ } catch (error) {
2686
+ // A delegation that lacks the file-storage grant over
2687
+ // `/.oracles` cannot load the owner copy: the user must
2688
+ // authorize again, exactly as without a delegation. (Boot throws
2689
+ // the failure as its RPC envelope, so it is parsed, not matched
2690
+ // by class.)
2691
+ if (parseOwnerCopyFailure(error)?.code === 'NO_VFS_DELEGATION')
2692
+ throw delegationRequiredError();
2693
+ throw error;
2694
+ }
2564
2695
  return {
2565
2696
  ok: true,
2566
2697
  result: await this.channelTurns!.submit(identity, input, requestHash),
2567
2698
  };
2568
2699
  } catch (error) {
2569
2700
  if (error instanceof ChannelError)
2570
- return { ok: false, status: error.status, message: error.message };
2701
+ return {
2702
+ ok: false,
2703
+ status: error.status,
2704
+ code: error.code,
2705
+ message: error.message,
2706
+ };
2571
2707
  throw error;
2572
2708
  }
2573
2709
  }
@@ -2653,6 +2789,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2653
2789
  `[user-do] could not delete the artefacts of ${sessionId}; their links expire on schedule: ${err instanceof Error ? err.message : String(err)}`,
2654
2790
  );
2655
2791
  });
2792
+ await this.domainContextStore
2793
+ ?.deleteForSession(sessionId)
2794
+ .catch((err: unknown) => {
2795
+ console.warn(
2796
+ `[domain-context] could not delete the provenance of ${sessionId}: ${err instanceof Error ? err.message : String(err)}`,
2797
+ );
2798
+ });
2656
2799
  await this.ctx.storage.delete(contextStatsKey(sessionId));
2657
2800
  await this.feedbackMarkers!.forgetSession(sessionId);
2658
2801
  this.markDirty();
@@ -2891,7 +3034,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
2891
3034
  raw: string,
2892
3035
  expiration?: number,
2893
3036
  ): Promise<void> {
2894
- this.delegations.set(userDid, { raw });
3037
+ this.delegations.set(
3038
+ userDid,
3039
+ await this.delegationRecord(raw, expiration),
3040
+ );
2895
3041
  this.delegationMissUntil = 0;
2896
3042
  await this.ctx.storage.put(META_DELEGATION, {
2897
3043
  raw,
@@ -3001,12 +3147,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
3001
3147
  await this.ctx.storage.get<StoredDelegation>(META_DELEGATION);
3002
3148
  const inMemory = this.delegations.get(userDid);
3003
3149
  if (inMemory) {
3150
+ const expiration = inMemory.expiration ?? stored?.expiration;
3004
3151
  return {
3005
3152
  present: true,
3006
3153
  source: 'memory',
3007
- ...(typeof stored?.expiration === 'number'
3008
- ? { expiration: stored.expiration }
3009
- : {}),
3154
+ ...(typeof expiration === 'number' ? { expiration } : {}),
3010
3155
  };
3011
3156
  }
3012
3157
  if (stored?.raw) {
@@ -4076,7 +4221,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4076
4221
  if (live.attemptSource !== 'begin')
4077
4222
  await assertChannelAttemptAllowed(req.identity, this.env);
4078
4223
  requireChannelDelegation(
4079
- !!this.delegations.get(req.identity.userDid)?.raw,
4224
+ this.delegations.get(req.identity.userDid),
4225
+ Math.floor(Date.now() / 1000),
4080
4226
  );
4081
4227
  }
4082
4228
  // An attempt that starts from `admitting` never reached the agent (the
@@ -4173,6 +4319,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4173
4319
  toolOutputCapChars,
4174
4320
  delivery,
4175
4321
  usage,
4322
+ domainContext,
4176
4323
  } = await this.prepareTurn(
4177
4324
  req,
4178
4325
  {
@@ -4188,9 +4335,28 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4188
4335
  abortController: live.abort,
4189
4336
  resumed: !freshInput,
4190
4337
  continuation: freshInput ? null : live.continuation,
4338
+ ...(stored.domainPins ? { domainPins: stored.domainPins } : {}),
4191
4339
  },
4192
4340
  );
4193
4341
  const sessionId = req.sessionId;
4342
+ // The anchors this run read are pinned on its row, so a resumed
4343
+ // attempt reads the same document revisions.
4344
+ if (
4345
+ domainContext &&
4346
+ JSON.stringify(domainContext.pins) !==
4347
+ JSON.stringify(stored.domainPins ?? {})
4348
+ ) {
4349
+ stored.domainPins = domainContext.pins;
4350
+ const request = JSON.stringify(stored);
4351
+ try {
4352
+ await this.runStore!.update(live.runId, { request });
4353
+ live.record.request = request;
4354
+ } catch (error) {
4355
+ console.warn(
4356
+ `[domain-context] could not pin the anchors of run ${live.runId}: ${error instanceof Error ? error.message : String(error)}`,
4357
+ );
4358
+ }
4359
+ }
4194
4360
  const sink = {
4195
4361
  emit: (eventName: string, payload: Record<string, unknown>) => {
4196
4362
  if (live.buffer.isClosed) return;
@@ -4203,6 +4369,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4203
4369
  // (the turn's deadline timer among them) run however the attempt ends.
4204
4370
  try {
4205
4371
  this.events.register(sessionId, sink);
4372
+ if (domainContext)
4373
+ await this.reportDomainContext(req, domainContext.provenance);
4206
4374
  if (byoNotice)
4207
4375
  sink.emit('error', {
4208
4376
  ...byoNotice,
@@ -4531,15 +4699,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4531
4699
  const window = await this.contextWindows.resolve(modelId, {
4532
4700
  ...(byoProvider ? { byoProvider } : {}),
4533
4701
  });
4534
- const budget = contextBudgetFor(
4535
- window,
4536
- contextKnobs(this.env, console),
4537
- await this.contextWindows.resolve(
4538
- llm && isProviderAdapter(llm)
4539
- ? llm.modelForRole('routing')
4540
- : OPENROUTER_MODEL_MAP.routing,
4541
- ),
4542
- );
4702
+ const budget = contextBudgetFor(window, contextKnobs(this.env, console));
4543
4703
  const stats = (await this.resultStore?.stats()) ?? {
4544
4704
  rows: 0,
4545
4705
  sqliteBytes: 0,
@@ -4897,27 +5057,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
4897
5057
  };
4898
5058
  }
4899
5059
 
4900
- /**
4901
- * The model that summarises the turn's history (the `routing` role): its
4902
- * window bounds what the summariser is handed.
4903
- */
4904
- private summaryModelId(
4905
- platform: LlmAdapter,
4906
- byoTurn: ByoTurnState | null,
4907
- ): string {
4908
- if (byoTurn)
4909
- return (
4910
- byoModelForRole(
4911
- byoTurn.credential.provider,
4912
- 'routing',
4913
- byoTurn.mainModelId,
4914
- ) ?? OPENROUTER_MODEL_MAP.routing
4915
- );
4916
- return isProviderAdapter(platform)
4917
- ? platform.modelForRole('routing')
4918
- : OPENROUTER_MODEL_MAP.routing;
4919
- }
4920
-
4921
5060
  /**
4922
5061
  * Build one attempt's agent and input (`buildTurn`). A build that fails
4923
5062
  * part-way leaves nothing behind: the cleanups it registered run (the
@@ -5029,6 +5168,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5029
5168
  credential: byoTurn.credential,
5030
5169
  mainModelId: byoTurn.mainModelId,
5031
5170
  chatGptBackend: byoTurn.chatGptBackend,
5171
+ streamLiveness: streamLivenessFromEnv(core.validatedEnv, 'byo'),
5032
5172
  },
5033
5173
  console,
5034
5174
  ),
@@ -5047,13 +5187,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5047
5187
  const windowResolution = await this.contextWindows.resolve(mainModelId, {
5048
5188
  ...(byoTurn ? { byoProvider: byoTurn.provider } : {}),
5049
5189
  });
5190
+ // The summary is written by the main model too (the `summarizer`
5191
+ // role), so the main model's window is the only one that counts.
5050
5192
  const contextBudget = contextBudgetFor(
5051
5193
  windowResolution,
5052
5194
  contextKnobs(this.env, console),
5053
- await this.contextWindows.resolve(
5054
- this.summaryModelId(baseAmbient.llm, byoTurn),
5055
- byoTurn ? { byoProvider: byoTurn.provider } : {},
5056
- ),
5057
5195
  );
5058
5196
  if (
5059
5197
  req.client === 'matrix' &&
@@ -5283,8 +5421,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5283
5421
  continuation: run.continuation,
5284
5422
  logger: console,
5285
5423
  });
5286
- // The turn's budget (turn-budget.ts): model calls are charged by the
5287
- // metered adapter every model of this turn comes from, tool calls by
5424
+ // The turn's budget (turn-budget.ts): model tokens are accounted (for
5425
+ // the usage line below, never limited) by the metered adapter every
5426
+ // model of this turn comes from, tool calls are charged by
5288
5427
  // the execution middleware below, and the deadline aborts the run
5289
5428
  // with the limit as the reason (the stream reports it as a terminal
5290
5429
  // error, not as a user abort).
@@ -5294,6 +5433,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5294
5433
  budget,
5295
5434
  outputReserveTokens: contextBudget.outputReserveTokens,
5296
5435
  signal: abortController.signal,
5436
+ logger: console,
5297
5437
  });
5298
5438
  const deadline = setTimeout(() => {
5299
5439
  if (abortController.signal.aborted) return;
@@ -5310,6 +5450,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5310
5450
  }, turnLimits.durationMs);
5311
5451
  turnDisposables.add(async () => {
5312
5452
  clearTimeout(deadline);
5453
+ // Heartbeats of model calls that never reported an end.
5454
+ meteredLlm.dispose();
5313
5455
  const usage = budget.snapshot();
5314
5456
  console.log(
5315
5457
  `[harness] turn ${req.requestId} usage: ~${usage.tokens} tokens (${usage.reportedTokens} reported over ${usage.modelCalls} model calls), ${usage.toolAttempts} tool attempts, ${usage.elapsedMs} ms`,
@@ -5379,6 +5521,18 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5379
5521
  contextBudget,
5380
5522
  turnBudget: budget,
5381
5523
  delivery,
5524
+ // Observe-only domain context, on the object's shared resolver (the
5525
+ // one `refresh_domain_context` invalidates); a resumed run reads the
5526
+ // anchors it pinned.
5527
+ ...(opts.domainContext
5528
+ ? {
5529
+ domainContext: opts.domainContext,
5530
+ domainResolver: this.domainResolver,
5531
+ onDomainProvenance: (provenance: DomainContextProvenance[]) =>
5532
+ void this.reportDomainContext(req, provenance),
5533
+ ...(run.domainPins ? { domainPins: run.domainPins } : {}),
5534
+ }
5535
+ : {}),
5382
5536
  ambient: {
5383
5537
  ...ambient,
5384
5538
  llm: meteredLlm,
@@ -5547,9 +5701,32 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
5547
5701
  /** The budget's usage so far (JSON), stored with the run's end. */
5548
5702
  usage: (): string => JSON.stringify(budget.snapshot()),
5549
5703
  delivery,
5704
+ domainContext: built.domainContext,
5550
5705
  };
5551
5706
  }
5552
5707
 
5708
+ /**
5709
+ * A turn's domain-context provenance: a `router_update` frame for the
5710
+ * client and the session's sockets, and a `domain_context_runs` row.
5711
+ * Diagnostics only — nothing here can fail the turn.
5712
+ */
5713
+ private async reportDomainContext(
5714
+ req: TurnRequest,
5715
+ provenance: DomainContextProvenance[],
5716
+ ): Promise<void> {
5717
+ const update: DomainContextRouterUpdate = {
5718
+ sessionId: req.sessionId,
5719
+ requestId: req.requestId,
5720
+ domainContext: provenance,
5721
+ };
5722
+ this.events.emit(EVENT_NAMES.router, { ...update });
5723
+ await this.domainContextStore?.record(
5724
+ req.sessionId,
5725
+ req.requestId,
5726
+ provenance,
5727
+ );
5728
+ }
5729
+
5553
5730
  /**
5554
5731
  * Node parity (`MessagesService.sendMessage`): every HTTP/SSE turn is
5555
5732
  * replayed into the user's oracle room as a thread under the session's
package/src/index.ts CHANGED
@@ -39,6 +39,7 @@ import { artifactSweepIntervalMs } from './artifacts/config';
39
39
  import { sweepExpiredArtifacts } from './artifacts/sweep';
40
40
  import { feedbackConfigFromEnv } from './feedback/config';
41
41
  import { LinearFeedbackSink } from './feedback/linear-sink';
42
+ import type { DomainContextOptions } from './core/domain-context';
42
43
 
43
44
  export * from './plugin-api';
44
45
  export * from './plugins';
@@ -56,6 +57,14 @@ export {
56
57
  createUserOracleDO,
57
58
  type OracleWorkerHooks,
58
59
  } from './do/user-oracle-do';
60
+ export type {
61
+ DocumentRequest,
62
+ DomainCapsuleInspection,
63
+ DomainContextOptions,
64
+ DomainContextPins,
65
+ DomainContextProvenance,
66
+ DomainDocumentRead,
67
+ } from './core/domain-context';
59
68
  export { createShell, type ListModelsOptions } from './shell/app';
60
69
  export { authenticate } from './shell/auth';
61
70
  export {
@@ -265,6 +274,12 @@ export interface CreateOracleWorkerOptions {
265
274
  * user object's ambient services.
266
275
  */
267
276
  hooks?: OracleWorkerHooks;
277
+ /**
278
+ * Observe-only domain context: the oracle's and the selected subject's
279
+ * anchored domain documents, verified by CID and handed to the model as
280
+ * retrieved data. Default off. See docs/configuration.md.
281
+ */
282
+ domainContext?: DomainContextOptions;
268
283
  }
269
284
 
270
285
  export interface OracleWorker {
@@ -341,6 +356,7 @@ export function createOracleWorker(
341
356
  const UserOracleDO = createUserOracleDO({
342
357
  core: coreFor,
343
358
  ...(opts.hooks ? { hooks: opts.hooks } : {}),
359
+ ...(opts.domainContext ? { domainContext: opts.domainContext } : {}),
344
360
  });
345
361
 
346
362
  return {
@@ -28,6 +28,7 @@ import {
28
28
  ByoModelFallbackChatModel,
29
29
  ByoModelFallbackState,
30
30
  } from './byo-model-fallback';
31
+ import type { StreamLivenessSettings } from './stream-liveness';
31
32
 
32
33
  export interface ByoTurnResolution {
33
34
  credential: ByoCredential;
@@ -35,6 +36,8 @@ export interface ByoTurnResolution {
35
36
  mainModelId: string;
36
37
  /** ChatGPT lane only: backend/proxy the requests go to. */
37
38
  chatGptBackend?: ChatGptBackendConfig;
39
+ /** The BYO lane's liveness budgets (`streamLivenessFromEnv(env, 'byo')`). */
40
+ streamLiveness?: StreamLivenessSettings;
38
41
  }
39
42
 
40
43
  export function createByoLlmAdapter(
@@ -68,6 +71,9 @@ export function createByoLlmAdapter(
68
71
  modelId,
69
72
  role,
70
73
  chatGptBackend: turn.chatGptBackend,
74
+ ...(turn.streamLiveness
75
+ ? { streamLiveness: turn.streamLiveness }
76
+ : {}),
71
77
  params: rest,
72
78
  logger,
73
79
  }),