@combycode/llm-sdk 2.0.1 → 2.2.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/CHANGELOG.md +289 -3
- package/dist/agent/context-registry/layers.d.ts +19 -0
- package/dist/agent/lazy-tools.d.ts +85 -0
- package/dist/agent/loop-config.d.ts +28 -0
- package/dist/agent/loop-internals.d.ts +13 -0
- package/dist/agent/loop.d.ts +33 -1
- package/dist/agent/types.d.ts +15 -0
- package/dist/bus/hook-map.d.ts +20 -0
- package/dist/helpers/define-tool.d.ts +15 -6
- package/dist/helpers/engine.d.ts +36 -1
- package/dist/helpers/mcp.d.ts +9 -0
- package/dist/helpers/moderate.d.ts +13 -1
- package/dist/helpers/one-shot.d.ts +19 -0
- package/dist/index.browser.js +850 -76
- package/dist/index.d.ts +11 -4
- package/dist/index.js +850 -76
- package/dist/llm/types/schema-utils.d.ts +19 -0
- package/dist/network/engine.d.ts +10 -2
- package/dist/network/queue-state-config.d.ts +10 -2
- package/dist/network/types.d.ts +6 -0
- package/dist/plugins/cost-collector/collector.d.ts +12 -0
- package/dist/plugins/cost-collector/cost-collector-types.d.ts +8 -0
- package/dist/plugins/mcp/result-cache.d.ts +8 -1
- package/dist/plugins/mcp/tools.d.ts +2 -0
- package/dist/plugins/mcp/types.d.ts +43 -0
- package/dist/plugins/telemetry/telemetry.d.ts +144 -0
- package/dist/types/request-context.d.ts +11 -0
- package/package.json +5 -2
|
@@ -6,3 +6,22 @@ import type { JsonSchema } from './tools';
|
|
|
6
6
|
* reject schemas without this explicit flag. Safe across all providers.
|
|
7
7
|
*/
|
|
8
8
|
export declare function ensureAdditionalProperties(schema: JsonSchema): JsonSchema;
|
|
9
|
+
/** Whose strict-mode rules to judge a schema against. */
|
|
10
|
+
export type StrictDialect = 'openai' | 'anthropic';
|
|
11
|
+
/** Can this schema satisfy the provider's strict mode AS WRITTEN?
|
|
12
|
+
*
|
|
13
|
+
* Strict mode is worth defaulting to — it is what makes a provider constrain the
|
|
14
|
+
* tool name and arguments DURING generation rather than checking after. But the
|
|
15
|
+
* two providers constrain different things, and a schema that violates either is
|
|
16
|
+
* rejected with a 400, not quietly degraded:
|
|
17
|
+
*
|
|
18
|
+
* openai every property must appear in `required`, at every nesting level
|
|
19
|
+
* anthropic a set of validation keywords is simply unsupported
|
|
20
|
+
*
|
|
21
|
+
* So strict is requested only where it can be honoured. The alternative — rewriting
|
|
22
|
+
* the schema to fit, promoting optional properties to required-and-nullable — changes
|
|
23
|
+
* what the tool actually receives, and the receiving end is the caller's code. */
|
|
24
|
+
export declare function strictSupport(schema: JsonSchema | undefined, dialect: StrictDialect): {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
reason?: string;
|
|
27
|
+
};
|
package/dist/network/engine.d.ts
CHANGED
|
@@ -21,11 +21,11 @@ import type { RequestContext } from '../types/request-context';
|
|
|
21
21
|
import type { RateLimiterConfig } from './rate-limiter';
|
|
22
22
|
import type { QueueConfig } from './request-queue';
|
|
23
23
|
import { QueueState } from './queue-state';
|
|
24
|
-
import type {
|
|
24
|
+
import type { RetryPolicyOverride } from './queue-state-config';
|
|
25
25
|
import type { ConnectFn, FetchFn, HttpRequest, HttpResponse, RealtimeConnection, SSEEvent, WsRequest } from './types';
|
|
26
26
|
export interface QueueSettings {
|
|
27
27
|
limits?: Partial<RateLimiterConfig>;
|
|
28
|
-
retry?:
|
|
28
|
+
retry?: RetryPolicyOverride;
|
|
29
29
|
queue?: Partial<QueueConfig>;
|
|
30
30
|
}
|
|
31
31
|
export interface NetworkEngineConfig {
|
|
@@ -39,6 +39,12 @@ export interface NetworkEngineConfig {
|
|
|
39
39
|
connect?: ConnectFn;
|
|
40
40
|
/** Pre-configured per-queue settings. Looked up by queueName at queue creation. */
|
|
41
41
|
queues?: Record<string, QueueSettings>;
|
|
42
|
+
/** Retry policy inherited by EVERY queue this engine creates.
|
|
43
|
+
*
|
|
44
|
+
* Retry is a cross-cutting setting, not something a caller should thread through each call, so
|
|
45
|
+
* it is configured once (`createEngine({ retry })`) and applies everywhere. Three layers,
|
|
46
|
+
* narrowest wins: `HttpRequest.retry` > `queues[name].retry` > this. */
|
|
47
|
+
retry?: RetryPolicyOverride;
|
|
42
48
|
}
|
|
43
49
|
/** Optional context for fetch/fetchStream. RequestContext + per-call overrides. */
|
|
44
50
|
export interface FetchOptions {
|
|
@@ -52,6 +58,8 @@ export declare class NetworkEngine {
|
|
|
52
58
|
readonly hooks: HookBus;
|
|
53
59
|
private readonly fetchFn;
|
|
54
60
|
private readonly connectFn;
|
|
61
|
+
/** Engine-wide retry policy, inherited by every queue created from here. */
|
|
62
|
+
private readonly defaultRetry;
|
|
55
63
|
private readonly settings;
|
|
56
64
|
private readonly queues;
|
|
57
65
|
constructor(config?: NetworkEngineConfig);
|
|
@@ -22,6 +22,14 @@ export interface BackoffConfig {
|
|
|
22
22
|
multiplier: number;
|
|
23
23
|
jitter: number;
|
|
24
24
|
}
|
|
25
|
+
/** A retry policy stated as a partial override of another one.
|
|
26
|
+
*
|
|
27
|
+
* `Partial<RetryConfig>` only makes the TOP-level keys optional, so it still demands a complete
|
|
28
|
+
* `backoff` object — which makes "override one knob, keep the rest" inexpressible even though the
|
|
29
|
+
* merge has always supported it. Engine-level and queue-level overrides take this instead. */
|
|
30
|
+
export type RetryPolicyOverride = Omit<Partial<RetryConfig>, 'backoff'> & {
|
|
31
|
+
backoff?: Partial<BackoffConfig>;
|
|
32
|
+
};
|
|
25
33
|
export interface ErrorRetryConfig {
|
|
26
34
|
retryable?: boolean;
|
|
27
35
|
maxRetries?: number;
|
|
@@ -34,7 +42,7 @@ export interface QueueStateConfig {
|
|
|
34
42
|
fetch: FetchFn;
|
|
35
43
|
hooks: HookBus;
|
|
36
44
|
limits: RateLimiterConfig;
|
|
37
|
-
retry?:
|
|
45
|
+
retry?: RetryPolicyOverride;
|
|
38
46
|
queue?: Partial<QueueConfig>;
|
|
39
47
|
}
|
|
40
48
|
export declare const Priority: {
|
|
@@ -43,4 +51,4 @@ export declare const Priority: {
|
|
|
43
51
|
readonly BACKGROUND: 2;
|
|
44
52
|
readonly LOW: 3;
|
|
45
53
|
};
|
|
46
|
-
export declare function mergeRetry(overrides?:
|
|
54
|
+
export declare function mergeRetry(overrides?: RetryPolicyOverride): RetryConfig;
|
package/dist/network/types.d.ts
CHANGED
|
@@ -69,6 +69,12 @@ export interface TraceContext {
|
|
|
69
69
|
sessionId?: string;
|
|
70
70
|
requestId?: string;
|
|
71
71
|
callId?: string;
|
|
72
|
+
/** W3C `traceparent` of the span this work runs UNDER, when the caller runs the SDK
|
|
73
|
+
* inside its own trace. It rides with the ids because every span we emit needs it,
|
|
74
|
+
* not only the ones built straight from a caller's RequestContext — an agent run
|
|
75
|
+
* reaches its tool calls and its nested agents through this object, and those were
|
|
76
|
+
* left rooting traces of their own while the LLM spans joined the caller's. */
|
|
77
|
+
traceparent?: string;
|
|
72
78
|
}
|
|
73
79
|
/** Point-in-time numeric state of one queue (for metrics / observability). */
|
|
74
80
|
export interface QueueSnapshot {
|
|
@@ -13,6 +13,8 @@ export declare class CostCollector {
|
|
|
13
13
|
private budgets;
|
|
14
14
|
private triggeredThresholds;
|
|
15
15
|
private _runningTotal;
|
|
16
|
+
/** Models already reported as unpriced, so the warning fires once rather than per call. */
|
|
17
|
+
private warnedUnpriced;
|
|
16
18
|
private watchedAgents;
|
|
17
19
|
private unsub;
|
|
18
20
|
private unsubMedia;
|
|
@@ -36,6 +38,16 @@ export declare class CostCollector {
|
|
|
36
38
|
import(entries: CostEntry[]): void;
|
|
37
39
|
private handleMediaGenerated;
|
|
38
40
|
private handleCompletion;
|
|
41
|
+
/** A total of exactly 0 because the model is not in the catalog reads identically to a
|
|
42
|
+
* total of 0 because the call was free — and it silently under-counts every budget
|
|
43
|
+
* and report built on it. `source: 'unknown'` already records the difference per
|
|
44
|
+
* entry, but nothing aggregated it, so a whole benchmark run once reported $0.00000
|
|
45
|
+
* for a live provider and looked like a free arm.
|
|
46
|
+
*
|
|
47
|
+
* Fires once per provider/model: an unpriced model is a configuration fact, not a
|
|
48
|
+
* per-request event, and repeating it on every call would train the reader to ignore
|
|
49
|
+
* it. Free calls are priced 'calculated' with an explicit note, so they stay silent. */
|
|
50
|
+
private noteIfUnpriced;
|
|
39
51
|
private checkBudgets;
|
|
40
52
|
private filterEntries;
|
|
41
53
|
private groupBy;
|
|
@@ -39,4 +39,12 @@ export interface CostSummary {
|
|
|
39
39
|
reasoning: number;
|
|
40
40
|
};
|
|
41
41
|
entries: number;
|
|
42
|
+
/** How many of `entries` could not be priced — no catalog entry for the model, or no
|
|
43
|
+
* applicable rate. They count as 0 in every field above, so a summary without this
|
|
44
|
+
* number cannot tell "this run was cheap" from "this run was never priced". Free
|
|
45
|
+
* calls are NOT counted here: they are priced, at zero. */
|
|
46
|
+
unpriced: number;
|
|
47
|
+
/** The distinct `provider/model` values behind `unpriced`, so the gap is actionable
|
|
48
|
+
* rather than merely visible. Empty when everything was priced. */
|
|
49
|
+
unpricedModels: string[];
|
|
42
50
|
}
|
|
@@ -21,7 +21,14 @@ export declare class McpResultCache {
|
|
|
21
21
|
* different entries. */
|
|
22
22
|
static key(method: string, params?: unknown): string;
|
|
23
23
|
get(key: string, now?: number): unknown | undefined;
|
|
24
|
-
/** Store only when the server actually asked for it. Returns whether anything was stored.
|
|
24
|
+
/** Store only when the server actually asked for it. Returns whether anything was stored.
|
|
25
|
+
*
|
|
26
|
+
* A non-positive `ttlMs` is an instruction, not a missing value: the server is saying *do not
|
|
27
|
+
* reuse this*. Any entry already held under that key is dropped, so the next `get` re-fetches.
|
|
28
|
+
* Without the eviction the hint is inert — a server that first said "cache for 60s" and then
|
|
29
|
+
* says "stale now" would keep being answered from the stale entry for the rest of the original
|
|
30
|
+
* TTL. Absent hints are different and must stay different: they carry no instruction, so an
|
|
31
|
+
* existing entry is left alone and pre-2026 servers behave exactly as before. */
|
|
25
32
|
set(key: string, value: unknown, hints: McpCacheHints | undefined, now?: number): boolean;
|
|
26
33
|
/** Drop everything — e.g. after a `*_changed` notification says the server moved on. */
|
|
27
34
|
clear(): void;
|
|
@@ -8,6 +8,8 @@ export interface McpToolAdapterOptions {
|
|
|
8
8
|
/** Validate `structuredContent` against the tool's `outputSchema`; on mismatch
|
|
9
9
|
* the tool returns an error string instead of the content. Default false. */
|
|
10
10
|
validateOutput?: boolean;
|
|
11
|
+
/** Register the tool without declaring it — see `AgentTool.lazy`. */
|
|
12
|
+
lazy?: boolean;
|
|
11
13
|
}
|
|
12
14
|
/** Map a `tools/call` result to our tool-result shape: a plain string when the
|
|
13
15
|
* content is text-only, else a ContentPart[] (images/audio kept as base64).
|
|
@@ -14,6 +14,40 @@ export interface JsonRpcResponse {
|
|
|
14
14
|
result?: unknown;
|
|
15
15
|
error?: JsonRpcError;
|
|
16
16
|
}
|
|
17
|
+
/** Behavioural hints a server publishes about a tool. Advisory and UNVERIFIED — a
|
|
18
|
+
* server asserts them about itself — so they inform a host's UX (confirm before a
|
|
19
|
+
* destructive call) and must never be treated as a security boundary.
|
|
20
|
+
*
|
|
21
|
+
* Open (CONSTITUTION R1): the spec gains hints over time and an unknown one must
|
|
22
|
+
* survive rather than be dropped. */
|
|
23
|
+
export interface McpToolAnnotations {
|
|
24
|
+
title?: string;
|
|
25
|
+
/** The tool does not modify its environment. Default false. */
|
|
26
|
+
readOnlyHint?: boolean;
|
|
27
|
+
/** The tool may perform destructive updates rather than only additive ones. */
|
|
28
|
+
destructiveHint?: boolean;
|
|
29
|
+
/** Repeated calls with the same arguments have no additional effect. */
|
|
30
|
+
idempotentHint?: boolean;
|
|
31
|
+
/** The tool touches an open world (the internet) rather than a closed one. */
|
|
32
|
+
openWorldHint?: boolean;
|
|
33
|
+
[hint: string]: unknown;
|
|
34
|
+
}
|
|
35
|
+
/** An icon a host may render beside the tool. Display only. */
|
|
36
|
+
export interface McpToolIcon {
|
|
37
|
+
src: string;
|
|
38
|
+
mimeType?: string;
|
|
39
|
+
sizes?: string[];
|
|
40
|
+
theme?: 'light' | 'dark' | (string & {});
|
|
41
|
+
}
|
|
42
|
+
/** How the tool must be invoked.
|
|
43
|
+
*
|
|
44
|
+
* `taskSupport: 'required'` means the client MUST call it as a task rather than a
|
|
45
|
+
* plain `tools/call`. Task invocation is not implemented here, so such a tool
|
|
46
|
+
* cannot be called through this client — carrying the field lets a caller SEE
|
|
47
|
+
* that instead of discovering it as a server error. Absent means `'forbidden'`. */
|
|
48
|
+
export interface McpToolExecution {
|
|
49
|
+
taskSupport?: 'required' | 'optional' | 'forbidden' | (string & {});
|
|
50
|
+
}
|
|
17
51
|
export interface McpToolDef {
|
|
18
52
|
name: string;
|
|
19
53
|
description?: string;
|
|
@@ -22,6 +56,15 @@ export interface McpToolDef {
|
|
|
22
56
|
inputSchema: Record<string, unknown>;
|
|
23
57
|
/** JSON Schema for the tool's `structuredContent` output (optional). */
|
|
24
58
|
outputSchema?: Record<string, unknown>;
|
|
59
|
+
/** Advisory behavioural hints. HOST-facing, never sent to the model: no
|
|
60
|
+
* provider's function-tool schema has a field that could carry them. */
|
|
61
|
+
annotations?: McpToolAnnotations;
|
|
62
|
+
/** Display icons for a host UI. */
|
|
63
|
+
icons?: McpToolIcon[];
|
|
64
|
+
/** Invocation requirements; see {@link McpToolExecution}. */
|
|
65
|
+
execution?: McpToolExecution;
|
|
66
|
+
/** Spec-defined passthrough metadata. */
|
|
67
|
+
_meta?: Record<string, unknown>;
|
|
25
68
|
}
|
|
26
69
|
/** A content block in a `tools/call` result. Open union — unknown types ignored. */
|
|
27
70
|
export type McpContentBlock = {
|
|
@@ -11,10 +11,29 @@
|
|
|
11
11
|
* into OTLP-compatible JSON for a real OTel exporter to forward. */
|
|
12
12
|
import type { HookBus } from '../../bus/hook-bus';
|
|
13
13
|
import type { HookName } from '../../bus/hook-map';
|
|
14
|
+
/** Derive a conformant hex id from one of our readable ids. `bytes` is 16 for a trace
|
|
15
|
+
* id, 8 for a span id.
|
|
16
|
+
*
|
|
17
|
+
* Deterministic on purpose: the same logical trace maps to the same OTLP id on every
|
|
18
|
+
* export and in every process, so a trace stitched together from two exports — or
|
|
19
|
+
* from two services sharing a sessionId — still joins up in the backend. */
|
|
20
|
+
export declare function toOtlpId(input: string, bytes: 8 | 16): string;
|
|
21
|
+
/** One attribute in OTLP's AnyValue shape, keeping numbers numeric. Integers go out as
|
|
22
|
+
* `intValue` carrying a STRING, which is how OTLP/JSON encodes 64-bit integers; send
|
|
23
|
+
* them as plain strings instead and no backend can sum them. */
|
|
24
|
+
export declare function toOtlpValue(value: unknown): Record<string, unknown>;
|
|
14
25
|
export type SpanKind = 'llm' | 'http' | 'media' | 'agent' | 'tool' | 'mcp' | 'other';
|
|
15
26
|
export interface Span {
|
|
16
27
|
traceId: string;
|
|
17
28
|
spanId: string;
|
|
29
|
+
/** The span this one runs under. Without it every span is a sibling and a backend
|
|
30
|
+
* draws a flat list instead of a tree — so a run reads as "9 things happened", not
|
|
31
|
+
* "a turn, which called a tool, which asked a second model".
|
|
32
|
+
*
|
|
33
|
+
* Resolved in this order: the innermost container span still open on this trace
|
|
34
|
+
* (`agent.run` / `tool.call`), else the app's span from a supplied `traceparent`,
|
|
35
|
+
* else none — this span is the root. */
|
|
36
|
+
parentSpanId?: string;
|
|
18
37
|
name: string;
|
|
19
38
|
kind: SpanKind;
|
|
20
39
|
startTime: number;
|
|
@@ -23,6 +42,34 @@ export interface Span {
|
|
|
23
42
|
status: 'unset' | 'ok' | 'error';
|
|
24
43
|
attributes: Record<string, unknown>;
|
|
25
44
|
}
|
|
45
|
+
/** What kind of work an event describes. `message` is conversation content, which is not
|
|
46
|
+
* a span — it is the thing you want in a debug store and NOT in your metrics backend,
|
|
47
|
+
* which is exactly why it filters separately. */
|
|
48
|
+
export type TraceEventType = 'agent' | 'tool' | 'llm' | 'http' | 'mcp' | 'media' | 'message' | 'other';
|
|
49
|
+
/** One piece of work, carrying enough of the tree that a consumer can push it straight
|
|
50
|
+
* into their own tracer without reconstructing anything. */
|
|
51
|
+
export interface TraceEvent {
|
|
52
|
+
type: TraceEventType;
|
|
53
|
+
/** The app's trace when it supplied a `traceparent`, else ours. */
|
|
54
|
+
traceId: string;
|
|
55
|
+
spanId: string;
|
|
56
|
+
/** Already resolved past anything this subscriber filtered out — see `survivingParent`. */
|
|
57
|
+
parentSpanId?: string;
|
|
58
|
+
/** The conventional name (`chat gpt-5.4-nano`, `execute_tool search`). */
|
|
59
|
+
name: string;
|
|
60
|
+
startTime: number;
|
|
61
|
+
endTime?: number;
|
|
62
|
+
durationMs?: number;
|
|
63
|
+
status: 'unset' | 'ok' | 'error';
|
|
64
|
+
attributes: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
/** Declarative on purpose, rather than a predicate: knowing the types up front lets a
|
|
67
|
+
* filtered-out event cost nothing, where a predicate would force us to build the payload
|
|
68
|
+
* just to let the caller throw it away. */
|
|
69
|
+
export interface TraceFilter {
|
|
70
|
+
types?: readonly TraceEventType[];
|
|
71
|
+
}
|
|
72
|
+
export type TraceHandler = (event: TraceEvent) => void;
|
|
26
73
|
export interface TelemetryEvent {
|
|
27
74
|
seq: number;
|
|
28
75
|
time: number;
|
|
@@ -50,6 +97,13 @@ export interface TelemetryMetrics {
|
|
|
50
97
|
avg: number;
|
|
51
98
|
};
|
|
52
99
|
}
|
|
100
|
+
/** Parse a W3C `traceparent`: `00-<32 hex trace>-<16 hex span>-<flags>`.
|
|
101
|
+
* Returns null for anything malformed or for the all-zero ids the spec forbids —
|
|
102
|
+
* a bad header must not silently reroute telemetry into a garbage trace. */
|
|
103
|
+
export declare function parseTraceparent(value: string | undefined): {
|
|
104
|
+
traceId: string;
|
|
105
|
+
spanId: string;
|
|
106
|
+
} | null;
|
|
53
107
|
/** OpenTelemetry Resource — identifies the SERVICE producing this telemetry, so
|
|
54
108
|
* a shared backend can separate streams from different apps and attribute cost
|
|
55
109
|
* per service (`sum by service.name`). Stamped on every span/metric/log. */
|
|
@@ -80,6 +134,32 @@ export interface TelemetryAdapterOptions {
|
|
|
80
134
|
* boundary (a shared collector, a vendor APM) and the message is replaced by a
|
|
81
135
|
* fixed `[redacted]` string while name/code/status are kept for triage. */
|
|
82
136
|
includeSensitiveData?: boolean;
|
|
137
|
+
/** Which event types to hand to `onTrace`. Omitted → everything.
|
|
138
|
+
*
|
|
139
|
+
* Filtering SPLICES the tree rather than punching holes in it: drop `http` and the
|
|
140
|
+
* spans under it re-parent to the nearest surviving ancestor. Dropping without that
|
|
141
|
+
* leaves orphans, and a backend draws an orphan as a second root — worse than not
|
|
142
|
+
* filtering at all. */
|
|
143
|
+
types?: readonly TraceEventType[];
|
|
144
|
+
/** Whether conversation content rides along on `message` events. Default `'none'`:
|
|
145
|
+
* prompts and completions are the debugging gold AND the PII, so sending them is a
|
|
146
|
+
* decision to make on purpose rather than inherit. `'full'` adds the Opt-In
|
|
147
|
+
* `gen_ai.input.messages` / `gen_ai.output.messages` attributes; `'none'` still
|
|
148
|
+
* reports the shape (counts and sizes), which is enough to spot a runaway prompt. */
|
|
149
|
+
content?: 'none' | 'full';
|
|
150
|
+
/** Fraction of TRACES to emit, 0..1. Default 1.
|
|
151
|
+
*
|
|
152
|
+
* Per trace, never per span: sampling spans independently shreds every tree it touches
|
|
153
|
+
* — a tool call with no run, a model call with no tool. The decision is a hash of the
|
|
154
|
+
* trace id, so it is stable across processes and two services sharing a trace agree
|
|
155
|
+
* without coordinating.
|
|
156
|
+
*
|
|
157
|
+
* This is HEAD sampling: the choice is made when the trace first appears, before we
|
|
158
|
+
* know whether it ends in an error. Keeping all errors needs tail sampling, which
|
|
159
|
+
* needs buffering; do that in your collector, which is built for it. */
|
|
160
|
+
sample?: number;
|
|
161
|
+
/** Convenience for the common case of a single sink — same as calling `onTrace`. */
|
|
162
|
+
onTrace?: TraceHandler;
|
|
83
163
|
}
|
|
84
164
|
export declare class TelemetryAdapter {
|
|
85
165
|
readonly events: TelemetryEvent[];
|
|
@@ -88,15 +168,79 @@ export declare class TelemetryAdapter {
|
|
|
88
168
|
/** Service identity stamped on exported telemetry. */
|
|
89
169
|
readonly resource: TelemetryResource;
|
|
90
170
|
private seq;
|
|
171
|
+
/** Discriminator for POINT spans (media, mcp connect/tool), whose natural keys are
|
|
172
|
+
* not unique — the same server reconnects, a run emits two images, two tool calls
|
|
173
|
+
* land in one millisecond. A duplicate span id inside a trace is invalid OTLP and
|
|
174
|
+
* the backend silently keeps only one. */
|
|
175
|
+
private spanSeq;
|
|
176
|
+
/** Per trace: the app's span from a `traceparent`, and the CONTAINER spans currently
|
|
177
|
+
* open on it. Together they decide what a new span hangs under — see `parentFor`.
|
|
178
|
+
* Both are cleared once a trace has nothing open, so a long-lived process does not
|
|
179
|
+
* accumulate an entry per conversation forever.
|
|
180
|
+
*
|
|
181
|
+
* A list, not a single slot: an agent nested in a tool call (C2 inside C1's tool) is a
|
|
182
|
+
* second run on the SAME trace, and with one slot it overwrote its own parent and then
|
|
183
|
+
* deleted it on close — leaving the rest of the outer run parentless. */
|
|
184
|
+
private appParent;
|
|
185
|
+
private containers;
|
|
91
186
|
private latSum;
|
|
92
187
|
private readonly open;
|
|
93
188
|
private readonly maxEvents;
|
|
94
189
|
private readonly includeSensitiveData;
|
|
95
190
|
private readonly unsub;
|
|
191
|
+
/** Subscribers, each with its own filter. Re-parenting is computed PER SINK: two
|
|
192
|
+
* consumers asking for different types each get a tree that is correct for them. */
|
|
193
|
+
private readonly sinks;
|
|
194
|
+
private readonly content;
|
|
195
|
+
private readonly sampleRate;
|
|
196
|
+
/** spanId → its parent and type, for EVERY span including filtered ones — walking up
|
|
197
|
+
* past a dropped ancestor is the whole point, so the dropped ones must still be here.
|
|
198
|
+
* Bounded, because a long-lived process would otherwise remember every span it ever
|
|
199
|
+
* saw. */
|
|
200
|
+
private readonly lineage;
|
|
201
|
+
private readonly maxLineage;
|
|
202
|
+
private msgSeq;
|
|
96
203
|
constructor(hooks: HookBus, opts?: TelemetryAdapterOptions);
|
|
204
|
+
/** Subscribe to the event stream. Returns an unsubscribe function.
|
|
205
|
+
*
|
|
206
|
+
* ```ts
|
|
207
|
+
* const stop = telemetry.onTrace({ types: ['agent', 'tool'] }, (e) => pipeline.push(e));
|
|
208
|
+
* ```
|
|
209
|
+
*/
|
|
210
|
+
onTrace(handler: TraceHandler): () => void;
|
|
211
|
+
onTrace(filter: TraceFilter, handler: TraceHandler): () => void;
|
|
212
|
+
/** Record a finished span and hand it to the subscribers. Every span reaches the store
|
|
213
|
+
* through here, so there is one place where an event can be missed rather than five. */
|
|
214
|
+
private recordSpan;
|
|
215
|
+
private remember;
|
|
216
|
+
private dispatch;
|
|
217
|
+
/** The nearest ancestor this subscriber actually receives. Without this, filtering out
|
|
218
|
+
* `http` would leave its children pointing at a span that never arrives, and a backend
|
|
219
|
+
* renders a dangling parent as a separate root. */
|
|
220
|
+
private survivingParent;
|
|
221
|
+
/** Hashed rather than random, so the same trace samples the same way in every process
|
|
222
|
+
* and a trace shared by two services is kept or dropped by both. */
|
|
223
|
+
private isSampled;
|
|
224
|
+
/** Conversation content, as its own event so it can be routed somewhere different from
|
|
225
|
+
* the spans — a debug store, not the metrics backend. */
|
|
226
|
+
private emitMessage;
|
|
97
227
|
/** Stop tapping the bus. */
|
|
98
228
|
destroy(): void;
|
|
99
229
|
private handle;
|
|
230
|
+
/** What a new span on this trace hangs under: the innermost container still open on
|
|
231
|
+
* it, else the app's span, else nothing (we are the root).
|
|
232
|
+
*
|
|
233
|
+
* A container wins over the app's span because an LLM call made during a run belongs
|
|
234
|
+
* to that run — attaching it straight to the app would flatten the very nesting the
|
|
235
|
+
* tree exists to show. A span joins the stack only after it is built, so nothing can
|
|
236
|
+
* become its own parent, and a run nested in a tool call lands under that tool call —
|
|
237
|
+
* exactly where it happened.
|
|
238
|
+
*
|
|
239
|
+
* Limit worth naming: with tools running in parallel two `tool.call` spans are open at
|
|
240
|
+
* once and "innermost" is merely the more recent one. Attributing a nested run to the
|
|
241
|
+
* right sibling needs real async context propagation, which this adapter does not
|
|
242
|
+
* have; sequential tools, the common case, are exact. */
|
|
243
|
+
private parentFor;
|
|
100
244
|
private openSpan;
|
|
101
245
|
private closeSpan;
|
|
102
246
|
private recordLatency;
|
|
@@ -25,6 +25,17 @@ export interface RequestContext {
|
|
|
25
25
|
* server/agent set it, else the LLM client mints in buildContext). Follows
|
|
26
26
|
* the whole chain; the request half of the trace id. */
|
|
27
27
|
requestId?: string;
|
|
28
|
+
/** W3C trace context of the span this work runs UNDER — `00-<32 hex trace>-<16 hex
|
|
29
|
+
* span>-<flags>`, exactly the `traceparent` header shape.
|
|
30
|
+
*
|
|
31
|
+
* Pass it and the SDK stops rooting its own trace: its spans join that trace and hang
|
|
32
|
+
* under that span. Without it the library cannot know it is inside an application's
|
|
33
|
+
* request, so the business chain and the model calls reach the backend as two
|
|
34
|
+
* unrelated traces.
|
|
35
|
+
*
|
|
36
|
+
* Sources: the inbound `traceparent` header, or an active span from an OTel SDK if
|
|
37
|
+
* the app already runs one. */
|
|
38
|
+
traceparent?: string;
|
|
28
39
|
/** = `history.id`. Stable for the lifetime of the conversation. */
|
|
29
40
|
conversationId?: string;
|
|
30
41
|
/** Unique per `.complete()` / `.stream()` call. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@combycode/llm-sdk",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"description": "Unified, pluggable AI SDK for accessing the LLMs of every major provider (Anthropic, OpenAI, Google, xAI, OpenRouter) through one API. Cross-environment: Node, Bun, and the browser.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -42,7 +42,10 @@
|
|
|
42
42
|
"lint:fix": "biome lint --write src tests",
|
|
43
43
|
"format": "biome format --write src tests",
|
|
44
44
|
"check": "biome check src tests",
|
|
45
|
-
"check:fix": "biome check --write src tests"
|
|
45
|
+
"check:fix": "biome check --write src tests",
|
|
46
|
+
"gate": "node ../../quality-gate/gate.mjs",
|
|
47
|
+
"gate:selftest": "node ../../quality-gate/selftest.mjs",
|
|
48
|
+
"gate:snapshot": "node ../../quality-gate/gate.mjs --only api-snapshot --update"
|
|
46
49
|
},
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@biomejs/biome": "^2.4.13",
|