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 id and non-null schema
  • Each derivation: non-blank path, non-blank expr, unique paths, valid JSONata syntax
  • Each metaDerivation: non-blank path, non-null property, unique path#property keys, valid expr
  • Each constraint: non-blank id, non-blank expr, non-null policy, unique ids, valid expr
  • Each effect: non-blank id/trigger, unique ids, and per-executor rules — caller requires emit; server requires request.url (or a requests fan-out); llm requires prompt; timer requires at or afterMs; response.set targets 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:

  1. Uses ExpressionPathExtractor to walk the JSONata AST of each derivation/meta/constraint/effect expression, extracting all JsonPath dependencies ($.-prefixed)
  2. Builds a DependencyGraph via DependencyGraph.Builder — adds nodes (BASE, DERIVED, META) and edges (dependency → dependent)
  3. Synthetic nodes $constraint:<id> and $effect:<id> are added to the graph so they dirty-propagate correctly
  4. Runs Kahn’s topological sort; throws CyclicDependencyException if the graph has a cycle
  5. 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:

  1. Writestate.setValue() writes each mutation into baseDoc and marks the path dirty
  2. PropagateDirtyPropagator.propagate() does BFS through the graph from the dirty set; also checks every wildcard-pattern node ([*]) against each mutated concrete path using matchesPattern()
  3. DeriveDerivationEvaluator iterates the topological evaluationLevels(), 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 in derivedCache
  4. MetaMetaDerivationEvaluator same loop over META nodes; for [*] paths, stores the same computed value for every concrete element via ArrayPathExpander
  5. ConstrainConstraintEvaluator checks each $constraint:* node in dirty set against state.mergedDocument() (global) or field value (scoped); throws ConstraintViolationException on ROLLBACK, records FLAG violations
  6. Emit effectsEffectDispatcher evaluates $effect:* trigger + dedupeKey against state.mergedDocument(), applies the edge / in-flight guard, and returns EffectRequest records (data only — no I/O)
  7. Commitstate.commit(), state.clearDirty(); fires the EffectSink post-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 $parent bound 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): evaluates expr against mergedDocument()
  • Scalar (single path): evaluates expr with $ = 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 as dispatchedEffects (the successor to the removed actions). No egress.
  • serverHttpEffectExecutor: a spec-provided-URL HTTP request behind the generic EgressGuard (block loopback/private/metadata, https-only, no cross-host redirects, size cap) with retries/backoff.
  • llmLlmEffectExecutor: calls the configured LlmClient with a state-derived prompt (optional structured-output schema); folds the JSON completion back.
  • timerTimerEffectExecutor: schedules the fold-back at at/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 (ModelServicevalem-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() under synchronized(rt) → return MutationOutcome (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 for ModelLoader

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 ChangeEvent to 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 operationscreate_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 and structuredContent; ROLLBACK constraint violations are surfaced structurally; every tool declares a title + safety annotations (readOnly/destructive/idempotent); create_model/evolve_spec embed the runtime’s own SpecGenerationSchema as their inputSchema.
  • Authoring / verificationvalidate_spec (ModelSpecValidator, findings without creating), eval_expression (evaluate one JSONata expr against sample input), test_spec (TestCaseRunner given→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.