Skip to content

Foundation — core, macros, facade

soma-core is the workspace’s dictionary. Every type below is referenced from at least one other crate, and nothing here executes anything. If you are rebuilding a mental model of Soma, start with the eleven contracts in the first section — those are the joints the whole system bends at.

The notation legend applies throughout. (!) marks a documented deviation, with the entry in the Debt Register.


Types, traits and serialization. The rule is no runtime, no network, no optional heavy dependency — verifiable with cargo tree -p somatize-core | grep tokio, which returns nothing.

The rule is deliberately not “no I/O”: LocalDataStore and its std::fs usage stay, because a filesystem costs a caller nothing. What was split out is soma-store, because S3 and Zarr each own a tokio::runtime::Runtime — see Distribution.

11 590 lines across 26 files · 11 traits · 45 structs · 35 enums · deps: somatize-macros

FileLinesOwns
soma-core/src/lib.rs98#![warn(missing_docs)], 27 pub mod, ~60 flat re-exports (!)
soma-core/src/any.rs22AsAny + a blanket impl — the supertrait that lets Filter and Step be downcast
soma-core/src/action.rs200The Bazel-style two-table cache model: HashAlgo, ContentHash, ActionResult, ActionCache, BlobStore
soma-core/src/cache.rs399CacheKey, CacheTier, Origin, EntryMeta, CacheStore
soma-core/src/canon.rs174Deterministic CBOR (RFC 8949 §4.2 + dCBOR floats) — canonical_bytes, hash_canonical
soma-core/src/codec.rs257The SOMA1 binary frame for Value
soma-core/src/control.rs209LoopCondition, LoopSignal, read_loop_signal, read_arm_selector
soma-core/src/effect.rs713The effect vocabulary: Effect, LlmRequest, EffectHandler, EffectResult, LlmResponse, NodeSpec, JoinPolicy, SuspendReason
soma-core/src/error.rs158SomaError (13 variants), Result<T>
soma-core/src/event.rs913Event — 30 variants across six levels (!)
soma-core/src/filter.rs299FilterKind, StreamMode, Distribution, RemoteTarget, FilterMeta, Filter
soma-core/src/fingerprint.rs512ArchitectureFingerprint, structural_similarity, pipeline_summary
soma-core/src/graph.rs1 137NodeKind, Node, EdgeKind, Edge, Graph + topo sort + mermaid/dot/text renderers
soma-core/src/keys.rs76Reserved output-store key prefixes (__state_, __input_, __input__)
soma-core/src/message.rs346Role, ContentBlock, Message, Messages
soma-core/src/node.rs224The unifying layer: NodeOutcome, NodeMeta, From<FilterMeta>, From<StepMeta>
soma-core/src/schema.rs365DataType, Schema, Dimension, compatibility predicates
soma-core/src/search.rs523Scale, SearchDimension, SearchSpace, Searchable
soma-core/src/state.rs141StateStore, MemoryStateStore
soma-core/src/step.rs370Transition, StepCtx<'a>, StepMeta, Step
soma-core/src/store/mod.rs503DataRef, StorageConfig, DataStore, LocalDataStore, StoreMeta (!)
soma-core/src/strategy.rs300TrainingStrategy + 6 satellite enums + Partition — description only
soma-core/src/study.rs1 092Direction, Objective, SearchStrategy, PruningStrategy, TrialState, Trial, Study
soma-core/src/summary.rs622RunOutcome, NodeCost, FlagCount, RunConclusion, RunSummary
soma-core/src/svg.rs355Graph::to_svg — self-contained SVG, longest-path layering. Declares no public type (!)
soma-core/src/tool.rs112ToolSpec — the MCP wire shape
soma-core/src/tracking.rs476RunKind, RunState, RunManifest, EventEnvelope, EventSink, Tracker
soma-core/src/util.rs102timestamp_id, extract_json, truncate
soma-core/src/value.rs267Value — 6 variants, all Arc-backed
soma-core/src/viz.rs215NodeStatus, NodeOverlay, GraphOverlay — pure data for the renderers
soma-core/src/virtual_value.rs410VirtualValue, ValueStatus

Eleven traits. None declares an associated type or a generic parameter — a uniformity that makes every one of them dyn-able, which is why the whole system can swap backends at runtime without a single generic bound leaking into a signature.

