deepclause-sdk 0.1.0 → 0.1.2

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.
Files changed (142) hide show
  1. package/README.md +83 -12
  2. package/dist/agent.d.ts +14 -1
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +117 -9
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +86 -26
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +15 -0
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.js +38 -0
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/cli/repl.d.ts +27 -0
  13. package/dist/cli/repl.d.ts.map +1 -1
  14. package/dist/cli/repl.js +191 -5
  15. package/dist/cli/repl.js.map +1 -1
  16. package/dist/cli/run.d.ts.map +1 -1
  17. package/dist/cli/run.js +16 -0
  18. package/dist/cli/run.js.map +1 -1
  19. package/dist/cli/visualize.d.ts +4 -0
  20. package/dist/cli/visualize.d.ts.map +1 -1
  21. package/dist/cli/visualize.js +29 -4
  22. package/dist/cli/visualize.js.map +1 -1
  23. package/dist/prolog/bridge.d.ts +3 -1
  24. package/dist/prolog/bridge.d.ts.map +1 -1
  25. package/dist/prolog/bridge.js +2 -0
  26. package/dist/prolog/bridge.js.map +1 -1
  27. package/dist/prolog/loader.d.ts +32 -0
  28. package/dist/prolog/loader.d.ts.map +1 -1
  29. package/dist/prolog/loader.js +87 -0
  30. package/dist/prolog/loader.js.map +1 -1
  31. package/dist/prolog-src/deepclause_mi.pl +1217 -120
  32. package/dist/repl/agent.d.ts +33 -1
  33. package/dist/repl/agent.d.ts.map +1 -1
  34. package/dist/repl/agent.js +91 -8
  35. package/dist/repl/agent.js.map +1 -1
  36. package/dist/repl/backend.d.ts +31 -0
  37. package/dist/repl/backend.d.ts.map +1 -1
  38. package/dist/repl/backend.js +122 -77
  39. package/dist/repl/backend.js.map +1 -1
  40. package/dist/repl/harness-tools.d.ts +49 -2
  41. package/dist/repl/harness-tools.d.ts.map +1 -1
  42. package/dist/repl/harness-tools.js +87 -7
  43. package/dist/repl/harness-tools.js.map +1 -1
  44. package/dist/repl/imagegen.d.ts +29 -0
  45. package/dist/repl/imagegen.d.ts.map +1 -0
  46. package/dist/repl/imagegen.js +84 -0
  47. package/dist/repl/imagegen.js.map +1 -0
  48. package/dist/repl/index.d.ts +20 -2
  49. package/dist/repl/index.d.ts.map +1 -1
  50. package/dist/repl/index.js +6 -0
  51. package/dist/repl/index.js.map +1 -1
  52. package/dist/repl/model-resolver.d.ts +59 -0
  53. package/dist/repl/model-resolver.d.ts.map +1 -0
  54. package/dist/repl/model-resolver.js +156 -0
  55. package/dist/repl/model-resolver.js.map +1 -0
  56. package/dist/repl/render.d.ts +36 -2
  57. package/dist/repl/render.d.ts.map +1 -1
  58. package/dist/repl/render.js +175 -17
  59. package/dist/repl/render.js.map +1 -1
  60. package/dist/repl/session.d.ts +129 -4
  61. package/dist/repl/session.d.ts.map +1 -1
  62. package/dist/repl/session.js +380 -17
  63. package/dist/repl/session.js.map +1 -1
  64. package/dist/repl/tool-arguments.d.ts +36 -0
  65. package/dist/repl/tool-arguments.d.ts.map +1 -0
  66. package/dist/repl/tool-arguments.js +123 -0
  67. package/dist/repl/tool-arguments.js.map +1 -0
  68. package/dist/repl/toolset.d.ts +28 -1
  69. package/dist/repl/toolset.d.ts.map +1 -1
  70. package/dist/repl/toolset.js +52 -4
  71. package/dist/repl/toolset.js.map +1 -1
  72. package/dist/repl/tui/transcript.d.ts +2 -0
  73. package/dist/repl/tui/transcript.d.ts.map +1 -1
  74. package/dist/repl/tui/transcript.js +16 -0
  75. package/dist/repl/tui/transcript.js.map +1 -1
  76. package/dist/runner.d.ts +44 -2
  77. package/dist/runner.d.ts.map +1 -1
  78. package/dist/runner.js +293 -33
  79. package/dist/runner.js.map +1 -1
  80. package/dist/system/assets/docs/AGENT_PROMPT.md +1000 -44
  81. package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
  82. package/dist/system/assets/docs/DML_REFERENCE.md +652 -39
  83. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +45 -1
  84. package/dist/system/assets/docs/TASK_PROMPT.md +2 -0
  85. package/dist/system/environment.d.ts +36 -0
  86. package/dist/system/environment.d.ts.map +1 -0
  87. package/dist/system/environment.js +81 -0
  88. package/dist/system/environment.js.map +1 -0
  89. package/dist/system/runtime/shell-manager.d.ts +40 -2
  90. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  91. package/dist/system/runtime/shell-manager.js +82 -16
  92. package/dist/system/runtime/shell-manager.js.map +1 -1
  93. package/dist/system/runtime/shell-tool-events.js +25 -1
  94. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  95. package/dist/types.d.ts +76 -2
  96. package/dist/types.d.ts.map +1 -1
  97. package/dist/visualize/ascii.d.ts +55 -0
  98. package/dist/visualize/ascii.d.ts.map +1 -0
  99. package/dist/visualize/ascii.js +242 -0
  100. package/dist/visualize/ascii.js.map +1 -0
  101. package/dist/visualize/display.d.ts +24 -0
  102. package/dist/visualize/display.d.ts.map +1 -0
  103. package/dist/visualize/display.js +29 -0
  104. package/dist/visualize/display.js.map +1 -0
  105. package/dist/visualize/graphics.d.ts +35 -0
  106. package/dist/visualize/graphics.d.ts.map +1 -0
  107. package/dist/visualize/graphics.js +96 -0
  108. package/dist/visualize/graphics.js.map +1 -0
  109. package/dist/visualize/image.d.ts +38 -0
  110. package/dist/visualize/image.d.ts.map +1 -0
  111. package/dist/visualize/image.js +95 -0
  112. package/dist/visualize/image.js.map +1 -0
  113. package/dist/visualize/index.d.ts +7 -0
  114. package/dist/visualize/index.d.ts.map +1 -1
  115. package/dist/visualize/index.js +9 -0
  116. package/dist/visualize/index.js.map +1 -1
  117. package/dist/visualize/raster.d.ts +23 -0
  118. package/dist/visualize/raster.d.ts.map +1 -0
  119. package/dist/visualize/raster.js +47 -0
  120. package/dist/visualize/raster.js.map +1 -0
  121. package/dist/visualize/svg.d.ts +39 -0
  122. package/dist/visualize/svg.d.ts.map +1 -0
  123. package/dist/visualize/svg.js +148 -0
  124. package/dist/visualize/svg.js.map +1 -0
  125. package/dist/visualize/transcode.d.ts +13 -0
  126. package/dist/visualize/transcode.d.ts.map +1 -0
  127. package/dist/visualize/transcode.js +40 -0
  128. package/dist/visualize/transcode.js.map +1 -0
  129. package/docs/images/repl.png +0 -0
  130. package/package.json +10 -6
  131. package/src/prolog-src/deepclause_mi.pl +1217 -120
  132. package/dist/compaction.d.ts +0 -98
  133. package/dist/compaction.d.ts.map +0 -1
  134. package/dist/compaction.js +0 -471
  135. package/dist/compaction.js.map +0 -1
  136. package/dist/repl/tools.d.ts +0 -53
  137. package/dist/repl/tools.d.ts.map +0 -1
  138. package/dist/repl/tools.js +0 -307
  139. package/dist/repl/tools.js.map +0 -1
  140. package/dist/system/assets/compactors/default-loop-compactor.dml +0 -43
  141. package/dist/system/assets/compactors/default-session-compactor.dml +0 -30
  142. 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,10 +154,19 @@ 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 one clause (new predicates only).
