Architecture overview
System Overview
Valem accepts a declarative model spec (generated by an LLM or authored by hand) and executes it as a live, reactive computation system over a hierarchical JSON state graph. When base state mutates, the runtime propagates changes through a precompiled dependency graph, re-evaluates only the affected derived fields in topological order, enforces constraints, emits effects, and records evaluation traces. All execution is deterministic and reproducible.
Effects are the functional-core / imperative-shell seam: the pure core never performs I/O — it describes an effect as data (an EffectRequest), an executor in the shell (valem-api) performs the I/O (HTTP, an LLM call, a scheduled timer, or a command surfaced to the caller), and the result re-enters as an ordinary mutation (the “fold-back”). Because every fold-back is a logged mutation, persisted state stays deterministic and replay never re-executes I/O. This is what turns Valem from a reactive calculator into a runtime for stateful AI agents, human-in-the-loop workflows, and time-driven systems — while keeping the deterministic, auditable core.
The live hot path is in-memory; durability is layered behind pluggable store interfaces (valem-persistence/*) — memory, filesystem, Postgres, Mongo, Redis, and S3 — with per-concern backend selection for spec/state/blob.
Component Map
┌─────────────────────────────┐
│ LLM Integration Layer │
│ SpecGenerator / LlmClient │
│ (prompt build, JSON parse, │
│ repair loop, max 3 retries)│
└────────────┬────────────────┘
│ model spec (JSON)
▼
┌─────────────────────────────┐
│ ModelSpecValidator │
│ (syntax, duplicate ids, │
│ JSONata compilation, │
│ cycle detection) │
└────────────┬────────────────┘
│ validated spec
▼
┌─────────────────────────────┐
│ ModelSpecCompiler │
│ (JSONata AST → DAG via │
│ ExpressionPathExtractor, │
│ Kahn topological sort) │
└────────────┬────────────────┘
│ CompiledModel
┌─────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌─────────────────┐ ┌──────────────────┐
│ ModelState │ │ ModelRuntime │ │ DerivationTrace │
│ │◄─┤ (hot path) │─►│ (ring buffer) │
│ baseDoc │ │ │ │ │
│ derivedCache │ │ mutate() → │ │ constraint evals │
│ metaCache │ │ DirtyPropagator │ │ + error records │
│ transaction │ │ → topo re-eval │ │ │
│ snapshot │ └────────┬────────┘ └──────────────────┘
└──────────────┘ │
│
┌───────────┴───────────┐
│ │
▼ ▼
┌────────────────────┐ ┌──────────────────────┐
│ ConstraintEvaluator│ │ EffectDispatcher │
│ │ │ │
│ global / scoped / │ │ trigger + dedupeKey →│
│ per-element; │ │ EffectRequest (data);│
│ ROLLBACK throws, │ │ EffectSink.submit() │
│ FLAG records │ │ (post-commit, no I/O)│
└────────────────────┘ └──────────────────────┘
│
┌──────────────────────────┴──────────────────────────┐
│ valem-service (ModelService) │
│ pure-Java orchestration; shared by all access layers│
│ ModelRegistry · BlobStore · JsonPatchTranslator │
└──────┬──────────────┬───────────────┬─────────┘
│ │ │
┌───────────────┘ │ └──────────────┐
▼ ▼ ▼
┌───────────────────────────────────┐ ┌──────────────────────────┐ ┌──────────────────────────┐
│ valem-api (Spring Boot) │ │ valem-mcp │ │ valem-console │
│ │ │ (MCP server, stdio) │ │ (script / REPL use) │
│ ModelController → REST endpoints │ │ │ │ │
│ BlobController │ │ McpServer → JSON-RPC 2.0│ │ JSON-over-stdin/stdout │
│ Persistence (pluggable backends, │ │ ToolRegistry → 16 tools │ │ one command per line; │
│ per-concern; optional) │ │ ResourceRegistry → │ │ all state in-memory │
│ WebSocket subscriptions │ │ guide/schema/examples │ │ for the session │
│ LLM spec generation │ │ in-memory; agent-session│ │ │
│ GlobalExceptionHandler (RFC 7807)│ │ state backend │ │ java -jar console.jar │
│ │ └──────────────────────────┘ └──────────────────────────┘
│ ┌──────────┐ ┌───────────────┐ │ Model Context Protocol client (echo '{}' | …)
│ │ REST API │ │ WebSocket │ │ (Claude Code / Desktop, …)
│ │ /models │ │ Subscriptions │ │
│ │ /blobs │ │ ChangeEvent │ │
│ └──────────┘ └───────────────┘ │
└───────────────────────────────────┘
│ │
▼ ▼
Effect executors Browser UI
(HTTP / LLM / timer; (valem-ui)
caller in response)
Component Descriptions
LLM Integration Layer (SpecGenerator)
Drives the spec generation loop. Builds a structured prompt (spec format schema + domain description + examples), sends it to LlmClient, strips markdown fences, parses the JSON response as ModelSpec, runs ModelSpecValidator, and retries with a repair prompt on failure (up to 3 attempts by default). Also provides generateEvolution() for incremental spec diffs: sends current spec + change request to the LLM, parses the response as SpecEvolution, and validates it by calling applyTo().
ModelSpecValidator
Validates a ModelSpec without compiling it:
- Non-blank
idand non-nullschema - Each derivation: non-blank
path, non-blankexpr, unique paths, valid JSONata syntax - Each metaDerivation: non-blank
path, non-nullproperty, uniquepath#propertykeys, valid expr - Each constraint: non-blank
id, non-blankexpr, non-nullpolicy, unique ids, valid expr - Each effect: non-blank
id/trigger, unique ids, and per-executor rules —callerrequiresemit;serverrequiresrequest.url(or arequestsfan-out);llmrequiresprompt;timerrequiresatorafterMs;response.settargets must be canonical addresses - Cycle detection via
ModelSpecCompiler.compile()(only if no expression errors)
Returns a ValidationResult with ERROR and WARNING severity findings. Never throws.
ModelSpecCompiler
Produces a CompiledModel from a valid spec:
- Uses
ExpressionPathExtractorto walk the JSONata AST of each derivation/meta/constraint/effect expression, extracting all JsonPath dependencies ($.-prefixed) - Builds a
DependencyGraphviaDependencyGraph.Builder— adds nodes (BASE, DERIVED, META) and edges (dependency → dependent) - Synthetic nodes
$constraint:<id>and$effect:<id>are added to the graph so they dirty-propagate correctly - Runs Kahn’s topological sort; throws
CyclicDependencyExceptionif the graph has a cycle - Stores lookup maps for fast runtime access:
derivationByPath,metaDerivationByKey
Reactive Evaluation Engine (ModelRuntime)
The hot path. mutate(Map<String, JsonNode>) runs the full pipeline under a transaction:
- Write —
state.setValue()writes each mutation intobaseDocand marks the path dirty - Propagate —
DirtyPropagator.propagate()does BFS through the graph from the dirty set; also checks every wildcard-pattern node ([*]) against each mutated concrete path usingmatchesPattern() - Derive —
DerivationEvaluatoriterates the topologicalevaluationLevels(), re-evaluating each EAGER DERIVED node in the dirty set against a level-aware view of the merged document (base fields plus all derivations from prior levels); stores result inderivedCache - Meta —
MetaDerivationEvaluatorsame loop over META nodes; for[*]paths, stores the same computed value for every concrete element viaArrayPathExpander - Constrain —
ConstraintEvaluatorchecks each$constraint:*node in dirty set againststate.mergedDocument()(global) or field value (scoped); throwsConstraintViolationExceptionon ROLLBACK, records FLAG violations - Emit effects —
EffectDispatcherevaluates$effect:*trigger +dedupeKeyagainststate.mergedDocument(), applies the edge / in-flight guard, and returnsEffectRequestrecords (data only — no I/O) - Commit —
state.commit(),state.clearDirty(); fires theEffectSinkpost-commit (the shell executes each request asynchronously and folds any result back as a later mutation); appends traces to ring buffer
On ConstraintViolationException: state.rollback() restores pre-mutation snapshot before re-throwing.
Expression evaluation context:
- Derivation expressions read from a level-aware view of the per-cycle merged document: base fields plus all derivations from prior topological levels. So a derivation can reference another derived field, as long as it sits at a later level; same-level derivations cannot see each other. Wildcard expressions also receive
$parentbound to the current array element. - Meta derivation expressions read from the same merged document (the element object for per-element
[*]properties) - Constraint (global) and effect trigger expressions read from
state.mergedDocument()— they see all derived values
ModelState
In-memory state container. Three layers:
- Base document (
ObjectNode) — directly writable fields;setInDoc()creates intermediate objects and arrays on demand - Derived cache (
Map<String, JsonNode>) — computed values keyed by$.path - Meta cache (
Map<String, JsonNode>) — keyed by$.path#property(e.g.$.loan.principal#maximum)
getValue(path) checks derived cache first, then baseDoc.at(JsonPointer). mergedDocument() deep-copies base and splices in all derived values — used by constraint/effect evaluators. A mutation cycle materializes it once and carries it across derivation levels and the meta/constraint/effect phases, so the deep-copy count is O(1) per cycle.
Transaction model: beginTransaction() takes a Snapshot; commit() discards it; rollback() calls restore(snapshot).
withModel(newCompiledModel) creates a new ModelState carrying forward baseDoc, filtered derived cache (entries whose path is still derived in the new model), and meta cache — used when evolving the spec.
ConstraintEvaluator
Post-derivation invariant checker. For each constraint whose synthetic node is in the dirty set:
- Global (no
path): evaluatesexpragainstmergedDocument() - Scalar (single
path): evaluatesexprwith$=state.getValue(path) - Array-scoped (
[*]in path): expands to concrete element paths, evaluates once per element - Multi-target (array of paths): evaluates once per listed path
ROLLBACK violations are collected and thrown as ConstraintViolationException after all constraints run. FLAG violations are returned in MutationResult.flaggedConstraints. Each evaluation produces a DerivationTrace record.
Effects — functional core / imperative shell
Effects let the deterministic core reach the world without giving up determinism or replayability.
Pure core (EffectDispatcher, in valem-core). After constraints pass, evaluates each $effect:* trigger + dedupeKey against mergedDocument(), applies the edge / in-flight guard (read from a statusPath sub-document), and emits EffectRequest records — data only, no I/O. ModelRuntime collects them and fires the registered EffectSink post-commit. Also owns reconcile (re-drive stuck effects on load), resolveFoldback (keyed compare-and-swap), and buildFor (trailing re-fire).
Imperative shell (valem-api). A CompositeEffectExecutor routes each request by executor to a shell that performs the impure work asynchronously, post-commit:
caller— pure; surfaced in the mutation response asdispatchedEffects(the successor to the removedactions). No egress.server—HttpEffectExecutor: a spec-provided-URL HTTP request behind the genericEgressGuard(block loopback/private/metadata, https-only, no cross-host redirects, size cap) with retries/backoff.llm—LlmEffectExecutor: calls the configuredLlmClientwith a state-derived prompt (optional structured-output schema); folds the JSON completion back.timer—TimerEffectExecutor: schedules the fold-back atat/afterMs; the clock lives in the shell, never the pure core.
Fold-back = an ordinary mutation. Each executor folds its result back via ModelService.completeFoldback, driving the statusPath state machine pending → in_flight → applied | failed | cancelled. Fold-backs use a keyed compare-and-swap: an in-flight effect whose input changed is re-evaluated against current state and either applied (CURRENT), discarded and re-fired for the latest value (SUPERSEDED, trailing debounce), or discarded because the trigger no longer holds (CANCELLED — also how a timer cancels). Because every fold-back is a logged mutation, replay never re-executes the I/O — it replays the recorded result.
DerivationTrace (ring buffer)
ModelRuntime maintains a ring buffer of 500 DerivationTrace records. Both derivation evaluations and constraint evaluations write traces. Accessible via rt.explain(prefix) and GET /models/{id}/explain/{path}.
Derivation traces carry targetPath = "$.field.path", the evaluated result, and the inputPaths the expression referenced. Constraint traces use synthetic target paths $constraint:<id> (URL-encode as %24constraint%3Aid) and carry constraintPassed instead of result.
Service Layer (ModelService — valem-service)
Pure-Java orchestration shared by both the REST API and the console application. Has no Spring dependency; can be instantiated with new ModelService(registry, blobStore).
Key responsibilities:
- Model lifecycle: validate → compile → apply initial state → register
- Mutation pipeline: access-check →
ModelRuntime.mutate()undersynchronized(rt)→ returnMutationOutcome(result + post-mutation snapshot, atomically) - Read operations: state, field value, history, schema, explain
- Snapshot / restore
- Spec evolution: apply diff → recompile → swap runtime in registry
- Blob upload/download (global and model-scoped)
- Startup restore:
loadModel(spec, optionalSnapshot)— bypasses validation and initial-state application, intended forModelLoader
Exceptions thrown by ModelService (ModelNotFoundException, ModelAlreadyExistsException, ModelValidationException, WriteAccessDeniedException, ReadAccessDeniedException, HistoryNotFoundException, BlobNotReferencedException, InvalidPatchException) are mapped to HTTP status codes by GlobalExceptionHandler in the API module, and surfaced as {"ok": false, "error": "..."} JSON by the console app.
Access Layer — REST API (valem-api)
Spring Boot REST (ModelController) — thin HTTP adapter over ModelService:
- Delegates to the service, maps results (and domain exceptions) to
ResponseEntity - After mutations: broadcasts
ChangeEventto WebSocket subscribers, and (when a durable backend is configured) appends the RFC 6902 mutation patch to the incremental log inside the model lock - After create: persists spec
- After spec evolve: persists evolved spec
- After delete: removes persisted data
WebSocket (ValemWebSocketHandler) — bare Spring WebSocket (not STOMP). Path /models/{modelId}/subscribe. Broadcasts ChangeEvent JSON frames to all open sessions for the model after each committed mutation. Optional ?paths= prefix filter.
Access Layer — Console App (valem-console)
Reads one JSON command object per line from stdin, writes one JSON response per line to stdout. Instantiates ModelService directly with InMemoryBlobStore and ModelRegistry. No persistence, no WebSocket.
Designed for AI agents and scripts that need to interact with Valem without running an HTTP server. The agent sends commands as JSON, receives results as JSON, and can pipe multiple commands in a single invocation.
echo '{"cmd":"create-model","spec":{...}}' | java -jar valem-console.jar
All model and blob operations map 1-to-1 to their REST counterparts (see reference/api-reference.md for the full command list). Binary data is passed as base64 strings.
Access Layer — MCP Server (valem-mcp)
Exposes ModelService over the Model Context Protocol so any MCP-compatible agent (Claude Code, Claude Desktop, …) can use Valem as the structured-state backend for an agent session. Same lean footprint as the console — instantiates ModelService directly with an in-memory store, no Spring, no MCP SDK — and speaks newline-delimited JSON-RPC 2.0 over stdio (McpServer: initialize / tools / resources handshake; stdout is protocol-only, diagnostics go to stderr).
The design principle is the agent generates, Valem verifies: rather than run its own (weaker) LLM to author specs, the server hands the connected agent the same verification substrate the runtime’s SpecGenerator loop uses. ToolRegistry exposes 16 tools in two groups:
- Model operations —
create_model,mutate,get_state,get_field,explain,get_history,evolve_spec,get_view, … (one-to-one with the REST/console surface). Object results carry both a text block andstructuredContent; ROLLBACK constraint violations are surfaced structurally; every tool declares a title + safety annotations (readOnly/destructive/idempotent);create_model/evolve_specembed the runtime’s ownSpecGenerationSchemaas theirinputSchema. - Authoring / verification —
validate_spec(ModelSpecValidator, findings without creating),eval_expression(evaluate one JSONata expr against sample input),test_spec(TestCaseRunnergiven→expect in a throwaway runtime),dry_run(compile + mutate a candidate spec in isolation).
ResourceRegistry adds the MCP resources capability — the ModelSpec authoring guide, the JSON schemas, and the bundled example specs — so the agent can read the format and study working models before it writes one. In-memory only (mirrors the console); durable, shared, multi-tenant state is the REST/managed path. See ../deployment/mcp-server.md.
Data Flow
1. Natural language domain description
│
▼
2. SpecGenerator (LLM loop) → ModelSpec JSON
└─ if invalid → repair prompt → LLM retry (up to 3×)
│
▼
3. ModelSpecValidator → confirms syntax, cycles, expression validity
│
▼
4. ModelSpecCompiler → CompiledModel (DAG + topological order)
│
▼
5. ModelRuntime created; empty ModelState allocated
ModelService.createModel() registers it in ModelRegistry
│
┌─────┘
│ (mutation arrives via POST /models/{id}/mutations — REST path)
│ or {"cmd":"mutate",...} — console path)
│ or tools/call {"name":"mutate",...} — MCP path)
▼
6. ModelService.mutate() → ModelRuntime.mutate():
a. beginTransaction (snapshot)
b. setValue() × N → dirtyPaths
c. DirtyPropagator → full dirty set (incl. wildcard matching)
d. DerivationEvaluator → derived fields re-evaluated (level-aware merged-document context)
e. MetaDerivationEvaluator → meta cache updated (merged-document context)
f. ConstraintEvaluator → invariants checked (mergedDocument context)
├─ ROLLBACK violation → state.rollback() → throw
│ REST: HTTP 409 via GlobalExceptionHandler
│ Console: {"ok":false,"error":"..."}
└─ FLAG violation → recorded
g. EffectDispatcher → triggers evaluated (mergedDocument) → EffectRequest list (no I/O)
h. state.commit() + clearDirty()
i. EffectSink.submit() × M (post-commit) → shell executes async → folds result back as a later mutation
Returns MutationOutcome (result + post-mutation snapshot, atomic)
│
├── REST path: MutationResponse returned; ChangeEvent broadcast to WS;
│ mutation patch appended to the log (durable backend), in-lock
└── Console: {"ok":true,"result":{"success":true,...}} written to stdout
│
▼
7. Constraint traces available via GET /models/{id}/explain/{path}
or {"cmd":"explain","id":"...","path":"..."}
Key Design Decisions
A summary of the key architectural decisions behind the runtime:
| Decision | One-line rationale |
|---|---|
| JSON Path (RFC 9535) for all field addresses | One unambiguous address dialect; expr stays JSONata and is never rewritten. |
| Level-aware merged snapshot for derivations | Deterministic chained derivations; level k+1 sees level k by dot-path. |
| JSONata for derivations/constraints/effects | Expressive yet LLM-generable; pre-compiled and memoised; generate-then-repair. |
| Rollback-on-violation default policy | State never holds a value that violates a rollback constraint; flag opts into soft. |
| Dependency graph as the core structure | Incremental evaluation — only nodes reachable from the change are touched. |
| In-memory state, pluggable persistence | Fast hot path; durability layered behind store interfaces (FS / DB / S3). |
| Spec evolution via diff objects | Rolling schema updates that preserve live state. |
| Single-lock mutation serialization | Corruption-free pipeline; scale via many independent models. |
| Shared pure-Java service layer | One orchestration core for both REST API and console; no Spring in core/service. |
Incremental schema/view/constants evolution + local $defs/$ref |
Targeted diffs for the three formerly wholesale-only sections; no silent elision, ~½ the tokens. |