Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
8074bdb
feat(task): play an outside agent through a stand-in agent in its place
earakely-scale Oct 19, 2026
f0c609b
docs: an episode plays its outside agents through a stand-in agent
earakely-scale Oct 19, 2026
c7773c7
fix(task): a turn's reply that can't reach its stand-in leaves the fa…
earakely-scale Oct 19, 2026
4072d2c
test: the stand-in's pinned requirements follow the protocol package'…
earakely-scale Oct 20, 2026
0ad10ac
feat(agents): Claude Code declares where it works on a VM host, so a …
earakely-scale Oct 20, 2026
89f819f
fix(episode): keep credentials from the stand-in, record failed calls…
earakely-scale Oct 20, 2026
ebb3a44
test(stand-in): count a killed child as stopped once it's a zombie
earakely-scale Oct 20, 2026
1196dd1
feat(protocol): StandInAgent, an agent with no model that a driver pl…
earakely-scale Oct 21, 2026
c8da802
Merge branch 'feat/stand-in-protocol' into feat/stand-in-agent
earakely-scale Oct 21, 2026
24c6f76
refactor(agents): the stand-in is an agent of the agents bundle, writ…
earakely-scale Oct 21, 2026
83a636e
refactor(protocol): ProxyAgent and urn:agentenv:proxy/v1, without wha…
earakely-scale Oct 21, 2026
c9cbae7
refactor(protocol): the content of the ProxyAgent rename the previous…
earakely-scale Oct 21, 2026
d9173f7
Merge remote-tracking branch 'origin/main' into feat/stand-in-protocol
earakely-scale Oct 23, 2026
639da22
feat(protocol): every action the driver's model takes runs in the pro…
earakely-scale Oct 23, 2026
38cb125
Merge branch 'feat/stand-in-protocol' into feat/stand-in-agent
earakely-scale Oct 23, 2026
a52d7a2
feat(task): play an outside agent through a ProxyAgent in its place
earakely-scale Oct 23, 2026
27bb322
fix(protocol): a tool action's deadline covers connecting, and turns …
earakely-scale Oct 23, 2026
845d7ed
fix(task): a turn's bash result shows a failed command's exit code an…
earakely-scale Oct 23, 2026
ef0e14a
Merge branch 'feat/stand-in-protocol' into feat/stand-in-agent
earakely-scale Oct 23, 2026
3bf71fb
Merge remote-tracking branch 'origin/main' into feat/stand-in-agent
earakely-scale Oct 23, 2026
88f9482
fix(bundle): plan_entities takes no agent override; plan passes its o…
earakely-scale Oct 23, 2026
e1aef46
test(env): the Modal load test builds its server from a context Modal…
earakely-scale Oct 23, 2026
6096598
Merge remote-tracking branch 'origin/main' into feat/stand-in-agent
earakely-scale Oct 23, 2026
c9ae1fb
fix(agents): the proxy needs protocol 0.1.370, and Gemini and OpenCod…
earakely-scale Oct 23, 2026
d482601
fix(task_steps): verify_sandbox and collect_artifacts reach an agent …
earakely-scale Oct 23, 2026
11523f2
fix(protocol): the proxy's tool deadlines hold on Python 3.10
earakely-scale Oct 23, 2026
b634036
fix(protocol): the proxy answers a stopped action, parses its own res…
earakely-scale Oct 24, 2026
326422a
fix(protocol): only the proxy's own bash is matched by its command alone
earakely-scale Oct 24, 2026
2347de4
feat(task): an outside agent's prompt can be a conversation with a us…
earakely-scale Oct 24, 2026
ecb94a7
feat(task): a proxy installs into the container an outside agent woul…
earakely-scale Oct 24, 2026
4a674fe
fix(task): the episode sends a proxy call again when it doesn't reach…
earakely-scale Oct 24, 2026
8de2c27
fix(protocol): the proxy runs sh where there is no bash
earakely-scale Oct 25, 2026
20b694f
fix(task): steps aimed at an agent installed into a container reach t…
earakely-scale Oct 25, 2026
07627ab
fix(task): every write into an agent installed in a container reaches…
earakely-scale Oct 25, 2026
dc96da4
fix(protocol): a proxy command is done when its shell exits
earakely-scale Oct 25, 2026
1e958de
Merge remote-tracking branch 'origin/main' into feat/stand-in-agent
earakely-scale Oct 25, 2026
e90c83b
refactor(bundle): import Task and the task store from their modules
earakely-scale Oct 26, 2026
ccd2ded
refactor(task): plainer names for where an installed agent works
earakely-scale Oct 26, 2026
1d057c4
refactor(agents): one host workdir per agent, checked against its own…
earakely-scale Oct 26, 2026
1156ebb
refactor(protocol): the proxy's bash and tool actions in their own ha…
earakely-scale Oct 26, 2026
e64ed21
refactor(task): the episode reads proxy/v1 as its models
earakely-scale Oct 26, 2026
6ba355d
fix(protocol): a command cancelled while its late output is awaited s…
earakely-scale Oct 26, 2026
d276f71
fix(load_artifact): a load aimed at an agent installed on a VM's host…
earakely-scale Oct 26, 2026
10061c8
Merge remote-tracking branch 'origin/main' into feat/stand-in-agent
earakely-scale Oct 26, 2026
6ba15e6
fix(episode): a call that waits for a turn's tool listing waits as lo…
earakely-scale Oct 26, 2026
2f00f10
test(proxy): pull the container install test's images from mirror.gcr…
earakely-scale Oct 26, 2026
1194607
fix(protocol): a turn's env servers list their tools within one deadl…
earakely-scale Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ updates:
- "/src/agent_env/agents/agents/opencode"
- "/src/agent_env/agents/agents/openhands"
- "/src/agent_env/agents/agents/pi"
- "/src/agent_env/agents/agents/proxy"
schedule:
interval: "weekly"
open-pull-requests-limit: 5
Expand Down
4 changes: 3 additions & 1 deletion .github/scripts/infra_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
"agentenv-opencode": "src/agent_env/agents/agents/opencode/Dockerfile",
"agentenv-openhands": "src/agent_env/agents/agents/openhands/Dockerfile",
"agentenv-pi": "src/agent_env/agents/agents/pi/Dockerfile",
"agentenv-proxy": "src/agent_env/agents/agents/proxy/Dockerfile",
}
_TAG = re.compile(r"^src-[0-9a-f]{64}$")
_DIGEST = re.compile(r"^sha256:[0-9a-f]{64}$")
Expand All @@ -55,7 +56,7 @@

