Skip to content

Agent instance rpc - #9

Merged
jannikluhn merged 5 commits into
mainfrom
agent-instance-rpc
Sep 23, 2026
Merged

jannikluhn merged 5 commits into
mainfrom
agent-instance-rpc

Conversation

@jannikluhn

Copy link
Copy Markdown
Contributor

Closes #8 and obsoletes #6

jannikluhn and others added 4 commits September 22, 2026 13:10
…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 blockchainluffy left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
@jannikluhn
jannikluhn merged commit aeb00d5 into main Sep 23, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prompt via RPC

2 participants