Skip to content
Conduit

mcp server

socratic-engine

Externalized epistemic scaffolding for AI agents. Recursive boolean certification trees.

rm-w3kufe/socratic-enginecommunitylistedunreviewed

Set up

Runs on your computer. Pick your agent and operating system.

Before you start

  • uv (`curl -LsSf https://astral.sh/uv/install.sh | sh`)
~/Library/Application Support/Claude/claude_desktop_config.json
{
  "mcpServers": {
    "socratic-engine": {
      "command": "uvx",
      "args": [
        "socratic-engine"
      ]
    }
  }
}

same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12

Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs

README

synced Oct 22, 2026

socratic-engine — externalized reasoning for AI agents

CI Python 3.10+ License: Apache-2.0 PyPI version MCP Compatible

Don't ask the language model to hold the whole recursion. Give the recursion a substrate.

socratic-engine is a model-agnostic reasoning substrate for AI agents: a recursive evaluator for trivalent logical trees with an explicit separation between truth, certification, and evidence.

The engine is deliberately small. It does not try to make an LLM "smarter". It gives the model a formal structure in which complex questioning can be proposed, executed recursively, inspected, and diagnosed outside the model's token-generation loop.

Status: v0.2.12 — core engine, VSL tree parser (block + inline notation), CLI (eval-tree, decide, match-procedure), MCP bridge, multi-bridge (health tracking + routing observability + duplicate-domain priority), VsmDocProvider, dialectical and jury operators, certified-UNKNOWN doctrine (indeterminacy vocabulary + gate), computation nodes (genome feature-chain evaluation), pragmatic predicates, context predicates (ctx_has, ctx_equals, ctx_contains, ctx_not_has), semantic simplification (NOT flattening, contradiction/tautology, dedup, absorption), short-circuit evaluation, tree DoS prevention (depth≤100, nodes≤10K via _TreeLimitCounter), cycle detection, caching, rate limiting, engine contract protocols (SocraticEngineProtocol, EvaluationProtocol), security hardening (enforce_limits=True by default, predicate error wrapping, arity validation), CI (pytest + coverage gate at 90%), 546-test suite, benchmarks, and the official state-canon bridge (bridge_statecanon.py) with end-to-end examples are working. The broader claim — that externalizing recursive structure improves reliability on tasks that exceed a model's implicit recursive reasoning capacity — is an experimental hypothesis, not a proclamation.


The bet

LLMs are excellent at generating hypotheses, decomposing problems, interpreting language, and proposing strategies.

They are less reliable when they must implicitly execute a deep recursive reasoning process while simultaneously maintaining all of its premises, branches, dependencies, and intermediate states in context.

The bet here is simple:

Let the model propose the questions. Let a deterministic substrate execute the recursion.

The model does not have to remember the entire reasoning process. The tree does.

flowchart LR
    A["LLM<br/>semantic proposal"] --> B["Socratic tree<br/>formal IR"]
    B --> C["SocraticEngine<br/>recursive execution"]
    C --> D["Evaluation tree<br/>truth + certification + evidence"]
    D --> E["explain / diagnose"]
    E --> A

This creates a division of labour:

ComponentResponsibility
LLMformulate hypotheses, propose questions, interpret results
Tree / IRpreserve recursive structure explicitly
SocraticEngineexecute logic deterministically
Predicatesperform domain-specific atomic checks
Evaluationpreserve truth, certification, evidence, context
diagnose()identify the branches that prevented certification

The central boundary is:

The LLM proposes. The engine validates and executes. The LLM does not certify.


What it is

At its core, the engine evaluates trees composed of:

  • AND
  • OR
  • NOT
  • XOR
  • IMPLIES
  • DIALECTICAL_AND
  • registered predicates
  • boolean literals

The logical result is trivalent:

TRUE
FALSE
UNKNOWN

But truth is not the same thing as certification.

Every evaluation can additionally carry:

truth
certified
evidence
source
context
metadata
children

So the result of a recursive evaluation is itself a recursive evaluation tree.

flowchart TD
    R["op:AND"] --> A["service_running<br/>TRUE ✓"]
    R --> B["schema_valid<br/>TRUE ✓"]
    R --> C["llm_judge<br/>TRUE ✗"]

    R --> T["root result<br/>TRUE / not certified"]

