Context
During #3 (Harness runtime loop) implementation, CC surfaced that Agent is not dyn-compatible in Rust because it uses trait_variant::make (RPITIT — return-position impl Trait in trait). As a result, StandardHarness<A: Agent> is generic over the concrete agent type rather than Arc<dyn Agent> like all other component traits.
This is the only asymmetry in the Rust component model. Every other component trait (ModelInterface, ToolRegistry, SandboxProvider, ContextManager, etc.) uses Arc<dyn Trait> and is dyn-compatible via a BoxFut<'a, T> hand-rolled pattern.
The Problem
The asymmetry in HarnessConfig:
// All other components:
pub model: Arc<dyn ModelInterface + Send + Sync>,
pub tool_registry: Arc<dyn ToolRegistry + Send + Sync>,
pub sandbox: Arc<dyn SandboxProvider + Send + Sync>,
// ...
// Agent — different:
// StandardHarness<A: Agent> is generic, not Arc<dyn Agent>
This has practical consequences:
-
Builder ergonomics: HarnessBuilder::build() returns StandardHarness<ConcreteAgent> — a concrete type, not impl Harness. Users who want to store a harness in a struct field need to know the concrete agent type, which leaks implementation details.
-
Testing: Injecting a MockAgent requires the harness to be parameterised as StandardHarness<MockAgent>. This works but is less ergonomic than Arc<dyn Agent>.
-
SubagentTool: The evaluator harness in SelfVerifying and child harnesses in SubagentTool are stored as Arc<dyn Harness>. If the harness is generic over A: Agent, Arc<dyn Harness> requires object safety — which means Harness itself must be dyn-compatible. This adds complexity.
Options
Option A: Accept the generic harness (current state)
Keep StandardHarness<A: Agent>. The Harness trait is object-safe (all methods take &self and return BoxFut). SubagentTool stores Arc<dyn Harness> which works. The generic parameter is contained inside StandardHarness — callers interact via the Harness trait.
Pros: no change needed, already working
Cons: HarnessBuilder::build() has a complex return type, slightly more verbose in tests
Option B: Make Agent dyn-compatible via BoxFut
Replace RPITIT on Agent::turn() with BoxFut:
// Instead of:
async fn turn(&self, context: Context, on_stream: Option<StreamHandler>) -> TurnResult;
// Use:
fn turn<'a>(
&'a self,
context: Context,
on_stream: Option<StreamHandler>,
) -> BoxFut<'a, TurnResult>;
This makes Agent dyn-compatible and Arc<dyn Agent + Send + Sync> works. All other component traits already use this pattern. StandardHarness becomes StandardHarness (no generic parameter).
Pros: consistent with all other component traits, simpler harness type, cleaner builder
Cons: slightly less ergonomic Agent implementations (must box the future explicitly)
Option C: Use trait_variant for dyn dispatch
The trait_variant crate (which is already in use) can generate both an async version and a dyn-compatible version. Investigate whether this covers the use case cleanly.
Recommendation
Option B is the most consistent with the existing codebase. All other component traits use BoxFut — Agent should too. The implementation ergonomics difference is minor (one .boxed() call per turn implementation) and the benefit is a fully consistent component model.
This is not urgent — Option A is working and contained. But it should be resolved before #7 (ContextManager) lands since that's when the harness type complexity starts to matter for callers assembling full harness instances.
Checklist
Related Issues
Context
During #3 (Harness runtime loop) implementation, CC surfaced that
Agentis notdyn-compatible in Rust because it usestrait_variant::make(RPITIT — return-positionimpl Traitin trait). As a result,StandardHarness<A: Agent>is generic over the concrete agent type rather thanArc<dyn Agent>like all other component traits.This is the only asymmetry in the Rust component model. Every other component trait (
ModelInterface,ToolRegistry,SandboxProvider,ContextManager, etc.) usesArc<dyn Trait>and is dyn-compatible via aBoxFut<'a, T>hand-rolled pattern.The Problem
The asymmetry in
HarnessConfig:This has practical consequences:
Builder ergonomics:
HarnessBuilder::build()returnsStandardHarness<ConcreteAgent>— a concrete type, notimpl Harness. Users who want to store a harness in a struct field need to know the concrete agent type, which leaks implementation details.Testing: Injecting a
MockAgentrequires the harness to be parameterised asStandardHarness<MockAgent>. This works but is less ergonomic thanArc<dyn Agent>.SubagentTool: The evaluator harness in
SelfVerifyingand child harnesses inSubagentToolare stored asArc<dyn Harness>. If the harness is generic overA: Agent,Arc<dyn Harness>requires object safety — which meansHarnessitself must be dyn-compatible. This adds complexity.Options
Option A: Accept the generic harness (current state)
Keep
StandardHarness<A: Agent>. TheHarnesstrait is object-safe (all methods take&selfand returnBoxFut).SubagentToolstoresArc<dyn Harness>which works. The generic parameter is contained insideStandardHarness— callers interact via theHarnesstrait.Pros: no change needed, already working
Cons:
HarnessBuilder::build()has a complex return type, slightly more verbose in testsOption B: Make Agent dyn-compatible via BoxFut
Replace RPITIT on
Agent::turn()withBoxFut:This makes
Agentdyn-compatible andArc<dyn Agent + Send + Sync>works. All other component traits already use this pattern.StandardHarnessbecomesStandardHarness(no generic parameter).Pros: consistent with all other component traits, simpler harness type, cleaner builder
Cons: slightly less ergonomic
Agentimplementations (must box the future explicitly)Option C: Use trait_variant for dyn dispatch
The
trait_variantcrate (which is already in use) can generate both an async version and a dyn-compatible version. Investigate whether this covers the use case cleanly.Recommendation
Option B is the most consistent with the existing codebase. All other component traits use
BoxFut—Agentshould too. The implementation ergonomics difference is minor (one.boxed()call perturnimplementation) and the benefit is a fully consistent component model.This is not urgent — Option A is working and contained. But it should be resolved before #7 (ContextManager) lands since that's when the harness type complexity starts to matter for callers assembling full harness instances.
Checklist
trait_variantoption for dyn dispatch (Option C)Agenttrait in all four languagesStandardHarnessto remove generic parameter (if Option B/C)HarnessBuilder::build()return typeRelated Issues