@alma-harness/core 0.1.0

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.
@@ -0,0 +1,1054 @@
1
+ /**
2
+ * Tenancy scope — the isolation boundary of the whole harness.
3
+ *
4
+ * `{org, uid}` derives from a server-verified token, never from a client
5
+ * payload, and is bound to tool registries by closure at construction time —
6
+ * the model never passes org/uid as parameters.
7
+ *
8
+ * @see docs/architecture.md §6.1, §6.8
9
+ */
10
+ interface Scope {
11
+ /** Organization (tenant) id. */
12
+ readonly org: string;
13
+ /** User id within the organization. */
14
+ readonly uid: string;
15
+ }
16
+ /**
17
+ * Thrown by {@link scopePath} when a scope segment could be used for path
18
+ * traversal or key-delimiter injection.
19
+ */
20
+ declare class InvalidScopeError extends Error {
21
+ constructor(segment: "org" | "uid", value: string);
22
+ }
23
+ /**
24
+ * The single place that builds `tenants/{org}/users/{uid}` — §6.8.
25
+ *
26
+ * A *concept*, not a literal storage path: the Firestore adapter renders it as
27
+ * a collection path, the Postgres adapter as row-level-security predicates
28
+ * (§6). Every store keys its data by this concept so scoped purge and export
29
+ * have one canonical addressing scheme.
30
+ */
31
+ declare function scopePath(scope: Scope): string;
32
+
33
+ /**
34
+ * Neutral message format — §6.1.
35
+ *
36
+ * The harness defines its own message vocabulary; provider adapters translate
37
+ * to/from each provider's wire format. Sessions persist ONLY this format — no
38
+ * opaque CLI transcripts, no derived project keys.
39
+ */
40
+ type MediaKind = "image" | "audio" | "document";
41
+ /**
42
+ * Reference to media held in product-owned storage — §6.1: media travels by
43
+ * reference and is never persisted inline.
44
+ *
45
+ * DECISION: the doc leaves the shape open; we use an opaque URI plus optional
46
+ * metadata. The product decides how the URI is dereferenced (GCS, S3, …); the
47
+ * harness never fetches it implicitly.
48
+ */
49
+ interface MediaRef {
50
+ uri: string;
51
+ contentType?: string;
52
+ bytes?: number;
53
+ /**
54
+ * Display name the person saw — spec 040. Optional, and deliberately not
55
+ * derived from `uri`: a storage path is an id, and the two coincide only by
56
+ * accident.
57
+ *
58
+ * Added for a migration that measured both cases. One store being moved
59
+ * repeats the name inside the message text (168 of 168 messages with an
60
+ * attachment), so the field is redundant there; another carries it in 125 of
61
+ * 125 document messages and in the text in NONE, so it is the only copy.
62
+ * Reconstructing it by parsing prose is the failure mode this avoids.
63
+ */
64
+ filename?: string;
65
+ }
66
+ /** One content block of a message — §6.1. */
67
+ type Block = {
68
+ type: "text";
69
+ text: string;
70
+ } | {
71
+ type: "tool_call";
72
+ id: string;
73
+ name: string;
74
+ input: unknown;
75
+ } | {
76
+ type: "tool_result";
77
+ callId: string;
78
+ output: unknown;
79
+ isError?: boolean;
80
+ } | {
81
+ type: "media";
82
+ kind: MediaKind;
83
+ ref: MediaRef;
84
+ };
85
+ interface MsgMeta {
86
+ /** ISO 8601 timestamp. */
87
+ at: string;
88
+ /** Channel the message arrived on / was sent to (e.g. "whatsapp", "portal"). */
89
+ channel?: string;
90
+ /** Model that produced an assistant message (e.g. "anthropic/<model-id>"). */
91
+ model?: string;
92
+ }
93
+ /** A conversation message in the neutral format — §6.1. */
94
+ interface Msg {
95
+ role: "user" | "assistant" | "tool";
96
+ blocks: Block[];
97
+ meta?: MsgMeta;
98
+ }
99
+
100
+ /**
101
+ * Model client contract — §6.2. One adapter per provider lives in
102
+ * `@alma-harness/providers`; each adapter swallows the provider differences
103
+ * (tool-call shapes, streaming, caching, structured output, token accounting)
104
+ * and has contract tests with recorded fixtures to catch API drift.
105
+ */
106
+ /**
107
+ * The providers Alma knows by name — a CLOSED union, because the price table,
108
+ * the client map, and the routing trail all key on it. Anthropic and OpenAI
109
+ * from birth (§3 principle 4); `openrouter` is the gateway bridge (spec:
110
+ * openrouter-gateway) — a gateway does not answer "who processed this data?"
111
+ * by itself, so its adapter requires a declared upstream allowlist.
112
+ */
113
+ type ProviderId = "anthropic" | "openai" | "openrouter";
114
+ interface ModelRef {
115
+ provider: ProviderId;
116
+ /** Provider-native model id. */
117
+ id: string;
118
+ }
119
+ /**
120
+ * System prompt block carrying the stable/volatile boundary — §6.2, §6.9.
121
+ * Stable blocks (persona, guidance) form the cacheable prefix; volatile blocks
122
+ * (date/time, interlocutor identity, recall) come after it. One discipline
123
+ * optimizes caching on both providers.
124
+ */
125
+ interface SystemBlock {
126
+ text: string;
127
+ volatility: "stable" | "volatile";
128
+ }
129
+ /**
130
+ * Wire spec of a tool as sent to the model — §6.4. Always DERIVED from the
131
+ * scoped registry, never a hand-maintained parallel list: the "tool exists on
132
+ * the server but is missing from the allowlist" bug class ceases to exist.
133
+ */
134
+ interface ToolSpec {
135
+ name: string;
136
+ description: string;
137
+ /** JSON Schema derived from the registered validation schema. */
138
+ inputSchema: Record<string, unknown>;
139
+ }
140
+ /**
141
+ * Token accounting for one model call — §6.2, §6.5.
142
+ *
143
+ * DECISION (spec 005): `inputTokens` EXCLUDES cache reads — it counts
144
+ * uncached, full-price input tokens only. Adapters normalize their wire
145
+ * semantics to this (Anthropic already reports it this way; the OpenAI
146
+ * adapter subtracts `cached_tokens`), so the BudgetGuard prices every
147
+ * provider identically.
148
+ */
149
+ interface Usage {
150
+ inputTokens: number;
151
+ outputTokens: number;
152
+ cacheReadInputTokens?: number;
153
+ cacheWriteInputTokens?: number;
154
+ }
155
+ /**
156
+ * DECISION: neutral stop vocabulary; adapters map provider-specific reasons
157
+ * (e.g. OpenAI `finish_reason`, Anthropic `stop_reason`) onto it.
158
+ * `refusal` (added by spec 002) is a provider-level safety decline — a harness
159
+ * serving people surfaces it as a first-class terminal state, never as a
160
+ * normal `end_turn`. `context_window_exceeded` (spec 002 review) means the
161
+ * conversation no longer fits the model's context window — distinct from
162
+ * `max_tokens` (output cap) so the long-context policy (§6.6) can react by
163
+ * pruning or summarizing.
164
+ */
165
+ type StopReason = "end_turn" | "tool_use" | "max_tokens" | "refusal" | "context_window_exceeded";
166
+ /**
167
+ * Neutral streaming event — §6.2 names the kinds ("text deltas, tool calls,
168
+ * usage"); DECISION: this union is the exact vocabulary both adapters must
169
+ * translate into. `tool_call` is emitted once per call with fully-parsed input.
170
+ */
171
+ type ModelEvent = {
172
+ type: "text_delta";
173
+ text: string;
174
+ } | {
175
+ type: "tool_call";
176
+ id: string;
177
+ name: string;
178
+ input: unknown;
179
+ } | {
180
+ type: "usage";
181
+ usage: Usage;
182
+ } | {
183
+ type: "stop";
184
+ reason: StopReason;
185
+ };
186
+ interface ModelRequest {
187
+ model: ModelRef;
188
+ /** With stable/volatile boundary markers — §6.9. */
189
+ system: SystemBlock[];
190
+ messages: Msg[];
191
+ /** Derived from the registry — never a parallel list (§6.4). */
192
+ tools: ToolSpec[];
193
+ maxTokens: number;
194
+ }
195
+ /**
196
+ * §6.2. `signal` (added by spec 005 review) lets the loop abort the in-flight
197
+ * provider call on cancellation or budget exhaustion — without it, tokens
198
+ * keep generating (and billing) after the harness has already terminated the
199
+ * turn, and a hung stream would keep the turn promise pending forever.
200
+ */
201
+ interface ModelClient {
202
+ stream(req: ModelRequest, opts?: {
203
+ signal?: AbortSignal;
204
+ }): AsyncIterable<ModelEvent>;
205
+ }
206
+
207
+ /**
208
+ * Budget — §6.5, spec: spend-store. Metering is mandatory; POLICY at the cap
209
+ * is product-owned. `perTurnUsd` is the one unconditional hard stop — at a
210
+ * sane level it trips only on malfunction (a tool loop), never on a
211
+ * legitimate conversation, and the session survives it. The persistent caps
212
+ * default to `warn` because this harness sits in front of people in fragile
213
+ * moments: a mid-conversation "budget exceeded" is a worse failure than the
214
+ * overspend. `BudgetGuard` enforcement is part of the privileged core — not a
215
+ * capability seam (§7.1); the `SpendStore` it accounts through is one.
216
+ */
217
+ /** The two caps that need spend surviving the turn — spec: spend-store. */
218
+ type PersistentCapName = "perSessionUsd" | "perTenantDayUsd";
219
+ /** A persistent cap with its crossing policy. */
220
+ interface PersistentCap {
221
+ usd: number;
222
+ /**
223
+ * DECISION (spec: spend-store): defaults to `"warn"` — the turn continues
224
+ * and the crossing lands on the cost trail and the `TurnResult`, exactly
225
+ * once per cap per turn. `"block"` (terminate `budget_exceeded`, refuse new
226
+ * turns at preflight) is the opt-in for machine-facing consumers — an eval
227
+ * sweep, a public agent's kill-switch — never the ambient default.
228
+ */
229
+ onExceeded?: "warn" | "block";
230
+ }
231
+ /** Dollar caps — §6.5, §8. All optional; an absent cap is uncapped. */
232
+ interface BudgetCaps {
233
+ /**
234
+ * Hard cap for a single turn. DECISION: a triggered turn (routine run, §8)
235
+ * is one turn, so this is also the per-run cap — no separate field.
236
+ */
237
+ perTurnUsd?: number;
238
+ /** Bare number = `warn` (spec: spend-store). Keyed {org, uid, sessionId}. */
239
+ perSessionUsd?: number | PersistentCap;
240
+ /**
241
+ * Bare number = `warn`. Keyed {org, UTC day} — deliberately org-wide across
242
+ * uids: an org-level number is what an operator caps or watches (§6.5).
243
+ */
244
+ perTenantDayUsd?: number | PersistentCap;
245
+ }
246
+ /** Thrown by {@link BudgetGuard.charge} when a block-mode cap is crossed. */
247
+ declare class BudgetExceededError extends Error {
248
+ readonly cap: keyof BudgetCaps;
249
+ readonly capUsd: number;
250
+ readonly spentUsd: number;
251
+ constructor(cap: keyof BudgetCaps, capUsd: number, spentUsd: number);
252
+ }
253
+ /**
254
+ * A {@link SpendStore} failure while a BLOCK-mode cap was configured — the
255
+ * fail-closed posture (spec: spend-store). Its own class because the loop must
256
+ * TERMINATE the turn on it wherever it surfaces: inside a delegate it would
257
+ * otherwise be swallowed into tool-result data like any handler error, and an
258
+ * opted-into stop would fail open exactly where the spend is.
259
+ */
260
+ declare class SpendAccountingError extends Error {
261
+ constructor(operation: "add" | "peek", cause: unknown);
262
+ }
263
+ interface BudgetGuard {
264
+ /**
265
+ * Prices `usage` via the versioned price table, accumulates spend — in
266
+ * memory for the turn, through the {@link SpendStore} for the persistent
267
+ * caps — and throws {@link BudgetExceededError} when `perTurnUsd` or a
268
+ * block-mode cap is crossed. Async since spec: spend-store — a persistent
269
+ * counter cannot hide behind a sync signature. Warn-mode crossings are not
270
+ * returned here: they surface on the guard's own state (see
271
+ * `TurnBudgetGuard`), so the loop can stamp them on the settling
272
+ * `CostEvent` even when this call throws. Every turn's usage + cost also
273
+ * lands in the AuditLog cost trail — §6.8.
274
+ */
275
+ charge(usage: Usage & {
276
+ model: ModelRef;
277
+ }): Promise<void>;
278
+ }
279
+ /** Addresses both counters one charge touches — spec: spend-store. */
280
+ interface SpendKey {
281
+ scope: Scope;
282
+ sessionId: string;
283
+ /** ISO 8601 — the store derives the UTC day bucket from it. */
284
+ at: string;
285
+ }
286
+ /** Post-operation counter totals. */
287
+ interface SpendTotals {
288
+ /** Total for {org, uid, sessionId}. */
289
+ sessionUsd: number;
290
+ /** Total for {org, UTC day} — across ALL uids and sessions of the org. */
291
+ tenantDayUsd: number;
292
+ }
293
+ /**
294
+ * Persistent spend accounting — capability seam (§7.1), spec: spend-store.
295
+ * WHERE spend accumulates is swappable; THAT it is accounted — and that caps
296
+ * are enforced, in the privileged guard — is not (the `AuditLog` idiom).
297
+ *
298
+ * Counters are content-free aggregates and deliberately do NOT participate in
299
+ * scoped purge (§10): retained as a legitimate-interest financial record —
300
+ * purging them would turn an erasure right into a budget reset.
301
+ */
302
+ interface SpendStore {
303
+ /**
304
+ * Atomically adds `usd` to BOTH counters and returns the post-add totals.
305
+ * Increment-and-return in one step is the load-bearing property: two
306
+ * concurrent turns must never both act on a stale total — read-modify-write
307
+ * is the store's job, not the guard's.
308
+ */
309
+ add(entry: SpendKey & {
310
+ usd: number;
311
+ }): Promise<SpendTotals>;
312
+ /** Current totals without charging — turn-start preflight, and the surface a product-side alerting watcher polls. */
313
+ peek(key: SpendKey): Promise<SpendTotals>;
314
+ }
315
+ /**
316
+ * One row of the per-provider/model price table — §6.5: versioned
317
+ * configuration data, not code.
318
+ */
319
+ interface ModelPrice {
320
+ model: ModelRef;
321
+ inputUsdPerMTok: number;
322
+ outputUsdPerMTok: number;
323
+ cacheReadUsdPerMTok?: number;
324
+ cacheWriteUsdPerMTok?: number;
325
+ }
326
+
327
+ /**
328
+ * Routing policy — complexity × sensitivity — §6.3.
329
+ *
330
+ * The policy declares, per sensitivity class, which providers/models may touch
331
+ * the data and under what condition (e.g. `health` only on providers with an
332
+ * adequate data-processing agreement, or after pseudonymization).
333
+ *
334
+ * `ModelPolicy` enforcement is part of the privileged core — deliberately NOT
335
+ * a capability seam (§7.1).
336
+ */
337
+ /** Task complexity tier — §6.3. */
338
+ type Tier = "mechanical" | "standard" | "complex";
339
+ /**
340
+ * Data sensitivity class — §6.3.
341
+ * `health` ⊃ special-category data under LGPD Art. 11 / GDPR Art. 9.
342
+ */
343
+ type Sensitivity = "public" | "internal" | "personal" | "health";
344
+ /** Ordered least → most sensitive — §6.3, spec 007. */
345
+ declare const SENSITIVITY_LEVELS: readonly Sensitivity[];
346
+ /**
347
+ * True when `a` is MORE sensitive than `b`. Spec 007: dispatch refuses a
348
+ * tool whose class exceeds the calling loop's declared sensitivity — a
349
+ * `health` tool in a `public` turn is a consumer bug surfaced loudly, never
350
+ * a silent data flow into a context routed for a lower class.
351
+ */
352
+ declare function sensitivityExceeds(a: Sensitivity, b: Sensitivity): boolean;
353
+ interface RoutingIntent {
354
+ tier: Tier;
355
+ sensitivity: Sensitivity;
356
+ /** Optional free-form task label, recorded in the routing trail. */
357
+ task?: string;
358
+ }
359
+ interface ModelChoice {
360
+ model: ModelRef;
361
+ /**
362
+ * DECISION: the "why" of §6.8's RoutingEvent is carried here so every
363
+ * resolution is auditable verbatim — a policy must explain itself.
364
+ */
365
+ rationale: string;
366
+ }
367
+ interface ModelPolicy {
368
+ /** Every resolution is recorded in the AuditLog routing trail — §6.3, §6.8. */
369
+ resolve(intent: RoutingIntent): ModelChoice;
370
+ }
371
+
372
+ /**
373
+ * Tenancy and audit — §6.8. Trails carry METADATA ONLY, never content — by
374
+ * construction, not by reviewer vigilance. Together with the logged-context
375
+ * invariant (§7.3) they answer "what exactly did the model see about this
376
+ * user?" as a query, not archaeology.
377
+ */
378
+ /**
379
+ * Access trail entry: what/when/via which tool — §6.8. Correlation ids
380
+ * (spec 007) make "what happened in this turn?" a filter, not a join
381
+ * heuristic; they are optional because non-turn contexts (consolidation
382
+ * jobs) have no turnId.
383
+ */
384
+ interface AccessEvent {
385
+ scope: Scope;
386
+ /** ISO 8601. */
387
+ at: string;
388
+ /** Tool that performed the access. */
389
+ tool: string;
390
+ /** DECISION: coarse verbs; finer detail goes in `resource`, never content. */
391
+ action: "read" | "write" | "delete" | "export";
392
+ /** Identifier of the touched resource (id/path) — never its content. */
393
+ resource?: string;
394
+ sessionId?: string;
395
+ turnId?: string;
396
+ }
397
+ /** Routing trail entry — §6.3, §6.8. */
398
+ interface RoutingEvent {
399
+ scope: Scope;
400
+ at: string;
401
+ tier: Tier;
402
+ sensitivity: Sensitivity;
403
+ /** The chosen model. */
404
+ model: ModelRef;
405
+ /** Why — carried verbatim from `ModelChoice.rationale`. */
406
+ rationale: string;
407
+ sessionId?: string;
408
+ turnId?: string;
409
+ }
410
+ /**
411
+ * Recall trail entry — §7.3, spec 012.
412
+ *
413
+ * The logged-context invariant says a new MODEL-VISIBLE input requires a new
414
+ * logged event type, never a side channel. The recall block is exactly that:
415
+ * content the model sees which is not in the conversation.
416
+ *
417
+ * DECISION (spec 012): this records PROVENANCE, not the rendered text. A
418
+ * verbatim copy of recalled content would be a copy surface erasure cannot
419
+ * reach without rewriting history — the failure spec 011 closed, one layer up.
420
+ * "What did the model see about this user at turn 12" stays answerable as
421
+ * "these facts and these episodes", each resolvable to its CURRENT state,
422
+ * including erased. Replay of recall is therefore provenance-level, not
423
+ * byte-level: between perfect replay and erasure, erasure wins.
424
+ */
425
+ interface RecallEvent {
426
+ scope: Scope;
427
+ at: string;
428
+ sessionId: string;
429
+ turnId: string;
430
+ /** Ids of the profile fact versions rendered into the block. */
431
+ factIds: readonly string[];
432
+ /** Ids of the episodes rendered into the block. */
433
+ episodeIds: readonly string[];
434
+ /** The single budget the whole block was assembled under (§6.7). */
435
+ budgetTokens: number;
436
+ /**
437
+ * Tokens the block the model actually SAW measured, by the core's own
438
+ * estimator — 0 when the block was dropped. (It formerly documented the
439
+ * assembler's self-reported number, which the core no longer trusts.)
440
+ */
441
+ estimatedTokens: number;
442
+ /** Measured size of a block that was dropped rather than shown. */
443
+ droppedTokens?: number;
444
+ /** True when the budget dropped content that would otherwise have shown. */
445
+ truncated: boolean;
446
+ /** Tiers whose read failed; their block is missing, the rest still rendered. */
447
+ degradedTiers?: readonly string[];
448
+ }
449
+ /**
450
+ * A field of the `ModelRequest` a `step:pre` interceptor may attempt — spec
451
+ * 029. Five, and the loop treats them in two classes: `system`, `messages` and
452
+ * `maxTokens` are the content and ceiling a hook may narrow; `model` and
453
+ * `tools` are privileged core and are repinned after the chain (§6.3, §6.4).
454
+ */
455
+ type ContextField = "system" | "messages" | "maxTokens" | "model" | "tools";
456
+ /**
457
+ * The SIZE of what a model call carried — spec 029. Metadata only: enough to
458
+ * answer "how much entered the model's view from outside the session log",
459
+ * never a copy of it.
460
+ */
461
+ interface ContextShape {
462
+ systemBlocks: number;
463
+ systemChars: number;
464
+ messages: number;
465
+ /** Blocks across all messages — a fabricated block carrying no prose still moves this. */
466
+ messageBlocks: number;
467
+ /**
468
+ * Characters in TEXT blocks only. A size signal for injected prose, NOT a
469
+ * byte count of the request: serializing tool payloads to measure them would
470
+ * put the dispatch path's cost on every rewriting step, and `messageBlocks`
471
+ * already catches what carries no text.
472
+ */
473
+ messageChars: number;
474
+ maxTokens: number;
475
+ }
476
+ /**
477
+ * `step:pre` rewrote what the model was about to see — §7.3, spec 029.
478
+ *
479
+ * The logged-context invariant says a new MODEL-VISIBLE input requires a new
480
+ * logged event type, never a side channel. §7.2 deliberately lets an
481
+ * interceptor rewrite the request, so `system` and `messages` were exactly
482
+ * that: content in front of the model appearing in no session entry.
483
+ *
484
+ * DECISION (spec 029): this records SHAPES, not the rewritten text — the same
485
+ * trade {@link RecallEvent} made and for the same reason (a verbatim copy is a
486
+ * surface erasure cannot reach), plus the one that governs every trail here:
487
+ * they carry metadata only, by construction.
488
+ *
489
+ * DECISION (spec 029): emitted when the chain TOUCHED a field, not when the
490
+ * core honored it. A rewrite the core discards — a raised `maxTokens`, a
491
+ * swapped `model` — is named in {@link refused}. Recording only what survived
492
+ * would leave the pins silent, which is half of what the slice fixed.
493
+ *
494
+ * This narrows §7.3's `step:pre` hole; it does not close it. A rewrite is
495
+ * still not byte-level reconstructable. What it can no longer be is unrecorded.
496
+ */
497
+ interface ContextEvent {
498
+ scope: Scope;
499
+ at: string;
500
+ sessionId: string;
501
+ turnId: string;
502
+ /** The turn step (1-based). A delegate's rewrite carries its PARENT's step. */
503
+ step: number;
504
+ /** True when the rewritten call was a delegate's, not the turn's own. */
505
+ delegate?: boolean;
506
+ /**
507
+ * Fields the chain touched — never empty, since the event exists because one
508
+ * was. Detected by REFERENCE (by value for `maxTokens`) against the pre-hook
509
+ * request, so a hook that rebuilds an identical array over-reports. That is
510
+ * the safe direction for a trail: a spurious entry is not a leak, a missing
511
+ * one is.
512
+ */
513
+ changed: readonly ContextField[];
514
+ /** Of those, the ones the core discarded or clamped rather than honored. */
515
+ refused?: readonly ContextField[];
516
+ /** What the model would have been sent. */
517
+ before: ContextShape;
518
+ /** What it WAS sent — post-clamp, post-pin. */
519
+ after: ContextShape;
520
+ }
521
+ /** Cost trail entry — §6.5, §6.8. */
522
+ interface CostEvent {
523
+ scope: Scope;
524
+ at: string;
525
+ model: ModelRef;
526
+ usage: Usage;
527
+ costUsd: number;
528
+ sessionId?: string;
529
+ turnId?: string;
530
+ /**
531
+ * Warn-mode persistent caps THIS settle crossed — spec: spend-store. At
532
+ * most once per cap per turn; absent on every other event.
533
+ */
534
+ capsCrossed?: readonly PersistentCapName[];
535
+ }
536
+ /**
537
+ * §6.8. The SINK is a capability seam (§7.1) — where trails are written is
538
+ * swappable; THAT they are written is not (emission lives in the privileged
539
+ * core and cannot be bypassed by hooks or configuration).
540
+ *
541
+ * DECISION (spec 027, revised): every method may return a promise, and the
542
+ * harness AWAITS it. The previous rule — "synchronous fire-and-forget so
543
+ * auditing never blocks the critical path" — could not be enforced and was not
544
+ * true: `void` is exactly the return type TypeScript lets an `async` function
545
+ * satisfy, so a DB- or HTTP-backed sink (the shape §5 promises for Postgres)
546
+ * was always assignable, and its rejection escaped as an unhandled rejection —
547
+ * ending the process while the turn reported success.
548
+ *
549
+ * The latency the old rule protected is now the SINK's choice, where it
550
+ * belongs: buffer internally and return synchronously to stay off the critical
551
+ * path, or return a promise and be awaited. Either satisfies the type.
552
+ *
553
+ * A sink that FAILS terminates the turn, on every family. Where trails are
554
+ * written is swappable (§7.1); that they are written is not, and a trail that
555
+ * silently stopped being written is the failure the invariant exists to catch.
556
+ *
557
+ * One ergonomic consequence, worth knowing before it surprises you: a bare
558
+ * `void` return type accepts a function returning ANYTHING, and the union does
559
+ * not inherit that rule. `access: (e) => log.push(e)` no longer compiles —
560
+ * `void log.push(e)`, or a block body, does. The error is at the type level
561
+ * and immediate, which is the trade for a contract that no longer lies about
562
+ * what it accepts.
563
+ */
564
+ interface AuditLog {
565
+ access(e: AccessEvent): void | Promise<void>;
566
+ routing(e: RoutingEvent): void | Promise<void>;
567
+ cost(e: CostEvent): void | Promise<void>;
568
+ /** §7.3 — what the model was shown from memory, by reference. */
569
+ recall(e: RecallEvent): void | Promise<void>;
570
+ /**
571
+ * §7.3, spec 029 — what a `step:pre` hook changed about the model request,
572
+ * by shape. Fires only when a hook actually touched one of the five fields,
573
+ * so an agent with no rewriting hooks never calls it. Required all the same:
574
+ * whether a trail is written is not a product choice.
575
+ */
576
+ context(e: ContextEvent): void | Promise<void>;
577
+ }
578
+ /**
579
+ * A trail sink failed — spec 027 review. TYPED, because "a failing audit sink
580
+ * terminates the turn" has to hold on every path, and the loop classifies
581
+ * errors by type: an untyped throw from a sink inside a delegate was caught by
582
+ * the tool-dispatch handler and became tool-result DATA, so the turn reported
583
+ * success with a trail silently unwritten. The same shape that made
584
+ * `SpendAccountingError` typed, for the same reason.
585
+ */
586
+ declare class AuditSinkError extends Error {
587
+ readonly family: "access" | "routing" | "cost" | "recall" | "context";
588
+ readonly cause: unknown;
589
+ constructor(family: "access" | "routing" | "cost" | "recall" | "context", cause: unknown);
590
+ }
591
+ /** Consent state for one integration — §6.8, §10. */
592
+ interface Consent {
593
+ granted: boolean;
594
+ /** ISO 8601 of the grant/revocation. */
595
+ at?: string;
596
+ /** Version of the consent text the user acted on. */
597
+ version?: string;
598
+ }
599
+ /**
600
+ * Per-integration consent gate — §6.8. Capability seam (§7.1).
601
+ * DECISION: `integration` is a product-defined slug (e.g. "calendar");
602
+ * absence of a record must resolve to `{ granted: false }`, never throw.
603
+ */
604
+ interface ConsentStore {
605
+ get(scope: Scope, integration: string): Promise<Consent>;
606
+ }
607
+
608
+ /**
609
+ * Session persistence — §6.6. Design inherited from the two production stores:
610
+ * transactional seq, chunking, TTL on the root doc, owner stamping for scoped
611
+ * purge. Sessions persist ONLY the neutral message format (§6.1).
612
+ *
613
+ * The long-context policy belongs to the HARNESS, not the store — and it is
614
+ * entry-time tool-output discipline plus windowing/summary at cache-cold
615
+ * boundaries, NEVER mid-history pruning: a consuming product measured pruning
616
+ * at +27% total cost (+99% cache writes) — see §6.6 and spec:
617
+ * tool-output-discipline.
618
+ */
619
+ interface LoadOpts {
620
+ /**
621
+ * DECISION: §6.6 leaves LoadOpts open; Phase 0 defines only recency
622
+ * windowing. `limit` returns the LAST `limit` messages, still in
623
+ * chronological order — recency is what context assembly wants. A
624
+ * non-positive limit yields an empty history.
625
+ */
626
+ limit?: number;
627
+ }
628
+ /** Capability seam — §7.1. Exercised by the shared contract suite (§6). */
629
+ interface SessionStore {
630
+ /**
631
+ * Appends entries atomically and in order (transactional seq — §6.6):
632
+ * concurrent appends to one session must never interleave or lose entries.
633
+ *
634
+ * PRECONDITION: every string in `entries`, keys included, is well-formed
635
+ * UTF-16. `runTurn` repairs each message as it is recorded (spec:
636
+ * well-formed-text), but only BEST-EFFORT — its catch keeps the unrepaired
637
+ * message — and a product writing here directly gets no such pass at all.
638
+ * `toWellFormedDeep` from this package is the remedy.
639
+ *
640
+ * ENFORCED by every adapter this package ships, and identically: a violation
641
+ * throws {@link import("./text").MalformedTextError} naming the offending
642
+ * entry and path, before anything is written. Spec 040 closed this; until
643
+ * then they DIVERGED — the in-memory store kept a lone surrogate while
644
+ * Postgres refused the write — and both contracts said "do not rely on
645
+ * either behaviour", which is not a contract. Refusing rather than repairing
646
+ * is the deliberate half: a store that silently rewrites the bytes of a
647
+ * record kept for years is worse than one that declines.
648
+ *
649
+ * A PRODUCT's own `SessionStore` is not covered by that sentence and owns
650
+ * the check itself — `assertWellFormed` is exported for exactly this, and
651
+ * the quickstart's store calls it. This distinction is not pedantry: the
652
+ * first draft of this paragraph said "every adapter", which read as every
653
+ * implementation, and the example in this repo was at that moment still
654
+ * accepting what Postgres rejects (spec 040 review).
655
+ */
656
+ append(scope: Scope, sessionId: string, entries: Msg[]): Promise<void>;
657
+ /**
658
+ * Chronological history. DECISION: an unknown session resolves to `[]`
659
+ * rather than throwing — "no history yet" and "no such session" are the
660
+ * same thing to a turn that is about to create one.
661
+ */
662
+ load(scope: Scope, sessionId: string, opts?: LoadOpts): Promise<Msg[]>;
663
+ /**
664
+ * LGPD/GDPR erasure — §6.6, §10. With `sessionId`, erases that session;
665
+ * without it, purges EVERY session in the scope (owner-stamped purge).
666
+ */
667
+ erase(scope: Scope, sessionId?: string): Promise<void>;
668
+ /**
669
+ * Drops the session's `tool_call` and `tool_result` blocks, keeping
670
+ * everything the user saw — spec 039.
671
+ *
672
+ * The split it serves: for a product whose conversations are a professional
673
+ * record kept for years, the tool traffic is the arguments and results of
674
+ * what the agent DID — richer, more sensitive, and useful for weeks rather
675
+ * than years. What the trail keeps is the metadata (§6.8); what this removes
676
+ * is the payload.
677
+ *
678
+ * TWO CONSTRAINTS, both enforced here rather than documented and hoped for.
679
+ *
680
+ * `inactiveSince` is REQUIRED, and the store refuses when the session holds
681
+ * an entry newer than it — reporting `expired: false`, never throwing.
682
+ * Removing blocks from a session still in use is the mid-history pruning
683
+ * §6.6 forbids, and the cost is measured rather than feared: a consuming
684
+ * product ran that experiment and saw total cost +27% with cache writes
685
+ * +99%, because prompt caching is a prefix match and removing any byte
686
+ * mid-history rewrites everything after it. On a session nobody will resume
687
+ * the same removal is free, and that condition is the entire difference.
688
+ *
689
+ * ALL the tool blocks go, or none. Not "the ones older than X" — spec 026
690
+ * taught the loop to pair every `tool_call` with a `tool_result` on every
691
+ * exit path, and a provider answers 400 for a dangling `tool_use`, so a
692
+ * partial expiry would manufacture exactly that. All-or-nothing is paired by
693
+ * construction.
694
+ *
695
+ * A message left with no blocks is removed. `media` blocks survive: they are
696
+ * `MediaRef` pointers rather than content, they are user-facing, and the
697
+ * storage holding the bytes has its own lifecycle that is not Alma's.
698
+ *
699
+ * SCHEDULING IS THE PRODUCT'S. This is one session; enumerating which
700
+ * sessions are stale is a cross-scope question, and every contract here is
701
+ * scope-bound by design (§6.1).
702
+ */
703
+ expireToolTraffic(scope: Scope, sessionId: string, opts: {
704
+ inactiveSince: string;
705
+ }): Promise<ToolTrafficExpiry>;
706
+ }
707
+ /** What one {@link SessionStore.expireToolTraffic} pass did — spec 039. */
708
+ interface ToolTrafficExpiry {
709
+ /** Blocks removed. */
710
+ blocks: number;
711
+ /** Messages left with nothing, and therefore removed entirely. */
712
+ messages: number;
713
+ /**
714
+ * False when the session was still active and nothing was touched.
715
+ *
716
+ * A refusal, not an error: a sweep over a thousand sessions where forty are
717
+ * still live is the normal case. A throw would make the caller write a
718
+ * try/catch per session and swallow it — and a swallowed refusal is how you
719
+ * stop noticing that nothing is being expired.
720
+ */
721
+ expired: boolean;
722
+ }
723
+
724
+ /**
725
+ * Lifecycle events — §7.2. A small, CLOSED set of typed hooks on the turn
726
+ * flow — enough for guards, telemetry, and extra policy without touching the
727
+ * loop. Deliberately not extensible: a new model-visible input requires a new
728
+ * logged event type (§7.3), never a side channel.
729
+ *
730
+ * Interceptors are capability-narrowing ONLY: a hook can veto, rewrite, or
731
+ * annotate, but it can never register tools outside the scoped registry,
732
+ * widen a scope, or bypass audit/budget. The loop enforces this by
733
+ * construction — hooks receive no registry or store handles.
734
+ *
735
+ * "By construction" became true of `step:pre` only in spec 029. Until then a
736
+ * hook could raise `maxTokens` — not audit or budget, but how far one step may
737
+ * spend before the cap gets its chance: a widening the sentence above already
738
+ * forbade and nothing enforced. See {@link StepDecision} for the backstop that
739
+ * now holds it.
740
+ */
741
+ /** How the turn was initiated — §8, §9. */
742
+ type TurnTrigger = "user" | "routine" | "system";
743
+ /**
744
+ * §9 step 4 — every turn ends with exactly one of these.
745
+ *
746
+ * `busy` joined the original four in spec 030: a turn that waited out
747
+ * `leaseWaitMs` for a session another turn was holding. It is its own reason
748
+ * because this union is the vocabulary every product switches on — a lease
749
+ * timeout arriving as `"error"` with a magic message would be untypeable, and
750
+ * it is the one terminal state where retrying the SAME turn is the right move.
751
+ */
752
+ type TerminalReason = "completed" | "budget_exceeded" | "cancelled" | "error" | "busy";
753
+ interface TurnStartEvent {
754
+ scope: Scope;
755
+ sessionId: string;
756
+ turnId: string;
757
+ trigger: TurnTrigger;
758
+ /** ISO 8601. */
759
+ at: string;
760
+ }
761
+ interface TurnEndEvent {
762
+ scope: Scope;
763
+ sessionId: string;
764
+ turnId: string;
765
+ terminalReason: TerminalReason;
766
+ usage: Usage;
767
+ costUsd: number;
768
+ /**
769
+ * Model calls the TURN made — spec 032. The same index `TurnEvent.step`
770
+ * carries and `maxSteps` bounds; a delegate's own steps are not counted, or
771
+ * the number and its ceiling would measure different things.
772
+ */
773
+ steps: number;
774
+ /**
775
+ * Wall-clock of the turn's own work, milliseconds — spec 032. Excludes the
776
+ * session lease wait (spec 030): a caller holds both ends of its `runTurn`
777
+ * call and can already measure total time, so reporting the WORK is what
778
+ * gives it the split it cannot reconstruct.
779
+ */
780
+ durationMs: number;
781
+ at: string;
782
+ }
783
+ /** `step:pre` — what the model is about to see, before it is sent. */
784
+ interface StepPreEvent {
785
+ scope: Scope;
786
+ sessionId: string;
787
+ turnId: string;
788
+ request: ModelRequest;
789
+ }
790
+ /**
791
+ * DECISION: outcome vocabulary for `step:pre` — rewrite happens by passing a
792
+ * modified event to `next()`; rejecting aborts the step with an auditable
793
+ * reason.
794
+ *
795
+ * The core applies a BACKSTOP after the chain returns, the way the registered
796
+ * ceiling still applies after every `tool:post` hook (see
797
+ * {@link ToolAnnotation}) — spec 029. `model` and `tools` are repinned,
798
+ * `maxTokens` is honored only when it NARROWS, and whichever of the five
799
+ * fields the chain touched is written to the `context` trail, honored or
800
+ * refused (§7.3).
801
+ *
802
+ * Rewrite by passing a modified event, never by mutating `event.request`: the
803
+ * request handed to the chain is FROZEN, so an in-place assignment throws. It
804
+ * used to succeed silently, bypassing both the backstop and the trail.
805
+ */
806
+ type StepDecision = {
807
+ action: "proceed";
808
+ request: ModelRequest;
809
+ } | {
810
+ action: "reject";
811
+ reason: string;
812
+ };
813
+ /** `tool:pre` — a tool call the model has requested, before dispatch. */
814
+ interface ToolPreEvent {
815
+ scope: Scope;
816
+ sessionId: string;
817
+ turnId: string;
818
+ call: {
819
+ id: string;
820
+ name: string;
821
+ input: unknown;
822
+ };
823
+ spec: ToolSpec;
824
+ }
825
+ type ToolPreDecision = {
826
+ action: "proceed";
827
+ input: unknown;
828
+ } | {
829
+ action: "veto";
830
+ reason: string;
831
+ };
832
+ /** `tool:post` — a tool result, before it is appended to the conversation. */
833
+ interface ToolPostEvent {
834
+ scope: Scope;
835
+ sessionId: string;
836
+ turnId: string;
837
+ call: {
838
+ id: string;
839
+ name: string;
840
+ input: unknown;
841
+ };
842
+ result: {
843
+ output: unknown;
844
+ isError: boolean;
845
+ };
846
+ }
847
+ /**
848
+ * What a `tool:post` hook contributes to a tool result — §7.2, spec:
849
+ * tool-output-discipline. `advisory` appends context (the canonical
850
+ * loop-hygiene use); `output` REPLACES what the model will see — the entry
851
+ * point where a product swaps an oversized result for the relevant slice
852
+ * plus a pointer, BEFORE it enters the transcript (removal later costs more
853
+ * than it saves — §6.6). Hooks run in order, each seeing the output as left
854
+ * by the previous. §7.3 holds in both directions: the session persists
855
+ * exactly what the model saw, and the replaced original is retained nowhere.
856
+ * The registered ceiling still applies after every hook — the core backstop.
857
+ */
858
+ interface ToolAnnotation {
859
+ advisory?: string;
860
+ /**
861
+ * Replaces the output the model will see — and the session will persist.
862
+ * A PRESENT key replaces even when its value is undefined (normalized to
863
+ * null), so a redaction hook can suppress a result outright.
864
+ */
865
+ output?: unknown;
866
+ }
867
+ /** Observe-mode hook — may be async; the loop awaits but never interprets it. */
868
+ type Observer<E> = (event: E) => void | Promise<void>;
869
+ /**
870
+ * Intercept-mode hook, middleware-style — §7.2: call `next()` to delegate
871
+ * down the chain (optionally with a rewritten event), or return without
872
+ * calling it to short-circuit.
873
+ */
874
+ type Interceptor<E, R> = (event: E, next: (event: E) => Promise<R>) => Promise<R>;
875
+ /** The closed hook set — §7.2. Keys are the event names, verbatim. */
876
+ interface LifecycleHooks {
877
+ "turn:start"?: Observer<TurnStartEvent>;
878
+ "turn:end"?: Observer<TurnEndEvent>;
879
+ "step:pre"?: Interceptor<StepPreEvent, StepDecision>;
880
+ "tool:pre"?: Interceptor<ToolPreEvent, ToolPreDecision>;
881
+ "tool:post"?: (event: ToolPostEvent) => ToolAnnotation | undefined | Promise<ToolAnnotation | undefined>;
882
+ }
883
+
884
+ /**
885
+ * Turn coordination — spec 030. Two failures the loop could not see, closed by
886
+ * one seam.
887
+ *
888
+ * A webhook that redelivers because it never saw a 200 used to make the
889
+ * harness run the turn again: the message sent twice, the memory written
890
+ * twice, the spend charged twice. And two DIFFERENT messages arriving on one
891
+ * session concurrently both loaded the same history and both appended, so the
892
+ * second turn never saw the first.
893
+ *
894
+ * The first needs an idempotency record; the second needs serialization.
895
+ * They are one seam because the lease is what makes the claim simple: with the
896
+ * session serialized, a claim has exactly two outcomes — fresh, or a completed
897
+ * turn to replay — and an in-flight claim is only reachable after a crash,
898
+ * never through concurrency.
899
+ *
900
+ * The idiom is `AuditLog`'s and `SpendStore`'s: WHERE a turn's coordination
901
+ * record lives is swappable (§7.1); THAT a turn is claimed before it runs is
902
+ * not.
903
+ */
904
+ /**
905
+ * Addresses one turn's idempotency record.
906
+ *
907
+ * DECISION (spec 030): keyed by the full scope AND the session, never by
908
+ * `idempotencyKey` alone. A key is unique only within the transport that
909
+ * issued it, and a global key space would let one tenant's retry collide with
910
+ * another's — the isolation boundary applies here like everywhere else (§6.1).
911
+ */
912
+ interface TurnKey {
913
+ scope: Scope;
914
+ sessionId: string;
915
+ /** Caller-supplied delivery identity — typically the inbound message id. */
916
+ idempotencyKey: string;
917
+ }
918
+ interface LeaseOpts {
919
+ /**
920
+ * How long the lease is held before it expires on its own. Must exceed a
921
+ * realistic worst-case turn: a live turn whose lease expires gets it stolen
922
+ * and interleaves, which is the failure the lease exists to prevent. It is a
923
+ * ceiling on how long a CRASHED holder can block a session, so it cannot
924
+ * simply be enormous either.
925
+ */
926
+ ttlMs: number;
927
+ /** How long to wait for a busy session before giving up. */
928
+ waitMs: number;
929
+ }
930
+ /**
931
+ * Proof that this holder owns the session — spec 030.
932
+ *
933
+ * The token exists so {@link TurnStore.release} can refuse a STALE one. A
934
+ * holder whose lease already expired must never release the lease the next
935
+ * turn is now holding: that would serialize nothing while appearing to, which
936
+ * is worse than no lease at all.
937
+ */
938
+ interface TurnLease {
939
+ readonly token: string;
940
+ /** ISO 8601. */
941
+ readonly expiresAt: string;
942
+ }
943
+ /**
944
+ * The replayable subset of a finished turn — spec 030.
945
+ *
946
+ * Deliberately NOT the loop's whole `TurnResult`. `capsCrossed`,
947
+ * `accountingError` and `budgetExceeded` describe the ORIGINAL run's
948
+ * infrastructure and enforcement state; re-reporting a cap crossing on every
949
+ * retry would double-count in exactly the product-side alerting spec 019
950
+ * built. What replays is what the turn produced, not how it went.
951
+ *
952
+ * It holds `reply` verbatim, which makes it a COPY SURFACE in the sense spec
953
+ * 010 defines — the price of replaying rather than refusing, paid explicitly.
954
+ * {@link TurnStore.erase} is how §10 reaches it.
955
+ */
956
+ interface CompletedTurn {
957
+ reply: Msg;
958
+ terminalReason: TerminalReason;
959
+ stopReason: StopReason | null;
960
+ usage: Usage;
961
+ costUsd: number;
962
+ /**
963
+ * What the original turn cost in steps and milliseconds — spec 032, carried
964
+ * for the same reason `usage` and `costUsd` are. A replay reporting
965
+ * `durationMs: 0` would be the same lie as one reporting `costUsd: 0`.
966
+ */
967
+ steps: number;
968
+ durationMs: number;
969
+ /** The original turn's id — correlation across the trails it already wrote. */
970
+ turnId: string;
971
+ /**
972
+ * Present when `terminalReason` is `"error"` — spec 033. It replays where
973
+ * `capsCrossed`, `accountingError` and `budgetExceeded` deliberately do not,
974
+ * and the difference is what each describes: those three are the original
975
+ * run's INFRASTRUCTURE and enforcement state, where re-reporting on every
976
+ * retry would double-count in a product's alerting. This is the turn's
977
+ * OUTCOME. A replayed failure that says `"error"` with no reason is strictly
978
+ * less than the turn it replays, and no double-counting argument applies to a
979
+ * string.
980
+ */
981
+ error?: string;
982
+ /** ISO 8601 of the ORIGINAL turn. */
983
+ at: string;
984
+ }
985
+ /**
986
+ * `fresh` — nothing has run under this key; the turn proceeds.
987
+ * `replay` — a turn already finished under it; its result is returned as-is.
988
+ *
989
+ * A turn that ended `error` or `budget_exceeded` still COMPLETES its claim, so
990
+ * a retry replays that outcome. A failed turn is a result, not an invitation
991
+ * to run it again and charge again.
992
+ */
993
+ type TurnClaim = {
994
+ status: "fresh";
995
+ } | {
996
+ status: "replay";
997
+ completed: CompletedTurn;
998
+ };
999
+ /**
1000
+ * Capability seam — §7.1, spec 030. Exercised by `describeTurnStoreContract`.
1001
+ *
1002
+ * Configured or not, with no half-protected mode: a product that wires this
1003
+ * decided double-execution is unacceptable, so a store failure terminates the
1004
+ * turn rather than degrading to "unprotected but running" — the fail-closed
1005
+ * posture `SpendAccountingError` takes under a block cap.
1006
+ */
1007
+ interface TurnStore {
1008
+ /**
1009
+ * Takes the session, waiting up to `opts.waitMs` for a busy one. Resolves
1010
+ * `null` when the wait expires — the caller ends the turn `"busy"` rather
1011
+ * than proceeding unserialized.
1012
+ *
1013
+ * Concurrent callers must see exactly ONE winner. That is the property the
1014
+ * whole seam stands on, and it is the store's job: a lease handed to two
1015
+ * holders serializes nothing.
1016
+ */
1017
+ acquire(scope: Scope, sessionId: string, opts: LeaseOpts): Promise<TurnLease | null>;
1018
+ /**
1019
+ * Releases the session. IDEMPOTENT, and a stale token is a no-op rather than
1020
+ * another holder's release (see {@link TurnLease}).
1021
+ */
1022
+ release(scope: Scope, sessionId: string, lease: TurnLease): Promise<void>;
1023
+ /** Records the attempt and reports whether this turn has already run. */
1024
+ claim(key: TurnKey): Promise<TurnClaim>;
1025
+ /** Stores the replayable record. Every later claim under this key replays it. */
1026
+ complete(key: TurnKey, completed: CompletedTurn): Promise<void>;
1027
+ /**
1028
+ * Drops a claim so a genuine retry may run — the path a turn takes when it
1029
+ * could not produce a result to store at all.
1030
+ */
1031
+ abandon(key: TurnKey): Promise<void>;
1032
+ /**
1033
+ * §10 erasure. With `sessionId`, clears that session's lease and records;
1034
+ * without it, every session in the scope. Mirrors `SessionStore.erase`
1035
+ * deliberately: a product erasing a session must erase its turn records in
1036
+ * the same breath, or the reply survives the erasure that removed it from
1037
+ * the transcript.
1038
+ */
1039
+ erase(scope: Scope, sessionId?: string): Promise<void>;
1040
+ }
1041
+ /**
1042
+ * A `TurnStore` operation failed — spec 030. Its own class for the reason
1043
+ * {@link import("./budget").SpendAccountingError} has one: the loop classifies
1044
+ * errors by TYPE, and inside a delegate an untyped throw is caught by the
1045
+ * tool-dispatch handler and becomes tool DATA, so a turn whose coordination
1046
+ * broke would report success.
1047
+ */
1048
+ declare class TurnStoreError extends Error {
1049
+ readonly operation: "acquire" | "release" | "claim" | "complete" | "abandon";
1050
+ readonly cause: unknown;
1051
+ constructor(operation: "acquire" | "release" | "claim" | "complete" | "abandon", cause: unknown);
1052
+ }
1053
+
1054
+ export { type ToolPostEvent as $, type AuditLog as A, type BudgetCaps as B, type CompletedTurn as C, type ProviderId as D, type RoutingEvent as E, type RoutingIntent as F, SENSITIVITY_LEVELS as G, type SessionStore as H, type Interceptor as I, SpendAccountingError as J, type SpendKey as K, type LeaseOpts as L, type ModelRef as M, type SpendStore as N, type Observer as O, type PersistentCap as P, type SpendTotals as Q, type RecallEvent as R, type Sensitivity as S, type Tier as T, type Usage as U, type StepDecision as V, type StepPreEvent as W, type StopReason as X, type SystemBlock as Y, type TerminalReason as Z, type ToolAnnotation as _, type Scope as a, type ToolPreDecision as a0, type ToolPreEvent as a1, type ToolSpec as a2, type ToolTrafficExpiry as a3, type TurnClaim as a4, type TurnEndEvent as a5, type TurnKey as a6, type TurnLease as a7, type TurnStartEvent as a8, type TurnStore as a9, TurnStoreError as aa, type TurnTrigger as ab, scopePath as ac, sensitivityExceeds as ad, type ModelPrice as b, type AccessEvent as c, AuditSinkError as d, type Block as e, BudgetExceededError as f, type BudgetGuard as g, type Consent as h, type ConsentStore as i, type ContextEvent as j, type ContextField as k, type ContextShape as l, type CostEvent as m, InvalidScopeError as n, type LifecycleHooks as o, type LoadOpts as p, type MediaKind as q, type MediaRef as r, type ModelChoice as s, type ModelClient as t, type ModelEvent as u, type ModelPolicy as v, type ModelRequest as w, type Msg as x, type MsgMeta as y, type PersistentCapName as z };