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.
- package/README.md +86 -3
- package/dist/agent.d.ts +17 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +148 -42
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +12 -12
- package/dist/cli/repl.d.ts +13 -1
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +177 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +3 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +119 -0
- package/dist/forms/registry.d.ts.map +1 -0
- package/dist/forms/registry.js +256 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/spec.d.ts +193 -0
- package/dist/forms/spec.d.ts.map +1 -0
- package/dist/forms/spec.js +295 -0
- package/dist/forms/spec.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog/loader.d.ts +18 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +26 -1
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +786 -40
- package/dist/repl/agent.d.ts +11 -0
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +23 -9
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/form-widget.d.ts +56 -0
- package/dist/repl/form-widget.d.ts.map +1 -0
- package/dist/repl/form-widget.js +289 -0
- package/dist/repl/form-widget.js.map +1 -0
- package/dist/repl/harness-tools.d.ts +4 -1
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +15 -58
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +24 -1
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +76 -8
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +20 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/process-backend.d.ts +2 -2
- package/dist/repl/process-backend.d.ts.map +1 -1
- package/dist/repl/process-executor.d.ts +11 -2
- package/dist/repl/process-executor.d.ts.map +1 -1
- package/dist/repl/process-executor.js +67 -18
- package/dist/repl/process-executor.js.map +1 -1
- package/dist/repl/process-worker.d.ts +35 -4
- package/dist/repl/process-worker.d.ts.map +1 -1
- package/dist/repl/process-worker.js +78 -38
- package/dist/repl/process-worker.js.map +1 -1
- package/dist/repl/processes.d.ts +4 -0
- package/dist/repl/processes.d.ts.map +1 -1
- package/dist/repl/processes.js +20 -0
- package/dist/repl/processes.js.map +1 -1
- package/dist/repl/render.d.ts +17 -0
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +60 -0
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +143 -7
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +317 -20
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tui/form-view.d.ts +17 -0
- package/dist/repl/tui/form-view.d.ts.map +1 -0
- package/dist/repl/tui/form-view.js +25 -0
- package/dist/repl/tui/form-view.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts +4 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -1
- package/dist/repl/tui/renderer.js +12 -0
- package/dist/repl/tui/renderer.js.map +1 -1
- package/dist/repl/worker-host.d.ts +16 -2
- package/dist/repl/worker-host.d.ts.map +1 -1
- package/dist/repl/worker-host.js +38 -6
- package/dist/repl/worker-host.js.map +1 -1
- package/dist/repl/worker-protocol.d.ts +14 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -1
- package/dist/repl/worker-protocol.js +8 -1
- package/dist/repl/worker-protocol.js.map +1 -1
- package/dist/runner.d.ts +69 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +381 -44
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +12 -0
- package/dist/sdk.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +452 -18
- package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +8 -4
- package/dist/system/assets/docs/DML_FORMS.md +740 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +239 -4
- package/dist/system/assets/docs/HARNESS_EXAMPLES.md +94 -0
- package/dist/system/assets/index.d.ts +2 -2
- package/dist/system/assets/index.d.ts.map +1 -1
- package/dist/system/assets/index.js +4 -0
- package/dist/system/assets/index.js.map +1 -1
- package/dist/system/environment.d.ts +6 -0
- package/dist/system/environment.d.ts.map +1 -1
- package/dist/system/environment.js +5 -4
- package/dist/system/environment.js.map +1 -1
- package/dist/types.d.ts +48 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- 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.
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
134
|
-
linked
|
|
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
|
|
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
|
|
246
|
-
directories
|
|
247
|
-
scripts instead of building them
|
|
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
|
|
316
|
-
|
|
317
|
-
|
|
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
|
|
1772
|
-
(`foreground`/`interactive`/`background`), `timeout` (ms)
|
|
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
|
|
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
|
-
-
|
|
245
|
-
|
|
246
|
-
|
|
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`
|
|
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
|