@agent-ops/synadia-agent-shim
v1.1.0
Published
Synadia Agent Protocol v0.3 shim — wraps agent CLIs (claude-code, codex, pi, gemini) and exposes them on a NATS bus.
Maintainers
Readme
synadia-agent-shim
A Go shim that wraps an agent CLI (claude-code, codex, pi, gemini, or any custom adapter) and exposes it on a NATS bus using the Synadia Agent Protocol v0.3.
Operators publish a prompt on agents.prompt.<token>.<owner>.<pane>;
the shim translates it into the CLI's native invocation, streams chunks
back, and emits heartbeats + status per spec. Control-plane verbs
(interrupt, redirect) are routed via orch.signal.>.
Extracted from danmestas/orch;
see docs/proposals/0001-extract-synadia-agent-shim.md
for the rationale.
Install
As a binary (npm)
npm install -g @agent-ops/synadia-agent-shimThe postinstall script fetches the matching release binary for your
OS/arch and places it at vendor/synadia-agent-shim. Two wrapper
scripts are exposed on $PATH:
synadia-agent-shim— canonicalorch-agent-shim— backwards-compat alias for one orch major release
As a Go library
go get github.com/danmestas/synadia-agent-shim/shim
go get github.com/danmestas/synadia-agent-shim/adapter/echoUsage (CLI)
synadia-agent-shim --agent claude-code --locator tmux:%37
synadia-agent-shim --agent claude-code --locator cmux:surface:30
synadia-agent-shim --agent claude-code --locator zmx:engineer-a
synadia-agent-shim --agent claude-code --pane %37 # deprecated alias for --locator tmux:%37Resolution order (most explicit wins):
| Setting | Source |
| --- | --- |
| NATS URL | --nats → $NATS_URL → ~/.sesh/hub.nats.url → nats://127.0.0.1:4222 |
| Owner | --owner → $ORCH_OWNER → $USER → /etc/passwd lookup |
| Session | --session → $SESH_SESSION → omitted from metadata |
| Instance ID | --instance-id → omitted (no slug-keyed subjects) |
| CWD | --cwd → tmux display-message -p '#{pane_current_path}' |
| Locator | --locator → --pane (deprecated; infers tmux:) → autodetect via $CMUX_SURFACE_ID / $ZMX_SESSION / $TMUX_PANE |
The shim exits when the bound pane dies (SIGCHLD from the parent
shell). orch-spawn backstops this by wait-ing on a sentinel pid.
The pane-watchdog (tmux display-message poll) currently only runs
under the tmux engine; cmux and zmx manage surface lifetime themselves.
Engine support matrix
The shim dispatches the inbound-prompt send-verb based on the
persistence engine it's running under. The engine is detected from
env vars at startup, or set explicitly via --locator.
| Engine | Detection (env) | Locator form | Send verb | Interrupt verb |
| --- | --- | --- | --- | --- |
| tmux | $TMUX_PANE (preferred) or $TMUX | tmux:%37 | tmux send-keys -l -t %37 <text> + tmux send-keys -t %37 Enter | tmux send-keys -t %37 C-c |
| cmux | $CMUX_SURFACE_ID | cmux:surface:30 or cmux:<UUID> | cmux send --surface <ref> -- <text>\n | cmux send-key --surface <ref> ctrl+c |
| zmx | $ZMX_SESSION | zmx:engineer-a | zmx send <session> <text>\r | zmx send <session> $'\x03' |
Detection precedence: cmux → zmx → tmux (most-specific first; cmux and
zmx pane environments sometimes also expose $TMUX, so engine-specific
markers win).
Heartbeats and $SRV.INFO.agents metadata publish both the new
engine and locator fields alongside the back-compat pane_id:
{
"metadata": {
"agent": "claude-code",
"owner": "tester",
"pane_id": "surface:30",
"engine": "cmux",
"locator": "cmux:surface:30"
}
}pane_id will be retired once downstream registry consumers
(orch-registry) adopt locator. The dual surface mirrors the
--instance-id dual-publish window.
--pane deprecation
--pane VALUE continues to work and is interpreted as
--locator tmux:VALUE, with a one-line stderr deprecation notice on
startup. It will be removed in the next shim release (see
CHANGELOG.md). Callers should migrate to --locator.
--instance-id
--instance-id <slug> attaches a human-readable worker identity to the
shim. Subject-safe charset [a-zA-Z0-9._-], length 1-128 — invalid
slugs are rejected at startup so a typo fails loud, not at first publish.
When set, the shim:
- Adds
instance_id: "<slug>"to$SRV.INFO.agentsmetadata alongside the existingpane_id. Discovery tools can filter by either;pane_idstays so pane-watchdog andtmux send-keysconsumers keep working. - Registers a SECOND prompt + status endpoint on the slug-keyed
subjects:
agents.prompt.<token>.<owner>.<slug>agents.status.<token>.<owner>.<slug>
- Publishes heartbeats on the slug-keyed subject too:
agents.hb.<token>.<owner>.<slug>
Dual-publish is gated by env var ORCH_SLUG_DUAL_PUBLISH:
| Value | Behavior (with --instance-id set) |
| --- | --- |
| unset / 1 | legacy pct<N> track + slug track both live (default — safe during rollout) |
| 0 | slug track only; legacy pct<N> subjects have no subscriber |
When --instance-id is not set, the shim runs as before — only the
legacy pct<N>-keyed track is registered, regardless of the env var.
The dual-publish window is intended to last two releases; the
legacy pct<N> track will be retired in a follow-up issue. Track the
deprecation in CHANGELOG.md.
Usage (Go SDK)
package main
import (
"context"
"log"
"github.com/danmestas/synadia-agent-shim/adapter/echo"
"github.com/danmestas/synadia-agent-shim/shim"
)
func main() {
cfg := shim.Config{
Agent: "echo",
Pane: "%1",
Owner: "you",
NATSURL: shim.ReadNATSURL(""),
Adapter: echo.New(),
// SubjectPrefix defaults to "agents".
// SignalPrefix defaults to "orch.signal".
}
if err := shim.Run(context.Background(), cfg); err != nil {
log.Fatal(err)
}
}Non-orch consumers can retarget the namespace:
cfg.SubjectPrefix = "dagnats" // dagnats.prompt.*, dagnats.status.*, ...
cfg.SignalPrefix = "dagnats.signal"Writing a custom adapter
See docs/adapter-sdk.md. Minimal contract:
type Adapter interface {
Start(ctx context.Context) error
OnPrompt(ctx context.Context, prompt string) error
Events() <-chan Chunk
Close() error
}Adapters that need imperative interrupt (TUI harnesses that don't
honour ctx.Done()) implement the optional Aborter interface — the
shim type-asserts and calls Abort on orch.signal.interrupt arrival.
Built-in adapters
adapter/claudecode— Anthropic claude-code CLIadapter/codex— OpenAI codexadapter/pi— Inflection piadapter/gemini— Google gemini-cliadapter/echo— reference adapter (no external deps)
Versioning
- shim v1.0.0 = current behavior at extraction time from orch
- Each Synadia spec version bump → shim major version bump
- Adapter API additions → minor version bump
- Bug fixes → patch
The Adapter and Config shapes are frozen at v1.
Releasing
Releases are tag-driven. Pushing an annotated tag vX.Y.Z triggers
.github/workflows/release.yml:
goreleaserbuilds platform archives + creates the GitHub Release.publish-npmsyncspackage.jsonversion from the tag, then runsnpm publish --access public.
To cut a release:
git tag vX.Y.Z
git push --tagsOne-time operator setup: set the NPM_TOKEN secret in the repo's
GitHub settings (Settings → Secrets → Actions) with an npm automation
token that has publish access to @agent-ops/synadia-agent-shim.
PRs run npm publish --dry-run in CI to catch packaging breakage
before a tag is pushed.
Wire surface
- Service name:
<SubjectPrefix>(defaultagents), per Synadia §3.1 - Prompt subject:
<SubjectPrefix>.prompt.<token>.<owner>.<session-or-pane> - Status subject:
<SubjectPrefix>.status.<token>.<owner>.<session-or-pane> - Heartbeat:
<SubjectPrefix>.hb.<token>.<owner>.<session-or-pane>every 30s - Signal:
<SignalPrefix>.<verb>.<token>.<owner>.<pane>(orch#133)
Envelope headers: W3C traceparent + Sesh-Task-Id / Sesh-Attempt
when set. See docs/architecture.md for the
full layout.
License
Apache-2.0. See LICENSE.