pub trait Filter: AsAny + Send + Sync {
fn config_hash(&self) -> CacheKey; // required
fn fit(&self, x: &Value, y: Option<&Value>) -> Result<Value>; // required
fn forward(&self, x: &Value, state: &Value) -> Result<Value>; // required
fn meta(&self) -> FilterMeta; // required
fn composite_fit(&self, peers, x, y) -> Option<Result<…>> { None } // provided :148
}

The central abstraction: fit() learns state, forward() transforms, and both are independently cacheable. composite_fit exists for differentiable groups that must train jointly — returning None means “I have nothing special to say”, so a normal filter never mentions it.

ImplementorCrateDistinguishing behaviour
PyFilterBridgesoma-python/src/bridge.rs:224Calls into a live Python object; identity delegated to soma._identity
SubprocessFiltersoma-worker/src/python_process.rs:1025Delegates over a pipe to an out-of-process interpreter

Roughly 40 further implementations exist in tests and fixtures. (!) The trait mixes computation with cache identity — D-91.

pub trait Step: AsAny + Send + Sync {
fn config_hash(&self) -> CacheKey;
fn meta(&self) -> StepMeta;
fn poll(&self, ctx: &StepCtx<'_>) -> Result<Transition>;
}

Filter’s sibling, and the whole agentic layer in one method. poll is synchronous and re-entrant: it returns a Transition describing what it wants, and a driver performs the effects and calls it again with the results. The driver loop is sketched in the doc at soma-core/src/step.rs:13.

The consequence that matters: a step holds no hidden state between turns. Everything it knows arrives through StepCtx::history (soma-core/src/step.rs:128), which is what makes journal replay exact rather than approximate.

ImplementorCrate
ReactStep, LlmStep, JudgeStepsoma-llm/src/steps.rs:214, :491, :586
PyStepBridgesoma-python/src/agentic.rs:883 — duck-typed: any object with poll(ctx)
ResearchStepsoma-agent/src/research.rs:260
pub trait CacheStore: Send + Sync {
fn get / put / exists / remove / metadata // required
fn put_with_origin(&self, key, value, origin) -> Result<()> // provided :222
fn put_computed(&self, key, value, origin, compute, deterministic) // provided :232
fn tier(&self) -> CacheTier { CacheTier::Memory } // provided :249
fn get_located(&self, key) -> Result<Option<(Value, CacheTier)>> // provided :259
}

A template method: the defaults discard the extra information so a minimal backend needs only five methods, and richer ones override. Implementors: MemoryCache, LocalCache, TieredCache, FsActionStore — all in soma-runtime.

DataStoresoma-core/src/store/mod.rs:208

Section titled “DataStore — soma-core/src/store/mod.rs:208”

put / get / exists / remove / config required; get_rows and meta provided by downloading everything and slicing locally (:227, :234). ZarrStore is the only implementor that overrides them, which is exactly the point — a chunked backend can serve a row range without the whole array.

Implementors: LocalDataStore (here, :262), S3DataStore, ZarrStore (Distribution).

EffectHandlersoma-core/src/effect.rs:262

Section titled “EffectHandler — soma-core/src/effect.rs:262”
pub trait EffectHandler: Send + Sync {
fn handles(&self, effect: &Effect) -> bool;
fn perform(&self, effect: &Effect) -> Result<EffectResult>;
}

Chain of responsibility, with the contract written into the doc at :256: “Handlers are tried in order; the first that claims an effect wins.” Implementors: LlmHandler (soma-llm/src/lib.rs:205), Toolbox (soma-llm/src/tools.rs:168), GraphHandler and SleepHandler (soma-runtime/src/effects/).

ActionCache and BlobStoresoma-core/src/action.rs:134, :143

Section titled “ActionCache and BlobStore — soma-core/src/action.rs:134, :143”

The two halves of the persistent cache, deliberately separate because they have different lifetimes: action records are kept forever, CAS blobs are evictable. Both are implemented by FsActionStore (soma-runtime/src/cache/fs_store.rs:281, :255) — one type, two roles.

(!) Neither is re-exported from soma-core/src/lib.rsD-84.

get / set / remove / clear / keys. One implementor (MemoryStateStore, :68), and (!) no injection site anywhere — D-36.

EventSink and Trackersoma-core/src/tracking.rs:243, :255

Section titled “EventSink and Tracker — soma-core/src/tracking.rs:243, :255”

EventSink::record(&self, event: &Event) with a defaulted no-op flush. Tracker is 7 required methods over a run directory. One implementor each: JsonlEventSink and LocalTracker, both in soma-runtime/src/tracking/.

Searchablesoma-core/src/search.rs:321

Section titled “Searchable — soma-core/src/search.rs:321”

The one non-object-safe trait here: search_space() has no receiver and from_sample carries where Self: Sized. Its only implementations are macro-generated (soma-macros/src/lib.rs:173) — there is not one hand-written impl in the workspace, which is the strongest possible evidence that the derive is the right interface.

fn as_any(&self) -> &dyn Any, with a blanket impl<T: Any> AsAny for T, used only as a supertrait of Filter and Step so that a concrete type can be recovered from a trait object. Exactly three downcast sites exist workspace-wide: soma-python/src/bridge.rs:355, soma-worker/src/worker.rs:158 and :482.

NameRoleKey fieldsfile:line
CacheKeySHA-256 newtype[u8; 32]soma-core/src/cache.rs:18
ContentHashBLAKE3/SHA-256 CAS addressalgo, digest (Copy)soma-core/src/action.rs:52
ActionResultOne cached action recordkey, outputs, bytes, compute_ms, deterministic, origin, timestampssoma-core/src/action.rs:110
EntryMetaCache entry metadatakey, size, timestamps, ttl, originsoma-core/src/cache.rs:185
FilterMetaWhat a filter says about itselfname, kind, cacheable, differentiable, deterministic, stream_mode, distribution, 2× schemasoma-core/src/filter.rs:73
StepMetaWhat a step says about itselfname, max_turns (24), journal, 2× schema, distributionsoma-core/src/step.rs:177
NodeMetaThe unified metadataname, effectful, kind, cacheable, deterministic, differentiable, distribution, 2× schemasoma-core/src/node.rs:72
StepCtx<'a>Everything a step seesnode_id, run_id, input, turn, results, historysoma-core/src/step.rs:115
LlmRequestA model call, as datamodel, messages, system, max_tokens, tools, effort, schemasoma-core/src/effect.rs:144
LlmResponse…and its replymessage, stop_reason, usage, modelsoma-core/src/effect.rs:330
UsageToken accountingu64, Copy, impl AddAssignsoma-core/src/effect.rs:411
NodeSpecA spawn targetruns, input, labelsoma-core/src/effect.rs:450
ToolSpecMCP tool descriptionname, description, inputSchemasoma-core/src/tool.rs:16
Message / MessagesConversationrole + Vec<ContentBlock>; Messages is a transparent newtypesoma-core/src/message.rs:137, :189
Schemadtype + shapedtype, shape: Option<Vec<Dimension>>, 9 named constructorssoma-core/src/schema.rs:104
Node / Edge / GraphThe user-facing structureGraph = nodes + edges + optional strategysoma-core/src/graph.rs:68, :229, :293
StudyA search, its trials and its provenance15 fields (!)soma-core/src/study.rs:319
TrialOne point in the spaceid, params, state, metrics, timingssoma-core/src/study.rs:235
SearchSpaceDimensions + frozen valuesdimensions, frozensoma-core/src/search.rs:172
ArchitectureFingerprintStructure-only identitydigest, node tokens, edge refs, config hashessoma-core/src/fingerprint.rs:36
RunManifestWhat a run was20 fields (!)soma-core/src/tracking.rs:93
EventEnvelopeA sequenced, timestamped eventseq, ts, flattened Eventsoma-core/src/tracking.rs:225
RunSummary / RunConclusionThe deterministic run story17 + 9 fieldssoma-core/src/summary.rs:331, :157
NodeOverlay / GraphOverlayPer-node render annotationsstatus, duration, tier, flagssoma-core/src/viz.rs:33, :53
LocalDataStoreFilesystem DataStoreconfig, base_pathsoma-core/src/store/mod.rs:241
MemoryStateStoreIn-process StateStoreMutex<HashMap<String, Arc<Value>>>soma-core/src/state.rs:46
PartitionNodes → a remote targetnode_ids, targetsoma-core/src/strategy.rs:107

GraphOverlay’s doc (soma-core/src/viz.rs:6) states the design rule behind this whole file group: it is “pure data — computed elsewhere and passed in, so rendering stays a dependency-free data→string transform”. The same argument appears in soma-core/src/summary.rs:5. That is why soma-core can render a graph to SVG without pulling in a rendering library.

The five in bold are the ones worth memorizing.

NameVariants!Why that choicefile:line
ValueTensor{values, shape}, Text, Json, Bytes, Object, Empty — all Arc-backedyesData enum; new payload kinds must not break consumers. Arc makes Clone O(1)soma-core/src/value.rs:15
NodeOutcomeProduced(Value), HandOff{target, carry}, Paused{turn, reason}noControl flow. “A wildcard arm here is a silent wrong answer” (:37)soma-core/src/node.rs:44
TransitionAwait(Vec<Effect>), Spawn{specs, join}, Goto{target, carry}, Suspend{reason}, Done(Value)noSame reason (:38)soma-core/src/step.rs:43
EffectLlm, Tool{name, args}, Graph{graph, input, mode}, Sleep, Custom{kind, payload}yesData — a handler that does not claim an effect ignores itsoma-core/src/effect.rs:35
NodeKindFilter{filter_name}, SubGraph{graph}, Loop{max_iterations, until}, Branch{arms}, Step{step_name}yesFive structural kinds; every behaviour is librarysoma-core/src/graph.rs:26
SomaError13 variants incl. Suspended, Pruned, SchemaMismatch, Execution, Other(String)yesSee what is healthy — 3 error enums workspace-widesoma-core/src/error.rs:14
Event30 variants, six levels (!)yesAlso the JSONL wire format — D-05soma-core/src/event.rs:54
StreamModeFixedState, Evolving, BarriernoControl flow, deliberate (:32)soma-core/src/filter.rs:37
FilterKindStateless, Trainable, Opaqueyessoma-core/src/filter.rs:16
Distribution / RemoteTargetLocal/Remote(t)/Any; WorkerId/Tagno(!) shadowed by Node.target: Option<String>D-52soma-core/src/filter.rs:53, :64
EffectResultLlm, Tool{output, is_error}, Graph, Node, Slept, Custom, Failed{message}yessoma-core/src/effect.rs:278
StopReasonEndTurn, MaxTokens, ToolUse, Refusal{category}yesMaxTokens and Refusal are errors in ReactStep, not empty repliessoma-core/src/effect.rs:392
JoinPolicyAll, AllSettled, Firstyessoma-core/src/effect.rs:482
SuspendReasonHuman{prompt, schema}, External{token}yessoma-core/src/effect.rs:507
GraphEffectModeForward, FityesOnly Forward filter-only sub-graphs are puresoma-core/src/effect.rs:134
LoopConditionBodyTerminal, WhenSignaled(NodeId), ExhaustyesAdjacently tagged — forced by the newtype variant (:21)soma-core/src/control.rs:28
EdgeKindData, Controlnosoma-core/src/graph.rs:220
DataTypeFloat64, Float32, Int64, Bool, Utf8, Bytes, Json, Messagesyessoma-core/src/schema.rs:12
DimensionFixed(usize), Dynamic(String)nosoma-core/src/schema.rs:115
Role / ContentBlocksystem/user/assistant; text/tool-use/tool-resultyessoma-core/src/message.rs:22, :59
VirtualValueMaterialized, Cached{key}, Deferred{producer, key}, Stream{source}yesLazy reference — what the executor actually storessoma-core/src/virtual_value.rs:26
DataRefLocal, S3, Cached, Stream (!), Inline, Zarryessoma-core/src/store/mod.rs:98
StorageConfigLocal, S3, Zarryessoma-core/src/store/mod.rs:161
CacheTier / Originmemory/local/remote (!); computed/ingested/streamed (!)nosoma-core/src/cache.rs:150, :161
TrainingStrategyLocal, DataParallel, ModelParallel, Federated, PopulationBased, CustomyesDescription only — execution lives in soma-runtimesoma-core/src/strategy.rs:22
GradientAggregation, CommunicationProtocol, FederatedAggregation, ClientSelection, ExploitStrategy, ExploreStrategysatellites of the aboveyessoma-core/src/strategy.rs:89:200
SearchStrategyGrid, Random, Bayesian, Hyperband (!), MultiObjective (!)nosoma-core/src/study.rs:113
PruningStrategyNone, Median, Percentile, Hyperband (!)nosoma-core/src/study.rs:179
SearchDimensionFloat, Int, Categorical, Conditional{parent, dimension}yesRecursive through Boxsoma-core/src/search.rs:36
TrialState / Direction / Scale / Scalarizersearch vocabularymixedsoma-core/src/study.rs:210, :15, search.rs:17, study.rs:48
RunKind / RunState / RunOutcome / NodeStatustracking vocabularyyesRunKind has a #[serde(other)] catch-allsoma-core/src/tracking.rs:27, :46, summary.rs:27, viz.rs:20
HashAlgoBlake3, Sha256yessoma-core/src/action.rs:32
LoopSignal / ValueStatus / StreamFormat (!)small vocabulariesmixedcontrol.rs:46, virtual_value.rs:65, store/mod.rs:145

The #[non_exhaustive] policy is the thing to take away. It is not applied uniformly, and the non-uniformity is the design: data enums get it so a consumer need not have an opinion about a new variant; control-flow enums every consumer must decide over — NodeOutcome, Transition, StreamMode — deliberately do not, so that adding a variant breaks every match and forces the decision. The reason is stated in each doc comment.

Graph soma-core/src/graph.rs:293
├──◆ Vec<Node> ──◆ [enum] NodeKind
│ ├──◆ Box<Graph> SubGraph — recursive
│ └──◆ LoopCondition ──▷ NodeId
├──◆ Vec<Edge> ──◆ [enum] EdgeKind
└──? Option<TrainingStrategy> ──◆ { GradientAggregation
| Vec<Partition> + CommunicationProtocol
| FederatedAggregation + ClientSelection
| ExploitStrategy + ExploreStrategy }
«trait» Filter ──▷ FilterMeta ─┐
«trait» Step ──▷ StepMeta ─┴──▷ NodeMeta « the adapter »
soma-core/src/node.rs:72
From<FilterMeta> → effectful: false
From<StepMeta> → effectful: true, cacheable: false, deterministic: false
Step::poll ──▷ [enum] Transition
├──◆ Vec<Effect> ──◆ LlmRequest ──◆ Messages ──◆ Vec<Message>
│ ──◆ Vec<ContentBlock>
│ ──◆ Vec<ToolSpec>
│ ──◆ Box<Graph> « Effect::Graph — a pipeline as a tool »
├──◆ Vec<NodeSpec> + JoinPolicy
├──◆ NodeId + Value
├──◆ SuspendReason
└──◆ Value
CacheKey ◁── CacheStore keys, ActionResult.key, DataRef::Cached,
VirtualValue::{Cached, Deferred}
ActionResult ──◆ BTreeMap<String, ContentHash> ──◆ HashAlgo
Study ──◆ SearchSpace ──◆ Vec<SearchDimension> ──◆ Box<SearchDimension> « Conditional »
├──◆ SearchStrategy, PruningStrategy, Vec<Objective> ──◆ Direction
└──◆ Vec<Trial> ──◆ TrialState, Vec<MetricRecord>
RunSummary ──◆ RunConclusion ──◆ RunOutcome, NodeCost, Vec<FlagCount>,
TrialSummary, AgentCost
└──? Option<ArchitectureFingerprint> ──◆ Vec<EdgeRef>
From → Tofile:line
FilterMeta → NodeMetasoma-core/src/node.rs:116
StepMeta → NodeMetasoma-core/src/node.rs:132
NodeMeta → FilterMeta (lossy, an inherent method, not From)soma-core/src/node.rs:160
Vec<f64> → Value (1-D tensor)soma-core/src/value.rs:185
serde_json::Value → Value::Jsonsoma-core/src/value.rs:195
Vec<Message> → Messages, IntoIterator for Messagessoma-core/src/message.rs:253, :259
Value → VirtualValue::Materializedsoma-core/src/virtual_value.rs:229
io::Error → SomaError::Io (#[from])soma-core/src/error.rs:108
AddAssign for Usagesoma-core/src/effect.rs:434

NodeMeta → FilterMeta is deliberately not a From impl: it drops the effectful bit, and making the lossy direction inconvenient is the point.

Symbolfile:lineWhy you would look
CacheKey::for_state / for_outputsoma-core/src/cache.rs:18The whole caching model in two functions
CacheKey::absorbsoma-core/src/cache.rs:86Exhaustive match on Value by design (:123) — the one place a new Value variant must be handled
canonical_bytessoma-core/src/canon.rsWhy two structurally equal configs hash the same
Graph::topological_sortsoma-core/src/graph.rs:450(!) sorts ascending then pops — roots come out descending
Graph::validatesoma-core/src/graph.rsCycle detection
Graph::contains_stepssoma-core/src/graph.rs:518Decides whether an Effect::Graph can be pure
Effect::is_pure / cache_keysoma-core/src/effect.rs:80:127The journal’s keying rule
LlmResponse::reject_non_answerssoma-core/src/effect.rsWhy length and content_filter are errors
read_loop_signal / read_arm_selectorsoma-core/src/control.rsHow data-dependent control flow reads its input
RunConclusion::render_headlinesoma-core/src/summary.rs:212The templated, deterministic run story
  • Strategy via dyn — every backend seam: CacheStore, DataStore, StateStore, EffectHandler. → Patterns
  • AdapterNodeMeta erases the Filter/Step distinction; the module doc at soma-core/src/node.rs:1 is the clearest statement of the design in the repo.
  • Chain of responsibilityEffectHandler::handles.
  • Interpreter / commandEffect describes work; the runtime performs it.
  • State machine / trampolineStep::poll → Transition, deliberately avoiding async fn in a trait.
  • CompositeNodeKind::SubGraph, SearchDimension::Conditional.
  • Template methodCacheStore and DataStore defaults.
  • NewtypeCacheKey([u8; 32]), Messages(Vec<Message>), ContentHash.
  • Flyweight / COW — every Value payload is Arc-wrapped, so Clone is a refcount bump.
  • Memento / journalEffect::cache_key as the journal key; StepCtx::history explicitly replacing hidden step state.
  • Null objectValue::Empty, ExecutionPlan::Empty, TrainingStrategy::Local.
  • Data-transfer objectGraphOverlay, RunSummary, RunConclusion.

Notably absent: typestate, PhantomData, and generic type parameters on any public trait. That absence is what keeps everything dyn-able.

  • D-05 Event — 30 variants, six concerns · D-06 wide structs with no builder
  • D-23 a serialization failure collides cache keys
  • D-33 to_plain_json contradicts its contract · D-35 nine never-constructed variants
  • D-51 four style tables keyed by magic strings · D-52 two placement mechanisms · D-53 enums shadowed by strings · D-56 NodeId = String
  • D-15 two duration formatters that disagree · D-17 four renderers
  • D-84 asymmetric re-exports · D-91, D-92, D-94, D-95

Two derive macros, and one job: make it impossible for a field to escape a cache key silently.

607 lines in one file · 0 traits · 0 public types · 2 proc macros · deps: syn, quote

Macrofile:lineGenerates
#[derive(SomaFilter)]soma-macros/src/lib.rs:30config_hash() from the canonical CBOR of every field, plus impl Searchable when #[soma(search(…))] attributes are present
#[derive(SomaStep)]soma-macros/src/lib.rs:533The same config_hash() for a Step — “what gives every step its journal key”

Supporting internals: StructAttrs (:201), FieldAttrs (:210), SearchAttrs (:215), parsers at :231 / :301 / :384, codegen at :403 (generate_search_dimension) and :473 (generate_from_sample).

#[soma(cache_version = "…")] lets an implementor bump the key deliberately when the behaviour changes without the fields changing — the escape hatch that makes field-derived identity safe.

Filter identity is the foundation of the whole cache, and the two languages solve it differently:

  • Rust — canonical CBOR of the field list, plus cache_version. Adding a field changes the key automatically.
  • Python — qualname + canonical config + a source-hash ladder (_cache_versioninspect.getsource → cloudpickle with a warning), in soma-python/python/soma/_identity.py:124. An unhashable config raises CacheConfigError, never a silent key.

(!) The generated code panics on a non-CBOR-serializable field, from inside config_hash(), which the executor calls on every node — D-29.

Note the dependency direction: soma-macros has a dev-dependency back on soma-core, path-only and deliberately unversioned. The comment at soma-macros/Cargo.toml:16 explains the publish cycle that would otherwise result.


124 lines in one file. Nine crate re-exports (core, compiler, runtime, memory, worker, agent, llm, coordinator, macros), a feature-gated store, and a prelude with 22 re-exports at soma/src/lib.rs:93.

(!) It covers 10 of 13 crates — somatize-mcp and somatize-python are workspace members it does not depend on — and it hand-rolls any(s3, zarr) with two complementary #[cfg] attributes at soma/src/lib.rs:77. See D-83. The comments at soma/src/lib.rs:64 and :82 record two previous instances of exactly this gap being found and fixed, which suggests the shape of the fix matters more than the fix.