75
- - `assert_many(clauses)` — add a block of clauses in written order.
76
- - `retract(clause)` / `delete("Name/Arity")` — remove clauses / a whole predicate.
77
- - `end_turn` — validate and commit the change (records a version).
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.
165
+ - `end_turn` — validate and commit the change (records a version). It also
166
+ probes the predicates you changed and returns a `warnings` list (shown as `⚠`)
167
+ when a helper looks nondeterministic — e.g. yields many solutions on fresh
168
+ variables because it is missing a commit. Fix such warnings with `once/1` or
169
+ `!` before moving on; they are exactly the predicates that flood `execute`.
78
170
  - `rollback` — discard the transaction and restore the last committed module.
79
171
 
80
172
  Edits are **refused** while no transaction is open, and after `end_turn` you must
@@ -82,12 +174,62 @@ call `begin_turn` again before editing further. If you finish a turn with a
82
174
  transaction still open, the host nudges you to `end_turn` or `rollback`. Commit
83
175
  incrementally for anything larger than a couple of predicates.
84
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
+
85
189
  Other tools: `reference(topic)` reads the DeepClause docs (`language`,
86
190
  `authoring`, `patterns`, `examples`, `snippets`, `tools`).
87
191
 
88
192
  End your turn by replying with text and no tool call. To ask the human a
