@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.
- package/README.md +4 -1
- package/package.json +4 -2
- package/src/channels/auth.ts +54 -11
- package/src/channels/contract.ts +122 -13
- package/src/channels/turns.ts +12 -2
- package/src/core/budgeted-llm.ts +115 -6
- package/src/core/context-budget.ts +24 -26
- package/src/core/context-window.ts +39 -12
- package/src/core/domain-context/document.ts +100 -0
- package/src/core/domain-context/findings.ts +25 -0
- package/src/core/domain-context/index.ts +28 -0
- package/src/core/domain-context/integrity.ts +60 -0
- package/src/core/domain-context/lru.ts +63 -0
- package/src/core/domain-context/resolver.ts +651 -0
- package/src/core/domain-context/transport.ts +340 -0
- package/src/core/domain-context/turn-context.ts +443 -0
- package/src/core/domain-context/types.ts +119 -0
- package/src/core/domain-context/validator.ts +41 -0
- package/src/core/env.ts +60 -6
- package/src/core/index.ts +2 -0
- package/src/core/llm.ts +225 -4
- package/src/core/main-agent-types.ts +28 -0
- package/src/core/main-agent.ts +61 -4
- package/src/core/middlewares/tool-execution.ts +19 -2
- package/src/core/registries.ts +14 -3
- package/src/core/state.ts +2 -1
- package/src/core/sub-agent-fallback.ts +14 -2
- package/src/core/turn-budget.ts +19 -22
- package/src/do/contracts.ts +23 -2
- package/src/do/domain-context-store.ts +123 -0
- package/src/do/run-request.ts +6 -0
- package/src/do/sse-stream.ts +1 -1
- package/src/do/transcript.ts +9 -0
- package/src/do/turn-body.ts +2 -1
- package/src/do/turn-metadata.ts +13 -6
- package/src/do/user-oracle-do.ts +237 -60
- package/src/index.ts +16 -0
- package/src/llm/byo-adapter.ts +6 -0
- package/src/llm/byo-catalog.ts +7 -4
- package/src/llm/byo-client.ts +42 -7
- package/src/llm/provider-error.ts +21 -3
- package/src/llm/stream-liveness.ts +498 -0
- package/src/plugins/firecrawl/firecrawl-tools.ts +6 -0
- package/src/shell/app.ts +41 -8
package/src/do/user-oracle-do.ts
CHANGED
|
@@ -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 {
|
|
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 {
|
|
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
|
-
/**
|
|
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,
|
|
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.
|
|
981
|
-
|
|
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(
|
|
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(
|
|
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)
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
|
5287
|
-
//
|
|
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 {
|
package/src/llm/byo-adapter.ts
CHANGED
|
@@ -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
|
}),
|