# Each image's check, beyond its build: the gateway imports its server, Postgres turns healthy, the sidecars and the
# browser answer their own commands, Claude Code's, Codex's, Gemini's, OpenCode's and pi's agents run their CLIs and
# import their servers, and OpenHands' imports its server, which loads the OpenHands SDK.
# import their servers, OpenHands' imports its server, which loads the OpenHands SDK, and the proxy imports its server.
_SMOKES = {
"agentenv-gateway": ["docker", "run", "--rm", "--entrypoint", "python", "{image}", "-c", "import gateway.entrypoint"],
"agentenv-db-web": ["docker", "run", "--rm", "--entrypoint", "/usr/bin/pgweb", "{image}", "--version"],
Expand All @@ -74,6 +75,7 @@
"agentenv-openhands": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", 'python -c "import a2a_server"'],
"agentenv-pi": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c",
'pi --version && python3 -c "import a2a_server"'],
"agentenv-proxy": ["docker", "run", "--rm", "--entrypoint", "sh", "{image}", "-c", 'python -c "import a2a_server"'],
}


Expand Down
12 changes: 9 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,9 +223,15 @@ orchestrators call it the same way. What callers rely on:
- A runner that must hear from a long step puts a zero-argument callable at
`context.metadata[HEARTBEAT_KEY]` (`task_step/context.py`); a step that blocks for long calls
`context.heartbeat()`, a no-op without one. The key is in `_REDACTED_KEYS`, so it is never persisted.
- An episode (`EpisodeSpec`, `task/episode/`) is one `Task.run()` of a copy of the task in which
the `deploy_agent` and `prompt_agent` steps of each of its `trainer_agents` are the plain-data
`episode_agent` and `episode_turn`, which wait for the caller instead of deploying or prompting. Its
- An episode (`EpisodeSpec`, `task/episode/`) is one `Task.run()` of a copy of the task, with
namespace routing on, in which each of its `trainer_agents` is played through a proxy agent
(the `agents` bundle's `proxy`, `@local/agentenv-framework/agents/proxy`, which the episode writes
from its folder as a bundle run would).
The agent's `deploy_agent` or `install_agent`, wrapped as `episode_agent`, deploys the proxy in the
agent's place when the task uses the agent's sandbox and serves it in this process when it doesn't;
the agent's `prompt_agent` steps message it as they would the agent, and the caller plays each
message as a `Turn` through its `urn:agentenv:proxy/v1`: every env tool call and `bash` command runs
in the proxy, which records the turn and checks it against the model calls the caller records. Its
record is tagged `context.metadata["episode"]` and names the original task, so it isn't resumable
with `start_step`. The episode tears down what its run deployed, and an abandoned one is recorded
`cancelled`.
Expand Down
25 changes: 17 additions & 8 deletions packages/agentenv-protocol/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -713,20 +713,27 @@ operations:
"skills"}]}`. A turn its sender cancels drops out of the list.
- `tools` (`{"token", "task_id"}`) lists what the turn can call, with input schemas: the
tools of each env server registered through mcp-config, and `bash`. Two servers offering
one name are refused with 409.
one name are refused with 409, and servers that haven't all listed their tools within
600 s get 502. `record` waits for the same listing.
- `act` (`{"token", "task_id", "action_id", "action"}`) runs one action: `{"kind": "tool",
"server", "name", "arguments"}` calls an env server through `ToolSession`, with the URL
and headers registered for it, and `{"kind": "bash", "command"}` runs `bash -c` in the
proxy's folder, as its user, in a process group of its own; either takes
proxy's folder, as its user, in a process group of its own. Where there is no `bash`, as
in a minimal image, it runs `sh -c`, and the `bash` tool's description says so. A command
is done when its shell exits: what it started in the background keeps running, and output
that arrives more than a second later is read and dropped. Either takes
`"timeout_seconds"` (3600 for `bash`, 600 for a tool by default). The driver picks
`action_id`, so a retried `act` returns the action it already started. It answers
`{"done", "result"}` at once when the action ends within the long-poll; `result`
(`{"token", "action_id"}`) long-polls one that doesn't. A `bash` result is
`{"exit_code", "stdout", "stderr", "timed_out"}`, a tool's `{"content", "is_error"}`.
`action_id`, unique for the proxy, so a retried `act` returns the action it already
started. It answers `{"done", "result"}` at once when the action ends within the long-poll;
`result` (`{"token", "action_id"}`) long-polls one that doesn't, and gets 404 once the
action's turn has closed. A `bash` result is `{"exit_code", "stdout", "stderr",
"timed_out"}`, with no `exit_code` when the command was stopped and 126 when it couldn't
start; a tool's is `{"content", "is_error"}`.
- `record` (`{"token", "task_id", "message", "model", "usage"}`) records one model call: an
OpenAI chat message, an Anthropic message or a string; anything else gets 400. Once a turn
records a call, each action must be one the last recorded call made, by name and
arguments, and all of them must run before the next `record` or the `reply`, else 409.
arguments (a `bash` call by its command alone, a whole float equal to its int), and all of
them must run before the next `record` or the `reply`, else 409.
Calls to tools the turn doesn't offer aren't checked.
- `reply` (`{"token", "task_id", "parts"}`, or `"error"` in place of `"parts"`) ends the
turn: parts complete the task, and an error fails it as `proxy.driver_error`.
Expand All @@ -735,7 +742,9 @@ A turn's actions belong to it: when it closes, by reply or withdrawal, those sti
are stopped, their whole process groups killed, and its results forgotten; the app's
lifespan stops what runs when it shuts down. The proxy writes the turn's trajectory as it
goes, readable live, as OTel GenAI spans in format `agentenv-proxy/v1`: `chat` for each
recorded call, `execute_tool` for each action, and `chain` for the turn. Skills it is given
recorded call, `execute_tool` for each action, and `chain` for the turn, each with an
OTel status: `ERROR` for an action that failed, a command that exited nonzero or timed out,
and a turn that ended in an error. Skills it is given
are written to `~/skills/<name>/`, once each, and a turn names each one's folder.
`ProxyAgent(shell=False)` is a proxy for an agent that works in no sandbox: it offers no
`bash` and keeps no skills. It holds the env servers' headers as any agent does, so run it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ class ProxyResultRequest(ExtensionRequest):

class ProxyBashResult(ExtensionResponse):
kind: Literal["bash"] = "bash"
exit_code: int | None # none when the command was stopped
exit_code: int | None = None # none when the command was stopped
stdout: str
stderr: str
timed_out: bool
Expand Down
Loading
Loading