Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Quickstart — stand up an agent that binds a coalition

audience: ai

This quickstart brings up a single AI agent in the integrator shape (the cheapest of the four shapes from overview): the agent observes a coalition’s public surfaces, acts on what it observes, and publishes no public mosaik commits. Section agents as citizens covers the other three shapes.

The code below assumes a Rust agent. Agents whose core lives in Python or another language follow the same pattern via whichever FFI they use to reach a mosaik Network handle.

What you need before you start

  • A CoalitionConfig from the coalition operator — either the struct definition or a serialised fingerprint paired with the struct in a shared crate.
  • A ticket from each per-citizen operator whose write- side primitive the agent intends to use. Read-only agents typically need none; bidding, submitting, or publishing agents need one per write-side surface.
  • The agent’s own policy code — however it loads, evaluates, and acts. Mosaik is agnostic about this.

Step 1 — declare the CoalitionConfig

Same pattern as the integrator quickstart:

use builder::LatticeConfig;
use coalition::{CoalitionConfig, ConfluenceConfig, OrganismRef};

const SUBSTRATE: CoalitionConfig<'static> = /* from operator release notes */;

The agent holds the constant as a compile-time input, not as a runtime registry lookup. This is the fingerprint-not-registry discipline; an agent that tries to “discover” its coalition via network queries has invited unauthenticated state into its core loop.

Step 2 — build the network handle

use std::sync::Arc;
use mosaik::Network;
use builder::UNIVERSE;

let network = Arc::new(Network::new(UNIVERSE).await?);

One Arc<Network> per agent process. Clone it into each task that needs a handle.

Step 3 — open subscriptions on the surfaces the agent reads

The agent’s observation set is whatever the policy needs. Examples:

// Subscribe to a lattice's auction outcomes — the agent
// wants to track recent bids and fills before acting.
let outcomes = offer::Offer::<Bundle>::outcomes(
    &network, &SUBSTRATE.lattices[0].offer,
).await?;

// Subscribe to a shared ledger confluence — the agent
// uses aggregated refunds as input to its utility
// function.
let refunds = ledger::Shared::<Refund>::read(
    &network, &SUBSTRATE.confluences[0],
).await?;

// Read the coalition's Atlas to discover per-citizen
// endpoint hints and MR_TDs, if needed at cold start.
if let Some(atlas_cfg) = SUBSTRATE.atlas() {
    let atlas = atlas::Atlas::<CitizenCard>::read(
        &network, atlas_cfg,
    ).await?;
    // Agent may cache atlas entries and refresh them on
    // a cadence matching its policy horizon.
}

// If the coalition ships an Almanac, use it as the time
// axis across uncorrelated citizen clocks.
if let Some(almanac_cfg) = SUBSTRATE.almanac() {
    let ticks = almanac::Almanac::<AlmanacTick>::read(
        &network, almanac_cfg,
    ).await?;
    // Pull `observed_at: Vec<(MemberId, SystemTime)>`
    // from each tick for per-member timestamp brackets.
}

Step 4 — drive the agent loop

An inference-only agent typically runs a select! loop over its subscriptions, feeds observations into the policy, and dispatches the resulting action wherever the action belongs (a write-side stream, an HTTP call, an on-chain tx).

loop {
    tokio::select! {
        Some(outcome) = outcomes.next() => {
            let obs = observation_from_outcome(&outcome);
            if let Some(action) = policy.decide(obs).await? {
                dispatch(action).await?;
            }
        }
        Some(refund) = refunds.next() => {
            policy.update_belief_from_refund(&refund);
        }
        _ = shutdown_signal() => break,
    }
}

Rules for an agent at this layer:

  • Policy is pure-ish. Write the policy so the same observation sequence reproduces the same action sequence given the same weights. This is not required by mosaik; it is required for the agent’s own debuggability when the policy is later migrated into an organism (shape 2, 3, or 4).
  • Side effects are explicit. dispatch(action) is the only point where the agent changes external state; the rest is observation and belief update.
  • No cross-task shared mutable state unless deliberate. Mosaik’s subscriptions are per-handle; aggregating across them should be done in one place.

Step 5 — ticketed write actions (if any)

When the agent actively writes, it holds one ticket per write-side surface, bonded at startup against the per-citizen operator’s issuance root:

// Example: the agent places bids on a lattice's `offer`.
// The Offer::<Bundle>::bid constructor verifies the
// ticket composition against offer's TicketValidator.
let bids = offer::Offer::<Bundle>::bid(
    &network, &SUBSTRATE.lattices[0].offer,
).await?;

bids.send(bundle).await?;

Ticket lifecycle is the agent’s responsibility: rotate before expiry, re-bond if a ticket-issuance root changes, fail loud if a per-citizen operator has revoked.

What this shape does not give the agent

  • Replayable outputs. An integrator-shape agent commits nothing on the universe. Its decisions are reproducible only if it keeps its own log.
  • Public identity. Peers cannot address the agent directly; other agents know it only through the side- effects of its actions.
  • Bonding with other agents. Other agents cannot hold a ticket against this one.

Graduate to shape 2 (standalone organism) when any of those becomes a requirement. See agents as citizens.

Cross-references