Volute Reasoning Engine — decorator-based epistemic enforcement
Project description
VRE — Volute Reasoning Engine
Stability
As of v1.0.0, VRE is stable. The public API is everything exported from
the top-level vre package and vre.core — the VRE class, the vre_guard
decorator, the repository backends, and the structured result and gap models they
return. Anything reached through a private submodule path or a leading-underscore
name is internal and may change without notice. The project follows semantic
versioning: breaking changes to the public
API land only in major releases and are preceded by a deprecation period with
runtime warnings. Every change is recorded in the CHANGELOG.
Epistemic enforcement for autonomous agents.
VRE is a Python library that gives an autonomous agent an explicit, inspectable model of what it knows, and refuses to let it act past that boundary. Before a guarded tool executes, VRE checks that the concepts the action touches are grounded, to the depth the graph requires, in an authored knowledge graph. If they are, the action proceeds. If they are not, it is blocked and the specific gap is surfaced as a structured object rather than a generic error.
It is deliberately narrow about what that means. VRE enforces consistency with a modeled domain. It does not manufacture understanding, verify that the model is correct, or check the truth of what the agent does. It is not a permissions system, not a sandbox, not a correctness guarantee, and not a hallucination filter. What it provides is one thing, done deterministically: an action executes only if it is epistemically justified against the graph and permitted by the policies on the edges it touches.
That last point is the distinctive one. The constraints VRE enforces are not a ruleset bolted alongside the knowledge. They are carried by the graph's own structure: depth requirements derive from where an edge sits, and policy gates live on the edges themselves. What an agent may do and what it understands are the same graph read two ways. The policy is the model.
VRE is precise about what it does and does not guarantee. Before relying on it, read the Trust Model, which states the guarantee, the assumptions it rests on, and the failure modes in full.
Table of Contents
- The Problem
- How It Works
- Getting Started
- Core API
- The
vre_guardDecorator - Callbacks
- Learning
- Policy System
- Integrations
- Future
- Tech Stack
- Project Structure
- Contributing
The Problem
Modern LLM-based agents fail in a specific and consistent way: they act as if they know more than they can justify.
This is not a capability problem. The models are capable. It is an epistemic problem — the agent has no internal representation of the boundary between what it can justify and what it is confabulating. Hallucination, unsafe execution, and overconfident planning are all symptoms of the same root cause: epistemic opacity.
When an agent is asked to delete files, migrate a database, or execute a shell command, the question is not only "can I do this?" but "do I have the knowledge to do it safely?" Current systems have no mechanism to answer that second question. They proceed anyway.
This is not hypothetical. In December 2025, Amazon's Kiro agent — given operator-level access to fix a small issue in AWS Cost Explorer — decided the correct approach was to delete and recreate the environment entirely, causing a 13-hour outage. In February 2026, OpenClaw deleted the inbox of Summer Yue — Meta's Director of AI Alignment — after context window compaction silently discarded her instruction to wait for approval before taking action. In each case, the agent acted confidently on knowledge it could not justify. The safety constraints were linguistic — instructions that could be forgotten, overridden, or reasoned around. VRE's constraints are structural.
VRE addresses this directly. It imposes a contract: before an action executes, the agent must demonstrate that the relevant concepts are grounded in the knowledge graph at the depth required for execution. If they are not, the action is blocked and the gap is surfaced explicitly. The agent does not guess. It does not proceed on partial knowledge. It is structurally incapable of executing an action that is not grounded in its epistemic model — and perhaps more importantly, it surfaces what it does not know. Absence of knowledge is treated as a first-class object.
How It Works
The Epistemic Graph
VRE maintains a graph of primitives — conceptual entities like file, create, permission, directory. These
are not tools or commands. They are concepts: the things an agent reasons about, not the mechanisms it uses to act.
Each primitive is grounded across a hierarchy of depth levels:
| Depth | Name | Question answered |
|---|---|---|
| D0 | EXISTENCE | Does this concept exist? |
| D1 | IDENTITY | What is it, in principle? |
| D2 | CAPABILITIES | What can happen to it / what can it do? |
| D3 | CONSTRAINTS | Under what conditions does that hold? |
| D4+ | IMPLICATIONS | What follows if it happens? |
Depth is monotonic: D3 grounding implies D0–D2 are also grounded. Depth requirements are derived from the graph structure itself — edges carry a source depth that determines when they become visible and a target depth that determines when they resolve. An integrator can also enforce a minimum depth floor (e.g. D3 for execution) as a secondary safety lever.
Why five levels, and why this order? The ladder is a deliberate theory of what it takes to understand a concept
well enough to act on it, not an arbitrary partition. Each rung answers a strictly harder question than the one below
it: that a thing exists (D0), what it is (D1), what can be done with it (D2), what bounds those operations (D3),
and what follows from them (D4+). The order is what lets edge placement encode policy. Because depth is monotonic,
placing a relatum at a deeper source level means everything shallower must be grounded before that relationship is even
visible; authoring an action's targeting edge at CONSTRAINTS (D3) rather than CAPABILITIES (D2) is how a seeder says
"you may not even resolve what this action applies to until you have grounded what bounds it." Whether an action is
permitted is then the product of resolving its grounded subgraph against that topology, never a fixed depth threshold.
A min_depth floor is an optional secondary lever an integrator can add on top, not the mechanism.
The ordering is opinionated, and it bends in some domains. In law or medicine the implications of an act often are its constraints, since a consequence can be exactly what forbids the act, so the clean D3-before-D4 split blurs. VRE does not claim the ladder is universal. It claims only that depth is contiguous and that the integrator places each edge at the level where, in their domain, the fact actually becomes true. The five levels are the default vocabulary, not an assertion that every domain carves understanding at exactly these joints.
Relata
Primitives are connected by typed, directional, depth-aware relata:
create --[APPLIES_TO @ D2]--> file
file --[CONSTRAINED_BY @ D3]--> permission
A relatum declares that understanding one concept at a given depth requires understanding another concept at a specified depth. When VRE resolves a grounding query, it follows these dependencies and checks that the entire connected subgraph meets the required depth. A relational gap — where a dependency's target is not grounded deeply enough — is surfaced as a distinct gap type.
Knowledge Gaps
When grounding fails, VRE returns structured gap objects — not generic errors. There are four gap types:
| Type | Meaning |
|---|---|
ExistenceGap |
The concept is not in the graph at all |
DepthGap |
The concept exists but is not grounded to the required depth |
RelationalGap |
A relatum's target does not meet the depth required by that edge |
ReachabilityGap |
The concept is not connected to the other submitted concepts |
Gaps are not failures to be hidden. They are information. An existence gap on network tells you the agent has no
epistemic model of networking — not that the request was malformed. The agent can
surface this gap to the user, initiate a learning flow, or escalate to a human.
When does a gap actually block? Grounding is closure-strict: check() follows the transitive prerequisites of the
submitted concepts, and a gap anywhere in that closure, not only on the concepts you named, makes grounded False.
That sounds strict, but it only ever blocks on modeled ignorance. A gated edge whose target is not grounded deeply
enough is a recorded known-unknown, the graph saying "there is more about this concept beyond your current depth, and
it matters," and acting past it is exactly the dishonesty VRE prevents. A constraint that no edge records is an
unknown-unknown: grounding cannot see it, so it does not block and the action proceeds. This is why a partial graph is
still actionable, and why a mechanical failure on an unmodeled constraint is a learning signal (see
Learning) rather than an epistemic error. A richer graph adds more context and more known-unknowns to
honor; it does not change the enforcement mechanism.
Getting Started
Installation
pip install vre
# or with Poetry
poetry add vre
VRE ships with a SQLite backend that works out of the box — no external services required.
The database defaults to ~/.vre/graph.db and is created automatically on first use.
For production deployments or larger graphs, an optional Neo4j backend is available:
pip install 'vre[neo4j]'
Infrastructure
SQLite (default) — no setup needed. The database file is created automatically.
Neo4j (optional) — requires a running Neo4j instance:
docker run -d \
--name neo4j \
-p 7474:7474 -p 7687:7687 \
-e NEO4J_AUTH=neo4j/password \
neo4j:latest
Seeding the Graph
The VRE repository ships with domain seeders in seeders/ and a
gap-demonstration script in scripts/. Seeders upsert primitives
by name (idempotent re-runs); the demo script clears the graph first to
produce its deterministic output. Use scripts/clear_graph.py if you want
a clean slate before seeding. See scripts/README.md
for details.
All scripts default to the SQLite backend. Pass --backend neo4j with
connection flags to use Neo4j instead.
# Fully grounded filesystem domain — 20 primitives, all at D3+ with complete relata
python seeders/seed_filesystem.py
# Gap demonstration graph — 10 primitives, deliberately shaped to produce each gap type
python scripts/seed_gaps.py
# Same commands with Neo4j:
python seeders/seed_filesystem.py \
--backend neo4j --neo4j-uri neo4j://localhost:7687 --neo4j-user neo4j --neo4j-password password
Core API
Connecting to VRE
from vre import VRE, SQLiteRepository
repo = SQLiteRepository() # defaults to ~/.vre/graph.db
vre = VRE(repo)
For a self-contained, ephemeral graph (tests, quickstarts, throwaway scripts),
pass :memory: and disable trace files so nothing touches disk:
repo = SQLiteRepository(":memory:") # ephemeral, no graph file written
vre = VRE(repo, persist_traces=False) # also skip JSONL trace files (see Checking Grounding)
Or with Neo4j:
from vre import VRE
from vre.core.backends import Neo4jRepository
repo = Neo4jRepository(
uri="neo4j://localhost:7687",
user="neo4j",
password="password",
)
vre = VRE(repo)
Agent Identity
An optional agent_key associates the VRE instance with a stable agent identity. The key is resolved via a file-based
registry (~/.vre/agents.json) so that the same key always maps to the same UUID,
even across restarts. When configured, every GroundingResult carries the agent's agent_id.
vre = VRE(repo, agent_key="my-agent", agent_name="My Agent")
vre.identity.agent_id # stable UUID, persisted across restarts
vre.identity.name # "My Agent"
agent_name is a human-readable label used only on first registration — subsequent calls with the same key return the
existing identity. Both parameters are optional; without agent_key, traces are
anonymous and vre.identity is None. You may also pass registry_path to customize the registry file location (
default: ~/.vre/agents.json).
Checking Grounding
result = vre.check(["create", "file"])
print(result.grounded) # True / False
print(result.resolved) # ["create", "file"] — canonical names after resolution
print(result.gaps) # [] or list of KnowledgeGap instances
print(result) # Full formatted epistemic trace
vre.check() derives depth requirements from graph structure — edges at higher source depths are only visible when the
source primitive is grounded to that depth. An optional min_depth parameter
lets integrators enforce a stricter floor (e.g. D3 for execution). If any concept is unknown, lacks the required depth,
has an unmet relational dependency, or is disconnected from the other submitted
concepts, grounded is False and the corresponding gaps are surfaced.
vre.check() is not side-effect-free. On each call it updates per-primitive grounding metrics in the graph
(best-effort: failures are logged, never raised) and, unless you constructed VRE with persist_traces=False,
appends a line to a daily JSONL trace under ~/.vre/traces/. Two consequences are worth planning for. The metrics
write-back means check() needs write access to the backend, so a strict read-replica or least-privilege
deployment will see those updates warn-and-skip rather than persist. And the trace directory grows unbounded, so
rotate it or turn it off with persist_traces=False where that matters.
Using the Trace as Agent Context
vre.check() can be called before an agent runs to pre-load the epistemic trace into the model's context window. Rather
than letting the LLM reason from general knowledge alone, you give it the
graph's structured knowledge of the relevant concepts before it decides what to do.
result = vre.check(["delete", "file"])
if result.grounded:
context = str(result) # full structured trace, formatted for readability
response = llm.invoke([
SystemMessage(content="You are a filesystem agent."),
SystemMessage(content=f"Epistemic context:\n{context}"),
HumanMessage(content=user_input),
])
else:
for gap in result.gaps:
print(f"Knowledge gap: {gap}")
This is particularly useful for planning-mode interactions: the agent receives structured knowledge of the relevant epistemic space before it proposes an action.
Checking Policy
policy = vre.check_policy(["delete", "file"], cardinality="multiple")
if policy.action == "BLOCK":
print(policy.reason)
for v in policy.violations:
print(f" - {v.message}")
check_policy evaluates over a grounded result, and grounding comes first. Passing concept names (as here) grounds
them internally via check() first, which records metrics and a trace exactly as a direct check() call does; an
ungrounded result fails closed with BLOCK before any policy runs, because policy enforcement is only meaningful
over a grounded closure. To reuse a grounding you already computed, pass a GroundingResult instead of names.
Called without a tool_call (as here), a callback-bearing policy can't be evaluated, so the gate fails closed and
the policy fires — pass tool_call=ToolCallContext(...) (as vre_guard does) when you want the callback consulted.
cardinality hints whether the operation targets a single entity ("single") or many ("multiple", e.g. recursive or
glob). An optional on_policy callback handles violations that require human
confirmation — it receives only the confirmation-required violations and returns True to proceed or False to block.
The vre_guard Decorator
vre_guard is the primary integration point. It wraps any callable and gates it behind a grounding check and a policy
evaluation before the function body executes. This is designed to wrap the tools
your agent uses to act on the world, ensuring that every action is epistemically justified and compliant with your
defined policies.
from vre.guard import vre_guard
@vre_guard(vre, concepts=["write", "file"])
def write_file(path: str, content: str) -> str:
...
Parameters
vre_guard(
vre, # VRE instance
concepts, # list[str] or Callable(*args, **kwargs) -> list[str]
cardinality=None, # str | None or Callable(*args, **kwargs) -> str | None
min_depth=None, # DepthLevel | None — enforces a minimum depth floor
on_trace=None, # Callable[[GroundingResult], None]
on_policy=None, # Callable[[list[PolicyViolation]], bool]
)
The guard does not orchestrate learning. When grounding fails, it returns the
GroundingResult and lets the integrator decide what to do next — typically
by exposing a separate learn_gaps tool that the agent can invoke. See
Learning.
concepts can be static or dynamic. Static is appropriate when a function always touches the same concept domain.
Dynamic is appropriate when the concepts depend on the actual arguments — for
example, a shell tool that must inspect the command string:
concepts = extract_concepts # your callable: maps the command string to primitives
@vre_guard(vre, concepts=concepts)
def shell_tool(command: str) -> str:
...
VRE does not own concept extraction. The integrator decides how to map tool arguments to primitives — an LLM call, a static alias table, a rule engine, or any combination.
cardinality can also be static or dynamic. When dynamic, it receives the same arguments as the decorated function:
def get_cardinality(command: str) -> str:
flags = {"-r", "-R", "-rf", "--recursive"}
tokens = set(command.split())
has_glob = any("*" in t for t in tokens)
return "multiple" if (flags & tokens or has_glob) else "single"
@vre_guard(vre, concepts=concepts, cardinality=get_cardinality)
def shell_tool(command: str) -> str:
...
Execution Flow
Each call runs the following sequence:
- Resolve concepts — map names to canonical primitives via the graph
- Ground — verify the subgraph meets depth requirements (graph-derived + optional
min_depthfloor) - Fire
on_trace— surface the epistemic result to the caller - If not grounded — return the
GroundingResultimmediately; the function does not execute - Evaluate policies — check all
APPLIES_TOrelata for applicable policy gates - If hard blocks — return
PolicyResult(BLOCK)immediately;on_policyis not consulted - If confirmation required — call
on_policywith pending violations; block if declined or no handler - If BLOCK — return the
PolicyResult; the function does not execute - Execute — call the original function and return its result
Concept Extraction and the Strength of the Guarantee
VRE's guarantee is structural between concept extraction and execution: once a call's concepts are fixed, grounding and policy are deterministic and cannot be talked around. But something has to turn a tool call into that list of concepts, and how it does so decides how strong the guarantee really is.
When concepts is a static list on the decorator, the conceptual footprint is fixed at code-authoring time. There is
no linguistic step between the call and the check, and the structural guarantee holds end to end. When concepts is a
callable, the footprint is computed at call time from the arguments, and that extraction step is a front door made of
language, whether you implement it with an LLM or with a rule-based parser. A broad tool whose meaning lives in its
arguments (a single shell_tool, say) forces dynamic extraction, because its static worst-case footprint is
"everything"; and a rule-based extractor for such a tool has to win its own arms race against $(...), aliases, and
base64 | sh.
The honest formulation: the strength of VRE's guarantee is a function of tool granularity. The strong-guarantee path is to decompose broad tools into narrow ones with static concepts, so that what an action touches is declared, not inferred. Treat dynamic extraction as a deliberate, documented downgrade for the cases that genuinely need it, not the default. This is also the direction agent tooling is already moving, toward narrow, single-purpose tools rather than one general shell hammer, so the safe path and the idiomatic one converge. The Trust Model states the same boundary from the other side: the determinism is of the enforcement path, given a faithful concept declaration.
Callbacks
on_trace
Called after grounding, whether grounded or not. Receives the full GroundingResult. Use this to render the epistemic
trace to your UI.
def on_trace(grounding: GroundingResult) -> None:
if grounding.grounded:
print(f"Grounded: {grounding.resolved}")
else:
for gap in grounding.gaps:
print(f"Gap: {gap}")
GroundingResult carries:
grounded: bool— whether all concepts are grounded with no gapsresolved: list[str]— canonical primitive names (or original if unresolvable)gaps: list[KnowledgeGap]— structured gap descriptions (ExistenceGap,DepthGap,RelationalGap,ReachabilityGap)trace: EpistemicResponse | None— the full subgraph with all primitives, depths, relata, and pathwayagent_id: UUID | None— the stable agent identifier, when the VRE instance was created with anagent_key
For convenience, result.get_primitives() and result.get_pathway_steps() return the trace's primitives
and pathway steps directly (or empty lists when no trace is present), so callers don't have to drill into
result.trace.result.* themselves.
The reference integration renders on_trace as a Rich tree:
VRE Epistemic Check
├── ◈ create ● ● ● ●
│ ├── APPLIES_TO → file (target D2)
│ └── REQUIRES → filesystem (target D3)
├── ◈ file ● ● ● ●
│ └── CONSTRAINED_BY → permission (target D3)
└── ✓ Grounded — EPISTEMIC PERMISSION GRANTED
on_policy
Called when policy evaluation produces violations that require human confirmation (requires_confirmation=True). Hard
blocks (requires_confirmation=False) are handled before on_policy is ever
consulted. Returns True to proceed, False to block.
from vre.core.policy.models import PolicyViolation
def on_policy(violations: list[PolicyViolation]) -> bool:
for v in violations:
answer = input(f"Policy gate: {v.message} [y/N]: ").strip().lower()
if answer != "y":
return False
return True
If on_policy is not provided and a policy requires confirmation, the guard returns
PolicyResult(action=PolicyAction.BLOCK) and the function does not execute.
Learning
VRE is a knowledge linter, not a knowledge builder. It identifies gaps and validates fills; the integrator owns the loop. When grounding fails, the integrator decides whether to surface the gaps to the user, escalate to a human, or run a learning loop that grows the graph through use.
This separation is deliberate. Loop orchestration is inherently integration-specific — different LLMs, different data sources, different retry/budget strategies. By keeping VRE's surface tight (identify gaps, persist fills), integrators can build whatever flow fits their stack without fighting the framework.
How It Works
VRE exposes three things:
vre.check(concepts)returns aGroundingResultwith structuredKnowledgeGapobjects when grounding failstemplate_for_gap(gap)returns a candidate to fill. For depth and relational gaps it is pre-seeded with the exact missing levels (gap.missing_levels— the holes in(current, required]not already present), one empty slot each, so the integrator fills only theproperties. VRE resolves which levels are missing; the integrator supplies what they contain (LLM structured output, user input, static rules)vre.learning_engine.learn_gap(gap, candidate)validates the candidate against the live graph and persists it (always stampedLEARNED)
A typical integrator-owned loop looks like this:
from vre.learning.templates import template_for_gap
grounding = vre.check(["delete", "file"])
while not grounding.grounded and grounding.gaps:
gap = grounding.gaps[0]
candidate = template_for_gap(gap) # pre-seeded with gap.missing_levels
filled = my_llm_fill(candidate, gap, grounding) # fill the properties of each slot
if filled is None:
break
vre.learning_engine.learn_gap(gap, filled)
grounding = vre.check(["delete", "file"])
learn_gap raises CandidateValidationError if the candidate is malformed or its prerequisites are not met (e.g.
re-authoring an already-grounded level, or placing an edge at a depth the source does not have), and GapResolvedError
if the gap already closed underneath the candidate. The integrator catches the error, revises or re-grounds, and
retries.
Candidate Types
Each gap type has a corresponding candidate model. Candidates carry only what's new — all context (primitive IDs, existing depths, required depths) lives on the gap itself.
| Gap Type | Candidate | What the Integrator Fills In |
|---|---|---|
ExistenceGap |
ExistenceCandidate |
D1 identity for a new concept (D0 is auto-generated) |
DepthGap |
DepthCandidate |
Missing depth levels with properties |
RelationalGap |
RelationalCandidate |
Missing depth levels on the edge target |
ReachabilityGap |
ReachabilityCandidate |
Edge placement: source name, target name, relation type, source/target depths |
ExistenceCandidate, DepthCandidate, and RelationalCandidate all use ProposedDepth:
from vre.learning.models import ProposedDepth
ProposedDepth(
level=DepthLevel.CAPABILITIES,
properties={"operations": ["read", "write"], "attributes": ["size", "permissions"]},
)
ReachabilityCandidate declares both source and target by name. At least one of them must match the gap's
primitive — the edge must fix this disconnection — but the integrator chooses the direction. An edge from an
existing connected node back to the orphan is just as valid as one originating from the orphan.
Provenance
learn_gap accepts an optional source: ProvenanceSource parameter (default LEARNED). The integrator decides
how to stamp persisted knowledge based on its own loop semantics — LEARNED for agent-proposed fills approved at
the persistence boundary, AUTHORED for content a human drafted directly. Both are human-attested by construction;
provenance is genealogy, not a trust gradient. The graph remembers not just what it knows, but how it came to know it.
Reachability Prerequisites
ReachabilityCandidate focuses solely on edge placement — it declares where the edge goes, not what depths need
to exist. If the source or target lacks the required depth level, learn_gap raises CandidateValidationError.
To handle this cleanly, the engine exposes reachability_prerequisites(gap, candidate) which returns a list of
DepthGap objects that must be filled before the edge can be placed. The integrator's loop checks prerequisites,
fills them, and only then calls learn_gap for the reachability candidate.
prereqs = vre.learning_engine.reachability_prerequisites(gap, filled)
for depth_gap in prereqs:
depth_filled = my_llm_fill(template_for_gap(depth_gap), depth_gap, grounding)
vre.learning_engine.learn_gap(depth_gap, depth_filled)
vre.learning_engine.learn_gap(gap, filled)
Reference Loop
The integrator owns the loop — VRE provides the pieces (check, template_for_gap, learn_gap) and you decide how
to drive them. A minimal loop grounds, fills each gap however you choose (LLM, human, or rules), persists, and
re-grounds until grounded:
from vre.core.errors import CandidateValidationError, GapResolvedError
from vre.learning.templates import template_for_gap
grounding = vre.check(concepts)
while not grounding.grounded and grounding.gaps:
gap = grounding.gaps[0]
candidate = my_fill(template_for_gap(gap), gap, grounding) # LLM / human / rules
if candidate is None:
break # the operator deliberately chose to leave this gap open
try:
vre.learning_engine.learn_gap(gap, candidate)
except GapResolvedError:
pass # the gap closed out from under us — just re-ground
except CandidateValidationError as err:
candidate = my_fill(template_for_gap(gap), gap, grounding, feedback=str(err)) # revise and retry
grounding = vre.check(concepts) # re-observe; the gap set shrinks
learn_gap is the privileged, human-gated persist call: agents propose candidates, they never get graph write
access. It validates each candidate against live graph state and raises CandidateValidationError (malformed or
out-of-scope) or GapResolvedError (the gap was already resolved). Reachability gaps additionally use
reachability_prerequisites (above) to fill missing depths before the edge is placed.
Policy System
Policies gate specific concept relationships: which actions require confirmation, under what cardinality conditions they fire, and what confirmation message to surface.
Policies are code-resident. You declare them in your own Python with the
policy_callback decorator, binding a callable to one APPLIES_TO edge
(source_primitive → target_primitive at a source_depth). The graph stores only
knowledge; policies are never persisted — so a tampered ~/.vre/graph.db can neither
inject code nor re-point a callback. The gate only ever invokes callables your own
imported code handed it.
Declaring a Policy
from vre import policy_callback, DepthLevel, PolicyCallContext, PolicyCallbackResult
@policy_callback(
source_primitive="delete",
target_primitive="file",
source_depth=DepthLevel.CONSTRAINTS, # pins exactly one APPLIES_TO edge (D3 / execution)
# key="..." identifies the policy; omitted here, so it defaults to the function name
name="Protected file guard",
requires_confirmation=False, # hard block — no confirmation prompt
confirmation_message="Deletion blocked by protected file policy.",
)
def protected_file(context: PolicyCallContext) -> PolicyCallbackResult:
"""Block deletion of files matching 'protected*'."""
command = context.tool_call.call_args[0] if context.tool_call.call_args else ""
targets = [t for t in command.split()[1:] if not t.startswith("-")]
for target in targets:
if target.startswith("protected"):
return PolicyCallbackResult(passed=False, message=f"'{target}' is a protected file.")
return PolicyCallbackResult(passed=True, message="No protected files affected.")
Importing the module that holds this declaration registers it. Import your policy
modules before constructing VRE — registration is an import-time side effect, and
VRE(...) validates every declared placement against the graph and then freezes the
registry (see Registration & validation below).
Policy Callbacks
A PolicyCallback is a callable attached to a Policy that runs during evaluation to make domain-specific pass/fail
decisions. This is distinct from on_policy, which handles human confirmation
after violations are collected. A policy callback determines whether a violation fires at all.
The callback receives a PolicyCallContext composed of four parts: tool_call (the invocation —
tool_name, call_args, call_kwargs), grounding (a bounded facade — agent_id and the
resolved_concepts grounded in this call), triggering_edge (the specific edge that fired the
callback — source/target concept and the source/target depths), and policy (the Policy that
fired, including its metadata). It returns a PolicyCallbackResult — passed=True suppresses the
violation, passed=False fires it.
Stateful callbacks (instances that can't be decorated) use the imperative twin,
register_policy:
from vre import register_policy, DepthLevel
register_policy(
RateLimiter(per_minute=5), # any callable taking a PolicyCallContext
key="rate_limit",
source_primitive="send", target_primitive="email",
source_depth=DepthLevel.CONSTRAINTS, name="Rate limit",
)
One callback, several edges — stack decorators, each with a distinct key (the decorator returns the original function, so it composes):
@policy_callback(key="protected_file", source_primitive="delete",
target_primitive="file", source_depth=DepthLevel.CONSTRAINTS, name="Protected file")
@policy_callback(key="protected_dir", source_primitive="delete",
target_primitive="directory", source_depth=DepthLevel.CONSTRAINTS, name="Protected dir")
def protected_delete(context): ...
A callback can make nuanced, context-aware decisions by inspecting both the command arguments (via the
ToolCallContext) and external state — for example, an rm guard that resolves literal filenames, expands globs
against the filesystem, and inspects directories recursively before deciding whether a protected path is in scope.
Evaluation Flow
- Cardinality filter — if the policy specifies a
trigger_cardinality, it only fires when the operation's cardinality matches - Callback evaluation — the callback runs with the full call context;
passed=Truesuppresses the violation. The gate fails closed with a detailed reason (the policy fires) when the callback cannot be evaluated — notool_callin context, or the callback raises — so a buggy callback never weakens a gate or escapes as a raw exception - Violation collection — unsuppressed policies produce
PolicyViolationobjects - Hard blocks vs confirmation — violations with
requires_confirmation=Falseare immediate blocks. Those withrequires_confirmation=Trueare deferred to theon_policyhandler
Registration & Validation
Declared policies live in a PolicyRegistry; the policy_callback decorator and register_policy write to a
module-global one that VRE reads by default (pass policy_registry= to use your own). At construction VRE:
- logs the registered policy keys — a
0 registeredline is your cue that a policy module was never imported; - validates every declared placement against the graph. A placement whose
APPLIES_TOedge is absent (typo, missing knowledge, or wrong depth) raisesPolicyPlacementError, because a declared gate that protects nothing is the one dangerous, otherwise-silent case. Passvalidate_policies=Falseto defer, then callvre.validate_policy_placements()yourself; - freezes the registry, so the invariant everything enforced was validated holds.
The symmetry: a missing callback fails closed (the policy fires); a missing edge fails loud (VRE refuses to
start). Pass expect_policies=N to additionally assert the registered count.
Multiple graphs in one process. Freeze is per-registry, so give each graph its own PolicyRegistry and decorate
with its .policy_callback; constructing one VRE validates and freezes only that registry, never another graph's:
from vre import VRE, PolicyRegistry, DepthLevel
reg_a = PolicyRegistry()
@reg_a.policy_callback(source_primitive="delete", target_primitive="file",
source_depth=DepthLevel.CONSTRAINTS, name="Protect A's files")
def protect_a(ctx): ...
vre_a = VRE(repo_a, policy_registry=reg_a) # validates + enforces only reg_a, against graph A
vre_b = VRE(repo_b, policy_registry=reg_b) # independent: reg_b, against graph B
The module-level policy_callback / register_policy are just this bound to a shared default registry — fine for the
common single-graph case.
Validated at init ≠ guaranteed to fire. Validation confirms the declared edge exists in the graph; it does not
guarantee the edge is grounded on a given call. APPLIES_TO is non-transitive, so grounding ["delete"] alone
strips the delete → file relatum from the trace — the placement validates clean yet never fires, and delete can
fully ground and PASS. Enforcement still requires the action and the object concept in the query (the same static
conjunction the tool declares or the extractor emits). This isn't a regression — a graph-resident policy on a stripped
relatum behaved identically — but the init check verifies the edge is reachable, not that any given call will hit it.
Integrations
VRE is framework-agnostic: vre_guard wraps any callable, and the learning loop is plain Python (see
Learning). A reference LangChain + Ollama agent previously lived under examples/; it has been retired —
it leaned on a framework that obscured where the guard sits and a shell=True pseudo-sandbox — in favor of a single
agent-driven showcase that teaches the concept-binding gradient end-to-end
(#114).
Future
Learning Through Failure
When a mechanical failure occurs during execution — permission denied, missing dependency, invalid path — the failure
reveals a constraint that was not modeled. The agent proposes the missing relatum
(e.g. create --[CONSTRAINED_BY]--> permission), seeks human validation, and persists the new knowledge. Depth was
honest before the failure and more complete after.
Knowledge Import
A pathway for growing an agent's graph from peer-published knowledge. An agent fetches a peer's subgraph for a target
concept and persists it locally as ordinary primitives stamped with provenance.source = PEER and a
(peer_name, imported_at) attestation. Imports are one-shot — refresh is an explicit operator action, never a live
link — which preserves the depth-explicit validated trust VRE's enforcement depends on while letting an agent grow
its graph from a community of peers instead of authoring every concept from scratch.
Epistemic Memory
A new class of memory that stores not just information but the agent's epistemic relationship to that information. Memories are indexed by concept and depth, decay or are reinforced based on usage and grounding history, and affect the agent's confidence in related concepts.
Tech Stack
| Concern | Technology |
|---|---|
| Language | Python 3.12+ |
| Epistemic graph | SQLite (default) or Neo4j (pip install 'vre[neo4j]') |
| Concept resolution | Exact, case-insensitive name match (no NLP) |
| Data models | Pydantic v2 |
| Package management | Poetry |
Project Structure
src/vre/
├── __init__.py # VRE public interface (check, check_policy, learning_engine)
├── guard.py # vre_guard decorator (grounding → policy → execution)
├── metrics.py # MetricsManager — best-effort grounding metric updates
├── tracing.py # TraceWriter + TraceManager — JSONL persistence
│
├── identity/
│ ├── models.py # AgentIdentity — stable UUID bound to a registration key
│ └── registry.py # AgentRegistry — file-based, append-only identity persistence
│
├── core/
│ ├── models.py # Primitive, Depth, Relatum, RelationType, DepthLevel, KnowledgeGap, Provenance
│ ├── errors.py # VREError hierarchy — typed exceptions for all failure modes
│ ├── backends/
│ │ ├── repository.py # Repository ABC — abstract persistence contract
│ │ ├── sqlite.py # SQLiteRepository — SQLite backend (default)
│ │ └── neo4j.py # Neo4jRepository — Neo4j backend (optional)
│ ├── grounding/
│ │ ├── engine.py # GroundingEngine — depth-gated query, gap detection
│ │ └── models.py # GroundingResult
│ └── policy/
│ ├── models.py # Policy, Cardinality, PolicyResult, PolicyViolation
│ ├── registry.py # PolicyRegistry, policy_callback, register_policy — code-resident policies
│ ├── gate.py # PolicyGate — overlays registry placements onto a trace
│ └── callback.py # PolicyCallContext, PolicyCallback protocol
│
└── learning/
├── models.py # Candidate models with validate_for_gap methods
├── templates.py # template_for_gap — gap → candidate model class
└── engine.py # LearningEngine — learn_gap, reachability_prerequisites
scripts/
├── clear_graph.py # Clear all primitives from the graph
└── seed_gaps.py # Seed gap-demonstration graph (10 primitives)
seeders/
└── seed_filesystem.py # Filesystem domain — 20 primitives, idempotent upsert
Guiding Principle
The agent must never act as if it knows more than it can justify.
VRE exists to enforce that rule — not as a policy, but as a structural property of the system.
Contributing
Contributions are welcome! Please open an issue or submit a pull request with your proposed changes. For major changes, please discuss them in an issue first to ensure alignment with the project's goals and architecture.
Areas where contributions would be particularly valuable:
- Additional seed scripts for more complex domains (e.g. networking, databases, cloud infrastructure)
- Integration examples with other Python agent frameworks or tool libraries — any integration submission should include a demo that exercises the integration and demonstrates epistemic resolution behavior
- VRE integration into other language environments (Node.js, Go, etc.)
This is a project that I am passionate about and is the culmination of almost 10 years of philosophical thought. I hope to connect with other like-minded community members who prioritize safety and epistemic integrity in autonomous agentic systems.
I look forward to seeing how this evolves!
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file vre-1.0.0.tar.gz.
File metadata
- Download URL: vre-1.0.0.tar.gz
- Upload date:
- Size: 86.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8cbc92798121951c9865d2ab0bb7790bbfd69cb1793e7f425b89d4bde82fc248
|
|
| MD5 |
c0f732d6fee08ded1253687761f4617d
|
|
| BLAKE2b-256 |
bf5fa2786b4b5bbf7921cfb4473f84aef00f85b9b5e621bb55a78debcd8f5e72
|
Provenance
The following attestation bundles were made for vre-1.0.0.tar.gz:
Publisher:
publish.yml on anormang1992/vre
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vre-1.0.0.tar.gz -
Subject digest:
8cbc92798121951c9865d2ab0bb7790bbfd69cb1793e7f425b89d4bde82fc248 - Sigstore transparency entry: 1853519774
- Sigstore integration time:
-
Permalink:
anormang1992/vre@f6c1545cbe7ec24024d4a0eb3c37a5f881e4a677 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/anormang1992
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f6c1545cbe7ec24024d4a0eb3c37a5f881e4a677 -
Trigger Event:
release
-
Statement type:
File details
Details for the file vre-1.0.0-py3-none-any.whl.
File metadata
- Download URL: vre-1.0.0-py3-none-any.whl
- Upload date:
- Size: 86.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12abd01651ee12af949fa916329e0fe2ff6c41960b5b991ad872175fe35af42e
|
|
| MD5 |
fc27dfdec078bf1b77b451e254624448
|
|
| BLAKE2b-256 |
5e11b44ae388fb1109c653ae60ad9b520124bff9ae2051cc13a91379ecde31fb
|
Provenance
The following attestation bundles were made for vre-1.0.0-py3-none-any.whl:
Publisher:
publish.yml on anormang1992/vre
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
vre-1.0.0-py3-none-any.whl -
Subject digest:
12abd01651ee12af949fa916329e0fe2ff6c41960b5b991ad872175fe35af42e - Sigstore transparency entry: 1853519813
- Sigstore integration time:
-
Permalink:
anormang1992/vre@f6c1545cbe7ec24024d4a0eb3c37a5f881e4a677 -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/anormang1992
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f6c1545cbe7ec24024d4a0eb3c37a5f881e4a677 -
Trigger Event:
release
-
Statement type: