Skip to content

Crate Structure

soma/
├── Cargo.toml # workspace definition
├── soma/ # facade crate (`somatize`) re-exporting the workspace
├── soma/ # facade crate (`somatize`), re-exports the rest
├── soma-core/ # types, traits, serialization, tracking schema,
│ # graph rendering (mermaid/dot/SVG + overlays)
├── soma-store/ # remote DataStore backends (S3, Zarr), off by default
├── soma-macros/ # #[derive(SomaFilter)] proc macro
├── soma-compiler/ # graph → execution plan, scheduler
├── soma-runtime/ # plan executor, events, cache, optimization,
│ # run directories (LocalTracker + RunReader)
├── soma-coordinator/ # worker registry, routing, health monitoring
├── soma-worker/ # remote execution daemon
├── soma-memory/ # KnowledgeBase + ChronosVector integration
├── soma-agent/ # ResearchStep: the research loop as a Step
├── soma-llm/ # providers (OpenAI-compatible), tools, MCP client
├── soma-mcp/ # MCP server for agent integration
├── soma-python/ # PyO3 bindings (pip install somatize)
├── notebooks/ # fourteen executed tutorial notebooks
└── docs/ # Starlight documentation (this site)

Thirteen crates. Note the published names are prefixed somatize- (somatize-core, somatize-runtime, …) — the directory names drop the prefix.

Acyclic, and read top to bottom — nothing below depends on anything above it.

soma-macros proc macros; no internal dependencies
soma-core types, traits, serialization
├── soma-store S3 / Zarr backends (feature-gated)
├── soma-compiler graph → execution plan
│ └── soma-runtime the executor, cache, effects
│ ├── soma-llm providers, tools, MCP client
│ ├── soma-worker remote execution daemon
│ │ └── soma-coordinator
│ ├── soma-agent ┐ both also on soma-memory
│ └── soma-mcp ┘
└── soma-memory experiment pool, KnowledgeBase
soma-python → core, compiler, runtime, llm, memory, store, worker
soma → the facade; re-exports all of the above

soma-macros takes soma-core as a dev-dependency, so its trybuild cases have the traits to derive against. That is not a cycle: no normal edge points back up.

The foundation. Defines all shared types, traits, and enums. Has no heavy dependencies.

Key exports:

ItemKindPurpose
FiltertraitThe fundamental computation unit (fit/forward)
SearchabletraitAuto-derived search space introspection
ValueenumTyped values flowing between filters
VirtualValueenumLazy references to values (Materialized, Cached, Deferred, Stream)
GraphstructCollection of nodes and edges
Node / EdgestructsGraph building blocks
EventenumStructured execution events (3 levels)
SearchDimensionenumFloat, Int, Categorical search parameters
SearchSpacestructAggregation of search dimensions
Study / TrialstructsOptimization orchestration
CacheKeynewtypeContent-addressable hash
CacheStoretraitK/V store interface
FilterMetastructMetadata: kind, differentiable, cacheable, stream mode
SchemastructInput/output type descriptions
SomaErrorenumError types

Derive macros:

MacroGenerates
#[derive(SomaFilter)]Filter + Searchable impls from struct fields

The proc macros: #[derive(SomaFilter)] and #[derive(SomaStep)]. The latter is what gives every step its journal key, so two structurally identical steps with different configuration do not share journal entries.

A misspelled attribute is a compile error rather than a silent no-op — #[soma(serach(...))] used to compile and do nothing. Errors carry spans, so the message points at the attribute rather than the whole derive.

Remote DataStore backends: S3 and Zarr, each behind a feature and off by default.

Split out of soma-core because each owns a tokio::runtime::Runtime and block_ons network I/O, so anything depending on soma-core inherited a runtime it never asked for. soma-core keeps LocalDataStore and its std::fs, which costs a caller nothing. See Design Decisions.

Converts Graph into ExecutionPlan. Pure logic, no I/O (except cache existence checks).

Key modules:

ModulePurpose
compiler.rsMain entry: compile(graph, …) -> ExecutionPlan; validation and gradient diagnostics
plan.rsExecutionPlan enum definition
scheduler.rsschedule(plan, workers) -> DistributionPlan — assigns nodes to workers