89
193
  question, write it in that final message.
90
194
 
195
+ # Writing files and images
196
+
197
+ - `write_file(path, content)` writes any workspace file (creating parent
198
+ directories). The content is an ordinary string, so use it for SVG, JSON, or
199
+ scripts instead of building them inside a shell command. Then
200
+ `show_image(path)` displays an image.
201
+ - From DML, `output(media("path.png"))` shows a workspace image and
202
+ `output(media{svg: Svg})` shows inline SVG. `save_image(Media, "out.svg")`
203
+ writes a reference to a file.
204
+ - `task("a red fox in snow", media(X))` generates an image and binds `X` to its
205
+ workspace path; display it with `output(media(X))`.
206
+ - Avoid multi-line literals inside `exec(bash(command: "…"))` (heredocs): `\n`
207
+ and `"` must be escaped for DML and again for the shell, and it breaks easily.
208
+ Use `write_file` for content; if you must use the shell, write the file with a
209
+ single-line `printf '%s' '…' > path`, not a heredoc.
210
+
211
+ # Choosing models
212
+
213
+ `with_model(Spec, Goal)` runs `Goal` on a different model, optionally at a
214
+ reasoning level. It applies to the `task/N`, `prompt/N` and `sample_token/N`
215
+ calls inside `Goal` (not to judgments):
216
+
217
+ ```prolog
218
+ with_model("openai:gpt-4o-mini", prompt("Classify as billing/orders: {Msg}", Team)).
219
+ with_model("anthropic:claude-sonnet-4#high", task("Reason carefully: {Q}", Out)).
220
+ with_model(model{provider: "google", id: "gemini-2.5-pro", temperature: 0.2}, Goal).
221
+ ```
222
+
223
+ Use it to route cheap work to a small model and hard work to a strong one, or to
224
+ tune a harness to a specific model. The innermost `with_model` wins; a nested
225
+ `with_reasoning/2` overrides the spec's reasoning level.
226
+
227
+ The models available this session are listed under **AVAILABLE MODELS** (above),
228
+ including their catalog price in USD per 1M tokens (input/output); call the
229
+ `models` tool for the current list and the active default. Prefer the cheapest
230
+ model that can do the job, and a `with_model` spec must name one of those
231
+ `provider:model` ids.
232
+
91
233
  # Your goal
92
234
 
93
235
  Satisfy the user's request — by answering directly, by running existing module
@@ -149,6 +291,51 @@ recent_changes(N, Cs) :-
149
291
  last_n(All, N, Cs).
150
292
  ```
151
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
+
152
339
  # Keep DML simple: orchestration, not algorithms
153
340
 
154
341
  DML is an **orchestration and verification** layer. A skill should read as a
@@ -197,6 +384,77 @@ There are no other host tools. Do not invent `fetch_url`, `web_search`,
197
384
  `calculator`, etc. — if the module needs one, write a script and run it with
198
385
  `bash`.
199
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
+
200
458
  # The `tool/2` annotation
201
459
 
202
460
  `tool/2` is **only an annotation**: it exposes an existing predicate to the
@@ -209,9 +467,67 @@ tool(count_words(File, Count), "Count words in a file.") :-
209
467
  count_words(File, Count).
210
468
  ```
211
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
+
212
505
  Logic placed only inside `tool/2` is **not** reachable from `execute`/`listing`;
213
506
  keep the `tool/2` body a one-line delegation.
214
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
+
215
531
  # File I/O
216
532
 
217
533
  Prefer `bash` (`exec(bash(command: ...), Result)`) for reading, writing, copying,
