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(®istry);
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 namedisplay_name= the agent's config namemodel_label= the agent'sprovider:modelstringcapabilities=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::TextDelta→ResponseChunk::TextDelta(1:1)StreamChunk::ToolCallDelta→ buffered untilToolCallEndStreamChunk::ToolCallEnd→ emitsToolCallStart(with full input) +ToolCallEndStreamChunk::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(drivesstream_chunksnatively)
What's coming
Tracked in the bridge repo:
AgentAdapter::submit_approval→ bridges toforge_tool::ApprovalHandlerAgentAdapter::send_interjection→ mid-loop user input (gated on Forge exposing an interjection channel)AgentAdapter::start_autonomy→ autonomous loops (gated on Forge'sStopWhenautonomy mode landing)harness_sdk_forge::observer— bridgesAgentLoopObserverevents intoharness_sdk::AutonomyEventfor 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
harness-sdk-forgerepo- Streaming — the underlying chunk stream
- Agents — building the Forge side