Normal Prolog-style clauses, unification, backtracking, cuts, and recursion.
Reference
DML language reference for the current runtime.
DML is a Prolog-style language for agent workflows. It combines declarative logic, backtracking, recursion, direct tool execution, and LLM-driven subtasks. This page reflects the current runtime behavior, including the modern task harness and task-loop semantics.
What DML is for
Use DML when the workflow itself matters: retries, fallback clauses, recursion, explicit tool routing, helper predicates, and a clear separation between deterministic logic and open-ended model calls.
task() and prompt() support typed outputs and tool use.
exec() calls runtime and MCP tools directly, while tool wrappers expose capabilities to the model loop.
Use helper files, scoped tools, params, memory operations, and standard SWI libraries.
agent_main is required
Every DML program needs at least one agent_main clause. The current runtime dispatches
agent_main/0 through agent_main/3. If the provided argument count does not match exactly,
the runtime falls back to the best available arity and leaves missing positions unbound.
% No arguments
agent_main :-
answer("Ready.").
% One positional argument
agent_main(Topic) :-
answer(Topic).
% Up to three positional arguments
agent_main(Topic, Mode, Limit) :-
answer("Running").
- CLI positional arguments arrive as strings.
- SDK
runDML()can pass structured values such as numbers, booleans, lists, and dict-like objects, which are serialized into Prolog terms. - Use named params for optional settings instead of packing too many meaning changes into positional args.
number_string/2 or atom_number/2.
task() uses memory, prompt() starts fresh
task() and prompt() are the core model-facing predicates. They share the same task harness and tool mechanism,
but differ in memory behavior.
| Predicate | Behavior |
|---|---|
task(Description) |
Run a subtask using accumulated memory from system(), user(), and earlier task history. |
task(Description, Var1[, Var2, Var3]) |
Run a subtask and return up to three outputs through typed or untyped output variables. |
prompt(Description) |
Run a model call with fresh memory and no prior conversation context. |
prompt(Description, Var1[, Var2, Var3]) |
Fresh-memory variant with typed or untyped outputs. |
system(Text) |
Add a system instruction or persona to the current memory. |
user(Text) |
Add a user turn to the current memory. |
Typed outputs
Typed wrappers let the runtime validate the model output before returning it to the clause.
task("Return the issue count.", integer(IssueCount)).
task("Return true or false only.", boolean(IsValid)).
task("List the file paths.", list(string(Paths))).
task("Return a JSON object with summary and severity.", object(Report)).
task("Return a floating-point score.", number(Score)).
- Supported wrappers include
string(),integer(),number(),float(),boolean(),list(...), andobject(). - Describe expected outputs plainly in the prompt text. The harness is more reliable when the prompt names the outputs and required format directly.
- Prefer
prompt()for independent critiques, classification passes, or fresh reasoning that should not inherit earlier context.
Memory isolation example
agent_main(Topic) :-
system("You are a researcher."),
task("Research {Topic} and produce a draft.", Draft),
prompt("Critique this draft from a skeptical perspective: {Draft}", Critique),
task("Revise the draft using this critique: {Critique}", Final),
answer(Final).
Tool wrappers are for the model loop, not ordinary predicate calls
DML tool wrappers expose capabilities that the model can decide to call while executing task() or prompt().
They are not regular predicates that you invoke directly from ordinary DML control flow.
tool(search(Query, Results), "Search the web") :-
exec(web_search(query: Query), Results).
tool(explain_sum(A, B, Explanation), "Calculate and explain a sum") :-
Total is A + B,
format(string(Desc), "Explain why ~w + ~w = ~w", [A, B, Total]),
task(Desc, Explanation).
finish(success) and set_result(variable, value) into the task loop.
If the task declares output variables, the model must set them before finish(true) succeeds.
- Nested
task()orprompt()calls inside a tool run with fresh memory, not the parent task memory. - The currently executing tool is hidden from the nested task loop to prevent immediate self-recursion.
- Use
with_tools(ToolList, Goal)andwithout_tools(ToolList, Goal)to narrow the available tool set for a nested model call.
tool(safe_research(Topic, Result), "Research using only the search tool") :-
with_tools([search], (
format(string(Desc), "Research ~w and summarize the results", [Topic]),
task(Desc, Result)
)).
tool(cheap_pass(Input, Output), "Run a cheaper pass without expensive tools") :-
without_tools([expensive_api], (
format(string(Desc), "Process this input conservatively: ~w", [Input]),
prompt(Desc, Output)
)).
exec() is the direct path to runtime and MCP tools
Use exec(ToolCall, Result) when the step is deterministic or external by nature: shell commands,
HTTP fetches, search calls, browser steps, or runtime-specific orchestration helpers.
exec(web_search(query: "latest AI news"), SearchResult).
exec(url_fetch(url: "https://example.com", save_to: "notes/example.html"), FetchResult).
exec(vm_exec(command: "git status --short"), ShellResult),
get_dict(stdout, ShellResult, StatusText).
Common runtime tools
| Tool | Notes |
|---|---|
web_search |
Search the web through the configured runtime provider. |
news_search |
Search recent news results. |
url_fetch |
Fetch a page and optionally save it into the workspace. |
bash |
Run a shell command in the active workspace. |
vm_exec |
Alias of bash; with --sandbox, execution happens in AgentVM. |
- Other tools can be runtime-role specific. The conductor and skill creator each add their own higher-level exec tools.
- MCP servers can add more tools without changing the DML language itself.
- Shell tools run in the configured workspace. In sandbox mode the workspace is mounted at
/workspace.
Handling structured results
exec(vm_exec(command: "echo '{\"ok\": true}'"), ShellResult),
get_dict(stdout, ShellResult, Stdout),
atom_json_dict(Stdout, Data, []),
get_dict(ok, Data, IsOk).
Most runtime tools return dict-like structures. Use get_dict/3 for field access and atom_json_dict/3
when a tool emits JSON text that you want to parse back into a dict.
Backtracking and explicit memory control are first-class
DML keeps the normal Prolog model: clauses can fail, the runtime backtracks to alternatives, and memory is restored with the choice point. That makes fallback workflows natural instead of bolted on.
| Predicate | Purpose |
|---|---|
output(Text) / yield(Text) |
Emit progress output without ending the run. |
log(Text) |
Emit a debug-style log message. |
answer(Text) |
Emit the final answer and commit execution. |
push_context, push_context(clear), pop_context |
Save and restore memory manually when you want local isolation inside a clause. |
clear_memory |
Clear accumulated model memory. |
Fallback via multiple clauses
agent_main(Question) :-
system("Answer concisely."),
task("Answer this question: {Question}", Draft),
validate_answer(Draft),
answer(Draft).
agent_main(Question) :-
system("Use a more thorough approach."),
task("Research this question carefully: {Question}", Final),
answer(Final).
Recursion and list processing
process_files([]).
process_files([File|Rest]) :-
task("Review this file: {File}"),
process_files(Rest).
agent_main(Files) :-
process_files(Files),
answer("All files processed.").
Normal Prolog constructs such as fail, true, disjunction, if-then-else, list decomposition,
arithmetic with is/2, and cuts remain available.
Named parameters, interpolation, and helper file loading
DML supports named runtime parameters, compile-time string interpolation, and workspace-relative helper loading. These features are what make larger skills maintainable.
Named params
agent_main :-
param(system_prompt, SystemPrompt),
param(mode, "Requested execution mode", Mode),
system(SystemPrompt),
answer(Mode).
param(Key, Value)looks up a named parameter passed at runtime.param(Key, Description, Value)is the self-documented form used by system skills and tooling.- Named params are especially useful for configuration-like inputs that should not occupy positional
agent_mainarguments.
Parameter declaration directives
:- param(system_prompt, "Optional system prompt override").
:- param(limit, "Maximum result count", 10).
These directives let a skill declare expected named parameters for documentation and tooling purposes.
Runtime lookups still happen through param/2 or param/3 inside executable clauses.
String interpolation
task("Summarize the findings for {Topic}", Summary).
output("Processed {Count} files successfully.").
Quoted strings support {Var} interpolation against bound variables. Use format(string(...), ...)
when you need precise formatting or want to build strings incrementally from non-string terms.
Libraries and helper files
:- use_module(library(lists)).
:- use_module(library(clpfd)).
:- consult('.deepclause/tools/my-skill/helpers/search_helpers.dml').
- Use
:- use_module(library(...)).for standard SWI-Prolog libraries such aslists,clpfd,clpq, andclpr. - Use
:- consult('relative/path.dml').for workspace-local helper predicates. - Local
:- use_module('path/to/helper.dml').also works today, but it behaves more like consult-style loading than strict SWI export filtering. - Keep non-DML helper assets, scripts, and per-skill virtual environments under
.deepclause/tools/lib/<name>/. Keep consulted.dmlhelpers wherever the skill structure stays clearest.
Write smaller, more explicit skills
- Use ordinary Prolog and
exec()for deterministic work such as parsing, arithmetic, file I/O, or branching on exact values. - Use
task()andprompt()only where open-ended reasoning or generation is genuinely needed. - Move reusable logic into helper predicates or helper files rather than growing one giant
agent_mainclause. - Prefer scoped tool exposure for nested model calls so the model only sees the tools relevant to that local step.
- Be explicit about output variables in prompt text. Smaller models behave better when the prompt states exactly what result names and formats are expected.
Common mistakes
- Treating a tool wrapper like a normal predicate call. Use
exec()directly or let the model call the wrapper duringtask()orprompt(). - Assuming nested
task()calls inherit the parent memory. They do not. - Forgetting that output variables in a task loop must be set through
set_resultbeforefinish(true). - Overloading positional arguments when a named param would be easier to read and safer to evolve.
- Using the model for logic that could be encoded directly as a deterministic predicate.