Skip to main content
Glama
aqalalwa

ariadnet

by aqalalwa

ariadnet

PyPI Version Python Versions License CI

A deterministic solver for AI agents that work with interdependent variables.

Language models are good at conversation and bad at bookkeeping: long chains of derived values, exact arithmetic, noticing contradictions, and knowing what is still missing. ariadnet takes that job away from the model. You describe the variables of a domain and the relations between them (a hypergraph: each relation links any number of variables), and ariadnet

  • derives every value that follows from what is known, in any direction (solve V = I * R for whichever variable is missing), including groups of equations that must be solved together;

  • reports conflicts and constraint violations instead of silently picking a side;

  • plans: tells the agent the smallest sets of inputs it still needs to ask for;

  • explains every derived value with its full provenance tree.

The name joins Ariadne, who gave Theseus the thread that led him out of the labyrinth, with net: every value keeps a thread you can follow back to where it came from.

The model keeps the parts it's good at: understanding the user, choosing what to ask, explaining results. ariadnet can be used three ways, all backed by the same engine:

Use it as

When

A Python library

Your agent (or any program) runs in Python

Tool definitions + a dispatcher

You drive a model API directly (Claude, OpenAI, ...)

An MCP server

Any MCP client: Claude Code, Claude Desktop, agent frameworks, other languages

Quickstart

pip install ariadnet
import ariadnet as an

graph = an.Graph(
    [an.Variable("V", unit="volt"), an.Variable("I", unit="ampere"),
     an.Variable("R", unit="ohm"), an.Variable("P", unit="watt")],
    [an.Equation("ohm", "V = I * R"), an.Equation("power", "P = V * I")],
)

session = graph.session()
print(session.update({"V": 12, "R": 6}).known)   # {'V': 12, 'I': 2, 'R': 6, 'P': 24}
print(session.explain("P"))                      # how P was derived, step by step

conflicted = graph.session().update({"V": 12, "I": 2, "R": 5})
print(conflicted.issues[0].message)              # known values contradict ohm (V = I * R): ...

No graph file, API key or model needed. Requires Python 3.10+; the only dependencies are sympy and pyyaml. For the MCP server, add the extra: pip install "ariadnet[mcp]". The terminal session above runs the same graph from a file, examples/electrical.yaml; the next section describes the format.

Related MCP server: Cruxible Core

Describe a domain

A graph file lists variables and relations. It is data, not code: expressions use a small, safe language (no eval), so graph files can be written by domain experts, reviewed like configuration, and shared between agents.

name: electrical
version: "1.0"
description: DC circuit with a single resistive load.

variables:
  V: {unit: volt, description: Voltage across the load}
  I: {unit: ampere, domain: positive, description: Current through the load}
  R: {unit: ohm, domain: positive, description: Resistance of the load}
  P: {unit: watt, domain: positive, description: Power dissipated by the load}

relations:
  - {id: ohm, kind: equation, expr: "V = I * R"}
  - {id: power, kind: equation, expr: "P = V * I"}
  - {id: fuse, kind: constraint, expr: "I <= 16", message: current exceeds the 16 A fuse}

tests:
  - given: {V: 12, R: 6}
    expect: {I: 2, P: 24}
  - given: {V: 12, I: 2, R: 5}
    expect_issues: [conflict]

Variables

Field

Meaning

type

number (default), integer, string, or boolean

description, unit

Shown to the model in tool schemas and the system prompt, so write them carefully

domain

For numbers: real (default), positive, nonnegative, negative. It also tells the solver which roots are valid (x**2 = 9 with positive gives 3, not ±3)

min, max

Bounds, checked on every value

choices

Allowed values. String matching ignores case and normalizes to the listed spelling

guess

Starting point for numeric root finding when an equation has no closed form

A bare string is shorthand for a number with that description: V: Voltage across the load.

Relations

kind

Fields

Derives

Example

equation

expr

