@theoremjs/agents 0.3.4-next.484.g20d8b9d → 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 (68) 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.js +11 -8
  63. package/esm/src/providers/openrouter/embeddings-protocol.d.ts +6 -0
  64. package/esm/src/providers/openrouter/{embedding-protocol.js → embeddings-protocol.js} +2 -2
  65. package/esm/src/providers/openrouter/provider.js +2 -2
  66. package/package.json +1 -1
  67. package/esm/src/providers/local/embedding-protocol.d.ts +0 -7
  68. package/esm/src/providers/openrouter/embedding-protocol.d.ts +0 -6
@@ -7,6 +7,7 @@ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
7
7
  import { replyIsJudged } from '../../../guardrails/progressive-yield.js';
8
8
  import { recordTaint } from '../../../guardrails/tool-result.js';
9
9
  import { waitDefault } from '../../../providers/shared/retry.js';
10
+ import { ANSWER_TOOL, answersByTool, withAnswerTool } from '../../answer-tool.js';
10
11
  import { stepReplyMessages } from '../../interaction-parts.js';
11
12
  import { profileTurnOutputs } from '../../registry/profile-outputs.js';
12
13
  import { providerCompleteRequest } from '../../registry/provider-request.js';
@@ -60,14 +61,14 @@ function holdModelCall(calls, tool) {
60
61
  return true;
61
62
  }
62
63
  /** The step's request, with what it gives the model noted on `state`, and its call's usage and trace. */
