deepclause-sdk 0.2.2 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/README.md +86 -3
  2. package/dist/agent.d.ts +17 -0
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +148 -42
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +12 -12
  7. package/dist/cli/repl.d.ts +13 -1
  8. package/dist/cli/repl.d.ts.map +1 -1
  9. package/dist/cli/repl.js +177 -5
  10. package/dist/cli/repl.js.map +1 -1
  11. package/dist/cli/run.d.ts.map +1 -1
  12. package/dist/cli/run.js +4 -0
  13. package/dist/cli/run.js.map +1 -1
  14. package/dist/forms/index.d.ts +3 -0
  15. package/dist/forms/index.d.ts.map +1 -0
  16. package/dist/forms/index.js +3 -0
  17. package/dist/forms/index.js.map +1 -0
  18. package/dist/forms/registry.d.ts +119 -0
  19. package/dist/forms/registry.d.ts.map +1 -0
  20. package/dist/forms/registry.js +256 -0
  21. package/dist/forms/registry.js.map +1 -0
  22. package/dist/forms/spec.d.ts +193 -0
  23. package/dist/forms/spec.d.ts.map +1 -0
  24. package/dist/forms/spec.js +295 -0
  25. package/dist/forms/spec.js.map +1 -0
  26. package/dist/index.d.ts +7 -2
  27. package/dist/index.d.ts.map +1 -1
  28. package/dist/index.js +7 -0
  29. package/dist/index.js.map +1 -1
  30. package/dist/prolog/loader.d.ts +18 -0
  31. package/dist/prolog/loader.d.ts.map +1 -1
  32. package/dist/prolog/loader.js +26 -1
  33. package/dist/prolog/loader.js.map +1 -1
  34. package/dist/prolog-src/deepclause_mi.pl +786 -40
  35. package/dist/repl/agent.d.ts +11 -0
  36. package/dist/repl/agent.d.ts.map +1 -1
  37. package/dist/repl/agent.js +23 -9
  38. package/dist/repl/agent.js.map +1 -1
  39. package/dist/repl/form-widget.d.ts +56 -0
  40. package/dist/repl/form-widget.d.ts.map +1 -0
  41. package/dist/repl/form-widget.js +289 -0
  42. package/dist/repl/form-widget.js.map +1 -0
  43. package/dist/repl/harness-tools.d.ts +4 -1
  44. package/dist/repl/harness-tools.d.ts.map +1 -1
  45. package/dist/repl/harness-tools.js +15 -58
  46. package/dist/repl/harness-tools.js.map +1 -1
  47. package/dist/repl/history.d.ts +24 -1
  48. package/dist/repl/history.d.ts.map +1 -1
  49. package/dist/repl/history.js +76 -8
  50. package/dist/repl/history.js.map +1 -1
  51. package/dist/repl/index.d.ts +20 -2
  52. package/dist/repl/index.d.ts.map +1 -1
  53. package/dist/repl/index.js +9 -0
  54. package/dist/repl/index.js.map +1 -1
  55. package/dist/repl/process-backend.d.ts +2 -2
  56. package/dist/repl/process-backend.d.ts.map +1 -1
  57. package/dist/repl/process-executor.d.ts +11 -2
  58. package/dist/repl/process-executor.d.ts.map +1 -1
  59. package/dist/repl/process-executor.js +67 -18
  60. package/dist/repl/process-executor.js.map +1 -1
  61. package/dist/repl/process-worker.d.ts +35 -4
  62. package/dist/repl/process-worker.d.ts.map +1 -1
  63. package/dist/repl/process-worker.js +78 -38
  64. package/dist/repl/process-worker.js.map +1 -1
  65. package/dist/repl/processes.d.ts +4 -0
  66. package/dist/repl/processes.d.ts.map +1 -1
  67. package/dist/repl/processes.js +20 -0
  68. package/dist/repl/processes.js.map +1 -1
  69. package/dist/repl/render.d.ts +17 -0
  70. package/dist/repl/render.d.ts.map +1 -1
  71. package/dist/repl/render.js +60 -0
  72. package/dist/repl/render.js.map +1 -1
  73. package/dist/repl/session.d.ts +143 -7
  74. package/dist/repl/session.d.ts.map +1 -1
  75. package/dist/repl/session.js +317 -20
  76. package/dist/repl/session.js.map +1 -1
  77. package/dist/repl/tui/form-view.d.ts +17 -0
  78. package/dist/repl/tui/form-view.d.ts.map +1 -0
  79. package/dist/repl/tui/form-view.js +25 -0
  80. package/dist/repl/tui/form-view.js.map +1 -0
  81. package/dist/repl/tui/renderer.d.ts +4 -0
  82. package/dist/repl/tui/renderer.d.ts.map +1 -1
  83. package/dist/repl/tui/renderer.js +12 -0
  84. package/dist/repl/tui/renderer.js.map +1 -1
  85. package/dist/repl/worker-host.d.ts +16 -2
  86. package/dist/repl/worker-host.d.ts.map +1 -1
  87. package/dist/repl/worker-host.js +38 -6
  88. package/dist/repl/worker-host.js.map +1 -1
  89. package/dist/repl/worker-protocol.d.ts +14 -0
  90. package/dist/repl/worker-protocol.d.ts.map +1 -1
  91. package/dist/repl/worker-protocol.js +8 -1
  92. package/dist/repl/worker-protocol.js.map +1 -1
  93. package/dist/runner.d.ts +69 -1
  94. package/dist/runner.d.ts.map +1 -1
  95. package/dist/runner.js +381 -44
  96. package/dist/runner.js.map +1 -1
  97. package/dist/sdk.d.ts.map +1 -1
  98. package/dist/sdk.js +12 -0
  99. package/dist/sdk.js.map +1 -1
  100. package/dist/system/assets/docs/AGENT_PROMPT.md +452 -18
  101. package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
  102. package/dist/system/assets/docs/DML_AUTHORING.md +8 -4
  103. package/dist/system/assets/docs/DML_FORMS.md +740 -0
  104. package/dist/system/assets/docs/DML_REFERENCE.md +239 -4
  105. package/dist/system/assets/docs/HARNESS_EXAMPLES.md +94 -0
  106. package/dist/system/assets/index.d.ts +2 -2
  107. package/dist/system/assets/index.d.ts.map +1 -1
  108. package/dist/system/assets/index.js +4 -0
  109. package/dist/system/assets/index.js.map +1 -1
  110. package/dist/system/environment.d.ts +6 -0
  111. package/dist/system/environment.d.ts.map +1 -1
  112. package/dist/system/environment.js +5 -4
  113. package/dist/system/environment.js.map +1 -1
  114. package/dist/types.d.ts +48 -1
  115. package/dist/types.d.ts.map +1 -1
  116. package/package.json +4 -2
  117. package/src/prolog-src/deepclause_mi.pl +786 -40
