Skip to content

feat(v1): Agent facade — programming over agents#1939

Open
hallerite wants to merge 15 commits into
mainfrom
feat/agent-programs
Open

feat(v1): Agent facade — programming over agents#1939
hallerite wants to merge 15 commits into
mainfrom
feat/agent-programs

Conversation

@hallerite

@hallerite hallerite commented Jul 7, 2026

Copy link
Copy Markdown
Member

What

The Agent facade: programming over agents as a first-class v1 surface.

An Agent is the complementary regrouping of the Rollout tuple — (harness × model × runtime policy), with the task per call — and one executable arrow:

import verifiers.v1 as vf
from verifiers.v1.harnesses.default import DefaultHarness, DefaultHarnessConfig

ctx = vf.ModelContext(model="z-ai/glm-5.2", client=vf.resolve_client(vf.EvalClientConfig()))
solver = vf.Agent(DefaultHarness(DefaultHarnessConfig()), ctx)
trace = await solver.run(vf.Task(vf.TaskData(idx=0, prompt="What is 2+2?")))

Construction is fully explicit, per v1 style — no id strings, no hidden client resolution: a concrete Harness object (stateless — one instance can back many agents) and a ModelContext (#1940's rename, binding cleanly here: an agent IS a model in a harness). The client is the caller's to build and share — agents on one endpoint share one connection pool, and prime-rl hands agents its renderer client through the same type.

Everything beyond the arrow is a parameter, not a concept:

  • Placement: runtime= places a run into a live box (borrowed — the run neither starts nor tears it down; creator owns the lifecycle). agent.provision(task) hands the program a box, so several runs — by one agent or several — can share one world (judge in the solver's sandbox). Borrowing a box its owner already tore down raises immediately, and a box torn down mid-run raises at the awaited run() with the raw failure chained underneath — lifetime bugs in the program, surfaced to the caller instead of captured as misattributed world errors on the trace.
  • Judgement: rides on the task. A Task subclass's hooks (setup/finalize) and signals (@reward/@metric) run exactly as in an eval; a plain base vf.Task(data) has neither, so the run is unscored — a pure Task -> Trace arrow. Impossible pairings are refused per run with the same guards as an eval: MCP / user-sim support, and NEEDS_CONTAINER against the runtime the run actually resolves to.
  • Model override: ctx= replaces the agent's model context for one run (dataclasses.replace(agent.ctx, model=...) — a judge sweeping models).
  • Chaining: plain traces -> Task functions, fan-out is plain asyncio.gather. TaskData.sources (trace ids) + TaskData.relation stamp lineage; each trace carries info["agent"] (harness, model, runtime type + descriptor, borrowed flag), so multi-agent programs stay reconstructable after the fact.

The Agent surface has no group verb: each run scores its rollout on its own, and comparing siblings (relative success, preference, advantages) belongs to whoever gathered the traces — in training, prime-rl samples the group. Existing group machinery (@group_reward, Task.score_group) is untouched by this PR; its deprecation is a separate discussion.

Every run is a standard Rollout (staged lifecycle, typed error attribution, token-true trace capture) — the Agent only decides what goes into the rollout. Entered as an async context manager it owns an InterceptionPool; un-entered, each run brings up its own per-rollout server (script mode).

Changes

Additive, small surface (15 files, +763/−42):

  • verifiers/v1/agent.py (new): Agent; EvalClientConfig/TrainClientConfig join the v1 exports; ModelContext.sampling gains a default.
  • verifiers/v1/rollout.py: optional runtime= — a borrowed live runtime skips make_runtime/start()/stop(); borrowing a stopped runtime raises up front (Runtime.stop() now marks the runtime stopped).
  • verifiers/v1/task.py: TaskData gains sources: tuple[str, ...] + relation: str | None lineage stamps.
  • verifiers/v1/env.py: one set of pairing guards for both entry paths — validate_pairing takes the task class and the runtime config the run resolves to (shared by Environment once at init and Agent.run per run), and the 24h remote-sandbox timeout cap is extracted as cap_remote_harness_timeout. Environment behavior unchanged.
  • docs/v1/agent-programs.md + examples/agent_programs/: guide and four runnable programs.
  • .gitignore: also match a .venv symlink (one had slipped into the tree).

No behavior changes to Environment, Episode, scoring, or the serve layer. A follow-up can have Environment consume an Agent internally, collapsing the remaining mirrored resolution logic.

Validation

ruff check, the full pytest tests/ suite, and the v1 e2e CI matrix (-m "not prime and not modal") with live model calls all pass. All four examples ran live end-to-end on Prime Inference (a self-hosted GLM-5.2 deployment standing in for the committed model ids):

  • smoke.py — the primitive on subprocess.
  • same_box_judge.py — one prime sandbox: solver runs borrowed, program writes the solver's trace into the box, judge runs in the same box, independently recomputes the answer, checks the trajectory for hardcoding, returns parseable JSON; lineage + same-descriptor stamps intact; sandbox torn down by its creator.
  • proposer_solver.py — cross-model chain, typed ProposedData/ProposedTask, asyncio.gather fan-out (n=4) scored by the task's own @reward, pooled interception.
  • world_verified.py — the real mini-swe-agent CLI through the facade in a fresh sandbox, with a runtime-aware @reward re-executing the artifact in the live box.

Design context

This is step 1 of the agent-programs design (prime-rl design/agent-programs.md, forthcoming): Agent facade → provenance stamps → judge/harness patterns → agent programs as envs, with groups sampled consumer-side by prime-rl. It is deliberately the library layer — a runnable value usable from any Python (scripts, notebooks, prime-rl's sampler); how multi-agent programs package/ship as evals is the follow-up conversation (see #1941 for a parallel take on that layer).

🤖 Generated with Claude Code


Note

Medium Risk
Touches core rollout/runtime lifecycle (borrowed boxes, teardown semantics) and shared pairing validation, but eval Environment paths are refactored rather than behavior-changed; main risk is incorrect sandbox lifetime or pairing edge cases in agent programs.

Overview
Introduces vf.Agent, a reusable harness × model × runtime value with agent.run(task) as the single execution path. Runs still go through standard Rollout machinery; the agent wires timeouts, pairing checks, optional shared InterceptionPool (via async with agent), agent.provision for shared sandboxes, and stamps trace.info["agent"] for attribution.

Placement: Rollout accepts an optional borrowed runtime= — no provision/start/stop on that path; Runtime.stopped is set at teardown and borrowing a dead box raises immediately.

Lineage: TaskData gains sources and relation for trace-derived tasks; multi-agent flow is plain functions plus asyncio.gather, not a built-in group API.

Shared eval guards: validate_pairing and cap_remote_harness_timeout are generalized for per-run Agent use (runtime-aware NEEDS_CONTAINER checks); Environment behavior is refactored to call the same helpers.

API/docs: Agent, EvalClientConfig, TrainClientConfig exported; ModelContext.sampling defaults when omitted. New docs/v1/agent-programs.md, mint nav entry, and four examples/agent_programs/ scripts. Minor .gitignore tweak for .venv.

Reviewed by Cursor Bugbot for commit 8f4f6ea. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add Agent facade for programming over agents with runtime borrowing and task lineage

  • Introduces Agent as a high-level class that wraps a Harness, ModelContext, and runtime policy to run Task instances via run() and provision(), with support for borrowed runtimes and shared InterceptionPool.
  • Adds TaskData.sources and TaskData.relation fields to carry lineage metadata across proposer→solver and judge workflows.
  • Refactors validate_pairing in env.py to accept an explicit task class and RuntimeConfig, and extracts cap_remote_harness_timeout as a shared helper used by both Environment and Agent.
  • Extends Rollout to accept a borrowed Runtime, with error handling for runtimes torn down before or during a run.
  • Adds example scripts for smoke testing, same-box judging, proposer→solver fan-out, and world-verified rewards under examples/agent_programs/.
  • Behavioral Change: ModelContext now defaults sampling to a fresh Sampling() instance rather than requiring it at construction.

Macroscope summarized 58a5e1d.

Comment thread verifiers/v1/agent.py
Comment thread verifiers/v1/agent.py Outdated
Comment thread verifiers/v1/agent.py
Comment thread verifiers/v1/agent.py
Comment thread verifiers/v1/agent.py
Comment thread verifiers/v1/agent.py
Comment thread verifiers/v1/agent.py Outdated
@hallerite hallerite force-pushed the feat/agent-programs branch from 617019a to 20ff1de Compare July 7, 2026 19:51
Comment thread verifiers/v1/agent.py Outdated
Comment thread examples/agent_programs/proposer_solver.py
Comment thread verifiers/v1/agent.py
Comment thread examples/agent_programs/same_box_judge.py Outdated
hallerite and others added 13 commits July 7, 2026 20:59
An Agent is (harness x rollout context x runtime policy) with one executable
arrow: agent.run(task) -> Trace. Placement is a parameter (runtime= borrows a
live box; provision() hands out boxes), judgement is optional (taskset=), and
chaining is plain traces -> Task functions (Task.sources/relation stamp lineage).
Rollout learns to run in a borrowed runtime (creator owns start/teardown).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Fan-out is plain asyncio.gather over agent.run; grouping (and comparative
scoring across a group) belongs to the consumer — in training, prime-rl
samples the group. No new group surface while @group_reward heads toward
deprecation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
An agent IS a model in a harness: Agent("codex", "z-ai/glm-5.2",
PrimeConfig()). Pass a model name (default Prime inference endpoint, or
client=/sampling= overrides), or a prebuilt RolloutContext for callers that
manage their own Client (prime-rl's orchestrator). make_context is folded in.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
v1 construction is explicit — no id-string sugar in the constructor. Build
the harness yourself (DefaultHarness(DefaultHarnessConfig())) or via
load_harness(config); harnesses are stateless, so one instance can back any
number of agents.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RolloutContext is already the canonical (model + client + sampling) carrier
every Rollout holds — the Agent binds one at construction instead of hiding
client resolution behind a string. Clients are built by the caller and shared
across agents on one endpoint (one connection pool); sampling gains a default.
EvalClientConfig/TrainClientConfig join the v1 exports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…race stamp

Align the facade files with main's ModelContext rename (#1940) and fold in
review cleanups: run() takes one ctx override instead of loose model=/sampling=
kwargs; the trace stamp is built inline where the resolved values live (the
fingerprint property promised a shape it never stamped); the shared NullTaskset
is a module constant; borrowed-runtime locality reads the instance classvar.
Group scoring stays in verifiers — the Agent surface just has no group verb;
docs now say exactly that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Runtime.stop() marks the runtime stopped (before the teardown await — no new
borrows once teardown begins); a Rollout handed a stopped borrow raises to the
caller up front instead of failing opaquely mid-harness and being captured as
trace data. A dead borrow is a lifetime bug in the borrowing program, not a
property of the rollout's world.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Extract validate_pairing (MCP / user-sim / NEEDS_CONTAINER) and the 24h
remote-sandbox harness-timeout cap out of Environment into shared env
helpers and apply them per run in Agent.run, against the resolved (or
borrowed) runtime. Also: provision() now reaches stop() when start()
fails partway (no leaked remote sandbox), and re-entering an Agent
raises instead of leaking the first interception pool.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
same_box_judge still used the pre-ModelContext string constructor (would
crash on ctx.model); proposer_solver now fails with a clear error when
the proposer's reply has no JSON object; world_verified bounds its
world-re-executing reward with a scoring timeout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@hallerite hallerite force-pushed the feat/agent-programs branch from 9511796 to 5eac481 Compare July 7, 2026 21:00
Comment thread examples/agent_programs/proposer_solver.py Outdated
# Conflicts:
#	.gitignore
#	docs/mint.json
#	verifiers/v1/clients/client.py
#	verifiers/v1/env.py
#	verifiers/v1/rollout.py
#	verifiers/v1/task.py
@hallerite hallerite force-pushed the feat/agent-programs branch from ea4dd75 to 8f4f6ea Compare July 11, 2026 05:03
@hallerite hallerite marked this pull request as ready for review July 11, 2026 05:03
@macroscopeapp

macroscopeapp Bot commented Jul 11, 2026

Copy link
Copy Markdown

Approvability

Verdict: Needs human review

This PR introduces a substantial new feature (Agent facade) with new user-facing APIs, new runtime patterns (borrowed boxes, runtime pooling), and modifications to core Rollout lifecycle handling. New features of this scope warrant human review.

You can customize Macroscope's approvability policy. Learn more.

…ng program

The tombstone catches borrow-after-teardown, but an owner tearing the box down
under an in-flight run surfaced as an opaque harness error captured onto the
trace (e.g. 'harness exited 137'). Same lifetime bug, same channel now: the
rollout re-attributes the failure and raises to the caller with the raw error
chained, instead of recording a misattributed world error as data.

Live-verified on docker: owner stop() at t=25s under a sleeping harness ->
ValueError('borrowed runtime ... torn down by its owner mid-run') from
HarnessError(exit 137).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eligotts added a commit that referenced this pull request Jul 11, 2026
… the resolved runtime

Three correctness holes vs the agent-programs work (#1939), two of them ours:

- The dead-borrow tombstone guard existed pre-merge and was silently lost
  adopting main's rollout.py — Runtime.stopped's docstring promised a refusal
  nothing enforced. Restored: borrowing a stopped box raises at the caller.
- An owner tearing a borrowed box down under an in-flight run surfaced as an
  opaque harness error captured onto the trace — a lifetime bug misattributed
  as world failure (and retryable). Ported #1939's re-attribution: same bug,
  same channel — raised to the caller with the raw failure chained.
- NEEDS_CONTAINER was checked against harness.config.runtime, not where the
  run actually lands: a container-needing task borrowed into a docker box
  under a subprocess-defaulting harness was wrongly refused, and the mirror
  case wrongly allowed. validate_task_pairing now takes the resolved
  runtime_config; Agent already resolved it and keyed its cache on it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
eligotts added a commit that referenced this pull request Jul 11, 2026
Adds what the reviews surfaced but the ledger didn't yet record: instance-level
retries (Topology.complete as the predicate, the RetryConfig slot kept open for
it), right-sizing graph wire payloads (eval-mode servers now ship training
tensors nobody reads), the best-of-N built-in as the group-reward successor and
migration story, the reviewed config-layer cleanup pass (topology.py
authoring/runner split killing the EnvConfig model_rebuild trick, RolloutLimits
duplication, the multiplex name collision, timeout vocabulary, dead env_id) with
its considered-and-rejected list, and the PR #1939 reconciliation end state.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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