deepclause-sdk 0.0.89 → 0.1.1

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 (329) hide show
  1. package/README.md +481 -941
  2. package/dist/agent.d.ts +19 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +75 -21
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +385 -178
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +161 -188
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.d.ts +3 -2
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +85 -411
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/login.d.ts +29 -0
  15. package/dist/cli/login.d.ts.map +1 -0
  16. package/dist/cli/login.js +184 -0
  17. package/dist/cli/login.js.map +1 -0
  18. package/dist/cli/readline.d.ts +6 -0
  19. package/dist/cli/readline.d.ts.map +1 -0
  20. package/dist/cli/readline.js +15 -0
  21. package/dist/cli/readline.js.map +1 -0
  22. package/dist/cli/repl-bin.d.ts +9 -0
  23. package/dist/cli/repl-bin.d.ts.map +1 -0
  24. package/dist/cli/repl-bin.js +16 -0
  25. package/dist/cli/repl-bin.js.map +1 -0
  26. package/dist/cli/repl.d.ts +216 -0
  27. package/dist/cli/repl.d.ts.map +1 -0
  28. package/dist/cli/repl.js +1392 -0
  29. package/dist/cli/repl.js.map +1 -0
  30. package/dist/cli/run.d.ts +17 -18
  31. package/dist/cli/run.d.ts.map +1 -1
  32. package/dist/cli/run.js +202 -183
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/cli/set-backend.d.ts +53 -0
  35. package/dist/cli/set-backend.d.ts.map +1 -0
  36. package/dist/cli/set-backend.js +259 -0
  37. package/dist/cli/set-backend.js.map +1 -0
  38. package/dist/cli/visualize.d.ts +42 -0
  39. package/dist/cli/visualize.d.ts.map +1 -0
  40. package/dist/cli/visualize.js +150 -0
  41. package/dist/cli/visualize.js.map +1 -0
  42. package/dist/compiler.d.ts +5 -52
  43. package/dist/compiler.d.ts.map +1 -1
  44. package/dist/compiler.js +4 -768
  45. package/dist/compiler.js.map +1 -1
  46. package/dist/index.d.ts +27 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +18 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/prolog/bridge.d.ts +5 -1
  51. package/dist/prolog/bridge.d.ts.map +1 -1
  52. package/dist/prolog/bridge.js +10 -1
  53. package/dist/prolog/bridge.js.map +1 -1
  54. package/dist/prolog-src/deepclause_analysis.pl +10 -2
  55. package/dist/prolog-src/deepclause_mi.pl +2440 -49
  56. package/dist/repl/agent.d.ts +90 -0
  57. package/dist/repl/agent.d.ts.map +1 -0
  58. package/dist/repl/agent.js +410 -0
  59. package/dist/repl/agent.js.map +1 -0
  60. package/dist/repl/auth.d.ts +45 -0
  61. package/dist/repl/auth.d.ts.map +1 -0
  62. package/dist/repl/auth.js +116 -0
  63. package/dist/repl/auth.js.map +1 -0
  64. package/dist/repl/backend.d.ts +321 -0
  65. package/dist/repl/backend.d.ts.map +1 -0
  66. package/dist/repl/backend.js +415 -0
  67. package/dist/repl/backend.js.map +1 -0
  68. package/dist/repl/compaction.d.ts +122 -0
  69. package/dist/repl/compaction.d.ts.map +1 -0
  70. package/dist/repl/compaction.js +318 -0
  71. package/dist/repl/compaction.js.map +1 -0
  72. package/dist/repl/export.d.ts +30 -0
  73. package/dist/repl/export.d.ts.map +1 -0
  74. package/dist/repl/export.js +23 -0
  75. package/dist/repl/export.js.map +1 -0
  76. package/dist/repl/frames.d.ts +21 -0
  77. package/dist/repl/frames.d.ts.map +1 -0
  78. package/dist/repl/frames.js +79 -0
  79. package/dist/repl/frames.js.map +1 -0
  80. package/dist/repl/harness-tools.d.ts +99 -0
  81. package/dist/repl/harness-tools.d.ts.map +1 -0
  82. package/dist/repl/harness-tools.js +192 -0
  83. package/dist/repl/harness-tools.js.map +1 -0
  84. package/dist/repl/highlight.d.ts +30 -0
  85. package/dist/repl/highlight.d.ts.map +1 -0
  86. package/dist/repl/highlight.js +285 -0
  87. package/dist/repl/highlight.js.map +1 -0
  88. package/dist/repl/history.d.ts +234 -0
  89. package/dist/repl/history.d.ts.map +1 -0
  90. package/dist/repl/history.js +661 -0
  91. package/dist/repl/history.js.map +1 -0
  92. package/dist/repl/imagegen.d.ts +29 -0
  93. package/dist/repl/imagegen.d.ts.map +1 -0
  94. package/dist/repl/imagegen.js +84 -0
  95. package/dist/repl/imagegen.js.map +1 -0
  96. package/dist/repl/index.d.ts +130 -0
  97. package/dist/repl/index.d.ts.map +1 -0
  98. package/dist/repl/index.js +112 -0
  99. package/dist/repl/index.js.map +1 -0
  100. package/dist/repl/judge.d.ts +51 -0
  101. package/dist/repl/judge.d.ts.map +1 -0
  102. package/dist/repl/judge.js +58 -0
  103. package/dist/repl/judge.js.map +1 -0
  104. package/dist/repl/markdown.d.ts +25 -0
  105. package/dist/repl/markdown.d.ts.map +1 -0
  106. package/dist/repl/markdown.js +103 -0
  107. package/dist/repl/markdown.js.map +1 -0
  108. package/dist/repl/model-resolver.d.ts +59 -0
  109. package/dist/repl/model-resolver.d.ts.map +1 -0
  110. package/dist/repl/model-resolver.js +156 -0
  111. package/dist/repl/model-resolver.js.map +1 -0
  112. package/dist/repl/picker.d.ts +34 -0
  113. package/dist/repl/picker.d.ts.map +1 -0
  114. package/dist/repl/picker.js +81 -0
  115. package/dist/repl/picker.js.map +1 -0
  116. package/dist/repl/project-context.d.ts +16 -0
  117. package/dist/repl/project-context.d.ts.map +1 -0
  118. package/dist/repl/project-context.js +23 -0
  119. package/dist/repl/project-context.js.map +1 -0
  120. package/dist/repl/render.d.ts +187 -0
  121. package/dist/repl/render.d.ts.map +1 -0
  122. package/dist/repl/render.js +808 -0
  123. package/dist/repl/render.js.map +1 -0
  124. package/dist/repl/session.d.ts +424 -0
  125. package/dist/repl/session.d.ts.map +1 -0
  126. package/dist/repl/session.js +1473 -0
  127. package/dist/repl/session.js.map +1 -0
  128. package/dist/repl/status.d.ts +100 -0
  129. package/dist/repl/status.d.ts.map +1 -0
  130. package/dist/repl/status.js +218 -0
  131. package/dist/repl/status.js.map +1 -0
  132. package/dist/repl/tools.d.ts +53 -0
  133. package/dist/repl/tools.d.ts.map +1 -0
  134. package/dist/repl/tools.js +307 -0
  135. package/dist/repl/tools.js.map +1 -0
  136. package/dist/repl/toolset.d.ts +32 -0
  137. package/dist/repl/toolset.d.ts.map +1 -0
  138. package/dist/repl/toolset.js +82 -0
  139. package/dist/repl/toolset.js.map +1 -0
  140. package/dist/repl/tui/editor.d.ts +17 -0
  141. package/dist/repl/tui/editor.d.ts.map +1 -0
  142. package/dist/repl/tui/editor.js +39 -0
  143. package/dist/repl/tui/editor.js.map +1 -0
  144. package/dist/repl/tui/status-bar.d.ts +26 -0
  145. package/dist/repl/tui/status-bar.d.ts.map +1 -0
  146. package/dist/repl/tui/status-bar.js +47 -0
  147. package/dist/repl/tui/status-bar.js.map +1 -0
  148. package/dist/repl/tui/theme.d.ts +30 -0
  149. package/dist/repl/tui/theme.d.ts.map +1 -0
  150. package/dist/repl/tui/theme.js +81 -0
  151. package/dist/repl/tui/theme.js.map +1 -0
  152. package/dist/repl/tui/transcript.d.ts +41 -0
  153. package/dist/repl/tui/transcript.d.ts.map +1 -0
  154. package/dist/repl/tui/transcript.js +139 -0
  155. package/dist/repl/tui/transcript.js.map +1 -0
  156. package/dist/runner.d.ts +143 -13
  157. package/dist/runner.d.ts.map +1 -1
  158. package/dist/runner.js +532 -290
  159. package/dist/runner.js.map +1 -1
  160. package/dist/sdk.d.ts.map +1 -1
  161. package/dist/sdk.js +0 -32
  162. package/dist/sdk.js.map +1 -1
  163. package/dist/system/assets/docs/AGENT_PROMPT.md +1447 -0
  164. package/dist/system/assets/docs/DML_AUTHORING.md +195 -0
  165. package/dist/system/assets/docs/DML_REFERENCE.md +344 -72
  166. package/dist/system/assets/docs/HARNESS_EXAMPLES.md +172 -0
  167. package/dist/system/assets/docs/HARNESS_PATTERNS.md +562 -0
  168. package/dist/system/assets/docs/META_PROMPT.md +68 -0
  169. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +117 -0
  170. package/dist/system/assets/index.d.ts +9 -17
  171. package/dist/system/assets/index.d.ts.map +1 -1
  172. package/dist/system/assets/index.js +26 -85
  173. package/dist/system/assets/index.js.map +1 -1
  174. package/dist/system/runtime/agentvm-manager.d.ts +9 -2
  175. package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
  176. package/dist/system/runtime/agentvm-manager.js +70 -44
  177. package/dist/system/runtime/agentvm-manager.js.map +1 -1
  178. package/dist/system/runtime/media.d.ts +23 -0
  179. package/dist/system/runtime/media.d.ts.map +1 -0
  180. package/dist/system/runtime/media.js +55 -0
  181. package/dist/system/runtime/media.js.map +1 -0
  182. package/dist/system/runtime/shell-limits.d.ts +21 -0
  183. package/dist/system/runtime/shell-limits.d.ts.map +1 -0
  184. package/dist/system/runtime/shell-limits.js +39 -0
  185. package/dist/system/runtime/shell-limits.js.map +1 -0
  186. package/dist/system/runtime/shell-manager.d.ts +8 -3
  187. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  188. package/dist/system/runtime/shell-manager.js +56 -39
  189. package/dist/system/runtime/shell-manager.js.map +1 -1
  190. package/dist/system/runtime/shell-tool-events.js +5 -0
  191. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  192. package/dist/system/runtime/token-usage.d.ts +2 -0
  193. package/dist/system/runtime/token-usage.d.ts.map +1 -1
  194. package/dist/system/runtime/token-usage.js +7 -0
  195. package/dist/system/runtime/token-usage.js.map +1 -1
  196. package/dist/tools.d.ts +3 -21
  197. package/dist/tools.d.ts.map +1 -1
  198. package/dist/tools.js +14 -168
  199. package/dist/tools.js.map +1 -1
  200. package/dist/types.d.ts +136 -119
  201. package/dist/types.d.ts.map +1 -1
  202. package/dist/validate.d.ts +19 -0
  203. package/dist/validate.d.ts.map +1 -0
  204. package/dist/validate.js +338 -0
  205. package/dist/validate.js.map +1 -0
  206. package/dist/visualize/ascii.d.ts +55 -0
  207. package/dist/visualize/ascii.d.ts.map +1 -0
  208. package/dist/visualize/ascii.js +242 -0
  209. package/dist/visualize/ascii.js.map +1 -0
  210. package/dist/visualize/display.d.ts +24 -0
  211. package/dist/visualize/display.d.ts.map +1 -0
  212. package/dist/visualize/display.js +29 -0
  213. package/dist/visualize/display.js.map +1 -0
  214. package/dist/visualize/graphics.d.ts +35 -0
  215. package/dist/visualize/graphics.d.ts.map +1 -0
  216. package/dist/visualize/graphics.js +96 -0
  217. package/dist/visualize/graphics.js.map +1 -0
  218. package/dist/visualize/image.d.ts +38 -0
  219. package/dist/visualize/image.d.ts.map +1 -0
  220. package/dist/visualize/image.js +95 -0
  221. package/dist/visualize/image.js.map +1 -0
  222. package/dist/visualize/index.d.ts +59 -0
  223. package/dist/visualize/index.d.ts.map +1 -0
  224. package/dist/visualize/index.js +198 -0
  225. package/dist/visualize/index.js.map +1 -0
  226. package/dist/visualize/raster.d.ts +23 -0
  227. package/dist/visualize/raster.d.ts.map +1 -0
  228. package/dist/visualize/raster.js +47 -0
  229. package/dist/visualize/raster.js.map +1 -0
  230. package/dist/visualize/svg.d.ts +39 -0
  231. package/dist/visualize/svg.d.ts.map +1 -0
  232. package/dist/visualize/svg.js +148 -0
  233. package/dist/visualize/svg.js.map +1 -0
  234. package/dist/visualize/transcode.d.ts +13 -0
  235. package/dist/visualize/transcode.d.ts.map +1 -0
  236. package/dist/visualize/transcode.js +40 -0
  237. package/dist/visualize/transcode.js.map +1 -0
  238. package/docs/images/repl.png +0 -0
  239. package/package.json +19 -15
  240. package/src/prolog-src/deepclause_analysis.pl +10 -2
  241. package/src/prolog-src/deepclause_mi.pl +2440 -49
  242. package/dist/cli/commands.d.ts +0 -42
  243. package/dist/cli/commands.d.ts.map +0 -1
  244. package/dist/cli/commands.js +0 -246
  245. package/dist/cli/commands.js.map +0 -1
  246. package/dist/cli/compile.d.ts +0 -99
  247. package/dist/cli/compile.d.ts.map +0 -1
  248. package/dist/cli/compile.js +0 -363
  249. package/dist/cli/compile.js.map +0 -1
  250. package/dist/cli/default-skills.d.ts +0 -10
  251. package/dist/cli/default-skills.d.ts.map +0 -1
  252. package/dist/cli/default-skills.js +0 -206
  253. package/dist/cli/default-skills.js.map +0 -1
  254. package/dist/cli/interactive.d.ts +0 -2
  255. package/dist/cli/interactive.d.ts.map +0 -1
  256. package/dist/cli/interactive.js +0 -20
  257. package/dist/cli/interactive.js.map +0 -1
  258. package/dist/cli/mcp.d.ts +0 -56
  259. package/dist/cli/mcp.d.ts.map +0 -1
  260. package/dist/cli/mcp.js +0 -138
  261. package/dist/cli/mcp.js.map +0 -1
  262. package/dist/cli/prompt.d.ts +0 -2
  263. package/dist/cli/prompt.d.ts.map +0 -1
  264. package/dist/cli/prompt.js +0 -2
  265. package/dist/cli/prompt.js.map +0 -1
  266. package/dist/cli/search.d.ts +0 -19
  267. package/dist/cli/search.d.ts.map +0 -1
  268. package/dist/cli/search.js +0 -189
  269. package/dist/cli/search.js.map +0 -1
  270. package/dist/cli/tool-args.d.ts +0 -2
  271. package/dist/cli/tool-args.d.ts.map +0 -1
  272. package/dist/cli/tool-args.js +0 -39
  273. package/dist/cli/tool-args.js.map +0 -1
  274. package/dist/cli/tools.d.ts +0 -36
  275. package/dist/cli/tools.d.ts.map +0 -1
  276. package/dist/cli/tools.js +0 -229
  277. package/dist/cli/tools.js.map +0 -1
  278. package/dist/cli/tui/index.d.ts +0 -22
  279. package/dist/cli/tui/index.d.ts.map +0 -1
  280. package/dist/cli/tui/index.js +0 -29
  281. package/dist/cli/tui/index.js.map +0 -1
  282. package/dist/cli/tui.d.ts +0 -167
  283. package/dist/cli/tui.d.ts.map +0 -1
  284. package/dist/cli/tui.js +0 -5228
  285. package/dist/cli/tui.js.map +0 -1
  286. package/dist/compiler_prompt.d.ts +0 -20
  287. package/dist/compiler_prompt.d.ts.map +0 -1
  288. package/dist/compiler_prompt.js +0 -870
  289. package/dist/compiler_prompt.js.map +0 -1
  290. package/dist/deepclause-cli.skill +0 -0
  291. package/dist/prolog-src/deepclause_mi.pl.bak +0 -570
  292. package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +0 -105
  293. package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +0 -700
  294. package/dist/system/assets/docs/TUI.md +0 -150
  295. package/dist/system/assets/recipes/deepclause-coding-workflow/SKILL.md +0 -100
  296. package/dist/system/assets/skills/conductor.dml +0 -76
  297. package/dist/system/assets/skills/deep-planner.dml +0 -412
  298. package/dist/system/assets/skills/plan.dml +0 -429
  299. package/dist/system/assets/skills/security-planner.dml +0 -337
  300. package/dist/system/assets/skills/skill-creator.dml +0 -187
  301. package/dist/system/runtime/catalog-recipes.d.ts +0 -24
  302. package/dist/system/runtime/catalog-recipes.d.ts.map +0 -1
  303. package/dist/system/runtime/catalog-recipes.js +0 -273
  304. package/dist/system/runtime/catalog-recipes.js.map +0 -1
  305. package/dist/system/runtime/catalog-skills.d.ts +0 -50
  306. package/dist/system/runtime/catalog-skills.d.ts.map +0 -1
  307. package/dist/system/runtime/catalog-skills.js +0 -152
  308. package/dist/system/runtime/catalog-skills.js.map +0 -1
  309. package/dist/system/runtime/conductor.d.ts +0 -105
  310. package/dist/system/runtime/conductor.d.ts.map +0 -1
  311. package/dist/system/runtime/conductor.js +0 -732
  312. package/dist/system/runtime/conductor.js.map +0 -1
  313. package/dist/system/runtime/console-capture.d.ts +0 -9
  314. package/dist/system/runtime/console-capture.d.ts.map +0 -1
  315. package/dist/system/runtime/console-capture.js +0 -67
  316. package/dist/system/runtime/console-capture.js.map +0 -1
  317. package/dist/system/runtime/dml-executor.d.ts +0 -52
  318. package/dist/system/runtime/dml-executor.d.ts.map +0 -1
  319. package/dist/system/runtime/dml-executor.js +0 -203
  320. package/dist/system/runtime/dml-executor.js.map +0 -1
  321. package/dist/system/runtime/runtime-tools.d.ts +0 -31
  322. package/dist/system/runtime/runtime-tools.d.ts.map +0 -1
  323. package/dist/system/runtime/runtime-tools.js +0 -312
  324. package/dist/system/runtime/runtime-tools.js.map +0 -1
  325. package/dist/system/runtime/skill-creator.d.ts +0 -38
  326. package/dist/system/runtime/skill-creator.d.ts.map +0 -1
  327. package/dist/system/runtime/skill-creator.js +0 -763
  328. package/dist/system/runtime/skill-creator.js.map +0 -1
  329. package/src/prolog-src/deepclause_mi.pl.bak +0 -570
