Skip to content

std.compute.coordination

std.compute.coordination is a CPU-first :compute module for deterministic multi-agent planning. It is meant for civilian swarms: field drones, farm robots, sensor walkers, irrigation rovers, inspection crawlers, and similar systems that need bounded coordination without a central operator in every loop.

The module is intentionally not an autopilot. It does not own sensors, radio, motors, filesystem handles, or network links. Callers provide observed agent state, work zones, hard obstacles, Gaussian influence fields, and output buffers. The planner writes assignments.

Current AOT examples use a direct module import for functions and selective type imports for struct literals:

use std.compute.coordination
use std.compute.coordination {
AgentState,
Assignment,
AssignmentStats,
CoordHealth,
GaussianKernel,
Obstacle,
PlanConfig,
WorkZone,
}

The aggregate std.compute module also re-exports coordination. Use std.compute.coordination_stl when a decision summary should become an STL event:

use std.compute.coordination_stl
AreaSurface
ConstantsDECISION_FRAME_BYTES, COORD_CELL_KEY_BYTES, COORD_CELL_NEIGHBOR_COUNT, COORD_UNREACHABLE_TRAVEL_TIME
Geometrydistance_sq, distance
Gaussian fieldsGaussianKernel, gaussian_at, field_at
Agent memoryAgentState, AgentReadiness, agent_stale_ticks, agent_memory_remaining_ticks, agent_memory_valid, agent_readiness, agent_can_travel
Work modelWorkZone, Obstacle, PlanConfig, ZoneReadiness, zone_readiness, zone_blocked
Candidate factsCandidateFacts, zone_idle_ticks, travel_time, candidate_facts, candidate_status
Catalog keysCoordCell, cell_from_units, coord_cell_equal, coord_cell_neighbor, write_coord_cell_neighbors, encode_coord_cell_key, decode_coord_cell_key
Planningscore_candidate, plan_greedy, Assignment, AssignmentStats, CoordHealth, assignment_ok, assignment_rejected, assignment_conflict, count_assignments_by_status, find_assignment_by_agent, find_assignment_by_zone, empty_assignment_stats, assignment_stats, assignment_stats_rejected_count, assignment_stats_conflict_count, assignment_stats_ok_rate, assignment_stats_rejected_rate, assignment_stats_conflict_rate, assignment_stats_stale_memory_rate, assignment_stats_health
Audit summaryDecisionFrame, DecisionLedger, summarize, encode_decision_frame, decode_decision_frame, merge_decision_frame, replay_decision_frames, decision_frame_total_rows, decision_ledger_total_rows, decision_ledger_rows_per_frame, decision_frame_health, decision_ledger_health, decision_ledger_tick_span, decision_frame_assignment_rate, decision_frame_rejected_rate, decision_frame_conflict_rate, decision_frame_stale_agent_rate, decision_ledger_assignment_rate, decision_ledger_rejected_rate, decision_ledger_conflict_rate, decision_ledger_stale_agent_rate
STL bridgestd.compute.coordination_stl: event_kind_for_frame, make_event, set_effect_from_frame, decode_event_frame, merge_event_frame

plan_greedy is deterministic and allocation-free. It writes one Assignment per input agent, marks stale agents as stale_memory, avoids hard obstacles, applies soft Gaussian coverage/avoidance fields, and gives each zone at most one owner per tick.

Gaussian kernels are the shared bridge between agricultural spray planning, robot influence fields, and the Gaussian-splat work already planned for :compute.

let coverage = GaussianKernel {
x: 0.0,
y: 4.0,
sigma: 2.0,
amplitude: 3.0,
}
let strength = coordination.gaussian_at(&coverage, 0.0, 4.0)

Use positive coverage fields to pull work toward under-served soil patches, sensor gaps, or inspection zones. Use avoid fields as positive penalties for spray drift, no-spray boundaries, people, roads, ponds, or fragile crops. Hard geometry still belongs in Obstacle; Gaussian fields are soft scores, not safety barriers.

Use CandidateFacts when a caller needs preflight logs, UI explanations, or a simple filter before invoking the planner.

let facts = coordination.candidate_facts(&agent, &zone, &cfg)
if facts.can_travel do
let eta = facts.travel_time
end

candidate_facts reports distance, speed-based travel time, ticks since the zone was last served, and whether the agent has fresh memory, positive tank, and positive speed. If speed is zero or negative, travel time is reported as COORD_UNREACHABLE_TRAVEL_TIME. Use candidate_status when a caller needs the planner’s status vocabulary for one agent-zone pair before scoring:

