@combycode/llm-sdk 1.7.0 → 2.0.1
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 +462 -1
- package/MIGRATION.md +93 -0
- package/README.md +17 -2
- package/dist/agent/loop-config.d.ts +22 -0
- package/dist/agent/loop-step-state.d.ts +2 -0
- package/dist/agent/loop.d.ts +7 -0
- package/dist/agent/reflect-retry.d.ts +56 -0
- package/dist/agent/tool-key.d.ts +3 -0
- package/dist/agent/types.d.ts +8 -2
- package/dist/helpers/mcp.d.ts +24 -2
- package/dist/helpers/provenance-types.d.ts +63 -0
- package/dist/helpers/provenance.d.ts +12 -0
- package/dist/helpers/transcribe.d.ts +35 -6
- package/dist/index.browser.js +2997 -708
- package/dist/index.d.ts +19 -7
- package/dist/index.js +2997 -708
- package/dist/llm/providers/anthropic/constants.d.ts +2 -0
- package/dist/llm/providers/openai/completions.d.ts +9 -0
- package/dist/llm/providers/openai/provenance.d.ts +26 -0
- package/dist/llm/providers/openai/responses.d.ts +4 -2
- package/dist/llm/providers/openai/transcription.d.ts +39 -2
- package/dist/llm/types/audio.d.ts +31 -0
- package/dist/llm/types/messages.d.ts +89 -1
- package/dist/llm/types/options.d.ts +15 -0
- package/dist/llm/types/request.d.ts +13 -1
- package/dist/llm/types/response.d.ts +31 -1
- package/dist/llm/types/stream.d.ts +14 -1
- package/dist/llm/types/tiers.d.ts +6 -6
- package/dist/network/queue-state-config.d.ts +5 -0
- package/dist/network/queue-state.d.ts +7 -0
- package/dist/network/types.d.ts +23 -0
- package/dist/plugins/context-guard/strategies/anchored.d.ts +47 -0
- package/dist/plugins/context-measurer/counter/hybrid.d.ts +4 -1
- package/dist/plugins/context-measurer/counter/tiktoken.d.ts +8 -1
- package/dist/plugins/mcp/base-transport.d.ts +16 -0
- package/dist/plugins/mcp/client.d.ts +144 -7
- package/dist/plugins/mcp/input-required.d.ts +35 -0
- package/dist/plugins/mcp/jsonrpc.d.ts +7 -0
- package/dist/plugins/mcp/oauth.d.ts +21 -1
- package/dist/plugins/mcp/protocol-version.d.ts +61 -0
- package/dist/plugins/mcp/result-cache.d.ts +31 -0
- package/dist/plugins/mcp/subscriptions.d.ts +69 -0
- package/dist/plugins/mcp/transport-http.d.ts +31 -0
- package/dist/plugins/mcp/transport-stdio.d.ts +2 -0
- package/dist/plugins/mcp/transport-ws.d.ts +11 -1
- package/dist/plugins/mcp/transport.d.ts +11 -0
- package/dist/plugins/mcp/types.d.ts +54 -2
- package/dist/plugins/telemetry/telemetry.d.ts +12 -0
- package/dist/util/http.d.ts +8 -0
- package/package.json +9 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,468 @@ 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
|
-
## [
|
|
7
|
+
## [2.0.1] - 2026-08-10
|
|
8
|
+
|
|
9
|
+
Three defects reported by a consumer within a day of 2.0.0 — all reachable by reading the shipped
|
|
10
|
+
`.d.ts`, none caught by our gate. See the note at the end.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- **`agent.stream()` now carries `phase` on text events.** The raw stream event had it, and the
|
|
15
|
+
agent mapper *used* it internally to keep commentary out of the answer — then yielded both deltas
|
|
16
|
+
through one `{ type: 'text', text }` with the phase stripped. A UI streaming those straight
|
|
17
|
+
through put the model's thinking-aloud into the transcript **as if it were the reply**, with no
|
|
18
|
+
way to tell them apart. `finalAnswerText()` could not help: it takes a finished message's
|
|
19
|
+
`content`, not deltas.
|
|
20
|
+
- Additive: `phase` is **absent** (not `undefined`) when the provider reports none, so every
|
|
21
|
+
non-codex provider is byte-identical to before.
|
|
22
|
+
- **Docs: `agent.run()` does not exist.** The agent-loop guide recommended it for a non-throwing
|
|
23
|
+
report. The class exposes `stop` / `complete` / `structuredComplete` / `stream`; the report is
|
|
24
|
+
reached with `try/catch` + `agent.lastReport`. The guide now shows that.
|
|
25
|
+
- **Docs: the 2.0.0 changelog overstated live commentary.** It said commentary "is still yielded to
|
|
26
|
+
the consumer (a UI may well want to render it live)" — true only in the sense that the bytes
|
|
27
|
+
arrived; they were unlabelled, so a UI could not act on them. The 2.0.0 entry now says so and
|
|
28
|
+
points here.
|
|
29
|
+
|
|
30
|
+
### Why this got out
|
|
31
|
+
|
|
32
|
+
The feature was verified end-to-end on the **buffered** path (`finalAnswerText`, `response.text`,
|
|
33
|
+
live-tested against real models) and never once from the **layer most consumers actually call**.
|
|
34
|
+
1778 tests, four MCP transports and two live corpora, and no check that a shipped type was usable
|
|
35
|
+
from `agent.stream()`. The gate was deep where it was pointed and blind where it was not — so the
|
|
36
|
+
release checklist now includes a consumer-surface pass over the published `.d.ts`.
|
|
37
|
+
|
|
38
|
+
## [2.0.0] - 2026-08-09
|
|
39
|
+
|
|
40
|
+
**Upgrading:** three things can require action, and none of them is a provider change — that is the
|
|
41
|
+
point of the facade. (1) Node **22+** is now required. (2) `tiktoken` is an optional **peer**: run
|
|
42
|
+
`npm i tiktoken` only if you use exact local OpenAI token counting. (3) If you switch exhaustively
|
|
43
|
+
over `FinishReason` or `ContentPart` without a `default` branch, add one — both are open by design
|
|
44
|
+
(CONSTITUTION R1) so future provider values arrive additively instead of breaking your build. The
|
|
45
|
+
only changed signature is `OpenAITranscriptionAdapter.transcribe()`, which now returns an object;
|
|
46
|
+
read `.text`. The `transcribe()` helper is unaffected. Full detail: [MIGRATION.md](./MIGRATION.md).
|
|
47
|
+
|
|
48
|
+
### Changed — packaging (install/runtime level; no source change for consumers)
|
|
49
|
+
|
|
50
|
+
- **Node floor raised to `>=22`** (was `>=18`). Node 18 and 20 are both end-of-life; 22 is also the
|
|
51
|
+
floor `openai-node` 7 adopted.
|
|
52
|
+
- **`tiktoken` is now an OPTIONAL PEER dependency**, not an `optionalDependency`.
|
|
53
|
+
`optionalDependencies` means *"do not fail the install if this package fails to build"* — npm
|
|
54
|
+
installs it regardless, so every consumer received its ~5.6 MB wasm file. **If you use exact
|
|
55
|
+
OpenAI token counting, run `npm i tiktoken`**; the error thrown when it is missing names the
|
|
56
|
+
package and the alternatives (count-API and heuristic counters need no extra packages).
|
|
57
|
+
- **The wasm no longer lands in consumer bundles.** The dynamic import used a string literal, which
|
|
58
|
+
every bundler resolves during module-graph construction — so the blob was emitted even for code
|
|
59
|
+
paths that were never reached (one consumer reported it as **88% of their production output**).
|
|
60
|
+
`sideEffects: false` cannot prevent this: emitting a dynamic-import chunk is a graph-resolution
|
|
61
|
+
outcome, not dead-code elimination. The specifier now lives in a variable, opaque to bundlers and
|
|
62
|
+
resolved identically at runtime. Verified with a control — a literal import emits a 5.3 MB
|
|
63
|
+
`.wasm`; the shipped build emits none, even when the consumer imports `ContextMeasurer` directly
|
|
64
|
+
and has `tiktoken` installed.
|
|
65
|
+
- **`tiktoken` still works in the browser.** It ships a wasm/ESM build that bundlers resolve for
|
|
66
|
+
browser targets, so it is deliberately *not* stubbed out of `index.browser.js`.
|
|
67
|
+
- **`HybridTokenCounter` builds its tiktoken counter lazily**, on first route to that strategy,
|
|
68
|
+
rather than in the constructor.
|
|
69
|
+
- **The "zero dependencies" claim is now qualified** as *zero **required** runtime dependencies*.
|
|
70
|
+
`dependencies` genuinely is empty, but a consumer reading only that field concluded there were no
|
|
71
|
+
runtime packages at all.
|
|
72
|
+
|
|
73
|
+
### Added — MCP speaks both protocol eras
|
|
74
|
+
|
|
75
|
+
`mcp` 2.0.0 shipped the **2026-07-28 revision**, which is not additive: it deletes the `initialize`
|
|
76
|
+
handshake, the session id, and the whole server→client back-channel. Real servers are still almost
|
|
77
|
+
entirely on 2025-11-25, so this is **dual-era or it is a regression** — both wires are supported and
|
|
78
|
+
neither is preferred.
|
|
79
|
+
|
|
80
|
+
**Verified against a real server, on every transport.** The modern wire was developed against our
|
|
81
|
+
own test doubles, which is not evidence: a shape that satisfies a fake can still be rejected by a
|
|
82
|
+
real implementation. Before release the whole surface was run against the official
|
|
83
|
+
`mcp` 2.0.0 Python server — negotiation, tools, resources, prompts, MRTR, `subscriptions/listen`
|
|
84
|
+
with live change events, result caching and era gating — over **stdio, Streamable HTTP and
|
|
85
|
+
WebSocket**, plus a real 2025-11-25 server (DeepWiki) to prove the fallback. That exercise found
|
|
86
|
+
six defects that no unit test could have caught, four of which failed *silently*: the per-request
|
|
87
|
+
`_meta` identity envelope was missing (on ordinary requests, and separately on the long-lived
|
|
88
|
+
`subscriptions/listen` POST, where the rejection surfaced as the stream simply ending — so `listen()`
|
|
89
|
+
returned a subscription that looked alive and delivered nothing); `connectMcp` never forwarded
|
|
90
|
+
`cacheResults`, making the opt-in cache a no-op; the discover probe omitted the
|
|
91
|
+
`MCP-Protocol-Version` header that modern servers route on; the routing headers keyed off an era
|
|
92
|
+
that is not yet set during the probe; and the long-lived POST accepted only `text/event-stream`,
|
|
93
|
+
which a modern server answers with `406`. All are fixed and pinned by regression tests.
|
|
94
|
+
|
|
95
|
+
- **`server/discover` negotiation with handshake fallback.** `ConnectMcpOptions.protocolMode`:
|
|
96
|
+
`'auto'` (default) probes the modern wire and falls back to `initialize`; `'legacy'` skips the
|
|
97
|
+
probe entirely (byte-identical to 1.x); a version string adopts that revision directly.
|
|
98
|
+
- **The fallback is a denylist, not an allowlist.** *Every* JSON-RPC error falls back to the
|
|
99
|
+
handshake, except a `-32022` whose `supported` list is modern-only and shares nothing with us —
|
|
100
|
+
a genuine incompatibility that must surface rather than be papered over. **Transport and network
|
|
101
|
+
errors are never treated as an era verdict**: silently downgrading the wire because a socket
|
|
102
|
+
blipped would be the worst available failure mode.
|
|
103
|
+
- **`McpClient.info` is unchanged on both wires.** A modern server has no `initialize` result, so
|
|
104
|
+
one is synthesised from the discover result and its `_meta` server-info stamp. Callers never
|
|
105
|
+
branch on the era (CONSTITUTION.md R2 — absorb the difference, never expose a union). The stamp is
|
|
106
|
+
display-only per spec, so absent *or* malformed degrades to a placeholder instead of failing the
|
|
107
|
+
connection.
|
|
108
|
+
- **New (additive):** `McpClient.protocolVersion`, `.era`, `.discoverResult`, the
|
|
109
|
+
`McpDiscoverResult` type, and the version registry (`MCP_KNOWN_PROTOCOL_VERSIONS`,
|
|
110
|
+
`mcpEraOf`, …). Versions are treated as an **enumerated set, not an ordered scalar** — comparing
|
|
111
|
+
`'zzz' > '2025-11-25'` is true and meaningless, so era questions go through the registry.
|
|
112
|
+
- **Methods the revision removed are gated by era.** `logging/setLevel` and `resources/subscribe`
|
|
113
|
+
throw on a modern session with a message naming the negotiated version and the replacement,
|
|
114
|
+
instead of letting the server answer a bare `-32601`. The `ping` keep-alive is not started on a
|
|
115
|
+
modern session. All of them are untouched on a handshake session.
|
|
116
|
+
- **A 4xx carrying a JSON-RPC error body no longer loses it.** The HTTP transport collapsed every
|
|
117
|
+
4xx into `ConnectionClosed`, which discarded exactly the `-32022` that negotiation depends on — a
|
|
118
|
+
modern-only server looked like a dead connection.
|
|
119
|
+
- New error codes: `HeaderMismatch` (-32020), `MissingRequiredClientCapability` (-32021),
|
|
120
|
+
`UnsupportedProtocolVersion` (-32022).
|
|
121
|
+
|
|
122
|
+
**Multi-round-trip requests (MRTR, SEP-2322)** — the modern replacement for the back-channel. Where
|
|
123
|
+
a handshake-era server *pushes* a `sampling/createMessage` at us mid-call, a 2026-07-28 server
|
|
124
|
+
*returns* `resultType: 'input_required'` with the questions it needs answered, and the client
|
|
125
|
+
re-issues the same call carrying the answers plus the server's opaque `requestState`.
|
|
126
|
+
|
|
127
|
+
- **One handler serves both wires.** MRTR is dispatched through the same `onServerRequest` path as
|
|
128
|
+
a pushed request, so a caller who wired up sampling/elicitation/roots once gets it on either wire
|
|
129
|
+
without knowing which is in play.
|
|
130
|
+
- **`McpCallResult` did not become a union.** Upstream models this as a separate
|
|
131
|
+
`InputRequiredResult`, which would break every consumer reading `.content`. We attach
|
|
132
|
+
`resultType` / `inputRequests` / `requestState` as optional fields instead (CONSTITUTION.md R2).
|
|
133
|
+
An **absent `resultType` reads as `'complete'`**, so every pre-2026 result behaves exactly as
|
|
134
|
+
before and costs no extra round-trip.
|
|
135
|
+
- Applied to `tools/call`, `prompts/get` and `resources/read`. `requestState` is echoed back
|
|
136
|
+
byte-exact and never inspected.
|
|
137
|
+
- A leg carrying state but no questions backs off (50 ms doubling to a 250 ms cap, reset by any leg
|
|
138
|
+
with real questions) rather than spinning against the server.
|
|
139
|
+
- `inputRequiredMaxRounds` (default **10**, matching the other SDKs) bounds the loop, because a
|
|
140
|
+
handler that never satisfies the server would otherwise retry forever.
|
|
141
|
+
|
|
142
|
+
**`subscriptions/listen` (SEP-2575)** — the single change-notification stream that replaces
|
|
143
|
+
`resources/subscribe` and the standalone notification channel at 2026-07-28.
|
|
144
|
+
|
|
145
|
+
- `McpClient.listen(filter, onEvent)` returns an `McpSubscription`. Every kind is **opt-in**
|
|
146
|
+
(`toolsListChanged`, `promptsListChanged`, `resourcesListChanged`, `resourceSubscriptions[]`) and
|
|
147
|
+
the server acknowledges with the subset it actually honoured — which **can be narrower than what
|
|
148
|
+
was requested**, so `subscription.honored` / `isHonored(kind)` is worth checking rather than
|
|
149
|
+
assuming. Frames are attributed by the `io.modelcontextprotocol/subscriptionId` stamp, so frames
|
|
150
|
+
for another subscription are ignored.
|
|
151
|
+
- Refused with a clear error on a handshake session, which keeps `subscribeResource()` the right
|
|
152
|
+
answer there instead of silently returning a subscription that never fires.
|
|
153
|
+
- **Works on every transport** — stdio, WebSocket and Streamable HTTP. On HTTP a listen is a POST
|
|
154
|
+
whose *response body* is the long-lived stream, so it goes through the streaming fetch rather than
|
|
155
|
+
the buffered POST path (which would surface frames only once the stream closed — i.e. never, for
|
|
156
|
+
a healthy subscription). Frames route identically on all three, so the client sees no difference.
|
|
157
|
+
- **The end of a stream is observable.** `subscription.active` / `.ended` report whether the stream
|
|
158
|
+
is still live and, if not, the error that killed it — a rejected subscription, a dropped
|
|
159
|
+
connection, or a clean server-side teardown. A subscription that silently stopped delivering is
|
|
160
|
+
otherwise indistinguishable from one where nothing has changed yet; failures also surface on the
|
|
161
|
+
`onMcpError` hook. `close()` on the client tears down every open stream.
|
|
162
|
+
|
|
163
|
+
**Hardening**
|
|
164
|
+
|
|
165
|
+
- **The stdio read buffer is bounded** (`maxBufferSize`, default **10 MB**, matching mcp-ts 1.30).
|
|
166
|
+
JSON-RPC over stdio is newline-delimited, so a server that never emits `\n` — a crash dump, a
|
|
167
|
+
binary blob on the wrong stream, a runaway log line — grew the buffer until the process died.
|
|
168
|
+
The limit applies to a single *unterminated* line, so a large burst of complete messages is
|
|
169
|
+
unaffected; on overflow the pending requests fail with a message naming the likely cause.
|
|
170
|
+
- **`Content-Type` is compared by media-type essence, not substring.** `contentType.includes(...)`
|
|
171
|
+
routed anything merely *containing* `text/event-stream` — e.g. `application/json;
|
|
172
|
+
profile="text/event-stream"` — into the SSE parser.
|
|
173
|
+
- **RFC 9207 `iss` validation** on the OAuth authorization response, checked **before** the code is
|
|
174
|
+
redeemed. This is the mix-up-attack defence: without it a malicious authorization server can hand
|
|
175
|
+
back a code minted by a different server and have the client replay the user's credentials
|
|
176
|
+
against it. Comparison is exact string equality per §2.4 — deliberately *not* URL-normalised,
|
|
177
|
+
since that leniency is what an attacker looks for. A **missing** `iss` is rejected when the server
|
|
178
|
+
advertises `authorization_response_iss_parameter_supported`, otherwise stripping the parameter
|
|
179
|
+
would skip the check. Pass it via `finishMcpAuth(..., { iss })`; optional, so existing callers
|
|
180
|
+
keep working.
|
|
181
|
+
- **`application_type: 'native'` is sent at dynamic client registration** (SEP-837). MCP clients are
|
|
182
|
+
normally local processes with a loopback redirect, and some authorization servers apply stricter
|
|
183
|
+
redirect-URI rules when the type is left to be guessed as `web`. An explicit value from the caller
|
|
184
|
+
still wins.
|
|
185
|
+
- **The WebSocket transport is kept** and now documents itself as non-standard. Upstream removed
|
|
186
|
+
theirs as "never part of the MCP specification"; ours is public API we shipped, and an upstream
|
|
187
|
+
deletion is not our deletion (R7). It is also duplex, so it supports `subscriptions/listen` today.
|
|
188
|
+
|
|
189
|
+
**Result cache hints (`ttlMs` / `cacheScope`)** — opt-in via `cacheResults`, off by default.
|
|
190
|
+
Settable on `connectMcp(config, { cacheResults: true })` as well as on `McpClient` directly, along
|
|
191
|
+
with `inputRequiredMaxRounds`.
|
|
192
|
+
|
|
193
|
+
- Honours the server's freshness hint on `tools/list`, `prompts/list`, `resources/list`,
|
|
194
|
+
`resources/templates/list` and `resources/read`. **A server that sends no hints caches nothing**,
|
|
195
|
+
so this is a no-op against every pre-2026 server.
|
|
196
|
+
- **`ttlMs: 0` means "immediately stale"** — a real instruction, not a missing value to be replaced
|
|
197
|
+
with a default.
|
|
198
|
+
- A paginated list is only as fresh as its shortest-lived page, so the effective TTL is the
|
|
199
|
+
**minimum** across pages.
|
|
200
|
+
- Entries are dropped on the matching `*_changed` notification **before** the caller's handler
|
|
201
|
+
runs, so a handler that re-lists synchronously never reads a stale entry. A
|
|
202
|
+
`notifications/resources/updated` drops only the named resource.
|
|
203
|
+
- `cacheScope` is recorded but never used to widen sharing: this cache lives inside one client with
|
|
204
|
+
one credential, where `public` buys nothing.
|
|
205
|
+
|
|
206
|
+
### Changed — `FinishReason` is now an OPEN union
|
|
207
|
+
|
|
208
|
+
- **`FinishReason` = `KnownFinishReason | (string & {})`.** Providers keep inventing terminal
|
|
209
|
+
states — four did so in a single upstream cycle — and against a closed union every one of those is
|
|
210
|
+
a breaking change for **every** consumer, including consumers of providers that changed nothing.
|
|
211
|
+
Opening it is what CONSTITUTION.md R1 exists for, and it means this is the **last** time this type
|
|
212
|
+
breaks anyone. Write a `default` branch; use `KnownFinishReason` for the documented set alone.
|
|
213
|
+
- **New known value `'malformed_tool_call'`** — the model tried to call a tool and produced
|
|
214
|
+
something unusable. Distinct from `error` (the request failed) and `tool_use` (a call we can
|
|
215
|
+
run), because this one is *recoverable*.
|
|
216
|
+
- **Google's `MALFORMED_FUNCTION_CALL` is now mapped.** It previously wasn't mapped at all, so it
|
|
217
|
+
fell through to `'stop'`: a turn where the model failed to produce a usable tool call looked like
|
|
218
|
+
a clean finish with no content.
|
|
219
|
+
|
|
220
|
+
### Added — agent + network
|
|
221
|
+
|
|
222
|
+
- **`reflectAndRetry` on `AgentLoop`** (google-adk 2.6 `ReflectAndRetryModelPlugin`).
|
|
223
|
+
Self-healing recovery from a model failure the model itself can fix: it receives structured
|
|
224
|
+
guidance naming the attempt and forbidding an identical retry, then the step runs again within a
|
|
225
|
+
bounded budget. **Off unless configured** — a retry costs a real request.
|
|
226
|
+
- **Not a network retry.** The engine already retries transport failures; this is for a request
|
|
227
|
+
that *succeeded* and came back unusable, which resending unchanged would never fix.
|
|
228
|
+
- The failed turn is **not appended to history**, so the model never learns from its own broken
|
|
229
|
+
output; usage from it *is* counted, because a wasted turn still costs money.
|
|
230
|
+
- Counts **consecutive** failures, so an agent that recovers and fails again later gets a fresh
|
|
231
|
+
budget rather than inheriting a spent one.
|
|
232
|
+
- `throwIfExceeded` (default `true`) decides raise-vs-return when the budget is spent; the error
|
|
233
|
+
names the option so the alternative is discoverable.
|
|
234
|
+
|
|
235
|
+
- **`checkProvenance()`** — detect provider provenance signals in a file (C2PA manifest, SynthID
|
|
236
|
+
watermark) via OpenAI's new `POST /v1/content_provenance_checks`. Bytes in, structured verdict
|
|
237
|
+
out, same shape as `moderate()`, with an honest-zero cost entry so the ledger records the call.
|
|
238
|
+
It is the only "was this AI-generated" primitive any tracked SDK ships.
|
|
239
|
+
- The result separates **`detected`** from **`trusted`**, and the docs say why: signals are
|
|
240
|
+
strippable — a re-encode, crop or screenshot usually removes them — so `detected: false` is
|
|
241
|
+
absence of evidence, not evidence a human made the file. Only a detected manifest that
|
|
242
|
+
*validated* is a positive statement.
|
|
243
|
+
- `detected` is true if ANY signal fired: audio carries SynthID only, so requiring both schemes
|
|
244
|
+
would report every audio file as clean.
|
|
245
|
+
- **`AnchoredStrategy`** for ContextGuard — one growing scratchpad instead of a chain of summaries
|
|
246
|
+
(ported from google-adk 1.5 `AnchoredContextCompactor`). `LayeredStrategy` emits a new summary
|
|
247
|
+
per compaction, so old facts get summarised repeatedly and drift; anchored merges each compaction
|
|
248
|
+
into a single head entry, so every fact is summarised from raw text exactly once. The trade is
|
|
249
|
+
stated in the file: one anchor means one blast radius.
|
|
250
|
+
- **Never splits a tool call from its result.** The retain boundary walks backwards past a
|
|
251
|
+
`tool_result` whose `tool_call` would be cut away — several providers reject an orphaned
|
|
252
|
+
result outright and the rest silently misread it.
|
|
253
|
+
- **A summariser returning nothing declines rather than dropping entries**: trading a context
|
|
254
|
+
overflow for silent data loss is strictly worse.
|
|
255
|
+
|
|
256
|
+
- **`toolNameCollisionPolicy`** on `AgentLoop` (`'warn'` default, `'error'`). Tools are registered
|
|
257
|
+
in a map keyed by function name / builtin type, so two tools sharing a key meant one **silently
|
|
258
|
+
replaced** the other and the model never saw it — surfacing much later as "the model called the
|
|
259
|
+
wrong tool", with nothing in the logs pointing at the cause. `'warn'` keeps last-write-wins (an
|
|
260
|
+
app relying on a deliberate override still works — R4) but emits an `onWarning`
|
|
261
|
+
(`code: 'tool_name_collision'`) naming the key and which tool lost; `'error'` throws at
|
|
262
|
+
construction or `addTool()`, before the model is called. Re-registering the *same* tool object is
|
|
263
|
+
not a collision.
|
|
264
|
+
- **Per-request retry overrides** — `HttpRequest.retry` (`maxRetries`, `totalTimeoutMs`,
|
|
265
|
+
`attemptTimeoutMs`, `maxRetryAfterMs`, `backoff`). A queue's retry policy is shared by every call
|
|
266
|
+
on it, so a one-off that needs to be more or less patient — a long batch submit, a health check
|
|
267
|
+
that should fail fast — previously had to accept the shared policy or get its own queue. Mirrors
|
|
268
|
+
Google moving `retryOptions` from client-level to per-request `HttpOptions` (google-ts 2.15).
|
|
269
|
+
`perKind` deliberately stays queue-level: one request cannot redefine which error classes are
|
|
270
|
+
retryable for everyone sharing the queue. Precedence is per-request → per-kind → queue default.
|
|
271
|
+
|
|
272
|
+
### Added — OpenAI Responses parity
|
|
273
|
+
|
|
274
|
+
- **Assistant `phase` (`commentary` | `final_answer`)** on `TextPart` and on streamed text events.
|
|
275
|
+
Codex-family models narrate before answering; `response.text` concatenates both, so **an agent's
|
|
276
|
+
final output used to include its own thinking-out-loud**. `AgentLoop` now derives its answer with
|
|
277
|
+
the new `finalAnswerText()` helper, which drops commentary. `response.text` and `contentText()`
|
|
278
|
+
are unchanged — callers who want everything still get everything.
|
|
279
|
+
- Open union (R1), and `finalAnswerText` excludes only what is explicitly `'commentary'` rather
|
|
280
|
+
than keeping only `'final_answer'`: the day a provider adds a third phase, an allow-list would
|
|
281
|
+
silently drop the answer.
|
|
282
|
+
- Streaming carries it too. `phase` is announced once on `response.output_item.added` and belongs
|
|
283
|
+
on every delta of that item, so the parser keeps per-stream item→phase state; concurrent streams
|
|
284
|
+
cannot leak phases into each other. Commentary is yielded to the consumer and preserved in the
|
|
285
|
+
assembled content as its own phase-tagged part. (In 2.0.0 the agent-layer event dropped the
|
|
286
|
+
phase, so a UI could not act on it — corrected in 2.0.1.)
|
|
287
|
+
- Nothing is inferred: a model that reports no phase produces parts with no phase, exactly as
|
|
288
|
+
before.
|
|
289
|
+
- **`name` + `namespace` on `function_call_output`.** The tool name is taken from the matching
|
|
290
|
+
call — tracked across messages while building the input, never invented, so a result with no
|
|
291
|
+
matching call simply omits it. `ToolResultPart.namespace` round-trips the namespace of a
|
|
292
|
+
namespaced tool. Probe-verified 2026-08-06: accepted, and a non-string `namespace` is rejected,
|
|
293
|
+
so the fields are validated rather than tolerated.
|
|
294
|
+
|
|
295
|
+
### Added — programmatic tool calling (the model writes code that calls your tools)
|
|
296
|
+
|
|
297
|
+
A model can now write a short program that orchestrates your tools itself, instead of
|
|
298
|
+
emitting one call at a time and waiting for each result. Previously the `program` items
|
|
299
|
+
in the response were dropped on the floor.
|
|
300
|
+
|
|
301
|
+
- **`ToolCaller` on `ToolCallPart` and `ToolResultPart`** — `{ type: 'direct' | 'program',
|
|
302
|
+
callerId? }`. Absent means what it always meant (the model called the tool itself), so
|
|
303
|
+
nothing changes for existing code. Open union with an optional payload rather than
|
|
304
|
+
`{type:'direct'} | {type:'program', callerId}` (R1 + R2), so a future caller kind is
|
|
305
|
+
additive; an unknown type is preserved rather than flattened to `direct`.
|
|
306
|
+
- **`ProgramCallPart` (`program_call`) and `ProgramResultPart` (`program_result`)** —
|
|
307
|
+
the code the model wrote, and what it returned. The code is plain readable JavaScript
|
|
308
|
+
and worth surfacing: it is the plan the model is executing.
|
|
309
|
+
- **`allowedCallers` is now enforced locally**, not only by the provider. A tool without
|
|
310
|
+
it is `direct`-only, so model-written code cannot reach a tool that never opted in. A
|
|
311
|
+
violation denies that one call with an error result to the model — the way a guardrail
|
|
312
|
+
trip does — rather than ending the run.
|
|
313
|
+
- **Round-tripping is the whole feature**, and three wire rules make it work (all found
|
|
314
|
+
live, none of them in any SDK's types):
|
|
315
|
+
- The `program` item is **rejected without the `reasoning` item that produced it**, so
|
|
316
|
+
that item is captured and re-emitted with it.
|
|
317
|
+
- **Dropping the program item is worse than an error**: the model silently re-emits the
|
|
318
|
+
program and runs it again from the start.
|
|
319
|
+
- `program_output` **requires its `id`** when replayed as history — unlike
|
|
320
|
+
`function_call_output`, which needs none. Without it a follow-up question 400s on a
|
|
321
|
+
conversation that had just succeeded.
|
|
322
|
+
- **Availability, checked model by model:** of the 53 gpt-5 / o3 / o4 / codex models
|
|
323
|
+
visible on the test account, only the **`gpt-5.6` family** (`luna`, `sol`, `terra`)
|
|
324
|
+
accepts the `programmatic_tool_calling` tool. Every other one returns
|
|
325
|
+
*"Tool 'programmatic_tool_calling' is not supported with <model>"*.
|
|
326
|
+
|
|
327
|
+
### Added — structured transcription
|
|
328
|
+
|
|
329
|
+
`transcribe()` returned `{ text }` and nothing else, so segments, speakers and word timings that
|
|
330
|
+
the provider had already computed were parsed and thrown away.
|
|
331
|
+
|
|
332
|
+
- **New request options:** `keywords` (spelling control for names and jargon), `languages`
|
|
333
|
+
(candidate languages when the language is unknown), `wordTimestamps`, and `diarization`.
|
|
334
|
+
- **New response fields, all optional:** `segments` (with `speaker` when diarizing), `words`,
|
|
335
|
+
detected `languages`, and `durationSeconds`. **`text` stays required** (R3), so existing code is
|
|
336
|
+
untouched — the additions appear only when the chosen model produces them.
|
|
337
|
+
- **Behaviourally verified, not just accepted** (E2). `keywords` changes the transcript: an invented
|
|
338
|
+
name that comes back as *"Zalbrequist"* without it comes back as *"Zylberquist"* with it, on
|
|
339
|
+
identical audio. `languages` changes what the model reports detecting, and an invalid code is
|
|
340
|
+
rejected. Both are `gpt-transcribe`-only; word timings are `whisper-1`-only; speaker labels are
|
|
341
|
+
`gpt-4o-transcribe-diarize`-only. **No model returns speakers and word timings together**, so
|
|
342
|
+
combining `wordTimestamps` with `diarization` throws before any request is sent.
|
|
343
|
+
- **Model-gated options are still sent.** A field the chosen model rejects produces a 400 naming
|
|
344
|
+
the parameter, rather than being dropped on our side — the caller learns their keywords did
|
|
345
|
+
nothing (R4: gating is internal, but silence is not a gate). On generateContent providers, which
|
|
346
|
+
have no structured endpoint at all, the same request emits an `onWarning`
|
|
347
|
+
(`transcription_option_unsupported`).
|
|
348
|
+
- **Transcription cost is now measured, not estimated.** These models return the audio duration
|
|
349
|
+
they billed for (`usage.seconds`, or a top-level `duration`), which is used when the caller
|
|
350
|
+
supplies none. Previously a non-WAV file with no `audioDurationSeconds` could only produce an
|
|
351
|
+
honest zero.
|
|
352
|
+
- **Google's equivalent is deliberately absent.** `audioTranscriptionConfig` is accepted *and
|
|
353
|
+
type-validated* by the Gemini Developer API and then completely ignored: a two-speaker round-trip
|
|
354
|
+
returned a response structurally identical to the control — no `speakerLabel`, no `words[]`
|
|
355
|
+
anywhere (2026-08-09). It is the second confirmed accepted-but-inert field after `top_k`. Shipping
|
|
356
|
+
it on the strength of the green probe would have meant a diarization feature that silently
|
|
357
|
+
returns nothing.
|
|
358
|
+
|
|
359
|
+
**Breaking (2.0):** `OpenAITranscriptionAdapter.transcribe()` returns
|
|
360
|
+
`OpenAITranscriptionResult` instead of `string`; read `.text`. The `transcribe()` helper is
|
|
361
|
+
unaffected — it already returned an object.
|
|
362
|
+
|
|
363
|
+
### Fixed — correctness
|
|
364
|
+
|
|
365
|
+
- **Parallel tool calls were broken on every chat-completions backend.** The loop answers a round
|
|
366
|
+
of parallel calls with one tool message holding a `tool_result` part per call, and this API wants
|
|
367
|
+
a separate `{role:'tool'}` message per `tool_call_id` — but only the **first** was emitted. Every
|
|
368
|
+
call after the first went unanswered and the provider rejected the whole request with
|
|
369
|
+
*"No tool output found for function call <id>"*. Affected OpenRouter and any use of
|
|
370
|
+
`api: 'completions'` on OpenAI/xAI; the Responses path was always correct. Present in 1.7.0 and
|
|
371
|
+
earlier; found by running the examples corpus, not by a unit test.
|
|
372
|
+
- **`serviceTier: 'fast'` is actually sent to OpenAI.** The value shipped in openai-ts 7.x but was
|
|
373
|
+
missing from our known-tier set, so `openaiRequestTier('fast')` fell through to `'auto'` — a
|
|
374
|
+
caller asking for Fast mode silently got the project default, with no error and no warning.
|
|
375
|
+
Probe-verified on `gpt-5.5`: `fast` accepted, `hyperfast` rejected, so the value is validated
|
|
376
|
+
rather than merely tolerated. Applies to Responses and chat-completions.
|
|
377
|
+
- **A `Retry-After` longer than we will honour now fails fast instead of parking the request.** New
|
|
378
|
+
config `RetryConfig.maxRetryAfterMs` (default **120s**). Previously an un-capped value was obeyed
|
|
379
|
+
verbatim: `Retry-After: 86400` held the request for a day, which from the caller's side is
|
|
380
|
+
indistinguishable from a hang. Worse, on the rate-limit path it also paused the **entire**
|
|
381
|
+
limiter — every request on that queue, not just the one that was throttled. Both paths are now
|
|
382
|
+
clamped, and an over-cap value is treated as a refusal rather than a delay.
|
|
383
|
+
- **`Retry-After` parsing hardened.** The HTTP-date form (RFC 9110) is now parsed instead of being
|
|
384
|
+
silently ignored, and malformed values (`NaN`, negative, non-finite, a past date) are discarded
|
|
385
|
+
rather than propagated — `setTimeout(fn, NaN)` fires immediately, which turned one bad header
|
|
386
|
+
into an instant retry storm.
|
|
387
|
+
|
|
388
|
+
### Unchanged, deliberately
|
|
389
|
+
|
|
390
|
+
- **Google Interactions keeps sending `temperature` and `top_p`.** google 2.15 deleted both from its
|
|
391
|
+
Interactions `GenerationConfig` type, which resembles the pattern behind two earlier live
|
|
392
|
+
breakages — but the wire disagrees: probed 2026-08-06 on `gemini-3.6-flash`, both are accepted
|
|
393
|
+
(200) and *validated* (`"warm"` / `-7` → 400). The removal is SDK-typing-only; stripping them
|
|
394
|
+
would have been the regression. Recorded at the call site so a later cycle does not "fix" it.
|
|
395
|
+
|
|
396
|
+
---
|
|
397
|
+
|
|
398
|
+
Upstream reconciliation for the 2026-07-27 clone refresh (10 SDKs). This batch is dominated by
|
|
399
|
+
**terminal-state correctness**: three providers widened response enums that our adapters silently
|
|
400
|
+
flattened to `'stop'`, so a caller could not distinguish a refusal, a context overflow, a queued
|
|
401
|
+
interaction or an outright failure from a clean finish.
|
|
402
|
+
|
|
403
|
+
### Fixed
|
|
404
|
+
- **Google Interactions no longer sends `cached_content`.** google 2.13 removed it from the
|
|
405
|
+
Interactions request model and the endpoint now hard-rejects it — live-probed:
|
|
406
|
+
`400 Unknown parameter 'cached_content'`. Any call passing `providerOptions.cachedContent` on
|
|
407
|
+
Interactions failed outright. The passthrough **moved to `generateContent`**, which still accepts
|
|
408
|
+
and validates it (top-level `cachedContent`), so the capability is preserved rather than dropped.
|
|
409
|
+
- **Anthropic `refusal` → `finishReason: 'content_filter'`** (was `'stop'`). A safety decline is a
|
|
410
|
+
block, not a clean finish; it now lines up with every other provider's block signal. The refusal
|
|
411
|
+
category enum also gained `general_harms` (anthropic 0.115).
|
|
412
|
+
- **Anthropic `model_context_window_exceeded` → `finishReason: 'length'`** (was `'stop'`), on both
|
|
413
|
+
the buffered and streamed reason maps.
|
|
414
|
+
- **OpenAI Responses `status: 'failed'` → `finishReason: 'error'`** (was `'stop'`), and `cancelled`
|
|
415
|
+
→ `'error'`. A Responses call can fail *inside a 200*, so there was no exception to catch and the
|
|
416
|
+
caller silently received an empty success.
|
|
417
|
+
- **Google Interactions `queued` no longer ends a stream.** The status is non-terminal, but the
|
|
418
|
+
stream parser emitted a terminal `done` for it, truncating the run.
|
|
419
|
+
|
|
420
|
+
### Security
|
|
421
|
+
- **`TelemetryAdapter` can redact provider error text.** New option
|
|
422
|
+
`includeSensitiveData` (default `true` — unchanged behaviour, and the same default as the OpenAI
|
|
423
|
+
Agents SDK's `trace_include_sensitive_data`). A provider's `error.message`/`error.raw` can echo
|
|
424
|
+
request content back (a refusal quotes the prompt, a validation error names the field and value),
|
|
425
|
+
and we stored it verbatim. With `includeSensitiveData: false` the message becomes `***REDACTED***`
|
|
426
|
+
and `raw` is dropped, while `name`/`code`/`status` are kept so traces stay triageable. URLs and
|
|
427
|
+
headers were, and remain, always redacted.
|
|
428
|
+
|
|
429
|
+
### Fixed (hardening)
|
|
430
|
+
- **SSE streams are now cancelled, not just unlocked.** `parseSSEStream` released the reader lock in
|
|
431
|
+
its `finally` but never cancelled the body, so a consumer that broke out early (abort, error, or a
|
|
432
|
+
`break` after the first token) left the HTTP response open until GC. Verified live on Anthropic,
|
|
433
|
+
OpenAI and Google: full streams unchanged, early `break` tears down cleanly.
|
|
434
|
+
- **Non-replayable request bodies are never retried.** A streamed body is consumed by the first
|
|
435
|
+
attempt, so a retry would send an empty/partial body. Our own `rawBody` callers all pass FormData
|
|
436
|
+
or bytes (replayable), so this is a guard against a caller-supplied stream rather than a live bug.
|
|
437
|
+
- **Case-insensitive response-header lookup.** `google/files.ts` guessed three casings of
|
|
438
|
+
`x-goog-upload-url` and would have missed any fourth. Header reads now go through one shared
|
|
439
|
+
`header()` helper in `util/http` (de-duplicated with the private copy in `llm/files/retrieve.ts`).
|
|
440
|
+
|
|
441
|
+
### Added
|
|
442
|
+
- **`topK` and `seed` sampling options.** Both were reachable on several providers and exposed by
|
|
443
|
+
none of our surface — parity gaps found by the new feature-matrix audit and closed the same day.
|
|
444
|
+
Each is emitted **only where the wire accepts it**, verified by live probe rather than inferred:
|
|
445
|
+
`topK` → Anthropic, Google (generateContent *and* Interactions), xAI chat, OpenRouter chat;
|
|
446
|
+
dropped for OpenAI, which defines no top-k. `seed` → OpenAI **chat-completions**, Google (both
|
|
447
|
+
surfaces), xAI (chat *and* responses), OpenRouter chat; dropped for Anthropic and OpenAI
|
|
448
|
+
**Responses**, which both reject it (400). Sending either to a surface that refuses it is a hard
|
|
449
|
+
error, so the gating is locked by unit tests and was live-verified end to end.
|
|
450
|
+
- **`docs/feature-matrix.json` — the parity matrix.** Every capability an official SDK exposes, how
|
|
451
|
+
each provider spells it, and where we stand, with citations into the version-pinned clones.
|
|
452
|
+
`scripts/validate-feature-matrix.mjs` runs as part of `bun run lint` and fails the build on a
|
|
453
|
+
broken citation, an unexplained `partial`/`beta`/`by-design`, or a duplicate id. It backs the
|
|
454
|
+
site's comparison page and is maintained by the upstream-update cycle.
|
|
455
|
+
- **`FinishReason` gains `'pending'`** — non-terminal: the provider accepted the request but has not
|
|
456
|
+
produced a completion (Google Interactions `queued`, OpenAI Responses `queued`/`in_progress` in
|
|
457
|
+
background mode). Treat as "poll/retry", never as a result. *Additive union member: exhaustive
|
|
458
|
+
`switch` statements over `FinishReason` should add a case.*
|
|
459
|
+
- **`CompletionResponse.error?: { code?, message? }`** — populated when `finishReason === 'error'`,
|
|
460
|
+
carrying the provider's own failure detail (e.g. OpenAI's new `data_residency_mismatch` code,
|
|
461
|
+
openai 6.49). Optional field; absent unless the provider reported a failure.
|
|
462
|
+
- Documented the OpenAI `reasoning.context` default (the `gpt-5.6` family defaults to `all_turns`,
|
|
463
|
+
earlier models to `current_turn`).
|
|
464
|
+
- **`itemId` on `text` / `thinking` stream events.** OpenAI Responses reports which output item a
|
|
465
|
+
delta belongs to (`item_id`); a turn can interleave deltas from several items, so consumers that
|
|
466
|
+
reassemble per item — rather than concatenating into one string — now can. Optional and additive:
|
|
467
|
+
ignoring it gives exactly the previous behaviour, and providers that report no item id (chat
|
|
468
|
+
completions) simply omit it. Live-confirmed present on every delta from the Responses API.
|
|
8
469
|
|
|
9
470
|
## [1.7.0] - 2026-07-16
|
|
10
471
|
|
package/MIGRATION.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Migrating to 2.0.0
|
|
2
|
+
|
|
3
|
+
**Most codebases need no source changes.** The point of this library is that provider churn is our
|
|
4
|
+
problem to absorb, not yours — and a whole cycle of it (a new MCP protocol revision, a new OpenAI
|
|
5
|
+
major, four SDK majors) landed here without becoming a breaking change for you.
|
|
6
|
+
|
|
7
|
+
Three things can require action, and none of them is a provider change.
|
|
8
|
+
|
|
9
|
+
## 1. Node 22+ is required
|
|
10
|
+
|
|
11
|
+
```json
|
|
12
|
+
"engines": { "node": ">=22", "bun": ">=1.1.0" }
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Node 18 and 20 are both end-of-life. 22 is also the floor `openai-node` 7 adopted.
|
|
16
|
+
|
|
17
|
+
**What to do:** upgrade the runtime. Nothing in your code changes.
|
|
18
|
+
|
|
19
|
+
## 2. `tiktoken` is now an optional PEER dependency
|
|
20
|
+
|
|
21
|
+
It used to be an `optionalDependency`, which means *"do not fail the install if this package fails
|
|
22
|
+
to build"* — npm installed it **anyway**. Every consumer received its ~5.6 MB wasm file, and
|
|
23
|
+
bundlers emitted it into production builds even when local token counting was never used. One
|
|
24
|
+
consumer measured it at **88% of their shipped output**.
|
|
25
|
+
|
|
26
|
+
**What to do:** if you use exact local OpenAI token counting, install it yourself:
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
npm install tiktoken
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
If you don't, do nothing — you now stop paying for a feature you never asked for. Token counting
|
|
33
|
+
still works without it: `countTokens` falls back to the provider count-API (Anthropic/Google) or a
|
|
34
|
+
calibrated heuristic. The error thrown when the package is genuinely needed names it and the
|
|
35
|
+
alternatives.
|
|
36
|
+
|
|
37
|
+
It still works in the **browser** when you do install it; it is deliberately not stubbed out.
|
|
38
|
+
|
|
39
|
+
## 3. Two unions are now open — add a `default` branch
|
|
40
|
+
|
|
41
|
+
`FinishReason` and `ContentPart` gained members and are now open unions
|
|
42
|
+
(`KnownFinishReason | (string & {})`). If you `switch` over either **exhaustively, with no
|
|
43
|
+
`default`**, TypeScript will now complain.
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
switch (res.finishReason) {
|
|
47
|
+
case 'stop': …
|
|
48
|
+
case 'tool_use': …
|
|
49
|
+
default: … // <- add this
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**This is deliberate, and it is the reason most of this release is not breaking.** Providers grew
|
|
54
|
+
four new terminal statuses in a single cycle. With a closed union, every one of those is a breaking
|
|
55
|
+
change for *every* consumer — including consumers of providers that changed nothing. Open unions
|
|
56
|
+
convert that into an additive change, at the cost of one `default` branch written once
|
|
57
|
+
(CONSTITUTION.md R1).
|
|
58
|
+
|
|
59
|
+
New members you can now handle if you want them: `'pending'` (queued / in-progress — previously
|
|
60
|
+
flattened to `'stop'`, claiming a clean finish for a response that had not run), and
|
|
61
|
+
`'malformed_tool_call'`. `ContentPart` gained `program_call` / `program_result`.
|
|
62
|
+
|
|
63
|
+
## 4. One changed signature
|
|
64
|
+
|
|
65
|
+
`OpenAITranscriptionAdapter.transcribe()` returns `OpenAITranscriptionResult` instead of `string`:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
// before
|
|
69
|
+
const text = await adapter.transcribe(req, fetch);
|
|
70
|
+
|
|
71
|
+
// after
|
|
72
|
+
const { text } = await adapter.transcribe(req, fetch);
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The result also carries optional `segments`, `words`, `languages` and `durationSeconds`.
|
|
76
|
+
|
|
77
|
+
**The `transcribe()` helper is unaffected** — it already returned an object, and `text` is still
|
|
78
|
+
required on it. Only the low-level adapter class changed.
|
|
79
|
+
|
|
80
|
+
## What did NOT break
|
|
81
|
+
|
|
82
|
+
Worth stating, because it is the whole design goal:
|
|
83
|
+
|
|
84
|
+
- **MCP 2025-11-25 keeps working, untouched.** The 2026-07-28 revision deletes the `initialize`
|
|
85
|
+
handshake, the session id and the entire back-channel — but this client speaks **both** wires and
|
|
86
|
+
prefers neither. No legacy path was removed. Even the WebSocket transport stays, documented as
|
|
87
|
+
non-standard, though upstream deleted theirs.
|
|
88
|
+
- **Every response type only gained optional fields.** Nothing was removed, narrowed, or made
|
|
89
|
+
required.
|
|
90
|
+
- **Every request option is still accepted.** Where a provider stopped taking one, we decide
|
|
91
|
+
internally whether it reaches the wire — your build does not break because of their typings.
|
|
92
|
+
|
|
93
|
+
Full detail in [CHANGELOG.md](./CHANGELOG.md).
|
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ A unified, pluggable AI SDK for accessing the LLMs of every major provider —
|
|
|
10
10
|
`model:tier` selectors, capability-based `select()`, tiered pricing, and cost
|
|
11
11
|
tracking.
|
|
12
12
|
- **Cross-environment.** The same code runs on Node, Bun, and the browser. Zero
|
|
13
|
-
runtime dependencies, ESM.
|
|
13
|
+
**required** runtime dependencies, ESM.
|
|
14
14
|
|
|
15
15
|
## Install
|
|
16
16
|
|
|
@@ -18,7 +18,22 @@ A unified, pluggable AI SDK for accessing the LLMs of every major provider —
|
|
|
18
18
|
npm install @combycode/llm-sdk # or: bun add @combycode/llm-sdk
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Upgrading from 1.x? See [MIGRATION.md](./MIGRATION.md) — three things can
|
|
22
|
+
require action, and none of them is a provider change.
|
|
23
|
+
|
|
24
|
+
Requires **Node ≥ 22** or Bun ≥ 1.1. (Node 18 and 20 are end-of-life; 22 is also the floor
|
|
25
|
+
`openai-node` 7 adopted.)
|
|
26
|
+
|
|
27
|
+
`dependencies` is empty. The one optional extra is `tiktoken`, an **optional peer** for exact local
|
|
28
|
+
OpenAI token counting — install it only if you want that:
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npm install tiktoken
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Without it everything still works: token counting falls back to the provider count-API
|
|
35
|
+
(Anthropic/Google) or a calibrated heuristic. It is a peer rather than an `optionalDependency` so
|
|
36
|
+
its ~5.6 MB wasm is not installed — or bundled — for people who never use it.
|
|
22
37
|
|
|
23
38
|
## Quickstart
|
|
24
39
|
|
|
@@ -4,6 +4,7 @@ import type { LLMClient } from '../llm/client';
|
|
|
4
4
|
import type { CacheConfig, ThinkingConfig } from '../llm/types/request';
|
|
5
5
|
import type { ConversationHistory } from './history';
|
|
6
6
|
import type { HistorySnapshot } from './history-types';
|
|
7
|
+
import type { ReflectAndRetryConfig } from './reflect-retry';
|
|
7
8
|
import type { AgentTool } from './types';
|
|
8
9
|
import type { Guardrail, ToolInputGuardrail } from './guardrail-types';
|
|
9
10
|
import type { PermissionPolicy } from '../plugins/permissions/policy';
|
|
@@ -23,6 +24,27 @@ export interface AgentLoopConfig {
|
|
|
23
24
|
context?: string;
|
|
24
25
|
/** Executable tools. Indexed by function name (FunctionTool) or type (BuiltinTool). */
|
|
25
26
|
tools?: AgentTool[];
|
|
27
|
+
/** What to do when two tools claim the same registry key.
|
|
28
|
+
*
|
|
29
|
+
* Registration is a map keyed by function name / builtin type, so a collision means one tool
|
|
30
|
+
* SILENTLY replaces another and the model never sees it. The failure then surfaces much later
|
|
31
|
+
* as "the model called the wrong tool", with nothing in the logs pointing at the cause.
|
|
32
|
+
*
|
|
33
|
+
* - `'warn'` (default) — keep last-write-wins, but emit an `onWarning`
|
|
34
|
+
* (`code: 'tool_name_collision'`) naming the key and which tool lost.
|
|
35
|
+
* - `'error'` — throw at construction / `addTool()`, before the model is ever called.
|
|
36
|
+
*
|
|
37
|
+
* Defaults to `'warn'` so an app that unknowingly has a collision keeps working
|
|
38
|
+
* (CONSTITUTION.md R4) — the collision just stops being invisible. */
|
|
39
|
+
toolNameCollisionPolicy?: 'warn' | 'error';
|
|
40
|
+
/** Self-healing recovery from a recoverable MODEL failure (a malformed tool call, a hallucinated
|
|
41
|
+
* tool name, a truncated call). The model is given structured guidance naming the attempt and
|
|
42
|
+
* told not to repeat the same call, then the step is retried within a bounded budget.
|
|
43
|
+
*
|
|
44
|
+
* Off unless configured: a retry costs a real request, so it is the caller's decision. This is
|
|
45
|
+
* NOT a network retry — the engine already handles transport failures. This one is for a request
|
|
46
|
+
* that succeeded and came back unusable, which resending unchanged would never fix. */
|
|
47
|
+
reflectAndRetry?: ReflectAndRetryConfig;
|
|
26
48
|
/** Reuse an existing history (or rehydrate from a snapshot). New history
|
|
27
49
|
* is created when omitted. */
|
|
28
50
|
history?: ConversationHistory | HistorySnapshot;
|
|
@@ -12,6 +12,8 @@ export interface ToolCallAccumEntry {
|
|
|
12
12
|
/** All mutable state for one streaming step inside AgentLoop.stream(). */
|
|
13
13
|
export interface StepState {
|
|
14
14
|
stepText: string;
|
|
15
|
+
/** Commentary deltas, kept apart from stepText so the step's answer excludes narration. */
|
|
16
|
+
stepCommentary: string;
|
|
15
17
|
stepThinking: string;
|
|
16
18
|
stepToolCalls: ToolCallPart[];
|
|
17
19
|
toolCallAccum: Map<string, ToolCallAccumEntry>;
|