@synadia-ai/nats-pi-channel
NATS Agent Protocol channel for PI Agent. Makes every PI session a discoverable, spec-compliant agent on NATS.
Package details
Install @synadia-ai/nats-pi-channel from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@synadia-ai/nats-pi-channel- Package
@synadia-ai/nats-pi-channel- Version
0.5.2- Published
- May 4, 2026
- Downloads
- 128/mo · 128/wk
- Author
- derek
- License
- Apache-2.0
- Types
- extension
- Size
- 53.5 KB
- Dependencies
- 4 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-ai/nats-pi-channel
NATS channel extension for PI Agent. Every running PI session becomes discoverable, addressable, and streamable over NATS — anyone with a NATS Agent Protocol client (e.g. @synadia-ai/agents or synadia-ai-agents) can find your session, prompt it, and stream the reply back.
Install
# From npm
pi install npm:@synadia-ai/nats-pi-channel
# From a local clone (development)
pi install /absolute/path/to/nats-pi-channel
When iterating on the SDKs locally, both @synadia-ai/agents and @synadia-ai/agent-service need a current dist/ for PI's loader to resolve the file: links — see README-DEV.md at the repo root for the build sequence.
Then start PI normally:
pi
You should see Connected to NATS (<server>) as agents.prompt.pi.<you>.<session> and a footer status line NATS: agents.prompt.pi.<you>.<session>.
Configure
Out of the box, no configuration is needed: PI connects to demo.nats.io and uses your $USER + the basename of the working directory as the subject tokens. Your session is reachable at:
agents.prompt.pi.<owner>.<session>
For real deployments, point PI at your own NATS via a context file. Two common setups:
Production with a NATS CLI context (already configured via nats context add):
// ~/.pi/agent/nats-channel.json
{
"context": "prod"
}
Pin a stable session name (so callers can address the same logical session even if you cd around):
{
"context": "prod",
"sessionName": "my-session"
}
Restart PI to pick up changes — or use the in-PI commands below.
Configuration reference
Config file lives at ~/.pi/agent/nats-channel.json:
| Field | Required | Default | Description |
|---|---|---|---|
context |
no | — | Name of a NATS CLI context (file under ~/.config/nats/context/<name>.json). When unset, falls back to $NATS_URL or, if that's also unset, the built-in demo.nats.io. |
sessionName |
no | sanitized basename of CWD | The 5th subject token. Override to give your session a stable, addressable name. |
The owner token (4th) is always derived from $USER — there's no override for it. For multi-tenant isolation, see Multi-tenancy below.
Environment variables
Env vars override the config file:
| Variable | Sets | Notes |
|---|---|---|
NATS_CONTEXT |
context |
Highest precedence — see below. |
NATS_URL |
raw URL (no auth context) | Used only when NATS_CONTEXT and config.context are both unset. |
NATS_SESSION_NAME |
sessionName |
Resolution order
- Built-in default —
demo.nats.io, no auth config.context— wizard-set / hand-edited NATS CLI context$NATS_URL— raw URL fallback (only consulted when no context is set)$NATS_CONTEXT— wins over everything
For sessionName: $NATS_SESSION_NAME overrides config.sessionName, which overrides the CWD-basename default.
In-PI commands
Available inside a running PI session:
| Command | What it does |
|---|---|
/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 |
/nats-configure writes the config file; restart PI to apply. (Live reconnect on context switch is a deferral — see Limitations.)
Verify
# Find your session (and any other agents on the same NATS)
nats req '$SRV.INFO.agents' '' --replies=0 --timeout=2s
# Watch heartbeats — your session beats every ~5 s
nats sub 'agents.hb.*.*.*'
A successful $SRV.INFO.agents response for a PI session looks like:
{
"type": "io.nats.micro.v1.info_response",
"name": "agents",
"id": "JC8O0IGAWI5APOHLAOA96N",
"version": "0.4.0",
"description": "PI agent (my-session) in /home/me",
"metadata": {
"agent": "pi",
"owner": "me",
"session": "my-session",
"protocol_version": "0.3",
"cwd": "/home/me"
},
"endpoints": [
{
"name": "prompt",
"subject": "agents.prompt.pi.me.my-session",
"queue_group": "agents",
"metadata": { "max_payload": "8MB", "attachments_ok": "true" }
},
{
"name": "status",
"subject": "agents.status.pi.me.my-session",
"queue_group": "agents"
}
]
}
If you see your agents.prompt.pi.<owner>.<session> subject in the response, you're discoverable. Multiple PI sessions show up as multiple responses to the same query — the cwd metadata field tells you which working directory each one was started from, useful when you've got several PI windows open and need to pick the right session to prompt.
Talk to your session
From the CLI:
# Plain text prompt
nats req agents.prompt.pi.<owner>.<session> "What files are here?" --wait-for-empty --timeout 120s
# JSON envelope (caller SDKs use this form)
nats req agents.prompt.pi.<owner>.<session> '{"prompt":"What files are here?"}' --wait-for-empty --timeout 120s
--wait-for-empty is required: replies stream as multiple chunks and end with an empty terminator message.
From TypeScript using @synadia-ai/agents:
import { connect } from "@nats-io/transport-node";
import { Agents } from "@synadia-ai/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();
Attachments
When a request envelope carries attachments, each file is decoded and staged at:
~/.pi/agent/attachments/<session>/<uuid>/<filename>
The absolute paths are prepended to the prompt text so PI's model can open them with its file tools. Files staged earlier in a session stay on disk so follow-up turns can reference them; the whole <session>/ directory is removed on session shutdown.
Encode files with base64 -w0 <file> (Linux/macOS) or Buffer.from(bytes).toString("base64") in Node before embedding in the JSON envelope. Caller SDKs do this for you.
Caller-side limits (rejected with 400 if violated):
contentmust be standard-alphabet padded base64 — no URL-safe variant, no whitespace.filenamemust be a plain basename. Path separators,.., absolute paths, and NUL bytes are rejected, not silently flattened.- The fully-encoded request must fit within the server-negotiated
max_payload(1 MB on a defaultnats-server, more if the operator raised--max_payload).
Concurrency
Each PI session processes one NATS request at a time. Additional requests queue until the session is idle. The local TUI input and inbound NATS prompts share the same agent — typing locally during a NATS-driven turn means that local output flows to the NATS reply alongside the remote prompt's response.
Multiple PI sessions on the same host register as distinct service instances; nats micro info agents aggregates across all of them. If two sessions try to register on the same owner + session, the later one auto-suffixes -2, -3, … — pick a stable name with /nats-configure session <name> if you want addressability.
Multi-tenancy
The agent subject layout has no per-tenant slot. For real isolation between tenants or environments, use NATS accounts and subject permissions — that's a server-side configuration, not an extension one. Within a single account, sessions with distinct owner values (i.e. different $USERs) coexist cleanly.
Limitations
Deliberate deferrals:
- No mid-stream queries. PI doesn't initiate permission prompts or clarifications over this channel; the protocol's
querychunk type is supported by callers but never emitted by the PI side. - No live reconfigure.
/nats-configurewrites the config file; PI must be restarted for the new context or session name to apply. - TUI bleed. Local typing during a NATS-driven turn flows to the NATS reply subject as part of the response.
Troubleshooting
NATS: disconnectedin footer — run/nats-status, then check the context file at~/.config/nats/context/<context>.jsonand that the NATS server is reachable.NATS: reconnecting…— the connection dropped; the client restores it automatically.- My session got a
-2suffix — another PI session was already registered on the sameowner + session. Use/nats-configure session <name>to pick a different one. nats reqhangs or returns nothing — pass--wait-for-empty. The protocol ends streams with an empty-body message, not a single response.400 attachment[N] has invalid base64 content— the caller emitted URL-safe base64 or unpadded output.Buffer.from(bytes).toString("base64")(Node) produces the right form.400 attachment[N] has unsafe filename— send the basename only ("report.pdf"), not a path ("./reports/report.pdf").- Stale attachments piling up under
~/.pi/agent/attachments/— clean session shutdown removes the whole<session>/tree, but a force-quit or crash leaves the per-request UUID directories on disk. Safe torm -rf ~/.pi/agent/attachments/<session>/between runs if you don't need to re-reference earlier attachments.
See also
- Sibling channel plugins:
openclaw(OpenClaw),claude-code(Claude Code). - The wire-level protocol behind it all:
synadia-ai/nats-agent-sdk-docs.
License
Apache-2.0