The agent step and tool calling
The
agentstep runs a model and its tools in a loop as one step of a route: the model asks for tools, Cameleer runs them through theai-toolenvelope (argument checks, deduplication, approvals, timeouts, audit), and the loop stops on the model's answer or a limit. The same loop runs inside every model component withtags. Recorded model responses letcameleer testcheck an agent route without a model.
Cargo feature: ai for the tools; the model components are in saas (openai, llm, anthropic, gemini) and cloud (azure-openai, aws-bedrock, google-vertexai) ยท Apache Camel: no agent EIP; the tags options mirror Camel 4.23's openai/llm component
The agent step
- route:
from:
uri: direct:support
steps:
- agent:
model: "openai:chat-completion?model=gpt-5.4-mini"
tools: support # ai-tool routes with this tag
systemMessage: You are a support agent. Look orders up before you refund them.
maxTurns: 8
maxTokens: 40000
deadline: 60s
memory: memory # keep conversations (CamelAiConversationId)
onApproval: direct:approvals # told when a tool call waits for approvalThe body is the user's message: text, or a list of {"role", "content"} messages. After the
step the body is the model's answer.
| option | default | description |
|---|---|---|
model | required | A model endpoint: openai: / llm: (chat-completion, responses, apiFormat=ollama, or any provider preset), anthropic:, gemini:, azure-openai:, aws-bedrock: (Converse), google-vertexai: (Gemini on Vertex AI) |
tools | none | Comma-separated ai-tool tags; * offers every tool; mcp:SERVER offers the tools imported from an MCP server (MCP client) |
systemMessage | the model endpoint's | The system prompt |
maxTurns | 10 | Model calls before the loop stops |
maxTokens | none | Total tokens (input and output) before the loop stops |
deadline | none | How long the loop may run |
memory | none | memory (in memory) or #bean (an AggregationRepository): keeps each conversation by CamelAiConversationId |
maxHistoryMessages | 100 | History sent to the model; older messages are dropped, never between a tool call and its result |
onApproval | none | An endpoint told when a tool call waits for approval |
parallelToolCalls | 4 | Tools of one turn run in parallel on at most this many threads |
maxResultChars | 20000 | Longer tool results are cut, with a note to the model |
failOnUnknownTool | false | Fail the exchange when the model names a tool that doesn't exist, instead of telling it |
record | file:PATH: append every model response to a recording | |
replay | file:PATH: answer from a recording instead of calling the model |
In the Rust DSL:
use cameleer_core::processor::agent::AgentDefinition;
b.from("direct:support").agent(
AgentDefinition::new("openai:chat-completion?model=gpt-5.4-mini").tools("support").memory("memory").max_turns(8),
);What the loop guarantees
- Every tool call goes through the
ai-toolenvelope: the arguments are validated, a retried call with the same call id runs once, destructive calls wait for approval, slow calls are cancelled, and each call is audited. - Every tool call gets a result in the conversation, including calls to tools that don't exist and calls that failed. Every provider requires one result per call id.
- The loop stops on the model's answer,
maxTurns,maxTokens,deadline, the same call failing three times, or a rejected approval.
| header | description |
|---|---|
CamelAiConversationId | in: the conversation, for memory, approvals and compensation |
CamelAiAgentStopReason | answer, max-turns, max-tokens, deadline, repeated-failure, approval-required, approval-pending, approval-denied |
CamelAiAgentTurns, CamelAiAgentToolCalls | Model calls and tool calls made |
CamelAiAgentInputTokens, CamelAiAgentOutputTokens, CamelAiAgentTotalTokens | Token usage across turns |
CamelAiApprovalHandle | The handle of a tool call that waits for approval |
Approvals pause the conversation
When a tool call needs approval (a destructiveHint tool, or an approval rule on the
ai-tool), the agent stops with approval-required. onApproval receives the handle
(CamelAiApprovalHandle), the tool's name and its arguments. With memory, the conversation
waits: calling the agent again answers approval-pending until a person decides. Once they
do, the next call resumes the conversation with the tool's result.
A person approves or rejects with an ai-tool producer:
- route:
from:
uri: platform-http:/approvals/approve
steps:
- setHeader:
name: CamelAiToolApprovalHandle
simple: "${header.handle}"
- setHeader:
name: CamelAiToolApprovedBy
simple: "${header.user}"
- to: ai-tool:refund?operation=approve # operation=deny rejects (the body is the reason)Tool calling in the model components
Every model component takes the same tool options, Camel's names on openai/llm. With
tags it offers those tools and runs the loop itself. The body is the answer, as with the
agent step, but nothing is remembered between calls.
- route:
from:
uri: direct:ask
steps:
- to: "aws-bedrock://claude?modelId=anthropic.claude-sonnet-4-5-v1:0&operation=converse&tags=support&maxToolIterations=10"| option | default | description |
|---|---|---|
tags | none | ai-tool tags to offer |
autoToolExecution | true | Run the tools and loop; false makes one call and returns the requested calls (CamelOpenAIToolCalls on openai, CamelAiRequestedToolCalls elsewhere) |
maxToolIterations | 50 | Model calls in the loop |
maxAgenticTokens | 0 (unlimited) | Total tokens before the loop stops |
parallelToolExecution | false | Run a turn's tools in parallel (on at most 4 threads); by default they run one after another, as in Camel |
hallucinatedToolNameStrategy | repromptModel | repromptModel tells the model the tool doesn't exist and lets it answer; failExchange fails the exchange (Camel's names; report and fail also work) |
record, replay | file:PATH, as on the agent step | |
maxRetries, retryDelay, fallbackModels, responseCache, responseCacheTtl, responseCacheSize, coalesce | Retries, fallback models, the response cache and coalescing (model providers) |
| component | wire format |
|---|---|
openai, llm | Chat Completions tools and tool_calls (also every provider preset, Ollama, vLLM, LM Studio; missing call ids are generated; DeepSeek, Qwen and Kimi reasoning_content is sent back with the calls); with streaming=true every turn is streamed, tool call deltas are joined by index and text deltas go to streamingChunkProcessor |
openai:responses, llm:responses | Responses API tools, function_call and function_call_output items matched by call_id, instructions |
openai/llm with apiFormat=ollama | Ollama's /api/chat: tool_calls with object arguments, tool messages with tool_name, generated call ids |
azure-openai | the same as openai, on Azure deployments |
aws-bedrock (Converse) | toolConfig, toolUse, toolResult; cachePoints with promptCaching=true |
google-vertexai, gemini | functionDeclarations, functionCall (its thoughtSignature is sent back), functionResponse |
anthropic | Messages API tools, tool_use, tool_result; cache_control breakpoints (promptCaching=auto) |
Testing without a model
Record a conversation once (record: file:support.jsonl), or write the responses by hand: one
JSON line per model response. Then replay it in a test with cameleer test:
# support-agent.test.yaml
- test:
name: a refund waits for approval, runs once, and the agent answers
routes: [support-agent.camel.yaml]
properties:
agent.replay: file:refund.jsonl # the route has replay: "{{agent.replay:}}"
payments.endpoint: mock:payments
steps:
- send:
to: direct:support
body: My order A1 arrived broken. Please refund it.
headers: {CamelAiConversationId: case-1}
expect:
headers: {CamelAiAgentStopReason: approval-required}
mocks: {"mock:payments": 0}
- approve: {tool: refund, by: alice}
expect: {mocks: {"mock:payments": 1}}
- send:
to: direct:support
body: continue
headers: {CamelAiConversationId: case-1}
expect:
body: Order A1 arrived damaged, so I refunded it in full.
- callTool: {name: refund, arguments: {orderId: B7}, callId: retry-1, approvedBy: alice}
- callTool: {name: refund, arguments: {orderId: B7}, callId: retry-1, approvedBy: alice}
expect: {duplicate: true, mocks: {"mock:payments": 2}}cameleer test support-agent.test.yamlSteps are send (to, body, headers), callTool (name, arguments, callId,
conversation, principal, approvedBy), approve / deny (tool, by, reason) and
compensate (conversation). expect checks body, bodyContains, headers, error,
status, isError, duplicate and mocks (messages per mock endpoint). Test files end in
.test.yaml, so cameleer run and cameleer validate skip them. file: paths are relative
to the test file. The command exits 1 when a test fails. The complete example is
examples/agents/support-agent.camel.yaml
with its test.
Not supported yet
- Streaming with Ollama's native API (
apiFormat=ollama); stream from Ollama through its OpenAI-compatible/v1API (the default format) instead. - Streaming in the tool loop of
azure-openai,aws-bedrock,google-vertexai,geminiandanthropic: withtagsthey wait for each complete answer.openai/llmstream every turn withstreaming=true. - Server-side conversation state of the Responses API (
previous_response_id): Cameleer sends the whole conversation each turn.
Related
- ai-tool: what a tool is and the envelope every call goes through
- MCP server: the same tools for MCP clients
- MCP client: tools of other MCP servers in the loop
- Model providers and call optimizations
- Cameleer for AI agents
Found a problem?
If something on this page doesn't match what Cameleer does, please
open an issue, or fork the repository, fix
it, add a test, run cargo test --workspace and open a pull request.