What it is
The nats package implements the
NATS Agent Protocol v0.3
for Phero agents. It provides two top-level types:
-
nats.Server: registers anyagent.Agentas a NATS micro service. It handles discovery (via$SRV.PING/INFO), streaming prompt responses, periodic heartbeats, and on-demand status replies. The agent is wire-compatible with the TypeScript and Python SDKs in the synadia-agents repository. -
nats.Client: discovers compliant agents on the NATS bus and sends them prompts.Client.AsTool()wraps a remote agent as anllm.Toolthat any local Phero agent can call.
Unlike the A2A transport (HTTP-based), the NATS transport uses pub/sub messaging, which makes it well-suited for service meshes and environments where NATS is already running.
Example: registering an agent as a NATS server
See the full code in examples/nats-agent/server.
package main
import (
"context"
"os"
"os/signal"
"syscall"
natsgo "github.com/nats-io/nats.go"
"github.com/henomis/phero/agent"
"github.com/henomis/phero/llm/openai"
natsagent "github.com/henomis/phero/nats"
)
func main() {
nc, _ := natsgo.Connect(natsgo.DefaultURL)
defer nc.Drain()
llmClient := openai.New(os.Getenv("OPENAI_API_KEY"))
a, _ := agent.New(llmClient, "my-agent", "A helpful assistant.")
srv, _ := natsagent.New(nc, a, "alice", "demo")
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
srv.Start(ctx) // blocks until ctx is cancelled
}
New(nc, agent, owner, name) registers the agent as a NATS micro service.
The owner and name values become the 4th and 5th tokens of the prompt subject
(agents.prompt.phero.<owner>.<name>).
Start(ctx) blocks until the context is cancelled, then drains cleanly.
Example: discovering and prompting a remote agent
See the full code in examples/nats-agent/client.
nc, _ := natsgo.Connect(natsgo.DefaultURL)
defer nc.Drain()
c := natsagent.NewClient(nc)
// Discover all compliant agents on the bus
agents, err := c.Discover(ctx)
if err != nil {
panic(err)
}
// Stream a prompt to the first discovered agent
stream, err := c.Prompt(ctx, agents[0], "What is NATS?")
if err != nil {
panic(err)
}
defer stream.Close()
text, _ := stream.Text(ctx)
fmt.Println(text)
Discover uses a stall strategy: it collects responses to a $SRV.INFO.agents
fan-out request for up to 750 ms of silence (configurable), capped by a 2-second absolute deadline.
Discovery options such as FilterByOwner and FilterByName narrow the results
client-side.
Wrapping a remote agent as a tool
Any discovered agent can be turned into an llm.Tool that a local Phero agent can call.
This is the building block for NATS-based multi-agent pipelines.
c := natsagent.NewClient(nc)
agents, _ := c.Discover(ctx, natsagent.FilterByName("researcher"))
tool, err := agents[0].AsTool("researcher", "Research a topic and return key facts.")
if err != nil {
panic(err)
}
orchestrator.AddTool(tool)
AgentHandle.AsTool(toolName, toolDesc) is shorthand for Client.AsTool(info, toolName, toolDesc).
The tool sends a JSON-encoded prompt to the remote agent's prompt subject and collects the streamed
response chunks before returning the full text to the LLM.
Multi-agent NATS pipeline
For a production-style example, see
examples/nats-agent/multi-agent.
It shows three specialised agents (researcher, writer, editor) each running as a NATS micro service,
coordinated by a local orchestrator that discovers them via $SRV.INFO.agents and calls
each one as an llm.Tool.
docker run --rm -p 4222:4222 nats
# Terminal 1
OPENAI_API_KEY=<key> go run ./examples/nats-agent/multi-agent/researcher
# Terminal 2
OPENAI_API_KEY=<key> go run ./examples/nats-agent/multi-agent/writer
# Terminal 3
OPENAI_API_KEY=<key> go run ./examples/nats-agent/multi-agent/editor
# Terminal 4 — orchestrator
OPENAI_API_KEY=<key> go run ./examples/nats-agent/multi-agent/orchestrator -topic "quantum computing"
API reference
Server
nats.New(nc, agent, owner, name, opts...) (*Server, error)— create a serversrv.Start(ctx context.Context)— start serving; blocks until ctx is cancelled
Server options
WithHeartbeatInterval(d time.Duration)— how often to publish a heartbeat (default: 10 s)WithMaxPayloadBytes(n int64)— advertised max payload size in the AgentCardWithAttachmentsOk(ok bool)— advertise attachment support in discovery metadata
Client
nats.NewClient(nc, opts...) *Client— create a clientclient.Discover(ctx, opts...) ([]*AgentHandle, error)— discover agents on the busclient.Prompt(ctx, info, text) (*Stream, error)— send a prompt and get a streamclient.AsTool(info, toolName, toolDesc) (*llm.Tool, error)— wrap a remote agent as a tool
Client options
WithDiscoveryTimeout(d time.Duration)— absolute deadline for discovery (default: 2 s)WithStallTimeout(d time.Duration)— silence window before discovery ends (default: 750 ms)WithInactivityTimeout(d time.Duration)— stream read timeout per chunk (default: 30 s)
Discovery options
FilterByAgent(agent string)— keep only agents with matchingmetadata.agentFilterByOwner(owner string)— keep only agents with matchingmetadata.ownerFilterByName(name string)— keep only agents with matching instance name
AgentHandle
handle.Prompt(ctx, text) (*Stream, error)— shorthand forClient.Prompthandle.AsTool(toolName, toolDesc) (*llm.Tool, error)— shorthand forClient.AsToolhandle.AgentInfo— embeddedAgentInfowith name, owner, subjects, etc.
Stream
stream.Text(ctx) (string, error)— collect all chunks into a single stringstream.Close() error— unsubscribe from the reply subject
Errors
ErrNoAgentsFound—Discoverfound no matching agentsErrEmptyPrompt— empty prompt passed toPromptErrPayloadTooLarge— prompt exceeds the agent's advertised max payloadErrAgentRequired— nil agent passed toNewErrConnectionRequired— nil NATS connection passed toNeworNewClientErrOwnerRequired/ErrNameRequired— blank owner or name passed toNew
Run the simple example
Start a NATS server first (plain core NATS — no JetStream needed):
docker run --rm -p 4222:4222 nats
# Terminal 1 — start the agent server
OPENAI_API_KEY=<key> go run ./examples/nats-agent/server -owner=alice -name=demo
# Terminal 2 — interactive client
go run ./examples/nats-agent/client
Interoperability
Phero agents are fully wire-compatible with the TypeScript and Python SDKs from synadia-agents. Any client speaking the NATS Agent Protocol can discover and prompt a Phero agent.
# Discover the running Phero agent with the Python SDK
uv run python examples/01-discover.py --url nats://127.0.0.1:4222
# Stream a single prompt
uv run python examples/02-prompt-text.py --url nats://127.0.0.1:4222 "What is 2+2?"