LLM Tool Calling (ReAct Agent)
PiSovereign includes a ReAct (Reason + Act) agent that enables the LLM to autonomously invoke tools — weather lookups, calendar queries, web searches, and more — instead of relying solely on rigid command parsing.
How It Works
When a user sends a general question (AgentCommand::Ask), the system follows
this flow:
- Collect tools — The
ToolRegistryasks each wired port which tool definitions are available (e.g., if no weather port is configured,get_weatheris omitted). - LLM + tools — The conversation history and tool JSON schemas are sent to
Ollama’s
/api/chatendpoint with thetoolsparameter. - Parse response — The LLM either returns a final text response or requests one or more tool calls.
- Execute tools — If tool calls are returned, the
ToolExecutordispatches each call to the appropriate port, collects results, and appends them asMessageRole::Toolmessages to the conversation. - Loop — Steps 2–4 repeat until the LLM produces a final response or a configurable iteration limit / timeout is reached.
User → LLM (with tool schemas)
├─ Final text → done
└─ Tool calls → execute → append results → loop back to LLM
Architecture
The implementation follows Clean Architecture:
| Layer | Component | Crate |
|---|---|---|
| Domain | ToolDefinition | domain |
| Domain | ToolCall, ToolResult, ToolCallingResult | domain |
| Domain | MessageRole::Tool, ChatMessage::tool() | domain |
| Application | ToolRegistryPort | application |
| Application | ToolExecutorPort | application |
| Application | InferencePort::generate_with_tools() | application |
| Application | ReActAgentService | application |
| Infrastructure | ToolRegistry | infrastructure |
| Infrastructure | ToolExecutor | infrastructure |
| Infrastructure | OllamaInferenceAdapter (extended) | infrastructure |
| Presentation | Wired in main.rs, used in chat handlers | presentation_http |
Available Tools
The following 18 tools are registered when their corresponding ports are wired:
| Tool | Port Required | Description |
|---|---|---|
get_weather | WeatherPort | Current weather and forecast |
search_web | WebSearchPort | Web search via Brave / DuckDuckGo |
list_calendar_events | CalendarPort | List upcoming calendar events |
create_calendar_event | CalendarPort | Create a new calendar event |
search_contacts | ContactPort | Search contacts by name/email |
get_contact | ContactPort | Get full contact details by ID |
list_tasks | TaskPort | List tasks/todos with filters |
create_task | TaskPort | Create a new task |
complete_task | TaskPort | Mark a task as completed |
create_reminder | ReminderPort | Schedule a reminder |
list_reminders | ReminderPort | List active reminders |
search_transit | TransitPort | Search public transit connections |
store_memory | MemoryStore | Store a fact in long-term memory |
recall_memory | MemoryStore | Recall facts from memory |
execute_code | CodeExecutionPort | Run code in a sandboxed container |
search_emails | EmailPort | Search emails by query |
draft_email | EmailPort + DraftStorePort | Draft an email |
send_email | EmailPort | Send an email |
Configuration
Add to config.toml:
[agent.tool_calling]
# Enable/disable the ReAct agent (default: true)
enabled = true
# Maximum ReAct loop iterations before forcing a final answer
max_iterations = 5
# Timeout per individual tool execution (seconds)
iteration_timeout_secs = 30
# Total timeout for the entire ReAct loop (seconds)
total_timeout_secs = 120
# Run tool calls in parallel when multiple are requested
parallel_tool_execution = true
# Tools that require user approval before execution (future use)
require_approval_for = []
When enabled = false, the system falls back to the standard
ChatService::chat_with_context flow without any tool calling.
Relationship to AgentService
The ReAct agent runs alongside the existing AgentService:
AgentServicehandles all structured commands (AgentCommandvariants likeGetWeather,SearchWeb,CreateTask, etc.) via pattern matching and dedicated handler methods.ReActAgentServicehandles general questions (AgentCommand::Ask) by letting the LLM decide which tools to call.
The command parsing flow remains unchanged — AgentService::parse_command()
still classifies user input. Only Ask commands are routed through the ReAct
agent when it’s enabled.
Extending with New Tools
To add a new tool:
- Define the port in
crates/application/src/ports/(if not already existing). - Add a tool definition in
ToolRegistry— create adef_your_tool()method returning aToolDefinitionwith parameter schemas. - Add execution logic in
ToolExecutor— create anexec_your_tool()method that extracts arguments, calls the port, and formats the result. - Wire the port in
ToolRegistry::collect_tools()andToolExecutor::execute()dispatch. - Connect in
main.rs— pass the port Arc to bothToolRegistryandToolExecutorviawith_your_port()builder methods.
Decorator Forwarding
All inference port decorators forward generate_with_tools() to their inner
adapter:
SanitizedInferencePort— forwards directly (no sanitization for tool iterations)CachedInferenceAdapter— forwards without caching (tool iterations are non-deterministic)SemanticCachedInferenceAdapter— forwards without semantic cachingDegradedInferenceAdapter— forwards with circuit-breaker trackingModelRoutingAdapter— routes to the most capable (fallback) model
Relationship to Agentic Mode
The ReAct agent handles single-turn tool calling — one user query, one LLM loop deciding which tools to invoke. Agentic Mode extends this to multi-agent orchestration:
| Aspect | ReAct Agent | Agentic Mode |
|---|---|---|
| Scope | Single query | Complex multi-step task |
| Agents | 1 LLM loop | Multiple parallel sub-agents |
| Endpoint | POST /v1/chat | POST /v1/agentic/tasks |
| Progress | Synchronous or SSE chat stream | SSE task progress stream |
| Config | [agent.tool_calling] | [agentic] |
Each agentic sub-agent internally uses the same ReAct tool-calling loop. The
orchestrator (AgenticOrchestrator) decomposes the user’s request, spawns
sub-agents, and aggregates their results.
See API Reference — Agentic Tasks for endpoint documentation.