Cordis → Rust Mapping (Phase 0, Step 5)
Source: DeepSeek Cordis paper ("A Programming Paradigm for Spatiotemporal Composability", Aug 2026, cordiverse/cordis + cordiverse/paper) and DeepSeek Harness (deepseek-ai/deepseek-harness, TS, ~60 packages, 12 layers).
Target: ARES /opt/ares (dirmacs/ares v0.7.3, 11 crates + ares-server root, Rust 1.91, Tokio/Axum).
This doc is strategy only — no code changes. Spike crate crates/ares-cordis-core (Phase 1) must prove the theorems before adoption.
1. Core Equation
Cordis: Γ^∞ = μΓ. Γ × (Γ → Γ) × Σ — unified context that lifts effect systems (revertible mutations) and coeffect systems (typed dependency declarations) to runtime.
Rust mapping:
#![allow(unused)] fn main() { pub struct Context { // Γ — value environment (store) store: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>, // Σ impl, see §3 // Isolate table: TypeId → Symbol (scoped identity) isolate: RwLock<HashMap<TypeId, Symbol>>, // Intercept table: TypeId → override (prototype-chain) intercept: RwLock<HashMap<TypeId, Arc<dyn Any + Send + Sync>>>, // Fiber that owns this context's lifecycle fiber: Arc<Fiber>, // Parent for hierarchical lookup (prototype chain) parent: Option<Arc<Context>>, // Root for epoch-computation reachability root: Weak<Context>, } }
Context::new_root() -> Arc<Context>createsFiber::Inactive.Context::extend(&self) -> Arc<Context>creates child withparent = Some(self)(lexical scope / request scope).Context::provide::<T: Service>(&self, svc: T)insertsTypeId::of::<T>() → Arc<T>intostore; witnessed by effect.Context::get::<T: Service>(&self) -> Option<Arc<T>>walksstore→intercept→parent.store(coeFFECT lookup).Context::isolate::<T>(&self, label: &str) -> Arc<Context>creates child whoseisolate[TypeId::of::<T>()] = Symbol(label).Context::intercept::<T>(&self, override: T) -> Arc<Context>creates child whoseintercept[TypeId::of::<T>()] = override.
No unsafe. No libloading in spike (stubbed); YAGNI HMR deferred behind #[cfg(feature = "hmr")].
2. Witnessed Effects — Temporal Composability
Cordis witnessed effect function: (Γ → Γ) × (Γ → Γ) pair (do + undo) with LIFO accumulator for revertible mutations. Guarantees: if fiber disposes, all effects it applied are reverted in reverse order.
Rust:
#![allow(unused)] fn main() { pub trait Disposable: Send + 'static { fn dispose(self: Box<Self>); } pub struct EffectGuard { // LIFO accumulator — Box<dyn FnOnce() + Send> acc: Vec<Box<dyn FnOnce() + Send>>, } impl Drop for EffectGuard { fn drop(&mut self) { while let Some(undo) = self.acc.pop() { undo(); } // reverse order } } pub trait Effect: Send + Sync + 'static { fn apply(&self, ctx: &Context) -> Box<dyn Disposable>; } // Helper on Context — mirrors Cordis Context::effect impl Context { pub fn effect<E: Effect>(&self, eff: E) -> Box<dyn Disposable> { let guard = eff.apply(self); self.fiber.accumulator.lock().push({ let ptr = /* capture undo closure */; Box::new(move || { /* undo */ }) }); guard } } }
Spike verification (Phase 1, §8): temporal composability test
#![allow(unused)] fn main() { #[tokio::test] async fn temporal_composability() { let ctx = Context::new_root(); let fiber = ctx.plugin(FooService::new(), FooConfig::default()).await; ctx.provide(BarService(42)); assert_eq!(ctx.get::<BarService>().unwrap().0, 42); fiber.dispose().await; assert!(ctx.get::<BarService>().is_none()); assert_eq!(ctx.snapshot(), pre_plugin_snapshot); } }
Must hold before Phase 2.
3. Coeffect Table Σ — Spatial Composability
Cordis Σ is a TypeId-keyed table of dependency declarations (inject = ["foo"]). Fiber recomputes epoch from dependency UIDs; if epoch unchanged, no reload.
Rust anymap/typemap equivalent (hand-rolled to avoid extra dep in spike):
#![allow(unused)] fn main() { pub struct CoeffectTable { // TypeId → (TypeId, Symbol, UID) injects: HashMap<TypeId, (Symbol, String)>, // String = epoch fragment ":uid" } impl CoeffectTable { pub fn declare<T: Service>(&mut self, label: Symbol) { self.injects.insert(TypeId::of::<T>(), (label, uid_for::<T>(label))); } pub fn epoch(&self) -> String { // Monoid over concatenation, per Cordis: ":uid1:uid2:..." let mut frags: Vec<_> = self.injects.values().map(|(_, uid)| uid).cloned().collect(); frags.sort(); frags.join(":") } } }
aranymapcrate is not needed;HashMap<TypeId, Box<dyn Any + Send + Sync>>withTypeId::of::<T>()suffices.SymbolisArc<str>or&'static strforisolatelabels (e.g.,tenant:abc).- Handlers currently take
State<AppState>(17–22 fields,src/lib.rs:230). New handlers:State<Arc<Context>>+ctx.get::<T>()whereTis declared asinject. Example:#![allow(unused)] fn main() { // Before (P0 god-struct): async fn chat(State(state): State<AppState>, ...) -> Response // After (decomposed Context): async fn chat(State(ctx): State<Arc<Context>>, ...) -> Response { let exec = ctx.get::<dyn AgentExecutionService>().expect("no execution service"); exec.execute(req, &ctx).await } }
Spike verification: spatial composability
#![allow(unused)] fn main() { #[tokio::test] async fn spatial_composability() { let ctx = Context::new_root(); let consumer = ConsumerService::new(inject: vec![TypeId::of::<FooService>()]); let fid = ctx.plugin(consumer, Config::default()).await; assert_eq!(ctx.fiber_state(fid), FiberState::Inactive); // dep missing ctx.provide(FooService); assert_eq!(ctx.fiber_state(fid), FiberState::Active); // auto-reload ctx.provide(FooService::v2()); // re-provide assert_eq!(ctx.fiber_epoch(fid).prev, ":foo_v1"); assert_eq!(ctx.fiber_epoch(fid).current, ":foo_v2"); // reload triggered } }
4. Isolate & Intercept
Isolate (spatial scoping)
Cordis isolate("name", label) creates realm where provide/inject are scoped.
Rust:
#![allow(unused)] fn main() { impl Context { pub fn isolate<T: Service>(&self, label: impl Into<Symbol>) -> Arc<Context> { let child = self.extend(); child.isolate.write().insert(TypeId::of::<T>(), label.into()); child } } // Usage: per-tenant tool isolation (P10, Phase 3) let tenant_ctx = root_ctx.isolate::<dyn ToolService>("tenant:acme"); tenant_ctx.provide(TenantToolService::new(tenant_id)); // tenant_ctx.get::<dyn ToolService>() returns tenant-scoped service // root_ctx.get::<dyn ToolService>() still returns fleet service }
Intercept (prototype-chain override)
Cordis intercept("key", config) overrides a coeffect without mutating the provider.
Rust:
#![allow(unused)] fn main() { impl Context { pub fn intercept<T: Service>(&self, override_val: T) -> Arc<Context> { let child = self.extend(); child.intercept.write().insert(TypeId::of::<T>(), Arc::new(override_val) as Arc<dyn Any + Send + Sync>); child } } // Usage: per-request model pinning (P10, Phase 5) let req_ctx = root_ctx.intercept(ModelOverride { model: "gpt-4o-mini".into() }); let llm = req_ctx.get::<dyn LlmService>().unwrap(); // sees override via prototype walk }
Lookup order (must walk): intercept → store → parent.intercept → parent.store → ... → root.
5. Fiber Lifecycle (with Inertial Lock)
Cordis Fiber states: Inactive → Reloading → Active → Unloading plus Inertia lock to serialize transitions (Thm 63 — guarded withdrawal: provider does not withdraw until dependents deactivate).
Rust:
#![allow(unused)] fn main() { pub enum FiberState { Inactive { error: Option<AppError> }, Reloading { iter: Box<dyn EffectIterator>, acc: EffectAcc, committed: CommittedView }, Active { acc: EffectAcc, committed: CommittedView }, Unloading { acc: EffectAcc, committed: CommittedView, outcome: Option<AppError> }, } pub struct Fiber { state: RwLock<FiberState>, inertia: Arc<tokio::sync::Mutex<()>>, // serialize transitions acc: Mutex<EffectAcc>, // Vec<Box<dyn FnOnce() + Send>> epoch: RwLock<String>, // computed epoch injects: CoeffectTable, committed: CommittedView, // snapshot for rollback } impl Fiber { pub async fn refresh(&self) { let _guard = self.inertia.lock().await; // Thm 63 let new_epoch = compute_epoch(&self.injects); if *self.epoch.read() == new_epoch { return; } // no change self.reload().await; } async fn reload(&self) { /* iterate effects, recompute, commit or rollback */ } async fn dispose(self: Arc<Self>) { /* LIFO undo, state → Inactive */ } } }
EffectIterisBox<dyn Iterator<Item = Box<dyn Effect>> + Send>— eachService::inityields effects.CommittedViewisHashMap<TypeId, Arc<dyn Any>>snapshot taken atActiveentry; used for rollback on failure.notify(see §7) triggersFiber::refresh()via BFS over dependent fibers.
File placement: crates/ares-cordis-core/src/fiber.rs (spike) → later crates/ares-context/src/fiber.rs.
6. Epoch — Hash of Dependency UIDs
Cordis epoch is monoid ":uid1:uid2:..." (concatenation). Fiber skips reload if epoch unchanged.
Rust:
#![allow(unused)] fn main() { pub fn compute_epoch(injects: &CoeffectTable) -> String { // Monoid over concatenation per paper §4.3 let mut frags: Vec<String> = injects.uids_sorted(); if frags.is_empty() { return ":".into(); } format!(":{}", frags.join(":")) } // uid_for<T> = format!("{}:{}", std::any::type_name::<T>(), label) // Example: epoch = ":ares_llm::LlmService:tenant_acme:ares_tools::ToolService:tenant_acme" }
- Epoch is String, not hash — paper uses concatenation for debuggability; if perf matters, switch to
sha2hash later. Fiber::refreshcomparesself.epoch.read()vscompute_epoch(&self.injects); logs diff viatracing::debug!.
7. Notify — Reactive Recomputation (tokio::sync::watch)
Cordis notify is fan-out to dependent fibers. In TS Harness it's EventEmitter; in Rust it's tokio::sync::watch.
#![allow(unused)] fn main() { pub struct ReflectService { // TypeId of changed service → watch channel sender notifiers: RwLock<HashMap<TypeId, watch::Sender<()>>>, // dependency graph: provider TypeId → [dependent FiberId] dependents: RwLock<HashMap<TypeId, Vec<FiberId>>>, } impl ReflectService { pub fn notify(&self, changed: TypeId) { // BFS walk dependents let deps = self.dependents.read().get(&changed).cloned().unwrap_or_default(); for fid in deps { if let Some(sender) = self.notifiers.read().get(&changed) { let _ = sender.send(()); // fan-out, ignore closed receivers } // also trigger Fiber::refresh via task tokio::spawn({ let fiber = self.fiber_for(fid); async move { fiber.refresh().await } }); } } } // DB-backed source example (replaces 60s poll in runtime_registry.rs etc.): // On Postgres NOTIFY (or polling fallback every 60s if no NOTIFY), call: // ctx.get::<ReflectService>().unwrap().notify(TypeId::of::<RuntimeToolService>()); }
Replaces: RuntimeToolRegistry::start_background_reload (60s poll, crates/ares-tools/src/runtime_registry.rs), ProviderRegistry poll (crates/ares-llm/src/provider_registry.rs), NvidiaCatalogCache::start_background_refresh (crates/ares-config/src/nvidia_catalog.rs).
8. Events — 5 Dispatch Modes
Cordis Events: emit / parallel / serial / bail / waterfall typed bus.
Rust:
#![allow(unused)] fn main() { #[derive(Clone, Copy, Debug)] pub enum Dispatch { Emit, // fire-and-forget, no return, no error propagation Parallel, // tokio::JoinSet, collect all, fail-open (one handler error doesn't cancel others) Serial, // sequential, fail-open Bail, // sequential, fail-fast (first error aborts) Waterfall, // sequential, each handler receives previous handler's output (chained) } pub struct EventsService { handlers: RwLock<HashMap<EventId, Vec<Handler>>>, // Handler = Box<dyn Fn(Value) -> Future<Output=Result<Value>> + Send> bus: broadcast::Sender<EventEnvelope>, // tokio::sync::broadcast for cross-task fan-out } impl EventsService { pub fn on(&self, event: EventId, handler: Handler) -> Box<dyn Disposable> { self.handlers.write().entry(event).or_default().push(handler); // return Disposable that removes handler on dispose (LIFO undo) Box::new(RemoveHandler { event, idx: len - 1 }) } pub async fn dispatch(&self, event: EventId, payload: Value, mode: Dispatch) -> Result<Value> { match mode { Dispatch::Emit => { self.bus.send(envelope(payload)); Ok(Value::Null) } Dispatch::Parallel => { /* JoinSet */ } Dispatch::Serial => { /* loop */ } Dispatch::Bail => { /* loop with bail */ } Dispatch::Waterfall => { /* chain payload through handlers */ } } } } }
Mapping from TS Harness (12 layers, ~60 packages) — in Rust, one crate suffices; do not replicate layering ceremony. EventsService is a Service itself (ctx.provide(EventsService::new())), so any fiber can ctx.get::<EventsService>().unwrap().on(...).
9. Loader & Config Reconciliation (Declarative)
Cordis Loader: Entry { id, plugin, config, disabled, isolate, intercept } + EntryTree(Vec<Entry>) persisted to config/entries.json (or config/cordis-entries.toon via toon-format 0.4.1). Loader::reconcile(current, desired) diffs incrementally.
Rust (Phase 3, crates/ares-context/src/loader.rs or crates/ares-config):
#![allow(unused)] fn main() { #[derive(Serialize, Deserialize, Clone)] pub struct Entry { pub id: String, // fiber id, e.g. "tool:calculator" pub plugin: PluginId, // e.g. "ares_tools::CalculatorService" pub config: serde_json::Value,// Plugin::Config serialized pub disabled: bool, pub isolate: Option<String>, // e.g. Some("tenant:acme") pub intercept: HashMap<String, Value>, } pub struct EntryTree(pub Vec<Entry>); impl Loader { pub fn reconcile(&self, current: &EntryTree, desired: &EntryTree) { // per-field dispatch (paper §5): // id/plugin change → rebuild fiber (dispose + new) // config change → fiber.update(new_config) // disabled toggle → fiber.retire() / fiber.begin() } } }
Persistence: config/entries.json (or config/cordis-entries.toon) separate from ares.toml symlink (/opt/ares-config/ares.toml) — do not conflict (see Assumptions in plan). Reuse toon-format serialization.
10. Plugin & RegistryService
Cordis plugins are FnOnce(&Context, Config) -> Result<Disposable> or struct with apply. Registry enforces single-source discipline.
Rust (Phase 2, crates/ares-cordis-core/src/registry.rs):
#![allow(unused)] fn main() { pub trait Plugin: Send + Sync + 'static { type Config: Serialize + DeserializeOwned + Send + Sync; fn apply(&self, ctx: &Context, config: Self::Config) -> Result<Box<dyn Disposable>>; } pub struct RegistryService { fibers: RwLock<HashMap<FiberId, Arc<Fiber>>>, } impl RegistryService { pub fn plugin<P: Plugin>(&self, plugin: P, config: P::Config) -> Result<FiberId> { // check duplicate provider: no two fibers may provide same TypeId in same isolate realm // if violation: return Err(AppError::Configuration("duplicate provider for TypeId")) // else: create Fiber, store, return FiberId } } }
Static registration (preferred production): inventory/linkme (compile-time plugin set) — real surface is RegistryService::plugin (single-source discipline); inventory::submit! / linkme::distributed_slice of fn(&Arc<Context>) -> Result<FiberId, CordisError> is the future shortcut once crate count stabilizes (see crates/ares-cordis-core/src/lib.rs HMR section and Wiring task). Dynamic HMR (dev only, behind #[cfg(feature = "hmr")] off by default): libloading path that dlopens .so and calls Plugin::apply via extern "C"; if libloading ABI fragility blocks (Rust 1.91 toolchain coupling, unsafe soundness), fall back to file-watch + full fiber reload (re-read config/TOON) — 90% of value per plan, see watcher fallback below.
HMR YAGNI decision (plan Assumptions §Contingencies)
Decision: DEFER libloading HMR, keep file-watch + Fiber::reload as production path.
- Rationale:
libloading::Library::new+Symbol<extern "C">requiresunsafe, a stablerepr(C)ABI boundary, and the.soto be built with the exact same Rust toolchain (1.91). ABI drift across patches,rust-doctorsoundness flags, andBox::leakownership hazards makelibloadingtoo brittle for a generic runtime. As plan contingency states: "IflibloadingHMR proves too complex for Rust (dynamic library ABI fragility,unsafesurface), fall back to file-watch + full fiber reload without dynamic code swapping …file watcher still triggers Fiber::reload()by re-reading config/TOON, which already covers 90% of self-evolution value. Dynamic code HMR can be deferred to a later phase behind#[cfg(feature = "hmr")]without blocking the core redesign." - Fallback implemented:
crates/ares-cordis-core/src/watcher.rs(watch_many/watch_cordis_entries) usesnotify::RecommendedWatcher(debounced500 ms+100 mssettle, same asAresConfigManager::start_watching) to watchconfig/agents/*.toon(recursive) andconfig/entries.json(orconfig/cordis-entries.toonparent dir). OnModify/Createit callsReflectService::notify(tid)which BFS-walksdependentsand spawnsFiber::refresh(epoch recompute viacompute_epoch). No restart, nolibloading. LogsConfiguration hot-reloaded successfully via Cordis watch(generalizesAresConfigManager'sConfiguration hot-reloaded successfullywhich is already proven on random-port E2E39476/39120— seedocs/cordis-redesign.md§9/9b). - Stub preserved:
crates/ares-cordis-core/src/hmr.rsis#[cfg(feature = "hmr")](Cargo featurehmr = ["dep:libloading"], off by default). It showslibloading::Library::new+get::<HmrEntryFn>+ ownedHmrLibraryholder (RAII, noBox::leak) callingcordis_plugin_apply(extern "C"). Enable withcargo build --features hmrand a.sobuilt with the same toolchain. Not invoked bysrc/main.rsorReflectService—watcheris the production path. Cargo.toml:[features] hmr = ["dep:libloading"](libloading 0.8optional,notify 8.2.0always forwatcher),default = [].
11. What Is Explicitly Not Ported in Spike (updated)
Per YAGNI (Phase 1, §8) + HMR deferral above:
- ❌
libloadingHMR DEFERRED — file-watch fallbackcrates/ares-cordis-core/src/watcher.rs(notify→ReflectService::notify→Fiber::reloadvia epoch) covers 90% value without dynamic code. Dynamic code swap remains ascrates/ares-cordis-core/src/hmr.rsstub behind#[cfg(feature = "hmr")](off by default,libloading 0.8optional). See HMR decision above andlib.rsHMR section. - ❌ WASM — deferred.
- ❌ Visual layer package (~60 TS packages, 12 layers) — in Rust, one crate; do not replicate ceremony.
- ❌
ares.tomlsymlink handling — keepAresConfigManager::start_watching()as-is for Phase 2; Loader is additive.watchergeneralizes it to Cordis entries/TOON without touchingares.tomlsymlink (/opt/ares-config/ares.toml).
12. Critical Anchors (Reread Before Phases 2/4/5)
/opt/ares/src/main.rsrun_server(lines 296–889, 17 steps) → becomesroot_ctx.plugin(...).plugin(...).await(5–8 lines). Every registry/pool/cache must migrate to aService./opt/ares/src/lib.rsAppState(230–274, 17–22 fields) +base_router()→Arc<Context>,build_router(ctx: Arc<Context>)./opt/ares/crates/ares-tools/src/runtime_registry.rsstart_background_reload(60s poll,ArcSwap) → epoch-drivennotify./opt/ares/crates/ares-llm/src/provider_registry.rsArcSwap<HashMap>+NvidiaCatalogCache→LlmServicewith circuit breaker./opt/ares/src/api/handlers/admin.rs190 KB, 5,946 lines — split by domain in Phase 6.
13. Consequences & Alternatives
- If
async fn in traitcausesdynissues, useasync_traitonly for that trait and document why (Rust 1.91 floor, 1.75+ stable forasync fn in trait, butdyn Servicemay needasync_trait— preferimpl Futurereturn). - If
TypeId+HashMapproves too coarse (downcasting ergonomics), evaluateanymap/typemapcrates — but hand-rolledHashMap<TypeId, Box<dyn Any>>is sufficient for spike. - If epoch String concatenation bloats logs, switch to
sha2digest and keep:uid1:uid2only intracing::debug!.