@ixo/oracle-runtime-workers 0.16.0 → 0.17.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.
- package/README.md +3 -0
- package/package.json +5 -3
- 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 +663 -0
- package/src/core/domain-context/transport.ts +382 -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 +111 -42
- 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/do/user-oracle-do.ts
CHANGED
|
@@ -69,10 +69,10 @@ import {
|
|
|
69
69
|
langsmithEnvFromWorkerEnv,
|
|
70
70
|
llmEnvFromWorkerEnv,
|
|
71
71
|
resolveLangsmithTracing,
|
|
72
|
+
streamLivenessFromEnv,
|
|
72
73
|
type LangsmithTracingDecision,
|
|
73
74
|
type OpenRouterLlmAdapter,
|
|
74
75
|
DEFAULT_MODEL_ID,
|
|
75
|
-
OPENROUTER_MODEL_MAP,
|
|
76
76
|
} from '../core/llm';
|
|
77
77
|
import { renderTurnTimeNote } from '../core/prompt-composer';
|
|
78
78
|
import { TURN_TIME_NOTE_KWARG, withTurnTimeNote } from '../core/turn-time-note';
|
|
@@ -97,7 +97,7 @@ import type { BaseChatModel } from '@langchain/core/language_models/chat_models'
|
|
|
97
97
|
import type { AmbientServices, LlmAdapter } from '../core/runtime-context';
|
|
98
98
|
import { chatGptBackendFromEnv } from '../llm/byo-client';
|
|
99
99
|
import { createByoLlmAdapter } from '../llm/byo-adapter';
|
|
100
|
-
import {
|
|
100
|
+
import { isByoModelId } from '../llm/byo-catalog';
|
|
101
101
|
import { handleByoRequest } from '../llm/byo-routes';
|
|
102
102
|
import {
|
|
103
103
|
WorkersByoService,
|
|
@@ -192,6 +192,7 @@ import {
|
|
|
192
192
|
type TurnIdentity,
|
|
193
193
|
type TurnRequest,
|
|
194
194
|
type TurnResult,
|
|
195
|
+
type DomainContextRouterUpdate,
|
|
195
196
|
type MemorySchemaDebug,
|
|
196
197
|
TURN_INTERRUPTED_MARKER,
|
|
197
198
|
type JsonString,
|
|
@@ -275,6 +276,14 @@ import {
|
|
|
275
276
|
} from '../core/middlewares/context-guard';
|
|
276
277
|
import { fetchOpenRouterContextLengths } from '../core/openrouter-pricing';
|
|
277
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';
|
|
278
287
|
import { artifactStorageConfig } from '../artifacts/config';
|
|
279
288
|
import {
|
|
280
289
|
ArtifactStore,
|
|
@@ -333,6 +342,8 @@ interface TurnBuildRun {
|
|
|
333
342
|
resumed: boolean;
|
|
334
343
|
/** The reply text the user already received (resumed attempts). */
|
|
335
344
|
continuation: string | null;
|
|
345
|
+
/** Domain anchors the run started with (resumed attempts keep them). */
|
|
346
|
+
domainPins?: DomainContextPins;
|
|
336
347
|
}
|
|
337
348
|
|
|
338
349
|
/**
|
|
@@ -536,6 +547,11 @@ export interface OracleWorkerHooks {
|
|
|
536
547
|
export interface UserOracleDOOptions {
|
|
537
548
|
core: (env: OracleWorkerEnv) => RuntimeCore;
|
|
538
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;
|
|
539
555
|
}
|
|
540
556
|
|
|
541
557
|
interface StoredDelegation {
|
|
@@ -619,7 +635,11 @@ function lastAiText(messages: BaseMessage[]): string {
|
|
|
619
635
|
}
|
|
620
636
|
|
|
621
637
|
/** Id of the final assistant message (what `POST /messages` reports as `message.id`). */
|
|
622
|
-
/**
|
|
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
|
+
*/
|
|
623
643
|
function parseUsage(raw: string | null): TurnUsage | null {
|
|
624
644
|
if (!raw) return null;
|
|
625
645
|
try {
|
|
@@ -642,7 +662,6 @@ function parseUsage(raw: string | null): TurnUsage | null {
|
|
|
642
662
|
toolAttempts: num('toolAttempts'),
|
|
643
663
|
elapsedMs: num('elapsedMs'),
|
|
644
664
|
limits: {
|
|
645
|
-
tokens: limit('tokens'),
|
|
646
665
|
tools: limit('tools'),
|
|
647
666
|
durationMs: limit('durationMs'),
|
|
648
667
|
},
|
|
@@ -653,7 +672,6 @@ function parseUsage(raw: string | null): TurnUsage | null {
|
|
|
653
672
|
usage.modelCalls === undefined ||
|
|
654
673
|
usage.toolAttempts === undefined ||
|
|
655
674
|
usage.elapsedMs === undefined ||
|
|
656
|
-
usage.limits.tokens === undefined ||
|
|
657
675
|
usage.limits.tools === undefined ||
|
|
658
676
|
usage.limits.durationMs === undefined
|
|
659
677
|
)
|
|
@@ -665,7 +683,6 @@ function parseUsage(raw: string | null): TurnUsage | null {
|
|
|
665
683
|
toolAttempts: usage.toolAttempts,
|
|
666
684
|
elapsedMs: usage.elapsedMs,
|
|
667
685
|
limits: {
|
|
668
|
-
tokens: usage.limits.tokens,
|
|
669
686
|
tools: usage.limits.tools,
|
|
670
687
|
durationMs: usage.limits.durationMs,
|
|
671
688
|
},
|
|
@@ -741,6 +758,15 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
741
758
|
* bounded lanes. Lives as long as the object.
|
|
742
759
|
*/
|
|
743
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
|
+
logger: console,
|
|
767
|
+
});
|
|
768
|
+
/** Per-turn domain-context provenance (`domain_context_runs`). */
|
|
769
|
+
private domainContextStore: DomainContextStore | null = null;
|
|
744
770
|
private runs: RunCoordinator | null = null;
|
|
745
771
|
private readonly runConfig: RunDurabilityConfig = runDurabilityConfig(
|
|
746
772
|
this.env,
|
|
@@ -1641,6 +1667,10 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1641
1667
|
});
|
|
1642
1668
|
await this.resultStore.setup();
|
|
1643
1669
|
this.attachmentTextCache = new AttachmentTextCacheStore(liveDb, console);
|
|
1670
|
+
// Constructing it touches no SQL. Only an observe turn records (and so
|
|
1671
|
+
// creates `domain_context_runs`); a session delete never creates it but
|
|
1672
|
+
// still removes rows an earlier observe deployment left behind.
|
|
1673
|
+
this.domainContextStore = new DomainContextStore(liveDb, console);
|
|
1644
1674
|
// The store exists whenever the bucket is bound, so artefacts made
|
|
1645
1675
|
// earlier stay readable, revocable and deleted with their session after
|
|
1646
1676
|
// the public origin is removed; only new ones need it.
|
|
@@ -1894,6 +1924,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
1894
1924
|
this.resultStore = null;
|
|
1895
1925
|
this.artifacts = null;
|
|
1896
1926
|
this.attachmentTextCache = null;
|
|
1927
|
+
this.domainContextStore = null;
|
|
1897
1928
|
// Its store is bound to the database just dropped: a caller that sees
|
|
1898
1929
|
// no scheduler boots again instead of reading a closed connection.
|
|
1899
1930
|
this.taskScheduler = null;
|
|
@@ -2760,6 +2791,13 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
2760
2791
|
`[user-do] could not delete the artefacts of ${sessionId}; their links expire on schedule: ${err instanceof Error ? err.message : String(err)}`,
|
|
2761
2792
|
);
|
|
2762
2793
|
});
|
|
2794
|
+
await this.domainContextStore
|
|
2795
|
+
?.deleteForSession(sessionId)
|
|
2796
|
+
.catch((err: unknown) => {
|
|
2797
|
+
console.warn(
|
|
2798
|
+
`[domain-context] could not delete the provenance of ${sessionId}: ${err instanceof Error ? err.message : String(err)}`,
|
|
2799
|
+
);
|
|
2800
|
+
});
|
|
2763
2801
|
await this.ctx.storage.delete(contextStatsKey(sessionId));
|
|
2764
2802
|
await this.feedbackMarkers!.forgetSession(sessionId);
|
|
2765
2803
|
this.markDirty();
|
|
@@ -4283,6 +4321,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4283
4321
|
toolOutputCapChars,
|
|
4284
4322
|
delivery,
|
|
4285
4323
|
usage,
|
|
4324
|
+
domainContext,
|
|
4286
4325
|
} = await this.prepareTurn(
|
|
4287
4326
|
req,
|
|
4288
4327
|
{
|
|
@@ -4298,9 +4337,28 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4298
4337
|
abortController: live.abort,
|
|
4299
4338
|
resumed: !freshInput,
|
|
4300
4339
|
continuation: freshInput ? null : live.continuation,
|
|
4340
|
+
...(stored.domainPins ? { domainPins: stored.domainPins } : {}),
|
|
4301
4341
|
},
|
|
4302
4342
|
);
|
|
4303
4343
|
const sessionId = req.sessionId;
|
|
4344
|
+
// The anchors this run read are pinned on its row, so a resumed
|
|
4345
|
+
// attempt reads the same document revisions.
|
|
4346
|
+
if (
|
|
4347
|
+
domainContext &&
|
|
4348
|
+
JSON.stringify(domainContext.pins) !==
|
|
4349
|
+
JSON.stringify(stored.domainPins ?? {})
|
|
4350
|
+
) {
|
|
4351
|
+
stored.domainPins = domainContext.pins;
|
|
4352
|
+
const request = JSON.stringify(stored);
|
|
4353
|
+
try {
|
|
4354
|
+
await this.runStore!.update(live.runId, { request });
|
|
4355
|
+
live.record.request = request;
|
|
4356
|
+
} catch (error) {
|
|
4357
|
+
console.warn(
|
|
4358
|
+
`[domain-context] could not pin the anchors of run ${live.runId}: ${error instanceof Error ? error.message : String(error)}`,
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
}
|
|
4304
4362
|
const sink = {
|
|
4305
4363
|
emit: (eventName: string, payload: Record<string, unknown>) => {
|
|
4306
4364
|
if (live.buffer.isClosed) return;
|
|
@@ -4313,6 +4371,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4313
4371
|
// (the turn's deadline timer among them) run however the attempt ends.
|
|
4314
4372
|
try {
|
|
4315
4373
|
this.events.register(sessionId, sink);
|
|
4374
|
+
if (domainContext)
|
|
4375
|
+
await this.reportDomainContext(req, domainContext.provenance);
|
|
4316
4376
|
if (byoNotice)
|
|
4317
4377
|
sink.emit('error', {
|
|
4318
4378
|
...byoNotice,
|
|
@@ -4641,15 +4701,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
4641
4701
|
const window = await this.contextWindows.resolve(modelId, {
|
|
4642
4702
|
...(byoProvider ? { byoProvider } : {}),
|
|
4643
4703
|
});
|
|
4644
|
-
const budget = contextBudgetFor(
|
|
4645
|
-
window,
|
|
4646
|
-
contextKnobs(this.env, console),
|
|
4647
|
-
await this.contextWindows.resolve(
|
|
4648
|
-
llm && isProviderAdapter(llm)
|
|
4649
|
-
? llm.modelForRole('routing')
|
|
4650
|
-
: OPENROUTER_MODEL_MAP.routing,
|
|
4651
|
-
),
|
|
4652
|
-
);
|
|
4704
|
+
const budget = contextBudgetFor(window, contextKnobs(this.env, console));
|
|
4653
4705
|
const stats = (await this.resultStore?.stats()) ?? {
|
|
4654
4706
|
rows: 0,
|
|
4655
4707
|
sqliteBytes: 0,
|
|
@@ -5007,27 +5059,6 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5007
5059
|
};
|
|
5008
5060
|
}
|
|
5009
5061
|
|
|
5010
|
-
/**
|
|
5011
|
-
* The model that summarises the turn's history (the `routing` role): its
|
|
5012
|
-
* window bounds what the summariser is handed.
|
|
5013
|
-
*/
|
|
5014
|
-
private summaryModelId(
|
|
5015
|
-
platform: LlmAdapter,
|
|
5016
|
-
byoTurn: ByoTurnState | null,
|
|
5017
|
-
): string {
|
|
5018
|
-
if (byoTurn)
|
|
5019
|
-
return (
|
|
5020
|
-
byoModelForRole(
|
|
5021
|
-
byoTurn.credential.provider,
|
|
5022
|
-
'routing',
|
|
5023
|
-
byoTurn.mainModelId,
|
|
5024
|
-
) ?? OPENROUTER_MODEL_MAP.routing
|
|
5025
|
-
);
|
|
5026
|
-
return isProviderAdapter(platform)
|
|
5027
|
-
? platform.modelForRole('routing')
|
|
5028
|
-
: OPENROUTER_MODEL_MAP.routing;
|
|
5029
|
-
}
|
|
5030
|
-
|
|
5031
5062
|
/**
|
|
5032
5063
|
* Build one attempt's agent and input (`buildTurn`). A build that fails
|
|
5033
5064
|
* part-way leaves nothing behind: the cleanups it registered run (the
|
|
@@ -5139,6 +5170,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5139
5170
|
credential: byoTurn.credential,
|
|
5140
5171
|
mainModelId: byoTurn.mainModelId,
|
|
5141
5172
|
chatGptBackend: byoTurn.chatGptBackend,
|
|
5173
|
+
streamLiveness: streamLivenessFromEnv(core.validatedEnv, 'byo'),
|
|
5142
5174
|
},
|
|
5143
5175
|
console,
|
|
5144
5176
|
),
|
|
@@ -5157,13 +5189,11 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5157
5189
|
const windowResolution = await this.contextWindows.resolve(mainModelId, {
|
|
5158
5190
|
...(byoTurn ? { byoProvider: byoTurn.provider } : {}),
|
|
5159
5191
|
});
|
|
5192
|
+
// The summary is written by the main model too (the `summarizer`
|
|
5193
|
+
// role), so the main model's window is the only one that counts.
|
|
5160
5194
|
const contextBudget = contextBudgetFor(
|
|
5161
5195
|
windowResolution,
|
|
5162
5196
|
contextKnobs(this.env, console),
|
|
5163
|
-
await this.contextWindows.resolve(
|
|
5164
|
-
this.summaryModelId(baseAmbient.llm, byoTurn),
|
|
5165
|
-
byoTurn ? { byoProvider: byoTurn.provider } : {},
|
|
5166
|
-
),
|
|
5167
5197
|
);
|
|
5168
5198
|
if (
|
|
5169
5199
|
req.client === 'matrix' &&
|
|
@@ -5393,8 +5423,9 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5393
5423
|
continuation: run.continuation,
|
|
5394
5424
|
logger: console,
|
|
5395
5425
|
});
|
|
5396
|
-
// The turn's budget (turn-budget.ts): model
|
|
5397
|
-
//
|
|
5426
|
+
// The turn's budget (turn-budget.ts): model tokens are accounted (for
|
|
5427
|
+
// the usage line below, never limited) by the metered adapter every
|
|
5428
|
+
// model of this turn comes from, tool calls are charged by
|
|
5398
5429
|
// the execution middleware below, and the deadline aborts the run
|
|
5399
5430
|
// with the limit as the reason (the stream reports it as a terminal
|
|
5400
5431
|
// error, not as a user abort).
|
|
@@ -5404,6 +5435,7 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5404
5435
|
budget,
|
|
5405
5436
|
outputReserveTokens: contextBudget.outputReserveTokens,
|
|
5406
5437
|
signal: abortController.signal,
|
|
5438
|
+
logger: console,
|
|
5407
5439
|
});
|
|
5408
5440
|
const deadline = setTimeout(() => {
|
|
5409
5441
|
if (abortController.signal.aborted) return;
|
|
@@ -5420,6 +5452,8 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5420
5452
|
}, turnLimits.durationMs);
|
|
5421
5453
|
turnDisposables.add(async () => {
|
|
5422
5454
|
clearTimeout(deadline);
|
|
5455
|
+
// Heartbeats of model calls that never reported an end.
|
|
5456
|
+
meteredLlm.dispose();
|
|
5423
5457
|
const usage = budget.snapshot();
|
|
5424
5458
|
console.log(
|
|
5425
5459
|
`[harness] turn ${req.requestId} usage: ~${usage.tokens} tokens (${usage.reportedTokens} reported over ${usage.modelCalls} model calls), ${usage.toolAttempts} tool attempts, ${usage.elapsedMs} ms`,
|
|
@@ -5489,6 +5523,18 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5489
5523
|
contextBudget,
|
|
5490
5524
|
turnBudget: budget,
|
|
5491
5525
|
delivery,
|
|
5526
|
+
// Observe-only domain context, on the object's shared resolver (the
|
|
5527
|
+
// one `refresh_domain_context` invalidates); a resumed run reads the
|
|
5528
|
+
// anchors it pinned.
|
|
5529
|
+
...(opts.domainContext
|
|
5530
|
+
? {
|
|
5531
|
+
domainContext: opts.domainContext,
|
|
5532
|
+
domainResolver: this.domainResolver,
|
|
5533
|
+
onDomainProvenance: (provenance: DomainContextProvenance[]) =>
|
|
5534
|
+
void this.reportDomainContext(req, provenance),
|
|
5535
|
+
...(run.domainPins ? { domainPins: run.domainPins } : {}),
|
|
5536
|
+
}
|
|
5537
|
+
: {}),
|
|
5492
5538
|
ambient: {
|
|
5493
5539
|
...ambient,
|
|
5494
5540
|
llm: meteredLlm,
|
|
@@ -5657,7 +5703,30 @@ export function createUserOracleDO(opts: UserOracleDOOptions) {
|
|
|
5657
5703
|
/** The budget's usage so far (JSON), stored with the run's end. */
|
|
5658
5704
|
usage: (): string => JSON.stringify(budget.snapshot()),
|
|
5659
5705
|
delivery,
|
|
5706
|
+
domainContext: built.domainContext,
|
|
5707
|
+
};
|
|
5708
|
+
}
|
|
5709
|
+
|
|
5710
|
+
/**
|
|
5711
|
+
* A turn's domain-context provenance: a `router_update` frame for the
|
|
5712
|
+
* client and the session's sockets, and a `domain_context_runs` row.
|
|
5713
|
+
* Diagnostics only — nothing here can fail the turn.
|
|
5714
|
+
*/
|
|
5715
|
+
private async reportDomainContext(
|
|
5716
|
+
req: TurnRequest,
|
|
5717
|
+
provenance: DomainContextProvenance[],
|
|
5718
|
+
): Promise<void> {
|
|
5719
|
+
const update: DomainContextRouterUpdate = {
|
|
5720
|
+
sessionId: req.sessionId,
|
|
5721
|
+
requestId: req.requestId,
|
|
5722
|
+
domainContext: provenance,
|
|
5660
5723
|
};
|
|
5724
|
+
this.events.emit(EVENT_NAMES.router, { ...update });
|
|
5725
|
+
await this.domainContextStore?.record(
|
|
5726
|
+
req.sessionId,
|
|
5727
|
+
req.requestId,
|
|
5728
|
+
provenance,
|
|
5729
|
+
);
|
|
5661
5730
|
}
|
|
5662
5731
|
|
|
5663
5732
|
/**
|
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
|
}),
|
package/src/llm/byo-catalog.ts
CHANGED
|
@@ -382,6 +382,8 @@ export const BYO_DEFAULT_MODEL: Record<ByoProvider, string> = {
|
|
|
382
382
|
* A role absent here falls back to the platform provider (deliberately:
|
|
383
383
|
* `embedding` everywhere — none of these providers is wired for the runtime's
|
|
384
384
|
* embedding path — and `vision` on DeepSeek, whose chat models are text-only).
|
|
385
|
+
* `main` and `summarizer` are not listed: both are the turn's selected model
|
|
386
|
+
* (`byoModelForRole`), so the history is summarized on the user's provider.
|
|
385
387
|
*/
|
|
386
388
|
export const BYO_ROLE_MODELS: Record<
|
|
387
389
|
ByoProvider,
|
|
@@ -447,6 +449,7 @@ export const BYO_ROLE_MODELS: Record<
|
|
|
447
449
|
*/
|
|
448
450
|
const KNOWN_MODEL_ROLES: ReadonlySet<string> = new Set([
|
|
449
451
|
'main',
|
|
452
|
+
'summarizer',
|
|
450
453
|
'skills',
|
|
451
454
|
'subagent',
|
|
452
455
|
'vision',
|
|
@@ -460,16 +463,16 @@ const KNOWN_MODEL_ROLES: ReadonlySet<string> = new Set([
|
|
|
460
463
|
|
|
461
464
|
/**
|
|
462
465
|
* Resolve the provider-native model id for a role on a BYO turn.
|
|
463
|
-
* `main`
|
|
464
|
-
* table. `null` → the role is not served by this provider and
|
|
465
|
-
* must fall back to the platform adapter.
|
|
466
|
+
* `main` and `summarizer` use the turn's selected model; other roles use the
|
|
467
|
+
* translation table. `null` → the role is not served by this provider and
|
|
468
|
+
* the caller must fall back to the platform adapter.
|
|
466
469
|
*/
|
|
467
470
|
export function byoModelForRole(
|
|
468
471
|
provider: ByoProvider,
|
|
469
472
|
role: ProviderModelRole | string,
|
|
470
473
|
mainModelId: string,
|
|
471
474
|
): string | null {
|
|
472
|
-
if (role === 'main') return mainModelId;
|
|
475
|
+
if (role === 'main' || role === 'summarizer') return mainModelId;
|
|
473
476
|
const map = BYO_ROLE_MODELS[provider];
|
|
474
477
|
const direct = map[role as ProviderModelRole];
|
|
475
478
|
if (direct) return direct;
|
package/src/llm/byo-client.ts
CHANGED
|
@@ -17,8 +17,13 @@
|
|
|
17
17
|
import type { BaseChatModel } from '@langchain/core/language_models/chat_models';
|
|
18
18
|
import { ChatOpenAI } from '@langchain/openai';
|
|
19
19
|
import type { ChatOpenAIFields, Logger } from '../plugin-api/types';
|
|
20
|
-
import type
|
|
20
|
+
import { streamLivenessFromEnv, type ProviderModelRole } from '../core/llm';
|
|
21
21
|
import type { ByoCredential } from './byo-catalog';
|
|
22
|
+
import {
|
|
23
|
+
livenessFetchFor,
|
|
24
|
+
livenessModelFields,
|
|
25
|
+
type StreamLivenessSettings,
|
|
26
|
+
} from './stream-liveness';
|
|
22
27
|
|
|
23
28
|
/** OpenAI-compatible base URLs for the API-key providers that need one. */
|
|
24
29
|
export const DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
|
|
@@ -133,6 +138,11 @@ export interface CreateByoChatModelArgs {
|
|
|
133
138
|
logger?: Logger;
|
|
134
139
|
/** ChatGPT lane only: where requests go (default: the real backend). */
|
|
135
140
|
chatGptBackend?: ChatGptBackendConfig;
|
|
141
|
+
/**
|
|
142
|
+
* Liveness budgets for this model's calls (default: the BYO lane's
|
|
143
|
+
* defaults, see `streamLivenessFromEnv`).
|
|
144
|
+
*/
|
|
145
|
+
streamLiveness?: StreamLivenessSettings;
|
|
136
146
|
}
|
|
137
147
|
|
|
138
148
|
const NOOP: Logger = {
|
|
@@ -164,17 +174,35 @@ export function createByoChatModel(
|
|
|
164
174
|
// "Thinking..." for a minute before any error surfaces. Two attempts keep
|
|
165
175
|
// transient-blip resilience while failing fast enough to report.
|
|
166
176
|
const maxRetries = 2;
|
|
177
|
+
// Every call runs behind the stream liveness guard with the BYO lane's
|
|
178
|
+
// budgets: a direct provider may stay silent while a model reasons, so
|
|
179
|
+
// its idle budget is longer than the platform lane's.
|
|
180
|
+
const liveness = args.streamLiveness ?? streamLivenessFromEnv({}, 'byo');
|
|
181
|
+
const callerConfiguration = asRecord(params?.configuration);
|
|
182
|
+
const guardedFetch = (base?: typeof fetch): typeof fetch =>
|
|
183
|
+
livenessFetchFor({
|
|
184
|
+
settings: liveness,
|
|
185
|
+
label: `byo:${credential.provider} ${String(role)} ${modelId}`,
|
|
186
|
+
logger,
|
|
187
|
+
callerFetch: callerConfiguration.fetch,
|
|
188
|
+
...(base ? { base } : {}),
|
|
189
|
+
});
|
|
167
190
|
|
|
168
191
|
switch (credential.provider) {
|
|
169
192
|
case 'openai':
|
|
170
193
|
return new ChatOpenAI({
|
|
171
194
|
__includeRawResponse: true,
|
|
172
195
|
maxRetries,
|
|
196
|
+
...livenessModelFields(liveness),
|
|
173
197
|
...params,
|
|
174
198
|
model: modelId,
|
|
175
199
|
apiKey: credential.apiKey,
|
|
176
200
|
temperature: undefined,
|
|
177
201
|
topP: undefined,
|
|
202
|
+
configuration: {
|
|
203
|
+
...callerConfiguration,
|
|
204
|
+
fetch: guardedFetch(),
|
|
205
|
+
},
|
|
178
206
|
});
|
|
179
207
|
|
|
180
208
|
case 'deepseek':
|
|
@@ -182,12 +210,14 @@ export function createByoChatModel(
|
|
|
182
210
|
temperature,
|
|
183
211
|
__includeRawResponse: true,
|
|
184
212
|
maxRetries,
|
|
213
|
+
...livenessModelFields(liveness),
|
|
185
214
|
...params,
|
|
186
215
|
model: modelId,
|
|
187
216
|
apiKey: credential.apiKey,
|
|
188
217
|
configuration: {
|
|
189
218
|
baseURL: DEEPSEEK_BASE_URL,
|
|
190
|
-
...
|
|
219
|
+
...callerConfiguration,
|
|
220
|
+
fetch: guardedFetch(),
|
|
191
221
|
},
|
|
192
222
|
});
|
|
193
223
|
|
|
@@ -196,12 +226,14 @@ export function createByoChatModel(
|
|
|
196
226
|
temperature,
|
|
197
227
|
__includeRawResponse: true,
|
|
198
228
|
maxRetries,
|
|
229
|
+
...livenessModelFields(liveness),
|
|
199
230
|
...params,
|
|
200
231
|
model: modelId,
|
|
201
232
|
apiKey: credential.apiKey,
|
|
202
233
|
configuration: {
|
|
203
234
|
baseURL: GEMINI_OPENAI_COMPAT_BASE_URL,
|
|
204
|
-
...
|
|
235
|
+
...callerConfiguration,
|
|
236
|
+
fetch: guardedFetch(),
|
|
205
237
|
},
|
|
206
238
|
});
|
|
207
239
|
|
|
@@ -211,6 +243,7 @@ export function createByoChatModel(
|
|
|
211
243
|
return new ChatOpenAI({
|
|
212
244
|
__includeRawResponse: true,
|
|
213
245
|
maxRetries,
|
|
246
|
+
...livenessModelFields(liveness),
|
|
214
247
|
...params,
|
|
215
248
|
model: modelId,
|
|
216
249
|
apiKey: credential.apiKey,
|
|
@@ -218,7 +251,8 @@ export function createByoChatModel(
|
|
|
218
251
|
topP: undefined,
|
|
219
252
|
configuration: {
|
|
220
253
|
baseURL: ANTHROPIC_OPENAI_COMPAT_BASE_URL,
|
|
221
|
-
...
|
|
254
|
+
...callerConfiguration,
|
|
255
|
+
fetch: guardedFetch(),
|
|
222
256
|
},
|
|
223
257
|
});
|
|
224
258
|
|
|
@@ -232,7 +266,7 @@ export function createByoChatModel(
|
|
|
232
266
|
// distinct headers upstream — send both. The backend rejects sampling
|
|
233
267
|
// params (`temperature`/`top_p`) the way the standard API does for
|
|
234
268
|
// reasoning models, and reports request errors with an EMPTY body —
|
|
235
|
-
// hence the diagnostic fetch.
|
|
269
|
+
// hence the diagnostic fetch, inside the liveness guard.
|
|
236
270
|
const sessionId = crypto.randomUUID();
|
|
237
271
|
return new ChatOpenAI({
|
|
238
272
|
__includeRawResponse: true,
|
|
@@ -249,6 +283,7 @@ export function createByoChatModel(
|
|
|
249
283
|
// Effort + human-readable reasoning summaries, exactly the pair the
|
|
250
284
|
// Codex clients send — summaries feed the portal's thinking stream.
|
|
251
285
|
reasoning: { effort: 'medium', summary: 'auto' },
|
|
286
|
+
...livenessModelFields(liveness),
|
|
252
287
|
...params,
|
|
253
288
|
model: modelId,
|
|
254
289
|
apiKey: credential.oauth.accessToken,
|
|
@@ -257,8 +292,8 @@ export function createByoChatModel(
|
|
|
257
292
|
topP: undefined,
|
|
258
293
|
configuration: {
|
|
259
294
|
baseURL: backend.baseUrl,
|
|
260
|
-
|
|
261
|
-
|
|
295
|
+
...callerConfiguration,
|
|
296
|
+
fetch: guardedFetch(chatGptDiagnosticFetch(logger)),
|
|
262
297
|
defaultHeaders: {
|
|
263
298
|
...chatGptBackendHeaders(backend),
|
|
264
299
|
'ChatGPT-Account-ID': credential.oauth.accountId,
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
isByoProvider,
|
|
15
15
|
type ByoProvider,
|
|
16
16
|
} from './byo-catalog';
|
|
17
|
+
import { findProviderStall, type ProviderStallError } from './stream-liveness';
|
|
17
18
|
|
|
18
19
|
/** `kind` value for the BYO degradation notice. */
|
|
19
20
|
export const BYO_FALLBACK_KIND = 'byo_fallback';
|
|
@@ -283,12 +284,27 @@ export interface ClassifyLlmErrorContext {
|
|
|
283
284
|
byoProvider?: ByoProvider | string | null;
|
|
284
285
|
}
|
|
285
286
|
|
|
287
|
+
/**
|
|
288
|
+
* What the user is told about a stalled call (`ProviderStallError`): a stall
|
|
289
|
+
* mid-reply reads differently from a model that never started.
|
|
290
|
+
*/
|
|
291
|
+
function stallMessage(stall: ProviderStallError): string {
|
|
292
|
+
return stall.phase === 'stream'
|
|
293
|
+
? 'The model stopped responding mid-reply. Please try again.'
|
|
294
|
+
: 'The model did not start responding. Please try again.';
|
|
295
|
+
}
|
|
296
|
+
|
|
286
297
|
export function classifyLlmError(
|
|
287
298
|
error: unknown,
|
|
288
299
|
ctx?: ClassifyLlmErrorContext,
|
|
289
300
|
): ClassifiedLlmError {
|
|
290
|
-
|
|
291
|
-
|
|
301
|
+
// A stall is found through the wrappers (the OpenAI SDK's
|
|
302
|
+
// `APIConnectionError`, LangChain's middleware errors) by its `cause`.
|
|
303
|
+
const stall = findProviderStall(error);
|
|
304
|
+
const parts: ErrorParts = stall
|
|
305
|
+
? { text: stall.message }
|
|
306
|
+
: extractErrorParts(error);
|
|
307
|
+
const kind: LlmErrorKind = stall ? 'timeout' : detectKind(parts);
|
|
292
308
|
const provider =
|
|
293
309
|
typeof ctx?.byoProvider === 'string' &&
|
|
294
310
|
isByoProvider(ctx.byoProvider) &&
|
|
@@ -305,7 +321,9 @@ export function classifyLlmError(
|
|
|
305
321
|
...(providerLabel && { providerLabel }),
|
|
306
322
|
...(parts.status !== undefined && { status: parts.status }),
|
|
307
323
|
retryable: RETRYABLE_KINDS.has(kind),
|
|
308
|
-
message:
|
|
324
|
+
message: stall
|
|
325
|
+
? stallMessage(stall)
|
|
326
|
+
: fallbackMessage(kind, provider, providerLabel),
|
|
309
327
|
detail: parts.text,
|
|
310
328
|
};
|
|
311
329
|
}
|