ainl-runtime
Alpha (0.3.5-alpha) — API subject to change.
Documentation map (ArmaraOS repo): docs/ainl-runtime.md (hub: features, testing, std::sync::Mutex rationale), docs/ainl-runtime-graph-patch.md (GraphPatch / patch adapters), docs/graph-memory.md (live daemon GraphMemoryWriter vs this crate), root ARCHITECTURE.md (layering).
ainl-runtime is the Rust orchestration layer for the unified AINL graph memory stack: it coordinates ainl-memory, ainl-persona’s EvolutionEngine (shared with ainl-graph-extractor’s GraphExtractorTask), and optional post-turn extraction, with a [TurnHooks] seam for hosts (e.g. OpenFang).
It is not the Python RuntimeEngine, not the MCP server, not the AINL CLI, and not an LLM or IR parser.
Inside this repo: the only Cargo consumer is openfang-runtime, via feature ainl-runtime-engine (see docs/ainl-runtime-integration.md). Default daemon builds now include that feature; runtime path selection is still controlled per agent (ainl_runtime_engine) / env (AINL_RUNTIME_ENGINE=1), and fallback chat paths continue using GraphMemoryWriter + ainl-memory directly.
What v0.3 provides (beyond v0.2)
Turn outcomes, warnings, and phases
run_turn/run_turn_asyncreturnResult<TurnOutcome, AinlRuntimeError>— not a bareTurnResult. UseTurnOutcome::CompletevsPartialSuccess(non-fatal write failures still return a fullTurnResultplusVec<TurnWarning>tagged withTurnPhase).TurnPhase(exported at crate root; see rustdoc) covers episode edges, fitness write-back, granular graph extraction, export refresh, and session row persist:EpisodeWrite,FitnessWriteBack,ExtractionPass,PatternPersistence,PersonaEvolution,ExportRefresh,RuntimeStatePersist
- When the scheduled
GraphExtractorTask::run_passruns (extraction_intervalcadence),ainl_graph_extractor::ExtractionReportfields map to warnings as:extract_error→TurnPhase::ExtractionPasspattern_error→TurnPhase::PatternPersistencepersona_error→TurnPhase::PersonaEvolutionsoTurnOutcome::PartialSuccesscan carry multiple extraction-related warnings in one turn. The report is still attached toTurnResult::extraction_reportwhen the pass ran (see teststest_turn_phase_granularity).
Delegation depth
- Internal depth guard — nested
run_turncalls increment a counter; beyondRuntimeConfig::max_delegation_depthyou getAinlRuntimeError::DelegationDepthExceeded(hard error).TurnInput::depthremains metadata for logging only.
Session persistence (RuntimeStateNode)
- Where it lives — one upserted graph row per agent in
ainl_memory.db(node_type = runtime_state), written throughGraphMemory::write_runtime_state(backed bySqliteGraphStore::write_runtime_state).AinlRuntime::newcallsGraphMemory::read_runtime_statebefore the first turn. - Fields —
turn_countandlast_extraction_at_turn(u64) keep scheduledGraphExtractorTask::run_passaligned across process restarts.persona_snapshot_jsonholdsserde_json::to_stringoutput of the compiled persona contribution string (restore withserde_json::from_str::<String>) so the first post-restart turn can reuse the in-memory cache without re-querying persona nodes. - Failures — SQLite persist errors are non-fatal: the turn still completes, but you get
TurnOutcome::PartialSuccesswith aTurnWarningwhoseTurnPhase::RuntimeStatePersistexplains the error (cadence resets on next cold start if the row never landed). - Tests —
cargo test -p ainl-runtime --test test_session_persistence(restart simulation on a shared temp DB).
Topic relevance (MemoryContext::relevant_semantic)
- Ranking — when you pass a non-empty message into
compile_memory_context_for(Some(...))or userun_turn(which always passes the current user text),relevant_semanticis ordered withainl_semantic_tagger::infer_topic_tagsoverlap on each node’stopic_cluster/topic:tags, withrecurrence_countas a tiebreaker; empty text or no inferred topic tags falls back to high-recurrence semantic selection. Crate re-exportsinfer_topic_tagsfor tests and tooling. - Migration — see Memory context / semantic ranking below:
compile_memory_context_for(None)does not reuse the latest episode body for ranking.
Procedural patches (PatchAdapter + GraphPatchAdapter)
- [
PatchAdapter] + [AdapterRegistry] — label-keyedexecute_patch(&PatchDispatchContext); register hosts withAinlRuntime::register_adapter/ inspect names withregistered_adapters.PatchDispatchResult:adapter_outputisSomeonly on successfulexecute_patch;adapter_nameis set whenever an adapter ran (including onErr, which is logged; dispatch continues and fitness may still update). - Reference [
GraphPatchAdapter] ("graph_patch") — built-in fallback; returns a small JSON summary{ "label", "patch_version", "frame_keys" }(with declared-read safety checks). Does not compile or run AINL IR in Rust. - [
PatchDispatchContext] — node + frame passed intoexecute_patch. - Fallback dispatch — if no adapter matches the procedural label,
run_turnuses the registeredgraph_patchadapter when present (install with [AinlRuntime::register_default_patch_adapters]). - Optional host hook — [
GraphPatchAdapter::with_host] + [GraphPatchHostDispatch] forwards that same summary JSON to another runtime (e.g. Python GraphPatch).
Limits (honest): Rust GraphPatch support is host-dispatch / extraction only. Python-side GraphPatch (full memory.patch, IR promotion, overwrite guards, engine integration) remains the rich path until a future convergence milestone.
ArmaraOS integration docs (repo root): docs/ainl-runtime-graph-patch.md (patch adapters + MemoryContext), docs/ainl-runtime-integration.md (optional openfang-runtime ainl-runtime-engine chat shim: manifest / env, build, limits).
What v0.2 still provides
- [
AinlRuntime] — owns a [ainl_memory::GraphMemory] over a [SqliteGraphStore], a stateful [GraphExtractorTask], and [RuntimeConfig]. - Persona evolution (direct) — [
AinlRuntime::evolution_engine] / [AinlRuntime::evolution_engine_mut], [AinlRuntime::apply_evolution_signals], [AinlRuntime::evolution_correction_tick], [AinlRuntime::persist_evolution_snapshot], [AinlRuntime::evolve_persona_from_graph_signals] (EvolutionEnginelives in ainl-persona; the extractor is an additional signal source, not a hard gate). - Boot — [
AinlRuntime::load_artifact] → [AinlGraphArtifact] (export_graph+validate_graph; fails on dangling edges). - Turn pipeline — [
AinlRuntime::run_turn]: validate subgraph, compile persona lines from persona nodes, [compile_memory_context], procedural patch dispatch (declared-read gating + fitness EMA), record an episodic node (user message + tools), [TurnHooks::on_emit] forEMIT_TOedges, run extractor everyextraction_intervalturns. - Legacy API — [
RuntimeContext] +record_*+ [RuntimeContext::run_graph_extraction_pass] returnsResult<ExtractionReport, String>for config/memory errors only; the inner extractor still returns a report (per-phase errors live onExtractionReport, usehas_errors()).
It still does not execute arbitrary AINL IR in Rust; hosts wire LLM/tools on top of [TurnOutcome] / [MemoryContext] / patch adapter JSON.
Memory context / semantic ranking (migration)
compile_memory_context_for(None) no longer inherits previous episode text for semantic ranking; pass Some(user_message) if you want topic-aware ranking.
compile_memory_context still calls compile_memory_context_for(None) — that path now behaves like an empty user message (high-recurrence fallback for MemoryContext::relevant_semantic), not “reuse the last episode body.” run_turn always passes the current turn’s user_message into memory compilation, so embedded turn pipelines keep topic-aware semantics without extra calls.
Episodic tools and episode identity
- Canonical
tools_invoked— Before SQLite persist, the runtime runsainl_semantic_tagger::tag_tool_nameson the turn’s tool strings and stores deduplicated, sorted canonical tool values (e.g.bash,search_web). Synonyms collapse where the tagger maps them.GraphQuery::episodes_with_toolshould query those canonical names. Seedocs/ainl-runtime.md(Episodic episodes: canonical tool names). - Episode id — The id exposed on the turn result is the graph node
idfor the episode row, not alwaysEpisodicNode::turn_id. Use it forEMIT_TOedges andneighbors(...). Hub:docs/ainl-runtime.md(Episode identity).
Optional Tokio API (async feature)
Enable features = ["async"] for [AinlRuntime::run_turn_async], [TurnHooksAsync], and Tokio (spawn_blocking for SQLite / graph work).
Why std::sync::Mutex, not tokio::sync::Mutex, for graph memory? With an async mutex, calling [AinlRuntime::new] or [AinlRuntime::sqlite_store] from a Tokio worker (including #[tokio::test]) would push you toward blocking_lock or cross-thread deadlocks when the “short lock” path blocks the executor. The async path instead keeps the graph in Arc<std::sync::Mutex<GraphMemory>> and confines heavy SQLite and graph mutation to tokio::task::spawn_blocking, which matches how openfang-runtime callers already isolate blocking work.
Minimal async example (body of an async fn; Cargo: ainl-runtime = { version = "…", features = ["async"] }):
use Arc;
use SqliteGraphStore;
use ;
let store = open?;
let cfg = RuntimeConfig ;
let hooks: = new;
let mut rt = new.with_hooks_async;
let _out = rt.run_turn_async.await?;
Quick start (AinlRuntime)
[]
= "0.3.5-alpha"
use ;
use SqliteGraphStore;
let store = open?;
let cfg = RuntimeConfig ;
let mut rt = new;
rt.register_default_patch_adapters; // GraphPatch fallback for procedural patches
let _artifact = rt.load_artifact?;
// Topic-aware semantic slice: pass Some(...). None = empty ranking input (not last episode).
let _ctx = rt.compile_memory_context_for?;
let out = rt.run_turn?;
RuntimeConfig
agent_id:String(empty disables graph extraction on [RuntimeContext]; required for [AinlRuntime] turns).max_delegation_depth: max nested [AinlRuntime::run_turn] entries tracked internally (default8); exceeded depth returns [AinlRuntimeError::DelegationDepthExceeded] (not [TurnInput::depth], which is metadata only).max_steps: cap for the exploratory BFS inrun_turn(default1000).extraction_interval: runGraphExtractorTask::run_passevery N turns (0= never).
AinlRuntimeError (hard failures from run_turn)
Message(String)— store / validation / config failures; usemessage_str()for a borrowed view, orFrom<String>/?when chaining.DelegationDepthExceeded { depth, max }— nestedrun_turnpastmax_delegation_depth; useis_delegation_depth_exceeded()ordelegation_depth_exceeded()instead of matching onTurnStatus(there is no soft depth outcome).AsyncJoinError/AsyncStoreError— only with theasyncfeature, fromrun_turn_async: blocking-pool join failure or SQLite error insidespawn_blocking(graph mutex remainsstd::sync::Mutex; see above).
Persona evolution and ArmaraOS (OpenFang)
Target convergence: AinlRuntime’s evolution engine (EvolutionEngine + scheduled GraphExtractorTask::run_pass) is the intended long-term convergence point for graph-driven persona persistence in the Rust stack.
Today: Until ArmaraOS migrates to ainl-runtime as its primary execution engine, openfang-runtime’s GraphMemoryWriter::run_persona_evolution_pass is the active evolution write path for dashboard agents (~/.armaraos/agents/<id>/ainl_memory.db). Do not call AinlRuntime::persist_evolution_snapshot or AinlRuntime::evolve_persona_from_graph_signals on that same database concurrently with that pass. If you embed AinlRuntime next to openfang while openfang still owns evolution, chain AinlRuntime::with_evolution_writes_enabled(false) so those two methods return an error instead of writing.
crates.io stack (registry consumers)
If you depend on ainl-runtime = "0.3.5-alpha"** from crates.io, let Cargo pick matching releases: **ainl-memory 0.1.8-alpha**, **ainl-persona 0.1.4**, **ainl-graph-extractor 0.1.5**, **ainl-semantic-tagger 0.1.2-alpha**. Older **ainl-persona 0.1.3** cannot pair with **ainl-memory 0.1.8-alpha** in the same graph (resolver conflict). See **docs/ainl-runtime-graph-patch.md` (dependency table).
License
MIT OR Apache-2.0