Skip to content

Commit 2294486

Browse files
authored
Update README.md
1 parent cdd3409 commit 2294486

1 file changed

Lines changed: 59 additions & 53 deletions

File tree

‎README.md‎

Lines changed: 59 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,13 @@
77
</div>
88

99
The trusted side of an agent platform: API server, controller, auth,
10-
billing, secrets. The untrusted side — the agent runtime itself — is
10+
billing, secrets.
11+
The untrusted side — the agent runtime itself — is
1112
`python-agent-harness`, executed as a **resident subprocess** per
1213
sandbox: `python-agent-harness serve`, a bidirectional JSON-lines
13-
protocol over stdin/stdout. The harness is never imported; the repos
14-
stay decoupled (the harness only needs to be on PATH of whatever runs
15-
the agent).
14+
protocol over stdin/stdout.
15+
The harness is never imported; the repos stay decoupled (the harness
16+
only needs to be on PATH of whatever runs the agent).
1617

1718
```
1819
┌──────────────────────────────────────────────────────────────────────┐
@@ -57,12 +58,12 @@ the agent).
5758
└──────────────────────────────────────────────────────────────────────┘
5859
```
5960

60-
Protocol split: **JSONL** is the backend↔harness link (pipes);
61-
**SSE** is the browser↔backend link. The controller is a protocol
62-
translator — each parsed JSONL line is fanned out to in-memory
63-
subscribers and re-emitted as an SSE `data:` frame, so the browser
64-
sees the same events the harness TUI renders (tool progress, todos,
65-
errors, mid-run questions).
61+
Protocol split: **JSONL** is the backend↔harness link (pipes); **SSE**
62+
is the browser↔backend link.
63+
The controller is a protocol translator — each parsed JSONL line is
64+
fanned out to in-memory subscribers and re-emitted as an SSE `data:`
65+
frame, so the browser sees the same events the harness TUI renders (tool
66+
progress, todos, errors, mid-run questions).
6667

6768
## Layout
6869

@@ -114,13 +115,14 @@ uvicorn app.main:app --reload # http://127.0.0.1:8000 (UI at /)
114115
```
115116

116117
`python-agent-harness` must be importable-on-PATH as a command; point
117-
`PAW_HARNESS__CMD` at the absolute binary if it is not. Auth: create
118-
a user via `/auth/register`, then log in; the UI does this for you.
118+
`PAW_HARNESS__CMD` at the absolute binary if it is not.
119+
Auth: create a user via `/auth/register`, then log in; the UI does this
120+
for you.
119121

120122
## The serve protocol (harness side)
121123

122-
One sandbox = one long-lived `serve` process. The web side writes
123-
ops, the harness answers with events:
124+
One sandbox = one long-lived `serve` process.
125+
The web side writes ops, the harness answers with events:
124126

125127
```
126128
host → harness: {"op": "submit", "prompt": ..., "run_id": ...}
@@ -134,18 +136,18 @@ harness → host: {"type": "ready"} then per-run
134136
Because the process is resident: conversation history persists across
135137
turns (multi-turn memory), no per-turn interpreter spawn, `answer`
136138
delivers the user's reply to a pending mid-run question (the agent's
137-
Question tool / PlanExit confirm), and cancel is a protocol message —
138-
no signal semantics.
139+
Question tool / PlanExit confirm), and cancel is a protocol message — no
140+
signal semantics.
139141

140142
`notify` lines carry the progress the UI renders, keyed by `kind`:
141-
`tool_start` (the round's tool names), `tool_calls` (the same round
142-
with each call's arguments, so a row reads `Bash(command='ls -la')`
143-
rather than a bare `Bash`), `tool_running`, `tool`, `todos`, `compact`,
144-
`retry`, `error`, and `ask` for a mid-run question. `tool_calls` is
145-
additive — a harness that does not send it degrades to the names from
146-
`tool_start`. `log` lines are shown too, except session bookkeeping
147-
(the generated session title), which says nothing about what the agent
148-
is doing.
143+
`tool_start` (the round's tool names), `tool_calls` (the same round with
144+
each call's arguments, so a row reads `Bash(command='ls -la')` rather
145+
than a bare `Bash`), `tool_running`, `tool`, `todos`, `compact`,
146+
`retry`, `error`, and `ask` for a mid-run question.
147+
`tool_calls` is additive — a harness that does not send it degrades to
148+
the names from `tool_start`.
149+
`log` lines are shown too, except session bookkeeping (the generated
150+
session title), which says nothing about what the agent is doing.
149151

