How OpenHuman Handles Tool Calling and Model Interactions via the TinyAgents Adapter Pattern

OpenHuman bridges its internal tool-calling API and LLM inference flow through the TinyAgents adapter, which performs bidirectional request/response conversion, real-time streaming of tool-call fragments, and preservation of rich error metadata.

The tinyhumansai/openhuman repository implements a thin but critical translation layer that unifies disparate provider APIs under a common tool-calling interface. This TinyAgents adapter pattern enables OpenHuman to treat any compatible LLM provider as a drop-in model while maintaining full support for native and prompt-guided tool invocations.

Core Adapter Architecture

The adapter lives in src/openhuman/agent/tinyagents/model.rs and performs three essential roles: converting outgoing requests into TinyAgents format, transforming incoming responses back to OpenHuman native types, and bridging streaming deltas for real-time UI updates.

Outgoing Request Conversion

When a turn executes, OpenHuman builds a ModelRequest containing chat history and advertised tool schemas. The native_chat_messages function (line 21 in src/openhuman/agent/tinyagents/model.rs) converts each ChatMessage into a TinyAgents-compatible Message via the shared logic in src/openhuman/agent/message_convert.rs.

Tool schemas undergo transformation into a P-format registry through pformat_registry_from_request. This registry enables the fallback text-parser to recover prompt-guided calls when the provider returns unstructured responses rather than native tool invocations.

Incoming Response Translation

After the provider returns a ChatResponse, the adapter creates a ModelResponse through response_to_model_response (line 59). This conversion follows a strict precedence:

  1. Native tool calls – Structured tool_calls in the response are copied verbatim into TaToolCall objects.
  2. Prompt-guided fallback – When no native calls exist but tools were advertised, the raw text parses against the P-format registry via parse_tool_calls_with_pformat.
  3. Visible content – Non-tool text becomes ContentBlock::Text, while reasoning blocks convert to typed ContentBlock instances that persist but remain excluded from Message::text().

Token usage metadata transfers into tinyagents::harness::usage::Usage, while provider-specific fields like charged USD and context window size store under the custom key openhuman_usage_meta within ModelResponse::raw. Helper functions openhuman_usage_meta_raw and merge_openhuman_usage_meta (lines 38-71) encapsulate this logic.

Real-Time Streaming Bridge

For streaming providers, the adapter’s forward_provider_delta function (line 34) relays incremental ProviderDelta events onto a tokio::sync::mpsc::UnboundedSender<ModelStreamItem>. This bridges the gap between provider-specific server-sent events and TinyAgents’ internal stream protocol.

The mapping converts:

  • TextDelta → MessageDelta::text
  • ThinkingDelta → MessageDelta::reasoning
  • ToolCallStart → ToolDelta with tool_name set, creating the UI row before arguments arrive
  • ToolCallArgsDelta → ToolDelta containing the argument fragment

These stream items propagate through OpenhumanEventBridge (implemented in src/openhuman/agent/tinyagents/host/mod.rs) to keep the UI timeline synchronized with the provider’s construction progress.

Error Preservation and Profile Overrides

TinyAgents represents model errors as simple strings (TinyAgentsError::Model(String)), which strips valuable type information. To preserve the original anyhow::Error carrying rich AgentError variants, the adapter stores the typed error in a shared ModelErrorSlot (Arc<Mutex<Option<anyhow::Error>>>) before returning the stringified version. The runner later consults this slot to restore down-castable errors for Sentry suppression and structured logging (see line 88 in src/openhuman/agent/tinyagents/model.rs).

When turns require precise model configurations—such as custom context windows or prompt-guided modes—OpenHuman wraps the underlying ChatModel in a ProfileOverrideModel. This wrapper swaps the ModelProfile without altering the wire protocol, ensuring the adapter continues to see standard request/response shapes while the provider receives corrected limits (defined at lines 95-100).

Implementation Examples

The following patterns demonstrate the adapter’s conversion flow in practice:

// Build a TinyAgents request from OpenHuman native data
let request = ModelRequest {
    messages: native_chat_messages(&model_req),
    tools: advertised_tools.clone(),
    ..Default::default()
};

// Execute via the TinyAgents harness
let response = chat_model.invoke(request).await?;

// Convert provider response back to OpenHuman format
let model_response = response_to_model_response(
    &chat_response,
    &pformat_registry_from_request(&original_request),
    !original_request.tools.is_empty(),
);

For streaming scenarios, forward deltas as they arrive from the provider:

// Forward streaming tool call initiation
forward_provider_delta(&tx, ProviderDelta::ToolCallStart {
    call_id: "call_1".into(),
    tool_name: "search".into(),
});

// Stream incremental argument fragments
forward_provider_delta(&tx, ProviderDelta::ToolCallArgsDelta {
    call_id: "call_1".into(),
    delta: "query=rust".into(),
});

To recover billing and usage metadata from a completed response:

if let Some(usage) = usage_info_from_response(&model_response) {
    println!("Charged ${:.2}, window {} tokens", 
             usage.charged_amount_usd, 
             usage.context_window);
}

Summary

  • The TinyAgents adapter in src/openhuman/agent/tinyagents/model.rs provides the critical translation seam between OpenHuman’s native types and the TinyAgents model API.
  • Bidirectional conversion handles both native structured tool calls and legacy prompt-guided invocations via the P-format registry.
  • Streaming bridge forwards incremental provider deltas through forward_provider_delta to enable real-time UI updates during tool construction.
  • Rich error preservation uses ModelErrorSlot to maintain typed anyhow::Error information across the adapter boundary.
  • Profile overrides allow dynamic adjustment of model parameters without breaking the adapter’s protocol expectations.

Frequently Asked Questions

What is the TinyAgents adapter pattern in OpenHuman?

The TinyAgents adapter pattern is a translation layer that allows OpenHuman to use any TinyAgents-compatible LLM provider while preserving OpenHuman’s native tool-calling semantics. It converts ChatMessage objects to TinyAgents format, handles streaming deltas, and ensures usage metadata survives round-trips across the model boundary.

How does OpenHuman handle streaming tool call fragments?

OpenHuman consumes incremental ProviderDelta events from the provider and forwards them via forward_provider_delta in src/openhuman/agent/tinyagents/model.rs. This function maps ToolCallStart and ToolCallArgsDelta events to TinyAgents ToolDelta items, allowing the UI to display tool construction in real-time before the final arguments complete.

What is the difference between native and prompt-guided tool calling?

Native tool calling occurs when the LLM returns structured tool_calls in its response, which the adapter copies directly into TaToolCall objects. Prompt-guided fallback triggers when the provider returns unstructured text; the adapter then parses that text against the P-format registry to extract tool invocations, supporting providers without native function-calling capabilities.

How does the adapter preserve error information across the boundary?

Before converting errors to TinyAgents’ simple string format, the adapter stores the original anyhow::Error in a thread-safe ModelErrorSlot. This preserves rich error variants for downstream observability tools, allowing the runner to retrieve the typed error for Sentry suppression and structured logging after the TinyAgents harness returns control.

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 →