@theokit/sdk-handoff 0.1.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 +25 -0
- package/LICENSE +201 -0
- package/README.md +96 -0
- package/dist/handoff-D-Ujv-lA.d.cts +131 -0
- package/dist/handoff-D-Ujv-lA.d.ts +131 -0
- package/dist/index.cjs +491 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +84 -0
- package/dist/index.d.ts +84 -0
- package/dist/index.js +486 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/tool-injector.cjs +339 -0
- package/dist/internal/tool-injector.cjs.map +1 -0
- package/dist/internal/tool-injector.d.cts +38 -0
- package/dist/internal/tool-injector.d.ts +38 -0
- package/dist/internal/tool-injector.js +335 -0
- package/dist/internal/tool-injector.js.map +1 -0
- package/package.json +76 -0
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { z, toJSONSchema } from 'zod';
|
|
2
|
+
import { createRequire } from 'module';
|
|
3
|
+
|
|
4
|
+
// src/internal/tool-injector.ts
|
|
5
|
+
|
|
6
|
+
// src/types/handoff.ts
|
|
7
|
+
var HandoffLoopError = class extends Error {
|
|
8
|
+
name = "HandoffLoopError";
|
|
9
|
+
depth;
|
|
10
|
+
chain;
|
|
11
|
+
constructor(depth, chain) {
|
|
12
|
+
super(
|
|
13
|
+
`Handoff loop exceeded max depth ${depth}. Chain: ${chain.join(" -> ")}. Use Agent.create({ maxHandoffDepth: N }) to raise the cap.`
|
|
14
|
+
);
|
|
15
|
+
this.depth = depth;
|
|
16
|
+
this.chain = chain;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var HandoffPairLoopError = class extends Error {
|
|
20
|
+
name = "HandoffPairLoopError";
|
|
21
|
+
senderAgentId;
|
|
22
|
+
receiverAgentId;
|
|
23
|
+
constructor(senderAgentId, receiverAgentId) {
|
|
24
|
+
super(
|
|
25
|
+
`Handoff loop: ${senderAgentId} -> ${receiverAgentId} already invoked in this send() call. Likely a ping-pong loop; revisit your handoff conditions.`
|
|
26
|
+
);
|
|
27
|
+
this.senderAgentId = senderAgentId;
|
|
28
|
+
this.receiverAgentId = receiverAgentId;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var HandoffSelfReferenceError = class extends Error {
|
|
32
|
+
name = "HandoffSelfReferenceError";
|
|
33
|
+
agentId;
|
|
34
|
+
constructor(agentId) {
|
|
35
|
+
super(
|
|
36
|
+
`Agent "${agentId}" has a self-reference in its handoffs[]. Self-handoff causes infinite recursion; introduce a sibling agent for re-entry.`
|
|
37
|
+
);
|
|
38
|
+
this.agentId = agentId;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
var HandoffReceiverDisposedError = class extends Error {
|
|
42
|
+
name = "HandoffReceiverDisposedError";
|
|
43
|
+
receiverAgentId;
|
|
44
|
+
constructor(receiverAgentId) {
|
|
45
|
+
super(
|
|
46
|
+
`Handoff target agent "${receiverAgentId}" is disposed. Don't dispose receivers while their parent is still active.`
|
|
47
|
+
);
|
|
48
|
+
this.receiverAgentId = receiverAgentId;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
var HandoffNameCollisionError = class extends Error {
|
|
52
|
+
name = "HandoffNameCollisionError";
|
|
53
|
+
conflictingName;
|
|
54
|
+
constructor(conflictingName) {
|
|
55
|
+
super(
|
|
56
|
+
`Two handoffs share the same tool name "${conflictingName}". Set { toolName } on at least one of them to disambiguate.`
|
|
57
|
+
);
|
|
58
|
+
this.conflictingName = conflictingName;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// src/internal/registry.ts
|
|
63
|
+
function createChainState(rootAgentId, maxDepth) {
|
|
64
|
+
return {
|
|
65
|
+
chain: [rootAgentId],
|
|
66
|
+
seenPairs: /* @__PURE__ */ new Set(),
|
|
67
|
+
maxDepth
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function recordHop(state, senderAgentId, receiverAgentId) {
|
|
71
|
+
const pairKey = `${senderAgentId}->${receiverAgentId}`;
|
|
72
|
+
if (state.seenPairs.has(pairKey)) {
|
|
73
|
+
throw new HandoffPairLoopError(senderAgentId, receiverAgentId);
|
|
74
|
+
}
|
|
75
|
+
state.seenPairs.add(pairKey);
|
|
76
|
+
state.chain.push(receiverAgentId);
|
|
77
|
+
const depth = state.chain.length - 1;
|
|
78
|
+
if (depth > state.maxDepth) {
|
|
79
|
+
throw new HandoffLoopError(state.maxDepth, [...state.chain]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
var tracerCache = /* @__PURE__ */ new Map();
|
|
83
|
+
function getTracer(name, version = "1.0.0") {
|
|
84
|
+
const cached = tracerCache.get(name);
|
|
85
|
+
if (cached !== void 0) return cached.tracer ?? void 0;
|
|
86
|
+
try {
|
|
87
|
+
const r = createRequire(import.meta.url);
|
|
88
|
+
const otel = r("@opentelemetry/api");
|
|
89
|
+
if (otel.trace?.getTracer === void 0) {
|
|
90
|
+
tracerCache.set(name, { tracer: null });
|
|
91
|
+
return void 0;
|
|
92
|
+
}
|
|
93
|
+
const tracer = otel.trace.getTracer(name, version);
|
|
94
|
+
tracerCache.set(name, { tracer });
|
|
95
|
+
return tracer;
|
|
96
|
+
} catch {
|
|
97
|
+
tracerCache.set(name, { tracer: null });
|
|
98
|
+
return void 0;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
var TRACER_NAME = "theokit-sdk-handoff";
|
|
102
|
+
var NOOP = { setAttribute: () => void 0, end: () => void 0 };
|
|
103
|
+
function safe(fn, fallback) {
|
|
104
|
+
try {
|
|
105
|
+
return fn();
|
|
106
|
+
} catch {
|
|
107
|
+
return fallback;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
function startHandoffSpan(attrs) {
|
|
111
|
+
const tracer = getTracer(TRACER_NAME);
|
|
112
|
+
if (tracer === void 0) return NOOP;
|
|
113
|
+
const span = safe(
|
|
114
|
+
() => tracer.startSpan("handoff.transfer", {
|
|
115
|
+
attributes: {
|
|
116
|
+
"handoff.from": attrs.from,
|
|
117
|
+
"handoff.to": attrs.to,
|
|
118
|
+
"handoff.reason": attrs.reason,
|
|
119
|
+
"handoff.depth": attrs.depth,
|
|
120
|
+
"handoff.tool_name": attrs.toolName
|
|
121
|
+
}
|
|
122
|
+
}),
|
|
123
|
+
void 0
|
|
124
|
+
);
|
|
125
|
+
if (span === void 0) return NOOP;
|
|
126
|
+
return {
|
|
127
|
+
setAttribute: (k, v) => safe(() => span.setAttribute(k, v), void 0),
|
|
128
|
+
end: () => safe(() => span.end(), void 0)
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// src/internal/dispatcher.ts
|
|
133
|
+
var warnedFilterOnce = false;
|
|
134
|
+
async function safeFilter(filter, history) {
|
|
135
|
+
if (filter === void 0) return history;
|
|
136
|
+
try {
|
|
137
|
+
const result = filter(history);
|
|
138
|
+
return result instanceof Promise ? await result : result;
|
|
139
|
+
} catch (err) {
|
|
140
|
+
if (!warnedFilterOnce) {
|
|
141
|
+
warnedFilterOnce = true;
|
|
142
|
+
process.stderr.write(
|
|
143
|
+
`[handoff] inputFilter threw, falling back to full history: ${err instanceof Error ? err.message : String(err)}
|
|
144
|
+
`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return history;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function parseHandoffInput(descriptor, raw) {
|
|
151
|
+
const inputType = descriptor.options.inputType;
|
|
152
|
+
if (inputType === void 0) return void 0;
|
|
153
|
+
const candidate = raw === null || raw === void 0 ? {} : raw;
|
|
154
|
+
return inputType.parse(candidate);
|
|
155
|
+
}
|
|
156
|
+
function isAgentDisposed(agent) {
|
|
157
|
+
const maybe = agent;
|
|
158
|
+
return maybe.disposed === true;
|
|
159
|
+
}
|
|
160
|
+
async function assertHandoffEnabled(descriptor, ctx, receiverAgentId) {
|
|
161
|
+
const opt = descriptor.options.isEnabled;
|
|
162
|
+
let enabled = true;
|
|
163
|
+
if (typeof opt === "boolean") enabled = opt;
|
|
164
|
+
else if (typeof opt === "function") {
|
|
165
|
+
const r = opt(ctx);
|
|
166
|
+
enabled = r instanceof Promise ? await r : r;
|
|
167
|
+
}
|
|
168
|
+
if (!enabled) {
|
|
169
|
+
throw new Error(`Handoff to ${receiverAgentId} is disabled (isEnabled returned false)`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function parseAndValidate(descriptor, rawInputJson) {
|
|
173
|
+
try {
|
|
174
|
+
return parseHandoffInput(descriptor, rawInputJson);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
const detail = err instanceof z.ZodError ? err.issues[0]?.message ?? "schema_invalid" : err instanceof Error ? err.message : String(err);
|
|
177
|
+
throw new Error(`Handoff input validation failed: ${detail}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
async function runOnHandoff(descriptor, ctx, parsedInput) {
|
|
181
|
+
const onHandoff = descriptor.options.onHandoff;
|
|
182
|
+
if (onHandoff === void 0) return;
|
|
183
|
+
const result = onHandoff(ctx, parsedInput);
|
|
184
|
+
if (result instanceof Promise) await result;
|
|
185
|
+
}
|
|
186
|
+
function extractUserText(content) {
|
|
187
|
+
if (typeof content === "string") return content;
|
|
188
|
+
if (!Array.isArray(content)) return void 0;
|
|
189
|
+
const text = content.filter((c) => c?.type === "text").map((c) => c.text).join("\n");
|
|
190
|
+
return text.length > 0 ? text : void 0;
|
|
191
|
+
}
|
|
192
|
+
function extractLastUserMessage(history, senderAgentId) {
|
|
193
|
+
for (let i = history.messages.length - 1; i >= 0; i -= 1) {
|
|
194
|
+
const m = history.messages[i];
|
|
195
|
+
if (m?.type !== "user" || m.message?.role !== "user") continue;
|
|
196
|
+
const text = extractUserText(m.message.content);
|
|
197
|
+
if (text !== void 0) return text;
|
|
198
|
+
}
|
|
199
|
+
return `(Handoff from ${senderAgentId} \u2014 no prior user message in history.)`;
|
|
200
|
+
}
|
|
201
|
+
async function dispatchHandoff(args) {
|
|
202
|
+
const { descriptor, senderAgentId, chainState, rawInputJson, history, messageOverride } = args;
|
|
203
|
+
const receiver = descriptor.target;
|
|
204
|
+
if (isAgentDisposed(receiver)) {
|
|
205
|
+
throw new HandoffReceiverDisposedError(receiver.agentId);
|
|
206
|
+
}
|
|
207
|
+
const depthAfterThisHop = chainState.chain.length;
|
|
208
|
+
const ctx = {
|
|
209
|
+
senderAgentId,
|
|
210
|
+
receiverAgentId: receiver.agentId,
|
|
211
|
+
currentDepth: depthAfterThisHop,
|
|
212
|
+
chain: [...chainState.chain, receiver.agentId]
|
|
213
|
+
};
|
|
214
|
+
await assertHandoffEnabled(descriptor, ctx, receiver.agentId);
|
|
215
|
+
const parsedInput = parseAndValidate(descriptor, rawInputJson);
|
|
216
|
+
await runOnHandoff(descriptor, ctx, parsedInput);
|
|
217
|
+
const filteredHistory = await safeFilter(descriptor.options.inputFilter, history);
|
|
218
|
+
recordHop(chainState, senderAgentId, receiver.agentId);
|
|
219
|
+
const lastUserMessage = messageOverride ?? extractLastUserMessage(filteredHistory, senderAgentId);
|
|
220
|
+
const reason = extractReason(parsedInput);
|
|
221
|
+
const span = startHandoffSpan({
|
|
222
|
+
from: senderAgentId,
|
|
223
|
+
to: receiver.agentId,
|
|
224
|
+
reason,
|
|
225
|
+
depth: depthAfterThisHop,
|
|
226
|
+
toolName: descriptor.resolvedToolName
|
|
227
|
+
});
|
|
228
|
+
try {
|
|
229
|
+
const run = await receiver.send(lastUserMessage);
|
|
230
|
+
const result = await run.wait();
|
|
231
|
+
const reply = buildReply(result, receiver.agentId);
|
|
232
|
+
return {
|
|
233
|
+
reply,
|
|
234
|
+
result: {
|
|
235
|
+
from: senderAgentId,
|
|
236
|
+
to: receiver.agentId,
|
|
237
|
+
depth: depthAfterThisHop,
|
|
238
|
+
toolName: descriptor.resolvedToolName,
|
|
239
|
+
...reason !== "" ? { reasonFromLlm: reason } : {}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
} finally {
|
|
243
|
+
span.end();
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
function extractReason(parsedInput) {
|
|
247
|
+
if (typeof parsedInput !== "object" || parsedInput === null) return "";
|
|
248
|
+
if (!("reason" in parsedInput)) return "";
|
|
249
|
+
return String(parsedInput.reason ?? "");
|
|
250
|
+
}
|
|
251
|
+
function buildReply(result, receiverAgentId) {
|
|
252
|
+
if (result.status === "finished" && result.result !== void 0) return result.result;
|
|
253
|
+
const suffix = result.error !== void 0 ? `: ${result.error.message}` : "";
|
|
254
|
+
return `(Handoff target ${receiverAgentId} returned status=${result.status}${suffix})`;
|
|
255
|
+
}
|
|
256
|
+
function toJsonSchema(schema, options = { unrepresentable: "any" }) {
|
|
257
|
+
return toJSONSchema(schema, options);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// src/internal/tool-injector.ts
|
|
261
|
+
function normalizeHandoffs(parentAgentId, entries) {
|
|
262
|
+
if (entries.length === 0) return [];
|
|
263
|
+
const out = [];
|
|
264
|
+
const seenNames = /* @__PURE__ */ new Set();
|
|
265
|
+
for (const entry of entries) {
|
|
266
|
+
const isDescriptor = typeof entry === "object" && entry !== null && "target" in entry && "options" in entry && "resolvedToolName" in entry;
|
|
267
|
+
const descriptor = isDescriptor ? entry : autoWrap(entry);
|
|
268
|
+
if (descriptor.target.agentId === parentAgentId) {
|
|
269
|
+
throw new HandoffSelfReferenceError(parentAgentId);
|
|
270
|
+
}
|
|
271
|
+
const name = descriptor.resolvedToolName;
|
|
272
|
+
if (seenNames.has(name)) {
|
|
273
|
+
throw new HandoffNameCollisionError(name);
|
|
274
|
+
}
|
|
275
|
+
seenNames.add(name);
|
|
276
|
+
out.push({ descriptor });
|
|
277
|
+
}
|
|
278
|
+
return out;
|
|
279
|
+
}
|
|
280
|
+
function autoWrap(agent) {
|
|
281
|
+
const name = resolveTargetName(agent);
|
|
282
|
+
return {
|
|
283
|
+
target: agent,
|
|
284
|
+
options: {},
|
|
285
|
+
resolvedToolName: `transfer_to_${name}`
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function resolveTargetName(agent) {
|
|
289
|
+
const candidate = agent.name ?? agent.agentId ?? "anonymous";
|
|
290
|
+
return slugify(candidate);
|
|
291
|
+
}
|
|
292
|
+
function slugify(input) {
|
|
293
|
+
return input.replace(/^agent-/i, "").replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64) || "anonymous";
|
|
294
|
+
}
|
|
295
|
+
function buildHandoffTool(parentAgentId, descriptor, maxHandoffDepth) {
|
|
296
|
+
const description = descriptor.options.toolDescription ?? `Transfer the conversation to the ${descriptor.target.agentId} agent. Use this when the user's request matches their specialty.`;
|
|
297
|
+
const inputZod = descriptor.options.inputType ?? z.object({
|
|
298
|
+
reason: z.string().optional().describe("Brief reason for the transfer (one short sentence).")
|
|
299
|
+
});
|
|
300
|
+
const inputSchema = toJsonSchema(inputZod);
|
|
301
|
+
return {
|
|
302
|
+
name: descriptor.resolvedToolName,
|
|
303
|
+
description,
|
|
304
|
+
inputSchema,
|
|
305
|
+
handler: async (input) => {
|
|
306
|
+
const chainState = createChainState(parentAgentId, maxHandoffDepth);
|
|
307
|
+
try {
|
|
308
|
+
const { reply, result } = await dispatchHandoff({
|
|
309
|
+
descriptor,
|
|
310
|
+
senderAgentId: parentAgentId,
|
|
311
|
+
chainState,
|
|
312
|
+
rawInputJson: input,
|
|
313
|
+
history: { messages: [] }
|
|
314
|
+
// v1: history replay deferred
|
|
315
|
+
});
|
|
316
|
+
return JSON.stringify({
|
|
317
|
+
ok: true,
|
|
318
|
+
transferred_to: result.to,
|
|
319
|
+
depth: result.depth,
|
|
320
|
+
reply
|
|
321
|
+
});
|
|
322
|
+
} catch (err) {
|
|
323
|
+
return JSON.stringify({
|
|
324
|
+
ok: false,
|
|
325
|
+
error: err instanceof Error ? err.name : "HandoffError",
|
|
326
|
+
message: err instanceof Error ? err.message : String(err)
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export { buildHandoffTool, normalizeHandoffs };
|
|
334
|
+
//# sourceMappingURL=tool-injector.js.map
|
|
335
|
+
//# sourceMappingURL=tool-injector.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/types/handoff.ts","../../src/internal/registry.ts","../../src/internal/telemetry.ts","../../src/internal/dispatcher.ts","../../src/internal/to-json-schema.ts","../../src/internal/tool-injector.ts"],"names":["z"],"mappings":"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","file":"tool-injector.js","sourcesContent":["/**\n * Public types for `Agent.create({ handoffs })` + `Handoff.create()` +\n * `Agent.handoffTo()` (Adoption Roadmap #4; ADRs D214-D229).\n *\n * Pattern: handoff-as-tool. Each handoff destination becomes a synthetic\n * `transfer_to_<receiver>` function tool exposed to the LLM. Runtime\n * intercepts the tool call and routes the next turn to the receiver.\n *\n * T4.1 follow-up (cycle #4 closed): `HandoffDescriptor` + its leaf-friendly\n * sibling types now live in `./handoff-descriptor.ts` (generic over\n * `TAgent`). This module re-exports the leaf types pinned to `SDKAgent`,\n * keeps the runtime error classes, and removes the back-edge to `agent.ts`.\n *\n * @public\n */\n\nimport type { SDKAgent } from \"@theokit/sdk\";\nimport type { ZodType } from \"zod\";\nimport type {\n HandoffContext,\n HandoffDescriptor as HandoffDescriptorGeneric,\n HandoffHistory,\n HandoffOptions,\n HandoffResult,\n} from \"./handoff-descriptor.js\";\n\nexport type { HandoffContext, HandoffHistory, HandoffOptions, HandoffResult };\n\n/**\n * `HandoffDescriptor` pinned to `SDKAgent` — back-compat shape for callers\n * that imported `import type { HandoffDescriptor } from \"@theokit/sdk\"`\n * before T4.1 follow-up.\n *\n * @public\n */\nexport type HandoffDescriptor<TInput extends ZodType = ZodType> = HandoffDescriptorGeneric<\n TInput,\n SDKAgent\n>;\n\n/** Throw when handoff depth exceeds `maxHandoffDepth` (default 5; D218). */\nexport class HandoffLoopError extends Error {\n override readonly name = \"HandoffLoopError\";\n readonly depth: number;\n readonly chain: ReadonlyArray<string>;\n constructor(depth: number, chain: ReadonlyArray<string>) {\n super(\n `Handoff loop exceeded max depth ${depth}. Chain: ${chain.join(\" -> \")}. ` +\n `Use Agent.create({ maxHandoffDepth: N }) to raise the cap.`,\n );\n this.depth = depth;\n this.chain = chain;\n }\n}\n\n/** Throw when the same (sender, receiver) pair invoked twice in one send() (D221). */\nexport class HandoffPairLoopError extends Error {\n override readonly name = \"HandoffPairLoopError\";\n readonly senderAgentId: string;\n readonly receiverAgentId: string;\n constructor(senderAgentId: string, receiverAgentId: string) {\n super(\n `Handoff loop: ${senderAgentId} -> ${receiverAgentId} already invoked in this send() call. ` +\n `Likely a ping-pong loop; revisit your handoff conditions.`,\n );\n this.senderAgentId = senderAgentId;\n this.receiverAgentId = receiverAgentId;\n }\n}\n\n/** Throw when an agent's `handoffs[]` includes a self-reference (EC-6). */\nexport class HandoffSelfReferenceError extends Error {\n override readonly name = \"HandoffSelfReferenceError\";\n readonly agentId: string;\n constructor(agentId: string) {\n super(\n `Agent \"${agentId}\" has a self-reference in its handoffs[]. ` +\n `Self-handoff causes infinite recursion; introduce a sibling agent for re-entry.`,\n );\n this.agentId = agentId;\n }\n}\n\n/** Throw when receiver is disposed at dispatch time (EC-5). */\nexport class HandoffReceiverDisposedError extends Error {\n override readonly name = \"HandoffReceiverDisposedError\";\n readonly receiverAgentId: string;\n constructor(receiverAgentId: string) {\n super(\n `Handoff target agent \"${receiverAgentId}\" is disposed. ` +\n `Don't dispose receivers while their parent is still active.`,\n );\n this.receiverAgentId = receiverAgentId;\n }\n}\n\n/** Throw when two handoffs in the same parent collide on tool name (D215). */\nexport class HandoffNameCollisionError extends Error {\n override readonly name = \"HandoffNameCollisionError\";\n readonly conflictingName: string;\n constructor(conflictingName: string) {\n super(\n `Two handoffs share the same tool name \"${conflictingName}\". ` +\n `Set { toolName } on at least one of them to disambiguate.`,\n );\n this.conflictingName = conflictingName;\n }\n}\n","/**\n * Handoff registry — pure state container per `Agent` instance.\n *\n * Holds the active dispatch chain (for depth + pair tracking) across the\n * lifetime of a single `agent.send()` call. Cleared between calls.\n *\n * @internal\n */\n\nimport { HandoffLoopError, HandoffPairLoopError } from \"../types/handoff.js\";\n\nexport interface HandoffChainState {\n /** Ordered chain of agentIds traversed so far (oldest first). */\n readonly chain: string[];\n /** Set of \"<sender>-><receiver>\" keys for pair-level loop detection (D221). */\n readonly seenPairs: Set<string>;\n /** Caller-supplied max depth (D218). */\n readonly maxDepth: number;\n}\n\nexport function createChainState(rootAgentId: string, maxDepth: number): HandoffChainState {\n return {\n chain: [rootAgentId],\n seenPairs: new Set(),\n maxDepth,\n };\n}\n\n/**\n * Record a handoff hop. Throws on depth-exceed (D218) or pair-loop (D221).\n * Mutates the state in place.\n */\nexport function recordHop(\n state: HandoffChainState,\n senderAgentId: string,\n receiverAgentId: string,\n): void {\n const pairKey = `${senderAgentId}->${receiverAgentId}`;\n if (state.seenPairs.has(pairKey)) {\n throw new HandoffPairLoopError(senderAgentId, receiverAgentId);\n }\n state.seenPairs.add(pairKey);\n state.chain.push(receiverAgentId);\n // chain.length = nodes; depth = hops = chain.length - 1.\n const depth = state.chain.length - 1;\n if (depth > state.maxDepth) {\n throw new HandoffLoopError(state.maxDepth, [...state.chain]);\n }\n}\n\nexport function currentDepth(state: HandoffChainState): number {\n return state.chain.length - 1;\n}\n","/**\n * D220 — Lazy-loaded OTel `handoff.transfer` span emitter.\n *\n * @internal\n */\n\n// Inline tracer-loader (same workaround as @theokit/sdk-cache/internal/telemetry.ts):\n// rollup-plugin-dts emits incomplete index.d.ts for newly-modified internal/ barrels\n// in @theokit/sdk. Runtime via the sub-path works; TypeScript users hit TS2305.\n// Inlining keeps sdk-handoff self-contained for the (small) observability hook.\nimport { createRequire } from \"node:module\";\n\ninterface SpanLike {\n setAttribute(key: string, value: string | number | boolean): SpanLike;\n end(): void;\n}\n\nconst _NOOP_SPAN: SpanLike = {\n setAttribute: () => _NOOP_SPAN,\n end: () => undefined,\n};\n\ninterface TracerLike {\n startSpan(\n name: string,\n options?: { attributes?: Record<string, string | number | boolean> },\n ): SpanLike;\n}\n\ninterface TracerCacheEntry {\n tracer: TracerLike | null;\n}\nconst tracerCache = new Map<string, TracerCacheEntry>();\n\nfunction getTracer(name: string, version = \"1.0.0\"): TracerLike | undefined {\n const cached = tracerCache.get(name);\n if (cached !== undefined) return cached.tracer ?? undefined;\n try {\n const r = createRequire(import.meta.url);\n const otel = r(\"@opentelemetry/api\") as {\n trace?: { getTracer: (n: string, v?: string) => TracerLike };\n };\n if (otel.trace?.getTracer === undefined) {\n tracerCache.set(name, { tracer: null });\n return undefined;\n }\n const tracer = otel.trace.getTracer(name, version);\n tracerCache.set(name, { tracer });\n return tracer;\n } catch {\n tracerCache.set(name, { tracer: null });\n return undefined;\n }\n}\n\nfunction resetTracerCacheForTests(): void {\n tracerCache.clear();\n}\n\nconst TRACER_NAME = \"theokit-sdk-handoff\";\n\ninterface HandoffSpanHandle {\n setAttribute(key: string, value: string | number | boolean): void;\n end(): void;\n}\n\nconst NOOP: HandoffSpanHandle = { setAttribute: () => undefined, end: () => undefined };\n\nfunction safe<T>(fn: () => T, fallback: T): T {\n try {\n return fn();\n } catch {\n return fallback;\n }\n}\n\nexport function startHandoffSpan(attrs: {\n from: string;\n to: string;\n reason: string;\n depth: number;\n toolName: string;\n}): HandoffSpanHandle {\n const tracer = getTracer(TRACER_NAME);\n if (tracer === undefined) return NOOP;\n const span: SpanLike | undefined = safe(\n () =>\n tracer.startSpan(\"handoff.transfer\", {\n attributes: {\n \"handoff.from\": attrs.from,\n \"handoff.to\": attrs.to,\n \"handoff.reason\": attrs.reason,\n \"handoff.depth\": attrs.depth,\n \"handoff.tool_name\": attrs.toolName,\n },\n }),\n undefined,\n );\n if (span === undefined) return NOOP;\n return {\n setAttribute: (k, v) => safe(() => span.setAttribute(k, v), undefined),\n end: () => safe(() => span.end(), undefined),\n };\n}\n\n/** Test-only — reset cached OTel handle. */\nfunction __resetHandoffOtelCacheForTests(): void {\n resetTracerCacheForTests();\n}\n","/**\n * Handoff dispatch orchestration.\n *\n * Pragmatic v1: when a handoff fires, the sender calls `receiver.send()`\n * with the (optionally filtered) history. The receiver's reply is returned\n * to the sender, which captures it as the answer to the user's question.\n *\n * NOTE: this is NOT pure peer-to-peer (the sender stays on the call stack\n * until the receiver returns). Pure intercept-and-swap requires deeper\n * agent-loop refactor; deferred to v2. v1 still validates the user-facing\n * value: \"agent A reasoned about routing, agent B answered.\"\n *\n * @internal\n */\n\nimport type { SDKAgent } from \"@theokit/sdk\";\nimport { z } from \"zod\";\nimport type {\n HandoffContext,\n HandoffDescriptor,\n HandoffHistory,\n HandoffResult,\n} from \"../types/handoff.js\";\nimport { HandoffReceiverDisposedError } from \"../types/handoff.js\";\nimport { type HandoffChainState, recordHop } from \"./registry.js\";\nimport { startHandoffSpan } from \"./telemetry.js\";\n\n/**\n * EC-2 / D228 — `safeFilter` wraps `inputFilter`. On exception, falls back\n * to the un-filtered history and warns to stderr once per process.\n */\nlet warnedFilterOnce = false;\nasync function safeFilter(\n filter: ((h: HandoffHistory) => HandoffHistory | Promise<HandoffHistory>) | undefined,\n history: HandoffHistory,\n): Promise<HandoffHistory> {\n if (filter === undefined) return history;\n try {\n const result = filter(history);\n return result instanceof Promise ? await result : result;\n } catch (err) {\n if (!warnedFilterOnce) {\n warnedFilterOnce = true;\n process.stderr.write(\n `[handoff] inputFilter threw, falling back to full history: ${err instanceof Error ? err.message : String(err)}\\n`,\n );\n }\n return history;\n }\n}\n\n/**\n * EC-4 / D229 — parse the LLM-provided JSON args. Returns undefined when\n * no `inputType` set; returns parsed value otherwise (default to `{}` on\n * empty/null input before Zod refinements).\n */\nfunction parseHandoffInput(descriptor: HandoffDescriptor, raw: unknown): unknown {\n const inputType = descriptor.options.inputType;\n if (inputType === undefined) return undefined;\n const candidate = raw === null || raw === undefined ? {} : raw;\n return inputType.parse(candidate);\n}\n\nfunction isAgentDisposed(agent: SDKAgent): boolean {\n // SDKAgent doesn't expose `disposed` publicly; check via duck-typing on\n // a known internal flag. Safe fallback: if we can't tell, assume alive.\n const maybe = agent as unknown as { disposed?: boolean };\n return maybe.disposed === true;\n}\n\n/**\n * Run a single handoff hop. Returns the receiver's reply text.\n *\n * Algorithm:\n * 1. EC-5: refuse if receiver disposed.\n * 2. Build HandoffContext.\n * 3. Check isEnabled() — if false, refuse with a clear error message.\n * 4. Parse inputType (D229).\n * 5. Run onHandoff(ctx, parsed) — throw aborts (D227).\n * 6. Apply inputFilter (safeFilter — D228).\n * 7. Record hop in chain state (depth + pair guards).\n * 8. Open OTel span (D220).\n * 9. Receiver: build the user-facing message and `await receiver.send(msg).then(wait)`.\n * 10. Close span + return reply.\n */\nasync function assertHandoffEnabled(\n descriptor: HandoffDescriptor,\n ctx: HandoffContext,\n receiverAgentId: string,\n): Promise<void> {\n const opt = descriptor.options.isEnabled;\n let enabled = true;\n if (typeof opt === \"boolean\") enabled = opt;\n else if (typeof opt === \"function\") {\n const r = opt(ctx);\n enabled = r instanceof Promise ? await r : r;\n }\n if (!enabled) {\n throw new Error(`Handoff to ${receiverAgentId} is disabled (isEnabled returned false)`);\n }\n}\n\nfunction parseAndValidate(descriptor: HandoffDescriptor, rawInputJson: unknown): unknown {\n try {\n return parseHandoffInput(descriptor, rawInputJson);\n } catch (err) {\n const detail =\n err instanceof z.ZodError\n ? (err.issues[0]?.message ?? \"schema_invalid\")\n : err instanceof Error\n ? err.message\n : String(err);\n throw new Error(`Handoff input validation failed: ${detail}`);\n }\n}\n\nasync function runOnHandoff(\n descriptor: HandoffDescriptor,\n ctx: HandoffContext,\n parsedInput: unknown,\n): Promise<void> {\n const onHandoff = descriptor.options.onHandoff;\n if (onHandoff === undefined) return;\n // biome-ignore lint/suspicious/noExplicitAny: parsedInput is typed unknown by design.\n const result = onHandoff(ctx, parsedInput as any);\n if (result instanceof Promise) await result;\n}\n\nfunction extractUserText(content: unknown): string | undefined {\n if (typeof content === \"string\") return content;\n if (!Array.isArray(content)) return undefined;\n const text = content\n .filter((c): c is { type: \"text\"; text: string } => (c as { type?: string })?.type === \"text\")\n .map((c) => c.text)\n .join(\"\\n\");\n return text.length > 0 ? text : undefined;\n}\n\nfunction extractLastUserMessage(history: HandoffHistory, senderAgentId: string): string {\n for (let i = history.messages.length - 1; i >= 0; i -= 1) {\n const m = history.messages[i] as {\n type?: string;\n message?: { role?: string; content?: unknown };\n };\n if (m?.type !== \"user\" || m.message?.role !== \"user\") continue;\n const text = extractUserText(m.message.content);\n if (text !== undefined) return text;\n }\n return `(Handoff from ${senderAgentId} — no prior user message in history.)`;\n}\n\nexport async function dispatchHandoff(args: {\n descriptor: HandoffDescriptor;\n senderAgentId: string;\n chainState: HandoffChainState;\n rawInputJson: unknown;\n /** The conversation so far (history wrapper). v1: just the LAST user message. */\n history: HandoffHistory;\n /** Override the message text sent to the receiver. Used by `Agent.handoffTo` imperative. */\n messageOverride?: string;\n}): Promise<{ reply: string; result: HandoffResult }> {\n const { descriptor, senderAgentId, chainState, rawInputJson, history, messageOverride } = args;\n const receiver = descriptor.target;\n\n if (isAgentDisposed(receiver)) {\n throw new HandoffReceiverDisposedError(receiver.agentId);\n }\n\n const depthAfterThisHop = chainState.chain.length;\n const ctx: HandoffContext = {\n senderAgentId,\n receiverAgentId: receiver.agentId,\n currentDepth: depthAfterThisHop,\n chain: [...chainState.chain, receiver.agentId],\n };\n\n await assertHandoffEnabled(descriptor, ctx, receiver.agentId);\n const parsedInput = parseAndValidate(descriptor, rawInputJson);\n await runOnHandoff(descriptor, ctx, parsedInput);\n\n // Filter history (D228 — resilient)\n const filteredHistory = await safeFilter(descriptor.options.inputFilter, history);\n\n // Record hop — may throw HandoffLoopError or HandoffPairLoopError\n recordHop(chainState, senderAgentId, receiver.agentId);\n\n const lastUserMessage = messageOverride ?? extractLastUserMessage(filteredHistory, senderAgentId);\n const reason = extractReason(parsedInput);\n\n const span = startHandoffSpan({\n from: senderAgentId,\n to: receiver.agentId,\n reason,\n depth: depthAfterThisHop,\n toolName: descriptor.resolvedToolName,\n });\n\n try {\n const run = await receiver.send(lastUserMessage);\n const result = await run.wait();\n const reply = buildReply(result, receiver.agentId);\n return {\n reply,\n result: {\n from: senderAgentId,\n to: receiver.agentId,\n depth: depthAfterThisHop,\n toolName: descriptor.resolvedToolName,\n ...(reason !== \"\" ? { reasonFromLlm: reason } : {}),\n },\n };\n } finally {\n span.end();\n }\n}\n\nfunction extractReason(parsedInput: unknown): string {\n if (typeof parsedInput !== \"object\" || parsedInput === null) return \"\";\n if (!(\"reason\" in parsedInput)) return \"\";\n return String((parsedInput as { reason: unknown }).reason ?? \"\");\n}\n\nfunction buildReply(\n result: { status: string; result?: string; error?: { message: string } },\n receiverAgentId: string,\n): string {\n if (result.status === \"finished\" && result.result !== undefined) return result.result;\n const suffix = result.error !== undefined ? `: ${result.error.message}` : \"\";\n return `(Handoff target ${receiverAgentId} returned status=${result.status}${suffix})`;\n}\n","/**\n * Zod v4 → JSON Schema adapter for sdk-handoff.\n *\n * Uses Zod v4's native `z.toJSONSchema()` directly. v3 fallback removed\n * after zod-v4-migration plan (ADR D2).\n *\n * @internal\n */\n\nimport { toJSONSchema } from \"zod\";\n\ninterface ToJsonSchemaOptions {\n /** `\"any\"` keeps transforms/refinements as `{}` (loose). Default: `\"any\"`. */\n unrepresentable?: \"any\" | \"throw\";\n}\n\n/**\n * Convert a Zod schema to a JSON Schema object via Zod v4 native.\n *\n * @internal\n */\nexport function toJsonSchema(\n schema: unknown,\n options: ToJsonSchemaOptions = { unrepresentable: \"any\" },\n): Record<string, unknown> {\n // eslint-disable-next-line @typescript-eslint/no-explicit-any -- generic ZodType erasure\n return toJSONSchema(schema as any, options) as Record<string, unknown>;\n}\n","/**\n * Convert `handoffs[]` entries into synthetic `transfer_to_<receiver>` tools\n * for injection into the agent's tool registry at construction time.\n *\n * The synthesized tool's handler calls `dispatchHandoff` internally and\n * returns the receiver's reply as `tool_result`. v1 trade-off documented\n * in dispatcher.ts.\n *\n * @internal\n */\n\nimport type { CustomTool, SDKAgent } from \"@theokit/sdk\";\nimport { z } from \"zod\";\nimport {\n type HandoffDescriptor,\n HandoffNameCollisionError,\n HandoffSelfReferenceError,\n} from \"../types/handoff.js\";\nimport { dispatchHandoff } from \"./dispatcher.js\";\nimport { createChainState } from \"./registry.js\";\nimport { toJsonSchema } from \"./to-json-schema.js\";\n\ninterface NormalizedHandoff {\n descriptor: HandoffDescriptor;\n}\n\n/**\n * Normalize each `handoffs[]` entry to a `HandoffDescriptor`. Raw `SDKAgent`\n * instances are auto-wrapped with default options. Validates:\n * - EC-6: no self-reference (would cause infinite recursion).\n * - D215: resolved tool names must be unique.\n */\nexport function normalizeHandoffs(\n parentAgentId: string,\n entries: ReadonlyArray<SDKAgent | HandoffDescriptor>,\n): NormalizedHandoff[] {\n if (entries.length === 0) return [];\n const out: NormalizedHandoff[] = [];\n const seenNames = new Set<string>();\n for (const entry of entries) {\n // Detect raw Agent vs HandoffDescriptor by presence of `.target`.\n const isDescriptor =\n typeof entry === \"object\" &&\n entry !== null &&\n \"target\" in entry &&\n \"options\" in entry &&\n \"resolvedToolName\" in entry;\n const descriptor = isDescriptor ? (entry as HandoffDescriptor) : autoWrap(entry as SDKAgent);\n if (descriptor.target.agentId === parentAgentId) {\n throw new HandoffSelfReferenceError(parentAgentId);\n }\n const name = descriptor.resolvedToolName;\n if (seenNames.has(name)) {\n throw new HandoffNameCollisionError(name);\n }\n seenNames.add(name);\n out.push({ descriptor });\n }\n return out;\n}\n\nfunction autoWrap(agent: SDKAgent): HandoffDescriptor {\n const name = resolveTargetName(agent);\n return {\n target: agent,\n options: {},\n resolvedToolName: `transfer_to_${name}`,\n };\n}\n\nfunction resolveTargetName(agent: SDKAgent): string {\n // Prefer a `name` field if exposed; fall back to a short agentId slug.\n const candidate = (agent as unknown as { name?: string }).name ?? agent.agentId ?? \"anonymous\";\n return slugify(candidate);\n}\n\nfunction slugify(input: string): string {\n return (\n input\n .replace(/^agent-/i, \"\")\n .replace(/[^a-zA-Z0-9_-]+/g, \"_\")\n .replace(/^_+|_+$/g, \"\")\n .slice(0, 64) || \"anonymous\"\n );\n}\n\n/**\n * Build a `CustomTool` for one handoff descriptor. The handler dispatches\n * the handoff using a fresh chain state per `send()`-level invocation.\n *\n * NOTE: this v1 builds a NEW chain state per tool invocation. Pure\n * cross-tool depth tracking within one send() requires per-Agent context\n * — deferred. The single-flight pair guard catches direct ping-pong even\n * without cross-invocation chain (since each call wraps the same depth\n * counter from 1).\n */\nexport function buildHandoffTool(\n parentAgentId: string,\n descriptor: HandoffDescriptor,\n maxHandoffDepth: number,\n): CustomTool {\n const description =\n descriptor.options.toolDescription ??\n `Transfer the conversation to the ${descriptor.target.agentId} agent. ` +\n `Use this when the user's request matches their specialty.`;\n\n const inputZod =\n descriptor.options.inputType ??\n z.object({\n reason: z.string().optional().describe(\"Brief reason for the transfer (one short sentence).\"),\n });\n // CustomTool.inputSchema expects a JSON schema (Record<string, unknown>),\n // not the raw Zod type. Convert lazily so we don't fail when Zod is missing.\n // Universal Zod 3+4 conversion (feature-detects native v4, falls back to lib on v3).\n const inputSchema = toJsonSchema(inputZod);\n\n return {\n name: descriptor.resolvedToolName,\n description,\n inputSchema,\n handler: async (input: unknown): Promise<string> => {\n const chainState = createChainState(parentAgentId, maxHandoffDepth);\n try {\n const { reply, result } = await dispatchHandoff({\n descriptor,\n senderAgentId: parentAgentId,\n chainState,\n rawInputJson: input,\n history: { messages: [] }, // v1: history replay deferred\n });\n return JSON.stringify({\n ok: true,\n transferred_to: result.to,\n depth: result.depth,\n reply,\n });\n } catch (err) {\n return JSON.stringify({\n ok: false,\n error: err instanceof Error ? err.name : \"HandoffError\",\n message: err instanceof Error ? err.message : String(err),\n });\n }\n },\n };\n}\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@theokit/sdk-handoff",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Inter-agent dispatch for @theokit/sdk — typed Handoff descriptors, loop protection, plugin-based wiring.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"homepage": "https://github.com/usetheo/theokit-sdk#readme",
|
|
7
|
+
"bugs": "https://github.com/usetheo/theokit-sdk/issues",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/usetheo/theokit-sdk.git",
|
|
11
|
+
"directory": "packages/sdk-handoff"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"sideEffects": false,
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.12.0"
|
|
17
|
+
},
|
|
18
|
+
"main": "./dist/index.cjs",
|
|
19
|
+
"module": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"typesVersions": {
|
|
22
|
+
"*": {
|
|
23
|
+
"internal/tool-injector": [
|
|
24
|
+
"./dist/internal/tool-injector.d.ts"
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"exports": {
|
|
29
|
+
".": {
|
|
30
|
+
"import": {
|
|
31
|
+
"types": "./dist/index.d.ts",
|
|
32
|
+
"default": "./dist/index.js"
|
|
33
|
+
},
|
|
34
|
+
"require": {
|
|
35
|
+
"types": "./dist/index.d.cts",
|
|
36
|
+
"default": "./dist/index.cjs"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"./internal/tool-injector": {
|
|
40
|
+
"import": {
|
|
41
|
+
"types": "./dist/internal/tool-injector.d.ts",
|
|
42
|
+
"default": "./dist/internal/tool-injector.js"
|
|
43
|
+
},
|
|
44
|
+
"require": {
|
|
45
|
+
"types": "./dist/internal/tool-injector.d.cts",
|
|
46
|
+
"default": "./dist/internal/tool-injector.cjs"
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
"./package.json": "./package.json"
|
|
50
|
+
},
|
|
51
|
+
"files": [
|
|
52
|
+
"dist",
|
|
53
|
+
"README.md",
|
|
54
|
+
"CHANGELOG.md",
|
|
55
|
+
"LICENSE"
|
|
56
|
+
],
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsup",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
61
|
+
},
|
|
62
|
+
"peerDependencies": {
|
|
63
|
+
"@theokit/sdk": ">=1.7.0",
|
|
64
|
+
"zod": "^4.0.0"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"tsup": "^8.3.5",
|
|
68
|
+
"typescript": "^5.7.2",
|
|
69
|
+
"vitest": "^4.1.8",
|
|
70
|
+
"@theokit/sdk": "workspace:*",
|
|
71
|
+
"zod": "^4.0.0"
|
|
72
|
+
},
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public"
|
|
75
|
+
}
|
|
76
|
+
}
|