Skip to content

feat(v1): Environment consumes the Agent — single-agent as the default program#1946

Draft
hallerite wants to merge 1 commit into
feat/agent-programsfrom
feat/environment-on-agent
Draft

feat(v1): Environment consumes the Agent — single-agent as the default program#1946
hallerite wants to merge 1 commit into
feat/agent-programsfrom
feat/environment-on-agent

Conversation

@hallerite

@hallerite hallerite commented Jul 7, 2026

Copy link
Copy Markdown
Member

What

Stacked on #1939. The Environment becomes the first consumer of the Agent: it is now a program of agent interactions over a taskset, running the simplest program — a single agent on the task, n times. Every episode's rollouts are built through an internal Agent, so a config-driven eval and a hand-written agent program resolve placement, stage timeouts, and pairing validation through one path. This is the substrate for pattern envs (proposer→solver, same-box judging, debate) as Environment subclasses that contribute only their role bindings and a program body — the machinery (episodes, serving, scoring, provenance) stays in the base.

Changes

  • verifiers/v1/resolve.py (new): per-run resolution — TimeoutConfig, resolve_runtime_config, validate_pairing, cap_remote_harness_timeout — moved out of env.py to sit below both consumers (also breaks the agent ← env import cycle). Pure move, no behavior change.
  • Agent.rollout(): the resolve-only half of run() — placement (task-resolved runtime or borrowed box), timeouts (agent > task, remote cap), pairing validation — returning the un-run Rollout. run() is now rollout(...) + await. The eval's serving resources (shared_urls, interception pool) inject here, so they never appear on the program-facing run().
  • Rollout.run() stamps provenance itself: trace.info["agent"] (harness, model, runtime type + descriptor, borrowed flag) moves from Agent.run into the rollout — every trace, eval or program, now carries it. One descriptor per rollout, queryable after the objects are gone.
  • Environment: drops its mirrored resolution (runtime_for, the four per-stage timeout fields); episode() builds rollouts via Environment.agent(ctx) — one cached internal Agent per env (resolution state spans the eval; ctx still passes per rollout, so the env-server's per-request contexts keep working).
  • Docs: env module docstring, ARCHITECTURE.md wiring sentence, and an "the eval runs on the same primitive" section in docs/agent-programs.md.

Net: 9 files, +224/−194 — mostly the move; Environment.episode shrinks from ~55 lines of resolution to a list comprehension.

Not in this PR (deliberately)

The pattern-env contract itself — role bindings as config (--solver.model ...), an overridable program hook, trainable-trace marking for training one role of a multi-agent program. Those decisions overlap with #1941's topology tier and should land after that synthesis; this PR only puts the seam in place so they become subclass surface, not new machinery.

Validation

  • ruff + pytest tests/v1 (non-e2e) pass.
  • Live e2e matrix on the subprocess runtime (-m "e2e and not docker and not prime and not modal"): 19 passed in test_e2e.py (incl. the shared-tool cases that run through the env-server worker pool — prime-rl's path) and all of test_envs.py — real model calls end-to-end through the rewired Environment.
  • Live eval spot-check: both rollouts of an echo-v1 eval return reward=1.0 with per-rollout provenance stamps on their traces.
  • examples/agent_programs/smoke.py re-run live: the agent-program path unchanged, stamp intact (now emitted by the rollout).

🤖 Generated with Claude Code

Note

Make Environment consume an internal Agent for building episode rollouts

  • Environment now lazily constructs and caches an internal Agent (via Environment.agent), and delegates rollout construction to Agent.rollout rather than handling it directly. This aligns episode resolution behavior with agent programs.
  • Agent gains a new rollout method that returns a configured Rollout object without executing it, accepting optional shared_urls and interception overrides. Agent.run now delegates to this method.
  • Provenance stamping (trace.info["agent"]) moves from Agent.run to Rollout.run, so all rollouts (including env-internal ones) carry consistent harness, model, and runtime metadata.
  • TimeoutConfig, resolve_runtime_config, validate_pairing, and cap_remote_harness_timeout are extracted from env.py into a new resolve.py module, and imports across the codebase are updated accordingly.
  • Behavioral Change: Environment no longer exposes per-timeout attributes or a runtime_for(task) method; callers needing per-task runtime resolution must use resolve_runtime_config from verifiers.v1.resolve directly.
📊 Macroscope summarized 59b2ca1. 6 files reviewed, 0 issues evaluated, 0 issues filtered, 0 comments posted

🗂️ Filtered Issues

No issues evaluated.

…t program

The Environment is now framed as a program of agent interactions over a
taskset, running the simplest one: every episode's rollouts are built
through an internal Agent, so an eval run and a hand-written agent
program resolve placement, timeouts, and pairing through one path.

- verifiers/v1/resolve.py (new): per-run resolution — TimeoutConfig,
  resolve_runtime_config, validate_pairing, cap_remote_harness_timeout —
  moved below both consumers (breaks the agent <- env import cycle).
- Agent.run splits into rollout() (resolve-only, returns the Rollout;
  accepts the eval's serving resources) + run() (rollout + await).
- Rollout.run stamps trace.info["agent"] itself: every trace — eval or
  program — carries provenance now.
- Environment drops its mirrored resolution (runtime_for, per-stage
  timeout fields); episode() goes through Agent.rollout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread verifiers/v1/env.py
return resolve_runtime_config(
self.harness.config.runtime, task, self._warned_resources
)
def agent(self, ctx: ModelContext) -> Agent:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium v1/env.py:214

Environment.agent() caches the first Agent it builds and ignores every later ctx passed by the caller. env.agent(ctx2) returns the cached agent bound to ctx1, so when downstream code uses the returned agent without re-passing ctx, runs silently go to the original model/client/sampling config. Consider updating the cached agent's context (or returning a new agent) when ctx differs from the cached one.

🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @verifiers/v1/env.py around line 214:

`Environment.agent()` caches the first `Agent` it builds and ignores every later `ctx` passed by the caller. `env.agent(ctx2)` returns the cached agent bound to `ctx1`, so when downstream code uses the returned agent without re-passing `ctx`, runs silently go to the original model/client/sampling config. Consider updating the cached agent's context (or returning a new agent) when `ctx` differs from the cached one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant