Agent instance rpc - #9
Conversation
…ay runs no container An Operator runs the agent now: `pi --mode rpc` behind a listener, in a container of their own. The Runtime opens one connection per Run and closes it at the end, so the Gateway holds no container runtime socket, names no host path, and carries no part of the agent's environment. One Run is four steps strictly in sequence, correlated by `id` and never pipelined: `switch_session` to the Session's derived path, `get_state` to prove it went where it was asked, `prompt`, then the stream read to `agent_settled`. The second step is there because `switch_session` is create-or-resume and that behaviour is undocumented: it is the only thing that can tell "created it" apart from "did something else and said it went fine". `src/agent-container/` is gone entirely, with its subpath, its typedoc entry point and its check-package blocks. The isolation container-per-Run bought did not disappear; it moved into the Operator's compose file. LF framing lives in `src/pi/framing.ts` alone and nothing reaches for `node:readline`, which is not protocol-compliant for it. The client is `node:net`, so `dependencies` is unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ocker socket Each of the four applications now comes up as two services that matter. `gateway` serves the Parties; `agent` is the Agent Instance, running `pi --mode rpc --no-approve` behind a `socat` listener in an image the Operator builds. The Gateway is told a host, a port and the sessions directory as the instance sees it, and nothing else about the agent: not the model credential, not the image, not a path the Docker daemon would resolve. What leaves the Gateway is the `/var/run/docker.sock` bind, `RUNTIME_DIR_HOST`, `AGENT_IMAGE`, `AGENT_NETWORK`, the agent-directory mounts, `docker-cli` from its Dockerfile, the build-only `agent-image` service and the whole `mounts:` block from `main.ts`. `AGENT_HOST` and `AGENT_PORT` became `AGENT_SERVER_HOST` and `AGENT_SERVER_PORT`: two addresses now cross on the agent network, one inbound and one outbound, and the short names could be read as either. There is no healthcheck on `agent`, because `fork` starts a `pi` per connection and a probe on an interval would boot and discard one for ever. The last acceptance criterion is not executed: the examples resolve the package from the registry at `^0.1.0` and the Runtime change is unpublished, so a message answered end to end waits on that. Recorded in the ticket. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The glossary retires Agent Container, Agent Container Runtime, Mount Table and Runtime Directory, all four naming `src/agent-container/`, and defines **Agent Instance** in their place. The entry keeps its last clause rather than tidying it away: with a listener that forks, "instance" names a socket most of the time and a process only while a Run is happening, and nothing in the framework can tell the difference or depends on the answer. Four entries are amended. **Runtime** states the Run-based reading of its own name outright, which is what makes keeping the name honest rather than merely cheap: the execution-environment reading was defensible while the framework started a container per Run, and is now false. **Agent Implementation** loses the paragraph that existed only to account for Runtime Directory, so "runtime" means two things again. **Workspace** records that the framework has no name for where it is. **Agent server** records the symmetry: two unauthenticated privileged addresses on the agent network pointing opposite ways, which is the same trust it always rested on and not a new one. The guide loses the sentence this whole change existed to delete, and its container-files steps now walk a reader through the two-service `compose.yml` the examples ship. The architecture page replaces its Agent Container section, and the diagram puts the Agent Instance outside what the Gateway owns. `CLAUDE.md` swaps the rule that went with the directory for the two that now need saying: the undocumented `switch_session` behaviour with the `get_state` that guards it, and the Session name grammar as a transcription of `pi`'s that nothing checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three things the two review axes found. `site/guide.md` wrote "the agent service", which is on the `_Avoid_` line of the Agent Instance entry the same change added, and `CLAUDE.md` says those lines are banned rather than discouraged. `scripts/reference/pages.ts` and `site/.vitepress/config.ts` still counted fifteen TypeDoc pages. Dropping the `/agent-container` entry point left fourteen, and `site/typedoc.jsonc` was corrected while these two were not. Both are files named in the `docsRoot` tripwire, which is where a reader goes looking. The RPC channel has a second correlation guard beyond the id, refusing an answer labelled as another command, and nothing exercised it. An instance that numbers its answers right and labels them wrong is still one whose answers cannot be read, and `switch_session` and `get_state` differ in exactly the field the next step branches on. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
blockchainluffy
left a comment
There was a problem hiding this comment.
Overall: this is a really good change and can be merged.
One thing I'd fix before merge: TCP keepalive
openRpcChannel sets setNoDelay(true) but never setKeepAlive, and Node defaults it to off.
The tests cover the agent hanging up cleanly and the connection erroring. What isn't covered is the peer disappearing silently: a network partition, a host losing power, or a firewall/NAT expiring an idle connection (usually around 5 minutes, which is well within a normal agent's thinking time).
That matters more here than it normally would, because:
- There are no timeouts anywhere by design, so this is an infinite wait, not a slow one
- The Signal Worker is serial, so one stuck connection blocks every user until someone restarts the Gateway
- It's a genuinely new failure mode. With container-per-Run the agent was a local child process, so its death was always visible as an exit. A dead TCP peer is not.
- This PR explicitly supports running the Agent Instance remotely, which is exactly where a NAT or firewall sits in the path
The fix is one line next to the existing setNoDelay:
socket.setKeepAlive(true, 30_000);That turns a permanent hang into a normal failed Run, and the existing dropped() handling already produces a good message for it. Nothing else changes.
The RPC socket set setNoDelay and nothing else, and Node leaves TCP keepalive off. A peer that stops existing without closing its end therefore reaches the Gateway as silence, which is indistinguishable from an Agent Instance that is thinking. There are no timeouts anywhere by design and the Signal Worker is serial, so that silence is an unbounded wait blocking every Party's queue. Container-per-Run could not have this failure: the agent was a local child process and its death arrived as an exit. Running the Agent Instance remotely, which this branch supports, is also where a NAT or a firewall sits in the path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Closes #8 and obsoletes #6