How the OpenHuman Agent Dispatcher Delegates Tool Execution via the TinyAgents Harness

OpenHuman delegates tool execution parsing and formatting to the TinyAgents harness through the ToolDispatcher trait in src/openhuman/agent/dispatcher.rs, which acts as a lossless adapter between OpenHuman's durable conversation structs and TinyAgents' lightweight dialect layer.

The OpenHuman agent loop requires a mechanism to translate raw LLM responses into executable tool calls and to format tool results back into messages the model can process. This translation layer is implemented in the TinyAgents harness integration, specifically within the dispatcher module of the tinyhumansai/openhuman repository. By abstracting the conversion logic behind the ToolDispatcher trait, OpenHuman supports multiple tool-calling protocols—XML, P-Format, and Native—without duplicating parsing code or coupling the core agent logic to a specific dialect.

The ToolDispatcher Trait: Core Abstraction

The ToolDispatcher trait defined in src/openhuman/agent/dispatcher.rs specifies the contract that each dialect implementation must fulfill. This abstraction allows the agent loop to remain agnostic to whether the model outputs XML tags, compact syntax, or structured JSON.

The trait exposes five essential operations:

  • parse_response – Converts a ChatResponse (raw LLM output) into a tuple of plain text and a vector of ParsedToolCall objects.
  • format_results – Transforms a slice of ToolExecutionResult structs into a ConversationMessage for the next turn.
  • prompt_instructions – Renders the system-prompt block that instructs the model on how to format tool calls.
  • to_provider_messages – Serializes the internal conversation history into provider-specific request formats (e.g., OpenAI message arrays).
  • should_send_tool_specs and tool_call_format – Helper methods consulted by the harness to determine protocol capabilities.

The dispatcher implements no security, approval-gating, sandboxing, or timeout logic—those responsibilities reside in separate core modules. Its sole purpose is performing lossless data conversion between OpenHuman's durable structs and the lightweight DialectMessage, ToolOutcome, and TranscriptEntry types used by TinyAgents.

Bridging Two Vocabularies: The Conversion Seam

The dispatcher acts as a bidirectional adapter, handling three critical translation paths through pure data-mapping functions.

Incoming Response Parsing

When the LLM returns a response, the flow moves from ChatResponse (OpenHuman core) to DialectResponse (TinyAgents) to extract tool calls. This logic is encapsulated in dispatch_parse (lines 82‑86):

fn dispatch_parse<D: ToolDialect>(
    dialect: &D,
    response: &ChatResponse,
) -> (String, Vec<ParsedToolCall>) {
    // Converts ChatResponse -> DialectResponse -> ParsedToolCall
}

Outgoing Result Formatting

After tools execute, ToolExecutionResult objects must become ConversationMessage instances. The dispatch_format_results helper (lines 89‑94) handles this by mapping through ToolOutcome and TranscriptEntry intermediates:

fn dispatch_format_results<D: ToolDialect>(
    dialect: &D,
    results: &[ToolExecutionResult],
) -> ConversationMessage {
    // Converts ToolExecutionResult -> ToolOutcome -> TranscriptEntry -> ConversationMessage
}

History Serialization

For provider requests, the dispatcher converts a vector of ConversationMessage into TranscriptEntry objects, then into provider-specific ChatMessage formats. This occurs in dispatch_provider_messages (lines 96‑101).

All conversion decisions (e.g., handling unknown roles) are delegated to the TinyAgents dialect implementation, keeping OpenHuman's conversion logic centralized and minimal.

Concrete Dialect Implementations

OpenHuman provides three concrete dispatchers, each forwarding to a corresponding TinyAgents dialect via the shared helper functions.

XmlToolDispatcher

The XmlToolDispatcher wraps XmlDialect to parse <tool_call> tags containing JSON bodies. It renders the full XML protocol within the system prompt, embedding the complete tool catalogue using XmlDialect::instructions.

PFormatToolDispatcher

The PFormatToolDispatcher utilizes PFormatDialect for the compact "tool_name[arg1|arg2]" syntax. Unlike XML, the catalogue rendering is handled separately by the ToolsSection struct, while the dispatcher manages the parsing and formatting of individual calls.

NativeToolDispatcher

The NativeToolDispatcher leverages NativeDialect to interface with provider-specific structured tool-calling APIs (OpenAI, Anthropic, etc.). It includes fallback text parsing for robustness when native APIs return unexpected formats. The implementation is typically a one-liner forwarding to dispatch_parse:

