@m64/nats-pi-channel
NATS Agent Protocol channel for PI Agent. Makes every PI session a discoverable, spec-compliant agent on NATS.
Package details
Install @m64/nats-pi-channel from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@m64/nats-pi-channel- Package
@m64/nats-pi-channel- Version
0.4.1- Published
- Apr 24, 2026
- Downloads
- 692/mo · 178/wk
- Author
- m64
- License
- MIT
- Types
- extension
- Size
- 41.7 KB
- Dependencies
- 2 dependencies · 1 peer
Pi manifest JSON
{
"extensions": [
"./extensions/nats-channel.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
@synadia/nats-pi-channel
Currently published on npm as
@m64/nats-pi-channel; moving to@synadia/nats-pi-channelonce Synadia publishing access lands. Install commands below use the current name.
NATS channel for PI Agent, implementing the NATS Agent Protocol v0.2.0.
Every PI session becomes a discoverable, addressable, streaming agent on NATS. Callers using any SDK that speaks the protocol - e.g. @synadia/agents - can enumerate running PI sessions, prompt them, and stream responses back.
Sibling implementations (same wire protocol): claude-code, openclaw.
How it works
On session start the extension:
- Connects to NATS using a configured context (or
demo.nats.ioby default). - Registers a NATS micro service named
agentswith spec metadata (agent,owner,session,protocol_version). - Adds a
promptendpoint atagents.pi.<owner>.<session>advertisingmax_payload: 1MBandattachments_ok: true. - Begins publishing heartbeats on
agents.pi.<owner>.<session>.heartbeatevery 30 s. - On each inbound prompt: decodes any attached files to
~/.pi/agent/attachments/<session>/<uuid>/<filename>, prepends their absolute paths to the prompt text, emits astatus: ackchunk, injects the augmented prompt into PI viapi.sendUserMessage(), streamstext_deltaevents back as typed{type:"response",data}chunks, and closes with the spec-mandated empty-body no-headers terminator. - Malformed envelopes, oversized payloads, invalid base64, and unsafe filenames are rejected at the wire with
Nats-Service-Error-Code: 400. Staging failures (disk full, permission denied) return500.
Multiple PI sessions on the same host register as distinct instances of the same service, each with a unique prompt endpoint subject - nats micro info agents aggregates across all of them.
Install
# From npm (once published)
pi install npm:@m64/nats-pi-channel
# From a local clone during development
pi install /absolute/path/to/nats-pi-channel
Then start PI normally:
pi
You should see Connected to NATS (<server>) as agents.pi.<you>.<session> and a footer status NATS: agents.pi.<you>.<session>.
Configure
Config lives at ~/.pi/agent/nats-channel.json:
{
"context": "my-nats-context",
"sessionName": "my-session"
}
| Field | Required | Default | Description |
|---|---|---|---|
context |
no | demo.nats.io |
NATS CLI context name in ~/.config/nats/context/ |
sessionName |
no | sanitized basename of CWD | Overrides the 4th subject token |
Environment variables take precedence over the file:
NATS_CONTEXT- select a NATS CLI contextNATS_SESSION_NAME- override the session name
In-PI commands
/nats-status- show current subject, service, instance id, protocol version, pending/queued counts/nats-configure- print current config/nats-configure <context>- switch NATS context/nats-configure session <name>- override session name/nats-configure session clear- revert to CWD basename
Changes take effect after restarting PI.
Tenant isolation
The spec reserves the subject structure for protocol use; there is no org segment. For multi-tenant isolation, use NATS accounts and subject permissions (spec §10.1). Within an account, collisions between two PI sessions on the same owner + session auto-suffix -2, -3, …
Subject hierarchy
agents.pi.<owner>.<session> # prompt endpoint (spec §2, §5)
agents.pi.<owner>.<session>.heartbeat # liveness beacon (spec §8)
piis bothmetadata.agentand its subject abbreviation (Appendix C).owner: sanitized$USER.session: sanitized basename of CWD, overridable.
Talking to a running PI session
Any caller speaking the protocol - a spec-compliant SDK or the nats CLI - can:
# Enumerate all compliant agents (includes Claude Code, OpenClaw, etc.)
nats req '$SRV.INFO.agents' '' --replies=0 --timeout=2s
# Send a plain-text prompt
nats req agents.pi.<owner>.<session> "What files are in the current directory?" --wait-for-empty --timeout 120s
# Or a JSON envelope
nats req agents.pi.<owner>.<session> '{"prompt":"What files are here?"}' --wait-for-empty --timeout 120s
With the TypeScript SDK:
import { connect } from "@nats-io/transport-node";
import { Agents } from "@synadia/agents";
const nc = await connect({ servers: "nats://localhost:4222" });
const agents = new Agents({ nc });
const [agent] = await agents.discover({ filter: { agent: "pi" } });
for await (const msg of await agent!.prompt("What files are here?")) {
if (msg.type === "response") process.stdout.write(msg.text);
}
await agents.close();
await nc.close();
Wire protocol (summary)
Full spec: https://github.com/synadia-ai/nats-agent-sdk-docs. Quick reference:
- Request: plain UTF-8 text OR JSON
{"prompt":"…","attachments":[{"filename":"…","content":"<base64>"},…]}. Attachmentcontentmust be RFC 4648 §4 base64 (standard alphabet, padded, no URL-safe variant, no whitespace). - Response: one or more typed chunks on the reply subject:
{"type":"response","data":"<text>"}- content{"type":"status","data":"ack"}- accepted / keep-alive
- Terminator: empty body and no headers (spec §6.5).
- Errors:
Nats-Service-Error-Codeheader with400/500, followed by the terminator.
Discovery
Any NATS Agent Protocol SDK will enumerate PI sessions automatically. Without an SDK:
# Micro service framework
nats micro list # shows all agents instances
nats micro info agents
# Heartbeats - track liveness without polling
nats sub 'agents.*.*.*.heartbeat'
Concurrency
Each PI session processes one NATS request at a time. Additional requests queue until the agent is idle. The user's local TUI input and inbound NATS prompts share the same agent session.
Attachments
When a request envelope contains attachments, each file is decoded and staged on disk at:
~/.pi/agent/attachments/<session>/<uuid>/<filename>
The absolute paths are then prepended to the prompt as:
[Attachments available at the following absolute paths]
- /home/you/.pi/agent/attachments/myproj/abcd-…/vacation.jpg
<original prompt text>
PI's model sees the list in the user message and can open the files with its file tools. The entire <session> directory is removed on session_shutdown; within a session, attachments from earlier turns remain on disk so follow-up turns can still reference them.
Caller-side constraints (rejected at the wire with 400 if violated):
contentmust be strict RFC 4648 §4 base64 - standard alphabet, padded, no URL-safe, no whitespace.filenamemust be a plain basename. Path separators (/,\),.., absolute paths, and NUL bytes are rejected rather than silently flattened.- Full encoded envelope must fit within
max_payload(1 MB).
Spec §5.5 reserves a future attachments endpoint at agents.pi.<owner>.<session>.attachments for chunked large-file upload; that lands in protocol 0.2 and will coexist with inline attachments.
Limitations
Deliberate deferrals:
- No mid-stream queries. PI doesn't currently initiate permission prompts or clarifications over this channel; the spec's
querychunk type (§7) is supported by callers but never emitted. - No live reconfigure.
/nats-configurewrites the config file; restart PI to apply. - TUI bleed. If the user types locally during a NATS-driven turn, that output flows to the NATS reply subject alongside the prompt's response.
Troubleshooting
NATS: disconnectedin footer. Check/nats-status, the context file at~/.config/nats/context/<context>.json, and NATS server reachability.NATS: reconnecting…. Connection dropped; restoring automatically.- My session got a
-2suffix. Another PI session on the sameowner + sessionwas already registered. Use/nats-configure session <name>to pick a different name. nats reqreturns nothing or hangs. Pass--wait-for-empty; the protocol signals end-of-stream with an empty-body message, not a single response.400 attachment[N] has invalid base64 content. The SDK / client emitted URL-safe base64 or unpadded output. Switch to RFC 4648 §4 (standard alphabet, padded) - Node'sBuffer.from(bytes).toString("base64")produces the correct form.400 attachment[N] has unsafe filename. Path separators,.., absolute paths, or NUL infilename. Send the basename only (e.g."report.pdf", not"./reports/report.pdf").
License
MIT