A model may produce a plausible answer. That answer can be represented as TRUE.

It does not follow that the system is allowed to certify it.


Why three values?

A certification system must distinguish:

FALSE

from:

UNKNOWN

If the system cannot establish a proposition because data is missing, a check timed out, or the subject of the question is absent, returning FALSE silently converts lack of evidence into evidence of falsity.

socratic-engine therefore treats indeterminacy as a first-class result:

TRUE      → the proposition evaluated true
FALSE     → the proposition evaluated false
UNKNOWN   → the proposition could not be decided

This matters particularly at routing boundaries.

For example:

TYPE = ""
question: "Does TYPE start with VSL-?"

→ UNKNOWN

not:

→ FALSE

The discipline is:

No subject, no judgment.


Truth is not certification

This is the epistemic boundary at the centre of the engine.

flowchart LR
    Q["Predicate"] --> T{"Truth?"}
    T -->|TRUE| C{"Certified?"}
    T -->|FALSE| C
    T -->|UNKNOWN| U["UNKNOWN<br/>insufficient evidence"]
    C -->|YES| V["Evidence-backed result"]
    C -->|NO| P["Uncertified claim<br/>or opinion"]

For example:

@engine.register("llm_judge")
def llm_judge(question, evidence, **kwargs):
    return PredicateResult(
        truth=Truth.TRUE,
        certified=False,
        evidence=evidence,
        source="llm",
    )

The LLM can say:

TRUE

without being allowed to say:

CERTIFIED

A deterministic predicate can instead return:

PredicateResult(
    truth=Truth.TRUE,
    certified=True,
    evidence={"pid": 1234},
    source="pgrep",
)

The distinction is intentional.

Certified UNKNOWN

Certification admits UNKNOWN — but only with proof of indeterminacy, never as a bare claim. A predicate returning UNKNOWN + certified=True without an indeterminacy evidence block ({kind, ...proof}) is degraded to uncertified with a warning. Valid kinds: undecidable-reduction, quantum-superposition, jury-hung, exhaustive-empty, vague-boundary, symmetric-tie.

PredicateResult(
    truth=Truth.UNKNOWN,
    certified=True,  # kept: proof attached
    evidence={"indeterminacy": {"kind": "exhaustive-empty",
                                "scanned": 4200}},
)

"Verified absence" (exhaustive search, empty result) is expressed as TRUE-with-evidence, not as UNKNOWN. Precedent: DIALECTICAL_AND already certifies UNKNOWN for proven contradiction.


Recursive questioning

The tree is not merely a data structure for the final answer.

It is the externalized representation of the questioning process.

A complex question can be decomposed:

flowchart TD
    Q["Is deployment safe?"]

    Q --> A["Code quality"]
    Q --> B["Data safety"]
    Q --> C["Compatibility"]

    A --> A1["Tests exist"]
    A --> A2["Complexity below threshold"]

    B --> B1["No PII"]
    B --> B2["Schema valid"]

    C --> C1["LLM contextual judgment"]

The model can generate this structure.

The engine recursively evaluates it.

The model receives the resulting structure rather than having to simulate the complete recursion itself.

That is the architectural experiment.


A minimal tree

from socratic_engine import SocraticEngine

engine = SocraticEngine()

tree = {
    "op": "AND",
    "children": [
        {"predicate": "type_prefix", "args": ["$type", "VSL-"]},
        {"predicate": "ctx_has", "args": ["$ctx", "status"]},
    ],
}

result = engine.evaluate(
    tree,
    {
        "type": "VSL-LANG-01",
        "status": "validated",
    },
)

print(result.truth)
print(result.certified)
print(result.explain())

The engine can evaluate arbitrary nesting because the same contract is applied at every level.


Computation nodes

A computation node evaluates a feature chain natively before deciding — each feature can reference raw inputs and previously computed derived features (d0 → d1 → d2), then a genome-style decision tree branches on the resolved values. Zero information loss versus flattening to predicates; the resolved features travel in evidence.

tree = {"computation": {
    "features": [
        {"op": "add", "args": ["x", "y"], "output_name": "d0"},
        {"op": "mul", "args": ["d0", "x"], "output_name": "d1"},
    ],
    "tree": {"condition": "d1", "threshold": 0.3, "operator": "gt",
             "left": True, "right": False},
}}
result = engine.evaluate(tree, {"x": 0.8, "y": 0.5})

Produced by RSI genome evolution (genome_to_computation_node) and evaluated without translation loss.


Registering domain predicates

The engine contains no domain-specific reasoning beyond its small built-in predicate set.

You add the atomic questions:

from socratic_engine import PredicateResult, Truth

@engine.register("service_running")
def service_running(name: str, **kwargs):
    # Replace with a real structural check.
    if name == "cache":
        return PredicateResult(
            truth=Truth.TRUE,
            certified=True,
            evidence={"pid": 1234},
            source="pgrep",
        )

    return PredicateResult(
        truth=Truth.FALSE,
        certified=True,
        evidence={"service": name, "running": False},
        source="pgrep",
    )

The engine does not need to know what a service is.

It only knows:

predicate → PredicateResult

That keeps the reasoning substrate domain-agnostic.


A complete example

from socratic_engine import SocraticEngine, Truth, PredicateResult

engine = SocraticEngine()


@engine.register("service_running")
def service_running(name: str, **kwargs):
    if name == "cache":
        return PredicateResult(
            truth=Truth.TRUE,
            certified=True,
            evidence={"pid": 1234, "uptime": "2h 15m"},
            source="pgrep",
        )

    return PredicateResult(
        truth=Truth.FALSE,
        certified=True,
        evidence={"service": name},
        source="pgrep",
    )


@engine.register("llm_judge")
def llm_judge(question: str, evidence: str, **kwargs):
    # In production: call the model here.
    return PredicateResult(
        truth=Truth.TRUE,
        certified=False,
        evidence=evidence,
        source="llm",
        metadata={"question": question},
    )


tree = {
    "op": "AND",
    "children": [
        {
            "predicate": "service_running",
            "args": ["cache"],
        },
        {
            "predicate": "llm_judge",
            "kwargs": {
                "question": "Is this deployment safe?",
                "evidence": "Traffic is currently below 45%",
            },
        },
    ],
}

result = engine.evaluate(tree)

print(result.explain())

The important result is not merely the boolean answer.

The evaluation preserves the distinction:

service_running → TRUE  [✓]
llm_judge       → TRUE  [✗]

root            → TRUE  [✗]

The claim can be true according to the predicate while still being uncertified.


Operator semantics

The engine implements:

OperatorLogical semanticsCertification
ANDall branches must be trueall children relevant to the result must be certified
ORat least one branch must be trueone true child must be certified
NOTinvert one propositionchild must be certified
XORexactly one true branchchildren must be certified
IMPLIESantecedent → consequentantecedent and consequent must be certified when relevant
DIALECTICAL_ANDcontradiction is not rejection — a certified TRUE/FALSE conflict yields UNKNOWN (certified), with thesis/antithesis in metadatain conflict: all children certified (the contradiction itself is a fact); without conflict: all children certified
JURYverdict by supermajority (default ⅔, configurable supermajority in (0.5, 1.0], quorum default = all children) — supermajority wins, else hungall votes certified (the verdict, including a hung one, certifies the procedure); no quorum → uncertified

The distinction matters for diagnosis.

For example:

A OR B

does not require both A and B to succeed.

If A is already certified TRUE, B is not a failure merely because it is FALSE or UNKNOWN.

The diagnostic engine follows the semantics of the operator rather than simply collecting every non-TRUE leaf.


Diagnosis: reason backwards from failure

evaluate() tells you what happened.

diagnose() asks:

Which branch actually prevented certification?

flowchart TD
    R["AND → TRUE ✗"] --> A["service_running → TRUE ✓"]
    R --> B["schema_valid → TRUE ✓"]
    R --> C["llm_judge → TRUE ✗"]

    C --> D["diagnostic cause:<br/>truth exists, certification does not"]

This is deliberately an inverse trace.

Instead of returning only:

BLOCKED

the engine can return the structural cause:

llm_judge
truth = TRUE
certified = FALSE
source = llm

That makes the result useful to an agent attempting another iteration.