fn parse_response(&self, response: &ChatResponse) -> (String, Vec<ParsedToolCall>) {
    dispatch_parse(&NativeDialect, response)
}

Configuring and Instantiating the Dispatcher

Dispatcher selection occurs during harness construction in src/openhuman/agent/harness/session/builder/factory.rs. The builder matches on the DispatcherKind enum to instantiate the appropriate boxed trait object:

let tool_dispatcher: Box<dyn ToolDispatcher> = match cfg.dispatcher {
    DispatcherKind::Native => Box::new(NativeToolDispatcher),
    DispatcherKind::Xml => Box::new(XmlToolDispatcher),
    DispatcherKind::Pformat => {
        Box::new(PFormatToolDispatcher::new(PFormatRegistry::new()))
    },
};

This boxed dispatcher is stored in the session and injected into the agent loop, ensuring that all subsequent tool-related operations use the selected protocol.

Integration with the Agent Loop

Once instantiated, the dispatcher serves four critical functions within the core execution pipeline:

  1. Parsing LLM turns – agent::task_dispatcher::dispatch_card invokes dispatcher.parse_response to extract text and tool calls from model outputs.
  2. Formatting results – The same dispatch card calls dispatcher.format_results to package tool outputs into the conversation transcript.
  3. Generating system prompts – agent::harness::subagent_runner::ops::runner calls dispatcher.prompt_instructions to embed tool-calling guidelines in the system message.
  4. Serializing history – The runner calls dispatcher.to_provider_messages to convert internal state into provider-compatible request payloads.

This delegation ensures that OpenHuman's core remains focused on orchestration and security while TinyAgents handles the syntactic details of tool dialects.

Summary

  • The ToolDispatcher trait in src/openhuman/agent/dispatcher.rs abstracts tool-calling protocols, enabling OpenHuman to support XML, P-Format, and Native dialects through a uniform interface.
  • Pure conversion helpers (dispatch_parse, dispatch_format_results, dispatch_provider_messages) map between OpenHuman's durable structs and TinyAgents' lightweight types without business logic.
  • Three concrete implementations (XmlToolDispatcher, PFormatToolDispatcher, NativeToolDispatcher) forward to TinyAgents dialects, with selection logic residing in src/openhuman/agent/harness/session/builder/factory.rs.
  • The dispatcher performs no security or sandboxing—it exclusively handles lossless data translation between the LLM interface and the agent core.
  • Using TinyAgents provides a stable, reusable dialect layer that improves maintainability and allows protocol improvements to propagate instantly across the codebase.

Frequently Asked Questions

What is the ToolDispatcher trait in OpenHuman?

The ToolDispatcher trait is a protocol abstraction defined in src/openhuman/agent/dispatcher.rs that standardizes how the agent loop parses LLM responses, formats tool results, generates system prompts, and serializes conversation history. It allows OpenHuman to support multiple tool-calling syntaxes (XML, P-Format, Native) without modifying the core agent logic, delegating the actual parsing to the TinyAgents harness.

How does OpenHuman convert between its internal structs and TinyAgents dialects?

OpenHuman uses three pure helper functions—dispatch_parse, dispatch_format_results, and dispatch_provider_messages—located in src/openhuman/agent/dispatcher.rs. These functions perform lossless bidirectional translation between OpenHuman's durable types (ChatResponse, ToolExecutionResult, ConversationMessage) and TinyAgents' lightweight intermediates (DialectResponse, ToolOutcome, TranscriptEntry), delegating dialect-specific decisions to the TinyAgents implementation.

Where is the dispatcher instance created in the OpenHuman codebase?

The dispatcher instance is instantiated in src/openhuman/agent/harness/session/builder/factory.rs during harness construction. The builder matches on the DispatcherKind configuration variant (Native, Xml, or Pformat) to create a boxed dyn ToolDispatcher that is stored in the session and subsequently used by the agent loop for all tool-related operations.

Why does OpenHuman use TinyAgents for tool calling instead of implementing parsing directly?

OpenHuman uses TinyAgents to provide a stable, language-agnostic dialect layer that can be reused across multiple hosts and projects. By delegating to TinyAgents, OpenHuman avoids duplicating parsing and formatting code, ensures that improvements to dialect robustness (such as more tolerant XML parsers) reflect immediately in the agent, and keeps the core codebase focused on orchestration rather than syntactic protocol details.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →