Skip to content

Tectum — Reference

Cognition × Application

Schema document format, node lifecycle hooks, subject naming rules, and the event envelope.

The event envelope

Every message on the bus is a TectumEvent. Fields:

Field Type Meaning
event_id UUID Identifies this envelope. A replay of the same logical event gets a fresh event_id.
event_type str Dotted namespace string, e.g. vision.frame, speech.utterance.
source str Producer identity, e.g. camera.front, agent.planner.
timestamp datetime (UTC) Wall-clock production time.
sequence_number int Monotonic counter scoped to source, for drop detection and ordering.
ttl_ms int Producer's hint for how long the event stays actionable. A sink may drop an expired one.
payload dict Type-specific body; schema is implied by event_type.
origin_node str The host that first emitted the event.
hops list[str] Ordered list of nodes/agents that have processed the event.
seen_by set[str] Subscribers that have acknowledged it.
binding_id str | None Correlates events in the same logical flow (e.g. perceive → plan → act), preserved across hops.
produced_by_schema / routed_by_schema str | None Which schema produced/routed the event, for audit and replay.
expectation / residual dict | None Predictive-routing fields: what the producer expected next, and the post-hoc difference from what actually happened.
audit dict | None Open dict for tracing and metrics.

Events are immutable in spirit: a node that transforms one produces a new event with hops/seen_by reflecting its own identity, rather than mutating the original in place. Wire format is JSON over NATS (to_json_bytes / from_json_bytes).

Subject namespace

The dotted prefix of every subject encodes which DAG it belongs to:

events.sensation.*   bottom-up: sensors -> filters -> ...in   (workspace is a TARGET)
events.workspace.*   the workspace layer: <agent>.in (dispatch), <agent>.out.<tool>
events.action.*      cross-effector / world-action coordination
events.system.*      Tectum's self-report: schema/node/substrate lifecycle
events.journal.*     durable-record emissions (effector write acknowledgements)

The sensation DAG is strictly bottom-up; the command DAG is strictly top-down (an agent-side wrapper publishes events.workspace.<agent>.out.<tool>, an effector consumes it). A subject belongs to at most one DAG.

Device token. A command-DAG subject may carry an optional fifth token: events.workspace.<agent>.out.<tool>[.<device>]. An agent that serves one specific device (a phone, say) stamps that token on everything it emits, and a schema binds the concrete deviced subject to the effector that reaches that device. The token's absence is not a default device — it means the agent has no device dimension. A binder that wants every device-scoped rail of one tool subscribes …out.<tool>.>, which does not also match the undeviced …out.<tool>; undeviced and device-scoped rails are deliberately different rails.

Control plane. actions.praxis.* is distinct from events.*: events.* is observation (anyone may watch), actions.praxis.* is actuation — the per-host control agent driving node processes. Keeping the trees separate lets a node subscribe its own control channels without pulling the whole observation stream.

Builder Produces
agent_in(agent_type) events.workspace.<agent_type>.in
agent_out(agent_type, tool, device=None) events.workspace.<agent_type>.out.<tool>[.<device>]
agent_out_wildcard(agent_type="*") events.workspace.<agent_type>.out.>
agent_out_device_wildcard(agent_type, tool) events.workspace.<agent_type>.out.<tool>.>
praxis_bind(node_name) actions.praxis.bind.<node_name>
praxis_exit(node_name) actions.praxis.exit.<node_name>
journal_ack(kind, binding_id) events.journal.<kind>.<binding_id or "none">

Node manifest and lifecycle

A node declares its contract once, at the @tectum_node decorator that wraps its class — there is no sidecar YAML to drift from:

Manifest field Meaning
name The node's registered name, e.g. effector.speech.
substrate Which substrate this node belongs to.
subscribes Subject patterns this node is designed to handle — a descriptive contract the schema loader validates bindings against, not a hard dependency on a publisher existing.
publishes Subject patterns this node may emit.
resources Advisory config, e.g. llm_endpoint, llm_model.
restart_policy never | on_failure | always.
delivery queue (a NATS queue group — many processes of this node form a worker pool, each event handled once) or broadcast (every instance sees every event; for taps/loggers).
concurrency How many events one process handles at once. 1 serializes on a shared resource (an audio device, a model session); more runs that many on_event handlers concurrently.

Importing a node module fires the decorator and registers the manifest — nothing else expensive is allowed to happen at import time. Heavy dependencies (model clients, nats, torch, transformers, …) are imported inside setup(), never at module top, so the scan pass can read every node's manifest offline without paying import or model-load cost.

A node implements three lifecycle hooks:

async def setup(self) -> None: ...      # acquire resources, open connections
async def on_event(self, event: TectumEvent) -> None: ...  # handle one delivered event
async def teardown(self) -> None: ...   # release resources

Nodes do not subscribe to the bus themselves. Bindings live in Praxis: the activator subscribes each binding's from subject and delivers matching events to the target node's on_event.

Praxis schema document

name: listening_mode
version: 1                 # authoring metadata only (no enforcement yet)
author: hand                # who/what authored this schema
substrate: home              # advisory home substrate for the schema
immutable: false            # true => only operator-class callers may deactivate
ttl: forever                # forever | <seconds>
description: "..."
requires:
  nodes: [audio.gateway, agent.persona_reflection, effector.speech]
bindings:
  - from: events.sensation.audio.transcript
    to: audio.gateway
  - from: events.sensation.audio.filtered
    to: agent.persona_reflection   # 'agent.X' is just a node name
audit:
  journal_threshold: 0.5

A binding delivers events matching from to a nodeto must be a registered node name listed in requires.nodes. Subject-to-subject re-publish bridges and inline payload predicates are deliberately not part of this model: payload filtering belongs in filter nodes, and behavior belongs in nodes. predicates (system-event reactions) are parsed but not yet acted on.

CLI

tectum scan                       # offline node graph (delivery/slots too)
tectum status                     # live-stack reachability + control/supervision graph
tectum up                         # control agent + supervised node fleet, hold until Ctrl-C
tectum up --no-supervise          # force-fork every node, no supervision

tectum --node effector.speech     # one node as its own process; --module/-e for custom nodes

tectum schema validate praxis/listening_mode.yaml   # offline type-check
tectum schema apply    praxis/listening_mode.yaml   # apply (spawns required absent nodes)
tectum schema unapply  listening_mode               # refused if immutable
tectum schema list                                  # active schemas + binding refcounts
tectum node list                                    # instances, heartbeat age, slots, supervision
tectum node restart    effector.speech              # real restart (supervisor respawns it)
tectum node stop       effector.speech              # stop + suppress respawn
tectum node spawn      effector.speech              # bring a required node up from its spec
tectum spec list                                    # launch specs the agent can respawn from

See the DevOps Manual for how these fit into a running deployment, and the Cookbook for worked schema examples.