@agent-ops/omp-nats-channel
v0.8.0
Published
NATS channel bridge for Oh My Pi (OMP) — makes every OMP session a discoverable, spec-compliant Synadia Agent Protocol agent on NATS.
Downloads
1,613
Maintainers
Readme
@synadia-ai/nats-omp-channel
NATS channel extension for Oh My Pi (OMP). Every running OMP session becomes discoverable, addressable, and streamable over NATS — anyone with a Synadia Agent Protocol for NATS client (e.g. @synadia-ai/agents) can find your session, prompt it, and stream the reply back.
OMP registers under agent kind op to distinguish it from upstream pi. If you also run a vanilla pi, you'll see both side-by-side on the bus.
Protocol versions
This adapter advertises both v0.3 and v0.4 of the Synadia Agent Protocol on the same NATS micro service. v0.3 is the default discovery shape (metadata.protocol_version = "0.3"); v0.4 lives behind the sesh.* metadata namespace so legacy callers never see it:
metadata["sesh.protocol_version"] = "0.4"metadata["sesh.v04_capabilities"] = "messages,artifacts,cards"
Callers (the sesh shim) publish prompts on agents.prompt.<machine>.<project>.<session>, receive streamed Message / statusUpdate events on agents.task.stream.<scope-kind>.<scope-id>.<task-id>, and fetch the OMP AgentCard from agents.card.<machine>.<session>.<name>. The clean v0.4 machine-rooted scheme is the only scheme; harness identity (oh-my-pi) lives in service metadata (the agent key), never in the subject.
Install
OMP doesn't have a plugin-install CLI today; wire the package in via config.yml:
# ~/.omp/agent/config.yml
extensions:
- /absolute/path/to/omp-nats-channelInstall the package's own deps once:
cd /absolute/path/to/omp-nats-channel
bun installThen start OMP normally:
ompYou should see Connected to NATS (<server>) as agents.prompt.<machine>.<project>.<session> on session start, and a footer status line ● nats://<machine>.<project>.<session> rendered above the editor.
Configure
Out of the box, no configuration is needed: OMP connects to demo.nats.io and derives the machine, project, and session tokens from its environment. Your session is reachable at:
agents.prompt.<machine>.<project>.<session>For real deployments, point OMP at your own NATS via a context file. Two common setups:
Production with a NATS CLI context (already configured via nats context add):
// ~/.omp/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 OMP to pick up changes — or use the in-OMP commands below.
Configuration reference
Config file lives at ~/.omp/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, then the sesh hub at ~/.sesh/hub.nats.url if present, then 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, use NATS accounts (server-side configuration).
Environment variables
Env vars override the config file:
| Variable | Sets | Notes |
|----------|------|-------|
| NATS_CONTEXT | context | Highest precedence. |
| NATS_URL | raw URL (no auth context) | Used only when NATS_CONTEXT and config.context are both unset. |
| SESH_SESSION | sessionName | Canonical sesh contract — set by sesh up --exec and orch-spawn SESH_* exports. Wins over NATS_SESSION_NAME. |
| NATS_SESSION_NAME | sessionName | Legacy operator override; kept for back-compat. |
| SESH_ROLE | metadata.role on the NATS Micro service registration | Free-form role token (^[a-z0-9_-]+$, 1–63 chars). Identifies the function this agent plays in the swarm — e.g. implementer, verifier, spy. Surfaced as metadata.role and as role in sesh's session manifest. Default worker. |
| SESH_CLASS | metadata.class on the NATS Micro service registration | One of active or observer. Coordination-subject routing keys on this: active agents subscribe to workers.*, observer agents subscribe to spies.*. Default active. |
Resolution order
- Built-in default —
demo.nats.io, no auth - Sesh hub URL —
~/.sesh/hub.nats.urlif present $NATS_URL— raw URL fallback (only consulted when no context is set)config.context— wizard-set / hand-edited NATS CLI context$NATS_CONTEXT— wins over everything
For sessionName (first non-empty wins): $SESH_SESSION (canonical sesh contract — also resolved via .sesh/sessions/<label>.json state-walk when unset) → $NATS_SESSION_NAME (legacy operator override) → config.sessionName → sanitised CWD-basename.
In-OMP commands
Available inside a running OMP 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 OMP 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 an OMP session looks like:
{
"type": "io.nats.micro.v1.info_response",
"name": "agents",
"id": "JC8O0IGAWI5APOHLAOA96N",
"version": "0.4.0",
"description": "OMP agent (my-session) in /home/me",
"metadata": {
"agent": "op",
"owner": "me",
"session": "my-session",
"protocol_version": "0.3",
"cwd": "/home/me"
},
"endpoints": [
{
"name": "prompt",
"subject": "agents.prompt.m4.my-project.my-session",
"queue_group": "agents",
"metadata": { "max_payload": "8MB", "attachments_ok": "true" }
},
{
"name": "status",
"subject": "agents.status.m4.my-project.my-session",
"queue_group": "agents"
}
]
}The cwd metadata field tells you which working directory each session was started from — useful when you've got several OMP windows open.
Talk to your session
From the CLI:
# Plain text prompt
nats req agents.prompt.<machine>.<project>.<session> "What files are here?" \
--wait-for-empty --reply-timeout 30s --timeout 120s
# JSON envelope (caller SDKs use this form)
nats req agents.prompt.<machine>.<project>.<session> '{"prompt":"What files are here?"}' \
--wait-for-empty --reply-timeout 30s --timeout 120s--wait-for-empty is required: replies stream as multiple chunks and end with an empty terminator message.
--reply-timeout matters too. Its default is 300 ms — the maximum gap allowed between consecutive replies. The agent publishes an immediate {type:"status",data:"ack"} chunk on request receipt, but the LLM's first response chunk typically lands 1–2 s later, so the default fires before the first response and the CLI exits after just the ack. Setting --reply-timeout 30s gives the LLM enough warm-up time. SDK callers (requestMany with strategy:"sentinel") don't hit this — they wait the full maxWait regardless of inter-arrival gaps.
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: "op" } });
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:
~/.omp/agent/attachments/<session>/<uuid>/<filename>The absolute paths are prepended to the prompt text so OMP'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 OMP 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 OMP 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. OMP doesn't initiate permission prompts or clarifications over this channel; the protocol's
querychunk type is supported by callers but never emitted by the OMP side. - No live reconfigure.
/nats-configurewrites the config file; OMP 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.
- No custom statusline segment. OMP's
StatusLineSegmentIdenum is fixed; the NATS indicator renders in the hook status row above the editor, not inline with the model/path/cost segments.
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 OMP session was already registered on the sameowner + session. Use/nats-configure session <name>to pick a different one. nats reqreturns only the initial ack and exits — pass--reply-timeout 30s(default is 300 ms, shorter than the gap between the ack chunk and the LLM's first response). See the "Talk to your session" section above for the full command.--wait-for-emptyalone isn't enough.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
~/.omp/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 ~/.omp/agent/attachments/<session>/between runs if you don't need to re-reference earlier attachments.
See also
- Sibling channel packages in this repo:
pi-nats-channel— upstream PI Agentclaude-nats-channel— Claude Codegemini-nats-channel— Gemini CLIgrok-nats-channel— Grok
- The wire-level protocol:
synadia-ai/synadia-agent-sdk-docs.
License
Apache-2.0
