deepclause-sdk 0.2.2 → 0.4.0

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 (117) hide show
  1. package/README.md +86 -3
  2. package/dist/agent.d.ts +17 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +148 -42
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +12 -12
  7. package/dist/cli/repl.d.ts +13 -1
  8. package/dist/cli/repl.d.ts.map +1 -1
  9. package/dist/cli/repl.js +177 -5
  10. package/dist/cli/repl.js.map +1 -1
  11. package/dist/cli/run.d.ts.map +1 -1
  12. package/dist/cli/run.js +4 -0
  13. package/dist/cli/run.js.map +1 -1
  14. package/dist/forms/index.d.ts +3 -0
  15. package/dist/forms/index.d.ts.map +1 -0
  16. package/dist/forms/index.js +3 -0
  17. package/dist/forms/index.js.map +1 -0
  18. package/dist/forms/registry.d.ts +119 -0
  19. package/dist/forms/registry.d.ts.map +1 -0
  20. package/dist/forms/registry.js +256 -0
  21. package/dist/forms/registry.js.map +1 -0
  22. package/dist/forms/spec.d.ts +193 -0
  23. package/dist/forms/spec.d.ts.map +1 -0
  24. package/dist/forms/spec.js +295 -0
  25. package/dist/forms/spec.js.map +1 -0
  26. package/dist/index.d.ts +7 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +7 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/prolog/loader.d.ts +18 -0
  31. package/dist/prolog/loader.d.ts.map +1 -1
  32. package/dist/prolog/loader.js +26 -1
  33. package/dist/prolog/loader.js.map +1 -1
  34. package/dist/prolog-src/deepclause_mi.pl +786 -40
  35. package/dist/repl/agent.d.ts +11 -0
  36. package/dist/repl/agent.d.ts.map +1 -1
  37. package/dist/repl/agent.js +23 -9
  38. package/dist/repl/agent.js.map +1 -1
  39. package/dist/repl/form-widget.d.ts +56 -0
  40. package/dist/repl/form-widget.d.ts.map +1 -0
  41. package/dist/repl/form-widget.js +289 -0
  42. package/dist/repl/form-widget.js.map +1 -0
  43. package/dist/repl/harness-tools.d.ts +4 -1
  44. package/dist/repl/harness-tools.d.ts.map +1 -1
  45. package/dist/repl/harness-tools.js +15 -58
  46. package/dist/repl/harness-tools.js.map +1 -1
  47. package/dist/repl/history.d.ts +24 -1
  48. package/dist/repl/history.d.ts.map +1 -1
  49. package/dist/repl/history.js +76 -8
  50. package/dist/repl/history.js.map +1 -1
  51. package/dist/repl/index.d.ts +20 -2
  52. package/dist/repl/index.d.ts.map +1 -1
  53. package/dist/repl/index.js +9 -0
  54. package/dist/repl/index.js.map +1 -1
  55. package/dist/repl/process-backend.d.ts +2 -2
  56. package/dist/repl/process-backend.d.ts.map +1 -1
  57. package/dist/repl/process-executor.d.ts +11 -2
  58. package/dist/repl/process-executor.d.ts.map +1 -1
  59. package/dist/repl/process-executor.js +67 -18
  60. package/dist/repl/process-executor.js.map +1 -1
  61. package/dist/repl/process-worker.d.ts +35 -4
  62. package/dist/repl/process-worker.d.ts.map +1 -1
  63. package/dist/repl/process-worker.js +78 -38
  64. package/dist/repl/process-worker.js.map +1 -1
  65. package/dist/repl/processes.d.ts +4 -0
  66. package/dist/repl/processes.d.ts.map +1 -1
  67. package/dist/repl/processes.js +20 -0
  68. package/dist/repl/processes.js.map +1 -1
  69. package/dist/repl/render.d.ts +17 -0
  70. package/dist/repl/render.d.ts.map +1 -1
  71. package/dist/repl/render.js +60 -0
  72. package/dist/repl/render.js.map +1 -1
  73. package/dist/repl/session.d.ts +143 -7
  74. package/dist/repl/session.d.ts.map +1 -1
  75. package/dist/repl/session.js +317 -20
  76. package/dist/repl/session.js.map +1 -1
  77. package/dist/repl/tui/form-view.d.ts +17 -0
  78. package/dist/repl/tui/form-view.d.ts.map +1 -0
  79. package/dist/repl/tui/form-view.js +25 -0
  80. package/dist/repl/tui/form-view.js.map +1 -0
  81. package/dist/repl/tui/renderer.d.ts +4 -0
  82. package/dist/repl/tui/renderer.d.ts.map +1 -1
  83. package/dist/repl/tui/renderer.js +12 -0
  84. package/dist/repl/tui/renderer.js.map +1 -1
  85. package/dist/repl/worker-host.d.ts +16 -2
  86. package/dist/repl/worker-host.d.ts.map +1 -1
  87. package/dist/repl/worker-host.js +38 -6
  88. package/dist/repl/worker-host.js.map +1 -1
  89. package/dist/repl/worker-protocol.d.ts +14 -0
  90. package/dist/repl/worker-protocol.d.ts.map +1 -1
  91. package/dist/repl/worker-protocol.js +8 -1
  92. package/dist/repl/worker-protocol.js.map +1 -1
  93. package/dist/runner.d.ts +69 -1
  94. package/dist/runner.d.ts.map +1 -1
  95. package/dist/runner.js +381 -44
  96. package/dist/runner.js.map +1 -1
  97. package/dist/sdk.d.ts.map +1 -1
  98. package/dist/sdk.js +12 -0
  99. package/dist/sdk.js.map +1 -1
  100. package/dist/system/assets/docs/AGENT_PROMPT.md +452 -18
  101. package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
  102. package/dist/system/assets/docs/DML_AUTHORING.md +8 -4
  103. package/dist/system/assets/docs/DML_FORMS.md +740 -0
  104. package/dist/system/assets/docs/DML_REFERENCE.md +239 -4
  105. package/dist/system/assets/docs/HARNESS_EXAMPLES.md +94 -0
  106. package/dist/system/assets/index.d.ts +2 -2
  107. package/dist/system/assets/index.d.ts.map +1 -1
  108. package/dist/system/assets/index.js +4 -0
  109. package/dist/system/assets/index.js.map +1 -1
  110. package/dist/system/environment.d.ts +6 -0
  111. package/dist/system/environment.d.ts.map +1 -1
  112. package/dist/system/environment.js +5 -4
  113. package/dist/system/environment.js.map +1 -1
  114. package/dist/types.d.ts +48 -1
  115. package/dist/types.d.ts.map +1 -1
  116. package/package.json +4 -2
  117. package/src/prolog-src/deepclause_mi.pl +786 -40
