torus-ai 0.2.1 → 0.4.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/README.md +76 -0
- package/dist/index.d.ts +128 -1
- package/dist/index.js +255 -0
- package/dist/index.js.map +1 -1
- package/package.json +44 -8
- package/src/index.ts +27 -0
- package/src/mcp-client.ts +114 -0
- package/src/pack.ts +155 -0
- package/src/packkit.ts +147 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { createSdkMcpServer, tool } from "./tools.ts";
|
|
2
|
+
import type { SdkMcpServer, ToolDefinition } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
// MCP client: connect to EXTERNAL Model Context Protocol servers (local stdio
|
|
5
|
+
// subprocesses or remote HTTP) and expose their tools to the agent. Each remote
|
|
6
|
+
// tool is wrapped as a normal SdkMcpServer tool, so it flows through the same
|
|
7
|
+
// registry, namespacing (mcp__<server>__<tool>), loop, and permission allowlist
|
|
8
|
+
// as in-process tools. Requires the optional `@modelcontextprotocol/sdk` package.
|
|
9
|
+
|
|
10
|
+
export type McpServerConfig =
|
|
11
|
+
| {
|
|
12
|
+
type?: "stdio";
|
|
13
|
+
command: string;
|
|
14
|
+
args?: string[];
|
|
15
|
+
env?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
| {
|
|
18
|
+
type: "http";
|
|
19
|
+
url: string;
|
|
20
|
+
headers?: Record<string, string>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export interface McpConnection {
|
|
24
|
+
/** Namespaced server — its tools become mcp__<name>__<tool>. */
|
|
25
|
+
server: SdkMcpServer;
|
|
26
|
+
/** Tear down the MCP client + transport. */
|
|
27
|
+
close(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function connect(config: McpServerConfig): Promise<any> {
|
|
31
|
+
const clientMod = await import("@modelcontextprotocol/sdk/client/index.js").catch(() => {
|
|
32
|
+
throw new Error("MCP client needs @modelcontextprotocol/sdk: run `npm i @modelcontextprotocol/sdk`.");
|
|
33
|
+
});
|
|
34
|
+
const Client = (clientMod as any).Client;
|
|
35
|
+
|
|
36
|
+
let transport: any;
|
|
37
|
+
if (config.type === "http") {
|
|
38
|
+
const mod = await import("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
39
|
+
transport = new (mod as any).StreamableHTTPClientTransport(
|
|
40
|
+
new URL(config.url),
|
|
41
|
+
config.headers ? { requestInit: { headers: config.headers } } : undefined,
|
|
42
|
+
);
|
|
43
|
+
} else {
|
|
44
|
+
const mod = await import("@modelcontextprotocol/sdk/client/stdio.js");
|
|
45
|
+
transport = new (mod as any).StdioClientTransport({
|
|
46
|
+
command: config.command,
|
|
47
|
+
args: config.args ?? [],
|
|
48
|
+
env: { ...process.env, ...(config.env ?? {}) } as Record<string, string>,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const client = new Client({ name: "torus", version: "0.4.0" }, { capabilities: {} });
|
|
53
|
+
await client.connect(transport);
|
|
54
|
+
return client;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Connect to one external MCP server and expose its tools as an SdkMcpServer. */
|
|
58
|
+
export async function connectMcpServer(name: string, config: McpServerConfig): Promise<McpConnection> {
|
|
59
|
+
const client = await connect(config);
|
|
60
|
+
const listed = await client.listTools();
|
|
61
|
+
|
|
62
|
+
const tools: ToolDefinition[] = (listed.tools ?? []).map((t: any) =>
|
|
63
|
+
tool(
|
|
64
|
+
t.name,
|
|
65
|
+
t.description ?? "",
|
|
66
|
+
(t.inputSchema as Record<string, unknown>) ?? { type: "object", properties: {} },
|
|
67
|
+
async (input: Record<string, unknown>) => {
|
|
68
|
+
const res: any = await client.callTool({ name: t.name, arguments: input ?? {} });
|
|
69
|
+
const text =
|
|
70
|
+
(res.content ?? [])
|
|
71
|
+
.filter((c: any) => c?.type === "text")
|
|
72
|
+
.map((c: any) => c.text)
|
|
73
|
+
.join("\n") || JSON.stringify(res.content ?? res);
|
|
74
|
+
return { content: text, isError: !!res.isError };
|
|
75
|
+
},
|
|
76
|
+
),
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
server: createSdkMcpServer({ name, version: "1.0.0", tools }),
|
|
81
|
+
close: async () => {
|
|
82
|
+
try {
|
|
83
|
+
await client.close();
|
|
84
|
+
} catch {
|
|
85
|
+
// best-effort teardown
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Connect to several external MCP servers at once.
|
|
93
|
+
*
|
|
94
|
+
* const { servers, close } = await connectMcpServers({
|
|
95
|
+
* github: { command: "npx", args: ["-y", "@modelcontextprotocol/server-github"], env: { GITHUB_TOKEN } },
|
|
96
|
+
* docs: { type: "http", url: "https://example.com/mcp" },
|
|
97
|
+
* });
|
|
98
|
+
* for await (const ev of query("...", { mcpServers: servers,
|
|
99
|
+
* permissions: { allowedTools: ["mcp__github__*"] } })) { ... }
|
|
100
|
+
* await close();
|
|
101
|
+
*/
|
|
102
|
+
export async function connectMcpServers(
|
|
103
|
+
configs: Record<string, McpServerConfig>,
|
|
104
|
+
): Promise<{ servers: SdkMcpServer[]; close(): Promise<void> }> {
|
|
105
|
+
const conns = await Promise.all(
|
|
106
|
+
Object.entries(configs).map(([name, cfg]) => connectMcpServer(name, cfg)),
|
|
107
|
+
);
|
|
108
|
+
return {
|
|
109
|
+
servers: conns.map((c) => c.server),
|
|
110
|
+
close: async () => {
|
|
111
|
+
await Promise.all(conns.map((c) => c.close()));
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
package/src/pack.ts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { builtinTools } from "./builtins.ts";
|
|
5
|
+
import { runLoop } from "./loop.ts";
|
|
6
|
+
import { matchesAllow, PermissionEngine } from "./permissions.ts";
|
|
7
|
+
import { createCatalogServer, type CatalogItem } from "./packkit.ts";
|
|
8
|
+
import { createDefaultProvider } from "./providers/cascade.ts";
|
|
9
|
+
import { ToolRegistry } from "./tools.ts";
|
|
10
|
+
import type {
|
|
11
|
+
AgentEvent,
|
|
12
|
+
CanUseTool,
|
|
13
|
+
ContentBlock,
|
|
14
|
+
Message,
|
|
15
|
+
ModelProvider,
|
|
16
|
+
SdkMcpServer,
|
|
17
|
+
} from "./types.ts";
|
|
18
|
+
|
|
19
|
+
// Specialize the generic engine to a product by loading a *pack* (an adapter):
|
|
20
|
+
// persona + sales playbook + policy + domain tools + catalog grounding + guardrails.
|
|
21
|
+
// One engine, many packs — don't fork the SDK per vertical.
|
|
22
|
+
|
|
23
|
+
export interface PackKnowledge {
|
|
24
|
+
/** Product catalog — auto-wired into a `search_catalog` tool for grounding. */
|
|
25
|
+
catalog?: CatalogItem[];
|
|
26
|
+
/** Short reference text (policies, FAQs) appended to the system prompt. */
|
|
27
|
+
faqs?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface PackGuardrails {
|
|
31
|
+
/** Allowlist of tool names the agent may call (namespaced, wildcards ok). */
|
|
32
|
+
allowedTools?: string[];
|
|
33
|
+
/** Tools that require explicit confirmation before running (namespaced names). */
|
|
34
|
+
confirm?: string[];
|
|
35
|
+
/** Extra custom gate, evaluated after allow/confirm. */
|
|
36
|
+
canUseTool?: CanUseTool;
|
|
37
|
+
/** Rules text (discount authority, no-overpromise, escalation) added to the prompt. */
|
|
38
|
+
policy?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AgentPack {
|
|
42
|
+
name: string;
|
|
43
|
+
persona: string; // who it is + voice (system prompt core)
|
|
44
|
+
playbook?: string; // sales stages + goal
|
|
45
|
+
tools?: SdkMcpServer[]; // domain actions (quote, reserve, invoice, handoff, ...)
|
|
46
|
+
knowledge?: PackKnowledge;
|
|
47
|
+
guardrails?: PackGuardrails;
|
|
48
|
+
model?: ModelProvider; // defaults to the free-first cascade
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SpecializeOptions {
|
|
52
|
+
provider?: ModelProvider;
|
|
53
|
+
/** Called when a `confirm` tool wants to run; return true to allow. */
|
|
54
|
+
onConfirm?: (toolName: string, input: Record<string, unknown>) => boolean | Promise<boolean>;
|
|
55
|
+
/** Allow built-in file tools (read/write/list). Off by default for packs. */
|
|
56
|
+
includeBuiltins?: boolean;
|
|
57
|
+
maxTurns?: number;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface SpecializedAgent {
|
|
61
|
+
pack: AgentPack;
|
|
62
|
+
system: string;
|
|
63
|
+
servers: SdkMcpServer[];
|
|
64
|
+
query(prompt: string | ContentBlock[], extra?: { maxTurns?: number }): AsyncGenerator<AgentEvent>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Build a ready-to-run specialized agent from a pack. */
|
|
68
|
+
export function createSpecializedAgent(pack: AgentPack, opts: SpecializeOptions = {}): SpecializedAgent {
|
|
69
|
+
// Assemble the system prompt from persona + playbook + policy + grounding + faqs.
|
|
70
|
+
const servers: SdkMcpServer[] = [...(pack.tools ?? [])];
|
|
71
|
+
if (pack.knowledge?.catalog?.length) servers.unshift(createCatalogServer(pack.knowledge.catalog));
|
|
72
|
+
|
|
73
|
+
const parts = [pack.persona.trim()];
|
|
74
|
+
if (pack.playbook) parts.push(`## Playbook\n${pack.playbook.trim()}`);
|
|
75
|
+
if (pack.guardrails?.policy) parts.push(`## Policy\n${pack.guardrails.policy.trim()}`);
|
|
76
|
+
if (servers.some((s) => s.tools.some((t) => t.name === "search_catalog"))) {
|
|
77
|
+
parts.push(
|
|
78
|
+
"Use the `search_catalog` tool for every product, price, or availability question. Never invent a price or claim availability you did not look up.",
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (pack.knowledge?.faqs) parts.push(`## Reference\n${pack.knowledge.faqs.trim()}`);
|
|
82
|
+
const system = parts.join("\n\n");
|
|
83
|
+
|
|
84
|
+
const confirmTools = pack.guardrails?.confirm ?? [];
|
|
85
|
+
const allow = pack.guardrails?.allowedTools;
|
|
86
|
+
const base = pack.guardrails?.canUseTool;
|
|
87
|
+
|
|
88
|
+
const canUseTool: CanUseTool = async (name, input) => {
|
|
89
|
+
if (confirmTools.includes(name)) {
|
|
90
|
+
const ok = opts.onConfirm ? await opts.onConfirm(name, input) : false;
|
|
91
|
+
if (!ok) return { behavior: "deny", message: `${name} requires confirmation and it was not granted.` };
|
|
92
|
+
}
|
|
93
|
+
if (allow && !matchesAllow(name, allow)) {
|
|
94
|
+
return { behavior: "deny", message: `${name} is not allowed by this pack's guardrails.` };
|
|
95
|
+
}
|
|
96
|
+
return base ? base(name, input) : { behavior: "allow" };
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const provider = opts.provider ?? pack.model ?? createDefaultProvider();
|
|
100
|
+
const includeBuiltins = opts.includeBuiltins ?? false;
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
pack,
|
|
104
|
+
system,
|
|
105
|
+
servers,
|
|
106
|
+
async *query(prompt, extra) {
|
|
107
|
+
const registry = new ToolRegistry();
|
|
108
|
+
if (includeBuiltins) registry.addBuiltins(builtinTools);
|
|
109
|
+
for (const s of servers) registry.addServer(s);
|
|
110
|
+
|
|
111
|
+
const content: ContentBlock[] =
|
|
112
|
+
typeof prompt === "string" ? [{ type: "text", text: prompt }] : prompt;
|
|
113
|
+
const messages: Message[] = [{ role: "user", content }];
|
|
114
|
+
|
|
115
|
+
const result = yield* runLoop({
|
|
116
|
+
provider,
|
|
117
|
+
registry,
|
|
118
|
+
permissions: new PermissionEngine({ canUseTool }),
|
|
119
|
+
system,
|
|
120
|
+
messages,
|
|
121
|
+
toolContext: { workspaceDir: process.cwd() },
|
|
122
|
+
maxTurns: extra?.maxTurns ?? opts.maxTurns,
|
|
123
|
+
});
|
|
124
|
+
yield { type: "result", finalText: result.finalText, turns: result.turns };
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Load a pack's content from a folder (so non-devs can edit it):
|
|
131
|
+
* persona.md · playbook.md · policy.md · catalog.json · faqs.md
|
|
132
|
+
* Code tools (quote/reserve/invoice/...) are passed via `opts.tools`.
|
|
133
|
+
*/
|
|
134
|
+
export async function loadPack(
|
|
135
|
+
dir: string,
|
|
136
|
+
opts: { tools?: SdkMcpServer[] } = {},
|
|
137
|
+
): Promise<AgentPack> {
|
|
138
|
+
const read = async (f: string): Promise<string | undefined> => {
|
|
139
|
+
const p = join(dir, f);
|
|
140
|
+
return existsSync(p) ? await readFile(p, "utf8") : undefined;
|
|
141
|
+
};
|
|
142
|
+
const catalogRaw = await read("catalog.json");
|
|
143
|
+
const policy = await read("policy.md");
|
|
144
|
+
return {
|
|
145
|
+
name: dir.split(/[\\/]/).filter(Boolean).pop() ?? "pack",
|
|
146
|
+
persona: (await read("persona.md")) ?? "",
|
|
147
|
+
playbook: await read("playbook.md"),
|
|
148
|
+
knowledge: {
|
|
149
|
+
catalog: catalogRaw ? (JSON.parse(catalogRaw) as CatalogItem[]) : undefined,
|
|
150
|
+
faqs: await read("faqs.md"),
|
|
151
|
+
},
|
|
152
|
+
guardrails: policy ? { policy } : undefined,
|
|
153
|
+
tools: opts.tools,
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/packkit.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { createSdkMcpServer, tool } from "./tools.ts";
|
|
2
|
+
import type { SdkMcpServer } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
// Reusable tool patterns for specializing an agent to a vertical (a "pack").
|
|
5
|
+
// Compose these into a pack's `tools`. They are deliberately backend-agnostic —
|
|
6
|
+
// swap the in-memory stubs for real catalog/CRM/payment integrations later.
|
|
7
|
+
|
|
8
|
+
export type CatalogItem = Record<string, unknown> & {
|
|
9
|
+
id?: string;
|
|
10
|
+
name?: string;
|
|
11
|
+
price?: number;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
available?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** A `search_catalog` tool over an in-memory product list (text + price + tags). */
|
|
17
|
+
export function createCatalogServer(
|
|
18
|
+
items: CatalogItem[],
|
|
19
|
+
opts: { serverName?: string } = {},
|
|
20
|
+
): SdkMcpServer {
|
|
21
|
+
const search = tool(
|
|
22
|
+
"search_catalog",
|
|
23
|
+
"Search the product catalog by text, max price, and tags. Returns matching items with prices and availability. Use this for every product/price/availability question — never guess.",
|
|
24
|
+
{
|
|
25
|
+
type: "object",
|
|
26
|
+
properties: {
|
|
27
|
+
query: { type: "string" },
|
|
28
|
+
maxPrice: { type: "number" },
|
|
29
|
+
tags: { type: "array", items: { type: "string" } },
|
|
30
|
+
limit: { type: "number" },
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
(input: { query?: string; maxPrice?: number; tags?: string[]; limit?: number }) => {
|
|
34
|
+
let res = items.filter((it) => it.available !== false);
|
|
35
|
+
if (input.query) {
|
|
36
|
+
const words = input.query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
37
|
+
res = res.filter((it) => {
|
|
38
|
+
const hay = JSON.stringify(it).toLowerCase();
|
|
39
|
+
return words.every((w) => hay.includes(w));
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (typeof input.maxPrice === "number") {
|
|
43
|
+
res = res.filter((it) => typeof it.price !== "number" || it.price <= input.maxPrice!);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(input.tags) && input.tags.length) {
|
|
46
|
+
res = res.filter((it) => Array.isArray(it.tags) && input.tags!.some((t) => it.tags!.includes(t)));
|
|
47
|
+
}
|
|
48
|
+
const out = res.slice(0, input.limit ?? 5);
|
|
49
|
+
return { content: out.length ? JSON.stringify(out, null, 2) : "No matching items." };
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
return createSdkMcpServer({ name: opts.serverName ?? "catalog", tools: [search] });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** `get_lead` / `update_lead` over an in-memory customer profile (the funnel state). */
|
|
56
|
+
export function createLeadMemoryServer(
|
|
57
|
+
initial: Record<string, unknown> = {},
|
|
58
|
+
): SdkMcpServer & { lead: Record<string, unknown> } {
|
|
59
|
+
const lead: Record<string, unknown> = { ...initial };
|
|
60
|
+
const get = tool(
|
|
61
|
+
"get_lead",
|
|
62
|
+
"Get what we know about the current customer (name, date, budget, stage, items seen).",
|
|
63
|
+
{ type: "object", properties: {} },
|
|
64
|
+
() => ({ content: JSON.stringify(lead, null, 2) }),
|
|
65
|
+
);
|
|
66
|
+
const update = tool(
|
|
67
|
+
"update_lead",
|
|
68
|
+
"Merge fields into the customer profile, e.g. { budget: 2000, stage: 'recommend' }.",
|
|
69
|
+
{ type: "object", properties: { fields: { type: "object" } }, required: ["fields"] },
|
|
70
|
+
(input: { fields: Record<string, unknown> }) => {
|
|
71
|
+
Object.assign(lead, input.fields ?? {});
|
|
72
|
+
return { content: `updated: ${Object.keys(input.fields ?? {}).join(", ") || "(none)"}` };
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
return Object.assign(createSdkMcpServer({ name: "lead", tools: [get, update] }), { lead });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface Invoice {
|
|
79
|
+
id: string;
|
|
80
|
+
amount: number;
|
|
81
|
+
currency: string;
|
|
82
|
+
items?: unknown;
|
|
83
|
+
customer?: unknown;
|
|
84
|
+
status: "pending";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A generic `create_invoice` settle tool: records an order + amount as pending
|
|
89
|
+
* and returns an invoice id. Provider-agnostic — wire your processor via
|
|
90
|
+
* `onCreate` (e.g. create a real payment link, then confirm via webhook).
|
|
91
|
+
*/
|
|
92
|
+
export function createInvoiceServer(
|
|
93
|
+
opts: { onCreate?: (inv: Invoice) => void } = {},
|
|
94
|
+
): SdkMcpServer & { invoices: Invoice[] } {
|
|
95
|
+
const invoices: Invoice[] = [];
|
|
96
|
+
let n = 0;
|
|
97
|
+
const create = tool(
|
|
98
|
+
"create_invoice",
|
|
99
|
+
"Record an order and amount as a pending invoice to settle, returning an invoice id. Call this only after the customer has agreed to buy.",
|
|
100
|
+
{
|
|
101
|
+
type: "object",
|
|
102
|
+
properties: {
|
|
103
|
+
amount: { type: "number" },
|
|
104
|
+
currency: { type: "string" },
|
|
105
|
+
items: {},
|
|
106
|
+
customer: {},
|
|
107
|
+
},
|
|
108
|
+
required: ["amount"],
|
|
109
|
+
},
|
|
110
|
+
(input: { amount: number; currency?: string; items?: unknown; customer?: unknown }) => {
|
|
111
|
+
const inv: Invoice = {
|
|
112
|
+
id: `inv_${++n}`,
|
|
113
|
+
amount: input.amount,
|
|
114
|
+
currency: input.currency ?? "USD",
|
|
115
|
+
items: input.items,
|
|
116
|
+
customer: input.customer,
|
|
117
|
+
status: "pending",
|
|
118
|
+
};
|
|
119
|
+
invoices.push(inv);
|
|
120
|
+
opts.onCreate?.(inv);
|
|
121
|
+
return {
|
|
122
|
+
content: JSON.stringify({ invoiceId: inv.id, status: inv.status, amount: inv.amount, currency: inv.currency }),
|
|
123
|
+
};
|
|
124
|
+
},
|
|
125
|
+
);
|
|
126
|
+
return Object.assign(createSdkMcpServer({ name: "billing", tools: [create] }), { invoices });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** A `handoff_human` escalation tool. Wire `onHandoff` to notify a real agent. */
|
|
130
|
+
export function createHandoffServer(
|
|
131
|
+
opts: { onHandoff?: (info: { reason: string; summary: string }) => void } = {},
|
|
132
|
+
): SdkMcpServer {
|
|
133
|
+
const handoff = tool(
|
|
134
|
+
"handoff_human",
|
|
135
|
+
"Escalate to a human agent with a reason and a short summary of the conversation so far. Use when you're stuck, the request is high-value, or the customer asks for a person.",
|
|
136
|
+
{
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: { reason: { type: "string" }, summary: { type: "string" } },
|
|
139
|
+
required: ["reason"],
|
|
140
|
+
},
|
|
141
|
+
(input: { reason: string; summary?: string }) => {
|
|
142
|
+
opts.onHandoff?.({ reason: input.reason, summary: input.summary ?? "" });
|
|
143
|
+
return { content: "Escalated to a human; they will take over shortly." };
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
return createSdkMcpServer({ name: "support", tools: [handoff] });
|
|
147
|
+
}
|