TypeScript Prolog runtime for agent workflows.
Designed in the spirit of just-bash: strong DX, safe defaults, predictable behavior in tool-calling loops.
npm install just-prologimport { Prolog } from "just-prolog";
const prolog = new Prolog({
program: `
parent(alice, bob).
parent(bob, carol).
ancestor(X, Y) :- parent(X, Y).
ancestor(X, Y) :- parent(X, Z), ancestor(Z, Y).
`,
});
const result = await prolog.query("ancestor(alice, Who).");
console.log(result.solutions.map((solution) => solution.text.Who));
// ["bob", "carol"]Use ProbLog with the prolog tagged template for readable, safe program text.
import { ProbLog, prolog } from "just-prolog";
const runtime = new ProbLog({
program: prolog`
0.5::heads1.
0.6::heads2.
someHeads :- heads1.
someHeads :- heads2.
evidence(heads1, false).
query(someHeads).
`,
backend: "exact",
});
const result = await runtime.infer();
if (result.error) {
throw new Error(`${result.error.code}: ${result.error.message}`);
}
console.log(result.probabilities.someHeads);
// 0.6For approximate inference, switch to backend: "sampling" and set seed / samples.
ProbLog also accepts predicates, occursCheck, maxDepth, and maxInferences runtime options.
import { ProbLog, definePredicate, isAtomTerm, prolog } from "just-prolog";
const retrieve = definePredicate("retrieve", 2, async function* ({ args, term }) {
const topic = args[0];
if (topic === undefined || !isAtomTerm(topic) || topic.name !== "weather") {
return;
}
yield [topic, term.atom("sunny")];
});
const runtime = new ProbLog({
predicates: [retrieve],
program: prolog`
0.8::tool_enabled.
forecast_ready :- tool_enabled, retrieve(weather, Chunk), Chunk = sunny.
query(forecast_ready).
`,
});
const result = await runtime.infer();
console.log(result.probabilities.forecast_ready);
// 0.8KnowledgeBase gives typed, composable rule construction.
import { KnowledgeBase } from "just-prolog";
const kb = KnowledgeBase.define({
parent: 2,
ancestor: 2,
});
const X = kb.variable("X");
const Y = kb.variable("Y");
const Z = kb.variable("Z");
kb.addFact("parent", ["tom", "bob"])
.addFact("parent", ["tom", "liz"])
.addRule("ancestor", [X, Y], [["parent", [X, Y]]])
.addRule(
"ancestor",
[X, Y],
[
["parent", [X, Z]],
["ancestor", [Z, Y]],
],
);
const result = await kb.query("ancestor", ["tom", kb.variable("Who")]);
console.log(result.solutions.map((solution) => solution.text.Who));
// ["bob", "liz"]What you get:
- Predicate-name safety from schema keys.
- Arity checks at compile-time (literal schemas) and runtime.
- Built-in term helpers:
kb.variable(kb.varshorthand),kb.atom,kb.number,kb.compound,kb.list.
Note: string inputs are atoms. Use kb.variable("Name") for variables.
createKnowledgeBase provides validator-backed row clients with inferred types.
import { createKnowledgeBase } from "just-prolog";
import { z } from "zod";
const kb = createKnowledgeBase({
repo: z.object({
owner: z.string(),
name: z.string(),
visibility: z.enum(["public", "private", "internal"]),
}),
pullRequest: z.object({
owner: z.string(),
repo: z.string(),
number: z.number().int().positive(),
state: z.enum(["open", "closed"]),
headSha: z.string(),
author: z.string(),
draft: z.boolean(),
mergedBy: z.string().nullable(),
labels: z.array(z.string()),
}),
});
await kb.repo.create({
owner: "vercel",
name: "nextjs",
visibility: "public",
});
const sameActor = kb.var("Actor");
const selfMerged = await kb.pullRequest.findMany({
where: {
author: sameActor,
mergedBy: sameActor,
},
});What you get:
- Runtime validation from any Standard Schema-compatible validator (
zod, etc.). - Full TypeScript inference for model clients like
kb.repo,kb.pullRequest. - Predicate field order inferred from object schema key order.
- Shared runtime option (
{ prolog }) when you need raw query interop.
Use tagged templates to interpolate dynamic data safely.
import { Prolog, prolog, query, raw } from "just-prolog";
const person = "tom";
const runtime = new Prolog({
program: prolog`
parent(${person}, bob).
parent(${person}, liz).
`,
});
const descendants = await query(runtime)`parent(${person}, Who)`.all();
const unsafe = "p(a)";
const noExecute = await query("p(a).")`${unsafe}`.all();
const execute = await query("p(a).")`${raw("p(a)")}`.all();Rules:
- Strings become atoms and are quoted/escaped when needed.
- Numbers become number terms.
- Arrays become Prolog lists.
raw(...)is explicit opt-in for unescaped insertion.
Core runtime:
new Prolog(options)create runtime.consult(program)append clauses.assert(clause)alias forconsult.assertz(clause)append one clause.retract(pattern)remove first matching clause.abolish(indicator)remove all clauses for indicator.query(queryText, options)collect solutions.queryFirst(queryText, options)first solution ornull.solve(queryText, options)async stream.
ProbLog runtime:
new ProbLog({ program, backend, predicates, occursCheck, maxDepth, maxInferences, seed, samples, tolerance })create probabilistic runtime.infer({ query?, evidence? })compute marginals and optional error.
Programmatic API:
new KnowledgeBase(options)orKnowledgeBase.define(schema, options).addFact(predicate, args).addRule(predicate, headArgs, bodyGoals).query(predicate, args, options).queryFirst(predicate, args, options).solve(predicate, args, options).
Schema-first API:
createKnowledgeBase(schema, options).kb.<model>.create(row).kb.<model>.createMany(rows).kb.<model>.findMany({ where, queryOptions }).kb.<model>.findFirst({ where, queryOptions }).kb.<model>.exists({ where, queryOptions }).
Template API:
prolog\...`` build escaped Prolog text.query(source)\...`` build + run escaped query templates.raw(source)explicit unescaped interpolation.
Solution object:
bindings: structured terms.text: rendered Prolog text.
- Facts and rules (
:-) - Lists (
[],[A, B],[H|T]) - Control operators:
,,;,->,! - Built-ins:
true/0,fail/0,=/2,\=/2,call/1,not/1 - Dynamic DB:
assertz/1,retract/1,abolish/1
import { Prolog, definePredicate, isAtomTerm } from "just-prolog";
const search = definePredicate("search", 2, async function* ({ args, term }) {
const q = args[0];
if (q === undefined || !isAtomTerm(q)) {
return;
}
yield [q, term.atom(`result_for_${q.name}`)];
});
const runtime = new Prolog({ predicates: [search] });
const result = await runtime.query("search(weather, Result).");Defaults:
maxDepth:256maxInferences:100000defaultMaxSolutions:1000
Tune in PrologOptions.
Unification/backtracking uses a trail-based binding store for low-allocation rollback.
Benchmarks: bench/query-engine.bench.ts
npm run bench:runReal agent-oriented examples live in examples/.
examples/agent-task-planning.tsexamples/problog-examples.tsexamples/progressive-context-routing.tsexamples/task-hierarchy-model.ts
packages/just-prolog-tool provides an AI SDK-compatible prolog tool builder.
import { createPrologTool } from "just-prolog-tool";
const { tools } = createPrologTool({
prologOptions: { program: "fact(answer)." },
});
const result = await tools.prolog.execute({ query: "fact(X)." });See packages/just-prolog-tool/README.md.
npm install
npm run typecheck
npm run test:run
bun run test:parity:bun
bun run test:offline
npm run bench:run
npm run build