BYTES AND BRAINS v0.3.0 . CRATES.IO
UPLINK OK
guide/04 . syscalls-reference // SOURCE: bytesandbrains/bb-ops/src/syscalls/ and bytesandbrains/bb-dsl/src/syscalls.rs
Chapter 4

Syscalls Reference

Chapter 3 covered the recording surface authors write into. Most of the recording lands on user code or role placeholders, but a handful of canonical NodeProtos come straight from the framework. Those are the syscalls. Each one ships with a stable (domain, op_type) key, a fixed input and output shape, a documented set of attributes, and a known firing pattern (synchronous, async-by-CommandId, trigger-only, or control-plane). The compiler knows about them. The engine dispatches against them via inventory-collected registrations. Authors compose them into a Graph either through one of the helpers in bb-dsl/src/syscalls.rs or by pushing a typed NodeProto with g.push_node.

This chapter is a reference. One section per syscall. Every claim cites a path and line in the bytesandbrains source.

Where syscalls come from

The framework’s syscall opset lives under the ai.bytesandbrains.syscall reverse-DNS root. Three sibling opsets share the same registration mechanism: ai.bytesandbrains.composite holds the bundle and unbundle ops, ai.bytesandbrains.address_book holds the typed peer-table ops, and the wire opset ai.bytesandbrains.wire (covered in Chapter 11) holds Send and Recv. Each domain string is a pub const re-exported by bb-ir::syscall_ids so the compiler and the runtime cite one declaration:

// from bytesandbrains/bb-ir/src/syscall_ids.rs:9
pub const SYSCALL_DOMAIN: &str = "ai.bytesandbrains.syscall";

Every concrete syscall self-registers with the global registry via inventory::submit!, and Engine::register_all_framework_syscalls walks the registry at Node::ensure_ready:

// from bytesandbrains/bb-ops/src/syscalls/mod.rs:1-15
//! Framework syscall components - each sub-directory hosts one or
//! more registerable ops. Self-registration is via
//! `inventory::submit!`; `Engine::register_all_framework_syscalls`
//! walks the registry at Node::ensure_ready.

None of the syscalls below are sensitive to the install signature. The same (domain, op_type) dispatch fires whether the host installed the syscall’s containing target alone (bb::install(.., &["MyModule"], ..)) or as part of a multi-target slice (bb::install(.., &["Client", "Server"], ..)). For the full install API see the engine chapter; the slice form is the only form, and length-1 covers the single-target case.

The invoke functions all share one signature. Inputs ride in a positional &[(&str, &dyn SlotValue)] slice keyed by port name; attributes are read off the NodeProto.attribute vector; the runtime side returns a DispatchResult that says whether the op produced an immediate value, suspended on a CommandId, or fired no value at all (an absorbed trigger). The two enum variants the rest of this chapter references both live on DispatchResult. Immediate(Vec<(String, Box<dyn SlotValue>)>) carries one entry per output port; an empty Vec means the op fired but had no value (a sink, or a gated absorb). Async(CommandId) parks the op on the engine’s pending_async table until the host completes the command.

Firing-behavior taxonomy

Every section below tags one of four behaviors:

  • Sync. Inputs land. Body runs. Outputs go out the same tick. The invoke returns DispatchResult::Immediate(...) with the produced values.
  • Async-by-CommandId. Body schedules work and returns DispatchResult::Async(cmd_id). The engine parks the op until a completion arrives on the matching command id. Used by Sleep and After against the scheduler, and by BootstrapOutput against the ingress queue.
  • Trigger-only. No real value flows. The output is a TriggerValue whose only meaning is “this fired”. Used by Pulse, EventSource, and OnTrigger as well as the gate ops on the wire chain.
  • Control-plane. No outputs at all (Immediate(vec![])). The op’s effect is a side-mutation on the runtime resource (a metric increment, an address book write, a record append). Authors compose these for telemetry and lifecycle wiring.

The behavior is fixed per syscall. Authors do not pick it; the engine relies on it.

Coordination

The coordination ops live in bytesandbrains/bb-ops/src/syscalls/coordination/. The eight original ops share the same file because each body follows the same template; a ninth op, DeadlineCheck, lives in its own sub-module because the compiler pass that inserts it needs the op-type and attribute name constants on a clean re-export surface (see bb-ops/src/syscalls/coordination/mod.rs:1-15).

Limit.Acquire

