Embedding OpenHuman: Harness vs CoreBuilder and Tokio Runtime Stack Requirements

Harness provides a high-level, one-call API for library-style embeddings that automatically manages the CoreRuntime lifecycle and workspace ownership, while CoreBuilder offers a low-level composable surface for hosts that already own a runtime, and both require configuring Tokio with AGENT_WORKER_STACK_BYTES (larger than the default 2 MiB) to prevent stack overflow during agent turns.

The OpenHuman framework from tinyhumansai/openhuman supports two distinct embedding strategies for integrating AI agents into Rust applications. Whether you are building a standalone script or embedding agents into an existing application like the desktop Tauri shell, choosing between the convenience wrapper and the composable builder interface determines how you manage the asynchronous runtime, workspace persistence, and background services.

Architectural Differences Between Harness and CoreBuilder

Purpose and Abstraction Level

Harness acts as a one-call front door designed for library-style embeddings. It constructs a CoreRuntime internally, applies default configurations for the provider and access tier, and exposes a simple run / turn API. According to the source in src/embed/harness/mod.rs, the harness is intended for hosts that do not already manage a CoreRuntime and need a managed workspace lifecycle.

CoreBuilder, defined in src/core/runtime/builder.rs, is the lower-level composable surface. It is used when the host application already owns a CoreRuntime—such as the desktop Tauri shell or a custom API server—and requires fine-grained control over services, domains, and tool groups. The host manipulates the runtime directly via the embed::Core facade.

Runtime Ownership and Creation

When you invoke Harness::builder(), the harness internally instantiates a CoreBuilder, configures it with defaults, calls .build().await, and stores the resulting Core inside the harness instance. The harness then supplies the high-level run method.

With CoreBuilder, you invoke CoreBuilder::new(host_kind) directly, chain configuration methods like .services(), .domains(), and .workspace(), and call .build().await to obtain a CoreRuntime that the host fully controls.

Workspace Lifecycle Management

The Harness owns a resolved workspace (ResolvedWorkspace) that is created during the build phase. If configured with Workspace::Ephemeral, the workspace is automatically removed when the harness is dropped, guaranteeing the workspace lives exactly as long as the harness session.

In contrast, CoreBuilder allows the caller to set an explicit workspace via .workspace(dir) or .action_dir(dir). The host manages the lifetime, enabling multiple CoreRuntime instances to share the same workspace directory if desired.

Process-Wide Constraints

The harness enforces a one-harness-per-process rule using a process-wide AtomicBool (HARNESS_LIVE). Attempting to create a second harness returns HarnessError::AlreadyRunning. This restriction protects global state such as the keyring master key, RPC bearer, and event bus.

CoreBuilder imposes no such restriction; multiple CoreRuntime instances can coexist, though they will share process-scoped resources unless the host manually isolates them.

Background Services and Transport

The harness is a harness-only embedder that does not start any transport; it uses ServiceSet::none() by default, meaning no HTTP or Socket.IO services are spawned.

CoreBuilder allows the host to enable a full set of services via ServiceSet::desktop(), a headless API via ServiceSet::headless_api(), or custom combinations. Transports and background loops start only when the host explicitly calls CoreRuntime::serve.

Embedding with Harness

Use the harness when you need a standalone library or script that runs a few turns without managing transports or background services. The builder is located in src/embed/harness/builder.rs.

use openhuman_core::{Access, Harness, Provider, Session, Workspace};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Build the harness – it creates its own CoreRuntime internally.
    let harness = Harness::builder()
        .provider(Provider::openai_compatible("https://api.example/v1", "sk-…")
            .model("gpt-5"))
        .workspace(Workspace::Ephemeral)           // temporary workspace
        .access(Access::readonly())                // read‑only access tier
        .session(Session::local("my‑host"))        // local session satisfies provider gate
        .backend_url("https://my-backend.example") // optional non‑default backend
        .build()
        .await?;

    // First turn.
    let first = harness.run("Summarize what you can see.").await?;
    println!("Reply: {}", first.reply);

    // Continue the same conversation.
    let second = harness
        .turn("Now list the risks.")
        .session(&first.session_id) // keep the same session
        .send()
        .await?;
    println!("Reply: {}", second.reply);

    Ok(())
}