@@ -22,9 +22,13 @@ end at the invariants listed at the end of this section.
22
22
  guess costs the work, the rework, and the user may never learn a guess was made.
23
23
  2. **Propose the steps, and let the user agree before you build.** Name the
24
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.
25
+ you will know it works. If the request needs **structured input from the
26
+ user** — fields, a choice, a confirmation — say that you will use a **form**
27
+ (see *Forms: structured data intake* below) and which shape: dynamic (the task
28
+ designs it) or fixed (the spec in the code).
29
+ For a new harness this is *the* important step: the design is still a sentence,
30
+ so redirecting it is cheap. For a small addition to an existing harness, one
31
+ line stating the change is enough.
28
32
  3. **Read first.** `listing` for what the module has; `listing(Name/Arity)` and
29
33
  `doc/2` for anything close to the request; `reference("examples")` or
30
34
  `reference("patterns")` for a shape that already fits. Reuse or generalize an
@@ -130,8 +134,9 @@ variable: a predicate's arguments are how a caller supplies values.
130
134
 
131
135
  - `listing` — the whole module: skill description, predicates, and docs.
132
136
  - `listing("Name/Arity")` — one predicate.
133
- - `listing("tools")` / `listing("scripts")` — the module's tool annotations /
134
- linked scripts.
137
+ - `listing("tools")` / `listing("scripts")` — the tools a nested agent can call
138
+ (the module's own annotations, plus the built-in form tools) / the linked
139
+ scripts.
135
140
 
136
141
  Call `listing` at the start of a session and again after every change.
137
142
 
@@ -145,7 +150,12 @@ Call `listing` at the start of a session and again after every change.
145
150
  shortened) or `bindings: "none"` (nothing returned) so a term you are not
146
151
  reading does not fill the transcript and your context. Both say what they left
147
152
  out, so an omitted binding never looks like a goal that bound nothing.
148
- - It runs on a scratch copy: it does not commit anything. Use it to test.
153
+ - It runs the goal against a **scratch copy of the module**: nothing it defines
154
+ is committed, so it is the way to test. The copy is of the *module*, not of the
155
+ world — `spawn`/`serve` inside `execute` create real, session-scoped processes
156
+ that outlive the call (and run the module as it was then). Testing a service
157
+ therefore leaves it running: `ps` shows it, and you can talk to it from a later
158
+ turn, or `/stop` it.
149
159
 
150
160
  # Modify the module
151
161
 
@@ -167,6 +177,18 @@ Editing is a **transaction**:
167
177
  when a helper looks nondeterministic — e.g. yields many solutions on fresh
168
178
  variables because it is missing a commit. Fix such warnings with `once/1` or
169
179
  `!` before moving on; they are exactly the predicates that flood `execute`.
180
+ **Read its result, not your intent.** `committed: false` with `changed: []`
181
+ means *nothing was recorded* — every edit in the turn was refused (check the
182
+ per-call results above it) or the block was identical. `changed` is
183
+ `+f/1` added, `-f/1` removed, `~f/1` rewritten in place; an arity change shows
184
+ as one `+` and one `-`.
185
+ - **Renaming by arity needs two edits.** A predicate is `name/arity`, so
186
+ `define("web_sources/2", …)` while `web_sources/3` exists *adds* `/2` and leaves
187
+ `/3` behind — a second predicate. `define` only replaces the arities its clause
188
+ block defines, and it refuses a head whose arity differs from the block
189
+ ("define was told to replace f/2, but the clause block defines f/3"), which is
190
+ the typo guard. To change an arity: `delete("f/3")` and `define` `f/2` in the
191
+ same transaction, and update every call site.
170
192
  - `rollback` — discard the transaction and restore the last committed module.
171
193
 
172
194
  Edits are **refused** while no transaction is open, and after `end_turn` you must
@@ -187,7 +209,171 @@ harness (`-l`, `/load`) is a user action; if that is what is wanted, say so rath
187
209
  than performing it as an edit.
188
210
 
189
211
  Other tools: `reference(topic)` reads the DeepClause docs (`language`,
190
- `authoring`, `patterns`, `examples`, `snippets`, `tools`).
212
+ `authoring`, `forms`, `patterns`, `examples`, `snippets`, `tools`).
213
+
214
+ # Forms: structured data intake
215
+
216
+ When a skill needs **structured data from the user** — a few fields, a choice, a
217
+ confirmation — use a **form**, not prose. Asking a question and parsing the reply
218
+ makes the user type the shape and you guess it; a form gives them the fields and
219
+ gives you the values keyed by name, already checked. Forms are always available —
220
+ no directive, nothing to import.
221
+
222
+ Two shapes. Pick by where the *fields* come from:
223
+
224
+ | The fields are… | Shape | What it costs |
225
+ |---|---|---|
226
+ | settled by the request or the context | **dynamic** — give `task/N` the form tools and let the model design the form from your description | a model call; the form fits the case |
227
+ | fixed and already known | **fixed** — write the spec in the code with `form/2` + `await_form/2` | no model; fully deterministic |
228
+
229
+ Both use the same host machinery and the same validation — only the author of the
230
+ spec differs. Prefer the dynamic shape when the input depends on what the user
231
+ asked for; the fixed shape when you know the fields and want the run to be
232
+ reproducible with no model.
233
+
234
+ ## Dynamic: the task decides, and designs the form
235
+
236
+ Give the task the form tools and say what you need. The model designs the form
237
+ from your description, waits for the user, and reports what they entered:
238
+
239
+ ```prolog
240
+ agent_main :-
241
+ system("You collect support tickets. Ask for whatever the case needs."),
242
+ with_tools([render_form, wait_for_form_change, close_form, set_result, finish],
243
+ task("Ask for the details this ticket needs, then report exactly what the user entered.",
244
+ string(Summary))),
245
+ answer(Summary).
246
+ ```
247
+
248
+ If the *shape* is known but the *values* come from the model, say so in the task
249
+ and let the model fill them: `task("Design a short form with subject, priority
250
+ and description.", ...)`.
251
+
252
+ That is the whole contract, and it is three calls:
253
+
254
+ 1. `render_form(spec)` — open a form; returns its id.
255
+ 2. `wait_for_form_change(formId)` — block until the user acts; returns
256
+ `{action, values, changed, errors}`. `action` is `"submit"` or `"cancel"` (or
257
+ an action the spec declared).
258
+ 3. `close_form(formId)` — when the loop is done.
259
+
260
+ In a **chat turn** (`chat/1`) you can stop after step 1: a conversation does not
261
+ end while a form it opened is still unanswered, so the turn waits for the person
262
+ and hands you the submission before it finishes. Step 2 is for another round on a
263
+ form you kept open (after `set_form_issues`/`update_form`).
264
+
265
+ **A refused submit is the host's business, not yours.** The spec's own
266
+ constraints (`required`, `enum`, ranges, `pattern`) and the harness's
267
+ `form_validate(+Values, -Errors)` run when the user submits. If they reject it,
268
+ the host puts the errors on the form and `wait_for_form_change` keeps waiting —
269
+ a refused submit is never returned. Never invent values on the user's behalf.
270
+
271
+ ## Fixed: the spec is in the code
272
+
273
+ When the fields are known, write the spec literally and drive it with the
274
+ primitives. `form/2` renders and binds a handle, `await_form/2` blocks through
275
+ refused submits, `close_form/1` closes:
276
+
277
+ ```prolog
278
+ doc(collect_claim/1, "Collect an expense claim. collect_claim(-Values).").
279
+
280
+ collect_claim(Values) :-
281
+ form(_{ title: "New expense claim",
282
+ fields: [ _{name: vendor, type: string, required: true},
283
+ _{name: amount, type: number, required: true},
284
+ _{name: note, widget: textarea} ] }, F),
285
+ await_form(F, Change),
286
+ close_form(F),
287
+ ( Change.action == submit % an atom here: submit, cancel
288
+ -> Change.values = Values % dict keyed by field name
289
+ ; Values = _{}
290
+ ).
291
+ ```
292
+
293
+ `form_advisories(+Values, -Warnings)` adds soft warnings that never block.
294
+ `form/2` and friends are **host handlers, not clauses**, so
295
+ `current_predicate(form/2)` reports `no` — exactly as it does for `exec/2`. That
296
+ is expected. Full reference: `reference("forms")`.
297
+
298
+ ## The spec (either shape)
299
+
300
+ `version` is not needed and a field's `type` defaults to `string`:
301
+
302
+ ```prolog
303
+ _{ title: "New expense claim",
304
+ fields: [ _{name: vendor, type: string, required: true},
305
+ _{name: amount, type: number, required: true},
306
+ _{name: note, widget: textarea} ],
307
+ actions: [ _{name: submit, title: "File claim"},
308
+ _{name: cancel, title: "Cancel"} ] }
309
+ ```
310
+
311
+ - `type`: `string` (default), `number`, `integer`, `boolean`, or an `object` /
312
+ `array` group. `enum` needs a `string` field.
313
+ - `widget` is a presentation hint — `text`, `textarea`, `number`, `slider`,
314
+ `select`, `radio`, `multiselect`, `checkbox`, `switch`, `date`, `time`,
315
+ `datetime`, `hidden` — and never affects validation.
316
+ - `actions` defaults to submit/cancel.
317
+
318
+ A form needs a **human**. `await_form/2` blocks the run until the UI submits, and
319
+ a run with no UI resolves the wait (`cancel` / `unavailable`) rather than hanging.
320
+ So exercise the logic *around* a form with `execute`, or ask the user to run it —
321
+ never treat a missing answer as a real one.
322
+
323
+ # Conversational agents
324
+
325
+ A **conversation** is a skill that talks with the person over many turns and acts
326
+ between them. It is a traditional agent loop: the model gets the harness's tools,
327
+ does what it needs, replies, and stops. There is **no `finish`, no `set_result`
328
+ and no nudging** — the reply *is* the answer, and the next message starts the next
329
+ turn.
330
+
331
+ Two primitives are the whole loop:
332
+
333
+ ```prolog
334
+ agent_main :-
335
+ system("You are a support assistant. Be brief."),
336
+ loop.
337
+
338
+ loop :-
339
+ input(A), % the person's next message; fails when there is none
340
+ chat(A), % one turn: tools, then it replies and stops
341
+ loop.
342
+ ```
343
+
344
+ - `input/1` waits for the next message and **has no prompt** — the embedding UI is
345
+ already a chat interface. It **fails** when there is nobody to talk to (a
346
+ non-interactive run), which is what ends the loop. Guard it (`-> ... ; true`)
347
+ when the rest of the harness should still run.
348
+ - `chat/1` appends the message as the user turn and runs the loop with every
349
+ `tool/N` you declare (plus the built-in form tools). The reply is streamed to
350
+ the UI and kept in memory; `chat(Message, Reply)` also binds it.
351
+ - **A failed turn does not end the conversation.** `chat/1` reports the failure,
352
+ keeps the memory as it was, and the next message starts a fresh turn — the
353
+ person is never dropped out of the chat.
354
+ - **Memory is the conversation.** The next `chat/1` sees what was said; a
355
+ `system/1` before the loop sets the persona and survives every turn.
356
+ - **`next/1` adds events.** The same inbox also carries a worker's message, so a
357
+ conversation can react to something that happened while the person was quiet:
358
+
359
+ ```prolog
360
+ loop(Me) :- next(Item), handle(Me, Item), loop(Me).
361
+ handle(_, user(Text)) :- chat(Text).
362
+ handle(_, message(mail(M))) :- system("New email."), chat("Summarise: {M}").
363
+ ```
364
+
365
+ `input/1` is `next/1` restricted to the person; a worker's message that arrives
366
+ first stays queued. Use it when a subagent or service should be able to wake the
367
+ conversation (a mail check, a finished job) — one turn at a time, and the turn
368
+ appears in the transcript like any other.
369
+ - Use it as the **foreground goal** (`/goal agent_main`, `-g agent_main`), not
370
+ inside `execute` or a subagent: those would wait on a prompt nobody can answer.
371
+
372
+ Do not reach for `task/N` for a conversation (it insists on a result and nags the
373
+ model), and do not reach for `chat/1` when a single turn must return a value —
374
+ that is `task/N`, with a **form** if the value comes from the person. A turn may
375
+ open a form: the person fills it in and the turn continues. Full guide:
376
+ [`docs/DML_CONVERSATION.md`](../../../../docs/DML_CONVERSATION.md).
191
377
 