PROPOSE
   ↓
BUILD
   ↓
EVALUATE
   ↓
DIAGNOSE
   ↓
REFINE
   ↓
EVALUATE
   ↓
...

The agent loop

The intended architecture is not:

LLM → answer

but:

flowchart LR
    A["Agent / LLM"] --> B["Propose tree"]
    B --> C["SocraticTreeBuilder"]
    C --> D["SocraticEngine"]
    D --> E{"Certified?"}
    E -->|yes| F["Decision / gate"]
    E -->|no| G["diagnose()"]
    G --> A

The important recursion is therefore not necessarily inside the language model.

It can occur at the system level:

LLM
 ↓
tree
 ↓
engine
 ↓
evaluation
 ↓
diagnosis
 ↓
LLM
 ↓
refined tree
 ↓
engine
 ↓
...

This is the mechanism this project is intended to make explicit and testable.


VSL as a reasoning representation

The repository includes a small recursive parser for declared:

socratic("NAME") = { ... }

blocks.

That allows the same questioning structure to exist as a language-level artifact rather than only as Python dictionaries.

flowchart LR
    A["Natural language / VSL"] --> B["Socratic tree"]
    B --> C["Validated IR"]
    C --> D["Recursive engine"]
    D --> E["Evaluation"]

This is important for the broader VSL/VSF work:

the language can describe how a question is decomposed, while the engine provides the execution semantics.

The tree therefore acts as an intermediate representation between semantic generation and deterministic execution.


Builder: the boundary before execution

SocraticTreeBuilder exists as a deliberate gate between an external proposer — including an LLM — and the evaluator.

It recursively validates:

  • operators;
  • operator arity;
  • registered predicates;
  • nested children;
  • arguments and keyword arguments.
LLM proposes
     │
     ▼
┌──────────────────┐
│ SocraticTreeBuilder │
│ structural gate  │
└────────┬─────────┘
         │ valid
         ▼
   SocraticEngine

An unknown predicate is rejected before execution.

A malformed NOT or IMPLIES is rejected before execution.

The model is therefore not trusted to produce executable logic merely because the syntax looks plausible.


Built-in predicates

The engine currently ships with deterministic predicates for structural classification and pragmatic behaviour:

PredicateQuestionMissing subject
type_globdoes TYPE match a glob?UNKNOWN
type_prefixdoes TYPE start with a prefix?UNKNOWN
type_regexdoes TYPE match a regex?UNKNOWN
type_hasdoes TYPE contain a token?UNKNOWN
doc_has_statusdoes a document declare a status?evaluated from document
ctx_hasdoes context contain a key/value?UNKNOWN
trend_upis a time series growing sustainably (min_delta, noise guard)?UNKNOWN
trend_downis a time series falling sustainably (min_delta, noise guard)?UNKNOWN
feedback_loopdoes a topology have a closed cycle (length ≥ 2) through target?UNKNOWN

Registering custom predicates is the intended extension point.

Costly predicates can be wrapped with @cached(ttl=...) — hits are marked metadata.cached=True (historical evidence, not a fresh measurement), UNKNOWN is never cached, and engine.cache.clear() forces re-verification.


MCP bridge

The engine can be exposed through the Model Context Protocol.

Install the optional dependency:

python -m pip install -e ".[mcp]"

Run the server:

python -m socratic_engine.mcp_server

The current bridge exposes:

ToolPurpose
socratic_buildvalidate a proposed reasoning tree
socratic_evaluateexecute a tree against context
socratic_diagnosereturn inverse failure traces
socratic_canon_query(opt-in) query a data source through the registered bridge
socratic_canon_matches(opt-in, multi-bridge) check if records match expected values
socratic_canon_field_equals(opt-in, multi-bridge) check if a field equals expected value
socratic_canon_drift(opt-in, multi-bridge) detect declared vs observed drift
socratic_canon_domains(opt-in, multi-bridge) list all available domains
socratic_canon_providers(opt-in, multi-bridge) list all registered providers

The MCP layer is intentionally thin.

It includes a per-tool sliding-window rate limiter (SOCRATIC_MCP_RATE_LIMIT, default 100 calls / SOCRATIC_MCP_RATE_WINDOW, default 60s): a rate-limited call returns error -32029 with retry_after_s — a transient signal, not a rejection.

