Orchestrate-Core Event ABI
Status: Design for the Event-ABI round of the orchestrator-as-plug-in work
(noetl/ai-meta#108, under the
Server Dissolution program).
The renderer, playbook model, command builders, and condition evaluator already
compile to both native and wasm32-unknown-unknown from noetl-orchestrate-core.
The last two modules — state and orchestrator/evaluate — consume the event
log, and the event type is the one thing that can't follow them as-is.
The boundary is unavoidable, and that's the point
db::Event is sqlx::FromRow — native-only. A db-free, wasm-safe core type can
never be FromRow, so it can never be query_as'd. Therefore a
db-row → core-event conversion exists no matter what — the only question is
whether the core event is the single event shape everywhere (a system-wide
refactor) or the read-shape at one boundary (this round). The conversion is the
same and cheap either way; the system-wide unification buys nothing on the
boundary and pre-empts the WAL event-shape design. So we take the boundary.
The core event type
The drive reads a clean subset of db::Event — never the DB-only fields (id
serial, catalog_id, parent_event_id, node_id, node_type, worker_id):
// noetl-orchestrate-core::event — pure, wasm-safe (chrono + serde_json, no sqlx)
pub struct Event {
pub event_id: i64, // snowflake — ordering
pub execution_id: i64,
pub event_type: String,
pub node_name: Option<String>,
pub status: String,
pub context: Option<serde_json::Value>,
pub result: Option<serde_json::Value>,
pub meta: Option<serde_json::Value>,
#[serde(alias = "created_at")]
pub timestamp: DateTime<Utc>, // event-sourced, never now()
pub parent_execution_id: Option<i64>,
pub attempt: Option<i32>,
}
Named to converge. The field set deliberately mirrors EventEnvelope (the
CQRS materializer's shape — already event_id/execution_id/event_type/
node_name/status/context/result/timestamp), and it lives under the
canonical name core::event::Event. So when #104
makes the JetStream WAL record the system's one true event, this isn't a fourth
shape to reconcile — it's the seed to promote to the WAL event. The
unification rides #104; it is explicitly not this round.
The conversion
- Server:
impl From<&db::Event> for core::event::Event— a field copy. Thecontext/resultValues are cloned, but references-in-state + block-b bounding already keep them small (events carrynoetl://references, not bulk payloads), so the clone is cheap.db::Eventand its 8query_assites are untouched. - In-process path (now):
trigger_orchestratorloadsdb::Events → maps toVec<core::event::Event>→ callsevaluate. One bounded map per drive. - Plug-in path (later): the bounded slice serializes over the #105 data-plane ABI — serde_json/CBOR bytes first, Arrow columnar when it pays. Bounded slice + refs-not-bulk keep it small.
Determinism — what earns the shadow-diff
The plug-in cutover is gated by a shadow-diff: run the plug-in alongside the
in-process orchestrator and compare emitted commands. For that to mean anything,
evaluate must be a pure function of (events, playbook) up to generated
ids. The drive today has ~11 Utc::now() calls (3 state fallbacks, 8
orchestrator) plus HashMap iteration. The round's audit:
- Timestamps come from events, never
now(). Thecreated_at-unreadable fallbacks become hard errors or are event-sourced. - Ordering is deterministic. HashMap iteration that feeds command ordering
becomes
BTreeMap/sorted — also a correctness win. - The diff compares content, not generated ids. Two runs legitimately mint
different
command_id/timestamps; the shadow-diff compares(execution_id, step, tool_kind, rendered input/context)and normalizes the freshly-generated ids.
The slices
core::event::Event+From<db::Event>+ the determinism audit — define the type, the boundary conversion, and make the drive event-sourced/ordered.state/orchestratorkeep usingdb::Eventuntil they move; this slice is additive + green.- Move
state(WorkflowState) into the core — switch it tocore::event::Event. - Move
orchestrator/evaluateinto the core — the top; it now has every dependency (renderer, model, commands, evaluator, state, event) in the core. - Then the plug-in: the data-plane ABI + a
command_emithost capability + the kernel scheduler + the shadow-diff → the livesystem/orchestrateplug-in.
Related
- Server Dissolution and the Global Grid — the program; this is step 2's last mile.
- CQRS Write-Path Cutover + Event WAL — where the canonical event (the WAL record) gets designed; this event type is its seed.
- System Pool + WASM Plug-ins — the data-plane ABI + capability ring the plug-in uses.