Counting-semaphore acquire. Returns a Trigger when the gate has room under its declared count, returns nothing when the gate is full.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:46-71
pub fn invoke_limit_acquire(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let name = node
        .attribute
        .iter()
        .find(|a| a.name == "name")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    let n = node
        .attribute
        .iter()
        .find(|a| a.name == "n")
        .map(|a| a.i as u32)
        .unwrap_or(1);
    if ctx.peers.gate.acquire(&name, n) {
        Ok(DispatchResult::Immediate(vec![(
            "trigger".to_string(),
            Box::new(TriggerValue),
        )]))
    } else {
        Ok(DispatchResult::Immediate(vec![]))
    }
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Limit.Acquire (mod.rs:278).
  • Inputs: none required; trigger upstream wires the op into the frontier.
  • Outputs: trigger: Trigger on acquire, no output on gate.
  • Attributes: name STRING (gate identity), n INT default 1 (the count the gate permits).
  • Firing: sync; trigger-only.

Limit.Release

Counting-semaphore release. Always succeeds. Used to return a slot to the named gate so a paired Acquire can refire.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:77-90
pub fn invoke_limit_release(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let name = node
        .attribute
        .iter()
        .find(|a| a.name == "name")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    ctx.peers.gate.release(&name);
    Ok(DispatchResult::Immediate(vec![]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Limit.Release (mod.rs:287).
  • Inputs: none required.
  • Outputs: none. Control-plane mutation only.
  • Attributes: name STRING.
  • Firing: sync; control-plane.

Any

First-arrival selector across variadic inputs scoped by a group attribute. The first input value emitted into the group propagates; subsequent values in the same group are absorbed.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:102-127
pub fn invoke_any(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let Some((_, first)) = inputs.first() else {
        return Ok(DispatchResult::Immediate(vec![]));
    };
    let group = node
        .attribute
        .iter()
        .find(|a| a.name == "group")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    if !group.is_empty() && !ctx.syscall.any_fired_groups.insert(group) {
        return Ok(DispatchResult::Immediate(vec![]));
    }
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        first.clone_boxed(),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Any (mod.rs:296).
  • Inputs: variadic. The first arriving input wins.
  • Outputs: value, carrying the winning input’s concrete type via clone_boxed.
  • Attributes: group STRING. Empty group degrades to “always fire” for tests; non-empty group gates on a first-fire latch in ctx.syscall.any_fired_groups (mod.rs:119).
  • Firing: sync; absorb on repeat fires.

Gate

A two-input release. Holds a value until its companion trigger arrives, then releases it downstream. The engine’s all-inputs-ready check already guarantees both have landed before the body runs, so the body is a one-liner that re-emits the value.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:137-152
pub fn invoke_gate(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let Some((_, value)) = inputs.first() else {
        return Err(OpError {
            detail: "Gate requires value input".to_string(),
            ..Default::default()
        });
    };
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        value.clone_boxed(),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Gate (mod.rs:305).
  • Inputs: value (the held value), trigger: Trigger (the release).
  • Outputs: value, preserving the input’s concrete type.
  • Attributes: none.
  • Firing: sync.

Hold.Stash

Stash a value in a named slot on the engine’s hold_table so a later Hold.Flush can release it. The op consumes a single BytesValue input; a TriggerValue-shaped input (no payload) stashes empty bytes under the slot.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:228-243
pub fn invoke_hold_stash(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let slot = node
        .attribute
        .iter()
        .find(|a| a.name == "slot")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    let bytes =
        crate::syscalls::first_input_optional_bytes("Hold.Stash", inputs)?.unwrap_or_default();
    ctx.syscall.hold_table.stash(&slot, bytes);
    Ok(DispatchResult::Immediate(vec![]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Hold.Stash (mod.rs:341).
  • Inputs: optional first input, decoded as BytesValue via first_input_optional_bytes (mod.rs:28-43). A missing input stashes empty bytes; an input that exists but is not BytesValue returns OpError with TypeMismatch so wiring errors surface immediately.
  • Outputs: none. Control-plane mutation only.
  • Attributes: slot STRING (the stash key).
  • Firing: sync; control-plane.

Hold.Flush

Release whatever bytes the slot holds and emit them downstream. Empty slot is a non-firing no-op.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:249-267
pub fn invoke_hold_flush(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let slot = node
        .attribute
        .iter()
        .find(|a| a.name == "slot")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    let Some(bytes) = ctx.syscall.hold_table.flush(&slot) else {
        return Ok(DispatchResult::Immediate(vec![]));
    };
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        Box::new(BytesValue(bytes)),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Hold.Flush (mod.rs:350).
  • Inputs: usually a TriggerValue to drive the firing; the body ignores inputs.
  • Outputs: value: BytesValue when the slot has bytes, no output otherwise.
  • Attributes: slot STRING.
  • Firing: sync.

Serialize.Enqueue

Append the input bytes to the FIFO under queue. Returns a Trigger so downstream wiring can chain.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:160-178
pub fn invoke_serialize_enqueue(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let queue = node
        .attribute
        .iter()
        .find(|a| a.name == "queue")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    let bytes = crate::syscalls::first_input_optional_bytes("Serialize.Enqueue", inputs)?
        .unwrap_or_default();
    ctx.syscall.serialize_queue.enqueue(&queue, bytes);
    Ok(DispatchResult::Immediate(vec![(
        "trigger".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Serialize.Enqueue (mod.rs:314).
  • Inputs: optional first input as BytesValue. Missing input enqueues empty bytes.
  • Outputs: trigger: Trigger.
  • Attributes: queue STRING.
  • Firing: sync.

Serialize.Dequeue

Pop the front of the FIFO. Emits a BytesValue when the queue is non-empty; fires nothing otherwise.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:184-202
pub fn invoke_serialize_dequeue(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let queue = node
        .attribute
        .iter()
        .find(|a| a.name == "queue")
        .map(|a| String::from_utf8_lossy(&a.s).into_owned())
        .unwrap_or_default();
    let Some(bytes) = ctx.syscall.serialize_queue.dequeue(&queue) else {
        return Ok(DispatchResult::Immediate(vec![]));
    };
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        Box::new(BytesValue(bytes)),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: Serialize.Dequeue (mod.rs:323).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: value: BytesValue when the queue is non-empty.
  • Attributes: queue STRING.
  • Firing: sync.

CorrelateTag

Mint a fresh request token from the runtime’s request tracker. Used upstream of correlated request/response pairs.

// from bytesandbrains/bb-ops/src/syscalls/coordination/mod.rs:210-220
pub fn invoke_correlate_tag(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let token = ctx.net.requests.mint_token();
    Ok(DispatchResult::Immediate(vec![(
        "token".to_string(),
        Box::new(CorrelationTokenValue(token.as_u64())),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:38).
  • Op type: CorrelateTag (mod.rs:332).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: token: CorrelationToken (u64 newtype).
  • Attributes: none.
  • Firing: sync.

DeadlineCheck

A clock-driven gate. Authors do not typically record this op directly; the compiler pass bb-compiler/src/insert_async_deadlines.rs inserts one upstream of every async op carrying a deadline_ns attribute (bb-ops/src/syscalls/coordination/deadline_check.rs:1-11). The body compares the engine’s now_ns against the deadline; on miss it fires a Trigger, on past-deadline it errors with "deadline exceeded".

// from bytesandbrains/bb-ops/src/syscalls/coordination/deadline_check.rs:34-59
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let deadline_ns = node
        .attribute
        .iter()
        .find(|a| a.name == ATTR_DEADLINE_NS)
        .map(|a| a.i as u64)
        .ok_or_else(|| OpError {
            detail: format!("DeadlineCheck missing required `{ATTR_DEADLINE_NS}` attribute"),
            ..Default::default()
        })?;
    let now_ns = ctx.time.scheduler.now_ns();
    if now_ns >= deadline_ns {
        return Err(OpError {
            detail: "deadline exceeded".to_string(),
            ..Default::default()
        });
    }
    Ok(DispatchResult::Immediate(vec![(
        "trigger".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (deadline_check.rs:24).
  • Op type: DeadlineCheck (deadline_check.rs:26).
  • Inputs: ignored.
  • Outputs: trigger: Trigger on pre-deadline, none on miss (op fails).
  • Attributes: deadline_ns INT required (deadline_check.rs:29). No default. Missing attribute returns OpError.
  • Firing: compiler-inserted; sync; trigger-only.

Trigger and timer sources

The trigger module sits under bytesandbrains/bb-ops/src/syscalls/triggers/ and covers Pulse, After, Interval, OnTrigger, and EventSource. The spec sits at IR_AND_DSL §5a Sub-A (triggers/mod.rs:1-5).

Pulse

A one-shot bootstrap trigger. Authors record a Pulse when they need a single firing at startup to seed a downstream chain.

// from bytesandbrains/bb-ops/src/syscalls/triggers/pulse.rs:19-28
pub fn invoke(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    Ok(DispatchResult::Immediate(vec![(
        "trigger".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (pulse.rs:14).
  • Op type: Pulse (pulse.rs:16).
  • Inputs: none.
  • Outputs: trigger: Trigger.
  • Attributes: none.
  • Firing: sync; trigger-only.

After

Schedule a delayed trigger. Returns Async(cmd); the engine routes the delayed Trigger through handle_completion after maturity (after.rs:1-6).

// from bytesandbrains/bb-ops/src/syscalls/triggers/after.rs:23-41
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let delay_ns = node
        .attribute
        .iter()
        .find(|a| a.name == "delay_ns")
        .map(|a| a.i as u64)
        .unwrap_or(0);
    let now = ctx.time.scheduler.now_ns();
    let cmd = ctx.allocate_command_id();
    ctx.time.scheduler.schedule(
        now.saturating_add(delay_ns),
        TimerKind::After { key: cmd.as_u64() },
    );
    Ok(DispatchResult::Async(cmd))
}
  • Domain: ai.bytesandbrains.syscall (after.rs:18).
  • Op type: After (after.rs:20).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: trigger: Trigger once the timer matures.
  • Attributes: delay_ns INT, default 0.
  • Firing: async-by-CommandId.

Interval

Periodic Trigger source. Reads period_ns, schedules the next firing, and emits the current tick.

// from bytesandbrains/bb-ops/src/syscalls/triggers/interval.rs:25-50
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let period_ns = node
        .attribute
        .iter()
        .find(|a| a.name == "period_ns")
        .map(|a| a.i as u64)
        .unwrap_or(1_000_000_000);
    let now = ctx.time.scheduler.now_ns();
    let op_key = ctx.current.op_ref.as_u64();
    ctx.time.scheduler.schedule(
        now.saturating_add(period_ns),
        TimerKind::Interval {
            period_ns,
            key: op_key,
        },
    );
    Ok(DispatchResult::Immediate(vec![(
        "tick".to_string(),
        Box::new(TimestampValue(now)),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (interval.rs:20).
  • Op type: Interval (interval.rs:22).
  • Inputs: none.
  • Outputs: tick: Timestamp (the engine’s now_ns at the firing edge).
  • Attributes: period_ns INT, default 1_000_000_000 (1s).
  • Firing: sync emit; the scheduler re-arms the next firing under TimerKind::Interval.

OnTrigger

Pass-through for a single Trigger input. Used to thread a Trigger across a partition without other side-effects.

// from bytesandbrains/bb-ops/src/syscalls/triggers/on_trigger.rs:19-34
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    if inputs.is_empty() {
        return Err(OpError {
            detail: "OnTrigger requires one input".into(),
            ..Default::default()
        });
    }
    Ok(DispatchResult::Immediate(vec![(
        "trigger".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (on_trigger.rs:14).
  • Op type: OnTrigger (on_trigger.rs:16).
  • Inputs: one input required (any type); body re-emits a fresh Trigger.
  • Outputs: trigger: Trigger.
  • Attributes: none.
  • Firing: sync; trigger-only.

EventSource

Bus-event-driven Trigger source. The install path parses each EventSource NodeProto’s kind attribute and calls Engine::register_event_subscription so the engine’s Phase 3 bus drain pushes the op onto the frontier when a matching event arrives (event_source.rs:1-9).

// from bytesandbrains/bb-ops/src/syscalls/triggers/event_source.rs:26-35
pub fn invoke(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    Ok(DispatchResult::Immediate(vec![(
        "event".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (event_source.rs:21).
  • Op type: EventSource (event_source.rs:23).
  • Inputs: none.
  • Outputs: event: Trigger (one per matching event arrival).
  • Attributes: kind STRING (the subscription key the install path reads to wire the subscription; see event_source.rs:1-9).
  • Firing: trigger-only; refires on every event arrival.

Clock and randomness

Sleep

Schedule a delay and suspend on the matching CommandId (bb-ops/src/syscalls/clock_rng/sleep.rs:1-2).

// from bytesandbrains/bb-ops/src/syscalls/clock_rng/sleep.rs:21-40
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let duration_ns = node
        .attribute
        .iter()
        .find(|a| a.name == "duration_ns")
        .map(|a| a.i as u64)
        .unwrap_or(0);
    let now = ctx.time.scheduler.now_ns();
    let cmd = ctx.allocate_command_id();
    ctx.time
        .scheduler
        .schedule(now.saturating_add(duration_ns), TimerKind::Sleep(cmd));
    Ok(DispatchResult::Async(cmd))
}
  • Domain: ai.bytesandbrains.syscall (sleep.rs:15).
  • Op type: Sleep (sleep.rs:17).
  • Inputs: usually a trigger.
  • Outputs: trigger: Trigger once the sleep matures.
  • Attributes: duration_ns INT, default 0.
  • Firing: async-by-CommandId.

Clock

Read the engine’s current time.

// from bytesandbrains/bb-ops/src/syscalls/clock_rng/clock.rs:19-29
pub fn invoke(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let now_ns = ctx.time.scheduler.now_ns();
    Ok(DispatchResult::Immediate(vec![(
        "now".to_string(),
        Box::new(TimestampValue(now_ns)),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (clock.rs:14).
  • Op type: Clock (clock.rs:16).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: now: Timestamp (u64 nanoseconds).
  • Attributes: none.
  • Firing: sync.

RngU64

Emit one u64 from the framework RNG.

// from bytesandbrains/bb-ops/src/syscalls/clock_rng/rng_u64.rs:19-29
pub fn invoke(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let value = ctx.syscall.rng.next_u64();
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        Box::new(U64Value(value)),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (rng_u64.rs:14).
  • Op type: RngU64 (rng_u64.rs:16).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: value: U64.
  • Attributes: none.
  • Firing: sync.

DeadlineMatch

First-arrival selector between a then trigger and a timeout trigger. Latched per (OpRef, ExecId) so the same op fires once per logical execution.

// from bytesandbrains/bb-ops/src/syscalls/clock_rng/deadline_match.rs:24-46
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    if inputs.is_empty() {
        return Err(OpError {
            detail: "DeadlineMatch requires at least one input".to_string(),
            ..Default::default()
        });
    }
    let latch_key = (ctx.current.op_ref.as_u64(), ctx.current.exec_id.as_u64());
    if !ctx.syscall.deadline_match_fired.insert(latch_key) {
        return Ok(DispatchResult::Immediate(vec![]));
    }
    Ok(DispatchResult::Immediate(vec![(
        "winner".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (deadline_match.rs:15).
  • Op type: DeadlineMatch (deadline_match.rs:17).
  • Inputs: then and timeout triggers (at least one required).
  • Outputs: winner: Trigger for the first arrival per (op_ref, exec_id); absorbed on repeat fires.
  • Attributes: none.
  • Firing: sync; trigger-only; latched per execution.

Telemetry

The telemetry module sits at bytesandbrains/bb-ops/src/syscalls/telemetry/mod.rs and ships four ops. The spec sits at IR_AND_DSL §5a Sub-C (telemetry/mod.rs:1-8). AppEmit and AppNotify push onto the engine’s pending_app_events queue; Phase 8 of the engine’s poll drains them into EngineStep::AppEvent. Record writes to the record buffer; IncrMetric bumps the counters HashMap.

AppEmit

Emit a named app event carrying a BytesValue payload. Reserved prefixes (bb., ai.bytesandbrains.) are rejected with OpErrorKind::BadInput so user emissions cannot impersonate the framework’s topics.

// from bytesandbrains/bb-ops/src/syscalls/telemetry/mod.rs:38-53
pub fn invoke_app_emit(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let name = read_name(node);
    let value_bytes =
        crate::syscalls::first_input_optional_bytes("AppEmit", inputs)?.unwrap_or_default();
    let event = AppEvent::emit(name, value_bytes).map_err(|e| OpError {
        kind: OpErrorKind::BadInput,
        reason: "reserved_topic_prefix",
        detail: e.to_string(),
    })?;
    ctx.syscall.pending_app_events.push(event);
    Ok(DispatchResult::Immediate(vec![]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:16).
  • Op type: AppEmit (mod.rs:129).
  • Inputs: optional first input as BytesValue.
  • Outputs: none. Control-plane only.
  • Attributes: name STRING.
  • Firing: sync; control-plane.

AppNotify

Emit a name-only event (no payload). Same reserved-prefix rule as AppEmit.

// from bytesandbrains/bb-ops/src/syscalls/telemetry/mod.rs:60-73
pub fn invoke_app_notify(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let name = read_name(node);
    let event = AppEvent::notify(name).map_err(|e| OpError {
        kind: OpErrorKind::BadInput,
        reason: "reserved_topic_prefix",
        detail: e.to_string(),
    })?;
    ctx.syscall.pending_app_events.push(event);
    Ok(DispatchResult::Immediate(vec![]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:16).
  • Op type: AppNotify (mod.rs:138).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: none.
  • Attributes: name STRING.
  • Firing: sync; control-plane.

Record

Append bytes to the engine’s record buffer under a name.

// from bytesandbrains/bb-ops/src/syscalls/telemetry/mod.rs:80-89
pub fn invoke_record(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let name = read_name(node);
    let bytes = crate::syscalls::first_input_optional_bytes("Record", inputs)?.unwrap_or_default();
    ctx.syscall.record_buffer.record(&name, bytes);
    Ok(DispatchResult::Immediate(vec![]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:16).
  • Op type: Record (mod.rs:147).
  • Inputs: optional first input as BytesValue.
  • Outputs: none.
  • Attributes: name STRING.
  • Firing: sync; control-plane.

IncrMetric

Increment a named counter on the engine’s counters HashMap.

// from bytesandbrains/bb-ops/src/syscalls/telemetry/mod.rs:95-109
pub fn invoke_incr_metric(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let name = read_name(node);
    let delta = node
        .attribute
        .iter()
        .find(|a| a.name == "delta")
        .map(|a| a.i)
        .unwrap_or(1) as u64;
    *ctx.syscall.counters.entry(name).or_insert(0) += delta;
    Ok(DispatchResult::Immediate(vec![]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:16).
  • Op type: IncrMetric (mod.rs:156).
  • Inputs: usually a trigger; body ignores inputs.
  • Outputs: none.
  • Attributes: name STRING; delta INT, default 1.
  • Firing: sync; control-plane.

Structural

The structural module at bytesandbrains/bb-ops/src/syscalls/structural/mod.rs ships PassThrough, Tee, and Constant. All three reach the IR through the syscall domain so the compiler can recognize them without a custom opset, and the DSL exposes the most-used one (pass_through) as a free function on bb_dsl::syscalls.

PassThrough

The framework’s identity op. Forwards the input value through a partition via SlotValue::clone_boxed so the concrete type survives. Authors reach for it when a partition needs a non-wire NodeProto, for instance a class that only re-broadcasts a value it received over the wire.

// from bytesandbrains/bb-ops/src/syscalls/structural/pass_through/mod.rs:33-48
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let Some((_, input)) = inputs.first() else {
        return Err(OpError {
            detail: "PassThrough requires one input".to_string(),
            ..Default::default()
        });
    };
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        input.clone_boxed(),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (pass_through/mod.rs:21).
  • Op type: PassThrough (pass_through/mod.rs:18, re-exported from bb_ir::syscall_ids::OP_PASS_THROUGH = "PassThrough" at bb-ir/src/syscall_ids.rs:37).
  • Inputs: one input, any concrete type.
  • Outputs: value, preserving the input’s concrete type.
  • Attributes: none.
  • Firing: sync.

The DSL helper lives at bb-dsl/src/syscalls.rs:37-48:

// from bytesandbrains/bb-dsl/src/syscalls.rs:37-48
pub fn pass_through(g: &mut Graph, input: Output) -> Output {
    let out_name = g.next_site_name();
    g.push_node(NodeProto {
        op_type: ids::PASS_THROUGH_OP.into(),
        domain: ids::SYSCALL_DOMAIN.into(),
        input: vec![input.name],
        output: vec![out_name.clone()],
        ..Default::default()
    });
    g.declare_value_info(&out_name, input.type_node);
    Output::new(out_name, input.type_node)
}

pass_through is re-exported on the crate facade at bytesandbrains/src/lib.rs:253. Authors import it as bytesandbrains::pass_through and call it directly:

use bytesandbrains::{pass_through, Graph, Module, Output};

struct PassDemo;
impl Module for PassDemo {
    fn name(&self) -> &str { "PassDemo" }
    fn body(&self, g: &mut Graph) {
        let raw: Output = g.input("raw");
        let forwarded: Output = pass_through(g, raw);
        g.output("forwarded", forwarded);
    }
}

Tee

Fan a single input out to N identical outputs via clone_boxed. Each output preserves the concrete type.

// from bytesandbrains/bb-ops/src/syscalls/structural/tee/mod.rs:20-43
pub fn invoke(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let Some((_, input)) = inputs.first() else {
        return Err(OpError {
            detail: "Tee requires one input".to_string(),
            ..Default::default()
        });
    };
    let fanout = node
        .attribute
        .iter()
        .find(|a| a.name == "fanout")
        .map(|a| a.i.max(1) as usize)
        .unwrap_or(2);

    let mut outs: Vec<(String, Box<dyn SlotValue>)> = Vec::with_capacity(fanout);
    for i in 0..fanout {
        outs.push((format!("out_{i}"), input.clone_boxed()));
    }
    Ok(DispatchResult::Immediate(outs))
}
  • Domain: ai.bytesandbrains.syscall (tee/mod.rs:12).
  • Op type: Tee (tee/mod.rs:11, mapped from OP_TEE = "Tee" at bb-ir/src/syscall_ids.rs:49).
  • Inputs: one input, any concrete type.
  • Outputs: out_0, out_1, …, out_{fanout-1}. Each preserves the input’s concrete type.
  • Attributes: fanout INT, default 2, lower-bounded at 1.
  • Firing: sync.

Constant

Emit the bytes of a TensorProto carried on the node’s value attribute. Downstream tensor-shaped consumers decode the bytes via Tensor::from_proto against their declared scalar type.

// from bytesandbrains/bb-ops/src/syscalls/structural/constant/mod.rs:25-48
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let value_attr = node
        .attribute
        .iter()
        .find(|a| a.name == "value")
        .ok_or_else(|| OpError {
            detail: "Constant requires a `value` attribute".to_string(),
            ..Default::default()
        })?;
    let Some(tensor) = &value_attr.t else {
        return Err(OpError {
            detail: "Constant `value` attribute missing TensorProto".to_string(),
            ..Default::default()
        });
    };
    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        Box::new(BytesValue(tensor.encode_to_vec())),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (constant/mod.rs:18, mapped from SYSCALL_DOMAIN).
  • Op type: Constant (constant/mod.rs:17, mapped from OP_CONSTANT = "Constant" at bb-ir/src/syscall_ids.rs:52).
  • Inputs: none.
  • Outputs: value: BytesValue (the encoded TensorProto).
  • Attributes: value TENSOR required. No default.
  • Firing: sync.

Composite

The composite module at bytesandbrains/bb-ops/src/syscalls/composite/ covers the ai.bytesandbrains.composite opset. Two ops: Bundle packs N typed slot values into one CompositeValue envelope so the framework can ship a tuple through a single port; Unbundle decomposes the envelope back into N typed outputs on the receiver. The Graph recorder exposes both as inherent methods at bb-dsl/src/graph.rs:216-295.

CompositeValue is in-process typed. Its children field holds Vec<Box<dyn SlotValue>> (bytesandbrains/bb-runtime/src/syscall/values.rs:80-85), and the hand-rolled wire codec at lines 114 through 165 only fires when the envelope crosses a Node boundary. The cross-Node path serializes each child as (type_hash, child.to_wire_bytes()) and the receiver materializes typed carriers through wire_decoder_registry() at bytesandbrains/bb-ir/src/slot_value.rs:197-206.

Bundle

Variadic input convention: the DSL stamps each input as child_{i} because input ordering carries the semantics, not the names. The op clones each input via SlotValue::clone_boxed and emits one CompositeValue on the bundle output. No bincode encode runs at this point: in-process forwarding pays one polymorphic clone per child, and the wire codec is deferred to to_wire_bytes if the envelope ever leaves the Node.

// from bytesandbrains/bb-ops/src/syscalls/composite/bundle.rs:31-51
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    if inputs.is_empty() {
        return Err(OpError {
            kind: OpErrorKind::MissingSlot,
            reason: "bundle_no_children",
            detail: "composite.Bundle: at least one child input required".into(),
        });
    }
    let mut children: Vec<Box<dyn SlotValue>> = Vec::with_capacity(inputs.len());
    for (_slot_name, value) in inputs {
        children.push(value.clone_boxed());
    }
    Ok(DispatchResult::Immediate(vec![(
        PORT_BUNDLE.to_string(),
        Box::new(CompositeValue { children }) as Box<dyn SlotValue>,
    )]))
}
  • Domain: ai.bytesandbrains.composite (bundle.rs:20).
  • Op type: Bundle (bundle.rs:22).
  • Inputs: variadic; one per child (at least one required). Each input flows through SlotValue::clone_boxed, preserving the concrete carrier type.
  • Outputs: bundle: CompositeValue (carries one typed Box<dyn SlotValue> per child).
  • Attributes: none on the runtime side. The DSL stamps two helper attributes (ai.bytesandbrains.composite.child_count INT, ai.bytesandbrains.composite.child_types STRING) for the matching Unbundle and for the compiler’s TypeSolver; see bb-dsl/src/graph.rs:236-246.
  • Firing: sync.

Unbundle

Decompose a CompositeValue into N per-child outputs named child_{i} to mirror the Bundle convention. Reads the declared child count from the ai.bytesandbrains.composite.child_count INT attribute and the denotations from ai.bytesandbrains.composite.child_types STRING; the runtime validates the envelope’s length against the declared count.

// from bytesandbrains/bb-ops/src/syscalls/composite/unbundle.rs:43-66
pub fn invoke(
    node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let composite = downcast_composite(inputs)?;
    let declared = declared_child_count(node)?;
    if composite.children.len() != declared {
        return Err(OpError {
            kind: OpErrorKind::ExecutionFailed,
            reason: "unbundle_child_count_mismatch",
            detail: format!(
                "composite.Unbundle: envelope carries {} children, declared {}",
                composite.children.len(),
                declared
            ),
        });
    }
    let mut outs: Vec<(String, Box<dyn SlotValue>)> = Vec::with_capacity(declared);
    for (i, child) in composite.children.iter().enumerate() {
        outs.push((format!("child_{i}"), child.clone_boxed()));
    }
    Ok(DispatchResult::Immediate(outs))
}
  • Domain: ai.bytesandbrains.composite (unbundle.rs:27).
  • Op type: Unbundle (unbundle.rs:29).
  • Inputs: bundle: CompositeValue (required).
  • Outputs: child_0, child_1, …, each emitted as the original concrete SlotValue carrier the sender bundled (PeerIdValue, CpuTensor, and so on). Downstream consumers downcast directly via .as_any().downcast_ref::<T>(). No BytesValue indirection, no bincode-against-denotation hop.
  • Attributes: ai.bytesandbrains.composite.child_count INT (required; unbundle.rs:33); ai.bytesandbrains.composite.child_types STRING (recorded by the DSL for the TypeSolver; unbundle.rs:36).
  • Firing: sync.

The Graph methods bundle and unbundle are the canonical authoring surface for both ops:

use bytesandbrains::types::{TYPE_BYTES, TYPE_TENSOR_F32};
use bytesandbrains::{Graph, Module};

struct CompositeDemo;
impl Module for CompositeDemo {
    fn name(&self) -> &str { "CompositeDemo" }
    fn body(&self, g: &mut Graph) {
        let params = g.input("params");
        let metadata = g.input("metadata");
        let envelope = g.bundle(&[params, metadata]);
        let parts = g.unbundle(envelope, &[&TYPE_TENSOR_F32, &TYPE_BYTES]);
        g.output("forwarded_params", parts[0].clone());
        g.output("forwarded_metadata", parts[1].clone());
    }
}

Address book

The ai.bytesandbrains.address_book opset exposes three custom ops that mutate and read the engine’s AddressBook from the data plane. Sources live at bytesandbrains/bb-ops/src/syscalls/peers/. The single-address Insert is the runtime-internal path the receiver-side merge in Engine::poll uses (bytesandbrains/bb-runtime/src/engine/poll.rs:1005-1062). InsertMany and Lookup carry the multi-address bag through the new TYPE_ADDRESS_VEC carrier; both have DSL helpers in bytesandbrains/bb-dsl/src/syscalls.rs:55-83.

AddressBook.Insert

Typed single-address insert. Adds the peer if it is new, dedupe-appends the address otherwise.

// from bytesandbrains/bb-ops/src/syscalls/peers/insert.rs:32-52
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let peer = downcast_peer(inputs)?;
    let addr = downcast_addr(inputs)?;
    let result = if ctx.peers.addresses.lookup(peer).is_some() {
        ctx.peers.addresses.register_address(peer, addr)
    } else {
        ctx.peers.addresses.add_peer(peer, vec![addr])
    };
    match result {
        Ok(()) => Ok(DispatchResult::Immediate(Vec::new())),
        Err(e) => Err(OpError {
            kind: OpErrorKind::ExecutionFailed,
            reason: "address_book_insert_failed",
            detail: format!("AddressBook::Insert: {e}"),
        }),
    }
}
  • Domain: ai.bytesandbrains.address_book (bb-ops/src/syscalls/peers/insert.rs:21).
  • Op type: Insert (bb-ops/src/syscalls/peers/insert.rs:23).
  • Inputs: peer: PeerId, addr: Multiaddress. Both required; missing or mistyped inputs return OpError.
  • Outputs: none. Control-plane mutation only.
  • Attributes: none.
  • Firing: sync; control-plane.

AddressBook.InsertMany

Typed batched insert. New peer creates an entry with ref_count = 1 seeded by the full Vec<Address>; known peer dedupe-appends each address one at a time via register_address so ref_count stays unchanged.

// from bytesandbrains/bb-ops/src/syscalls/peers/insert_many.rs:33-67
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let peer = downcast_peer(inputs)?;
    let addresses = downcast_addresses(inputs)?;
    if addresses.is_empty() {
        return Err(OpError {
            kind: OpErrorKind::ExecutionFailed,
            reason: "address_book_insert_many_empty",
            detail: "AddressBook::InsertMany: input `addresses` is empty".into(),
        });
    }
    let result = if ctx.peers.addresses.lookup(peer).is_some() {
        let mut last = Ok(());
        for addr in addresses {
            last = ctx.peers.addresses.register_address(peer, addr);
            if last.is_err() {
                break;
            }
        }
        last
    } else {
        ctx.peers.addresses.add_peer(peer, addresses)
    };
    match result {
        Ok(()) => Ok(DispatchResult::Immediate(Vec::new())),
        Err(e) => Err(OpError {
            kind: OpErrorKind::ExecutionFailed,
            reason: "address_book_insert_many_failed",
            detail: format!("AddressBook::InsertMany: {e}"),
        }),
    }
}
  • Domain: ai.bytesandbrains.address_book (bb-ops/src/syscalls/peers/insert_many.rs:22).
  • Op type: InsertMany (bb-ops/src/syscalls/peers/insert_many.rs:24).
  • Inputs: peer: PeerId, addresses: AddressVec. Both required. Empty input vec surfaces as OpError with reason address_book_insert_many_empty.
  • Outputs: none. Control-plane mutation only.
  • Attributes: none.
  • Firing: sync; control-plane.
  • DSL helper: bb_dsl::syscalls::address_book_insert_many(g, peer, addrs) at bytesandbrains/bb-dsl/src/syscalls.rs:55-66.

AddressBook.Lookup

Typed lookup. Returns the full ordered address slice on a TYPE_ADDRESS_VEC carrier. Callers needing a single entry pick one at the call site.

// from bytesandbrains/bb-ops/src/syscalls/peers/lookup.rs:29-49
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let peer = downcast_peer(inputs)?;
    let addrs = ctx
        .peers
        .addresses
        .lookup(peer)
        .map(|s| s.to_vec())
        .ok_or_else(|| OpError {
            kind: OpErrorKind::ExecutionFailed,
            reason: "address_book_lookup_miss",
            detail: format!("AddressBook::Lookup: peer {peer} not in address book"),
        })?;
    Ok(DispatchResult::Immediate(vec![(
        PORT_ADDRESSES.to_string(),
        Box::new(AddressVecValue(addrs)) as Box<dyn SlotValue>,
    )]))
}
  • Domain: ai.bytesandbrains.address_book (bb-ops/src/syscalls/peers/lookup.rs:19).
  • Op type: Lookup (bb-ops/src/syscalls/peers/lookup.rs:21).
  • Inputs: peer: PeerId required.
  • Outputs: addresses: AddressVec (the full ordered slice).
  • Attributes: none.
  • Firing: sync.
  • DSL helper: bb_dsl::syscalls::address_book_lookup(g, peer) at bytesandbrains/bb-dsl/src/syscalls.rs:72-83.

The DSL recording surface for the batched + lookup paths:

use bytesandbrains::{Graph, Module, Output};
use bb_dsl::syscalls::{address_book_insert_many, address_book_lookup};

struct PeersDemo;
impl Module for PeersDemo {
    fn name(&self) -> &str { "PeersDemo" }
    fn body(&self, g: &mut Graph) {
        let peer: Output = g.input("peer");
        let addrs: Output = g.input("addresses");
        let _trig = address_book_insert_many(g, peer.clone(), addrs);
        let _looked_up: Output = address_book_lookup(g, peer);
    }
}

Synchronization

GateDispatch

A variadic synchronization barrier. The engine’s all-inputs-ready check already gates op dispatch on every input; this op gives that semantic a name so one Trigger output can fan into many downstream consumers.

// from bytesandbrains/bb-ops/src/syscalls/sync/gate_dispatch.rs:30-39
pub fn invoke(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    Ok(DispatchResult::Immediate(vec![(
        "out".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (gate_dispatch.rs:22).
  • Op type: GateDispatch (gate_dispatch.rs:24, re-exported from bb_ir::syscall_ids::OP_GATE_DISPATCH = "GateDispatch" at bb-ir/src/syscall_ids.rs:46).
  • Inputs: variadic; the body ignores values but the engine waits on every input.
  • Outputs: out: Trigger.
  • Attributes: none.
  • Firing: sync; trigger-only.

The DSL recorder lives at bb-dsl/src/syscalls.rs:87-98:

use bytesandbrains::{Graph, Module, Output};
use bb_dsl::syscalls::gate_dispatch;

struct GateDemo;
impl Module for GateDemo {
    fn name(&self) -> &str { "GateDemo" }
    fn body(&self, g: &mut Graph) {
        let a: Output = g.input("a");
        let b: Output = g.input("b");
        let c: Output = g.input("c");
        let trig = gate_dispatch(g, &[a, b, c]);
        g.output("trigger", trig);
    }
}

Wire-chain gates

The framework’s wire chain inserts five gate syscalls around every wire.Send and wire.Recv. Authors rarely record any of these directly; the compiler’s wire-cutting passes (insert_peer_health_gate_tx, insert_backoff_gate_tx, insert_dedup_gate_rx, insert_peer_health_gate_rx, insert_backoff_gate_rx) emit them automatically. They are listed here because they show up in compiled graph dumps and surface in EngineStep::OpFailed with the labels their bodies emit. The TX chain is wire::Send → PeerHealthGateTx → BackoffGateTx → outbound; the RX chain is wire::Recv → DedupGateRx → PeerHealthGateRx → BackoffGateRx → user consumer (gates/mod.rs:1-13).

PeerHealthGateTx

Consults the PeerGovernor for the destination peer before every outbound send.

// from bytesandbrains/bb-ops/src/syscalls/gates/peer_health_tx.rs:36-64
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let peer = read_peer_attr(node).ok_or_else(|| OpError {
        detail: format!("PeerHealthGateTx missing required `{ATTR_PEER}` attribute"),
        ..Default::default()
    })?;
    let now_ns = ctx.time.scheduler.now_ns();

    match ctx
        .peers
        .governor
        .check_outbound(peer, ctx.peers.backoff, now_ns)
    {
        Decision::Allow => Ok(DispatchResult::Immediate(vec![(
            "trigger".to_string(),
            Box::new(TriggerValue),
        )])),
        Decision::Deny(reason) => Err(OpError {
            detail: format!(
                "PeerHealthGateTx denied send to peer {peer:?}: reason={}",
                reason_label(&reason),
            ),
            ..Default::default()
        }),
    }
}
  • Domain: ai.bytesandbrains.syscall (peer_health_tx.rs:27).
  • Op type: PeerHealthGateTx (peer_health_tx.rs:29).
  • Inputs: ignored.
  • Outputs: trigger: Trigger on Allow; the op fails on Deny with reason blocklisted / not_allowlisted / cooldown (peer_health_tx.rs:67-73).
  • Attributes: peer BYTES via bb_ir::wire_shape::ATTR_PEER (the destination peer’s multihash; peer_health_tx.rs:31).
  • Firing: compiler-inserted; sync; trigger-only on Allow.

BackoffGateTx

Consults the BackoffTable for the destination peer between PeerHealthGateTx and wire.Send.

// from bytesandbrains/bb-ops/src/syscalls/gates/backoff_tx.rs:36-58
pub fn invoke(
    node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let peer = read_peer_attr(node).ok_or_else(|| OpError {
        detail: format!("BackoffGateTx missing required `{ATTR_PEER}` attribute"),
        ..Default::default()
    })?;
    let now_ns = ctx.time.scheduler.now_ns();

    if ctx.peers.backoff.should_retry(peer, now_ns) {
        Ok(DispatchResult::Immediate(vec![(
            "trigger".to_string(),
            Box::new(TriggerValue),
        )]))
    } else {
        Err(OpError {
            detail: format!("BackoffGateTx held send to peer {peer:?}: reason=cooldown"),
            ..Default::default()
        })
    }
}
  • Domain: ai.bytesandbrains.syscall (backoff_tx.rs:26).
  • Op type: BackoffGateTx (backoff_tx.rs:28).
  • Inputs: ignored.
  • Outputs: trigger: Trigger on retry-eligible; op fails with reason=cooldown when still in backoff.
  • Attributes: peer BYTES (backoff_tx.rs:30).
  • Firing: compiler-inserted; sync; trigger-only on retry-eligible.

DedupGateRx

Hashes the inbound value’s wire bytes and drops repeat arrivals via InboundDedup (dedup_rx.rs:1-9).

// from bytesandbrains/bb-ops/src/syscalls/gates/dedup_rx.rs:27-54
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let (_, value) = inputs.first().ok_or_else(|| OpError {
        detail: "DedupGateRx requires one input".into(),
        ..Default::default()
    })?;

    let bytes = value.to_wire_bytes().map_err(|e| OpError {
        kind: bb_runtime::bus::OpErrorKind::ExecutionFailed,
        reason: "wire_encode_failed",
        detail: format!("DedupGateRx: input wire encode failed: {e}"),
    })?;
    let hash = fnv1a_64(&bytes);
    if ctx.net.dedup.record(hash) {
        return Err(OpError {
            detail: "DedupGateRx dropped envelope: reason=duplicate".into(),
            ..Default::default()
        });
    }

    Ok(DispatchResult::Immediate(vec![(
        "value".to_string(),
        value.clone_boxed(),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (dedup_rx.rs:22).
  • Op type: DedupGateRx (dedup_rx.rs:24).
  • Inputs: one input, any type.
  • Outputs: value, preserving the input’s concrete type on first arrival.
  • Attributes: none. The inbound src_peer comes off the engine’s ctx.current.inbound.src_peer.
  • Firing: compiler-inserted; sync; drops on duplicate.

PeerHealthGateRx

Consults PeerGovernor::check_inbound for the envelope source peer between DedupGateRx and BackoffGateRx (peer_health_rx.rs:1-13).

// from bytesandbrains/bb-ops/src/syscalls/gates/peer_health_rx.rs:33-63
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let (_, value) = inputs.first().ok_or_else(|| OpError {
        detail: "PeerHealthGateRx requires one input".into(),
        ..Default::default()
    })?;

    let Some(src_peer) = ctx.current.inbound.src_peer else {
        return Err(OpError {
            detail: "PeerHealthGateRx: no envelope source peer in runtime context".into(),
            ..Default::default()
        });
    };

    match ctx.peers.governor.check_inbound(src_peer) {
        Decision::Allow => Ok(DispatchResult::Immediate(vec![(
            "value".to_string(),
            value.clone_boxed(),
        )])),
        Decision::Deny(reason) => Err(OpError {
            detail: format!(
                "PeerHealthGateRx denied envelope from peer {src_peer:?}: reason={}",
                reason_label(&reason),
            ),
            ..Default::default()
        }),
    }
}
  • Domain: ai.bytesandbrains.syscall (peer_health_rx.rs:26).
  • Op type: PeerHealthGateRx (peer_health_rx.rs:28).
  • Inputs: one input, any type.
  • Outputs: value on Allow.
  • Attributes: none. The inbound src_peer comes off ctx.current.inbound.src_peer populated by the engine per inbound envelope.
  • Firing: compiler-inserted; sync; passes through on Allow.

BackoffGateRx

Consults the BackoffTable for the inbound source peer. The closing op on the RX chain.

// from bytesandbrains/bb-ops/src/syscalls/gates/backoff_rx.rs:29-60
pub fn invoke(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let (_, value) = inputs.first().ok_or_else(|| OpError {
        detail: "BackoffGateRx requires one input".into(),
        ..Default::default()
    })?;

    let Some(src_peer) = ctx.current.inbound.src_peer else {
        return Err(OpError {
            detail: "BackoffGateRx: no envelope source peer in runtime context".into(),
            ..Default::default()
        });
    };

    let now_ns = ctx.time.scheduler.now_ns();
    if ctx.peers.backoff.should_retry(src_peer, now_ns) {
        Ok(DispatchResult::Immediate(vec![(
            "value".to_string(),
            value.clone_boxed(),
        )]))
    } else {
        Err(OpError {
            detail: format!(
                "BackoffGateRx dropped envelope from peer {src_peer:?}: reason=cooldown"
            ),
            ..Default::default()
        })
    }
}
  • Domain: ai.bytesandbrains.syscall (backoff_rx.rs:23).
  • Op type: BackoffGateRx (backoff_rx.rs:25).
  • Inputs: one input, any type.
  • Outputs: value on retry-eligible.
  • Attributes: none.
  • Firing: compiler-inserted; sync.

Lifecycle

Three ops support the bootstrap-as-graph wiring noted in bb-ops/src/syscalls/lifecycle/mod.rs:1-8. Authors usually do not record them directly; Module::bootstrap and the install-time lifecycle plumbing put them on the graph.

LifecyclePhase

A phase-gated trigger. The engine pushes the op onto the frontier only when fire_lifecycle(phase) is called, so the body just emits a Trigger when invoked.

// from bytesandbrains/bb-ops/src/syscalls/lifecycle/mod.rs:30-39
pub fn invoke_lifecycle_phase(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    Ok(DispatchResult::Immediate(vec![(
        "trigger".to_string(),
        Box::new(TriggerValue),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:18).
  • Op type: LifecyclePhase (mod.rs:108).
  • Inputs: ignored.
  • Outputs: trigger: Trigger.
  • Attributes: phase STRING. The install path parses each node’s phase attribute and calls Engine::register_lifecycle_op (mod.rs:30-39 commentary).
  • Firing: phase-gated; trigger-only.

BootstrapDispatch

Mint a CommandId paired with a downstream BootstrapOutput so an external host can park completion of a bootstrap step on the ingress queue.

// from bytesandbrains/bb-ops/src/syscalls/lifecycle/mod.rs:45-57
pub fn invoke_bootstrap_dispatch(
    _node: &NodeProto,
    _inputs: &[(&str, &dyn SlotValue)],
    ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let cmd = ctx.allocate_command_id();
    Ok(DispatchResult::Immediate(vec![(
        "cmd".to_string(),
        Box::new(CommandIdValue(bb_runtime::ids::CommandId::from(
            cmd.as_u64(),
        ))),
    )]))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:18).
  • Op type: BootstrapDispatch (mod.rs:117).
  • Inputs: ignored.
  • Outputs: cmd: CommandId.
  • Attributes: none.
  • Firing: sync.

BootstrapOutput

Park on the CommandId minted by a matching BootstrapDispatch. Returns Async(cmd_id) so the engine routes a completion through the ingress queue once the host fires it.

// from bytesandbrains/bb-ops/src/syscalls/lifecycle/mod.rs:68-89
pub fn invoke_bootstrap_output(
    _node: &NodeProto,
    inputs: &[(&str, &dyn SlotValue)],
    _ctx: &mut RuntimeResourceRef<'_>,
) -> Result<DispatchResult, OpError> {
    let cmd_handle = inputs
        .iter()
        .find(|(name, _)| *name == "cmd")
        .map(|(_, h)| *h)
        .ok_or_else(|| OpError {
            detail: "BootstrapOutput: missing `cmd` input".into(),
            ..Default::default()
        })?;
    let cmd = cmd_handle
        .as_any()
        .downcast_ref::<CommandIdValue>()
        .ok_or_else(|| OpError {
            detail: "BootstrapOutput: `cmd` input is not a CommandIdValue".into(),
            ..Default::default()
        })?;
    Ok(DispatchResult::Async(CommandId::new(cmd.0.as_u64())))
}
  • Domain: ai.bytesandbrains.syscall (mod.rs:18).
  • Op type: BootstrapOutput (mod.rs:126).
  • Inputs: cmd: CommandId required.
  • Outputs: trigger: Trigger on completion.
  • Attributes: none.
  • Firing: async-by-CommandId.

Composing syscalls in a Module

Most syscalls do not have a dedicated DSL helper. Authors record them by pushing a typed NodeProto via g.push_node, using the attr_int, attr_string, and attr_tensor helpers (bb-dsl/src/graph.rs:675-740) for attributes. The pattern is identical across every syscall in the chapter; one example covers them all:

use bytesandbrains::proto::onnx::NodeProto;
use bytesandbrains::types::TYPE_BYTES;
use bytesandbrains::{Graph, Module};
use bb_dsl::graph::{attr_int, attr_string};

struct TimedReport;
impl Module for TimedReport {
    fn name(&self) -> &str { "TimedReport" }
    fn body(&self, g: &mut Graph) {
        // Schedule a Pulse trigger to seed the chain.
        let pulse_name = g.next_site_name();
        g.push_node(NodeProto {
            op_type: "Pulse".into(),
            domain: "ai.bytesandbrains.syscall".into(),
            output: vec![pulse_name.clone()],
            ..Default::default()
        });
        g.declare_value_info(&pulse_name, &TYPE_BYTES);

        // Read the clock when the pulse fires.
        let now_name = g.next_site_name();
        g.push_node(NodeProto {
            op_type: "Clock".into(),
            domain: "ai.bytesandbrains.syscall".into(),
            input: vec![pulse_name.clone()],
            output: vec![now_name.clone()],
            ..Default::default()
        });
        g.declare_value_info(&now_name, &TYPE_BYTES);

        // Increment a counter on the same tick.
        let metric_sink = g.next_site_name();
        g.push_node(NodeProto {
            op_type: "IncrMetric".into(),
            domain: "ai.bytesandbrains.syscall".into(),
            input: vec![pulse_name],
            output: vec![metric_sink],
            attribute: vec![
                attr_string("name", "report_pulses"),
                attr_int("delta", 1),
            ],
            ..Default::default()
        });
    }
}

The recording layer is intentionally thin. The compiler sees the same NodeProtos the runtime will dispatch. Authors who outgrow the g.push_node form can wrap the recording in a helper of their own shape; the framework asks nothing more than the canonical (domain, op_type) pair and the documented attributes.