@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
@@ -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` uses the turn's selected model; other roles use the translation
464
- * table. `null` → the role is not served by this provider and the caller
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;
@@ -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 { ProviderModelRole } from '../core/llm';
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
- ...asRecord(params?.configuration),
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
- ...asRecord(params?.configuration),
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
- ...asRecord(params?.configuration),
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
- fetch: chatGptDiagnosticFetch(logger),
261
- ...asRecord(params?.configuration),
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
- const parts = extractErrorParts(error);
291
- const kind = detectKind(parts);
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: fallbackMessage(kind, provider, providerLabel),
324
+ message: stall
325
+ ? stallMessage(stall)
326
+ : fallbackMessage(kind, provider, providerLabel),
309
327
  detail: parts.text,
310
328
  };
311
329
  }