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
@@ -11,6 +11,7 @@
11
11
  - [exec/2](#exec2)
12
12
  - [Tool Definitions](#tool-definitions)
13
13
  - [Tool Scoping](#tool-scoping)
14
+ - [Model Selection](#model-selection)
14
15
  - [system/1, user/1](#memory-predicates)
15
16
  - [output/1, yield/1, log/1](#output-predicates)
16
17
  - [answer/1](#answer1)
@@ -104,8 +105,28 @@ agent_main :-
104
105
 
105
106
  ### Task Predicates
106
107
 
108
+ Both `task/N` and `prompt/N` run an LLM step and bind typed outputs. They differ in
109
+ **what conversation the model sees**, which is the single most important thing to
110
+ know about them.
111
+
112
+ | | `task/N` | `prompt/N` |
113
+ |---|---|---|
114
+ | Memory | the accumulated conversation, **and it is written back** | **fresh** — empty; does not read or write it |
115
+ | Tools | the harness's `tool/2` predicates | **none** (plain text completion) |
116
+ | 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 |
117
+
107
118
  #### `task(+Description)`
108
119
 
120
+ Invoke an LLM to perform a task. The description is sent to the model.
121
+
122
+ ```prolog
123
+ agent_main :-
124
+ system("You are a coding assistant."),
125
+ task("Write a function to calculate factorial in Python."),
126
+ answer("Code generated!").
127
+ ```
128
+
129
+
109
130
  Invoke an LLM to perform a task. The description is sent to the model.
110
131
 
111
132
  ```prolog
@@ -158,6 +179,132 @@ Bind up to three variables.
158
179
  > `listing` and `export` reverse this, so you always read/write `task/N` and
159
180
  > `prompt/N` — never author `task_named/3` yourself.
160
181
 
182
+ ### Memory: what the model sees, and how it grows
183
+
184
+ A `task/N` call is **not** a fresh conversation. It is sent the memory the session
185
+ has accumulated, and the agent's full resulting memory is written back — so each
186
+ `task/N` sees every earlier one, plus its own prompt and reply:
187
+
188
+ ```prolog
189
+ step(Text, Summary, Tags) :-
190
+ task("Summarise: {Text}", Summary), % sees: nothing yet
191
+ task("Tag this summary: {Summary}", Tags). % sees: the first task's prompt and reply
192
+ ```
193
+
194
+ `system/1` and `user/1` add to that same memory, and **they are backtrackable** —
195
+ a `system/1` inside a branch that is later undone stops contributing:
196
+
197
+ ```prolog
198
+ brief(Text, Out) :-
199
+ system("Answer in one sentence."),
200
+ task("Summarise: {Text}", Out).
201
+ ```
202
+
203
+ Two consequences worth planning for:
204
+
205
+ - **Memory grows with every task.** A loop of `task/N` calls sends an ever longer
206
+ conversation, which costs tokens and eventually dilutes the instruction. Pass
207
+ values as explicit arguments rather than relying on the model to remember, and
208
+ start a fresh step when the history is no longer relevant.
209
+ - **A `task/N` inside a `tool/2` starts with empty memory.** Tool bodies run in
210
+ their own engine, which is created with no memory of the caller, so a nested
211
+ agent sees only what the *tool arguments* carry. Put the context in the argument,
212
+ or add it inside the tool body with `system/1`:
213
+
214
+ ```prolog
215
+ summarise(Text, Summary) :-
216
+ task("Summarise this text in two sentences: {Text}", Summary).
217
+
218
+ tool(summarise(Text, Summary), "Summarise a passage.") :-
219
+ summarise(Text, Summary).
220
+ ```
221
+
222
+ **A fresh context: `prompt/N`, or `with_fresh_memory/2`.** `prompt/N` takes the same
223
+ arguments as `task/N` but sends **no memory and no tools**, and leaves the memory
224
+ untouched. Use it when the step must answer from its own prompt alone — a
225
+ classifier, an extractor, a translation — or when a long history has started to
226
+ interfere:
227
+
228
+ ```prolog
229
+ route(Message, Team) :-
230
+ prompt("Classify as billing, orders or account: {Message}", Team).
231
+
232
+ % The conversation so far cannot leak into the classification, and the
233
+ % classification cannot leak into it.
234
+ ```
235
+
236
+ Because `prompt/N` has no tools, a step that must read a file or search an index
237
+ has to be a `task/N`. Choose by what the step needs: the harness's capabilities
238
+ (`task/N`) or an uncontaminated answer (`prompt/N`).
239
+
240
+ `prompt/N` isolates **one call**. To isolate a **group** of steps — several
241
+ `task/N` calls that share a fresh context with each other but nothing with the
242
+ caller — wrap them in `with_fresh_memory/1`:
243
+
244
+ ```prolog
245
+ describe(Paper, Report) :-
246
+ with_fresh_memory((
247
+ task("Summarise the method of: {Paper}", Method),
248
+ task("Summarise the results of: {Paper}", Results),
249
+ task("Write a two-sentence abstract from {Method} and {Results}", Report)
250
+ )).
251
+ % The three steps see each other's turns, because they accumulate within the
252
+ % scope. Nothing the caller accumulated reaches them, and nothing they
253
+ % accumulate reaches the caller — the memory is restored on exit.
254
+ ```
255
+
256
+ With `with_tools/2` / `without_tools/2` it composes into a scope that controls both
257
+ the conversation and the capabilities:
258
+
259
+ ```prolog
260
+ audit(App, Finding) :-
261
+ with_fresh_memory(without_tools([apply_fix],
262
+ task("Review {App} against the checklist and report one finding.", Finding))).
263
+ ```
264
+
265
+ ### Declared Output Shapes: `schema/2`
266
+
267
+ A type says "a list of dicts"; a schema says what is *in* them. Declare the shape
268
+ once, name it in the `task/N` output, and every step gets a value that matches it —
269
+ defaults filled, and a missing required field or a wrong-typed field failing the
270
+ step instead of quietly yielding an empty result:
271
+
272
+ ```prolog
273
+ schema(author, [
274
+ field(name, string, required),
275
+ field(affiliation, string, default("not stated")),
276
+ field(email, string, default("")),
277
+ field(country, string, default(""))
278
+ ]).
279
+
280
+ extract_authors(Text, Authors) :-
281
+ task("From the front matter extract every author: {Text}",
282
+ list(schema(author, Authors))).
283
+ % Authors = [author{name:"Ashish Vaswani", affiliation:"Google Brain", email:"", country:""} …]
284
+ ```
285
+
286
+ - An absent field with a `default(Value)` is filled.
287
+ - An absent `required` field **fails the step** with a message naming the field, so
288
+ the agent can correct it in the same turn.
289
+ - A field of the wrong type fails the same way. Types are `string`, `integer`,
290
+ `number`, `boolean`, `object`, `array`.
291
+ - An **undeclared** field is kept, not dropped: a model mistake stays visible
292
+ - The field list is **described to the model** in the step's own instructions, and a
293
+ missing required or wrong-typed field is reported by name inside the same turn,
294
+ so the agent usually fixes it before the step ends
295
+ rather than disappearing.
296
+ - `schema/2` is a fact, like `doc/2` — it appears in the listing and travels with
297
+ `export`.
298
+
299
+ For a single object rather than a list, use `schema(author, Author)` (no `list/1`).
300
+ You can also apply a schema by hand to any dict:
301
+
302
+ ```prolog
303
+ schema_fill(author, Raw, Author), % fills defaults, validates
304
+ ```
305
+
306
+ `end_turn` reports a `task/N` that names a schema which was never declared.
307
+
161
308
  ### Type-Safe Output Variables
162
309
 
163
310
  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.
@@ -216,11 +363,15 @@ Or positional if the tool supports it:
216
363
  exec(get_time, CurrentTime)
217
364
  ```
218
365
 
219
- The `bash` tool accepts an optional wall-clock `timeoutMs` and always returns the
220
- wall-clock duration as `timeMs`:
366
+ The `bash` tool takes exactly two inputs — `command` (a shell string) or `argv` (a
367
+ list, quoted by the runtime) — plus an optional wall-clock `timeoutMs`. There is
368
+ **no `stdin`**: pass data as an `argv` element, or write it to a workspace file and
369
+ name the file. stdin is `/dev/null`, so a command that reads it sees EOF rather
370
+ than waiting. It always returns the wall-clock duration as `timeMs`:
221
371
 
222
372
  ```prolog
223
373
  exec(bash(command: "sleep 30", timeoutMs: 5000), R),
374
+ exec(bash(argv: ["python3", Script, Query]), R), % no quoting to write
224
375
  get_dict(timeMs, R, Took),
225
376
  ( get_dict(exitCode, R, 124)
226
377
  -> format(string(Msg), "timed out after ~w ms", [Took])
@@ -253,6 +404,25 @@ tool(count_words(File, Count), "Count words in a workspace file.") :-
253
404
  count_words(File, Count).
254
405
  ```
255
406
 
407
+ The head and its delegate must have the **same name and the same arity**. The
408
+ tool body is what runs when the nested agent calls the tool, so a body that
409
+ calls a different arity either calls nothing (`existence_error`) or calls the
410
+ tool again and recurses forever:
411
+
412
+ ```prolog
413
+ % WRONG: declares /2 but calls /3, so the tool fails when a nested agent uses it.
414
+ tool(bibliography(Query, Max), "…") :-
415
+ bibliography(Query, Max, Entries).
416
+
417
+ % WRONG: calls itself; a nested agent recurses until the budget is gone.
418
+ tool(citations(Text, Citations), "…") :-
419
+ citations(Text, Citations).
420
+ ```
421
+
422
+ Nothing is checked when the clause is loaded, so both mistakes look fine until a
423
+ nested agent calls the tool. `end_turn` reports them as contract warnings; treat
424
+ them as errors, not style notes.
425
+
256
426
  A predicate defined *only* inside a `tool/2` clause is invisible to
257
427
  `execute` / `/goal` — the loader asserts it as `tool(Head)`, never as `Head`. Split
258
428
  the two so one implementation is reachable from both surfaces:
@@ -262,6 +432,12 @@ the two so one implementation is reachable from both surfaces:
262
432
  task("Count the words in report.txt.", N). % nested agent calls the tool
263
433
  ```
264
434
 
435
+ The only tools a nested agent can call are the harness's `tool/2` predicates;
436
+ registered host tools (for example `bash`) are reachable from DML code through
437
+ `exec/2`, not as tools by name. So a task description that says "use the
438
+ `fs_read` tool" works only if the module itself defines `fs_read/2` **and**
439
+ annotates it.
440
+
265
441
  The tool head's **last argument is the output**; the description is shown to the
266
442
  nested agent. Keep the `tool/2` body a single delegation — put no logic in it.
267
443
 
@@ -284,26 +460,103 @@ Tools can use `task()` and `prompt()` internally to combine Prolog logic with LL
284
460
 
285
461
  ```prolog
286
462
  % A predicate that computes then explains.
463
+ % The nested task is told to work from the given figures — it has no tools it
464
+ % needs, so it cannot try to re-enter this predicate.
287
465
  explain_calculation(A, B, Explanation) :-
288
466
  Sum is A + B, % Prolog computation
289
- format(string(Desc), "Explain ~w + ~w = ~w to a child", [A, B, Sum]),
290
- task(Desc, Explanation). % LLM explanation
467
+ format(string(Desc),
468
+ "Explain ~w + ~w = ~w to a child, using only these figures.",
469
+ [A, B, Sum]),
470
+ task(Desc, Explanation).
291
471
 
292
472
  tool(explain_calculation(A, B, Explanation), "Calculate and explain the result.") :-
293
473
  explain_calculation(A, B, Explanation).
474
+ ```
294
475
 
295
- % A predicate that uses the LLM for formatting.
296
- format_data(Data, FormattedOutput) :-
297
- format(string(Desc), "Format this data nicely: ~w", [Data]),
298
- task(Desc, FormattedOutput).
476
+ **Do not wrap an LLM step in a `tool/2` that advertises the same job.** The tool
477
+ is declared to the agent running *inside* that same `task/N`, so the description
478
+ invites the agent to do the very thing it was asked, by calling the tool:
299
479
 
300
- tool(format_data(Data, FormattedOutput), "Format data nicely using LLM.") :-
301
- format_data(Data, FormattedOutput).
480
+ ```prolog
481
+ % WARNING: this shape self-delegates.
482
+ extract_authors(Text, Authors) :-
483
+ task("Extract every author and their affiliation(s) from: {Text}", list(object(Authors))).
484
+
485
+ tool(extract_authors(Text, Authors), "Extract every author and their affiliation(s).") :-
486
+ extract_authors(Text, Authors).
302
487
  ```
303
488
 
489
+ Called as a predicate (`/goal extract_authors(…, A)`, `execute`, or from another
490
+ predicate), `extract_authors` runs its `task/N`, and that agent is offered
491
+ `extract_authors` — a tool described as doing exactly its job. The runtime refuses
492
+ the re-entrant call (a tool whose predicate is already running is withheld, and a
493
+ call that slips through is refused with a reason), so it cannot recurse forever.
494
+ But the agent still spends a turn on it, is told the tool failed, and then does
495
+ the work by hand — a confusing transcript and a wasted call.
496
+
497
+ Two ways to avoid it, in order of preference:
498
+
499
+ **Keep the LLM step a skill, and give `tool/2` only to capabilities.** A skill is
500
+ called from DML (`/goal`, `execute`, another predicate) and does not need to be a
501
+ tool. Annotate the operations a nested agent should *use* — deterministic or
502
+ side-effecting work such as reading a PDF, searching an index, or shelling out:
503
+
504
+ ```prolog
505
+ % Skill: an LLM step. Call it from DML; no tool/2.
506
+ extract_authors(Text, Authors) :-
507
+ task("Extract every author and their affiliation(s) from: {Text}", list(object(Authors))).
508
+
509
+ % Tools: capabilities for nested agents. No task/N, so no self-reference.
510
+ read_pdf(Path, Text) :-
511
+ script_file(pdf_to_text, Script),
512
+ format(atom(Cmd), "python3 '~w' '~w'", [Script, Path]),
513
+ exec(bash(command: Cmd), R), get_dict(stdout, R, Text).
514
+
515
+ tool(read_pdf(Path, Text), "Convert a workspace PDF to text.") :- read_pdf(Path, Text).
516
+ ```
517
+
518
+ **Or scope the nested task's tools so the tool cannot be reached.** If a `task/N`
519
+ inside a predicate must not use a tool, say so with `without_tools/2` (or list
520
+ exactly what it may use with `with_tools/2`):
521
+
522
+ ```prolog
523
+ extract_authors(Text, Authors) :-
524
+ without_tools([extract_authors],
525
+ task("Extract every author and their affiliation(s) from: {Text}",
526
+ list(object(Authors)))).
527
+ ```
528
+
529
+ Scoping is the narrower fix: it keeps the tool advertised to *other* agents (which
530
+ is usually why it exists) while removing it from this one.
531
+
304
532
  **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.
305
533
 
306
- **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`.
534
+ **Tool Access:** A nested agent's only callable tools are the harness's `tool/2`
535
+ predicates; host tools such as `bash` are reachable from DML code via `exec/2`,
536
+ never by name from a task description. A tool that is **already executing** cannot
537
+ be re-entered: a call to it from inside itself is refused with an explanation, so a
538
+ tool cannot recurse into itself. The refusal is deliberate and visible — the tool is
539
+ declared to the agent with the reason on its description, so the call comes back as
540
+ "already running further up the call chain" rather than as a missing tool, which
541
+ would invite a retry.
542
+
543
+ > The withholding is keyed on *tool* execution, and only that is guaranteed. Whether a
544
+ > running tool is also *removed* from the tool list presented to the agent inside it
545
+ > is not something the current test suite covers; the refusal above is what stops the
546
+ > recursion. Do not rely on the tool being absent from `task/N`'s tool list.
547
+
548
+ That keying has a consequence worth knowing: a predicate that is also a tool does
549
+ **not** count as "executing" when it is called as a predicate (`/goal`, `execute`,
550
+ or from another predicate). So for
551
+
552
+ ```prolog
553
+ extract_authors(Text, Authors) :- task("…", list(object(Authors))).
554
+ tool(extract_authors(Text, Authors), "…") :- extract_authors(Text, Authors).
555
+ ```
556
+
557
+ `/goal extract_authors(Text, A)` still offers `extract_authors` to the agent inside
558
+ `task/N`. Use `with_tools/2` / `without_tools/2` to scope it, or keep the LLM step
559
+ out of the tool set entirely (see **Using `task()` Inside Tools** above).
307
560
 
308
561
  ```prolog
309
562
  % Example: passing context explicitly.
@@ -326,15 +579,19 @@ You can control which tools are available to nested `task()` calls using `with_t
326
579
 
327
580
  #### `with_tools(+ToolList, +Goal)`
328
581
 
329
- Run Goal with only the specified tools available to nested tasks:
582
+ Run Goal with only the specified tools available to nested tasks. `ToolList`
583
+ holds tool *names* (atoms), not predicates:
330
584
 
331
585
  ```prolog
332
- % Only allow tool_a and tool_b in the nested task
333
- tool(restricted_task(Input, Output), "Runs with limited tools") :-
334
- with_tools([tool_a, tool_b], (
335
- format(string(Desc), "Process '~w' using available tools", [Input]),
586
+ % The implementation defines what the tool does; the annotation exposes it.
587
+ restricted_work(Input, Output) :-
588
+ with_tools([search_papers, read_paper], (
589
+ format(string(Desc), "Process '~w' using the available tools", [Input]),
336
590
  task(Desc, Output)
337
591
  )).
592
+
593
+ tool(restricted_task(Input, Output), "Runs with limited tools") :-
594
+ restricted_work(Input, Output).
338
595
  ```
339
596
 
340
597
  #### `without_tools(+ToolList, +Goal)`
@@ -342,12 +599,14 @@ tool(restricted_task(Input, Output), "Runs with limited tools") :-
342
599
  Run Goal with the specified tools excluded from nested tasks:
343
600
 
344
601
  ```prolog
345
- % Exclude expensive_tool from the nested task
346
- tool(cheap_task(Input, Output), "Runs without expensive tools") :-
602
+ cheap_work(Input, Output) :-
347
603
  without_tools([expensive_tool], (
348
604
  format(string(Desc), "Process '~w' cheaply", [Input]),
349
605
  task(Desc, Output)
350
606
  )).
607
+
608
+ tool(cheap_task(Input, Output), "Runs without expensive tools") :-
609
+ cheap_work(Input, Output).
351
610
  ```
352
611
 
353
612
  #### Using Tool Scoping in `agent_main`
@@ -371,6 +630,37 @@ agent_main :-
371
630
 
372
631
  ---
373
632
 
633
+ ### Model Selection
634
+
635
+ #### `with_model(+Spec, +Goal)`
636
+
637
+ Run `Goal` with a different model — and optionally a reasoning level — for every
638
+ `task/N`, `prompt/N` and `sample_token/N` call inside it. Judgments
639
+ (`choose/rate/verify/probability`) are not affected.
640
+
641
+ ```prolog
642
+ % a cheap model for classification
643
+ with_model("openai:gpt-4o-mini",
644
+ prompt("Classify as billing/orders/account: {Message}", Team)).
645
+
646
+ % a strong model with high reasoning, only for the hard step
647
+ with_model("anthropic:claude-sonnet-4#high",
648
+ task("Review this diff and list defects:\n{Diff}", Notes)).
649
+
650
+ % full control with a dict
651
+ with_model(model{provider: "google", id: "gemini-2.5-pro",
652
+ temperature: 0.2, maxTokens: 8192}, Goal).
653
+ ```
654
+
655
+ `Spec` is a string — `"provider:model"`, `"provider/model"`, `"model"`, each with
656
+ an optional `#reasoning` suffix (`off|minimal|low|medium|high|xhigh|max`) — or a
657
+ `model{provider, id, temperature, maxTokens, reasoning}` dict. The innermost
658
+ `with_model` wins, and a nested `with_reasoning/2` overrides the spec's reasoning.
659
+ The default is the session's active model. An unknown or unsigned-in model fails
660
+ with a clear error.
661
+
662
+ ---
663
+
374
664
  ### Memory Predicates
375
665
 
376
666
  Memory predicates build the conversation context sent to the LLM.
@@ -429,24 +719,77 @@ Memory stores `media{path, mime}` — never the image bytes. Judgments
429
719
 
430
720
  ---
431
721
 
722
+ #### Running a command with data in it
723
+
724
+ `argv` takes the command and its arguments as a **list**, and the runtime quotes
725
+ each element. Pass values here and you never write a quote, so an argument
726
+ containing a space, a quote, `$`, a backtick or `;` is inert:
727
+
728
+ ```prolog
729
+ exec(bash(argv: ["python3", Script, UserText]), R), get_dict(stdout, R, Text).
730
+ ```
731
+
732
+ It is the same shell underneath (and the same sandbox), so pipelines and redirection
733
+ still work with `command`; `argv` is the way to pass *data*. Prefer it whenever an
734
+ argument comes from a file, a model, or the user. There is no `stdin` argument.
735
+
736
+ #### Do not parse in DML — shell out or ask the model
737
+
738
+ String handling is the fiddliest part of Prolog, and hand-rolled parsing is the
739
+ easiest way to write DML that looks fine and is quietly wrong. Do not build a
740
+ parser out of `sub_string/5`, `split_string/4`, `atom_concat/3`, `format/2` and an
741
+ accumulator. Two better answers, depending on what the work actually is:
742
+
743
+ - **Mechanical** (pull a field, change a format, count, filter, join): write a
744
+ **helper script** and run it. A script is a file the harness links with
745
+ `script_file/2`, it is testable on its own, and Python/jq/sed are built for this.
746
+ Have it emit **JSON** and read the result with `json_get/3` / `json_gets/3` /
747
+ `json_text/2`, rather than splitting its stdout by hand.
748
+ - **Meaning** (find the field named X, classify, summarise, decide): make it a
749
+ **model call** with a typed output — `task("…", list(string(Items)))`,
750
+ `schema/2`, or `choose`/`rate`/`verify` for a bounded judgment. The model is the
751
+ right tool for understanding text; Prolog is the right tool for *sequencing and
752
+ checking* it.
753
+
754
+ So `exec(bash(command: "cat", stdin: "…"), R), get_dict(stdout, R, O)` has no
755
+ place here on either count: there is no stdin, and the structured reading of the
756
+ result is a job for a script or a model, not for the DML around the call.
757
+
432
758
  ### File I/O and paths — prefer the shell
433
759
 
434
- **Prefer `exec(bash(...))` for file operations.**
435
- Reading, writing, copying, or converting files is usually clearer and more
436
- robust as a shell command; reach for the DML predicates below only when the
437
- content must be bound and reasoned over inside Prolog. Pulling a file into DML
438
- just to hand it back to the shell is a smell (`DML_AUTHORING.md` §2).
760
+ **`/workspace` is the project, and it is the working directory.** A DML path is
761
+ either absolute under `/workspace` or relative to it, and the two forms name the
762
+ same file:
763
+
764
+ out/report.txt and /workspace/out/report.txt
765
+
766
+ Prefer the `/workspace/...` form: the same name works from DML, from
767
+ `exec(bash(...))`, and from the shell tool, on every backend, so a path never has
768
+ to be translated between them. There is **no paths file and no search-path
769
+ registry** — nothing to declare, register, or discover. `script_file(Name, Path)`
770
+ links a file that belongs to the harness (it is bundled on `export`); it does not
771
+ resolve anything at run time.
772
+
773
+ **Everything outside `/workspace` is the ephemeral VM filesystem.** `/etc`,
774
+ `/tmp`, `/home` are not this machine. They are an in-memory filesystem that
775
+ belongs to the session and is discarded when it ends, so they are fine for scratch
776
+ and useless for results. **Only files under `/workspace` reach the host.** For
777
+ scratch, use `$TMPDIR`, which is a per-session directory in every backend (a bare
778
+ `/tmp` is the real one when nothing is sandboxed).
439
779
 
440
- If you do use them: every path in DML is **workspace-relative** and confined to
441
- the workspace. The
442
- engine mounts the workspace at `/workspace`, but you must not write
443
- `/workspace/...` in DML — passing an absolute path is rejected, as is `..`. File
444
- predicates all take a relative path:
780
+ **Prefer `exec(bash(...))` for file operations.** Reading, writing, copying, or
781
+ converting files is usually clearer and more robust as a shell command; reach for
782
+ the DML predicates below only when the content must be bound and reasoned over
783
+ inside Prolog. Pulling a file into DML just to hand it back to the shell is a
784
+ smell (`DML_AUTHORING.md` §2).
785
+
786
+ The file predicates take a path — absolute under `/workspace`, or relative:
445
787
 
446
788
  ```prolog
447
- read_file("images/a.png", Bytes),
448
- write_file("out/report.txt", Text),
449
- make_directory_path("out/nested"), % create a directory (and parents)
789
+ read_file("/workspace/images/a.png", Bytes),
790
+ read_file("images/a.png", Bytes), % same file
791
+ write_file("/workspace/out/report.txt", Text),
792
+ make_directory_path("/workspace/out/nested"), % create a directory (and parents)
450
793
  append_file("out/log.txt", Line),
451
794
  delete_file("out/report.txt"),
452
795
  directory_files("images", Names), % bare entry names: ["a.png", ...]
@@ -458,10 +801,24 @@ time_file("images/a.png", Modified).
458
801
  `write_file/2` does **not** create parent directories — call
459
802
  `make_directory_path/1` first. `directory_files/2` returns bare entry names (no
460
803
  directory), so build a path with `atom_concat('images/', Name, Path)` when you
461
- need one. `bash` runs with a wider view of the sandbox than DML has; convert
462
- anything from the shell to a workspace-relative path before handing it to DML.
804
+ need one.
805
+
806
+ These predicates are **real clauses in the session module**, not only
807
+ interpreter handlers, so DML code, a nested `task/N`, and a direct host query
808
+ (the SDK's `runDML`, or `deepclause repl`) all resolve the same definition and
809
+ the same path.
810
+
811
+ `open/3`, `close/1`, `read_string/3` and `write/2` are SWI builtins and cannot be
812
+ shadowed, but they resolve paths the same way — the working directory is
813
+ `/workspace`, so `open("f.txt", read, S)` reads the workspace file. They are not
814
+ a special case with their own path rule.
815
+
816
+ Paths are **isolated by the mount, not by string checks**: there is no rewriting
817
+ layer, so a path is passed through to the filesystem as written. `..` does not
818
+ climb out of the workspace, because above `/workspace` is the VM's own empty
819
+ filesystem. The one thing a mount cannot police is a **symlink inside the
820
+ workspace** pointing outside it; avoid committing those.
463
821
 
464
- ---
465
822
 
466
823
  ### Output Predicates
467
824
 
@@ -477,6 +834,25 @@ agent_main :-
477
834
  answer("Done").
478
835
  ```
479
836
 
837
+ **Use it for debugging.** When a predicate does not do what you expect, add
838
+ `output/1` calls between the steps: you see how far execution gets, what the
839
+ intermediate values actually are, and which call returned nothing. It costs one
840
+ line, it is invisible to the model (unlike a `task/N` description), and it is the
841
+ fastest way to find where a chain goes wrong:
842
+
843
+ ```prolog
844
+ summarise(Doc, Summary) :-
845
+ extract_sections(Doc, Sections),
846
+ format(string(S), "sections: ~w", [Sections]), output(S),
847
+ pick_main(Sections, Main),
848
+ format(string(M), "main: ~w", [Main]), output(M),
849
+ task("Summarise this in one sentence: ~w", [Main], Summary).
850
+ ```
851
+
852
+ Remove them before the final version, or keep the ones that read as progress
853
+ reports — a predicate that says what it is doing is easier to debug the next
854
+ time too.
855
+
480
856
  #### `yield(+Text)`
481
857
 
482
858
  Alias for `output/1`.
@@ -564,6 +940,40 @@ agent_main(Topic) :-
564
940
 
565
941
  ---
566
942
 
943
+ ## Patterns: one template, build or parse
944
+
945
+ `pattern/3` is `format/3` and its inverse. Literal text matches exactly and `$name`
946
+ marks a slot, so the same template renders a string or parses it — the direction
947
+ follows from what is bound:
948
+
949
+ ```prolog
950
+ pattern("Name: $name, age: $age", Text, [name="Ada", age=36]).
951
+ % Text = "Name: Ada, age: 36"
952
+
953
+ pattern("Name: $name, age: $age", "Name: Ada, age: 36", Bindings).
954
+ % Bindings = [name="Ada", age="36"]
955
+
956
+ pattern("DOI: $doi", Text, [doi=D]). % build a DOI line
957
+ pattern("n=$n:int", "n=42", B). % B = [n=42] (typed slot)
958
+ ```
959
+
960
+ - Bindings are a list of `Name=Value`, so `member(Name=Value, Bs)` reads them.
961
+ - A slot consumes **up to the next literal**, so the literals decide the split and
962
+ `"$left - $right"` against `"a - b - c"` gives `left="a"`, `right="b - c"`.
963
+ - `$name:type` converts on parse and validates on build. Types: `string` (default),
964
+ `int`, `integer`, `number`. A parse that does not match throws, rather than
965
+ binding the text.
966
+ - Slot names match case- and separator-insensitively, like `dict_field/3`.
967
+ - `$` is the marker rather than `{…}`, so a template can carry JSON or any other
968
+ brace-bearing text verbatim, and it never collides with the `{Var}`
969
+ interpolation in `task/N` descriptions. `$$` is a literal dollar.
970
+ - A template must be **anchored by literal text**: `"$a$b"` is rejected as
971
+ ambiguous rather than resolved arbitrarily.
972
+
973
+ This is the deterministic alternative to asking the model to reformat text: parse
974
+ the parts you can read exactly, and use a `task/N` only for what genuinely needs
975
+ language understanding.
976
+
567
977
  ## Backtracking
568
978
 
569
979
  DML supports full Prolog backtracking, **including across LLM calls**. This enables powerful retry logic:
@@ -623,20 +1033,112 @@ all: arithmetic, counting, ordering, membership, `findall/3`, `aggregate_all/3`,
623
1033
  list/set operations, and constraint or optimisation problems. Reach for `task/N`
624
1034
  only when the step genuinely needs language understanding.
625
1035
 
626
- ### Lists
1036
+ ### Lists and common operations
1037
+
1038
+ A list is `[1,2,3]` or `[Head|Tail]`; the recursion below is the shape everything
1039
+ else is built on. Prefer the library predicates to hand-rolled recursion.
1040
+
627
1041
  ```prolog
628
1042
  process_items([]).
629
1043
  process_items([H|T]) :-
630
1044
  format(string(Msg), "Processing: ~w", [H]),
631
1045
  output(Msg),
632
1046
  process_items(T).
1047
+ ```
633
1048
 
634
- agent_main :-
635
- process_items([apple, banana, cherry]),
636
- answer("All items processed").
1049
+ `maplist/3`, `include/3`, `exclude/3`, `foldl/4` and `predsort/3` take a **goal with
1050
+ extra arguments**, written `[X,Y]>>Goal` (a *yall* lambda). The list before `>>` is
1051
+ the extra arguments — so `[X,Y]>>(Y is X*X)` is a two-argument goal. The lambda's
1052
+ arity must match the predicate you are using.
1053
+
1054
+ **Transform**
1055
+
1056
+ | Want | Write |
1057
+ |---|---|
1058
+ | Apply to each | `maplist([X,Y]>>(Y is X*X), [1,2,3], L)` → `[1,4,9]` |
1059
+ | Combine two lists | `maplist([A,B,C]>>(C is A+B), [1,2], [10,20], L)` → `[11,22]` |
1060
+ | Apply, skipping failures | `convlist([X,Y]>>(integer(X), Y is X*2), [1,a,3], L)` → `[2,6]` |
1061
+ | Keep / drop matching | `include(P, L, Keep)` / `exclude(P, L, Drop)` |
1062
+ | Reduce to one value | `foldl([X,A0,A]>>(A is A0+X), [1,2,3], 0, S)` → `6` |
1063
+ | Reverse | `reverse([1,2,3], L)` |
1064
+ | Take / split | `append(Chunk, Rest, List)`, or `length(Chunk, 2)` to bound it |
1065
+ | A list of N blanks | `length(L, 3)` |
1066
+
1067
+ **Select and aggregate**
1068
+
1069
+ | Want | Write |
1070
+ |---|---|
1071
+ | Element at position | `nth0(1, [a,b,c], E)` (0-based) / `nth1(1, …, E)` (1-based) |
1072
+ | Remove one occurrence | `select(b, [a,b,c], L)` → `[a,c]` |
1073
+ | Check membership (once) | `memberchk(b, L)` — semidet, no choice point |
1074
+ | Last element | `last([a,b,c], E)` |
1075
+ | Sum | `sum_list([1,2,3], S)` → `6` |
1076
+ | Largest / smallest | `max_member(M, L)` / `min_member(M, L)` |
1077
+ | Any aggregate | `aggregate_all(max(N), member(N,[1,9,3]), M)` → `9`; also `min`, `sum`, `count`, `bag` |
1078
+
1079
+ **Sort, group, dedupe**
1080
+
1081
+ | Want | Write |
1082
+ |---|---|
1083
+ | Sort and dedupe | `sort([3,1,2,1], L)` → `[1,2,3]` |
1084
+ | Sort keeping duplicates | `msort([3,1,2,1], L)` → `[1,1,2,3]` (stable) |
1085
+ | Sort pairs by key | `keysort([b-2,a-1], L)` → `[a-1,b-2]` (stable) |
1086
+ | Sort compounds by an argument | `sort(2, @>=, [a-1,b-3,c-2], L)` — the `2` is the argument position |
1087
+ | Sort with your own comparison | `predsort([O,A,B]>>compare(O,A,B), L, Sorted)` |
1088
+ | Dedupe, keeping first order | `list_to_set([a,b,a,c], L)` → `[a,b,c]` |
1089
+ | Group by key | `group_pairs_by_key([a-1,a-2,b-3], G)` → `[a-[1,2], b-[3]]` |
1090
+ | Set difference / intersect / union | `subtract/3`, `intersection/3`, `union/3` |
1091
+ | Zip / unzip | `pairs_keys_values(Pairs, Keys, Values)`, `pairs_keys/2`, `pairs_values/2` |
1092
+
1093
+ **Strings in a list.** `split_string/4` leaves empty strings behind, which is the
1094
+ usual reason a list "has blanks in it":
1095
+
1096
+ ```prolog
1097
+ split_string("a,,b", ",", "", Parts), % Parts = ["a","","b"]
1098
+ exclude(==(""), Parts, Clean). % Clean = ["a","b"]
1099
+ include(dif(""), Parts, Clean). % the same, by unification
1100
+ exclude(=([]), Lists, NonEmpty). % drop empty lists
1101
+ ```
1102
+
1103
+ **A field from a list of dicts** — the most common list task with model output:
1104
+
1105
+ ```prolog
1106
+ json_gets(Doc, "rows.*.name", Names), % one call, collects
1107
+ findall(N, (member(D, Dicts), dict_field(D, name, N)), Names). % from a list
1108
+ ```
1109
+
1110
+ To sort a list of dicts, use `predsort/3`: `sort/4` addresses arguments of a
1111
+ compound and a dict has none.
1112
+
1113
+ **Batch a list into chunks.** There is no builtin; this is the definition:
1114
+
1115
+ ```prolog
1116
+ chunks([], _, []).
1117
+ chunks(List, N, [Chunk|Rest]) :-
1118
+ N > 0,
1119
+ length(Chunk, N),
1120
+ append(Chunk, Tail, List),
1121
+ chunks(Tail, N, Rest).
1122
+ chunks(List, N, [List]) :-
1123
+ N > 0,
1124
+ length(List, Len),
1125
+ Len < N.
1126
+ % chunks([1,2,3,4,5], 2, L) -> L = [[1,2],[3,4],[5]]
637
1127
  ```
638
1128
 
639
- Not all builtin SWI Prolog predicates are guaranteed to work at this stage!
1129
+ ### Traps
1130
+
1131
+ - **`setof/3` and `bagof/3` fail when there are no solutions**; `findall/3` succeeds
1132
+ with `[]`. Use `findall/3` unless you want that failure.
1133
+ - **`sort/2` silently removes duplicates.** Use `msort/2` to keep them.
1134
+ - **`max_list([])` fails**, and `sum_list([a,b])` raises a type error — guard a
1135
+ possibly-empty or possibly-non-numeric list before aggregating.
1136
+ - **A nondeterministic goal inside `maplist`, `include` or `foldl` multiplies
1137
+ solutions**, and re-runs any model call in it. Keep the goal semidet
1138
+ (`once/1`, `->`, a cut).
1139
+ - **Not available in this build:** `nth0/4` (replace an element), `numlist/4` (range
1140
+ with a step), `chunk/3`, `rename_keys/3`. Replace an element with
1141
+ `nth0(I, L, _, Tail), nth0(I, New, V, Tail)`, and use `chunks/3` above.
640
1142
 
641
1143
  ### Arithmetic
642
1144
  ```prolog
@@ -806,6 +1308,71 @@ exact_max(X, Y) :-
806
1308
  ```
807
1309
 
808
1310
  ### Dictionaries (SWI-Prolog)
1311
+
1312
+ **Keys are atoms.** `get_dict/3` is strict about the key type and *throws* rather
1313
+ than failing: `get_dict("affiliation", D)` raises `type_error(dict-key,
1314
+ affiliation)`, so no guard sees it as a "no". A string is not even valid dict
1315
+ literal syntax (`x{"a":1}` is a syntax error).
1316
+
1317
+ Model output makes this worse, because the field name varies — `Name`, `name`,
1318
+ `full_name`. Use `dict_field/3,4`, which matches an atom **or** a string key,
1319
+ ignoring case and `_`/`-`/space:
1320
+
1321
+ ```prolog
1322
+ dict_field(Dict, "affiliation", Affiliation), % fails if absent
1323
+ dict_field(Dict, full_name, "unknown", Name), % with a default
1324
+ ```
1325
+
1326
+ **JSON.** `atom_json_dict/3` works in both directions, which is the whole story —
1327
+ reading and writing:
1328
+
1329
+ ```prolog
1330
+ atom_json_dict(Text, Dict, []), % JSON text -> dict (Text bound)
1331
+ atom_json_dict(Text, Dict, []), % dict -> JSON text (Text unbound)
1332
+ ```
1333
+
1334
+ Reading a nested value is one call with a path rather than a chain of lookups:
1335
+
1336
+ ```prolog
1337
+ json_get(Doc, "results.0.authors.1.name", Name), % dotted path
1338
+ json_get(Doc, [results, 0, authors, 1, name], Name), % or a list
1339
+ json_get(Doc, "results.0.year", "unknown", Year), % with a default
1340
+ json_gets(Doc, "results.*.title", Titles), % project: collect `*`
1341
+ json_gets(Doc, "results.*.authors.*.name", Names), % nested `*` flattens
1342
+ json_put(Doc, "meta.reviewed", yes, Updated), % set a key path
1343
+ json_text(Doc, Text), % compact JSON
1344
+ ```
1345
+
1346
+ A missing path **fails**; it does not throw and does not give `""`, so absence is
1347
+ visible. Key matching is the same tolerant match as `dict_field/3` (atom or string,
1348
+ any case, `_`/`-`/space ignored). `json_put/4` sets keys and creates intermediate
1349
+ dicts; it rejects an index path, so lists are read-only. For layout or streaming,
1350
+ `json_write_dict/3` from `library(http/json)` is available.
1351
+
1352
+ **Parsing text with a grammar.** DCGs work — the loader runs `expand_term/2`, so
1353
+ `-->` compiles, and `library(dcg/basics)` imports:
1354
+
1355
+ ```prolog
1356
+ :- use_module(library(dcg/basics)).
1357
+
1358
+ key_value(K, V) --> string_without(":", K), ":", string(V).
1359
+
1360
+ parse_kv(Text, Pairs) :-
1361
+ string_chars(Text, Chars), % phrase/2 wants a LIST, not a string
1362
+ phrase(lines(Pairs), Chars).
1363
+
1364
+ lines([K-V|Rest]) --> key_value(K, V), "\n", !, lines(Rest).
1365
+ lines([K-V]) --> key_value(K, V).
1366
+ ```
1367
+
1368
+ `phrase/2,3` takes a **code or char list**, so `string_chars/2` (or
1369
+ `string_codes/2`) comes first — passing a string raises `type_error(list, …)`.
1370
+ Also useful here: `number_codes/2`, `read_term_from_atom/3`,
1371
+ `split_string/4`. DCGs give you a documented, testable parser instead of a chain of
1372
+ regex calls, and the same grammar can generate text as well as accept it.
1373
+
1374
+ The rest of this section is plain SWI dict access, for keys you control:
1375
+
809
1376
  ```prolog
810
1377
  agent_main :-
811
1378
  D = point{x: 10, y: 20},
@@ -946,6 +1513,52 @@ backtracking reuses an answer instead of re-querying the model.
946
1513
  always one of your option atoms.
947
1514
  - Prefer one `judge/2` batch over several one-off calls when you need more than
948
1515
  one or two judgments.
1516
+ ### Load-time directives
1517
+
1518
+ A `.dml` file may contain directives. These are handled:
1519
+
1520
+ ```prolog
1521
+ :- use_module(library(clpfd)). % library import
1522
+ :- consult("other.dml"). % load another workspace file
1523
+ :- param(topic, "the subject"). % declared session parameter
1524
+ :- repl_delete(old_skill/2). % remove a predicate (same as the bare fact form)
1525
+ :- require_sandbox. % refuse to load unless the shell is sandboxed
1526
+ ```
1527
+
1528
+ `:- require_sandbox.` is a declaration that the harness must run with an
1529
+ isolated shell. It is checked **at load**, and the load **fails** when the
1530
+ effective backend is not a sandbox (`bwrap`, AgentVM, or `sandbox-exec`) — it
1531
+ does not silently run unsandboxed. "Effective" is deliberate: bubblewrap being
1532
+ installed while the session resolves to unsandboxed execution still fails the
1533
+ check. Only the **user** sets this (the `/set-require-sandbox` command); an agent
1534
+ cannot add or remove it.
1535
+
1536
+ `:- repl_delete(Name/Arity).` abolishes a predicate, which `retract/1` cannot do:
1537
+ retracting the last clause leaves an empty predicate whose calls fail silently.
1538
+ The bare fact form `repl_delete(Name/Arity).` works the same way.
1539
+
1540
+ These apply to a file **loaded** into a session (`deepclause repl -l file.dml`).
1541
+ They are not the way to edit a live harness. `consult/1`, a file-list directive and
1542
+ `use_module/1` on a workspace `.dml` assert straight into the module rather than
1543
+ through the REPL's edit tools, so the predicates they add are absent from the
1544
+ transaction's tracked set — the nondeterminism probe does not see them, and a
1545
+ directive that fails inside the file only warns. They do still appear in the next
1546
+ commit's diff, so the change is recorded without any account of where it came from —
1547
+ and outside a transaction there is no snapshot, so nothing validates the load at all.
1548
+ An agent changing a harness adds code with `assert`/`assert_many`/`define` inside a
1549
+ transaction instead.
1550
+
1551
+ Any other directive does nothing, and the loader now says so — it reports
1552
+ `unrecognised directive ignored: …` after the file loads, so a directive that
1553
+ never took effect is not mistaken for one that did. `dynamic/1`,
1554
+ `discontiguous/1` and `multifile/1` are accepted and ignored silently, since
1555
+ session predicates are asserted and already behave that way.
1556
+
1557
+ A **predicate with no clauses fails silently** when called — it does not error —
1558
+ so an emptied or never-written predicate looks like "no results" at the call site.
1559
+ `current_predicate(Name/Arity)` in a `/goal` answers whether it exists, and a
1560
+ `doc/2` for a predicate with no clauses is reported at `end_turn`.
1561
+
949
1562
  ## The DML REPL (`deepclause repl`)
950
1563
 
951
1564
  DML skills can also be grown interactively in a persistent **living module**.