T-minus event coordination for multi-agent systems.
A Rust library + CLI for scheduling countdown events where agents confirm readiness before an event fires. Think of it as mission control for AI agent teams β coordinating meetings, reviews, deployments, and multi-step campaigns with quorum requirements.
βββββββββββββββββββββββββββββββββββββββββββ
β CLI (main.rs) β
β schedule β confirm β defer β tick β
β status β campaign create/link/order β
βββββββββββββββββββββββββββββββββββββββββββ
β
βββββββββββββββββββββββββββββββββββββββββββ
β Engine (engine.rs) β
β βββββββββββββββ βββββββββββββββββ β
β β Events β β Campaigns β β
β β schedule β β create β β
β β confirm β β add_event β β
β β defer β β link (DAG) β β
β β tick β β execution_orderβ β
β βββββββββββββββ βββββββββββββββββ β
β β β
β ββββββββ΄βββββββ β
β β SQLite β β
β β (db.rs) β β
β βββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββ
t-minus separates event coordination from persistence. The Engine holds all business logic; db.rs handles SQLite serialization. This makes the engine testable with an in-memory database (Engine::in_memory()) while production uses a file-backed store.
- Quorum-gated firing β Events only fire when enough agents confirm. No fire-and-forget.
- Deferral cascade β Agents can defer, extending the countdown. Deferred agents reset to pending after the extension.
- Campaign DAGs β Multi-step campaigns use dependency graphs with topological sorting and cycle detection.
- Tick-based processing β A single
tick(now)call fires ready events and marks missed ones, making it easy to integrate with cron or event loops.
use t_minus::{Engine, AgentId, EventKind};
use chrono::{Duration, Utc};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut engine = Engine::new("coordination.db".as_ref())?;
// Schedule a deployment review in 2 hours with 1-hour countdown
let event = engine.schedule_event(
EventKind::Deploy,
Utc::now() + Duration::hours(2),
Duration::hours(1),
AgentId("coordinator".into()),
vec![AgentId("builder".into()), AgentId("auditor".into())],
2, // quorum: both must confirm
serde_json::json!({"crate": "fleet-midi"}),
)?;
// Agents confirm readiness
engine.confirm(event.id, &AgentId("builder".into()))?;
engine.confirm(event.id, &AgentId("auditor".into()))?;
// Process: fires events that reached quorum
let result = engine.tick(Utc::now())?;
for id in &result.fired {
println!("π Event {} fired!", id);
}
Ok(())
}# Install
cargo install --git https://github.com/SuperInstance/t-minus
# Schedule a review in 5 minutes, needs 2 confirmations
t-minus schedule review +5m --quorum 2 --attendees alice,bob,carol
# Agent confirms readiness
t-minus confirm <event-id> alice
# Check status with live countdowns
t-minus status
# Process fired/missed events
t-minus tick
# Create a deployment pipeline campaign
t-minus campaign create "release-v2"
t-minus campaign add <campaign-id> checkpoint +10m
t-minus campaign link <campaign-id> <checkpoint-id> <review-id>
t-minus campaign order <campaign-id> # topological sort| Type / Function | Signature | Description |
|---|---|---|
Engine::new |
fn new(db_path: &Path) -> Result<Self, TMinusError> |
Create engine with SQLite persistence |
Engine::in_memory |
fn in_memory() -> Result<Self, TMinusError> |
Create engine with :memory: store (testing) |
Engine::schedule_event |
fn schedule_event(&mut self, kind, scheduled_at, t_minus, organizer, attendees, quorum, payload) -> Result<TMinusEvent, TMinusError> |
Schedule a new countdown event |
Engine::confirm |
fn confirm(&mut self, event_id: Uuid, agent_id: &AgentId) -> Result<TMinusEvent, TMinusError> |
Agent confirms attendance |
Engine::defer |
fn defer(&mut self, event_id: Uuid, agent_id: &AgentId, duration: Duration) -> Result<TMinusEvent, TMinusError> |
Agent defers with a delay |
Engine::tick |
fn tick(&mut self, now: DateTime<Utc>) -> Result<TickResult, TMinusError> |
Process fired/missed events |
Engine::create_campaign |
fn create_campaign(&mut self, name: String) -> Result<Campaign, TMinusError> |
Create a named campaign |
Engine::campaign_link |
fn campaign_link(&mut self, campaign_id, from, to) -> Result<Campaign, TMinusError> |
Add dependency edge (cycle-safe) |
Engine::campaign_execution_order |
fn campaign_execution_order(&self, campaign_id: Uuid) -> Result<Vec<Uuid>, TMinusError> |
Topological sort of campaign events |
TMinusEvent::has_quorum |
fn has_quorum(&self) -> bool |
Whether confirmations meet quorum |
TMinusEvent::is_fired |
fn is_fired(&self, now: DateTime<Utc>) -> bool |
Fire time passed + quorum met |
TMinusEvent::is_missed |
fn is_missed(&self, now: DateTime<Utc>) -> bool |
Fire time passed + quorum not met |
Campaign::execution_order |
fn execution_order(&self) -> Result<Vec<Uuid>, Vec<Uuid>> |
Kahn's algorithm topological sort |
pub struct TMinusEvent {
pub id: Uuid,
pub kind: EventKind, // Meeting | Checkpoint | Review | Deploy | Custom
pub scheduled_at: DateTime<Utc>,
pub t_minus: Duration,
pub organizer: AgentId,
pub attendees: Vec<(AgentId, ResponseStatus)>,
pub quorum: usize,
pub payload: serde_json::Value,
}
pub enum ResponseStatus { Pending, Confirmed, Deferred(Duration), Missed }
pub struct Campaign { pub id: Uuid, pub name: String, pub events: Vec<Uuid>, pub dependencies: Vec<(Uuid, Uuid)> }
pub struct TickResult { pub fired: Vec<Uuid>, pub missed: Vec<Uuid> }Schedule β Pending β [Confirmed | Deferred | Missed]
β β
Quorum met? Defer β extend T-minus
β β β
Fire! Missed Re-pend
- Schedule β Organizer creates event with attendees, quorum, and T-minus countdown.
- Pending β Attendees receive invitation; all start as
Pending. - Confirm β Attendee signals readiness. When
confirmed_count() >= quorum, event is ready. - Defer β Attendee requests delay.
apply_deferral_cascade()extends T-minus by max deferral. - Tick β At fire time, events with quorum are fired and removed; events without quorum are missed.
t-minus is designed to be the coordination layer beneath an agent orchestrator. The orchestrator calls tick() periodically (e.g., every 30 seconds via a background task) and reacts to TickResult:
// In your orchestrator's event loop
loop {
let result = engine.tick(Utc::now())?;
for id in &result.fired {
let event = engine.get_event(*id)?; // will be None (already removed)
// Use your own event log or hook into the payload
}
std::thread::sleep(Duration::seconds(30).to_std()?);
}The database schema is simple and queryable:
-- List upcoming events with quorum status
SELECT id, kind, scheduled_at, quorum, attendees FROM events;
-- List campaigns
SELECT id, name, events, dependencies FROM campaigns;You can point a lightweight dashboard directly at tminus.db (SQLite supports concurrent reads).
The payload field is a serde_json::Value. Use it to store webhook URLs, Slack channel IDs, or MQTT topics. When an event fires, your integration layer reads the payload and routes notifications:
let event = engine.schedule_event(
EventKind::Deploy,
scheduled_at, t_minus, organizer, attendees, quorum,
serde_json::json!({"webhook": "https://hooks.slack.com/...", "channel": "#deploys"}),
)?;A campaign's dependency graph maps directly to CI/CD stages:
let campaign = engine.create_campaign("deploy-pipeline".into())?;
// Stage 1: Unit tests β Stage 2: Integration tests β Stage 3: Deploy
engine.campaign_link(campaign.id, test_event, integration_event)?;
engine.campaign_link(campaign.id, integration_event, deploy_event)?;The execution_order() gives the exact sequence. Cycles are rejected at link time.
cargo test # 28 integration tests
cargo test -- --nocapture # verbose
cargo build # CLI binary
cargo run -- schedule meeting +5m --quorum 2 --attendees alice,bobMIT