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.
- package/README.md +45 -6
- package/dist/cli/config.d.ts +2 -2
- package/dist/cli/repl.d.ts +36 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +414 -14
- package/dist/cli/repl.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +547 -2
- package/dist/repl/backend.d.ts +9 -0
- package/dist/repl/backend.d.ts.map +1 -1
- package/dist/repl/backend.js +44 -0
- package/dist/repl/backend.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +36 -0
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +173 -0
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +29 -0
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +71 -0
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +6 -0
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -1
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/picker.d.ts +3 -0
- package/dist/repl/picker.d.ts.map +1 -1
- package/dist/repl/picker.js +132 -0
- package/dist/repl/picker.js.map +1 -1
- package/dist/repl/process-backend.d.ts +31 -0
- package/dist/repl/process-backend.d.ts.map +1 -0
- package/dist/repl/process-backend.js +10 -0
- package/dist/repl/process-backend.js.map +1 -0
- package/dist/repl/process-executor.d.ts +67 -0
- package/dist/repl/process-executor.d.ts.map +1 -0
- package/dist/repl/process-executor.js +371 -0
- package/dist/repl/process-executor.js.map +1 -0
- package/dist/repl/process-worker.d.ts +10 -0
- package/dist/repl/process-worker.d.ts.map +1 -0
- package/dist/repl/process-worker.js +210 -0
- package/dist/repl/process-worker.js.map +1 -0
- package/dist/repl/processes.d.ts +282 -0
- package/dist/repl/processes.d.ts.map +1 -0
- package/dist/repl/processes.js +557 -0
- package/dist/repl/processes.js.map +1 -0
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +19 -5
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +142 -3
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +435 -4
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tui/autocomplete.d.ts.map +1 -1
- package/dist/repl/tui/autocomplete.js +7 -0
- package/dist/repl/tui/autocomplete.js.map +1 -1
- package/dist/repl/tui/harness-view.d.ts +102 -0
- package/dist/repl/tui/harness-view.d.ts.map +1 -0
- package/dist/repl/tui/harness-view.js +323 -0
- package/dist/repl/tui/harness-view.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -1
- package/dist/repl/tui/renderer.js +6 -1
- package/dist/repl/tui/renderer.js.map +1 -1
- package/dist/repl/tui/services-panel.d.ts +35 -0
- package/dist/repl/tui/services-panel.d.ts.map +1 -0
- package/dist/repl/tui/services-panel.js +108 -0
- package/dist/repl/tui/services-panel.js.map +1 -0
- package/dist/repl/tui/status-bar.d.ts +2 -0
- package/dist/repl/tui/status-bar.d.ts.map +1 -1
- package/dist/repl/tui/status-bar.js +2 -0
- package/dist/repl/tui/status-bar.js.map +1 -1
- package/dist/repl/tui/tool-pane.d.ts.map +1 -1
- package/dist/repl/tui/tool-pane.js +6 -2
- package/dist/repl/tui/tool-pane.js.map +1 -1
- package/dist/repl/worker-host.d.ts +80 -0
- package/dist/repl/worker-host.d.ts.map +1 -0
- package/dist/repl/worker-host.js +305 -0
- package/dist/repl/worker-host.js.map +1 -0
- package/dist/repl/worker-protocol.d.ts +123 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -0
- package/dist/repl/worker-protocol.js +9 -0
- package/dist/repl/worker-protocol.js.map +1 -0
- package/dist/runner.d.ts +24 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +58 -0
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +247 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +199 -0
- package/package.json +7 -6
- 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.
|
|
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/
|
|
39
|
-
"examples:smoke": "tsx sdk-examples/
|
|
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
|
|
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": ">=
|
|
71
|
+
"node": ">=20.0.0"
|
|
71
72
|
},
|
|
72
73
|
"files": [
|
|
73
74
|
"dist",
|