@@ -220,6 +536,11 @@ and converting files. Use the DML file predicates (`read_file/2`, `write_file/2`
220
536
  bound and reasoned over inside Prolog. Every path is workspace-relative and
221
537
  confined to the workspace.
222
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
+
223
544
  # Mixing languages: interpolation, quoting, escaping
224
545
 
225
546
  DML often drives a second language — a shell command, a Python/Node script, a
@@ -239,7 +560,19 @@ implements. Never nest code inside code.
239
560
  doubles a single quote (`'it''s'`).
240
561
  - Prefer `~q`/`~s` over `~w` whenever the value is not a controlled atom.
241
562
 
242
- ## Shell: pass data, never splice code
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.
243
576
 
244
577
  Do not interpolate data into a command string — quoting differs and injection is
245
578
  easy:
@@ -384,11 +717,21 @@ More patterns: `reference("patterns")`; shapes: `reference("examples")`.
384
717
 
385
718
  # Editing rules
386
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.
387
726
  - Reuse before you add: generalize a predicate and take its inputs as arguments;
388
727
  never bake in a host path, a fixed filename, or a one-off value.
389
728
  - Add a `doc/2` for every predicate you add or change, and keep `module_doc/1`
390
729
  current — it is the skill description.
391
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.
392
735
  - After finishing, if the work revealed a reusable capability, say so and offer
393
736
  to add it rather than committing it unasked.
394
737
 
@@ -407,6 +750,7 @@ More patterns: `reference("patterns")`; shapes: `reference("examples")`.
407
750
  - [exec/2](#exec2)
408
751
  - [Tool Definitions](#tool-definitions)
409
752
  - [Tool Scoping](#tool-scoping)
753
+ - [Model Selection](#model-selection)
410
754
  - [system/1, user/1](#memory-predicates)
411
755
  - [output/1, yield/1, log/1](#output-predicates)
412
756
  - [answer/1](#answer1)
@@ -500,8 +844,28 @@ agent_main :-
500
844
 
501
845
  ### Task Predicates
502
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
+
503
857
  #### `task(+Description)`
504
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
+
505
869
  Invoke an LLM to perform a task. The description is sent to the model.
506
870
 
507
871
  ```prolog
@@ -554,6 +918,132 @@ Bind up to three variables.
554
918
  > `listing` and `export` reverse this, so you always read/write `task/N` and
555
919
  > `prompt/N` — never author `task_named/3` yourself.
556
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
+
557
1047
  ### Type-Safe Output Variables
558
1048
 
559
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.
@@ -612,11 +1102,15 @@ Or positional if the tool supports it:
612
1102
  exec(get_time, CurrentTime)
613
1103
  ```
614
1104
 
615
- The `bash` tool accepts an optional wall-clock `timeoutMs` and always returns the
616
- wall-clock duration as `timeMs`:
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`:
617
1110
 
618
1111
  ```prolog
619
1112
  exec(bash(command: "sleep 30", timeoutMs: 5000), R),
1113
+ exec(bash(argv: ["python3", Script, Query]), R), % no quoting to write
620
1114
  get_dict(timeMs, R, Took),
621
1115
  ( get_dict(exitCode, R, 124)
622
1116
  -> format(string(Msg), "timed out after ~w ms", [Took])
@@ -649,6 +1143,25 @@ tool(count_words(File, Count), "Count words in a workspace file.") :-
649
1143
  count_words(File, Count).
650
1144
  ```
651
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
+
652
1165
  A predicate defined *only* inside a `tool/2` clause is invisible to
653
1166
  `execute` / `/goal` — the loader asserts it as `tool(Head)`, never as `Head`. Split
654
1167
  the two so one implementation is reachable from both surfaces:
@@ -658,6 +1171,12 @@ the two so one implementation is reachable from both surfaces:
658
1171
  task("Count the words in report.txt.", N). % nested agent calls the tool
659
1172
  ```
660
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
+
661
1180
  The tool head's **last argument is the output**; the description is shown to the
662
1181
  nested agent. Keep the `tool/2` body a single delegation — put no logic in it.
663
1182
 
@@ -680,26 +1199,103 @@ Tools can use `task()` and `prompt()` internally to combine Prolog logic with LL
680
1199
 
681
1200
  ```prolog
682
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.
683
1204
  explain_calculation(A, B, Explanation) :-
684
1205
  Sum is A + B, % Prolog computation
685
- format(string(Desc), "Explain ~w + ~w = ~w to a child", [A, B, Sum]),
686
- task(Desc, Explanation). % LLM explanation
1206
+ format(string(Desc),
1207
+ "Explain ~w + ~w = ~w to a child, using only these figures.",
1208
+ [A, B, Sum]),
1209
+ task(Desc, Explanation).
687
1210
 
688
1211
  tool(explain_calculation(A, B, Explanation), "Calculate and explain the result.") :-
689
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
+ ```
690
1227
 
691
- % A predicate that uses the LLM for formatting.
692
- format_data(Data, FormattedOutput) :-
693
- format(string(Desc), "Format this data nicely: ~w", [Data]),
694
- task(Desc, FormattedOutput).
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.
695
1235
 
696
- tool(format_data(Data, FormattedOutput), "Format data nicely using LLM.") :-
697
- format_data(Data, FormattedOutput).
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))).
1247
+
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).
1253
+
1254
+ tool(read_pdf(Path, Text), "Convert a workspace PDF to text.") :- read_pdf(Path, Text).
698
1255
  ```
699
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
+
700
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.
701
1272
 
702
- **Tool Access:** The nested agent has access to all DML tools **except** the tool that is currently executing (automatic call stack exclusion to prevent infinite recursion). You can further control tool access using `with_tools/2` and `without_tools/2`.
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).
703
1299
 
704
1300
  ```prolog
705
1301
  % Example: passing context explicitly.
@@ -722,15 +1318,19 @@ You can control which tools are available to nested `task()` calls using `with_t
722
1318
 
723
1319
  #### `with_tools(+ToolList, +Goal)`
724
1320
 
725
- 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:
726
1323
 
727
1324
  ```prolog
728
- % Only allow tool_a and tool_b in the nested task
729
- tool(restricted_task(Input, Output), "Runs with limited tools") :-
730
- with_tools([tool_a, tool_b], (
731
- 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]),
732
1329
  task(Desc, Output)
733
1330
  )).
1331
+
1332
+ tool(restricted_task(Input, Output), "Runs with limited tools") :-
1333
+ restricted_work(Input, Output).
734
1334
  ```
735
1335
 
736
1336
  #### `without_tools(+ToolList, +Goal)`
@@ -738,12 +1338,14 @@ tool(restricted_task(Input, Output), "Runs with limited tools") :-
738
1338
  Run Goal with the specified tools excluded from nested tasks:
739
1339
 
740
1340
  ```prolog
741
- % Exclude expensive_tool from the nested task
742
- tool(cheap_task(Input, Output), "Runs without expensive tools") :-
1341
+ cheap_work(Input, Output) :-
743
1342
  without_tools([expensive_tool], (
744
1343
  format(string(Desc), "Process '~w' cheaply", [Input]),
745
1344
  task(Desc, Output)
746
1345
  )).
1346
+
1347
+ tool(cheap_task(Input, Output), "Runs without expensive tools") :-
1348
+ cheap_work(Input, Output).
747
1349
  ```
748
1350
 
749
1351
  #### Using Tool Scoping in `agent_main`
@@ -767,6 +1369,37 @@ agent_main :-
767
1369
 
768
1370
  ---
769
1371
 
1372
+ ### Model Selection
1373
+
1374
+ #### `with_model(+Spec, +Goal)`
1375
+
1376
+ Run `Goal` with a different model — and optionally a reasoning level — for every
1377
+ `task/N`, `prompt/N` and `sample_token/N` call inside it. Judgments
1378
+ (`choose/rate/verify/probability`) are not affected.
1379
+
1380
+ ```prolog
1381
+ % a cheap model for classification
1382
+ with_model("openai:gpt-4o-mini",
1383
+ prompt("Classify as billing/orders/account: {Message}", Team)).
1384
+
1385
+ % a strong model with high reasoning, only for the hard step
1386
+ with_model("anthropic:claude-sonnet-4#high",
1387
+ task("Review this diff and list defects:\n{Diff}", Notes)).
1388
+
1389
+ % full control with a dict
1390
+ with_model(model{provider: "google", id: "gemini-2.5-pro",
1391
+ temperature: 0.2, maxTokens: 8192}, Goal).
1392
+ ```
1393
+
1394
+ `Spec` is a string — `"provider:model"`, `"provider/model"`, `"model"`, each with
1395
+ an optional `#reasoning` suffix (`off|minimal|low|medium|high|xhigh|max`) — or a
1396
+ `model{provider, id, temperature, maxTokens, reasoning}` dict. The innermost
1397
+ `with_model` wins, and a nested `with_reasoning/2` overrides the spec's reasoning.
1398
+ The default is the session's active model. An unknown or unsigned-in model fails
1399
+ with a clear error.
1400
+
1401
+ ---
1402
+
770
1403
  ### Memory Predicates
771
1404
 
772
1405
  Memory predicates build the conversation context sent to the LLM.
@@ -825,24 +1458,77 @@ Memory stores `media{path, mime}` — never the image bytes. Judgments
825
1458
 
826
1459
  ---
827
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
+
828
1497
  ### File I/O and paths — prefer the shell
829
1498
 
830
- **Prefer `exec(bash(...))` for file operations.**
831
- Reading, writing, copying, or converting files is usually clearer and more
832
- robust as a shell command; reach for the DML predicates below only when the
833
- content must be bound and reasoned over inside Prolog. Pulling a file into DML
834
- 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:
1502
+
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.
835
1511
 
836
- If you do use them: every path in DML is **workspace-relative** and confined to
837
- the workspace. The
838
- engine mounts the workspace at `/workspace`, but you must not write
839
- `/workspace/...` in DML — passing an absolute path is rejected, as is `..`. File
840
- predicates all take a relative path:
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:
841
1526
 
842
1527
  ```prolog
843
- read_file("images/a.png", Bytes),
844
- write_file("out/report.txt", Text),
845
- make_directory_path("out/nested"), % create a directory (and parents)
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)
846
1532
  append_file("out/log.txt", Line),
847
1533
  delete_file("out/report.txt"),
848
1534
  directory_files("images", Names), % bare entry names: ["a.png", ...]
@@ -854,10 +1540,24 @@ time_file("images/a.png", Modified).
854
1540
  `write_file/2` does **not** create parent directories — call
855
1541
  `make_directory_path/1` first. `directory_files/2` returns bare entry names (no
856
1542
  directory), so build a path with `atom_concat('images/', Name, Path)` when you
857
- need one. `bash` runs with a wider view of the sandbox than DML has; convert
858
- anything from the shell to a workspace-relative path before handing it to DML.
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.
859
1560
 
860
- ---
861
1561
 
862
1562
  ### Output Predicates
863
1563
 
@@ -873,6 +1573,25 @@ agent_main :-
873
1573
  answer("Done").
874
1574
  ```
875
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
+
876
1595
  #### `yield(+Text)`
877
1596
 
878
1597
  Alias for `output/1`.
@@ -960,6 +1679,40 @@ agent_main(Topic) :-
960
1679
 
961
1680
  ---
962
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
+
963
1716
  ## Backtracking
964
1717
 
965
1718
  DML supports full Prolog backtracking, **including across LLM calls**. This enables powerful retry logic:
@@ -1019,20 +1772,112 @@ all: arithmetic, counting, ordering, membership, `findall/3`, `aggregate_all/3`,
1019
1772
  list/set operations, and constraint or optimisation problems. Reach for `task/N`
1020
1773
  only when the step genuinely needs language understanding.
1021
1774
 
1022
- ### 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
+
1023
1780
  ```prolog
1024
1781
  process_items([]).
1025
1782
  process_items([H|T]) :-
1026
1783
  format(string(Msg), "Processing: ~w", [H]),
1027
1784
  output(Msg),
1028
1785
  process_items(T).
1786
+ ```
1029
1787
 
1030
- agent_main :-
1031
- process_items([apple, banana, cherry]),
1032
- answer("All items processed").
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
1033
1840
  ```
1034
1841
 
1035
- Not all builtin SWI Prolog predicates are guaranteed to work at this stage!
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]]
1866
+ ```
1867
+
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.
1036
1881
 
1037
1882
  ### Arithmetic
1038
1883
  ```prolog
@@ -1202,6 +2047,71 @@ exact_max(X, Y) :-
1202
2047
  ```
1203
2048
 
1204
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
+
1205
2115
  ```prolog
1206
2116
  agent_main :-
1207
2117
  D = point{x: 10, y: 20},
@@ -1342,6 +2252,52 @@ backtracking reuses an answer instead of re-querying the model.
1342
2252
  always one of your option atoms.
1343
2253
  - Prefer one `judge/2` batch over several one-off calls when you need more than
1344
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
+
1345
2301
  ## The DML REPL (`deepclause repl`)
1346
2302
 
1347
2303
  DML skills can also be grown interactively in a persistent **living module**.