feat(multi-agent): native concurrent floor (bench eval run --agents) + bf.* trajectory tree + hosted medical benchmark#846
feat(multi-agent): native concurrent floor (bench eval run --agents) + bf.* trajectory tree + hosted medical benchmark#846Yiminnn wants to merge 53 commits into
Conversation
Adds src/benchflow/arena/ — a self-contained, OPT-IN scaffold for the missing
"inter-agent" axis of multi-agent: N agents acting concurrently on ONE shared
environment (the deferred arena-concurrent mode). Touches no existing scored
path — the sequential scene path and the scalar reward path are untouched.
- protocol.py — turn-poll contract (Seam 3): observe/act, status in
{waiting, not_your_turn, your_turn(request_id), done}, stale-request rejection.
- runtime.py — run_arena (Seam 1 + eval glue): N seats concurrently via
asyncio.gather; a worker cap bounds concurrent DECISIONS (not waiting, so
interdependent seats never deadlock); a wall-clock deadline reaps stragglers.
- reward.py — SharedEnvReward (Seam 4): one shared standings map -> a per-seat
reward vector (pvp net / coop joint). Each seat is one RolloutNode, so this
rides the existing per-node Reward contract — no change to the scalar path.
tests/test_arena.py: an in-memory turn-gated fake env + scripted policies prove
concurrency, turn-gating, stale-request rejection, straggler reaping, and zero-
sum scoring — no ACP/sandbox/LLM (asyncio.run, no plugin dep).
examples/arena/duel_deepseek.py: a REAL run — two deepseek-v4-pro seats play one
shared rock-paper-scissors round concurrently through run_arena. Verified live:
seat-0 paper vs seat-1 scissors -> reward -100 / +100, chips conserved.
Seam 2 (a co-tenant SharedManifestEnvironment provisioning one service per scene)
is deferred to the caller; this scaffold drives any SeatClient. Build the rest at
the trigger — when an in-tree benchmark needs BenchFlow to host concurrent seats.
… trajectories
Each arena seat's raw LLM now goes through BenchFlow's provider proxy (so usage/
cost is tracked per agent), and every turn is captured as a per-seat trajectory.
- policy.py: ProxyChatPolicy + provider_config — reads BENCHFLOW_PROVIDER_BASE_URL
/_API_KEY/_MODEL (the proxy the SDK injects; falls back to provider-native env),
calls the OpenAI-compatible /chat/completions, tags each request with x-bf-seat,
and records the call.
- trajectory.py: SeatTrajectory — per-seat <seat>.trajectory.jsonl (status,
observation, legal_actions, action, llm {model, messages, response, usage}).
- runtime.py: run_arena gains an on_turn hook for per-seat decision capture.
tests: ProxyChatPolicy routes through BENCHFLOW_PROVIDER_* (httpx MockTransport,
no real LLM); SeatTrajectory writes per-seat jsonl; on_turn emits one record per
move; a 3-seat concurrent run.
examples: floor_deepseek.py (N-seat high-card via the proxy env) and
run_through_proxy.py — a REAL run: 3 deepseek-v4 seats through a loopback proxy
started by ensure_litellm_runtime. Verified live: the raw key is hidden from the
seats (proxy isolation invariant), proxy usage is aggregated (3298 tokens,
$0.0014, usage_source=provider_response), and each seat's decision trajectory is
captured with real model content.
…format After the run, write the proxy's captured exchanges to <run>/trajectory/llm_trajectory.jsonl via trajectory.to_jsonl(redact_keys=True) — mirroring rollout._write_llm_trajectory — so the per-seat raw-LLM calls land in the same on-disk trajectory format the viewer/verifier already read. Verified live: 3 deepseek-v4 seats through the proxy -> 3 raw exchanges captured + per-seat usage/cost, alongside the per-seat decision trajectories.
…roxy Demonstrates the intra-agent axis alongside the inter-agent arena: a real langgraph.StateGraph in the Multi-Agent-Medical-Assistant pattern (supervisor/ router -> KB / web-search specialists -> confidence-gated handoff -> output guardrail), run HOSTED BY BenchFlow — every node's LLM goes through a loopback LiteLLM proxy (ensure_litellm_runtime), so per-node usage/cost is tracked and the raw-LLM exchanges persist to <run>/trajectory/llm_trajectory.jsonl. The agent never sees the raw provider key. Verified live (deepseek-v4-pro): path supervisor -> retrieve_kb -> answer -> web_search -> answer -> guardrail (confidence gate fired), 4 node LLM calls captured, usage/cost aggregated through the proxy. langgraph/langchain-openai are example-only deps (not added to the core).
examples/sample_runs/ packs two real proxy-hosted runs (3-seat arena + LangGraph medical assistant) with their trajectories + a README of the commands/config. Adds a short pre-stop flush delay in the runners so the proxy's canonical llm_trajectory.jsonl captures as many calls as possible (final-callback flush is a LiteLLM timing artifact; per-seat decision trajectories are complete).
📦 Sample run results — multi-agent hosted by BenchFlow (proxy-tracked + trajectories)Two real runs, each with every agent's raw LLM routed through a loopback BenchFlow LiteLLM proxy ( ⬇️ Download the packed results folder: Shared config
1) Inter-agent arena — 3 concurrent seatsset -a; . ./sb-run.env; set +a
uv run python examples/arena/run_through_proxy.py 3→ 3 deepseek-v4 seats play one shared high-card round via 2) Intra-agent LangGraph Medical-Assistantuv pip install langgraph langchain-openai
set -a; . ./sb-run.env; set +a
MEDICAL_CONFIDENCE_THRESHOLD=1.01 \
uv run python examples/medical/run_through_proxy.py "What are the main side effects of metformin?"→ real Note: the proxy's canonical |
Medical-Assistant bench — native vs BenchFlow-hostedSame query (
Behaviour is identical; observability is the whole difference. Native gives you only the answer. Routing each specialist's LLM call through BenchFlow is what yields usage/cost + the raw-LLM trajectory, and the agent never sees the raw provider key. Why the original hosted run was a single file — and the fixLangGraph runs the nodes sequentially, and they shared one proxy → one callback log → one combined Entire running folder (
|
…es + native-vs-hosted runner Route each Medical-Assistant node (supervisor/answer/guardrail) through its own LiteLLM proxy so each agent yields a separate llm_trajectory + usage, instead of one shared proxy emitting a single combined log. Adds run_native_vs_hosted.py to run the bench direct-vs-hosted and diff observability.
…hmark Host the souvikmajumder26/Multi-Agent-Medical-Assistant pattern (LangGraph supervisor→specialists: router → KB/web specialists → confidence-gated handoff → guardrail) as a first-class BenchFlow benchmark that runs in a Docker sandbox with deepseek-v4-pro routed through the LiteLLM proxy. - medical-assistant ACP-shim agent (src/benchflow/agents/medical_acp_shim.py), registered in agents/registry.py; each graph node streamed as its own ACP tool_call step so the multi-agent structure is visible in the trajectory. - benchmarks/medical-assistant/: benchmark.yaml, 3 Harbor drug-safety tasks with deterministic keyword verifiers, job config, run script, README. - Verified: bench run in Docker → 3/3, errors=0; per-rollout llm_trajectory (proxy raw-LLM) + acp_trajectory (per-node steps) captured. The upstream's Azure-embeddings/Qdrant/CV/Tavily stack is out of scope on the deepseek-only path; the router→specialists→handoff→guardrail control flow is hosted faithfully with an in-process KB standing in for RAG.
Hosted as a BenchFlow benchmark — run in Docker (deepseek-v4-pro via the proxy)Per the Result:
Every rollout captured both trajectories under
Usage is Note on the verifierFirst run scored ibuprofen 0.5: the answer was clinically correct (named both real cautions — renal impairment + anticoagulant/bleeding) but the verifier required the drug-class token "NSAID" rather than the cautions the question asks for. The verifier was corrected to test the two actual cautions (matches the task KB); the re-run above is the honest harness result. Run itset -a; . ~/sb-run.env; set +a
python benchmarks/medical-assistant/run_medical_assistant.py
# or: bench eval run --config benchmarks/medical-assistant/medical-assistant-deepseek.yaml |
Track a multi-agent workflow's raw LLM calls through ONE pooled LiteLLM proxy as a structured trace tree, instead of one undifferentiated llm_trajectory. - providers/litellm_logging.py: the callback records per-call bf.* metadata (agent_id, agent_name, span_kind, parent_agent_id, run_id, session_id, and any other bf.* key) under request.body['bf'] instead of dropping all but model_group. One shared proxy log is now splittable per agent. - trajectories/agents.py: build_agent_tree() reconstructs an unmixed AgentTree from the tagged exchanges (group by agent, link by parent pointer), the same parent-pointer model LangSmith/OTel use. - examples/medical: the LangGraph slice tags each node via extra_body; verified end-to-end as one unmixed tree on local, docker, and daytona (run_agent_tree.py, run_in_sandbox.py). The single-trajectory problem is solved the industry way. - examples/arena/run_per_seat_proxy.py + arena/policy.py: per-seat proxy variant for black-box agents (separate trajectory per seat). - docs: reference/multi-agent-trajectory.md (the contract + industry comparison) and task-standard.md G7 (the normative bf.* metadata contract). Tests: tests/test_litellm_logging.py + tests/trajectories/test_agent_tree.py. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX
Run N real autonomous ACP agents playing ONE shared casinobench World concurrently — the live realization of the deferred arena-concurrent mode. - run_floor.py: starts casinobench's shared multi-seat World on the host, then runs a roster of seats with asyncio.gather; each seat is a connect_acp agent in its own DockerSandbox reaching the World over the docker bridge. Subscription agents (codex / claude-code) upload their host auth per seat and call their provider directly (acp_trajectory only); deepseek/proxy seats also get a per-seat raw llm_trajectory. Trajectories stream live (survive the wall-clock timeout); agents choose games freely. - town_snapshot.py: a live Stanford-Town-style viewer — casinobench's render_html canvas floor in live mode (agents walking to game stations) with a click-to-open per-agent run dossier injected; serves same-origin JSON so a Cloudflare tunnel can publish it, and falls back to the persisted run after the World stops. - agent_env/: the casino-agent-seat image — Node + codex-acp + claude-agent-acp (via benchflow's install commands) + the casino seven-tool CLI. The shim and the casino CLI package are assembled into the build context (gitignored; see README). Verified live with 10 concurrent agents (codex/gpt-5.5 + claude/sonnet-4-6 + claude/haiku-4-5) across all 18 casinobench games. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX
Casino floor — 10 concurrent real agents, all 18 gamesLive realization of
Total chips on the floor: 9007 (started 10000; the rest went to the house games). Per-seat trajectories captured under |
Run N agents on ONE shared task + its ONE service concurrently in ONE shared sandbox, each agent in its own /work/<seat> folder, declared via --agents agents.yaml, with per-agent ACP + raw-LLM trajectories in SEPARATE files. Builds on the arena concurrency scaffold (#846) and reuses connect_acp, the per-seat LiteLLM proxy, and the trajectory writer — no new concurrency or trajectory machinery. - arena/agents_manifest.py: pydantic AgentsManifest + count fan-out; each seat is `agent:` (prebuilt) XOR `manifest:` (BYOA, strict ACP-contract loader -> register_agent). Unique seat ids enforced. - arena/agent_driver.py: protocol-branched connect/prompt/close — acp/acpx via connect_acp+execute_prompts (covers raw ACP + ai-sdk); session-factory via Agent.connect/Session.prompt (structural; no session-factory agent registered yet). import_callable, never eval. - arena/instructions.py + AgentConfig.instruction_filename: write each seat's instructions into /work/<seat>/{CLAUDE.md|GEMINI.md|AGENTS.md} before launch. - arena/concurrent_floor.py: shared-sandbox orchestrator (asyncio.gather over seats), subscription-vs-proxy decided by ACTUAL auth (uses_native_subscription_auth), auto-loop + service-rounds (mock-service-driven rounds), per-seat try/except so one bad seat never kills the floor, separate acp_/llm_ trajectory files + roster.json/floor.json. - arena/bootstrap.py + cli/arena.py: `benchflow arena run --agents` seeds ONE shared sandbox + host service, runs, tears down; opt-in standings->reward vector. - examples/casino/agents.yaml + prompts/, examples/arena/README.md, docs note. Additive/opt-in: no scored path touched. 26 new tests; daytona parity deferred. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX
New on this branch: native concurrent multi-agent runner (
|
| File | Role |
|---|---|
arena/agents_manifest.py |
pydantic agents.yaml parser, count fan-out, agent: (prebuilt) XOR manifest: (BYOA), strict ACP-contract BYOA loader → register_agent, unique seat ids |
arena/agent_driver.py |
protocol-branched connect/prompt/close — acp/acpx→connect_acp+execute_prompts (covers raw ACP and ai-sdk); session-factory→Agent.connect/Session.prompt (omnigent). import_callable, never eval |
arena/instructions.py + AgentConfig.instruction_filename |
write each seat's instructions: into /work/<seat>/{CLAUDE.md|GEMINI.md|AGENTS.md} before launch |
arena/concurrent_floor.py |
shared-sandbox orchestrator (asyncio.gather), subscription-vs-proxy by actual auth (uses_native_subscription_auth), auto-loop + service-rounds (mock-service-driven rounds), per-seat try/except, separate acp_/llm_ trajectory files + roster.json/floor.json |
arena/bootstrap.py + cli/arena.py |
benchflow arena run --agents seeds ONE shared sandbox + host service, runs, tears down; opt-in standings→SharedEnvReward vector |
examples/casino/agents.yaml + prompts/, examples/arena/README.md |
worked example + docs |
Output layout
out/native-floor/casino/
├── roster.json # seat → agent / model / protocol / byoa
├── floor.json # per-seat status + raw flag + (opt) standings + reward
└── <seat>/trajectory/
├── acp_trajectory.jsonl # every seat
└── llm_trajectory.jsonl # PROXY seats only (oauth seats are acp-only by design)
Three agent paths, one config
raw ACP / ai-sdk / omnigent (prebuilt) or a BYOA manifest.toml all collapse to one AgentConfig. acp is the verified path (covers ai-sdk too); session-factory is structural/unverified — no session-factory agent is registered in this repo yet, so it's tested with a fake.
Multi-round (answering "can the mock server trigger multiple rounds?")
Yes. execute_prompts is re-entrant. drive: auto-loop (default) runs multi-round inside one prompt (the agent owns its observe→act loop); drive: service-rounds has the service drive the rounds — the runner polls per seat and re-prompts (nudges) only on YOUR_TURN, until DONE/deadline.
Tests
26 new tests (tests/test_{agents_manifest,agent_driver,agent_instructions,concurrent_floor,arena_cli}.py), all green; broad arena/registry regression green; ruff clean. (One pre-existing repo test, test_cli_verify_nonexistent_roundtrip, fails identically on clean HEAD — unrelated, hits an LLM judge.)
A code review pass was run; all findings addressed — notably the subscription gate now keys on actual auth (an API-key claude/codex seat correctly takes the proxy+raw path, not forced oauth-only), raw is reported only when a proxy actually ran, session-factory timeouts are non-fatal, and duplicate seat names are rejected.
Deliberate scope notes
- Kept the working
examples/casino/run_floor.py(per-agent-sandbox live-viewer demo) untouched; added the shared-sandbox native path alongside it (sameroster.json/<seat>/trajectory/layout, so the viewer still works). - Deferred (noted, not built): daytona shared-sandbox parity (the CLI fails closed on
--environment daytona),#847's exacttrajectory/agents/<seat>/nesting +agent_graph.json, CI smoke for the baked image.
Happy to split this into its own PR or retitle 846 if you'd prefer to keep the medical scope clean — just say which.
…e shared floor Validated against a real 5-seat docker run of examples/casino/agents.yaml. - sandbox/process.py: DockerProcess wrote its env to a FIXED /tmp/.benchflow_env and then `source … && rm`-ed it. With N agents in ONE shared container (the concurrent floor) they raced — one seat's cleanup deleted the file another was about to source (`bash: /tmp/.benchflow_env: No such file or directory`, ACP connect rc=1). Give each DockerProcess a unique /tmp/.benchflow_env_<uuid>. Identical behavior for single-agent runs. - arena: add `services.seat_env` (e.g. CASINOBENCH_SEAT_ID) so each seat's id is exported under the var the task CLI uses to identify the player; wire it in _seat_env and the casino example. After the fix: all 5 seats connect into the one shared sandbox (0 env errors, 0 ACP failures), each streams its own acp_trajectory.jsonl, and the deepseek proxy seat gets a separate raw llm_trajectory.jsonl (subscription seats stay acp-only). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX
…s proxy seat - bootstrap: start the host service with start_new_session=True and tear it down by signalling the whole process GROUP (SIGTERM -> wait -> sweep with SIGKILL -> reap), so uvicorn workers don't outlive the run (observed: leftover casino-service children after the floor finished). Reap the leader after SIGKILL on the health-fail and timeout paths too. - process.py: trap-based env-file cleanup (parity with the Daytona path) so the per-uuid /tmp/.benchflow_env_<uuid> is removed even if `source` fails or the process is signalled. - examples/casino/agents.yaml: swap the proxy seat from deepagents to openhands (protocol=acp, non-subscription -> routed through the per-seat LiteLLM proxy -> raw + acp trajectory; model reaches it via LLM_MODEL on the proxy path). - tests: process-group teardown reaps backgrounded children; dead-proc is a no-op. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX
✅ Validated end-to-end on docker — real 5-agent concurrent runRan
Standings (chips, start ≈1000) → reward vector: What this proves
Two real bugs the run surfaced — fixed (
|
…own viewer
The native runner wrote roster/floor + per-seat trajectories but (unlike
run_floor.py) never snapshotted the World event log, so the casino town viewer
had no events to animate the board for a finished run. Add opt-in
`services.events_path`; before teardown the floor fetches `{ "jsonl": … }` and
writes events.jsonl into the run dir. Wire it in the casino example.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX
The floor's ensure_litellm_runtime call didn't pass the sandbox handle, so a proxy seat (deepseek/deepagents) on --sandbox daytona raised 'sandbox-local LiteLLM requires a sandbox handle' — daytona/modal are sandbox-local (the proxy must run INSIDE the shared remote sandbox so the agent reaches it on localhost, unlike docker which runs it on the host + bridge). Pass sandbox=sandbox; ignored for docker. Verified: the deepseek seat now starts its in-sandbox proxy on daytona and plays.
…o-stop)
The floor's _make_sandbox used DaytonaSandbox's 0 defaults for auto_stop/auto_delete,
so Daytona stopped+deleted the shared sandbox during agent think-gaps mid-play —
every ACP session died ('remote container killed / idle sleep') and the run crashed
before writing floor.json. Match the normal rollout (setup.py) 1440-min keep-alive;
teardown() still deletes the sandbox explicitly when the floor finishes.
…h the floor) _run_seat's finally called stop_provider_runtime unguarded; on daytona that stop runs a sandbox exec which can error during concurrent teardown, and a raise from a finally propagates out of the seat → crashes the whole gather → reaps the shared sandbox → cascade of 'Bearer token invalid'/'sandbox not found'. The seat result is already computed, so suppress it — the floor completes and writes floor.json.
…'t OOM The floor packs N ACP agents + proxy-seat in-sandbox LiteLLM + the service into ONE sandbox; the SandboxConfig 1cpu/2GB default OOM-killed their ACP transports on daytona mid-play (agents had already logged 30+ tool calls). Docker survived on the host's resources. Request 8cpu/16GB for the floor sandbox.
…ses host mem) The 4cpu/8GB cap is a daytona necessity (one remote sandbox holds the whole floor); on docker it only over-constrains the container — the host manages memory. Leaving it on docker starved the sandbox on a busy host and failed the per-seat env-file writes. Apply cpus/memory_mb only for --sandbox daytona.
The ACP agent's stdio rides a long-lived SSH session to the daytona sandbox that goes silent during LLM think-gaps; with no traffic the gateway idle-drops it and the agent's stdout hits EOF (TransportClosed 'remote session killed'), killing seats mid-play — docker (a local pipe) never does this. Add ServerAliveInterval 15 / ServerAliveCountMax 8 / TCPKeepAlive to DaytonaProcess's ssh config. Result: a 5-agent daytona floor now COMPLETES 5/5 seats + 7 games (was 0/5, all dropped).
…Attempts) The residual daytona transport drops were all at connect-time — 5 seats race the SSH handshake concurrently at startup and some initial streams close before the agent listens (the ACP retry then recovers, so seats still connect). Add ConnectTimeout 30 + ConnectionAttempts 3 so the first connect lands more often — complements the ServerAliveInterval keepalive for mid-session.
|
Daily scan status (2026-07-03): current head |
|
Users Simulation review (2026-07-03): blocked. The draft remains non-mergeable. Targeted checks passed (
No live trajectory was generated, so the new |
A fixed 'native-floor' name made concurrent floor runs on one host share a docker-compose project and tear each other down at startup.
The floor never ran install_agent (only the rollout plane does), so every non-baked npm agent died rc=127 at launch; install each roster agent once per sandbox, serialized (concurrent npm installs into the shared prefix race). It also built agent_env by hand without the _BENCHFLOW_SUBSCRIPTION_AUTH marker resolve_agent_env would set, so codex/claude oauth seats fell to a key-less proxy; _subscription_marker replicates that one decision.
… path resolve - claude-agent-acp enumerates model ALIASES (fable, sonnet, opus[1m]) and rejects full ids (claude-fable-5) with an opaque -32603; when the exact id isn't among the advertised config-option values, resolve to the alias whose base name is a token of the id. Capability-first, no registry change. - the pre-connect 'which' agent-path resolve is advisory: under load a Daytona session command times out transiently and was killing the seat (openhands on a busy 4-cpu floor); on failure keep the unresolved launch command, which fails loudly later only if the binary truly is missing.
The adapter's model catalog is gated by account state in ~/.claude.json: with only .credentials.json uploaded the sandbox catalog omits gated models (e.g. fable) and rejects them. 0.40.0 also predates the current catalog.
build_town.py (multi-seat Stanford-town build with per-seat reasoning overlay), snapshot.py (post-run events fallback, no-flash static render), floor.html trajectory viewer, and the smoke/mixed rosters used to verify pi-acp/openhands/opencode/codex-acp/claude-agent-acp on docker + daytona.
…rics - nudge-on-event (auto-loop): an ACP agent acts only when its own loop polls or the harness opens a prompt turn — after a seat's prompt ends with wall-clock left, re-prompt it with just the pending turn/event digest (agent self-decides play/switch/leave); capped by nudge_max, two ignored nudges end the seat honestly, lobby-with-no-events means it chose to stop - acp_trajectory rows now carry ts (client arrival stamp — the ACP wire has no timestamps): think-gaps and turn latency become reconstructable - floor.json honesty: per-seat casino_actions/casino_timeouts from the real event log (acp calls are polls+scripting, not activity) + world outcomes (/_admin/outcomes: sat_out, busted)
PROMPT.md now documents all seven tools (leave was missing — the only queue escape) plus the etiquette the world enforces: observe --wait to wait for free, leave stale queues, silence gets defaulted then sat out.
|
Automation status refresh (2026-07-04): current head |
|
Users Simulation automation review (2026-07-04): still blocked on current draft head Current-head user simulation found real CLI contract blockers:
Not currently counted as a blocker: an in-memory |
|
Users Simulation automation review (2026-07-05): blocked on head Fresh isolated validation reproduced the merge blockers:
Labels: keep |
|
Users Simulation automation review (2026-07-06): still blocked on draft/dirty PR head Blockers reproduced:
Keep |
|
Users Simulation automation review (2026-07-07): still blocked on current draft head Fresh isolated validation added one more current-head blocker on top of the existing draft/dirty/no-current-CI state:
Keep |
|
Users Simulation automation review (2026-07-08): blocked on head Fresh blockers:
Passing evidence:
Thermo-nuclear review: the real issue is integration-boundary mess, not the bf-tree code itself. The alias crash plus prompt-source duplication should be fixed before this draft can be considered mergeable. Labels: kept |
|
Users Simulation automation review (2026-07-10): still blocked on head Live gate remains unchanged: this PR is still draft and conflicting, so no merge-readiness decision can clear it. Cheap checks in an isolated worktree passed after syncing dev deps:
Thermo-nuclear blockers remain:
Keeping |
|
Users Simulation automation review (2026-07-11): still blocked on head Live PR state is still not mergeable: draft, Cheap deterministic checks:
Strict review blockers:
Final verdict: blocked. Passing focused unit tests are not enough while ruff, ty, CLI, artifact contract, docs/example, and structural gates still fail. |
|
Automation cleanup pushed: Fixed deterministic code/config blockers:
Validation run locally on the pushed tree:
Still blocked / no label changes:
Keeping |
Summary
An opt-in, additive path for multi-agent on BenchFlow — the missing
inter-agent concurrency axis — taken from scaffold to real autonomous agents
competing live in one shared sandbox, plus the intra-agent trajectory tracking
that makes a shared proxy legible, plus a hosted multi-agent benchmark (medical).
Three additive layers (no scored single-agent path is touched):
1. Native concurrent multi-agent floor —
bench eval run --agents(src/benchflow/arena/,benchmarks/casinobench/)N real autonomous ACP agents play ONE shared environment concurrently, competing on
one leaderboard — reached through the standard run command, not a bespoke verb.
--agents roster.yamlis the file form of repeated--agent/--model(a pure agents list; rejects run-level keys). Task(
--tasks-dir/--game), service (--environment-manifest), sandbox, out, drive,prompt all follow the normal
eval runflags.--agentsis mutually exclusivewith
--agent;arena runis a deprecated hidden alias.<agent>-<model>(e.g.codex-acp-gpt-5.5-0) so thefloor + viewer identify agent + model at a glance.
rollout sandbox via
ManifestEnvironmentonlocalhost:9001(no hostsubprocess — the earlier host-bridge design was removed). N seats share ONE
service; each seat runs in
/work/<seat>with its ownacp_trajectory.jsonl, andAPI-key seats additionally get a per-seat LiteLLM proxy → raw
llm_trajectory.jsonl(subscription/oauth seats go provider-direct, acp-only).
benchmarks/casinobench— env-0 task package (environment.toml+casinobench-baseimage baking the engine[service]+ thecasinoCLI). Thedeterministic engine/CLI/service live in the separate benchflow-ai/casinobench
repo; this benchmark references them via the Docker build context.
events.jsonlsnapshot → replayable Stanford-Town board2. Multi-agent trajectory tracking — one proxy, structured tree (
bf.*)The fix for "a shared proxy yields one undifferentiated
llm_trajectory": tag eachcall with
bf.*metadata and reconstruct the agent tree (OTel GenAI / LangSmithparent_run_idshape).providers/litellm_logging.py— the callback records per-callbf.{agent_id,agent_name,span_kind,parent_agent_id,run_id,session_id,…}.trajectories/agents.py—build_agent_tree()rebuilds an unmixed AgentTree.docs/reference/multi-agent-trajectory.md+task-standard.mdG7.3. Hosted multi-agent benchmark — Multi-Agent Medical Assistant (
benchmarks/medical-assistant/,examples/medical/)souvikmajumder26/Multi-Agent-Medical-Assistanthosted on BenchFlow: a realLangGraph supervisor→specialists graph (router → KB/web → confidence-gated handoff →
guardrail) run via ACP shim, each node tagged so the whole run reconstructs as one
unmixed tree — verified end-to-end on local, docker, and daytona.
Casino hardening (this branch)
Reward-integrity + robustness fixes found by inspecting real runs (casinobench-repo
commits, referenced via the build context):
BENCHFLOW_SEED, letting a seat compute the deterministic roulette spin and bet thewinning number (observed 26/27 straight-up, +45k). Now a secret
os.urandomseed (
CASINO_WORLD_SEEDpins it for tests);BENCHFLOW_SEEDno longer forwarded.Rerun confirms roulette back to ~fair (10–25% small-sample), max chips 2.4× not 46×.
CASINOBENCH_SEAT_IDdefaults to its/work/<seat>folder, so it registers under its real name even when an agent runtime doesn't
propagate the env (fixed the deepseek seat showing as
agent).(was: the opponent pinned forever, no per-turn timeout).
dossier rows show the command.
Test plan
162 passedon the branch).casino-service, separate per-seattrajectories, standings + reward vector, animated board — rerun after each fix.
bf.*agent-tree unmixed on local + docker + daytona.by the agent) — follow-up.
agent_graph.jsontrace layout — follow-up.🤖 Generated with Claude Code
https://claude.ai/code/session_01JDzpphBuqpjMH23dVJEoYX