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"]KnowledgeBase 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.
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/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
npm run bench:run
npm run build