Skip to content

Follow-up: Agent dyn-compatibility in Rust — resolve generic harness asymmetry #45

@sbeardsley

Description

@sbeardsley

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:

  1. 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.

  2. Testing: Injecting a MockAgent requires the harness to be parameterised as StandardHarness<MockAgent>. This works but is less ergonomic than Arc<dyn Agent>.

  3. 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 BoxFutAgent 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

  • Investigate trait_variant option for dyn dispatch (Option C)
  • Decide: Option A (accept generic), Option B (BoxFut), or Option C (trait_variant)
  • If Option B or C: update Agent trait in all four languages
  • Update StandardHarness to remove generic parameter (if Option B/C)
  • Update HarnessBuilder::build() return type
  • Verify all existing Agent tests still pass
  • Verify harness tests still pass with updated Agent

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions