Rust Prolog Integration Guide#
This guide covers Prolog scripting support in Rust TEA, enabling neurosymbolic AI workflows that combine Rust’s performance with Prolog’s logical reasoning capabilities.
Prerequisites#
System Requirements#
Rust 1.70+ with Cargo
SWI-Prolog 9.1+ (system-wide installation required)
pkg-config (for locating SWI-Prolog libraries)
Installing SWI-Prolog#
# Ubuntu/Debian (recommended PPA for 9.1+)
sudo apt-add-repository ppa:swi-prolog/stable
sudo apt update
sudo apt install swi-prolog swi-prolog-nox
# macOS
brew install swi-prolog
# Fedora
sudo dnf install pl
# Windows
# Download from https://www.swi-prolog.org/download/stable
# or: choco install swi-prolog
# Verify version (must be 9.1+)
swipl --version
Verify pkg-config Can Find SWI-Prolog#
# Check if swipl is found
pkg-config --modversion swipl
# If not found, you may need to set PKG_CONFIG_PATH
export PKG_CONFIG_PATH=$(swipl --dump-runtime-variables | grep PLBASE | cut -d= -f2 | tr -d '"')/lib/pkgconfig:$PKG_CONFIG_PATH
Enabling Prolog in Rust TEA#
Prolog is an optional feature in Rust TEA. Enable it during build:
# Build with Prolog support
cargo build --features prolog
# Run with Prolog support
cargo run --features prolog -- run my-agent.yaml
# Run tests with Prolog
cargo test --features prolog
Cargo.toml Configuration#
If using TEA as a dependency:
[dependencies]
the_edge_agent = { version = "0.7", features = ["prolog"] }
Architecture: Prolog-Side Parsing#
As of v0.7.8, Rust TEA uses Prolog-side parsing via tea_load_code/1, achieving full parity with Python TEA.
How It Works#
TEA predicates are loaded from
tea_prolog_predicates.plviaconsult/1User code is passed to
tea_load_code/1which uses Prolog’s nativeread_term/3Each term is classified by Prolog itself using
tea_process_term/1:Directives:
(:-Body)→ executed immediatelyRules:
(Head :- Body)→ asserted withassertz/1Facts: Ground compound terms (detected by
tea_is_fact/1)Queries: Everything else → called directly
User-asserted facts are cleaned up via
tea_cleanup_facts/0
Why Prolog-Side Parsing?#
This approach provides:
100% accurate parsing - Prolog parses Prolog syntax correctly
No edge case bugs - Handles commas in quotes, operators, etc.
Cross-runtime parity - Same architecture as Python TEA
Simpler maintenance - No regex patterns to maintain
TEA Predicates#
The following predicates are defined in rust/src/engine/tea_prolog_predicates.pl:
tea_load_code/1- Entry point: loads code from stringtea_process_term/1- Classifies and processes each termtea_is_fact/1- Determines if a term should be assertedtea_action_predicate/1- Lists predicates that should be called, not assertedtea_cleanup_facts/0- Cleans up user-asserted facts
Historical Note#
Before v0.7.8, Rust used host-side heuristic parsing with regex patterns.
This legacy code is preserved as execute_node_code_legacy() for reference.
Writing Prolog Nodes#
Method 1: Language Attribute (Recommended)#
- name: process
language: prolog
run: |
state(value, V),
Result is V * 2,
return(result, Result).
Method 2: Marker Comment#
- name: process
run: |
% prolog
state(value, V),
Result is V * 2,
return(result, Result).
Method 3: Explicit Type#
- name: process
run:
type: prolog
code: |
state(value, V),
Result is V * 2,
return(result, Result).
State Interface#
Reading State: state/2#
% Read a single value
state(key_name, Value),
% Read multiple values
state(input, Input),
state(count, Count),
Writing State: return/2#
Use return/2 to update state from Prolog nodes:
% Return a computed value
state(value, V),
Result is V * 2,
return(result, Result).
% Return multiple values
return(a, 1),
return(b, 2),
return(status, 'completed').
% Last-write-wins for duplicate keys
return(x, 1),
return(x, 2),
return(x, 3). % State will have x = 3
Cross-Runtime Parity: The return/2 predicate works identically in both Python and Rust TEA implementations.
Security Note: Using return/2 or state/2 disables sandbox protection for that query, since these predicates require dynamic assertions that the sandbox blocks. Avoid mixing state/return predicates with potentially dangerous operations.
Inline Rule Definitions#
You can define custom Prolog rules inline within a node’s code block. Rules are automatically detected and asserted before the main query executes, then cleaned up afterward.
Basic Syntax#
- name: apply_rule
language: prolog
run: |
% Define a rule inline
add_ten(X, Y) :- Y is X + 10.
% Use state and apply rule
state(doubled, D),
add_ten(D, R),
return(result, R).
Multiple Rules#
- name: compute_with_rules
language: prolog
run: |
% Multiple rule definitions
double(X, Y) :- Y is X * 2.
add_five(X, Y) :- Y is X + 5.
% Chain the rules
state(value, V),
double(V, D),
add_five(D, R),
return(result, R).
Complex Rule Bodies#
Rules can have complex bodies with multiple goals, cuts, and conditionals:
- name: categorize
language: prolog
run: |
categorize(X, large) :- X >= 100.
categorize(X, medium) :- X >= 50, X < 100.
categorize(X, small) :- X < 50.
state(value, V),
categorize(V, Category),
return(category, Category).
Multi-Line Rules#
Rules can span multiple lines until the period terminator:
- name: complex_rule
language: prolog
run: |
validate_and_process(Input, Output) :-
Input > 0,
Input < 1000,
Temp is Input * 2,
Output is Temp + 10.
state(value, V),
validate_and_process(V, R),
return(result, R).
How It Works (Prolog-Side Parsing)#
As of v0.7.8, the Rust implementation uses Prolog-side parsing via the
tea_load_code/1 predicate, which delegates parsing to SWI-Prolog’s native
read_term/3. This ensures 100% accurate parsing of all Prolog syntax,
including edge cases like commas in quoted strings.
Code Submission: User code is passed to
tea_load_code/1Native Parsing: SWI-Prolog’s
read_term/3parses each termClassification: Each term is classified as:
Directive:
(:-Body)- executed immediatelyRule:
(Head :- Body)- asserted usingassertz/1Fact: Ground compound terms - asserted to knowledge base
Query: Everything else - executed immediately
Cleanup: User-asserted facts are cleaned up after execution
This approach matches Python’s janus-swi integration for cross-runtime parity.
Isolation Between Nodes#
Rules are cleaned up after each node execution, ensuring:
No pollution between nodes
Parallel execution remains thread-safe (rules are per-engine)
Same code works in different nodes without conflicts
Pre-Loaded Modules#
The following modules are automatically available without explicit imports:
Module |
Description |
Example Predicates |
|---|---|---|
|
List manipulation |
|
|
Finite domain constraints |
|
|
Higher-order predicates |
|
|
Aggregation |
|
This means CLP(FD) constraints work immediately:
- name: solve
language: prolog
run: |
% No explicit import needed!
X in 1..10,
Y in 1..10,
X + Y #= 15,
label([X, Y]).
Complete Examples#
Example 1: Compute and Return#
Use return/2 to update state directly from Prolog:
name: prolog-compute
state_schema:
value: int
result: int
doubled: int
nodes:
- name: compute
language: prolog
run: |
state(value, V),
Result is V * 2,
Doubled is V + V,
return(result, Result),
return(doubled, Doubled).
edges:
- from: __start__
to: compute
- from: compute
to: __end__
Example 2: Validation Pattern#
Use Prolog for validation with conditional edges:
name: prolog-validation
state_schema:
value: int
is_valid: bool
nodes:
- name: validate
language: prolog
run: |
state(value, V),
V > 0, % Must be positive
V < 1000, % Must be < 1000
0 is V mod 2, % Must be even
return(is_valid, true).
- name: on_invalid
language: lua
run: |
return { is_valid = false }
edges:
- from: __start__
to: validate
- from: validate
to: __end__
condition: true # Prolog succeeded
- from: validate
to: on_invalid
condition: false # Prolog failed
- from: on_invalid
to: __end__
Example 3: CLP(FD) Constraint Solving#
name: constraint-solver
state_schema:
x: int
y: int
target_sum: int
nodes:
- name: solve
language: prolog
run: |
state(target_sum, Sum),
X in 1..100,
Y in 1..100,
X + Y #= Sum,
X #< Y,
label([X, Y]).
edges:
- from: __start__
to: solve
- from: solve
to: __end__
Example 4: Knowledge Graph Validation#
name: kg-validation
state_schema:
person: str
relationship: str
target: str
valid: bool
nodes:
- name: check_relationship
language: prolog
run: |
state(person, P),
state(relationship, R),
state(target, T),
% Valid relationships in our domain
(R = parent ; R = sibling ; R = spouse),
% Convert to atoms for matching
(atom(P) -> PA = P ; atom_string(PA, P)),
(atom(T) -> TA = T ; atom_string(TA, T)),
% Simple validation
PA \= TA. % Can't relate to self
- name: mark_valid
language: lua
run: |
return { valid = true }
- name: mark_invalid
language: lua
run: |
return { valid = false }
edges:
- from: __start__
to: check_relationship
- from: check_relationship
to: mark_valid
condition: true
- from: check_relationship
to: mark_invalid
condition: false
Rust API Usage#
Programmatic Loading#
use the_edge_agent::{YamlEngine, Executor};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Load YAML agent
let engine = YamlEngine::new();
let graph = engine.load_from_file("my-agent.yaml")?;
// Compile the graph
let compiled = graph.compile()?;
// Create executor
let executor = Executor::new(compiled)?;
// Run with initial state
let initial_state = serde_json::json!({
"value": 42
});
let final_state = executor.invoke(initial_state)?;
println!("Result: {:?}", final_state);
Ok(())
}
Feature Detection#
#[cfg(feature = "prolog")]
fn run_prolog_agent() {
// Prolog-specific code
}
#[cfg(not(feature = "prolog"))]
fn run_prolog_agent() {
eprintln!("Prolog feature not enabled. Build with: cargo build --features prolog");
}
Sandbox and Security#
Prolog code runs in a sandboxed environment. The following are restricted:
File I/O (
open/3,read/1,write/1,read_term/2)Shell execution (
shell/1,process_create/3)Network access
Module loading from filesystem
Safe predicates remain available:
Arithmetic operations
List manipulation
CLP(FD) constraints
findall/3,aggregate_all/3Term manipulation
Timeout Protection#
All Prolog queries have timeout protection (default: 30 seconds).
If a query exceeds the timeout, execution fails gracefully.
Runtime Comparison: Python vs Rust#
Feature |
Python (janus-swi) |
Rust (swipl-rs) |
|---|---|---|
Bindings |
janus-swi (official) |
swipl-rs (community) |
SWI-Prolog Version |
9.1+ required |
9.1+ recommended |
|
Full support |
Full support |
Module pre-loading |
Auto (clpfd, lists, apply, aggregate) |
Auto (clpfd, lists, apply, aggregate) |
|
Full support |
Full support |
Sandbox |
Default enabled |
Default enabled* |
Timeout protection |
30s default |
30s default |
Thread safety |
Thread-local predicates |
RwLock + state caching |
* Sandbox is automatically disabled when using state/2 or return/2 predicates due to technical constraints with dynamic assertions.
Best Practices for Portable Agents#
To write YAML agents that work in both Python and Rust:
Use
state/2for reading - Works identically in both runtimesUse
return/2for writing - Now supported in both runtimesUse Prolog for validation - Success/failure patterns work everywhere
Keep security in mind - Using TEA predicates disables sandbox
# Portable pattern: Pure Prolog compute and return
nodes:
- name: compute
language: prolog
run: |
state(value, V),
V >= 0,
V =< 100,
Doubled is V * 2,
return(doubled, Doubled),
return(status, 'processed').
edges:
- from: __start__
to: compute
- from: compute
to: __end__
Troubleshooting#
“Prolog feature not enabled”#
Build with the Prolog feature:
cargo build --features prolog
cargo run --features prolog -- run my-agent.yaml
“Cannot find SWI-Prolog”#
Verify SWI-Prolog is installed:
swipl --versionCheck pkg-config can find it:
pkg-config --modversion swipl
Set PKG_CONFIG_PATH if needed:
export PKG_CONFIG_PATH=$(swipl --dump-runtime-variables | grep PLBASE | cut -d= -f2 | tr -d '"')/lib/pkgconfig:$PKG_CONFIG_PATH
“Failed to parse Prolog code: Exception”#
This error indicates a Prolog exception occurred during execution. Common causes:
Missing period at the end of clauses
Unbalanced parentheses
Invalid predicate names (must start with lowercase)
Undefined predicates
Trying to assert built-in predicates (e.g., conjunctions like
foo, baras facts)
Debug by testing Prolog code directly:
swipl
?- state(value, V), V2 is V * 2, return(result, V2).
“No permission to modify static procedure”#
This error occurs when tea_load_code/1 tries to assert a term that looks like a fact
but is actually a built-in operator. Common cases:
Conjunctions without definitions:
5 > 3.(comparison without assignment)Operators as top-level terms:
X = 5.(unification without context)
Solution: Ensure your code uses return/2 to return values:
% Wrong - tries to assert (>) which is a built-in
5 > 3.
% Correct - executes comparison and returns result
(5 > 3 -> R = true ; R = false), return(result, R).
“Unknown procedure: state/2” or “Unknown procedure: return/2”#
This indicates TEA predicates weren’t loaded properly. Causes:
Missing tea_prolog_predicates.pl file - Rust looks for this at:
$CARGO_MANIFEST_DIR/src/engine/tea_prolog_predicates.pl(dev/test)Falls back to embedded predicates
consult/1 failed - The file exists but couldn’t be loaded
Debug steps:
# Verify the predicates file exists
ls rust/src/engine/tea_prolog_predicates.pl
# Test loading it directly
swipl -g "consult('rust/src/engine/tea_prolog_predicates.pl'), listing(tea_load_code/1)" -t halt
Sandbox Violations in YAML Agents#
Note: As of v0.7.8, sandbox is NOT enforced in execute_node_code to ensure
compatibility with TEA predicates (state/2, return/2). This matches Python’s
janus-swi behavior.
If you need sandboxed execution for untrusted code, use the execute() method
directly with sandbox=true in the runtime configuration.
Build Fails on Linux#
Ensure development headers are installed:
# Ubuntu/Debian
sudo apt install swi-prolog-nox
# Fedora
sudo dnf install pl-devel
Build Fails on macOS#
Ensure Homebrew paths are correct:
brew install swi-prolog
export PKG_CONFIG_PATH=/opt/homebrew/lib/pkgconfig:$PKG_CONFIG_PATH
Build from Source#
If building TEA from source with Prolog:
# Clone repository
git clone https://github.com/your-org/the_edge_agent.git
cd the_edge_agent/rust
# Build with Prolog
cargo build --features prolog
# Run tests
cargo test --features prolog
# Install CLI
cargo install --path . --features prolog