deepclause-sdk 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent.d.ts +12 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +116 -9
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +24 -24
- package/dist/cli/repl.d.ts +21 -4
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +293 -44
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +10 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/prolog/loader.d.ts +32 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +87 -0
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +1028 -118
- package/dist/repl/agent.d.ts +15 -1
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +44 -5
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/backend.d.ts.map +1 -1
- package/dist/repl/backend.js +18 -5
- package/dist/repl/backend.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +24 -2
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +14 -5
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/index.d.ts +8 -0
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +1 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/render.d.ts +60 -3
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +114 -28
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +61 -4
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +174 -9
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/status.d.ts +3 -1
- package/dist/repl/status.d.ts.map +1 -1
- package/dist/repl/status.js +4 -0
- package/dist/repl/status.js.map +1 -1
- package/dist/repl/tool-arguments.d.ts +36 -0
- package/dist/repl/tool-arguments.d.ts.map +1 -0
- package/dist/repl/tool-arguments.js +123 -0
- package/dist/repl/tool-arguments.js.map +1 -0
- package/dist/repl/toolset.d.ts +28 -1
- package/dist/repl/toolset.d.ts.map +1 -1
- package/dist/repl/toolset.js +52 -4
- package/dist/repl/toolset.js.map +1 -1
- package/dist/repl/tui/autocomplete.d.ts +19 -0
- package/dist/repl/tui/autocomplete.d.ts.map +1 -0
- package/dist/repl/tui/autocomplete.js +41 -0
- package/dist/repl/tui/autocomplete.js.map +1 -0
- package/dist/repl/tui/document.d.ts +51 -0
- package/dist/repl/tui/document.d.ts.map +1 -0
- package/dist/repl/tui/document.js +160 -0
- package/dist/repl/tui/document.js.map +1 -0
- package/dist/repl/tui/picker.d.ts +31 -0
- package/dist/repl/tui/picker.d.ts.map +1 -0
- package/dist/repl/tui/picker.js +43 -0
- package/dist/repl/tui/picker.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts +98 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -0
- package/dist/repl/tui/renderer.js +284 -0
- package/dist/repl/tui/renderer.js.map +1 -0
- package/dist/repl/tui/status-bar.d.ts +4 -2
- package/dist/repl/tui/status-bar.d.ts.map +1 -1
- package/dist/repl/tui/status-bar.js +80 -24
- package/dist/repl/tui/status-bar.js.map +1 -1
- package/dist/repl/tui/terminal.d.ts +28 -0
- package/dist/repl/tui/terminal.d.ts.map +1 -0
- package/dist/repl/tui/terminal.js +94 -0
- package/dist/repl/tui/terminal.js.map +1 -0
- package/dist/repl/tui/theme.d.ts +60 -0
- package/dist/repl/tui/theme.d.ts.map +1 -1
- package/dist/repl/tui/theme.js +142 -0
- package/dist/repl/tui/theme.js.map +1 -1
- package/dist/repl/tui/tool-pane.d.ts +68 -0
- package/dist/repl/tui/tool-pane.d.ts.map +1 -0
- package/dist/repl/tui/tool-pane.js +257 -0
- package/dist/repl/tui/tool-pane.js.map +1 -0
- package/dist/repl/tui/working-line.d.ts +23 -0
- package/dist/repl/tui/working-line.d.ts.map +1 -0
- package/dist/repl/tui/working-line.js +58 -0
- package/dist/repl/tui/working-line.js.map +1 -0
- package/dist/runner.d.ts +31 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +192 -17
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +925 -43
- package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
- package/dist/system/assets/docs/DML_REFERENCE.md +620 -39
- package/dist/system/assets/docs/PROLOG_SNIPPETS.md +45 -1
- package/dist/system/assets/docs/TASK_PROMPT.md +2 -0
- package/dist/system/environment.d.ts +36 -0
- package/dist/system/environment.d.ts.map +1 -0
- package/dist/system/environment.js +81 -0
- package/dist/system/environment.js.map +1 -0
- package/dist/system/runtime/shell-manager.d.ts +40 -2
- package/dist/system/runtime/shell-manager.d.ts.map +1 -1
- package/dist/system/runtime/shell-manager.js +82 -16
- package/dist/system/runtime/shell-manager.js.map +1 -1
- package/dist/system/runtime/shell-tool-events.js +25 -1
- package/dist/system/runtime/shell-tool-events.js.map +1 -1
- package/dist/types.d.ts +27 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/prolog-src/deepclause_mi.pl +1028 -118
- package/dist/compaction.d.ts +0 -98
- package/dist/compaction.d.ts.map +0 -1
- package/dist/compaction.js +0 -471
- package/dist/compaction.js.map +0 -1
- package/dist/repl/tools.d.ts +0 -53
- package/dist/repl/tools.d.ts.map +0 -1
- package/dist/repl/tools.js +0 -307
- package/dist/repl/tools.js.map +0 -1
- package/dist/system/assets/compactors/default-loop-compactor.dml +0 -43
- package/dist/system/assets/compactors/default-session-compactor.dml +0 -30
- package/dist/system/assets/docs/META_PROMPT.md +0 -68
|
@@ -6,6 +6,85 @@ reusable harness: a library of predicates, each one a skill.
|
|
|
6
6
|
The REPL may start with an **empty module** or with a module **loaded** from
|
|
7
7
|
disk. Inspect what is there before you change anything.
|
|
8
8
|
|
|
9
|
+
# Two plans: build a harness, or change one
|
|
10
|
+
|
|
11
|
+
Everything after this section is reference. This is the order to work in. Both
|
|
12
|
+
plans start the same way — **think, then say what you are going to do** — and both
|
|
13
|
+
end at the invariants listed at the end of this section.
|
|
14
|
+
|
|
15
|
+
## To build a harness
|
|
16
|
+
|
|
17
|
+
1. **Understand the request before touching the module.** Settle, for yourself,
|
|
18
|
+
the inputs, the output, and what makes the result correct. If that does not
|
|
19
|
+
settle it — the inputs are unclear, "it" refers to something you cannot see,
|
|
20
|
+
two designs would both do — **ask the user and stop**, ending the turn with the
|
|
21
|
+
question and no tool call. One question costs one turn; a harness built on a
|
|
22
|
+
guess costs the work, the rework, and the user may never learn a guess was made.
|
|
23
|
+
2. **Propose the steps, and let the user agree before you build.** Name the
|
|
24
|
+
predicates you will add, which one is the entry point, what each does, and how
|
|
25
|
+
you will know it works. For a new harness this is *the* important step: the
|
|
26
|
+
design is still a sentence, so redirecting it is cheap. For a small addition to
|
|
27
|
+
an existing harness, one line stating the change is enough.
|
|
28
|
+
3. **Read first.** `listing` for what the module has; `listing(Name/Arity)` and
|
|
29
|
+
`doc/2` for anything close to the request; `reference("examples")` or
|
|
30
|
+
`reference("patterns")` for a shape that already fits. Reuse or generalize an
|
|
31
|
+
existing predicate rather than adding a second one.
|
|
32
|
+
4. **Write the smallest end-to-end version** — one predicate, thin guards, at most
|
|
33
|
+
a couple of `task/N` / `prompt/N` calls. No options, no handling for cases that
|
|
34
|
+
have not happened, no helper layer yet (*Start small, then improve*).
|
|
35
|
+
5. **Add it in one transaction:** `begin_turn`, then `define(Head, "clauses")` with
|
|
36
|
+
the whole predicate and its clauses in the order written, then `end_turn`.
|
|
37
|
+
6. **Run it on a real input** with `execute` — the actual request, not `foo`. Fix
|
|
38
|
+
what the run shows, and `rollback` a transaction that failed.
|
|
39
|
+
7. **Answer the `end_turn` warnings**, especially nondeterminism: a predicate that
|
|
40
|
+
calls `task/N`, `exec/2`, or writes a file must yield exactly one solution
|
|
41
|
+
(`once/1`, `->`, or `!`).
|
|
42
|
+
8. **Stop and show the user**: what works, what it does not do yet, how to run it
|
|
43
|
+
(`deepclause run <file>`, or `/dc-run <skill>` under pi), and what you would add
|
|
44
|
+
next. Extend when they ask for the next piece.
|
|
45
|
+
9. **Only then the production details** — typed or `schema/2` outputs, a
|
|
46
|
+
`script_file/2` for mechanical work, `tool/2` to expose the predicate, and
|
|
47
|
+
`doc/2` + `module_doc/1` to describe it.
|
|
48
|
+
|
|
49
|
+
## To change a harness
|
|
50
|
+
|
|
51
|
+
1. **Read the current behaviour** — `listing(Name/Arity)`, its `doc/2`,
|
|
52
|
+
`module_doc/1`. Do not edit from memory, or from what the name suggests.
|
|
53
|
+
2. **Reproduce it** with `execute`, and keep the actual output. A fix for
|
|
54
|
+
something you cannot reproduce is a guess. When it is not obvious where the
|
|
55
|
+
chain goes wrong, add intermediate `output/1` calls —
|
|
56
|
+
`format(string(M), "after step 2: ~w", [X]), output(M)` — to see how far it
|
|
57
|
+
gets and what the values are. They are invisible to the model, so they are the
|
|
58
|
+
cheap way to find the broken step; take them out (or keep the ones that read
|
|
59
|
+
as progress) before you finish.
|
|
60
|
+
3. **Say what you will change and why**, in a line or two, before editing. If the
|
|
61
|
+
right change is not obvious — the behaviour is right in one case and wrong in
|
|
62
|
+
another, or the fix would alter a contract other predicates rely on — **ask the
|
|
63
|
+
user rather than deciding for them.**
|
|
64
|
+
4. **Make the smallest change** that makes that case work, preferring to generalize
|
|
65
|
+
the existing predicate. `listing` shows what else calls it.
|
|
66
|
+
5. **Replace by predicate:** `begin_turn`, `define` — not clause-by-clause
|
|
67
|
+
`assert`/`retract` patching — `end_turn`.
|
|
68
|
+
6. **Re-run the same goal** and compare with step 2. If the change is
|
|
69
|
+
user-visible, say so plainly; do not widen the scope while you are in there.
|
|
70
|
+
7. **Keep the documentation in step** (`doc/2`, `module_doc/1`), and leave no
|
|
71
|
+
transaction open.
|
|
72
|
+
|
|
73
|
+
## Neither plan is a rabbit hole
|
|
74
|
+
|
|
75
|
+
- One step at a time, on the code that step needs. Do not refactor, rename, or tidy
|
|
76
|
+
what the request did not ask about.
|
|
77
|
+
- If a step is not working after a couple of attempts, stop and report what you
|
|
78
|
+
tried, what happened, and what you need. Reporting is cheaper than grinding.
|
|
79
|
+
- Do not search the machine, or read the whole reference, for a convention that is
|
|
80
|
+
not documented: the sections below say what exists.
|
|
81
|
+
- Hand over what works rather than perfecting it privately. An incomplete harness
|
|
82
|
+
that runs and has been demonstrated beats a complete one nobody has seen.
|
|
83
|
+
|
|
84
|
+
The invariants both plans end at: edit only through the transaction tools; never
|
|
85
|
+
load a file to change the module; paths are `/workspace/...`; do not parse text in
|
|
86
|
+
DML; take inputs as arguments; give a side effect exactly one solution.
|
|
87
|
+
|
|
9
88
|
# The module
|
|
10
89
|
|
|
11
90
|
A module is a set of Prolog clauses plus annotations:
|
|
@@ -62,6 +141,10 @@ Call `listing` at the start of a session and again after every change.
|
|
|
62
141
|
and output. A goal is ordinary Prolog/DML, e.g. `count_words("report.txt", N)`.
|
|
63
142
|
The result also carries `elapsedMs`, the wall-clock duration, so you can see
|
|
64
143
|
what is slow.
|
|
144
|
+
- When a goal binds one large term, pass `bindings: "brief"` (names kept, values
|
|
145
|
+
shortened) or `bindings: "none"` (nothing returned) so a term you are not
|
|
146
|
+
reading does not fill the transcript and your context. Both say what they left
|
|
147
|
+
out, so an omitted binding never looks like a goal that bound nothing.
|
|
65
148
|
- It runs on a scratch copy: it does not commit anything. Use it to test.
|
|
66
149
|
|
|
67
150
|
# Modify the module
|
|
@@ -71,9 +154,14 @@ Editing is a **transaction**:
|
|
|
71
154
|
- `begin_turn` — open a transaction (snapshot the module). **Required before any
|
|
72
155
|
edit.**
|
|
73
156
|
- `define(head, clauses)` — replace the predicate(s) the clause block defines.
|
|
74
|
-
- `assert(clause)` — add
|
|
75
|
-
|
|
76
|
-
|
|
157
|
+
- `assert(clause)` / `assert_many(clauses)` — add clauses to a **new**
|
|
158
|
+
predicate, in written order. Refused when the predicate already has clauses:
|
|
159
|
+
appending would leave both versions live with their clauses interleaved. Use
|
|
160
|
+
`define` to change an existing predicate.
|
|
161
|
+
- `retract(clause)` — remove the clauses **matching** a pattern, e.g.
|
|
162
|
+
`retract("f(1)")` or `retract("f(X) :- g(X)")`; it reports how many were
|
|
163
|
+
removed and fails when none matched.
|
|
164
|
+
- `delete("Name/Arity")` — remove a whole predicate.
|
|
77
165
|
- `end_turn` — validate and commit the change (records a version). It also
|
|
78
166
|
probes the predicates you changed and returns a `warnings` list (shown as `⚠`)
|
|
79
167
|
when a helper looks nondeterministic — e.g. yields many solutions on fresh
|
|
@@ -86,6 +174,18 @@ call `begin_turn` again before editing further. If you finish a turn with a
|
|
|
86
174
|
transaction still open, the host nudges you to `end_turn` or `rollback`. Commit
|
|
87
175
|
incrementally for anything larger than a couple of predicates.
|
|
88
176
|
|
|
177
|
+
**Change the module only through those tools.** Do not add code by loading a file:
|
|
178
|
+
`:- consult("…")`, a file-list directive, `use_module` on a workspace `.dml`, or
|
|
179
|
+
writing a file and loading it are *load* paths, not edits. They assert straight into
|
|
180
|
+
the live module instead of through the edit tools, so those predicates are missing
|
|
181
|
+
from the transaction's tracked set: the nondeterminism probe skips them, and a
|
|
182
|
+
directive that fails inside the file only warns. They still show up in the next
|
|
183
|
+
commit's diff, so the change lands with no record of where it came from — which is
|
|
184
|
+
exactly what makes it hard to review or undo. If you want code from another file,
|
|
185
|
+
read it (`bash`, `read_file`) and add it with `assert_many`/`define`. Loading a
|
|
186
|
+
harness (`-l`, `/load`) is a user action; if that is what is wanted, say so rather
|
|
187
|
+
than performing it as an edit.
|
|
188
|
+
|
|
89
189
|
Other tools: `reference(topic)` reads the DeepClause docs (`language`,
|
|
90
190
|
`authoring`, `patterns`, `examples`, `snippets`, `tools`).
|
|
91
191
|
|
|
@@ -191,6 +291,51 @@ recent_changes(N, Cs) :-
|
|
|
191
291
|
last_n(All, N, Cs).
|
|
192
292
|
```
|
|
193
293
|
|
|
294
|
+
# Start small, then improve
|
|
295
|
+
|
|
296
|
+
Build the smallest thing that answers the request end to end — often one predicate
|
|
297
|
+
— run it on a real input, and **stop to show the user**. Say what works, what it
|
|
298
|
+
does not do yet, and what you would add next; let their reply decide. A harness
|
|
299
|
+
should grow in the direction the user takes it, not in the direction you imagine.
|
|
300
|
+
|
|
301
|
+
Do not add capability before a request needs it: no error taxonomy before an error,
|
|
302
|
+
no retry or fallback policy before a failure, no caching, options, or configuration
|
|
303
|
+
layer without a reason, no predicate per case for cases you have not seen. Prefer
|
|
304
|
+
the smallest change that makes the failing input work.
|
|
305
|
+
|
|
306
|
+
This is not only taste. Every predicate is part of the harness the next turn has to
|
|
307
|
+
reason about, and every `tool/2` is another action offered to a model that has to
|
|
308
|
+
choose well. A small commit is one reviewable unit, the `end_turn` probes report on
|
|
309
|
+
a small surface, and `rollback` stays cheap. **A harness that is incomplete but
|
|
310
|
+
working and demonstrated is a better answer than one that is complete and
|
|
311
|
+
unverified.**
|
|
312
|
+
|
|
313
|
+
Signs you are overengineering: a predicate with one caller whose arguments are all
|
|
314
|
+
literals from the request; an abstraction built for a single shape; a helper
|
|
315
|
+
"framework" written before the first end-to-end run; a harness that does not run
|
|
316
|
+
yet because the parts are still being made tidy.
|
|
317
|
+
|
|
318
|
+
# Do not parse in DML
|
|
319
|
+
|
|
320
|
+
Prolog string handling is the fiddliest part of this language, and a hand-rolled
|
|
321
|
+
parser out of `sub_string/5`, `split_string/4`, `atom_concat/3`, `format/2` and an
|
|
322
|
+
accumulator is the easiest way to write DML that looks right and is quietly wrong.
|
|
323
|
+
Do not build one. Decide what the work is and put it where it belongs:
|
|
324
|
+
|
|
325
|
+
- **Mechanical** — pull a field, change a format, count, filter, join: write a
|
|
326
|
+
**helper script** (`script_file/2`, run with `exec(bash(argv: [...]))`). It is a
|
|
327
|
+
real file you can test on its own, in a language built for text. Have it emit
|
|
328
|
+
**JSON** and read it with `json_get/3` / `json_gets/3` / `json_text/2`, rather
|
|
329
|
+
than splitting its stdout by hand.
|
|
330
|
+
- **Meaning** — find the field named X, classify, summarise, decide: make it a
|
|
331
|
+
**model call** with a typed output: `task("…", list(string(Items)))`, `schema/2`,
|
|
332
|
+
or `choose`/`rate`/`verify` for a bounded judgment.
|
|
333
|
+
|
|
334
|
+
`exec(bash(...))` takes `command` or `argv` (plus `timeoutMs`) — there is **no
|
|
335
|
+
`stdin`**. Pass data as an `argv` element or through a workspace file. `argv` is
|
|
336
|
+
quoted by the runtime, so an argument can contain quotes, spaces, `$` or newlines
|
|
337
|
+
safely; `command` is for pipelines and redirection.
|
|
338
|
+
|
|
194
339
|
# Keep DML simple: orchestration, not algorithms
|
|
195
340
|
|
|
196
341
|
DML is an **orchestration and verification** layer. A skill should read as a
|
|
@@ -239,6 +384,77 @@ There are no other host tools. Do not invent `fetch_url`, `web_search`,
|
|
|
239
384
|
`calculator`, etc. — if the module needs one, write a script and run it with
|
|
240
385
|
`bash`.
|
|
241
386
|
|
|
387
|
+
# Declared output shapes
|
|
388
|
+
|
|
389
|
+
`list(object(B))` says "a list of dicts" but not what is in them, which is how a
|
|
390
|
+
harness ends up with `""` where an affiliation should be, or an empty result where
|
|
391
|
+
the model used a different key spelling. Declare the shape once and name it:
|
|
392
|
+
|
|
393
|
+
```prolog
|
|
394
|
+
schema(author, [
|
|
395
|
+
field(name, string, required),
|
|
396
|
+
field(affiliation, string, default("not stated")),
|
|
397
|
+
field(email, string, default(""))
|
|
398
|
+
]).
|
|
399
|
+
|
|
400
|
+
extract_authors(Text, Authors) :-
|
|
401
|
+
task("From the front matter extract every author: {Text}",
|
|
402
|
+
list(schema(author, Authors))).
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
Defaults are filled, an absent `required` field or a wrong type fails the step with
|
|
406
|
+
a message naming the field (so the agent can fix it in the same turn), and an
|
|
407
|
+
undeclared field is kept so a model mistake stays visible; the field list is described
|
|
408
|
+
to the model in the step's own instructions. `schema/2` is a fact like
|
|
409
|
+
`doc/2`. Use `schema_fill(author, Raw, Author)` to apply one to a dict by hand.
|
|
410
|
+
|
|
411
|
+
# Parsing and formatting with one template
|
|
412
|
+
|
|
413
|
+
`pattern/3` builds a string and parses it with the same template. Literal text
|
|
414
|
+
matches exactly and `$name` is a slot; the direction follows from what is bound:
|
|
415
|
+
|
|
416
|
+
```prolog
|
|
417
|
+
pattern("Name: $name, age: $age", Text, [name="Ada", age=36]). % build
|
|
418
|
+
pattern("Name: $name, age: $age", "Name: Ada, age: 36", Bs). % parse
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
Bindings are `Name=Value` pairs. A slot consumes up to the next literal, so the
|
|
422
|
+
literals decide the split. `$name:int` converts a numeric slot. `$$` is a literal
|
|
423
|
+
dollar, and a single `{` or `}` is literal text, so a template can carry JSON.
|
|
424
|
+
|
|
425
|
+
Prefer this to asking a model to reformat text: parse what you can read exactly and
|
|
426
|
+
use a `task/N` only where language understanding is genuinely needed.
|
|
427
|
+
|
|
428
|
+
# Memory: `task/N` vs `prompt/N`
|
|
429
|
+
|
|
430
|
+
`task/N` **shares and accumulates** the conversation: it is sent the memory built
|
|
431
|
+
so far and its reply is written back, so a second `task/N` sees the first. A `task/N`
|
|
432
|
+
inside a `tool/2` body starts with **empty** memory (tool bodies run in their own
|
|
433
|
+
engine) — pass context as a tool argument, or add it with `system/1` in the body.
|
|
434
|
+
`system/1` and `user/1` contribute to that memory and are backtrackable.
|
|
435
|
+
|
|
436
|
+
`prompt/N` takes the same arguments but sends **no memory and no tools**, and does
|
|
437
|
+
not write to memory. Use it for a classification, extraction, or transformation
|
|
438
|
+
that must answer from its own prompt alone, or when a long history is interfering.
|
|
439
|
+
A step that needs the harness's tools must be a `task/N`.
|
|
440
|
+
|
|
441
|
+
`with_fresh_memory(Goal)` does the same for a **group** of steps: inside it the
|
|
442
|
+
memory starts empty and accumulates normally between the steps, and it is restored
|
|
443
|
+
on exit, so nothing crosses in either direction. Wrap a multi-step sub-computation
|
|
444
|
+
whose internal back-and-forth is irrelevant to the caller:
|
|
445
|
+
|
|
446
|
+
```prolog
|
|
447
|
+
explain(Paper, Report) :-
|
|
448
|
+
with_fresh_memory((
|
|
449
|
+
task("Summarise the method of: {Paper}", Method),
|
|
450
|
+
task("Summarise the results of: {Paper}", Results),
|
|
451
|
+
task("Write an abstract from {Method} and {Results}", Report)
|
|
452
|
+
)).
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
Long task chains grow the context and cost; pass values as explicit arguments
|
|
456
|
+
rather than relying on the model to remember them.
|
|
457
|
+
|
|
242
458
|
# The `tool/2` annotation
|
|
243
459
|
|
|
244
460
|
`tool/2` is **only an annotation**: it exposes an existing predicate to the
|
|
@@ -251,9 +467,67 @@ tool(count_words(File, Count), "Count words in a file.") :-
|
|
|
251
467
|
count_words(File, Count).
|
|
252
468
|
```
|
|
253
469
|
|
|
470
|
+
The head and its delegate must have the **same name and arity**; the output is
|
|
471
|
+
the last argument. The tool body is what runs when a nested agent calls the tool,
|
|
472
|
+
so `tool(bibliography(Q, N), "…") :- bibliography(Q, N, E)` (different arity) or
|
|
473
|
+
`tool(citations(T, C), "…") :- citations(T, C)` (calls itself) fails only at call
|
|
474
|
+
time. `end_turn` reports both as tool-contract warnings — fix them, don't ship
|
|
475
|
+
them. The only tools a nested agent can call are the harness's `tool/2`
|
|
476
|
+
predicates; a host tool like `bash` is reachable from DML through `exec/2`, so a
|
|
477
|
+
task description may only name a tool the module itself defines and annotates.
|
|
478
|
+
|
|
479
|
+
**Do not annotate an LLM step with a `tool/2` that advertises the same job.** The
|
|
480
|
+
tool is declared to the agent inside that same `task/N`, so the description invites
|
|
481
|
+
it to call the tool to do what it was already asked. The runtime refuses the
|
|
482
|
+
re-entrant call, but the agent wastes a turn, is told the tool failed, and does the
|
|
483
|
+
work by hand:
|
|
484
|
+
|
|
485
|
+
```prolog
|
|
486
|
+
% BAD: extract_authors is offered to the agent inside extract_authors's task/N.
|
|
487
|
+
extract_authors(Text, Authors) :- task("Extract every author from: {Text}", list(object(Authors))).
|
|
488
|
+
tool(extract_authors(Text, Authors), "Extract every author.") :- extract_authors(Text, Authors).
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
`tool/2` is for **capabilities** a nested agent should use — reading a PDF,
|
|
492
|
+
searching an index, shelling out — not for the LLM steps themselves. Keep a
|
|
493
|
+
`task/N`-backed predicate a skill (call it with `/goal`, `execute`, or from another
|
|
494
|
+
predicate). If the tool must stay, scope the nested task:
|
|
495
|
+
|
|
496
|
+
```prolog
|
|
497
|
+
extract_authors(Text, Authors) :-
|
|
498
|
+
without_tools([extract_authors],
|
|
499
|
+
task("Extract every author from: {Text}", list(object(Authors)))).
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
`with_tools([…], Goal)` narrows to a list; `without_tools([…], Goal)` removes one.
|
|
503
|
+
They apply to the `task/N` they wrap, however it was reached.
|
|
504
|
+
|
|
254
505
|
Logic placed only inside `tool/2` is **not** reachable from `execute`/`listing`;
|
|
255
506
|
keep the `tool/2` body a one-line delegation.
|
|
256
507
|
|
|
508
|
+
# Dictionaries and JSON from model output
|
|
509
|
+
|
|
510
|
+
Dict keys are **atoms** and `get_dict/3` throws on a string key
|
|
511
|
+
(`get_dict("name", D)` → `type_error(dict-key, name)`), while the model varies the
|
|
512
|
+
spelling (`Name`/`name`/`full_name`). Read model fields with `dict_field/3,4`:
|
|
513
|
+
`dict_field(D, "affiliation", A)`, or `dict_field(D, country, "", C)` for a
|
|
514
|
+
default. Reserve `get_dict/3` for keys you control.
|
|
515
|
+
|
|
516
|
+
For anything nested, use a JSON path instead of chaining lookups:
|
|
517
|
+
`json_get(Doc, "results.0.authors.1.name", Name)`,
|
|
518
|
+
`json_get(Doc, "results.0.year", "unknown", Year)`,
|
|
519
|
+
`json_gets(Doc, "results.*.title", Titles)` (collect across a list; nested `*`
|
|
520
|
+
flattens), and `json_put(Doc, "meta.reviewed", yes, Updated)`. A missing path
|
|
521
|
+
fails, never yields `""` and never throws.
|
|
522
|
+
|
|
523
|
+
JSON text both ways is `atom_json_dict/3` — `atom_json_dict(Text, Dict, [])` with
|
|
524
|
+
`Text` bound parses, with `Text` unbound it writes. `json_text/2` is the compact
|
|
525
|
+
writer.
|
|
526
|
+
|
|
527
|
+
**Parsing text:** prefer a DCG (`-->`, which the loader compiles) over a pile of
|
|
528
|
+
regex calls. `phrase/2` takes a **char list**, so run `string_chars(Text, Chars)`
|
|
529
|
+
first — passing a string raises `type_error(list, …)`.
|
|
530
|
+
|
|
257
531
|
# File I/O
|
|
258
532
|
|
|
259
533
|
Prefer `bash` (`exec(bash(command: ...), Result)`) for reading, writing, copying,
|
|
@@ -262,6 +536,11 @@ and converting files. Use the DML file predicates (`read_file/2`, `write_file/2`
|
|
|
262
536
|
bound and reasoned over inside Prolog. Every path is workspace-relative and
|
|
263
537
|
confined to the workspace.
|
|
264
538
|
|
|
539
|
+
These predicates are real session clauses, so they resolve identically from DML
|
|
540
|
+
and from a host query. `open/3`, `close/1`, `read_string/3` and `write/2` cannot
|
|
541
|
+
be made workspace-resolving (SWI will not shadow an imported builtin) — use
|
|
542
|
+
`read_file/2` and `write_file/2` for whole-file I/O.
|
|
543
|
+
|
|
265
544
|
# Mixing languages: interpolation, quoting, escaping
|
|
266
545
|
|
|
267
546
|
DML often drives a second language — a shell command, a Python/Node script, a
|
|
@@ -281,7 +560,19 @@ implements. Never nest code inside code.
|
|
|
281
560
|
doubles a single quote (`'it''s'`).
|
|
282
561
|
- Prefer `~q`/`~s` over `~w` whenever the value is not a controlled atom.
|
|
283
562
|
|
|
284
|
-
## Shell: pass data
|
|
563
|
+
## Shell: pass data as a list
|
|
564
|
+
|
|
565
|
+
`exec(bash(argv: [...], timeoutMs: N), R)` takes the command and its arguments as
|
|
566
|
+
a **list**. The runtime quotes every element, so an argument containing a space, a
|
|
567
|
+
quote, `$`, a backtick or `;` is data and cannot escape:
|
|
568
|
+
|
|
569
|
+
```prolog
|
|
570
|
+
exec(bash(argv: ["python3", Script, Text]), R), get_dict(stdout, R, Text).
|
|
571
|
+
```
|
|
572
|
+
|
|
573
|
+
Use `argv` whenever any argument is not a literal you wrote yourself — a path from
|
|
574
|
+
a file, model output, user input. Use `command` only for pipelines and redirection,
|
|
575
|
+
where every interpolated value must still go through a file or stdin.
|
|
285
576
|
|
|
286
577
|
Do not interpolate data into a command string — quoting differs and injection is
|
|
287
578
|
easy:
|
|
@@ -426,11 +717,21 @@ More patterns: `reference("patterns")`; shapes: `reference("examples")`.
|
|
|
426
717
|
|
|
427
718
|
# Editing rules
|
|
428
719
|
|
|
720
|
+
- **Edit through the transaction tools only** — `begin_turn` then
|
|
721
|
+
`define`/`assert`/`assert_many`/`retract`/`delete`, then `end_turn`. Never
|
|
722
|
+
`consult`, `use_module` a workspace `.dml`, or load a file to add code: those
|
|
723
|
+
assert past the edit tools, so the predicates miss the tracked set and the probes
|
|
724
|
+
that go with it, and the change arrives in the diff with nothing saying who made
|
|
725
|
+
it or why.
|
|
429
726
|
- Reuse before you add: generalize a predicate and take its inputs as arguments;
|
|
430
727
|
never bake in a host path, a fixed filename, or a one-off value.
|
|
431
728
|
- Add a `doc/2` for every predicate you add or change, and keep `module_doc/1`
|
|
432
729
|
current — it is the skill description.
|
|
433
730
|
- Test with `execute` before `end_turn`.
|
|
731
|
+
- Do not parse text in DML: shell out to a helper script for mechanical work, or
|
|
732
|
+
ask the model with a typed output. There is no `stdin` on `exec(bash(...))`.
|
|
733
|
+
- Ship the smallest working version first and show the user; add the next piece when
|
|
734
|
+
the next request asks for it. Do not build for cases nobody has raised.
|
|
434
735
|
- After finishing, if the work revealed a reusable capability, say so and offer
|
|
435
736
|
to add it rather than committing it unasked.
|
|
436
737
|
|
|
@@ -543,8 +844,28 @@ agent_main :-
|
|
|
543
844
|
|
|
544
845
|
### Task Predicates
|
|
545
846
|
|
|
847
|
+
Both `task/N` and `prompt/N` run an LLM step and bind typed outputs. They differ in
|
|
848
|
+
**what conversation the model sees**, which is the single most important thing to
|
|
849
|
+
know about them.
|
|
850
|
+
|
|
851
|
+
| | `task/N` | `prompt/N` |
|
|
852
|
+
|---|---|---|
|
|
853
|
+
| Memory | the accumulated conversation, **and it is written back** | **fresh** — empty; does not read or write it |
|
|
854
|
+
| Tools | the harness's `tool/2` predicates | **none** (plain text completion) |
|
|
855
|
+
| Use for | a step in a multi-step flow that should know what came before | classification, extraction, transformation, or a bounded question that must not be swayed by the conversation |
|
|
856
|
+
|
|
546
857
|
#### `task(+Description)`
|
|
547
858
|
|
|
859
|
+
Invoke an LLM to perform a task. The description is sent to the model.
|
|
860
|
+
|
|
861
|
+
```prolog
|
|
862
|
+
agent_main :-
|
|
863
|
+
system("You are a coding assistant."),
|
|
864
|
+
task("Write a function to calculate factorial in Python."),
|
|
865
|
+
answer("Code generated!").
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
|
|
548
869
|
Invoke an LLM to perform a task. The description is sent to the model.
|
|
549
870
|
|
|
550
871
|
```prolog
|
|
@@ -597,6 +918,132 @@ Bind up to three variables.
|
|
|
597
918
|
> `listing` and `export` reverse this, so you always read/write `task/N` and
|
|
598
919
|
> `prompt/N` — never author `task_named/3` yourself.
|
|
599
920
|
|
|
921
|
+
### Memory: what the model sees, and how it grows
|
|
922
|
+
|
|
923
|
+
A `task/N` call is **not** a fresh conversation. It is sent the memory the session
|
|
924
|
+
has accumulated, and the agent's full resulting memory is written back — so each
|
|
925
|
+
`task/N` sees every earlier one, plus its own prompt and reply:
|
|
926
|
+
|
|
927
|
+
```prolog
|
|
928
|
+
step(Text, Summary, Tags) :-
|
|
929
|
+
task("Summarise: {Text}", Summary), % sees: nothing yet
|
|
930
|
+
task("Tag this summary: {Summary}", Tags). % sees: the first task's prompt and reply
|
|
931
|
+
```
|
|
932
|
+
|
|
933
|
+
`system/1` and `user/1` add to that same memory, and **they are backtrackable** —
|
|
934
|
+
a `system/1` inside a branch that is later undone stops contributing:
|
|
935
|
+
|
|
936
|
+
```prolog
|
|
937
|
+
brief(Text, Out) :-
|
|
938
|
+
system("Answer in one sentence."),
|
|
939
|
+
task("Summarise: {Text}", Out).
|
|
940
|
+
```
|
|
941
|
+
|
|
942
|
+
Two consequences worth planning for:
|
|
943
|
+
|
|
944
|
+
- **Memory grows with every task.** A loop of `task/N` calls sends an ever longer
|
|
945
|
+
conversation, which costs tokens and eventually dilutes the instruction. Pass
|
|
946
|
+
values as explicit arguments rather than relying on the model to remember, and
|
|
947
|
+
start a fresh step when the history is no longer relevant.
|
|
948
|
+
- **A `task/N` inside a `tool/2` starts with empty memory.** Tool bodies run in
|
|
949
|
+
their own engine, which is created with no memory of the caller, so a nested
|
|
950
|
+
agent sees only what the *tool arguments* carry. Put the context in the argument,
|
|
951
|
+
or add it inside the tool body with `system/1`:
|
|
952
|
+
|
|
953
|
+
```prolog
|
|
954
|
+
summarise(Text, Summary) :-
|
|
955
|
+
task("Summarise this text in two sentences: {Text}", Summary).
|
|
956
|
+
|
|
957
|
+
tool(summarise(Text, Summary), "Summarise a passage.") :-
|
|
958
|
+
summarise(Text, Summary).
|
|
959
|
+
```
|
|
960
|
+
|
|
961
|
+
**A fresh context: `prompt/N`, or `with_fresh_memory/2`.** `prompt/N` takes the same
|
|
962
|
+
arguments as `task/N` but sends **no memory and no tools**, and leaves the memory
|
|
963
|
+
untouched. Use it when the step must answer from its own prompt alone — a
|
|
964
|
+
classifier, an extractor, a translation — or when a long history has started to
|
|
965
|
+
interfere:
|
|
966
|
+
|
|
967
|
+
```prolog
|
|
968
|
+
route(Message, Team) :-
|
|
969
|
+
prompt("Classify as billing, orders or account: {Message}", Team).
|
|
970
|
+
|
|
971
|
+
% The conversation so far cannot leak into the classification, and the
|
|
972
|
+
% classification cannot leak into it.
|
|
973
|
+
```
|
|
974
|
+
|
|
975
|
+
Because `prompt/N` has no tools, a step that must read a file or search an index
|
|
976
|
+
has to be a `task/N`. Choose by what the step needs: the harness's capabilities
|
|
977
|
+
(`task/N`) or an uncontaminated answer (`prompt/N`).
|
|
978
|
+
|
|
979
|
+
`prompt/N` isolates **one call**. To isolate a **group** of steps — several
|
|
980
|
+
`task/N` calls that share a fresh context with each other but nothing with the
|
|
981
|
+
caller — wrap them in `with_fresh_memory/1`:
|
|
982
|
+
|
|
983
|
+
```prolog
|
|
984
|
+
describe(Paper, Report) :-
|
|
985
|
+
with_fresh_memory((
|
|
986
|
+
task("Summarise the method of: {Paper}", Method),
|
|
987
|
+
task("Summarise the results of: {Paper}", Results),
|
|
988
|
+
task("Write a two-sentence abstract from {Method} and {Results}", Report)
|
|
989
|
+
)).
|
|
990
|
+
% The three steps see each other's turns, because they accumulate within the
|
|
991
|
+
% scope. Nothing the caller accumulated reaches them, and nothing they
|
|
992
|
+
% accumulate reaches the caller — the memory is restored on exit.
|
|
993
|
+
```
|
|
994
|
+
|
|
995
|
+
With `with_tools/2` / `without_tools/2` it composes into a scope that controls both
|
|
996
|
+
the conversation and the capabilities:
|
|
997
|
+
|
|
998
|
+
```prolog
|
|
999
|
+
audit(App, Finding) :-
|
|
1000
|
+
with_fresh_memory(without_tools([apply_fix],
|
|
1001
|
+
task("Review {App} against the checklist and report one finding.", Finding))).
|
|
1002
|
+
```
|
|
1003
|
+
|
|
1004
|
+
### Declared Output Shapes: `schema/2`
|
|
1005
|
+
|
|
1006
|
+
A type says "a list of dicts"; a schema says what is *in* them. Declare the shape
|
|
1007
|
+
once, name it in the `task/N` output, and every step gets a value that matches it —
|
|
1008
|
+
defaults filled, and a missing required field or a wrong-typed field failing the
|
|
1009
|
+
step instead of quietly yielding an empty result:
|
|
1010
|
+
|
|
1011
|
+
```prolog
|
|
1012
|
+
schema(author, [
|
|
1013
|
+
field(name, string, required),
|
|
1014
|
+
field(affiliation, string, default("not stated")),
|
|
1015
|
+
field(email, string, default("")),
|
|
1016
|
+
field(country, string, default(""))
|
|
1017
|
+
]).
|
|
1018
|
+
|
|
1019
|
+
extract_authors(Text, Authors) :-
|
|
1020
|
+
task("From the front matter extract every author: {Text}",
|
|
1021
|
+
list(schema(author, Authors))).
|
|
1022
|
+
% Authors = [author{name:"Ashish Vaswani", affiliation:"Google Brain", email:"", country:""} …]
|
|
1023
|
+
```
|
|
1024
|
+
|
|
1025
|
+
- An absent field with a `default(Value)` is filled.
|
|
1026
|
+
- An absent `required` field **fails the step** with a message naming the field, so
|
|
1027
|
+
the agent can correct it in the same turn.
|
|
1028
|
+
- A field of the wrong type fails the same way. Types are `string`, `integer`,
|
|
1029
|
+
`number`, `boolean`, `object`, `array`.
|
|
1030
|
+
- An **undeclared** field is kept, not dropped: a model mistake stays visible
|
|
1031
|
+
- The field list is **described to the model** in the step's own instructions, and a
|
|
1032
|
+
missing required or wrong-typed field is reported by name inside the same turn,
|
|
1033
|
+
so the agent usually fixes it before the step ends
|
|
1034
|
+
rather than disappearing.
|
|
1035
|
+
- `schema/2` is a fact, like `doc/2` — it appears in the listing and travels with
|
|
1036
|
+
`export`.
|
|
1037
|
+
|
|
1038
|
+
For a single object rather than a list, use `schema(author, Author)` (no `list/1`).
|
|
1039
|
+
You can also apply a schema by hand to any dict:
|
|
1040
|
+
|
|
1041
|
+
```prolog
|
|
1042
|
+
schema_fill(author, Raw, Author), % fills defaults, validates
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
`end_turn` reports a `task/N` that names a schema which was never declared.
|
|
1046
|
+
|
|
600
1047
|
### Type-Safe Output Variables
|
|
601
1048
|
|
|
602
1049
|
You can wrap output variables with type specifiers to enforce strict validation on the LLM's output. The runtime will ensure the value matches the specified type.
|
|
@@ -655,11 +1102,15 @@ Or positional if the tool supports it:
|
|
|
655
1102
|
exec(get_time, CurrentTime)
|
|
656
1103
|
```
|
|
657
1104
|
|
|
658
|
-
The `bash` tool
|
|
659
|
-
wall-clock
|
|
1105
|
+
The `bash` tool takes exactly two inputs — `command` (a shell string) or `argv` (a
|
|
1106
|
+
list, quoted by the runtime) — plus an optional wall-clock `timeoutMs`. There is
|
|
1107
|
+
**no `stdin`**: pass data as an `argv` element, or write it to a workspace file and
|
|
1108
|
+
name the file. stdin is `/dev/null`, so a command that reads it sees EOF rather
|
|
1109
|
+
than waiting. It always returns the wall-clock duration as `timeMs`:
|
|
660
1110
|
|
|
661
1111
|
```prolog
|
|
662
1112
|
exec(bash(command: "sleep 30", timeoutMs: 5000), R),
|
|
1113
|
+
exec(bash(argv: ["python3", Script, Query]), R), % no quoting to write
|
|
663
1114
|
get_dict(timeMs, R, Took),
|
|
664
1115
|
( get_dict(exitCode, R, 124)
|
|
665
1116
|
-> format(string(Msg), "timed out after ~w ms", [Took])
|
|
@@ -692,6 +1143,25 @@ tool(count_words(File, Count), "Count words in a workspace file.") :-
|
|
|
692
1143
|
count_words(File, Count).
|
|
693
1144
|
```
|
|
694
1145
|
|
|
1146
|
+
The head and its delegate must have the **same name and the same arity**. The
|
|
1147
|
+
tool body is what runs when the nested agent calls the tool, so a body that
|
|
1148
|
+
calls a different arity either calls nothing (`existence_error`) or calls the
|
|
1149
|
+
tool again and recurses forever:
|
|
1150
|
+
|
|
1151
|
+
```prolog
|
|
1152
|
+
% WRONG: declares /2 but calls /3, so the tool fails when a nested agent uses it.
|
|
1153
|
+
tool(bibliography(Query, Max), "…") :-
|
|
1154
|
+
bibliography(Query, Max, Entries).
|
|
1155
|
+
|
|
1156
|
+
% WRONG: calls itself; a nested agent recurses until the budget is gone.
|
|
1157
|
+
tool(citations(Text, Citations), "…") :-
|
|
1158
|
+
citations(Text, Citations).
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
Nothing is checked when the clause is loaded, so both mistakes look fine until a
|
|
1162
|
+
nested agent calls the tool. `end_turn` reports them as contract warnings; treat
|
|
1163
|
+
them as errors, not style notes.
|
|
1164
|
+
|
|
695
1165
|
A predicate defined *only* inside a `tool/2` clause is invisible to
|
|
696
1166
|
`execute` / `/goal` — the loader asserts it as `tool(Head)`, never as `Head`. Split
|
|
697
1167
|
the two so one implementation is reachable from both surfaces:
|
|
@@ -701,6 +1171,12 @@ the two so one implementation is reachable from both surfaces:
|
|
|
701
1171
|
task("Count the words in report.txt.", N). % nested agent calls the tool
|
|
702
1172
|
```
|
|
703
1173
|
|
|
1174
|
+
The only tools a nested agent can call are the harness's `tool/2` predicates;
|
|
1175
|
+
registered host tools (for example `bash`) are reachable from DML code through
|
|
1176
|
+
`exec/2`, not as tools by name. So a task description that says "use the
|
|
1177
|
+
`fs_read` tool" works only if the module itself defines `fs_read/2` **and**
|
|
1178
|
+
annotates it.
|
|
1179
|
+
|
|
704
1180
|
The tool head's **last argument is the output**; the description is shown to the
|
|
705
1181
|
nested agent. Keep the `tool/2` body a single delegation — put no logic in it.
|
|
706
1182
|
|
|
@@ -723,26 +1199,103 @@ Tools can use `task()` and `prompt()` internally to combine Prolog logic with LL
|
|
|
723
1199
|
|
|
724
1200
|
```prolog
|
|
725
1201
|
% A predicate that computes then explains.
|
|
1202
|
+
% The nested task is told to work from the given figures — it has no tools it
|
|
1203
|
+
% needs, so it cannot try to re-enter this predicate.
|
|
726
1204
|
explain_calculation(A, B, Explanation) :-
|
|
727
1205
|
Sum is A + B, % Prolog computation
|
|
728
|
-
format(string(Desc),
|
|
729
|
-
|
|
1206
|
+
format(string(Desc),
|
|
1207
|
+
"Explain ~w + ~w = ~w to a child, using only these figures.",
|
|
1208
|
+
[A, B, Sum]),
|
|
1209
|
+
task(Desc, Explanation).
|
|
730
1210
|
|
|
731
1211
|
tool(explain_calculation(A, B, Explanation), "Calculate and explain the result.") :-
|
|
732
1212
|
explain_calculation(A, B, Explanation).
|
|
1213
|
+
```
|
|
1214
|
+
|
|
1215
|
+
**Do not wrap an LLM step in a `tool/2` that advertises the same job.** The tool
|
|
1216
|
+
is declared to the agent running *inside* that same `task/N`, so the description
|
|
1217
|
+
invites the agent to do the very thing it was asked, by calling the tool:
|
|
1218
|
+
|
|
1219
|
+
```prolog
|
|
1220
|
+
% WARNING: this shape self-delegates.
|
|
1221
|
+
extract_authors(Text, Authors) :-
|
|
1222
|
+
task("Extract every author and their affiliation(s) from: {Text}", list(object(Authors))).
|
|
1223
|
+
|
|
1224
|
+
tool(extract_authors(Text, Authors), "Extract every author and their affiliation(s).") :-
|
|
1225
|
+
extract_authors(Text, Authors).
|
|
1226
|
+
```
|
|
1227
|
+
|
|
1228
|
+
Called as a predicate (`/goal extract_authors(…, A)`, `execute`, or from another
|
|
1229
|
+
predicate), `extract_authors` runs its `task/N`, and that agent is offered
|
|
1230
|
+
`extract_authors` — a tool described as doing exactly its job. The runtime refuses
|
|
1231
|
+
the re-entrant call (a tool whose predicate is already running is withheld, and a
|
|
1232
|
+
call that slips through is refused with a reason), so it cannot recurse forever.
|
|
1233
|
+
But the agent still spends a turn on it, is told the tool failed, and then does
|
|
1234
|
+
the work by hand — a confusing transcript and a wasted call.
|
|
1235
|
+
|
|
1236
|
+
Two ways to avoid it, in order of preference:
|
|
1237
|
+
|
|
1238
|
+
**Keep the LLM step a skill, and give `tool/2` only to capabilities.** A skill is
|
|
1239
|
+
called from DML (`/goal`, `execute`, another predicate) and does not need to be a
|
|
1240
|
+
tool. Annotate the operations a nested agent should *use* — deterministic or
|
|
1241
|
+
side-effecting work such as reading a PDF, searching an index, or shelling out:
|
|
1242
|
+
|
|
1243
|
+
```prolog
|
|
1244
|
+
% Skill: an LLM step. Call it from DML; no tool/2.
|
|
1245
|
+
extract_authors(Text, Authors) :-
|
|
1246
|
+
task("Extract every author and their affiliation(s) from: {Text}", list(object(Authors))).
|
|
733
1247
|
|
|
734
|
-
%
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
1248
|
+
% Tools: capabilities for nested agents. No task/N, so no self-reference.
|
|
1249
|
+
read_pdf(Path, Text) :-
|
|
1250
|
+
script_file(pdf_to_text, Script),
|
|
1251
|
+
format(atom(Cmd), "python3 '~w' '~w'", [Script, Path]),
|
|
1252
|
+
exec(bash(command: Cmd), R), get_dict(stdout, R, Text).
|
|
738
1253
|
|
|
739
|
-
tool(
|
|
740
|
-
format_data(Data, FormattedOutput).
|
|
1254
|
+
tool(read_pdf(Path, Text), "Convert a workspace PDF to text.") :- read_pdf(Path, Text).
|
|
741
1255
|
```
|
|
742
1256
|
|
|
1257
|
+
**Or scope the nested task's tools so the tool cannot be reached.** If a `task/N`
|
|
1258
|
+
inside a predicate must not use a tool, say so with `without_tools/2` (or list
|
|
1259
|
+
exactly what it may use with `with_tools/2`):
|
|
1260
|
+
|
|
1261
|
+
```prolog
|
|
1262
|
+
extract_authors(Text, Authors) :-
|
|
1263
|
+
without_tools([extract_authors],
|
|
1264
|
+
task("Extract every author and their affiliation(s) from: {Text}",
|
|
1265
|
+
list(object(Authors)))).
|
|
1266
|
+
```
|
|
1267
|
+
|
|
1268
|
+
Scoping is the narrower fix: it keeps the tool advertised to *other* agents (which
|
|
1269
|
+
is usually why it exists) while removing it from this one.
|
|
1270
|
+
|
|
743
1271
|
**Memory Isolation:** When `task()` or `prompt()` runs inside a tool, the nested agent starts with **fresh memory**. It does NOT inherit the parent's accumulated memory from `system()`, `user()`, or previous `task()` calls. If you need context in a nested task, pass it explicitly as a tool argument or add it with `system()` inside the tool body.
|
|
744
1272
|
|
|
745
|
-
**Tool Access:**
|
|
1273
|
+
**Tool Access:** A nested agent's only callable tools are the harness's `tool/2`
|
|
1274
|
+
predicates; host tools such as `bash` are reachable from DML code via `exec/2`,
|
|
1275
|
+
never by name from a task description. A tool that is **already executing** cannot
|
|
1276
|
+
be re-entered: a call to it from inside itself is refused with an explanation, so a
|
|
1277
|
+
tool cannot recurse into itself. The refusal is deliberate and visible — the tool is
|
|
1278
|
+
declared to the agent with the reason on its description, so the call comes back as
|
|
1279
|
+
"already running further up the call chain" rather than as a missing tool, which
|
|
1280
|
+
would invite a retry.
|
|
1281
|
+
|
|
1282
|
+
> The withholding is keyed on *tool* execution, and only that is guaranteed. Whether a
|
|
1283
|
+
> running tool is also *removed* from the tool list presented to the agent inside it
|
|
1284
|
+
> is not something the current test suite covers; the refusal above is what stops the
|
|
1285
|
+
> recursion. Do not rely on the tool being absent from `task/N`'s tool list.
|
|
1286
|
+
|
|
1287
|
+
That keying has a consequence worth knowing: a predicate that is also a tool does
|
|
1288
|
+
**not** count as "executing" when it is called as a predicate (`/goal`, `execute`,
|
|
1289
|
+
or from another predicate). So for
|
|
1290
|
+
|
|
1291
|
+
```prolog
|
|
1292
|
+
extract_authors(Text, Authors) :- task("…", list(object(Authors))).
|
|
1293
|
+
tool(extract_authors(Text, Authors), "…") :- extract_authors(Text, Authors).
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
`/goal extract_authors(Text, A)` still offers `extract_authors` to the agent inside
|
|
1297
|
+
`task/N`. Use `with_tools/2` / `without_tools/2` to scope it, or keep the LLM step
|
|
1298
|
+
out of the tool set entirely (see **Using `task()` Inside Tools** above).
|
|
746
1299
|
|
|
747
1300
|
```prolog
|
|
748
1301
|
% Example: passing context explicitly.
|
|
@@ -765,15 +1318,19 @@ You can control which tools are available to nested `task()` calls using `with_t
|
|
|
765
1318
|
|
|
766
1319
|
#### `with_tools(+ToolList, +Goal)`
|
|
767
1320
|
|
|
768
|
-
Run Goal with only the specified tools available to nested tasks
|
|
1321
|
+
Run Goal with only the specified tools available to nested tasks. `ToolList`
|
|
1322
|
+
holds tool *names* (atoms), not predicates:
|
|
769
1323
|
|
|
770
1324
|
```prolog
|
|
771
|
-
%
|
|
772
|
-
|
|
773
|
-
with_tools([
|
|
774
|
-
format(string(Desc), "Process '~w' using available tools", [Input]),
|
|
1325
|
+
% The implementation defines what the tool does; the annotation exposes it.
|
|
1326
|
+
restricted_work(Input, Output) :-
|
|
1327
|
+
with_tools([search_papers, read_paper], (
|
|
1328
|
+
format(string(Desc), "Process '~w' using the available tools", [Input]),
|
|
775
1329
|
task(Desc, Output)
|
|
776
1330
|
)).
|
|
1331
|
+
|
|
1332
|
+
tool(restricted_task(Input, Output), "Runs with limited tools") :-
|
|
1333
|
+
restricted_work(Input, Output).
|
|
777
1334
|
```
|
|
778
1335
|
|
|
779
1336
|
#### `without_tools(+ToolList, +Goal)`
|
|
@@ -781,12 +1338,14 @@ tool(restricted_task(Input, Output), "Runs with limited tools") :-
|
|
|
781
1338
|
Run Goal with the specified tools excluded from nested tasks:
|
|
782
1339
|
|
|
783
1340
|
```prolog
|
|
784
|
-
|
|
785
|
-
tool(cheap_task(Input, Output), "Runs without expensive tools") :-
|
|
1341
|
+
cheap_work(Input, Output) :-
|
|
786
1342
|
without_tools([expensive_tool], (
|
|
787
1343
|
format(string(Desc), "Process '~w' cheaply", [Input]),
|
|
788
1344
|
task(Desc, Output)
|
|
789
1345
|
)).
|
|
1346
|
+
|
|
1347
|
+
tool(cheap_task(Input, Output), "Runs without expensive tools") :-
|
|
1348
|
+
cheap_work(Input, Output).
|
|
790
1349
|
```
|
|
791
1350
|
|
|
792
1351
|
#### Using Tool Scoping in `agent_main`
|
|
@@ -899,24 +1458,77 @@ Memory stores `media{path, mime}` — never the image bytes. Judgments
|
|
|
899
1458
|
|
|
900
1459
|
---
|
|
901
1460
|
|
|
1461
|
+
#### Running a command with data in it
|
|
1462
|
+
|
|
1463
|
+
`argv` takes the command and its arguments as a **list**, and the runtime quotes
|
|
1464
|
+
each element. Pass values here and you never write a quote, so an argument
|
|
1465
|
+
containing a space, a quote, `$`, a backtick or `;` is inert:
|
|
1466
|
+
|
|
1467
|
+
```prolog
|
|
1468
|
+
exec(bash(argv: ["python3", Script, UserText]), R), get_dict(stdout, R, Text).
|
|
1469
|
+
```
|
|
1470
|
+
|
|
1471
|
+
It is the same shell underneath (and the same sandbox), so pipelines and redirection
|
|
1472
|
+
still work with `command`; `argv` is the way to pass *data*. Prefer it whenever an
|
|
1473
|
+
argument comes from a file, a model, or the user. There is no `stdin` argument.
|
|
1474
|
+
|
|
1475
|
+
#### Do not parse in DML — shell out or ask the model
|
|
1476
|
+
|
|
1477
|
+
String handling is the fiddliest part of Prolog, and hand-rolled parsing is the
|
|
1478
|
+
easiest way to write DML that looks fine and is quietly wrong. Do not build a
|
|
1479
|
+
parser out of `sub_string/5`, `split_string/4`, `atom_concat/3`, `format/2` and an
|
|
1480
|
+
accumulator. Two better answers, depending on what the work actually is:
|
|
1481
|
+
|
|
1482
|
+
- **Mechanical** (pull a field, change a format, count, filter, join): write a
|
|
1483
|
+
**helper script** and run it. A script is a file the harness links with
|
|
1484
|
+
`script_file/2`, it is testable on its own, and Python/jq/sed are built for this.
|
|
1485
|
+
Have it emit **JSON** and read the result with `json_get/3` / `json_gets/3` /
|
|
1486
|
+
`json_text/2`, rather than splitting its stdout by hand.
|
|
1487
|
+
- **Meaning** (find the field named X, classify, summarise, decide): make it a
|
|
1488
|
+
**model call** with a typed output — `task("…", list(string(Items)))`,
|
|
1489
|
+
`schema/2`, or `choose`/`rate`/`verify` for a bounded judgment. The model is the
|
|
1490
|
+
right tool for understanding text; Prolog is the right tool for *sequencing and
|
|
1491
|
+
checking* it.
|
|
1492
|
+
|
|
1493
|
+
So `exec(bash(command: "cat", stdin: "…"), R), get_dict(stdout, R, O)` has no
|
|
1494
|
+
place here on either count: there is no stdin, and the structured reading of the
|
|
1495
|
+
result is a job for a script or a model, not for the DML around the call.
|
|
1496
|
+
|
|
902
1497
|
### File I/O and paths — prefer the shell
|
|
903
1498
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
content must be bound and reasoned over inside Prolog. Pulling a file into DML
|
|
908
|
-
just to hand it back to the shell is a smell (`DML_AUTHORING.md` §2).
|
|
1499
|
+
**`/workspace` is the project, and it is the working directory.** A DML path is
|
|
1500
|
+
either absolute under `/workspace` or relative to it, and the two forms name the
|
|
1501
|
+
same file:
|
|
909
1502
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
1503
|
+
out/report.txt and /workspace/out/report.txt
|
|
1504
|
+
|
|
1505
|
+
Prefer the `/workspace/...` form: the same name works from DML, from
|
|
1506
|
+
`exec(bash(...))`, and from the shell tool, on every backend, so a path never has
|
|
1507
|
+
to be translated between them. There is **no paths file and no search-path
|
|
1508
|
+
registry** — nothing to declare, register, or discover. `script_file(Name, Path)`
|
|
1509
|
+
links a file that belongs to the harness (it is bundled on `export`); it does not
|
|
1510
|
+
resolve anything at run time.
|
|
1511
|
+
|
|
1512
|
+
**Everything outside `/workspace` is the ephemeral VM filesystem.** `/etc`,
|
|
1513
|
+
`/tmp`, `/home` are not this machine. They are an in-memory filesystem that
|
|
1514
|
+
belongs to the session and is discarded when it ends, so they are fine for scratch
|
|
1515
|
+
and useless for results. **Only files under `/workspace` reach the host.** For
|
|
1516
|
+
scratch, use `$TMPDIR`, which is a per-session directory in every backend (a bare
|
|
1517
|
+
`/tmp` is the real one when nothing is sandboxed).
|
|
1518
|
+
|
|
1519
|
+
**Prefer `exec(bash(...))` for file operations.** Reading, writing, copying, or
|
|
1520
|
+
converting files is usually clearer and more robust as a shell command; reach for
|
|
1521
|
+
the DML predicates below only when the content must be bound and reasoned over
|
|
1522
|
+
inside Prolog. Pulling a file into DML just to hand it back to the shell is a
|
|
1523
|
+
smell (`DML_AUTHORING.md` §2).
|
|
1524
|
+
|
|
1525
|
+
The file predicates take a path — absolute under `/workspace`, or relative:
|
|
915
1526
|
|
|
916
1527
|
```prolog
|
|
917
|
-
read_file("images/a.png", Bytes),
|
|
918
|
-
|
|
919
|
-
|
|
1528
|
+
read_file("/workspace/images/a.png", Bytes),
|
|
1529
|
+
read_file("images/a.png", Bytes), % same file
|
|
1530
|
+
write_file("/workspace/out/report.txt", Text),
|
|
1531
|
+
make_directory_path("/workspace/out/nested"), % create a directory (and parents)
|
|
920
1532
|
append_file("out/log.txt", Line),
|
|
921
1533
|
delete_file("out/report.txt"),
|
|
922
1534
|
directory_files("images", Names), % bare entry names: ["a.png", ...]
|
|
@@ -928,10 +1540,24 @@ time_file("images/a.png", Modified).
|
|
|
928
1540
|
`write_file/2` does **not** create parent directories — call
|
|
929
1541
|
`make_directory_path/1` first. `directory_files/2` returns bare entry names (no
|
|
930
1542
|
directory), so build a path with `atom_concat('images/', Name, Path)` when you
|
|
931
|
-
need one.
|
|
932
|
-
|
|
1543
|
+
need one.
|
|
1544
|
+
|
|
1545
|
+
These predicates are **real clauses in the session module**, not only
|
|
1546
|
+
interpreter handlers, so DML code, a nested `task/N`, and a direct host query
|
|
1547
|
+
(the SDK's `runDML`, or `deepclause repl`) all resolve the same definition and
|
|
1548
|
+
the same path.
|
|
1549
|
+
|
|
1550
|
+
`open/3`, `close/1`, `read_string/3` and `write/2` are SWI builtins and cannot be
|
|
1551
|
+
shadowed, but they resolve paths the same way — the working directory is
|
|
1552
|
+
`/workspace`, so `open("f.txt", read, S)` reads the workspace file. They are not
|
|
1553
|
+
a special case with their own path rule.
|
|
1554
|
+
|
|
1555
|
+
Paths are **isolated by the mount, not by string checks**: there is no rewriting
|
|
1556
|
+
layer, so a path is passed through to the filesystem as written. `..` does not
|
|
1557
|
+
climb out of the workspace, because above `/workspace` is the VM's own empty
|
|
1558
|
+
filesystem. The one thing a mount cannot police is a **symlink inside the
|
|
1559
|
+
workspace** pointing outside it; avoid committing those.
|
|
933
1560
|
|
|
934
|
-
---
|
|
935
1561
|
|
|
936
1562
|
### Output Predicates
|
|
937
1563
|
|
|
@@ -947,6 +1573,25 @@ agent_main :-
|
|
|
947
1573
|
answer("Done").
|
|
948
1574
|
```
|
|
949
1575
|
|
|
1576
|
+
**Use it for debugging.** When a predicate does not do what you expect, add
|
|
1577
|
+
`output/1` calls between the steps: you see how far execution gets, what the
|
|
1578
|
+
intermediate values actually are, and which call returned nothing. It costs one
|
|
1579
|
+
line, it is invisible to the model (unlike a `task/N` description), and it is the
|
|
1580
|
+
fastest way to find where a chain goes wrong:
|
|
1581
|
+
|
|
1582
|
+
```prolog
|
|
1583
|
+
summarise(Doc, Summary) :-
|
|
1584
|
+
extract_sections(Doc, Sections),
|
|
1585
|
+
format(string(S), "sections: ~w", [Sections]), output(S),
|
|
1586
|
+
pick_main(Sections, Main),
|
|
1587
|
+
format(string(M), "main: ~w", [Main]), output(M),
|
|
1588
|
+
task("Summarise this in one sentence: ~w", [Main], Summary).
|
|
1589
|
+
```
|
|
1590
|
+
|
|
1591
|
+
Remove them before the final version, or keep the ones that read as progress
|
|
1592
|
+
reports — a predicate that says what it is doing is easier to debug the next
|
|
1593
|
+
time too.
|
|
1594
|
+
|
|
950
1595
|
#### `yield(+Text)`
|
|
951
1596
|
|
|
952
1597
|
Alias for `output/1`.
|
|
@@ -1034,6 +1679,40 @@ agent_main(Topic) :-
|
|
|
1034
1679
|
|
|
1035
1680
|
---
|
|
1036
1681
|
|
|
1682
|
+
## Patterns: one template, build or parse
|
|
1683
|
+
|
|
1684
|
+
`pattern/3` is `format/3` and its inverse. Literal text matches exactly and `$name`
|
|
1685
|
+
marks a slot, so the same template renders a string or parses it — the direction
|
|
1686
|
+
follows from what is bound:
|
|
1687
|
+
|
|
1688
|
+
```prolog
|
|
1689
|
+
pattern("Name: $name, age: $age", Text, [name="Ada", age=36]).
|
|
1690
|
+
% Text = "Name: Ada, age: 36"
|
|
1691
|
+
|
|
1692
|
+
pattern("Name: $name, age: $age", "Name: Ada, age: 36", Bindings).
|
|
1693
|
+
% Bindings = [name="Ada", age="36"]
|
|
1694
|
+
|
|
1695
|
+
pattern("DOI: $doi", Text, [doi=D]). % build a DOI line
|
|
1696
|
+
pattern("n=$n:int", "n=42", B). % B = [n=42] (typed slot)
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
- Bindings are a list of `Name=Value`, so `member(Name=Value, Bs)` reads them.
|
|
1700
|
+
- A slot consumes **up to the next literal**, so the literals decide the split and
|
|
1701
|
+
`"$left - $right"` against `"a - b - c"` gives `left="a"`, `right="b - c"`.
|
|
1702
|
+
- `$name:type` converts on parse and validates on build. Types: `string` (default),
|
|
1703
|
+
`int`, `integer`, `number`. A parse that does not match throws, rather than
|
|
1704
|
+
binding the text.
|
|
1705
|
+
- Slot names match case- and separator-insensitively, like `dict_field/3`.
|
|
1706
|
+
- `$` is the marker rather than `{…}`, so a template can carry JSON or any other
|
|
1707
|
+
brace-bearing text verbatim, and it never collides with the `{Var}`
|
|
1708
|
+
interpolation in `task/N` descriptions. `$$` is a literal dollar.
|
|
1709
|
+
- A template must be **anchored by literal text**: `"$a$b"` is rejected as
|
|
1710
|
+
ambiguous rather than resolved arbitrarily.
|
|
1711
|
+
|
|
1712
|
+
This is the deterministic alternative to asking the model to reformat text: parse
|
|
1713
|
+
the parts you can read exactly, and use a `task/N` only for what genuinely needs
|
|
1714
|
+
language understanding.
|
|
1715
|
+
|
|
1037
1716
|
## Backtracking
|
|
1038
1717
|
|
|
1039
1718
|
DML supports full Prolog backtracking, **including across LLM calls**. This enables powerful retry logic:
|
|
@@ -1093,20 +1772,112 @@ all: arithmetic, counting, ordering, membership, `findall/3`, `aggregate_all/3`,
|
|
|
1093
1772
|
list/set operations, and constraint or optimisation problems. Reach for `task/N`
|
|
1094
1773
|
only when the step genuinely needs language understanding.
|
|
1095
1774
|
|
|
1096
|
-
### Lists
|
|
1775
|
+
### Lists and common operations
|
|
1776
|
+
|
|
1777
|
+
A list is `[1,2,3]` or `[Head|Tail]`; the recursion below is the shape everything
|
|
1778
|
+
else is built on. Prefer the library predicates to hand-rolled recursion.
|
|
1779
|
+
|
|
1097
1780
|
```prolog
|
|
1098
1781
|
process_items([]).
|
|
1099
1782
|
process_items([H|T]) :-
|
|
1100
1783
|
format(string(Msg), "Processing: ~w", [H]),
|
|
1101
1784
|
output(Msg),
|
|
1102
1785
|
process_items(T).
|
|
1786
|
+
```
|
|
1103
1787
|
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1788
|
+
`maplist/3`, `include/3`, `exclude/3`, `foldl/4` and `predsort/3` take a **goal with
|
|
1789
|
+
extra arguments**, written `[X,Y]>>Goal` (a *yall* lambda). The list before `>>` is
|
|
1790
|
+
the extra arguments — so `[X,Y]>>(Y is X*X)` is a two-argument goal. The lambda's
|
|
1791
|
+
arity must match the predicate you are using.
|
|
1792
|
+
|
|
1793
|
+
**Transform**
|
|
1794
|
+
|
|
1795
|
+
| Want | Write |
|
|
1796
|
+
|---|---|
|
|
1797
|
+
| Apply to each | `maplist([X,Y]>>(Y is X*X), [1,2,3], L)` → `[1,4,9]` |
|
|
1798
|
+
| Combine two lists | `maplist([A,B,C]>>(C is A+B), [1,2], [10,20], L)` → `[11,22]` |
|
|
1799
|
+
| Apply, skipping failures | `convlist([X,Y]>>(integer(X), Y is X*2), [1,a,3], L)` → `[2,6]` |
|
|
1800
|
+
| Keep / drop matching | `include(P, L, Keep)` / `exclude(P, L, Drop)` |
|
|
1801
|
+
| Reduce to one value | `foldl([X,A0,A]>>(A is A0+X), [1,2,3], 0, S)` → `6` |
|
|
1802
|
+
| Reverse | `reverse([1,2,3], L)` |
|
|
1803
|
+
| Take / split | `append(Chunk, Rest, List)`, or `length(Chunk, 2)` to bound it |
|
|
1804
|
+
| A list of N blanks | `length(L, 3)` |
|
|
1805
|
+
|
|
1806
|
+
**Select and aggregate**
|
|
1807
|
+
|
|
1808
|
+
| Want | Write |
|
|
1809
|
+
|---|---|
|
|
1810
|
+
| Element at position | `nth0(1, [a,b,c], E)` (0-based) / `nth1(1, …, E)` (1-based) |
|
|
1811
|
+
| Remove one occurrence | `select(b, [a,b,c], L)` → `[a,c]` |
|
|
1812
|
+
| Check membership (once) | `memberchk(b, L)` — semidet, no choice point |
|
|
1813
|
+
| Last element | `last([a,b,c], E)` |
|
|
1814
|
+
| Sum | `sum_list([1,2,3], S)` → `6` |
|
|
1815
|
+
| Largest / smallest | `max_member(M, L)` / `min_member(M, L)` |
|
|
1816
|
+
| Any aggregate | `aggregate_all(max(N), member(N,[1,9,3]), M)` → `9`; also `min`, `sum`, `count`, `bag` |
|
|
1817
|
+
|
|
1818
|
+
**Sort, group, dedupe**
|
|
1819
|
+
|
|
1820
|
+
| Want | Write |
|
|
1821
|
+
|---|---|
|
|
1822
|
+
| Sort and dedupe | `sort([3,1,2,1], L)` → `[1,2,3]` |
|
|
1823
|
+
| Sort keeping duplicates | `msort([3,1,2,1], L)` → `[1,1,2,3]` (stable) |
|
|
1824
|
+
| Sort pairs by key | `keysort([b-2,a-1], L)` → `[a-1,b-2]` (stable) |
|
|
1825
|
+
| Sort compounds by an argument | `sort(2, @>=, [a-1,b-3,c-2], L)` — the `2` is the argument position |
|
|
1826
|
+
| Sort with your own comparison | `predsort([O,A,B]>>compare(O,A,B), L, Sorted)` |
|
|
1827
|
+
| Dedupe, keeping first order | `list_to_set([a,b,a,c], L)` → `[a,b,c]` |
|
|
1828
|
+
| Group by key | `group_pairs_by_key([a-1,a-2,b-3], G)` → `[a-[1,2], b-[3]]` |
|
|
1829
|
+
| Set difference / intersect / union | `subtract/3`, `intersection/3`, `union/3` |
|
|
1830
|
+
| Zip / unzip | `pairs_keys_values(Pairs, Keys, Values)`, `pairs_keys/2`, `pairs_values/2` |
|
|
1831
|
+
|
|
1832
|
+
**Strings in a list.** `split_string/4` leaves empty strings behind, which is the
|
|
1833
|
+
usual reason a list "has blanks in it":
|
|
1834
|
+
|
|
1835
|
+
```prolog
|
|
1836
|
+
split_string("a,,b", ",", "", Parts), % Parts = ["a","","b"]
|
|
1837
|
+
exclude(==(""), Parts, Clean). % Clean = ["a","b"]
|
|
1838
|
+
include(dif(""), Parts, Clean). % the same, by unification
|
|
1839
|
+
exclude(=([]), Lists, NonEmpty). % drop empty lists
|
|
1840
|
+
```
|
|
1841
|
+
|
|
1842
|
+
**A field from a list of dicts** — the most common list task with model output:
|
|
1843
|
+
|
|
1844
|
+
```prolog
|
|
1845
|
+
json_gets(Doc, "rows.*.name", Names), % one call, collects
|
|
1846
|
+
findall(N, (member(D, Dicts), dict_field(D, name, N)), Names). % from a list
|
|
1847
|
+
```
|
|
1848
|
+
|
|
1849
|
+
To sort a list of dicts, use `predsort/3`: `sort/4` addresses arguments of a
|
|
1850
|
+
compound and a dict has none.
|
|
1851
|
+
|
|
1852
|
+
**Batch a list into chunks.** There is no builtin; this is the definition:
|
|
1853
|
+
|
|
1854
|
+
```prolog
|
|
1855
|
+
chunks([], _, []).
|
|
1856
|
+
chunks(List, N, [Chunk|Rest]) :-
|
|
1857
|
+
N > 0,
|
|
1858
|
+
length(Chunk, N),
|
|
1859
|
+
append(Chunk, Tail, List),
|
|
1860
|
+
chunks(Tail, N, Rest).
|
|
1861
|
+
chunks(List, N, [List]) :-
|
|
1862
|
+
N > 0,
|
|
1863
|
+
length(List, Len),
|
|
1864
|
+
Len < N.
|
|
1865
|
+
% chunks([1,2,3,4,5], 2, L) -> L = [[1,2],[3,4],[5]]
|
|
1107
1866
|
```
|
|
1108
1867
|
|
|
1109
|
-
|
|
1868
|
+
### Traps
|
|
1869
|
+
|
|
1870
|
+
- **`setof/3` and `bagof/3` fail when there are no solutions**; `findall/3` succeeds
|
|
1871
|
+
with `[]`. Use `findall/3` unless you want that failure.
|
|
1872
|
+
- **`sort/2` silently removes duplicates.** Use `msort/2` to keep them.
|
|
1873
|
+
- **`max_list([])` fails**, and `sum_list([a,b])` raises a type error — guard a
|
|
1874
|
+
possibly-empty or possibly-non-numeric list before aggregating.
|
|
1875
|
+
- **A nondeterministic goal inside `maplist`, `include` or `foldl` multiplies
|
|
1876
|
+
solutions**, and re-runs any model call in it. Keep the goal semidet
|
|
1877
|
+
(`once/1`, `->`, a cut).
|
|
1878
|
+
- **Not available in this build:** `nth0/4` (replace an element), `numlist/4` (range
|
|
1879
|
+
with a step), `chunk/3`, `rename_keys/3`. Replace an element with
|
|
1880
|
+
`nth0(I, L, _, Tail), nth0(I, New, V, Tail)`, and use `chunks/3` above.
|
|
1110
1881
|
|
|
1111
1882
|
### Arithmetic
|
|
1112
1883
|
```prolog
|
|
@@ -1276,6 +2047,71 @@ exact_max(X, Y) :-
|
|
|
1276
2047
|
```
|
|
1277
2048
|
|
|
1278
2049
|
### Dictionaries (SWI-Prolog)
|
|
2050
|
+
|
|
2051
|
+
**Keys are atoms.** `get_dict/3` is strict about the key type and *throws* rather
|
|
2052
|
+
than failing: `get_dict("affiliation", D)` raises `type_error(dict-key,
|
|
2053
|
+
affiliation)`, so no guard sees it as a "no". A string is not even valid dict
|
|
2054
|
+
literal syntax (`x{"a":1}` is a syntax error).
|
|
2055
|
+
|
|
2056
|
+
Model output makes this worse, because the field name varies — `Name`, `name`,
|
|
2057
|
+
`full_name`. Use `dict_field/3,4`, which matches an atom **or** a string key,
|
|
2058
|
+
ignoring case and `_`/`-`/space:
|
|
2059
|
+
|
|
2060
|
+
```prolog
|
|
2061
|
+
dict_field(Dict, "affiliation", Affiliation), % fails if absent
|
|
2062
|
+
dict_field(Dict, full_name, "unknown", Name), % with a default
|
|
2063
|
+
```
|
|
2064
|
+
|
|
2065
|
+
**JSON.** `atom_json_dict/3` works in both directions, which is the whole story —
|
|
2066
|
+
reading and writing:
|
|
2067
|
+
|
|
2068
|
+
```prolog
|
|
2069
|
+
atom_json_dict(Text, Dict, []), % JSON text -> dict (Text bound)
|
|
2070
|
+
atom_json_dict(Text, Dict, []), % dict -> JSON text (Text unbound)
|
|
2071
|
+
```
|
|
2072
|
+
|
|
2073
|
+
Reading a nested value is one call with a path rather than a chain of lookups:
|
|
2074
|
+
|
|
2075
|
+
```prolog
|
|
2076
|
+
json_get(Doc, "results.0.authors.1.name", Name), % dotted path
|
|
2077
|
+
json_get(Doc, [results, 0, authors, 1, name], Name), % or a list
|
|
2078
|
+
json_get(Doc, "results.0.year", "unknown", Year), % with a default
|
|
2079
|
+
json_gets(Doc, "results.*.title", Titles), % project: collect `*`
|
|
2080
|
+
json_gets(Doc, "results.*.authors.*.name", Names), % nested `*` flattens
|
|
2081
|
+
json_put(Doc, "meta.reviewed", yes, Updated), % set a key path
|
|
2082
|
+
json_text(Doc, Text), % compact JSON
|
|
2083
|
+
```
|
|
2084
|
+
|
|
2085
|
+
A missing path **fails**; it does not throw and does not give `""`, so absence is
|
|
2086
|
+
visible. Key matching is the same tolerant match as `dict_field/3` (atom or string,
|
|
2087
|
+
any case, `_`/`-`/space ignored). `json_put/4` sets keys and creates intermediate
|
|
2088
|
+
dicts; it rejects an index path, so lists are read-only. For layout or streaming,
|
|
2089
|
+
`json_write_dict/3` from `library(http/json)` is available.
|
|
2090
|
+
|
|
2091
|
+
**Parsing text with a grammar.** DCGs work — the loader runs `expand_term/2`, so
|
|
2092
|
+
`-->` compiles, and `library(dcg/basics)` imports:
|
|
2093
|
+
|
|
2094
|
+
```prolog
|
|
2095
|
+
:- use_module(library(dcg/basics)).
|
|
2096
|
+
|
|
2097
|
+
key_value(K, V) --> string_without(":", K), ":", string(V).
|
|
2098
|
+
|
|
2099
|
+
parse_kv(Text, Pairs) :-
|
|
2100
|
+
string_chars(Text, Chars), % phrase/2 wants a LIST, not a string
|
|
2101
|
+
phrase(lines(Pairs), Chars).
|
|
2102
|
+
|
|
2103
|
+
lines([K-V|Rest]) --> key_value(K, V), "\n", !, lines(Rest).
|
|
2104
|
+
lines([K-V]) --> key_value(K, V).
|
|
2105
|
+
```
|
|
2106
|
+
|
|
2107
|
+
`phrase/2,3` takes a **code or char list**, so `string_chars/2` (or
|
|
2108
|
+
`string_codes/2`) comes first — passing a string raises `type_error(list, …)`.
|
|
2109
|
+
Also useful here: `number_codes/2`, `read_term_from_atom/3`,
|
|
2110
|
+
`split_string/4`. DCGs give you a documented, testable parser instead of a chain of
|
|
2111
|
+
regex calls, and the same grammar can generate text as well as accept it.
|
|
2112
|
+
|
|
2113
|
+
The rest of this section is plain SWI dict access, for keys you control:
|
|
2114
|
+
|
|
1279
2115
|
```prolog
|
|
1280
2116
|
agent_main :-
|
|
1281
2117
|
D = point{x: 10, y: 20},
|
|
@@ -1416,6 +2252,52 @@ backtracking reuses an answer instead of re-querying the model.
|
|
|
1416
2252
|
always one of your option atoms.
|
|
1417
2253
|
- Prefer one `judge/2` batch over several one-off calls when you need more than
|
|
1418
2254
|
one or two judgments.
|
|
2255
|
+
### Load-time directives
|
|
2256
|
+
|
|
2257
|
+
A `.dml` file may contain directives. These are handled:
|
|
2258
|
+
|
|
2259
|
+
```prolog
|
|
2260
|
+
:- use_module(library(clpfd)). % library import
|
|
2261
|
+
:- consult("other.dml"). % load another workspace file
|
|
2262
|
+
:- param(topic, "the subject"). % declared session parameter
|
|
2263
|
+
:- repl_delete(old_skill/2). % remove a predicate (same as the bare fact form)
|
|
2264
|
+
:- require_sandbox. % refuse to load unless the shell is sandboxed
|
|
2265
|
+
```
|
|
2266
|
+
|
|
2267
|
+
`:- require_sandbox.` is a declaration that the harness must run with an
|
|
2268
|
+
isolated shell. It is checked **at load**, and the load **fails** when the
|
|
2269
|
+
effective backend is not a sandbox (`bwrap`, AgentVM, or `sandbox-exec`) — it
|
|
2270
|
+
does not silently run unsandboxed. "Effective" is deliberate: bubblewrap being
|
|
2271
|
+
installed while the session resolves to unsandboxed execution still fails the
|
|
2272
|
+
check. Only the **user** sets this (the `/set-require-sandbox` command); an agent
|
|
2273
|
+
cannot add or remove it.
|
|
2274
|
+
|
|
2275
|
+
`:- repl_delete(Name/Arity).` abolishes a predicate, which `retract/1` cannot do:
|
|
2276
|
+
retracting the last clause leaves an empty predicate whose calls fail silently.
|
|
2277
|
+
The bare fact form `repl_delete(Name/Arity).` works the same way.
|
|
2278
|
+
|
|
2279
|
+
These apply to a file **loaded** into a session (`deepclause repl -l file.dml`).
|
|
2280
|
+
They are not the way to edit a live harness. `consult/1`, a file-list directive and
|
|
2281
|
+
`use_module/1` on a workspace `.dml` assert straight into the module rather than
|
|
2282
|
+
through the REPL's edit tools, so the predicates they add are absent from the
|
|
2283
|
+
transaction's tracked set — the nondeterminism probe does not see them, and a
|
|
2284
|
+
directive that fails inside the file only warns. They do still appear in the next
|
|
2285
|
+
commit's diff, so the change is recorded without any account of where it came from —
|
|
2286
|
+
and outside a transaction there is no snapshot, so nothing validates the load at all.
|
|
2287
|
+
An agent changing a harness adds code with `assert`/`assert_many`/`define` inside a
|
|
2288
|
+
transaction instead.
|
|
2289
|
+
|
|
2290
|
+
Any other directive does nothing, and the loader now says so — it reports
|
|
2291
|
+
`unrecognised directive ignored: …` after the file loads, so a directive that
|
|
2292
|
+
never took effect is not mistaken for one that did. `dynamic/1`,
|
|
2293
|
+
`discontiguous/1` and `multifile/1` are accepted and ignored silently, since
|
|
2294
|
+
session predicates are asserted and already behave that way.
|
|
2295
|
+
|
|
2296
|
+
A **predicate with no clauses fails silently** when called — it does not error —
|
|
2297
|
+
so an emptied or never-written predicate looks like "no results" at the call site.
|
|
2298
|
+
`current_predicate(Name/Arity)` in a `/goal` answers whether it exists, and a
|
|
2299
|
+
`doc/2` for a predicate with no clauses is reported at `end_turn`.
|
|
2300
|
+
|
|
1419
2301
|
## The DML REPL (`deepclause repl`)
|
|
1420
2302
|
|
|
1421
2303
|
DML skills can also be grown interactively in a persistent **living module**.
|