192
378
  # Concurrency and subagents
193
379
 
@@ -213,6 +399,13 @@ A subagent is isolated and asynchronous:
213
399
  `answer/1` — never by an output argument. Write `send(Parent, result(...))` in
214
400
  the child and `receive` it in the parent (pass `self(Me)` into the child).
215
401
  - **It outlives the turn.** Do not busy-wait; report the id and use `ps` later.
402
+ `serve(Name, Loop, Pid)` binds the pid; `serve/2` only registers the name.
403
+ - **`ps` shows the name you talk to.** A `serve(Name, Goal)` process is labelled
404
+ `Name`, not the goal functor, so `ps`/`/ps` line up with `whereis(Name, P)` and
405
+ `service_call(Name, …)`. `process_info/2` keeps the goal, and its `usage`
406
+ counts *model* tokens: a service that only relays messages legitimately shows
407
+ zeros, so do not read zero usage as "not running" — `state` is the field for
408
+ that.
216
409
 
217
410
  Patterns:
218
411
 
@@ -223,6 +416,12 @@ Patterns:
223
416
  - **Background a long goal** with `spawn(Goal)`. If you need the result, have the
224
417
  goal `send(Parent, result(...))` and `receive` it, or make it a service. A
225
418
  one-shot subagent's terminal result can also be read with `wait(Pid, ok(Text))`.
419
+ - **A turn can suspend itself** at a safe point and be resumed later:
420
+ `suspend(loop(State))` stops the turn, the host records the continuation and the
421
+ memory, and `deepclause repl --session <id>` runs it again. It refuses while the
422
+ turn holds an open stream or form — close it first — and it is always between
423
+ host calls, never inside a `task/N`/`exec/2`. Use it for a long conversation or
424
+ service loop whose state is already the loop's argument.
226
425
  - **A service** is `serve(Name, Loop)` where `Loop` handles
