Metadata-Version: 2.4
Name: thinkeryzu-agent-proxy
Version: 0.3.1
Summary: Turn-aware JSON stream around an interactive claude TUI or codex app-server.
Author: Kui-Feng Lee
License: MIT
Project-URL: Source, https://github.com/ThinkerYzu/agent-proxy
Project-URL: Documentation, https://github.com/ThinkerYzu/agent-proxy/blob/master/API.md
Project-URL: Issues, https://github.com/ThinkerYzu/agent-proxy/issues
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pyte>=0.8.2
Requires-Dist: ptyprocess>=0.7
Provides-Extra: dev
Requires-Dist: pytest>=8.0; extra == "dev"
Dynamic: license-file

# 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](https://github.com/ThinkerYzu/agent-proxy/blob/master/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](https://github.com/ThinkerYzu/agent-proxy/blob/master/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](https://github.com/ThinkerYzu/agent-proxy/blob/master/RELEASE-NOTES.md)
for changes since v0.2.0. The earlier v0.3.0 release was withdrawn.

```bash
echo '{"type":"submit","text":"Reply with exactly: PONG"}' | agent-proxy
```

```json
{"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](https://github.com/ThinkerYzu/agent-proxy/blob/master/API.md#sessions) 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](https://github.com/ThinkerYzu/agent-proxy/blob/master/API.md#the-codex-agent).

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](https://github.com/ThinkerYzu/r2d2box),
which [agent-desktop-env](https://github.com/ThinkerYzu/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](https://github.com/ThinkerYzu/agent-proxy/blob/master/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](https://github.com/ThinkerYzu/agent-proxy/blob/master/API.md#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](https://github.com/ThinkerYzu/agent-proxy/blob/master/API.md#invocation).

The PyPI distribution is **`thinkeryzu-agent-proxy`**; the command is
**`agent-proxy`**. uv uses PyPI and needs no separate publication step.

```bash
uv tool install thinkeryzu-agent-proxy
agent-proxy --help
```

Run without a permanent tool installation:

```bash
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:

```bash
pip install 'git+https://github.com/ThinkerYzu/agent-proxy@v0.3.1'
```

[RELEASING.md](https://github.com/ThinkerYzu/agent-proxy/blob/master/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:

```json
{"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

```bash
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:

```bash
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:

```bash
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:

```bash
python3 tools/capture_fixture.py ~/.claude/projects/<dir>/<session>.jsonl /tmp/<name>.jsonl
python3 -m agentproxy.replay /tmp/<name>.jsonl --quiet --assert-drained
```