let status = coordination.candidate_status(&agent, &zone, obstacles, obstacle_count, &cfg)
if status == .unsafe_assignment do
// hard geometry rejects this pair
end

candidate_status returns ok, stale_memory, no_candidate, or unsafe_assignment; capacity_exceeded remains a caller-buffer result from plan_greedy. Use agent_stale_ticks when a caller needs the clamped age of an observation, and agent_memory_remaining_ticks when it needs the freshness budget before checking agent_memory_valid:

let age = coordination.agent_stale_ticks(&agent, &cfg)
let remaining = coordination.agent_memory_remaining_ticks(&agent, &cfg)

Future observation timestamps clamp to zero, matching zone_idle_ticks for future last_served_tick values. Expired memory and exact-TTL memory both report zero remaining ticks. Use agent_readiness when a caller needs the priority-ordered reason an agent cannot be dispatched:

let readiness = coordination.agent_readiness(&agent, &cfg)
if readiness == .stopped do
// memory and tank are usable, but speed is zero
end

AgentReadiness is ready, inactive, stale_memory, empty_tank, or stopped. Use agent_can_travel when a caller only needs the boolean form before choosing a zone:

if coordination.agent_can_travel(&agent, &cfg) do
// memory is valid, tank is positive, and speed is positive
end

candidate_facts intentionally does not fold in Obstacle geometry. Use zone_readiness when a caller needs the reason for hard obstacle rejection before planning:

let zone_state = coordination.zone_readiness(&zone, obstacles, obstacle_count, &cfg)
if zone_state == .blocked do
// hard geometry rejects this zone before scoring
end

ZoneReadiness is ready or blocked. Use zone_blocked when a caller only needs the boolean form:

if coordination.zone_blocked(&zone, obstacles, obstacle_count, &cfg) do
// the planner will reject this zone for hard geometry
end

Hard safety remains in score_candidate and plan_greedy; zone_readiness and zone_blocked expose the same hard-obstacle predicate for preflight logs and operator UI.

CoordCell is the LMX-facing key material. It does not open or write an LMX store; it provides deterministic cell IDs and canonical key bytes for callers that maintain read-heavy field maps or zone catalogs.

let cell = coordination.cell_from_units(x_units, y_units, cell_size_units, 0)
var key: [24]u8 = undefined
if coordination.encode_coord_cell_key(key[0..], &cell) != .ok do
return 1
end

The key layout is "JCK1" magic, level as u32 little-endian, then x and y as i64 little-endian. cell_from_units uses floor division for negative coordinates, so cells stay stable across the origin. Callers own the world-units quantizer; the planner does not guess how meters, rows, map tiles, or simulation units should map to integer space.

For nearby reads, write the center cell plus its eight adjacent cells:

var cells: [9]CoordCell = undefined
let count = coordination.write_coord_cell_neighbors(&cell, cells, 9)

The order is deterministic: center, west, east, south, north, southwest, southeast, northwest, northeast. Use coord_cell_neighbor for a single offset cell and write_coord_cell_neighbors when an LMX caller wants to probe the 3x3 catalog window around an agent or zone.

var assignments: [3]Assignment = undefined
let written = coordination.plan_greedy(
agents,
3,
zones,
3,
obstacles,
1,
coverage_fields,
1,
avoid_fields,
1,
&cfg,
assignments,
3,
)

The score combines:

  • readiness gating through agent_readiness
  • hard-safety gating through zone_readiness
  • zone demand
  • zone priority
  • positive Gaussian coverage
  • negative Gaussian avoid fields
  • distance cost
  • stale-memory penalty
  • recharge priority when an agent has low tank state

Inactive and stale agents are emitted as stale_memory rows. Empty-tank and stopped agents are emitted as no_candidate rows before zone selection, so a zero-speed agent cannot receive an assignment simply because a zone scored well. If a ready agent has no safe unassigned zone because hard geometry blocks the candidate set, the row is emitted as unsafe_assignment.

The first shipped planner is deliberately simple. It is the stable scalar reference surface. More sophisticated planners can later sit beside it, but they must keep the same evidence discipline: deterministic inputs, explicit buffers, and testable assignment output.

Planner output is just caller-owned Assignment rows. The query helpers keep common UI/controller loops deterministic:

var row: Assignment = undefined
if coordination.find_assignment_by_agent(assignments, written, agent_id, &row) == .ok do
if coordination.assignment_ok(&row) do
// row.zone_id is owned by this agent for the tick
end
end

Use assignment_rejected when one row failed to produce usable zone ownership. Use assignment_conflict when a single row should be treated as a safety or capacity conflict:

