@anuma/sdk 6.15.2 → 6.17.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,578 @@
1
+ type LlmapiMessage = {
2
+ /**
3
+ * Content is the message content
4
+ */
5
+ content?: Array<LlmapiMessageContentPart>;
6
+ role?: LlmapiRole;
7
+ /**
8
+ * ToolCallID is the ID of the tool call this message is responding to (only for tool role)
9
+ */
10
+ tool_call_id?: string;
11
+ /**
12
+ * ToolCalls contains tool/function calls made by the assistant (only for assistant role)
13
+ */
14
+ tool_calls?: Array<LlmapiToolCall>;
15
+ /**
16
+ * Type is the message type (for Responses API: "message")
17
+ */
18
+ type?: string;
19
+ };
20
+ /**
21
+ * File is used when Type=input_file (for Responses API)
22
+ */
23
+ type LlmapiMessageContentFile = {
24
+ /**
25
+ * FileData is the base64-encoded file content
26
+ */
27
+ file_data?: string;
28
+ /**
29
+ * FileID is the ID of an uploaded file
30
+ */
31
+ file_id?: string;
32
+ /**
33
+ * FileURL is the URL to the file
34
+ */
35
+ file_url?: string;
36
+ /**
37
+ * Filename is the name of the file
38
+ */
39
+ filename?: string;
40
+ };
41
+ /**
42
+ * ImageURL is used when Type=image_url or Type=input_image
43
+ */
44
+ type LlmapiMessageContentImage = {
45
+ /**
46
+ * Detail is the OpenAI detail hint (auto|low|high)
47
+ */
48
+ detail?: string;
49
+ /**
50
+ * URL is the image URL or data URI
51
+ */
52
+ url?: string;
53
+ };
54
+ type LlmapiMessageContentPart = {
55
+ file?: LlmapiMessageContentFile;
56
+ image_url?: LlmapiMessageContentImage;
57
+ /**
58
+ * Text holds the text content when Type=text or Type=input_text
59
+ */
60
+ text?: string;
61
+ /**
62
+ * Type is the block type (`text`, `image_url`, or `input_file`)
63
+ */
64
+ type?: string;
65
+ };
66
+ /**
67
+ * Role is the message role (system, user, assistant, tool)
68
+ */
69
+ type LlmapiRole = string;
70
+ type LlmapiToolCall = {
71
+ function?: LlmapiToolCallFunction;
72
+ /**
73
+ * ID is the unique identifier for this tool call
74
+ */
75
+ id?: string;
76
+ /**
77
+ * Type is the type of tool call (always "function" for now)
78
+ */
79
+ type?: string;
80
+ };
81
+ /**
82
+ * Function contains the function call details
83
+ */
84
+ type LlmapiToolCallFunction = {
85
+ /**
86
+ * Arguments is the JSON string of arguments to pass to the function
87
+ */
88
+ arguments?: string;
89
+ /**
90
+ * Name is the name of the function to call
91
+ */
92
+ name?: string;
93
+ };
94
+
95
+ type LlmapiChatCompletionTool = {
96
+ [key: string]: unknown;
97
+ };
98
+
99
+ /**
100
+ * Lifecycle hooks for runToolLoop. All hooks are optional and observer
101
+ * errors are swallowed (sync throws and async rejections both) so a buggy
102
+ * hook can't crash the loop. Hook names match the Claude Agent SDK shape
103
+ * (camelCase).
104
+ *
105
+ * Hooks are awaited synchronously at each fire site, so slow hooks
106
+ * serialize into loop latency — keep them fast and do any heavy work
107
+ * (network exports, file IO) asynchronously inside your own queue.
108
+ */
109
+
110
+ type LlmTokenUsage = {
111
+ inputTokens?: number;
112
+ outputTokens?: number;
113
+ };
114
+ type ModelCallStartEvent = {
115
+ runId: string;
116
+ stepIndex: number;
117
+ model: string;
118
+ messages: Array<LlmapiMessage>;
119
+ tools: Array<LlmapiChatCompletionTool>;
120
+ requestBody: Record<string, unknown>;
121
+ };
122
+ type ModelCallEndEvent = {
123
+ runId: string;
124
+ stepIndex: number;
125
+ content: string;
126
+ toolCalls: Array<{
127
+ id: string;
128
+ name: string;
129
+ arguments: string;
130
+ }>;
131
+ usage?: LlmTokenUsage;
132
+ finishReason?: string;
133
+ error?: string;
134
+ };
135
+ type ToolUseStartEvent = {
136
+ runId: string;
137
+ stepIndex: number;
138
+ toolCallId: string;
139
+ name: string;
140
+ rawArguments: string;
141
+ parsedArguments?: Record<string, unknown>;
142
+ };
143
+ type ToolUseEndEvent = {
144
+ runId: string;
145
+ stepIndex: number;
146
+ toolCallId: string;
147
+ name: string;
148
+ result?: unknown;
149
+ error?: string;
150
+ errorType?: "parse" | "timeout" | "execution";
151
+ };
152
+ type RunStartEvent = {
153
+ runId: string;
154
+ model: string;
155
+ messages: Array<LlmapiMessage>;
156
+ tools: Array<LlmapiChatCompletionTool>;
157
+ };
158
+ type RunEndEvent = {
159
+ runId: string;
160
+ finalContent: string;
161
+ /** Number of LLM rounds executed. */
162
+ totalSteps: number;
163
+ };
164
+ type RunErrorEvent = {
165
+ runId: string;
166
+ error: string;
167
+ /**
168
+ * Only `"model"` today. Tool failures surface on {@link RunHooks.afterToolUse}
169
+ * with `errorType`, not on `onRunError`, so a tool stage isn't needed yet.
170
+ * Left as a union to leave room for `"validation"` or `"tool"` later
171
+ * without a breaking change.
172
+ */
173
+ stage: "model";
174
+ /**
175
+ * The underlying `Error` when one is available. `RunErrorEvent` predates
176
+ * this field, so `error` (a string message) is kept for backwards
177
+ * compatibility; new consumers should prefer `errorObject` when present
178
+ * for stack traces and `cause`.
179
+ */
180
+ errorObject?: Error;
181
+ };
182
+ type RunHooks = {
183
+ /**
184
+ * Fires once per `runToolLoop` invocation, before any pre-processor or
185
+ * model call. The `messages` payload is the raw caller-supplied
186
+ * conversation — pre-processors that enrich it (e.g. web search context)
187
+ * run afterwards, so `beforeModelCall.messages` reflects the
188
+ * post-enrichment state.
189
+ */
190
+ onRunStart?: (e: RunStartEvent) => Promise<void> | void;
191
+ /**
192
+ * Fires exactly once per run on clean completion. Mutually exclusive
193
+ * with `onRunError` — a run that completes fires `onRunEnd`; a run that
194
+ * errors or is aborted fires `onRunError`. Consumers can rely on
195
+ * exactly one terminal hook per `onRunStart`.
196
+ */
197
+ onRunEnd?: (e: RunEndEvent) => Promise<void> | void;
198
+ /**
199
+ * Fires exactly once per run when the loop errors or is aborted.
200
+ * `stage` is `"model"` for stream/provider errors and for aborts;
201
+ * tool execution failures surface as structured `{ error, errorType }`
202
+ * results on `afterToolUse` rather than firing `onRunError`.
203
+ * Mutually exclusive with `onRunEnd`.
204
+ */
205
+ onRunError?: (e: RunErrorEvent) => Promise<void> | void;
206
+ beforeModelCall?: (e: ModelCallStartEvent) => Promise<void> | void;
207
+ afterModelCall?: (e: ModelCallEndEvent) => Promise<void> | void;
208
+ /**
209
+ * Fires before each tool the model invoked — including server-side
210
+ * tools that have no client executor. Today this is observe-only.
211
+ * TODO(follow-up PR): allow returning { args?, abort?: { reason } } to
212
+ * mutate arguments or short-circuit the tool call.
213
+ *
214
+ * Pairing with {@link afterToolUse} is asymmetric: server-side tools
215
+ * routed via `onToolCall` get `beforeToolUse` but no `afterToolUse`,
216
+ * because they don't execute in-process. Tools blocked by a failed
217
+ * dependency or a dependency cycle DO get `afterToolUse` with
218
+ * `errorType: "execution"` — consumers tallying matched pairs should
219
+ * filter on executor-backed tools to avoid spurious imbalances.
220
+ */
221
+ beforeToolUse?: (e: ToolUseStartEvent) => Promise<void> | void;
222
+ /**
223
+ * Fires after each executor-backed tool finishes (success or failure)
224
+ * and after each dependency-blocked tool is skipped (`errorType:
225
+ * "execution"`). Does NOT fire for server-side tools routed via
226
+ * `onToolCall`. See {@link beforeToolUse} for the pairing contract.
227
+ */
228
+ afterToolUse?: (e: ToolUseEndEvent) => Promise<void> | void;
229
+ };
230
+
231
+ /**
232
+ * Telemetry sink contract for the observability adapter.
233
+ *
234
+ * The SDK never talks to a vendor directly — it emits structured events and
235
+ * metrics through this interface, and the app adapts it to PostHog, Datadog,
236
+ * OpenTelemetry, etc. Both methods are optional so a sink that only cares
237
+ * about events (or only about metrics) can implement half the surface.
238
+ *
239
+ * Implementations must be fast and must not throw: hooks are awaited
240
+ * synchronously inside the tool loop (see `runHooks.ts`), so a slow or
241
+ * throwing sink serializes into run latency or — worse — surfaces as a run
242
+ * error. Queue network exports asynchronously inside your sink.
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * import { createMetricsHooks, type TelemetrySink } from "@anuma/sdk/telemetry";
247
+ *
248
+ * // posthog-js: capture(event, properties)
249
+ * const sink: TelemetrySink = {
250
+ * track: (event, properties) => posthog.capture(event, properties),
251
+ * metric: (name, value, tags) => statsd.histogram(name, value, tags),
252
+ * };
253
+ *
254
+ * // posthog-node takes ONE object, so the arguments do not line up:
255
+ * // track: (event, properties) => posthog.capture({ distinctId, event, properties })
256
+ *
257
+ * const hooks = createMetricsHooks(sink);
258
+ * // pass `hooks` wherever RunHooks are accepted, or compose with your own:
259
+ * // composeHooks([hooks, myTracingHooks])
260
+ * ```
261
+ */
262
+ interface TelemetrySink {
263
+ /**
264
+ * Record a discrete event (run started, tool failed, recall degraded…).
265
+ * `properties` are flat, JSON-serializable, and free of message contents —
266
+ * only identifiers, counts, and timings.
267
+ */
268
+ track?: (event: string, properties: Record<string, unknown>) => void;
269
+ /**
270
+ * Record a numeric measurement (latency, duration, token count). `tags`
271
+ * are low-cardinality string dimensions (model, toolName, outcome, lane).
272
+ */
273
+ metric?: (name: string, value: number, tags: Record<string, string>) => void;
274
+ }
275
+ /**
276
+ * Default sink that discards everything. Useful as a fallback when telemetry
277
+ * is configured off, and as the base for sinks that override one method.
278
+ *
279
+ * Frozen. It is a published singleton, and `createMetricsHooks` closes over the
280
+ * object rather than its methods, so one module assigning `noopTelemetrySink.track`
281
+ * would reroute every already-constructed adapter that shares the reference — a
282
+ * test stub that forgets to restore changes behaviour for the rest of the suite.
283
+ * Freezing now rather than later: adding it later is the breaking direction,
284
+ * because it turns a silent assignment into a strict-mode TypeError.
285
+ */
286
+ declare const noopTelemetrySink: TelemetrySink;
287
+
288
+ /**
289
+ * {@link RunHooks} adapter that turns tool-loop lifecycle hooks into
290
+ * telemetry events and metrics on a {@link TelemetrySink}.
291
+ *
292
+ * Events emitted (all with `runId`; no message contents or tool arguments):
293
+ *
294
+ * - `run.started` — `{ runId, model }`
295
+ * - `run.completed` — `{ runId, totalSteps, durationMs }`
296
+ * - `run.failed` — `{ runId, durationMs, errorType, stage }`
297
+ * - `model.call.completed` — `{ runId, stepIndex, latencyMs, model?, inputTokens?, outputTokens?, finishReason? }`
298
+ * - `model.call.failed` — `{ runId, stepIndex, latencyMs, model? }`
299
+ * - `tool.call.completed` — `{ runId, stepIndex, toolCallId, toolName, durationMs }`
300
+ * - `tool.call.failed` — `{ runId, stepIndex, toolCallId, toolName, durationMs, errorType }`
301
+ *
302
+ * The raw `error` message string is NOT reported unless the caller opts in
303
+ * with {@link MetricsHooksOptions.includeErrorMessages} — see that option for
304
+ * why. `errorType` is always safe and always reported where the source event
305
+ * carries one.
306
+ *
307
+ * Metrics emitted:
308
+ *
309
+ * - `run.duration` (ms, tags: model?, outcome)
310
+ * - `model.call.latency` (ms, tags: model?, outcome)
311
+ * - `model.call.tokens` (count, tags: direction: input|output) when usage is present
312
+ * - `tool.call.duration` (ms, tags: toolName, outcome, errorType?)
313
+ *
314
+ * Pairing notes (mirrors the contract documented in `runHooks.ts`):
315
+ *
316
+ * - `onRunEnd` / `onRunError` are mutually exclusive and fire exactly once per
317
+ * run, so every started run resolves to exactly one terminal event.
318
+ * - Server-side tools routed via `onToolCall` get `beforeToolUse` with no
319
+ * `afterToolUse`. The adapter therefore never emits a completion for a tool
320
+ * it saw only start; the pending timer is dropped when the run terminates.
321
+ * - `afterToolUse` without a matching `beforeToolUse` (defensive — not a
322
+ * documented case) is still reported, with `durationMs` omitted.
323
+ * - Per-run state is keyed by `runId` and cleared on the terminal hook, so
324
+ * concurrent runs and long-lived adapter instances do not leak timers. The
325
+ * `after*` hooks read that state without creating it, so a hook arriving
326
+ * after the terminal one cannot resurrect an entry nothing would delete
327
+ * again.
328
+ */
329
+
330
+ interface MetricsHooksOptions {
331
+ /**
332
+ * Override the clock used for every duration this adapter reports.
333
+ *
334
+ * The default is monotonic where the runtime has `performance.now()`
335
+ * (browser / RN / Node), falling back to `Date.now()`. That is about
336
+ * monotonicity, not precision: `Date.now()` steps BACKWARD on an NTP
337
+ * correction, a VM resume from snapshot, or a container host clock sync, and
338
+ * a step landing mid-run makes `now() - startedAt` negative. dogstatsd
339
+ * accepts a negative histogram sample, so it is ingested and corrupts the
340
+ * min/p50/p95 for that bucket — silently wrong data, which is the one thing
341
+ * this module exists to prevent.
342
+ *
343
+ * Supply your own only to make durations deterministic in a test.
344
+ */
345
+ now?: () => number;
346
+ /**
347
+ * Report the raw `error` message string on `run.failed`,
348
+ * `model.call.failed`, and `tool.call.failed`. Default `false`.
349
+ *
350
+ * Off by default because those strings are free text and routinely quote
351
+ * the input that caused the failure. `JSON.parse` echoes the offending tool
352
+ * arguments verbatim (`Unexpected token 'h', "hunter2pass"... is not valid
353
+ * JSON`), `executeToolCall` wraps whatever a host executor threw, and an
354
+ * executor with `deAnonymizeArgs` runs on de-redacted PII. Sending them to
355
+ * a sink contradicts {@link TelemetrySink}, which states that properties
356
+ * carry only identifiers, counts, and timings.
357
+ *
358
+ * Turn it on when the sink is infrastructure you own and you accept the
359
+ * exposure. Prefer `errorType`, which is always reported and always safe.
360
+ */
361
+ includeErrorMessages?: boolean;
362
+ }
363
+ /**
364
+ * Build {@link RunHooks} that report run lifecycle, model calls, and tool
365
+ * calls to `sink`. The returned hooks are synchronous and never throw — a
366
+ * throwing sink method is swallowed with a console-free `noop` (the tool loop
367
+ * already swallows hook errors; we avoid double-logging here).
368
+ */
369
+ declare function createMetricsHooks(sink: TelemetrySink, opts?: MetricsHooksOptions): RunHooks;
370
+
371
+ /**
372
+ * Unified memory layer types — the Recall / Retain API.
373
+ *
374
+ * This module defines the public surface for the new unified memory layer
375
+ * that sits above `memoryVault` (curated facts) and `memoryEngine` (raw
376
+ * conversation chunks). Callers should prefer these types over the legacy
377
+ * `VaultSearchResult` / `ChunkSearchResult` shapes; the underlying stores
378
+ * may converge later, the API will not.
379
+ */
380
+
381
+ /**
382
+ * Budget controls retrieval depth/cost. Higher budgets enable more
383
+ * candidate sources and the cross-encoder reranker.
384
+ *
385
+ * - `low`: cosine + BM25 + recency. No reranker. Mobile default.
386
+ * - `mid`: + cross-encoder rerank.
387
+ * - `high`: + multi-hop graph traversal. LLM-free — query decomposition
388
+ * (when wanted) lives in the tool/agent layer ({@link createRecallTool}),
389
+ * not inside `recall()` itself (719/B4).
390
+ */
391
+ type Budget = "low" | "mid" | "high";
392
+ /** Soft-degradation signals surfaced via {@link RecallDiagnostics.degraded}. */
393
+ type RecallDegradation =
394
+ /** Rerank was requested (budget mid/high) but the cross-encoder didn't run
395
+ * this call — unavailable (e.g. React Native) or a transient failure. */
396
+ "rerank-unavailable"
397
+ /**
398
+ * @deprecated 719/B4 moved query decomposition out of `recall()`. Budget
399
+ * `high` no longer requires `decomposeOptions` and this signal is never
400
+ * emitted. Prefer `"decompose-moved"` for the upgrade breadcrumb. Kept in
401
+ * the union so existing telemetry consumers stay typed.
402
+ */
403
+ | "decompose-unavailable"
404
+ /**
405
+ * `budget: 'high'` with `decomposeOptions` set but no usable
406
+ * {@link RecallOptions.subQueries}. Pre-B4 this call shape triggered LLM
407
+ * rewrite inside `recall()`; post-B4 it stays on the single-query high
408
+ * path (rerank + graph) and callers must pass facets (or use
409
+ * {@link createRecallTool}). Surfaced so silent quality regressions show
410
+ * up in diagnostics instead of looking healthier than before.
411
+ */
412
+ | "decompose-moved"
413
+ /** There was no usable cosine lane, so results were ranked on BM25 (lexical)
414
+ * alone: either the query embedding failed (or came back empty), or no
415
+ * candidate had a vector to score against it because the row (re)embed failed.
416
+ * Also set when the chunk lane — cosine-only, with no lexical equivalent — was
417
+ * skipped for the same reason. The embeddings provider is a single upstream
418
+ * with no fallback, so this is a whole-provider outage rather than a per-item
419
+ * miss — expect a quality drop, not a total loss.
420
+ *
421
+ * NOT set for a partial failure that leaves cosine running (a sub-query embed
422
+ * failing back to the single-query path, or a row batch failing while other
423
+ * rows keep usable vectors) — those are logged, not reported, so this stays a
424
+ * reliable outage signal rather than a general embedding-error counter. */
425
+ | "embeddings-unavailable";
426
+ /**
427
+ * Per-call recall observability payload (see {@link RecallOptions.onDiagnostics}).
428
+ * All timings are wall-clock milliseconds. Lane counts are post-dedupe,
429
+ * pre-fusion. Intended to be forwarded to a metrics sink (e.g. PostHog).
430
+ */
431
+ interface RecallDiagnostics {
432
+ /** Budget actually executed (may have downgraded from the requested one). */
433
+ usedBudget: Budget;
434
+ /** Whether the cross-encoder actually reranked the fact lane this call. */
435
+ reranked: boolean;
436
+ /** Total candidates considered before truncation. */
437
+ candidateCount: number;
438
+ /** Total vault size when the fact lane ran (absent if it didn't). */
439
+ vaultSize?: number;
440
+ /**
441
+ * Which vault read path the fact lane actually executed: `true` for the
442
+ * projected key scan that decrypts only the admission window, `false` for the
443
+ * legacy whole-vault load. Absent when the fact lane didn't run.
444
+ *
445
+ * Reported because "the option was passed" and "the branch ran" are different
446
+ * facts, and #845 needed the second one: the projected path was enabled in
447
+ * production and the p50 did not move, with no way to tell a flag that never
448
+ * reached the bundle from a projection that isn't cheaper at that vault size.
449
+ */
450
+ decryptLast?: boolean;
451
+ /**
452
+ * Rows the fact lane paid to decrypt. Absent when it didn't run.
453
+ *
454
+ * Read against {@link RecallDiagnostics.vaultSize} — that ratio is the whole
455
+ * point. `decryptLast` true with `vaultRowsDecrypted` ≈ `vaultSize` means the
456
+ * admission window is admitting the entire vault and the projection is buying
457
+ * nothing. Far below `vaultSize` with latency unchanged means the decrypt was
458
+ * never the cost.
459
+ */
460
+ vaultRowsDecrypted?: number;
461
+ /**
462
+ * Rows the fact lane had to re-embed through the portal because their stored
463
+ * vector was unusable — stale `embedding_model`, wrong dimension, or
464
+ * unparseable. Absent when the fact lane didn't run.
465
+ *
466
+ * Expected to be 0 on a healthy vault: rows are embedded at write time and the
467
+ * re-embed writes the current model back, so a persistently non-zero value
468
+ * means the writeback is not sticking and every turn is paying for it. On the
469
+ * LEGACY read path this batch is uncapped, so it can be the whole vault.
470
+ */
471
+ vaultRowsEmbedded?: number;
472
+ /** Facts the fact lane returned (post-dedupe, pre-fusion). */
473
+ factCount: number;
474
+ /** Chunks the chunk lane returned (post-dedupe, pre-fusion). */
475
+ chunkCount: number;
476
+ /** Wall-clock phase timings (ms). */
477
+ timings: {
478
+ /** Whole `recall()` call. */
479
+ total: number;
480
+ /** Parallel query-embed + graph/temporal side-lane build. */
481
+ prep: number;
482
+ /** Vault fact-lane search (`searchVaultMemoriesWithSize`). */
483
+ factLane: number;
484
+ /**
485
+ * The cross-encoder's share of {@link factLane} — wall-clock spent inside
486
+ * `rerankPairs`. Billed even when the CE threw partway through, because a
487
+ * rerank that burned three seconds and then failed still cost them.
488
+ *
489
+ * A SUBSET of `factLane`, not a sibling: read `factLane - rerank` for
490
+ * everything else the lane did (query embed, vault read, fused ranking).
491
+ * 0 when the CE did not run; `reranked` is what distinguishes that from a
492
+ * rerank that cost nothing.
493
+ *
494
+ * Exists because #845 spent three rounds arguing about which stage inside
495
+ * the fact lane dominated — whole-vault read, then admission window, then
496
+ * the CE — with one aggregate number for all of them. Every hypothesis was
497
+ * an inference; this makes the question a query.
498
+ */
499
+ rerank: number;
500
+ /**
501
+ * The query embed's share of {@link factLane} — one portal round trip.
502
+ *
503
+ * Also a SUBSET of `factLane`. Together with `rerank` this makes the lane
504
+ * decomposable: `factLane - queryEmbed - rerank` is the local work (vault
505
+ * read, decrypt, BM25/cosine fusion), and each of the three implies a
506
+ * completely different fix.
507
+ *
508
+ * 0 when the lane returned before embedding, which includes every empty
509
+ * vault — `prepareVaultCandidates` short-circuits first. That is why the
510
+ * fast `vault_size = 0` population never established a baseline for this
511
+ * cost, and why the ~850ms floor on the smallest NON-empty vaults had no
512
+ * attributable owner.
513
+ */
514
+ queryEmbed: number;
515
+ /** Chunk-lane search (`searchChunksOp`). */
516
+ chunkLane: number;
517
+ /** Cross-lane RRF fusion + provenance dedup after both lanes. */
518
+ fuse: number;
519
+ };
520
+ /** Soft-degradation signals that fired this call (empty when clean). */
521
+ degraded: RecallDegradation[];
522
+ }
523
+
524
+ /**
525
+ * Adapter that turns {@link RecallDiagnostics} (the `onDiagnostics` hook on
526
+ * {@link RecallOptions}) into telemetry events and metrics on a
527
+ * {@link TelemetrySink}.
528
+ *
529
+ * Events emitted:
530
+ *
531
+ * - `recall.completed` — once per `recall()` call with budget, lane counts,
532
+ * and whether the cross-encoder reranked.
533
+ * - `recall.degraded` — once per entry in `diagnostics.degraded`, with the
534
+ * degradation reason. A clean recall emits none.
535
+ *
536
+ * Metrics emitted (all tagged `lane`):
537
+ *
538
+ * - `recall.duration` (ms) for each timing lane: `total`, `prep`, `factLane`,
539
+ * `rerank`, `queryEmbed`, `chunkLane`, `fuse`. `rerank` and `queryEmbed`
540
+ * are SUBSETS of `factLane` — do not sum the lanes.
541
+ * - `recall.candidates` (count), `recall.facts` (count), `recall.chunks` (count)
542
+ * - `recall.vault.size`, `recall.vault.rows_decrypted`,
543
+ * `recall.vault.rows_embedded` when the fact lane reported them.
544
+ *
545
+ * COVERAGE — this handler only sees recalls the host starts itself: a direct
546
+ * `recall()` call, or `reflect()` (`ReflectOptions extends RecallOptions`, and
547
+ * reflect forwards the options object). It does NOT see recalls that happen
548
+ * inside a tool-loop run. `RecallOptions.onDiagnostics` is a single slot, not a
549
+ * composable hook list, and both in-run paths already occupy it to read
550
+ * `degraded` for themselves — the recall tool (`lib/memory/recallTool.ts`) and
551
+ * the vault search tool (`lib/memoryVault/searchTool.ts`). Neither forwards a
552
+ * caller's callback, and neither exposes the slot on its own options.
553
+ *
554
+ * Two consequences to plan for:
555
+ *
556
+ * - Event volume reports host-initiated recall only. A near-empty
557
+ * `recall.completed` stream means the app recalls through the tools. It does
558
+ * not mean recall is broken or that the sink is misconfigured.
559
+ * - These events carry no `runId`, so you cannot join them to `run.failed` from
560
+ * `createMetricsHooks`. That join needs two changes in the memory layer, not
561
+ * here: make `onDiagnostics` forwardable through both tools, and thread the
562
+ * loop's `runId` (minted in `lib/chat/toolLoop.ts`) across the executor
563
+ * boundary onto `RecallDiagnostics`.
564
+ *
565
+ * A host that already holds a `runId` can stamp it today with no signature
566
+ * change: wrap the sink rather than the handler, and put it on `track`
567
+ * properties only. A metric tag of that cardinality is a problem for the
568
+ * backend — see {@link TelemetrySink} on keeping tags low-cardinality.
569
+ */
570
+
571
+ /**
572
+ * Build an `onDiagnostics` callback that forwards recall diagnostics to
573
+ * `sink`. Never throws — a throwing sink method is swallowed, matching the
574
+ * `recall()` contract that diagnostics must not break retrieval.
575
+ */
576
+ declare function createRecallDiagnosticsHandler(sink: TelemetrySink): (diagnostics: RecallDiagnostics) => void;
577
+
578
+ export { type MetricsHooksOptions, type RecallDiagnostics, type RunHooks, type TelemetrySink, createMetricsHooks, createRecallDiagnosticsHandler, noopTelemetrySink };