package/dist/cli/repl.js CHANGED
@@ -31,12 +31,15 @@ import { StatusBarComponent } from '../repl/tui/status-bar.js';
31
31
  import { Container, HStack, ScrollView, Spacer, TuiAltScreen, VStack, matchesKey, } from '@earendil-works/pi-tui';
32
32
  import { editorTheme, formatInputAnswer } from '../repl/tui/theme.js';
33
33
  import { PromptEditor } from '../repl/tui/editor.js';
34
+ import { FormView } from '../repl/tui/form-view.js';
35
+ import { applyFormKey, initialState, rows } from '../repl/form-widget.js';
34
36
  import { loadProjectContext } from '../repl/project-context.js';
35
37
  import { createPiBackend, THINKING_LEVELS } from '../repl/backend.js';
36
38
  import { createReplToolset } from '../repl/toolset.js';
37
39
  import { takeStartupPrologMessages } from '../prolog/loader.js';
38
40
  import { createPiImageBackend } from '../repl/imagegen.js';
39
41
  import { EnvModelResolver, PiModelResolver } from '../repl/model-resolver.js';
42
+ import { pickForm } from '../repl/form-widget.js';
40
43
  import { parseModelId, resolveModelSlotConfig } from '../system/config/model-slots.js';
41
44
  import { loadReplConfig, resolveReplCompactionConfig, updateConfig } from './config.js';
42
45
  import { renderDiagramImage, resolveMedia, } from '../visualize/index.js';
