@theokit/sdk-handoff 0.1.2 → 0.1.4

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 CHANGED
@@ -1,5 +1,273 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.1.4
4
+
5
+ ### Patch Changes
6
+
7
+ - 1a4bbcf: The declared `@theokit/sdk` peer ranges stop promising versions the packages do not compile against.
8
+
9
+ All three declared `>=4.0.0`. `4.0.1` is the lowest published version that range admits — what a
10
+ consumer pinning conservatively, or resolving under an older transitive constraint, lands on. npm
11
+ resolves the combination with no `ERESOLVE` and no peer warning, and the build then fails on
12
+ `TS2552: Cannot find name` and `TS2305: has no exported member`.
13
+
14
+ The floors were measured by bisecting the 116 stable 4.x releases with a real build as the oracle.
15
+ Each one has its immediately preceding version failing, so these are exact versions rather than
16
+ intervals:
17
+
18
+ | package | floor | evidence |
19
+ | ---------------------- | ---------- | ------------------------------- |
20
+ | `@theokit/sdk-budget` | `>=4.54.0` | `4.53.1` fails, `4.54.0` passes |
21
+ | `@theokit/sdk-handoff` | `>=4.54.0` | `4.53.1` fails, `4.54.0` passes |
22
+ | `@theokit/sdk-memory` | `>=4.53.1` | `4.53.0` fails, `4.53.1` passes |
23
+
24
+ `sdk-memory` sits one release below the other two: this is not one shared migration, it is three
25
+ packages that each drifted past their own declared floor.
26
+
27
+ The oracle deletes every `dist/` before building. Without that the build reads a sibling's output
28
+ compiled against a different version, which is how a package "passes" against an SDK missing its
29
+ symbols — the failure mode that made the earlier measurement disagree with CI
30
+ (usetheokit/theokit-sdk#423).
31
+
32
+ ## 0.1.3
33
+
34
+ ### Patch Changes
35
+
36
+ - e3f2a82: Public-API documentation reviewed file by file, and corrected wherever it disagreed
37
+ with the code. The docblocks ship in the `.d.ts`, so these read as behaviour changes
38
+ in an editor even though no behaviour changed.
39
+
40
+ The corrections that change what a caller would do:
41
+
42
+ - **`sdk-cache` documented its own premise backwards.** The header example labelled a
43
+ semantic hit as if it avoided the provider call. `asPlugin()` returns the cached
44
+ answer as `recalledContext`, which the agent loop injects as a `<memory-context>`
45
+ block _before_ the prompt — the request still goes to the provider. The two modes
46
+ are now labelled separately, with a table saying which one short-circuits and which
47
+ one seeds.
48
+ - **`sdk-handoff`'s five error classes said "throw".** Under the plugin wiring the
49
+ handler never throws; every failure becomes a tool result `{"ok":false,…}` handed
50
+ back to the model. Each class now says where it is actually observable. The header
51
+ also told readers to `import { Handoff } from "@theokit/sdk"`, from which it was
52
+ extracted.
53
+ - **`sdk-budget`'s `charge()` claimed idempotency across concurrent calls.** The mutex
54
+ serialises, it does not deduplicate: two identical calls record twice. Related, and
55
+ newly documented: with `maxUsd` set, a model missing from the pricing table denies
56
+ every request rather than passing it — and the table matches by exact string, so
57
+ `"openai/gpt-4o"` does not match `"gpt-4o"`.
58
+ - **The three `memory-*` adapters advertised an env-var fallback they do not read**,
59
+ and their peer dependencies are required rather than optional. Their behavioural
60
+ differences are now stated where they break the "interchangeable adapter"
61
+ assumption — honcho ignores `k` and always throws on `delete`; mem0 recalls across
62
+ sessions by design; supermemory ignores `sessionId` entirely.
63
+ - **`sdk-memory`'s `truncated` flag was documented as its own inverse**, and its
64
+ dreaming sweep claimed a mutex it never takes against the writer it names.
65
+ - **`sdk-tools`** corrected `run_vitest`'s unreachable `no_vitest` code, `truncation`'s
66
+ replacement-character claim, and two return shapes missing a live error code.
67
+ - **`acp`/`cli`** corrected sixteen statements including a named error class that is
68
+ not the one raised, a handler documented as calling `fork()` that refuses
69
+ unconditionally, handlers described as pure that mint ids and mutate a store, a
70
+ config loader credited to Zod in a package that does not import it, and a `--force`
71
+ scaffold described as atomic that deletes the destination before the rename.
72
+
73
+ Undocumented public symbols were documented across every package, with each claim
74
+ checked against the implementation rather than inferred from the name.
75
+
76
+ - e368fc1: Every published declaration file now compiles without `skipLibCheck` (#345). The
77
+ DTS rollup emitted symbols as a re-export from a chunk while omitting them from
78
+ that chunk's `import`, and dropped type-only imports from external packages —
79
+ leaving 51 unresolved references across ten of the twelve packages. Nothing broke
80
+ at runtime, and `tsc` stayed green for anyone with `skipLibCheck` on, but a
81
+ consumer running type-aware lint saw every type reached through one degrade to
82
+ `error`.
83
+
84
+ The declarations are repaired at build time from the compiler's own diagnostics.
85
+ No source or API change.
86
+
87
+ - 5742ab2: `Handoff.asPlugin(...).register()` now returns a promise that settles once the transfer tools are
88
+ registered.
89
+
90
+ It used to start an unawaited async IIFE and return, so the tools appeared a module-load later —
91
+ whether they existed for the first `send()` depended on timing no caller controlled — and
92
+ `HandoffSelfReferenceError` / `HandoffNameCollisionError` became unhandled rejections that could not
93
+ be caught around `Agent.create`, leaving an agent silently without handoff tools. The plugin
94
+ contract already typed `register` as returning `void | Promise<void>` and the manager already
95
+ awaited it. The import stays lazy.
96
+
97
+ - 33524a5: A handoff now tells the receiving agent what the user asked.
98
+
99
+ Both wirings built the transfer tool through a handler that dispatched with an empty transcript, so
100
+ the receiver was sent the literal string `(Handoff from <sender> — no prior user message in
101
+ history.)` and answered from that plus its own system prompt. The handler now forwards the
102
+ supervisor's transcript, which the SDK hands every tool handler, and the dispatcher takes the last
103
+ user turn from it.
104
+
105
+ `HandoffOptions.inputFilter` was dead in the same way — invoked, always with nothing to filter, so a
106
+ caller who wired a redactor believed the transcript was being scrubbed. It now receives the real
107
+ transcript, and dropping a message there does keep it from the receiver.
108
+
109
+ - 9f0362c: `Handoff.create()` refusals now carry an error class and a stable code.
110
+
111
+ Both of its guards raised a bare error, so a caller wanting to distinguish "no target given" from
112
+ "the target is not an agent" had only the message text to match on — and message text is not a
113
+ contract. It changes whenever someone improves the wording, and nothing tells the consumer their
114
+ check stopped working.
115
+
116
+ Each refusal is now a `ConfigurationError` with its own code. The change is additive rather than
117
+ breaking: the error class extends `Error`, so anything catching `Error` still catches it, and both
118
+ messages are unchanged.
119
+
120
+ - 1484293: The schema converter that decides what a model is shown now has tests.
121
+
122
+ It converts a handoff's input schema into the JSON Schema the model receives, and it had no coverage
123
+ at all. A defect there is not a crash — the model is shown the wrong contract, then fails to satisfy
124
+ it for reasons no error message explains.
125
+
126
+ Eleven tests now pin the emitted schema rather than the fact that a call returned: the required and
127
+ optional split, nested objects and arrays, enums, the primitive type mapping, and both behaviours for
128
+ inputs JSON Schema cannot represent.
129
+
130
+ That last pair is the wrapper's actual reason to exist, and it was verified against the schema library
131
+ rather than taken from the comment describing it: by default an unrepresentable input degrades to an
132
+ empty schema instead of throwing, which is the opposite of what the underlying library does on its
133
+ own. Callers who ask for the strict behaviour still get the library's own error, message included.
134
+
135
+ - c5e78c1: `HandoffOptions.tools` now restricts the receiving agent, as its name always implied.
136
+
137
+ Nothing in the package read it. A caller passing `{ tools: ["read_file"] }` — the option was
138
+ presented as an allowlist — got no restriction and no warning, which is worse than an absent option
139
+ because it gives false assurance. It is wired to `SendOptions.activeTools`, the same
140
+ `withToolWhitelist` path `Agent.fork`'s `allowedTools` uses: exact name matching, an empty list
141
+ means the empty set (fail-closed), and omitting the option imposes no restriction.
142
+
143
+ Local runtime only — a cloud agent ignores `activeTools`, and the docblock now says so.
144
+
145
+ - aadc9dd: Seventeen more negative-case tests now identify which failure they caught, and a registry test suite
146
+ stops sleeping to make timestamps differ.
147
+
148
+ Most of those assertions turned out to be under-asserting rather than untestable: twelve of them sat
149
+ on errors that were **already typed**, and simply checked that something threw. They now name the
150
+ class, the stable code and a message fragment — which means a change that swaps one failure for
151
+ another is caught, where before any error at all satisfied the test.
152
+
153
+ Four remain matched on a message fragment because the underlying error genuinely has no type yet, and
154
+ one of those is filed separately: a public entry point throwing a plain error gives callers nothing to
155
+ branch on but a string that changes whenever someone improves the wording.
156
+
157
+ Four more were reclassified out of scope after reading the source rather than the name: they raise
158
+ errors owned by Node, by the schema library, or by a database driver, and pinning a third-party class
159
+ buys little.
160
+
161
+ Separately, the live-agent-registry tests slept thirteen times — some to force last-used timestamps
162
+ apart so eviction ordering could be asserted, others to let fire-and-forget cleanup finish. Both are
163
+ now driven by the test clock, a mechanism this same file already used elsewhere and which needed no
164
+ production change. The file runs in a fraction of the time and no longer depends on how busy the
165
+ machine is.
166
+
167
+ - 3724b43: The rule that turns an agent's name into a tool-safe slug existed twice — once in `handoff.ts`,
168
+ once in `tool-injector.ts` — byte-identical apart from a parameter name, and covered by no test at
169
+ all. Two copies of one rule drift the moment either is adjusted, and nothing would have reported
170
+ it: a handoff tool named one way and a dispatcher expecting another.
171
+
172
+ It is now one function with tests describing the behaviour it already had: the `agent-` prefix
173
+ stripped case-insensitively, runs of unsafe characters collapsed to one underscore, underscores
174
+ trimmed from both ends, `"anonymous"` when nothing survives, and a 64-character cap. No result
175
+ changes.
176
+
177
+ The input is now bounded before the slug rules run. CodeQL flags one of those expressions as
178
+ polynomial backtracking; stated plainly, the quadratic cost **could not be reproduced** — V8
179
+ resolves 100,000 characters of the worst-case shape in under a millisecond. The bound is defence
180
+ in depth against an engine that does not optimise it, not a fix for a demonstrated exploit, and
181
+ nothing beyond the bound could have reached the 64-character result anyway.
182
+
183
+ - e699569: **The repository moved to the official `usetheokit` organization.** Every `repository`, `bugs` and `homepage` field now points there, along with the README, `CONTRIBUTING.md`, `SECURITY.md` and the issue templates. Existing clones and any URL already published keep working — GitHub redirects a transferred repository permanently — so this is a correctness fix for the metadata npm renders, not a break.
184
+
185
+ **The Apache-2.0 text every package ships was replaced with the official one.** The copy distributed until now had paragraph 4(d) truncated: it read "except as required for describing the origin of the Work and reproducing the content of the NOTICE file", dropping "reasonable and customary use" from the licensed clause. §4(d) governs what a redistributor must do with attribution notices, and the omission narrowed it.
186
+
187
+ That matters more than a typo would. The manifests declare the SPDX identifier `Apache-2.0`, which is an assertion that the terms are _the_ Apache-2.0 terms — a licence scanner resolves the identifier and never reads the file. A consumer's compliance review, which does read the file, would find a body that no longer matches the identifier and has no name of its own. Every `LICENSE` in this repository is now byte-identical to the canonical text, with the appendix filled in.
188
+
189
+ Nothing else about the terms changed: the licence is the same licence it has always been meant to be, and no package changes what it grants.
190
+
191
+ - e3f2a82: `@opentelemetry/api` is now declared as an optional peer dependency, so the spans these two
192
+ packages emit can actually reach a collector.
193
+
194
+ Both lazily `require("@opentelemetry/api")` from their own directory, but neither manifest
195
+ declared it in any dependency field. Under an isolated `node_modules` layout the specifier is
196
+ therefore not linked under the package, the require throws, the loader caches a `null` tracer,
197
+ and every span degrades to a no-op — silently, with no warning, unlike `@theokit/sdk`, which
198
+ prints one when telemetry is enabled and OTel is absent. For `sdk-cache` that covered both of
199
+ its main paths (`cache.lookup` on every send, `cache.store` on every reply), so an operator
200
+ reading a trace saw no cache activity at all and had no way to tell that from a cache that was
201
+ never consulted.
202
+
203
+ The declaration matches `@theokit/sdk`'s: `peerDependencies` plus `peerDependenciesMeta.optional`,
204
+ so nothing is installed for anyone who does not want OTel, and users who do want it get their
205
+ copy linked where the require can find it.
206
+
207
+ - e3f2a82: Every symbol these packages declare in `exports` now reaches the `.d.ts` they publish.
208
+
209
+ Sixty-six declarations across twenty-three published files did not compile, and four entry
210
+ points silently omitted names their own barrel exports — `@theokit/sdk/internal/security`
211
+ dropped seven at once. Runtime was never affected; this is types-only. A consumer with
212
+ `skipLibCheck` on saw nothing, and a consumer running type-aware lint saw every type reached
213
+ through one of them degrade to `error`.
214
+
215
+ The cause was `stripInternal`, which deletes a declaration when the literal `@internal`
216
+ appears in ANY leading comment range of it. The tag was being used here to mean "outside the
217
+ semver contract" — `internal/persistence/sqlite-open.ts` said so in those words, on a subpath
218
+ the manifest publishes and a back-compat test pins. The compiler reads it as "erase this", and
219
+ the two meanings only diverge in the published artifact. It now says the semver exemption in
220
+ prose, and the tag is gone from the symbols that are published.
221
+
222
+ Two further mechanisms had the same cause and a wider blast radius. A tag in a BARREL header
223
+ deleted the first `export … from` beneath it; a tag in a MODULE header deleted the following
224
+ `import`, so `import { z } from "zod"` vanished and every type it bound became
225
+ `Cannot find name`. Nothing was added to any `exports` map and no `export` line changed — a
226
+ deleted import was never privacy, only a broken declaration.
227
+
228
+ `@theokit/sdk-handoff`'s `./internal` entry left `SDKAgent` and `CustomTool` unbound, from a
229
+ different defect: the declaration repair only ever looked at `exports["."]`, so it fixed each
230
+ package's main entry and shipped the rest unrepaired. It now covers every declared subpath, and
231
+ binds the side-effect import form (`import '@theokit/sdk';`) the rollup emits with the names
232
+ stripped out.
233
+
234
+ Three gates were widened or added so this cannot return silently: the declaration typecheck
235
+ now covers all 45 published entries rather than 12, a new export-parity check fails when a
236
+ source barrel exports a name the emit omits, and public-API documentation coverage is gated at
237
+ 100%.
238
+
239
+ Two consequences worth naming rather than discovering. `coerceToKnownAgentRunErrorCode` — the
240
+ boundary helper the 4.x release notes point at as the migration path off the open
241
+ `AgentRunErrorCode` union — was tagged internal and therefore absent from the published types; it
242
+ is now exported and documented, which is a small addition to the public surface. And
243
+ `packages/sdk/typedoc.json` sets `excludeInternal: true`, so the generated API reference gains the
244
+ ~57 symbols whose tags were removed. That is the intended direction: those symbols are published,
245
+ and the reference now says so.
246
+
247
+ - c7385d2: Test runs no longer claim every core on the host.
248
+
249
+ None of the package configs capped `maxWorkers`, so vitest's default applied: `os.availableParallelism()`,
250
+ one fork per core, each booting a full test environment. The repo's `test` script is
251
+ `turbo run test --filter='./packages/*'`, so that default is paid once per package _concurrently_ —
252
+ nproc forks times turbo's concurrency, on nproc cores. Measured on a 12-thread machine during an
253
+ unrelated investigation, two vitest pools alone were enough to reach load average 33.89 with the
254
+ desktop unusable; a full fan-out is several times that.
255
+
256
+ `@theokit/sdk` is the interesting case. B-104 recorded on 2026-08-19 that the `poolOptions.forks.*`
257
+ block was 100% dead in Vitest 4, deleted it, and noted that `fileParallelism: false` was forcing
258
+ `maxWorkers` to 1 unconditionally, so a fork-count knob could not act. B-059 then flipped
259
+ `fileParallelism` to `true` on 2026-08-20, which made the knob able to act again — and nothing
260
+ reintroduced one, so the package silently went back to the uncapped default. That comment has been
261
+ corrected along with the config; it claimed no knob existed, which is no longer true.
262
+
263
+ The cap leaves 4 cores free (`Math.max(2, cpus().length - 4)`), scaling with the runner rather than
264
+ hard-coding one machine's core count. It costs no wall-clock: measured in `theokit-ui`, the full
265
+ suite ran 73.96s at 4 workers against 74.36s at 12, so the parallelism above the cap was already
266
+ noise. Verified as resolved config rather than as file contents — `createVitest` reports
267
+ `maxWorkers: 8` on a 12-thread host, which is the formula, not the default.
268
+
269
+ This changes no published behaviour; it is test tooling only. Refs usetheokit/theokit-ui#51.
270
+
3
271
  ## 0.1.2
4
272
 
5
273
  ### Patch Changes
package/LICENSE CHANGED
@@ -137,8 +137,8 @@
137
137
 
138
138
  6. Trademarks. This License does not grant permission to use the trade
139
139
  names, trademarks, service marks, or product names of the Licensor,
140
- except as required for describing the origin of the Work and
141
- reproducing the content of the NOTICE file.
140
+ except as required for reasonable and customary use in describing the
141
+ origin of the Work and reproducing the content of the NOTICE file.
142
142
 
143
143
  7. Disclaimer of Warranty. Unless required by applicable law or
144
144
  agreed to in writing, Licensor provides the Work (and each
package/README.md CHANGED
@@ -91,6 +91,19 @@ const support = await Agent.create({
91
91
 
92
92
  See the monorepo `CHANGELOG.md` for the 1.x → 2.0 package-split migration notes.
93
93
 
94
+ ## API reference
95
+
96
+ Every symbol this package exports, with the exact specifier to import it from, is in the generated
97
+ capability map that ships inside `@theokit/sdk`:
98
+
99
+ ```
100
+ node_modules/@theokit/sdk/docs/harness-capability-map.md # symbol -> import specifier
101
+ node_modules/@theokit/sdk/docs/error-codes.md # every `code` an error can carry
102
+ ```
103
+
104
+ Both are generated from the built type declarations, so they describe the version you installed
105
+ rather than the version someone wrote a page about.
106
+
94
107
  ## License
95
108
 
96
109
  Apache-2.0.
@@ -0,0 +1,255 @@
1
+ import { SDKAgent } from '@theokit/sdk';
2
+ import { ZodType } from 'zod';
3
+
4
+ /**
5
+ * Type-leaf — `HandoffDescriptor` extracted as a generic over `TAgent` so
6
+ * neither `agent.ts` nor `handoff.ts` need to import the other for type
7
+ * resolution. Closes the audit's last LOW type-only cycle #4
8
+ * (`types/agent.ts ↔ types/handoff.ts`) per plan
9
+ * arch-review-fixes-2026-06-06 § Phase 4 / T4.1 follow-up.
10
+ *
11
+ * BREAKING (per user direction "sem retro compat"): `HandoffDescriptor`
12
+ * gained a second generic parameter `TAgent` for the target shape. Existing
13
+ * consumers using `HandoffDescriptor<MyInput>` now resolve to
14
+ * `HandoffDescriptor<MyInput, SDKAgent>` via the back-compat default in
15
+ * `handoff.ts`'s re-export.
16
+ *
17
+ * @public
18
+ */
19
+
20
+ /**
21
+ * Read-only snapshot of a handoff about to happen, passed to `onHandoff` and `isEnabled`.
22
+ *
23
+ * @public
24
+ */
25
+ interface HandoffContext {
26
+ /** The `parentAgentId` the wiring was built with — `"anonymous"` when it was left unset. */
27
+ readonly senderAgentId: string;
28
+ /** `agentId` of the target about to receive the conversation. */
29
+ readonly receiverAgentId: string;
30
+ /**
31
+ * Hops recorded so far in this dispatch. Because chain state is rebuilt per tool invocation, it
32
+ * is `1` for essentially every real handoff — it is not a running total across a `send()`.
33
+ */
34
+ readonly currentDepth: number;
35
+ /**
36
+ * Agent ids traversed, ending with the receiver this context describes. Same caveat as
37
+ * `currentDepth`: it covers this dispatch, not the whole conversation.
38
+ */
39
+ readonly chain: ReadonlyArray<string>;
40
+ }
41
+ /**
42
+ * The transcript wrapper passed to `inputFilter`. `messages` is widened to
43
+ * `unknown[]` so this type doesn't import from `messages.ts` (avoids cycle
44
+ * — implementations cast to `SDKMessage[]` internally).
45
+ *
46
+ * Always arrives EMPTY today — see {@link HandoffOptions.inputFilter}.
47
+ *
48
+ * @public
49
+ */
50
+ interface HandoffHistory {
51
+ readonly messages: ReadonlyArray<unknown>;
52
+ }
53
+ /**
54
+ * Options accepted by `Handoff.create(target, opts?)`.
55
+ *
56
+ * @public
57
+ */
58
+ interface HandoffOptions<TInput extends ZodType = ZodType> {
59
+ /**
60
+ * Name of the synthetic tool, replacing the derived `transfer_to_<slug>`.
61
+ *
62
+ * Taken verbatim — it is not validated against the provider's tool-name rules, and it is what the
63
+ * collision check compares, so two targets sharing an explicit name still collide.
64
+ */
65
+ readonly toolName?: string;
66
+ /**
67
+ * Description shown to the model, replacing the generic "Transfer the conversation to the
68
+ * `<agentId>` agent." This is the ONLY thing telling the model when to pick this target over its
69
+ * siblings, so a default description in a multi-target setup routes badly.
70
+ */
71
+ readonly toolDescription?: string;
72
+ /**
73
+ * Called after the input is parsed and BEFORE the receiver is invoked. `parsed` is whatever
74
+ * `inputType` produced, or `undefined` when no `inputType` was set.
75
+ *
76
+ * Awaited, and NOT isolated: throwing from here aborts the handoff, which is the supported way to
77
+ * veto one after inspecting the arguments. Use it for logging and audit; the transfer has not
78
+ * happened yet.
79
+ */
80
+ readonly onHandoff?: (ctx: HandoffContext, parsed: TInput extends ZodType ? unknown : undefined) => void | Promise<void>;
81
+ /**
82
+ * Zod schema for the arguments the model must supply, replacing the default
83
+ * `{ reason?: string }`. It becomes the tool's JSON Schema, so it is also how you ask the model
84
+ * for structured routing data.
85
+ *
86
+ * Parsed with `.parse()`, so a rejection aborts the handoff. Note the parsed value is used ONLY
87
+ * for `onHandoff` and for lifting a `reason` field into telemetry — it is NOT forwarded to the
88
+ * receiver, which sees only the message text.
89
+ */
90
+ readonly inputType?: TInput;
91
+ /**
92
+ * Hook to rewrite the transcript before the receiver sees it — the intended place for redaction.
93
+ *
94
+ * It receives the supervisor's transcript as the tool handler saw it, and the receiver is sent
95
+ * the last user turn SURVIVING this filter — so dropping a message here does keep it from the
96
+ * receiver. Until #354 both wirings passed `{ messages: [] }`, which made this hook a no-op that
97
+ * looked like redaction.
98
+ *
99
+ * Failures are swallowed: a throw falls back to the unfiltered history with one warning on
100
+ * stderr per process — so a broken redactor fails OPEN, not closed.
101
+ */
102
+ readonly inputFilter?: (history: HandoffHistory) => HandoffHistory | Promise<HandoffHistory>;
103
+ /**
104
+ * Restrict the receiving agent, for THIS handoff only, to the tools named here.
105
+ *
106
+ * Wired to `SendOptions.activeTools`: names match EXACTLY against the receiver's registered tool
107
+ * names, an empty list restricts to the empty set (fail-closed), and omitting the option imposes
108
+ * no restriction. It narrows what the receiver may call; it never grants a tool the receiver
109
+ * does not have.
110
+ *
111
+ * LOCAL RUNTIME ONLY — a cloud agent ignores `activeTools`, so this cannot restrict one. Before
112
+ * #356 nothing read this field at all, in either runtime.
113
+ */
114
+ readonly tools?: ReadonlyArray<string>;
115
+ /**
116
+ * Gate on this handoff, as a boolean or a predicate evaluated at dispatch time with the same
117
+ * `ctx` that `onHandoff` receives.
118
+ *
119
+ * `false` does NOT hide the tool from the model — the tool is still registered and still
120
+ * offered; the dispatch simply fails with `Handoff to <id> is disabled (isEnabled returned
121
+ * false)`, which the tool wiring hands back as a failed tool result. To remove a target from the
122
+ * model's view, leave it out of `targets`.
123
+ */
124
+ readonly isEnabled?: boolean | ((ctx: HandoffContext) => boolean | Promise<boolean>);
125
+ }
126
+ /**
127
+ * Public `Handoff` shape — what `Handoff.create()` returns. Read-only
128
+ * accessors only; behavior lives in the engine.
129
+ *
130
+ * Generic over `TAgent` so this leaf has no dependency on a concrete
131
+ * agent type. Consumers typically import the convenience alias
132
+ * `HandoffDescriptor<TInput>` from `@theokit/sdk` which fixes `TAgent`
133
+ * to `SDKAgent`.
134
+ *
135
+ * @public
136
+ */
137
+ interface HandoffDescriptor$1<TInput extends ZodType = ZodType, TAgent = unknown> {
138
+ readonly target: TAgent;
139
+ readonly options: HandoffOptions<TInput>;
140
+ /** Resolved tool name (after applying toolName override or default `transfer_to_<receiver>`). */
141
+ readonly resolvedToolName: string;
142
+ }
143
+
144
+ /**
145
+ * Public types for `Agent.create({ handoffs })` + `Handoff.create()` +
146
+ * `Agent.handoffTo()` (Adoption Roadmap #4; ADRs D214-D229).
147
+ *
148
+ * Pattern: handoff-as-tool. Each handoff destination becomes a synthetic
149
+ * `transfer_to_<receiver>` function tool exposed to the LLM. Runtime
150
+ * intercepts the tool call and routes the next turn to the receiver.
151
+ *
152
+ * T4.1 follow-up (cycle #4 closed): `HandoffDescriptor` + its leaf-friendly
153
+ * sibling types now live in `./handoff-descriptor.ts` (generic over
154
+ * `TAgent`). This module re-exports the leaf types pinned to `SDKAgent`,
155
+ * keeps the runtime error classes, and removes the back-edge to `agent.ts`.
156
+ *
157
+ * @public
158
+ */
159
+
160
+ /**
161
+ * What `Handoff.create` returns: a target plus its options plus the resolved tool name.
162
+ *
163
+ * Pinned to `SDKAgent` — the back-compat shape for callers who imported
164
+ * `import type { HandoffDescriptor } from "@theokit/sdk"` before the T4.1 follow-up. It is a plain
165
+ * data record: constructing one by hand works, and skips the target validation `Handoff.create`
166
+ * performs.
167
+ *
168
+ * @public
169
+ */
170
+ type HandoffDescriptor<TInput extends ZodType = ZodType> = HandoffDescriptor$1<TInput, SDKAgent>;
171
+ /**
172
+ * Thrown when a chain exceeds `maxHandoffDepth` (default 5). `depth` is the CAP that was exceeded,
173
+ * not the depth reached; `chain` is the full path of agent ids.
174
+ *
175
+ * Rare in practice: chain state is rebuilt per dispatch, so depth restarts at 1 on every tool call.
176
+ * Repeated ping-pong surfaces as {@link HandoffPairLoopError} instead. *
177
+ * WHERE YOU SEE IT: only when you drive a handoff yourself, via `handoffTo(...)`. In the
178
+ * tool-based wirings (`Handoff.asPlugin` / `Agent.create({ handoffs })`) the handler catches every
179
+ * error and hands the MODEL a `{"ok":false,"error":"<name>","message":"…"}` tool result, so this
180
+ * class is observable there only as that `error` string.
181
+ */
182
+ declare class HandoffLoopError extends Error {
183
+ readonly name = "HandoffLoopError";
184
+ readonly depth: number;
185
+ readonly chain: ReadonlyArray<string>;
186
+ constructor(depth: number, chain: ReadonlyArray<string>);
187
+ }
188
+ /**
189
+ * Thrown when the same `sender -> receiver` pair fires twice inside one dispatch — the ping-pong
190
+ * guard, and the loop protection that actually fires in practice.
191
+ *
192
+ * A -> B -> A is allowed by this check (different pairs); a repeated A -> B is not. *
193
+ * WHERE YOU SEE IT: only when you drive a handoff yourself, via `handoffTo(...)`. In the
194
+ * tool-based wirings (`Handoff.asPlugin` / `Agent.create({ handoffs })`) the handler catches every
195
+ * error and hands the MODEL a `{"ok":false,"error":"<name>","message":"…"}` tool result, so this
196
+ * class is observable there only as that `error` string.
197
+ */
198
+ declare class HandoffPairLoopError extends Error {
199
+ readonly name = "HandoffPairLoopError";
200
+ readonly senderAgentId: string;
201
+ readonly receiverAgentId: string;
202
+ constructor(senderAgentId: string, receiverAgentId: string);
203
+ }
204
+ /**
205
+ * Thrown when a target's `agentId` equals the parent's — self-handoff, which recurses forever.
206
+ *
207
+ * Compared against `parentAgentId` as a STRING, which defaults to `"anonymous"` in
208
+ * `Handoff.asPlugin`: leave it unset and a genuine self-reference goes undetected.
209
+ *
210
+ * Raised while the target list is normalised, which in `Handoff.asPlugin` happens inside an
211
+ * unawaited async registration — it arrives as an unhandled rejection there, not as a throw from
212
+ * `Agent.create`.
213
+ */
214
+ declare class HandoffSelfReferenceError extends Error {
215
+ readonly name = "HandoffSelfReferenceError";
216
+ readonly agentId: string;
217
+ constructor(agentId: string);
218
+ }
219
+ /**
220
+ * Thrown when the target agent was disposed before the handoff reached it — detected at dispatch
221
+ * time, since nothing unregisters the tool when an agent is disposed.
222
+ *
223
+ * Typical cause: the receiver was created in a narrower scope than the sender and cleaned up first. *
224
+ * WHERE YOU SEE IT: only when you drive a handoff yourself, via `handoffTo(...)`. In the
225
+ * tool-based wirings (`Handoff.asPlugin` / `Agent.create({ handoffs })`) the handler catches every
226
+ * error and hands the MODEL a `{"ok":false,"error":"<name>","message":"…"}` tool result, so this
227
+ * class is observable there only as that `error` string.
228
+ */
229
+ declare class HandoffReceiverDisposedError extends Error {
230
+ readonly name = "HandoffReceiverDisposedError";
231
+ readonly receiverAgentId: string;
232
+ constructor(receiverAgentId: string);
233
+ }
234
+ /**
235
+ * Thrown when two targets of the same parent resolve to the same `transfer_to_*` name — the model
236
+ * would have no way to pick between them.
237
+ *
238
+ * Easy to hit without duplicate agents, but not in the way the folding rule suggests: `-` and `_`
239
+ * SURVIVE the slug, and only runs of other characters fold to a single `_`, which is then trimmed
240
+ * at both ends. So `"billing EU"`, `"billing_EU"`, `"billing.EU"` and `"billing (EU)"` all resolve
241
+ * to `transfer_to_billing_EU` and collide — the last one because the `_` left by the closing paren
242
+ * is trimmed off the end. `"billing-EU"` keeps its hyphen, resolves to `transfer_to_billing-EU`,
243
+ * and collides with none of them. Set `toolName` on one of the colliding pair.
244
+ *
245
+ * Raised while the target list is normalised, which in `Handoff.asPlugin` happens inside an
246
+ * unawaited async registration — it arrives as an unhandled rejection there, not as a throw from
247
+ * `Agent.create`.
248
+ */
249
+ declare class HandoffNameCollisionError extends Error {
250
+ readonly name = "HandoffNameCollisionError";
251
+ readonly conflictingName: string;
252
+ constructor(conflictingName: string);
253
+ }
254
+
255
+ export { type HandoffDescriptor as H, type HandoffOptions as a, HandoffLoopError as b, HandoffNameCollisionError as c, HandoffPairLoopError as d, HandoffReceiverDisposedError as e, HandoffSelfReferenceError as f };