deepclause-sdk 0.1.3 → 0.2.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 (92) hide show
  1. package/README.md +45 -6
  2. package/dist/cli/config.d.ts +2 -2
  3. package/dist/cli/repl.d.ts +36 -0
  4. package/dist/cli/repl.d.ts.map +1 -1
  5. package/dist/cli/repl.js +414 -14
  6. package/dist/cli/repl.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/prolog-src/deepclause_mi.pl +547 -2
  12. package/dist/repl/backend.d.ts +9 -0
  13. package/dist/repl/backend.d.ts.map +1 -1
  14. package/dist/repl/backend.js +44 -0
  15. package/dist/repl/backend.js.map +1 -1
  16. package/dist/repl/harness-tools.d.ts +36 -0
  17. package/dist/repl/harness-tools.d.ts.map +1 -1
  18. package/dist/repl/harness-tools.js +173 -0
  19. package/dist/repl/harness-tools.js.map +1 -1
  20. package/dist/repl/history.d.ts +29 -0
  21. package/dist/repl/history.d.ts.map +1 -1
  22. package/dist/repl/history.js +71 -0
  23. package/dist/repl/history.js.map +1 -1
  24. package/dist/repl/index.d.ts +6 -0
  25. package/dist/repl/index.d.ts.map +1 -1
  26. package/dist/repl/index.js +9 -1
  27. package/dist/repl/index.js.map +1 -1
  28. package/dist/repl/picker.d.ts +3 -0
  29. package/dist/repl/picker.d.ts.map +1 -1
  30. package/dist/repl/picker.js +132 -0
  31. package/dist/repl/picker.js.map +1 -1
  32. package/dist/repl/process-backend.d.ts +31 -0
  33. package/dist/repl/process-backend.d.ts.map +1 -0
  34. package/dist/repl/process-backend.js +10 -0
  35. package/dist/repl/process-backend.js.map +1 -0
  36. package/dist/repl/process-executor.d.ts +67 -0
  37. package/dist/repl/process-executor.d.ts.map +1 -0
  38. package/dist/repl/process-executor.js +371 -0
  39. package/dist/repl/process-executor.js.map +1 -0
  40. package/dist/repl/process-worker.d.ts +10 -0
  41. package/dist/repl/process-worker.d.ts.map +1 -0
  42. package/dist/repl/process-worker.js +210 -0
  43. package/dist/repl/process-worker.js.map +1 -0
  44. package/dist/repl/processes.d.ts +282 -0
  45. package/dist/repl/processes.d.ts.map +1 -0
  46. package/dist/repl/processes.js +557 -0
  47. package/dist/repl/processes.js.map +1 -0
  48. package/dist/repl/render.d.ts.map +1 -1
  49. package/dist/repl/render.js +19 -5
  50. package/dist/repl/render.js.map +1 -1
  51. package/dist/repl/session.d.ts +142 -3
  52. package/dist/repl/session.d.ts.map +1 -1
  53. package/dist/repl/session.js +435 -4
  54. package/dist/repl/session.js.map +1 -1
  55. package/dist/repl/tui/autocomplete.d.ts.map +1 -1
  56. package/dist/repl/tui/autocomplete.js +7 -0
  57. package/dist/repl/tui/autocomplete.js.map +1 -1
  58. package/dist/repl/tui/harness-view.d.ts +102 -0
  59. package/dist/repl/tui/harness-view.d.ts.map +1 -0
  60. package/dist/repl/tui/harness-view.js +323 -0
  61. package/dist/repl/tui/harness-view.js.map +1 -0
  62. package/dist/repl/tui/renderer.d.ts.map +1 -1
  63. package/dist/repl/tui/renderer.js +6 -1
  64. package/dist/repl/tui/renderer.js.map +1 -1
  65. package/dist/repl/tui/services-panel.d.ts +35 -0
  66. package/dist/repl/tui/services-panel.d.ts.map +1 -0
  67. package/dist/repl/tui/services-panel.js +108 -0
  68. package/dist/repl/tui/services-panel.js.map +1 -0
  69. package/dist/repl/tui/status-bar.d.ts +2 -0
  70. package/dist/repl/tui/status-bar.d.ts.map +1 -1
  71. package/dist/repl/tui/status-bar.js +2 -0
  72. package/dist/repl/tui/status-bar.js.map +1 -1
  73. package/dist/repl/tui/tool-pane.d.ts.map +1 -1
  74. package/dist/repl/tui/tool-pane.js +6 -2
  75. package/dist/repl/tui/tool-pane.js.map +1 -1
  76. package/dist/repl/worker-host.d.ts +80 -0
  77. package/dist/repl/worker-host.d.ts.map +1 -0
  78. package/dist/repl/worker-host.js +305 -0
  79. package/dist/repl/worker-host.js.map +1 -0
  80. package/dist/repl/worker-protocol.d.ts +123 -0
  81. package/dist/repl/worker-protocol.d.ts.map +1 -0
  82. package/dist/repl/worker-protocol.js +9 -0
  83. package/dist/repl/worker-protocol.js.map +1 -0
  84. package/dist/runner.d.ts +24 -0
  85. package/dist/runner.d.ts.map +1 -1
  86. package/dist/runner.js +58 -0
  87. package/dist/runner.js.map +1 -1
  88. package/dist/system/assets/docs/AGENT_PROMPT.md +247 -0
  89. package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
  90. package/dist/system/assets/docs/DML_REFERENCE.md +199 -0
  91. package/package.json +7 -6
  92. package/src/prolog-src/deepclause_mi.pl +547 -2