227
426
  `request(From, Ref, Request)` by `send(From, reply(Ref, Reply))`, then recurses.
228
427
  Call it with `service_call(Name, Request, Reply)`.
@@ -242,10 +441,10 @@ question, write it in that final message.
242
441
 
243
442
  # Writing files and images
244
443
 
245
- - `write_file(path, content)` writes any workspace file (creating parent
246
- directories). The content is an ordinary string, so use it for SVG, JSON, or
247
- scripts instead of building them inside a shell command. Then
248
- `show_image(path)` displays an image.
444
+ - `write_file(path, content)` writes any workspace file. It does **not** create
445
+ parent directories; call `make_directory_path/1` first. The content is an
446
+ ordinary string, so use it for SVG, JSON, or scripts instead of building them
447
+ inside a shell command. Then `show_image(path)` displays an image.
249
448
  - From DML, `output(media("path.png"))` shows a workspace image and
250
449
  `output(media{svg: Svg})` shows inline SVG. `save_image(Media, "out.svg")`
251
450
  writes a reference to a file.
@@ -312,9 +511,9 @@ country(ch, "Switzerland").
312
511
  rate(chf, 1.08, "2026-01").
313
512
  ```
314
513
 
315
- Facts persist across turns and sessions, appear in `listing` and the manifest,
316
- and travel with `export`. Prefer a fact for stable knowledge the user will ask
317
- about again; keep large datasets in a file or a linked script.
514
+ Facts persist across turns and sessions, appear in `listing`, and travel with
515
+ `export`. Prefer a fact for stable knowledge the user will ask about again; keep
516
+ large datasets in a file or a linked script.
318
517
 
319
518
  The module also records its own **history**, queryable from DML (system-owned:
320
519
  excluded from `listing`/`export`, but callable):
@@ -802,6 +1001,8 @@ More patterns: `reference("patterns")`; shapes: `reference("examples")`.
802
1001
  - [system/1, user/1](#memory-predicates)
803
1002
  - [output/1, yield/1, log/1](#output-predicates)
804
1003
  - [answer/1](#answer1)
1004
+ - [Forms (structured human input)](#forms-structured-human-input)
1005
+ - [Conversation: `chat/1` and `input/1`](#conversation-chat1-and-input1)
805
1006
  - [Processes and concurrency](#processes-and-concurrency)
806
1007
  - [Control Flow](#control-flow)
807
1008
  - [String Interpolation](#string-interpolation)
@@ -1199,6 +1400,35 @@ tool(count_words(File, Count), "Count words in a workspace file.") :-
1199
1400
  count_words(File, Count).
1200
1401
  ```
1201
1402
 
1403
+ The tool head's **last argument is the output**; the ones before it are inputs. A
1404
+ tool that needs more than one input therefore needs a **result argument**: a write
1405
+ tool is `fs_write(Path, Content, Result)`, not `fs_write(Path, Content)` —
1406
+ otherwise the content is taken for the output, the model is never offered a way
1407
+ to pass it, and the write silently does nothing.
1408
+
1409
+ ```prolog
1410
+ fs_write(Path, Content, ok) :-
1411
+ file_directory_name(Path, Dir),
1412
+ ( Dir == "." -> true ; make_directory_path(Dir) ),
1413
+ write_file(Path, Content).
1414
+
1415
+ tool(fs_write(Path, Content, Result),
1416
+ "Write a text file, creating parent directories.",
1417
+ [string, string]) :-
1418
+ fs_write(Path, Content, Result).
1419
+ ```
1420
+
1421
+ `tool/3` takes the **input types** after the description, in order, one per input
1422
+ (`[string, string]` above); the last argument is still the output. A tool whose
1423
+ output argument is left unbound fails with an explanation rather than reporting a
1424
+ silent success.
1425
+
1426
+ `tool/2` and `tool/3` are **source annotations, not executable predicates**.
1427
+ Never call or assert `tool/N` as a goal. The loader installs the implementation
1428
+ as `tool(Head) :- Body` and registers its schema separately; nested agents call
1429
+ the tool by the head's predicate name, while DML calls the underlying predicate
1430
+ directly.
1431
+
1202
1432
  The head and its delegate must have the **same name and the same arity**. The
1203
1433
  tool body is what runs when the nested agent calls the tool, so a body that
1204
1434
  calls a different arity either calls nothing (`existence_error`) or calls the
@@ -1646,6 +1876,37 @@ workspace** pointing outside it; avoid committing those.
1646
1876
 
1647
1877
  ### Output Predicates
1648
1878
 
