agent-proxy
A stdio wrapper around an interactive claude TUI that states turn
boundaries instead of leaving clients to infer them. With --agent codex it
wraps codex app-server instead, behind the same protocol. It replaces claudejson, an
earlier unpublished wrapper, inheriting its pty and transcript machinery and
replacing its turn accounting.
API.md is the wire contract — every command, every message, and what a client may assume about the order. Read it first if you are writing a client.
The design documents that argue why the protocol has this shape are not
published with the code. What is here is meant to stand on its own: API.md for
the contract, RELEASING.md for cutting a version, and the
module docstrings for the reasoning — turn_tracker.py on what moves a turn,
fallback.py on why a marker alone cannot be relied on to end one.
Status
v0.3.1 adds structured questions and client permission approvals, and is
the first PyPI release as thinkeryzu-agent-proxy. The command remains
agent-proxy and Python imports remain agentproxy. See the
release notes
for changes since v0.2.0. The earlier v0.3.0 release was withdrawn.
echo '{"type":"submit","text":"Reply with exactly: PONG"}' | agent-proxy
{"seq": 1, "type": "ready", "outstanding": {"user": 0, "unowned": 0, "background": 0, "monitor": 0, "agent": 0, "orphan": 0}, "session_id": "…"}
{"seq": 2, "type": "ack", "turn": {"id": "t-1", "kind": "user"}, "outstanding": {"user": 1, "unowned": 0, "background": 0, "monitor": 0, "agent": 0, "orphan": 0}}
{"seq": 3, "type": "turn_start", "turn": {"id": "t-1", "kind": "user"}, "outstanding": {"user": 1, "unowned": 0, "background": 0, "monitor": 0, "agent": 0, "orphan": 0}}
{"seq": 4, "type": "text", "turn": {"id": "t-1", "kind": "user"}, "outstanding": {"user": 1, "unowned": 0, "background": 0, "monitor": 0, "agent": 0, "orphan": 0}, "text": "PONG"}
{"seq": 5, "type": "turn_end", "turn": {"id": "t-1", "kind": "user"}, "outstanding": {"user": 0, "unowned": 0, "background": 0, "monitor": 0, "agent": 0, "orphan": 0}, "basis": "marker:turn_duration", "outcome": "success", "usage": {"duration_ms": 1670, "output_tokens": 5}}
Unrecognised arguments go through to the inner claude, so agent-proxy --model opus works as expected. Set AGENTPROXY_LOG=/path/to/file to record
the pty bytes, the stdin lines and the state transitions behind the stream.
The session flags are the exception — the proxy resolves those itself so it
can name the session before it types anything. ready is always the first
message and always carries a real session_id, including for --continue,
a bare --resume, and --fork-session. Keep that id and agent-proxy --resume <id> picks the conversation up in a later process; see
API.md for the full table.
agent-proxy --agent codex runs codex app-server as the inner agent
instead, signed in with ChatGPT; the client reads the same stream, and
ready says "agent": "codex". Its arguments are mapped onto codex's own
or refused, and a few counts read differently, since codex keeps no list of
background commands: see API.md.
Every requirement, constraint and success criterion that can be checked
without a client has a test naming it, in tests/requirements/.
Both clients have migrated — r2d2box, which agent-desktop-env mounts, and a second client that is not public — so claudejson is retired.
v0.3.1 adds structured agent questions for both backends.
Use agent-proxy --user-input (or add --agent codex) with a client that
supports the new input_request / answer exchange. Questions expire after
24 hours by default; configure seconds with AGENTPROXY_INPUT_TIMEOUT_S.
Claude input support requires Claude Code >= 2.1.316. See
API.md: User input for the full contract and Codex setup.
Existing invocations keep questions disabled.
Use agent-proxy --no-bypass-permissions (also with --agent codex) to use
native permission checks and ask through the client's choice-prompt flow.
The flag implies --user-input. Permission prompts show the operation and
Allow once / Deny choices, with the same configurable 24-hour deadline.
Dismissal and expiry deny. See permission approvals
for backend policies and supported approval types. Permission choices are available in v0.3.1.
Installing
The Claude backend requires Claude Code ≥ 2.1.221 for its explicit
--autocompact 1m launch default; caller overrides are described in
API.md.
The PyPI distribution is thinkeryzu-agent-proxy; the command is
agent-proxy. uv uses PyPI and needs no separate publication step.
uv tool install thinkeryzu-agent-proxy
agent-proxy --help
Run without a permanent tool installation:
uvx --from thinkeryzu-agent-proxy agent-proxy --help
For a project environment, use uv pip install thinkeryzu-agent-proxy or
pip install thinkeryzu-agent-proxy. Pin with
thinkeryzu-agent-proxy==0.3.1 when needed. Claude/Codex must be installed and
authenticated separately.
Older GitHub tags used distribution metadata named agent-proxy; update
requirements to thinkeryzu-agent-proxy for current releases. Python imports
and the command name are unchanged. GitHub also provides a wheel, source
archive and installable tag:
pip install 'git+https://github.com/ThinkerYzu/agent-proxy@v0.3.1'
RELEASING.md covers Trusted Publisher setup, validation, publication and withdrawal.
Commands
| What you want | Command |
|---|---|
| a prompt answered | {"type": "submit", "text": "…"} on stdin |
answer an agent question (with --user-input) |
{"type":"answer","request":"i-1","answers":{"q-1":{"choices":["o-2"]}}} on stdin |
| authoritative state, any time | {"type": "status"} on stdin |
a turn taken back — dropped if still queued, interrupted with Esc if running |
{"type": "cancel", "turn": "t-6"} on stdin; the turn ends outcome: "cancelled" once the inner is seen to stop |
to know which ack is yours |
add a "ref" of your own to a command — it comes back on the answer |
the tracker against a recorded session, no claude |
python3 -m agentproxy.replay tests/fixtures/synthetic-session.jsonl --assert-drained |
What it emits
Line-delimited JSON. Every message carries seq, its type, every
outstanding count, and — when it belongs to a turn — that turn's id and kind:
{"seq": 41, "type": "turn_start", "turn": {"id": "t-6", "kind": "unowned"},
"outstanding": {"user": 0, "unowned": 1, "background": 0, "monitor": 0, "agent": 0, "orphan": 0}, "caused_by": "bsynthimport"}
That one line is the project in miniature: a turn nobody prompted, named, and tied to the background command whose completion woke it.
Layout
| Path | What it is |
|---|---|
API.md |
the stdio protocol reference for clients |
RELEASING.md |
how a release is cut, and what a version number promises |
.github/workflows/ |
CI on every push, a GitHub Release on every v* tag |
src/agentproxy/turn_tracker.py |
the turn table, the task table, binding and end-of-turn; fed by transcript entries, or by stated facts for an agent that reports its own boundaries |
src/agentproxy/protocol.py |
the envelope and the stdout writer |
src/agentproxy/commands.py |
the stdin command parser, the same for every inner agent |
src/agentproxy/codex_cli.py |
the process boundary for --agent codex: startup, the app-server's pipes, the loop |
src/agentproxy/codex_proxy.py |
the codex wiring: send gate, cancel, the two fallbacks, refused server requests |
src/agentproxy/codex_session.py |
codex notifications as turns and tasks, no I/O |
src/agentproxy/app_server.py |
JSON-RPC with the app-server: ids, responses, server requests |
src/agentproxy/inner_codex.py |
codex arguments mapped or refused, the environment, which thread to open |
src/agentproxy/proxy.py |
the wiring — every decision, no process IO |
src/agentproxy/cli.py |
the process boundary — pty, transcript, stdout, signals |
src/agentproxy/liveness.py |
idle or active, settled, and the two dialogs that block input |
src/agentproxy/fallback.py |
ending a turn that no marker closed |
src/agentproxy/submit_queue.py |
accepted prompts, typed one at a time |
src/agentproxy/inner_claude.py |
argv, env and session id for the inner spawn |
src/agentproxy/orphans.py |
processes the inner lost track of: the subreaper flag, adoption, reaping, the orphan count |
src/agentproxy/replay.py |
replay a recorded transcript, with no live claude |
src/agentproxy/{pty_driver,io_loop,screen_model,input_encoder,transcript_tailer,ids,log}.py |
inherited from claudejson |
tools/make_synthetic_fixture.py |
build the test session out of real entry shapes |
tools/capture_fixture.py |
cut a fixture out of a real session transcript |
tests/harness.py |
a proxied session with the pty taken out, shared by the suites below |
tests/fake_app_server.py, tests/codex_harness.py |
a stand-in codex app-server built from recorded message shapes, and a codex session driven against it |
tests/requirements/ |
one test group per SPEC requirement, constraint and success criterion |
tests/requirements/reference_client.py |
a client with no inference layer, used to check the protocol keeps its word |
Building and testing
python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
pytest -q
396 tests, none of which needs a claude or codex binary (one of them
proves the subreaper mechanism on the real kernel, in a subprocess; the codex
ones run against tests/fake_app_server.py). Ten more do — seven against
claude, three against codex — and are opt-in twice over: a marker alone
deselects nothing on a bare pytest, and these spend real quota:
AGENTPROXY_RUN_LIVE=1 pytest -q -m live # both
AGENTPROXY_RUN_LIVE=1 pytest -q -m live tests/live/test_live_session.py # claude
AGENTPROXY_RUN_LIVE=1 pytest -q -m live tests/live/test_live_codex.py # codex
The codex tests are skipped when there is no codex on the path, and need it
signed in with ChatGPT. Worth running when either binary changes version:
they are the only tests that read what the installed binary actually writes
— claude's transcript entries, codex app-server's messages, whose protocol
codex marks experimental — rather than the shapes the fixtures model.
Fixtures
tests/fixtures/synthetic-session.jsonl is a 93-entry session assembled by
tools/make_synthetic_fixture.py. Its conversation is invented; every entry
shape in it — envelope fields, content blocks, marker subtypes, the two
background-launch wordings, the queue-operation pairs, the cluster
/compact writes after its boundary — was copied from a real recorded
session. The tracker reads only the shapes, so nothing is lost, and the
repository carries no transcript of real work.
Its sidecar, synthetic-session.prompts.json, names which entries were
prompts a client typed — provenance a recording cannot carry on its own.
Regenerate with:
python3 tools/make_synthetic_fixture.py
The output is byte-stable, so a regeneration shows up as a reviewable diff and a test fails if the committed fixture drifts from the builder.
To check the tracker against a real session on your own machine — worth doing
when claude changes shape, and not something to commit:
python3 tools/capture_fixture.py ~/.claude/projects/<dir>/<session>.jsonl /tmp/<name>.jsonl
python3 -m agentproxy.replay /tmp/<name>.jsonl --quiet --assert-drained
Metadata
Release files for thinkeryzu-agent-proxy 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| thinkeryzu_agent_proxy-0.3.1.tar.gz | 118.8 kB | Without provenance | Details for thinkeryzu_agent_proxy-0.3.1.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| thinkeryzu_agent_proxy-0.3.1-py3-none-any.whl | Python 3 | none | any | Details for thinkeryzu_agent_proxy-0.3.1-py3-none-any.whl |
Total release size: 224.8 kB
Release files / thinkeryzu_agent_proxy-0.3.1.tar.gz
| Download URL | thinkeryzu_agent_proxy-0.3.1.tar.gz |
|---|---|
| Size | 118.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1885bdfe1b1551b7252fe51b8da9cef78c4ddb41cbd107487fe23884fd4d0a17
|
|
BLAKE2b-256 checksum How to use checksums |
d89c7a2f6a9c5e664b20abb0cabc661fbe21883ffbb341f3c2279699ec9716f1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / thinkeryzu_agent_proxy-0.3.1-py3-none-any.whl
| Download URL | thinkeryzu_agent_proxy-0.3.1-py3-none-any.whl |
|---|---|
| Size | 106.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7f38355cd3c2652568ab79a957a1653840f4777eacc2df0cd577549621bb4462
|
|
BLAKE2b-256 checksum How to use checksums |
7eb968b538a1f6f0cef897743012b67f50bfac8ce19a223674eb51e830a97e98
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.15 {"installer":{"name":"uv","version":"0.12.15","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"26.04","id":"resolute","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for thinkeryzu-agent-proxy 0.3.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.