63
- function startProviderCall(generation, system, state, answerOnly) {
64
+ function startProviderCall(generation, system, state, answerOnly, labels) {
64
65
  const usage = startCallUsage(system.text, {
65
66
  history: state.currentHistory,
66
67
  input: generation.input,
67
68
  });
68
69
  state.lastCall = usage;
69
70
  const genForStep = generationForProviderStep(generation, state, answerOnly);
70
- const request = providerCompleteRequest(state.tools, genForStep, system.text);
71
+ const request = withAnswerTool(providerCompleteRequest(state.tools, genForStep, system.text), generation, labels);
71
72
  addRequestUrls(state.givenUrls, request);
72
73
  addRequestDestinations(state.destinations, request, state.givenUrls.own);
73
74
  if (generation.canary && !state.canaryGiven) {
@@ -118,6 +119,42 @@ function tallyCallEvent(event, tally, call) {
118
119
  tally.replied = true;
119
120
  return false;
120
121
  }
122
+ /**
123
+ * The structured output the call gave through the answer tool, shown as the step's, with the answer
124
+ * tool's calls taken out of the step's calls. An answer ends the turn, so the step's other calls do
125
+ * not run, and the call that gave it completed.
126
+ */
127
+ async function* showAnswer(shown, tally, step) {
128
+ if (!answersByTool(step.generation))
129
+ return;
130
+ const answer = tally.calls.find((call) => call.name === ANSWER_TOOL && !call.failure);
131
+ tally.calls = answer ? [] : tally.calls.filter((call) => call.name !== ANSWER_TOOL);
132
+ if (!answer)
133
+ return;
134
+ if (tally.stop?.kind === 'tool') {
135
+ tally.stop = { kind: 'completed' };
136
+ step.state.lastStop = tally.stop;
137
+ }
138
+ const event = { type: 'structured', structured: answer.arguments };
139
+ if (tallyCallEvent(event, tally, step.call))
140
+ return;
141
+ if (releasesNow(event, step.control, step.state, step.buffer))
142
+ yield* shown.show(event);
143
+ }
144
+ /**
145
+ * A tool call's preview. The answer tool's streams as the structured reply's draft.
146
+ *
147
+ * why: A preview of the call that follows, which owns the arguments: not the turn's record. It streams
148
+ * as the reply's text does, through its own gate, and like that text is held only when delivery is buffered.
149
+ */
150
+ async function* showToolDraft(event, shown, generation, buffer) {
151
+ if (buffer.holdVisible)
152
+ return;
153
+ if (event.name === ANSWER_TOOL && answersByTool(generation))
154
+ yield* shown.draft(event.text);
155
+ else
156
+ yield event;
157
+ }
121
158
  /** What sets one model call's text apart from the reply text before it: a blank line. */
122
159
  const CALL_BREAK = '\n\n';
123
160
  /**
@@ -130,7 +167,8 @@ function withCallBreak(event, tally, state, generation) {
130
167
  if (tally.text)
131
168
  return event;
132
169
  const before = state.replyText;
133
- const breaks = before && generation.structured === null && !/\s$/.test(before) && !/^\s/.test(event.text);
170
+ const prose = generation.structured === null || answersByTool(generation);
171
+ const breaks = before && prose && !/\s$/.test(before) && !/^\s/.test(event.text);
134
172
  tally.replyAt = before.length + (breaks ? CALL_BREAK.length : 0);
135
173
  return breaks ? { ...event, text: `${CALL_BREAK}${event.text}` } : event;
136
174
  }
@@ -194,7 +232,7 @@ async function* executeAutonomousStep(args, state, buffer = {
194
232
  holdVisible: false,
195
233
  }) {
196
234
  const { profile, generation, system, provider, signal } = args;
197
- const { usage, genForStep, request, call } = startProviderCall(generation, system, state, isAnswerOnlyStep(profile, generation, state.stepCount));
235
+ const { usage, genForStep, request, call } = startProviderCall(generation, system, state, isAnswerOnlyStep(profile, generation, state.stepCount), profile.labels);
198
236
  const tally = { calls: [], reasoning: [], text: '', failures: [], replied: false };
199
237
  const control = {
200
238
  withholdVisible: false,
@@ -209,7 +247,7 @@ async function* executeAutonomousStep(args, state, buffer = {
209
247
  canaryGiven: state.canaryGiven,
210
248
  ...(state.ownTools ? { ownTools: state.ownTools } : {}),
211
249
  };
212
- const shown = new StructuredDrafts(seen);
250
+ const shown = new StructuredDrafts({ ...seen, generation });
213
251
  try {
214
252
  for await (const event of yieldProviderEvents({
215
253
  ...seen,
@@ -219,6 +257,7 @@ async function* executeAutonomousStep(args, state, buffer = {
219
257
  signal,
220
258
  control,
221
259
  tools: state.tools,
260
+ answerTool: answersByTool(generation),
222
261
  })) {
223
262
  if (observeCallEvent(usage, event))
224
263
  continue;
@@ -234,14 +273,11 @@ async function* executeAutonomousStep(args, state, buffer = {
234
273
  continue;
235
274
  }
236
275
  if (event.type === 'tool_draft') {
237
- // why: A preview of the call that follows, which owns the arguments: not the turn's record. It streams
238
- // as the reply's text does, through its own gate, and like that text is held only when delivery is buffered.
239
- if (!buffer.holdVisible)
240
- yield event;
276
+ yield* showToolDraft(event, shown, generation, buffer);
241
277
  continue;
242
278
  }
243
279
  // why: The model's history keeps what it wrote; only the reply the host gets is set apart.
244
- const next = event.type === 'text' ? withCallBreak(event, tally, state, genForStep) : event;
280
+ const next = event.type === 'text' ? withCallBreak(event, tally, state, generation) : event;
245
281
  if (tallyCallEvent(event, tally, call))
246
282
  continue;
247
283
  if (!releasesNow(next, control, state, buffer))
@@ -254,6 +290,7 @@ async function* executeAutonomousStep(args, state, buffer = {
254
290
  throw err;
255
291
  }
256
292
  yield* shown.end();
293
+ yield* showAnswer(shown, tally, { generation, state, control, buffer, call });
257
294
  keepStreamCarry(state, control);
258
295
  const resume = resumeOf(args.mayResume, profile, tally);
259
296
  if (!resume) {
@@ -24,18 +24,20 @@ interface OutboundStreamControl {
24
24
  declare function shouldSkipStreamEvent(event: ModelEvent, profile: Profile): boolean;
25
25
  /** Host-visible output a mid-stream block withholds. A thought is never withheld, only omitted from. */
26
26
  declare function isWithheldOnBlock(event: ModelEvent): boolean;
27
+ export type ShownGeneration = Pick<ResolvedGeneration, 'structured' | 'stream' | 'tools'>;
27
28
  /**
28
- * An event as the host gets it, or nothing. The text of a call that asked for structured output is
29
- * that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered` not at all,
30
- * since the `structured` event is the reply whole. The turn keeps it as text, which its checks,
31
- * repairs and history read.
29
+ * An event as the host gets it, or nothing. On a turn that answers through a response format, the
30
+ * text is that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered`
31
+ * not at all, since the `structured` event is the reply whole. The turn keeps it as text, which its
32
+ * checks, repairs and history read. On a turn that answers by tool, the text is progress.
32
33
  */
33
- declare function shownToHost(event: TurnEvent, generation: Pick<ResolvedGeneration, 'structured' | 'stream'>): TurnEvent | undefined;
34
+ declare function shownToHost(event: TurnEvent, generation: ShownGeneration): TurnEvent | undefined;
34
35
  /**
35
36
  * A step's events as the host gets them (`shownToHost`), with a structured reply's drafts read
36
- * first at `reply_structured`, where the whole is read. The reply's text has already passed the
37
- * `reply` gate; a match at `reply_structured` stops the drafts, and the `structured` event still
38
- * arrives as that check leaves it. The held tail is read and released when the step's stream ends.
37
+ * first at `reply_structured`, where the whole is read. The drafts are the reply's text, or the
38
+ * answer tool's arguments as the model writes them. Text has already passed the `reply` gate; a
39
+ * match at `reply_structured` stops the drafts, and the `structured` event still arrives as that
40
+ * check leaves it. The held tail is read and released when the step's stream ends.
39
41
  */
40
42
  declare class StructuredDrafts {
41
43
  private readonly drafts;
@@ -43,6 +45,8 @@ declare class StructuredDrafts {
43
45
  /** Reads the stream's own checks off what the step knows of the turn. */
44
46
  constructor(seen: Pick<StreamArgs, 'profile' | 'generation' | 'privateSystem' | 'givenUrls' | 'canaryGiven' | 'ownTools'>);
45
47
  show(event: TurnEvent): AsyncGenerator<TurnEvent>;
48
+ /** Part of the structured output's JSON, as the model writes it. */
49
+ draft(json: string): AsyncGenerator<TurnEvent>;
46
50
  /** The drafts' tail the gate still holds, once the step's stream has ended. */
47
51
  end(): Generator<TurnEvent>;
48
52
  }
@@ -65,6 +69,8 @@ interface StreamArgs {
65
69
  ownTools?: OwnTools;
66
70
  /** The turn's tools: a draft is read at the boundary its tool's calls are. */
67
71
  tools: ToolRegistry;
72
+ /** True when the turn answers through the answer tool (`answersByTool`). */
73
+ answerTool?: boolean;
68
74
  }
69
75
  declare function yieldProviderEvents(args: StreamArgs): AsyncGenerator<StreamEvent>;
70
76
  export type { OutboundStreamControl, StreamArgs };
@@ -7,6 +7,7 @@ import { resolveGuardrailPolicy } from '../../../guardrails/policy.js';
7
7
  import { createDraftGate, createOutboundProgressiveGate, DraftReader, } from '../../../guardrails/progressive-yield.js';
8
8
  import { EGRESS_RULES } from '../../../guardrails/rules.js';
9
9
  import { thoughtGuardFor, } from '../../../guardrails/thought-guard.js';
10
+ import { ANSWER_TOOL, answersByTool } from '../../answer-tool.js';
10
11
  import { profileTurnOutputs } from '../../registry/profile-outputs.js';
11
12
  import { toolArgumentsBoundary } from '../../tools/execute.js';
12
13
  function shouldSkipStreamEvent(event, profile) {
@@ -69,21 +70,22 @@ function isWithheldOnBlock(event) {
69
70
  return event.type === 'text' || event.type === 'media';
70
71
  }
71
72
  /**
72
- * An event as the host gets it, or nothing. The text of a call that asked for structured output is
73
- * that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered` not at all,
74
- * since the `structured` event is the reply whole. The turn keeps it as text, which its checks,
75
- * repairs and history read.
73
+ * An event as the host gets it, or nothing. On a turn that answers through a response format, the
74
+ * text is that output's JSON: under `sse` it goes out as `structured_draft`, and under `buffered`
75
+ * not at all, since the `structured` event is the reply whole. The turn keeps it as text, which its
76
+ * checks, repairs and history read. On a turn that answers by tool, the text is progress.
76
77
  */
77
78
  function shownToHost(event, generation) {
78
- if (!generation.structured || event.type !== 'text')
79
+ if (!generation.structured || answersByTool(generation) || event.type !== 'text')
79
80
  return event;
80
81
  return generation.stream === false ? undefined : { type: 'structured_draft', text: event.text };
81
82
  }
82
83
  /**
83
84
  * A step's events as the host gets them (`shownToHost`), with a structured reply's drafts read
84
- * first at `reply_structured`, where the whole is read. The reply's text has already passed the
85
- * `reply` gate; a match at `reply_structured` stops the drafts, and the `structured` event still
86
- * arrives as that check leaves it. The held tail is read and released when the step's stream ends.
85
+ * first at `reply_structured`, where the whole is read. The drafts are the reply's text, or the
86
+ * answer tool's arguments as the model writes them. Text has already passed the `reply` gate; a
87
+ * match at `reply_structured` stops the drafts, and the `structured` event still arrives as that
88
+ * check leaves it. The held tail is read and released when the step's stream ends.
87
89
  */
88
90
  class StructuredDrafts {
89
91
  drafts;
@@ -102,9 +104,15 @@ class StructuredDrafts {
102
104
  yield shown;
103
105
  return;
104
106
  }
105
- const text = await this.drafts.read(shown.text);
107
+ yield* this.draft(shown.text);
108
+ }
109
+ /** Part of the structured output's JSON, as the model writes it. */
110
+ async *draft(json) {
111
+ if (this.generation.stream === false)
112
+ return;
113
+ const text = await this.drafts.read(json);
106
114
  if (text)
107
- yield { ...shown, text };
115
+ yield { type: 'structured_draft', text };
108
116
  }
109
117
  /** The drafts' tail the gate still holds, once the step's stream has ended. */
110
118
  *end() {
@@ -131,11 +139,13 @@ class ToolDrafts {
131
139
  policy;
132
140
  context;
133
141
  tools;
142
+ answerTool;
134
143
  drafts = new Map();
135
- constructor(policy, context, tools) {
144
+ constructor(policy, context, tools, answerTool) {
136
145
  this.policy = policy;
137
146
  this.context = context;
138
147
  this.tools = tools;
148
+ this.answerTool = answerTool;
139
149
  }
140
150
  /**
141
151
  * Shows `event` when it is a draft and says whether it was; any other event closes its call's
@@ -178,7 +188,9 @@ class ToolDrafts {
178
188
  this.drafts.delete(event.tool.callId);
179
189
  }
180
190
  async open(event) {
181
- const boundary = toolArgumentsBoundary(this.tools, event.name);
191
+ // why: The answer's drafts are the structured output's, read at `reply_structured` by `StructuredDrafts`.
192
+ const answer = this.answerTool && event.name === ANSWER_TOOL;
193
+ const boundary = answer ? undefined : toolArgumentsBoundary(this.tools, event.name);
182
194
  const gate = boundary ? await createDraftGate(this.policy, this.context, boundary) : null;
183
195
  const draft = { name: event.name, gate };
184
196
  this.drafts.set(event.callId, draft);
@@ -216,7 +228,7 @@ async function* yieldProviderEvents(args) {
216
228
  const { canaryCarry, thoughtCarry } = control ?? {};
217
229
  const gate = await createOutboundProgressiveGate(policy, context, 'reply', canaryCarry);
218
230
  const thoughts = await thoughtGuardFor(policy, context, thoughtCarry);
219
- const drafts = new ToolDrafts(policy, context, args.tools);
231
+ const drafts = new ToolDrafts(policy, context, args.tools, args.answerTool === true);
220
232
  /** The streamed event whose reply sits in the gate's lookback; released tails keep its shape. */
221
233
  let pendingStream = null;
222
234
  let withholdVisible = false;
@@ -29,7 +29,7 @@ export { outOfScopeFields, profileTypesForField } from './profile-scope.js';
29
29
  export type { DecisionModelBinding, ModelBinding, RuntimeAbilities } from './protocols/binding.js';
30
30
  export { checkBinding, commonModelSettingsSchema, decisionModelBindingSchema, keySlotSchema, keySlotsOf, modelBindingSchema, } from './protocols/binding.js';
31
31
  export type { ProviderLimits } from './protocols/limits.js';
32
- export { providerLimitsSchema, resolveProviderLimits } from './protocols/limits.js';
32
+ export { providerLimitsSchema } from './protocols/limits.js';
33
33
  export type { EstimatedCost, ModelPrice, PriceTable, Rates } from './protocols/prices.js';
34
34
  export { costFromPrices } from './protocols/prices.js';
35
35
  export type { Continuation, DecisionCall, EmbeddingCall, EmbeddingMediaKind, EmbeddingModelLimits, ExtensionBodyAt, ExtensionReader, LiveToolResult, ModelProfileType, Protocol, ProtocolContentEvent, ProtocolContext, ProtocolDefinition, ProtocolEvent, ProtocolExtension, ProtocolOperations, ProtocolRequest, ProtocolSession, ProtocolSocket, SendInit, UpstreamFailure, } from './protocols/protocol.js';
@@ -18,7 +18,7 @@ export { PROFILE_GRAPH, profileGraphFacet, spineFacetsForProfileType, } from './
18
18
  export { unsetValue } from './profile-presence.js';
19
19
  export { outOfScopeFields, profileTypesForField } from './profile-scope.js';
20
20
  export { checkBinding, commonModelSettingsSchema, decisionModelBindingSchema, keySlotSchema, keySlotsOf, modelBindingSchema, } from './protocols/binding.js';
21
- export { providerLimitsSchema, resolveProviderLimits } from './protocols/limits.js';
21
+ export { providerLimitsSchema } from './protocols/limits.js';
22
22
  export { costFromPrices } from './protocols/prices.js';
23
23
  export { carriesTo, defineProtocol, extendProtocol, modelOptions } from './protocols/protocol.js';
24
24
  export { bearer, createProviderRegistry, defineProvider, endpointUrlSchema, header, query, } from './protocols/provider.js';
@@ -40,6 +40,18 @@ export const PROFILE_FIELD_PRESENCE = {
40
40
  },
41
41
  'models.*.allowEffortSelect': { unset: 'Off', value: false },
42
42
  'models.*.timeoutMs': { unset: 'No timeout' },
43
+ 'models.*.limits': { unset: 'No limits' },
44
+ ...Object.fromEntries([
45
+ 'operationTimeoutMs',
46
+ 'connectTimeoutMs',
47
+ 'liveStepTimeoutMs',
48
+ 'maxWireRecordBytes',
49
+ 'maxWireResponseBytes',
50
+ 'maxEventBytes',
51
+ 'maxStepBytes',
52
+ 'maxStepEvents',
53
+ 'maxPendingToolCalls',
54
+ ].map((field) => [`models.*.limits.${field}`, { unset: 'No limit' }])),
43
55
  'models.*.compaction': { unset: 'Off' },
44
56
  'models.*.compaction.maxTokens': { required: true },
45
57
  'models.*.compaction.compactAt': { required: true },
@@ -25,10 +25,10 @@ const MODEL_PROFILE_TYPES = [
25
25
  const TURN_TYPES = ['text', 'image', 'speech', 'live'];
26
26
  /** Types whose `inputs` are turn inputs (text, files, slots) rather than decision state. */
27
27
  const TURN_INPUT_TYPES = ['text', 'image'];
28
- const TURN_INPUT_REASON = 'a decision takes JSON state, not turn text, files, or slots';
28
+ const TURN_INPUT_REASON = 'a decision takes JSON state, and live takes its input through live.ingress';
29
29
  /** Types that take slots and context: the turn types, and a call. */
30
30
  const SLOT_TYPES = ['text', 'image', 'live'];
31
- const SLOT_REASON = 'a decision takes JSON state, and speech reads its transcript only';
31
+ const SLOT_REASON = 'a decision takes JSON state, and an embedding call embeds its input alone';
32
32
  /** Guardrails a host profile may set keep `host`; the rest guard a model turn. */
33
33
  function turnGuardrailTypes(key) {
34
34
  return HOST_GUARDRAIL_FIELDS.includes(key)
@@ -79,11 +79,11 @@ export const PROFILE_FIELD_SCOPE = {
79
79
  },
80
80
  'identity.system': {
81
81
  profileTypes: ['text', 'image', 'live'],
82
- reason: 'speech has no system channel (the input text is the transcript) and a decision is asked only through its questions',
82
+ reason: 'speech has no system channel (the input text is the transcript), a decision is asked only through its questions, and an embedding call embeds its input alone',
83
83
  },
84
84
  'identity.systemByRole': {
85
85
  profileTypes: ['text', 'image', 'live'],
86
- reason: 'speech has no system channel (the input text is the transcript) and a decision is asked only through its questions',
86
+ reason: 'speech has no system channel (the input text is the transcript), a decision is asked only through its questions, and an embedding call embeds its input alone',
87
87
  },
88
88
  models: { profileTypes: MODEL_PROFILE_TYPES, reason: 'a host profile runs no model' },
89
89
  'models.*.protocol': {
@@ -96,7 +96,7 @@ export const PROFILE_FIELD_SCOPE = {
96
96
  },
97
97
  'models.*.compaction': {
98
98
  profileTypes: ['text', 'image', 'speech'],
99
- reason: 'live compacts with live.contextCompression',
99
+ reason: 'live compacts with live.contextCompression, and decision and embedding calls keep no conversation to compact',
100
100
  },
101
101
  'profile.defaultModel': {
102
102
  profileTypes: [...TURN_TYPES, 'embedding'],
@@ -116,7 +116,7 @@ export const PROFILE_FIELD_SCOPE = {
116
116
  live: { profileTypes: ['live'], reason: 'only live profiles open a realtime session' },
117
117
  tools: {
118
118
  profileTypes: ['text', 'image', 'live', 'host'],
119
- reason: 'speech and decision profiles call no tools',
119
+ reason: 'speech, decision, and embedding profiles call no tools',
120
120
  },
121
121
  'tools.t1Policy': {
122
122
  profileTypes: ['text', 'image'],
@@ -132,7 +132,7 @@ export const PROFILE_FIELD_SCOPE = {
132
132
  },
133
133
  'inputs.voice': {
134
134
  profileTypes: ['text'],
135
- reason: 'no image model reads audio, and a decision takes JSON state',
135
+ reason: 'no image model reads audio, live takes audio through live.ingress, a decision takes JSON state, and an embedding call takes text and files',
136
136
  },
137
137
  'inputs.text': {
138
138
  profileTypes: [...TURN_INPUT_TYPES, 'embedding'],
@@ -176,7 +176,7 @@ export const PROFILE_FIELD_SCOPE = {
176
176
  },
177
177
  outputs: {
178
178
  profileTypes: ['text', 'image', 'speech'],
179
- reason: 'live output is the realtime session, and decision and host profiles produce no turn output',
179
+ reason: 'live output is the realtime session, and decision, embedding, and host profiles produce no turn output',
180
180
  },
181
181
  'outputs.structured': {
182
182
  profileTypes: ['text'],
@@ -205,27 +205,27 @@ export const PROFILE_FIELD_SCOPE = {
205
205
  },
206
206
  'guardrails.quota': {
207
207
  profileTypes: turnGuardrailTypes('quota'),
208
- reason: 'quota counts model turns',
208
+ reason: 'quota counts model turns, and decision, embedding, and host profiles run none',
209
209
  },
210
210
  'guardrails.detect': {
211
211
  profileTypes: turnGuardrailTypes('detect'),
212
- reason: 'the decision path runs none of the turn guardrails',
212
+ reason: 'decision and embedding calls run none of the turn guardrails',
213
213
  },
214
214
  'guardrails.blockedReply': {
215
215
  profileTypes: ['text', 'image', 'live'],
216
- reason: 'only a reply can be blocked, and speech, decision, and host profiles write none',
216
+ reason: 'only a reply can be blocked, and speech, decision, embedding, and host profiles write none',
217
217
  },
218
218
  'guardrails.network': {
219
219
  profileTypes: ['text', 'image', 'live', 'host'],
220
- reason: 'speech and decision profiles call no tools',
220
+ reason: 'speech, decision, and embedding profiles call no tools',
221
221
  },
222
222
  'guardrails.taint': {
223
223
  profileTypes: ['text', 'image', 'live'],
224
- reason: 'speech and decision profiles call no tools, and a host profile runs no turn to taint',
224
+ reason: 'speech, decision, and embedding profiles call no tools, and a host profile runs no turn to taint',
225
225
  },
226
226
  'guardrails.disclosure': {
227
227
  profileTypes: ['decision'],
228
- reason: 'disclosure gates state leaving a decision profile for its model',
228
+ reason: 'only a decision profile sends state to its model, and disclosure gates that state',
229
229
  },
230
230
  };
231
231
  /**
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
2
  import { type JsonObject } from '../json.js';
3
3
  import { type KeySlot } from '../schema.js';
4
+ import { type ProviderLimits } from './limits.js';
4
5
  import { type ModelProfileType, type Protocol } from './protocol.js';
5
6
  import type { RegisteredProvider } from './provider.js';
6
7
  /** A vault slot name. */
@@ -31,9 +32,10 @@ export interface ModelBinding extends z.infer<typeof commonModelSettingsSchema>
31
32
  keySlot?: string;
32
33
  fallbackKeySlot?: string;
33
34
  options?: JsonObject;
35
+ limits?: ProviderLimits;
34
36
  }
35
37
  /** A decision profile's model: no generation settings, and a time limit. */
36
- export interface DecisionModelBinding extends Pick<ModelBinding, 'protocol' | 'provider' | 'apiId' | 'keySlot' | 'fallbackKeySlot' | 'options'> {
38
+ export interface DecisionModelBinding extends Pick<ModelBinding, 'protocol' | 'provider' | 'apiId' | 'keySlot' | 'fallbackKeySlot' | 'options' | 'limits'> {
37
39
  timeoutMs?: number;
38
40
  }
39
41
  export declare const modelBindingSchema: z.ZodType<ModelBinding>;
@@ -3,6 +3,7 @@ import { TheoremError } from '../../guardrails/error.js';
3
3
  import { labelText } from '../../guardrails/labels.js';
4
4
  import { jsonObjectSchema } from '../json.js';
5
5
  import { KEY_SLOT_NAME } from '../schema.js';
6
+ import { providerLimitsSchema } from './limits.js';
6
7
  import { modelOptions } from './protocol.js';
7
8
  /** A vault slot name. */
8
9
  export const keySlotSchema = z.string().regex(KEY_SLOT_NAME);
@@ -61,6 +62,10 @@ const modelBinding = z.strictObject({
61
62
  .optional()
62
63
  // labels-exempt: builder field description, read by the studio and docs
63
64
  .describe("What this model's protocol takes beyond the shared settings."),
65
+ limits: providerLimitsSchema
66
+ .optional()
67
+ // labels-exempt: builder field description, read by the studio and docs
68
+ .describe("Theorem's cut-offs for this model's calls; each one left out is no limit."),
64
69
  });
65
70
  true;
66
71
  export const modelBindingSchema = modelBinding;
@@ -72,6 +77,7 @@ const decisionModelBinding = modelBinding
72
77
  keySlot: true,
73
78
  fallbackKeySlot: true,
74
79
  options: true,
80
+ limits: true,
75
81
  })
76
82
  .extend({
77
83
  timeoutMs: z
@@ -1,15 +1,14 @@
1
1
  import { z } from 'zod';
2
- /** The budgets every provider call runs under; a host may tighten or loosen each. */
2
+ /** The budgets a provider call runs under. Each one left out is no limit: the provider's own applies. */
3
3
  export interface ProviderLimits {
4
- operationTimeoutMs: number;
5
- connectTimeoutMs: number;
6
- liveStepTimeoutMs: number;
7
- maxWireRecordBytes: number;
8
- maxWireResponseBytes: number;
9
- maxEventBytes: number;
10
- maxStepBytes: number;
11
- maxStepEvents: number;
12
- maxPendingToolCalls: number;
4
+ operationTimeoutMs?: number;
5
+ connectTimeoutMs?: number;
6
+ liveStepTimeoutMs?: number;
7
+ maxWireRecordBytes?: number;
8
+ maxWireResponseBytes?: number;
9
+ maxEventBytes?: number;
10
+ maxStepBytes?: number;
11
+ maxStepEvents?: number;
12
+ maxPendingToolCalls?: number;
13
13
  }
14
14
  export declare const providerLimitsSchema: z.ZodType<ProviderLimits>;
15
- export declare function resolveProviderLimits(overrides?: Partial<ProviderLimits>): ProviderLimits;
@@ -2,27 +2,13 @@ import { z } from 'zod';
2
2
  const positiveBudget = z.number().int().positive().max(Number.MAX_SAFE_INTEGER);
3
3
  const timerBudget = positiveBudget.max(2147483647);
4
4
  export const providerLimitsSchema = z.strictObject({
5
- operationTimeoutMs: timerBudget,
6
- connectTimeoutMs: timerBudget,
7
- liveStepTimeoutMs: timerBudget,
8
- maxWireRecordBytes: positiveBudget,
9
- maxWireResponseBytes: positiveBudget,
10
- maxEventBytes: positiveBudget,
11
- maxStepBytes: positiveBudget,
12
- maxStepEvents: positiveBudget,
13
- maxPendingToolCalls: positiveBudget,
5
+ operationTimeoutMs: timerBudget.optional(),
6
+ connectTimeoutMs: timerBudget.optional(),
7
+ liveStepTimeoutMs: timerBudget.optional(),
8
+ maxWireRecordBytes: positiveBudget.optional(),
9
+ maxWireResponseBytes: positiveBudget.optional(),
10
+ maxEventBytes: positiveBudget.optional(),
11
+ maxStepBytes: positiveBudget.optional(),
12
+ maxStepEvents: positiveBudget.optional(),
13
+ maxPendingToolCalls: positiveBudget.optional(),
14
14
  });
15
- export function resolveProviderLimits(overrides = {}) {
16
- return Object.freeze(providerLimitsSchema.parse({
17
- operationTimeoutMs: 120000,
18
- connectTimeoutMs: 30000,
19
- liveStepTimeoutMs: 120000,
20
- maxWireRecordBytes: 16 * 1024 * 1024,
21
- maxWireResponseBytes: 64 * 1024 * 1024,
22
- maxEventBytes: 16 * 1024 * 1024,
23
- maxStepBytes: 64 * 1024 * 1024,
24
- maxStepEvents: 100000,
25
- maxPendingToolCalls: 32,
26
- ...overrides,
27
- }));
28
- }
@@ -3,7 +3,7 @@ import { type JsonObject } from '../json.js';
3
3
  import type { KeyVault } from '../registry/vault.js';
4
4
  import type { DecisionResult, KeySlot } from '../types.js';
5
5
  import { type DecisionModelBinding, type ModelBinding } from './binding.js';
6
- import { type ProviderLimits } from './limits.js';
6
+ import type { ProviderLimits } from './limits.js';
7
7
  import type { DecisionCall, EmbeddingCall, EmbeddingResult, ModelProfileType, ProtocolOperations } from './protocol.js';
8
8
  import type { RegisteredProvider } from './provider.js';
9
9
  /** Opens a WebSocket with the headers and subprotocols a sign-in put on it. */
@@ -18,7 +18,8 @@ export type OpenWebSocket = (url: string, init: {
18
18
  export declare function plainWebSocket(provider: string): OpenWebSocket;
19
19
  /** What a host lends every provider call: its keys, its network, its clock, and its budgets. */
20
20
  export interface ProviderHostOptions {
21
- limits?: Partial<ProviderLimits>;
21
+ /** Overrides the model's own `limits`, one field at a time. */
22
+ limits?: ProviderLimits;
22
23
  vault?: KeyVault;
23
24
  fetch?: typeof fetch;
24
25
  wait?: Wait;
@@ -7,7 +7,6 @@ import { KEY_FALLBACK_ROW } from '../../providers/types.js';
7
7
  import { jsonValueSchema } from '../json.js';
8
8
  import { bytesToBase64 } from '../util/base64.js';
9
9
  import { bindingServe, keySlotsOf, } from './binding.js';
10
- import { resolveProviderLimits } from './limits.js';
11
10
  import { costFromPrices } from './prices.js';
12
11
  /**
13
12
  * The runtime's own WebSocket. It takes no headers, so a sign-in that needs them is refused; a host
@@ -249,7 +248,7 @@ function protocolContext(setup, slot, raw, wire = { sent: false }, asked = []) {
249
248
  apiId,
250
249
  options: setup.binding.options ?? {},
251
250
  builtInTools,
252
- limits: setup.limits ?? resolveProviderLimits(),
251
+ limits: setup.limits ?? setup.binding.limits ?? {},
253
252
  ...(setup.signal ? { signal: setup.signal } : {}),
254
253
  tapUpstream: tap,
255
254
  async *send(body, init) {
@@ -11,9 +11,9 @@ export declare class ProviderBudget {
11
11
  private stepBytes;
12
12
  private stepEvents;
13
13
  private calls;
14
- private deadline;
14
+ private deadline?;
15
15
  private releaseSignal;
16
- constructor(limits: ProviderLimits, caller?: AbortSignal, timeoutMs?: number);
16
+ constructor(limits: ProviderLimits, caller?: AbortSignal, timeoutMs?: number | undefined);
17
17
  resumeOperation(): void;
18
18
  /** Milliseconds before the running timer ends the call. */
19
19
  timeLeft(): number;