1879
+ #### `output(html(+Path))`
1880
+
1881
+ Display a workspace HTML file as an interactive miniapp:
1882
+
1883
+ ```prolog
1884
+ agent_main :-
1885
+ output(html("miniapp/index.html")).
1886
+ ```
1887
+
1888
+ The path is workspace-relative. The web host loads the file, inlines relative
1889
+ images, CSS and scripts from the same directory tree, and renders it in a
1890
+ sandboxed iframe. The miniapp runs without same-origin access to the app.
1891
+
1892
+ Inside the miniapp, `window.dc.call(Goal)` runs a DML goal in the session or run
1893
+ that rendered it and returns a promise:
1894
+
1895
+ ```html
1896
+ <script>
1897
+ window.dc.call('double(21, X).').then((result) => {
1898
+ console.log(result.bindings[0].value); // 42
1899
+ });
1900
+ </script>
1901
+ ```
1902
+
1903
+ The result contains `{ ok, output, bindings, error? }`. Calls are scoped to the
1904
+ rendering owner; the iframe cannot access the app DOM, OPFS, cookies or other
1905
+ sessions. Keep miniapp calls bounded and deterministic.
1906
+
1907
+ The same rendering is available through an `html-file` fenced block in an
1908
+ answer. `output(html(Path))` is the DML-level form.
1909
+
1649
1910
  #### `output(+Text)`
1650
1911
 
1651
1912
  Emit an output event to the caller. Does NOT affect the LLM conversation. Useful for progress updates:
@@ -1725,6 +1986,147 @@ asker(Question, Answer) :-
1725
1986
  ).
1726
1987
  ```
1727
1988
 
1989
+ ### Conversation: `chat/1` and `input/1`
1990
+
1991
+ A conversation is a harness that talks with the person over many turns and acts
1992
+ between them. Failures of `input/1` end the loop.
1993
+
1994
+ `write/1`, `writeln/1` and `format/2` are captured as output events in the REPL
1995
+ (and in `execute`); outside it they write to the process stdout as usual.
1996
+
1997
+ #### `input(-Message)`
1998
+
1999
+ Wait for the person's **next message**, with no prompt — the embedding UI is a
2000
+ chat interface and is already asking. Fails when there is nobody to talk to (a
2001
+ non-interactive run), which is how a conversation loop ends:
2002
+
2003
+ ```prolog
2004
+ loop :-
2005
+ input(A),
2006
+ chat(A),
2007
+ loop.
2008
+ ```
2009
+
2010
+ `input/2` (with a prompt) is for a question inside a task; `input/1` is the turn
2011
+ boundary of a conversation.
2012
+
2013
+ #### `next(-Item)`
2014
+
2015
+ The next thing from the conversation **inbox**: `user(Text)` when the person
2016
+ speaks, or `message(Term)` when a worker (a subagent or service) sent this
2017
+ session a message. Fails when there is nothing left. This is how a conversation
2018
+ also reacts to events, without polling:
2019
+
2020
+ ```prolog
2021
+ loop(Me) :- next(Item), handle(Me, Item), loop(Me).
2022
+
2023
+ handle(_, user(Text)) :- chat(Text).
2024
+ handle(_, message(mail(M))) :- system("New email."), chat("Summarise: {M}").
2025
+ ```
2026
+
2027
+ `input/1` is `next/1` restricted to the person — a worker's message that arrives
2028
+ first stays queued for `next/1`.
2029
+
2030
+ #### `chat(+Message)` / `chat(+Message, -Reply)`
2031
+
2032
+ One **conversational turn**: append `Message` as the user message, run the agent
2033
+ loop with every `tool/N` the harness declares, and stop when the model stops
2034
+ calling tools. A traditional agent loop — no `finish`, no `set_result`, no
2035
+ nudging. The reply is streamed to the UI and kept in memory, so the next `chat/1`
2036
+ sees the conversation; `chat/2` also binds the last assistant message.
2037
+
2038
+ ```prolog
2039
+ agent_main :-
2040
+ system("You are a support assistant. Be brief."),
2041
+ loop.
2042
+
2043
+ loop :- input(A), chat(A), loop.
2044
+ ```
2045
+
2046
+ Use it as the foreground goal (not inside `execute` or a subagent). A turn may
2047
+ open a form, so structured input fits inside a conversation. **A failed turn does
2048
+ not end the conversation**: `chat/1` reports the failure, leaves memory as it was,
2049
+ and the next message starts a fresh turn. See
2050
+ [`docs/DML_CONVERSATION.md`](../../../../docs/DML_CONVERSATION.md).
2051
+
2052
+ ### Forms (structured human input)
2053
+
2054
+ A form asks the human for typed, validated input and hands the values back keyed
2055
+ by field name. Forms are always available — there is no directive and nothing to
2056
+ enable.
2057
+
2058
+ **Two shapes**, by where the fields come from: **dynamic** — the model designs
2059
+ the form from a description (give `task/N` the form tools); **fixed** — the spec
2060
+ is written in the code, with no model. Both use the same registry and the same
2061
+ validation.
2062
+
2063
+ **The spec.** `version` is not needed and a field's `type` defaults to `string`:
2064
+
2065
+ ```prolog
2066
+ Spec = _{ title: "Refund request",
2067
+ fields: [ _{name: amount, type: number, required: true},
2068
+ _{name: reason, type: string, widget: select, required: true,
2069
+ enum: ["duplicate_charge", "not_received", "other"]},
2070
+ _{name: notes, widget: textarea, maxLength: 500} ],
2071
+ actions: [ _{name: submit, title: "Issue refund", kind: primary},
2072
+ _{name: cancel, title: "Cancel", kind: link} ] }.
2073
+ ```
2074
+
2075
+ Field `type` is `string` (default), `number`, `integer`, `boolean`, or an
2076
+ `object` / `array` group; `enum` requires a `string` field. `widget` is a
2077
+ presentation hint only — `text | textarea | number | slider | select | radio |
2078
+ multiselect | checkbox | switch | date | time | datetime | hidden` — and never
2079
+ affects validation. `actions` defaults to submit/cancel.
2080
+
2081
+ **Dynamic: a model drives the loop.** Give `task/N` the form tools and it
2082
+ designs the form from the prompt, waits, and reports:
2083
+
2084
+ ```prolog
2085
+ with_tools([render_form, wait_for_form_change, close_form, set_result, finish],
2086
+ task("Ask for the amount and a reason, then report what the user entered.",
2087
+ string(Summary)))
2088
+ ```
2089
+
2090
+ The model's three calls: `render_form(Spec)` → a form id;
2091
+ `wait_for_form_change(FormId)` → `{action, values, changed, errors}` with
2092
+ `action` = `"submit"` | `"cancel"`; `close_form(FormId)`.
2093
+
2094
+ In a **conversation** (`chat/1`) the third leg is optional: a chat turn does not
2095
+ end while a form it opened is still unanswered, so it waits for the person and
2096
+ hands back the submission. `wait_for_form_change` is then only needed for another
2097
+ round on a form you kept open.
2098
+
2099
+ **Fixed: the harness drives it itself**, with no model, using the primitives
2100
+ `form/2`, `form_update/2`, `form_errors/2`, `await_form/2,3` and `close_form/1`:
2101
+
2102
+ ```prolog
2103
+ agent_main :-
2104
+ form(Spec, F), % render; F is the form handle
2105
+ await_form(F, Change), % blocks until the user acts
2106
+ close_form(F),
2107
+ ( Change.action == submit % an atom: submit, cancel, …
2108
+ -> Change.values = Values % dict keyed by field name
2109
+ ; Values = _{}
2110
+ ).
2111
+ ```
2112
+
2113
+ `Change` is `{ formId, revision, action, values, changed, errors }`; `action` is
2114
+ an **atom**.
2115
+
2116
+ **Validation is the host's.** On a submit the host enforces the spec (`required`,
2117
+ `enum`, ranges, `pattern`, lengths) and the harness's `form_validate(+Values,
2118
+ -Errors)`. A refused submit is never returned: the host puts the errors on the
2119
+ form and `await_form/2` / `wait_for_form_change` keeps waiting. Soft rules go in
2120
+ `form_advisories(+Values, -Advisories)` (never blocking; the `advisories` tool).
2121
+
2122
+ `form/2` and friends are **host handlers, not clauses**, so
2123
+ `current_predicate(form/2)` reports `no` — exactly as it does for `exec/2`. That
2124
+ is expected; it does not mean forms are unavailable. The model-facing tools
2125
+ (`render_form`, `update_form`, `set_form_issues`, `wait_for_form_change`,
2126
+ `validate_form`, `advisories`, `close_form`) are built in.
2127
+
2128
+ Full reference: `reference("forms")`.
2129
+
1728
2130
  ---
1729
2131
 
1730
2132
  ## Processes and concurrency
@@ -1768,8 +2170,11 @@ collect([Topic|Topics], [Topic-Summary|Rest]) :-
1768
2170
  collect(Topics, Rest).
1769
2171
  ```