The reasoning contract remains in the engine.


Bridge to state-canon (official)

The official integration between the engine and state-canon lives in socratic_engine/bridge_statecanon.py.

Division of labor (from the "Relationship to state-canon" section):

state-canon grounds the agent in what is observed. socratic-engine constrains how claims about that state are evaluated.

The bridge registers four predicates on any SocraticEngine, all prefixed canon_ — they query the provider (the reconciled ground truth, observed) and convert the result into certified evidence:

PredicateQuestion it answers
canon_query(domain, filter)Is there at least one record matching the filter?
canon_matches(domain, filter, expected)Do the records match the expected fields exactly?
canon_field_equals(domain, filter, field, expected)Does a specific field equal the expected value?
canon_drift(domain, filter, declared_field, observed_field)Have declared and observed drifted apart?

filter and expected accept either a JSON string or a Python dict.

Semantics follow the engine's epistemology (R9 — no silent concession):

  • TRUE / FALSE are returned certified (structural evidence exists).
  • UNKNOWN is returned uncertified when there is no evidence — no records, an unknown filter field, a missing field, or an unparseable filter. No inventing, no silent routing to else_home.
from socratic_engine import SocraticEngine
from socratic_engine.bridge_statecanon import StateCanonBridge

eng = SocraticEngine()
bridge = StateCanonBridge(eng, provider)   # provider: state_canon.StateProvider

ev = eng.evaluate({"op": "DIALECTICAL_AND", "children": [
    {"predicate": "canon_field_equals",
     "args": ["services", '{"name": "api"}', "declared_active", True]},
    {"predicate": "canon_field_equals",
     "args": ["services", '{"name": "api"}', "observed_active", True]},
]})
# declared TRUE, observed FALSE → certified conflict → UNKNOWN certified,
# with metadata.thesis / metadata.antithesis — the dialectical operator
# turns the drift into productive indetermination, not a false binary.

The MCP server accepts an optional provider (opt-in, R6): when set, the canon_* predicates are registered and socratic_canon_query appears in tools/list.

Multi-bridge (new)

For scenarios requiring certification across multiple data sources, the MultiBridge routes canon_* predicates to different providers by domain:

from socratic_engine.multi_bridge import MultiBridge

bridge = MultiBridge()
bridge.add_provider("agent-state", vsm_provider, ["tasks", "sessions"])
bridge.add_provider("infra-state", ssh_provider, ["services", "lxcs"])
bridge.add_provider("vsm-docs", doc_provider, ["boot", "s1-operations"])
bridge.register(eng)

# Now the engine can certify across all domains:
ev = eng.evaluate({"op": "AND", "children": [
    {"predicate": "canon_query", "args": ["services", '{"name": "api"}']},
    {"predicate": "canon_query", "args": ["boot", '{"name": "covenant"}']},
]})

Or load from config:

SOCRATIC_BRIDGE_CONFIG=bridge_config.json python -m socratic_engine.mcp_server

Provider health tracking: each provider tracks consecutive failures. After 3 failures, the provider is marked unhealthy and canon_providers returns UNKNOWN. Failed queries return UNKNOWN (not FALSE) — a provider crash is indetermination, not falsity.

Routing observability: every canon_* predicate includes routing info in its evidence: provider name, domain, latency_ms, record_count. This makes provider selection inspectable in the evaluation tree.

Duplicate-domain priority: add_provider(..., priority=N) and the priority field in bridge config (default 0, higher wins; tie keeps the first registered). Introspection (canon_providers, canon_domains) never mutates health — observing a provider must not heal it.

See socratic_engine/multi_bridge.py for the full API and config format.


CLI

Evaluate a tree

socratic-engine eval-tree tree.json --doc-type THEORY-VC-01

A successful response contains the structured decision together with its explanation and diagnostic information.

Match a procedure

socratic-engine match-procedure --input '{"instrument":"x","hook":"session.idle","context":{},"repoRoot":"."}'

Matches current context against past procedures in learning_records/procedures/ (instrument/hook/success/recency scoring). Returns reuse / adapt / skip with confidence. Thin-dispatcher counterpart to the opencode scenario-memory plugin.

