Skip to content

Commit 6da0de8

Browse files
committed
Add tool-help discovery and framework comparison benchmarks
1 parent b2bf02b commit 6da0de8

15 files changed

Lines changed: 606 additions & 38 deletions

README.MD

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,12 @@ Run benchmark:
127127
bun run bench:agent
128128
```
129129

130+
Framework comparison benchmark:
131+
132+
```bash
133+
bun run bench:frameworks
134+
```
135+
130136
## Documentation
131137

132138
- [Docs Home](./docs/README.md)
@@ -139,6 +145,8 @@ bun run bench:agent
139145
- [LLM Integration](./docs/integrations/llm.md)
140146
- [Architecture Walkthrough](./docs/walkthrough.md)
141147
- [Agent-Native Integration](./docs/agent-native.md)
148+
- [Benchmarks](./docs/benchmarks.md)
149+
- [Agent Feature Roadmap](./docs/agent-roadmap.md)
142150

143151
## Contributing
144152

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
import { Bench } from "tinybench";
2+
import { AgentBridge, createMcpRequestHandler } from "../src/agent";
3+
4+
type HandlerArgs = { text: string };
5+
type HandlerResult = { normalized: string; hasEmail: boolean; length: number };
6+
7+
type Dispatcher = {
8+
name: string;
9+
invoke: (args: HandlerArgs) => Promise<unknown>;
10+
};
11+
12+
const sampleArgs: HandlerArgs = {
13+
text: "Contact support@example.com for status updates",
14+
};
15+
16+
const baseHandler = async ({ text }: HandlerArgs): Promise<HandlerResult> => ({
17+
normalized: text.toLowerCase(),
18+
hasEmail: /@/.test(text),
19+
length: text.length,
20+
});
21+
22+
async function createQirrelDispatcher(handler: typeof baseHandler): Promise<Dispatcher[]> {
23+
const bridge = new AgentBridge();
24+
bridge.registerApiTool(
25+
{
26+
name: "benchmark.parse",
27+
description: "Benchmark parser",
28+
inputSchema: {
29+
type: "object",
30+
properties: {
31+
text: { type: "string" },
32+
},
33+
required: ["text"],
34+
},
35+
},
36+
handler,
37+
);
38+
39+
const mcp = createMcpRequestHandler(bridge);
40+
41+
return [
42+
{
43+
name: "Qirrel AgentBridge",
44+
invoke: (args) => bridge.callTool("benchmark.parse", args),
45+
},
46+
{
47+
name: "Qirrel MCP Handler",
48+
invoke: async (args) =>
49+
mcp({
50+
jsonrpc: "2.0",
51+
id: 1,
52+
method: "tools/call",
53+
params: {
54+
name: "benchmark.parse",
55+
arguments: args,
56+
},
57+
}),
58+
},
59+
];
60+
}
61+
62+
async function createLangChainDispatcher(handler: typeof baseHandler): Promise<Dispatcher | undefined> {
63+
try {
64+
const [{ tool }, { z }] = await Promise.all([import("langchain"), import("zod")]);
65+
66+
const wrapped = tool(
67+
async ({ text }: HandlerArgs) => handler({ text }),
68+
{
69+
name: "benchmark.parse",
70+
description: "Benchmark parser",
71+
schema: z.object({
72+
text: z.string(),
73+
}),
74+
},
75+
);
76+
77+
return {
78+
name: "LangChain tool()",
79+
invoke: (args) => wrapped.invoke(args),
80+
};
81+
} catch {
82+
return undefined;
83+
}
84+
}
85+
86+
async function createAiSdkDispatcher(handler: typeof baseHandler): Promise<Dispatcher | undefined> {
87+
try {
88+
const [{ tool }, { z }] = await Promise.all([import("ai"), import("zod")]);
89+
90+
const wrapped = tool({
91+
description: "Benchmark parser",
92+
inputSchema: z.object({
93+
text: z.string(),
94+
}),
95+
execute: async ({ text }: HandlerArgs) => handler({ text }),
96+
});
97+
98+
return {
99+
name: "AI SDK tool()",
100+
invoke: async (args) => {
101+
if (typeof wrapped.execute !== "function") {
102+
throw new Error("AI SDK tool execute() is unavailable");
103+
}
104+
return wrapped.execute(args, {} as never);
105+
},
106+
};
107+
} catch {
108+
return undefined;
109+
}
110+
}
111+
112+
async function run(): Promise<void> {
113+
const bench = new Bench({ time: 1_000, warmupTime: 300 });
114+
const qirrelDispatchers = await createQirrelDispatcher(baseHandler);
115+
const langchainDispatcher = await createLangChainDispatcher(baseHandler);
116+
const aiDispatcher = await createAiSdkDispatcher(baseHandler);
117+
118+
bench.add("Direct handler", async () => {
119+
await baseHandler(sampleArgs);
120+
});
121+
122+
for (const dispatcher of qirrelDispatchers) {
123+
bench.add(dispatcher.name, async () => {
124+
await dispatcher.invoke(sampleArgs);
125+
});
126+
}
127+
128+
if (langchainDispatcher) {
129+
bench.add(langchainDispatcher.name, async () => {
130+
await langchainDispatcher.invoke(sampleArgs);
131+
});
132+
}
133+
134+
if (aiDispatcher) {
135+
bench.add(aiDispatcher.name, async () => {
136+
await aiDispatcher.invoke(sampleArgs);
137+
});
138+
}
139+
140+
await bench.run();
141+
142+
const rows = bench.tasks.map((task) => ({
143+
name: task.name,
144+
"ops/sec": task.result ? Math.round(task.result.hz) : 0,
145+
"avg ms": task.result ? Number(task.result.mean.toFixed(3)) : 0,
146+
"p99 ms": task.result ? Number(task.result.p99.toFixed(3)) : 0,
147+
}));
148+
149+
console.table(rows);
150+
151+
if (!langchainDispatcher || !aiDispatcher) {
152+
const missing = [
153+
!langchainDispatcher ? "langchain" : null,
154+
!aiDispatcher ? "ai" : null,
155+
].filter(Boolean);
156+
console.log(`Skipped optional frameworks: ${missing.join(", ")}`);
157+
}
158+
}
159+
160+
void run();

0 commit comments

Comments
 (0)