1770
2172
 
1771
- `spawn_opt/3` takes a dict of options: `name`, `priority`
1772
- (`foreground`/`interactive`/`background`), `timeout` (ms), and `code`.
2173
+ `spawn_opt/3` takes the keys `name`, `priority`
2174
+ (`foreground`/`interactive`/`background`), `timeout` (ms) and `code` — as a dict
2175
+ (`_{name: worker}`), a list (`[name(worker), priority(background)]`) or a single
2176
+ option (`name(worker)`). An option it does not understand is an error; it used to
2177
+ be ignored, so the process silently kept its default label.
1773
2178
 
1774
2179
  The goal's shape decides the lifecycle:
1775
2180
 
@@ -1798,9 +2203,11 @@ session; `whereis(Name, Pid)` resolves it. A name is released when the process
1798
2203
  exits. A process's default label is its goal's functor, so `send(keeper, stop)`
1799
2204
  reaches an unregistered `keeper/…` process.
1800
2205
 
1801
- ### `serve(+Name, +Goal)` / `service_call(+Target, +Request, -Reply[, +Timeout])` / `service_cast(+Target, +Message)`
2206
+ ### `serve(+Name, +Goal[, -Pid])` / `service_call(+Target, +Request, -Reply[, +Timeout])` / `service_cast(+Target, +Message)`
1802
2207
 
1803
- `serve/2` is `spawn/2` + `register/2`. `service_call` sends
2208
+ `serve/2` is `spawn/2` + `register/2` and binds nothing; `serve/3` also binds the
2209
+ pid (what you need to `stop`, `wait` or link it — `spawn/2` already returns one, so
2210
+ there is no separate "options" argument). `service_call` sends
1804
2211
  `request(Self, Ref, Request)` and waits for `reply(Ref, Reply)`, ignoring other
1805
2212
  messages; `service_cast` sends without waiting. A service is an ordinary
1806
2213
  `receive` loop:
