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