theorum 0.1.13 → 0.1.15

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.
@@ -45,8 +45,8 @@ function resetTools() {
45
45
  }
46
46
  Object.assign(CATALOG.tools, HARNESS_TOOLS);
47
47
  }
48
- /** Gemini Interactions inline MIME part type (adapter wire map). */
49
- const GEMINI_INPUT_KINDS = {
48
+ /** MIME essence normalized media part category (shared ingress map). */
49
+ const MEDIA_INPUT_KINDS = {
50
50
  'image/png': 'image',
51
51
  'image/jpeg': 'image',
52
52
  'image/jpg': 'image',
@@ -95,8 +95,8 @@ function mimeAllowed(accept, mime) {
95
95
  return allowed === actual;
96
96
  });
97
97
  }
98
- function geminiKindForMime(mime) {
99
- return GEMINI_INPUT_KINDS[mimeEssence(mime)];
98
+ function mediaKindForMime(mime) {
99
+ return MEDIA_INPUT_KINDS[mimeEssence(mime)];
100
100
  }
101
101
  /** Require a host-declared model spec for an allowed profile model id. */
102
102
  function requireModelSpec(profile, modelId) {
@@ -132,4 +132,4 @@ function modelEntryByApiId(specs, apiId) {
132
132
  function clampThinkingLevelForApiId(specs, apiId, level) {
133
133
  return clampLevels(modelEntryByApiId(specs, apiId), level);
134
134
  }
135
- export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, geminiKindForMime, getTool, listBuiltinIds, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, };
135
+ export { CATALOG, clampThinkingLevel, clampThinkingLevelForApiId, getTool, listBuiltinIds, mediaKindForMime, mimeAllowed, mimeEssence, modelEntryByApiId, registerTools, requireModelSpec, resetTools, };
@@ -9,7 +9,7 @@ import { TheorumError } from '../../guardrails/error.js';
9
9
  import { wrapUserData } from '../engine/boundary.js';
10
10
  import { synthesizeRepairPrompt } from '../engine/repair.js';
11
11
  import { assertAttachmentLimits, requireMediaLimits } from './attachments.js';
12
- import { geminiKindForMime, mimeAllowed, mimeEssence } from './catalog.js';
12
+ import { mediaKindForMime, mimeAllowed, mimeEssence } from './catalog.js';
13
13
  function listedValue(allowed, value) {
14
14
  if (!value) {
15
15
  return undefined;
@@ -83,10 +83,10 @@ function resolveImageFormat(profile, _model, slots) {
83
83
  size,
84
84
  };
85
85
  }
86
- function assertGeminiMime(mime) {
87
- const kind = geminiKindForMime(mime);
86
+ function assertMediaMime(mime) {
87
+ const kind = mediaKindForMime(mime);
88
88
  if (!kind) {
89
- throw new TheorumError(`MIME '${mime}' is not a Gemini input type`);
89
+ throw new TheorumError(`MIME '${mime}' is not a supported media input type`);
90
90
  }
91
91
  return kind;
92
92
  }
@@ -96,12 +96,12 @@ function mediaParts(profile, model, blobs, channel) {
96
96
  throw new TheorumError(`Profile ${profile.id} does not accept ${channel}`);
97
97
  }
98
98
  const maxInputImages = profile.outputs.image?.maxInputImages;
99
- const imageCount = blobs.filter((blob) => geminiKindForMime(blob.mimeType) === 'image').length;
99
+ const imageCount = blobs.filter((blob) => mediaKindForMime(blob.mimeType) === 'image').length;
100
100
  if (maxInputImages !== undefined && imageCount > maxInputImages) {
101
101
  throw new TheorumError(`At most ${maxInputImages} reference images on ${model}`);
102
102
  }
103
103
  return blobs.map((blob) => {
104
- const kind = assertGeminiMime(blob.mimeType);
104
+ const kind = assertMediaMime(blob.mimeType);
105
105
  if (!mimeAllowed(accept, blob.mimeType)) {
106
106
  throw new TheorumError(`MIME '${blob.mimeType}' is not accepted on ${profile.id}`);
107
107
  }
@@ -21,8 +21,8 @@ export type CustomToolId = HarnessToolId | (string & Record<never, never>);
21
21
  export type ToolId = BuiltinToolId | CustomToolId;
22
22
  /** Id of a host-registered structured output schema. */
23
23
  export type StructuredSchemaId = string;
24
- /** Interactions inline part types Gemini accepts besides text. */
25
- export type GeminiInputKind = 'image' | 'audio' | 'video' | 'document';
24
+ /** Normalized multimodal part category (image, audio, video, document). */
25
+ export type MediaInputKind = 'image' | 'audio' | 'video' | 'document';
26
26
  /** Host-owned profile identifier. */
27
27
  export type ProfileId = string;
28
28
  /** Named Gemini key bucket used by host-provided transports. */
@@ -354,7 +354,7 @@ export interface InteractionTextPart {
354
354
  }
355
355
  /** Inline media part sent to provider adapters after MIME validation. */
356
356
  export interface InteractionMediaPart {
357
- type: GeminiInputKind;
357
+ type: MediaInputKind;
358
358
  mimeType: string;
359
359
  data: string;
360
360
  }
@@ -6,7 +6,7 @@
6
6
  * normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
7
7
  *
8
8
  * Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
9
- * environment variables (see docs/SECRETS.md).
9
+ * environment variables (see src/providers/CONTRACT.md).
10
10
  *
11
11
  * @module
12
12
  */
@@ -6,7 +6,7 @@
6
6
  * normalized `TurnEvent` objects. No external SDK dependency — raw fetch + SSE.
7
7
  *
8
8
  * Hosts pass `baseUrl` explicitly. THEORUM does not read `OLLAMA_HOST` or other
9
- * environment variables (see docs/SECRETS.md).
9
+ * environment variables (see src/providers/CONTRACT.md).
10
10
  *
11
11
  * @module
12
12
  */
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { isAbortError, TheorumError, toErrorEvent } from '../guardrails/error.js';
12
12
  import { eventsFromComplete, eventsFromDelta, extractTokenEvent, groundingFromEvent, tryStructured, } from '../kernel/engine/delta.js';
13
+ import { asRecord } from '../kernel/engine/record.js';
13
14
  import { exposeForTests } from './expose-for-tests.js';
14
15
  import { tapFetch } from './google-tap.js';
15
16
  import { toInteractionsBody } from './interactions.js';
@@ -109,8 +110,32 @@ function foldPayload(event, acc) {
109
110
  if (groundingEvent && !events.some((e) => e.type === 'grounding')) {
110
111
  events.push(groundingEvent);
111
112
  }
113
+ const evidenceEvent = evidenceFromGooglePayload(event);
114
+ if (evidenceEvent && !events.some((e) => e.type === 'evidence')) {
115
+ events.push(evidenceEvent);
116
+ }
112
117
  return events;
113
118
  }
119
+ /** Preserve raw Google Interactions tool payloads so hosts can inspect everything returned. */
120
+ function evidenceFromGooglePayload(event) {
121
+ for (const key of ['delta', 'step']) {
122
+ const payload = asRecord(event[key]);
123
+ if (!payload) {
124
+ continue;
125
+ }
126
+ const type = String(payload.type ?? '');
127
+ if (type === 'google_search_result' ||
128
+ type === 'google_maps' ||
129
+ type === 'google_maps_result' ||
130
+ type.startsWith('google_')) {
131
+ return {
132
+ type: 'evidence',
133
+ evidence: { provider: 'google', raw: payload },
134
+ };
135
+ }
136
+ }
137
+ return undefined;
138
+ }
114
139
  function withTap(req, transport) {
115
140
  return {
116
141
  ...transport,
@@ -172,5 +197,6 @@ exposeForTests('provider', {
172
197
  isCompleteEvent,
173
198
  foldDeltaPayload,
174
199
  foldPayload,
200
+ evidenceFromGooglePayload,
175
201
  withTap,
176
202
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "theorum",
3
- "version": "0.1.13",
3
+ "version": "0.1.15",
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",
@@ -1,227 +0,0 @@
1
- # Compaction
2
-
3
- Optional, profile-driven compaction for managing conversation history when it
4
- approaches a model's context window limit.
5
-
6
- The kernel owns the trigger, the split logic, and the timing. The host (or the
7
- kernel itself, for the easy path) owns the execution and reassembly.
8
-
9
- ## Why
10
-
11
- Long-running conversations accumulate history that eventually exceeds the
12
- model's context window. Compaction types the trigger and split into a config
13
- block; the host still owns how summaries are shaped and persisted.
14
-
15
- ## Configuration
16
-
17
- Compaction is configured per model in a profile's `model.config`:
18
-
19
- ```ts
20
- const speakerModel: ModelSpec = {
21
- apiId: "gemini-3.5-flash",
22
- // ...existing model config...
23
- compaction: {
24
- maxTokens: 2000, // budget for the chosen meter
25
- compactAt: 0.75, // fire when 75% full
26
- previousExchanges: 8, // keep last 8 exchanges verbatim
27
- profile: "my.compactor", // profile that does the compacting
28
- timing: "after", // 'before' or 'after' the turn
29
- meter: "history", // default; or "input"
30
- },
31
- };
32
- ```
33
-
34
- ### `meter`
35
-
36
- What the threshold compares:
37
-
38
- | Value | Meaning |
39
- | --- | --- |
40
- | `'history'` (default) | Conversational history only. Host `input.historyTokens`, else local estimate of `input.history`. Excludes system, tool schemas, and this-turn attachments. |
41
- | `'input'` | Full-prompt provider input tokens. For `timing: 'before'`, pass previous turn's usage as `input.inputTokens`. For `timing: 'after'`, the kernel uses this turn's `tokens.input`. |
42
-
43
- Provider `tokens` events always stay on the stream for billing / observability.
44
- With `meter: 'history'` they do **not** gate compaction. With `meter: 'input'`
45
- they (or host-threaded `inputTokens`) **do**.
46
-
47
- ### `maxTokens`
48
-
49
- Budget compared as `tokens > compactAt * maxTokens`.
50
-
51
- - With `meter: 'history'`: set from the model's context window minus known fixed
52
- costs (system, tools, output headroom).
53
- - With `meter: 'input'`: set as a full-prompt ceiling (or baseline + headroom).
54
- Small history-style budgets (e.g. 2000) will fire often if system + tools are
55
- large — that is expected for this meter.
56
-
57
- ### How `meter: 'history'` counts
58
-
59
- 1. If the host sets `input.historyTokens`, that value is used.
60
- 2. Otherwise the kernel estimates from `input.history` (or `[]`):
61
- - **Text** — tiktoken `o200k_base` (via `gpt-tokenizer`) over `content`,
62
- text `parts`, and `tool_calls` arguments. Declared local BPE; Gemini has
63
- no open JS tokenizer — pass `historyTokens` for Gemini `countTokens`.
64
- The BPE ranks load **lazily on first text estimate** — not when the package
65
- is imported. `meter: 'input'`, host `historyTokens`, empty history, and
66
- media-only estimates never load the tokenizer.
67
- - **Media parts** — minimum stubs when size/duration are unknown (not
68
- payload bytes). Image/document: 258 (one still-image / page unit).
69
- Audio: 32 (1s @ 32/s). Video: 263 (1s @ 263/s). Larger Gemini 2.x images
70
- are 258×tiles; Gemini 3 uses `media_resolution` budgets (often 560–1120).
71
- Prefer `historyTokens` when the host knows better.
72
- - Current-turn `attachments` / `voice` are **not** history.
73
-
74
- The same history estimator is used by `splitForCompaction` for fractional
75
- `previousExchanges` (integer / `0` splits do not need the tokenizer). Empty or
76
- missing history is **0** (does not fire).
77
-
78
- ### `compactAt`
79
-
80
- Fraction of `maxTokens` at which compaction fires. Must be in (0, 1).
81
-
82
- ### `previousExchanges`
83
-
84
- How many recent exchanges to preserve verbatim after compaction. An exchange
85
- starts at each user message and includes all subsequent messages (assistant
86
- replies, tool calls, tool results) until the next user message.
87
-
88
- - **`>= 1` (integer)** — keep that many recent exchanges.
89
- - **`(0, 1)` (fraction)** — retain exchanges that fit within this fraction of
90
- `maxTokens` (history estimate), walking backwards. Must be less than
91
- `compactAt`.
92
- - **`0`** — compact everything; no tail is retained.
93
-
94
- ### `profile`
95
-
96
- Profile id of the compaction agent. Must be registered before the owning
97
- profile.
98
-
99
- ### `timing`
100
-
101
- - **`'before'`** — compact synchronously before the turn.
102
- - **`'after'`** — signal on the `done` event; host runs compaction async.
103
-
104
- ### `trigger`
105
-
106
- Optional custom gate. When set, it **replaces** the default
107
- `tokens > compactAt * maxTokens` check. The kernel still resolves `{ meter,
108
- tokens }` first and passes them as `CompactionTriggerContext` so the host can
109
- combine token pressure with other signals (e.g. free RAM):
110
-
111
- ```ts
112
- compaction: {
113
- maxTokens: 2000,
114
- compactAt: 0.75,
115
- previousExchanges: 8,
116
- profile: "my.compactor",
117
- timing: "after",
118
- trigger: (ctx) =>
119
- ctx.tokens > ctx.compactAt * ctx.maxTokens || hostRamPressure(),
120
- },
121
- ```
122
-
123
- Sync and async triggers are both accepted. Omit `trigger` to keep the default
124
- threshold.
125
-
126
- ## The compaction profile
127
-
128
- A compaction profile is a standard THEORUM profile. A simple summarizer:
129
-
130
- ```ts
131
- registerProfile(
132
- defineProfile({
133
- id: "my.compactor",
134
- identity: {
135
- handle: "Compactor",
136
- system: "Summarize this conversation concisely. Preserve unresolved "
137
- + "issues, decisions made, and key facts. Drop greetings and filler.",
138
- },
139
- model: {
140
- ...modelAllow("gemini35FlashLite"),
141
- thinking: "minimal",
142
- maxSteps: 1,
143
- },
144
- tools: { allow: [] },
145
- inputs: { text: true },
146
- outputs: { structured: "my.summary.schema" },
147
- guardrails: {
148
- canary: false,
149
- sanitizeInput: false,
150
- redactSensitive: false,
151
- },
152
- }),
153
- );
154
- ```
155
-
156
- ## Usage: `meter: 'history'` (default)
157
-
158
- ```ts
159
- const nextReq: TurnRequest = {
160
- profile: "my.agent",
161
- input: {
162
- text: userMessage,
163
- history: conversationHistory,
164
- // optional: historyTokens: hostHistoryCount,
165
- },
166
- };
167
- ```
168
-
169
- ## Usage: `meter: 'input'`
170
-
171
- ```ts
172
- // timing: 'before' — thread previous turn's full-prompt usage
173
- input: {
174
- text: userMessage,
175
- history: conversationHistory,
176
- inputTokens: previousTokensInput,
177
- }
178
-
179
- // timing: 'after' — kernel reads this turn's tokens.input automatically
180
- for await (const event of runTurn(req, provider)) {
181
- if (event.type === "done" && event.compaction?.needed) {
182
- const { history, tokens, meter, promptTokens } = event.compaction;
183
- // meter === 'input'; tokens === promptTokens (when known)
184
- }
185
- }
186
- ```
187
-
188
- If the compaction profile uses a different provider, pass `compactionProvider`
189
- on the `TurnRequest`.
190
-
191
- ## Exported API
192
-
193
- | Export | Kind | Description |
194
- | --- | --- | --- |
195
- | `CompactionSpec` | type | Config on `ModelSpec` |
196
- | `CompactionMeter` | type | `'history' \| 'input'` |
197
- | `CompactionTriggerContext` | type | Args for optional `trigger` |
198
- | `CompactionSignal` | type | `done.compaction`: `meter`, `tokens`, optional `promptTokens`, `history` |
199
- | `CompactionSplit` / `CompactionTokens` | type | Split result / resolved meter count |
200
- | `resolveHistoryTokens` | async function | Host `historyTokens` or local history estimate |
201
- | `resolveCompactionTokens` | async function | Resolve `{ meter, tokens }` for a turn |
202
- | `estimateHistoryTokens` | async function | tiktoken `o200k_base` + media stubs (lazy BPE) |
203
- | `HISTORY_TEXT_ENCODING` | const | `'o200k_base'` |
204
- | `HISTORY_MEDIA_TOKENS` | const | Media minima: image/document 258, audio 32, video 263 |
205
- | `compactionNeeded` | function | `(tokens, spec) => boolean` |
206
- | `shouldCompact` | async function | Custom `trigger` or `compactionNeeded` |
207
- | `splitForCompaction` | async function | `(history, spec) => { toCompact, toRetain }` |
208
-
209
- ## Validation
210
-
211
- At `registerProfile` time:
212
-
213
- - `maxTokens` must be > 0
214
- - `compactAt` must be in (0, 1)
215
- - `previousExchanges` as a fraction must be < `compactAt`
216
- - `previousExchanges` >= 1 must be an integer
217
- - `meter`, when set, must be `'history'` or `'input'`
218
- - The named compaction profile must already be registered
219
-
220
- ## 0.1.13
221
-
222
- - Default meter is **history** (local BPE + media stubs, or host `historyTokens`).
223
- - Optional `meter: 'input'` gates on full-prompt provider usage.
224
- - Budget field is `maxTokens` (not `maxHistoryTokens`).
225
- - `CompactionSignal` uses `meter` + `tokens` (no deprecated aliases).
226
- - Optional `trigger` replaces the default threshold check.
227
- - History BPE (`gpt-tokenizer` / `o200k_base`) loads lazily on first text estimate.
package/docs/SECRETS.md DELETED
@@ -1,65 +0,0 @@
1
- # Theorum Provider Configuration Guide
2
-
3
- Theorum does not own secrets and does not read environment variables.
4
-
5
- Host applications own credentials, runtime configuration, and secret storage. Theorum receives provider configuration as explicit function arguments.
6
-
7
- ## 1. Package Boundary
8
-
9
- - Do not create `.env` files in this repository.
10
- - Do not commit key templates to this repository.
11
- - Do not teach Theorum to discover keys from the shell or process environment.
12
- - Business applications pass credentials into `createProvider`.
13
-
14
- ## 2. Single door: `createProvider`
15
-
16
- ```ts
17
- import { createProvider, runTurn } from 'theorum';
18
-
19
- const provider = createProvider(profile, {
20
- // geminiInteractions / google
21
- gemini: {
22
- vault: {
23
- freeA: hostResolvedFreeAKey,
24
- freeB: hostResolvedFreeBKey,
25
- freeC: hostResolvedFreeCKey,
26
- paid: hostResolvedPaidKey,
27
- },
28
- },
29
- // openAi / openrouter (chat or speech role — same credentials)
30
- openRouter: {
31
- apiKey: hostResolvedOpenRouterKey,
32
- },
33
- // openAi / local (Ollama, llama.cpp, vLLM, LM Studio, …)
34
- // Hosts that honor OLLAMA_HOST should resolve it themselves — THEORUM does not.
35
- local: {
36
- baseUrl: hostResolvedLocalBaseUrl, // optional; default http://127.0.0.1:11434
37
- },
38
- });
39
-
40
- for await (const event of runTurn({ profile: profile.id, input: { text: '…' } }, provider)) {
41
- // …
42
- }
43
- ```
44
-
45
- `createProvider` picks the transport from `profile.model.protocol` / `provider` (and whether the profile is a speech role). Hosts do not choose a separate speech constructor. OpenRouter's Vercel AI SDK dependency loads only when an `openAi` + `openrouter` chat provider first calls `complete` — Google and local paths never import it.
46
-
47
- ## 3. Tracing
48
-
49
- Tracing is silent by default. Hosts opt in by passing a sink to `runTurn`.
50
-
51
- ```ts
52
- import { createProvider, jsonlSink, runTurn } from 'theorum';
53
-
54
- const provider = createProvider(profile, { openRouter: { apiKey: hostResolvedOpenRouterKey } });
55
-
56
- for await (const event of runTurn(request, provider, jsonlSink(hostTraceDir))) {
57
- // stream events
58
- }
59
- ```
60
-
61
- For live verification, pass credentials explicitly:
62
-
63
- ```bash
64
- deno run --allow-net scripts/verify-live.ts --api-key "<host-resolved-key>"
65
- ```
package/docs/STOP.md DELETED
@@ -1,85 +0,0 @@
1
- # Turn stop and resume
2
-
3
- Providers end turns for many reasons (completed, length, tool call, filter,
4
- network drop). THEORUM normalizes those into `TurnStop` on terminal `done`
5
- events so hosts can decide Continue / auto-continue without parsing each
6
- adapter.
7
-
8
- ## `done.stop`
9
-
10
- Adapters attach a `stop` when the turn ends cleanly enough to classify:
11
-
12
- | `kind` | Meaning |
13
- | --- | --- |
14
- | `completed` | Normal completion |
15
- | `length` | Output / budget cut off |
16
- | `tool` | Model requested tool use |
17
- | `filtered` | Content filter |
18
- | `provider_error` | Upstream failure |
19
- | `cancelled` | User / host abort |
20
- | `stream_incomplete` | Stream ended without a terminal reason (tunnel drop, etc.) |
21
-
22
- OpenRouter maps `finish_reason` (+ optional native reason). Google Interactions
23
- maps terminal `status`. Local OpenAI-compat servers use the same OpenRouter
24
- finish-reason mapping. Hosts classify client SSE drops with
25
- `turnStopFromClientStreamEnd`.
26
-
27
- ## Profile resume policy
28
-
29
- Under `outputs.resume`:
30
-
31
- ```ts
32
- outputs: {
33
- structured: null,
34
- resume: {
35
- allowContinue: ['length', 'stream_incomplete', 'provider_error'],
36
- autoContinue: ['length', 'stream_incomplete'],
37
- },
38
- }
39
- ```
40
-
41
- - `allowContinue` — kinds eligible for a Continue CTA / `continueFrom` turn
42
- (default: length, stream_incomplete, provider_error).
43
- - `autoContinue` — kinds the host may silently resume **once** after a short
44
- pause (`AUTO_CONTINUE_DELAY_MS`, 1500). Kernel does not loop; hosts call
45
- `continueFrom` at most once. User `cancelled` is never auto-continued.
46
-
47
- Helpers: `isResumeableStop`, `shouldAutoContinue`, `isUserCancelledStop`.
48
-
49
- ## Continuing a turn
50
-
51
- Pass `continueFrom` on the next `TurnRequest`. The kernel appends the fixed
52
- `CONTINUE_INSTRUCTION` (do not invent per-app continue prompts):
53
-
54
- ```ts
55
- for await (const event of runTurn({
56
- profile: "my.agent",
57
- input: { text: "" },
58
- continueFrom: {
59
- stop: previousDone.stop,
60
- partialText: bufferedAssistantText,
61
- // optional: partialArtifact
62
- },
63
- }, provider)) {
64
- // …
65
- }
66
- ```
67
-
68
- ## Errors
69
-
70
- `GenerationStopError` carries a `stop` for hosts that prefer throw/catch over
71
- stream events. `isGenerationStopError` narrows it.
72
-
73
- ## Exported API
74
-
75
- | Export | Kind | Description |
76
- | --- | --- | --- |
77
- | `TurnStop` / `TurnStopKind` | type | Normalized stop on `done` |
78
- | `TurnContinueFrom` | type | Partial state for resume |
79
- | `ProfileResumeSpec` | type | `outputs.resume` policy |
80
- | `CONTINUE_INSTRUCTION` | const | Fixed continue system text |
81
- | `DEFAULT_AUTO_CONTINUE` | const | Default one-shot auto-continue kinds |
82
- | `AUTO_CONTINUE_DELAY_MS` | const | Suggested pause before auto-continue |
83
- | `isResumeableStop` / `shouldAutoContinue` / `isUserCancelledStop` | function | Host policy helpers |
84
- | `turnStopFromOpenRouter` / `turnStopFromInteractionStatus` / `turnStopFromClientStreamEnd` | function | Provider / client mappers |
85
- | `GenerationStopError` / `isGenerationStopError` | class / function | Optional throw path |