A Unified Runtime Across Agent Harnesses Demands Semantic Normalization, Not a Single Implementation
This is a series of about six articles discussing the six core problems encountered during the construction of a multi-Agent system for my open-source project Pragma. Starting from the engineering capabilities a long-running Agent system needs: how to replace the execution environment, how to organize context, how multiple experts collaborate, how experience accumulates, how complex tasks are composed, and how the entire working method becomes an asset that can be versioned and shared. Welcome to download, experience, star, fork, and submit PRs. Supports Windows and Mac clients, and can be downloaded and experienced directly.
Nowadays, there are more and more general-purpose large language models, and it's hard to say any single one is "dominant." At the same time, the number of general-purpose Agent Harnesses built around models is also increasing.
I subscribe to ChatGPT Pro and Gemini Pro myself, have topped up the DeepSeek API, and previously subscribed to Claude. To leverage the capabilities of different models as much as possible, I've also installed multiple Harnesses locally, with Codex as my main one, alongside Claude Code, PI, and Antigravity.
A very natural question is: Why not connect all models to the same Harness?
Of course, you can, and many Agent products do exactly that. But I increasingly tend to believe that only the most suitable harness can bring out a model's maximum capabilities.
The model provides the core reasoning, generation, and tool selection capabilities, while the Harness determines how the model runs: how the Agent Loop is designed, what tools are available, how to access the workspace, how to manage context, how to execute Shell commands, how to request permissions, whether it can launch Sub-Agents, how to resume Sessions...
Both together determine the final Agent's capability. In the future, this combination might emerge: vertical model + vertical harness = vertical Agent. A specific model doesn't need to be the strongest in all Harnesses; it just needs to find the execution environment best suited to it.
Why "Multi-Model" Alone Is Far From Enough
Many AI applications treat the "model" as the most important runtime choice: add a dropdown box for model names, send requests to different providers, and that completes the so-called multi-model architecture. This might be enough for ordinary chat applications, but it's far from sufficient for Agent systems. The same model running in a plain API, Codex, Claude Code, or another Agent Harness does not yield the same capability. The model is only responsible for reasoning and generation; what truly determines what an Agent can do is the entire execution environment outside the model: the Agent Loop, tool system, workspace access, Session management, permission mechanisms, context compression, sub-Agent capabilities, and cancellation and recovery protocols.
Joel Niklaus publicly shared a very interesting set of experiments on Hugging Face, fixing two sets of models: GLM-5.2 744B-A40B and Gemma 4 26B-A4B, then having 10 different Agent Harnesses run the same batch of 250 SWE-bench Pro tasks, one rollout per task, totaling: 10 harnesses × 2 models × 250 tasks = 5,000 rollouts.
The result was that with the model completely unchanged, just switching the Harness, GLM-5.2's Pass@1 went directly from 23% to 52%. There is no "universally best Harness"; the correlation coefficient of Harness rankings between the two models was only -0.05, essentially meaning no correlation at all.
The final conclusion is: Harness performance is highly dependent on the specific model; you cannot discuss "which Harness is the strongest" in isolation from the model. This goes back to the point above: only the most suitable harness can bring out a model's maximum capabilities.
What an Agent Team Truly Needs to Unify
If you want to build an Agent Team system where each teammate can be bound to a different: Harness + Model
For example:
Product Expert → Claude Code + Claude
Architecture Expert → Codex + GPT
Research Expert → Gemini / Antigravity
Coding Expert → PI + some Coding Model
Then the system must first solve one problem:
How does the upper-layer Agent Team uniformly orchestrate these completely different Harnesses?
This is the main topic this article wants to discuss. It can be a Team, a Sub-Agent called by an Expert, or a Flow orchestrated by a fixed process. They can also continue to combine with each other to form more complex Agent systems. They are collectively referred to here as Agent Team. The Agent Team should not need to know whether Codex, Claude Code, or PI is running underneath. Therefore, a unified Runtime layer is needed in between: unified Session management, unified Streaming Event implementation, unified Feature protocols (mcp, skills, etc.), defining a Runtime Driver SPI, with each different harness implementing its own Driver.
What we need to do at the core is define the API interfaces we need, unify the upper-layer execution semantics, but allow each Harness to retain its own native implementation methods and capabilities.
What Are Driver, Adapter, and Session
The integration methods for different Harnesses vary greatly.
They can be broadly categorized into: in-process SDK, resident protocol process (e.g., JSON-RPC / stdin / stdout), CLI subprocess + stream-json / NDJSON, SDK wrapping CLI, remote Runtime. Some SDKs indeed still start a CLI underneath, but you cannot assume all SDKs are just CLI Wrappers.
Therefore, the Core should not care about how it actually starts.
The Runtime can be broken down into the following core concepts:
RuntimeDriver = The concrete implementation of a Harness
RuntimeAdapter = The public handle after Runtime registration
RuntimeAgentSession = The logical Session that the Core actually operates on
NativeSession = The Harness's own native Session
RuntimeSubmitHandle = The execution handle for a single Run / Turn
In one sentence:
Driver is the implementation side, Adapter is the public side, Session is the runtime side, Run is the execution side.
The Driver SPI is defined via defineRuntimeDriver(). Through this definition, the transformation from native implementation to system implementation is completed, including the implementation of various Features, session creation, streaming event mapping, etc.
const adapter = defineRuntimeDriver({
descriptor,
features,
createSession() {},
startTurn() {},
mapEvent() {},
cancelTurn() {},
steerTurn() {},
closeSession() {},
})
The internal definition is the RuntimeDriver, which is the part each harness must implement individually. The return value adapter is the handle for this runtime, and subsequent use of this handle completes all harness calls. For the core layer, the only thing truly perceived is the RuntimeAgentSession; it does not care whether the underlying layer is Codex, Claude Code, PI, or even some remote harness in the cloud. Then, the streaming data returned by different harnesses is also normalized into unified variables at the driver layer.
interface RuntimeAgentSession {
info()
messages()
contextWindow?: {
inspect()
canCompact()
compact()
}
submit(...)
steer(...)
close()
}
There is an easily overlooked issue here, which is the sessions corresponding to different harnesses. A Session is not just an ID; a cross-Harness Runtime needs to distinguish at least three layers of identity.
1. System Session
This is the Agent Team system's own Session: systemSessionId. It belongs to the system definition, not to any specific Harness.
It is responsible for connecting:
Expert
Execution
Workspace
Persistence
Event Log
Runtime Session
Even if the underlying Runtime changes in the future, the system still needs its own Session Identity.
2. Native Runtime Session
This is the Harness's own Session, for example:
PI → AgentSession
Codex → threadId
Claude Code → sessionId
Qoder CLI → sessionId
Antigravity → conversationId
In the system, it can be abstracted as:
RuntimeSessionRef {
type
id
}
When restoring a Session, the Core needs to ensure that: systemSessionId <-> RuntimeSessionRef can still be correctly mapped. You cannot use a Claude Code sessionId to restore a Codex Runtime, nor can you only save the native Session ID and lose the application-layer Session.
3. Run / Turn
A Session may execute many rounds of tasks. Therefore:
const run = session.submit(...)
Should return an independent execution handle:
interface RuntimeSubmitHandle {
runId
events
result
cancel()
}
Thus, close() closes the Session. And cancel() cancels a specific Run that is currently executing. Steering should also explicitly act on a specific runId. Only in this way can concurrency, cancellation, recovery, and parent-child task relationships within an Agent Team be truly handled.
How Different Harnesses Are Actually Integrated
The implementation methods for different Harness Runtimes are not the same:
Here, we must mention the ACP (Agent Client Protocol) protocol, an open-source protocol jointly launched by JetBrains and Zed, among others. Its positioning is similar to LSP (Language Server Protocol) in the programming domain, specifically designed for AI coding agents. Based on the JSON-RPC 2.0 specification, it facilitates host environments calling multiple different Agents. Here, you only need to implement one set of ACP runtime driver to quickly connect to all harnesses supporting the ACP protocol. Currently, although some harnesses have claimed to support the ACP protocol, many tool implementations are incomplete compared to CLI or SDK, so self-integration methods are still used. For example, cursor-agent acp and grok agent stdio, which support the ACP protocol relatively well, can be quickly integrated for trial use. ACP can be seen as: a highly standardized Runtime Driver Protocol.
In the future, a RemoteRuntimeDriver can also be implemented, placing the real Harness in a remote Sandbox, container, or Worker for execution. To the Core, Local Runtime and Remote Runtime ultimately remain just different Drivers.
How to Adapt to Different Harness Features
There is another very troublesome problem across Harnesses: the capabilities supported by different Harnesses are completely different.
For example: mcp, skills, thinking, steering, Image Attachment, Permissions, Resume. So, a Runtime Feature Framework needs to be defined, listing all required features, and the driver implements these features or marks them as unsupported. Each Harness must explicitly answer:
supported
degraded(reason)
unsupported(reason)
notApplicable(reason)
Here, degraded is very important. In the real world, Harnesses are rarely just: supported or unsupported. Often, it's actually:
It can work, but only supports a certain mode
It can work, but cannot steering
It can work, but the context window cannot get a precise denominator
It can work, but images can only be degraded to local file references
Feature is not just metadata; it can contain real implementation. For Features that need to prepare resources, it is itself a piece of implementation. For example:
const mcp = runtimeFeature.session({
async prepare(ctx) { ... }
});
const permissions = runtimeFeature.session({
needs: { mcp },
async prepare(ctx, { mcp }) { ... }
});
const skills = runtimeFeature.session({
needs: { mcp, permissions },
async prepare(ctx, { mcp, permissions }) { ... }
});
Thus, the Driver does not need to manually write: start MCP first, then start permission relay, then materialize Skill, and finally create Session. Features directly declare dependencies among themselves. The Core builds a Preparation Graph based on dependency relationships.
The Core will build a preparation dependency graph, managing the initialization order and resource release of Features. Moreover, the resources obtained by these Features, such as sockets, relays, MCP registries, etc., all enter the RuntimeResourceScope and are uniformly reclaimed when the Session ends.
The Core is responsible for executing the graph but does not hardcode the preparation order of specific Harnesses.
Session Scope and Turn Scope
Features should also have lifecycles. Some resources belong to the entire Session: MCP Server, Permission Relay, Managed HOME, Skill Directory, Native Process.
Some only belong to a single Turn: temporary Attachments, Turn-specific Model Selection, temporary resources for a specific run.
So, Runtime Features can be distinguished: driver, session, turn.
Session Features are prepared when the Session is created and released when the Session is closed. Turn Features are prepared during each submit() and released after the Run ends.
All dynamically created resources, such as:
socket
relay
MCP registry lease
temporary directory
listener
subprocess
All enter the RuntimeResourceScope.
This solves a very practical problem: What if initialization fails halfway?
For example: 1. MCP created successfully, 2. Permission Relay created successfully, 3. Skill materialization failed.
If resources are entirely managed by each Driver themselves, leaks are very likely. With a unified Resource Scope, whether it's: normal termination, initialization failure, cancellation, exception, Session close,
Resources can be released along the same lifecycle path.
What Specifically Needs to Be Adapted
The hardest part of implementing a Driver is not actually: spawn("claude"). The six categories of Harness differences that are truly difficult to adapt mainly include: Session, Streaming Event, Tools / MCP, Permissions, Context / Compaction, Process Environment. Of course, the actual Features are far more than six, also including: Model Discovery, Model Selection, Thinking, Attachments, Usage, Cancellation, Steering... But the six categories above are usually where Harness-specific logic most often appears.
Streaming Event is the most typical example among them. Taking Event as an example, what different Harnesses return is completely different:
PI → AgentEvent
Codex → app-server notification
Claude → stream-json
Qoder → SDKMessage
Agy → stream-json / NDJSON
So, each Driver needs: mapEvent(nativeEvent) to map into the Core. But what truly needs to be unified here is not just event names. If the Core only defines:
message.delta
thought.delta
tool.started
tool.completed
usage
progress
It's still far from enough. What truly needs to be unified is: the lifecycle, order, and causal relationships of events. Runtime Events roughly include:
run.started, run.completed, run.failed, run.cancelled
message.delta, message.completed
thought.delta
tool.started, tool.delta, tool.approval_requested, tool.completed, tool.failed
usage.updated
context-window.updated
progress
artifact.created
agent.command
At the same time, each event should also carry information like: eventId, sequence, runId, parentRunId, source, sessionId, parentSessionId, path.
Why is such complexity needed? Because a single Agent Chat only needs to know: "What did the model output?"
But an Agent Team needs to know: Who produced it? Which Run does it belong to? Who started it? Which Session does it belong to? Which Tool Call? What is the order? Has it entered a Terminal State?
So, Event Normalization is essentially not: JSON field format conversion, but: converting the native lifecycles of different Harnesses into unified system execution semantics.
Supported Is Not "Code Written"
The Feature Framework also has a design point I consider very important: declaring support for a capability must have evidence.
For example, if a Runtime Adapter writes: supportsMcp = true, what does it actually mean?
It might just be: the configuration file was written in. But the Harness didn't discover this MCP Server at all.
Or the Harness might have discovered: the MCP Server, but the model never successfully called it.
For example, MCP:
Materialized
→ MCP configuration has been generated
Discovered
→ Harness indeed discovered this MCP Server
Executed
→ Truly completed a side-effect-free Tool Call once
Only the last state can be said with some confidence: supported.
Similarly, Streaming cannot claim: supportsStreaming just because it finally got the complete answer.
At a minimum, it should have truly observed: delta -> delta -> message.completed -> run.completed.
supported is not declared by the Driver itself, but proven together by three layers: "implementation + contract verification + real Harness verification."
- Feature Implementation proves "the code indeed implements this capability." For example, MCP is not simply writing supportsMcp: true, but having a real runtimeFeature.session({ prepare() {} }); for capabilities like cancellation, if enabled, cancelTurn() must be implemented, otherwise defineRuntimeDriver() will report an error during registration. That is, first ensure "the code shape is complete."
- Runtime Conformance defines "what constitutes correct behavior." For example, declaring support for Streaming is not just about being able to return the full text in the end, but events must meet conditions.
- Runtime Probe runs these capabilities with a real Harness, and Probe Evidence saves the results of this real verification.
The true meaning of supported should be: This Feature is not only implemented in code, but also conforms to the unified Runtime behavior specification, and has actually been executed and verified on a real Harness.
degraded(EVIDENCE_PENDING) — not unimplemented, but the implementation already exists, yet there isn't enough real Probe evidence to elevate it to supported.
Written at the End
A good Runtime layer should not eliminate the differences of Harnesses. How to build a unified Runtime across Harnesses — what is unified here is semantics, not implementation, allowing different Harness differences to exist at runtime.