@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
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,290 @@ 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
|
+
|
|
116
|
+
## [2.1.0] — 2026-08-17
|
|
117
|
+
|
|
118
|
+
Minor, not major: everything below is additive or a bug fix, and no export was removed or
|
|
119
|
+
renamed. One caveat worth reading before upgrading — see **`defineTool` optional parameters** under
|
|
120
|
+
Fixed, which tightens an inferred type and can therefore surface a compile error in code that was
|
|
121
|
+
already wrong at runtime.
|
|
122
|
+
|
|
123
|
+
### Added
|
|
124
|
+
|
|
125
|
+
- **Lazy tool loading — register a tool without declaring it.** `lazy: true` on `defineTool`, on an
|
|
126
|
+
`AgentTool`, or on a whole MCP server via `connectMcp(cfg, { lazy: true })`. The tool is registered,
|
|
127
|
+
namespaced and collision-checked exactly as before, but is not placed in the `tools` array: the
|
|
128
|
+
model finds it with a built-in `tool_search`, which returns full schemas as data, and runs it
|
|
129
|
+
through a built-in `call_tool`. Both are declared only when at least one lazy tool exists, so an app
|
|
130
|
+
that never opts in sees nothing new.
|
|
131
|
+
|
|
132
|
+
Measured over 308 tools, six tasks, three reps, both providers: identical correctness, **−72%** cost
|
|
133
|
+
per task on `claude-haiku-4.5` and **−97%** on `gpt-5.4-nano`, for one extra round trip. The saving
|
|
134
|
+
is not from caching — it is from never sending the tool block. Schemas arrive in a tool RESULT,
|
|
135
|
+
which lands after the cached prefix, so the declared array never changes and no discovery event can
|
|
136
|
+
invalidate it. Promoting tools into the array instead costs **+63%** versus never deferring.
|
|
137
|
+
|
|
138
|
+
**It is not always a win.** Below roughly a hundred richly-schema'd tools it costs more than
|
|
139
|
+
declaring everything, because what remains in the prefix falls under the provider's minimum
|
|
140
|
+
cacheable size while a search round trip is still paid. There is deliberately no automatic
|
|
141
|
+
threshold: whether deferring pays depends on schema size, not tool count.
|
|
142
|
+
|
|
143
|
+
`ToolCallReport.toolName` names the tool that actually ran, never `call_tool`, and carries
|
|
144
|
+
`discoveredVia: 'search'`. A new `onToolSearch` hook reports queries, what matched, and — the field
|
|
145
|
+
worth alerting on — which queries matched nothing. Tuning via `lazyTools: { limit, maxSearches }`.
|
|
146
|
+
|
|
147
|
+
- **`CostSummary.unpriced` / `.unpricedModels` — a $0.00 total no longer hides a failed lookup.** A
|
|
148
|
+
model with no catalog entry was priced at zero and summed into every total, so a report read $0.00
|
|
149
|
+
when it meant "could not price this", and a budget built on that total silently never fired. The
|
|
150
|
+
per-entry `cost.source: 'unknown'` tag already recorded it; nothing aggregated it. The collector
|
|
151
|
+
now also emits one `onWarning` per unknown model (`code: 'unpriced_model'`) — once per model, not
|
|
152
|
+
per request. Genuinely free calls are unaffected: they are priced `'calculated'` at zero with a
|
|
153
|
+
note. The usual cause is a model id that reaches the provider but is not a catalog key, e.g.
|
|
154
|
+
`anthropic/claude-haiku-4-5` against the catalog's `anthropic/claude-haiku-4.5`.
|
|
155
|
+
- **`strictSupport(schema, dialect)` — ask whether a schema can satisfy a provider's strict mode.**
|
|
156
|
+
Returns `{ ok, reason }`, where `reason` names the property or keyword responsible. Exported
|
|
157
|
+
because the answer differs per provider and was otherwise only discoverable by getting a 400.
|
|
158
|
+
- **`complete({ seed, topK })` and `CompleteResult.error`.** All three existed on `LLMClient` and the
|
|
159
|
+
agent path but not on the one-shot helper, so a documented example demonstrating them did not
|
|
160
|
+
compile. `error` surfaces the in-band failure some providers report instead of throwing (OpenAI
|
|
161
|
+
Responses `status: 'failed'`), which otherwise reads as a successful empty answer.
|
|
162
|
+
|
|
163
|
+
- **`complete({ cache })` — prompt caching is reachable from the one-shot helper.** `CompleteOptions`
|
|
164
|
+
had no `cache` field at all, so asking for it did nothing: the option was dropped in silence, with
|
|
165
|
+
no error and no warning, while `LLMClient` and every adapter supported it fully. It matters most
|
|
166
|
+
exactly where this helper is convenient — a long system prompt or a large tool block, which sit at
|
|
167
|
+
the front of the request and are the cheapest part to cache.
|
|
168
|
+
- Found by a benchmark that reported zero cached tokens for every arm it measured.
|
|
169
|
+
|
|
170
|
+
### Fixed
|
|
171
|
+
|
|
172
|
+
- **Any OpenAI tool with an OPTIONAL parameter was rejected outright.** The library forced
|
|
173
|
+
`strict: true` on every function tool while sending the schema as written. OpenAI's strict mode
|
|
174
|
+
requires every property to appear in `required`, at every nesting level, and answers a schema that
|
|
175
|
+
does not with `400 Invalid schema: 'required' is required to be supplied` — never a degraded
|
|
176
|
+
result. So `defineTool({ optional: [...] })`, a documented feature, could not be used on OpenAI at
|
|
177
|
+
all, and neither could most MCP servers. The same forcing applied to structured output on both
|
|
178
|
+
OpenAI APIs.
|
|
179
|
+
|
|
180
|
+
On Responses, where strict has long been the default, it is now requested only where the schema
|
|
181
|
+
can satisfy the provider. Elsewhere it stays OPT-IN — see the next entry. The rules differ per
|
|
182
|
+
provider, measured live rather than read off the docs:
|
|
183
|
+
|
|
184
|
+
| | OpenAI | Anthropic |
|
|
185
|
+
|---|---|---|
|
|
186
|
+
| optional properties (not in `required`) | rejected | fine |
|
|
187
|
+
| `minimum` / `maximum` / `exclusive*` / `multipleOf` / `maxItems` | fine | rejected |
|
|
188
|
+
| `additionalProperties: true` | rejected | rejected |
|
|
189
|
+
| `{ type: 'object' }` with no `properties` key | rejected | fine |
|
|
190
|
+
| more than 20 strict tools per request | fine | rejected |
|
|
191
|
+
|
|
192
|
+
Two consequences: a generic router tool — one whose parameter must accept any shape — can never
|
|
193
|
+
be strict, and past Anthropic's cap the defaulted tools give up strict together rather than the
|
|
194
|
+
first 20 keeping it by array order. Passing `strict` explicitly still wins in either direction,
|
|
195
|
+
including past the cap. A no-argument tool is unaffected: `properties: {}` is present but empty,
|
|
196
|
+
which both providers accept.
|
|
197
|
+
|
|
198
|
+
Nothing caught this because nothing executed it: every example declared its tool parameters as
|
|
199
|
+
required, and the MCP server used throughout the corpus marks everything required. Typecheck,
|
|
200
|
+
API snapshot, doc-snippet compilation and consumer install all passed on code the API refuses.
|
|
201
|
+
|
|
202
|
+
- **Strict stays OPT-IN on Anthropic and OpenAI Chat Completions.** It was briefly defaulted on
|
|
203
|
+
during this cycle and reverted before release, so behaviour on both is unchanged from 2.0.1.
|
|
204
|
+
|
|
205
|
+
What decided it: strict makes no measurable difference to argument quality — 40 of 40 calls
|
|
206
|
+
conformed with it and without it on both providers, including prompts written to pull away from
|
|
207
|
+
the schema. Its one real effect is that Anthropic then refuses to call a tool that was never
|
|
208
|
+
declared (10/10 undeclared without it, 0/10 with it), and that only matters when something puts
|
|
209
|
+
an undeclared tool in front of the model, which ordinary use does not.
|
|
210
|
+
|
|
211
|
+
Against that, Anthropic's strict mode carries limits no per-schema check can predict: at most 20
|
|
212
|
+
strict tools per request, at most 24 optional parameters summed across all strict schemas
|
|
213
|
+
(nested ones included), and an opaque complexity limit on top — 24 optional parameters spread
|
|
214
|
+
over four tools compiles, the same 24 in one tool answers "Schema is too complex for
|
|
215
|
+
compilation". Twelve ordinary tools with five optional parameters each already exceed the second.
|
|
216
|
+
The first two are aggregates, so they cannot live in a per-schema predicate; the third has no
|
|
217
|
+
published formula. Opt-in is the only honest default there.
|
|
218
|
+
|
|
219
|
+
- **`defineTool` typed optional parameters as always present.** Keys listed in `optional` inferred as
|
|
220
|
+
required in the `execute` args, so `args.unit.toUpperCase()` typechecked and threw at runtime on
|
|
221
|
+
every call where the model omitted the argument — the expected case for an optional argument. They
|
|
222
|
+
now infer as `| undefined`.
|
|
223
|
+
|
|
224
|
+
- **`complete()` sent different options depending on whether `tools` was passed.** The helper has two
|
|
225
|
+
branches, and the tools branch forwarded only `structured` — silently dropping `providerOptions`,
|
|
226
|
+
`audio`, `outputModalities` and `serviceTier`, which the no-tools branch honoured. Adding a tool to
|
|
227
|
+
a working call could therefore change behaviour that has nothing to do with tools. Both branches
|
|
228
|
+
now forward the same set.
|
|
229
|
+
|
|
230
|
+
### Added
|
|
231
|
+
|
|
232
|
+
- **MCP tool definitions carry what the spec publishes.** `McpToolDef` now models `annotations`
|
|
233
|
+
(`readOnlyHint` / `destructiveHint` / `idempotentHint` / `openWorldHint`), `icons`, `execution`
|
|
234
|
+
and `_meta`. The data always arrived — `tools/list` results are passed through unparsed — but was
|
|
235
|
+
untyped, so a host could not act on it without a cast. These stay **host-facing**: no provider's
|
|
236
|
+
function-tool schema has a field that could carry them, so none is sent to the model.
|
|
237
|
+
- `execution.taskSupport: 'required'` means the tool MUST be invoked as a task. Task invocation is
|
|
238
|
+
not implemented, so such a tool cannot be called through this client — the field being visible is
|
|
239
|
+
what lets a caller see that instead of meeting it as a server error.
|
|
240
|
+
- **MCP `outputSchema` reaches the model when asked for** — `connectMcp(cfg, { validateOutput: true })`.
|
|
241
|
+
It was read for local validation only, while OpenAI Responses accepts `output_schema` and the
|
|
242
|
+
library already emitted it for hand-written tools; the two ends were never connected.
|
|
243
|
+
- **Opt-in, because declaring it is a promise about the RESULT.** OpenAI then rejects the turn
|
|
244
|
+
— *"expected a JSON string because the function declares output_schema"* — unless the result is
|
|
245
|
+
JSON matching that schema, so the tool result changes from prose to structured data. MCP returns
|
|
246
|
+
`structuredContent` exactly when a tool publishes `outputSchema`, and that is now what comes
|
|
247
|
+
back — under the same flag, so nothing changes for anyone who did not ask for it.
|
|
248
|
+
- Verified end to end against a live MCP server through OpenAI Responses, in both modes.
|
|
249
|
+
- **`moderate()` now returns the shape of its input.** Overloads: one input (a string, or one
|
|
250
|
+
content-part array forming a single multimodal item) returns a `ModerationResult`; a list of
|
|
251
|
+
inputs returns `ModerationResult[]`. The mapping was documented in prose from the start but the
|
|
252
|
+
signature returned the bare union, so `result.flagged` was a type error and **every documented
|
|
253
|
+
example failed to compile**. The wide-union overload is kept, so code that already narrows still
|
|
254
|
+
compiles. No runtime change.
|
|
255
|
+
- **`toolKey` / `describeTool` are exported.** `AgentTool.definition` is a `Tool` union (function
|
|
256
|
+
tool or builtin), so `.name` does not exist on it; reading a tool's name previously required
|
|
257
|
+
hand-narrowing at every call site.
|
|
258
|
+
- **`createEngine({ retry })` — retry policy configurable where it belongs.** The machinery existed
|
|
259
|
+
and worked, but the only way to reach it was hand-building an `HttpRequest`; nothing on
|
|
260
|
+
`createEngine()` exposed it. Retry is cross-cutting, so it is now an engine-level setting inherited
|
|
261
|
+
by every queue, with `createEngine({ queues })` for one provider and `HttpRequest.retry` for one
|
|
262
|
+
request. Nested groups (`backoff`, `perKind`) merge rather than replace, so overriding one knob
|
|
263
|
+
does not reset the schedule.
|
|
264
|
+
- New type `RetryPolicyOverride`. `Partial<RetryConfig>` only makes top-level keys optional, so it
|
|
265
|
+
still demanded a complete `backoff` — the partial override the merge always supported was not
|
|
266
|
+
expressible. Engine, queue and `mergeRetry` now take the deeper-partial type; strictly wider, so
|
|
267
|
+
existing config keeps compiling.
|
|
268
|
+
- Additive: omitting all of it is byte-identical to before.
|
|
269
|
+
|
|
270
|
+
### Fixed
|
|
271
|
+
|
|
272
|
+
- **Docs: the per-request retry sample could never compile.** `docs/guide/network.md` showed
|
|
273
|
+
`complete({ retry: { attempts, initialDelay, maxDelay, expBase, httpStatusCodes } })`. `complete()`
|
|
274
|
+
takes no `retry` option (`TS2353`), and not one of those field names exists on
|
|
275
|
+
`RequestRetryOverride` — the real fields are `maxRetries` / `totalTimeoutMs` / `attemptTimeoutMs` /
|
|
276
|
+
`maxRetryAfterMs` / `backoff{initialMs,maxMs,multiplier,jitter}`, and the override rides on
|
|
277
|
+
`engine.fetch()`. The feature was always correct; only its documentation was wrong. Same class as
|
|
278
|
+
the 2.0.0 `agent.run()` defect, on an option object rather than a method, which is why the
|
|
279
|
+
consumer-surface check did not see it.
|
|
280
|
+
|
|
281
|
+
- **MCP result cache: `ttlMs: 0` now evicts.** The hint was documented as "immediately stale — not
|
|
282
|
+
the same as absent", but `set()` funnelled it through the same branch as a missing hint and stored
|
|
283
|
+
nothing. Any entry already held survived, so a server that said "cache for 60s" and later "stale
|
|
284
|
+
now" kept being answered from the stale entry for the rest of the original TTL — the instruction
|
|
285
|
+
was accepted and inert. A non-positive `ttlMs` now drops the entry for that key. Absent hints are
|
|
286
|
+
unchanged and still leave an existing entry alone, so pre-2026 servers behave exactly as before.
|
|
287
|
+
- The existing unit test asserted `set(…, { ttlMs: 0 })` returned `false` and `get` was
|
|
288
|
+
`undefined` — on an *empty* cache, where that holds whether or not the hint does anything. It
|
|
289
|
+
passed for the wrong reason. Found by writing the MCP protocol example as a consumer.
|
|
290
|
+
|
|
7
291
|
## [2.0.1] - 2026-08-10
|
|
8
292
|
|
|
9
293
|
Three defects reported by a consumer within a day of 2.0.0 — all reachable by reading the shipped
|
|
@@ -449,9 +733,11 @@ interaction or an outright failure from a clean finish.
|
|
|
449
733
|
error, so the gating is locked by unit tests and was live-verified end to end.
|
|
450
734
|
- **`docs/feature-matrix.json` — the parity matrix.** Every capability an official SDK exposes, how
|
|
451
735
|
each provider spells it, and where we stand, with citations into the version-pinned clones.
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
736
|
+
|
|
737
|
+
> **Corrected 2026-08-10.** This entry also described the release tooling that consumes the
|
|
738
|
+
> matrix. That does not belong in a library changelog — a consumer of the package cannot see it,
|
|
739
|
+
> run it, or care about it — and the tool it named did not exist. Only the shipped data is
|
|
740
|
+
> described here now.
|
|
455
741
|
- **`FinishReason` gains `'pending'`** — non-terminal: the provider accepted the request but has not
|
|
456
742
|
produced a completion (Google Interactions `queued`, OpenAI Responses `queued`/`in_progress` in
|
|
457
743
|
background mode). Treat as "poll/retry", never as a result. *Additive union member: exhaustive
|
|
@@ -26,7 +26,13 @@ export declare const LAYER_CHAT_FACTS = "chat.facts";
|
|
|
26
26
|
export declare const LAYER_EXECUTOR_TOOL_EXAMPLES = "executor.tool-examples";
|
|
27
27
|
/** ContextGuard's compaction summary layer (replaces compacted message ranges). */
|
|
28
28
|
export declare const LAYER_CONTEXT_GUARD_SUMMARY = "context-guard.summary";
|
|
29
|
+
/** How to reach tools that are registered but not declared. Present only while at least
|
|
30
|
+
* one lazy tool exists. See `writeLazyToolsProtocol`. */
|
|
31
|
+
export declare const LAYER_LAZY_TOOLS = "agentloop.lazy-tools";
|
|
29
32
|
export declare const PRIORITY_AGENTLOOP_SYSTEM = 10;
|
|
33
|
+
/** Just after the agent's own system text: the model needs to know its tools are not
|
|
34
|
+
* listed before anything else it is told about them. */
|
|
35
|
+
export declare const PRIORITY_LAZY_TOOLS = 20;
|
|
30
36
|
export declare const PRIORITY_LEGACY_SYSTEM = 50;
|
|
31
37
|
export declare const PRIORITY_AGENTLOOP_CONTEXT = 100;
|
|
32
38
|
export declare const PRIORITY_MEMORY = 200;
|
|
@@ -35,5 +41,18 @@ export declare const PRIORITY_EXECUTOR_TOOL_EXAMPLES = 280;
|
|
|
35
41
|
export declare const PRIORITY_CONTEXT_GUARD_SUMMARY = 300;
|
|
36
42
|
/** Set the AgentLoop's persona/system layer on a registry (or remove if blank). */
|
|
37
43
|
export declare function writeAgentLoopSystem(registry: ContextRegistry, text: string | undefined, owner: string): void;
|
|
44
|
+
/** Tell the model that its tools are not all listed, and how to reach the rest.
|
|
45
|
+
*
|
|
46
|
+
* Without this the feature under-performs badly and quietly: measured over 24 live runs
|
|
47
|
+
* against 308 lazy tools, the model scored 8/12 and 9/12 — sometimes never searching at
|
|
48
|
+
* all, more often searching once for a request that needed two capabilities and
|
|
49
|
+
* answering from the one tool it found. With the protocol stated, the same tasks and the
|
|
50
|
+
* same ranker scored 18/18. The tool descriptions alone are not enough, because a model
|
|
51
|
+
* has no reason to suspect a tool exists that it cannot see.
|
|
52
|
+
*
|
|
53
|
+
* Deliberately a few lines and NOT a catalog. A catalog is paid for on every turn for
|
|
54
|
+
* the whole conversation, which is most of what makes eager exposure expensive in the
|
|
55
|
+
* first place; this is a fixed cost of roughly one sentence. */
|
|
56
|
+
export declare function writeLazyToolsProtocol(registry: ContextRegistry, active: boolean, owner: string): void;
|
|
38
57
|
/** Set the AgentLoop's run-scenario context layer (or remove if blank). */
|
|
39
58
|
export declare function writeAgentLoopContext(registry: ContextRegistry, text: string | undefined, owner: string): void;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/** Lazy tool loading: `tool_search` + `call_tool`.
|
|
2
|
+
*
|
|
3
|
+
* A tool registered with `lazy: true` is NOT placed in the `tools` array. The model
|
|
4
|
+
* finds it by searching, and calls it through `call_tool`. The point is what does NOT
|
|
5
|
+
* move: the declared tool array never changes, so no discovery event can invalidate the
|
|
6
|
+
* cached prefix. Schemas travel as tool RESULTS, which land in history after the prefix.
|
|
7
|
+
*
|
|
8
|
+
* Measured against declaring everything (308 tools, 6 tasks, 3 reps, both providers —
|
|
9
|
+
* `bench/lazy-tools-e2e`): identical correctness (72/72), −72% cost on claude-haiku-4.5
|
|
10
|
+
* and −97% on gpt-5.4-nano, at one extra round trip per task.
|
|
11
|
+
*
|
|
12
|
+
* Three details here are load-bearing and each came from a measurement that failed
|
|
13
|
+
* first. They are not stylistic:
|
|
14
|
+
*
|
|
15
|
+
* 1. `call_tool` is SINGULAR. A batching form — `call_tools({ calls: [...] })` — is
|
|
16
|
+
* returned as a JSON *string* rather than an array by claude-haiku about half the
|
|
17
|
+
* time (19/30 vs 30/30), because a router's `input` must be open and an open object
|
|
18
|
+
* cannot be strict, so no grammar holds the shape. Batching is not lost: the model
|
|
19
|
+
* emits several parallel `call_tool` calls in one turn instead.
|
|
20
|
+
*
|
|
21
|
+
* 2. `tool_search` REPORTS QUERIES THAT MATCHED NOTHING. Merging results silently makes
|
|
22
|
+
* a failed lookup indistinguishable from one whose hits were folded in with the
|
|
23
|
+
* others, and the model then answers confidently from the tools it did get. Measured
|
|
24
|
+
* on colloquial phrasing: without this, 34/36 recall and 17/18 correct; with it,
|
|
25
|
+
* 36/36 and 18/18.
|
|
26
|
+
*
|
|
27
|
+
* 3. Ranking is deliberately weak, and that is survivable only because the MODEL writes
|
|
28
|
+
* the query. Token overlap against raw user text scores 0–1 of 8 on indirect or
|
|
29
|
+
* colloquial phrasing (`bench/tool-ranking`). The model's rewriting is what carries
|
|
30
|
+
* it. Better ranking would reduce how far that rewriting has to reach; it is not what
|
|
31
|
+
* makes the feature work.
|
|
32
|
+
*/
|
|
33
|
+
import type { AgentTool } from './types';
|
|
34
|
+
/** Tuning for lazy tool exposure. Every field is optional; the defaults are what the
|
|
35
|
+
* measurements used. */
|
|
36
|
+
export interface LazyToolsConfig {
|
|
37
|
+
/** Schemas returned per query. Default 5, capped at 20 — the cap is a real bound, not
|
|
38
|
+
* a formality: returning everything re-creates the cost the feature exists to avoid. */
|
|
39
|
+
limit?: number;
|
|
40
|
+
/** Searches allowed per run. Default 5. Exceeding it returns a tool result saying so
|
|
41
|
+
* and leaves the run alive: a model that loops on search should be told, not killed. */
|
|
42
|
+
maxSearches?: number;
|
|
43
|
+
}
|
|
44
|
+
/** The two names this module owns. Registered like any other tool, so the existing
|
|
45
|
+
* collision policy applies to them and there is no second registry. */
|
|
46
|
+
export declare const LAZY_SEARCH_TOOL = "tool_search";
|
|
47
|
+
export declare const LAZY_CALL_TOOL = "call_tool";
|
|
48
|
+
/** Rank candidates by token overlap over name + description + parameter names, with name
|
|
49
|
+
* matches weighted double — a query naming the domain should beat a filler whose long
|
|
50
|
+
* description happens to share vocabulary. Local and dependency-free by design: no
|
|
51
|
+
* embeddings, no network call on the discovery path. */
|
|
52
|
+
export declare function rankTools(query: string, candidates: AgentTool[], limit: number): AgentTool[];
|
|
53
|
+
/** Per-run search budget. Reset at the start of each run, not shared across runs.
|
|
54
|
+
*
|
|
55
|
+
* Internal: deliberately NOT exported from the package. It is wiring between this module
|
|
56
|
+
* and `AgentLoop`, and a consumer has no use for it — publishing it would put two types
|
|
57
|
+
* in the public API that can never be called usefully from outside. */
|
|
58
|
+
interface LazySearchState {
|
|
59
|
+
searches: number;
|
|
60
|
+
}
|
|
61
|
+
/** Build the two built-in tools. Returned as ordinary `AgentTool`s so they register,
|
|
62
|
+
* dispatch, time out and report through exactly the same path as any other tool.
|
|
63
|
+
*
|
|
64
|
+
* The dependency shape is inline rather than a named interface on purpose: a named type
|
|
65
|
+
* in an exported signature lands in the published `.d.ts`, and this one is wiring
|
|
66
|
+
* between here and `AgentLoop` that a consumer can never usefully call.
|
|
67
|
+
*
|
|
68
|
+
* `lazyTools` and `eagerNames` are functions, not arrays, because tools can be added
|
|
69
|
+
* after construction and search must see them. */
|
|
70
|
+
export declare function createLazyTools(deps: {
|
|
71
|
+
lazyTools: () => AgentTool[];
|
|
72
|
+
eagerNames: () => string[];
|
|
73
|
+
state: LazySearchState;
|
|
74
|
+
config: LazyToolsConfig;
|
|
75
|
+
onSearch?: (info: {
|
|
76
|
+
queries: string[];
|
|
77
|
+
matched: string[];
|
|
78
|
+
unmatched: string[];
|
|
79
|
+
}) => void;
|
|
80
|
+
}): AgentTool[];
|
|
81
|
+
/** The inner tool a `call_tool` invocation targeted, for reporting. Returns null for any
|
|
82
|
+
* other call. Traces that attribute every tool call to `call_tool` are useless, and this
|
|
83
|
+
* is the one real regression the design causes — so it is fixed at the source. */
|
|
84
|
+
export declare function unwrapLazyCall(toolName: string, args: Record<string, unknown>): string | null;
|
|
85
|
+
export {};
|
|
@@ -6,6 +6,7 @@ import type { ConversationHistory } from './history';
|
|
|
6
6
|
import type { HistorySnapshot } from './history-types';
|
|
7
7
|
import type { ReflectAndRetryConfig } from './reflect-retry';
|
|
8
8
|
import type { AgentTool } from './types';
|
|
9
|
+
import type { LazyToolsConfig } from './lazy-tools';
|
|
9
10
|
import type { Guardrail, ToolInputGuardrail } from './guardrail-types';
|
|
10
11
|
import type { PermissionPolicy } from '../plugins/permissions/policy';
|
|
11
12
|
import type { ApprovalRequest, ApprovalDecision } from './approval-types';
|
|
@@ -13,6 +14,25 @@ import type { Persistence } from '../plugins/persistence/types';
|
|
|
13
14
|
export interface AgentLoopConfig {
|
|
14
15
|
/** LLM client. AgentLoop reads `client.model` and uses `client.complete`/`client.stream`. */
|
|
15
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>;
|
|
16
36
|
/** Persona / role text for the agent. Stored as the `agentloop.system` registry
|
|
17
37
|
* layer (priority 10). Composed with other system-tagged layers when sending.
|
|
18
38
|
* When passed as a function, it is re-evaluated at the start of every
|
|
@@ -37,6 +57,14 @@ export interface AgentLoopConfig {
|
|
|
37
57
|
* Defaults to `'warn'` so an app that unknowingly has a collision keeps working
|
|
38
58
|
* (CONSTITUTION.md R4) — the collision just stops being invisible. */
|
|
39
59
|
toolNameCollisionPolicy?: 'warn' | 'error';
|
|
60
|
+
/** Tuning for tools registered with `lazy: true`. Has no effect when none are — the
|
|
61
|
+
* built-in `tool_search` / `call_tool` are declared only if a lazy tool exists, so an
|
|
62
|
+
* app that never uses the feature never sees them.
|
|
63
|
+
*
|
|
64
|
+
* There is deliberately no `threshold` here: whether deferring pays depends on the
|
|
65
|
+
* SIZE of the tool schemas, not their count, so an automatic cutoff would be guessing.
|
|
66
|
+
* Mark tools lazy explicitly. */
|
|
67
|
+
lazyTools?: LazyToolsConfig;
|
|
40
68
|
/** Self-healing recovery from a recoverable MODEL failure (a malformed tool call, a hallucinated
|
|
41
69
|
* tool name, a truncated call). The model is given structured guidance naming the attempt and
|
|
42
70
|
* told not to repeat the same call, then the step is retried within a bounded budget.
|
|
@@ -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,12 +23,25 @@ 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;
|
|
29
35
|
private _systemThunk;
|
|
30
36
|
private _context;
|
|
31
37
|
private _tools;
|
|
38
|
+
private _lazyConfig;
|
|
39
|
+
/** Per-run search budget, reset at the start of every run. */
|
|
40
|
+
private _lazyState;
|
|
41
|
+
/** Installed on the first `lazy` registration and never removed, so the declared tool
|
|
42
|
+
* array stays byte-identical for the life of the conversation — which is the entire
|
|
43
|
+
* reason the design is cheap. */
|
|
44
|
+
private _lazyInstalled;
|
|
32
45
|
private _history;
|
|
33
46
|
private _reports;
|
|
34
47
|
private _metadata;
|
|
@@ -103,7 +116,26 @@ export declare class AgentLoop {
|
|
|
103
116
|
private runApprovalGate;
|
|
104
117
|
/** Emit onToolCallComplete, push report, and return success content part. */
|
|
105
118
|
private buildSuccessResult;
|
|
106
|
-
/**
|
|
119
|
+
/** Declare `tool_search` + `call_tool`, once, on the first lazy registration.
|
|
120
|
+
*
|
|
121
|
+
* They go through `registerTool` like anything else, so the collision policy covers
|
|
122
|
+
* them and there is no second registry to keep in sync. They are never removed: the
|
|
123
|
+
* declared array must stay identical for the whole conversation or the cached prefix
|
|
124
|
+
* is invalidated, which is the cost the feature exists to avoid. */
|
|
125
|
+
private installLazyTools;
|
|
126
|
+
/** Publish (or remove) the "your tools are not all listed" layer.
|
|
127
|
+
*
|
|
128
|
+
* Separate from `installLazyTools` because tools are registered in the constructor
|
|
129
|
+
* BEFORE `_history` exists, and the layer lives in the history's registry. The
|
|
130
|
+
* constructor calls this again once history is built.
|
|
131
|
+
*
|
|
132
|
+
* The model has no reason to suspect a tool it cannot see, and the failure without
|
|
133
|
+
* this is quiet — it answers from whatever it did find. Measured at 8/12 and 9/12
|
|
134
|
+
* without the protocol, 18/18 with it, same tasks and same ranker. */
|
|
135
|
+
private syncLazyProtocol;
|
|
136
|
+
/** Merge agent's tool definitions with caller-provided tools (caller wins on conflict).
|
|
137
|
+
*
|
|
138
|
+
* Lazy tools are registered but NOT declared — that filter is the whole mechanism. */
|
|
107
139
|
private toolDefinitions;
|
|
108
140
|
private beginRun;
|
|
109
141
|
private finalizeRun;
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -37,6 +37,16 @@ export interface LearnInput {
|
|
|
37
37
|
export interface AgentTool {
|
|
38
38
|
/** Tool schema sent to the LLM. */
|
|
39
39
|
definition: Tool;
|
|
40
|
+
/** Register the tool but do NOT declare it: the model finds it with `tool_search` and
|
|
41
|
+
* invokes it through `call_tool`. Exposure only — registration, validation and
|
|
42
|
+
* collision checking are unchanged, and nothing happens mid-run.
|
|
43
|
+
*
|
|
44
|
+
* Worth it when the tool block is large and a run touches few of them: measured at
|
|
45
|
+
* −72% (claude-haiku-4.5) and −97% (gpt-5.4-nano) cost per task across 308 tools, with
|
|
46
|
+
* identical correctness, for one extra round trip. NOT worth it for a small tool set —
|
|
47
|
+
* below roughly a hundred richly-schema'd tools the saving inverts, because the
|
|
48
|
+
* remaining prefix falls under the provider's minimum cacheable size. */
|
|
49
|
+
lazy?: boolean;
|
|
40
50
|
/** Execute the tool. Return string or structured content. */
|
|
41
51
|
execute: (args: Record<string, unknown>, context: ToolExecutionContext) => Promise<string | ContentPart[]>;
|
|
42
52
|
/** Optional: derive out-of-band metadata from a successful tool result, attached
|
|
@@ -73,6 +83,11 @@ export interface ToolCallReport {
|
|
|
73
83
|
/** Out-of-band metadata from the tool's `customDataExtractor`, if any. Never sent
|
|
74
84
|
* to the model; present only when an extractor returned a value. */
|
|
75
85
|
customData?: unknown;
|
|
86
|
+
/** `'search'` when the model reached this tool through `call_tool` after finding it
|
|
87
|
+
* with `tool_search`. Absent for a normally-declared tool. `toolName` is the tool that
|
|
88
|
+
* actually ran either way — without that unwrapping every lazy call would report
|
|
89
|
+
* `call_tool` and attribution would be worthless. */
|
|
90
|
+
discoveredVia?: 'search';
|
|
76
91
|
}
|
|
77
92
|
export interface StepReport {
|
|
78
93
|
index: number;
|
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;
|
|
@@ -368,6 +374,19 @@ export interface CostEntry {
|
|
|
368
374
|
providerEvidence: Record<string, unknown>;
|
|
369
375
|
tags: Record<string, string | undefined>;
|
|
370
376
|
}
|
|
377
|
+
/** One `tool_search` call by the model, when lazy tools are in use.
|
|
378
|
+
*
|
|
379
|
+
* `unmatched` is the field that matters: a query that found nothing is how a
|
|
380
|
+
* multi-capability request quietly becomes a partial answer. Watch it to see whether the
|
|
381
|
+
* catalog's descriptions actually match the words your users reach for. */
|
|
382
|
+
export interface ToolSearchContext {
|
|
383
|
+
agentId: string;
|
|
384
|
+
queries: string[];
|
|
385
|
+
/** Tool names returned to the model, deduplicated across queries. */
|
|
386
|
+
matched: string[];
|
|
387
|
+
/** Queries that matched no tool at all. */
|
|
388
|
+
unmatched: string[];
|
|
389
|
+
}
|
|
371
390
|
export interface CostEntryContext {
|
|
372
391
|
entry: CostEntry;
|
|
373
392
|
runningTotal: number;
|
|
@@ -577,6 +596,7 @@ export interface HookMap {
|
|
|
577
596
|
onToolCallStart: ToolCallStartContext;
|
|
578
597
|
onToolCallComplete: ToolCallCompleteContext;
|
|
579
598
|
onToolCallError: ToolCallErrorContext;
|
|
599
|
+
onToolSearch: ToolSearchContext;
|
|
580
600
|
onRunComplete: RunCompleteContext;
|
|
581
601
|
onRunError: RunErrorContext;
|
|
582
602
|
onGuardrailTriggered: GuardrailTriggeredContext;
|