00 — Why This Document
You have two world-class specs: the Graphonomous CL engine (Feb 21) and the OpenSentience runtime (Feb 24). Both are implementation-ready with typed Elixir structs, OTP supervision trees, and phased build plans.
But there's a specific gap neither spec fills: the exact sequence of terminal commands, file creation, and dependency wiring that gets you from an empty directory to a running MCP server that an LLM can talk to. Specs tell you what to build. This document tells you exactly how to start building it, today.
This is not another spec. This is a build journal template — the document you open in a split pane while your terminal is on the other side.
Claude Desktop (or any MCP client) connects to Graphonomous. You type "remember that I prefer PostgreSQL over MySQL for OLTP workloads." Graphonomous stores a semantic node. You type "what databases do I prefer?" Graphonomous retrieves the node by embedding similarity. You type "actually, I tried CockroachDB and it's better." Graphonomous updates the node, adjusts confidence, and the old belief decays. That's a continual learning engine. That's a GitHub repo worth starring. That's the demo that turns your $800K portfolio into a $3M company.
01 — Dependency Lockfile
Every dependency below has been verified to exist, be actively maintained, and work together in the Elixir ecosystem as of February 2026.
| Dependency | Version | Purpose | Status |
|---|---|---|---|
| Elixir | 1.17+ | Language runtime | Stable |
| OTP | 27+ | Erlang runtime | Stable |
| anubis_mcp | ~> 0.17 | MCP server SDK (Elixir-native, Anubis.Server) | Active development, hex.pm |
| exqlite | ~> 0.27 | SQLite3 NIF bindings for Elixir | Stable, hex.pm |
| sqlite_vec | ~> 0.1 | Elixir wrapper for sqlite-vec extension | Exists on GitHub (joelpaulkoch/sqlite_vec) |
| bumblebee | ~> 0.6 | HuggingFace models in Elixir (embeddings) | Stable, hex.pm |
| nx | ~> 0.9 | Numerical computing (tensors) | Stable, hex.pm |
| exla | ~> 0.9 | XLA compiler backend for Nx (CPU/GPU) | Stable, hex.pm |
| jason | ~> 1.4 | JSON encoding/decoding | Stable |
| telemetry | ~> 1.2 | Event system (for FleetPrompt hooks later) | Stable |
defmodule Graphonomous.MixProject do
use Mix.Project
def project do
[
app: :graphonomous,
version: "0.1.0",
elixir: "~> 1.17",
start_permanent: Mix.env() == :prod,
deps: deps(),
name: "Graphonomous",
description: "Continual learning engine — self-evolving knowledge graphs for AI agents",
source_url: "https://github.com/ampersandbox/graphonomous"
]
end
def application do
[
extra_applications: [:logger],
mod: {Graphonomous.Application, []}
]
end
defp deps do
[
# MCP Server
{:anubis_mcp, "~> 0.17"},
# Storage
{:exqlite, "~> 0.27"},
# sqlite-vec wrapper (check hex.pm or use GitHub)
{:sqlite_vec, github: "joelpaulkoch/sqlite_vec"},
# Local Embeddings
{:bumblebee, "~> 0.6"},
{:nx, "~> 0.9"},
{:exla, "~> 0.9"},
# Utilities
{:jason, "~> 1.4"},
{:telemetry, "~> 1.2"},
# Dev/Test
{:ex_doc, "~> 0.34", only: :dev, runtime: false}
]
end
end
sqlite-vec is a C extension loaded into SQLite. If the
Elixir wrapper doesn't cooperate, you can load the
extension manually via
Exqlite.Sqlite3.execute(conn, "SELECT
load_extension('./vec0')")
after downloading the prebuilt binary for your platform
from the sqlite-vec GitHub releases. This is exactly how
the Python and Node.js ecosystems use it.
02 — Day 1 Commands
Open your terminal. Run these in order. Do not skip steps.
# 1. Create the project
mix new graphonomous --sup
cd graphonomous
# 2. Replace mix.exs with the one above (Section 01)
# 3. Fetch dependencies
mix deps.get
# 4. Compile — this will take a while (EXLA downloads XLA)
mix compile
# 5. Verify SQLite works
iex -S mix
# In IEx:
{:ok, conn} = Exqlite.Sqlite3.open(":memory:")
:ok = Exqlite.Sqlite3.execute(conn, "CREATE TABLE test (id INTEGER PRIMARY KEY)")
# Should return :ok
# 6. Verify Bumblebee + embeddings work (first run downloads ~90MB model)
{:ok, model} = Bumblebee.load_model({:hf, "sentence-transformers/all-MiniLM-L6-v2"})
{:ok, tok} = Bumblebee.load_tokenizer({:hf, "sentence-transformers/all-MiniLM-L6-v2"})
serving = Bumblebee.Text.TextEmbedding.text_embedding(model, tok,
output_pool: :mean_pooling, output_attribute: :hidden_state, embedding_processor: :l2_norm)
result = Nx.Serving.run(serving, "hello world")
# Should return %{embedding: #Nx.Tensor<f32[384]...>}
# 7. Run tests (should pass with default test)
mix test
If all 7 steps pass, your foundation is solid. You have SQLite, vector embeddings, and a supervised OTP application. Everything else builds on top of this.
03 — Project Structure
04 — Core Types
defmodule Graphonomous.Types.Node do
@moduledoc "A knowledge node in the continual learning graph."
@type node_type :: :episodic | :semantic | :procedural
@type t :: %__MODULE__{
id: binary(),
content: binary(), # Human-readable knowledge
node_type: node_type(),
confidence: float(), # 0.0–1.0, decays over time
embedding: binary() | nil, # 384-dim f32 blob
metadata: map(),
source: binary() | nil, # Where this knowledge came from
access_count: non_neg_integer(),
created_at: DateTime.t(),
updated_at: DateTime.t(),
last_accessed_at: DateTime.t()
}
defstruct [
:id, :content, :embedding, :source,
node_type: :semantic,
confidence: 0.5,
metadata: %{},
access_count: 0,
created_at: nil,
updated_at: nil,
last_accessed_at: nil
]
end
defmodule Graphonomous.Types.Edge do
@moduledoc "A weighted, typed, decaying edge between knowledge nodes."
@type edge_type :: :causal | :related | :contradicts | :supports | :derived_from
@type t :: %__MODULE__{
id: binary(),
source_id: binary(),
target_id: binary(),
edge_type: edge_type(),
weight: float(), # 0.0–1.0, strength of relationship
metadata: map(),
created_at: DateTime.t(),
last_activated_at: DateTime.t()
}
defstruct [
:id, :source_id, :target_id,
edge_type: :related,
weight: 0.5,
metadata: %{},
created_at: nil,
last_activated_at: nil
]
end
defmodule Graphonomous.Types.Outcome do
@moduledoc "An outcome report from OpenSentience — the causal feedback signal."
@type status :: :success | :partial_success | :failure | :timeout
@type t :: %__MODULE__{
action_id: binary(),
status: status(),
confidence: float(), # How reliable is this outcome signal
causal_node_ids: [binary()], # Which graph nodes informed the action
evidence: map(),
observed_at: DateTime.t()
}
defstruct [
:action_id, :status, :causal_node_ids, :observed_at,
confidence: 0.5,
evidence: %{}
]
end
05 — SQLite Schema
This goes in lib/graphonomous/store.ex and runs
automatically when the application starts. No Ecto needed
for v0.1 — raw SQL keeps things simple and gives you full
control over sqlite-vec.
-- nodes: the knowledge graph
CREATE TABLE IF NOT EXISTS nodes (
id TEXT PRIMARY KEY,
content TEXT NOT NULL,
node_type TEXT NOT NULL DEFAULT 'semantic',
confidence REAL NOT NULL DEFAULT 0.5,
embedding BLOB, -- 384-dim f32 from all-MiniLM-L6-v2
metadata TEXT DEFAULT '{}', -- JSON
source TEXT,
access_count INTEGER DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_accessed_at TEXT NOT NULL
);
-- edges: relationships between nodes
CREATE TABLE IF NOT EXISTS edges (
id TEXT PRIMARY KEY,
source_id TEXT NOT NULL REFERENCES nodes(id),
target_id TEXT NOT NULL REFERENCES nodes(id),
edge_type TEXT NOT NULL DEFAULT 'related',
weight REAL NOT NULL DEFAULT 0.5,
metadata TEXT DEFAULT '{}',
created_at TEXT NOT NULL,
last_activated_at TEXT NOT NULL
);
-- outcomes: causal feedback log (from OpenSentience)
CREATE TABLE IF NOT EXISTS outcomes (
id TEXT PRIMARY KEY,
action_id TEXT NOT NULL,
status TEXT NOT NULL,
confidence REAL NOT NULL,
causal_node_ids TEXT NOT NULL, -- JSON array of node IDs
evidence TEXT DEFAULT '{}',
observed_at TEXT NOT NULL,
processed_at TEXT
);
-- indexes
CREATE INDEX IF NOT EXISTS idx_nodes_type ON nodes(node_type);
CREATE INDEX IF NOT EXISTS idx_nodes_confidence ON nodes(confidence);
CREATE INDEX IF NOT EXISTS idx_edges_source ON edges(source_id);
CREATE INDEX IF NOT EXISTS idx_edges_target ON edges(target_id);
CREATE INDEX IF NOT EXISTS idx_outcomes_causal ON outcomes(causal_node_ids);
-- Initialize sqlite-vec for vector search on the nodes table
-- (run after loading the extension)
-- SELECT vector_init('nodes', 'embedding', 'type=FLOAT32,dimension=384');
06 — MCP Server
This is how any LLM client (Claude Desktop, Cursor, ChatGPT) talks to Graphonomous. The Anubis library uses a component-based pattern where each MCP tool is its own module.
defmodule Graphonomous.MCP.Server do
use Anubis.Server,
name: "graphonomous",
version: "0.1.0",
capabilities: [:tools]
# Register all MCP tools as components
component Graphonomous.MCP.StoreNode
component Graphonomous.MCP.RetrieveContext
component Graphonomous.MCP.LearnFromOutcome
component Graphonomous.MCP.QueryGraph
end
defmodule Graphonomous.MCP.StoreNode do
@moduledoc """
Store a knowledge node in the graph.
The agent has learned something — persist it as a typed,
confidence-scored node with an embedding for later retrieval.
"""
use Anubis.Server.Component, type: :tool
schema do
field :content, :string, required: true,
description: "The knowledge to store (natural language)"
field :node_type, :string,
description: "episodic, semantic, or procedural"
field :confidence, :number,
description: "0.0-1.0 confidence in this knowledge"
field :source, :string,
description: "Where this knowledge came from"
end
@impl true
def execute(params, frame) do
node = Graphonomous.store_node(%{
content: params.content,
node_type: Map.get(params, :node_type, "semantic"),
confidence: Map.get(params, :confidence, 0.5),
source: Map.get(params, :source)
})
{:ok, Jason.encode!(%{
node_id: node.id,
status: "stored",
confidence: node.confidence
}), frame}
end
end
defmodule Graphonomous.MCP.LearnFromOutcome do
@moduledoc """
Process an outcome from OpenSentience.
This is the causal feedback loop — the thing nobody else has.
When an action succeeds or fails, this tool receives:
- Which graph nodes informed the action (causal_node_ids)
- Whether the action succeeded or failed (status)
- How confident we are in this outcome (confidence)
It then updates the confidence of those causal nodes:
- Success → confidence increases (the knowledge was useful)
- Failure → confidence decreases (the knowledge was wrong or stale)
"""
use Anubis.Server.Component, type: :tool
schema do
field :action_id, :string, required: true,
description: "ID of the action that produced this outcome"
field :status, :string, required: true,
description: "success, partial_success, failure, or timeout"
field :confidence, :number, required: true,
description: "0.0-1.0 how reliable is this outcome signal"
field :causal_node_ids, :string, required: true,
description: "JSON array of node IDs that informed this action"
end
@impl true
def execute(params, frame) do
node_ids = Jason.decode!(params.causal_node_ids)
result = Graphonomous.learn_from_outcome(%{
action_id: params.action_id,
status: String.to_existing_atom(params.status),
confidence: params.confidence,
causal_node_ids: node_ids
})
{:ok, Jason.encode!(%{
processed: length(node_ids),
updates: result.updates
}), frame}
end
end
Once the MCP server compiles, add it to your Claude
Desktop config. In
claude_desktop_config.json:
{"mcpServers": {"graphonomous": {"command":
"elixir", "args": ["--no-halt", "-S", "mix",
"run"]}}}
Or for stdio transport:
claude mcp add graphonomous -- elixir --no-halt -S
mix run
07 — Build Plan
This is the Mem0 playbook adapted for your stack. Ship the simplest thing that works, then add sophistication. Each week ends with something demoable.
mix new graphonomous --sup, wire
deps, verify compile
Graphonomous.Store — SQLite schema
creation, node CRUD (insert, get, update,
delete)
Graphonomous.Embedder — Bumblebee
GenServer wrapping all-MiniLM-L6-v2 as
Nx.Serving, auto-embed on node creation
Graphonomous.Graph GenServer — ETS
cache for hot nodes, edge CRUD, basic traversal
✓ DEMO: In IEx, store 10 facts, query by natural language, get ranked results
Graphonomous.MCP.Server —
Anubis.Server with stdio transport, register
empty tools
store_node tool — wired to Graph
module, embedding happens automatically
retrieve_context tool — vector
search + graph traversal, returns ranked nodes
with IDs (causal_context)
query_graph tool — list nodes by
type, get node by ID, get edges for node
mix anubis.stdio.interactive
✓ DEMO: Claude Desktop talks to Graphonomous. Store knowledge, retrieve it, see it working
learn_from_outcome tool — receive
outcome, look up causal nodes, apply confidence
update
new_confidence = old * (1 - learning_rate)
+ outcome_signal * learning_rate
where outcome_signal is +1 for success, -0.5 for
failure, scaled by outcome confidence
Graphonomous.Consolidator —
GenServer on timer. Every N minutes: decay
unused node confidence by 2%, prune nodes below
0.1 threshold, merge near-duplicate nodes
(cosine similarity > 0.95)
[:graphonomous, :outcome, :processed],
[:graphonomous, :node, :decayed],
[:graphonomous, :node, :pruned]
✓ DEMO: The causal feedback loop works. Knowledge that leads to failures loses confidence. Knowledge that leads to successes gets stronger. The graph learns.
mix deps.get && mix run + Claude
Desktop config snippet
mix docs —
ExDoc with examples for every public function
mix hex.publish
✓ SHIPPED: graphonomous on GitHub + hex.pm. The first continual learning MCP server in Elixir.
08 — Scope Control
This is as important as what it includes. Every feature below is in your specs. None of them belong in v0.1.
| Feature | Why Skip | When to Add |
|---|---|---|
| OpenSentience integration | Graphonomous must work standalone first | v0.2 — after Graphonomous has users |
| Deliberatic escalation | Needs working graph + outcomes first | v0.3 — after outcome patterns emerge |
| FleetPrompt event hooks | Telemetry events are the foundation; strategies come later | v0.2 — expose telemetry, let community build handlers |
| PostgreSQL / pgvector mode | SQLite-only keeps edge-first promise real | v0.2 — Ecto adapter layer |
| Federation / multi-instance sync | Single-node must be solid first | v1.0 — distributed Erlang or CRDT sync |
| Goal graph | OpenSentience owns this | When OpenSentience ships |
| REST API / Phoenix | MCP is the API for v0.1 | v0.2 — Phoenix optional layer |
| Custom embedding models | all-MiniLM-L6-v2 is good enough for launch | v0.2 — configurable model |
| Web dashboard | CLI and MCP tools are enough to demo | v0.3 — Phoenix LiveView graph visualization |
If it's not in the 4-week plan, it doesn't exist until the 4-week plan is done. No exceptions. No "let me just quickly add..." No scope creep. No new spec writing. The only output for the next 28 days is committed code in a GitHub repository.
09 — Success Criteria
| Test | Expected Result | Why It Matters |
|---|---|---|
mix test |
All green | Basic quality bar |
Claude Desktop → store_node
|
Node persisted in SQLite with embedding | MCP works, embeddings work |
Claude Desktop →
retrieve_context
|
Returns semantically similar nodes ranked by confidence × similarity | Vector search works, graph works |
learn_from_outcome with success
|
Causal node confidence increases | THE differentiator works |
learn_from_outcome with failure
|
Causal node confidence decreases | System learns from mistakes |
| Wait 5 minutes, query again | Unused nodes decayed slightly, active nodes retained | Consolidation works |
mix hex.publish --dry-run |
Package validates | Ready for Hex.pm |
Fresh clone →
mix deps.get && mix test
|
Passes on clean machine | Actually distributable |
10 — Ship It
You have: the Graphonomous spec (architecture), the
OpenSentience spec (runtime), the reality check (market
validation), and now the implementation blueprint (exact
commands). There is nothing left to research, plan, or
design. The next file you create should be
mix.exs.