@theoremjs/agents 0.3.4-next.482.g7e608f2 → 0.3.4-next.508.geda6db0

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 (71) hide show
  1. package/README.md +61 -51
  2. package/esm/mod.d.ts +2 -2
  3. package/esm/mod.js +1 -1
  4. package/esm/src/guardrails/credential-scan.d.ts +1 -1
  5. package/esm/src/guardrails/credential-scan.js +41 -18
  6. package/esm/src/guardrails/destinations.js +6 -2
  7. package/esm/src/guardrails/detect-at.d.ts +1 -3
  8. package/esm/src/guardrails/detect-at.js +6 -22
  9. package/esm/src/guardrails/egress-automata.js +203 -202
  10. package/esm/src/guardrails/egress-stream.js +10 -3
  11. package/esm/src/guardrails/egress-urls.js +4 -2
  12. package/esm/src/guardrails/injection-patterns.d.ts +3 -1
  13. package/esm/src/guardrails/injection-patterns.js +87 -4
  14. package/esm/src/guardrails/labels.d.ts +2 -2
  15. package/esm/src/guardrails/labels.js +7 -5
  16. package/esm/src/guardrails/mod.d.ts +1 -1
  17. package/esm/src/guardrails/normalize.js +4 -2
  18. package/esm/src/guardrails/prompt-echo.js +4 -2
  19. package/esm/src/guardrails/sensitive.d.ts +2 -2
  20. package/esm/src/guardrails/sensitive.js +1 -1
  21. package/esm/src/interface/profile-interface.js +13 -0
  22. package/esm/src/kernel/answer-tool.d.ts +14 -0
  23. package/esm/src/kernel/answer-tool.js +26 -0
  24. package/esm/src/kernel/engine/decision-references.d.ts +14 -0
  25. package/esm/src/kernel/engine/decision-references.js +55 -0
  26. package/esm/src/kernel/engine/decision.d.ts +3 -3
  27. package/esm/src/kernel/engine/decision.js +38 -22
  28. package/esm/src/kernel/engine/embedding.js +0 -2
  29. package/esm/src/kernel/engine/runner/gates.js +38 -4
  30. package/esm/src/kernel/engine/runner/steps.js +47 -10
  31. package/esm/src/kernel/engine/runner/stream.d.ts +14 -8
  32. package/esm/src/kernel/engine/runner/stream.js +25 -13
  33. package/esm/src/kernel/mod.d.ts +1 -1
  34. package/esm/src/kernel/mod.js +1 -1
  35. package/esm/src/kernel/profile-presence.js +12 -0
  36. package/esm/src/kernel/profile-scope.js +14 -14
  37. package/esm/src/kernel/protocols/binding.d.ts +3 -1
  38. package/esm/src/kernel/protocols/binding.js +6 -0
  39. package/esm/src/kernel/protocols/limits.d.ts +10 -11
  40. package/esm/src/kernel/protocols/limits.js +9 -23
  41. package/esm/src/kernel/protocols/runtime.d.ts +3 -2
  42. package/esm/src/kernel/protocols/runtime.js +1 -2
  43. package/esm/src/kernel/provider-budget.d.ts +2 -2
  44. package/esm/src/kernel/provider-budget.js +22 -12
  45. package/esm/src/kernel/provider-runtime.js +2 -2
  46. package/esm/src/kernel/registry/profiles.js +1 -1
  47. package/esm/src/kernel/registry/resolve.js +4 -0
  48. package/esm/src/kernel/registry/schemas.js +1 -1
  49. package/esm/src/kernel/schema.js +12 -2
  50. package/esm/src/kernel/types.d.ts +4 -2
  51. package/esm/src/observability/spans.js +7 -8
  52. package/esm/src/providers/embedding-limits.js +4 -4
  53. package/esm/src/providers/gemini/{embedding-protocol.d.ts → embeddings-protocol.d.ts} +3 -3
  54. package/esm/src/providers/gemini/{embedding-protocol.js → embeddings-protocol.js} +3 -3
  55. package/esm/src/providers/gemini/live-protocol.js +4 -1
  56. package/esm/src/providers/gemini/provider.js +2 -2
  57. package/esm/src/providers/local/embeddings-protocol.d.ts +7 -0
  58. package/esm/src/providers/local/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  59. package/esm/src/providers/local/provider.js +2 -2
  60. package/esm/src/providers/mod.d.ts +3 -3
  61. package/esm/src/providers/mod.js +3 -3
  62. package/esm/src/providers/openrouter/decisions-protocol.d.ts +5 -3
  63. package/esm/src/providers/openrouter/decisions-protocol.js +55 -4
  64. package/esm/src/providers/openrouter/embeddings-protocol.d.ts +6 -0
  65. package/esm/src/providers/openrouter/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  66. package/esm/src/providers/openrouter/provider.js +2 -2
  67. package/esm/src/providers/system-one/protocol.d.ts +4 -0
  68. package/esm/src/providers/system-one/protocol.js +25 -24
  69. package/package.json +1 -1
  70. package/esm/src/providers/local/embedding-protocol.d.ts +0 -7
  71. package/esm/src/providers/openrouter/embedding-protocol.d.ts +0 -6
@@ -21,6 +21,9 @@ function combinedSignal(signals) {
21
21
  }