Any one of its variables from the others (symbolically via sympy, numerically when there's no closed form)

V = I * R

function

output, and expr or fn (+ inputs)

output from its inputs (one direction)

0.15 if seats >= 100 else 0

table

keys, rows

The other columns of the first matching row ("*" matches anything)

price list, tax rates

constraint

expr, optional message

Nothing; it must hold once its variables are known

discount <= 0.3

Once all of a relation's variables are known, it also serves as a consistency check. If equations can't be solved one at a time, the solver tries solving groups of them jointly (a + b = 10, a - b = 2 gives a = 6, b = 4).

Expression language. Arithmetic (+ - * / // % **), comparisons (chainable), and/or/not, x if cond else y, in with literal lists, string concatenation, the constants pi and e, and the functions abs sqrt exp log log10 sin cos tan asin acos atan atan2 floor ceil min max round. Equations use the numeric subset.

Python code as a relation. A function relation can call your code, such as a database lookup or an API call. Reference it by name in the file and pass it in when loading. If the function raises, the error is reported as an error issue and doesn't crash the engine.

  - {id: fx, kind: function, output: rate, fn: lookup_fx_rate, inputs: [currency]}
graph = ariadnet.load("pricing.yaml", functions={"lookup_fx_rate": lookup_fx_rate})

Tests travel with the graph. tests: entries (given, expect, expect_issues; an expected value of null means "must stay unknown") run without any model:

ariadnet check examples/*.yaml

The examples folder has three complete domains: electrical (equations and a joint solve), pricing (tables, tiered rules, approval constraints) and loan (numeric root finding).

Use it from Python

import ariadnet as an

graph = an.load("examples/electrical.yaml")   # immutable; share it freely
session = graph.session()                      # the values for one task

solution = session.update({"V": 12, "R": 6})
solution["P"]                  # 24
solution.records["I"]          # Value(name='I', value=2, source='derived', via=('ohm',), inputs=('V', 'R'))
solution.issues                # ()  -- conflicts, violations, ambiguities, ...
print(session.explain("P"))    # provenance tree

session.set("R", 3)            # changing an input recomputes everything downstream
session.unset("R")             # retracting one drops what was derived from it
session.missing_for("P")       # [('I',), ('R',)] -- the smallest sets of inputs still needed

A session stores only asserted values (each with its source: "user", "document", "agent", ...). Derived values are recomputed whenever something changes, so they can never go stale. Graphs can also be built in code with an.Graph, an.Variable, an.Equation, an.Function, an.Table and an.Constraint (see examples/quickstart.py). Subclass an.Relation to add a new kind of relation, and register it for graph files with an.load(..., kinds={"mykind": factory}).

Give it to a model as tools

Toolkit turns a graph into tool definitions, a system prompt, and a dispatcher:

from ariadnet.tools import Toolkit

toolkit = Toolkit(an.load("examples/loan.yaml"))
toolkit.definitions()             # Anthropic format; definitions("openai") for Chat Completions
toolkit.system_prompt()           # rules for using the tools + variable glossary + relations
toolkit.call(name, input, session_id=conversation_id)   # -> JSON-serializable dict

Tool

Does

set_values

Records values (with a source) and returns what is now known, what changed, what is still unknown, and any issues

unset_values

Retracts values; everything derived from them is recomputed

get_state

Returns the current state

missing_for

Returns the smallest sets of variables that would determine a target

explain

Returns the provenance tree of a value

The schemas are generated from the graph: variable names become enums, and each variable's type, unit, domain and description go into the schema. Invalid input never raises. It comes back as {"error": "unknown variable 'Vx'; did you mean 'V'?"} so the model can correct itself, and you should send it back as an error tool result (is_error: true for Claude).

examples/claude_agent.py is a complete conversational agent on the Claude API, in one short file:

pip install "anthropic>=1.8"
python examples/claude_agent.py "What would I pay monthly on a 300k loan at 6%?"

Sessions live in a SessionStore (in memory by default). To share state between processes or agents, implement its three methods (load, save, delete) over Redis or a database. save receives the revision the session was loaded at, so two agents writing at once get ConcurrentModificationError instead of overwriting each other.

Serve it over MCP

ariadnet mcp examples/pricing.yaml                                 # stdio
ariadnet mcp examples/pricing.yaml --transport streamable-http --port 8000

Claude Code: claude mcp add pricing -- ariadnet mcp /path/to/pricing.yaml. Claude Desktop and other clients, in their MCP configuration:

{
  "mcpServers": {
    "pricing": { "command": "ariadnet", "args": ["mcp", "/path/to/pricing.yaml"] }
  }
}

The server exposes the same five tools plus describe_graph. Every tool takes an optional session argument, so one server can keep several conversations apart. The server's instructions carry the same system prompt the Toolkit generates. To embed the server in your own application, call ariadnet.mcp_server.create_server(graph, store=...).

Command line

ariadnet check GRAPH...                  validate graph files and run their tests
ariadnet solve GRAPH NAME=VALUE...       print everything that follows (--explain, --missing, --json)
ariadnet tools GRAPH [--format openai]   print tool definitions
ariadnet prompt GRAPH                    print the system prompt
ariadnet mcp GRAPH                       run an MCP server

How it works

  1. Forward propagation. Repeatedly find a relation where everything but one target is known, compute the target, and record which relation and inputs produced it. Equations are solved symbolically once per target and cached. Candidate values outside the target's type, domain or bounds are dropped. If several valid ones remain, the result is an ambiguous issue rather than a guess.

  2. Joint solving. When propagation stalls, groups of equations that share unknowns are handed to sympy together, and the values the group fully determines are kept.

  3. Consistency checks. Every relation whose variables are all known is checked (with a tolerance). Failures become conflict or violation issues that list the values involved and where each came from.

  4. Backward planning. missing_for searches the hypergraph from the target back to the known values, and returns the minimal sets of inputs that would close the gap.

Limits worth knowing: planning is structural, so an option can still fail at run time (for example, a table has no matching row). Joint solving is limited to settings.max_system_size equations (default 6). Units are metadata only; there is no unit conversion yet.

Security

Graph files never execute code: expressions are parsed with ast and compiled from a whitelist, and fn: names resolve only to callables you pass in explicitly. A hostile graph file can still describe equations that are expensive to solve, so treat graph files from untrusted sources the way you'd treat any untrusted input. See SECURITY.md.

Contributing

See CONTRIBUTING.md. In short: uv sync, then uv run pytest, uv run ruff check ., and uv run mypy.

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    Enables solving Constraint Satisfaction Problems (CSP) like N-Queens, graph coloring, and Sudoku, as well as Linear Programming optimization problems through both MCP tools and HTTP API endpoints.
    2
    MIT
  • A
    license
    A
    quality
    C
    maintenance
    Deterministic logical reasoning server for MCP clients that compiles facts, rules, and queries to Prolog and returns exact answers with proof trees.
    4
    MIT
  • A
    license
    Not graded
    quality
    A
    maintenance
    Provides deterministic decision and constraint evaluation for AI agents. Supports ruleset validation, fact evaluation, and constraint checking via MCP tools.
    Apache 2.0