@combycode/llm-sdk 2.1.0 → 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 +109 -0
- package/dist/agent/loop-config.d.ts +19 -0
- package/dist/agent/loop-internals.d.ts +13 -0
- package/dist/agent/loop.d.ts +6 -0
- package/dist/bus/hook-map.d.ts +6 -0
- package/dist/helpers/engine.d.ts +19 -0
- package/dist/index.browser.js +434 -47
- package/dist/index.d.ts +1 -1
- package/dist/index.js +434 -47
- package/dist/network/types.d.ts +6 -0
- package/dist/plugins/telemetry/telemetry.d.ts +144 -0
- package/dist/types/request-context.d.ts +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,115 @@ All notable changes to `@combycode/llm-sdk` are documented here. The format foll
|
|
|
4
4
|
[Keep a Changelog](https://keepachangelog.com/) and the project adheres to
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## [2.2.0] — 2026-08-17
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Agents can be named: `label`, `source` and an `attributes` bag.** An unlabelled agent
|
|
12
|
+
exported as a bare `invoke_agent` carrying only a per-process id, so a trace could not say
|
|
13
|
+
which agent ran or be compared across runs. `label` becomes `gen_ai.agent.name` and names
|
|
14
|
+
the span (`invoke_agent briefing`); `source` records which part of the host system the
|
|
15
|
+
agent belongs to, as free text because the taxonomy is the application's; `attributes`
|
|
16
|
+
stamps anything else onto the run. Library attributes win on a key collision, so the bag
|
|
17
|
+
cannot rewrite what a span claims to be.
|
|
18
|
+
|
|
19
|
+
- **`onTrace` -- an event surface, so this SDK can be one source in a bigger pipeline.**
|
|
20
|
+
Configure it at `createEngine({ telemetry: { types, content, sample, onTrace } })` and take
|
|
21
|
+
the levels you want: an operator reading business traces does not want our HTTP retries,
|
|
22
|
+
and an SDK that ships its own exporter just competes with the pipeline they already run.
|
|
23
|
+
Events carry `traceId` / `spanId` / `parentSpanId`, so a consumer pushes them straight
|
|
24
|
+
into their own tracer. Nothing is sent anywhere by the library.
|
|
25
|
+
|
|
26
|
+
Filtering **splices** the tree rather than punching holes in it -- drop `http` and its
|
|
27
|
+
children re-parent to the nearest ancestor that subscriber still receives, because a
|
|
28
|
+
dangling parent renders as a second root. Sampling is per **trace**, hashed from the
|
|
29
|
+
trace id so two services sharing one agree without coordinating; sampling per span would
|
|
30
|
+
shred every tree it touched. Conversation content is off by default and rides on
|
|
31
|
+
`message` events only, never on spans, so spans can go to a metrics backend without
|
|
32
|
+
carrying prompts into it.
|
|
33
|
+
|
|
34
|
+
- **The SDK can run inside your application's trace.** Pass `ctx.traceparent` -- the W3C header
|
|
35
|
+
shape -- and every span the run emits joins that trace and hangs under that span instead of
|
|
36
|
+
rooting one of its own. A business chain and the model calls it triggers now arrive as one
|
|
37
|
+
request rather than two unrelated traces. A malformed header is ignored rather than fatal.
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- **Exported spans follow the GenAI semantic conventions.** `agent.run` and `tool.call` export as
|
|
42
|
+
`invoke_agent` and `execute_tool {name}`, carrying `gen_ai.operation.name`, `gen_ai.agent.id`,
|
|
43
|
+
`gen_ai.tool.name` and `gen_ai.tool.call.id`. Names only this library understood forced every
|
|
44
|
+
consumer to write its own mapping; a backend that speaks the conventions now recognises the work
|
|
45
|
+
without one. Internal names are unchanged, so `snapshot()` and the sandbox sidebar group as
|
|
46
|
+
before.
|
|
47
|
+
|
|
48
|
+
**If you read span attributes,** three keys moved: `tool.name` → `gen_ai.tool.name`,
|
|
49
|
+
`agent.id` → `gen_ai.agent.id`, `agent.model` → `gen_ai.request.model`. Span *names* in
|
|
50
|
+
`snapshot()` are untouched; only the exported ones changed.
|
|
51
|
+
|
|
52
|
+
### Fixed
|
|
53
|
+
|
|
54
|
+
- **Spans had no parent, so a backend drew a flat list instead of a tree.** Every span was a
|
|
55
|
+
sibling, and a turn read as "nine things happened" rather than "a run, which called a tool, which
|
|
56
|
+
asked a second model". Spans now carry `parentSpanId`, resolved to the innermost enclosing
|
|
57
|
+
`agent.run` / `tool.call`, else the caller's span, else none.
|
|
58
|
+
|
|
59
|
+
- **An agent nested inside a tool call orphaned the run around it.** The enclosing span was tracked
|
|
60
|
+
as one slot per trace, so a second run on the same trace overwrote it and then deleted it on
|
|
61
|
+
close, leaving the rest of the outer run parentless. It is a stack now, and a container is removed
|
|
62
|
+
by id rather than popped, because parallel tool calls close out of order.
|
|
63
|
+
|
|
64
|
+
- **`traceparent` was dropped at two layers, each by hand-picking fields off the trace.** `beginRun`
|
|
65
|
+
built `{ sessionId, requestId }` and `LLMClient` handed `{ sessionId, requestId, callId }` to the
|
|
66
|
+
network layer. One request became three traces: the model calls joined the caller's, while
|
|
67
|
+
`agent.run`, every `tool.call`, every nested agent and every `http.request` rooted their own. The
|
|
68
|
+
trace now travels whole, and `RunTrace` replaces a shape written inline at eleven signatures.
|
|
69
|
+
|
|
70
|
+
- **One agent run arrived as several unrelated traces.** The agent built a `runTrace` for its own
|
|
71
|
+
spans and never handed it to the LLM calls it made, so each call fell through to mint-if-absent and
|
|
72
|
+
invented its own `requestId`. Since the trace id is `sessionId:requestId`, a single conversation
|
|
73
|
+
fragmented: measured against a live Grafana Tempo endpoint, one turn with one tool call produced
|
|
74
|
+
SIX traces. Every span looked correct on its own, which is why it survived until telemetry was
|
|
75
|
+
pointed at a real backend.
|
|
76
|
+
|
|
77
|
+
- **A caller's own trace ids were discarded, then half-honoured.** `ctx.sessionId` / `ctx.requestId`
|
|
78
|
+
now win over the agent's, and the run trace is derived in ONE place from them — deriving it
|
|
79
|
+
separately for agent spans and LLM calls meant a caller passing only `sessionId` split the run in
|
|
80
|
+
two. `ctx.conversationId` likewise wins over the history id instead of being silently overwritten.
|
|
81
|
+
|
|
82
|
+
- **`agent.run` and `tool.call` spans used an entity id as the trace id** — the run id and the tool
|
|
83
|
+
call id respectively — putting them in a different trace from the work they describe. One span's
|
|
84
|
+
trace id was literally `t1`. MCP spans keyed their trace by server name, merging every call to a
|
|
85
|
+
server over the process lifetime into one eternal trace.
|
|
86
|
+
|
|
87
|
+
- **Span ids collided once a run shared one trace.** The span KEY (`llm:${traceId}`) doubled as the
|
|
88
|
+
span ID, so every LLM call in a run emitted the same id and the collector merged them into one
|
|
89
|
+
span. Key and id are now separate.
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
- **`toOtlpTraces()` produced JSON that only LOOKED like OTLP, and no collector would accept it.**
|
|
95
|
+
Trace ids went out as `s:r` and span ids as `llm:s:r` where the protocol requires 16- and 8-byte
|
|
96
|
+
hex; `kind` was the string `'llm'` where it must be the int enum; and every attribute value was
|
|
97
|
+
`String(value)`, so `gen_ai.usage.input_tokens` arrived as text and could not be summed by any
|
|
98
|
+
backend. Ids are now derived deterministically from the readable internal ones, so a trace split
|
|
99
|
+
across two exports still joins up.
|
|
100
|
+
|
|
101
|
+
- **LLM spans used attribute names no backend recognises.** `gen_ai.provider` / `gen_ai.model` are
|
|
102
|
+
not in the OTel GenAI semantic conventions; the required names are `gen_ai.provider.name` and
|
|
103
|
+
`gen_ai.operation.name`, with `gen_ai.request.model`. A span carrying the old names is not
|
|
104
|
+
identified as a model call at all. Adds `gen_ai.response.model` (the model that actually answered,
|
|
105
|
+
which an alias can change) and `gen_ai.conversation.id` (the agent's history id).
|
|
106
|
+
|
|
107
|
+
- **Point spans could share an id, and the backend silently dropped the duplicates.**
|
|
108
|
+
`mcp:connect:${server}` repeated on every reconnect and `media:${traceId}` repeated for a second
|
|
109
|
+
image in the same run; `mcp:tool:…:${Date.now()}` collided for two calls in one millisecond. A
|
|
110
|
+
duplicate span id within a trace is invalid OTLP, so those runs looked like they did less work
|
|
111
|
+
than they did.
|
|
112
|
+
|
|
113
|
+
The in-memory model is unchanged — `snapshot()` still returns readable ids and the domain `kind`,
|
|
114
|
+
which is what the sandbox groups by. Only the export is translated.
|
|
115
|
+
|
|
7
116
|
## [2.1.0] — 2026-08-17
|
|
8
117
|
|
|
9
118
|
Minor, not major: everything below is additive or a bug fix, and no export was removed or
|
|
@@ -14,6 +14,25 @@ import type { Persistence } from '../plugins/persistence/types';
|
|
|
14
14
|
export interface AgentLoopConfig {
|
|
15
15
|
/** LLM client. AgentLoop reads `client.model` and uses `client.complete`/`client.stream`. */
|
|
16
16
|
client: LLMClient;
|
|
17
|
+
/** Human name for this agent, e.g. `'briefing'`. Without it telemetry only has the
|
|
18
|
+
* agent's generated id, and a trace reads as `invoke_agent` with no clue which of your
|
|
19
|
+
* agents ran — the ids differ per process, so they cannot be compared across runs
|
|
20
|
+
* either. With it the span becomes `invoke_agent briefing` and carries
|
|
21
|
+
* `gen_ai.agent.name`, which is what the conventions ask for. */
|
|
22
|
+
label?: string;
|
|
23
|
+
/** Which part of YOUR system this agent belongs to, e.g. `'customer'`, `'moderation'`.
|
|
24
|
+
*
|
|
25
|
+
* Free text rather than a fixed set, because the taxonomy is the application's: a
|
|
26
|
+
* library cannot know whether you divide by product surface, team, or bounded context,
|
|
27
|
+
* and forcing our categories on you would only make you encode yours inside a `label`.
|
|
28
|
+
* Exported as `agent.source` — our attribute, not a convention one; the GenAI spec has
|
|
29
|
+
* no term for it. */
|
|
30
|
+
source?: string;
|
|
31
|
+
/** Extra attributes stamped on this agent's span, for whatever the fixed fields do not
|
|
32
|
+
* cover — tenant, tier, experiment arm. Keys are used verbatim, so namespace them
|
|
33
|
+
* (`app.tenant`) to stay clear of convention attributes; ours win on a collision, so a
|
|
34
|
+
* stray key here cannot corrupt `gen_ai.*`. */
|
|
35
|
+
attributes?: Record<string, string | number | boolean>;
|
|
17
36
|
/** Persona / role text for the agent. Stored as the `agentloop.system` registry
|
|
18
37
|
* layer (priority 10). Composed with other system-tagged layers when sending.
|
|
19
38
|
* When passed as a function, it is re-evaluated at the start of every
|
|
@@ -7,6 +7,19 @@ import type { StreamEvent } from '../llm/types/stream';
|
|
|
7
7
|
import type { TraceContext } from '../network/types';
|
|
8
8
|
import type { AgentStreamEvent, AgentTool, ToolCallReport, ToolExecutionContext } from './types';
|
|
9
9
|
import type { StepState } from './loop-step-state';
|
|
10
|
+
/** The trace one agent run belongs to — resolved once in `beginRun` and handed to
|
|
11
|
+
* everything the run emits: its own span, its LLM calls, its tool calls, and any agent
|
|
12
|
+
* nested inside a tool.
|
|
13
|
+
*
|
|
14
|
+
* Not a bare `TraceContext` because `sessionId`/`requestId` are always resolved here
|
|
15
|
+
* (mint-if-absent), while `traceparent` appears only when the caller runs us inside a
|
|
16
|
+
* span of its own. Named rather than written inline because the shape was repeated at
|
|
17
|
+
* eleven signatures — and adding a field to ten of them is how a run ends up split
|
|
18
|
+
* across two traces. */
|
|
19
|
+
export type RunTrace = TraceContext & {
|
|
20
|
+
sessionId: string;
|
|
21
|
+
requestId: string;
|
|
22
|
+
};
|
|
10
23
|
/** Create a fresh StepState for the start of a streaming step. */
|
|
11
24
|
export declare function makeStepState(): StepState;
|
|
12
25
|
/** Accumulate one SSE StreamEvent into StepState.
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -23,6 +23,12 @@ import type { PermissionPolicy } from '../plugins/permissions/policy';
|
|
|
23
23
|
import type { ApprovalRequest, ApprovalDecision, PendingToolCall } from './approval-types';
|
|
24
24
|
export declare class AgentLoop {
|
|
25
25
|
readonly id: string;
|
|
26
|
+
/** Human name, surfaced as `gen_ai.agent.name` — see AgentLoopConfig.label. */
|
|
27
|
+
readonly label?: string;
|
|
28
|
+
/** Which part of the host system this agent belongs to. */
|
|
29
|
+
readonly source?: string;
|
|
30
|
+
/** Extra attributes stamped on this agent's spans. */
|
|
31
|
+
readonly attributes?: Record<string, string | number | boolean>;
|
|
26
32
|
readonly client: LLMClient;
|
|
27
33
|
readonly hooks: HookBus;
|
|
28
34
|
private _system;
|
package/dist/bus/hook-map.d.ts
CHANGED
|
@@ -226,6 +226,12 @@ export interface AgentDestroyContext {
|
|
|
226
226
|
export interface RunStartContext {
|
|
227
227
|
runId: string;
|
|
228
228
|
agentId: string;
|
|
229
|
+
/** Human name for the agent, when one was configured. */
|
|
230
|
+
label?: string;
|
|
231
|
+
/** Which part of the host system the agent belongs to. */
|
|
232
|
+
source?: string;
|
|
233
|
+
/** Extra attributes the host stamped on this agent. */
|
|
234
|
+
attributes?: Record<string, string | number | boolean>;
|
|
229
235
|
userMessage: string | ContentPart[] | Message[];
|
|
230
236
|
model: string;
|
|
231
237
|
system?: string;
|
package/dist/helpers/engine.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { EngineConnect, EngineFetch, EngineFetchStream, FetchFn } from '../
|
|
|
24
24
|
import { Cache } from '../plugins/cache/cache';
|
|
25
25
|
import { CostCollector } from '../plugins/cost-collector/collector';
|
|
26
26
|
import { ModelCatalog } from '../plugins/model-catalog/catalog';
|
|
27
|
+
import { TelemetryAdapter, type TelemetryAdapterOptions } from '../plugins/telemetry/telemetry';
|
|
27
28
|
import type { Persistence } from '../plugins/persistence/types';
|
|
28
29
|
export interface EngineHandle {
|
|
29
30
|
/** Trace session id — minted once for this engine (the holder), shared by
|
|
@@ -54,6 +55,10 @@ export interface EngineHandle {
|
|
|
54
55
|
/** CostCollector — subscribes to onCompletion + onMediaGenerated and
|
|
55
56
|
* prices via catalog. Call `engine.cost.total()` for a running tally. */
|
|
56
57
|
cost: CostCollector;
|
|
58
|
+
/** TelemetryAdapter — present only when `telemetry` was configured, because an
|
|
59
|
+
* unwanted one would sit there accumulating spans for a process that never reads
|
|
60
|
+
* them. Subscribe with `engine.telemetry.onTrace(...)`, or use it directly. */
|
|
61
|
+
telemetry: TelemetryAdapter | null;
|
|
57
62
|
/** API keys per provider. Helpers (createLLM, createAgent,
|
|
58
63
|
* createMediaOutput, complete) read these to wire LLM clients without
|
|
59
64
|
* the caller passing apiKey explicitly. */
|
|
@@ -95,6 +100,20 @@ export interface EngineConfig {
|
|
|
95
100
|
/** Per-provider API keys. Helpers consult this when no apiKey is passed
|
|
96
101
|
* alongside `model: 'provider/...'`. */
|
|
97
102
|
apiKeys?: Partial<Record<ProviderName, string>>;
|
|
103
|
+
/** Observability. Omitted → no adapter is built and nothing is collected.
|
|
104
|
+
*
|
|
105
|
+
* ```ts
|
|
106
|
+
* createEngine({
|
|
107
|
+
* telemetry: {
|
|
108
|
+
* types: ['agent', 'tool'], // http/llm detail stays out
|
|
109
|
+
* content: 'none', // conversation text off by default
|
|
110
|
+
* sample: 0.05, // per trace, not per span
|
|
111
|
+
* onTrace: (e) => myPipeline.push(e),
|
|
112
|
+
* },
|
|
113
|
+
* });
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
telemetry?: TelemetryAdapterOptions;
|
|
98
117
|
/** Retry policy for every request this engine makes.
|
|
99
118
|
*
|
|
100
119
|
* Retry is a cross-cutting concern, so it is configured once here rather than threaded through
|