Cache keys are not resolved here: they depend on materialized input, so resolution happens per node at runtime. See Caching.

Executes plans. This is where computation happens.

Key modules:

ModulePurpose
graph_session.rsGraphSession — the primary orchestrator (graph + library + cache + events)
executor.rsTree-walk plan executor, and the per-node runtime cache resolution
forward.rsForward-pass helpers shared by the executors
node_catalog.rsNodeCatalog — every node (filter or step), their states, and the compiler’s NodeRegistry
event_bus.rsAsync broadcast of Event to subscribers, plus lossless sinks
cache/fs_store.rsFsActionStore — action records + BLAKE3 content-addressed blobs
cache/gc.rsValue-density eviction down to a size budget (soma cache gc)
cache/memory.rsIn-memory LRU with a byte budget
cache/local.rsFilesystem cache tier
cache/tiered.rsMemory → filesystem, with promotion
executors/study.rsStudyRunner: samples params, runs trials, replays completed ones
executors/pbt.rsPbtRunner: population-based train → evaluate → exploit/explore
executors/stream.rsChunked stream processing with state management
executors/simple.rsSingle-graph execution
sampler/mod.rsSampler trait, grid and random samplers
sampler/bayesian.rsTPE (Tree-Parzen Estimator) sampler
pruner.rsMedianPruner and PercentilePruner
runner/LocalRunner and RemoteRunner behind the Runner trait
tracking/local_tracker.rsWrites a run directory: manifest, status, artifacts, event sink
tracking/jsonl_sink.rsThe lossless events.jsonl sink
tracking/reader.rsRunReader — every aggregate a chart or report needs
tracking/summary.rssummarize() — one run directory folded into a RunSummary
tracking/head.rs.soma/HEAD: parent resolution, checkout, advance_head

A daemon process that runs on lab machines. Receives serialized plans and executes them.

Key modules:

ModulePurpose
worker.rsThe Worker itself: cache, filter library, data stores, env manager
server.rsAxum HTTP/WS server: register, heartbeat, accept plans
protocol.rsMessage types for coordinator/worker communication
ws_transport.rsWebSocket framing for plans, chunks and event streams
python_process.rsRuns user Python filters in an isolated interpreter process
env_manager.rsPer-pipeline venv/conda with incremental dependency updates
detect.rsWorker self-description (GPU, RAM, Python envs)

The experiment pool: what has been tried, what it descended from, and what came of it.

Key modules:

ModulePurpose
record.rsExperimentRecord — the experiments.jsonl line format, plus its back-compat contract
derivation.rsDerivationMove / Change — the edge between a parent run and its child
retrieval.rsBM25 + structural + recency + importance ranking; the Embedder seam
knowledge_base.rsThe KnowledgeBase trait, its analytics defaults, and MemoryKnowledgeBase
file_kb.rsAppend-only JSONL backend with offset-based refresh()
chronos_kb.rsChronosVector-backed vector index (feature chronos)

The research loop, as a Step. Proposes an experiment, runs it via Effect::Graph, reads the metrics, decides whether to continue.

Key modules:

ModulePurpose
research.rsResearchStep: the loop, its prompt, and its record-keeping
action.rsActionRunExperiment or Conclude, and nothing else

It owns no loop, no journal and no record type: those are the effect driver’s, the journal’s and somatize-memory’s. See Agents & Memory.

Model providers and tools. One OpenAI-compatible client serves ollama, HuggingFace, NVIDIA, Kimi, GLM, DeepSeek, Groq, vLLM and the rest; the catalog is TOML data, not code, so adding a provider is not a patch. Each entry carries its RetryPolicy and its Quirks, which is why there is no if id == "openai" anywhere.

Retries live in the client rather than in the step: a 429 is transport, not domain. Retry-After is honoured in both RFC forms, the wall-clock budget is checked before sleeping, and giving up reports the last failure plus the first when they differ. Retries never reach the EventBus.

