Tool-calling Agent
Build a reusable tool-calling agent using createAgent and tool(). The agent automatically loops through think → tool calls → observation until it produces a final answer.
Plugins used: createAgent, tool() (from flowneer/presets/agent)
The code
typescript
import "dotenv/config";
import { OpenAI } from "openai";
import { tool, createAgent } from "flowneer/presets/agent";
import type { LlmAdapter, AgentState } from "flowneer/presets/agent";
// ─── Tools ───────────────────────────────────────────────────────────────────
const calculator = tool(
({ expression }: { expression: string }) => {
// Safe arithmetic only — replace with a proper parser in production
return Function(`"use strict"; return (${expression})`)();
},
{
name: "calculator",
description: "Evaluate a mathematical expression",
params: {
expression: {
type: "string",
description: "A JavaScript arithmetic expression, e.g. '(12 * 4) / 2'",
required: true,
},
},
},
);
const getTime = tool(() => new Date().toUTCString(), {
name: "get_time",
description: "Get the current UTC date and time",
params: {},
});
const webSearch = tool(
async ({ query }: { query: string }) => {
// Replace with a real search API call
return `[mock search results for: ${query}]`;
},
{
name: "web_search",
description: "Search the web for up-to-date information",
params: {
query: { type: "string", description: "Search query", required: true },
},
},
);
// ─── LLM adapter (OpenAI) ────────────────────────────────────────────────────
const openai = new OpenAI();
const callLlm: LlmAdapter = async (messages, toolDefs) => {
const res = await openai.chat.completions.create({
model: "gpt-4o-mini",
messages: messages as any,
tools: toolDefs.map((t) => ({ type: "function" as const, function: t })),
tool_choice: "auto",
});
const msg = res.choices[0]!.message;
if (msg.tool_calls?.length) {
return {
toolCalls: msg.tool_calls.map((tc: any) => ({
id: tc.id,
name: tc.function.name,
args: JSON.parse(tc.function.arguments),
})),
};
}
return { text: msg.content ?? "" };
};
// ─── Agent (create once, reuse forever) ──────────────────────────────────────
const agent = createAgent({
tools: [calculator, getTime, webSearch],
callLlm,
systemPrompt:
"You are a helpful assistant. Use the available tools when needed. " +
"Always show your reasoning before giving a final answer.",
maxIterations: 8,
});
// ─── Run ─────────────────────────────────────────────────────────────────────
const state: AgentState = {
input: "What is 1337 * 42, and what time is it right now?",
messages: [],
};
await agent.run(state);
console.log(state.output);
if (state.__reactExhausted) {
console.warn("Agent hit the iteration limit without finishing.");
}How it works
tool()wraps each function with a name, description, and parameter schema.createAgent()returns aFlowBuilder<AgentState>that wires.withTools()+.withReActLoop()internally.- On each iteration the
callLlmadapter receives the full conversation history and tool schemas. If the model returnstool_calls, Flowneer dispatches them and injects the results intostate.messagesbefore the next iteration. - When the model returns plain text with no tool calls, the loop ends and
state.outputis set.
Variations
Swap the model — change "gpt-4o-mini" to "gpt-4o" or any Claude / Gemini adapter.
Add memory — call .withMemory(new BufferWindowMemory(10)) on the returned FlowBuilder to persist context across runs.
Cap cost — add .withTokenBudget(...) to stop the loop when the token budget is exceeded.
Stream chunks — replace agent.run(state) with:
typescript
for await (const event of agent.stream(state)) {
if (event.type === "chunk") process.stdout.write(String(event.data));
}