theorum 0.1.7 → 0.1.10

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 (37) hide show
  1. package/esm/src/cli/commands/bench.d.ts +20 -0
  2. package/esm/src/cli/commands/bench.js +454 -0
  3. package/esm/src/cli/commands/fuzz-guardrails.d.ts +9 -0
  4. package/esm/src/cli/commands/fuzz-guardrails.js +440 -0
  5. package/esm/src/cli/index.js +20 -1
  6. package/esm/src/guardrails/injection.d.ts +0 -1
  7. package/esm/src/guardrails/injection.js +105 -12
  8. package/esm/src/guardrails/normalize.d.ts +11 -0
  9. package/esm/src/guardrails/normalize.js +110 -0
  10. package/esm/src/guardrails/sanitize.d.ts +10 -2
  11. package/esm/src/guardrails/sanitize.js +27 -1
  12. package/esm/src/guardrails/sensitive.js +5 -3
  13. package/esm/src/kernel/engine/boundary.js +10 -10
  14. package/esm/src/kernel/engine/runner/mod.js +4 -1
  15. package/esm/src/kernel/engine/runner/tools.js +11 -5
  16. package/esm/src/kernel/types.d.ts +1 -1
  17. package/esm/src/observability/trace-record.d.ts +1 -0
  18. package/esm/src/observability/trace-record.js +4 -4
  19. package/esm/src/providers/create-provider.js +2 -0
  20. package/esm/src/providers/expose-for-tests.d.ts +1 -0
  21. package/esm/src/providers/expose-for-tests.js +21 -0
  22. package/esm/src/providers/gemini-tape.js +9 -0
  23. package/esm/src/providers/google-tap.js +2 -0
  24. package/esm/src/providers/interactions.js +16 -0
  25. package/esm/src/providers/keys.js +11 -0
  26. package/esm/src/providers/openrouter-payload.d.ts +9 -2
  27. package/esm/src/providers/openrouter-payload.js +23 -7
  28. package/esm/src/providers/openrouter.js +101 -140
  29. package/esm/src/providers/pcm.js +2 -0
  30. package/esm/src/providers/provider.js +13 -0
  31. package/esm/src/providers/speech.js +10 -0
  32. package/esm/src/providers/sse.js +2 -0
  33. package/esm/src/streaming/mod.d.ts +7 -0
  34. package/esm/src/streaming/mod.js +7 -0
  35. package/esm/src/streaming/readStreamingJsonStringField.d.ts +6 -0
  36. package/esm/src/streaming/readStreamingJsonStringField.js +55 -0
  37. package/package.json +6 -3
@@ -7,6 +7,7 @@
7
7
  * @module
8
8
  */
9
9
  import { isAbortError, TheorumError, UPSTREAM_FAILED } from '../guardrails/error.js';
10
+ import { exposeForTests } from './expose-for-tests.js';
10
11
  const ATTEMPTS = 3;
11
12
  const LAST_ATTEMPT = ATTEMPTS - 1;
12
13
  const BACKOFF_FIRST_MS = 1000;
@@ -137,3 +138,13 @@ async function fetchGemini(url, init, bucket, transport) {
137
138
  return last;
138
139
  }
139
140
  export { fetchGemini, withGeminiKey };
