Tool Calling
Enable LLMs to call external tools during execution
Tool Calling
Stevora supports LLM tool calling (function calling) with a multi-round execution loop, automatic argument parsing, and full tracing of every tool invocation. When a model decides it needs to call a tool, Stevora executes it, feeds the result back to the model, and continues until the model produces a final response.
Defining tools in a step
Tools are defined inline on the LLM step using the tools array. Each tool needs a name, description, and parameters object (JSON Schema format):
{
"type": "llm",
"name": "research-agent",
"model": "gpt-4o",
"systemPrompt": "You are a research assistant. Use the provided tools to answer questions.",
"messages": [
{ "role": "user", "content": "{{state.question}}" }
],
"tools": [
{
"name": "search_knowledge_base",
"description": "Search the internal knowledge base for relevant documents",
"parameters": {
"type": "object",
"properties": {
"query": { "type": "string", "description": "The search query" },
"limit": { "type": "number", "description": "Max results to return" }
},
"required": ["query"]
}
},
{
"name": "get_customer_data",
"description": "Fetch customer details by ID",
"parameters": {
"type": "object",
"properties": {
"customerId": { "type": "string" }
},
"required": ["customerId"]
}
}
],
"maxToolRounds": 5
}The schema follows the llmToolDefSchema validation from llm.schema.ts:
const llmToolDefSchema = z.object({
name: z.string().min(1),
description: z.string(),
parameters: z.record(z.unknown()),
});Hosted remote tools
For hosted Stevora, register an HTTPS endpoint for each tool name:
const tool = await stevora.remoteTools.create({
name: 'get_customer_data',
url: 'https://your-app.com/api/stevora/tools/customer',
timeoutMs: 10000,
});
// Store once. Stevora only returns this signing secret at creation.
console.log(tool.secret);When the model calls get_customer_data, Stevora sends:
{
"tool": "get_customer_data",
"arguments": { "customerId": "cus_123" },
"context": {
"workflowRunId": "...",
"stepRunId": "...",
"llmCallId": "...",
"toolCallId": "..."
}
}Your endpoint must return a JSON object. Stevora feeds that object back to the model and continues the same tool loop.
Every request includes:
| Header | Purpose |
|---|---|
X-Stevora-Signature | HMAC-SHA256 of timestamp + "." + rawBody |
X-Stevora-Timestamp | Timestamp included in the signature |
X-Stevora-Tool | Registered tool name |
X-Stevora-Idempotency-Key | Stable key for retry-safe handling |
Remote endpoints must use HTTPS and resolve only to public IP addresses. Responses are limited to 1MB and the configured timeout is capped at 30 seconds. Treat remote tools as idempotent; reject duplicate idempotency keys for any endpoint that changes data.
Multi-round tool loop
When the model's response includes tool calls, Stevora enters a loop:
- The model's response (with tool calls) is appended to the conversation as an
assistantmessage - Each tool call is executed via its registered handler
- Tool results are appended as
toolmessages with the correspondingtoolCallId - The updated conversation is sent back to the model
- If the model requests more tool calls, the loop repeats
This continues until the model returns a response without tool calls, or the maxToolRounds limit is reached.
// From llm-step-handler.ts -- the core loop
while (toolRounds <= (step.maxToolRounds ?? 10)) {
const response = await provider.complete(request);
if (response.toolCalls.length > 0 && step.tools && step.tools.length > 0) {
toolRounds++;
messages = [
...messages,
{
role: 'assistant',
content: response.content ?? '',
toolCalls: response.toolCalls,
},
];
const toolResults = await executeToolCalls(response.toolCalls, {
llmCallId,
stepRunId,
workflowRunId: ctx.workflowRunId,
workspaceId: ctx.workspaceId,
toolRound: toolRounds,
});
messages = [...messages, ...toolResults];
continue;
}
finalResponse = response;
break;
}The default maxToolRounds is 10. You can set it from 1 to 20. If the model exhausts all rounds without producing a final answer, the step fails with the code MAX_TOOL_ROUNDS.
Tool tracing
Every tool invocation is persisted to the toolTrace table with full input/output recording. Each trace captures:
| Field | Description |
|---|---|
toolName | Name of the tool called |
inputJson | Parsed arguments the model passed to the tool |
outputJson | Return value from the tool handler |
durationMs | Wall-clock execution time |
status | completed or failed |
error | Error details if the tool failed |
sequence | Order of execution within a single LLM call |
llmCallId | Links back to the LLM call that triggered this tool |
stepRunId | Links to the parent step run |
workflowRunId | Links to the parent workflow run |
Successful tool executions also emit a TOOL_CALL_COMPLETED workflow event in the same database transaction, so tool activity shows up in the workflow event timeline.
This tracing is automatic -- you do not need to add any instrumentation to your tool handlers. Stevora wraps every call and records the result regardless of success or failure.
Error handling
Tool handlers can throw errors. When a handler throws, Stevora:
- Records a
failedtool trace with the error message - Returns the error to the model as a JSON string:
{ "error": "..." } - Continues the tool loop so the model can decide how to proceed
This means a single tool failure does not crash the step. The model receives the error and can retry, call a different tool, or produce a final answer based on the tools that did succeed.