150152
## Configuration (env, prefix `PAW_`)
151153

@@ -166,37 +168,41 @@ is doing.
166168

167169
- **Routes are thin, controllers own the rules**: a route resolves
168170
dependencies, calls a controller, and maps the result to a response
169-
schema — nothing else. No route touches the DB, the filesystem or
170-
crypto. A controller refuses work by raising from
171-
`controllers/errors.py` (`NotFound`, `Conflict`, `InvalidRequest`,
172-
`PayloadTooLarge`, `Unauthorized`, `Forbidden`); one handler in
173-
`main.py` renders that as FastAPI's own `{"detail": ...}` shape with
174-
the status the error type carries. So business logic never imports
175-
`HTTPException`, and a controller stays callable from a test, a CLI
176-
or a worker thread.
177-
- **Stateless mechanism vs stateful policy** is the `infra`/
178-
`controllers` line, not "core vs supporting". `infra` holds
179-
primitives with no entities and no session — password hashing, JWT,
180-
Fernet `encrypt`/`decrypt` — and imports nothing but `infra`.
181-
Anything that takes a `Session`, reads or writes an ORM entity, or
182-
can refuse a request lives in `controllers`, even for supporting
183-
areas like accounts and secrets. Moving those down would make the
184-
bottom layer import `models` and `controllers.errors`, inverting the
185-
dependency direction.
171+
schema — nothing else.
172+
No route touches the DB, the filesystem or crypto.
173+
A controller refuses work by raising from `controllers/errors.py`
174+
(`NotFound`, `Conflict`, `InvalidRequest`, `PayloadTooLarge`,
175+
`Unauthorized`, `Forbidden`); one handler in `main.py` renders that as
176+
FastAPI's own `{"detail": ...}` shape with the status the error type
177+
carries.
178+
So business logic never imports `HTTPException`, and a controller
179+
stays callable from a test, a CLI or a worker thread.
180+
- **Stateless mechanism vs stateful policy** is the
181+
`infra`/`controllers` line, not "core vs supporting".
182+
`infra` holds primitives with no entities and no session — password
183+
hashing, JWT, Fernet `encrypt`/`decrypt` — and imports nothing but
184+
`infra`.
185+
Anything that takes a `Session`, reads or writes an ORM entity, or can
186+
refuse a request lives in `controllers`, even for supporting areas
187+
like accounts and secrets.
188+
Moving those down would make the bottom layer import `models` and
189+
`controllers.errors`, inverting the dependency direction.
186190
- **Decoupling**: the harness is a black-box binary driven by its
187191
documented JSONL protocols (the same `start`/`delta`/`notify`/
188192
`log`/`result` line shapes on the resident `serve` pipe and the
189193
one-shot `headless --json` pipe; `seq` for ordering, `run_id` for
190-
correlation, `usage` for billing). No imports, no shared state; the
191-
web side can be versioned and deployed independently.
192-
- **Runs are protocol turns, not process lifecycles**: one
193-
conversation turn = one `op:submit` = one `Run` row; the resident
194-
process survives the run and serves the next turn. Events stream to
195-
subscribers over SSE exactly as the harness emitted them (plus run
196-
lifecycle events), and the `result` line lands in the DB.
194+
correlation, `usage` for billing).
195+
No imports, no shared state; the web side can be versioned and
196+
deployed independently.
197+
- **Runs are protocol turns, not process lifecycles**: one conversation
198+
turn = one `op:submit` = one `Run` row; the resident process survives
199+
the run and serves the next turn.
200+
Events stream to subscribers over SSE exactly as the harness emitted
201+
them (plus run lifecycle events), and the `result` line lands in the
202+
DB.
197203
- **Secrets** are Fernet-encrypted at rest and never returned by the
198-
API; they are meant to be injected into the sandbox environment by
199-
the sandbox manager (not exposed to agents via the API).
200-
- **Billing** is a token ledger: the controller snapshots
201-
`result.usage` (input/output/rounds) from the harness into
202-
`usage_events`, attributed to the user and conversation.
204+
API; they are meant to be injected into the sandbox environment by the
205+
sandbox manager (not exposed to agents via the API).
206+
- **Billing** is a token ledger: the controller snapshots `result.usage`
207+
(input/output/rounds) from the harness into `usage_events`, attributed
208+
to the user and conversation.

0 commit comments

Comments
 (0)