@@ -189,6 +189,54 @@ than performing it as an edit.
189
189
  Other tools: `reference(topic)` reads the DeepClause docs (`language`,
190
190
  `authoring`, `patterns`, `examples`, `snippets`, `tools`).
191
191
 
192
+ # Concurrency and subagents
193
+
194
+ A **subagent** is a session-scoped DML process that keeps running after this
195
+ turn. Reach for one when work is long-running, must wait for messages, or can
196
+ run in parallel — not when a single `task/N` or `execute` call does the job.
197
+
198
+ | The work is… | Use |
199
+ |---|---|
200
+ | one model call, or a quick goal | `task/N` / `execute` |
201
+ | the same goal over a list, in parallel | `swarm/3` / `pmap/3` |
202
+ | long, and the user can wait | `spawn(Goal)` — report the id and carry on |
203
+ | a long-lived message handler | `serve(Name, Loop)` + `service_call`/`service_cast` |
204
+ | a decision from the user mid-run | `input/2` inside the subagent |
205
+ | several specialised roles collaborating | `serve` each role; a coordinator wires them |
206
+
207
+ A subagent is isolated and asynchronous:
208
+
209
+ - **Private snapshot.** Its `assert`/`retract` do not touch the harness, and it
210
+ does not see commits made after it started.
211
+ - **Value semantics.** `spawn` and `send` copy terms. A child cannot bind a
212
+ variable in the parent, so a result comes back by `send/2` or as the child's
213
+ `answer/1` — never by an output argument. Write `send(Parent, result(...))` in
214
+ the child and `receive` it in the parent (pass `self(Me)` into the child).
215
+ - **It outlives the turn.** Do not busy-wait; report the id and use `ps` later.
216
+
217
+ Patterns:
218
+
219
+ - **Fan out over a list** with `swarm(Worker, Items, Results)` — ordered, one
220
+ process per item (at most 4 at once), `ok(Value)`/`failed` per item — or
221
+ `pmap/3` (bare values, fails on any failure). Bound the list: each item is a
222
+ process and usually a model call, and the session has a process limit.
223
+ - **Background a long goal** with `spawn(Goal)`. If you need the result, have the
224
+ goal `send(Parent, result(...))` and `receive` it, or make it a service. A
225
+ one-shot subagent's terminal result can also be read with `wait(Pid, ok(Text))`.
226
+ - **A service** is `serve(Name, Loop)` where `Loop` handles
227
+ `request(From, Ref, Request)` by `send(From, reply(Ref, Reply))`, then recurses.
228
+ Call it with `service_call(Name, Request, Reply)`.
229
+ - **Recursive delegation** is a subagent that spawns subagents; bound the depth.
230
+
231
+ Subagent tools: `spawn(goal, name?)`, `ps`, `send(to, message)`,
232
+ `call(to, message)`, `stop(target)`, `swarm(worker, items)`, `wait(pid)`. Users
233
+ also have `/spawn`, `/send`, `/call`, `/stop`, and `/ps --watch --logs` (a live
234
+ follow of every subagent's streamed output, reasoning, and tool calls).
235
+
236
+ Do not spawn a subagent for a single model call, and do not fan out an unbounded
237
+ list. If the work needs the harness's shared state, or the user's answer now,
238
+ keep it in the foreground.
239
+
192
240
  End your turn by replying with text and no tool call. To ask the human a
193
241
  question, write it in that final message.
194
242
 
@@ -754,6 +802,7 @@ More patterns: `reference("patterns")`; shapes: `reference("examples")`.
754
802
  - [system/1, user/1](#memory-predicates)
755
803
  - [output/1, yield/1, log/1](#output-predicates)
756
804
  - [answer/1](#answer1)
805
+ - [Processes and concurrency](#processes-and-concurrency)
757
806
  - [Control Flow](#control-flow)
758
807
  - [String Interpolation](#string-interpolation)
759
808
  - [Backtracking](#backtracking)
@@ -1122,6 +1171,13 @@ A command is also killed when it produces no output for `DC_HOST_SHELL_IDLE_TIME
1122
1171
  (default 180 000 ms); `DC_HOST_SHELL_TIMEOUT_MS` sets a default wall-clock cap
1123
1172
  (0 = disabled). Exit code `124` means a timeout.
1124
1173
 
1174
+ **The host may refuse a tool.** A session can set a tool **policy** — a whitelist
1175
+ or blacklist, by name or glob. A refused `exec/2` fails exactly as a tool that
1176
+ returned failure, and the reason is reported to the operator. The policy applies
1177
+ to *every* `exec/2`, including one inside a `tool/2` body, so a tool cannot reach
1178
+ a capability the policy blocked. `ask_user` is exempt: it is the human channel,
1179
+ not a capability.
1180
+
1125
1181
  ---
1126
1182
 
1127
1183
  ### Tool Definitions
@@ -1428,6 +1484,29 @@ agent_main :-
1428
1484
  answer("Explained!").
1429
1485
  ```
1430
1486
 
1487
+ #### `push_context` / `pop_context`
1488
+
1489
+ Save the current memory to a stack and restore it, which makes memory
1490
+ **backtrackable**. `push_context(clear)` also empties the active memory.
1491
+
1492
+ ```prolog
1493
+ agent_main(Draft) :-
1494
+ push_context(clear),
1495
+ system("You are a terse reviewer."),
1496
+ task("Review this in one line: {Draft}", string(Note)),
1497
+ pop_context,
1498
+ answer(Note).
1499
+ ```
1500
+
1501
+ #### `clear_memory`
1502
+
1503
+ Clear the active memory without saving it.
1504
+
1505
+ #### `get_memory(-Messages)`
1506
+
1507
+ Bind the current memory as a list of `message{role, content, media}` dicts, for a
1508
+ predicate that needs to inspect or rebuild it.
1509
+
1431
1510
  ---
1432
1511
 
1433
1512
  ### Media Predicates
@@ -1456,6 +1535,12 @@ agent_main :-
1456
1535
  Memory stores `media{path, mime}` — never the image bytes. Judgments
1457
1536
  (`choose`/`rate`/`verify`/`probability`) stay text-only.
1458
1537
 
1538
+ #### `save_image(+Media, +OutPath)`
1539
+
1540
+ Write a media reference to a workspace file — a copied image
1541
+ (`save_image(media("in.png", "image/png"), "out.png")`) or inline SVG
1542
+ (`save_image(media{svg: Svg}, "out.svg")`). Parent directories must exist.
1543
+
1459
1544
  ---
1460
1545
 
1461
1546
  #### Running a command with data in it
@@ -1624,6 +1709,168 @@ agent_main :-
1624
1709
 
1625
1710
  ---
1626
1711
 
1712
+ ### Human input
1713
+
1714
+ #### `input(+Prompt, -Input)`
1715
+
1716
+ Ask the human and bind their reply. In the REPL this opens the editor's input
1717
+ prompt; in a non-interactive run (`-g`, piped stdin, CI) it **fails** rather than
1718
+ hang, so guard it when a goal may run unattended:
1719
+
1720
+ ```prolog
1721
+ asker(Question, Answer) :-
1722
+ ( input(Question, Answer)
1723
+ -> true
1724
+ ; Answer = "(no answer available)"
1725
+ ).
1726
+ ```
1727
+
1728
+ ---
1729
+
1730
+ ## Processes and concurrency
1731
+
1732
+ A **process** (a "subagent") is an isolated DML execution with its own private
1733
+ memory, a private snapshot of this module, and a mailbox. Processes outlive the
1734
+ turn that started them, so a harness can background long work, wait for
1735
+ messages, or run many things at once.
1736
+
1737
+ Process primitives work anywhere a goal runs, including inside a `tool/2` body:
1738
+ a tool executes in its own engine, but `self/1` is the process the tool runs in,
1739
+ so a tool can route work to other processes.
1740
+
1741
+ A process runs the harness **as of its spawn**: later edits to a predicate do not
1742
+ reach a process already running, while a process spawned afterwards sees them.
1743
+ Inspect the code a process runs with `ps(code: "<pid or name>")`, and the
1744
+ session's current harness version with `ps(code: "")`.
1745
+
1746
+ ### `spawn(+Goal, -Pid)` / `spawn_opt(+Goal, -Pid, +Options)`
1747
+
1748
+ Start `Goal` in a new process and return its pid immediately. `Goal` is **copied**:
1749
+ variables in the child are fresh, so a child can never bind a variable in the
1750
+ parent. A result comes back by message (`send/2`) or as the child's `answer/1`.
1751
+
1752
+ ```prolog
1753
+ worker(Topic, Parent) :-
1754
+ task("Summarise {Topic} in one sentence.", string(Summary)),
1755
+ send(Parent, done(Topic, Summary)),
1756
+ answer(Summary).
1757
+
1758
+ fan_out(Topics, Results) :-
1759
+ self(Me),
1760
+ maplist(spawn_worker(Me), Topics, _Pids),
1761
+ collect(Topics, Results).
1762
+
1763
+ spawn_worker(Me, Topic, Pid) :- spawn(worker(Topic, Me), Pid).
1764
+
1765
+ collect([], []).
1766
+ collect([Topic|Topics], [Topic-Summary|Rest]) :-
1767
+ receive(done(Topic, Summary), 300000),
1768
+ collect(Topics, Rest).
1769
+ ```
1770
+
1771
+ `spawn_opt/3` takes a dict of options: `name`, `priority`
1772
+ (`foreground`/`interactive`/`background`), `timeout` (ms), and `code`.
1773
+
1774
+ The goal's shape decides the lifecycle:
1775
+
1776
+ - **one-shot** — calls `output/1`/`answer/1` and returns.
1777
+ - **message-waiting** — loops on `receive/1,2`, handling terms from other
1778
+ processes or from the user.
1779
+ - **interactive** — calls `input/2` and blocks until the user answers.
1780
+
1781
+ ### `send(+Target, +Message)` / `receive(?Message[, +Timeout])`
1782
+
1783
+ `send/2` copies a term into another process's mailbox; `Target` is a pid, a
1784
+ registered name, or a process label. `receive/2` pops the oldest message and
1785
+ fails on timeout. `receive_match/3` pops the oldest message that unifies with a
1786
+ template, requeues the rest, and fails on timeout.
1787
+
1788
+ ```prolog
1789
+ receive(request(From, Ref, Query), 30000),
1790
+ handle(Query, Answer),
1791
+ send(From, reply(Ref, Answer)).
1792
+ ```
1793
+
1794
+ ### `self/1`, `register/2`, `whereis/2`
1795
+
1796
+ `self(Pid)` is this process's pid. `register(Name, Pid)` names a process in the
1797
+ session; `whereis(Name, Pid)` resolves it. A name is released when the process
1798
+ exits. A process's default label is its goal's functor, so `send(keeper, stop)`
1799
+ reaches an unregistered `keeper/…` process.
1800
+
1801
+ ### `serve(+Name, +Goal)` / `service_call(+Target, +Request, -Reply[, +Timeout])` / `service_cast(+Target, +Message)`
1802
+
1803
+ `serve/2` is `spawn/2` + `register/2`. `service_call` sends
1804
+ `request(Self, Ref, Request)` and waits for `reply(Ref, Reply)`, ignoring other
1805
+ messages; `service_cast` sends without waiting. A service is an ordinary
1806
+ `receive` loop:
1807
+
1808
+ ```prolog
1809
+ counter_service(Count) :-
1810
+ receive(Msg),
1811
+ ( Msg = request(From, Ref, read) -> send(From, reply(Ref, Count)), counter_service(Count)
1812
+ ; Msg = request(From, Ref, bump) -> Next is Count + 1, send(From, reply(Ref, Next)), counter_service(Next)
1813
+ ; Msg = stop -> true
1814
+ ; counter_service(Count)
1815
+ ).
1816
+ ```
1817
+
1818
+ ### `swarm(+Worker, +Items, -Results[, +Concurrency])` / `pmap(+Worker, +Items, -Values)`
1819
+
1820
+ Run `call(Worker, Item, Value)` once per item, each in its own process, and
1821
+ collect the results **in input order**. At most `Concurrency` items run at once
1822
+ (default 4), so a large list does not fire every model call simultaneously.
1823
+ `swarm` returns `ok(Value)`/`failed` per item; `pmap` returns bare values and
1824
+ fails if any item failed.
1825
+
1826
+ ```prolog
1827
+ swarm(summarise_file, Files, Results, 4),
1828
+ findall(S, member(ok(S), Results), Summaries).
1829
+ ```
1830
+
1831
+ ### `wait(+Pid[, -Outcome])`
1832
+
1833
+ Block until a process terminates, then report what happened. `Outcome` is
1834
+ `ok(Result)`, `failed(Error)`, or `killed(Reason)`; `Result` is the process's
1835
+ terminal text as a Prolog string. `wait/1` is semidet — it succeeds only on a
1836
+ successful finish.
1837
+
1838
+ ```prolog
1839
+ spawn(research("qec", Me), Pid),
1840
+ wait(Pid, ok(Text)), % Text is the subagent's terminal text
1841
+ ```
1842
+
1843
+ For a structured result, prefer the message form: have the child
1844
+ `send(Parent, result(Term))` and `receive` it in the parent.
1845
+
1846
+ ### `exit/1,2` and stopping
1847
+
1848
+ `exit(Reason)` terminates the current process. `exit(Pid, Reason)` terminates
1849
+ another (a registered name or a process label works too); there is no `stop/1`
1850
+ predicate, so `exit/2` is how DML cancels a peer. (The `/stop` command and the
1851
+ `stop` tool are the interactive equivalents.) `process_info(Pid, Info)` returns a
1852
+ dict (`pid`, `name`, `state`, `priority`, `mailbox`, `usage`, …). `sleep(Ms)`
1853
+ yields; `now_ms(T)` and `make_ref(Ref)` are available.
1854
+
1855
+ ### Rules
1856
+
1857
+ - **Isolation.** A process runs against a private module snapshot: its
1858
+ `assert`/`retract` do not affect the harness, and it does not see commits made
1859
+ after it started.
1860
+ - **Value semantics.** `spawn` and `send` copy terms; never rely on a shared
1861
+ variable with a child.
1862
+ - **Determinism.** A process that calls `task/N`, `exec/2`, `read_media/2`, or
1863
+ writes a file should yield one solution (`once/1`, `->`, `!`).
1864
+ - **Bound the work.** Every `swarm` item is a process and usually a model call;
1865
+ the session has a **live-process limit** (default 16 concurrent — finished
1866
+ processes do not count, so a long session can fan out again) and the provider
1867
+ has rate limits. A `swarm` item whose process cannot be spawned is reported as
1868
+ `failed(...)`; `pmap` then fails.
1869
+ - **CPU-bound Prolog** blocks the in-process host (L1). Long compute belongs in
1870
+ the REPL's `--execution worker` (L2), not a `swarm` on the default host.
1871
+
1872
+ ---
1873
+
1627
1874
  ## Control Flow
1628
1875
 
1629
1876
  DML supports standard Prolog control flow:
@@ -272,3 +272,24 @@ text-only — never attach media to them.
272
272
  `/dc-run` skill.
273
273
  - Media: reference images with `media(Path, Mime)` (`read_media/2` +
274
274
  `attach_media/1`); never inline bytes.
275
+
276
+ ## 7. Concurrency and subagents
277
+
278
+ Most skills are a short sequence of `task/N` calls in the foreground. Reach for a
279
+ **subagent** only when the work must outlive the turn, wait for a message, or run
280
+ in parallel.
281
+
282
+ - **Fan out over a list** (`swarm/3`, `pmap/3`): the same worker over many items,
283
+ each in its own process, results in input order. Bound the list.
284
+ - **Background a long goal** (`spawn/2`): report the id and carry on; the user
285
+ watches it with `/ps --watch --logs`.
286
+ - **A service** (`serve/2` + `service_call/3`): a long-lived `receive` loop that
287
+ answers requests; also good for a single-writer or rate-limited step.
288
+
289
+ A subagent is isolated (private module snapshot) and asynchronous (value
290
+ semantics): its `assert`/`retract` do not touch the harness, and a result comes
291
+ back by `send/2` or the child's `answer/1`, never by an output argument. Prefer
292
+ `swarm` over hand-rolled `spawn`+`receive` unless you need a custom protocol.
293
+
294
+ Do not fan out an unbounded list (each item is a process and usually a model
295
+ call), and do not spawn for a single model call.
@@ -15,6 +15,7 @@
15
15
  - [system/1, user/1](#memory-predicates)
16
16
  - [output/1, yield/1, log/1](#output-predicates)
17
17
  - [answer/1](#answer1)
18
+ - [Processes and concurrency](#processes-and-concurrency)
18
19
  - [Control Flow](#control-flow)
19
20
  - [String Interpolation](#string-interpolation)
20
21
  - [Backtracking](#backtracking)
@@ -383,6 +384,13 @@ A command is also killed when it produces no output for `DC_HOST_SHELL_IDLE_TIME
383
384
  (default 180 000 ms); `DC_HOST_SHELL_TIMEOUT_MS` sets a default wall-clock cap
384
385
  (0 = disabled). Exit code `124` means a timeout.
385
386
 
387
+ **The host may refuse a tool.** A session can set a tool **policy** — a whitelist
388
+ or blacklist, by name or glob. A refused `exec/2` fails exactly as a tool that
389
+ returned failure, and the reason is reported to the operator. The policy applies
390
+ to *every* `exec/2`, including one inside a `tool/2` body, so a tool cannot reach
391
+ a capability the policy blocked. `ask_user` is exempt: it is the human channel,
392
+ not a capability.
393
+
386
394
  ---
387
395
 
388
396
  ### Tool Definitions
@@ -689,6 +697,29 @@ agent_main :-
689
697
  answer("Explained!").
690
698
  ```
691
699
 
700
+ #### `push_context` / `pop_context`
701
+
702
+ Save the current memory to a stack and restore it, which makes memory
703
+ **backtrackable**. `push_context(clear)` also empties the active memory.
704
+
705
+ ```prolog
706
+ agent_main(Draft) :-
707
+ push_context(clear),
708
+ system("You are a terse reviewer."),
709
+ task("Review this in one line: {Draft}", string(Note)),
710
+ pop_context,
711
+ answer(Note).
712
+ ```
713
+
714
+ #### `clear_memory`
715
+
716
+ Clear the active memory without saving it.
717
+
718
+ #### `get_memory(-Messages)`
719
+
720
+ Bind the current memory as a list of `message{role, content, media}` dicts, for a
721
+ predicate that needs to inspect or rebuild it.
722
+
692
723
  ---
693
724
 
694
725
  ### Media Predicates
@@ -717,6 +748,12 @@ agent_main :-
717
748
  Memory stores `media{path, mime}` — never the image bytes. Judgments
718
749
  (`choose`/`rate`/`verify`/`probability`) stay text-only.
719
750
 
751
+ #### `save_image(+Media, +OutPath)`
752
+
753
+ Write a media reference to a workspace file — a copied image
754
+ (`save_image(media("in.png", "image/png"), "out.png")`) or inline SVG
755
+ (`save_image(media{svg: Svg}, "out.svg")`). Parent directories must exist.
756
+
720
757
  ---
721
758
 
722
759
  #### Running a command with data in it
@@ -885,6 +922,168 @@ agent_main :-
885
922
 
886
923
  ---
887
924
 
925
+ ### Human input
926
+
927
+ #### `input(+Prompt, -Input)`
928
+
929
+ Ask the human and bind their reply. In the REPL this opens the editor's input
930
+ prompt; in a non-interactive run (`-g`, piped stdin, CI) it **fails** rather than
931
+ hang, so guard it when a goal may run unattended:
932
+
933
+ ```prolog
934
+ asker(Question, Answer) :-
935
+ ( input(Question, Answer)
936
+ -> true
937
+ ; Answer = "(no answer available)"
938
+ ).
939
+ ```
940
+
941
+ ---
942
+
943
+ ## Processes and concurrency
944
+
945
+ A **process** (a "subagent") is an isolated DML execution with its own private
946
+ memory, a private snapshot of this module, and a mailbox. Processes outlive the
947
+ turn that started them, so a harness can background long work, wait for
948
+ messages, or run many things at once.
949
+
950
+ Process primitives work anywhere a goal runs, including inside a `tool/2` body:
951
+ a tool executes in its own engine, but `self/1` is the process the tool runs in,
952
+ so a tool can route work to other processes.
953
+
954
+ A process runs the harness **as of its spawn**: later edits to a predicate do not
955
+ reach a process already running, while a process spawned afterwards sees them.
956
+ Inspect the code a process runs with `ps(code: "<pid or name>")`, and the
957
+ session's current harness version with `ps(code: "")`.
958
+
959
+ ### `spawn(+Goal, -Pid)` / `spawn_opt(+Goal, -Pid, +Options)`
960
+
961
+ Start `Goal` in a new process and return its pid immediately. `Goal` is **copied**:
962
+ variables in the child are fresh, so a child can never bind a variable in the
963
+ parent. A result comes back by message (`send/2`) or as the child's `answer/1`.
964
+
965
+ ```prolog
966
+ worker(Topic, Parent) :-
967
+ task("Summarise {Topic} in one sentence.", string(Summary)),
968
+ send(Parent, done(Topic, Summary)),
969
+ answer(Summary).
970
+
971
+ fan_out(Topics, Results) :-
972
+ self(Me),
973
+ maplist(spawn_worker(Me), Topics, _Pids),
974
+ collect(Topics, Results).
975
+
976
+ spawn_worker(Me, Topic, Pid) :- spawn(worker(Topic, Me), Pid).
977
+
978
+ collect([], []).
979
+ collect([Topic|Topics], [Topic-Summary|Rest]) :-
980
+ receive(done(Topic, Summary), 300000),
981
+ collect(Topics, Rest).
982
+ ```
983
+
984
+ `spawn_opt/3` takes a dict of options: `name`, `priority`
985
+ (`foreground`/`interactive`/`background`), `timeout` (ms), and `code`.
986
+
987
+ The goal's shape decides the lifecycle:
988
+
989
+ - **one-shot** — calls `output/1`/`answer/1` and returns.
990
+ - **message-waiting** — loops on `receive/1,2`, handling terms from other
991
+ processes or from the user.
992
+ - **interactive** — calls `input/2` and blocks until the user answers.
993
+
994
+ ### `send(+Target, +Message)` / `receive(?Message[, +Timeout])`
995
+
996
+ `send/2` copies a term into another process's mailbox; `Target` is a pid, a
997
+ registered name, or a process label. `receive/2` pops the oldest message and
998
+ fails on timeout. `receive_match/3` pops the oldest message that unifies with a
999
+ template, requeues the rest, and fails on timeout.
1000
+
1001
+ ```prolog
1002
+ receive(request(From, Ref, Query), 30000),
1003
+ handle(Query, Answer),
1004
+ send(From, reply(Ref, Answer)).
1005
+ ```
1006
+
1007
+ ### `self/1`, `register/2`, `whereis/2`
1008
+
1009
+ `self(Pid)` is this process's pid. `register(Name, Pid)` names a process in the
1010
+ session; `whereis(Name, Pid)` resolves it. A name is released when the process
1011
+ exits. A process's default label is its goal's functor, so `send(keeper, stop)`
1012
+ reaches an unregistered `keeper/…` process.
1013
+
1014
+ ### `serve(+Name, +Goal)` / `service_call(+Target, +Request, -Reply[, +Timeout])` / `service_cast(+Target, +Message)`
1015
+
1016
+ `serve/2` is `spawn/2` + `register/2`. `service_call` sends
1017
+ `request(Self, Ref, Request)` and waits for `reply(Ref, Reply)`, ignoring other
1018
+ messages; `service_cast` sends without waiting. A service is an ordinary
1019
+ `receive` loop:
1020
+
1021
+ ```prolog
1022
+ counter_service(Count) :-
1023
+ receive(Msg),
1024
+ ( Msg = request(From, Ref, read) -> send(From, reply(Ref, Count)), counter_service(Count)
1025
+ ; Msg = request(From, Ref, bump) -> Next is Count + 1, send(From, reply(Ref, Next)), counter_service(Next)
1026
+ ; Msg = stop -> true
1027
+ ; counter_service(Count)
1028
+ ).
1029
+ ```
1030
+
1031
+ ### `swarm(+Worker, +Items, -Results[, +Concurrency])` / `pmap(+Worker, +Items, -Values)`
1032
+
1033
+ Run `call(Worker, Item, Value)` once per item, each in its own process, and
1034
+ collect the results **in input order**. At most `Concurrency` items run at once
1035
+ (default 4), so a large list does not fire every model call simultaneously.
1036
+ `swarm` returns `ok(Value)`/`failed` per item; `pmap` returns bare values and
1037
+ fails if any item failed.
1038
+
1039
+ ```prolog
1040
+ swarm(summarise_file, Files, Results, 4),
1041
+ findall(S, member(ok(S), Results), Summaries).
1042
+ ```
1043
+
1044
+ ### `wait(+Pid[, -Outcome])`
1045
+
1046
+ Block until a process terminates, then report what happened. `Outcome` is
1047
+ `ok(Result)`, `failed(Error)`, or `killed(Reason)`; `Result` is the process's
1048
+ terminal text as a Prolog string. `wait/1` is semidet — it succeeds only on a
1049
+ successful finish.
1050
+
1051
+ ```prolog
1052
+ spawn(research("qec", Me), Pid),
1053
+ wait(Pid, ok(Text)), % Text is the subagent's terminal text
1054
+ ```
1055
+
1056
+ For a structured result, prefer the message form: have the child
1057
+ `send(Parent, result(Term))` and `receive` it in the parent.
1058
+
1059
+ ### `exit/1,2` and stopping
1060
+
1061
+ `exit(Reason)` terminates the current process. `exit(Pid, Reason)` terminates
1062
+ another (a registered name or a process label works too); there is no `stop/1`
1063
+ predicate, so `exit/2` is how DML cancels a peer. (The `/stop` command and the
1064
+ `stop` tool are the interactive equivalents.) `process_info(Pid, Info)` returns a
1065
+ dict (`pid`, `name`, `state`, `priority`, `mailbox`, `usage`, …). `sleep(Ms)`
1066
+ yields; `now_ms(T)` and `make_ref(Ref)` are available.
1067
+
1068
+ ### Rules
1069
+
1070
+ - **Isolation.** A process runs against a private module snapshot: its
1071
+ `assert`/`retract` do not affect the harness, and it does not see commits made
1072
+ after it started.
1073
+ - **Value semantics.** `spawn` and `send` copy terms; never rely on a shared
1074
+ variable with a child.
1075
+ - **Determinism.** A process that calls `task/N`, `exec/2`, `read_media/2`, or
1076
+ writes a file should yield one solution (`once/1`, `->`, `!`).
1077
+ - **Bound the work.** Every `swarm` item is a process and usually a model call;
1078
+ the session has a **live-process limit** (default 16 concurrent — finished
1079
+ processes do not count, so a long session can fan out again) and the provider
1080
+ has rate limits. A `swarm` item whose process cannot be spawned is reported as
1081
+ `failed(...)`; `pmap` then fails.
1082
+ - **CPU-bound Prolog** blocks the in-process host (L1). Long compute belongs in
1083
+ the REPL's `--execution worker` (L2), not a `swarm` on the default host.
1084
+
1085
+ ---
1086
+
888
1087
  ## Control Flow
889
1088
 
890
1089
  DML supports standard Prolog control flow:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepclause-sdk",
3
- "version": "0.1.3",
3
+ "version": "0.2.2",
4
4
  "description": "Neuro-symbolic runtime: a self-modifying DML/Prolog REPL, one-shot runner, and TypeScript SDK",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,9 +35,10 @@
35
35
  "clean": "rm -rf dist",
36
36
  "prepublishOnly": "npm run build",
37
37
  "example": "tsx sdk-examples/quick-start.ts",
38
- "examples": "tsx sdk-examples/basic-usage.ts",
39
- "examples:smoke": "tsx sdk-examples/no-api-key.ts && npm run example && npm run examples && tsx sdk-examples/neurosymbolic.ts 1 && tsx sdk-examples/deep-research.ts \"test topic\"",
38
+ "examples": "tsx sdk-examples/neurosymbolic.ts",
39
+ "examples:smoke": "npm run example && tsx sdk-examples/tools.ts && npm run examples && tsx sdk-examples/judgments.ts && tsx sdk-examples/subagents.ts && tsx sdk-examples/debate.ts",
40
40
  "cli": "tsx src/cli/index.ts",
41
+ "repl": "tsx src/cli/index.ts repl",
41
42
  "sync:agent-prompt": "node scripts/sync-agent-prompt.mjs",
42
43
  "check:agent-prompt": "node scripts/sync-agent-prompt.mjs --check"
43
44
  },
@@ -45,13 +46,13 @@
45
46
  "@ai-sdk/anthropic": "^3.0.35",
46
47
  "@ai-sdk/google": "^3.0.60",
47
48
  "@ai-sdk/openai": "^3.0.25",
48
- "@resvg/resvg-js": "^2.6.2",
49
49
  "@earendil-works/pi-ai": "^1.0.4",
50
50
  "@earendil-works/pi-coding-agent": "^1.0.4",
51
51
  "@earendil-works/pi-tui": "^1.0.4",
52
+ "@resvg/resvg-js": "^2.6.2",
52
53
  "ai": "^6.0.116",
53
54
  "commander": "^12.1.0",
54
- "deepclause-agentvm": "^0.0.5",
55
+ "deepclause-agentvm": "^0.4.0",
55
56
  "fflate": "^0.8.3",
56
57
  "zod": "^3.23.0"
57
58
  },
@@ -67,7 +68,7 @@
67
68
  "vitest": "^4.1.10"
68
69
  },
69
70
  "engines": {
70
- "node": ">=18.0.0"
71
+ "node": ">=20.0.0"
71
72
  },
72
73
  "files": [
73
74
  "dist",