A general-purpose agent harness that gives LLM agents a CLI-based computer to complete tasks the way humans do
Project description
UniHarness
The core Python library for building computer-using AI agents. UniHarness is an agent harness — the complete runtime layer that gives any LLM access to a computer via the terminal, completing tasks the way developers do.
Vendor-agnostic. Works with Anthropic, OpenAI, DeepSeek, open-weight models via OpenRouter, or any OpenAI-compatible endpoint. The model is a parameter — swap it without changing your agent.
This is the library package. For the project overview and motivation, see the main README.
Installation
pip install uniharness
Requirements: Python 3.11+
Optional dependencies
pip install uniharness[langsmith] # LangSmith tracing
pip install uniharness[braintrust] # Braintrust observability
pip install uniharness[observe] # Both
Quick Start
Basic usage
import asyncio
from uniharness import create_agent
from uniharness.computer import LocalNativeComputer
async def main():
async with await create_agent(
model="openai:gpt-5.5",
computer=LocalNativeComputer(),
) as agent:
result = await agent.ainvoke({
"messages": [{"role": "user", "content": "Find all TODO comments in this project"}]
})
print(result["messages"][-1].content)
asyncio.run(main())
Using any OpenAI-compatible model
from uniharness import create_agent, ModelProfile
from uniharness.computer import LocalNativeComputer
model = ModelProfile(
model="deepseek:deepseek-v4-flash",
base_url="https://api.deepseek.com/v1",
api_key="your-key",
context_window=64000,
)
async with await create_agent(
model=model,
computer=LocalNativeComputer(),
) as agent:
result = await agent.ainvoke({"messages": [...]})
Streaming responses
async with await create_agent(
model="openai:gpt-5.5",
computer=LocalNativeComputer(),
) as agent:
async for event in agent.astream_events(
{"messages": [{"role": "user", "content": "Explain this codebase"}]},
version="v2",
):
# Process events: on_chat_model_stream, on_tool_start, on_tool_end, etc.
print(event["event"], event.get("data"))
Custom computer environment
from uniharness import create_agent
from uniharness.computer import LocalNativeComputer, RemoteE2BComputer
# Local execution
async with await create_agent(
model="openai:gpt-5.5",
computer=LocalNativeComputer(),
) as agent:
...
# Cloud sandbox via E2B
async with await create_agent(
model="openai:gpt-5.5",
computer=RemoteE2BComputer(api_key="your-e2b-key"),
) as agent:
...
Defining subagents
from uniharness import create_agent, AgentDefinition
from uniharness.computer import LocalNativeComputer
agents = {
"researcher": AgentDefinition(
description="Research agent for deep-diving into codebases",
system_prompt="You are a code research specialist...",
tools=["Read", "Glob", "Grep", "WebSearch"],
model="fast", # Uses the fast model for efficiency
),
}
async with await create_agent(
model="openai:gpt-5.5",
computer=LocalNativeComputer(),
agents=agents,
) as agent:
...
MCP server integration
async with await create_agent(
model="openai:gpt-5.5",
computer=LocalNativeComputer(),
mcp_servers={
"github": {"type": "http", "url": "https://mcp.github.com/mcp"},
"filesystem": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-filesystem"],
},
},
) as agent:
...
Web search and fetch
async with await create_agent(
model="openai:gpt-5.5",
computer=LocalNativeComputer(),
search_provider=("tavily", "your-tavily-key"),
fetch_provider=("jina", "your-jina-key"),
) as agent:
...
API Reference
create_agent()
The main entry point. Creates a fully configured agent with tools, skills, and middleware.
async def create_agent(
model: str | BaseChatModel | ModelProfile, # LLM to use (required)
computer: Computer, # Execution environment (required)
*,
fast_model: str | BaseChatModel | ModelProfile | None = None, # For subagent routing
mcp_servers: Mapping[str, McpServerConfig] | None = None, # MCP tool servers
agents: Mapping[str, AgentDefinition] | None = None, # Subagent definitions
search_provider: SearchProvider | None = None, # Web search provider
fetch_provider: FetchProvider | None = None, # Web fetch provider
skill_paths: Sequence[str] = DEFAULT_SKILL_PATHS, # Skill discovery directories
system_prompt: str | None = None, # Override default prompt
reminders: Sequence[Reminder] = BUILTIN_REMINDERS, # Dynamic message annotations
extra_tools: Sequence[BaseAgentTool[Any]] | None = None, # Additional custom tools
checkpointer: Checkpointer | None = None, # LangGraph checkpointer
) -> Agent
Parameters:
| Parameter | Type | Description |
|---|---|---|
model |
str | BaseChatModel | ModelProfile |
LLM specifier string (e.g. "openai:gpt-5.5"), a pre-configured LangChain model, or a ModelProfile with context window config. |
computer |
Computer |
Execution environment for CLI tools. Use LocalNativeComputer() for local or RemoteE2BComputer() for cloud sandbox. |
fast_model |
same as model |
Optional lightweight model for subagents marked with model="fast". |
mcp_servers |
Mapping[str, McpServerConfig] |
Dict mapping server names to MCP server configs. Supports "stdio", "sse", and "http" transports. |
agents |
Mapping[str, AgentDefinition] |
Dict mapping subagent type names to their definitions. Parent agent spawns these via the Agent tool. |
search_provider |
SearchProvider |
Web search backend. Pass ("tavily", api_key) or ("brave", api_key) for convenience. |
fetch_provider |
FetchProvider |
Web fetch backend. Pass ("jina", api_key) or ("firecrawl", api_key) for convenience. |
skill_paths |
Sequence[str] |
Directories to scan for SKILL.md-based skills. Defaults to /mnt/skills, ~/.uniharness/skills, .uniharness/skills. |
system_prompt |
str |
Override the auto-composed system prompt entirely. |
reminders |
Sequence[Reminder] |
Dynamic message annotation rules evaluated on every turn. |
extra_tools |
Sequence[BaseAgentTool] |
Additional tool instances appended to the built-in set. |
checkpointer |
Checkpointer |
LangGraph checkpointer for conversation persistence. |
Agent
The managed agent instance. Use as an async context manager.
async with await create_agent(model=..., computer=...) as agent:
# Invoke for a single response
result = await agent.ainvoke({"messages": [...]})
# Stream events
async for event in agent.astream_events({"messages": [...]}, version="v2"):
...
Properties: model, model_name, computer, tools, skills, mcps, agents, system_prompt, graph
ModelProfile
Configuration for an LLM with context window management.
ModelProfile(
model="openai:gpt-5.5",
context_window=200000, # Max tokens
compaction_threshold=160000, # When to trigger context compaction (default: 75% of context_window)
api_key="...", # Optional: provider API key
base_url="...", # Optional: custom endpoint
)
AgentDefinition
Declarative specification for subagents.
AgentDefinition(
description="Short description shown to parent agent",
system_prompt="Full system prompt for the subagent",
tools=["Read", "Glob", "Grep"], # Subset of available tools
model="fast", # "fast" uses fast_model, "main" uses primary model
)
Built-in Tools
| Tool | Description |
|---|---|
BashTool |
Execute shell commands (foreground or background with timeout) |
ReadTool |
Read file contents with line numbers, supports images |
WriteTool |
Create or overwrite files |
EditTool |
Exact string replacements in files |
GlobTool |
Pattern-based file search |
GrepTool |
Regex search across files (via ripgrep) |
WebSearchTool |
Web search via Tavily or Brave |
WebFetchTool |
Fetch and extract web page content via Jina or Firecrawl |
SkillTool |
Invoke extensible skills by name |
AgentTool |
Spawn subagents for parallel/specialized work |
TodoWriteTool |
Maintain structured todo lists |
PresentToUserTool |
Mark files for delivery to user |
Custom tools
Extend BaseAgentTool to create your own:
from uniharness.tools import BaseAgentTool
from uniharness.types import ToolResult
from pydantic import BaseModel, Field
class MyToolInput(BaseModel):
query: str = Field(description="The search query")
class MyTool(BaseAgentTool[MyToolInput]):
name = "MyTool"
description = "Does something useful"
args_schema = MyToolInput
async def execute(self, params: MyToolInput) -> ToolResult:
# Your implementation
return ToolResult(output="Result here")
# Pass to create_agent
agent = await create_agent(
model="...",
computer=LocalNativeComputer(),
extra_tools=[MyTool()],
)
Key Concepts
Computer Protocol
The foundational abstraction. Every agent gets a computer — a pluggable execution environment that abstracts where CLI tools run. This is what makes UniHarness agents general-purpose: the same tools work whether the agent is on your laptop, in a VM, or in the cloud.
Implementations must provide:
start()/stop()— Lifecycle management (idempotent)run(command, timeout)— Execute shell commandsupload(src, dst)/download(src, dst)— File transfer
Built-in implementations:
LocalNativeComputer— Runs commands on the local machine via transient bash subprocessesLocalVMComputer— Runs inside a Lima VM (macOS) or WSL (Windows)RemoteE2BComputer— Runs in an E2B cloud sandbox with auto-pause/resume
Harness System
The harness is the runtime augmentation layer — the "operating system" that wraps the raw LLM with everything it needs to function as a capable agent:
- Environment detection — Working directory, git status, platform, shell, timezone
- Context compaction — Automatic conversation summarization when approaching the context window limit (3-phase state machine: NONE → REQUESTING → APPLYING)
- Permission gating — Safety rules that validate tool calls before execution
- Skill discovery — Scans filesystem paths for SKILL.md-based extensible skills
- Dynamic reminders — Injects
<system-reminder>tags based on conversation state (e.g. available skills, background task completions)
Prompt Composition
System prompts are assembled from modular Markdown fragments in prompts/fragments/. Sections cover identity, agency, task execution, tool instructions, tone, and environment context. Supports ${VAR} substitution and conditional inclusion.
MCP Integration
Connect external tool servers via the Model Context Protocol. Supports stdio, sse, and http transports. Discovered tools are exposed to the agent as mcp__<server>__<tool>.
Skills
Skills are filesystem-based extensions with a SKILL.md frontmatter file:
---
name: pdf
description: Extract and process PDF documents
---
## Instructions
...
Default discovery paths: /mnt/skills, ~/.uniharness/skills, .uniharness/skills.
Architecture
uniharness/
├── __init__.py # Public API: Agent, AgentDefinition, ModelProfile, create_agent
├── types.py # Framework-agnostic core types (ToolResult, AgentContext, etc.)
├── tasks.py # Background task lifecycle (TaskRegistry)
├── computer/ # Computer protocol + implementations
│ ├── base.py # Protocol definition, Mount, ExecutionMetadata
│ ├── local/ # LocalNativeComputer, LocalVMComputer (Lima/WSL)
│ └── remote/ # RemoteE2BComputer (E2B cloud sandbox)
├── harness/ # Agent runtime augmentation
│ ├── definition.py # AgentDefinition — declarative subagent specs
│ ├── model.py # ModelProfile — LLM + context window config
│ ├── environment.py # Runtime context detection (pwd, git, platform)
│ ├── permission.py # Safety rules and permission gating
│ ├── reminders.py # Dynamic message annotation system
│ └── skills.py # Skill discovery and lazy loading
├── tools/ # Tool implementations
│ ├── base.py # BaseAgentTool[ParamsT] abstract class
│ ├── cli/ # Bash, Read, Write, Edit, Glob, Grep
│ ├── web/ # WebSearch, WebFetch + provider plugins
│ ├── task/ # Agent (subagent spawning), TaskOutput, TaskStop
│ ├── skill.py # Skill invocation tool
│ ├── todo/ # TodoWrite tool
│ └── ui/ # PresentToUser tool
├── prompts/ # Composable prompt system
│ ├── content.py # Markdown fragment loader with variable substitution
│ ├── sections.py # Section-based prompt composition
│ └── fragments/ # 35+ .md prompt content files
├── mcp/ # Model Context Protocol integration
│ ├── _client.py # Per-server MCP connection
│ ├── _connector.py # Multi-server connection manager
│ └── _tool.py # MCP tool wrapper as BaseAgentTool
└── langchain/ # LangChain/LangGraph integration (isolated module)
├── agent.py # Agent class + create_agent() factory
├── middleware.py # Runtime middleware (compaction, permissions, reminders)
├── adapter.py # BaseAgentTool → LangChain StructuredTool
└── subagent.py # Isolated subagent execution
Note: The core library is framework-agnostic. LangChain imports are confined to the
langchain/module. Tools, types, and computer abstractions have no LangChain dependency.
Development
cd libs/uniharness
# Install dependencies
uv sync --group test
# Run tests
make test # Unit tests with coverage
make integration_test # Integration tests (requires API keys)
# Code quality
make lint # Ruff + mypy strict
make format # Auto-fix formatting
See the Contributing Guide for more details.
Status
Pre-Experimental (0.0.x) — API may change without notice. Clean architecture and code quality take priority over backward compatibility. We ship fast and refactor freely — backward compatibility constraints come later.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file uniharness-0.0.1.tar.gz.
File metadata
- Download URL: uniharness-0.0.1.tar.gz
- Upload date:
- Size: 131.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
163ab62a36ef83cdc91ee72f49a65e8ccda4aeb84e871735793c8f3056989a62
|
|
| MD5 |
f00d5f557987d29d63485ec61b6bfbe8
|
|
| BLAKE2b-256 |
676286cc0e9e948dbac2f5c293561a8bce90563e2a13da388946a92607c81a3e
|
File details
Details for the file uniharness-0.0.1-py3-none-any.whl.
File metadata
- Download URL: uniharness-0.0.1-py3-none-any.whl
- Upload date:
- Size: 187.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3018ab2f94689aed75cdf8ee9fe429e2c798c2d36ff4144698728076439238b2
|
|
| MD5 |
5df1b930d064bf95efebe3d84ec993e9
|
|
| BLAKE2b-256 |
600f60cb81bb866dbf08606ddfef3ab20fecfc6937c5a7fccfecc42950ae3f66
|