if coordination.assignment_conflict(&row) do
// unsafe_assignment or capacity_exceeded
end

Use count_assignments_by_status for a single status bucket. Use assignment_stats when a dashboard or controller needs all buckets in one pass:

let stats: AssignmentStats = coordination.assignment_stats(assignments, written)
let ok_rate = coordination.assignment_stats_ok_rate(&stats)
let stale_rate = coordination.assignment_stats_stale_memory_rate(&stats)
let rejected = coordination.assignment_stats_rejected_count(&stats)

AssignmentStats records total rows plus ok, no_candidate, stale_memory, capacity_exceeded, and unsafe_assignment buckets. assignment_stats_conflict_count folds the two deconfliction/capacity buckets into one count. Use assignment_stats_ok_rate, assignment_stats_rejected_rate, assignment_stats_conflict_rate, and assignment_stats_stale_memory_rate when a UI needs normalized live-buffer signals. find_assignment_by_zone only returns successful rows, so rejected or stale rows with zone_id == 0 never look like zone ownership.

For a compact controller or dashboard signal, classify the same stats:

let health: CoordHealth = coordination.assignment_stats_health(&stats)

CoordHealth is empty, healthy, degraded, failed, or conflicted. Conflicts win over success counts; otherwise all-ok rows are healthy, partial success is degraded, and zero successful rows is failed.

Storage is outside the hot loop.

  • std.db.lsm is the write-optimized substrate for observation logs, mission event streams, and replay input.
  • std.stl.lsm_store is the audit layer: encode a DecisionFrame into Event.effect_inline, choose an intent event kind, then append through the LSM-backed STL store.
  • std.db.lmx is the read-optimized B+ tree path for future static field maps, zone catalogs, and geometry indexes. CoordCell plus the JCK1 key codec provide stable key material for those catalogs, while the store remains owned by the caller.

DecisionFrame is the handoff point:

let frame = coordination.summarize(assignments, written, cfg.tick)
var effect: [28]u8 = undefined
if coordination.encode_decision_frame(effect[0..], &frame) != .ok do
return 1
end

For callers that already use STL events, std.compute.coordination_stl performs the event shaping:

var e = coordination_stl.make_event(&frame, timestamp_nanos, epoch)

The bridge selects IntentConflicted when conflicts are present, IntentSatisfied when at least one assignment succeeded, IntentRejected when all rows were rejected, and IntentReceived for an empty frame. The module lives under std.compute, not std.stl, because core STL must not depend on a higher-profile compute planner.

The LSM-backed proof appends those generated events through std.stl.lsm_store, reads them back by id and insertion rank, decodes the inline frame, merges it into a DecisionLedger, flushes the store, and verifies post-flush readback.

That frame is small enough for STL inline effects. It records the tick, assigned count, rejected count, conflict count, and stale-agent count. Full observations and field maps should stay in LSM/LMX keyed storage; use CoordCell keys for read-heavy LMX catalogs, and use the STL event to anchor the decision summary for replay and accountability.

After reading events back from STL/LSM, decode each inline effect and merge it into a ledger:

var ledger = coordination.empty_decision_ledger()
var decoded: DecisionFrame = undefined
if coordination.decode_decision_frame(effect[0..], &decoded) == .ok do
coordination.merge_decision_frame(&ledger, &decoded)
end

DecisionLedger accumulates first tick, last tick, frame count, assigned rows, rejected rows, conflicts, and stale-agent rows. Use decision_ledger_tick_span to report last_tick - first_tick for the replay window. Empty and single-tick ledgers return zero. Use decision_frame_total_rows / decision_ledger_total_rows when a caller needs the denominator behind the rate helpers. Use decision_ledger_rows_per_frame to report average replay load per decoded frame. Use decision_frame_assignment_rate / decision_ledger_assignment_rate for bounded success-rate signals, decision_frame_rejected_rate / decision_ledger_rejected_rate for normalized rejection pressure, use decision_frame_conflict_rate / decision_ledger_conflict_rate when a dashboard needs normalized deconfliction pressure, and use decision_frame_stale_agent_rate / decision_ledger_stale_agent_rate to track memory freshness pressure while the full event stream remains in storage.

Use decision_frame_health for one decoded frame and decision_ledger_health for an accumulated replay ledger when a consumer wants the same CoordHealth vocabulary used by planner-output stats.

The focused proof gate is:

Terminal window
cd janus
./scripts/zb test-coordination
./scripts/zb test-coordination-stl
./scripts/zb test-coordination-stl-lsm

The aggregate ./scripts/zb test also depends on the same smoke harnesses.