@@ -232,6 +235,10 @@ export function classifyReplInput(line) {
232
235
  export async function runGoalWithRenderer(repl, goal, options) {
233
236
  let hadError = false;
234
237
  let printedAny = false;
238
+ let answered = false;
239
+ // A turn that suspended *succeeded* — it stopped at a safe point and the host
240
+ // recorded where to continue. Reporting `false.` would read as a failure.
241
+ let suspended = false;
235
242
  for await (const event of repl.query(goal, { signal: options.signal })) {
236
243
  if (event.type === 'solution') {
237
244
  options.renderer.solution(event.solution?.bindings ?? []);
@@ -243,6 +250,14 @@ export async function runGoalWithRenderer(repl, goal, options) {
243
250
  hadError = true;
244
251
  options.renderer.render(event);
245
252
  }
253
+ else if (event.type === 'answer') {
254
+ answered = true;
255
+ options.renderer.render(event);
256
+ }
257
+ else if (event.type === 'suspend') {
258
+ suspended = true;
259
+ options.renderer.render(event);
260
+ }
246
261
  else if (event.type === 'finished') {
247
262
  // Nothing to print for finished.
248
263
  }
@@ -250,11 +265,63 @@ export async function runGoalWithRenderer(repl, goal, options) {
250
265
  options.renderer.render(event);
251
266
  }
252
267
  }
253
- if (!printedAny && !hadError) {
254
- options.renderer.writeLine('false.');
268
+ if (!printedAny && !hadError && !suspended) {
269
+ // A goal that committed with `answer/1` succeeded; one that produced neither
270
+ // a solution nor an answer simply failed. (`goalTerminator`/`noSolution`
271
+ // keep `--json` stdout pure, where a raw `false.` would corrupt the stream.)
272
+ if (answered)
273
+ options.renderer.goalTerminator();
274
+ else
275
+ options.renderer.noSolution();
255
276
  }
256
277
  return hadError ? 1 : 0;
257
278
  }
279
+ /**
280
+ * The REPL's form interaction (v0): a field-by-field wizard over the existing
281
+ * input channel. The pi-tui form widget is the next step (docs/DML_FORMS.md §9).
282
+ */
283
+ async function presentFormWidget(repl, form) {
284
+ const spec = form.spec;
285
+ if (!spec)
286
+ return;
287
+ const change = await pickForm({ spec, values: form.values, issues: form.issues });
288
+ repl.session.submitForm(form.formId, change
289
+ ? { action: change.action, values: change.values }
290
+ : { action: 'cancel', values: form.values ?? {} });
291
+ }
292
+ /**
293
+ * The TUI/non-TTY form interaction: a field-by-field wizard over the existing
294
+ * input channel. The plain-TTY path uses the widget instead.
295
+ */
296
+ async function presentForm(repl, form) {
297
+ const spec = form.spec;
298
+ if (!spec)
299
+ return;
300
+ const values = { ...(form.values ?? {}) };
301
+ const fields = (spec.fields ?? []).filter((field) => field.type !== 'object' && field.widget !== 'hidden');
302
+ for (const field of fields) {
303
+ const label = field.title ?? field.name;
304
+ const current = values[field.name];
305
+ const hint = current === undefined || current === '' ? '' : ` (${String(current)})`;
306
+ const answer = await repl.session.requestHumanInput(`${label}${field.required ? ' *' : ''}${hint}`);
307
+ if (answer === undefined) {
308
+ repl.session.submitForm(form.formId, { action: 'cancel', values });
309
+ return;
310
+ }
311
+ if (answer.trim() !== '')
312
+ values[field.name] = coerceFieldValue(field.type, answer.trim());
313
+ }
314
+ repl.session.submitForm(form.formId, { action: 'submit', values });
315
+ }
316
+ function coerceFieldValue(type, raw) {
317
+ if (type === 'number' || type === 'integer') {
318
+ const n = Number(raw);
319
+ return Number.isFinite(n) ? n : raw;
320
+ }
321
+ if (type === 'boolean')
322
+ return /^(y|yes|true|1)$/i.test(raw);
323
+ return raw;
324
+ }
258
325
  function selectRenderMode(options) {
259
326
  if (options.json)
260
327
  return 'json';
@@ -262,6 +329,18 @@ function selectRenderMode(options) {
262
329
  return 'plain';
263
330
  return process.stdout.isTTY ? 'rich' : 'plain';
264
331
  }
332
+ /**
333
+ * Should a form re-presented after a rejected submit be cancelled instead?
334
+ *
335
+ * A scripted run (`--answer`) supplies the same canned reply to every prompt. The
336
+ * host re-shows a refused submit so a *person* can correct it — but re-feeding the
337
+ * canned answer would refuse, re-show, refuse … forever. So in that mode the
338
+ * second presentation cancels and the run finishes. A model's own `update_form`
339
+ * carries no issues and is unaffected.
340
+ */
341
+ export function cancelsScriptedRetry(options, form) {
342
+ return options.answer !== undefined && form.op === 'update' && (form.issues?.length ?? 0) > 0;
343
+ }
265
344
  /**
266
345
  * Build the unified input channel from CLI flags (proposal §13.6):
267
346
  * - `--answer` supplies a canned reply; `--no-ask` fails immediately;
@@ -270,7 +349,20 @@ function selectRenderMode(options) {
270
349
  */
271
350
  export function buildCliOnInput(options) {
272
351
  if (options.answer !== undefined) {
273
- return async () => options.answer;
352
+ // A promptless request is `input/1` — the person's next chat message. A
353
+ // scripted run has exactly one conversation message; replaying it would make
354
+ // a chat loop turn forever, so after it there is nobody to talk to and the
355
+ // conversation ends. Prompted requests (a form field, a question) are not
356
+ // limited: the same canned reply answers each one.
357
+ let usedForMessage = false;
358
+ return async (prompt) => {
359
+ if (prompt === '') {
360
+ if (usedForMessage)
361
+ return undefined;
362
+ usedForMessage = true;
363
+ }
364
+ return options.answer;
365
+ };
274
366
  }
275
367
  if (options.noAsk) {
276
368
  return async () => undefined;
@@ -379,7 +471,10 @@ export async function createReplRuntime(options) {
379
471
  llmBackend = pi.backend;
380
472
  harnessModel = pi.modelId.split('/').slice(1).join('/') || pi.modelId;
381
473
  harnessProvider = pi.provider ?? harnessProvider;
382
- contextWindow = pi.contextWindow;
474
+ contextWindow = pi.contextWindow ?? resolveModelSlotConfig(config, 'run', {
475
+ modelId: options.model ?? config.models.run,
476
+ temperature: options.temperature ?? config.temperatures.run,
477
+ }).contextWindow;
383
478
  reasoning = pi.reasoning;
384
479
  thinkingLevel = pi.thinkingLevel;
385
480
  modelResolver = new PiModelResolver(pi.registry);
@@ -499,6 +594,15 @@ export async function createReplRuntime(options) {
499
594
  judgeSelection,
500
595
  imageBackend,
501
596
  modelResolver,
597
+ onForm: async (form) => {
598
+ if (cancelsScriptedRetry(options, form)) {
599
+ repl.session.submitForm(form.formId, { action: 'cancel', values: form.values ?? {} });
600
+ return;
601
+ }
602
+ return options.plain && process.stdin.isTTY && process.stdout.isTTY
603
+ ? presentFormWidget(repl, form)
604
+ : presentForm(repl, form);
605
+ },
502
606
  history: historyOptions,
503
607
  harness: harnessName,
504
608
  execution: options.execution,
@@ -553,6 +657,7 @@ export async function createReplRuntime(options) {
553
657
  historyEnabled: Boolean(historyOptions),
554
658
  inputHistory,
555
659
  statusSnapshot: () => statusTracker.snapshot(),
660
+ observeEvent: (event) => statusTracker.observe(event),
556
661
  tuiMode: options.tui ?? config.repl.tui ?? 'auto',
557
662
  setOnInput: (handler) => repl.setOnInput(handler),
558
663
  missingCredential,
@@ -637,6 +742,28 @@ async function printBanner(runtime, loaded) {
637
742
  * starting blank. Bounded, and skipped when the session was not resumed or
638
743
  * tracing was off.
639
744
  */
745
+ /**
746
+ * Continue a session that was suspended with `suspend/1`.
747
+ *
748
+ * The turn stopped at a safe point and the host wrote the goal that resumes it
749
+ * (docs/DML_REPL_SUSPEND_RESUME.md). Resuming means running that goal once the
750
+ * harness and memory are restored — so `--session` picks the program up where it
751
+ * left off instead of dropping the user into a half-finished state. The record is
752
+ * cleared first: if the continuation suspends again it writes a fresh one.
753
+ */
754
+ async function resumeSuspendedContinuation(runtime) {
755
+ const pending = runtime.repl.session.pendingSuspension?.();
756
+ if (!pending)
757
+ return;
758
+ runtime.renderer.writeLine(`resuming the suspended continuation: ${pending.continuation}`);
759
+ await runtime.repl.session.clearSuspension?.();
760
+ try {
761
+ await runGoalWithRenderer(runtime.repl, pending.continuation, { renderer: runtime.renderer });
762
+ }
763
+ catch (error) {
764
+ runtime.renderer.writeLine(`could not resume ${pending.continuation}: ${String(error.message)}`);
765
+ }
766
+ }
640
767
  async function renderResumedSession(runtime) {
641
768
  const events = await runtime.repl.resumedTranscript().catch(() => []);
642
769
  if (events.length === 0)
@@ -1108,7 +1235,10 @@ export async function runInteractiveGoal(repl, renderer, goal, options, ask, sig
1108
1235
  finally {
1109
1236
  await iterator.return?.(undefined);
1110
1237
  }
1111
- renderer.writeLine(answered ? '.' : 'false.');
1238
+ if (answered)
1239
+ renderer.goalTerminator();
1240
+ else
1241
+ renderer.noSolution();
1112
1242
  }
1113
1243
  /**
1114
1244
  * Interactive TUI for the REPL: a scrolling transcript, a fixed editor, and a
@@ -1141,6 +1271,9 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1141
1271
  const working = new WorkingLine(tui);
1142
1272
  const renderer = new TuiRenderer({
1143
1273
  document,
1274
+ // The TUI draws through its own switch, but the status tracker must still see
1275
+ // every event (usage, model_call, …) or the footer loses its counters.
1276
+ onEvent: (event) => runtime.observeEvent(event),
1144
1277
  onWorking: (label) => {
1145
1278
  working.set(label);
1146
1279
  tui.requestRender();
@@ -1661,9 +1794,13 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1661
1794
  // the first and last line of output are not jammed against the chrome. Fixed
1662
1795
  // spacers rather than padding inside the scroll view: they do not scroll away and
1663
1796
  // do not consume scrollback.
1797
+ // The form panel: shown while a form is open, and the TUI edits its fields
1798
+ // through the editor (one prompt each), updating this panel as it goes.
1799
+ const formView = new FormView();
1664
1800
  const dock = new VStack([
1665
1801
  { component: working, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
1666
1802
  { component: servicesPanel, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
1803
+ { component: formView, basis: 'auto', grow: 0, shrink: 1, minSize: 0 },
1667
1804
  { component: new Spacer(1), basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1668
1805
  { component: editorContainer, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
1669
1806
  { component: statusBar, basis: 'auto', grow: 0, shrink: 1, minSize: 1 },
@@ -1690,10 +1827,44 @@ export async function runTui(runtime, options, onExit, loaded = []) {
1690
1827
  ]));
1691
1828
  };
1692
1829
  applyLayout();
1830
+ // A form opened by the running turn is shown in the dock and edited field by
1831
+ // field through the editor; the plain-TTY path uses the raw widget instead.
1832
+ runtime.repl.session.setFormHandler(async (form) => {
1833
+ const spec = form.spec;
1834
+ if (!spec)
1835
+ return;
1836
+ let state = initialState(spec, form.values ?? {}, form.issues ?? []);
1837
+ formView.setState(state);
1838
+ tui.requestRender();
1839
+ const list = rows(spec);
1840
+ try {
1841
+ for (let index = 0; index < list.length; index += 1) {
1842
+ const row = list[index];
1843
+ if (row.kind !== 'field')
1844
+ continue;
1845
+ const answer = await runtime.repl.session.requestHumanInput(`${row.title}${row.required ? ' *' : ''}`);
1846
+ if (answer === undefined) {
1847
+ runtime.repl.session.submitForm(form.formId, { action: 'cancel', values: state.values });
1848
+ return;
1849
+ }
1850
+ if (answer !== '') {
1851
+ state = applyFormKey({ ...state, focus: index, editing: true, buffer: answer }, 'enter').state;
1852
+ formView.setState(state);
1853
+ tui.requestRender();
1854
+ }
1855
+ }
1856
+ runtime.repl.session.submitForm(form.formId, { action: 'submit', values: state.values });
1857
+ }
1858
+ finally {
1859
+ formView.setState(undefined);
1860
+ tui.requestRender();
1861
+ }
1862
+ });
1693
1863
  tui.start();
1694
1864
  await printBanner(runtime, loaded);
1695
1865
  await renderResumedSession(runtime);
1696
1866
  tui.requestRender();
1867
+ await resumeSuspendedContinuation(runtime);
1697
1868
  try {
1698
1869
  while (true) {
1699
1870
  // A slash command typed while a turn/command was running: run it now that
@@ -1883,6 +2054,7 @@ export async function startRepl(options) {
1883
2054
  }
1884
2055
  await printBanner(runtime, options.load ?? []);
1885
2056
  await renderResumedSession(runtime);
2057
+ await resumeSuspendedContinuation(runtime);
1886
2058
  return await runInteractive(runtime, options, saveHarnessOnExit);
1887
2059
  }
1888
2060
  finally {