Harness integration

Drop a Forge agent into a Codex-style terminal via harness-sdk-forge.

Harness integration

The harness-sdk-forge bridge crate connects Forge agents to the harness-sdk TUI runtime. Drop in any StreamingToolLoopAgent and you get a Codex-style terminal with real per-token streaming, tool-call rendering, and approval handling.

What it does

┌────────────────────────────────────────────────────────────┐
│  forge-rs (StreamingToolLoopAgent)                         │
│      ↓ stream_chunks                                       │
│  harness-sdk-forge::ChunkTranslator                        │
│      ↓ ResponseChunk events                                │
│  harness-sdk::AgentAdapter (the ForgeAdapter)              │
│      ↓ BoxStream<ResponseChunk>                            │
│  harness-tui-kit (Ratatui chat widget)                     │
│      ↓ rendered frames                                     │
│  Your terminal                                             │
└────────────────────────────────────────────────────────────┘

Text deltas reach the terminal as the upstream wire produces them. Tool calls render as the chat widget already expects (a tool-start event with the full input, then a tool-result event when the agent's loop completes the call).

Installation

[dependencies]
harness-sdk-forge = { git = "https://github.com/l1feai/harness-sdk-forge", branch = "main" }
tokio = { version = "1", features = ["full"] }

The bridge re-exports the entire Forge SDK plus harness-sdk and harness-tui-kit types — one import gets you everything.

Minimal example

use std::sync::Arc;
use harness_sdk_forge::prelude::*;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // 1. Build a Forge agent — full SDK is re-exported here.
    let model: Arc<dyn LanguageModel> = build_my_model()?;
    let registry = ToolRegistry::new();
    let approval = AutoApprove::shared();

    let config = AgentConfig::new("my-agent", "anthropic:claude-sonnet-4-5-20250929")
        .with_system_prompt("You are a helpful assistant.")
        .with_tool_registry(&registry);

    let forge_agent = Arc::new(StreamingToolLoopAgent::new(
        config,
        StreamingLoopConfig::default(),
        model,
        registry,
        approval,
    ));

    // 2. Wrap as a Harness adapter and start the TUI.
    let adapter: Arc<dyn AgentAdapter> = Arc::new(ForgeAdapter::new(forge_agent));
    let app = AgentApp::builder().adapter(adapter).build()?;
    app.run().await?;

    Ok(())
}

That's the full file. ~30 lines, including imports.

The ForgeAdapter

pub struct ForgeAdapter { /* ... */ }

impl ForgeAdapter {
    pub fn new(agent: Arc<StreamingToolLoopAgent>) -> Self;

    pub fn with_id(self, id: impl Into<String>) -> Self;
    pub fn with_display_name(self, name: impl Into<String>) -> Self;
    pub fn with_model_label(self, label: impl Into<String>) -> Self;
    pub fn with_capabilities(self, caps: AdapterCapabilities) -> Self;

    pub fn forge_agent(&self) -> &Arc<StreamingToolLoopAgent>;
}

Defaults:

  • id = the agent's config name
  • display_name = the agent's config name
  • model_label = the agent's provider:model string
  • capabilities = AdapterCapabilities::chat_only() (streaming + chat, no autonomy / multi-agent / tool_calls UI / thinking display)

To advertise the harness's tool-call UI:

let caps = AdapterCapabilities::chat_only().with_tool_calls(true);
let adapter = ForgeAdapter::new(agent).with_capabilities(caps);

The ChunkTranslator

The bridge's translation layer is exposed if you need it directly:

use harness_sdk_forge::ChunkTranslator;

let mut translator = ChunkTranslator::new("my-agent");
for forge_chunk in forge_chunks {
    for harness_chunk in translator.translate(forge_chunk) {
        emit(harness_chunk);
    }
}
for harness_chunk in translator.finish() {
    emit(harness_chunk);
}

Behavior:

  • StreamChunk::TextDeltaResponseChunk::TextDelta (1:1)
  • StreamChunk::ToolCallDelta → buffered until ToolCallEnd
  • StreamChunk::ToolCallEnd → emits ToolCallStart (with full input) + ToolCallEnd
  • StreamChunk::Done { usage }UsageUpdate + Done
  • Errors mid-stream → ResponseChunk::Error (does not terminate the wrapper)

What's wired

The current v0.1.0-alpha.1 wires:

  • AgentAdapter::name
  • AgentAdapter::capabilities
  • AgentAdapter::list_agents
  • AgentAdapter::send_message (drives stream_chunks natively)

What's coming

Tracked in the bridge repo:

  • AgentAdapter::submit_approval → bridges to forge_tool::ApprovalHandler
  • AgentAdapter::send_interjection → mid-loop user input (gated on Forge exposing an interjection channel)
  • AgentAdapter::start_autonomy → autonomous loops (gated on Forge's StopWhen autonomy mode landing)
  • harness_sdk_forge::observer — bridges AgentLoopObserver events into harness_sdk::AutonomyEvent for the activity feed

Multi-agent

To expose multiple Forge agents in one harness, build multiple adapters:

let researcher = Arc::new(ForgeAdapter::new(research_agent).with_id("researcher"));
let writer = Arc::new(ForgeAdapter::new(writer_agent).with_id("writer"));

// harness-sdk's multi-adapter router handles the rest
let app = AgentApp::builder()
    .adapter(researcher)
    .adapter(writer)
    .build()?;

Next