Cameleer

The agent step and tool calling

The agent step runs a model and its tools in a loop as one step of a route: the model asks for tools, Cameleer runs them through the ai-tool envelope (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 with tags. Recorded model responses let cameleer test check 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

yaml example: agent-step-support
- 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 approval

The body is the user's message: text, or a list of {"role", "content"} messages. After the step the body is the model's answer.

optiondefaultdescription
modelrequiredA 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)
toolsnoneComma-separated ai-tool tags; * offers every tool; mcp:SERVER offers the tools imported from an MCP server (MCP client)
systemMessagethe model endpoint'sThe system prompt
maxTurns10Model calls before the loop stops
maxTokensnoneTotal tokens (input and output) before the loop stops
deadlinenoneHow long the loop may run
memorynonememory (in memory) or #bean (an AggregationRepository): keeps each conversation by CamelAiConversationId
maxHistoryMessages100History sent to the model; older messages are dropped, never between a tool call and its result
onApprovalnoneAn endpoint told when a tool call waits for approval
parallelToolCalls4Tools of one turn run in parallel on at most this many threads
maxResultChars20000Longer tool results are cut, with a note to the model
failOnUnknownToolfalseFail the exchange when the model names a tool that doesn't exist, instead of telling it
recordfile:PATH: append every model response to a recording
replayfile:PATH: answer from a recording instead of calling the model

In the Rust DSL:

rust
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-tool envelope: 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.
headerdescription
CamelAiConversationIdin: the conversation, for memory, approvals and compensation
CamelAiAgentStopReasonanswer, max-turns, max-tokens, deadline, repeated-failure, approval-required, approval-pending, approval-denied
CamelAiAgentTurns, CamelAiAgentToolCallsModel calls and tool calls made
CamelAiAgentInputTokens, CamelAiAgentOutputTokens, CamelAiAgentTotalTokensToken usage across turns
CamelAiApprovalHandleThe 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:

yaml example: agent-step-approve
- 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.

yaml example: agent-step-tags
- route:
    from:
      uri: direct:ask
      steps:
        - to: "aws-bedrock://claude?modelId=anthropic.claude-sonnet-4-5-v1:0&operation=converse&tags=support&maxToolIterations=10"
optiondefaultdescription
tagsnoneai-tool tags to offer
autoToolExecutiontrueRun the tools and loop; false makes one call and returns the requested calls (CamelOpenAIToolCalls on openai, CamelAiRequestedToolCalls elsewhere)
maxToolIterations50Model calls in the loop
maxAgenticTokens0 (unlimited)Total tokens before the loop stops
parallelToolExecutionfalseRun a turn's tools in parallel (on at most 4 threads); by default they run one after another, as in Camel
hallucinatedToolNameStrategyrepromptModelrepromptModel 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, replayfile:PATH, as on the agent step
maxRetries, retryDelay, fallbackModels, responseCache, responseCacheTtl, responseCacheSize, coalesceRetries, fallback models, the response cache and coalescing (model providers)
componentwire format
openai, llmChat 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:responsesResponses API tools, function_call and function_call_output items matched by call_id, instructions
openai/llm with apiFormat=ollamaOllama's /api/chat: tool_calls with object arguments, tool messages with tool_name, generated call ids
azure-openaithe same as openai, on Azure deployments
aws-bedrock (Converse)toolConfig, toolUse, toolResult; cachePoints with promptCaching=true
google-vertexai, geminifunctionDeclarations, functionCall (its thoughtSignature is sent back), functionResponse
anthropicMessages 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:

yaml
# 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}}
bash
cameleer test support-agent.test.yaml

Steps 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 /v1 API (the default format) instead.
  • Streaming in the tool loop of azure-openai, aws-bedrock, google-vertexai, gemini and anthropic: with tags they wait for each complete answer. openai/llm stream every turn with streaming=true.
  • Server-side conversation state of the Responses API (previous_response_id): Cameleer sends the whole conversation each turn.

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.

Edit this page on GitHub