Inline VSL notation

Trees can also be written inline: socratic(predicate, arg, ...) is shorthand for {"predicate": ..., "args": [...]}.

socratic-engine eval-tree <(echo 'socratic(type_prefix, "$type", "VSL-")') --doc-type VSL-X

Decide: evaluate decisions before execution

The decide command evaluates decisions through the socratic engine, enabling RSI (Recursive Self-Improvement) by logging decision outcomes for learning.

# Simple reversible decision
socratic-engine decide --decision "Use JSONL format" --json

# Decision with alternatives and impact
socratic-engine decide \
  --decision "Restructure docs/" \
  --alternatives "Keep,Reorganize" \
  --impact "All references" \
  --json

# Irreversible decision (requires approval)
socratic-engine decide \
  --decision "Delete production data" \
  --reversible false \
  --approved \
  --json

# Additional context
socratic-engine decide \
  --decision "Deploy v2.0" \
  --context '{"environment": "production", "rollback_plan": true}' \
  --json

Decision logic:

  • Reversible decisions (--reversible true, default) pass automatically
  • Irreversible decisions (--reversible false) require --approved flag
  • Without approval, irreversible decisions return UNKNOWN (uncertified)

Output (JSON):

{
  "truth": "TRUE",
  "certified": true,
  "home": "pass",
  "explain": "op:AND → true [✓]...",
  "decision": "Use JSONL format",
  "context": {
    "reversible": "true",
    "has_alternatives": false,
    "has_impact": false,
    "approved": false
  }
}

This makes the same engine usable from:

  • agent write-time gates;
  • commit hooks;
  • scripts;
  • CI/CD;
  • MCP clients;
  • other VSM/VSF components;
  • decision evaluation before execution (RSI integration).

Install

From PyPI (v0.2.10)

pip install socratic-engine

With MCP support

pip install "socratic-engine[mcp]"

From source

git clone https://github.com/rm-w3kufe/socratic-engine.git
cd socratic-engine

python3 -m venv .venv
.venv/bin/python -m pip install -e .

Run the tests

python3 -m pytest tests/ -q

The current repository snapshot passes:

519 passed

