theorum 0.1.13 → 0.1.14

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.
@@ -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 |