AgentProcess¶
Base class for all agents. Subclass it and implement handle().
See Core Concepts and Getting Started for usage examples.
civitas.process.AgentProcess(name, mailbox_size=1000, max_retries=3, shutdown_timeout=30.0, handle_timeout=None)
¶
Base class for all agent processes in Civitas.
Developers subclass this and override lifecycle hooks: - on_start(): called once before the first message - handle(message): called for every incoming message - on_error(error, message): called when handle() raises - on_stop(): called on graceful shutdown (always — even on crash)
Messaging methods available inside hooks: - send(recipient, payload, message_type): fire-and-forget - ask(recipient, payload, message_type, timeout): request-reply - send_capable(capability, payload, message_type): route to any capable agent - broadcast(pattern, payload): send to all matching agents - reply(payload): return from handle() for request-reply
Observability helpers (call from inside handle()): - llm_span(model, attrs): context manager for LLM call spans - tool_span(tool_name, attrs): context manager for tool call spans
Capability declaration (class-level, inherited and overridable): capabilities: list[str] = ["text.summarize", "text.translate"] capability_metadata: dict[str, Any] = { "text.summarize": {"description": "...", "version": "1"} }
Source code in civitas/process.py
320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 | |
on_start()
async
¶
Called once before the first message. Initialize self.state here.
Runs synchronously during startup — for a dynamically spawned agent it
executes inside the spawn call, so await self.spawn(...) does not
return until on_start() finishes (and raises TimeoutError if it
outlasts the ask timeout). Keep it fast: do slow / I/O-bound work (LLM
calls, browser sessions) in handle(), kicked off by a fire-and-forget
message the spawner sends right after the spawn is confirmed. Spawn-time
config is available here as self.config.
Source code in civitas/process.py
handle(message)
async
¶
Called for every incoming message.
Return self.reply(...) for request-reply. Return None for fire-and-forget.
on_error(error, message)
async
¶
Called when handle() raises an exception.
Return an ErrorAction. Default: ESCALATE (crash, let supervisor decide).
on_stop()
async
¶
on_child_terminated(name, reason)
async
¶
Called when a dynamically spawned child is permanently removed.
reason is one of: "restarts_exhausted", "despawned", "clean_exit". Default implementation logs a warning. Override to re-spawn, alert, etc.
Source code in civitas/process.py
send(recipient, payload, message_type='message')
async
¶
Fire-and-forget: send a message to another agent by name.
Source code in civitas/process.py
ask(recipient, payload, message_type='message', timeout=30.0, *, fail_if_suspended=False)
async
¶
Request-reply: send a message and await a response.
timeout (v0.10.0): a positive value is a bounded wait (default 30s,
unchanged). None — or -1/any value <= 0 — means wait
INDEFINITELY until the recipient replies. This is the HITL case: an
ask() to an agent that suspends for approval buffers until the agent
is resumed (hours/days later), then returns the real reply. The
suspension itself has always been indefinite; this lets the caller wait
it out.
Caveat: an indefinite ask() holds resources (the pending reply, and on
ZMQ/NATS an open subscription) for the whole wait. Fine for a background
worker driving a HITL flow; a request-scoped caller (e.g. an HTTP
handler) must NOT block a connection for days — use send() + poll/
webhook instead.
Source code in civitas/process.py
broadcast(pattern, payload)
async
¶
Send a message to all agents matching a glob pattern.
Source code in civitas/process.py
reply(payload)
¶
Create a reply message. Return this from handle() for request-reply.
Source code in civitas/process.py
checkpoint()
async
¶
Save self.state to the configured StateStore.
Call this from handle() after completing a meaningful unit of work. On restart, self.state is automatically restored from the last checkpoint. Agents that never call checkpoint() incur zero overhead.
Source code in civitas/process.py
spawn(agent_class, name, config=None, *, wait=True)
async
¶
Spawn a dynamic agent via the nearest ancestor DynamicSupervisor.
The nearest-ancestor special case of :meth:spawn_into — it resolves the
ancestor DynamicSupervisor name wired at startup and delegates. Sends a
civitas.dynamic.spawn message and awaits confirmation. Raises SpawnError if
no DynamicSupervisor ancestor exists or spawn is denied. Returns the agent
name on success.
With wait=True (default) the call returns only after the child reaches
RUNNING/SUSPENDED and raises SpawnError if its start fails. With
wait=False it returns as soon as the child's task exists; a later start
failure is delivered via on_child_terminated (R1 · D2).
Source code in civitas/process.py
despawn(name)
async
¶
Hard-stop a dynamic child immediately.
Cancels the agent's task. on_stop() still fires. Pending ask() callers into the agent receive SpawnError. The slot is freed immediately.
Source code in civitas/process.py
stop(name, drain='current', timeout=30.0)
async
¶
Soft-stop a dynamic child. Awaitable — returns when fully stopped.
drain="current" — finishes the message currently being handled, then stops. drain="all" — drains the full mailbox, then stops. timeout — fallback hard stop if drain isn't complete in time.
Source code in civitas/process.py
civitas.process.ProcessStatus
¶
Bases: Enum
Lifecycle states for an AgentProcess.
civitas.process.Mailbox(maxsize=1000, priority_maxsize=100)
¶
Bounded async queue for incoming messages with priority support.
High-priority system messages (priority > 0) are placed at the front. Normal messages follow FIFO order. Backpressure is applied when the mailbox is full — the sender awaits until space is available.
Source code in civitas/process.py
put(message)
async
¶
Enqueue a message. Priority messages bypass the normal queue.
Source code in civitas/process.py
put_nowait(message)
¶
Synchronous enqueue for callers that cannot await (D-E4-2) — e.g. an
asyncio.Task done-callback. Priority messages only; raises
asyncio.QueueFull on a bounded queue at capacity (agents' normal
queue backpressure is unaffected — this bypasses put() entirely).
Source code in civitas/process.py
get()
async
¶
Dequeue the next message. Priority messages are served first.
Messages whose ttl has elapsed are discarded with a warning instead of being returned; the search continues for the next message.
Source code in civitas/process.py
get_priority()
async
¶
Dequeue the next priority-queue message only, leaving normal messages buffered.
Used while an agent is SUSPENDED (durable-suspension S3): control messages (priority > 0) are actioned while business messages stay in the normal queue, preserving FIFO order and backpressure for resume.
Concurrency footgun (Oracle finding #10): _notify is shared and is
also set by normal-queue puts. We therefore clear it and re-check the
priority queue before awaiting, so a normal put only produces a
bounded spurious wakeup (no busy-loop) and a priority put racing with
the clear is never lost.
Source code in civitas/process.py
empty()
¶
depth()
¶
Total buffered messages (both queues). Sync-safe — used by the Worker health responder's snapshot (D5, report-only).
peek()
¶
Non-destructively snapshot all buffered messages (priority first),
WITHOUT consuming them (v0.9.6, control-plane-writes.md §6 mailbox
introspection). Reads asyncio.Queue's backing collections.deque
(._queue) directly -- the only non-consuming way to inspect it;
get()/drain() all consume. For introspection/reporting only.
Source code in civitas/process.py
drain()
¶
Remove and return all buffered messages, priority queue first.