at 100% statement coverage (CI gates at 90%; uncovered lines are documented # pragma: no cover defensive branches — see the rationale inline in the source).

(includes integration tests with state-canon, available when that package is installed or reachable at ~/state-canon-mcp)

Releases

Releases are automated via GitHub Actions, triggered by a tag. The workflow verifies version sync across pyproject.toml, __init__.py, mcp_server.py, and tests before publishing to PyPI.

# Ship a new version:
git tag vX.Y.Z
git push origin vX.Y.Z

See .github/workflows/release.yml for details.


Repository layout

socratic-engine/
├── README.md
├── ROADMAP.md
├── pyproject.toml
├── LICENSE
├── benchmarks/
│   └── benchmark.py        # deep/wide trees, cache speedup, diagnose
├── docs/
│   ├── ONTOLOGY.md         # epistemic model
│   ├── ARCHITECTURE.md     # how it fits together (code-faithful)
│   └── EXAMPLES.md         # executable use cases
├── socratic_engine/
│   ├── __init__.py
│   ├── engine.py           # trivalent evaluator + certification + diagnosis
│   ├── tree.py             # builder + VSL parser + tree routing + TreeExecutor
│   ├── cli.py              # command-line interface
│   ├── mcp_server.py       # MCP / JSON-RPC bridge + rate limiter
│   ├── multi_bridge.py     # multi-provider routing (canon_* by domain)
│   ├── bridge_statecanon.py # official state-canon bridge (opt-in)
│   ├── semantics.py        # semantic simplification (NOT flattening, contradiction/tautology, dedup, absorption)
│   ├── engine_contract.py  # SocraticEngineProtocol + EvaluationProtocol
│   └── providers/
│       ├── __init__.py
│       └── vsm_doc.py      # VSM documentation filesystem provider
└── tests/
    ├── test_engine.py
    ├── test_mcp_server.py
    ├── test_cli.py
    ├── test_cli_decide.py
    ├── test_bridge_statecanon.py
    ├── test_multi_bridge.py
    ├── test_vsm_doc.py
    ├── test_state_canon_integration.py
    ├── test_enforce_limits.py
    ├── test_engine_contract.py
    ├── test_falsification.py
    ├── test_recursion.py
    └── test_semantics.py

Relationship to state-canon

socratic-engine and state-canon solve different parts of the same larger problem.

flowchart LR
    A["Agent / LLM"]

    A --> B["state-canon"]
    A --> C["socratic-engine"]

    B --> B1["What is the current state?"]
    B1 --> B2["reconciled ground truth"]

    C --> C1["What must be true?"]
    C1 --> C2["recursive certification"]

    B2 --> D["Grounded decision"]
    C2 --> D

A useful way to state the division is:

state-canon grounds the agent in what is observed.
socratic-engine constrains how claims about that state are evaluated.

state-canon answers questions such as:

"What is actually running?"
"Has reality drifted from the declared state?"

socratic-engine answers questions such as:

"Given these premises, what follows?"
"Which branch prevents certification?"
"Is there enough evidence to pass this gate?"

Together they can form a larger agent-governance loop:

          observed reality
                 │
                 ▼
           state-canon
                 │
          grounded context
                 │
                 ▼
        socratic-engine
                 │
       certified evaluation
                 │
                 ▼
             decision
                 │
                 ▼
            vsf-rsi
                 │
         learned pattern
                 │
                 ▼
          future improvement

The important boundary remains the same:

The thing that produces a claim should not be the sole authority that certifies the claim.

Three-tool integration

socratic-engine is the reasoning substrate. Combined with state-canon and vsf-rsi:

ToolRoleQuestion it answers
state-canonGround truth"What is actually true?"
socratic-engineReasoning"What follows from these premises?"
vsf-rsiLearning"What should we remember for next time?"

Integration by platform

OpenCode — add to opencode.json:

{
  "mcp": {
    "socratic-engine": {
      "type": "local",
      "command": ["python3", "/path/to/socratic_mcp_with_bridge.py"]
    }
  }
}

Claude Code — add to CLAUDE.md:

## Tools
- `socratic_engine.evaluate(tree, context)` — evaluate decisions

Any Python agent:

from socratic_engine import SocraticEngine
engine = SocraticEngine()
result = engine.evaluate(tree, context)

Human CLI:

pip install socratic-engine
python -m socratic_engine evaluate tree.json --context ctx.json

Full pattern

from socratic_engine import SocraticEngine
from state_canon import StateCanon
from vsf_rsi.scenario_memory import record

# 1. Get ground truth
canon = StateCanon()
state = canon.query("services", {"name": "api"})

# 2. Evaluate decision
engine = SocraticEngine()
result = engine.evaluate(tree, {"state": state})

# 3. Learn from outcome
record({
    "fault_signature": "api_timeout",
    "decision": "increase_timeout",
    "outcome": "success" if result.certified else "failure"
})

Measured, not proclaimed

The central research question is empirical:

Does externalizing recursive reasoning into an executable tree improve reliability on tasks where the same model performs poorly when recursion is kept implicit?

This repository is therefore better treated as an experimental instrument than as a claim that the problem has already been solved.

A useful comparison is:

CONTROL
LLM
 ↓
recursive problem
 ↓
answer


EXTERNALIZED
LLM
 ↓
reasoning tree
 ↓
SocraticEngine
 ↓
evaluation / diagnosis
 ↓
LLM
 ↓
answer

The measurements that matter include:

  • logical correctness;
  • premise retention;
  • contradiction rate;
  • recursive depth successfully handled;
  • recovery after a failed branch;
  • number of model iterations;
  • token cost;
  • deterministic reproducibility.

A result that saves tokens while producing worse decisions is not a win.


Security & robustness (v0.2.10)

Tree limits (enabled by default)

The engine prevents pathological trees from crashing the process:

LimitValueConfigurable
Max depth100No (hardcoded)
Max nodes10,000No (hardcoded)
Cycle detectionEnabledVia _visited set
# Limits are ON by default (v0.2.9+)
engine.evaluate(tree)  # Safe — limits enforced

# Disable only when you know what you're doing
engine.evaluate(tree, enforce_limits=False)

Arity validation

Operators validate their number of children:

OperatorRequired childrenError on mismatch
NOTExactly 1ValueError
IMPLIESExactly 2ValueError
XORExactly 2ValueError
AND/OR≥0 (warns if 0)UserWarning

Predicate safety

  • Error wrapping: Exceptions from user predicates are wrapped in RuntimeError with context (name, args, kwargs)
  • Overwrite warning: Registering a predicate with an existing name emits UserWarning
  • No shell injection: subprocess calls use argument lists, not shell=True

Verified by independent audit

  • 22 independent logic tests (Kleene trivalence, short-circuit, De Morgan): 22/22 correct
  • Thread-safety: 16 threads × 200 evaluations on shared engine: no errors
  • Stack limits: RecursionError at ~498 nested NOT (without limits), clean ValueError with limits
  • Scaling: O(n) confirmed (100→1600 children, ratio ≤2.04x)
  • Decision evaluation: 16 tests for decide command (reversible/irreversible/approval)

Design principles

R9 — no silent concession

If a proposition cannot be decided, return UNKNOWN.

Do not silently convert missing evidence into a false proposition or route an undecidable branch through an implicit fallback.

R10 — opinion is not evidence

An LLM can generate a judgment.

It cannot make that judgment certified merely by asserting it.

R10.1 — the LLM proposes, the engine executes

The external model may propose a reasoning tree.

The builder validates its structure.

The engine executes the recursion.

Domain agnosticism

The engine does not contain the meaning of the domain.

Predicates do.

Traceability

Every recursive evaluation remains inspectable.

The result is not just a scalar; it is a tree.

Diagnosis

Failure should produce a useful next question, not merely a blocked state.


What this is not

socratic-engine is:

  • not an agent framework;
  • not a prompt-engineering framework;
  • not a vector database;
  • not a replacement for an LLM;
  • not a claim that symbolic logic alone produces intelligence.

It is a reasoning substrate.

Its purpose is narrower:

externalize recursive logical structure so that a language model can participate in complex reasoning without being solely responsible for executing the recursion.


Roadmap

See ROADMAP.md for the full development history and future plans.

Current status

  • v0.2.11 — decide CLI command: evaluate decisions before execution for RSI integration (519 tests)
  • v0.2.10 — Context predicates: ctx_equals, ctx_contains, ctx_not_has for tree-based evaluation (503 tests)
  • v0.2.9 — Security hardening: enforce_limits=True by default, cycle detection, XOR arity validation, predicate error wrapping, predicate overwrite warnings (479 tests)
  • v0.2.8 — published on PyPI (engine contract protocols, _TreeLimitCounter bypass fix, 479 tests)
  • v0.2.7 — _TreeLimitCounter bypass fix, 11 regression tests
  • v0.3.x — paraconsistent logic, frame semantics, stakeholder graphs

The roadmap is intentionally open: the next features should be driven by failures observed in the experimental programme, not by feature accumulation.


Going deeper

  • docs/ONTOLOGY.md — the epistemic model: truth, certification, UNKNOWN, operator semantics, and the LLM boundary.
  • docs/ARCHITECTURE.md — how the modules fit together, written against the code.
  • docs/EXAMPLES.md — executable use cases: classification, deploy gates, diagnosis, dialectics, caching, rate limiting.
  • ROADMAP.md — current development programme (tracked as issue #1).
  • benchmarks/benchmark.py — performance numbers (deep/wide trees, cache speedup, diagnose).
  • tests/ — executable specification of the current semantics.

Lineage & philosophy

Built on Stafford Beer's Viable System Model and the Cybersyn project (Chile, 1971) — a system is viable when it can be described, governed, and audited. "Don't trust, verify" isn't a slogan here; it's the reconciler and the verify-at-every-boundary discipline made mechanical. Community-first, and deliberately from the Global South — the heir to Cybersyn's bet that good cybernetics serves people.

License

Code: Apache-2.0 (LICENSE) · Docs: CC-BY-4.0.

This module is deliberately more permissive than the AGPL core it was extracted from — it is meant to be adopted, embedded, and improved by the community. Improvements can flow back; the module stays clean of AGPL code by construction.

↑↓ navigate openesc close