Repository navigation
feat(protocol): ProxyAgent, an agent with no model whose turns a driver answers through proxy/v1 - #280
Merged
Conversation
…ays through driver/v1 urn:agentenv:driver/v1 hands an agent's turns to a driver outside its sandbox: claim sets the token, next long-polls for the open turns, reply ends one with its text or error and trajectory, and exec and output run a command where the agent works. StandInAgent serves it: each message is an open turn until the driver replies, a cancelled one drops out, skills go to ~/skills/<name>/, and commands run as bash -c in its folder, as its user, with a 3600 s timeout that kills their process group. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…t nothing uses StandInAgent is ProxyAgent and driver/v1 is proxy/v1, with routes under /ext/proxy and its models named Proxy*. Gone: the shell=False mode, whose refusals guarded calls no caller makes; mcp-config, whose registrations nothing read, with role taken through agent-config instead so a deploy naming one still hands it over; and the module's own serve entry point. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… commit left out The previous commit moved stand_in.py and its tests to their new names but not their new content, nor the extension's, the exports' or the README's. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…xy, which records the turn proxy/v1 gains tools, which lists what a turn can call (the env servers registered through mcp-config, whose tools the proxy now calls with ToolSession, and bash), and act and result, which run one action, a tool call or a bash command, by a driver-picked id, answer at once when it ends within the long-poll, and return a structured result. record takes each model call, as an OpenAI chat message, an Anthropic message or a string, and once a turn records one, every action must be one the last recorded call made and all must run before the next record or the reply. The proxy writes the turn's trajectory itself, live: a chat span per recorded call, an execute_tool span per action and a chain span for the turn, so reply carries only parts or an error. Content travels as parts, and a message with a part that isn't text fails before the driver sees it. A turn's actions stop when it closes, and the app's lifespan stops what still runs. Tokens are compared as bytes, a skill's folder is removed when writing it fails and a skill name is taken once, and shell=False offers no bash and keeps no skills. Co-Authored-By: Claude Opus 5.5 <[email protected]>
…hold one connection per server - A tool action's timeout_seconds bounds connecting to its env server and the call together; the proxy's sessions set no deadline of their own, which cut a longer request off at 600 s. Listing a turn's tools is bounded the same way. - A turn opens one connection per env server, under a lock, shared by its actions; none is opened or kept once the turn has closed. - record checks and sets the turn's pending calls with no wait in between, so of two model calls recorded at once the second is refused. Co-Authored-By: Claude Opus 5.5 <[email protected]>
| try: | ||
| called = await asyncio.wait_for(self._called(turn, action), timeout) | ||
| result = ProxyToolResult(content=called.content, is_error=called.isError) | ||
| except TimeoutError: |
There was a problem hiding this comment.
On supported Python 3.10 installations, asyncio.wait_for raises asyncio.TimeoutError, which is distinct from built-in TimeoutError. The new catches in _perform and _tools miss it. A timed-out action then fails the act or result request instead of returning a tool-error result, and a timed-out listing skips its intended 502 response.
Catch asyncio.TimeoutError at both boundaries.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/agentenv-protocol/src/agentenv_protocol/a2a_agent/proxy.py
Line: 342
Comment:
**Python 3.10 timeouts escape**
On supported Python 3.10 installations, `asyncio.wait_for` raises `asyncio.TimeoutError`, which is distinct from built-in `TimeoutError`. The new catches in `_perform` and `_tools` miss it. A timed-out action then fails the `act` or `result` request instead of returning a tool-error result, and a timed-out listing skips its intended 502 response.
Catch `asyncio.TimeoutError` at both boundaries.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
earakely-scale
added a commit
that referenced
this pull request
Oct 23, 2026
…e declare where they work on a VM host #280 shipped in protocol 0.1.370; 0.1.367 to 0.1.369 came out before it and have no proxy/v1. Gemini and OpenCode, new on main, install on a VM host as root, in /workspace and /app, so a proxy installed in their place works there. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This adds
urn:agentenv:proxy/v1andProxyAgent, an A2A agent with no model. Another program, the driver, plays it. The proxy is deployed where the played agent would run, and every action the driver's model takes runs in it: a call to an env's tools, or abashcommand. The proxy records the turn as the agent's trajectory.This PR ships the protocol half. #270 uses it: it adds the proxy to the
agentsbundle as an agent folder, likeclaude-codeandpi, and plays it from the Episode API. A bundled agent installs the protocol from PyPI, so #270's proxy needs this release (0.1.367).proxy/v1
Every operation is a
POSTunder/ext/proxy/whose body carries the token.claim{token}next{token, after}after, then returns{version, turns}. Each turn has its task id, context id,parts(text only for now), itssystem_promptand its skills.tools{token, task_id}bashwhen the proxy has a shell, then each registered env server's tools, withserver. Two servers offering one name get 409.act{token, task_id, action_id, action}toolorbashaction in the turn. It answers{done, result}if the action finishes within the long-poll, else{done: false}. A retry with the sameaction_idreturns the action already started.result{token, action_id}record{token, task_id, message, model?, usage?}reply{token, task_id, parts or error}proxy.driver_error. While a recorded call hasn't run, a reply with parts gets 409.A
bashresult is{exit_code, stdout, stderr, timed_out}; a tool result is{content, is_error}.ProxyAgent
tasks/cancelwithdraws it. A message with a part that isn't text fails asproxy.unsupported_partbefore the driver sees it.bash. It runsbash -cin the proxy's folder, as its user, in a process group of its own. On a timeout (3600 s by default), or when its turn closes, the whole group is killed.shell=Falseis a proxy for an agent that works in no sandbox: it offers nobashand keeps no skills.timeout_seconds(600 s by default) covers connecting to the server and the call together. Listing a turn's tools is bounded the same way.stopping_actions) stops every turn's actions when the app stops; a subclass declaring its own definition passes it on.agentenv-proxy/v1, as OTel GenAI spans:chatspan per recorded call, with its model and token usage when given;execute_toolspan per action;chainspan per turn, with the prompt, the system prompt and the model.It is served live through trajectory/v1.
~/skills/<name>/, and a turn names each skill's folder. A write that fails leaves no folder, and a name already added gets 409.system_prompt.Verification
test_a2a_proxy.py, 31 tests, cover the proxy app in process. Each passed 3 times in a row:bash's structured result, the fast path, an idempotent retry, a timeout with a background child, and a turn closing mid-action;record: the call check, two calls recorded at once, the message shapes and refused ones, span order and usage;shell=False, and the app stopping.🤖 Generated with Claude Code