22
22
  return { signal: controller.signal, dispose };
23
23
  }
24
+ function over(value, limit) {
25
+ return limit !== undefined && value > limit;
26
+ }
24
27
  export class ProviderBudget {
25
28
  limits;
26
29
  controller = new AbortController();
@@ -38,10 +41,14 @@ export class ProviderBudget {
38
41
  const effective = combinedSignal(caller ? [caller, this.controller.signal] : [this.controller.signal]);
39
42
  this.signal = effective.signal;
40
43
  this.releaseSignal = effective.dispose;
44
+ if (timeoutMs === undefined)
45
+ return;
41
46
  this.startTimer(timeoutMs);
42
47
  this.deadline = performance.now() + timeoutMs;
43
48
  }
44
49
  resumeOperation() {
50
+ if (this.deadline === undefined)
51
+ return;
45
52
  const remaining = this.deadline - performance.now();
46
53
  if (remaining <= 0) {
47
54
  this.controller.abort(new TheoremError('timeout', labelText('error.timeout')));
@@ -51,7 +58,8 @@ export class ProviderBudget {
51
58
  }
52
59
  /** Milliseconds before the running timer ends the call. */
53
60
  timeLeft() {
54
- return (this.timerDeadline ?? this.deadline) - performance.now();
61
+ const end = this.timerDeadline ?? this.deadline;
62
+ return end === undefined ? Number.POSITIVE_INFINITY : end - performance.now();
55
63
  }
56
64
  startTimer(ms) {
57
65
  this.stopTimer();
@@ -73,8 +81,9 @@ export class ProviderBudget {
73
81
  this.releaseSignal();
74
82
  }
75
83
  startLiveStep() {
76
- if (this.timer === undefined)
77
- this.startTimer(this.limits.liveStepTimeoutMs);
84
+ const ms = this.limits.liveStepTimeoutMs;
85
+ if (this.timer === undefined && ms !== undefined)
86
+ this.startTimer(ms);
78
87
  }
79
88
  fail() {
80
89
  const error = new TheoremError('bad_response', labelText('provider.budget_exhausted'), {
@@ -95,9 +104,9 @@ export class ProviderBudget {
95
104
  this.checkDeadline();
96
105
  this.stepBytes += bytes;
97
106
  this.stepEvents++;
98
- if (bytes > this.limits.maxEventBytes ||
99
- this.stepBytes > this.limits.maxStepBytes ||
100
- this.stepEvents > this.limits.maxStepEvents)
107
+ if (over(bytes, this.limits.maxEventBytes) ||
108
+ over(this.stepBytes, this.limits.maxStepBytes) ||
109
+ over(this.stepEvents, this.limits.maxStepEvents))
101
110
  this.fail();
102
111
  }
103
112
  event(event) {
@@ -106,7 +115,7 @@ export class ProviderBudget {
106
115
  this.calls.add(event.call.callId);
107
116
  if (event.type === 'tool_cancel')
108
117
  this.calls.delete(event.callId);
109
- if (this.calls.size > this.limits.maxPendingToolCalls)
118
+ if (over(this.calls.size, this.limits.maxPendingToolCalls))
110
119
  this.fail();
111
120
  }
112
121
  resetStep() {
@@ -138,7 +147,7 @@ export class ProviderBudget {
138
147
  : data instanceof ArrayBuffer || ArrayBuffer.isView(data)
139
148
  ? data.byteLength
140
149
  : 0;
141
- if (bytes <= this.limits.maxWireRecordBytes)
150
+ if (!over(bytes, this.limits.maxWireRecordBytes))
142
151
  return;
143
152
  event.stopImmediatePropagation();
144
153
  try {
@@ -168,7 +177,8 @@ export class ProviderBudget {
168
177
  }
169
178
  const reader = response.body.getReader();
170
179
  let responseBytes = 0;
171
- const record = response.headers.get('content-type')?.includes('text/event-stream')
180
+ const record = this.limits.maxWireRecordBytes !== undefined &&
181
+ response.headers.get('content-type')?.includes('text/event-stream')
172
182
  ? new SseRecordBudget(this)
173
183
  : undefined;
174
184
  const abort = () => {
@@ -190,8 +200,8 @@ export class ProviderBudget {
190
200
  }
191
201
  responseBytes += next.value.byteLength;
192
202
  this.wireBytes += next.value.byteLength;
193
- if (responseBytes > this.limits.maxWireResponseBytes ||
194
- this.wireBytes > this.limits.maxWireResponseBytes)
203
+ if (over(responseBytes, this.limits.maxWireResponseBytes) ||
204
+ over(this.wireBytes, this.limits.maxWireResponseBytes))
195
205
  this.fail();
196
206
  record?.accept(next.value);
197
207
  controller.enqueue(next.value);
@@ -233,7 +243,7 @@ class SseRecordBudget {
233
243
  this.emptyCR = false;
234
244
  }
235
245
  this.recordBytes++;
236
- if (this.recordBytes > this.budget.limits.maxWireRecordBytes)
246
+ if (over(this.recordBytes, this.budget.limits.maxWireRecordBytes))
237
247
  this.budget.fail();
238
248
  if (this.emptyCR) {
239
249
  this.recordBytes = 0;
@@ -5,7 +5,7 @@ import { historyHash, providerCheckpointSchema, } from './checkpoint.js';
5
5
  import { stepReplyMessages } from './interaction-parts.js';
6
6
  import { jsonObjectSchema, jsonValueSchema } from './json.js';
7
7
  import { keySlotsOf, protocolOf, } from './protocols/binding.js';
8
- import { resolveProviderLimits } from './protocols/limits.js';
8
+ import { providerLimitsSchema } from './protocols/limits.js';
9
9
  import { carriesTo, } from './protocols/protocol.js';
10
10
  import { callProtocol, plainWebSocket } from './protocols/runtime.js';
11
11
  import { ProviderBudget } from './provider-budget.js';
@@ -188,7 +188,7 @@ const SECRET_FIELD = /^(?:authorization|proxy-authorization|cookie|set-cookie|ap
188
188
  * the host's budgets, with every key it signs with kept out of errors and traces.
189
189
  */
190
190
  export function guardedCall(provider, binding, profileType, host, req) {
191
- const limits = resolveProviderLimits(host.limits);
191
+ const limits = Object.freeze(providerLimitsSchema.parse({ ...binding.limits, ...host.limits }));
192
192
  const budget = new ProviderBudget(limits, req.signal, req.timeoutMs ?? (req.liveConnection ? limits.connectTimeoutMs : limits.operationTimeoutMs));
193
193
  const secrets = new Set();
194
194
  const remember = (value) => {
@@ -50,7 +50,7 @@ function validateDecisionBinding(profileId, modelId, binding) {
50
50
  if ('retry' in binding) {
51
51
  throw new TheoremError('config',
52
52
  // labels-exempt: developer contract / internal diagnostic
53
- `Profile ${profileId} model '${modelId}': decision retry configuration is unsupported; POSTs are never retried`);
53
+ `Profile ${profileId} model '${modelId}': a decision binding takes no retry; a decision retries only a transient failure, by the kernel's rule`);
54
54
  }
55
55
  if (binding.timeoutMs !== undefined &&
56
56
  (!Number.isFinite(binding.timeoutMs) || binding.timeoutMs <= 0)) {
@@ -4,6 +4,7 @@ import { TheoremError } from '../../guardrails/error.js';
4
4
  import { resolveGuardrailPolicy } from '../../guardrails/policy.js';
5
5
  import { replyIsJudged } from '../../guardrails/progressive-yield.js';
6
6
  import { sanitizeTurnRequest } from '../../guardrails/sanitize.js';
7
+ import { ANSWER_TOOL } from '../answer-tool.js';
7
8
  import { DEFAULT_MAX_STEPS } from '../profile-presence.js';
8
9
  import { profileTurnResumption } from '../stop.js';
9
10
  import { projectTools } from '../tools/project.js';
@@ -171,6 +172,9 @@ function resolveTurnInRegistry(registry, req, model) {
171
172
  const builtins = toolSnapshot.builtins;
172
173
  const structuredId = resolveStructured(profile, input.slots);
173
174
  assertOutputMode(profile, structuredId);
175
+ if (structuredId && toolSnapshot.wire.some((tool) => tool.name === ANSWER_TOOL)) {
176
+ throw new TheoremError('config', `Profile ${profile.id}: a structured turn answers through '${ANSWER_TOOL}', so no tool of its own may take that name`);
177
+ }
174
178
  assertSpeechRole(profile, safe);
175
179
  const builtInArgs = resolveBuiltInArgs(profile, picked, binding, safe.builtInArgs);
176
180
  const transport = resolveTransport(profile, binding);
@@ -5,7 +5,7 @@ function createSchemaRegistry() {
5
5
  return {
6
6
  register(id, spec) {
7
7
  if ('enforced' in spec) {
8
- throw new TheoremError('config', `registerStructured '${id}': enforced was removed; every structured schema is sent to the model as its response format`);
8
+ throw new TheoremError('config', `registerStructured '${id}': enforced was removed; every structured schema is sent to the model, as its response format or as its answer tool's input`);
9
9
  }
10
10
  if (!spec.jsonSchema ||
11
11
  typeof spec.jsonSchema !== 'object' ||
@@ -520,6 +520,16 @@ export const PROFILE_FIELDS = withScopeAndPresence({
520
520
  'providerContinuation.history': field("'host' | 'stateless' | 'provider'", 'Who holds the conversation a checkpoint continues: the host passes history back, the provider keeps it and the checkpoint travels alone, or the profile is stateless and every request stands alone.', ['host', 'stateless', 'provider']),
521
521
  'models.*.apiId': field('string', "The model's id as the provider lists it, such as gemini-2.5-flash; sent on every call. The name this model has in the profile is its key in models."),
522
522
  'models.*.timeoutMs': field('number', 'How long a decision waits for its provider, in milliseconds. Omit for no timeout.'),
523
+ 'models.*.limits': field('ProviderLimits', "The points at which Theorem stops this model's calls. Each one left out is no limit, so the provider's own limits apply. A runTurn limits value overrides each field it sets."),
524
+ 'models.*.limits.operationTimeoutMs': field('number', 'How long one call may take, in milliseconds, retries included.'),
525
+ 'models.*.limits.connectTimeoutMs': field('number', 'How long a live session may take to open, in milliseconds.'),
526
+ 'models.*.limits.liveStepTimeoutMs': field('number', 'How long a live session may wait for the model to answer, in milliseconds.'),
527
+ 'models.*.limits.maxWireRecordBytes': field('number', 'The most bytes in one record the provider sends: a stream event or a socket message.'),
528
+ 'models.*.limits.maxWireResponseBytes': field('number', 'The most bytes the provider sends for one step.'),
529
+ 'models.*.limits.maxEventBytes': field('number', 'The most bytes in one event of a step.'),
530
+ 'models.*.limits.maxStepBytes': field('number', 'The most bytes in all the events of one step.'),
531
+ 'models.*.limits.maxStepEvents': field('number', 'The most events in one step.'),
532
+ 'models.*.limits.maxPendingToolCalls': field('number', 'The most tool calls one step may ask for.'),
523
533
  decision: field('{ contract: DecisionContractId }', 'Names the host decision this profile makes.'),
524
534
  'decision.contract': field('string', 'Your name for this decision, on its traces. Not sent to the model. Not checked against the questions.'),
525
535
  'models.*.efforts': field('Record<string, ThinkingLevel>', 'Named thinking levels. A turn picks one when allowEffortSelect is on. A provider refuses a level its model does not take. Speech ignores them, and so may an image profile without includeText.'),
@@ -762,12 +772,12 @@ export const EXTRA_FIELDS = {
762
772
  noul: 'One number, which may weigh named criteria.',
763
773
  box: 'Where something is in an image: its top, bottom, left and right edges, with a confidence.',
764
774
  }),
765
- 'decision.questions.instructions': field('DecisionEntry', "What to ask about the state. Name a state field in backticks, such as `photo`, and one of the decision's images by its reference, such as <ginger>. A box looks in its In image, else in the image of the first field it names."),
775
+ 'decision.questions.instructions': field('DecisionEntry', "What to ask about the state. Name a state field in backticks, such as `photo`, and one of the decision's images by its reference, such as <ginger>. A box names what to find and the image it is in, such as the ginger in `photo`."),
766
776
  'decision.questions.image': {
767
777
  ...field('string', "The image a box is in: one of the decision's images, such as <ghs02>, or a state field its instructions name, such as `photo`. The model is told it in the instructions."),
768
778
  unset: 'The image the instructions name',
769
779
  },
770
- 'decision.images': field('Record<string, DecisionAsset>', 'Pictures sent with every decision, beside the state. A question names one by its reference, such as <ghs02>. A PNG or JPEG.'),
780
+ 'decision.images': field('Record<string, DecisionAsset>', "Pictures a decision can send. Write one's reference, such as <ghs02>, in the state or a question, and the model reads the picture there; a picture nothing references is not sent. A PNG or JPEG."),
771
781
  'decision.images.name': field('string', 'The reference a question writes, without its angle brackets: ghs02 is written <ghs02>. Lowercase letters, digits and _.'),
772
782
  'registerTool.name': field('string', "The tool's id, which profiles list in tools.allow (or a model's builtInTools) and the model calls a custom tool by."),
773
783
  'registerTool.description': field('string', 'Tells the model what the tool does (custom tools only).'),
@@ -458,8 +458,10 @@ export interface DecisionRequest {
458
458
  state: Exclude<DecisionJson, null>;
459
459
  questions: Record<string, DecisionQuestion>;
460
460
  /**
461
- * Images by reference. Write a reference as a string where its image belongs, in `state`, in a
462
- * question's instructions, or in its criteria; the provider puts the image there.
461
+ * Images by reference. Write a reference in a string where its image belongs, in `state`, in a
462
+ * question's instructions, or in its criteria, as the whole string or inside it; the model reads
463
+ * the image in its place, once for each use. Keys are never read. Only the images some string
464
+ * references are sent, so pick references that cannot occur in your text by accident.
463
465
  */
464
466
  assets?: Record<string, DecisionAsset>;
465
467
  signal?: AbortSignal;
@@ -47,15 +47,14 @@ function mergeSpans(spans) {
47
47
  return out;
48
48
  }
49
49
  function applySpans(text, spans) {
50
- let out = text;
51
- const merged = mergeSpans(spans);
52
- for (let i = merged.length - 1; i >= 0; i -= 1) {
53
- const span = merged[i];
54
- if (span) {
55
- out = out.slice(0, span.start) + tokenFor(span.kind) + out.slice(span.end);
56
- }
50
+ const pieces = [];
51
+ let at = 0;
52
+ for (const span of mergeSpans(spans)) {
53
+ pieces.push(text.slice(at, span.start), tokenFor(span.kind));
54
+ at = span.end;
57
55
  }
58
- return out;
56
+ pieces.push(text.slice(at));
57
+ return pieces.join('');
59
58
  }
60
59
  function spansFromPatterns(text, patterns, kind) {
61
60
  const spans = [];
@@ -1,7 +1,7 @@
1
- import { geminiEmbedding } from './gemini/embedding-protocol.js';
2
- import { ollamaEmbedding } from './local/embedding-protocol.js';
3
- import { openRouterEmbedding } from './openrouter/embedding-protocol.js';
4
- const SHIPPED = [geminiEmbedding, openRouterEmbedding, ollamaEmbedding];
1
+ import { geminiEmbeddings } from './gemini/embeddings-protocol.js';
2
+ import { ollamaEmbeddings } from './local/embeddings-protocol.js';
3
+ import { openRouterEmbeddings } from './openrouter/embeddings-protocol.js';
4
+ const SHIPPED = [geminiEmbeddings, openRouterEmbeddings, ollamaEmbeddings];
5
5
  /**
6
6
  * What a binding's embedding model takes, read from its protocol: one Theorem ships, or one of
7
7
  * `protocols`. Undefined when neither has it, and the kernel then holds requests to the profile alone.
@@ -1,8 +1,8 @@
1
1
  import type { Protocol } from '../../kernel/protocols/protocol.js';
2
- /** What a `geminiEmbedding` binding sets in `options`. */
3
- export interface GeminiEmbeddingOptions {
2
+ /** What a `geminiEmbeddings` binding sets in `options`. */
3
+ export interface GeminiEmbeddingsOptions {
4
4
  /** For `gemini-embedding-001`; Gemini Embedding 2 refuses it and reads the request's `task` from the text. */
5
5
  taskType?: 'RETRIEVAL_QUERY' | 'RETRIEVAL_DOCUMENT' | 'SEMANTIC_SIMILARITY' | 'CLASSIFICATION' | 'CLUSTERING' | 'QUESTION_ANSWERING' | 'FACT_VERIFICATION' | 'CODE_RETRIEVAL_QUERY';
6
6
  outputDimensionality?: 768 | 1536 | 3072;
7
7
  }
8
- export declare const geminiEmbedding: Protocol<GeminiEmbeddingOptions>;
8
+ export declare const geminiEmbeddings: Protocol<GeminiEmbeddingsOptions>;
@@ -76,8 +76,8 @@ function requestBody(input, parsed, shape) {
76
76
  body.outputDimensionality = parsed.outputDimensionality;
77
77
  return body;
78
78
  }
79
- export const geminiEmbedding = defineProtocol({
80
- id: 'geminiEmbedding',
79
+ export const geminiEmbeddings = defineProtocol({
80
+ id: 'geminiEmbeddings',
81
81
  name: 'Gemini Embeddings',
82
82
  description: "Google's embedding API for text and supported media.",
83
83
  endpoints: { embedding: '/models/{apiId}:embedContent' },
@@ -88,7 +88,7 @@ export const geminiEmbedding = defineProtocol({
88
88
  create(context) {
89
89
  return {
90
90
  async embed(request) {
91
- const parsed = modelOptions(geminiEmbedding, context.apiId, request.options);
91
+ const parsed = modelOptions(geminiEmbeddings, context.apiId, request.options);
92
92
  const taskField = TASK_FIELD_MODELS.test(context.apiId);
93
93
  const shape = { taskField, ...(request.task ? { task: request.task } : {}) };
94
94
  const inputs = Array.isArray(request.input) ? request.input : [request.input];
@@ -20,8 +20,11 @@ function socketTrouble(frame, during) {
20
20
  /** Waits for `setupComplete`; a refusal here comes before any event, so the kernel can try the other key. */
21
21
  async function setupComplete(frames, context) {
22
22
  let timer;
23
+ const limit = context.limits.connectTimeoutMs;
23
24
  const late = new Promise((_, reject) => {
24
- timer = setTimeout(() => reject(new TheoremError('timeout', `Gemini Live setup timed out after ${context.limits.connectTimeoutMs}ms`)), context.limits.connectTimeoutMs);
25
+ if (limit === undefined)
26
+ return;
27
+ timer = setTimeout(() => reject(new TheoremError('timeout', `Gemini Live setup timed out after ${limit}ms`)), limit);
25
28
  });
26
29
  try {
27
30
  for (;;) {
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { labelText } from '../../guardrails/labels.js';
3
3
  import { defineProvider, endpointUrlSchema, header, query, } from '../../kernel/protocols/provider.js';
4
- import { geminiEmbedding } from './embedding-protocol.js';
4
+ import { geminiEmbeddings } from './embeddings-protocol.js';
5
5
  import { geminiInteractions } from './interactions-protocol.js';
6
6
  import { geminiLive } from './live-protocol.js';
7
7
  import { geminiPrices } from './prices.js';
@@ -31,7 +31,7 @@ export const geminiProvider = defineProvider({
31
31
  const liveOrigin = `${host.protocol === 'http:' ? 'ws:' : 'wss:'}//${host.host}`;
32
32
  return [
33
33
  { protocol: geminiInteractions, baseUrl: `${origin}/v1beta` },
34
- { protocol: geminiEmbedding, baseUrl: `${origin}/v1beta` },
34
+ { protocol: geminiEmbeddings, baseUrl: `${origin}/v1beta` },
35
35
  // probed 2026-09-23: Live's WebSocket takes the key only in the URL's query.
36
36
  { protocol: geminiLive, baseUrl: `${liveOrigin}/ws`, authorize: query('key') },
37
37
  ];
@@ -0,0 +1,7 @@
1
+ import { type Protocol } from '../../kernel/protocols/mod.js';
2
+ /** What an `ollamaEmbeddings` binding sets in `options`. */
3
+ export interface OllamaEmbeddingsOptions {
4
+ truncate?: boolean;
5
+ dimensions?: number;
6
+ }
7
+ export declare const ollamaEmbeddings: Protocol<OllamaEmbeddingsOptions>;
@@ -23,8 +23,8 @@ function strings(input) {
23
23
  return item.parts[0].text;
24
24
  });
25
25
  }
26
- export const ollamaEmbedding = defineProtocol({
27
- id: 'ollamaEmbedding',
26
+ export const ollamaEmbeddings = defineProtocol({
27
+ id: 'ollamaEmbeddings',
28
28
  name: 'Ollama Embeddings',
29
29
  description: 'Ollama’s local embedding endpoint.',
30
30
  endpoints: { embedding: '/api/embed' },
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { bearer, defineProvider, endpointUrlSchema, } from '../../kernel/protocols/provider.js';
3
3
  import { openAi } from '../open-ai/protocol.js';
4
- import { ollamaEmbedding } from './embedding-protocol.js';
4
+ import { ollamaEmbeddings } from './embeddings-protocol.js';
5
5
  /** A server on your machine or network that speaks OpenAI's chat API: Ollama, llama.cpp, vLLM, LM Studio. */
6
6
  export const localProvider = defineProvider({
7
7
  id: 'local',
@@ -16,7 +16,7 @@ export const localProvider = defineProvider({
16
16
  serves: (at) => [
17
17
  { protocol: openAi, baseUrl: at.baseUrl },
18
18
  {
19
- protocol: ollamaEmbedding,
19
+ protocol: ollamaEmbeddings,
20
20
  baseUrl: at.baseUrl,
21
21
  url: ({ path }) => {
22
22
  const base = new URL(at.baseUrl);
@@ -17,15 +17,15 @@ export { bearer, createProviderRegistry, defineProvider, endpointUrlSchema, head
17
17
  export type { OpenWebSocket, ProviderHostOptions } from '../kernel/protocols/runtime.js';
18
18
  export type { CredentialContext, CredentialResolver, KeyCredential, KeyVault, } from '../kernel/registry/vault.js';
19
19
  export { embeddingModelLimits } from './embedding-limits.js';
20
- export { geminiEmbedding } from './gemini/embedding-protocol.js';
20
+ export { geminiEmbeddings } from './gemini/embeddings-protocol.js';
21
21
  export { geminiInteractions } from './gemini/interactions-protocol.js';
22
22
  export { geminiLive } from './gemini/live-protocol.js';
23
23
  export { geminiProvider } from './gemini/provider.js';
24
- export { ollamaEmbedding } from './local/embedding-protocol.js';
24
+ export { ollamaEmbeddings } from './local/embeddings-protocol.js';
25
25
  export { localProvider } from './local/provider.js';
26
26
  export { openAi } from './open-ai/protocol.js';
27
27
  export { openRouterDecisions } from './openrouter/decisions-protocol.js';
28
- export { openRouterEmbedding } from './openrouter/embedding-protocol.js';
28
+ export { openRouterEmbeddings } from './openrouter/embeddings-protocol.js';
29
29
  export { openRouterImages } from './openrouter/images-protocol.js';
30
30
  export { openRouterOpenAi } from './openrouter/protocol.js';
31
31
  export { openRouterProvider } from './openrouter/provider.js';
@@ -8,15 +8,15 @@ export { jsonObjectSchema, jsonValueSchema } from '../kernel/json.js';
8
8
  export { carriesTo, defineProtocol, extendProtocol, modelOptions, } from '../kernel/protocols/protocol.js';
9
9
  export { bearer, createProviderRegistry, defineProvider, endpointUrlSchema, header, query, } from '../kernel/protocols/provider.js';
10
10
  export { embeddingModelLimits } from './embedding-limits.js';
11
- export { geminiEmbedding } from './gemini/embedding-protocol.js';
11
+ export { geminiEmbeddings } from './gemini/embeddings-protocol.js';
12
12
  export { geminiInteractions } from './gemini/interactions-protocol.js';
13
13
  export { geminiLive } from './gemini/live-protocol.js';
14
14
  export { geminiProvider } from './gemini/provider.js';
15
- export { ollamaEmbedding } from './local/embedding-protocol.js';
15
+ export { ollamaEmbeddings } from './local/embeddings-protocol.js';
16
16
  export { localProvider } from './local/provider.js';
17
17
  export { openAi } from './open-ai/protocol.js';
18
18
  export { openRouterDecisions } from './openrouter/decisions-protocol.js';
19
- export { openRouterEmbedding } from './openrouter/embedding-protocol.js';
19
+ export { openRouterEmbeddings } from './openrouter/embeddings-protocol.js';
20
20
  export { openRouterImages } from './openrouter/images-protocol.js';
21
21
  export { openRouterOpenAi } from './openrouter/protocol.js';
22
22
  export { openRouterProvider } from './openrouter/provider.js';
@@ -1,4 +1,6 @@
1
1
  import { type Protocol } from '../../kernel/protocols/mod.js';
2
- import { type SystemOneOptions } from '../system-one/protocol.js';
3
- /** OpenRouter's decision API: System One's questions and answers, at OpenRouter's own endpoint. */
4
- export declare const openRouterDecisions: Protocol<SystemOneOptions>;
2
+ /**
3
+ * OpenRouter's decision API: questions about a state, answered in one call. It takes no options:
4
+ * images and box questions need no opt-in, and a request with an image runs in long context.
5
+ */
6
+ export declare const openRouterDecisions: Protocol<Record<string, never>>;
@@ -1,9 +1,60 @@
1
- import { extendProtocol } from '../../kernel/protocols/mod.js';
2
- import { systemOne } from '../system-one/protocol.js';
3
- /** OpenRouter's decision API: System One's questions and answers, at OpenRouter's own endpoint. */
4
- export const openRouterDecisions = extendProtocol(systemOne, {
1
+ import { z } from 'zod';
2
+ import { splitReferences } from '../../kernel/engine/decision-references.js';
3
+ import { jsonObjectSchema } from '../../kernel/json.js';
4
+ import { defineProtocol } from '../../kernel/protocols/mod.js';
5
+ import { decisionAnswered } from '../system-one/protocol.js';
6
+ /**
7
+ * OpenRouter's decision API: questions about a state, answered in one call. It takes no options:
8
+ * images and box questions need no opt-in, and a request with an image runs in long context.
9
+ */
10
+ export const openRouterDecisions = defineProtocol({
5
11
  id: 'openRouterDecisions',
6
12
  name: 'OpenRouter Decisions',
7
13
  description: "OpenRouter's decision API: questions about a state, answered in one call.", // labels-exempt: builder field description, read by the studio and docs
8
14
  endpoints: { decision: '/alpha/decisions' },
15
+ options: z.strictObject({}),
16
+ create(context) {
17
+ return {
18
+ decide(request) {
19
+ const assets = request.assets ?? {};
20
+ const body = jsonObjectSchema.parse({
21
+ model: context.apiId,
22
+ state: withImages(request.state, assets),
23
+ questions: Object.fromEntries(Object.entries(request.questions).map(([id, question]) => [
24
+ id,
25
+ {
26
+ ...question,
27
+ instructions: withImages(question.instructions, assets),
28
+ ...('criteria' in question && question.criteria !== undefined
29
+ ? { criteria: withImages(question.criteria, assets) }
30
+ : {}),
31
+ },
32
+ ])),
33
+ });
34
+ return decisionAnswered('openRouterDecisions', context.send(body, { stream: false }));
35
+ },
36
+ };
37
+ },
9
38
  });
39
+ /**
40
+ * Each reference becomes its image, where the reference was, as TypeSafe reads references. A string
41
+ * that is one reference becomes the image part; a longer one becomes its text and images in order.
42
+ * why: OpenRouter ignores an `assets` map; it reads image parts in place.
43
+ */
44
+ function withImages(value, assets) {
45
+ if (typeof value === 'string') {
46
+ const pieces = splitReferences(value, assets);
47
+ if (pieces.every((piece) => typeof piece === 'string'))
48
+ return value;
49
+ const parts = pieces.map((piece) => typeof piece === 'string' ? piece : imagePart(piece.asset));
50
+ return parts.length === 1 ? parts[0] : parts;
51
+ }
52
+ if (Array.isArray(value))
53
+ return value.map((item) => withImages(item, assets));
54
+ if (value !== null && typeof value === 'object')
55
+ return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, withImages(item, assets)]));
56
+ return value;
57
+ }
58
+ function imagePart({ mimeType, data }) {
59
+ return { type: 'image_url', image_url: { url: `data:${mimeType};base64,${data}` } };
60
+ }
@@ -0,0 +1,6 @@
1
+ import { type Protocol } from '../../kernel/protocols/mod.js';
2
+ /** What an `openRouterEmbeddings` binding sets in `options`. */
3
+ export interface OpenRouterEmbeddingsOptions {
4
+ encodingFormat?: 'float' | 'base64';
5
+ }
6
+ export declare const openRouterEmbeddings: Protocol<OpenRouterEmbeddingsOptions>;
@@ -38,8 +38,8 @@ function embeddingValues(value) {
38
38
  }
39
39
  return Array.from(new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / Float32Array.BYTES_PER_ELEMENT));
40
40
  }
41
- export const openRouterEmbedding = defineProtocol({
42
- id: 'openRouterEmbedding',
41
+ export const openRouterEmbeddings = defineProtocol({
42
+ id: 'openRouterEmbeddings',
43
43
  name: 'OpenRouter Embeddings',
44
44
  description: 'OpenRouter embedding models, including supported multimodal models.',
45
45
  endpoints: { embedding: '/embeddings' },
@@ -1,7 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { bearer, defineProvider, endpointUrlSchema, } from '../../kernel/protocols/provider.js';
3
3
  import { openRouterDecisions } from './decisions-protocol.js';
4
- import { openRouterEmbedding } from './embedding-protocol.js';
4
+ import { openRouterEmbeddings } from './embeddings-protocol.js';
5
5
  import { openRouterImages } from './images-protocol.js';
6
6
  import { openRouterOpenAi } from './protocol.js';
7
7
  const connection = z.strictObject({
@@ -42,7 +42,7 @@ export const openRouterProvider = defineProvider({
42
42
  const decisionUrl = at.decisionUrl ?? 'https://openrouter.ai/api/alpha/decisions';
43
43
  return [
44
44
  { protocol: openRouterOpenAi, baseUrl, headers },
45
- { protocol: openRouterEmbedding, baseUrl, headers },
45
+ { protocol: openRouterEmbeddings, baseUrl, headers },
46
46
  { protocol: openRouterImages, baseUrl, headers },
47
47
  { protocol: openRouterDecisions, baseUrl: decisionUrl, url: () => decisionUrl, headers },
48
48
  ];
@@ -1,4 +1,6 @@
1
+ import { type JsonObject } from '../../kernel/json.js';
1
2
  import { type Protocol } from '../../kernel/protocols/mod.js';
3
+ import type { DecisionResult } from '../../kernel/types.js';
2
4
  /** What a System One binding may set. Both are TypeSafe's opt-in "danger zone" headers. */
3
5
  export interface SystemOneOptions {
4
6
  images?: boolean;
@@ -6,3 +8,5 @@ export interface SystemOneOptions {
6
8
  }
7
9
  /** TypeSafe's decision API: questions about a state, answered in one call. */
8
10
  export declare const systemOne: Protocol<SystemOneOptions>;
11
+ /** The first decision reply, read into the kernel's result. Shared by every decision API that answers as System One does. */
12
+ export declare function decisionAnswered(protocol: string, rows: AsyncIterable<JsonObject>): Promise<DecisionResult>;
@@ -56,34 +56,35 @@ export const systemOne = defineProtocol({
56
56
  ? { assets: assetsOnWire(request.assets) }
57
57
  : {}),
58
58
  });
59
- for await (const row of context.send(body, { stream: false, headers })) {
60
- const parsed = reply.safeParse(row);
61
- if (!parsed.success)
62
- throw new TheoremError('bad_response', labelText('protocol.reply_shape', {
63
- protocol: 'systemOne',
64
- issues: z.prettifyError(parsed.error),
65
- }));
66
- const { model, answers, usage } = parsed.data;
67
- // invariant: The kernel validates every answer against its question before exposing it.
68
- return {
69
- model,
70
- answers: answers,
71
- ...(usage
72
- ? {
73
- usage: {
74
- inputTokens: usage.input_tokens,
75
- outputTokens: usage.output_tokens,
76
- ...(usage.cost === undefined ? {} : { costUsd: usage.cost }),
77
- },
78
- }
79
- : {}),
80
- };
81
- }
82
- throw new TheoremError('bad_response', labelText('provider.decision_failed'));
59
+ return await decisionAnswered('systemOne', context.send(body, { stream: false, headers }));
83
60
  },
84
61
  };
85
62
  },
86
63
  });
64
+ /** The first decision reply, read into the kernel's result. Shared by every decision API that answers as System One does. */
65
+ export async function decisionAnswered(protocol, rows) {
66
+ for await (const row of rows) {
67
+ const parsed = reply.safeParse(row);
68
+ if (!parsed.success)
69
+ throw new TheoremError('bad_response', labelText('protocol.reply_shape', { protocol, issues: z.prettifyError(parsed.error) }));
70
+ const { model, answers, usage } = parsed.data;
71
+ // invariant: The kernel validates every answer against its question before exposing it.
72
+ return {
73
+ model,
74
+ answers: answers,
75
+ ...(usage
76
+ ? {
77
+ usage: {
78
+ inputTokens: usage.input_tokens,
79
+ outputTokens: usage.output_tokens,
80
+ ...(usage.cost === undefined ? {} : { costUsd: usage.cost }),
81
+ },
82
+ }
83
+ : {}),
84
+ };
85
+ }
86
+ throw new TheoremError('bad_response', labelText('provider.decision_failed'));
87
+ }
87
88
  const FORMATS = { 'image/png': 'png', 'image/jpeg': 'jpeg' };
88
89
  function assetsOnWire(assets) {
89
90
  return Object.fromEntries(Object.entries(assets).map(([reference, { mimeType, data }]) => [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@theoremjs/agents",
3
- "version": "0.3.4-next.482.g7e608f2",
3
+ "version": "0.3.4-next.508.geda6db0",
4
4
  "description": "A flat TypeScript agent kernel for typed profiles, deterministic turn execution, registered tools, provider adapters, guardrails, and host-injected traces.",
5
5
  "keywords": [
6
6
  "agent",
@@ -1,7 +0,0 @@
1
- import { type Protocol } from '../../kernel/protocols/mod.js';
2
- /** What an `ollamaEmbedding` binding sets in `options`. */
3
- export interface OllamaEmbeddingOptions {
4
- truncate?: boolean;
5
- dimensions?: number;
6
- }
7
- export declare const ollamaEmbedding: Protocol<OllamaEmbeddingOptions>;
@@ -1,6 +0,0 @@
1
- import { type Protocol } from '../../kernel/protocols/mod.js';
2
- /** What an `openRouterEmbedding` binding sets in `options`. */
3
- export interface OpenRouterEmbeddingOptions {
4
- encodingFormat?: 'float' | 'base64';
5
- }
6
- export declare const openRouterEmbedding: Protocol<OpenRouterEmbeddingOptions>;