Also holds Toolbox, the MCP client, ReactStep and JudgeStep. The crate is entirely blocking by decision — the effect driver runs it on threads — which is why no lock is ever held across an await in the core.

An MCP server exposing the project to an agent: 20 tools over code, knowledge, project state and the experiment pool. The rendered text is the API, so every result ends with a next: line and a run_dir:.

run_pipeline and run_study execute: a model describes a graph out of the project’s own filters — the ones list_filters lists and read_filter_source reads — and soma-mcp/src/exec.rs runs it in a Python subprocess rooted at the project directory. A config value written as {"__search__": {...}} becomes a search dimension, so the only difference between running a graph and searching it is which values were marked. Both say in their own descriptions that they execute project code.

Worker registry and placement, with a soma-coordinator binary. Workers heartbeat every 10 seconds and the coordinator reaps whoever goes quiet.

POST /submit places: it returns a worker and takes a lease rather than proxying the plan, so tensor payloads travel client→worker directly instead of through the coordinator twice. /complete releases the lease. Authentication is a bearer header compared in constant time.

Published as somatize. Re-exports the workspace so a Rust caller adds one dependency instead of eight, and carries the prelude — which reaches the effectful half too: Step, Transition, Effect, NodeOutcome, SomaStep.

PyO3 bindings. Exposes the full API to Python.

Key structure:

soma-python/
├── src/ # one module per area, not one file
│ ├── lib.rs # the module definition and its exports
│ ├── graph.rs # PyGraph — the bulk of the surface
│ ├── agentic.rs # Agent, Judge, Tool, StepCtx, the step bridge
│ ├── study.rs # PyStudy, PyTrial
│ ├── readers.rs # run directories and the experiment pool
│ ├── bridge.rs # the Python filter as a Rust Filter
│ ├── convert.rs # Value ↔ Python, natively (no JSON round-trip)
│ ├── run.rs, cache.rs, worker.rs
├── python/soma/
│ ├── __init__.py # re-exports
│ ├── _soma.pyi # the extension's surface, checked against the build
│ ├── py.typed # the package means what it says about itself
│ ├── _graph.py # class Graph(_RustGraph) — where its methods are declared
│ ├── filter.py # Filter base class, >> and | operators
│ ├── search.py # search() descriptor and FilterMeta
│ ├── chain.py # Chain/Fork lazy builder types
│ ├── builder.py # Graph materialization (_walk algorithm)
│ ├── agentic.py # patterns as functions returning a Graph
│ ├── library.py # Eval, Accumulator, Retriever, Compact
│ ├── _orchestrator.py # train/eval/forward/backward/step/materialize
│ ├── _composite.py # DifferentiableFilter (torch)
│ ├── _audit.py # gradient_audit(), AuditScope, ChannelConfig, the flags
│ ├── _study.py # class Study(_Study); search_space/apply_params
│ ├── _tracking.py # track_run
│ ├── _checkpoint.py # state/load_state/save/load, the .somack bundle
│ ├── _compile.py # CompileInfo (dict + notebook repr)
│ ├── _identity.py # cache identity: the code-fingerprint ladder
│ ├── _runs.py # RunView / RunList over run directories
│ ├── _experiments.py # reads experiments.jsonl
│ ├── _lineage.py # checkout/head/detach/reindex over .soma/HEAD
│ ├── _cache_cli.py # the `soma` CLI: cache, runs, graph, report, kb
│ ├── cli.py # the `somatize-worker` CLI
│ ├── lab.py # remote connection (connect/health/info/workers)
│ └── viz/ # optional plotly/pandas figures — the somatize[viz] extra
├── pyproject.toml # maturin build config
└── Cargo.toml

soma.Graph and soma.Study are Python subclasses of the extension classes, and their methods are declared in the class body — the implementations still live in _orchestrator, _checkpoint and the rest, but which methods exist is one list you can read. They used to be assigned onto the Rust class at import time from seven modules, which meant the surface of a graph depended on what had been imported.

The package ships py.typed and a hand-written _soma.pyi for the extension; the Python layer above it is annotated in place. Because a hand-written stub rots silently, tests/test_stubs.py compares it against the module that was actually compiled.