@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.
- package/dist/expo/index.cjs +45 -5
- package/dist/expo/index.d.mts +64 -1
- package/dist/expo/index.d.ts +64 -1
- package/dist/expo/index.mjs +44 -5
- package/dist/react/index.cjs +46 -13
- package/dist/react/index.d.mts +64 -1
- package/dist/react/index.d.ts +64 -1
- package/dist/react/index.mjs +45 -13
- package/dist/telemetry/index.cjs +255 -0
- package/dist/telemetry/index.d.mts +578 -0
- package/dist/telemetry/index.d.ts +578 -0
- package/dist/telemetry/index.mjs +226 -0
- package/package.json +7 -1
|
@@ -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 };
|