@@ -1815,6 +2222,11 @@ counter_service(Count) :-
1815
2222
  ).
1816
2223
  ```
1817
2224
 
2225
+
2226
+ The process is labelled with `Name` (not the goal functor), so `ps`, `whereis/2`
2227
+ and `service_call/3` all use the same handle. `process_info/2` reports both the
2228
+ label and the registered names, and its `usage` counts model tokens — a relay
2229
+ service shows zeros while running.
1818
2230
  ### `swarm(+Worker, +Items, -Results[, +Concurrency])` / `pmap(+Worker, +Items, -Values)`
1819
2231
 
1820
2232
  Run `call(Worker, Item, Value)` once per item, each in its own process, and
@@ -1828,6 +2240,28 @@ swarm(summarise_file, Files, Results, 4),
1828
2240
  findall(S, member(ok(S), Results), Summaries).
1829
2241
  ```
1830
2242
 
2243
+ ### `suspend(+Continuation)`
2244
+
2245
+ Stop the turn at a **safe point** and let the host persist where to continue.
2246
+ `Continuation` is the goal that resumes — the entry point of the loop whose state
2247
+ is its argument:
2248
+
2249
+ ```prolog
2250
+ loop(N) :-
2251
+ ( N >= 2
2252
+ -> suspend(loop(N)) % stop; a later --session resumes with loop(N)
2253
+ ; M is N + 1, loop(M)
2254
+ ).
2255
+ ```
2256
+
2257
+ The turn ends cleanly (like `answer/1`), the host records the continuation and the
2258
+ memory, and `deepclause repl --session <id>` runs it. **A turn cannot suspend while
2259
+ holding something a snapshot cannot carry**: an open stream (`open/3,4` … `close/1`)
2260
+ or an open form (`form/2` … `close_form/1`) makes it fail with
2261
+ `cannot suspend while holding …`. A suspension is always *between* host calls —
2262
+ never inside a `task/N`, `exec/2` or form wait — and the program chooses the point.
2263
+ See [`docs/DML_REPL_SUSPEND_RESUME.md`](../../../../docs/DML_REPL_SUSPEND_RESUME.md).
2264
+
1831
2265
  ### `wait(+Pid[, -Outcome])`
1832
2266
 
1833
2267
  Block until a process terminates, then report what happened. `Outcome` is
@@ -0,0 +1,28 @@
1
+ # DeepClause Chat Agent
2
+
3
+ You are an AI agent in an **ongoing conversation** with a person. Each message is
4
+ one turn; the conversation continues after you reply.
5
+
6
+ ## Available Tools
7
+
8
+ {TOOL_DESCRIPTIONS}
9
+
10
+ ## How a turn works
11
+
12
+ - You receive one message at a time — from the person, or from the runtime.
13
+ - Call tools when they help. You may call several in a turn, and you see their
14
+ results before you reply.
15
+ - When you have nothing left to do, **reply in plain text and stop.** That ends
16
+ the turn. There is no `finish` tool and no results to set; the next message
17
+ starts the next turn.
18
+ - Do not narrate a tool call instead of making it, and do not ask for permission
19
+ to continue.
20
+
21
+ ## Style
22
+
23
+ - Answer in the conversation's language and register. Be brief unless asked.
24
+ - Report what you did, not what you would do.
25
+ - If a tool fails, say what failed and what you will try — or ask one question.
26
+ - Never invent the person's words, a value, or a tool result you did not get.
27
+
28
+ {ENVIRONMENT_SECTION}
@@ -241,9 +241,13 @@ text-only — never attach media to them.
241
241
  - Replace a predicate with `define(head, clauses)`, where the head names the
242
242
  predicate being replaced and must be one of the predicates the block defines.
243
243
  Never patch it clause by clause.
244
- - `assert`/`assert_many` are for a **new** predicate: they append, so against an
245
- existing one both versions would stay live with their clauses interleaved, and
246
- the runtime refuses the call rather than let that happen.
244
+ - Open the transaction once with `begin_turn`; it is idempotent and returns a
245
+ reminder if it is already open. Do not call it again before every edit.
246
+ - `assert`/`assert_many` are only for a **new** predicate: they reject a
247
+ predicate that already has any clauses. This includes `doc/2` and other
248
+ annotation/fact predicates. There is no append operation in the authoring
249
+ tools. To change an existing predicate, use `define` with its complete clause
250
+ set; to add a `doc/2` fact, include all existing doc facts in that `define`.
247
251
  - `retract` removes the clauses matching its pattern and says how many; `delete`
248
252
  removes a whole predicate. `retract` fails when nothing matched — check that
249
253
  the pattern is the one you meant.
@@ -283,7 +287,7 @@ in parallel.
283
287
  each in its own process, results in input order. Bound the list.
284
288
  - **Background a long goal** (`spawn/2`): report the id and carry on; the user
285
289
  watches it with `/ps --watch --logs`.
286
- - **A service** (`serve/2` + `service_call/3`): a long-lived `receive` loop that
290
+ - **A service** (`serve/2` — or `serve/3` for the pid — plus `service_call/3`): a long-lived `receive` loop that
287
291
  answers requests; also good for a single-writer or rate-limited step.
288
292
 
289
293
  A subagent is isolated (private module snapshot) and asynchronous (value