AgentMesh¶
The central class. Manages NATS connection, agent subscriptions, and lifecycle.
Construction¶
from openagentmesh import AgentMesh
# Connect to an existing NATS server
mesh = AgentMesh("nats://localhost:4222")
# Connect using default localhost URL
mesh = AgentMesh()
AgentMesh(url, *, creds=None, tls_cert=None, tls_key=None, tls_ca=None)¶
Connect to a running NATS server. Defaults to nats://localhost:4222 when no URL is provided.
| Parameter | Type | Default | Description |
|---|---|---|---|
url |
str |
"nats://localhost:4222" |
NATS connection URL |
creds |
str \| None |
None |
Path to a NATS .creds file. When omitted, resolves from OAM_CREDS, then the creds field of .oam-url; otherwise connects open. See Securing the Mesh. |
tls_cert |
str \| None |
None |
Client certificate for mTLS |
tls_key |
str \| None |
None |
Client key for mTLS |
tls_ca |
str \| None |
None |
CA bundle used to verify the server |
Connecting without valid credentials to a server that requires them raises ConnectionDenied (code connection_denied).
AgentMesh.local()¶
Async context manager that starts an embedded NATS subprocess with JetStream and pre-created KV buckets. For tests and quick demos only. The NATS process stops when the context exits.
async with AgentMesh.local() as mesh:
# embedded NATS starts, KV buckets created
result = await mesh.call("echo", {"message": "hello"})
# NATS stops when context exits
mesh.instance_id¶
A read-only attribute holding a stable per-process identifier (UUID4 hex). Each AgentMesh() instance generates its own at construction; it does not change for the lifetime of the instance.
The SDK auto-stamps X-Mesh-Instance-Id: {mesh.instance_id} on every outbound NATS message (call, send, stream, agent replies, publisher emissions). Receivers can read the header to attribute messages to a specific replica when multiple instances of the same agent name are deployed.
User-supplied headers (where the API accepts them) take priority: the SDK only sets a default when the header is not already present.
See ADR-0059 for design rationale.
Lifecycle¶
Two lifecycle models, one for each participation pattern.
mesh.run()¶
Blocking lifecycle for providers and hybrids. Connects to NATS, subscribes all registered agents, and blocks until interrupted. Similar to uvicorn.run().
This is the standard entry point for any process that registers agents with @mesh.agent.
async with mesh:¶
Scoped lifecycle for consumers. Connects on entry, disconnects on exit. No agent registration; used by scripts, notebooks, and orchestrators that only discover and call agents.
Embedding in an existing async application (e.g. FastAPI lifespan):
Registration¶
@mesh.agent(spec)¶
Decorator to register an async function as a mesh agent. Takes a single AgentSpec instance.
from openagentmesh import AgentMesh, AgentSpec
mesh = AgentMesh()
spec = AgentSpec(
name="nlp.summarizer",
description="Summarizes text to a target length.",
tags=["text", "summarization"],
)
@mesh.agent(spec)
async def summarize(req: SummarizeInput) -> SummarizeOutput:
...
Capabilities are inferred from the handler shape at decoration time. AgentSpec carries only human-authored metadata; invocable and streaming are never declared manually.
@mesh.agent(spec, *, sources=[...])¶
Bind an agent to one or more declarative trigger surfaces (ADR-0052). Sources are runtime wiring; they do not appear in the catalog. The handler's first-parameter type hint determines what the source dispatches:
| Annotation | Receives |
|---|---|
bytes |
Raw payload bytes |
Model (Pydantic) |
Validated Model instance |
KVEntry[Model] |
Full KV entry (key, value, revision, operation) with value validated to Model. Use KVEntry[bytes] to skip validation. |
MeshMessage[Model] |
Full NATS envelope (subject, headers, payload) with payload validated to Model. |
When the handler takes KVEntry or MeshMessage, the agent is not invocable via mesh.call (the runtime cannot synthesize an envelope from a wire payload). Use plain Pydantic input or no input for invocable shapes.
from openagentmesh import AgentMesh, AgentSpec, KVEntry
mesh = AgentMesh()
@mesh.agent(
AgentSpec(name="watcher", description="reacts to detection records"),
sources=[mesh.kv_source("wildfire.detection.*")],
)
async def react(entry: KVEntry[DetectionRecord]) -> None:
if entry.operation == "PUT" and entry.value.state == "pending":
...
mesh.subject_source(subject, *, queue_group=None)¶
NATS subject source. Wildcards (*, >) are supported. queue_group enables at-most-one-of-N delivery across replicas. Returns a SubjectSource (exported from openagentmesh for type annotations).
mesh.kv_source(pattern, *, queue_group=None, on_init="replay")¶
KV-watch source on the mesh-context bucket. on_init="replay" (default) fires the handler for every existing entry under the pattern at agent startup, then continues with live updates. on_init="skip" waits for the initial snapshot to drain and then triggers only on subsequent changes. queue_group is reserved for JetStream-backed consumers (raises NotImplementedError in v1). Returns a KVSource (exported from openagentmesh for type annotations).
| Handler shape | invocable |
streaming |
Consumer API |
|---|---|---|---|
async def f(req) -> Out: return ... |
True |
False |
mesh.call() |
async def f(req) -> Chunk: yield ... |
True |
True |
mesh.stream() |
async def f() -> Out: return ... |
True |
False |
mesh.call() |
async def f() -> Event: yield ... |
False |
True |
mesh.subscribe() |
async def f(): ... |
False |
False |
(background task) |
@mesh.agent(spec, *, active_when=...)¶
Gate the agent's subscription on a condition (ADR-0055). The agent stays in the catalog either way; while the condition is false it is unsubscribed and callers get not_available. Composes with sources — gated sources deliver nothing while the agent is offline. See Lifecycle Gates.
@mesh.agent(
AgentSpec(name="coordinator", description="active-incident work"),
active_when=mesh.kv_condition("incident.mode", lambda v: v == b'"active"'),
)
async def coordinator(brief: Brief) -> Assignment:
...
mesh.kv_condition(key, predicate, *, initial=False, drain_timeout=30.0)¶
Gate on a mesh-context KV key. predicate receives the key's raw bytes value (None when absent or deleted). The current value is read and applied when the mesh starts; initial is the fallback state if that read fails. On gate close, in-flight handlers get drain_timeout seconds to finish.
mesh.subject_condition(subject, predicate, *, initial=False, drain_timeout=30.0)¶
Gate on messages arriving on a plain NATS subject. predicate receives each message's payload bytes; the agent's state follows the most recent verdict. initial is the state before the first message.
Invocation¶
Four interaction modes. See Invocation for patterns and semantics.
await mesh.call(name, payload=None, timeout=30.0)¶
Synchronous request/reply. Blocks until the agent responds or times out.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Agent name |
payload |
Any |
None |
Input payload (dict, Pydantic model, or any JSON-serializable value) |
timeout |
float |
30.0 |
Timeout in seconds |
Returns: dict with the deserialized response payload.
Raises: NotFound when nobody serves the agent and it is not in the
catalog (immediate, via NATS no-responders); NotAvailable when it is in the
catalog but a lifecycle gate has it offline;
AgentDied when the agent leaves the mesh while your request is in flight
(sub-second, via death notices); MeshTimeout
when the deadline expires with the agent still connected.
async for chunk in mesh.stream(name, payload=None, timeout=60.0)¶
Streaming request. Yields response chunks as dicts. Raises AgentDied from
the generator if the agent leaves the mesh mid-stream.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Agent name |
payload |
Any |
None |
Input payload (dict, Pydantic model, or any JSON-serializable value) |
timeout |
float |
60.0 |
Total stream timeout in seconds |
Yields: dict chunks.
await mesh.send(name, payload, *, on_reply, on_error, reply_to, timeout)¶
Async callback invocation. Three modes: fire-and-forget, managed callback, or manual reply subject.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Agent name |
payload |
Any |
None |
Input payload (dict, Pydantic model, or any JSON-serializable value) |
on_reply |
Callable[[dict], Awaitable[None]] \| None |
None |
Callback for each reply message |
on_error |
Callable[[MeshError], Awaitable[None]] \| None |
None |
Callback for timeout or error |
reply_to |
str \| None |
None |
Manual reply subject (mutually exclusive with on_reply) |
timeout |
float |
60.0 |
Inactivity timeout (only applies with on_reply) |
# Managed callback
await mesh.send("summarizer", payload, on_reply=handle, on_error=on_err, timeout=30.0)
# Fire-and-forget
await mesh.send("summarizer", payload)
# Manual reply subject
await mesh.send("summarizer", payload, reply_to="mesh.results.abc")
await mesh.publish(subject, payload, *, headers=None)¶
Publish a payload to an arbitrary NATS subject without addressing a specific agent. Use for broadcasting events to a flat domain subject (sensors, tickers, scenario commands, status feeds) rather than to an agent's auto-mapped invocation subject.
| Parameter | Type | Default | Description |
|---|---|---|---|
subject |
str |
required | NATS subject (no wildcards) |
payload |
BaseModel \| bytes \| str |
required | Payload to publish |
headers |
dict[str, str] \| None |
None |
Optional user headers (override SDK defaults) |
The SDK auto-stamps three headers: X-Mesh-Request-Id (uuid hex), X-Mesh-Instance-Id (this mesh's id, ADR-0059), and X-Mesh-Content-Type (application/json for BaseModel, application/octet-stream for bytes, text/plain for str). User-supplied headers take priority.
Wildcards (*, >) raise ValueError. Subscribe-side wildcards are still supported via mesh.subscribe(subject=...).
class Reading(BaseModel):
sensor_id: str
value: float
await mesh.publish("sensor.temperature", Reading(sensor_id="s1", value=42.0))
await mesh.publish("logs.audit", "user-x logged in")
await mesh.publish("binary.frames", b"\x00\x01\x02...")
See ADR-0058.
Subscription¶
async for msg in mesh.subscribe(*, agent, channel, subject, timeout)¶
Subscribe to events on a subject, agent, or channel. Returns an async generator yielding dicts.
| Parameter | Type | Default | Description |
|---|---|---|---|
agent |
str \| None |
None |
Agent's dotted name (resolves to its event subject) |
channel |
str \| None |
None |
Channel prefix (subscribes to mesh.agent.{channel}.>) |
subject |
str \| None |
None |
Raw NATS subject |
timeout |
float \| None |
None |
Inactivity timeout in seconds |
At least one of agent, channel, or subject must be provided. agent and subject are mutually exclusive.
# Subscribe to an agent's event stream
async for event in mesh.subscribe(agent="price-feed"):
print(event["symbol"], event["price"])
# Subscribe to all events in a channel
async for event in mesh.subscribe(channel="finance"):
print(event)
# Subscribe to a raw subject
async for msg in mesh.subscribe(subject="mesh.results.abc123", timeout=30.0):
print(msg)
break
Discovery¶
await mesh.catalog(channel=None, tags=None, streaming=None, invocable=None)¶
Lightweight listing of registered agents. Returns typed CatalogEntry objects.
| Parameter | Type | Default | Description |
|---|---|---|---|
channel |
str \| None |
None |
Filter by name prefix (an entry matches when its name equals channel or starts with channel + ".") |
tags |
list[str] \| None |
None |
Filter by tags (all must match) |
streaming |
bool \| None |
None |
Filter by streaming capability |
invocable |
bool \| None |
None |
Filter by invocable capability |
Returns: list[CatalogEntry]
catalog = await mesh.catalog(channel="nlp")
for entry in catalog:
print(entry.name, "-", entry.description)
# entry.invocable, entry.streaming, entry.version, entry.tags also available
await mesh.discover(channel=None)¶
Full AgentContract objects for all matching agents.
| Parameter | Type | Default | Description |
|---|---|---|---|
channel |
str \| None |
None |
Filter by name prefix (same semantics as catalog(channel=...)) |
Returns: list[AgentContract]
await mesh.contract(name)¶
Fetch a single agent's full contract. This is the authoritative source.
| Parameter | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Agent's dotted name |
Returns: AgentContract
contract = await mesh.contract("nlp.summarizer")
contract.name # "nlp.summarizer"
contract.description # "Summarizes text..."
contract.input_schema # JSON Schema dict
contract.output_schema # JSON Schema dict
contract.invocable # True
contract.streaming # False
KV Store¶
Shared KV store for structured data exchange between agents.
await mesh.kv.put(key, value)¶
Store a value.
await mesh.kv.get(key)¶
Retrieve a value by key. Returns str.
async with mesh.kv.cas(key) as entry¶
Single-attempt compare-and-swap. Read entry.value, modify it, and the new value is written on exit with CAS semantics. For concurrent access, use update() instead.
await mesh.kv.update(key, fn)¶
CAS update with automatic retry. fn receives the current value and returns the new value. On revision conflict, the value is re-read and fn is called again.
def increment(value: str) -> str:
return str(int(value) + 1)
await mesh.kv.update("counter", increment)
async for value in mesh.kv.watch(key)¶
Watch a key for changes. Yields the new value on each update.
await mesh.kv.delete(key)¶
Delete a key.
await mesh.kv.list(prefix)¶
One-shot snapshot of all entries under a prefix or wildcard pattern (ADR-0060). NATS subject wildcards (*, >) are accepted. Returns list[KVEntry[bytes]] with key, value, revision, and operation.
entries = await mesh.kv.list("wildfire.detection.*")
for e in entries:
print(e.key, e.revision, len(e.value))
async with mesh.kv.try_cas(key) as entry¶
Non-raising compare-and-swap (ADR-0060). On conflict, entry.committed is False and no exception is raised. Use for election semantics where losing the race is data, not error.
async with mesh.kv.try_cas("election.key") as entry:
if entry.value == "pending":
entry.value = f"assigned:{mesh.instance_id}"
if entry.committed:
# I won the race
...
await mesh.kv.create(key, value)¶
Put-if-absent (ADR-0060). Returns the new revision number on success. Raises KVKeyExists if the key already exists. Accepts BaseModel, bytes, or str.
Pydantic helpers¶
mesh.kv.put_model(key, model), mesh.kv.get_model(key, Model), mesh.kv.cas_model(key, Model), mesh.kv.try_cas_model(key, Model), mesh.kv.list_models(prefix, Model). Same semantics as the bytes-shaped methods, with serialization to/from model.model_dump_json() handled internally.
async with mesh.kv.try_cas_model("wildfire.detection.d1", DetectionRecord) as entry:
if entry.value.state == "pending":
entry.value.state = f"assigned:{mesh.instance_id}"
Workspace (Object Store)¶
Shared binary artifact storage backed by the NATS JetStream Object Store (mesh-artifacts bucket). Use for files, images, embeddings, or any binary payload too large for the KV store.
await mesh.workspace.put(key, data)¶
Store a binary artifact.
| Parameter | Type | Description |
|---|---|---|
key |
str |
Artifact key (supports / for hierarchy, e.g. docs/report.pdf) |
data |
bytes \| str |
Content to store. Strings are UTF-8 encoded. |
await mesh.workspace.get(key)¶
Retrieve an artifact by key. Returns bytes.
| Parameter | Type | Description |
|---|---|---|
key |
str |
Artifact key |
Returns: bytes
await mesh.workspace.delete(key)¶
Delete an artifact.
| Parameter | Type | Description |
|---|---|---|
key |
str |
Artifact key |
# Store and retrieve a binary artifact
await mesh.workspace.put("results/output.png", image_bytes)
data = await mesh.workspace.get("results/output.png")
await mesh.workspace.delete("results/output.png")
Observability¶
Structured log events and runtime level control (ADR-0048). See Observability for the model.
async for event in mesh.observe.logs(agent=None, *, level=None)¶
Tail log events as typed LogEvent objects. Runs until the caller breaks
out of the loop.
| Parameter | Type | Description |
|---|---|---|
agent |
str \| None |
Agent name; None tails the whole mesh (mesh.logs.>) |
level |
str \| None |
Minimum level to yield (debug, info, warn, error) |
Yields: LogEvent — fields timestamp, level, agent, event,
request_id, message, data.
await mesh.observe.get(agent)¶
Effective config for an agent. Returns ObserveConfig with log_level and
source ("agent", "global", or "default").
await mesh.observe.set(agent, *, log_level)¶
Set the per-agent log level (debug, info, warn, error, off).
Applies live via KV watch — no restart.
await mesh.observe.set_global(*, log_level)¶
Set the mesh-wide default level. Per-agent overrides win.
await mesh.observe.set("nlp.summarizer", log_level="debug")
async for event in mesh.observe.logs("nlp.summarizer"):
print(event.event, event.data)
Usage Attribution¶
Opt-in LLM usage reporting (ADR-0023). See Usage Attribution for the model.
report_usage(usage)¶
Module-level function (from openagentmesh import report_usage). Report LLM
usage from inside a handler while a call()/stream() request is in flight.
May be called multiple times per request: token and cost fields accumulate,
model keeps the last reported value. Raises RuntimeError outside a
request context.
The host stamps the merged result on the X-Mesh-Usage reply header (the
stream-end frame for streamers) and publishes a usage_reported observe
event at info level.
Usage¶
Pydantic model carrying self-reported usage. All fields optional:
input_tokens, output_tokens, total_tokens (int), model (str),
estimated_cost_usd (float).
from openagentmesh import Usage, report_usage
@mesh.agent
async def summarize(req: SummarizeInput) -> SummarizeOutput:
result = await call_llm(req.text)
report_usage(Usage(
input_tokens=result.input_tokens,
output_tokens=result.output_tokens,
model="claude-sonnet-4-20250514",
))
return SummarizeOutput(summary=result.text)