Embedding with CoreBuilder

Use CoreBuilder when building host applications that require explicit control over service selection and runtime sharing, such as the desktop UI or a headless API server.

use openhuman_core::{
    core::runtime::{CoreBuilder, ServiceSet, DomainSet},
    embed::Core,
    HostKind,
};

#[tokio::main]
async fn main() -> anyhow::Result<()> {
    // Create a CoreBuilder for a CLI host.
    let builder = CoreBuilder::new(HostKind::Cli)
        .services(ServiceSet::none())            // no transport, only background services
        .domains(DomainSet::harness())           // minimal agent core
        .workspace("/tmp/openhuman-workspace");  // explicit workspace path

    // Build the runtime.
    let runtime = builder.build().await?;

    // Use the typed Core facade to run a turn.
    let turn = runtime.core().agent().turn("Explain OpenHuman's architecture.");
    let outcome = turn.send().await?;
    println!("Reply: {}", outcome.reply);

    Ok(())
}

Tokio Runtime Stack Requirements

Both embedding approaches rely on the host’s Tokio runtime. An OpenHuman agent turn spawns a large asynchronous state machine; the default Tokio worker stack (2 MiB) is insufficient for sub-agent nesting and will cause a stack overflow. The library exposes two constants in src/core/runtime/builder.rs that the host must use when constructing its runtime:

  • AGENT_WORKER_STACK_BYTES – recommended stack size for each Tokio worker thread.
  • MAX_BLOCKING_THREADS – number of blocking threads Tokio should allocate for CPU-bound work.

As documented in src/embed/harness/mod.rs, you must configure the runtime with these constants whenever you embed OpenHuman:

use openhuman_core::core::runtime::{AGENT_WORKER_STACK_BYTES, MAX_BLOCKING_THREADS};

let runtime = tokio::runtime::Builder::new_multi_thread()
    .enable_all()
    .thread_stack_size(AGENT_WORKER_STACK_BYTES)   // ↑ larger than default
    .max_blocking_threads(MAX_BLOCKING_THREADS)   // ↑ enough for blocking tasks
    .build()
    .expect("tokio runtime");

Without this configuration, the process will abort during any turn that delegates to a sub-agent.

Summary

  • Harness is a high-level convenience wrapper in src/embed/harness/mod.rs that builds and owns a CoreRuntime, enforces one instance per process, and manages ephemeral workspaces automatically.
  • CoreBuilder in src/core/runtime/builder.rs provides the low-level API for hosts that need to share runtimes, customize service sets, or manage workspace lifetimes manually.
  • Both approaches require the host to configure a Tokio runtime with AGENT_WORKER_STACK_BYTES and MAX_BLOCKING_THREADS to prevent stack overflow during agent execution.
  • Harness uses ServiceSet::none() and does not start transports, while CoreBuilder allows enabling ServiceSet::desktop() or ServiceSet::headless_api() for full networking capabilities.

Frequently Asked Questions

Can I create multiple Harness instances in the same process?

No. The harness enforces a one-harness-per-process rule using the HARNESS_LIVE atomic boolean. Attempting to build a second harness will return HarnessError::AlreadyRunning to protect global state such as the keyring master key and event bus. If you need multiple runtimes, use CoreBuilder instead.

Which embedding method should I use for a Tauri desktop application?

Use CoreBuilder. The Tauri shell already owns the CoreRuntime and requires fine-grained control over which services, domains, and tool groups are active. The builder allows you to configure ServiceSet::desktop() and manage the workspace independently of the UI lifecycle.

What happens if I don't increase the Tokio thread stack size?

The process will abort with a stack overflow. The default Tokio worker stack of 2 MiB is insufficient for the asynchronous state machine spawned during an agent turn, particularly when delegating to sub-agents. You must use AGENT_WORKER_STACK_BYTES when building the Tokio runtime as shown in src/core/runtime/builder.rs.

Can multiple CoreRuntime instances share the same workspace?

Yes. When using CoreBuilder, you can specify the same directory path via .workspace(dir) for multiple runtime instances. The host manages the workspace lifetime, allowing persistence across runtime restarts or sharing between concurrent instances, unlike the ephemeral workspace owned by a Harness instance.

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 →