Lifecycle Gates¶
Some agents should only be online while a condition holds. A wildfire response fleet has no business consuming requests between incidents; a batch enricher should sit out business hours; a canary agent should serve only while a feature flag is set. Checking the condition inside the handler works, but the agent still accepts (then rejects) requests, still occupies its queue-group slot, and scatters lifecycle logic across every handler.
active_when declares the condition once, on the decorator. The SDK
watches it and subscribes or unsubscribes the agent as the condition flips
— no controller process, no restarts.
import json
from openagentmesh import AgentMesh, AgentSpec
mesh = AgentMesh()
# Only subscribes while the incident.mode KV key is "active".
@mesh.agent(
AgentSpec(
name="wildfire.coordinator",
description="Assigns tasks to response fleets during active wildfire incidents",
),
active_when=mesh.kv_condition("incident.mode", lambda v: json.loads(v) == "active" if v else False),
)
async def coordinator(brief: IncidentBrief) -> TaskAssignment:
return await llm.assign_tasks(brief)
Set incident.mode to "active" — from any process on the mesh — and the
coordinator subscribes. Set it to anything else and the coordinator drains
in-flight work, then unsubscribes.
await mesh.kv.put("incident.mode", json.dumps("active")) # gate opens
await mesh.kv.put("incident.mode", json.dumps("contained")) # gate closes
Conditions¶
Two condition factories ship with the SDK:
| Factory | Signal | Predicate receives |
|---|---|---|
mesh.kv_condition(key, predicate) |
A key in the shared mesh-context KV bucket |
The key's raw bytes value, or None when absent/deleted |
mesh.subject_condition(subject, predicate) |
Messages on a plain NATS subject | Each message's payload bytes |
Both accept two keyword arguments:
initial(defaultFalse) — the agent's state before the first signal. For KV conditions this rarely matters: the SDK reads the key's current value when the mesh starts and applies the predicate immediately, soinitialis only the fallback when that read fails (e.g. missing permissions). For subject conditions it is the state until the first message arrives.drain_timeout(default30.0) — how long in-flight handlers may run after the gate closes before the agent unsubscribes anyway.
Any object with predicate, initial, and drain_timeout attributes
satisfies the Condition protocol; KVCondition and SubjectCondition
are the shipped implementations.
What a closed gate means¶
- The RPC subscription is gone. The agent has left its queue group, so NATS stops routing requests to it. Other replicas whose gates are open keep serving.
- Sources are gated too. No
kv_sourceorsubject_sourcedelivers messages while the agent is offline;sourcesandactive_whencompose. - The catalog entry stays. The catalog describes capability, not availability — discovery still lists the agent, and its contract is unchanged.
not_available vs not_found¶
Calling an agent whose gate is closed raises NotAvailable
(not_available on the wire): the request got no responders, but the agent
is still in the catalog, so it exists and may come back. Calling an agent
that was never registered raises NotFound. The distinction tells a caller
whether to retry later or give up.
One edge: a request published in the instant the gate is closing can be
dropped rather than answered — the caller sees a MeshTimeout for that
request, and NotAvailable from the next one. Treat both as retryable.
from openagentmesh import NotAvailable
try:
result = await mesh.call("wildfire.coordinator", {"summary": "smoke report"})
except NotAvailable:
... # gated offline — retry when the incident activates
Drain semantics¶
When the gate closes, the agent first leaves its queue group — new
requests stop arriving — and in-flight handlers get up to drain_timeout
seconds to finish. Requests that complete within the window return their
results normally, even though the gate is already closed. Only then are
source subscriptions and background tasks torn down.
Multiple instances¶
Each instance watches the condition independently. All instances see the same KV value and the same predicate, so they converge on the same state without coordination — closing a gate takes every replica offline, opening it brings them all back.
Observability¶
Gate transitions publish agent_activated and agent_deactivated log
events at info level on the agent's mesh.logs.{name} subject — visible
in oam observe logs like every other observability event.
Secured meshes¶
Gates ride existing permission surfaces: kv_condition needs read access
to the mesh-context bucket (all shipped roles have it),
and subject_condition needs subscribe permission on its subject — the
same constraint as subject_source. No extra grants are required.