Agent Harnessing Patterns
How modern open-source coding agents harness language models—the architectural patterns for loops, tools, permissions, sessions, and multi-agent orchestration extracted from OpenCode, Codex, OpenHands, Oh My OpenCode, and Pi.
The term agent harness refers to the entire software system that wraps a language model and turns it into a working coding agent. The model provides reasoning; the harness provides everything else—loops, tools, permissions, memory, session persistence, and the control surfaces that keep the agent useful and safe.
Between 2024 and 2026, a generation of open-source coding agents matured enough to reveal recurring architectural patterns. This entry extracts those patterns from five systems: OpenCode, Codex, OpenHands, Oh My OpenCode, and Pi.
The Five Systems
| System | Language | Core Pattern | Distinguishing Feature |
|---|---|---|---|
| OpenCode | TypeScript | Event-bus + plugin hooks | 7-level config hierarchy, message parts system |
| Codex | Rust | Channel-based orchestration | Three-phase tool execution (approval → sandbox → retry) |
| OpenHands | Python | Event stream + action/observation | Docker-isolated runtime, multi-agent delegation |
| Oh My OpenCode | TypeScript | Hook injection + category routing | Role-separated orchestrator/implementer agents |
| Pi | TypeScript | Minimal core + extensions | Steering/follow-up message queues, pluggable operations |
Pattern 1: The Agent Loop
Every system implements the same fundamental cycle. A loop calls the model, receives tool-call requests, executes them, feeds results back, and repeats until the model signals completion or a limit is hit.
graph TD
A[User Input] --> B[Build Context]
B --> C[Call LLM]
C --> D{Tool Calls?}
D -->|Yes| E[Execute Tools]
E --> F[Collect Observations]
F --> B
D -->|No| G[Return Response]
style A fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
style B fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style C fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
style D fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style E fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style F fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style G fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
The implementations differ in how they manage the loop’s edges:
Loop Termination
All five systems must decide when to stop. The strategies overlap:
- Finish signal: The model returns text without tool calls (all systems).
- Step limit: A configurable maximum number of iterations (OpenCode’s
max_steps, OpenHands’IterationControlFlag, Codex’s turn-level abort). - Budget ceiling: Token or dollar cost limits that halt execution (OpenHands’
BudgetControlFlagwith per-taskmax_budget). - User cancellation: Abort signals propagated through the loop (Codex uses Rust’s
CancellationToken; OpenCode usesAbortController; Pi checksAbortSignalbetween tool batches).
Stuck Detection
Agents can enter infinite loops—calling the same tool with the same arguments and getting the same result. Two approaches have emerged:
Doom loop detection (OpenCode): Track the last three tool calls. If they’re identical, pause and ask the user for permission before continuing.
Pattern matching (OpenHands): A dedicated StuckDetector class checks five scenarios—repeated action-observation pairs, error loops, agent monologues, action-observation pattern cycles, and context window overflow loops. When a loop is detected, it raises AgentStuckInLoopError and offers recovery options: truncate memory, restart from last user message, or stop.
Streaming Architecture
All five systems stream model output rather than waiting for completion. This enables progressive UI updates and early tool-call detection. The common pattern uses async iterators:
// Simplified streaming pattern (Pi)
for await (const event of stream) {
if (event.type === 'text_delta')
ui.appendText(event.delta)
else if (event.type === 'tool_call_end')
results.push(await executeTool(event.toolCall))
}
Pi introduces steering messages—the user can inject a message mid-loop (between tool executions) to redirect the agent without waiting for the turn to end. OpenCode handles this through its event bus. The rest require the user to cancel and re-prompt.
Pattern 2: Tool Systems
Every coding agent ships with roughly the same core tools: read, write/edit, bash, grep/glob, and some form of web access. The interesting patterns are in how tools are defined, validated, and executed.
Tool Definition
Three tiers of abstraction have emerged:
Schema-first (OpenCode, Codex, Pi): Tools are defined with a JSON Schema (via Zod, TypeBox, or direct JSON), a description string, and an execute function. The harness validates arguments before execution.
// OpenCode pattern
Tool.define("bash", () => ({
description: "Execute a shell command",
parameters: z.object({
command: z.string(),
timeout: z.number().optional(),
}),
async execute(args, ctx) {
// ...
return { title: "bash", output: stdout }
}
}))
Action classes (OpenHands): Tools are typed Python dataclasses in an action hierarchy. Each action class has a runnable flag, security risk level, and confirmation state:
@dataclass
class CmdRunAction(Action):
command: str
blocking: bool = True
runnable: ClassVar[bool] = True
security_risk: ActionSecurityRisk = UNKNOWN
Pluggable operations (Pi): Tool implementations accept an operations interface, allowing the file system or shell to be swapped at construction time—for SSH execution, container isolation, or testing:
const tool = createReadTool(cwd, {
operations: {
readFile: async (path) => remoteRead(path),
access: async (path) => remoteAccess(path),
}
})
Parallel Tool Execution
Codex implements read/write locking for tools. Read-only tools (read_file, list_dir) acquire a shared read lock and run concurrently. Mutating tools (apply_patch, shell) acquire an exclusive write lock and run serially. This is a direct application of the readers-writer lock pattern to agent tool execution.
OpenHands takes a different approach: one pending action at a time, strictly sequential. The _pending_action property blocks the next _step() call until the current observation arrives.
MCP Integration
The Model Context Protocol has become a standard for tool interoperability. OpenCode, Oh My OpenCode, and Pi all support MCP servers as dynamic tool sources:
// OpenCode: MCP tools auto-discovered
MCP.tools(config.mcp.servers) → Tool[]
// Converted to AI SDK format, tracked for status changes
Oh My OpenCode adds a layer: skills can embed MCP server definitions in their YAML frontmatter, which are automatically registered at session start.
Pattern 3: Permission and Safety
The most architecturally significant pattern in modern agent harnesses is the permission system. All five systems implement some form of it, but the approaches differ substantially.
graph LR
subgraph OPENCODE["OpenCode"]
OC1[Wildcard Patterns]
OC2[allow / deny / ask]
OC3[Per-agent Rules]
end
subgraph CODEX["Codex"]
CX1[Approval Policy]
CX2[Sandbox Selection]
CX3[Cached Decisions]
end
subgraph OPENHANDS["OpenHands"]
OH1[Security Analyzer]
OH2[Risk Levels]
OH3[Confirmation Gate]
end
style OPENCODE fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style CODEX fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style OPENHANDS fill:#0a0a0a,stroke:#00ff00,stroke-width:1px,color:#cccccc
style OC1 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style OC2 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style OC3 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style CX1 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style CX2 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style CX3 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style OH1 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style OH2 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style OH3 fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
OpenCode: Pattern-Based Rules
OpenCode uses glob-pattern permissions with three actions—allow, deny, ask—layered across agents:
permission:
read:
"*": allow
"*.env": ask
"~/.ssh/*": deny
bash: ask
edit: allow
Rules merge hierarchically: built-in defaults → config-level → session-level → tool-level. The ask action pauses the loop and publishes a PermissionNext.Event.Asked event; a UI subscriber prompts the user and replies with once, always, or reject.
Codex: Three-Phase Orchestration
Codex’s ToolOrchestrator runs every tool call through three stages:
-
Approval: Check whether the action needs user consent. Approval decisions are cached by semantic keys (file paths for patches, command prefixes for shell). An approval for
src/main.rscarries over to the next edit of that same file within the session. -
Sandbox selection: Choose the appropriate sandbox (macOS Seatbelt, Linux seccomp, Windows restricted token, or none). The sandbox mode can be read-only, workspace-write, or full-access.
-
Execution with retry: Run the tool. If it fails inside the sandbox (e.g., permission denied), offer to escalate—run again without the sandbox, with the user’s explicit approval.
This is the most sophisticated safety model among the five systems. It balances security with usability by learning from each approval within a session.
OpenHands: Security Risk Classification
OpenHands classifies every action by risk level (LOW, MEDIUM, HIGH, UNKNOWN). A security analyzer inspects the action before execution. High-risk actions gate the agent into AWAITING_USER_CONFIRMATION state. The user confirms or rejects, and the agent resumes or stops.
Pattern 4: Provider Abstraction
All five systems abstract across LLM providers. The approaches reflect different trade-offs between simplicity and control.
| System | Abstraction Layer | Providers Supported |
|---|---|---|
| OpenCode | Vercel AI SDK | 20+ (Anthropic, OpenAI, Google, Azure, Bedrock, etc.) |
| Codex | Custom Rust client | OpenAI Responses API + compatible endpoints |
| OpenHands | litellm | All litellm-supported providers |
| Oh My OpenCode | Inherits from OpenCode | 20+ (via OpenCode) |
| Pi | Custom registry + per-provider implementations | 20+ (Anthropic, OpenAI, Google, xAI, Groq, Ollama, etc.) |
The key design question is whether to use an existing SDK (Vercel AI SDK, litellm) or build provider adapters from scratch. OpenCode and OpenHands use existing SDKs and get breadth for free. Codex and Pi build their own for tighter control over streaming, caching, and cost tracking.
Pi’s approach is notable: each provider has a dedicated implementation file with a stream function, but all are accessed through a single stream(model, context, options) entry point. Provider-specific quirks (Anthropic’s cache control, OpenAI’s reasoning tokens) are handled inside each adapter.
Pattern 5: Session and Context Management
Long coding sessions exceed context windows. Every system needs a strategy for persistence and compression.
Conversation Persistence
OpenCode: Messages are stored as structured parts (text, reasoning, tool calls, snapshots, patches, compaction summaries). Each part is a discriminated union with its own schema. Sessions can be forked at any message, creating a branching conversation tree.
Pi: Sessions are stored as JSONL files with a tree structure—each entry has an id and parentId. Branching happens in-place without new files. Entry types include messages, compaction summaries, model changes, labels, and custom extension state.
OpenHands: Events are persisted to a file store. Each event gets a sequential ID and timestamp. Secret values are masked with <secret_hidden> before storage.
Context Compaction
When the conversation exceeds the context window, all systems compress history:
- OpenCode: A compaction agent summarizes early messages and inserts a special
CompactionPart. Subsequent LLM calls only see messages after the compaction point. - Pi: Rolling summarization keeps the last N tokens of recent messages and summarizes everything before that. File read/modify tracking helps the summary retain awareness of what’s been done.
- OpenHands: A
Condensersystem with two strategies—ConversationWindowCondenser(sliding window) andLLMCondenser(LLM-generated summary). - Oh My OpenCode: Adds preemptive compaction hooks and a
compaction-context-injectorthat enriches the summary with project-specific context before compression.
Instruction Injection
OpenCode injects file-specific instructions by scanning for AGENTS.md, CLAUDE.md, and CONTEXT.md files relative to whatever file the agent is editing. Pi reads .pi/AGENTS.md into the system prompt. OpenHands loads microagent markdown files with frontmatter metadata for project-specific and agent-specific knowledge injection.
Pattern 6: Multi-Agent Orchestration
This is where the systems diverge most. Three approaches have emerged.
Subagent Spawning (OpenCode, Pi)
The primary agent can spawn secondary agents for specific tasks. In OpenCode, agents are classified by mode—primary (build, plan), subagent (explore, general)—and each has its own permissions, model, and system prompt. The task tool creates a new session for the subagent.
Pi takes a deliberately minimal approach: no built-in sub-agents. Instead, extensions can implement them. The steering and follow-up message queues provide the primitives for agent-to-agent communication.
Parent-Child Delegation (OpenHands)
OpenHands implements a formal delegation pattern. The AgentController has parent and delegate fields. When an agent issues an AgentDelegateAction, a child controller is created with a shared event stream but separate state. Events flow to the delegate until it finishes, then the parent resumes.
Key characteristics:
- Shared event stream (all agents write to the same history)
- Shared metrics (parent snapshot captured at delegation time)
- Global iteration counter across the hierarchy
- Clean delegation chain that can nest multiple levels
Role-Separated Orchestration (Oh My OpenCode)
Oh My OpenCode implements the most elaborate multi-agent pattern—a three-layer hierarchy:
Layer 1 — Planning: Prometheus conducts interviews and generates plans. Metis performs gap analysis. Momus reviews the plan.
Layer 2 — Orchestration: Atlas coordinates execution but is explicitly forbidden from writing code. It can read files, run diagnostics, and verify results, but must delegate all implementation.
Layer 3 — Execution: Sisyphus-Junior executes tasks with category-specific prompts and model selection. Specialist agents (Oracle for architecture, Librarian for documentation, Explore for codebase search) handle specific needs.
graph TD
subgraph PLAN["PLANNING"]
PR[Prometheus<br/>Planner]
ME[Metis<br/>Gap Analyzer]
MO[Momus<br/>Reviewer]
PR --> ME --> MO
end
subgraph ORCH["ORCHESTRATION"]
AT[Atlas<br/>Conductor]
end
subgraph EXEC["EXECUTION"]
SJ[Sisyphus-Junior<br/>Implementer]
OR[Oracle<br/>Architecture]
LI[Librarian<br/>Documentation]
EX[Explore<br/>Search]
end
MO --> AT
AT --> SJ
AT --> OR
AT --> LI
AT --> EX
style PLAN fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#666666
style ORCH fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
style EXEC fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#666666
style PR fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style ME fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style MO fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style AT fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
style SJ fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style OR fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style LI fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style EX fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
The strict orchestrator-cannot-implement rule is enforced by hook injection: the atlas hook injects DELEGATION_REQUIRED system directives when violations are detected. This prevents the common failure mode where an orchestrator agent starts implementing instead of delegating.
Pattern 7: Event Systems
Agent harnesses need internal communication between components—the UI must know when tools execute, the permission system must intercept tool calls, session persistence must capture everything.
Publish-Subscribe (OpenCode)
OpenCode uses a typed event bus:
Bus.publish(Session.Event.Updated, { info })
Bus.subscribe(PermissionNext.Event.Asked, (event) => {
// Show approval dialog
})
Events flow between the agent loop, TUI, permission system, MCP manager, and session storage. The system is fully decoupled—components only know about event types, not each other.
Event Stream with Subscribers (OpenHands)
OpenHands uses a thread-safe queue with typed subscriber levels. Events are processed FIFO, and subscribers execute in thread pools:
class EventStreamSubscriber(str, Enum):
AGENT_CONTROLLER = 'agent_controller'
SERVER = 'server'
RUNTIME = 'runtime'
MEMORY = 'memory'
Each event carries causality metadata—an observation’s _cause field points to the action that triggered it, enabling full trace reconstruction.
Async Channels (Codex)
Codex uses Rust’s async channels for a clean producer-consumer pattern:
pub struct Codex {
pub tx_sub: Sender<Submission>, // User sends operations
pub rx_event: Receiver<Event>, // User receives events
pub agent_status: watch::Receiver<AgentStatus>,
}
This channel-based design fully decouples the frontend from the agent engine. The TUI, API server, and IDE extensions all consume the same event channel.
Pattern 8: Configuration Hierarchies
All systems support layered configuration, but OpenCode’s seven-level hierarchy is the most elaborate:
- Managed config (system-wide)
- Remote
.well-known(organization-level) - Global user config (
~/.config/opencode/opencode.json) - Custom
OPENCODE_CONFIGenvironment variable - Project config (
opencode.jsonin project root) .opencodedirectories (subdirectory overrides)- Inline
OPENCODE_CONFIG_CONTENT(for CI/CD)
Each level can override agents, tools, permissions, models, and system prompts. Oh My OpenCode adds its own layer on top with category-based model routing—a task(category="visual") call automatically selects a vision-capable model, while task(category="business-logic") routes to a reasoning-focused model.
Pi takes the opposite approach: flat settings with CLI flags taking highest precedence, then project-local .pi/settings.json, then user-global ~/.pi/agent/settings.json. Simpler, but less suitable for enterprise environments.
Pattern 9: Extensibility
The extensibility patterns reveal a spectrum from opinionated to open:
Plugin Systems (OpenCode, Oh My OpenCode)
OpenCode plugins are npm packages that implement a PluginInput interface. They can add tools, modify system prompts, and intercept hooks. Oh My OpenCode is itself an OpenCode plugin—one that registers 11 agents, 37 hooks, and 25 tools.
Extension API (Pi)
Pi extensions are TypeScript modules that receive an ExtensionAPI object:
export default function(pi: ExtensionAPI) {
pi.registerTool({ ... })
pi.registerCommand({ ... })
pi.registerShortcut({ ... })
pi.registerProvider({ ... })
pi.on('tool:result', (event) => { ... })
}
Extensions can register tools, commands, keyboard shortcuts, LLM providers, and event listeners. They can also provide custom rendering for tool calls and results in the TUI.
Skills and Instructions
Multiple systems support markdown-based knowledge injection:
- OpenCode:
SKILL.mdfiles discovered from global and project directories, loaded with YAML frontmatter metadata. - Pi: Skills are markdown files with frontmatter, discovered from
~/.pi/agent/skills/and.pi/skills/. - OpenHands: Microagents are markdown files categorized as
REPO_KNOWLEDGEorAGENT_KNOWLEDGE, injected into system prompts. - Oh My OpenCode: Skills can embed MCP definitions and are routed to agents based on task category.
Pattern 10: Runtime Isolation
The question of where agent-generated code runs is architecturally significant.
Codex offers the most granular sandbox model: platform-specific sandboxes (macOS Seatbelt, Linux seccomp, Windows restricted tokens) with three modes—read-only, workspace-write, and full-access. Sandbox escalation on failure asks the user before running unrestricted.
OpenHands provides the strongest isolation: a full Docker container runtime. The agent’s shell commands, file operations, and browser interactions all happen inside a container. The abstract Runtime base class has implementations for Docker, Kubernetes, local, and remote execution, plus third-party runtimes (Daytona, E2B, Modal).
OpenCode and Pi rely on file-path validation and permission rules rather than OS-level sandboxing. The trade-off is lower setup friction versus weaker isolation guarantees.
Convergent Architecture
Despite independent development, these five systems converge on a common high-level architecture:
graph TB
subgraph HARNESS["AGENT HARNESS"]
UI[Interface Layer<br/>TUI / Web / IDE]
EVENTS[Event System<br/>Bus / Stream / Channel]
LOOP[Agent Loop<br/>Prompt → Tool → Observe]
TOOLS[Tool Registry<br/>Define / Validate / Execute]
PERM[Permission Layer<br/>Rules / Approval / Sandbox]
PROV[Provider Abstraction<br/>Unified LLM API]
SESS[Session Manager<br/>Persist / Compact / Fork]
EXT[Extension System<br/>Plugins / Skills / MCP]
end
UI --> EVENTS
EVENTS --> LOOP
LOOP --> TOOLS
TOOLS --> PERM
LOOP --> PROV
LOOP --> SESS
EXT --> TOOLS
EXT --> PROV
style HARNESS fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
style UI fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style EVENTS fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style LOOP fill:#0a0a0a,stroke:#00ff00,stroke-width:2px,color:#cccccc
style TOOLS fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style PERM fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style PROV fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style SESS fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
style EXT fill:#0a0a0a,stroke:#333333,stroke-width:1px,color:#cccccc
The components are the same; the implementations reflect genuine trade-offs:
| Trade-off | Minimal (Pi) | Moderate (OpenCode, Codex) | Maximal (OpenHands, Oh My OpenCode) |
|---|---|---|---|
| Agent count | 1 | 2-3 modes | 5-11 specialized agents |
| Isolation | Path validation | Permissions + patterns | Docker containers / OS sandbox |
| Config | Flat files | Hierarchical merge | Hierarchical + category routing |
| Extension | TypeScript API | Plugin + MCP | Plugin + hooks + skills + MCP |
| Context mgmt | Rolling summary | Compaction agent | Condenser + microagents |
Lessons
Several practical lessons emerge from studying these systems:
The loop is simple; the edges are hard. The core while (tool_calls) { execute(); } loop is trivial. Everything around it—stuck detection, cancellation, permission checking, context compaction, error recovery—is where the real engineering lives.
Permission caching matters. Codex’s insight of caching approval decisions by semantic key (file path, command prefix) rather than exact match dramatically reduces user friction while maintaining safety.
Orchestrator and implementer should be separate roles. Oh My OpenCode’s strict rule that the orchestrator cannot write code prevents a common failure mode in multi-agent systems where the coordinator starts doing the work itself and loses track of the plan.
Markdown is the universal skill format. Every system that supports knowledge injection settled on markdown with YAML frontmatter independently. It’s human-writable, LLM-readable, and version-controllable.
Provider abstraction is deeper than endpoint swapping. Model-specific system prompt formatting, token counting, cache handling, and cost calculation all vary by provider. Systems that treat providers as interchangeable (just swap the API key) consistently hit edge cases.
See Also
- Scaffolding — the broader concept of external structures that enable agents
- The Agent Loop — the fundamental cycle at the heart of every harness
- Tool Use — how agents extend their capabilities through function calls
- Autonomy Levels — how harnesses implement different levels of human oversight
- Memory Systems — the persistence component of agent harnesses
- Sandboxing — isolation patterns for safe code execution
Related Entries
Autonomy Levels
A developmental taxonomy of agent independence—from fully supervised infancy to unsupervised autonomy, with the stages between.
AnatomyMemory Systems
How agents remember—from ephemeral context windows to persistent knowledge stores, and the mechanisms that connect past experience to present action.
AnatomyScaffolding
The external structures—code, tools, memory systems—that transform a language model into an agent capable of action and persistence.
EthologyThe Agent Loop
The fundamental cycle that defines agent behavior: observe → reason → act → observe. The heartbeat of agency.
AnatomyTool Use
The anatomy of how agents extend beyond language to act on the world through function calls, APIs, and external systems.