@@ -0,0 +1,1473 @@
1
+ /**
2
+ * DMLSession — a persistent REPL living module.
3
+ *
4
+ * Owns one Prolog session (the "living module"), one `DMLRunner` bound to it,
5
+ * and the harness memory slot. Goal-mode queries create a `query_start/5`
6
+ * frame, register it with the runner, and drive it with `runFrame`.
7
+ */
8
+ import * as fs from 'fs/promises';
9
+ import * as os from 'os';
10
+ import * as path from 'path';
11
+ import { strFromU8, strToU8, unzipSync, zipSync } from 'fflate';
12
+ import { loadProlog, mountWorkspace } from '../prolog/loader.js';
13
+ import { getWorkspaceDocAssetPath } from '../system/assets/index.js';
14
+ import { DMLRunner, toJsValue } from '../runner.js';
15
+ import { compactMemory, estimateMemoryTokens, shouldCompact, toCompactionEvent, } from './compaction.js';
16
+ import { HarnessAgent } from './agent.js';
17
+ import { createHarnessTools } from './harness-tools.js';
18
+ import { buildParamsTerm, plainValue, quotePrologString } from './frames.js';
19
+ import { buildHarnessHeader, } from './export.js';
20
+ import { HistoryStore, harnessHash, } from './history.js';
21
+ import { buildDiagramPrompt, extractMermaid, parseMermaidFlowchart, rasterizeSvg, renderFlowchartSvg, renderMermaidAscii, resolveMedia, validateMermaid, } from '../visualize/index.js';
22
+ import { generateLlmReply } from '../prolog/bridge.js';
23
+ import { formatModelCatalogEntry } from './model-resolver.js';
24
+ let sessionCounter = 0;
25
+ function nextSessionId() {
26
+ sessionCounter += 1;
27
+ return `repl_${Date.now().toString(36)}_${sessionCounter}`;
28
+ }
29
+ export class DMLSession {
30
+ swipl;
31
+ runner;
32
+ sessionId;
33
+ workspacePath;
34
+ gasLimit;
35
+ model;
36
+ provider;
37
+ tools;
38
+ toolPolicy;
39
+ inputHandler;
40
+ maxDepth;
41
+ maxModelCalls;
42
+ hostToolsCatalog;
43
+ /** TypeScript compaction (ported from pi). */
44
+ compactionSettings;
45
+ compactionAuto = false;
46
+ contextWindow;
47
+ llmBackend;
48
+ imageBackend;
49
+ modelResolver;
50
+ temperature = 0.7;
51
+ maxTokens = 65536;
52
+ /** Persisted authoring-agent conversation (agent mode). */
53
+ agentMessages = [];
54
+ harnessAgent;
55
+ /** Open edit transaction (agent mode): predicate list at snapshot. */
56
+ transactionBefore = null;
57
+ transactionDirty = false;
58
+ /** Whether the last agent turn committed an edit (for the history record). */
59
+ lastTurnCommitted = false;
60
+ /** Version recorded by the last commit, when history is enabled. */
61
+ lastCommitVersion;
62
+ /** Current nested frame depth (used for the depth budget). */
63
+ frameDepth = 0;
64
+ /** Model calls made in the current turn (shared across all frames). */
65
+ modelCallsThisTurn = 0;
66
+ /** Current budget; grows by maxModelCalls when the user extends it. */
67
+ modelCallBudget;
68
+ dmlReference;
69
+ dmlAuthoring;
70
+ harnessExamples;
71
+ harnessPatterns;
72
+ prologSnippets;
73
+ disposed = false;
74
+ /** History recorder; present only when the caller enables it. */
75
+ history;
76
+ historyOptions;
77
+ resume;
78
+ /** Harness this session edits. */
79
+ harnessName;
80
+ autoLoadHarness;
81
+ /** Monotonic per-session turn counter used by history records. */
82
+ turnCounter = 0;
83
+ historyWarned = false;
84
+ constructor(swipl, options) {
85
+ this.swipl = swipl;
86
+ this.sessionId = options.sessionId ?? nextSessionId();
87
+ this.workspacePath = options.workspacePath;
88
+ this.gasLimit = options.gasLimit;
89
+ this.model = options.runnerOptions.model;
90
+ this.provider = String(options.runnerOptions.provider);
91
+ this.tools = options.tools ?? new Map();
92
+ this.toolPolicy = options.toolPolicy ?? null;
93
+ this.inputHandler = options.onInput
94
+ ?? (options.askUser ? (prompt, choices) => options.askUser(prompt, choices).then((r) => ('cancelled' in r ? undefined : r.answer)) : undefined)
95
+ ?? options.onInputRequired
96
+ ?? undefined;
97
+ this.maxDepth = options.maxDepth ?? 3;
98
+ this.maxModelCalls = options.maxModelCalls ?? 100;
99
+ this.modelCallBudget = this.maxModelCalls;
100
+ this.hostToolsCatalog = options.hostToolsCatalog;
101
+ this.historyOptions = options.history;
102
+ this.resume = options.resume ?? false;
103
+ this.harnessName = options.harnessName ?? 'main';
104
+ this.autoLoadHarness = options.autoLoadHarness ?? true;
105
+ this.compactionSettings = options.compaction?.settings;
106
+ this.compactionAuto = options.compaction?.auto ?? false;
107
+ this.contextWindow = options.contextWindow;
108
+ this.llmBackend = options.runnerOptions.llmBackend;
109
+ this.imageBackend = options.runnerOptions.imageBackend;
110
+ this.modelResolver = options.runnerOptions.modelResolver;
111
+ this.temperature = options.runnerOptions.temperature;
112
+ this.maxTokens = options.runnerOptions.maxTokens;
113
+ this.runner = new DMLRunner(swipl, {
114
+ ...options.runnerOptions,
115
+ compactMessages: options.compaction
116
+ ? (messages, lastInputTokens, contextWindow) => this.compactLoopMessages(messages, lastInputTokens, contextWindow)
117
+ : undefined,
118
+ });
119
+ this.runner.bindSession(this.sessionId);
120
+ }
121
+ static async open(options) {
122
+ const swipl = await loadProlog();
123
+ const session = new DMLSession(swipl, options);
124
+ // Mount the workspace so DML file I/O and `[file].` consult resolve under
125
+ // /workspace exactly as they do for runDML.
126
+ if (options.workspacePath) {
127
+ mountWorkspace(swipl, options.workspacePath);
128
+ }
129
+ // Resuming: read the saved session snapshot up-front so the restored
130
+ // conversation memory can seed create_session; the harness itself is
131
+ // loaded once the session exists.
132
+ let snapshot = null;
133
+ if (options.resume) {
134
+ if (!options.history) {
135
+ throw new Error('resume requires history to be enabled');
136
+ }
137
+ const store = new HistoryStore(options.workspacePath ?? process.cwd(), session.sessionId, options.history, session.harnessName);
138
+ snapshot = await store.readSessionHarness();
139
+ if (!snapshot) {
140
+ throw new Error(`session not found or has no saved harness: ${session.sessionId}`);
141
+ }
142
+ }
143
+ const sessionParams = {
144
+ workspace_path: options.workspacePath ?? './',
145
+ trace: options.trace ?? false,
146
+ ...options.params,
147
+ };
148
+ if (options.gasLimit !== undefined) {
149
+ sessionParams.gas_limit = options.gasLimit;
150
+ }
151
+ const harnessMemory = snapshot?.memory?.harness?.trim()
152
+ ? snapshot.memory.harness
153
+ : `[${session.serializeMemory(options.initialHarnessMemory ?? [])}]`;
154
+ const metaMemory = snapshot?.memory?.meta?.trim() ? snapshot.memory.meta : '[]';
155
+ const result = session.prologQuery(`deepclause_mi:create_session('${session.sessionId}', ${buildParamsTerm(sessionParams)}, ${harnessMemory}, ${metaMemory})`);
156
+ if (!result) {
157
+ throw new Error('Failed to create REPL session');
158
+ }
159
+ await session.initHistory();
160
+ if (snapshot) {
161
+ if (snapshot.memory?.agent) {
162
+ session.agentMessages = snapshot.memory.agent;
163
+ }
164
+ await session.restoreHarnessFromHistory(snapshot);
165
+ }
166
+ else if (session.autoLoadHarness) {
167
+ // One harness per name: a fresh session starts from the harness's current
168
+ // content file (`harness.dml`), not an empty module.
169
+ const head = await session.history?.readHeadModule();
170
+ if (head?.module.trim()) {
171
+ const loaded = await session.loadCode(head.module);
172
+ if (loaded.error) {
173
+ session.warnHistory(new Error(`could not load harness '${session.harnessName}': ${loaded.error}`));
174
+ }
175
+ }
176
+ }
177
+ return session;
178
+ }
179
+ getSessionId() {
180
+ return this.sessionId;
181
+ }
182
+ /** Parse and load DML source into the living module. */
183
+ async loadCode(code) {
184
+ if (this.disposed) {
185
+ return { error: 'Session has been disposed' };
186
+ }
187
+ const result = this.prologQuery(`deepclause_mi:load_code('${this.sessionId}', ${quoteString(code)}, Error)`);
188
+ const error = plainValue(result?.Error);
189
+ if (error === undefined || error === null) {
190
+ return { error: 'Failed to load code' };
191
+ }
192
+ return error === 'none' ? {} : { error: String(error) };
193
+ }
194
+ /**
195
+ * Load a DML file (or a `.zip` bundle) into the living module.
196
+ *
197
+ * A `.zip` bundle extracts its script files into the workspace and loads
198
+ * `harness.dml`; a single-file `.dml` export materializes any inlined
199
+ * `embedded_file/2` facts back into the workspace.
200
+ */
201
+ async load(file) {
202
+ const before = this.predicateList();
203
+ // On a fresh harness the loaded file *is* the baseline, not a version.
204
+ const recordLoad = this.history?.hasCommits ?? false;
205
+ const target = path.isAbsolute(file)
206
+ ? file
207
+ : path.resolve(this.workspacePath ?? process.cwd(), file);
208
+ try {
209
+ if (target.toLowerCase().endsWith('.zip')) {
210
+ const result = await this.loadBundle(target);
211
+ if (!result.error && recordLoad)
212
+ await this.recordHistoryChange('load', this.turnCounter, before);
213
+ return result;
214
+ }
215
+ const code = await fs.readFile(target, 'utf8');
216
+ const result = await this.loadCode(code);
217
+ if (!result.error) {
218
+ this.prologQuery(`deepclause_mi:repl_materialize_embedded('${this.sessionId}')`);
219
+ if (recordLoad)
220
+ await this.recordHistoryChange('load', this.turnCounter, before);
221
+ }
222
+ return result;
223
+ }
224
+ catch (error) {
225
+ const message = error instanceof Error ? error.message : String(error);
226
+ return { error: `Failed to read ${file}: ${message}` };
227
+ }
228
+ }
229
+ /**
230
+ * Establish a fresh harness's baseline from its current module (called after
231
+ * any `-l` files are loaded). A no-op once the harness has a baseline.
232
+ */
233
+ async finalizeHarnessBaseline() {
234
+ if (!this.history)
235
+ return;
236
+ await this.history.setInitialBaseline(this.moduleSource());
237
+ }
238
+ /** Workspace-relative files linked by `script_file/2` facts. */
239
+ scriptFiles() {
240
+ const result = this.prologQuery(`deepclause_mi:repl_script_files('${this.sessionId}', Paths)`);
241
+ const paths = plainValue(result?.Paths);
242
+ if (!Array.isArray(paths))
243
+ return [];
244
+ return paths.map((value) => String(plainValue(value) ?? '')).filter(Boolean);
245
+ }
246
+ // ---- History (docs/DML_REPL_SESSION_HISTORY.md) ----
247
+ async initHistory() {
248
+ if (!this.historyOptions)
249
+ return;
250
+ try {
251
+ const store = new HistoryStore(this.workspacePath ?? process.cwd(), this.sessionId, this.historyOptions, this.harnessName);
252
+ await store.init();
253
+ const existing = await store.readFacts();
254
+ if (existing.trim()) {
255
+ this.prologQuery(`deepclause_mi:repl_history_load('${this.sessionId}', ${quoteString(existing)})`);
256
+ }
257
+ this.history = store;
258
+ this.assertHistoryFacts(await store.startSession({ model: this.model, backend: this.provider }, { resume: this.resume }));
259
+ }
260
+ catch (error) {
261
+ this.warnHistory(error);
262
+ }
263
+ }
264
+ /**
265
+ * Restore a resumed session's harness from its durable snapshot and continue
266
+ * its turn numbering. The saved module is a restore, not a user load, so it
267
+ * is not recorded as a new change.
268
+ */
269
+ async restoreHarnessFromHistory(snapshot) {
270
+ this.turnCounter = (await this.history?.lastTurn(this.sessionId)) ?? 0;
271
+ const module = snapshot.module.trim();
272
+ if (module) {
273
+ const result = await this.loadCode(snapshot.module);
274
+ if (result.error) {
275
+ this.warnHistory(new Error(`could not restore harness: ${result.error}`));
276
+ }
277
+ }
278
+ // Consistency check: the saved hash covers the module *and* its scripts;
279
+ // scripts live in the workspace and may have been edited since.
280
+ const hash = harnessHash(snapshot.module, await this.scriptHashes());
281
+ if (snapshot.hash && hash !== snapshot.hash) {
282
+ this.warnHistory(new Error('harness or its scripts differ from the saved session state'));
283
+ }
284
+ }
285
+ /**
286
+ * Write the durable harness snapshot beside the session so `--session` can
287
+ * restore it even after the process exits. Captures the *current* module
288
+ * (including goal-mode drift) plus the serialized conversation memory.
289
+ */
290
+ async persistSessionHarness(version, clean) {
291
+ if (!this.history)
292
+ return;
293
+ try {
294
+ const module = this.moduleSource();
295
+ const scripts = await this.scriptHashes();
296
+ const hash = harnessHash(module, scripts);
297
+ const isClean = clean
298
+ ?? (this.history.currentHash ? hash === this.history.currentHash : version === 0);
299
+ await this.history.writeSessionHarness({
300
+ sessionId: this.sessionId,
301
+ version,
302
+ hash,
303
+ clean: isClean,
304
+ savedAt: new Date().toISOString(),
305
+ scripts,
306
+ memory: {
307
+ harness: this.dumpMemoryTerm('harness'),
308
+ meta: this.dumpMemoryTerm('meta'),
309
+ ...(this.agentMessages.length > 0 ? { agent: this.agentMessages } : {}),
310
+ },
311
+ module,
312
+ });
313
+ }
314
+ catch (error) {
315
+ this.warnHistory(error);
316
+ }
317
+ }
318
+ /** Serialize a live memory slot back to a Prolog term (for resume). */
319
+ dumpMemoryTerm(slot) {
320
+ const result = this.prologQuery(`deepclause_mi:repl_memory_term('${this.sessionId}', ${slot}, Text)`);
321
+ const text = plainValue(result?.Text);
322
+ return typeof text === 'string' && text.trim() ? text : '[]';
323
+ }
324
+ assertHistoryFacts(facts) {
325
+ if (!this.history || facts.length === 0)
326
+ return;
327
+ try {
328
+ this.prologQuery(`deepclause_mi:repl_history_record('${this.sessionId}', ${quoteString(facts.join('\n'))})`);
329
+ }
330
+ catch (error) {
331
+ this.warnHistory(error);
332
+ }
333
+ }
334
+ recordHistoryEvent(turn, event) {
335
+ this.history?.recordEvent({ turn, frame: event.frameKind }, event);
336
+ }
337
+ manifestText() {
338
+ const result = this.prologQuery(`deepclause_mi:repl_manifest('${this.sessionId}', Text)`);
339
+ return String(plainValue(result?.Text) ?? '');
340
+ }
341
+ predicateList() {
342
+ const result = this.prologQuery(`deepclause_mi:repl_user_predicates_text('${this.sessionId}', Text)`);
343
+ return String(plainValue(result?.Text) ?? '')
344
+ .split(',')
345
+ .map((entry) => entry.trim())
346
+ .filter(Boolean);
347
+ }
348
+ moduleSource() {
349
+ const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', none, Text)`);
350
+ return String(plainValue(result?.Text) ?? '');
351
+ }
352
+ async scriptHashes() {
353
+ const { createHash } = await import('crypto');
354
+ const out = [];
355
+ for (const rel of this.scriptFiles()) {
356
+ try {
357
+ const content = await fs.readFile(this.resolveWorkspacePath(rel));
358
+ out.push({ path: rel, sha256: createHash('sha256').update(content).digest('hex') });
359
+ }
360
+ catch {
361
+ out.push({ path: rel, sha256: 'missing' });
362
+ }
363
+ }
364
+ return out;
365
+ }
366
+ diffPredicates(before, after, touched) {
367
+ const beforeSet = new Set(before);
368
+ const afterSet = new Set(after);
369
+ const byPredicate = new Map();
370
+ for (const predicate of touched) {
371
+ if (!afterSet.has(predicate))
372
+ continue;
373
+ byPredicate.set(predicate, { predicate, action: beforeSet.has(predicate) ? 'replaced' : 'added' });
374
+ }
375
+ for (const predicate of before) {
376
+ if (!afterSet.has(predicate))
377
+ byPredicate.set(predicate, { predicate, action: 'removed' });
378
+ }
379
+ return [...byPredicate.values()].sort((a, b) => a.predicate.localeCompare(b.predicate));
380
+ }
381
+ touchedPredicates() {
382
+ return this.lastCommitPredicates().split(',').map((entry) => entry.trim()).filter(Boolean);
383
+ }
384
+ /** Record a harness version; returns the new version number. */
385
+ async recordHistoryChange(kind, turn, before, overrides = {}) {
386
+ if (!this.history)
387
+ return undefined;
388
+ try {
389
+ const after = this.predicateList();
390
+ const touched = overrides.touched ?? (kind === 'commit' ? this.touchedPredicates() : after);
391
+ const changed = this.diffPredicates(before, after, touched);
392
+ const { facts, version } = await this.history.recordChange({
393
+ kind,
394
+ sessionId: this.sessionId,
395
+ turn,
396
+ changed,
397
+ scripts: await this.scriptHashes(),
398
+ module: this.moduleSource(),
399
+ manifest: this.manifestText(),
400
+ });
401
+ this.assertHistoryFacts(facts);
402
+ await this.persistSessionHarness(version, true);
403
+ return version;
404
+ }
405
+ catch (error) {
406
+ this.warnHistory(error);
407
+ return undefined;
408
+ }
409
+ }
410
+ async finishHistoryTurn(record) {
411
+ if (!this.history)
412
+ return;
413
+ try {
414
+ this.assertHistoryFacts(await this.history.recordTurn(record));
415
+ await this.history.flushEvents();
416
+ }
417
+ catch (error) {
418
+ this.warnHistory(error);
419
+ }
420
+ }
421
+ warnHistory(error) {
422
+ if (this.historyWarned)
423
+ return;
424
+ this.historyWarned = true;
425
+ if (process.env.DEBUG_REPL) {
426
+ console.error('[repl] history recording failed:', error);
427
+ }
428
+ }
429
+ /** Extract a `.zip` bundle and load its harness.dml. */
430
+ async loadBundle(zipPath) {
431
+ const data = new Uint8Array(await fs.readFile(zipPath));
432
+ const entries = unzipSync(data);
433
+ const harnessName = Object.keys(entries).find((name) => name === 'harness.dml' || name.endsWith('/harness.dml'));
434
+ if (!harnessName) {
435
+ return { error: 'bundle does not contain harness.dml' };
436
+ }
437
+ const files = [];
438
+ for (const [name, bytes] of Object.entries(entries)) {
439
+ if (name === harnessName || name === 'manifest.json')
440
+ continue;
441
+ const rel = name.replace(/^\.\//, '');
442
+ if (path.isAbsolute(rel) || rel.split('/').includes('..')) {
443
+ return { error: `unsafe bundle entry: ${name}` };
444
+ }
445
+ this.resolveWorkspacePath(rel);
446
+ files.push({ rel, bytes });
447
+ }
448
+ for (const file of files) {
449
+ const abs = this.resolveWorkspacePath(file.rel);
450
+ await fs.mkdir(path.dirname(abs), { recursive: true });
451
+ await fs.writeFile(abs, Buffer.from(file.bytes));
452
+ }
453
+ return this.loadCode(strFromU8(entries[harnessName]));
454
+ }
455
+ /** Read-only listing of the living module (`all`, `Name/Arity`, or `tools`). */
456
+ async listing(filter = 'all') {
457
+ const filterTerm = filter === 'all' || filter === 'tools'
458
+ ? filter
459
+ : toListingFilter(filter);
460
+ const result = this.prologQuery(`deepclause_mi:repl_listing('${this.sessionId}', ${filterTerm}, Text)`);
461
+ const text = plainValue(result?.Text);
462
+ return typeof text === 'string' ? text : '';
463
+ }
464
+ /**
465
+ * Run a goal and enumerate its solutions.
466
+ *
467
+ * Yields `solution` events (one per answer), interleaved with `output`/`log`/
468
+ * `answer` events from the goal, and finally `finished` or `error`.
469
+ */
470
+ async *query(goal, options = {}) {
471
+ if (this.disposed) {
472
+ yield { type: 'error', content: 'Session has been disposed' };
473
+ return;
474
+ }
475
+ this.modelCallsThisTurn = 0;
476
+ this.modelCallBudget = this.maxModelCalls;
477
+ const frameKind = options.frameKind ?? 'object';
478
+ const start = this.prologQuery(`deepclause_mi:repl_initial_state('${this.sessionId}', ${frameKind}, State), `
479
+ + `deepclause_mi:query_start('${this.sessionId}', ${quoteString(goal)}, ${frameKind}, State, Error)`);
480
+ const error = plainValue(start?.Error);
481
+ if (error === undefined || error === null) {
482
+ yield { type: 'error', content: 'Failed to start query' };
483
+ return;
484
+ }
485
+ if (error !== 'none') {
486
+ yield { type: 'error', content: String(error) };
487
+ return;
488
+ }
489
+ this.turnCounter += 1;
490
+ const historyTurn = this.turnCounter;
491
+ const historyStart = Date.now();
492
+ const frame = this.runner.registerDefaultFrame(frameKind);
493
+ if (!frame) {
494
+ yield { type: 'error', content: 'Failed to create query frame' };
495
+ return;
496
+ }
497
+ let historyOutcome = 'query';
498
+ try {
499
+ for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions(options))) {
500
+ this.recordHistoryEvent(historyTurn, event);
501
+ if (event.type === 'error')
502
+ historyOutcome = 'failed';
503
+ yield event;
504
+ }
505
+ }
506
+ finally {
507
+ this.runner.endFrame(frame.id);
508
+ }
509
+ // An aborted goal may have asserted scratch state; preserve it.
510
+ if (options.signal?.aborted) {
511
+ historyOutcome = 'aborted';
512
+ const saved = await this.saveCurrentHarness('cancelled');
513
+ yield {
514
+ type: 'error',
515
+ content: saved ? `turn aborted; current harness saved to ${saved}` : 'turn aborted',
516
+ };
517
+ }
518
+ await this.finishHistoryTurn({
519
+ turn: historyTurn,
520
+ mode: 'goal',
521
+ request: goal,
522
+ outcome: historyOutcome,
523
+ durationMs: Date.now() - historyStart,
524
+ modelCalls: this.modelCallsThisTurn,
525
+ });
526
+ }
527
+ /** Load the bundled DML reference once, for inclusion in the meta prompt. */
528
+ async loadDmlReference() {
529
+ if (this.dmlReference !== undefined) {
530
+ return this.dmlReference;
531
+ }
532
+ try {
533
+ this.dmlReference = await fs.readFile(getWorkspaceDocAssetPath('dml-reference'), 'utf8');
534
+ }
535
+ catch {
536
+ this.dmlReference = '';
537
+ }
538
+ return this.dmlReference;
539
+ }
540
+ /** Load the curated authoring patterns once, for the meta prompt. */
541
+ async loadDmlAuthoring() {
542
+ if (this.dmlAuthoring !== undefined) {
543
+ return this.dmlAuthoring;
544
+ }
545
+ try {
546
+ this.dmlAuthoring = await fs.readFile(getWorkspaceDocAssetPath('dml-authoring'), 'utf8');
547
+ }
548
+ catch {
549
+ this.dmlAuthoring = '';
550
+ }
551
+ return this.dmlAuthoring;
552
+ }
553
+ /** Load a bundled doc asset once (cached by field). */
554
+ async loadDocAsset(name) {
555
+ const cached = name === 'harness-examples'
556
+ ? this.harnessExamples
557
+ : name === 'harness-patterns'
558
+ ? this.harnessPatterns
559
+ : this.prologSnippets;
560
+ if (cached !== undefined)
561
+ return cached;
562
+ let text = '';
563
+ try {
564
+ text = await fs.readFile(getWorkspaceDocAssetPath(name), 'utf8');
565
+ }
566
+ catch {
567
+ text = '';
568
+ }
569
+ if (name === 'harness-examples')
570
+ this.harnessExamples = text;
571
+ else if (name === 'harness-patterns')
572
+ this.harnessPatterns = text;
573
+ else
574
+ this.prologSnippets = text;
575
+ return text;
576
+ }
577
+ /** Predicates replaced by the most recent successful commit ("name/arity, …"). */
578
+ lastCommitPredicates() {
579
+ const result = this.prologQuery(`deepclause_mi:repl_last_commit_summary('${this.sessionId}', Text)`);
580
+ return String(plainValue(result?.Text) ?? '');
581
+ }
582
+ /**
583
+ * Serialize the *current* living module (including any scratch edits) to a
584
+ * temp .dml file. Used as an abort safety net so a cancelled turn's work is
585
+ * not lost.
586
+ */
587
+ /** Sorted arities of a user predicate in the living module (e.g. `agent_main`). */
588
+ predicateArities(name) {
589
+ const safe = /^[a-z_][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
590
+ const result = this.prologQuery(`deepclause_mi:repl_predicate_arities('${this.sessionId}', ${safe}, Arities)`);
591
+ const arities = plainValue(result?.Arities);
592
+ if (!Array.isArray(arities))
593
+ return [];
594
+ return arities
595
+ .map((arity) => Number(plainValue(arity)))
596
+ .filter((arity) => Number.isFinite(arity));
597
+ }
598
+ /** True when the living module has no user predicates (nothing worth saving). */
599
+ isEmpty() {
600
+ const result = this.prologQuery(`deepclause_mi:repl_user_predicates_text('${this.sessionId}', Text)`);
601
+ return String(plainValue(result?.Text) ?? '').trim() === '';
602
+ }
603
+ /** Number of user predicates (`Name/Arity`) currently in the harness. */
604
+ predicateCount() {
605
+ return this.predicateList().length;
606
+ }
607
+ async saveCurrentHarness(reason, header = `aborted turn: ${reason}`) {
608
+ const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', ${quoteString(header)}, Text)`);
609
+ const text = String(plainValue(result?.Text) ?? '');
610
+ if (!text.trim()) {
611
+ return null;
612
+ }
613
+ // A bundle (module + current scripts) so an aborted
614
+ // turn's work is fully recoverable, not just the module source.
615
+ const file = path.join(os.tmpdir(), `deepclause-repl-${this.sessionId}-${Date.now()}.zip`);
616
+ await this.writeBundle(file, text, this.scriptFiles(), header);
617
+ return file;
618
+ }
619
+ /** Clear the living module and memories (the `reset.` command). */
620
+ async reset() {
621
+ if (this.isFrozen()) {
622
+ throw new Error('harness is frozen; reset is disabled');
623
+ }
624
+ const before = this.predicateList();
625
+ this.prologQuery(`deepclause_mi:clear_session('${this.sessionId}')`);
626
+ this.runner.clearFrames();
627
+ await this.recordHistoryChange('reset', this.turnCounter, before);
628
+ }
629
+ /**
630
+ * Undo the most recent harness transaction by restoring the previous history
631
+ * version. No-op when there is nothing to undo; refused when frozen. The
632
+ * rollback itself is recorded as a change (harness_rollback/5).
633
+ */
634
+ async rollback() {
635
+ if (this.disposed)
636
+ return { ok: false, error: 'Session has been disposed' };
637
+ if (!this.history)
638
+ return { ok: false, error: 'history is not enabled; rollback is unavailable' };
639
+ if (this.isFrozen())
640
+ return { ok: false, error: 'harness is frozen; rollback is disabled' };
641
+ const plan = await this.history.prepareRollback();
642
+ if (!plan)
643
+ return { ok: false, error: 'no harness changes to roll back' };
644
+ const result = this.prologQuery(`deepclause_mi:repl_rollback('${this.sessionId}', ${quoteString(plan.module)}, Result)`);
645
+ const outcome = plainValue(result?.Result);
646
+ if (outcome !== 'ok') {
647
+ return { ok: false, error: String(outcome ?? 'rollback failed') };
648
+ }
649
+ this.assertHistoryFacts(await this.history.recordRollback(plan.from, plan.to, this.turnCounter, this.sessionId, plan.module));
650
+ return { ok: true, from: plan.from, to: plan.to };
651
+ }
652
+ // ---- Freeze lock (M2) ----
653
+ /** Make the living module read-only (assert/retract/load/commit refused). */
654
+ async freeze() {
655
+ this.prologQuery(`deepclause_mi:repl_freeze('${this.sessionId}')`);
656
+ }
657
+ /** Like freeze(), but `unfreeze.` is refused for the rest of the process. */
658
+ async lock() {
659
+ this.prologQuery(`deepclause_mi:repl_lock('${this.sessionId}')`);
660
+ }
661
+ /** Clear the lock list. Throws when the harness is irreversibly locked. */
662
+ async unfreeze() {
663
+ const result = this.prologQuery(`deepclause_mi:repl_try_unfreeze('${this.sessionId}', Error)`);
664
+ const error = plainValue(result?.Error);
665
+ if (error !== undefined && error !== null && error !== 'none') {
666
+ throw new Error(String(error));
667
+ }
668
+ }
669
+ isFrozen() {
670
+ const result = this.prologQuery(`deepclause_mi:repl_is_frozen('${this.sessionId}')`);
671
+ return result?.success === true;
672
+ }
673
+ // ---- Export (M2) ----
674
+ /**
675
+ * Serialize the living module.
676
+ *
677
+ * `.zip` produces the self-contained bundle (harness.dml + linked script
678
+ * files + manifest.json). Any other extension produces a single `.dml` with
679
+ * the linked scripts auto-inlined as `embedded_file/2` facts.
680
+ */
681
+ async export(file, options = {}) {
682
+ const header = options.header ?? buildHarnessHeader({
683
+ timestamp: new Date().toISOString(),
684
+ backend: options.skill ? 'skill' : this.provider,
685
+ model: this.model,
686
+ });
687
+ const result = this.prologQuery(`deepclause_mi:export_harness('${this.sessionId}', ${quoteString(header)}, Text)`);
688
+ const moduleText = String(plainValue(result?.Text) ?? '');
689
+ const target = path.resolve(this.workspacePath ?? process.cwd(), file ?? options.path ?? 'harness.dml');
690
+ const scripts = this.scriptFiles();
691
+ if (target.toLowerCase().endsWith('.zip')) {
692
+ await this.writeBundle(target, moduleText, scripts, header);
693
+ }
694
+ else {
695
+ await fs.writeFile(target, await this.inlineScripts(moduleText, scripts), 'utf8');
696
+ }
697
+ const stats = this.prologQuery(`deepclause_mi:export_stats('${this.sessionId}', Clauses, Tools, Scripts)`);
698
+ return {
699
+ path: target,
700
+ clauses: Number(plainValue(stats?.Clauses) ?? 0),
701
+ tools: Number(plainValue(stats?.Tools) ?? 0),
702
+ scripts: Number(plainValue(stats?.Scripts) ?? 0),
703
+ };
704
+ }
705
+ /** Inline linked script files as `embedded_file/2` facts on a single .dml. */
706
+ async inlineScripts(moduleText, scripts) {
707
+ const facts = [];
708
+ for (const rel of [...scripts].sort()) {
709
+ const content = await this.readWorkspaceFile(rel);
710
+ if (content === null)
711
+ continue;
712
+ facts.push(`embedded_file(${quotePrologString(rel)}, ${quotePrologString(content)}).`);
713
+ }
714
+ if (facts.length === 0)
715
+ return moduleText;
716
+ return `% inlined script files (materialized on load)\n${facts.join('\n')}\n\n${moduleText}`;
717
+ }
718
+ /** Write a `.zip` bundle: harness.dml + linked files + manifest.json. */
719
+ async writeBundle(target, moduleText, scripts, header) {
720
+ const entries = { 'harness.dml': strToU8(moduleText) };
721
+ const manifestFiles = [];
722
+ for (const rel of [...scripts].sort()) {
723
+ const content = await this.readWorkspaceFile(rel);
724
+ if (content === null)
725
+ continue;
726
+ const bytes = strToU8(content);
727
+ entries[rel] = bytes;
728
+ manifestFiles.push({ path: rel, bytes: bytes.length });
729
+ }
730
+ entries['manifest.json'] = strToU8(`${JSON.stringify({
731
+ version: 1,
732
+ header,
733
+ model: this.model,
734
+ provider: this.provider,
735
+ timestamp: new Date().toISOString(),
736
+ files: manifestFiles,
737
+ }, null, 2)}\n`);
738
+ await fs.mkdir(path.dirname(target), { recursive: true });
739
+ await fs.writeFile(target, Buffer.from(zipSync(entries, { level: 6 })));
740
+ }
741
+ /**
742
+ * Run a goal in an isolated `execute` child frame and collect its result.
743
+ * The child frame uses the living scratch module and may itself call task()
744
+ * (RLM-style recursion); its harness-memory writes are discarded afterward.
745
+ */
746
+ async executeGoal(goal, limit = 5, onEvent, signal) {
747
+ const startedAt = Date.now();
748
+ const elapsed = () => Date.now() - startedAt;
749
+ const childDepth = this.frameDepth + 1;
750
+ if (childDepth > this.maxDepth) {
751
+ return {
752
+ bindings: [],
753
+ solutions: [],
754
+ output: '',
755
+ error: `maximum frame depth (${this.maxDepth}) exceeded`,
756
+ elapsedMs: elapsed(),
757
+ };
758
+ }
759
+ const start = this.prologQuery(`deepclause_mi:repl_execute_frame('${this.sessionId}', ${quoteString(goal)}, FrameId, Error)`);
760
+ const error = plainValue(start?.Error);
761
+ if (error === undefined || error === null) {
762
+ return { bindings: [], solutions: [], output: '', error: 'Failed to start execute frame', elapsedMs: elapsed() };
763
+ }
764
+ if (error !== 'none') {
765
+ return { bindings: [], solutions: [], output: '', error: String(error), elapsedMs: elapsed() };
766
+ }
767
+ const frameId = String(plainValue(start?.FrameId));
768
+ const frame = this.runner.registerFrame(frameId, 'nested', 1);
769
+ // `execute` is a scratch test: capture harness memory and restore it after,
770
+ // so a goal's `system/1`/`user/1` writes do not leak into the live harness.
771
+ const memoryBefore = this.getMemory('harness');
772
+ const solutions = [];
773
+ let output = '';
774
+ let runError;
775
+ this.frameDepth = childDepth;
776
+ try {
777
+ for await (const event of this.runner.runFrame(frame.id, this.buildRunOptions({ signal }))) {
778
+ onEvent?.(event);
779
+ if (event.type === 'solution') {
780
+ if (solutions.length < limit) {
781
+ solutions.push(event.solution?.bindings ?? []);
782
+ }
783
+ // Stop enumerating once the limit is reached: a nondeterministic goal
784
+ // (e.g. an unlabelled CLP model) can otherwise produce unbounded
785
+ // solutions and flood the session before the turn timeout.
786
+ if (solutions.length >= limit) {
787
+ break;
788
+ }
789
+ }
790
+ else if (event.type === 'output' || event.type === 'answer') {
791
+ output += `${event.content ?? ''}\n`;
792
+ }
793
+ else if (event.type === 'error') {
794
+ runError = event.content;
795
+ }
796
+ }
797
+ }
798
+ finally {
799
+ this.runner.endFrame(frame.id);
800
+ this.frameDepth = childDepth - 1;
801
+ this.setMemory('harness', memoryBefore);
802
+ }
803
+ return {
804
+ bindings: solutions[0] ?? [],
805
+ solutions,
806
+ output: output.slice(0, 4000),
807
+ error: runError,
808
+ elapsedMs: elapsed(),
809
+ };
810
+ }
811
+ // ---- Authoring agent (TS) ----------------------------------------------
812
+ /**
813
+ * The TypeScript authoring agent: a normal `LLMBackend` agent with harness
814
+ * tools. It runs one or more committed turns in a loop until the model calls
815
+ * `finish`. It runs one or more committed turns per request.
816
+ */
817
+ async *agentPrompt(request, options = {}) {
818
+ if (this.disposed) {
819
+ yield { type: 'error', content: 'Session has been disposed' };
820
+ return;
821
+ }
822
+ if (!this.llmBackend) {
823
+ yield { type: 'error', content: 'authoring agent requires an injected LLM backend' };
824
+ return;
825
+ }
826
+ this.turnCounter += 1;
827
+ const turn = this.turnCounter;
828
+ const historyStart = Date.now();
829
+ this.lastTurnCommitted = false;
830
+ this.lastCommitVersion = undefined;
831
+ // No automatic transaction: `define`/`assert`/… are refused until the agent
832
+ // calls `begin_turn`, so the transaction is explicit and visible.
833
+ const agent = await this.getHarnessAgent();
834
+ let failed = false;
835
+ let nudges = 0;
836
+ const maxNudges = 3;
837
+ let requestText = request;
838
+ try {
839
+ for (;;) {
840
+ for await (const event of agent.run(requestText, { signal: options.signal })) {
841
+ // A nested (`execute` child) error is a tool result the agent can
842
+ // react to; only the agent's own errors fail the turn.
843
+ if (event.type === 'error' && event.frameKind !== 'nested')
844
+ failed = true;
845
+ this.recordHistoryEvent(turn, event);
846
+ yield event;
847
+ }
848
+ if (options.signal?.aborted || failed)
849
+ break;
850
+ // The loop ended with an open transaction: nudge the agent to commit or
851
+ // discard it rather than auto-committing silently.
852
+ if (this.transactionBefore === null)
853
+ break;
854
+ if (nudges >= maxNudges)
855
+ break;
856
+ nudges += 1;
857
+ requestText = 'NUDGE: you still have an OPEN edit transaction with uncommitted changes. '
858
+ + 'Call `end_turn` to commit it (or `rollback` to discard it), then call `finish`.';
859
+ this.recordHistoryEvent(turn, { type: 'output', content: requestText });
860
+ yield { type: 'output', content: requestText };
861
+ }
862
+ }
863
+ catch (error) {
864
+ failed = true;
865
+ const message = error instanceof Error ? error.message : String(error);
866
+ this.recordHistoryEvent(turn, { type: 'error', content: message });
867
+ yield { type: 'error', content: message };
868
+ }
869
+ finally {
870
+ this.agentMessages = agent.getMessages();
871
+ }
872
+ if (options.signal?.aborted || failed) {
873
+ await this.rollbackTurn();
874
+ }
875
+ else if (this.transactionBefore !== null) {
876
+ // Last resort after the nudges: commit so nothing is lost, and show it.
877
+ await this.endTurn();
878
+ const marker = {
879
+ type: 'tool_call',
880
+ toolName: 'end_turn',
881
+ toolArgs: {},
882
+ toolState: 'completed',
883
+ toolSummary: 'commit transaction (host; agent did not end_turn)',
884
+ };
885
+ this.recordHistoryEvent(turn, marker);
886
+ yield marker;
887
+ }
888
+ await this.persistSessionHarness(this.history?.currentVersion ?? 0);
889
+ const outcome = options.signal?.aborted
890
+ ? 'aborted'
891
+ : failed
892
+ ? 'failed'
893
+ : this.lastTurnCommitted
894
+ ? 'committed'
895
+ : 'answered';
896
+ await this.finishHistoryTurn({
897
+ turn,
898
+ mode: 'prompt',
899
+ request,
900
+ outcome,
901
+ commitVersion: this.lastCommitVersion,
902
+ durationMs: Date.now() - historyStart,
903
+ modelCalls: this.modelCallsThisTurn,
904
+ error: failed ? 'authoring turn failed' : undefined,
905
+ });
906
+ }
907
+ /**
908
+ * Deliver a steering message into the running authoring turn (agent mode).
909
+ * Ignored when no agent turn is active.
910
+ */
911
+ steer(text) {
912
+ this.harnessAgent?.steer(text);
913
+ }
914
+ async getHarnessAgent() {
915
+ if (this.harnessAgent)
916
+ return this.harnessAgent;
917
+ this.harnessAgent = new HarnessAgent({
918
+ tools: createHarnessTools(this),
919
+ llmBackend: this.llmBackend,
920
+ model: this.model,
921
+ temperature: this.temperature,
922
+ maxTokens: this.maxTokens,
923
+ streaming: true,
924
+ maxModelCalls: this.maxModelCalls,
925
+ askInput: (prompt, options) => this.requireInput(prompt, options),
926
+ contextWindow: this.contextWindow,
927
+ compaction: this.compactionSettings
928
+ ? { settings: this.compactionSettings, auto: this.compactionAuto }
929
+ : undefined,
930
+ workspacePath: this.workspacePath,
931
+ currentModel: `${this.provider}:${this.model}`,
932
+ modelCatalog: this.modelResolver?.catalog(),
933
+ knownMessages: this.agentMessages,
934
+ });
935
+ return this.harnessAgent;
936
+ }
937
+ /** Read the current harness manifest for the agent system prompt. */
938
+ async manifest() {
939
+ return this.manifestText();
940
+ }
941
+ /** The models available in this session (agent `models` tool). */
942
+ async listModels() {
943
+ const current = `${this.provider}:${this.model}`;
944
+ const resolver = this.modelResolver;
945
+ if (!resolver) {
946
+ return `current model: ${current}\n(no model catalog is configured; with_model is unavailable)`;
947
+ }
948
+ const models = resolver.catalog();
949
+ const body = models.length > 0
950
+ ? models.map((model) => ` ${formatModelCatalogEntry(model)}`).join('\n')
951
+ : ' (none)';
952
+ return `current model: ${current}\n\navailable models (price = USD per 1M tokens, input/output):\n${body}`;
953
+ }
954
+ /**
955
+ * Generate a diagram of one entry predicate: ask the active model for Mermaid,
956
+ * then render it deterministically to a Unicode tree (always) and an SVG (for
957
+ * the terminal-image path). One model call.
958
+ */
959
+ async diagram(entry, mode = 'presentation', options = {}) {
960
+ if (!this.llmBackend) {
961
+ throw new Error('diagram generation needs a model backend');
962
+ }
963
+ const listing = await this.listing('all');
964
+ const resolved = (entry ?? '').trim() || this.defaultDiagramEntry(listing);
965
+ if (!resolved) {
966
+ throw new Error('the harness has no predicate to diagram yet');
967
+ }
968
+ const prompt = buildDiagramPrompt({ entry: resolved, mode, listing });
969
+ const { text } = await generateLlmReply({
970
+ messages: [{ role: 'user', content: prompt }],
971
+ modelOptions: { provider: this.provider, model: this.model, temperature: this.temperature },
972
+ llmBackend: this.llmBackend,
973
+ signal: options.signal,
974
+ });
975
+ const mermaid = extractMermaid(text);
976
+ const warnings = validateMermaid(mermaid);
977
+ const svg = renderFlowchartSvg(parseMermaidFlowchart(mermaid));
978
+ return {
979
+ entry: resolved,
980
+ mode,
981
+ mermaid,
982
+ ascii: renderMermaidAscii(mermaid),
983
+ svg: svg.svg,
984
+ width: svg.width,
985
+ height: svg.height,
986
+ warnings,
987
+ };
988
+ }
989
+ /** Diagram of a skill for the authoring agent's `visualize` tool. */
990
+ async visualize(entry, mode = 'presentation') {
991
+ const result = await this.diagram(entry, mode);
992
+ const png = await rasterizeSvg(result.svg, { width: result.width, height: result.height });
993
+ if (png) {
994
+ return {
995
+ message: `Diagram of ${result.entry} (${result.mode}) is displayed to the user as an image. Do not repeat the diagram text.`,
996
+ event: { type: 'media', media: { mimeType: 'image/svg+xml', data: png.toString('base64') } },
997
+ };
998
+ }
999
+ return { message: result.ascii };
1000
+ }
1001
+ /** Display a workspace image (agent `show_image` tool). */
1002
+ async showImage(relPath) {
1003
+ return this.mediaEventFor(relPath);
1004
+ }
1005
+ /** Generate an image and display it (agent `generate_image` tool). */
1006
+ async generateImage(prompt) {
1007
+ if (!this.imageBackend) {
1008
+ return { message: 'Image generation is unavailable: no image backend is configured.' };
1009
+ }
1010
+ const result = await this.imageBackend.generate({ prompt });
1011
+ const shown = await this.mediaEventFor(result.path);
1012
+ return { message: `Generated ${result.path} (${result.mimeType}). ${shown.message}`, event: shown.event };
1013
+ }
1014
+ /** Write a workspace file (agent `write_file` tool); creates parent dirs. */
1015
+ async writeFile(relPath, content) {
1016
+ try {
1017
+ const abs = this.resolveWorkspacePath(relPath);
1018
+ await fs.mkdir(path.dirname(abs), { recursive: true });
1019
+ await fs.writeFile(abs, content, 'utf8');
1020
+ return { ok: true };
1021
+ }
1022
+ catch (error) {
1023
+ return { ok: false, error: error.message };
1024
+ }
1025
+ }
1026
+ /**
1027
+ * Build a `media` event for a workspace image (resolved to a PNG for inline
1028
+ * display). Returning an event lets any host render it through its normal
1029
+ * event pipeline — no session-specific image channel needed.
1030
+ */
1031
+ async mediaEventFor(relPath) {
1032
+ const resolved = await resolveMedia(this.workspacePath ?? process.cwd(), { path: relPath });
1033
+ if (!resolved) {
1034
+ return { message: `Cannot read image ${relPath}.` };
1035
+ }
1036
+ const event = {
1037
+ type: 'media',
1038
+ media: { path: relPath, mimeType: resolved.mimeType, data: resolved.pngBase64 },
1039
+ };
1040
+ const message = resolved.pngBase64
1041
+ ? `Image ${relPath} (${resolved.mimeType}) is displayed to the user.`
1042
+ : `Image ${relPath} (${resolved.mimeType}) is at workspace path ${relPath} `
1043
+ + '(not rasterized for inline display: SVG needs @resvg/resvg-js, rasters need sharp).';
1044
+ return { message, event };
1045
+ }
1046
+ /** Pick a default diagram entry: `agent_main/N`, else the first predicate. */
1047
+ defaultDiagramEntry(listing) {
1048
+ const mains = this.predicateArities('agent_main');
1049
+ if (mains.length > 0) {
1050
+ return `agent_main/${mains[0]}`;
1051
+ }
1052
+ const reserved = new Set(['doc', 'module_doc', 'tool', 'system', 'user', 'param', 'parameters', 'answer']);
1053
+ for (const line of listing.split('\n')) {
1054
+ const match = /^([a-z][A-Za-z0-9_]*)\/(\d+)\s*$/.exec(line.trim());
1055
+ if (match && !reserved.has(match[1])) {
1056
+ return `${match[1]}/${match[2]}`;
1057
+ }
1058
+ }
1059
+ return undefined;
1060
+ }
1061
+ async beginTurn() {
1062
+ this.prologQuery(`deepclause_mi:repl_snapshot_save('${this.sessionId}')`);
1063
+ this.prologQuery(`deepclause_mi:repl_edit_touched_clear('${this.sessionId}')`);
1064
+ this.transactionBefore = this.predicateList();
1065
+ this.transactionDirty = false;
1066
+ }
1067
+ async endTurn() {
1068
+ if (this.transactionBefore === null) {
1069
+ return { committed: false, changed: [], error: 'no open transaction' };
1070
+ }
1071
+ const before = this.transactionBefore;
1072
+ const after = this.predicateList();
1073
+ const changed = this.simplePredicateDiff(before, after);
1074
+ const touched = this.editTouched();
1075
+ this.transactionBefore = null;
1076
+ const committed = this.transactionDirty;
1077
+ let version;
1078
+ if (committed && this.history) {
1079
+ version = await this.recordHistoryChange('commit', this.turnCounter, before, { touched });
1080
+ }
1081
+ this.lastTurnCommitted = committed;
1082
+ this.lastCommitVersion = version;
1083
+ this.transactionDirty = false;
1084
+ this.prologQuery(`deepclause_mi:repl_edit_touched_clear('${this.sessionId}')`);
1085
+ await this.persistSessionHarness(this.history?.currentVersion ?? 0);
1086
+ // Advisory: probe changed pure predicates for accidental nondeterminism.
1087
+ let warnings = [];
1088
+ if (committed) {
1089
+ const candidates = new Set();
1090
+ for (const entry of touched)
1091
+ candidates.add(entry.trim());
1092
+ for (const entry of changed)
1093
+ if (entry.startsWith('+'))
1094
+ candidates.add(entry.slice(1));
1095
+ warnings = await this.probeNondeterminism([...candidates]);
1096
+ }
1097
+ return { committed: true, changed, warnings };
1098
+ }
1099
+ /**
1100
+ * Probe changed predicates for accidental nondeterminism: a pure predicate
1101
+ * that yields more than `threshold` solutions on fresh variables is very likely
1102
+ * missing an `once/1`/`!` and would flood an `execute`. Fact tables and
1103
+ * side-effecting predicates are skipped by the Prolog probe.
1104
+ */
1105
+ async probeNondeterminism(indicators) {
1106
+ const reserved = new Set(['doc', 'module_doc', 'tool']);
1107
+ const warnings = [];
1108
+ const threshold = 100;
1109
+ const bound = 200000;
1110
+ for (const indicator of indicators.slice(0, 8)) {
1111
+ const match = /^([a-z][A-Za-z0-9_]*)\/(\d+)$/.exec(indicator);
1112
+ if (!match || reserved.has(match[1]))
1113
+ continue;
1114
+ const name = match[1];
1115
+ const arity = match[2];
1116
+ try {
1117
+ const result = this.prologQuery(`deepclause_mi:repl_probe_nondeterminism('${this.sessionId}', ${name}, ${arity}, ${threshold}, ${bound}, Result)`);
1118
+ const verdict = String(plainValue(result?.Result) ?? '');
1119
+ if (verdict === 'many') {
1120
+ warnings.push(`${name}/${arity} yields more than ${threshold} solutions when called with fresh variables. `
1121
+ + 'If a single answer is intended, make it deterministic with once/1 or !.');
1122
+ }
1123
+ else if (verdict === 'timeout') {
1124
+ warnings.push(`${name}/${arity} did not settle within the inference limit when probed `
1125
+ + '(possible unbounded search, e.g. label/1 without once/1).');
1126
+ }
1127
+ }
1128
+ catch {
1129
+ // A probe failure must never fail the commit.
1130
+ }
1131
+ }
1132
+ return warnings;
1133
+ }
1134
+ /** Predicates touched by edit tools in the current transaction. */
1135
+ editTouched() {
1136
+ const result = this.prologQuery(`deepclause_mi:repl_edit_touched_text('${this.sessionId}', Text)`);
1137
+ return String(plainValue(result?.Text) ?? '')
1138
+ .split(',')
1139
+ .map((entry) => entry.trim())
1140
+ .filter(Boolean);
1141
+ }
1142
+ async rollbackTurn() {
1143
+ if (this.transactionBefore === null)
1144
+ return;
1145
+ this.prologQuery(`deepclause_mi:repl_snapshot_restore('${this.sessionId}')`);
1146
+ this.transactionBefore = null;
1147
+ this.lastTurnCommitted = false;
1148
+ this.lastCommitVersion = undefined;
1149
+ this.transactionDirty = false;
1150
+ }
1151
+ simplePredicateDiff(before, after) {
1152
+ const beforeSet = new Set(before);
1153
+ const afterSet = new Set(after);
1154
+ const changed = [];
1155
+ for (const predicate of after)
1156
+ if (!beforeSet.has(predicate))
1157
+ changed.push(`+${predicate}`);
1158
+ for (const predicate of before)
1159
+ if (!afterSet.has(predicate))
1160
+ changed.push(`-${predicate}`);
1161
+ return changed.sort();
1162
+ }
1163
+ /** Edit tools require an open transaction (opened by `begin_turn`). */
1164
+ requireTransaction() {
1165
+ if (this.transactionBefore === null) {
1166
+ return { ok: false, error: 'no open transaction; call begin_turn before editing' };
1167
+ }
1168
+ return undefined;
1169
+ }
1170
+ editResult(result) {
1171
+ if (result === 'ok') {
1172
+ this.transactionDirty = true;
1173
+ return { ok: true };
1174
+ }
1175
+ return { ok: false, error: String(result) };
1176
+ }
1177
+ async define(head, clauses) {
1178
+ const closed = this.requireTransaction();
1179
+ if (closed)
1180
+ return closed;
1181
+ const result = this.prologQuery(`deepclause_mi:repl_define('${this.sessionId}', ${quoteString(head)}, ${quoteString(clauses)}, Result)`);
1182
+ return this.editResult(plainValue(result?.Result));
1183
+ }
1184
+ async assertClause(clause) {
1185
+ const closed = this.requireTransaction();
1186
+ if (closed)
1187
+ return closed;
1188
+ const result = this.prologQuery(`deepclause_mi:repl_assert('${this.sessionId}', ${quoteString(clause)}, Result)`);
1189
+ return this.editResult(plainValue(result?.Result));
1190
+ }
1191
+ async assertMany(clauses) {
1192
+ const closed = this.requireTransaction();
1193
+ if (closed)
1194
+ return closed;
1195
+ const result = this.prologQuery(`deepclause_mi:repl_assert_many('${this.sessionId}', ${quoteString(clauses)}, Result)`);
1196
+ return this.editResult(plainValue(result?.Result));
1197
+ }
1198
+ async retractClause(clause) {
1199
+ const closed = this.requireTransaction();
1200
+ if (closed)
1201
+ return closed;
1202
+ const result = this.prologQuery(`deepclause_mi:repl_retract('${this.sessionId}', ${quoteString(clause)}, Result)`);
1203
+ return this.editResult(plainValue(result?.Result));
1204
+ }
1205
+ async deletePredicate(nameArity) {
1206
+ const closed = this.requireTransaction();
1207
+ if (closed)
1208
+ return closed;
1209
+ const match = /^\s*([^\s/]+)\s*\/\s*(\d+)\s*$/.exec(nameArity);
1210
+ if (!match) {
1211
+ return { ok: false, error: `expected Name/Arity, got '${nameArity}'` };
1212
+ }
1213
+ const [, name, arityText] = match;
1214
+ const atom = /^[a-z][A-Za-z0-9_]*$/.test(name) ? name : `'${name.replace(/'/g, "''")}'`;
1215
+ const result = this.prologQuery(`deepclause_mi:repl_delete_predicate('${this.sessionId}', ${atom}, ${Number(arityText)}, Result)`);
1216
+ return this.editResult(plainValue(result?.Result));
1217
+ }
1218
+ async reference(topic) {
1219
+ switch (topic.trim().toLowerCase()) {
1220
+ case 'language':
1221
+ case 'dml':
1222
+ return this.loadDmlReference();
1223
+ case 'authoring':
1224
+ return this.loadDmlAuthoring();
1225
+ case 'examples':
1226
+ return this.loadDocAsset('harness-examples');
1227
+ case 'patterns':
1228
+ case 'harness-patterns':
1229
+ return this.loadDocAsset('harness-patterns');
1230
+ case 'snippets':
1231
+ return this.loadDocAsset('prolog-snippets');
1232
+ case 'tools':
1233
+ return this.hostToolsCatalog ?? '(no host capability tools registered)';
1234
+ default:
1235
+ return `unknown reference topic '${topic}'. Topics: language, authoring, examples, patterns, snippets, tools.`;
1236
+ }
1237
+ }
1238
+ async execute(goal, limit, signal, onEvent) {
1239
+ return this.executeGoal(goal, limit ?? 5, onEvent, signal);
1240
+ }
1241
+ /**
1242
+ * Prompt mode: English request -> meta task -> DML -> commit -> entry run.
1243
+ *
1244
+ * The meta task runs in a `meta` frame with exactly the host meta tools.
1245
+ * Its scratch edits are discarded; the returned DML source is the single
1246
+ * source of truth and is committed replace-by-predicate. A failed commit
1247
+ * triggers at most one repair round; any failure leaves the harness
1248
+ * byte-identical.
1249
+ */
1250
+ /** Prompt mode: author the harness through the TypeScript agent. */
1251
+ async *prompt(request, options = {}) {
1252
+ yield* this.agentPrompt(request, options);
1253
+ }
1254
+ async dispose() {
1255
+ if (this.disposed)
1256
+ return;
1257
+ this.disposed = true;
1258
+ if (this.history) {
1259
+ try {
1260
+ await this.persistSessionHarness(this.history.currentVersion);
1261
+ this.assertHistoryFacts(await this.history.endSession('dispose'));
1262
+ }
1263
+ catch (error) {
1264
+ this.warnHistory(error);
1265
+ }
1266
+ }
1267
+ this.prologQuery(`deepclause_mi:destroy_session('${this.sessionId}')`);
1268
+ this.runner.clearFrames();
1269
+ await this.runner.dispose();
1270
+ }
1271
+ buildRunOptions(options) {
1272
+ const inputHandler = options.onInput ?? this.inputHandler;
1273
+ return {
1274
+ workspacePath: this.workspacePath,
1275
+ gasLimit: options.gasLimit ?? this.gasLimit,
1276
+ tools: options.tools ?? this.tools,
1277
+ toolPolicy: options.toolPolicy ?? this.toolPolicy,
1278
+ onInputRequired: inputHandler
1279
+ ? (prompt) => this.requireInput(prompt)
1280
+ : async () => undefined,
1281
+ signal: options.signal,
1282
+ onModelCall: () => this.reserveModelCall(),
1283
+ };
1284
+ }
1285
+ // ---- TypeScript compaction (ported from pi) ----
1286
+ /** Read a live memory slot as `MemoryMessage[]`. */
1287
+ getMemory(slot) {
1288
+ const result = this.prologQuery(`deepclause_mi:repl_get_memory('${this.sessionId}', ${slot}, Messages)`);
1289
+ const raw = toJsValue(result?.Messages);
1290
+ if (!Array.isArray(raw))
1291
+ return [];
1292
+ const messages = [];
1293
+ for (const entry of raw) {
1294
+ const value = toJsValue(entry);
1295
+ if (!value || typeof value !== 'object')
1296
+ continue;
1297
+ const role = String(toJsValue(value.role) ?? 'user');
1298
+ const content = String(toJsValue(value.content) ?? '');
1299
+ if (role !== 'system' && role !== 'user' && role !== 'assistant')
1300
+ continue;
1301
+ messages.push({ role, content });
1302
+ }
1303
+ return messages;
1304
+ }
1305
+ /** Replace a live memory slot. */
1306
+ setMemory(slot, messages) {
1307
+ const term = messages
1308
+ .map((message) => `message{role: ${message.role}, content: ${quoteString(message.content)}}`)
1309
+ .join(', ');
1310
+ this.prologQuery(`deepclause_mi:repl_set_memory('${this.sessionId}', ${slot}, [${term}])`);
1311
+ }
1312
+ /** Build the `memory_compaction` event for a compaction result. */
1313
+ compactionEvent(scope, trigger, result) {
1314
+ const event = toCompactionEvent(scope, trigger, result);
1315
+ const suffix = event.error ? ` (${event.error})` : '';
1316
+ return {
1317
+ type: 'memory_compaction',
1318
+ content: `compact ${scope}.${trigger} ${event.action} ${event.beforeTokens} -> ${event.afterTokens} tokens${suffix}`,
1319
+ compactionScope: scope,
1320
+ compactionTrigger: trigger,
1321
+ compactionAction: event.action,
1322
+ beforeTokens: event.beforeTokens,
1323
+ afterTokens: event.afterTokens,
1324
+ compactionError: event.error,
1325
+ };
1326
+ }
1327
+ /** Loop compaction hook: run before each model call inside a frame. */
1328
+ async compactLoopMessages(messages, lastInputTokens, contextWindow) {
1329
+ const settings = this.compactionSettings;
1330
+ if (!settings?.enabled || !this.llmBackend) {
1331
+ return { messages };
1332
+ }
1333
+ const contextTokens = lastInputTokens ?? estimateMemoryTokens(messages);
1334
+ if (!shouldCompact(contextTokens, contextWindow ?? this.contextWindow ?? 0, settings)) {
1335
+ return { messages };
1336
+ }
1337
+ const result = await compactMemory({
1338
+ messages,
1339
+ backend: this.llmBackend,
1340
+ model: this.model,
1341
+ settings,
1342
+ force: true,
1343
+ });
1344
+ return result.applied
1345
+ ? { messages: result.messages, event: this.compactionEvent('loop', 'before_model_call', result) }
1346
+ : { messages };
1347
+ }
1348
+ /**
1349
+ * Explicitly compact the authoring conversation. Used by `compact.`; returns
1350
+ * the before/after token counts.
1351
+ */
1352
+ async compact(customInstructions) {
1353
+ if (!this.harnessAgent) {
1354
+ return { applied: false, beforeTokens: 0, afterTokens: 0, error: 'compaction is disabled' };
1355
+ }
1356
+ const result = await this.harnessAgent.compactMemory(true, undefined, customInstructions);
1357
+ return {
1358
+ applied: result.applied,
1359
+ beforeTokens: result.tokensBefore,
1360
+ afterTokens: result.tokensAfter,
1361
+ error: result.error,
1362
+ };
1363
+ }
1364
+ // ---- Workspace file tools (meta task) ----
1365
+ /** Resolve a workspace-relative path, rejecting traversal/absolute paths. */
1366
+ resolveWorkspacePath(relPath) {
1367
+ const workspace = path.resolve(this.workspacePath ?? process.cwd());
1368
+ if (path.isAbsolute(relPath)) {
1369
+ throw new Error(`path must be workspace-relative: ${relPath}`);
1370
+ }
1371
+ const abs = path.resolve(workspace, relPath);
1372
+ if (abs !== workspace && !abs.startsWith(workspace + path.sep)) {
1373
+ throw new Error(`path escapes the workspace: ${relPath}`);
1374
+ }
1375
+ return abs;
1376
+ }
1377
+ normalizeRel(relPath) {
1378
+ return path.relative(path.resolve(this.workspacePath ?? process.cwd()), this.resolveWorkspacePath(relPath))
1379
+ .split(path.sep).join('/');
1380
+ }
1381
+ /** Read a workspace file. */
1382
+ async readWorkspaceFile(relPath) {
1383
+ try {
1384
+ return await fs.readFile(this.resolveWorkspacePath(this.normalizeRel(relPath)), 'utf8');
1385
+ }
1386
+ catch {
1387
+ return null;
1388
+ }
1389
+ }
1390
+ /**
1391
+ * Replace the human-in-the-loop channel after construction. The CLI uses
1392
+ * this to route input through the REPL's own readline interface instead of a
1393
+ * second interface on the same stdin.
1394
+ */
1395
+ setInputHandler(handler) {
1396
+ this.inputHandler = handler;
1397
+ }
1398
+ /**
1399
+ * @deprecated Terminal images now flow as `media` events (rendered by the
1400
+ * host), so this is a no-op kept for backwards compatibility.
1401
+ */
1402
+ setImageHandler(_handler) {
1403
+ // no-op
1404
+ }
1405
+ /** Resolve a human answer, or undefined when no channel is configured. */
1406
+ async requireInput(prompt, options) {
1407
+ if (!this.inputHandler) {
1408
+ return undefined;
1409
+ }
1410
+ return this.inputHandler(prompt, options);
1411
+ }
1412
+ /**
1413
+ * Reserve one model call for the current turn. When the budget is reached,
1414
+ * the human is asked whether to extend it by another `maxModelCalls`
1415
+ * (interactive runs); autonomous runs abort. `0` means unlimited.
1416
+ */
1417
+ async reserveModelCall() {
1418
+ if (this.maxModelCalls <= 0) {
1419
+ this.modelCallsThisTurn += 1;
1420
+ return true;
1421
+ }
1422
+ if (this.modelCallsThisTurn >= this.modelCallBudget) {
1423
+ const extend = await this.askBudgetExtension();
1424
+ if (!extend) {
1425
+ return false;
1426
+ }
1427
+ this.modelCallBudget += this.maxModelCalls;
1428
+ }
1429
+ this.modelCallsThisTurn += 1;
1430
+ return true;
1431
+ }
1432
+ async askBudgetExtension() {
1433
+ if (!this.inputHandler) {
1434
+ return false;
1435
+ }
1436
+ const answer = await this.inputHandler(`Max model-call budget reached (${this.modelCallBudget}). Continue for another ${this.maxModelCalls} calls? (yes/no)\n`
1437
+ + 'change the limit: repl.maxModelCalls in .deepclause/config.json '
1438
+ + '("repl": { "maxModelCalls": N }), or pass --max-model-calls N; 0 means unlimited', ['yes', 'no']);
1439
+ return /^(y|yes)$/i.test((answer ?? '').trim());
1440
+ }
1441
+ prologQuery(goal) {
1442
+ try {
1443
+ const result = this.swipl.prolog.query(goal).once();
1444
+ return (result ?? null);
1445
+ }
1446
+ catch (error) {
1447
+ if (process.env.DEBUG_REPL) {
1448
+ console.error('[repl] Prolog query failed:', goal, error);
1449
+ }
1450
+ return null;
1451
+ }
1452
+ }
1453
+ serializeMemory(messages) {
1454
+ return messages
1455
+ .map((message) => `message{role: ${message.role}, content: ${quoteString(message.content)}}`)
1456
+ .join(', ');
1457
+ }
1458
+ }
1459
+ /** Quote a JS string as a Prolog double-quoted string. */
1460
+ export function quoteString(value) {
1461
+ return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
1462
+ }
1463
+ /** Turn `fact/2` into the Prolog term `fact/2`; other filters become atoms. */
1464
+ function toListingFilter(filter) {
1465
+ if (/^[a-z][a-zA-Z0-9_]*\/\d+$/.test(filter)) {
1466
+ return filter;
1467
+ }
1468
+ if (/^[a-z][a-zA-Z0-9_]*$/.test(filter)) {
1469
+ return filter;
1470
+ }
1471
+ return `'${filter.replace(/'/g, "''")}'`;
1472
+ }
1473
+ //# sourceMappingURL=session.js.map