{
  "access": "public",
  "type": "tutorial",
  "format": "markdown",
  "title": "Logic Cells",
  "chunked": true,
  "url": "https://library.datagrout.ai/logic-cells",
  "summary": "The complete guide to DataGrout's symbolic reasoning layer: engines, rules, in-rule tool calls, tabling, probabilistic reasoning, batteries, and cross-namespace composition.",
  "content_markdown": "# Logic Cells\n\nThe complete guide to DataGrout's symbolic reasoning layer: engines, rules, in-rule tool calls, tabling, probabilistic reasoning, batteries, and cross-namespace composition.\n\nFor the per-tool API reference (`logic.remember`, `logic.query`, `logic.assert`, …) see the [Logic Tools Reference](logic-tools). This guide covers what you can *do inside* a cell.\n\n## What a logic cell is\n\nA logic cell is a durable, per-namespace fact-and-rule space with a Prolog engine attached. Facts and rules persist in encrypted storage as the source of truth; a warm Prolog worker per namespace serves queries at sub-millisecond latency and is rebuilt automatically from storage whenever it goes cold. You never manage workers — assert facts, store rules, query.\n\nNamespaces are fully isolated: their facts, rules, engine choice, and worker are their own. `logic.worlds` lists yours.\n\n## Engines: SWI, Scryer, and the ISO default\n\nEvery namespace runs on one of two Prolog engines:\n\n| | Scryer (ISO) | SWI |\n|---|---|---|\n| ISO conformance | strict | no (superset) |\n| Tabling (`:- table`) | ✅ | ✅ |\n| `call_tool` in queries & stored rules | ✅ | ✅ |\n| Probabilistic rules (`P::head`) | ✅ via battery rewrite | ✅ native |\n| SWI dicts (`_{k: v}`), SWI-only builtins | — | ✅ |\n\n**The default is ISO.** A fresh namespace is configured `auto` and resolves to Scryer. If you later store a rule that genuinely needs SWI (dicts, SWI-only builtins), an `auto` namespace upgrades to SWI automatically — **one-way and sticky** for the life of the namespace. Rules that use `call_tool` or ProbLog notation do *not* trigger the upgrade; both are fully supported on the ISO engine.\n\nInspect or set the engine with `logic.namespace`:\n\n```json\n{ \"namespace\": \"my-app\" }                      // read: configured + resolved engine, pin state\n{ \"namespace\": \"my-app\", \"engine\": \"swi\" }     // set explicitly (auto | swi | scryer)\n{ \"namespace\": \"my-app\", \"iso_pin\": true }     // pin ISO-only (see below)\n```\n\nThe read form works before the namespace exists and reports exactly what first use will establish.\n\n### ISO pinning\n\n`iso_pin: true` makes the ISO guarantee **inviolable**: the namespace can never fall back to SWI, and rules using non-ISO constructs are *rejected* rather than triggering an upgrade. Pinning is monotonic — it cannot be undone; fork to a new namespace if you need a fresh engine choice. Use it when strict ISO conformance is a requirement (compliance tiers, reactor apps), not just a preference.\n\nSwitching a populated namespace onto Scryer reloads it immediately and returns any non-ISO rules that had to be disabled (`disabled_rules`), so nothing dies silently.\n\n## Rules (constraints)\n\nStore rules through `logic.assert` with the `prolog` field (or `logic.constrain`):\n\n```json\n{ \"namespace\": \"crm\", \"prolog\": \"hot_lead(L) :- attribute(L, score, S), S > 80.\" }\n```\n\nRules are validated before compilation (call-graph analysis, termination classification, sandbox check) and each one gets a **Cognitive Trust Certificate** recording the assurances: `validated`, `termination_class` (`datalog` / `well_founded` / `generator` / `unbounded`), `sandboxed`, cycle-freedom, and the defined/called predicate lists. Queries mint their own certificates referencing the rules they exercised. Rules that call tools (see below) are fully certifiable.\n\nMulti-clause blobs are fine — facts and rules can share one `prolog` string. Engine directives like `:- table` are recognized, applied, and reported separately (`directives` in the response) — they are never stored or certified as rules.\n\n## Predicates available inside rules and queries\n\n### Fact predicates\n\n`entity(Name)`, `attribute(Entity, Attr, Value)`, `relation(Subj, Rel, Obj)`, `metric(Entity, Metric, Value)`, `tag(Entity, Tag)`, `context(Key, Value)` — plus handle-aware variants (`attribute_h/4`, `relation_h/4`, …) when you need the fact handle (e.g. for probability joins).\n\n### Graph and probability\n\n`path/5`, `path_prob/6`, `reachable/4`, `reachable_prob/5`, `neighbors/4`; `probability(Goal, P)` (noisy-OR marginal over weighted rules); `lc_fact_weight(Handle, P)`. On ISO namespaces the probabilistic interface is `psuccess/2` (see Probabilistic rules below).\n\n### Utilities\n\n`findall/3`, `aggregate_all/3`, `member/2`, `memberchk/2`, `append/3`, `sort/2`, `msort/2`, `length/2`, `between/3`, `sum_list/2`, `max_list/2`, `min_list/2`, `nth0/3`, `nth1/3`, `include/3`, `exclude/3`, `maplist/2-4`, term inspection (`functor/3`, `is_list/1`, `compound/1`), arithmetic and comparison operators, and string helpers (`atom_string/2`, `string_concat/3`, …). Note `catch/3` is **not** available to user goals — error handling uses bound error markers instead (see below).\n\n### `call_tool/3` — tools as evidence, mid-proof\n\nRules and queries can call any tool on your server, right in the middle of resolution:\n\n```prolog\nakin(A, B) :-\n    ev(A, VA), ev(B, VB), A \\== B,\n    call_tool('data-grout@1/linalg.cosine_sim@1', [a-VA, b-VB], R),\n    member(similarity-S, R), S > 0.5.\n```\n\n- **Args** are a pair-list (`[key-Value, ...]`) — it becomes the tool's JSON arguments.\n- **Results** come back as a pair-list too: read fields with `member(key-V, R)` or `memberchk/2`. Lists of records arrive as lists of pair-lists.\n- Multi-solution backtracking over `call_tool` is safe and returns every solution (bounded at 1,000 per query).\n- Billing, policy, and PII rules apply exactly as if the agent called the tool directly.\n\n**`call_tool/4`** additionally binds the response metadata (credits, cache_ref, certificate) for cost-tracking and audit rules: `call_tool(Tool, Args, Payload, Meta)`.\n\n**`call_tool_cached/3,4`** memoizes on the canonicalized `(tool, args)` — the first call dispatches, every repeat is a plain fact lookup. Use it for repeated identical evidence calls and inside tabled reasoning. Ground arguments only; the cache is per-worker and bounded (512 entries); `lc_clear_tool_cache/0` drops it.\n\n**Limits and refusals.** At most 64 tool calls per query, one nesting level of tool→query recursion, and `logic.*` tools are refused as inner calls against the *running* namespace (they would deadlock its worker) — cross-namespace logic calls have their own predicates below. Refusals and tool failures never abort your query: the result binds an error marker your rule can pattern-match, and comparisons against it simply fail. For tool failures and refusals the marker carries a pair-list you can read with `member/2`:\n\n```prolog\ncall_tool(T, A, R),\n( R = '$call_tool_error'(E) -> member(reason-Why, E), handle(Why)\n; use(R) ).\n```\n\n`E` is `[error-Message, reason-Why]` (reasons include `tool_error`, `tool_exception`, `denylisted_lc_tool`, `depth_exceeded`, `parse_error`, `unsafe_inner_goal`); transport-level failures bind an atom instead (e.g. `'$call_tool_error'(json_parse_failed)`). The shape is identical on both engines.\n\n### `call_ns/3,4` — query another namespace\n\n```prolog\ncall_ns('forensics', 'causal_chain(cert_expired, X, Path)', Solutions).\ncall_ns(NS, Goal, Solutions, Limit).   % default Limit 50\n```\n\nEvaluates a goal against another of your namespaces — on *that* namespace's own worker and engine (an ISO cell can query an SWI cell and vice versa) — and binds the solutions as a list of `[VarName-Value, ...]` pair-lists. The target must be a *different* namespace than the running query; chains are depth-limited. Errors bind `'$call_ns_error'(Reason)`.\n\n### `call_tool_into/4` — route a tool's results into a namespace\n\n```prolog\ncall_tool_into('salesforce@1/get-accounts@1', [region-west], 'crm_mirror', Receipt).\n```\n\nDispatches the tool, then materializes its result *as facts* in the target namespace: each record becomes an entity (named from its `id`/`name` field, or content-hashed — so re-running converges instead of duplicating) with an attribute per scalar field. `Receipt` reports `entities`, `facts_written`, and any nested fields that were skipped. Same-namespace targets are refused; errors bind `'$call_tool_into_error'(Reason)`.\n\n### `findall_limit/4`\n\n`findall/3` with a solution cap — the right tool for bounding generative sub-goals inside a rule.\n\n## Tabling\n\n`:- table pred/arity` is honored on **both engines**. Tabled predicates memoize their answers, which makes recursive definitions over cyclic data terminate:\n\n```json\n{ \"prolog\": \":- table reach/2. reach(X,Y) :- edge(X,Y). reach(X,Z) :- edge(X,Y), reach(Y,Z).\" }\n```\n\nThe directive may precede or follow its clauses, in the same batch or a later one. Tables are invalidated automatically when facts change, so tabled answers always reflect current data. Tabling composes with `call_tool_cached` (table the pure predicates; make evidence calls cached).\n\n## Probabilistic rules\n\nProbLog-style weight annotations work on both engines:\n\n```json\n{ \"prolog\": \"0.7::likes(alice, prolog).\" }\n```\n\n- On **SWI**, the notation is native; query marginals with `probability(Goal, P)`.\n- On **ISO/Scryer** namespaces, the rule is transparently rewritten to `prob_rule/2` form and the `prob-core-iso` battery is auto-installed; query with `psuccess(Goal, P)` (plus `pand/2` for conjunctions and `expected/3` for expected values). The namespace stays on the ISO engine.\n\nWeighted rules **accumulate** under their head — each `P::head :- body` is an additive rule, matching ProbLog authorship. Re-stating a body updates its weight; pass `mode: \"replace\"` to overwrite the whole set (how you express a correction).\n\n## Batteries\n\nBatteries are installable rule packs, scoped per namespace:\n\n- `batteries.search` / `batteries.install_many` / `batteries.installed` / `batteries.validate` / `batteries.remove`\n- `batteries.validate` runs every predicate the battery defines and tells you exactly which facts to assert to make it fire — run it after install.\n\nFour rule libraries are **preloaded** into every worker on both engines (no install needed): **forensic** (causal chains, blast radius, contradictions — see the [Forensic Tools](forensic-tools) page), **fsm** (state-machine reachability, cycles, validation), **plan-fsm** (plan execution tracking), and **lumen-bench** (usage/cost analysis over `_lumen` facts).\n\n## Configuration quick reference\n\n| Setting | Where | Values / limits |\n|---|---|---|\n| Engine | `logic.namespace` `engine` | `auto` (default, ISO-first) / `swi` / `scryer` |\n| ISO pin | `logic.namespace` `iso_pin` | `true` (one-way) |\n| Namespace | every `logic.*` tool | any string; default `\"default\"` |\n| Query timeout | `logic.query` `timeout` | 1–10 s (default 3) |\n| Result paging | `logic.query` `limit`/`offset` | limit ≤ 500 (default 50), `has_more` in response |\n| Solutions per streamed query | fixed | 1,000 |\n| Tool calls per query | fixed | 64 |\n| Assert mode | `logic.assert`/`remember` `mode` | `append` / `upsert` / `preview`; `dry_run: true` validates only |\n| Weighted-rule mode | `logic.assert` `mode` | `merge` (default, additive) / `replace` |\n\n## Serving rules as apps\n\nAny rule in a cell can be exposed as a live HTTP API and shareable web form — see [Reactor Apps & Smart Panels](reactor-apps).\n\n## Error conventions\n\nInside a query, tool and cross-namespace failures **bind markers instead of throwing**: `'$call_tool_error'(Detail)`, `'$call_ns_error'(Reason)`, `'$call_tool_into_error'(Reason)`. For `call_tool`, `Detail` is a `[error-Message, reason-Why]` pair-list (read it with `member/2`); for the cross-namespace predicates the argument is the reason itself. The shapes are identical on both engines. Pattern-match them when you want fallback behavior; otherwise downstream comparisons fail cleanly and the query simply yields fewer solutions. Errors that reach the top level (syntax errors, timeouts, undefined predicates) come back as structured tool errors with a diagnostic and, where possible, a suggestion.\n"
}