141
+ exposeForTests('keys', {
142
+ waitDefault,
143
+ isQuota,
144
+ isTransientHttp,
145
+ isTransientThrown,
146
+ requireKey,
147
+ backoffMs,
148
+ canOverflow,
149
+ withApiKey,
150
+ });
@@ -28,5 +28,12 @@ interface OpenRouterWireIds {
28
28
  declare function resolveOpenRouterModel(modelId: ModelId | string, customMap?: Record<string, string>, wire?: OpenRouterWireIds): string;
29
29
  /** Convert a provider-neutral request into an OpenRouter chat completion payload. */
30
30
  declare function toOpenRouterPayload(req: ProviderCompleteRequest, config: OpenRouterConfig): Record<string, unknown>;
31
- export type { OpenRouterConfig, OpenRouterWireIds };
32
- export { resolveOpenRouterModel, toOpenRouterPayload };
31
+ interface ResolvedPlugins {
32
+ plugins: Array<{
33
+ id: string;
34
+ }>;
35
+ webSearch: boolean;
36
+ }
37
+ declare function resolveOpenRouterPlugins(builtins: readonly string[]): ResolvedPlugins;
38
+ export type { OpenRouterConfig, OpenRouterWireIds, ResolvedPlugins };
39
+ export { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload };
@@ -167,13 +167,29 @@ function toOpenRouterPayload(req, config) {
167
167
  if (tools.length > 0) {
168
168
  payload.tools = tools;
169
169
  }
170
- const plugins = req.builtins
171
- .map((id) => getTool(id)?.openRouterPlugin)
172
- .filter((id) => Boolean(id))
173
- .map((id) => ({ id }));
174
- if (plugins.length > 0) {
175
- payload.plugins = plugins;
170
+ const resolved = resolveOpenRouterPlugins(req.builtins);
171
+ if (resolved.webSearch) {
172
+ payload.web_search_options = {};
173
+ }
174
+ if (resolved.plugins.length > 0) {
175
+ payload.plugins = resolved.plugins;
176
176
  }
177
177
  return payload;
178
178
  }
179
- export { resolveOpenRouterModel, toOpenRouterPayload };
179
+ function resolveOpenRouterPlugins(builtins) {
180
+ let webSearch = false;
181
+ const plugins = [];
182
+ for (const id of builtins) {
183
+ const pluginId = getTool(id)?.openRouterPlugin;
184
+ if (!pluginId)
185
+ continue;
186
+ if (pluginId === 'web') {
187
+ webSearch = true;
188
+ }
189
+ else {
190
+ plugins.push({ id: pluginId });
191
+ }
192
+ }
193
+ return { plugins, webSearch };
194
+ }
195
+ export { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload };
@@ -11,9 +11,9 @@ import { createOpenRouter } from '@openrouter/ai-sdk-provider';
11
11
  import { jsonSchema, streamText, tool, } from 'ai';
12
12
  import { isAbortError, toErrorEvent } from '../guardrails/error.js';
13
13
  import { tryStructured } from '../kernel/engine/delta.js';
14
- import { getTool } from '../kernel/registry/catalog.js';
15
- import { resolveOpenRouterModel, toOpenRouterPayload, } from './openrouter-payload.js';
16
- import { takeSsePayloads } from './sse.js';
14
+ import { getStructured } from '../kernel/registry/schemas.js';
15
+ import { resolveOpenRouterModel, resolveOpenRouterPlugins, toOpenRouterPayload, } from './openrouter-payload.js';
16
+ import { exposeForTests } from './expose-for-tests.js';
17
17
  function trimApiKey(explicitKey) {
18
18
  if (explicitKey?.trim()) {
19
19
  return explicitKey.trim();
@@ -26,7 +26,6 @@ function createAccumulator() {
26
26
  evidenceSeen: false,
27
27
  emittedTokens: false,
28
28
  errored: false,
29
- toolInputs: new Map(),
30
29
  };
31
30
  }
32
31
  function mediaPart(part) {
@@ -61,9 +60,6 @@ function parseToolInput(raw) {
61
60
  function stringDefault(value, fallback) {
62
61
  return value === undefined ? fallback : value;
63
62
  }
64
- function optionalSingle(value) {
65
- return value === undefined ? undefined : [value];
66
- }
67
63
  function fallbackToolCallId(name) {
68
64
  return `call_${stringDefault(name, 'tool')}`;
69
65
  }
@@ -111,34 +107,21 @@ function contentHistoryMessage(msg) {
111
107
  const content = contentFromOptionalParts(msg.parts, msg.content);
112
108
  return { role: msg.role, content };
113
109
  }
114
- function sourceValue(part, key) {
115
- const source = part;
116
- const value = source[key];
117
- return typeof value === 'string' ? value : undefined;
118
- }
119
- function sourceUrl(part) {
120
- return sourceValue(part, 'url');
121
- }
122
- function sourceTitle(part) {
123
- return sourceValue(part, 'title') ?? sourceUrl(part);
124
- }
125
- function sourceList(title, url) {
126
- if (title === undefined || url === undefined) {
127
- return undefined;
128
- }
129
- return [{ title, uri: url, type: 'web' }];
130
- }
131
110
  function sourceEvent(part) {
132
- const raw = part;
133
- const url = sourceUrl(part);
134
- const title = sourceTitle(part);
111
+ if (part.sourceType !== 'url') {
112
+ return {
113
+ type: 'evidence',
114
+ evidence: { provider: 'openrouter', raw: part },
115
+ };
116
+ }
117
+ const title = part.title ?? part.url;
135
118
  return {
136
119
  type: 'evidence',
137
120
  evidence: {
138
121
  provider: 'openrouter',
139
- raw,
140
- citations: optionalSingle(url),
141
- sources: sourceList(title, url),
122
+ raw: part,
123
+ citations: [part.url],
124
+ sources: [{ title, uri: part.url, type: 'web' }],
142
125
  },
143
126
  };
144
127
  }
@@ -171,19 +154,17 @@ function buildTools(dynamicTools) {
171
154
  }
172
155
  return tools;
173
156
  }
174
- function openRouterPlugins(req) {
175
- const plugins = req.builtins
176
- .map((id) => getTool(id)?.openRouterPlugin)
177
- .filter((id) => Boolean(id))
178
- .map((id) => ({ id }));
179
- return plugins.length > 0 ? plugins : undefined;
180
- }
181
157
  function openRouterSettings(req) {
182
- const plugins = openRouterPlugins(req);
183
- if (!plugins) {
158
+ const { plugins, webSearch } = resolveOpenRouterPlugins(req.builtins);
159
+ if (plugins.length === 0 && !webSearch) {
184
160
  return undefined;
185
161
  }
186
- return { plugins };
162
+ const settings = {};
163
+ if (plugins.length > 0)
164
+ settings.plugins = plugins;
165
+ if (webSearch)
166
+ settings.web_search_options = {};
167
+ return settings;
187
168
  }
188
169
  function tokensFromUsage(usage) {
189
170
  const input = usage.inputTokens ?? 0;
@@ -269,17 +250,6 @@ function toolArguments(input) {
269
250
  }
270
251
  return { value: input };
271
252
  }
272
- function isEmptyRecord(input) {
273
- return Boolean(input) && Object.keys(input ?? {}).length === 0;
274
- }
275
- function toolCallArguments(part, acc) {
276
- const fromInput = toolArguments(part.input);
277
- const rawInput = acc.toolInputs.get(part.toolCallId);
278
- if ((!fromInput || isEmptyRecord(fromInput)) && rawInput) {
279
- return toolArguments(parseToolInput(rawInput));
280
- }
281
- return fromInput;
282
- }
283
253
  function toolResultData(output) {
284
254
  return rawRecord(output);
285
255
  }
@@ -333,45 +303,12 @@ function rawEvents(raw, acc) {
333
303
  }
334
304
  return events;
335
305
  }
336
- async function collectRawEvents(res) {
337
- if (!res.body) {
338
- return [];
339
- }
340
- const reader = res.body.getReader();
341
- const decoder = new TextDecoder();
342
- const events = [];
343
- const acc = createAccumulator();
344
- let buffer = '';
345
- let pendingEvent = '';
346
- while (true) {
347
- const { done, value } = await reader.read();
348
- if (done) {
349
- break;
350
- }
351
- buffer += decoder.decode(value, { stream: true });
352
- const taken = takeSsePayloads(buffer, pendingEvent);
353
- buffer = taken.rest;
354
- pendingEvent = taken.pendingEvent;
355
- for (const payload of taken.payloads) {
356
- events.push(...rawEvents(payload, acc));
357
- }
358
- }
359
- return events;
360
- }
361
- function captureFetch(config, setCapture) {
362
- const fetchFn = config.fetch ?? fetch;
363
- return async (input, init) => {
364
- const res = await fetchFn(input, init);
365
- setCapture(collectRawEvents(res.clone()).catch(() => []));
366
- return res;
367
- };
368
- }
369
- function toolCallEvent(part, acc) {
306
+ function toolCallEvent(part) {
370
307
  return {
371
308
  type: 'tool',
372
309
  tool: {
373
310
  name: part.toolName,
374
- arguments: toolCallArguments(part, acc),
311
+ arguments: toolArguments(part.input),
375
312
  id: part.toolCallId,
376
313
  },
377
314
  };
@@ -401,22 +338,7 @@ function providerMetadataEvent(part, acc) {
401
338
  }
402
339
  return evidenceFromMetadata(part.providerMetadata, acc);
403
340
  }
404
- function appendToolInput(part, acc) {
405
- if (part.type === 'tool-input-start') {
406
- acc.toolInputs.set(part.id, '');
407
- return true;
408
- }
409
- if (part.type === 'tool-input-delta') {
410
- const current = acc.toolInputs.get(part.id) ?? '';
411
- acc.toolInputs.set(part.id, current + part.delta);
412
- return true;
413
- }
414
- return false;
415
- }
416
341
  function eventFromPart(part, acc) {
417
- if (appendToolInput(part, acc)) {
418
- return [];
419
- }
420
342
  const mapped = primaryEventFromPart(part, acc);
421
343
  if (mapped) {
422
344
  return [mapped];
@@ -432,11 +354,15 @@ function primaryEventFromPart(part, acc) {
432
354
  case 'reasoning-delta':
433
355
  return { type: 'thought', text: part.text };
434
356
  case 'tool-call':
435
- return toolCallEvent(part, acc);
357
+ return toolCallEvent(part);
436
358
  case 'tool-result':
437
359
  return toolResultEvent(part);
438
- case 'source':
360
+ case 'source': {
361
+ if (acc.evidenceSeen)
362
+ return undefined;
363
+ acc.evidenceSeen = true;
439
364
  return sourceEvent(part);
365
+ }
440
366
  case 'finish':
441
367
  return finishEvent(part, acc);
442
368
  case 'error':
@@ -456,27 +382,12 @@ function finishEvent(part, acc) {
456
382
  }
457
383
  return event;
458
384
  }
459
- function emitRawEvents(rawCapture, acc) {
460
- return (rawCapture ?? Promise.resolve([])).then((events) => events.filter((event) => {
461
- if (event.type !== 'evidence') {
462
- return true;
463
- }
464
- if (acc.evidenceSeen) {
465
- return false;
466
- }
467
- acc.evidenceSeen = true;
468
- return true;
469
- }));
470
- }
471
385
  function createStreamContext(req, config, apiKey) {
472
- let rawCapture;
473
386
  const openrouter = createOpenRouter({
474
387
  apiKey,
475
388
  baseURL: config.baseUrl,
476
389
  headers: openRouterHeaders(config),
477
- fetch: captureFetch(config, (capture) => {
478
- rawCapture = capture;
479
- }),
390
+ fetch: config.fetch,
480
391
  compatibility: 'strict',
481
392
  });
482
393
  return {
@@ -485,27 +396,26 @@ function createStreamContext(req, config, apiKey) {
485
396
  apiId: req.apiId,
486
397
  openRouterId: req.openRouterId,
487
398
  }),
488
- rawCapture: () => rawCapture,
489
399
  };
490
400
  }
491
401
  function streamTextOptions(req, context) {
492
402
  return {
493
403
  model: context.openrouter.chat(context.modelName, openRouterSettings(req)),
494
- system: req.system,
404
+ instructions: req.system,
495
405
  messages: buildMessages(req),
496
406
  allowSystemInMessages: true,
497
407
  temperature: req.temperature,
498
408
  maxOutputTokens: req.maxOutputTokens,
499
409
  tools: buildTools(req.dynamicTools),
500
410
  providerOptions: providerOptionsFor(req),
501
- includeRawChunks: true,
411
+ include: { rawChunks: true },
502
412
  abortSignal: req.signal,
503
413
  onError: () => undefined,
504
414
  };
505
415
  }
506
416
  async function* yieldAiSdkStream(req, acc, context) {
507
417
  const result = streamText(streamTextOptions(req, context));
508
- for await (const part of result.fullStream) {
418
+ for await (const part of result.stream) {
509
419
  if (part.type === 'raw') {
510
420
  req.tapGemini?.(rawRecord(part.rawValue) ?? { rawValue: part.rawValue });
511
421
  for (const event of rawEvents(part.rawValue, acc)) {
@@ -518,16 +428,6 @@ async function* yieldAiSdkStream(req, acc, context) {
518
428
  }
519
429
  }
520
430
  }
521
- async function* yieldCapturedRawEvents(context, acc) {
522
- for (const event of await emitRawEvents(context.rawCapture(), acc)) {
523
- yield event;
524
- }
525
- }
526
- async function* yieldCapturedRawEventsUnchecked(context) {
527
- for (const event of await (context.rawCapture() ?? Promise.resolve([]))) {
528
- yield event;
529
- }
530
- }
531
431
  function missingOpenRouterKey() {
532
432
  return toErrorEvent('missing OpenRouter API key');
533
433
  }
@@ -541,14 +441,12 @@ async function* streamOpenRouter(req, config) {
541
441
  const context = createStreamContext(req, config, apiKey);
542
442
  try {
543
443
  yield* yieldAiSdkStream(req, acc, context);
544
- yield* yieldCapturedRawEvents(context, acc);
545
444
  yield* finalEvents(req, acc);
546
445
  }
547
446
  catch (err) {
548
447
  if (isAbortError(err)) {
549
448
  throw err;
550
449
  }
551
- yield* yieldCapturedRawEventsUnchecked(context);
552
450
  yield toErrorEvent(err);
553
451
  }
554
452
  }
@@ -564,16 +462,34 @@ function* finalEvents(req, acc) {
564
462
  }
565
463
  yield { type: 'done' };
566
464
  }
567
- function providerOptionsFor(req) {
568
- if (req.thinking === 'none') {
465
+ function responseFormatFor(req) {
466
+ if (!req.structured)
467
+ return undefined;
468
+ const spec = getStructured(req.structured);
469
+ if (!spec.jsonSchema)
569
470
  return undefined;
570
- }
571
471
  return {
572
- openrouter: {
573
- reasoning: { effort: req.thinking },
472
+ type: 'json_schema',
473
+ json_schema: {
474
+ name: String(req.structured),
475
+ strict: true,
476
+ schema: spec.jsonSchema,
574
477
  },
575
478
  };
576
479
  }
480
+ function providerOptionsFor(req) {
481
+ const openrouter = {};
482
+ if (req.thinking !== 'none') {
483
+ openrouter.reasoning = { effort: req.thinking };
484
+ }
485
+ const responseFormat = responseFormatFor(req);
486
+ if (responseFormat) {
487
+ openrouter.response_format = responseFormat;
488
+ }
489
+ if (Object.keys(openrouter).length === 0)
490
+ return undefined;
491
+ return { openrouter };
492
+ }
577
493
  function openRouterHeaders(config) {
578
494
  const headers = {};
579
495
  if (config.siteUrl) {
@@ -591,3 +507,48 @@ function createOpenRouterProvider(config = {}) {
591
507
  };
592
508
  }
593
509
  export { createOpenRouterProvider, resolveOpenRouterModel, toOpenRouterPayload };
510
+ exposeForTests('openrouter', {
511
+ trimApiKey,
512
+ createAccumulator,
513
+ mediaPart,
514
+ contentFromParts,
515
+ parseToolInput,
516
+ stringDefault,
517
+ fallbackToolCallId,
518
+ toolResultContent,
519
+ assistantToolCallContent,
520
+ historyMessage,
521
+ contentFromOptionalParts,
522
+ contentHistoryMessage,
523
+ buildMessages,
524
+ schemaForTool,
525
+ buildTools,
526
+ openRouterSettings,
527
+ tokensFromUsage,
528
+ rawRecord,
529
+ stringArray,
530
+ metadataRecord,
531
+ citationCandidates,
532
+ nestedCitations,
533
+ metadataAnnotations,
534
+ evidenceFromMetadata,
535
+ toolArguments,
536
+ toolResultData,
537
+ rawThoughtEvent,
538
+ rawChoiceMessageEvidence,
539
+ rawEvents,
540
+ toolCallEvent,
541
+ toolResultEvent,
542
+ tokenEvent,
543
+ providerMetadataEvent,
544
+ eventFromPart,
545
+ primaryEventFromPart,
546
+ finishEvent,
547
+ sourceEvent,
548
+ finalEvents,
549
+ responseFormatFor,
550
+ providerOptionsFor,
551
+ openRouterHeaders,
552
+ missingOpenRouterKey,
553
+ streamTextOptions,
554
+ });
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * @module
5
5
  */
6
+ import { exposeForTests } from './expose-for-tests.js';
6
7
  const SAMPLE_RATE = 24000;
7
8
  function writeAscii(view, offset, str) {
8
9
  for (let i = 0; i < str.length; i++)
@@ -33,3 +34,4 @@ export function wrapPcmAsWav(pcm, sampleRate = SAMPLE_RATE) {
33
34
  new Uint8Array(buf, 44).set(pcm);
34
35
  return new Uint8Array(buf);
35
36
  }
37
+ exposeForTests('pcm', { writeAscii, wrapPcmAsWav });
@@ -15,6 +15,7 @@ import { toInteractionsBody } from './interactions.js';
15
15
  import { fetchGemini } from './keys.js';
16
16
  import { wrapPcmAsWav } from './pcm.js';
17
17
  import { INTERACTIONS_URL, takeSsePayloads } from './sse.js';
18
+ import { exposeForTests } from './expose-for-tests.js';
18
19
  const HTTP_OK = 200;
19
20
  function base64ToBytes(data) {
20
21
  const bin = atob(data);
@@ -161,3 +162,15 @@ function createInteractionsProvider(transport) {
161
162
  };
162
163
  }
163
164
  export { createInteractionsProvider };
165
+ exposeForTests('provider', {
166
+ base64ToBytes,
167
+ bytesToBase64,
168
+ isRawPcmMime,
169
+ normalizeSpeechMedia,
170
+ eventType,
171
+ isDeltaEvent,
172
+ isCompleteEvent,
173
+ foldDeltaPayload,
174
+ foldPayload,
175
+ withTap,
176
+ });
@@ -8,6 +8,7 @@
8
8
  */
9
9
  import { toErrorEvent } from '../guardrails/error.js';
10
10
  import { wrapPcmAsWav } from './pcm.js';
11
+ import { exposeForTests } from './expose-for-tests.js';
11
12
  const HTTP_OK = 200;
12
13
  function bytesToBase64(bytes) {
13
14
  let bin = '';
@@ -124,3 +125,12 @@ function createSpeechProvider(config = {}) {
124
125
  };
125
126
  }
126
127
  export { createSpeechProvider, streamSpeech };
128
+ exposeForTests('speech', {
129
+ bytesToBase64,
130
+ extractInputText,
131
+ resolveSpeechWireModel,
132
+ buildHeaders,
133
+ buildPayload,
134
+ requestSpeech,
135
+ yieldSpeechSuccess,
136
+ });
@@ -1,3 +1,4 @@
1
+ import { exposeForTests } from './expose-for-tests.js';
1
2
  const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions?alt=sse';
2
3
  const DATA_PREFIX = 'data: ';
3
4
  const EVENT_PREFIX = 'event: ';
@@ -51,3 +52,4 @@ function takeSsePayloads(buffer, pendingEvent = '') {
51
52
  return { rest, payloads, pendingEvent: sseEvent };
52
53
  }
53
54
  export { INTERACTIONS_URL, takeSsePayloads };
55
+ exposeForTests('sse', { asObject, dataRecord, takeSsePayloads });
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Structured-output streaming helpers (incomplete JSON text buffers).
3
+ *
4
+ * @module
5
+ */
6
+ import "../../_dnt.polyfills.js";
7
+ export { readStreamingJsonStringField } from './readStreamingJsonStringField.js';
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Structured-output streaming helpers (incomplete JSON text buffers).
3
+ *
4
+ * @module
5
+ */
6
+ import "../../_dnt.polyfills.js";
7
+ export { readStreamingJsonStringField } from './readStreamingJsonStringField.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Read one string field from incomplete JSON while structured output streams as text deltas.
3
+ *
4
+ * The buffer may lack a closing quote; any decoded prefix is returned for live preview.
5
+ */
6
+ export declare function readStreamingJsonStringField(jsonText: string, key: string): string | null;
@@ -0,0 +1,55 @@
1
+ const JSON_ESCAPES = {
2
+ n: '\n',
3
+ t: '\t',
4
+ r: '\r',
5
+ '"': '"',
6
+ '\\': '\\',
7
+ '/': '/',
8
+ };
9
+ function decodeEscapedChar(ch, jsonText, index) {
10
+ if (ch === 'u' && index + 4 < jsonText.length) {
11
+ const hex = jsonText.slice(index + 1, index + 5);
12
+ if (/^[0-9a-fA-F]{4}$/.test(hex)) {
13
+ return { text: String.fromCharCode(Number.parseInt(hex, 16)), next: index + 4 };
14
+ }
15
+ }
16
+ const mapped = JSON_ESCAPES[ch];
17
+ return { text: mapped ?? ch, next: index };
18
+ }
19
+ /**
20
+ * Read one string field from incomplete JSON while structured output streams as text deltas.
21
+ *
22
+ * The buffer may lack a closing quote; any decoded prefix is returned for live preview.
23
+ */
24
+ export function readStreamingJsonStringField(jsonText, key) {
25
+ const keyPattern = new RegExp(`"${key}"\\s*:\\s*"`);
26
+ const match = keyPattern.exec(jsonText);
27
+ if (!match || match.index === undefined) {
28
+ return null;
29
+ }
30
+ let i = match.index + match[0].length;
31
+ let result = '';
32
+ let escaped = false;
33
+ while (i < jsonText.length) {
34
+ const ch = jsonText[i];
35
+ if (escaped) {
36
+ const decoded = decodeEscapedChar(ch, jsonText, i);
37
+ result += decoded.text;
38
+ i = decoded.next;
39
+ escaped = false;
40
+ i += 1;
41
+ continue;
42
+ }
43
+ if (ch === '\\') {
44
+ escaped = true;
45
+ }
46
+ else if (ch === '"') {
47
+ return result;
48
+ }
49
+ else {
50
+ result += ch;
51
+ }
52
+ i += 1;
53
+ }
54
+ return result;
55
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theorum",
3
- "version": "0.1.7",
3
+ "version": "0.1.10",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, dynamic tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",
@@ -45,6 +45,9 @@
45
45
  },
46
46
  "./presets/google": {
47
47
  "import": "./esm/src/presets/google.js"
48
+ },
49
+ "./streaming": {
50
+ "import": "./esm/src/streaming/mod.js"
48
51
  }
49
52
  },
50
53
  "scripts": {},
@@ -54,8 +57,8 @@
54
57
  "type": "module",
55
58
  "sideEffects": false,
56
59
  "dependencies": {
57
- "@openrouter/ai-sdk-provider": "1.5.4",
58
- "ai": "5.0.244",
60
+ "@openrouter/ai-sdk-provider": "^3.0.0",
61
+ "ai": "^7.0.0",
59
62
  "@deno/shim-deno": "~0.18.0"
60
63
  },
61
64
  "devDependencies": {