Tool Calling Internals
Most users can skip this page. For day-to-day usage, start with Basic Agent, Tools, and AgentBuilder & Capabilities.The agent’s ability to use tools is built on a clean separation of concerns: a driver decides which tools to call (by consulting the LLM), and an executor runs the actual tools. Two contracts define this boundary, and three driver implementations satisfy the first contract in different ways.
Architecture Overview
AgentLoop owns both the driver and the executor. Before the first step, it binds the tool runtime to the driver via CanAcceptToolRuntime::withToolRuntime(), ensuring the driver has access to the same Tools collection and ToolExecutor that the loop manages. This binding happens once per execute() / iterate() call.
The Two Contracts
CanUseTools (Driver Contract)
The driver receives the currentAgentState, consults the LLM (or a scripted scenario), and returns an updated state with a new AgentStep attached. The step may contain tool calls, a final response, or an error:
- Compiling messages from state via
CanCompileMessages - Sending the messages to the LLM with tool schemas
- Parsing the LLM response for tool calls
- Delegating tool execution to the
ToolExecutor - Formatting execution results as follow-up messages
- Building and attaching the
AgentStepto the returned state
CanExecuteToolCalls (Executor Contract)
The executor receives a set ofToolCalls and the current AgentState, runs each tool, and returns the results:
- Resolving tool instances from the
Toolscollection - Injecting context (agent state, tool call metadata) into tools that request it
- Validating arguments against the tool schema
- Running the tool and capturing the result
- Handling errors, interception hooks, and events
ToolCallingDriver
ToolCallingDriver uses the LLM’s native function calling API. This is the default driver created by AgentLoop::default() and is the recommended choice for models that support function calling (GPT-4o, Claude, Gemini, etc.).
How It Works
Each invocation ofuseTools() follows this sequence:
-
Compile messages. The message compiler (default:
ConversationWithCurrentToolTrace) produces aMessagescollection from the agent state. This compiler includes the full conversation history plus trace messages from the current execution only. -
Build the inference request. The driver assembles an
InferenceRequestwith the compiled messages, tool schemas from theToolscollection, the model name, tool choice strategy, and any cached context. -
Send to the LLM. The request is dispatched through the
InferenceRuntime, which handles provider-specific API formatting, retries, and streaming. -
Parse tool calls. The
InferenceResponseis inspected fortoolCalls. If present, they are forwarded to theToolExecutor. -
Execute tools. The
ToolExecutorruns each tool call and returnsToolExecutions. -
Format results. The
ToolExecutionFormatterconverts eachToolExecutioninto a pair of messages: an assistant message withtool_callsmetadata, and atoolrole message with the execution result (or error). -
Build the step. An
AgentStepis created with the input messages, output messages, inference response, and tool executions, then attached to the state viawithCurrentStep().
Configuration
Note: You will needuse Cognesy\Polyglot\Inference\Data\ToolChoice;for theToolChoicevalue object.
Tool Choice Strategies
ThetoolChoice parameter accepts a ToolChoice value object:
| Factory Method | Behavior |
|---|---|
ToolChoice::auto() | The LLM decides whether to call a tool or respond directly (default) |
ToolChoice::required() | The LLM must call at least one tool |
ToolChoice::none() | Tool calling is disabled; the LLM responds with text only |
ToolChoice::specific('toolName') | The LLM must call the specified tool |
Tool Args Leak Protection
Some LLM providers accidentally echo tool call arguments as the response content. TheToolCallingDriver detects this by parsing the content as JSON and comparing it against the tool call arguments. If they match, the content is silently discarded to prevent duplicate data in the conversation.
ReActDriver
ReActDriver implements the ReAct (Reasoning + Acting) pattern using structured output extraction. Instead of relying on native function calling, it prompts the LLM to output a JSON decision with explicit thought, type, tool, args, and answer fields.
How It Works
-
Build system prompt. The
MakeReActPromptaction generates a system prompt that describes the available tools and the expected ReAct JSON format. -
Extract decision. The
StructuredOutputRuntimeextracts aReActDecisionobject from the LLM response. This uses the configuredOutputMode(typically JSON) and includes retry logic for extraction failures. -
Validate decision. The
ReActValidatorchecks that the decision has a valid type, references an existing tool, and includes valid arguments. -
Route by type.
- If the decision type is
call_tool: convert it toToolCalls, execute via theToolExecutor, and format the results as Thought/Action/Observation messages. - If the decision type is
final_answer: extract the answer text and build a final response step.
- If the decision type is
-
Optional final inference. When
finalViaInferenceistrue, the driver makes a separate LLM call to produce the final answer, using the full conversation as context. This can improve answer quality at the cost of an extra API call.
Configuration
Error Handling
TheReActDriver handles two categories of extraction failures:
-
Extraction failure. If the
StructuredOutputRuntimecannot parse the LLM output into aReActDecision, the driver builds a failure step with adecision_extractionpseudo-tool execution and marks the state as failed. -
Validation failure. If the decision is extracted but fails validation (invalid type, unknown tool, missing arguments), the driver builds a failure step with a
decision_validationpseudo-tool execution and marks the state as failed.
DecisionExtractionFailed, ValidationFailed) for observability.
ToolExecutor
ToolExecutor is the default CanExecuteToolCalls implementation. It is created automatically by AgentLoop::default() and handles the complete lifecycle of executing a tool call, including interception hooks, event emission, and error handling.
Execution Pipeline
For each tool call in theToolCalls collection, the executor runs this pipeline:
Tool Context Injection
Tools can opt into receiving execution context by implementing one or both of these interfaces:CanAccessAgentState — The tool receives a read-only copy of the current AgentState before invocation. This is useful for tools that need to inspect the conversation history, metadata, or execution status:
CanAccessToolCall — The tool receives the ToolCall object that triggered it. Useful for correlation and tracing, especially in subagent tools that emit their own events:
Configuration
Error Handling Modes
ThethrowOnToolFailure and stopOnToolBlock flags control how the executor responds to problems:
| Flag | Default | When true |
|---|---|---|
throwOnToolFailure | false | Throws a ToolExecutionException immediately when a tool returns a Failure result. The exception propagates to the AgentLoop, which catches it and marks the step as failed. |
stopOnToolBlock | false | When a beforeToolUse interceptor blocks a tool call, the executor stops processing remaining tool calls in the batch and returns what it has so far. |
false (the default), the executor collects all results — successes, failures, and blocked executions — and returns them as a ToolExecutions collection. The driver then formats them as messages and includes them in the step output, allowing the LLM to see and react to the errors on the next iteration.
ToolExecution Result
Each tool execution produces aToolExecution value object containing:
Message Formatting
After tool execution, the results must be formatted as messages that the LLM can understand on the next iteration. Each driver handles this differently:ToolCallingDriver: Native Format
TheToolExecutionFormatter produces two messages per tool execution:
- Assistant message with
tool_callsmetadata — represents the LLM’s decision to call the tool. - Tool message with the execution result — either the successful return value or an error description.
tool_execution_id metadata tag for correlation.
ReActDriver: Observation Format
TheReActFormatter produces messages in the Thought/Action/Observation pattern:
- Assistant message containing the thought and action text from the
ReActDecision. - User message (observation) containing the tool execution result, formatted as
Observation: <result>.
Events
Both drivers and the executor emit events at key lifecycle points. These can be observed viaAgentLoop::wiretap() or AgentLoop::onEvent():
| Event | Emitted By | When |
|---|---|---|
InferenceRequestStarted | Driver | Before sending the request to the LLM |
InferenceResponseReceived | Driver | After receiving the LLM response |
ToolCallStarted | ToolExecutor | Before executing a tool |
ToolCallCompleted | ToolExecutor | After a tool execution completes |
DecisionExtractionFailed | ReActDriver | When structured output extraction fails |
ValidationFailed | ReActDriver | When a ReAct decision fails validation |
When to Use Which Driver
| ToolCallingDriver | ReActDriver | |
|---|---|---|
| Requires | LLM with native function calling support | Any LLM capable of JSON output |
| Tool selection | Native API — reliable, low latency | Structured output extraction — extra parsing step |
| Reasoning | Implicit in the LLM’s response | Explicit thought field in the decision |
| Reliability | Higher (native API contract) | Lower (depends on extraction quality) |
| Flexibility | Standard tool schemas only | Custom decision schemas possible |
| Retry support | Handled by provider retry policy | Built-in maxRetries for extraction failures |
| Best for | Production agents with capable models | Models without function calling, or when explicit reasoning traces are needed |
Custom Drivers
You can implementCanUseTools to create a custom driver. If your driver uses tools, also implement CanAcceptToolRuntime so the AgentLoop can inject the tool collection and executor:
AgentLoop will call withToolRuntime() before the first step, passing the same Tools and ToolExecutor it manages internally.