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
@@ -20,12 +20,79 @@
20
20
  create_engine/6,
21
21
  step_engine/4,
22
22
  destroy_engine/1,
23
+ % Frame predicates (M0: reentrant, frame-aware execution)
24
+ create_frame/6,
25
+ create_frame_raw/5,
26
+ step_frame/4,
27
+ end_frame/1,
28
+ frame_info/4,
29
+ default_frame/2,
30
+ % REPL session and query mode (M1)
31
+ create_session/4,
32
+ clear_session/1,
33
+ destroy_session/1,
34
+ load_code/3,
35
+ repl_initial_state/3,
36
+ query_start/5,
37
+ repl_solve/4,
38
+ collect_bindings/2,
39
+ repl_listing/2,
40
+ repl_listing/3,
41
+ repl_reserved/1,
42
+ % Freeze lock and export (M2)
43
+ repl_freeze/1,
44
+ repl_unfreeze/1,
45
+ repl_lock/1,
46
+ repl_is_frozen/1,
47
+ repl_try_unfreeze/2,
48
+ export_harness/3,
49
+ export_stats/4,
50
+ repl_script_files/2,
51
+ repl_materialize_embedded/1,
52
+ % Prompt mode (M3)
53
+ repl_meta_prompt/2,
54
+ repl_meta_prompt/3,
55
+ repl_extract_code/2,
56
+ repl_assert/3,
57
+ repl_assert_many/3,
58
+ repl_retract/3,
59
+ repl_define/4,
60
+ repl_delete_predicate/4,
61
+ repl_edit_touched/2,
62
+ repl_edit_touched_text/2,
63
+ repl_edit_touched_clear/1,
64
+ repl_set_dml_reference/2,
65
+ repl_set_authoring_examples/2,
66
+ repl_manifest/2,
67
+ repl_history_load/2,
68
+ repl_history_record/2,
69
+ repl_history_summary/2,
70
+ repl_rollback/3,
71
+ repl_user_predicates/2,
72
+ repl_user_predicates_text/2,
73
+ repl_predicate_arities/3,
74
+ repl_probe_nondeterminism/6,
75
+ repl_set_meta_catalog/2,
76
+ repl_set_meta_template/2,
77
+ repl_set_harness_examples/2,
78
+ repl_set_prolog_snippets/2,
79
+ repl_run_meta/4,
80
+ repl_snapshot_save/1,
81
+ repl_snapshot_restore/1,
82
+ repl_commit/3,
83
+ repl_last_commit_summary/2,
84
+ repl_execute_frame/4,
85
+ repl_record_meta/3,
86
+ repl_memory_term/3,
87
+ repl_get_memory/3,
88
+ repl_set_memory/3,
23
89
  post_agent_result/4,
24
90
  post_exec_result/3,
25
91
  post_sample_token_result/3,
26
92
  post_llm_result/3,
27
93
  post_judgment_result/3,
28
94
  provide_input/2,
95
+ provide_input_cancelled/1,
29
96
  mi/3,
30
97
  % Tool engine predicates for isolated tool execution
31
98
  create_tool_engine/4,
@@ -42,7 +109,25 @@
42
109
  :- discontiguous var_to_name/3.
43
110
 
44
111
  %% Dynamic predicates for session state (engine management only, not memory!)
45
- :- dynamic session_engine/2. % session_engine(SessionId, Engine)
112
+ :- dynamic session_engine/2. % session_engine(SessionId, Engine) - legacy default engine handle
113
+ :- dynamic session_frame/4. % session_frame(FrameId, SessionId, Kind, Depth)
114
+ :- dynamic session_frame_engine/2. % session_frame_engine(FrameId, Engine)
115
+ :- dynamic session_default_frame/2. % session_default_frame(SessionId, FrameId)
116
+ :- dynamic session_repl_memory/2. % session_repl_memory(SessionId, HarnessMemory) - object/entry memory
117
+ :- dynamic session_meta_memory/2. % session_meta_memory(SessionId, MetaMemory) - code-writing task memory
118
+ :- dynamic session_repl_mode/2. % session_repl_mode(SessionId, true/false) - gates format/writeln output shims
119
+ :- dynamic session_frozen/2. % session_frozen(SessionId, true/false) - read-only lock
120
+ :- dynamic session_locked/2. % session_locked(SessionId, true/false) - irreversibly frozen (--locked)
121
+ :- dynamic session_directive/2. % session_directive(SessionId, Directive) - recorded :- directives for export
122
+ :- dynamic session_meta_catalog/2. % session_meta_catalog(SessionId, CatalogText) - host tool catalog for the meta prompt
123
+ :- dynamic session_dml_reference/2. % session_dml_reference(SessionId, Text) - bundled DML reference for the meta prompt
124
+ :- dynamic session_authoring_examples/2. % session_authoring_examples(SessionId, Text) - curated authoring patterns
125
+ :- dynamic session_meta_template/2. % session_meta_template(SessionId, Template) - bundled META_PROMPT.md template
126
+ :- dynamic session_harness_examples/2. % session_harness_examples(SessionId, Text) - bundled HARNESS_EXAMPLES.md
127
+ :- dynamic session_prolog_snippets/2. % session_prolog_snippets(SessionId, Text) - bundled PROLOG_SNIPPETS.md
128
+ :- dynamic session_last_commit/2. % session_last_commit(SessionId, [Name/Arity]) - predicates the last commit replaced
129
+ :- dynamic session_edit_touched/2. % session_edit_touched(SessionId, Name/Arity) - agent-mode edit transactions
130
+ :- dynamic repl_snapshot/2. % repl_snapshot(SessionId, Snapshot) - transactional edit snapshot
46
131
  :- dynamic session_params/2. % session_params(SessionId, ParamsDict)
47
132
  :- dynamic session_user_tools/2. % session_user_tools(SessionId, ToolName)
48
133
  :- dynamic session_user_tool_schema/3. % session_user_tool_schema(SessionId, ToolName, Schema)
@@ -56,6 +141,7 @@
56
141
  :- dynamic session_trace_enabled/2. % session_trace_enabled(SessionId, true/false)
57
142
  :- dynamic session_pending_signal/2. % session_pending_signal(SessionId, Signal) - fallback for engine_fetch
58
143
  :- dynamic session_user_input/2. % session_user_input(SessionId, Input) - for ask_user tool
144
+ :- dynamic session_input_cancelled/1. % session_input_cancelled(SessionId) - input/2 should fail (non-interactive)
59
145
 
60
146
  %% ============================================================
61
147
  %% DML Parsing
@@ -251,6 +337,7 @@ extract_var_info(Bindings, Term, BareVar, Info) :-
251
337
  ).
252
338
 
253
339
  %% wrapper_type(?Wrapper, ?Type)
340
+ wrapper_type(media, media).
254
341
  wrapper_type(string, string).
255
342
  wrapper_type(integer, integer).
256
343
  wrapper_type(float, number).
@@ -260,6 +347,183 @@ wrapper_type(list, array).
260
347
  wrapper_type(object, object).
261
348
  wrapper_type(dict, object).
262
349
 
350
+ %% ============================================================
351
+ %% Source-facing view of user clauses
352
+ %%
353
+ %% At load time task/N and prompt/N are rewritten to task_named/3 and
354
+ %% prompt_named/3, and interpolated descriptions are hoisted into a preceding
355
+ %% format/3. Those internal forms must not leak into listing/export — the meta
356
+ %% task only knows task/N — so reverse them for display only.
357
+ %% ============================================================
358
+
359
+ %% clean_clause_body(+Body, -CleanBody)
360
+ clean_clause_body(Body, Clean) :-
361
+ conj_list(Body, Goals),
362
+ clean_goal_list(Goals, CleanGoals),
363
+ ( CleanGoals = [Single]
364
+ -> Clean = Single
365
+ ; list_to_conjunction(CleanGoals, Clean)
366
+ ).
367
+
368
+ conj_list((A, B), List) :-
369
+ !,
370
+ conj_list(A, LA),
371
+ conj_list(B, LB),
372
+ append(LA, LB, List).
373
+ conj_list(Goal, [Goal]).
374
+
375
+ clean_goal_list([], []) :- !.
376
+ clean_goal_list([Goal], [Clean]) :-
377
+ !,
378
+ clean_atomic(Goal, Clean).
379
+ clean_goal_list([First, Second | Rest], [CleanFirst | CleanRest]) :-
380
+ ( merge_interpolation(First, Second, CleanFirst)
381
+ -> clean_goal_list(Rest, CleanRest)
382
+ ; clean_atomic(First, CleanFirst),
383
+ clean_goal_list([Second | Rest], CleanRest)
384
+ ).
385
+
386
+ %% merge_interpolation(+FormatGoal, +TaskGoal, -Merged)
387
+ %% Fold `format(string(D), T, Args), task_named(D, ...)` back into
388
+ %% `task("…{Var}…", ...)`.
389
+ merge_interpolation(FormatGoal, TaskGoal, Merged) :-
390
+ FormatGoal = format(string(D), Template, Args),
391
+ ( TaskGoal = task_named(D, Vars, Infos)
392
+ ; TaskGoal = prompt_named(D, Vars, Infos)
393
+ ),
394
+ task_functor(TaskGoal, Functor),
395
+ interpolate_template(Template, Args, Description),
396
+ clean_task(Functor, Description, Vars, Infos, Merged).
397
+
398
+ task_functor(task_named(_, _, _), task).
399
+ task_functor(prompt_named(_, _, _), prompt).
400
+
401
+ clean_atomic(task_named(D, Vars, Infos), Clean) :-
402
+ !,
403
+ clean_task(task, D, Vars, Infos, Clean).
404
+ clean_atomic(prompt_named(D, Vars, Infos), Clean) :-
405
+ !,
406
+ clean_task(prompt, D, Vars, Infos, Clean).
407
+ clean_atomic((A ; B), (CA ; CB)) :-
408
+ !,
409
+ clean_clause_body(A, CA),
410
+ clean_clause_body(B, CB).
411
+ clean_atomic((A -> B), (CA -> CB)) :-
412
+ !,
413
+ clean_clause_body(A, CA),
414
+ clean_clause_body(B, CB).
415
+ clean_atomic((A -> B ; C), (CA -> CB ; CC)) :-
416
+ !,
417
+ clean_clause_body(A, CA),
418
+ clean_clause_body(B, CB),
419
+ clean_clause_body(C, CC).
420
+ clean_atomic(\+(A), \+ CA) :-
421
+ !,
422
+ clean_clause_body(A, CA).
423
+ clean_atomic(not(A), not(CA)) :-
424
+ !,
425
+ clean_clause_body(A, CA).
426
+ % Wrappers: recurse into their goal arguments so a task/prompt inside is also
427
+ % reversed (the load-time rewrite descends into these via transform_task_calls).
428
+ clean_atomic(with_tools(Tools, Goal), with_tools(Tools, CleanGoal)) :-
429
+ !,
430
+ clean_clause_body(Goal, CleanGoal).
431
+ clean_atomic(without_tools(Tools, Goal), without_tools(Tools, CleanGoal)) :-
432
+ !,
433
+ clean_clause_body(Goal, CleanGoal).
434
+ clean_atomic(with_judgment(Backend, Goal), with_judgment(Backend, CleanGoal)) :-
435
+ !,
436
+ clean_clause_body(Goal, CleanGoal).
437
+ clean_atomic(require_judgment(Backend, Goal), require_judgment(Backend, CleanGoal)) :-
438
+ !,
439
+ clean_clause_body(Goal, CleanGoal).
440
+ clean_atomic(once(Goal), once(CleanGoal)) :-
441
+ !,
442
+ clean_clause_body(Goal, CleanGoal).
443
+ clean_atomic(ignore(Goal), ignore(CleanGoal)) :-
444
+ !,
445
+ clean_clause_body(Goal, CleanGoal).
446
+ clean_atomic(catch(Goal, Catcher, Recovery), catch(CleanGoal, Catcher, CleanRecovery)) :-
447
+ !,
448
+ clean_clause_body(Goal, CleanGoal),
449
+ clean_clause_body(Recovery, CleanRecovery).
450
+ clean_atomic(forall(Generator, Action), forall(CleanGenerator, CleanAction)) :-
451
+ !,
452
+ clean_clause_body(Generator, CleanGenerator),
453
+ clean_clause_body(Action, CleanAction).
454
+ clean_atomic(findall(Template, Goal, List), findall(Template, CleanGoal, List)) :-
455
+ !,
456
+ clean_clause_body(Goal, CleanGoal).
457
+ clean_atomic(Module:Goal, Module:CleanGoal) :-
458
+ !,
459
+ clean_clause_body(Goal, CleanGoal).
460
+ clean_atomic(Goal, Goal).
461
+
462
+ %% clean_task(+Functor, +Description, +Vars, +Infos, -Clean)
463
+ clean_task(Functor, Description, Vars, Infos, Clean) :-
464
+ clean_typed_args(Vars, Infos, CleanVars),
465
+ Clean =.. [Functor, Description | CleanVars].
466
+
467
+ clean_typed_args([], [], []) :- !.
468
+ clean_typed_args([Var | Vars], [Info | Infos], [Wrapped | Wrappeds]) :-
469
+ !,
470
+ typed_arg(Var, Info, Wrapped),
471
+ clean_typed_args(Vars, Infos, Wrappeds).
472
+ clean_typed_args(Vars, _, Vars).
473
+
474
+ %% typed_arg(+Var, +Info, -Wrapped) — reverse typed_var{...} to its wrapper.
475
+ typed_arg(Var, Info, Wrapped) :-
476
+ is_dict(Info),
477
+ get_dict(type, Info, Type),
478
+ !,
479
+ typed_arg_type(Type, Var, Info, Wrapped).
480
+ typed_arg(Var, _, Var).
481
+
482
+ typed_arg_type(media, Var, _, media(Var)) :- !.
483
+ typed_arg_type(string, Var, _, Var) :- !.
484
+ typed_arg_type(integer, Var, _, integer(Var)) :- !.
485
+ typed_arg_type(number, Var, _, number(Var)) :- !.
486
+ typed_arg_type(boolean, Var, _, boolean(Var)) :- !.
487
+ typed_arg_type(object, Var, _, object(Var)) :- !.
488
+ typed_arg_type(array, Var, Info, list(Wrapped)) :-
489
+ !,
490
+ ( get_dict(itemType, Info, ItemType)
491
+ -> true
492
+ ; ItemType = string
493
+ ),
494
+ Wrapped =.. [ItemType, Var].
495
+ typed_arg_type(_, Var, _, Var).
496
+
497
+ %% interpolate_template(+Template, +Args, -Description)
498
+ %% Reverse of the {Var} -> ~w hoist. Succeeds only when every placeholder is a
499
+ %% plain variable, so the caller can fall back to keeping the format/3 goal.
500
+ interpolate_template(Template, Args, Description) :-
501
+ string(Template),
502
+ string_codes(Template, Codes),
503
+ interpolate_codes(Codes, Args, OutCodes),
504
+ string_codes(Description, OutCodes).
505
+
506
+ interpolate_codes([], [], []) :- !.
507
+ %% A {Var} placeholder consumes one argument; require Args to be exhausted
508
+ %% exactly (the []/[] base case above fails if either side has leftovers).
509
+ interpolate_codes([0'~, 0'w | Rest], [Arg | Args], Out) :-
510
+ !,
511
+ named_var(Arg),
512
+ term_string(Arg, Name, [numbervars(true)]),
513
+ string_codes(Name, NameCodes),
514
+ interpolate_codes(Rest, Args, Tail),
515
+ append([0'{|NameCodes], [0'}|Tail], Out).
516
+ interpolate_codes([0'~, 0'~ | Rest], Args, [0'~ | Out]) :-
517
+ !,
518
+ interpolate_codes(Rest, Args, Out).
519
+ interpolate_codes([Code | Rest], Args, [Code | Out]) :-
520
+ interpolate_codes(Rest, Args, Out).
521
+
522
+ %% An interpolation argument is a variable or a `$VAR(N)` term (after
523
+ %% numbervars), both of which print as a stable name (A, B, ...).
524
+ named_var(Arg) :- var(Arg), !.
525
+ named_var('$VAR'(_)).
526
+
263
527
  %% var_to_name(+Bindings, +Var, -Name)
264
528
  %% Look up a variable's name from the bindings list
265
529
  var_to_name(Bindings, Var, Name) :-
@@ -269,7 +533,15 @@ var_to_name(Bindings, Var, Name) :-
269
533
  ).
270
534
 
271
535
  %% process_clause(+Term, +SessionId)
272
- %% Process a single clause and add to session
536
+ %% Process a single clause and add to session.
537
+ %% Reject reserved-name definitions (third place of the reserved guard, so
538
+ %% consulted/generated code cannot shadow the REPL control surface).
539
+ process_clause(Term, _SessionId) :-
540
+ repl_reserved_clause(Term),
541
+ !,
542
+ throw(error(permission_error(modify, reserved_name, Term),
543
+ context(process_clause/2, 'reserved predicate'))).
544
+
273
545
  process_clause((:- Directive), SessionId) :-
274
546
  !,
275
547
  process_directive(Directive, SessionId).
@@ -281,8 +553,9 @@ process_clause((tool(ToolHead, Description) :- Body), SessionId) :-
281
553
  extract_tool_schema(ToolHead, Description, ToolName, Schema),
282
554
  assertz(session_user_tools(SessionId, ToolName)),
283
555
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
284
- % Store the source code for the tool description
285
- format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
556
+ % Store the source code for the tool description. Reverses the load-time
557
+ % task/prompt rewrites and normalizes variables for deterministic export.
558
+ format_tool_source(ToolHead, Description, Body, SourceCode),
286
559
  assertz(SessionId:tool_source(ToolName, SourceCode)),
287
560
  % Assert the tool implementation (use just ToolHead for execution)
288
561
  assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
@@ -294,8 +567,8 @@ process_clause((tool(ToolHead) :- Body), SessionId) :-
294
567
  extract_tool_schema(ToolHead, none, ToolName, Schema),
295
568
  assertz(session_user_tools(SessionId, ToolName)),
296
569
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
297
- % Store the source code for the tool description
298
- format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
570
+ % Store the source code for the tool description (reverses task rewrites).
571
+ format_tool_source(ToolHead, none, Body, SourceCode),
299
572
  assertz(SessionId:tool_source(ToolName, SourceCode)),
300
573
  % Assert the tool implementation
301
574
  assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
@@ -309,6 +582,15 @@ process_clause((Head :- Body), SessionId) :-
309
582
  ),
310
583
  assertz(SessionId:(Head :- ExpandedBody)).
311
584
 
585
+ %% repl_delete/1 is a load-time directive that removes a predicate. It must
586
+ %% be handled before the generic fact clause so it is never asserted itself.
587
+ process_clause(repl_delete(Name/Arity), SessionId) :-
588
+ !,
589
+ ( atom(Name), integer(Arity)
590
+ -> catch(abolish(SessionId:Name/Arity), _, true)
591
+ ; true
592
+ ).
593
+
312
594
  process_clause(Fact, SessionId) :-
313
595
  % Simple fact
314
596
  assertz(SessionId:Fact).
@@ -426,15 +708,27 @@ workspace_module_reference(Module) :-
426
708
  process_use_module_directive(Module, SessionId) :-
427
709
  ( workspace_module_reference(Module)
428
710
  -> process_directive(consult(Module), SessionId)
429
- ; SessionId:use_module(Module)
711
+ ; SessionId:use_module(Module),
712
+ import_library_into_mi(Module)
430
713
  ).
431
714
 
432
715
  process_use_module_directive(Module, Exports, SessionId) :-
433
716
  ( workspace_module_reference(Module)
434
717
  -> process_directive(consult(Module), SessionId)
435
- ; SessionId:use_module(Module, Exports)
718
+ ; SessionId:use_module(Module, Exports),
719
+ import_library_into_mi(Module)
436
720
  ).
437
721
 
722
+ %% System libraries are also imported into the meta-interpreter module itself,
723
+ %% so meta-calls (`call_with_inference_limit/3`, `once/1`, `catch/3`, ...) can
724
+ %% resolve them. Without this, `call_with_inference_limit(label(Xs), ...)` fails
725
+ %% with `existence_error(deepclause_mi:label/1)` because clpfd was imported only
726
+ %% into the session module.
727
+ import_library_into_mi(library(Name)) :-
728
+ !,
729
+ catch(deepclause_mi:use_module(library(Name)), _, true).
730
+ import_library_into_mi(_).
731
+
438
732
  %% process_directive(+Directive, +SessionId)
439
733
  process_directive(param(Key, Desc), SessionId) :-
440
734
  !,
@@ -454,9 +748,17 @@ process_directive(consult(File), SessionId) :-
454
748
  %% Handle use_module directive
455
749
  process_directive(use_module(Module), SessionId) :-
456
750
  !,
751
+ ( workspace_module_reference(Module)
752
+ -> true
753
+ ; record_directive(SessionId, use_module(Module))
754
+ ),
457
755
  process_use_module_directive(Module, SessionId).
458
756
  process_directive(use_module(Module, Exports), SessionId) :-
459
757
  !,
758
+ ( workspace_module_reference(Module)
759
+ -> true
760
+ ; record_directive(SessionId, use_module(Module, Exports))
761
+ ),
460
762
  process_use_module_directive(Module, Exports, SessionId).
461
763
  %% Handle list-style consult: :- [File] or :- [File1, File2, ...]
462
764
  process_directive([File|Files], SessionId) :-
@@ -473,6 +775,9 @@ process_directive(_, _).
473
775
  %% create_engine(+SessionId, +MemoryId, +Args, +Params, +InitialMemory, -Engine)
474
776
  %% Args is a list of positional arguments for agent_main
475
777
  %% Params is a dict of named parameters (already asserted as param/2 facts)
778
+ %%
779
+ %% The engine is registered as the session's default `query` frame so that
780
+ %% step_engine/4 keeps working as a thin wrapper over step_frame/4.
476
781
  create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
477
782
  assertz(session_params(SessionId, Params)),
478
783
  determine_agent_goal(SessionId, Args, Goal),
@@ -484,12 +789,1712 @@ create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
484
789
  (get_dict(gas_limit, Params, GasLimit) -> true ; GasLimit = -1),
485
790
  % Create initial state with provided memory, params, context stack, trace depth and gas
486
791
  InitialState = state{memory: InitialMemory, params: Params, context_stack: [], depth: 0, gas_remaining: GasLimit},
487
- % Create the engine - pass SessionId and initial state to mi/3
488
- engine_create(_,
489
- deepclause_mi:mi(Goal, InitialState, SessionId),
490
- Engine),
792
+ create_frame(SessionId, query, 0, Goal, InitialState, FrameId),
793
+ retractall(session_default_frame(SessionId, _)),
794
+ assertz(session_default_frame(SessionId, FrameId)),
795
+ session_frame_engine(FrameId, Engine),
796
+ retractall(session_engine(SessionId, _)),
491
797
  assertz(session_engine(SessionId, Engine)).
492
798
 
799
+ %% ============================================================
800
+ %% Frames (re-entrant execution)
801
+ %% ============================================================
802
+ %%
803
+ %% Historically a session owned exactly one engine (session_engine/2).
804
+ %% Prompt mode requires nested engines: a meta frame that calls `execute`
805
+ %% which runs a child frame, which may itself call task() and spawn a
806
+ %% further frame. A frame is therefore (FrameId, SessionId, Kind, Depth)
807
+ %% with its engine stored in session_frame_engine/2.
808
+ %%
809
+ %% The legacy step_engine/4 resolves the session's default query frame and
810
+ %% delegates to step_frame/4, so single-engine callers are unaffected.
811
+
812
+ default_frame(SessionId, FrameId) :-
813
+ session_default_frame(SessionId, FrameId).
814
+
815
+ %% frame_info(+FrameId, -SessionId, -Kind, -Depth)
816
+ frame_info(FrameId, SessionId, Kind, Depth) :-
817
+ session_frame(FrameId, SessionId, Kind, Depth).
818
+
819
+ %% create_frame_raw(+SessionId, +Kind, +Depth, +EngineGoal, -FrameId)
820
+ %% Registers a frame whose engine runs EngineGoal verbatim. Most callers want
821
+ %% create_frame/6, which wraps the goal in the mi/3 meta-interpreter; goal mode
822
+ %% uses this to run repl_solve/4 directly (mi/3 cuts after the first solution).
823
+ create_frame_raw(SessionId, Kind, Depth, EngineGoal, FrameId) :-
824
+ gensym(frame_, FrameId),
825
+ engine_create(_, EngineGoal, Engine),
826
+ assertz(session_frame(FrameId, SessionId, Kind, Depth)),
827
+ assertz(session_frame_engine(FrameId, Engine)).
828
+
829
+ %% create_frame(+SessionId, +Kind, +Depth, +Goal, +InitialState, -FrameId)
830
+ %% Kind = query | meta | nested | object
831
+ %% Creates an engine running deepclause_mi:mi(Goal, InitialState, SessionId)
832
+ %% and registers it as a frame. The caller is responsible for depth-first
833
+ %% stepping: a parent frame is suspended while a child frame runs, so the
834
+ %% per-session result/signal slots are never concurrently pending.
835
+ create_frame(SessionId, Kind, Depth, Goal, InitialState, FrameId) :-
836
+ create_frame_raw(SessionId, Kind, Depth,
837
+ deepclause_mi:mi(Goal, InitialState, SessionId),
838
+ FrameId).
839
+
840
+ %% step_frame(+FrameId, -Status, -Content, -Payload)
841
+ step_frame(FrameId, Status, Content, Payload) :-
842
+ ( session_frame(FrameId, SessionId, _Kind, _Depth),
843
+ session_frame_engine(FrameId, Engine)
844
+ -> step_engine_handle(SessionId, Engine, Status, Content, Payload)
845
+ ; Status = error,
846
+ Content = 'No engine found for frame',
847
+ Payload = none
848
+ ).
849
+
850
+ %% step_engine_handle(+SessionId, +Engine, -Status, -Content, -Payload)
851
+ %% Shared stepping logic for both step_frame/4 and the legacy step_engine/4.
852
+ step_engine_handle(SessionId, Engine, Status, Content, Payload) :-
853
+ catch(
854
+ ( engine_next(Engine, Result)
855
+ -> process_engine_result(Result, Status, Content, Payload)
856
+ ; % Engine finished - return trace if enabled
857
+ Status = finished,
858
+ Content = '',
859
+ ( session_trace_enabled(SessionId, true),
860
+ session_trace_log(SessionId, TraceLog)
861
+ -> Payload = payload{trace: TraceLog}
862
+ ; Payload = none
863
+ )
864
+ ),
865
+ Error,
866
+ ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
867
+ Status = error,
868
+ Content = ErrMsg,
869
+ ( session_trace_enabled(SessionId, true),
870
+ session_trace_log(SessionId, TraceLog)
871
+ -> Payload = payload{trace: TraceLog}
872
+ ; Payload = none
873
+ )
874
+ )
875
+ ).
876
+
877
+ %% end_frame(+FrameId)
878
+ %% Destroys the frame's engine and unregisters the frame. Also clears the
879
+ %% session's default-frame pointer if this frame was the default.
880
+ end_frame(FrameId) :-
881
+ ( session_frame_engine(FrameId, Engine)
882
+ -> catch(engine_destroy(Engine), _, true)
883
+ ; true
884
+ ),
885
+ ( session_frame(FrameId, SessionId, _, _)
886
+ -> retractall(session_default_frame(SessionId, FrameId))
887
+ ; true
888
+ ),
889
+ retractall(session_frame_engine(FrameId, _)),
890
+ retractall(session_frame(FrameId, _, _, _)).
891
+
892
+ %% ============================================================
893
+ %% REPL Session and Goal Mode (M1)
894
+ %% ============================================================
895
+ %%
896
+ %% A REPL session owns a persistent living module. Unlike create_engine/6,
897
+ %% which is tied to a single run and destroyed by destroy_engine/1, a session
898
+ %% lives across inputs; reset/clear_session empties it without tearing down the
899
+ %% process-level SWI instance.
900
+
901
+ %% create_session(+SessionId, +Params, +HarnessMemory, +MetaMemory)
902
+ create_session(SessionId, Params, HarnessMemory, MetaMemory) :-
903
+ retractall(session_params(SessionId, _)),
904
+ assertz(session_params(SessionId, Params)),
905
+ ( get_dict(trace, Params, true)
906
+ -> TraceEnabled = true
907
+ ; TraceEnabled = false
908
+ ),
909
+ retractall(session_trace_enabled(SessionId, _)),
910
+ assertz(session_trace_enabled(SessionId, TraceEnabled)),
911
+ retractall(session_trace_log(SessionId, _)),
912
+ assertz(session_trace_log(SessionId, [])),
913
+ retractall(session_repl_memory(SessionId, _)),
914
+ assertz(session_repl_memory(SessionId, HarnessMemory)),
915
+ retractall(session_meta_memory(SessionId, _)),
916
+ assertz(session_meta_memory(SessionId, MetaMemory)),
917
+ retractall(session_repl_mode(SessionId, _)),
918
+ assertz(session_repl_mode(SessionId, true)),
919
+ retractall(session_frozen(SessionId, _)),
920
+ assertz(session_frozen(SessionId, false)),
921
+ retractall(session_locked(SessionId, _)),
922
+ assertz(session_locked(SessionId, false)).
923
+
924
+ %% repl_abolish_user_predicates(+SessionId)
925
+ %% Removes only the harness's own predicates; imported builtins are untouched.
926
+ repl_abolish_user_predicates(SessionId) :-
927
+ ( current_module(SessionId)
928
+ -> findall(Name/Arity,
929
+ ( current_predicate(SessionId:Name/Arity),
930
+ repl_user_predicate(SessionId, Name, Arity)
931
+ ),
932
+ UserPreds0),
933
+ sort(UserPreds0, UserPreds),
934
+ forall(member(Name/Arity, UserPreds),
935
+ catch(abolish(SessionId:Name/Arity), _, true))
936
+ ; true
937
+ ).
938
+
939
+ %% clear_session(+SessionId)
940
+ %% Empties the living module, ends all frames, and resets memories. Session
941
+ %% params (workspace/gas limit) are preserved so the REPL stays configured.
942
+ clear_session(SessionId) :-
943
+ ( findall(FrameId, session_frame(FrameId, SessionId, _, _), FrameIds)
944
+ -> forall(member(FrameId, FrameIds), end_frame(FrameId))
945
+ ; true
946
+ ),
947
+ ( session_engine(SessionId, Engine)
948
+ -> catch(engine_destroy(Engine), _, true)
949
+ ; true
950
+ ),
951
+ retractall(session_engine(SessionId, _)),
952
+ retractall(session_default_frame(SessionId, _)),
953
+ retractall(session_repl_memory(SessionId, _)),
954
+ assertz(session_repl_memory(SessionId, [])),
955
+ retractall(session_meta_memory(SessionId, _)),
956
+ assertz(session_meta_memory(SessionId, [])),
957
+ retractall(repl_snapshot(SessionId, _)),
958
+ retractall(session_user_tools(SessionId, _)),
959
+ retractall(session_user_tool_schema(SessionId, _, _)),
960
+ repl_abolish_user_predicates(SessionId).
961
+
962
+ %% destroy_session(+SessionId)
963
+ %% Tears a REPL session down completely: clears the module/frames and removes
964
+ %% the session's dynamic bookkeeping so the process can host another session.
965
+ destroy_session(SessionId) :-
966
+ clear_session(SessionId),
967
+ retractall(session_repl_memory(SessionId, _)),
968
+ retractall(session_meta_memory(SessionId, _)),
969
+ retractall(session_repl_mode(SessionId, _)),
970
+ retractall(session_frozen(SessionId, _)),
971
+ retractall(session_locked(SessionId, _)),
972
+ retractall(session_directive(SessionId, _)),
973
+ retractall(session_meta_catalog(SessionId, _)),
974
+ retractall(session_dml_reference(SessionId, _)),
975
+ retractall(session_authoring_examples(SessionId, _)),
976
+ retractall(session_meta_template(SessionId, _)),
977
+ retractall(session_harness_examples(SessionId, _)),
978
+ retractall(session_prolog_snippets(SessionId, _)),
979
+ retractall(session_last_commit(SessionId, _)),
980
+ retractall(repl_snapshot(SessionId, _)),
981
+ retractall(session_params(SessionId, _)),
982
+ retractall(session_trace_enabled(SessionId, _)),
983
+ retractall(session_trace_log(SessionId, _)).
984
+
985
+ %% load_code(+SessionId, +Code, -Error)
986
+ %% Parses DML source into the session module, running the normal source
987
+ %% preprocessing (task/prompt rewrite, interpolation, expand_term).
988
+ load_code(SessionId, Code, Error) :-
989
+ catch(
990
+ ( guard_not_frozen(SessionId),
991
+ parse_dml_string(Code, SessionId),
992
+ Error = none
993
+ ),
994
+ LoadError,
995
+ format(atom(Error), '~w', [LoadError])
996
+ ).
997
+
998
+ %% repl_initial_state(+SessionId, +FrameKind, -InitialState)
999
+ %% Builds the state{} threaded through a REPL frame. M1 seeds harness memory
1000
+ %% from session_repl_memory/2; M4 adds the meta/scratch slots.
1001
+ repl_initial_state(SessionId, FrameKind, InitialState) :-
1002
+ session_params(SessionId, Params),
1003
+ ( get_dict(gas_limit, Params, GasLimit)
1004
+ -> true
1005
+ ; GasLimit = -1
1006
+ ),
1007
+ % Two-level memory: meta (code-writing) and harness (runtime) are kept
1008
+ % apart. The state carries the slot so write-back goes to the right one.
1009
+ ( FrameKind == meta
1010
+ -> MemorySlot = meta,
1011
+ ( session_meta_memory(SessionId, Memory)
1012
+ -> true
1013
+ ; Memory = []
1014
+ )
1015
+ ; MemorySlot = harness,
1016
+ ( session_repl_memory(SessionId, Memory)
1017
+ -> true
1018
+ ; Memory = []
1019
+ )
1020
+ ),
1021
+ InitialState = state{
1022
+ memory: Memory,
1023
+ params: Params,
1024
+ context_stack: [],
1025
+ depth: 0,
1026
+ gas_remaining: GasLimit,
1027
+ memory_slot: MemorySlot,
1028
+ frame_kind: FrameKind
1029
+ }.
1030
+
1031
+ %% query_start(+SessionId, +Text, +FrameKind, +InitialState, -Error)
1032
+ %% Parses and preprocesses a goal string, then creates a frame running
1033
+ %% repl_solve/4. The frame becomes the session's default frame so that
1034
+ %% step_engine/4 and the runner can drive it.
1035
+ query_start(SessionId, Text, FrameKind, InitialState, Error) :-
1036
+ catch(
1037
+ ( read_repl_goal(SessionId, Text, Goal, Names),
1038
+ create_frame_raw(SessionId, FrameKind, 0,
1039
+ deepclause_mi:repl_solve(Goal, Names, SessionId, InitialState),
1040
+ FrameId),
1041
+ retractall(session_default_frame(SessionId, _)),
1042
+ assertz(session_default_frame(SessionId, FrameId)),
1043
+ session_frame_engine(FrameId, Engine),
1044
+ retractall(session_engine(SessionId, _)),
1045
+ assertz(session_engine(SessionId, Engine)),
1046
+ Error = none
1047
+ ),
1048
+ QueryError,
1049
+ format(atom(Error), '~w', [QueryError])
1050
+ ).
1051
+
1052
+ %% ensure_full_stop(+Text, -TextWithStop)
1053
+ %% Appends a terminating full stop + newline when the goal text lacks one, so
1054
+ %% both `goal: p(X).` and `-g "p(X)"` read cleanly.
1055
+ ensure_full_stop(Text, Text1) :-
1056
+ atom_string(TextAtom, Text),
1057
+ ( atom_ends_with_stop(TextAtom)
1058
+ -> atom_concat(TextAtom, '\n', Text1)
1059
+ ; atom_concat(TextAtom, '.\n', Text1)
1060
+ ).
1061
+
1062
+ %% atom_ends_with_stop(+Atom)
1063
+ %% True when the last non-whitespace character is a full stop. A naive
1064
+ %% "ends with ." test would append an extra full stop to any block that ends
1065
+ %% with a newline, and read_term/3 then fails on the lone '.' (end_of_clause).
1066
+ atom_ends_with_stop(TextAtom) :-
1067
+ atom_string(TextAtom, TextStr),
1068
+ string_length(TextStr, Len0),
1069
+ trim_trailing_ws(TextStr, Len0, Len),
1070
+ Len > 0,
1071
+ Pos is Len - 1,
1072
+ sub_string(TextStr, Pos, 1, _, ".").
1073
+
1074
+ trim_trailing_ws(Str, Len0, Len) :-
1075
+ ( Len0 > 0,
1076
+ Pos is Len0 - 1,
1077
+ sub_string(Str, Pos, 1, _, Ch),
1078
+ ( Ch == " " ; Ch == "\t" ; Ch == "\n" ; Ch == "\r" )
1079
+ -> Len1 is Len0 - 1,
1080
+ trim_trailing_ws(Str, Len1, Len)
1081
+ ; Len = Len0
1082
+ ).
1083
+
1084
+ %% read_repl_goal(+SessionId, +Text, -Goal, -Names)
1085
+ %% Names is the read_term variable_names list (Name=Var), used to render
1086
+ %% bindings for solution events.
1087
+ read_repl_goal(SessionId, Text, Goal, Names) :-
1088
+ % read_term/3 requires a full stop; REPL callers may omit it (`-g "p(X)"`).
1089
+ ensure_full_stop(Text, Text1),
1090
+ open_string(Text1, Stream),
1091
+ read_term(Stream, RawGoal, [module(SessionId), variable_names(Names0)]),
1092
+ close(Stream),
1093
+ ( RawGoal == end_of_file
1094
+ -> throw(error(syntax_error('empty goal'), _))
1095
+ ; true
1096
+ ),
1097
+ % Apply only the goal-relevant source transforms. Do NOT run
1098
+ % preprocess_loaded_term/3 here: its file-load term expansion flattens a
1099
+ % list goal such as ["file.dml"] into its elements.
1100
+ transform_task_calls(RawGoal, Names0, Transformed),
1101
+ % Use the body expander (not expand_term_interp/3) so a `{Var}` string is
1102
+ % hoisted into a goal *before* the goal that uses it. expand_term_interp
1103
+ % embeds the interpolation term in the argument position instead, which is
1104
+ % wrong for a raw goal such as verify(X, "Is {X} > 0?", V).
1105
+ expand_body_interp(Transformed, Names0, Goal),
1106
+ Names = Names0.
1107
+
1108
+ %% repl_solve(+Goal, +Names, +SessionId, +State)
1109
+ %% Enumerating goal-mode solver. Yields one solution(Bindings, Deterministic)
1110
+ %% per answer and fails so engine_next/2 backtracks for the next solution.
1111
+ %% answer/1 commits (throws '$answer_commit') and stops enumeration.
1112
+ %% mi/3 is deliberately not used: it cuts after the first solution.
1113
+ repl_solve(Goal, Names, SessionId, State) :-
1114
+ nb_setval(current_session_id, SessionId),
1115
+ catch(
1116
+ repl_solve_loop(Goal, Names, SessionId, State),
1117
+ '$answer_commit',
1118
+ true
1119
+ ).
1120
+
1121
+ repl_solve_loop(Goal, Names, SessionId, State) :-
1122
+ mi_call(Goal, State, StateOut),
1123
+ collect_bindings(Names, Bindings),
1124
+ store_memory_for_state(SessionId, StateOut),
1125
+ engine_yield(solution(Bindings, false)),
1126
+ fail.
1127
+
1128
+ %% collect_bindings(+Names, -Bindings)
1129
+ %% Bindings is a list of Name-RenderedText. Unbound variables render as '_'.
1130
+ collect_bindings(Names, Bindings) :-
1131
+ findall(Name-Text,
1132
+ ( member(Name=Var, Names),
1133
+ render_binding(Var, Text)
1134
+ ),
1135
+ Bindings).
1136
+
1137
+ render_binding(Var, '_') :-
1138
+ var(Var),
1139
+ !.
1140
+ render_binding(Value, Text) :-
1141
+ with_output_to(string(Text), write_term(Value, [quoted(true), max_depth(20)])).
1142
+
1143
+ %% store_memory_for_state(+SessionId, +State)
1144
+ %% Writes the threaded memory back to the slot named by the state.
1145
+ store_memory_for_state(SessionId, State) :-
1146
+ ( is_dict(State),
1147
+ get_dict(memory, State, Memory)
1148
+ -> ( get_dict(memory_slot, State, meta)
1149
+ -> retractall(session_meta_memory(SessionId, _)),
1150
+ assertz(session_meta_memory(SessionId, Memory))
1151
+ ; retractall(session_repl_memory(SessionId, _)),
1152
+ assertz(session_repl_memory(SessionId, Memory))
1153
+ )
1154
+ ; true
1155
+ ).
1156
+
1157
+ %% repl_listing(+SessionId, +Filter, -Text)
1158
+ %% Read-only listing used by the meta tool and the `listing.` REPL command.
1159
+ repl_listing(SessionId, Filter, Text) :-
1160
+ with_output_to(string(Text), repl_listing_output(SessionId, Filter)).
1161
+
1162
+ %% repl_listing(+Filter, -Text) uses the current session id.
1163
+ repl_listing(Filter, Text) :-
1164
+ get_session_id(SessionId),
1165
+ repl_listing(SessionId, Filter, Text).
1166
+
1167
+ repl_listing_output(SessionId, Filter) :-
1168
+ ( Filter == tools
1169
+ -> forall(session_user_tool_schema(SessionId, Name, Schema),
1170
+ ( SessionId:tool_source(Name, Source)
1171
+ -> format("~w - ~w~n~w~n~n", [Name, Schema, Source])
1172
+ ; format("~w - ~w~n~n", [Name, Schema])
1173
+ )
1174
+ )
1175
+ ; Filter == history
1176
+ -> repl_history_listing(SessionId)
1177
+ ; ( current_predicate(SessionId:module_doc/1),
1178
+ SessionId:module_doc(Skill)
1179
+ -> format("skill: ~w~n~n", [Skill])
1180
+ ; true
1181
+ ),
1182
+ findall(Name/Arity,
1183
+ ( current_predicate(SessionId:Name/Arity),
1184
+ repl_user_predicate(SessionId, Name, Arity)
1185
+ ),
1186
+ Preds0),
1187
+ sort(Preds0, Preds),
1188
+ forall(member(Name/Arity, Preds),
1189
+ ( repl_listing_match(Filter, Name, Arity)
1190
+ -> format("~w/~w~n", [Name, Arity]),
1191
+ ( current_predicate(SessionId:doc/2),
1192
+ SessionId:doc(Name/Arity, DocText)
1193
+ -> format(" % ~w~n", [DocText])
1194
+ ; true
1195
+ ),
1196
+ functor(Head, Name, Arity),
1197
+ forall(clause(SessionId:Head, ClauseBody), print_user_clause(Head, ClauseBody))
1198
+ ; true
1199
+ )
1200
+ )
1201
+ ).
1202
+
1203
+ repl_listing_match(all, _, _) :- !.
1204
+ repl_listing_match(Name/Arity, Name, Arity) :- !.
1205
+ repl_listing_match(Filter, Name, _) :-
1206
+ atom(Filter),
1207
+ !,
1208
+ ( sub_atom(Name, 0, _, _, Filter)
1209
+ ; sub_atom(Filter, 0, _, _, Name)
1210
+ ).
1211
+ repl_listing_match(_, _, _).
1212
+
1213
+ %% repl_user_predicate(+SessionId, +Name, +Arity)
1214
+ %% Excludes reserved and internal predicates from listing/export.
1215
+ repl_user_predicate(_SessionId, Name, _Arity) :-
1216
+ repl_reserved_name(Name),
1217
+ !,
1218
+ fail.
1219
+ repl_user_predicate(_SessionId, Name, _Arity) :-
1220
+ internal_session_predicate(Name),
1221
+ !,
1222
+ fail.
1223
+ repl_user_predicate(_SessionId, Name, _Arity) :-
1224
+ history_predicate(Name),
1225
+ !,
1226
+ fail.
1227
+ repl_user_predicate(SessionId, Name, Arity) :-
1228
+ % User DML clauses are asserted (interpreted) into the session module.
1229
+ % Exclude predicates imported via use_module (e.g. library(clpfd)) and
1230
+ % foreign/builtin predicates. Build a callable head rather than a predicate
1231
+ % indicator: predicate_property/2 wants a goal (Module:Goal).
1232
+ functor(Head, Name, Arity),
1233
+ predicate_property(SessionId:Head, interpreted),
1234
+ \+ predicate_property(SessionId:Head, imported_from(_)),
1235
+ \+ predicate_property(SessionId:Head, foreign).
1236
+
1237
+ internal_session_predicate(param).
1238
+ internal_session_predicate(param_decl).
1239
+ internal_session_predicate(tool_source).
1240
+ internal_session_predicate('__session__').
1241
+
1242
+ %% history_predicate(+Name)
1243
+ %% Runtime-owned harness-history facts. Excluded from the user predicate set (so
1244
+ %% they are neither listed as harness predicates nor exported nor replaced by a
1245
+ %% commit) and protected from user definition -- but CALLABLE from DML, so the
1246
+ %% harness can query what happened in previous sessions.
1247
+ history_predicate(Name) :-
1248
+ atom(Name),
1249
+ sub_atom(Name, 0, _, _, harness_).
1250
+
1251
+ %% repl_reserved_name(+Name)
1252
+ %% Names owned by the REPL runtime. M4 uses repl_reserved/1 to refuse goals
1253
+ %% that try to re-enter or shadow the REPL control surface.
1254
+ %% Any repl_*/meta_* name is reserved (namespace guard), plus the explicit
1255
+ %% control names below. repl_delete is a protected exception for definition
1256
+ %% purposes (see repl_protected_name/1).
1257
+ repl_reserved_name(Name) :-
1258
+ atom(Name),
1259
+ ( sub_atom(Name, 0, _, _, repl_)
1260
+ ; sub_atom(Name, 0, _, _, meta_)
1261
+ ).
1262
+
1263
+ repl_reserved_name(repl_meta_prompt).
1264
+ repl_reserved_name(repl_extract_code).
1265
+ repl_reserved_name(repl_reserved).
1266
+ repl_reserved_name(repl_reserved_name).
1267
+ repl_reserved_name(repl_tool_catalog).
1268
+ repl_reserved_name(repl_listing).
1269
+ repl_reserved_name(repl_assert).
1270
+ repl_reserved_name(repl_retract).
1271
+ repl_reserved_name(repl_delete).
1272
+ repl_reserved_name(repl_freeze).
1273
+ repl_reserved_name(repl_unfreeze).
1274
+ repl_reserved_name(repl_is_frozen).
1275
+ repl_reserved_name(repl_solve).
1276
+ repl_reserved_name(repl_initial_state).
1277
+ repl_reserved_name(repl_user_predicate).
1278
+ repl_reserved_name(query_start).
1279
+ repl_reserved_name(read_repl_goal).
1280
+ repl_reserved_name(create_session).
1281
+ repl_reserved_name(clear_session).
1282
+ repl_reserved_name(destroy_session).
1283
+ repl_reserved_name(create_frame).
1284
+ repl_reserved_name(create_frame_raw).
1285
+ repl_reserved_name(step_frame).
1286
+ repl_reserved_name(end_frame).
1287
+ repl_reserved_name(frame_info).
1288
+ repl_reserved_name(default_frame).
1289
+ repl_reserved_name(load_code).
1290
+ repl_reserved_name(freeze).
1291
+ repl_reserved_name(unfreeze).
1292
+ repl_reserved_name(reset).
1293
+ repl_reserved_name(export).
1294
+
1295
+ %% repl_reserved(+TermOrNameArity)
1296
+ repl_reserved(Name) :-
1297
+ atom(Name),
1298
+ repl_reserved_name(Name).
1299
+ repl_reserved(Name/Arity) :-
1300
+ atom(Name),
1301
+ integer(Arity),
1302
+ repl_reserved_name(Name).
1303
+ repl_reserved(Term) :-
1304
+ compound(Term),
1305
+ functor(Term, Name, _),
1306
+ repl_reserved_name(Name).
1307
+
1308
+ %% ============================================================
1309
+ %% Freeze Lock and Export (M2)
1310
+ %% ============================================================
1311
+
1312
+ %% repl_freeze(+SessionId)
1313
+ %% Makes the living module read-only. Queries, `execute`, and nested task()
1314
+ %% calls still work; only mutation is refused.
1315
+ repl_freeze(SessionId) :-
1316
+ retractall(session_frozen(SessionId, _)),
1317
+ assertz(session_frozen(SessionId, true)).
1318
+
1319
+ %% repl_lock(+SessionId)
1320
+ %% Irreversibly freezes: repl_unfreeze/1 refuses. Used by --locked.
1321
+ repl_lock(SessionId) :-
1322
+ repl_freeze(SessionId),
1323
+ retractall(session_locked(SessionId, _)),
1324
+ assertz(session_locked(SessionId, true)).
1325
+
1326
+ %% repl_unfreeze(+SessionId)
1327
+ repl_unfreeze(SessionId) :-
1328
+ ( session_locked(SessionId, true)
1329
+ -> throw(error(permission_error(modify, locked_harness, SessionId),
1330
+ context(repl_unfreeze/1, 'harness is locked; unfreeze is disabled')))
1331
+ ; retractall(session_frozen(SessionId, _)),
1332
+ assertz(session_frozen(SessionId, false))
1333
+ ).
1334
+
1335
+ %% repl_is_frozen(+SessionId)
1336
+ repl_is_frozen(SessionId) :-
1337
+ session_frozen(SessionId, true),
1338
+ !.
1339
+
1340
+ %% repl_try_unfreeze(+SessionId, -Error)
1341
+ %% Non-throwing wrapper so the host can report a locked harness cleanly.
1342
+ repl_try_unfreeze(SessionId, Error) :-
1343
+ catch(repl_unfreeze(SessionId), UnfreezeError,
1344
+ format(atom(Error), '~w', [UnfreezeError])),
1345
+ ( var(Error)
1346
+ -> Error = none
1347
+ ; true
1348
+ ).
1349
+
1350
+ %% guard_not_frozen(+SessionId)
1351
+ %% Shared mutation guard for assert/retract/consult/load.
1352
+ guard_not_frozen(SessionId) :-
1353
+ ( session_frozen(SessionId, true)
1354
+ -> throw(error(permission_error(modify, frozen_harness, SessionId),
1355
+ context(mi_call/3, 'harness is frozen; assert/retract/load are disabled')))
1356
+ ; true
1357
+ ).
1358
+
1359
+ %% record_directive(+SessionId, +Directive)
1360
+ record_directive(SessionId, Directive) :-
1361
+ ( session_directive(SessionId, Directive)
1362
+ -> true
1363
+ ; assertz(session_directive(SessionId, Directive))
1364
+ ).
1365
+
1366
+ %% ---- Export (serialization) ----
1367
+
1368
+ %% export_harness(+SessionId, +Header, -Text)
1369
+ %% Deterministically serialize the living module into self-contained DML:
1370
+ %% directives, tools, embedded scripts, then user predicates sorted name/arity.
1371
+ export_harness(SessionId, Header, Text) :-
1372
+ with_output_to(string(Text), export_harness_output(SessionId, Header)).
1373
+
1374
+ export_harness_output(SessionId, Header) :-
1375
+ ( Header == none
1376
+ -> true
1377
+ ; format("% ~w~n", [Header])
1378
+ ),
1379
+ export_directives(SessionId),
1380
+ export_tools(SessionId),
1381
+ export_clauses(SessionId).
1382
+
1383
+ export_directives(SessionId) :-
1384
+ findall(Directive, session_directive(SessionId, Directive), Directives0),
1385
+ sort(Directives0, Directives),
1386
+ forall(member(Directive, Directives), format(":- ~q.~n", [Directive])),
1387
+ ( Directives == []
1388
+ -> true
1389
+ ; nl
1390
+ ).
1391
+
1392
+ export_tools(SessionId) :-
1393
+ findall(Name, session_user_tool_schema(SessionId, Name, _), Names0),
1394
+ sort(Names0, Names),
1395
+ forall(member(Name, Names),
1396
+ ( SessionId:tool_source(Name, Source)
1397
+ -> format("~w~n~n", [Source])
1398
+ ; true
1399
+ )).
1400
+
1401
+ export_clauses(SessionId) :-
1402
+ findall(Name/Arity,
1403
+ ( current_predicate(SessionId:Name/Arity),
1404
+ export_user_predicate(SessionId, Name, Arity)
1405
+ ),
1406
+ Preds0),
1407
+ sort(Preds0, Preds),
1408
+ forall(member(Name/Arity, Preds),
1409
+ ( functor(Head, Name, Arity),
1410
+ forall(clause(SessionId:Head, Body), export_clause(Head, Body))
1411
+ )).
1412
+
1413
+ export_user_predicate(SessionId, Name, Arity) :-
1414
+ repl_user_predicate(SessionId, Name, Arity),
1415
+ \+ tool_predicate_name(Name).
1416
+
1417
+ %% export_clause(+Head, +Body)
1418
+ %% Copy + numbervars the clause so variable names are deterministic (A, B, …),
1419
+ %% which is required for byte-stable export → reload → export.
1420
+ %% export_stats(+SessionId, -Clauses, -Tools, -Scripts)
1421
+ export_stats(SessionId, Clauses, Tools, Scripts) :-
1422
+ findall(Name/Arity,
1423
+ ( current_predicate(SessionId:Name/Arity),
1424
+ export_user_predicate(SessionId, Name, Arity)
1425
+ ),
1426
+ Preds0),
1427
+ sort(Preds0, Preds),
1428
+ findall(Count,
1429
+ ( member(PredName/PredArity, Preds),
1430
+ functor(PredHead, PredName, PredArity),
1431
+ aggregate_all(count, clause(SessionId:PredHead, _), Count)
1432
+ ),
1433
+ PredCounts),
1434
+ sum_list(PredCounts, Clauses),
1435
+ findall(Name, session_user_tool_schema(SessionId, Name, _), ToolNames0),
1436
+ sort(ToolNames0, ToolNames),
1437
+ length(ToolNames, Tools),
1438
+ ( current_predicate(SessionId:script_file/2)
1439
+ -> findall(Path, SessionId:script_file(_, Path), ScriptPaths0),
1440
+ sort(ScriptPaths0, ScriptPaths),
1441
+ length(ScriptPaths, Scripts)
1442
+ ; Scripts = 0
1443
+ ).
1444
+
1445
+ export_clause(Head, Body) :-
1446
+ print_user_clause(Head, Body),
1447
+ nl.
1448
+
1449
+ %% format_tool_source(+Head, +Description, +Body, -SourceCode)
1450
+ %% Source text for a tool, used by `listing(tools)` and export. Reverses the
1451
+ %% load-time task/prompt rewrites so readers see `task/N`, not `task_named/3`.
1452
+ format_tool_source(Head, none, Body, SourceCode) :-
1453
+ !,
1454
+ copy_term(Head-Body, NH-NB),
1455
+ numbervars(NH-NB, 0, _),
1456
+ clean_clause_body(NB, CleanBody),
1457
+ ( CleanBody == true
1458
+ -> format(string(SourceCode), "tool(~q).", [NH])
1459
+ ; format(string(SourceCode), "tool(~q) :-~n ~q.", [NH, CleanBody])
1460
+ ).
1461
+ format_tool_source(Head, Description, Body, SourceCode) :-
1462
+ copy_term(Head-Description-Body, NH-ND-NB),
1463
+ numbervars(NH-ND-NB, 0, _),
1464
+ clean_clause_body(NB, CleanBody),
1465
+ ( CleanBody == true
1466
+ -> format(string(SourceCode), "tool(~q, ~q).", [NH, ND])
1467
+ ; format(string(SourceCode), "tool(~q, ~q) :-~n ~q.", [NH, ND, CleanBody])
1468
+ ).
1469
+
1470
+ %% print_user_clause(+Head, +Body)
1471
+ %% Copy + numbervars for deterministic variable names, reverse the load-time
1472
+ %% task/prompt rewrites (see clean_clause_body/2), then print in source form.
1473
+ print_user_clause(Head, Body) :-
1474
+ copy_term(Head-Body, NumberedHead-NumberedBody),
1475
+ numbervars(NumberedHead-NumberedBody, 0, _),
1476
+ clean_clause_body(NumberedBody, CleanBody),
1477
+ ( CleanBody == true
1478
+ -> format("~q.~n", [NumberedHead])
1479
+ ; format("~q :-~n ~q.~n", [NumberedHead, CleanBody])
1480
+ ).
1481
+
1482
+ tool_predicate_name(tool).
1483
+ tool_predicate_name(tool_source).
1484
+
1485
+ %% repl_script_files(+SessionId, -Paths)
1486
+ %% Workspace-relative files linked by script_file/2 facts; the host bundles them.
1487
+ repl_script_files(SessionId, Paths) :-
1488
+ ( current_predicate(SessionId:script_file/2)
1489
+ -> % Only literal paths count; a stray variable path is ignored.
1490
+ findall(Path,
1491
+ ( SessionId:script_file(_, Path),
1492
+ (atom(Path) ; string(Path))
1493
+ ),
1494
+ Paths0),
1495
+ sort(Paths0, Paths)
1496
+ ; Paths = []
1497
+ ).
1498
+
1499
+ %% repl_materialize_embedded(+SessionId)
1500
+ %% Writes every embedded_file(Path, Content) fact to its workspace path. Used
1501
+ %% after loading a single-file .dml export (where linked scripts are inlined).
1502
+ repl_materialize_embedded(SessionId) :-
1503
+ ( current_predicate(SessionId:embedded_file/2)
1504
+ -> forall(SessionId:embedded_file(Path, Content),
1505
+ ( resolve_workspace_path(Path, Full),
1506
+ catch(
1507
+ ( file_directory_name(Full, Dir),
1508
+ make_directory_path(Dir),
1509
+ setup_call_cleanup(
1510
+ open(Full, write, Stream),
1511
+ format(Stream, "~s", [Content]),
1512
+ close(Stream))
1513
+ ),
1514
+ _,
1515
+ true
1516
+ )
1517
+ ))
1518
+ ; true
1519
+ ).
1520
+
1521
+ %% ============================================================
1522
+ %% Prompt Mode (M3)
1523
+ %% ============================================================
1524
+
1525
+ %% repl_protected_name(+Name)
1526
+ %% Reserved names that generated code may not (re)define. repl_delete is a
1527
+ %% legitimate harness directive and is allowed through.
1528
+ repl_protected_name(Name) :-
1529
+ history_predicate(Name).
1530
+ repl_protected_name(Name) :-
1531
+ repl_reserved_name(Name),
1532
+ Name \== repl_delete.
1533
+
1534
+ repl_protected_clause((Head :- _)) :-
1535
+ callable(Head),
1536
+ functor(Head, Name, _),
1537
+ repl_protected_name(Name).
1538
+ repl_protected_clause(Head) :-
1539
+ callable(Head),
1540
+ functor(Head, Name, _),
1541
+ repl_protected_name(Name).
1542
+
1543
+ %% repl_set_meta_catalog(+SessionId, +CatalogText)
1544
+ repl_set_meta_catalog(SessionId, CatalogText) :-
1545
+ retractall(session_meta_catalog(SessionId, _)),
1546
+ assertz(session_meta_catalog(SessionId, CatalogText)).
1547
+
1548
+ %% repl_prepare_single(+SessionId, +ClauseText, -PreparedTerms)
1549
+ %% Reads exactly ONE source term and runs the SAME load-time preprocessing as
1550
+ %% parse_dml_string/2: transform_task_calls/3 (task/N, prompt/N ->
1551
+ %% task_named/3, prompt_named/3), expand_interpolations/3 ({Var} strings), and
1552
+ %% expand_term/2 (e.g. DCG rules). Scratch asserts must be preprocessed exactly
1553
+ %% like a file load, or a clause asserted mid-turn keeps task/N, which has no
1554
+ %% runtime handler and fails silently.
1555
+ %%
1556
+ %% Trailing content is an error so `assert` can never silently drop clauses that
1557
+ %% `assert_many` would have kept.
1558
+ repl_prepare_single(SessionId, ClauseText, PreparedTerms) :-
1559
+ ensure_full_stop(ClauseText, Text1),
1560
+ open_string(Text1, Stream),
1561
+ read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
1562
+ ( read_term(Stream, Extra, [module(SessionId)]),
1563
+ Extra \== end_of_file
1564
+ -> close(Stream),
1565
+ throw(error(syntax_error('assert expects a single clause; use assert_many for multiple'), _))
1566
+ ; close(Stream)
1567
+ ),
1568
+ preprocess_loaded_term(Term, Bindings, PreparedTerms).
1569
+
1570
+ %% repl_read_clause(+SessionId, +ClauseText, -Clause)
1571
+ %% Reads exactly one term without preprocessing. Used by retract, which only
1572
+ %% needs the predicate's name/arity. Trailing content is an error.
1573
+ repl_read_clause(SessionId, ClauseText, Clause) :-
1574
+ ensure_full_stop(ClauseText, Text1),
1575
+ open_string(Text1, Stream),
1576
+ read_term(Stream, Clause, [module(SessionId)]),
1577
+ ( read_term(Stream, Extra, [module(SessionId)]),
1578
+ Extra \== end_of_file
1579
+ -> close(Stream),
1580
+ throw(error(syntax_error('retract expects a single clause pattern'), _))
1581
+ ; close(Stream)
1582
+ ).
1583
+
1584
+ %% repl_head_of(+Term, -Head)
1585
+ %% Strips a rule body, so retracting with a full clause pattern targets the
1586
+ %% predicate instead of accidentally matching every rule ((:-)/2).
1587
+ repl_head_of((Head :- _Body), Head) :- !.
1588
+ repl_head_of(Head, Head).
1589
+
1590
+ %% repl_assert_clause(+PreparedTerm, +SessionId)
1591
+ %% Asserts one already-preprocessed term, rejecting reserved names.
1592
+ repl_assert_clause(PreparedTerm, SessionId) :-
1593
+ ( repl_protected_clause(PreparedTerm)
1594
+ -> throw(error(permission_error(modify, reserved_name, PreparedTerm),
1595
+ context(repl_assert/3, 'cannot define a reserved predicate')))
1596
+ ; process_clause(PreparedTerm, SessionId)
1597
+ ).
1598
+
1599
+ %% repl_assert(+SessionId, +ClauseText, -Result)
1600
+ %% Parses and asserts generated DML with the load-time preprocessing applied,
1601
+ %% so scratch-asserted clauses behave exactly like loaded ones.
1602
+ %% Refused when frozen; reserved names are rejected.
1603
+ repl_assert(SessionId, ClauseText, Result) :-
1604
+ catch(
1605
+ ( guard_not_frozen(SessionId),
1606
+ repl_prepare_single(SessionId, ClauseText, PreparedTerms),
1607
+ forall(member(PreparedTerm, PreparedTerms),
1608
+ repl_assert_clause(PreparedTerm, SessionId)),
1609
+ Result = ok
1610
+ ),
1611
+ AssertError,
1612
+ format(atom(Result), 'error(~w)', [AssertError])
1613
+ ).
1614
+
1615
+ %% repl_set_dml_reference(+SessionId, +Text)
1616
+ %% Stores the bundled DML reference so the meta prompt can include it.
1617
+ repl_set_dml_reference(SessionId, Text) :-
1618
+ retractall(session_dml_reference(SessionId, _)),
1619
+ assertz(session_dml_reference(SessionId, Text)).
1620
+
1621
+ %% repl_set_authoring_examples(+SessionId, +Text)
1622
+ %% Stores the curated authoring patterns for the meta prompt.
1623
+ repl_set_authoring_examples(SessionId, Text) :-
1624
+ retractall(session_authoring_examples(SessionId, _)),
1625
+ assertz(session_authoring_examples(SessionId, Text)).
1626
+
1627
+ %% repl_set_meta_template(+SessionId, +Template)
1628
+ %% Stores the meta-prompt template (the bundled META_PROMPT.md). Placeholders
1629
+ %% {{CATALOG}}, {{REFERENCE}}, {{EXAMPLES}}, {{MANIFEST}}, {{MEMORY}}, {{REQUEST}}
1630
+ %% are filled by repl_meta_prompt/3.
1631
+ repl_set_meta_template(SessionId, Template) :-
1632
+ retractall(session_meta_template(SessionId, _)),
1633
+ assertz(session_meta_template(SessionId, Template)).
1634
+
1635
+ %% repl_set_harness_examples(+SessionId, +Text)
1636
+ %% Stores the bundled HARNESS_EXAMPLES.md ({{EXAMPLES}}).
1637
+ repl_set_harness_examples(SessionId, Text) :-
1638
+ retractall(session_harness_examples(SessionId, _)),
1639
+ assertz(session_harness_examples(SessionId, Text)).
1640
+
1641
+ %% repl_set_prolog_snippets(+SessionId, +Text)
1642
+ %% Stores the bundled PROLOG_SNIPPETS.md ({{SNIPPETS}}).
1643
+ repl_set_prolog_snippets(SessionId, Text) :-
1644
+ retractall(session_prolog_snippets(SessionId, _)),
1645
+ assertz(session_prolog_snippets(SessionId, Text)).
1646
+
1647
+ %% repl_parse_clauses(+SessionId, +ClausesText, -Terms)
1648
+ %% Parse and preprocess a block of DML clauses without asserting them.
1649
+ repl_parse_clauses(SessionId, ClausesText, Terms) :-
1650
+ ensure_full_stop(ClausesText, Text1),
1651
+ open_string(Text1, Stream),
1652
+ repl_read_prepared_terms(Stream, SessionId, Terms),
1653
+ close(Stream).
1654
+
1655
+ repl_read_prepared_terms(Stream, SessionId, Terms) :-
1656
+ read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
1657
+ ( Term == end_of_file
1658
+ -> Terms = []
1659
+ ; % Apply the same preprocessing as parse_dml_string/2 so scratch clauses
1660
+ % match loaded ones (task/prompt rewrite, interpolation, expand_term).
1661
+ preprocess_loaded_term(Term, Bindings, Prepared),
1662
+ % Directives (e.g. use_module(library(clpfd))) must run while parsing so
1663
+ % their operators are visible to the terms that follow; keep them out of
1664
+ % the returned list so repl_commit_terms does not process them twice.
1665
+ partition_directives(Prepared, Rules, Directives),
1666
+ forall(member(Directive, Directives), process_clause(Directive, SessionId)),
1667
+ append(Rules, Rest, Terms),
1668
+ repl_read_prepared_terms(Stream, SessionId, Rest)
1669
+ ).
1670
+
1671
+ partition_directives([], [], []).
1672
+ partition_directives([Term|Terms], Rules, Directives) :-
1673
+ ( Term = (:- _)
1674
+ -> Rules = Rules0,
1675
+ Directives = [Term|Directives0]
1676
+ ; Rules = [Term|Rules0],
1677
+ Directives = Directives0
1678
+ ),
1679
+ partition_directives(Terms, Rules0, Directives0).
1680
+
1681
+ %% repl_commit_terms(+SessionId, +Terms)
1682
+ %% Assert already-preprocessed terms (the shared tail of assert_many/define).
1683
+ repl_commit_terms(SessionId, Terms) :-
1684
+ forall(member(PreparedTerm, Terms),
1685
+ ( repl_protected_clause(PreparedTerm)
1686
+ -> throw(error(permission_error(modify, reserved_name, PreparedTerm),
1687
+ context(repl_assert_many/3, 'cannot define a reserved predicate')))
1688
+ ; process_clause(PreparedTerm, SessionId)
1689
+ )).
1690
+
1691
+ %% repl_assert_many(+SessionId, +ClausesText, -Result)
1692
+ %% Parses a block of clauses and asserts them in the order written. Prefer this
1693
+ %% for defining a predicate: clause order matters for backtracking, so asserting
1694
+ %% one clause at a time can produce a wrong program.
1695
+ repl_assert_many(SessionId, ClausesText, Result) :-
1696
+ catch(
1697
+ ( guard_not_frozen(SessionId),
1698
+ repl_parse_clauses(SessionId, ClausesText, Terms),
1699
+ repl_commit_terms(SessionId, Terms),
1700
+ repl_note_touched(SessionId, Terms),
1701
+ Result = ok
1702
+ ),
1703
+ AssertError,
1704
+ format(atom(Result), 'error(~w)', [AssertError])
1705
+ ).
1706
+
1707
+ %% repl_term_indicator(+Term, -Name/Arity)
1708
+ %% The plain predicate a parsed clause defines. Directives and tool/2 wrappers
1709
+ %% have no plain indicator here (process_clause owns them).
1710
+ repl_term_indicator((:- _), _) :- !, fail.
1711
+ repl_term_indicator((tool(_, _) :- _), _) :- !, fail.
1712
+ repl_term_indicator((tool(_) :- _), _) :- !, fail.
1713
+ repl_term_indicator(Term, Name/Arity) :-
1714
+ repl_head_of(Term, Head),
1715
+ callable(Head),
1716
+ functor(Head, Name, Arity).
1717
+
1718
+ %% repl_note_touched(+SessionId, +Terms)
1719
+ %% Record the predicate indicators a term list defines, for history recording.
1720
+ repl_note_touched(SessionId, Terms) :-
1721
+ forall(( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
1722
+ ( session_edit_touched(SessionId, Name/Arity)
1723
+ -> true
1724
+ ; assertz(session_edit_touched(SessionId, Name/Arity))
1725
+ )).
1726
+
1727
+ %% repl_edit_touched(+SessionId, -Indicators)
1728
+ %% Predicates touched by edit tools since the last clear (sorted, unique).
1729
+ repl_edit_touched(SessionId, Indicators) :-
1730
+ findall(Name/Arity, session_edit_touched(SessionId, Name/Arity), Raw),
1731
+ sort(Raw, Indicators).
1732
+
1733
+ %% repl_edit_touched_text(+SessionId, -Text)
1734
+ %% "Name/Arity, Name/Arity" of the predicates an agent turn touched.
1735
+ repl_edit_touched_text(SessionId, Text) :-
1736
+ repl_edit_touched(SessionId, Indicators),
1737
+ with_output_to(string(Text), print_pred_list(Indicators)).
1738
+
1739
+ repl_edit_touched_clear(SessionId) :-
1740
+ retractall(session_edit_touched(SessionId, _)).
1741
+
1742
+ %% repl_callable_indicator(+Head, -Name, -Arity)
1743
+ %% Validate a predicate head: an ordinary (non-operator, non-system) functor.
1744
+ repl_callable_indicator(Head, Name, Arity) :-
1745
+ callable(Head),
1746
+ functor(Head, Name, Arity),
1747
+ atom(Name),
1748
+ Name \== '[]',
1749
+ Name \== '{}',
1750
+ \+ repl_system_functor(Name/Arity).
1751
+
1752
+ %% Functors that are list/operator/system structure, never user predicates.
1753
+ repl_system_functor(Name/_) :-
1754
+ member(Name, ['.', ',', ';', ':-', '->', ':', '|', '!', '\\+', '=', 'is']).
1755
+
1756
+ %% repl_retract(+SessionId, +ClausePattern, -Result)
1757
+ %% Retracts matching user clauses. Reserved names are protected.
1758
+ repl_retract(SessionId, ClauseText, Result) :-
1759
+ catch(
1760
+ ( guard_not_frozen(SessionId),
1761
+ repl_read_clause(SessionId, ClauseText, Pattern),
1762
+ repl_head_of(Pattern, Clause),
1763
+ repl_callable_indicator(Clause, Name, Arity),
1764
+ ( repl_protected_clause(Clause)
1765
+ -> throw(error(permission_error(modify, reserved_name, Clause),
1766
+ context(repl_retract/3, 'cannot retract a reserved predicate')))
1767
+ ; true
1768
+ ),
1769
+ functor(Head, Name, Arity),
1770
+ retractall(SessionId:Head),
1771
+ ( session_user_tools(SessionId, Name)
1772
+ -> retractall(session_user_tools(SessionId, Name)),
1773
+ retractall(session_user_tool_schema(SessionId, Name, _)),
1774
+ retractall(SessionId:tool_source(Name, _))
1775
+ ; true
1776
+ ),
1777
+ ( session_edit_touched(SessionId, Name/Arity)
1778
+ -> true
1779
+ ; assertz(session_edit_touched(SessionId, Name/Arity))
1780
+ ),
1781
+ Result = ok
1782
+ ),
1783
+ RetractError,
1784
+ format(atom(Result), 'error(~w)', [RetractError])
1785
+ ).
1786
+
1787
+ %% repl_delete_predicate(+SessionId, +Name, +Arity, -Result)
1788
+ %% Fully remove a predicate. Unlike retract (which only removes matching
1789
+ %% clauses and can leave an empty predicate behind), this abolishes it.
1790
+ repl_delete_predicate(SessionId, Name, Arity, Result) :-
1791
+ catch(
1792
+ ( guard_not_frozen(SessionId),
1793
+ atom(Name), integer(Arity), Arity >= 0,
1794
+ ( repl_protected_name(Name)
1795
+ -> throw(error(permission_error(modify, reserved_name, Name/Arity),
1796
+ context(repl_delete_predicate/4, 'cannot delete a reserved predicate')))
1797
+ ; true
1798
+ ),
1799
+ catch(abolish(SessionId:Name/Arity), _, true),
1800
+ ( session_user_tools(SessionId, Name)
1801
+ -> retractall(session_user_tools(SessionId, Name)),
1802
+ retractall(session_user_tool_schema(SessionId, Name, _)),
1803
+ retractall(SessionId:tool_source(Name, _))
1804
+ ; true
1805
+ ),
1806
+ ( session_edit_touched(SessionId, Name/Arity)
1807
+ -> true
1808
+ ; assertz(session_edit_touched(SessionId, Name/Arity))
1809
+ ),
1810
+ Result = ok
1811
+ ),
1812
+ DeleteError,
1813
+ format(atom(Result), 'error(~w)', [DeleteError])
1814
+ ).
1815
+
1816
+ %% repl_define(+SessionId, +HeadPatternText, +ClausesText, -Result)
1817
+ %% Replace every predicate the clause block defines. It parses the block,
1818
+ %% computes its indicators (plus the head pattern's), abolishes each, then
1819
+ %% asserts the block. This makes `define` safe no matter how many predicates a
1820
+ %% block spans, so a multi-predicate block cannot append duplicate clauses.
1821
+ repl_define(SessionId, HeadPatternText, ClausesText, Result) :-
1822
+ catch(
1823
+ ( guard_not_frozen(SessionId),
1824
+ repl_read_clause(SessionId, HeadPatternText, Pattern),
1825
+ repl_head_of(Pattern, HeadClause),
1826
+ repl_callable_indicator(HeadClause, HeadName, HeadArity),
1827
+ repl_parse_clauses(SessionId, ClausesText, Terms),
1828
+ findall(Name/Arity,
1829
+ ( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
1830
+ BlockIndicators0),
1831
+ sort([HeadName/HeadArity|BlockIndicators0], Indicators),
1832
+ forall(member(Name/Arity, Indicators),
1833
+ ( repl_protected_name(Name)
1834
+ -> throw(error(permission_error(modify, reserved_name, Name/Arity),
1835
+ context(repl_define/4, 'cannot define a reserved predicate')))
1836
+ ; catch(abolish(SessionId:Name/Arity), _, true),
1837
+ ( session_user_tools(SessionId, Name)
1838
+ -> retractall(session_user_tools(SessionId, Name)),
1839
+ retractall(session_user_tool_schema(SessionId, Name, _)),
1840
+ retractall(SessionId:tool_source(Name, _))
1841
+ ; true
1842
+ )
1843
+ )),
1844
+ repl_commit_terms(SessionId, Terms),
1845
+ repl_note_touched(SessionId, Terms),
1846
+ Result = ok
1847
+ ),
1848
+ DefineError,
1849
+ format(atom(Result), 'error(~w)', [DefineError])
1850
+ ).
1851
+
1852
+ %% repl_extract_code(+Text, -Source)
1853
+ %% Extracts a ```dml fenced block; falls back to the whole answer.
1854
+ repl_extract_code(Text, Source) :-
1855
+ atom_string(TextAtom, Text),
1856
+ ( sub_string(TextAtom, Start0, _, _, "```dml")
1857
+ -> Start is Start0 + 6,
1858
+ % After = 0 takes the remainder of the text (an unbound After would
1859
+ % bind Rest to the empty prefix first).
1860
+ sub_string(TextAtom, Start, _, 0, Rest),
1861
+ % Search for the closing fence by offset: binding Before=0 here would
1862
+ % require the fence to be at the very start of Rest.
1863
+ ( sub_string(Rest, FenceEnd, _, _, "```")
1864
+ -> sub_string(Rest, 0, FenceEnd, _, Source)
1865
+ ; Source = Rest
1866
+ )
1867
+ ; Source = TextAtom
1868
+ ).
1869
+
1870
+ %% repl_manifest(+SessionId, -ManifestText)
1871
+ %% Compact signatures + tools + entries + directives for the meta prompt.
1872
+ repl_manifest(SessionId, Manifest) :-
1873
+ findall(Name/Arity,
1874
+ ( current_predicate(SessionId:Name/Arity),
1875
+ repl_user_predicate(SessionId, Name, Arity)
1876
+ ),
1877
+ Preds0),
1878
+ sort(Preds0, Preds),
1879
+ findall(Name, session_user_tool_schema(SessionId, Name, _), Tools0),
1880
+ sort(Tools0, Tools),
1881
+ findall(Directive, session_directive(SessionId, Directive), Directives0),
1882
+ sort(Directives0, Directives),
1883
+ ( current_predicate(SessionId:module_doc/1),
1884
+ SessionId:module_doc(Summary0)
1885
+ -> Summary = Summary0
1886
+ ; Summary = ""
1887
+ ),
1888
+ ( current_predicate(SessionId:doc/2)
1889
+ -> findall(PI-DocText, SessionId:doc(PI, DocText), Docs0)
1890
+ ; Docs0 = []
1891
+ ),
1892
+ repl_history_summary(SessionId, HistorySummary),
1893
+ with_output_to(string(Manifest),
1894
+ ( ( Summary == ""
1895
+ -> format("- skill: (none; add module_doc/1 so the harness describes itself and can be routed)~n")
1896
+ ; format("- skill: ~w~n", [Summary])
1897
+ ),
1898
+ format("- predicates: ~w~n", [Preds]),
1899
+ format("- tools: ~w~n", [Tools]),
1900
+ format("- directives: ~w~n", [Directives]),
1901
+ ( Docs0 == []
1902
+ -> format("- docs: (none; add doc(Name/Arity, Text) facts)~n")
1903
+ ; forall(member(PI-DocText, Docs0), format("- doc(~w): ~w~n", [PI, DocText]))
1904
+ ),
1905
+ format("- history: ~w~n", [HistorySummary])
1906
+ )).
1907
+
1908
+ %% repl_meta_memory_text(+SessionId, -Text)
1909
+ repl_meta_memory_text(SessionId, Text) :-
1910
+ ( session_meta_memory(SessionId, Memory)
1911
+ -> true
1912
+ ; Memory = []
1913
+ ),
1914
+ last_n(Memory, 10, Recent),
1915
+ with_output_to(string(Text),
1916
+ forall(member(message{role: Role, content: Content}, Recent),
1917
+ format("~w: ~w~n", [Role, Content]))).
1918
+
1919
+ %% repl_memory_term(+SessionId, +Slot, -TermText)
1920
+ %% Serializes a live memory slot (harness|meta) as a Prolog term so a session
1921
+ %% can persist it and restore it on `--session` resume.
1922
+ repl_memory_term(SessionId, Slot, Text) :-
1923
+ ( Slot == meta
1924
+ -> ( session_meta_memory(SessionId, Memory)
1925
+ -> true
1926
+ ; Memory = []
1927
+ )
1928
+ ; ( session_repl_memory(SessionId, Memory)
1929
+ -> true
1930
+ ; Memory = []
1931
+ )
1932
+ ),
1933
+ with_output_to(string(Text),
1934
+ write_term(Memory, [quoted(true), max_depth(0)])).
1935
+
1936
+ last_n(List, N, Tail) :-
1937
+ length(List, Len),
1938
+ ( Len =< N
1939
+ -> Tail = List
1940
+ ; Skip is Len - N,
1941
+ length(Prefix, Skip),
1942
+ append(Prefix, Tail, List)
1943
+ ).
1944
+
1945
+ %% ============================================================
1946
+ %% Harness history (see docs/DML_REPL_SESSION_HISTORY.md)
1947
+ %% ============================================================
1948
+
1949
+ %% repl_history_load(+SessionId, +Text)
1950
+ %% Asserts harness_* history facts (the persisted .deepclause/repl/history.dml)
1951
+ %% into the session module so the harness can query prior sessions.
1952
+ repl_history_load(SessionId, Text) :-
1953
+ ( var(Text)
1954
+ -> true
1955
+ ; open_string(Text, Stream),
1956
+ repl_history_read_stream(Stream, SessionId),
1957
+ close(Stream)
1958
+ ).
1959
+
1960
+ repl_history_read_stream(Stream, SessionId) :-
1961
+ catch(read_term(Stream, Term, [module(SessionId)]), _, Term = end_of_file),
1962
+ ( Term == end_of_file
1963
+ -> true
1964
+ ; repl_history_assert_term(Term, SessionId),
1965
+ repl_history_read_stream(Stream, SessionId)
1966
+ ).
1967
+
1968
+ %% repl_history_record(+SessionId, +FactText)
1969
+ %% Asserts one or more history facts into the live module. The recorder persists
1970
+ %% the same facts to history.dml; here they become queryable.
1971
+ repl_history_record(SessionId, FactText) :-
1972
+ ( var(FactText)
1973
+ -> true
1974
+ ; ensure_full_stop(FactText, Text1),
1975
+ open_string(Text1, Stream),
1976
+ repl_history_read_stream(Stream, SessionId),
1977
+ close(Stream)
1978
+ ).
1979
+
1980
+ %% Only harness_* facts may be asserted through this path.
1981
+ repl_history_assert_term(Term, SessionId) :-
1982
+ ( nonvar(Term),
1983
+ Term \== end_of_file,
1984
+ Term \== none,
1985
+ callable(Term),
1986
+ functor(Term, Name, _),
1987
+ history_predicate(Name)
1988
+ -> % harness_head is a singleton: keep the latest (rollback moves it back).
1989
+ ( Name == harness_head
1990
+ -> retractall(SessionId:harness_head(_, _, _, _))
1991
+ ; true
1992
+ ),
1993
+ % Migrate pre-/5 harness_change facts (they carried an unused Entry arg).
1994
+ ( Term = harness_change(V, Ts, Kind, Sid, Turn, _Entry)
1995
+ -> assertz(SessionId:harness_change(V, Ts, Kind, Sid, Turn))
1996
+ ; assertz(SessionId:Term)
1997
+ )
1998
+ ; true
1999
+ ).
2000
+
2001
+ %% repl_rollback(+SessionId, +Source, -Result)
2002
+ %% Restores the living module from a history version's serialized source:
2003
+ %% abolishes the user predicates and reloads the source (directives, tools and
2004
+ %% entries come from the source too). Refused when frozen. Runtime conversation
2005
+ %% memory is preserved.
2006
+ repl_rollback(SessionId, Source, Result) :-
2007
+ catch(
2008
+ ( guard_not_frozen(SessionId),
2009
+ repl_abolish_user_predicates(SessionId),
2010
+ retractall(session_directive(SessionId, _)),
2011
+ retractall(session_user_tools(SessionId, _)),
2012
+ retractall(session_user_tool_schema(SessionId, _, _)),
2013
+ ( current_predicate(SessionId:tool_source/2)
2014
+ -> retractall(SessionId:tool_source(_, _))
2015
+ ; true
2016
+ ),
2017
+ ( Source == ""
2018
+ -> Result = ok
2019
+ ; load_code(SessionId, Source, LoadError),
2020
+ ( LoadError == none
2021
+ -> Result = ok
2022
+ ; format(atom(Result), 'error(~w)', [LoadError])
2023
+ )
2024
+ )
2025
+ ),
2026
+ RollbackError,
2027
+ format(atom(Result), 'error(~w)', [RollbackError])
2028
+ ).
2029
+
2030
+ %% repl_user_predicates(+SessionId, -Preds)
2031
+ %% Sorted Name/Arity list of the harness's own predicates.
2032
+ repl_user_predicates(SessionId, Preds) :-
2033
+ findall(Name/Arity,
2034
+ ( current_predicate(SessionId:Name/Arity),
2035
+ repl_user_predicate(SessionId, Name, Arity)
2036
+ ),
2037
+ Preds0),
2038
+ sort(Preds0, Preds).
2039
+
2040
+ %% repl_user_predicates_text(+SessionId, -Text)
2041
+ %% Rendered "name/arity, name/arity" for change diffing on the host side.
2042
+ repl_user_predicates_text(SessionId, Text) :-
2043
+ repl_user_predicates(SessionId, Preds),
2044
+ with_output_to(string(Text), print_pred_list(Preds)).
2045
+
2046
+ %% repl_predicate_arities(+SessionId, +Name, -Arities)
2047
+ %% Sorted arities of a user predicate, e.g. agent_main -> [0,1,2]. Used by
2048
+ %% `deepclause run` to pick the agent_main/N to call for the given arguments.
2049
+ repl_predicate_arities(SessionId, Name, Arities) :-
2050
+ atom(Name),
2051
+ findall(Arity,
2052
+ ( current_predicate(SessionId:Name/Arity),
2053
+ repl_user_predicate(SessionId, Name, Arity)
2054
+ ),
2055
+ Arities0),
2056
+ sort(Arities0, Arities).
2057
+
2058
+ %% ============================================================
2059
+ %% Nondeterminism probe (end_turn advisory)
2060
+ %%
2061
+ %% After a commit, the host may probe each changed pure predicate: if calling it
2062
+ %% with fresh variables yields more than a threshold of solutions, the predicate
2063
+ %% is probably missing a commit (`once/1` / `!`) and would flood an `execute`.
2064
+ %% Side-effecting predicates are never run (see repl_impure_name/1), fact tables
2065
+ %% are skipped, and the search is bounded by an inference limit.
2066
+ %% ============================================================
2067
+
2068
+ %% Functors with side effects: model calls, host tools, file IO, DB mutation.
2069
+ repl_impure_name(task). repl_impure_name(prompt). repl_impure_name(exec).
2070
+ repl_impure_name(system). repl_impure_name(user). repl_impure_name(answer).
2071
+ repl_impure_name(input). repl_impure_name(output). repl_impure_name(read_media).
2072
+ repl_impure_name(attach_media). repl_impure_name(save_image). repl_impure_name(gen_image).
2073
+ repl_impure_name(judge). repl_impure_name(choose). repl_impure_name(rate).
2074
+ repl_impure_name(verify). repl_impure_name(probability).
2075
+ repl_impure_name(write_file). repl_impure_name(append_file). repl_impure_name(delete_file).
2076
+ repl_impure_name(make_directory_path). repl_impure_name(open). repl_impure_name(close).
2077
+ repl_impure_name(assert). repl_impure_name(assertz). repl_impure_name(asserta).
2078
+ repl_impure_name(retract). repl_impure_name(retractall).
2079
+ repl_impure_name(consult). repl_impure_name(use_module). repl_impure_name(load_files).
2080
+
2081
+ %% repl_body_impure(+Goal) — true when the goal tree calls a side-effecting name.
2082
+ repl_body_impure(Goal) :-
2083
+ callable(Goal),
2084
+ functor(Goal, Name, _),
2085
+ repl_impure_name(Name),
2086
+ !.
2087
+ repl_body_impure((A, _)) :- repl_body_impure(A).
2088
+ repl_body_impure((_, B)) :- repl_body_impure(B).
2089
+ repl_body_impure((A ; B)) :- ( repl_body_impure(A) ; repl_body_impure(B) ).
2090
+ repl_body_impure((A -> B)) :- ( repl_body_impure(A) ; repl_body_impure(B) ).
2091
+ repl_body_impure(\+ Goal) :- repl_body_impure(Goal).
2092
+ repl_body_impure(once(Goal)) :- repl_body_impure(Goal).
2093
+ repl_body_impure(ignore(Goal)) :- repl_body_impure(Goal).
2094
+ repl_body_impure(forall(Gen, Action)) :- ( repl_body_impure(Gen) ; repl_body_impure(Action) ).
2095
+ repl_body_impure(catch(Goal, _, Recovery)) :- ( repl_body_impure(Goal) ; repl_body_impure(Recovery) ).
2096
+ repl_body_impure(findall(_, Goal, _)) :- repl_body_impure(Goal).
2097
+ repl_body_impure(findall(_, Goal, _, _)) :- repl_body_impure(Goal).
2098
+ repl_body_impure(call_with_inference_limit(Goal, _, _)) :- repl_body_impure(Goal).
2099
+ repl_body_impure(with_output_to(_, Goal)) :- repl_body_impure(Goal).
2100
+ repl_body_impure(_:Goal) :- repl_body_impure(Goal).
2101
+
2102
+ repl_predicate_pure(SessionId, Name, Arity) :-
2103
+ functor(Head, Name, Arity),
2104
+ \+ ( clause(SessionId:Head, Body), repl_body_impure(Body) ).
2105
+
2106
+ %% True when the predicate has at least one clause with a real body (not a fact).
2107
+ repl_predicate_has_rule(SessionId, Name, Arity) :-
2108
+ functor(Head, Name, Arity),
2109
+ clause(SessionId:Head, Body),
2110
+ Body \== true,
2111
+ !.
2112
+
2113
+ %% repl_probe_nondeterminism(+SessionId, +Name, +Arity, +Threshold, +Bound, -Result)
2114
+ %% Result: one | many | timeout | data | unsafe | removed | error.
2115
+ repl_probe_nondeterminism(SessionId, Name, Arity, Threshold, Bound, Result) :-
2116
+ ( \+ current_predicate(SessionId:Name/Arity)
2117
+ -> Result = removed
2118
+ ; \+ repl_predicate_has_rule(SessionId, Name, Arity)
2119
+ -> Result = data
2120
+ ; \+ repl_predicate_pure(SessionId, Name, Arity)
2121
+ -> Result = unsafe
2122
+ ; functor(Head, Name, Arity),
2123
+ N is Threshold + 1,
2124
+ catch(
2125
+ ( with_output_to(string(_),
2126
+ call_with_inference_limit(call_nth(SessionId:Head, N), Bound, R)),
2127
+ ( R == false -> Result = one
2128
+ ; R == inference_limit_exceeded -> Result = timeout
2129
+ ; R == true -> Result = many
2130
+ ; R == (!) -> Result = many % success is returned as `!` in this build
2131
+ ; Result = error
2132
+ )
2133
+ ),
2134
+ _,
2135
+ Result = error
2136
+ )
2137
+ ).
2138
+
2139
+ %% repl_history_listing(+SessionId)
2140
+ %% Human-readable tail for `listing(history)`.
2141
+ repl_history_listing(SessionId) :-
2142
+ ( current_predicate(SessionId:harness_head/4),
2143
+ SessionId:harness_head(Version, Timestamp, HeadSession, _Turn)
2144
+ -> format("harness_head(~w, ~w, ~w)~n", [Version, Timestamp, HeadSession])
2145
+ ; format("harness_head(none)~n")
2146
+ ),
2147
+ ( current_predicate(SessionId:harness_session/5)
2148
+ -> forall(SessionId:harness_session(Sid, Started, Model, Backend, Dir),
2149
+ format("harness_session(~w, ~w, ~w, ~w, ~w)~n", [Sid, Started, Model, Backend, Dir]))
2150
+ ; true
2151
+ ),
2152
+ ( current_predicate(SessionId:harness_change/5)
2153
+ -> forall(SessionId:harness_change(V, Ts, Kind, Sid, Turn),
2154
+ format("harness_change(~w, ~w, ~w, ~w, ~w)~n", [V, Ts, Kind, Sid, Turn]))
2155
+ ; true
2156
+ ).
2157
+
2158
+ %% repl_history_summary(+SessionId, -Text)
2159
+ %% One-line history for the meta prompt manifest.
2160
+ repl_history_summary(SessionId, Text) :-
2161
+ ( current_predicate(SessionId:harness_head/4),
2162
+ SessionId:harness_head(Version, Timestamp, HeadSession, _Turn)
2163
+ -> true
2164
+ ; Version = none, Timestamp = none, HeadSession = none
2165
+ ),
2166
+ ( current_predicate(SessionId:harness_session/5)
2167
+ -> findall(s, SessionId:harness_session(_, _, _, _, _), Sessions0),
2168
+ length(Sessions0, Sessions)
2169
+ ; Sessions = 0
2170
+ ),
2171
+ ( current_predicate(SessionId:harness_change/5)
2172
+ -> findall(c, SessionId:harness_change(_, _, _, _, _), Changes0),
2173
+ length(Changes0, Changes)
2174
+ ; Changes = 0
2175
+ ),
2176
+ format(atom(Text), 'version ~w (updated ~w by ~w), ~w session(s), ~w change(s)',
2177
+ [Version, Timestamp, HeadSession, Sessions, Changes]).
2178
+
2179
+ %% repl_meta_prompt(+SessionId, +Request, -PromptText)
2180
+ %% Assembles the deterministic code-writing prompt: authoring contract, host
2181
+ %% tool catalog, harness manifest, recent meta memory, the request, and the
2182
+ %% output contract.
2183
+ repl_meta_prompt(SessionId, Request, Prompt) :-
2184
+ ( session_meta_catalog(SessionId, Catalog) -> true ; Catalog = "" ),
2185
+ ( session_dml_reference(SessionId, Reference) -> true ; Reference = "" ),
2186
+ ( session_authoring_examples(SessionId, Authoring) -> true ; Authoring = "" ),
2187
+ ( session_harness_examples(SessionId, Examples) -> true ; Examples = "" ),
2188
+ ( session_prolog_snippets(SessionId, Snippets) -> true ; Snippets = "" ),
2189
+ repl_manifest(SessionId, Manifest),
2190
+ repl_meta_memory_text(SessionId, MemoryText),
2191
+ % The template is the bundled META_PROMPT.md, injected by the host via
2192
+ % repl_set_meta_template/2. The fallback keeps the prompt usable when no
2193
+ % template was set (e.g. a direct repl_meta_prompt/3 call).
2194
+ ( session_meta_template(SessionId, Template)
2195
+ -> true
2196
+ ; Template = "You are the DeepClause REPL harness author.~n~nHOST TOOL CATALOG~n{{CATALOG}}~n~nUSER REQUEST~n{{REQUEST}}~n"
2197
+ ),
2198
+ replace_all(Template, "{{CATALOG}}", Catalog, P1),
2199
+ replace_all(P1, "{{REFERENCE}}", Reference, P2),
2200
+ replace_all(P2, "{{AUTHORING}}", Authoring, P3),
2201
+ replace_all(P3, "{{EXAMPLES}}", Examples, P4),
2202
+ replace_all(P4, "{{SNIPPETS}}", Snippets, P5),
2203
+ replace_all(P5, "{{MANIFEST}}", Manifest, P6),
2204
+ replace_all(P6, "{{MEMORY}}", MemoryText, P7),
2205
+ replace_all(P7, "{{REQUEST}}", Request, Prompt).
2206
+
2207
+ %% replace_all(+Text, +From, +To, -Out)
2208
+ %% Literal, all-occurrences substring replacement. Fills the meta-prompt
2209
+ %% template's {{PLACEHOLDER}} sections.
2210
+ replace_all(Text, From, To, Out) :-
2211
+ string_codes(Text, TextCodes),
2212
+ string_codes(From, FromCodes),
2213
+ string_codes(To, ToCodes),
2214
+ replace_all_codes(TextCodes, FromCodes, ToCodes, OutCodes),
2215
+ string_codes(Out, OutCodes).
2216
+
2217
+ replace_all_codes([], _, _, []) :- !.
2218
+ replace_all_codes(Codes, From, To, Out) :-
2219
+ From \== [],
2220
+ ( append(From, Rest, Codes)
2221
+ -> append(To, Tail, Out),
2222
+ replace_all_codes(Rest, From, To, Tail)
2223
+ ; Codes = [C|Cs],
2224
+ Out = [C|Tail],
2225
+ replace_all_codes(Cs, From, To, Tail)
2226
+ ).
2227
+
2228
+ repl_meta_prompt(Request, Prompt) :-
2229
+ get_session_id(SessionId),
2230
+ repl_meta_prompt(SessionId, Request, Prompt).
2231
+
2232
+ %% repl_run_meta(+SessionId, +PromptText, -FrameId, -Error)
2233
+ %% Creates the meta frame running the code-writing task.
2234
+ repl_run_meta(SessionId, PromptText, FrameId, Error) :-
2235
+ catch(
2236
+ ( repl_initial_state(SessionId, meta, State),
2237
+ create_frame_raw(SessionId, meta, 0,
2238
+ deepclause_mi:repl_meta_solve(PromptText, SessionId, State),
2239
+ FrameId),
2240
+ Error = none
2241
+ ),
2242
+ CreateError,
2243
+ format(atom(Error), '~w', [CreateError])
2244
+ ).
2245
+
2246
+ repl_meta_solve(PromptText, SessionId, State) :-
2247
+ nb_setval(current_session_id, SessionId),
2248
+ catch(
2249
+ ( mi_call(
2250
+ task_named(PromptText, [DMLCode, Answer],
2251
+ [typed_var{name:'DMLCode', type:string},
2252
+ typed_var{name:'Answer', type:string}]),
2253
+ State, _StateOut),
2254
+ % The task's own memory (incl. the assembled prompt and tool results)
2255
+ % is ephemeral. The host records a clean user/assistant pair with
2256
+ % repl_record_meta/3 after the turn; that becomes the next turn's
2257
+ % conversation history (a normal messages array).
2258
+ engine_yield(meta_result(DMLCode, Answer))
2259
+ ),
2260
+ MetaError,
2261
+ ( format(atom(Msg), '~w', [MetaError]),
2262
+ engine_yield(error(Msg))
2263
+ )
2264
+ ).
2265
+
2266
+ %% repl_record_meta(+SessionId, +Request, +Summary)
2267
+ %% Appends a short outcome note to meta memory (bounded).
2268
+ repl_record_meta(SessionId, Request, Summary) :-
2269
+ ( session_meta_memory(SessionId, Memory)
2270
+ -> true
2271
+ ; Memory = []
2272
+ ),
2273
+ append(Memory,
2274
+ [message{role: user, content: Request},
2275
+ message{role: assistant, content: Summary}],
2276
+ Combined),
2277
+ % Keep every system message (compaction checkpoints) and the last 20
2278
+ % conversational messages; naive last_n/3 would evict old checkpoints.
2279
+ findall(M, (member(M, Combined), repl_system_message(M)), SystemMessages),
2280
+ findall(M, (member(M, Combined), \+ repl_system_message(M)), Conversational),
2281
+ last_n(Conversational, 20, Recent),
2282
+ append(SystemMessages, Recent, Bounded),
2283
+ retractall(session_meta_memory(SessionId, _)),
2284
+ assertz(session_meta_memory(SessionId, Bounded)).
2285
+
2286
+ %% repl_system_message(+Message)
2287
+ repl_system_message(message{role: system, content: _}).
2288
+
2289
+ %% repl_get_memory(+SessionId, +Slot, -Messages)
2290
+ %% Read a live memory slot as the message list stored in the session.
2291
+ repl_get_memory(SessionId, Slot, Messages) :-
2292
+ ( Slot == meta
2293
+ -> ( session_meta_memory(SessionId, Memory)
2294
+ -> true
2295
+ ; Memory = []
2296
+ )
2297
+ ; ( session_repl_memory(SessionId, Memory)
2298
+ -> true
2299
+ ; Memory = []
2300
+ )
2301
+ ),
2302
+ Messages = Memory.
2303
+
2304
+ %% repl_set_memory(+SessionId, +Slot, +Messages)
2305
+ %% Replace a live memory slot (used by host-side TypeScript compaction).
2306
+ repl_set_memory(SessionId, Slot, Messages) :-
2307
+ ( Slot == meta
2308
+ -> retractall(session_meta_memory(SessionId, _)),
2309
+ assertz(session_meta_memory(SessionId, Messages))
2310
+ ; retractall(session_repl_memory(SessionId, _)),
2311
+ assertz(session_repl_memory(SessionId, Messages))
2312
+ ).
2313
+
2314
+ %% ---- Edit transactions ----
2315
+
2316
+ %% repl_snapshot_save(+SessionId)
2317
+ %% Snapshots the living module, tool bookkeeping, directives, and harness
2318
+ %% memory so a failed prompt leaves the harness untouched.
2319
+ repl_snapshot_save(SessionId) :-
2320
+ retractall(repl_snapshot(SessionId, _)),
2321
+ findall(Clause,
2322
+ ( current_predicate(SessionId:Name/Arity),
2323
+ repl_user_predicate(SessionId, Name, Arity),
2324
+ functor(Head, Name, Arity),
2325
+ clause(SessionId:Head, Body),
2326
+ ( Body == true
2327
+ -> Clause = Head
2328
+ ; Clause = (Head :- Body)
2329
+ )
2330
+ ),
2331
+ Clauses),
2332
+ findall(Name, session_user_tools(SessionId, Name), Tools),
2333
+ findall(Name-Schema, session_user_tool_schema(SessionId, Name, Schema), Schemas),
2334
+ ( current_predicate(SessionId:tool_source/2)
2335
+ -> findall(Name-Source, SessionId:tool_source(Name, Source), ToolSources)
2336
+ ; ToolSources = []
2337
+ ),
2338
+ findall(Directive, session_directive(SessionId, Directive), Directives),
2339
+ ( session_repl_memory(SessionId, Memory) -> true ; Memory = [] ),
2340
+ assertz(repl_snapshot(SessionId, snapshot{
2341
+ clauses: Clauses,
2342
+ tools: Tools,
2343
+ schemas: Schemas,
2344
+ tool_sources: ToolSources,
2345
+ directives: Directives,
2346
+ memory: Memory
2347
+ })).
2348
+
2349
+ %% repl_snapshot_restore(+SessionId)
2350
+ repl_snapshot_restore(SessionId) :-
2351
+ ( repl_snapshot(SessionId, Snapshot)
2352
+ -> retract(repl_snapshot(SessionId, Snapshot)),
2353
+ findall(Name/Arity,
2354
+ ( current_predicate(SessionId:Name/Arity),
2355
+ repl_user_predicate(SessionId, Name, Arity)
2356
+ ),
2357
+ Preds),
2358
+ forall(member(AbName/AbArity, Preds),
2359
+ catch(abolish(SessionId:AbName/AbArity), _, true)),
2360
+ forall(member(Clause, Snapshot.clauses), assertz(SessionId:Clause)),
2361
+ retractall(session_user_tools(SessionId, _)),
2362
+ forall(member(Tool, Snapshot.tools), assertz(session_user_tools(SessionId, Tool))),
2363
+ retractall(session_user_tool_schema(SessionId, _, _)),
2364
+ forall(member(SchemaName-Schema, Snapshot.schemas),
2365
+ assertz(session_user_tool_schema(SessionId, SchemaName, Schema))),
2366
+ ( current_predicate(SessionId:tool_source/2)
2367
+ -> retractall(SessionId:tool_source(_, _))
2368
+ ; true
2369
+ ),
2370
+ forall(member(SourceName-Source, Snapshot.tool_sources),
2371
+ assertz(SessionId:tool_source(SourceName, Source))),
2372
+ retractall(session_directive(SessionId, _)),
2373
+ forall(member(Directive, Snapshot.directives),
2374
+ assertz(session_directive(SessionId, Directive))),
2375
+ retractall(session_repl_memory(SessionId, _)),
2376
+ assertz(session_repl_memory(SessionId, Snapshot.memory))
2377
+ ; true
2378
+ ).
2379
+
2380
+ %% repl_parse_source(+SessionId, +Source, -Preds, -Error)
2381
+ %% Validates generated DML and collects the predicates it defines.
2382
+ repl_parse_source(SessionId, Source, Preds, Error) :-
2383
+ catch(
2384
+ ( open_string(Source, Stream),
2385
+ repl_collect_source_predicates(Stream, SessionId, Preds0),
2386
+ close(Stream),
2387
+ ( member(Name0/_, Preds0),
2388
+ repl_protected_name(Name0)
2389
+ -> format(atom(Error), 'generated code defines reserved predicate ~w', [Name0])
2390
+ ; sort(Preds0, Preds),
2391
+ Error = none
2392
+ )
2393
+ ),
2394
+ ParseError,
2395
+ format(atom(Error), '~w', [ParseError])
2396
+ ).
2397
+
2398
+ %% repl_collect_source_predicates(+Stream, +SessionId, -Preds)
2399
+ %% Reads with the session's operator table and executes `:- use_module(...)`
2400
+ %% directives as it goes, exactly like the load path. This is what lets library
2401
+ %% operators (CLP(FD)'s #=/ins, etc.) be used in committed source: without it,
2402
+ %% validation parsed the source with an empty operator table and rejected code
2403
+ %% that loads fine.
2404
+ repl_collect_source_predicates(Stream, SessionId, Preds) :-
2405
+ read_term(Stream, Term, [module(SessionId)]),
2406
+ ( Term == end_of_file
2407
+ -> Preds = []
2408
+ ; ( Term = (:- Directive)
2409
+ -> catch(process_directive(Directive, SessionId), _, true),
2410
+ Preds = Rest
2411
+ ; Term = repl_delete(_)
2412
+ -> Preds = Rest
2413
+ ; Term = (Head :- _)
2414
+ -> functor(Head, Name, Arity),
2415
+ Preds = [Name/Arity|Rest]
2416
+ ; functor(Term, Name, Arity),
2417
+ Preds = [Name/Arity|Rest]
2418
+ ),
2419
+ repl_collect_source_predicates(Stream, SessionId, Rest)
2420
+ ).
2421
+
2422
+ %% repl_commit(+SessionId, +Source, -Result)
2423
+ %% Transactional commit: validate, replace-by-predicate, then load the returned
2424
+ %% source. Result = ok | 'error(Message)'. Never throws; failures are reported
2425
+ %% through Result so the caller can decide whether to repair.
2426
+ repl_commit(SessionId, Source, Result) :-
2427
+ catch(
2428
+ repl_commit_unsafe(SessionId, Source, Result),
2429
+ CommitError,
2430
+ format(atom(Result), 'error(~w)', [CommitError])
2431
+ ).
2432
+
2433
+ repl_commit_unsafe(SessionId, Source, Result) :-
2434
+ guard_not_frozen(SessionId),
2435
+ repl_parse_source(SessionId, Source, Preds, ParseError),
2436
+ repl_commit_from_parse(SessionId, Source, Preds, ParseError, Result).
2437
+
2438
+ repl_commit_from_parse(_SessionId, _Source, _Preds, ParseError, Result) :-
2439
+ ParseError \== none,
2440
+ !,
2441
+ commit_error(ParseError, Result).
2442
+ repl_commit_from_parse(SessionId, Source, Preds, none, Result) :-
2443
+ forall(member(Name/Arity, Preds),
2444
+ catch(abolish(SessionId:Name/Arity), _, true)),
2445
+ load_code(SessionId, Source, LoadError),
2446
+ ( LoadError == none
2447
+ -> retractall(session_last_commit(SessionId, _)),
2448
+ assertz(session_last_commit(SessionId, Preds))
2449
+ ; true
2450
+ ),
2451
+ repl_commit_from_load(LoadError, Result).
2452
+
2453
+ %% repl_last_commit_summary(+SessionId, -Text)
2454
+ %% Rendered "name/arity, name/arity" of the predicates the last commit replaced.
2455
+ repl_last_commit_summary(SessionId, Text) :-
2456
+ ( session_last_commit(SessionId, Preds), Preds \= []
2457
+ -> with_output_to(string(Text), print_pred_list(Preds))
2458
+ ; Text = ""
2459
+ ).
2460
+
2461
+ print_pred_list([]).
2462
+ print_pred_list([Name/Arity]) :-
2463
+ !,
2464
+ format("~w/~w", [Name, Arity]).
2465
+ print_pred_list([Name/Arity|Rest]) :-
2466
+ format("~w/~w, ", [Name, Arity]),
2467
+ print_pred_list(Rest).
2468
+
2469
+ repl_commit_from_load(LoadError, Result) :-
2470
+ LoadError \== none,
2471
+ !,
2472
+ commit_error(LoadError, Result).
2473
+ repl_commit_from_load(none, ok).
2474
+
2475
+ commit_error(Message, Result) :-
2476
+ format(atom(Result), 'error(~w)', [Message]).
2477
+
2478
+ %% repl_execute_frame(+SessionId, +GoalText, -FrameId, -Error)
2479
+ %% Creates a child frame for the meta `execute` tool.
2480
+ repl_execute_frame(SessionId, GoalText, FrameId, Error) :-
2481
+ catch(
2482
+ ( read_repl_goal(SessionId, GoalText, Goal, Names),
2483
+ ( repl_reserved(Goal)
2484
+ -> throw(error(permission_error(execute, reserved_predicate, Goal),
2485
+ context(repl_execute_frame/4, 'cannot execute a reserved predicate')))
2486
+ ; true
2487
+ ),
2488
+ repl_initial_state(SessionId, nested, State),
2489
+ create_frame_raw(SessionId, nested, 1,
2490
+ deepclause_mi:repl_solve(Goal, Names, SessionId, State),
2491
+ FrameId),
2492
+ Error = none
2493
+ ),
2494
+ ExecuteError,
2495
+ format(atom(Error), '~w', [ExecuteError])
2496
+ ).
2497
+
493
2498
  %% determine_agent_goal(+SessionId, +Args, -Goal)
494
2499
  %% Args is a list of positional arguments - passed directly to agent_main
495
2500
  %% Prefers arity that matches the number of arguments provided
@@ -529,31 +2534,12 @@ determine_agent_goal(SessionId, Args, Goal) :-
529
2534
  determine_agent_goal(SessionId, _Args, SessionId:agent_main).
530
2535
 
531
2536
  %% step_engine(+SessionId, -Status, -Content, -Payload)
2537
+ %% Legacy single-engine entry point: resolves the session's default frame and
2538
+ %% delegates to step_frame/4. Kept as a thin wrapper so existing callers are
2539
+ %% unaffected by the frame refactor.
532
2540
  step_engine(SessionId, Status, Content, Payload) :-
533
- ( session_engine(SessionId, Engine)
534
- -> catch(
535
- ( engine_next(Engine, Result)
536
- -> process_engine_result(Result, Status, Content, Payload)
537
- ; % Engine finished - return trace if enabled
538
- Status = finished,
539
- Content = '',
540
- ( session_trace_enabled(SessionId, true),
541
- session_trace_log(SessionId, TraceLog)
542
- -> Payload = payload{trace: TraceLog}
543
- ; Payload = none
544
- )
545
- ),
546
- Error,
547
- ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
548
- Status = error,
549
- Content = ErrMsg,
550
- ( session_trace_enabled(SessionId, true),
551
- session_trace_log(SessionId, TraceLog)
552
- -> Payload = payload{trace: TraceLog}
553
- ; Payload = none
554
- )
555
- )
556
- )
2541
+ ( session_default_frame(SessionId, FrameId)
2542
+ -> step_frame(FrameId, Status, Content, Payload)
557
2543
  ; Status = error,
558
2544
  Content = 'No engine found for session',
559
2545
  Payload = none
@@ -561,8 +2547,15 @@ step_engine(SessionId, Status, Content, Payload) :-
561
2547
 
562
2548
  %% process_engine_result(+Result, -Status, -Content, -Payload)
563
2549
  process_engine_result(output(Text), output, Text, none) :- !.
2550
+ process_engine_result(media(Payload), media, '', Payload) :- is_dict(Payload), !.
564
2551
  process_engine_result(log(Text), log, Text, none) :- !.
565
2552
  process_engine_result(answer(Text), answer, Text, none) :- !.
2553
+ %% Current 8-arg format (with model spec from with_model/2)
2554
+ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec), request_agent_loop, '',
2555
+ payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec}) :- !.
2556
+ %% Prompt-family 6-arg format (tool scope + model spec; no reasoning/recipe)
2557
+ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ModelSpec), request_agent_loop, '',
2558
+ payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: none, recipeContext: "", modelSpec: ModelSpec}) :- !.
566
2559
  %% Note: Memory and tool scope are now passed in the payload for the agent loop
567
2560
  process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
568
2561
  payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext}) :- !.
@@ -574,14 +2567,25 @@ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), request_age
574
2567
  payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: none, reasoningEffort: none, recipeContext: ""}) :- !.
575
2568
  process_engine_result(request_exec(Tool, Args), request_exec, '',
576
2569
  payload{toolName: Tool, args: Args}) :- !.
2570
+ process_engine_result(request_sample_token(Prompt, Allowed, ModelSpec), request_sample_token, '',
2571
+ payload{prompt: Prompt, allowedTokens: Allowed, modelSpec: ModelSpec}) :- !.
577
2572
  process_engine_result(request_sample_token(Prompt, Allowed), request_sample_token, '',
578
- payload{prompt: Prompt, allowedTokens: Allowed}) :- !.
2573
+ payload{prompt: Prompt, allowedTokens: Allowed, modelSpec: none}) :- !.
2574
+ process_engine_result(request_llm(Messages, ModelSpec), request_llm, '',
2575
+ payload{messages: Messages, modelSpec: ModelSpec}) :- !.
579
2576
  process_engine_result(request_llm(Messages), request_llm, '',
580
- payload{messages: Messages}) :- !.
2577
+ payload{messages: Messages, modelSpec: none}) :- !.
581
2578
  process_engine_result(request_judgment(Payload), request_judgment, '', Payload) :- !.
582
2579
  %% Tool result from inline tool execution
583
2580
  process_engine_result(tool_result(Result), tool_result, '',
584
2581
  payload{result: Result}) :- !.
2582
+ %% Goal-mode solution: bindings is a list of Name-RenderedText pairs.
2583
+ process_engine_result(solution(Bindings, Deterministic), solution, '',
2584
+ payload{bindings: Bindings, deterministic: Deterministic}) :- !.
2585
+ %% Prompt-mode meta task result: generated DML source and an optional direct
2586
+ %% answer for simple questions.
2587
+ process_engine_result(meta_result(Code, Answer), meta_result, '',
2588
+ payload{code: Code, answer: Answer}) :- !.
585
2589
  process_engine_result(wait_input(Prompt), wait_input, Prompt, none) :- !.
586
2590
  process_engine_result(error(Msg), error, Msg, none) :- !.
587
2591
  process_engine_result(Other, error, Msg, none) :-
@@ -589,15 +2593,23 @@ process_engine_result(Other, error, Msg, none) :-
589
2593
 
590
2594
  %% destroy_engine(+SessionId)
591
2595
  destroy_engine(SessionId) :-
2596
+ % Destroy every frame belonging to this session (the default query frame and
2597
+ % any nested frames still alive). end_frame/1 also clears the default pointer.
2598
+ ( findall(FrameId, session_frame(FrameId, SessionId, _, _), FrameIds)
2599
+ -> forall(member(FrameId, FrameIds), end_frame(FrameId))
2600
+ ; true
2601
+ ),
592
2602
  ( session_engine(SessionId, Engine)
593
2603
  -> catch(engine_destroy(Engine), _, true)
594
2604
  ; true
595
2605
  ),
2606
+ retractall(session_default_frame(SessionId, _)),
596
2607
  retractall(session_engine(SessionId, _)),
597
2608
  retractall(session_params(SessionId, _)),
598
2609
  retractall(session_user_tools(SessionId, _)),
599
2610
  retractall(session_user_tool_schema(SessionId, _, _)),
600
2611
  retractall(session_pending_input(SessionId, _)),
2612
+ retractall(session_input_cancelled(SessionId)),
601
2613
  retractall(session_agent_result(SessionId, _)),
602
2614
  retractall(session_exec_result(SessionId, _)),
603
2615
  retractall(session_sample_token_result(SessionId, _)),
@@ -671,6 +2683,13 @@ provide_input(SessionId, Input) :-
671
2683
  assertz(session_pending_input(SessionId, Input)),
672
2684
  post_signal_to_engine(SessionId, input_provided).
673
2685
 
2686
+ %% provide_input_cancelled(+SessionId)
2687
+ %% Non-interactive runs have no human. Signal the waiting input/2 to fail
2688
+ %% instead of blocking.
2689
+ provide_input_cancelled(SessionId) :-
2690
+ assertz(session_input_cancelled(SessionId)),
2691
+ post_signal_to_engine(SessionId, input_provided).
2692
+
674
2693
  %% post_signal_to_engine(+SessionId, +Signal)
675
2694
  %% Posts a signal via dynamic predicate (most reliable for WASM engines)
676
2695
  %% Falls back to engine_post only if dynamic assert fails
@@ -790,6 +2809,25 @@ get_reasoning_effort(State, Effort) :-
790
2809
  ; Effort = none
791
2810
  ).
792
2811
 
2812
+ %% set_model_state(+StateIn, +Spec, -StateOut)
2813
+ set_model_state(StateIn, Spec, StateOut) :-
2814
+ StateOut = StateIn.put(model_spec, Spec).
2815
+
2816
+ %% clear_model_state(+StateIn, -StateOut)
2817
+ clear_model_state(StateIn, StateOut) :-
2818
+ ( get_dict(model_spec, StateIn, _)
2819
+ -> del_dict(model_spec, StateIn, _, StateOut)
2820
+ ; StateOut = StateIn
2821
+ ).
2822
+
2823
+ %% get_model_spec(+State, -Spec)
2824
+ %% `none` means "no override" (use the session model).
2825
+ get_model_spec(State, Spec) :-
2826
+ ( get_dict(model_spec, State, Spec)
2827
+ -> true
2828
+ ; Spec = none
2829
+ ).
2830
+
793
2831
  %% ============================================================
794
2832
  %% Recipe Context Helpers
795
2833
  %% ============================================================
@@ -1166,8 +3204,9 @@ mi_call(task(Desc), StateIn, StateOut) :-
1166
3204
  get_tool_scope(StateIn, ToolScope),
1167
3205
  get_reasoning_effort(StateIn, ReasoningEffort),
1168
3206
  get_recipe_context(StateIn, RecipeContext),
3207
+ get_model_spec(StateIn, ModelSpec),
1169
3208
  % Yield request with current memory and tool scope
1170
- engine_yield(request_agent_loop(InterpDesc, [], UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext)),
3209
+ engine_yield(request_agent_loop(InterpDesc, [], UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec)),
1171
3210
  % Handle signals (tool calls) until agent_done
1172
3211
  handle_agent_signals(SessionId, StateIn, StateAfterAgent),
1173
3212
  session_agent_result(SessionId, Result),
@@ -1237,7 +3276,8 @@ mi_call(prompt(Desc), StateIn, StateOut) :-
1237
3276
  get_depth(StateIn, Depth),
1238
3277
  add_trace_entry(SessionId, llm_call, prompt, [InterpDesc], Depth),
1239
3278
  % prompt/N is a simple LLM call - no tools, no tool scope
1240
- engine_yield(request_agent_loop(InterpDesc, [], [], [], none)),
3279
+ get_model_spec(StateIn, ModelSpec),
3280
+ engine_yield(request_agent_loop(InterpDesc, [], [], [], none, ModelSpec)),
1241
3281
  % Wait for agent_done (no tool calls expected since no tools provided)
1242
3282
  handle_agent_signals(SessionId, StateIn, StateOut),
1243
3283
  session_agent_result(SessionId, Result),
@@ -1272,7 +3312,8 @@ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
1272
3312
  get_depth(StateIn, Depth),
1273
3313
  add_trace_entry(SessionId, llm_call, prompt, [InterpDesc], Depth),
1274
3314
  % prompt/N is a simple LLM call - no tools, no tool scope
1275
- engine_yield(request_agent_loop(InterpDesc, VarNames, [], [], none)),
3315
+ get_model_spec(StateIn, ModelSpec),
3316
+ engine_yield(request_agent_loop(InterpDesc, VarNames, [], [], none, ModelSpec)),
1276
3317
  % Wait for agent_done (no tool calls expected since no tools provided)
1277
3318
  handle_agent_signals(SessionId, StateIn, StateOut),
1278
3319
  session_agent_result(SessionId, Result),
@@ -1299,7 +3340,8 @@ mi_call_sample_token(Desc, Allowed, Token, StateIn, StateOut) :-
1299
3340
  get_session_id(SessionId),
1300
3341
  get_depth(StateIn, Depth),
1301
3342
  add_trace_entry(SessionId, llm_call, sample_token, [InterpDesc, Allowed], Depth),
1302
- engine_yield(request_sample_token(InterpDesc, Allowed)),
3343
+ get_model_spec(StateIn, ModelSpec),
3344
+ engine_yield(request_sample_token(InterpDesc, Allowed, ModelSpec)),
1303
3345
  wait_sample_token_signal(SessionId),
1304
3346
  ( session_sample_token_result(SessionId, Result)
1305
3347
  -> retract(session_sample_token_result(SessionId, _)),
@@ -1507,7 +3549,8 @@ mi_call_llm(Messages, Reply, StateIn, StateOut) :-
1507
3549
  get_session_id(SessionId),
1508
3550
  get_depth(StateIn, Depth),
1509
3551
  add_trace_entry(SessionId, llm_call, llm, [Messages], Depth),
1510
- engine_yield(request_llm(Messages)),
3552
+ get_model_spec(StateIn, ModelSpec),
3553
+ engine_yield(request_llm(Messages, ModelSpec)),
1511
3554
  wait_llm_signal(SessionId),
1512
3555
  ( session_llm_result(SessionId, Result)
1513
3556
  -> retract(session_llm_result(SessionId, _)),
@@ -1550,7 +3593,8 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
1550
3593
  get_tool_scope(StateIn, ToolScope),
1551
3594
  get_reasoning_effort(StateIn, ReasoningEffort),
1552
3595
  get_recipe_context(StateIn, RecipeContext),
1553
- engine_yield(request_agent_loop(InterpDesc, VarNames, UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext)),
3596
+ get_model_spec(StateIn, ModelSpec),
3597
+ engine_yield(request_agent_loop(InterpDesc, VarNames, UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec)),
1554
3598
  % Handle signals (tool calls) until agent_done
1555
3599
  handle_agent_signals(SessionId, StateIn, StateAfterAgent),
1556
3600
  session_agent_result(SessionId, Result),
@@ -1665,6 +3709,17 @@ mi_call(with_reasoning(Goal, Effort), StateIn, StateOut) :-
1665
3709
  mi_call(Goal, ScopedState, StateWithScope),
1666
3710
  clear_reasoning_effort_state(StateWithScope, StateOut).
1667
3711
 
3712
+ %% mi_call(with_model(Spec, Goal), +StateIn, -StateOut)
3713
+ %% Run Goal with a specific model, and optionally a reasoning level. `Spec` is a
3714
+ %% string — "provider:model", "provider:model#reasoning", "model",
3715
+ %% "model#reasoning" — or a model{provider, id, temperature, maxTokens, reasoning}
3716
+ %% dict. It applies to task/N, prompt/N and sample_token/N (not judgments).
3717
+ mi_call(with_model(Spec, Goal), StateIn, StateOut) :-
3718
+ !,
3719
+ set_model_state(StateIn, Spec, ScopedState),
3720
+ mi_call(Goal, ScopedState, StateWithScope),
3721
+ clear_model_state(StateWithScope, StateOut).
3722
+
1668
3723
  %% mi_call(with_recipe(Goal, Query), +StateIn, -StateOut)
1669
3724
  %% Run Goal with a recipe loaded as transient context (not persisted to memory).
1670
3725
  mi_call(with_recipe(Goal, Query), StateIn, StateOut) :-
@@ -1697,6 +3752,80 @@ mi_call(get_memory(Messages), StateIn, StateIn) :-
1697
3752
  !,
1698
3753
  get_memory(StateIn, Messages).
1699
3754
 
3755
+ %% ============================================================
3756
+ %% Media (see docs/DML_MEDIA.md)
3757
+ %% ============================================================
3758
+
3759
+ %% mi_call(read_media(+Path, -Media), +StateIn, -StateOut)
3760
+ %% Binds Media = media(Path, Mime) for a workspace-relative image. The bytes
3761
+ %% stay on disk; only the reference enters memory.
3762
+ mi_call(read_media(Path, Media), StateIn, StateIn) :-
3763
+ !,
3764
+ get_params(StateIn, Params),
3765
+ interpolate_desc(Path, Params, P),
3766
+ media_mime(P, Mime),
3767
+ Media = media(P, Mime).
3768
+
3769
+ %% mi_call(attach_media(+Media), +StateIn, -StateOut)
3770
+ %% Appends a user memory message carrying a reference-only media entry, so the
3771
+ %% next task/N can see the image.
3772
+ mi_call(attach_media(Media), StateIn, StateOut) :-
3773
+ !,
3774
+ media_dict(Media, media{path: Path, mime: Mime}),
3775
+ format(atom(Note), '[attached ~w: ~w]', [Mime, Path]),
3776
+ Message = message{role: user, content: Note, media: [media{path: Path, mime: Mime}]},
3777
+ get_memory(StateIn, Memory),
3778
+ append(Memory, [Message], NewMemory),
3779
+ set_memory(StateIn, NewMemory, StateOut).
3780
+
3781
+ %% mi_call(save_image(+Media, +OutPath), +StateIn, -StateOut)
3782
+ %% Write a media reference to a workspace file: `save_image(media(Path, Mime), "out.png")`,
3783
+ %% `save_image(media{svg: Svg}, "out.svg")`. Parent directories must exist.
3784
+ mi_call(save_image(Media, Out), StateIn, StateIn) :-
3785
+ !,
3786
+ media_dict(Media, Dict),
3787
+ ( get_dict(kind, Dict, inline_svg),
3788
+ get_dict(data, Dict, Svg)
3789
+ -> resolve_workspace_path(Out, OutAbs),
3790
+ open(OutAbs, write, Stream),
3791
+ write(Stream, Svg),
3792
+ close(Stream)
3793
+ ; get_dict(path, Dict, Path),
3794
+ Path \== ""
3795
+ -> resolve_workspace_path(Path, InAbs),
3796
+ resolve_workspace_path(Out, OutAbs),
3797
+ copy_file(InAbs, OutAbs)
3798
+ ; throw(error(domain_error(save_image, Media), save_image/2))
3799
+ ).
3800
+
3801
+ %% media_dict(+Media, -Dict)
3802
+ %% Normalizes an image reference for display/attachment:
3803
+ %% media(Path, Mime) | media(Path) | a bare Path string
3804
+ %% media{path: Path} | media{path: Path, mime: Mime}
3805
+ %% media{svg: SvgString} (inline SVG)
3806
+ %% media{data: Base64} | media{data: Base64, mime: Mime} (inline bytes)
3807
+ media_dict(media(Path, Mime), media{path: Path, mime: Mime}) :- !.
3808
+ media_dict(media(Path), media{path: Path, mime: Mime}) :- !, media_mime(Path, Mime).
3809
+ media_dict(media{path: Path, mime: Mime}, media{path: Path, mime: Mime}) :- !.
3810
+ media_dict(media{path: Path}, media{path: Path, mime: Mime}) :- !, media_mime(Path, Mime).
3811
+ media_dict(media{svg: Svg}, media{path: "", mime: "image/svg+xml", kind: inline_svg, data: Svg}) :- !.
3812
+ media_dict(media{data: Data, mime: Mime}, media{path: "", mime: Mime, kind: inline_data, data: Data}) :- !.
3813
+ media_dict(media{data: Data}, media{path: "", mime: "image/png", kind: inline_data, data: Data}) :- !.
3814
+ media_dict(Path, media{path: Path, mime: Mime}) :- media_mime(Path, Mime).
3815
+
3816
+ %% media_mime(+Path, -Mime)
3817
+ %% Allowed media types, matching pi image content blocks.
3818
+ media_mime(Path, Mime) :-
3819
+ ( string(Path) -> atom_string(Atom, Path) ; Atom = Path ),
3820
+ ( sub_atom(Atom, _, _, 0, '.png') -> Mime = "image/png"
3821
+ ; sub_atom(Atom, _, _, 0, '.svg') -> Mime = "image/svg+xml"
3822
+ ; sub_atom(Atom, _, _, 0, '.jpg') -> Mime = "image/jpeg"
3823
+ ; sub_atom(Atom, _, _, 0, '.jpeg') -> Mime = "image/jpeg"
3824
+ ; sub_atom(Atom, _, _, 0, '.gif') -> Mime = "image/gif"
3825
+ ; sub_atom(Atom, _, _, 0, '.webp') -> Mime = "image/webp"
3826
+ ; fail
3827
+ ).
3828
+
1700
3829
  %% ============================================================
1701
3830
  %% Output Predicates
1702
3831
  %% ============================================================
@@ -1709,6 +3838,29 @@ mi_call(answer(Text), StateIn, StateIn) :-
1709
3838
  engine_yield(answer(InterpText)),
1710
3839
  throw('$answer_commit').
1711
3840
 
3841
+ %% mi_call(output(Media), +StateIn, -StateOut)
3842
+ %% Present an image instead of text: `output(media(Path, Mime))`, `output(media(Path))`,
3843
+ %% or a dict form (`media{svg: Svg}`, `media{data: Base64, mime: Mime}`). The host
3844
+ %% renders it inline (kitty/iTerm2) or reports the path.
3845
+ %% This clause must come before the generic `output(Text)` clause; it fails
3846
+ %% through for anything that is not a media reference.
3847
+ mi_call(output(Media), StateIn, StateIn) :-
3848
+ nonvar(Media),
3849
+ ( Media = media(_, _)
3850
+ ; Media = media(_)
3851
+ ; is_dict(Media)
3852
+ ),
3853
+ media_dict(Media, Dict),
3854
+ !,
3855
+ get_dict(mime, Dict, Mime),
3856
+ get_dict(path, Dict, Path),
3857
+ ( get_dict(data, Dict, Data) -> true ; Data = "" ),
3858
+ ( get_dict(kind, Dict, Kind) -> true ; Kind = file ),
3859
+ get_session_id(SessionId),
3860
+ get_depth(StateIn, Depth),
3861
+ add_trace_entry(SessionId, media, media, [Path], Depth),
3862
+ engine_yield(media(media{path: Path, mimeType: Mime, kind: Kind, data: Data})).
3863
+
1712
3864
  %% mi_call(output(Text), +StateIn, -StateOut)
1713
3865
  mi_call(output(Text), StateIn, StateIn) :-
1714
3866
  get_params(StateIn, Params),
@@ -1729,8 +3881,11 @@ mi_call(input(Prompt, Input), StateIn, StateIn) :-
1729
3881
  engine_yield(wait_input(InterpPrompt)),
1730
3882
  % Use get_next_signal helper which handles both dynamic predicate and engine_fetch
1731
3883
  get_next_signal(SessionId, _Signal),
1732
- % Retrieve the input provided via provide_input/2
1733
- ( session_pending_input(SessionId, Input)
3884
+ % A cancelled input request fails the goal (non-interactive policy).
3885
+ ( session_input_cancelled(SessionId)
3886
+ -> retractall(session_input_cancelled(SessionId)),
3887
+ fail
3888
+ ; session_pending_input(SessionId, Input)
1734
3889
  -> retract(session_pending_input(SessionId, Input)),
1735
3890
  add_trace_with_result(SessionId, exit, input, [InterpPrompt], Input, Depth)
1736
3891
  ; Input = "", % Default to empty if no input provided
@@ -1752,6 +3907,27 @@ mi_call(log(Text), StateIn, StateIn) :-
1752
3907
  interpolate_desc(Text, Params, InterpText),
1753
3908
  engine_yield(log(InterpText)).
1754
3909
 
3910
+ %% mi_call(format(Format, Args), +StateIn, -StateOut)
3911
+ %% In REPL mode, capture format/2 output as an output event rather than
3912
+ %% writing to the process stdout. Outside the REPL (runDML), keep normal
3913
+ %% behavior so existing traces/prompts are unaffected.
3914
+ mi_call(format(Format, Args), StateIn, StateIn) :-
3915
+ get_session_id(SessionId),
3916
+ ( session_repl_mode(SessionId, true)
3917
+ -> with_output_to(string(Text), format(Format, Args)),
3918
+ mi_call(output(Text), StateIn, StateIn)
3919
+ ; format(Format, Args)
3920
+ ).
3921
+
3922
+ %% mi_call(writeln(Value), +StateIn, -StateOut)
3923
+ mi_call(writeln(Value), StateIn, StateIn) :-
3924
+ get_session_id(SessionId),
3925
+ ( session_repl_mode(SessionId, true)
3926
+ -> with_output_to(string(Text), writeln(Value)),
3927
+ mi_call(output(Text), StateIn, StateIn)
3928
+ ; writeln(Value)
3929
+ ).
3930
+
1755
3931
  %% ============================================================
1756
3932
  %% Context Stack Management - for memory save/restore
1757
3933
  %% ============================================================
@@ -1878,20 +4054,57 @@ mi_call(false, _StateIn, _StateOut) :-
1878
4054
  %% mi_call(retractall(Head), +StateIn, -StateOut)
1879
4055
  %% Database predicates must operate on the session module so DML-side facts
1880
4056
  %% (draft/4, claim/3, ...) are asserted/retracted in the right module.
4057
+ %% repl_reserved_clause(+Term)
4058
+ %% True when Term defines a reserved predicate. directives and the legitimate
4059
+ %% repl_delete harness form are exempt.
4060
+ repl_reserved_clause((:- _)) :-
4061
+ !,
4062
+ fail.
4063
+ repl_reserved_clause(repl_delete(_)) :-
4064
+ !,
4065
+ fail.
4066
+ repl_reserved_clause((tool(ToolHead, _) :- _)) :-
4067
+ !,
4068
+ functor(ToolHead, Name, _),
4069
+ repl_reserved_name(Name).
4070
+ repl_reserved_clause((tool(ToolHead) :- _)) :-
4071
+ !,
4072
+ functor(ToolHead, Name, _),
4073
+ repl_reserved_name(Name).
4074
+ repl_reserved_clause((Head :- _)) :-
4075
+ functor(Head, Name, _),
4076
+ repl_protected_name(Name).
4077
+ repl_reserved_clause(Head) :-
4078
+ functor(Head, Name, _),
4079
+ repl_protected_name(Name).
4080
+
1881
4081
  mi_call(assertz(Head), StateIn, StateIn) :-
1882
4082
  get_session_id(SessionId),
4083
+ guard_not_frozen(SessionId),
4084
+ guard_reserved_clause(Head),
1883
4085
  assertz(SessionId:Head).
1884
4086
 
1885
4087
  mi_call(asserta(Head), StateIn, StateIn) :-
1886
4088
  get_session_id(SessionId),
4089
+ guard_not_frozen(SessionId),
4090
+ guard_reserved_clause(Head),
1887
4091
  asserta(SessionId:Head).
1888
4092
 
4093
+ guard_reserved_clause(Head) :-
4094
+ ( repl_reserved_clause(Head)
4095
+ -> throw(error(permission_error(modify, reserved_name, Head),
4096
+ context(mi_call/3, 'cannot define a reserved predicate')))
4097
+ ; true
4098
+ ).
4099
+
1889
4100
  mi_call(retract(Head), StateIn, StateIn) :-
1890
4101
  get_session_id(SessionId),
4102
+ guard_not_frozen(SessionId),
1891
4103
  retract(SessionId:Head).
1892
4104
 
1893
4105
  mi_call(retractall(Head), StateIn, StateIn) :-
1894
4106
  get_session_id(SessionId),
4107
+ guard_not_frozen(SessionId),
1895
4108
  retractall(SessionId:Head).
1896
4109
 
1897
4110
  %% ============================================================
@@ -2100,6 +4313,7 @@ resolve_workspace_path(RelPath, FullPath) :-
2100
4313
  mi_call(consult(File), StateIn, StateIn) :-
2101
4314
  !,
2102
4315
  get_session_id(SessionId),
4316
+ guard_not_frozen(SessionId),
2103
4317
  resolve_workspace_path(File, FullPath),
2104
4318
  readutil:read_file_to_string(FullPath, Code, []),
2105
4319
  consult_string(Code, SessionId).
@@ -2146,7 +4360,14 @@ consult_clauses(Stream, SessionId) :-
2146
4360
  ).
2147
4361
 
2148
4362
  %% consult_process_term(+Term, +SessionId)
2149
- %% Process a single term read during consult
4363
+ %% Process a single term read during consult. Reserved-name definitions are
4364
+ %% refused so consulted files cannot shadow the REPL control surface.
4365
+ consult_process_term(Term, _SessionId) :-
4366
+ repl_reserved_clause(Term),
4367
+ !,
4368
+ throw(error(permission_error(modify, reserved_name, Term),
4369
+ context(consult_process_term/2, 'reserved predicate'))).
4370
+
2150
4371
  consult_process_term(:-(Directive), SessionId) :-
2151
4372
  % Handle directives - route through mi_call so custom predicates like consult/1 work
2152
4373
  !,
@@ -2165,7 +4386,7 @@ consult_process_term((tool(ToolHead, Description) :- Body), SessionId) :-
2165
4386
  extract_tool_schema(ToolHead, Description, ToolName, Schema),
2166
4387
  assertz(session_user_tools(SessionId, ToolName)),
2167
4388
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
2168
- format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
4389
+ format_tool_source(ToolHead, Description, Body, SourceCode),
2169
4390
  assertz(SessionId:tool_source(ToolName, SourceCode)),
2170
4391
  assertz(SessionId:(tool(ToolHead) :- Body)).
2171
4392
 
@@ -2175,7 +4396,7 @@ consult_process_term((tool(ToolHead) :- Body), SessionId) :-
2175
4396
  extract_tool_schema(ToolHead, none, ToolName, Schema),
2176
4397
  assertz(session_user_tools(SessionId, ToolName)),
2177
4398
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
2178
- format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
4399
+ format_tool_source(ToolHead, none, Body, SourceCode),
2179
4400
  assertz(SessionId:tool_source(ToolName, SourceCode)),
2180
4401
  assertz(SessionId:(tool(ToolHead) :- Body)).
2181
4402
 
@@ -2284,6 +4505,22 @@ mi_call(delete_directory(Dir), StateIn, StateIn) :-
2284
4505
  resolve_workspace_path(Dir, FullPath),
2285
4506
  delete_directory(FullPath).
2286
4507
 
4508
+ %% mi_call(size_file(File, Size), +StateIn, -StateOut)
4509
+ %% size_file/2 is a plain SWI builtin, so it would otherwise resolve `File`
4510
+ %% against the WASM process cwd (not the workspace) and demand `/workspace/...`.
4511
+ mi_call(size_file(File, Size), StateIn, StateIn) :-
4512
+ !,
4513
+ resolve_workspace_path(File, FullPath),
4514
+ size_file(FullPath, Size).
4515
+
4516
+ %% mi_call(time_file(File, Time), +StateIn, -StateOut)
4517
+ %% Modifications time (seconds since epoch); same workspace-relative rule as
4518
+ %% size_file/2.
4519
+ mi_call(time_file(File, Time), StateIn, StateIn) :-
4520
+ !,
4521
+ resolve_workspace_path(File, FullPath),
4522
+ time_file(FullPath, Time).
4523
+
2287
4524
  %% ============================================================
2288
4525
  %% Catch/Throw - STATE THREADED
2289
4526
  %% ============================================================
@@ -2365,6 +4602,9 @@ is_mi_special_predicate(yield(_)).
2365
4602
  is_mi_special_predicate(log(_)).
2366
4603
  is_mi_special_predicate(system(_)).
2367
4604
  is_mi_special_predicate(user(_)). %% Add user message to context
4605
+ is_mi_special_predicate(read_media(_,_)).
4606
+ is_mi_special_predicate(attach_media(_)).
4607
+ is_mi_special_predicate(save_image(_,_)).
2368
4608
  is_mi_special_predicate(task(_)).
2369
4609
  is_mi_special_predicate(task(_,_)).
2370
4610
  is_mi_special_predicate(task(_,_,_)).
@@ -2388,6 +4628,7 @@ is_mi_special_predicate(with_tools(_,_)).
2388
4628
  is_mi_special_predicate(without_tools(_,_)).
2389
4629
  %% Reasoning effort scoping
2390
4630
  is_mi_special_predicate(with_reasoning(_,_)).
4631
+ is_mi_special_predicate(with_model(_,_)).
2391
4632
  %% Recipe context scoping
2392
4633
  is_mi_special_predicate(with_recipe(_,_)).
2393
4634
  %% Semantic judgment predicates
@@ -2402,6 +4643,16 @@ is_mi_special_predicate(with_judgment(_,_)).
2402
4643
  is_mi_special_predicate(require_judgment(_,_)).
2403
4644
  %% Context stack management
2404
4645
  is_mi_special_predicate(get_memory(_)).
4646
+ is_mi_special_predicate(format(_,_)).
4647
+ is_mi_special_predicate(writeln(_)).
4648
+ is_mi_special_predicate(input(_,_)).
4649
+ is_mi_special_predicate(call(_)).
4650
+ is_mi_special_predicate(call(_,_)).
4651
+ is_mi_special_predicate(call(_,_,_)).
4652
+ is_mi_special_predicate(call(_,_,_,_)).
4653
+ is_mi_special_predicate(once(_)).
4654
+ is_mi_special_predicate(ignore(_)).
4655
+ is_mi_special_predicate(not(_)).
2405
4656
  is_mi_special_predicate(push_context).
2406
4657
  is_mi_special_predicate(push_context(_)).
2407
4658
  is_mi_special_predicate(pop_context).
@@ -2459,6 +4710,8 @@ is_mi_special_predicate(close(_)).
2459
4710
  is_mi_special_predicate(read_string(_,_,_)).
2460
4711
  is_mi_special_predicate(write(_,_)).
2461
4712
  is_mi_special_predicate(exists_file(_)).
4713
+ is_mi_special_predicate(size_file(_,_)).
4714
+ is_mi_special_predicate(time_file(_,_)).
2462
4715
  is_mi_special_predicate(exists_directory(_)).
2463
4716
  is_mi_special_predicate(directory_files(_,_)).
2464
4717
  is_mi_special_predicate(make_directory(_)).
@@ -2472,6 +4725,105 @@ is_mi_special_predicate(use_module(_,_)).
2472
4725
  is_mi_special_predicate([_]).
2473
4726
  is_mi_special_predicate([_|_]).
2474
4727
 
4728
+ %% mi_call(call(Goal), +StateIn, -StateOut)
4729
+ %% Route call/1 through the meta-interpreter so backtracking and the
4730
+ %% reserved-predicate guard apply (a bare call/repl_* would bypass them).
4731
+ mi_call(call(Goal), StateIn, StateOut) :-
4732
+ guard_reserved_goal(Goal, StateIn),
4733
+ !,
4734
+ mi_call(Goal, StateIn, StateOut).
4735
+
4736
+ %% mi_call(call(Closure, Extra...), +StateIn, -StateOut)
4737
+ %% call/2..4 must also route through the meta-interpreter; otherwise a wrapped
4738
+ %% task/prompt/exec runs in raw Prolog, where task_named/3 does not exist.
4739
+ mi_call(call(Goal, A), StateIn, StateOut) :-
4740
+ !,
4741
+ extend_callable(Goal, [A], Callable),
4742
+ mi_call(call(Callable), StateIn, StateOut).
4743
+
4744
+ mi_call(call(Goal, A, B), StateIn, StateOut) :-
4745
+ !,
4746
+ extend_callable(Goal, [A, B], Callable),
4747
+ mi_call(call(Callable), StateIn, StateOut).
4748
+
4749
+ mi_call(call(Goal, A, B, C), StateIn, StateOut) :-
4750
+ !,
4751
+ extend_callable(Goal, [A, B, C], Callable),
4752
+ mi_call(call(Callable), StateIn, StateOut).
4753
+
4754
+ %% extend_callable(+Closure, +ExtraArgs, -Callable)
4755
+ extend_callable(Closure, Extra, Callable) :-
4756
+ Closure =.. List,
4757
+ append(List, Extra, Extended),
4758
+ Callable =.. Extended.
4759
+
4760
+ %% mi_call(once(Goal), +StateIn, -StateOut)
4761
+ %% once/1 must route through the meta-interpreter. As a plain builtin it would
4762
+ %% run Goal in raw Prolog, so a wrapped task/prompt/exec fails with
4763
+ %% task_named/3 undefined. Commits to the first solution of Goal, like once/1.
4764
+ mi_call(once(Goal), StateIn, StateOut) :-
4765
+ !,
4766
+ once(mi_call(Goal, StateIn, StateOut)).
4767
+
4768
+ %% mi_call(ignore(Goal), +StateIn, -StateOut)
4769
+ mi_call(ignore(Goal), StateIn, StateOut) :-
4770
+ !,
4771
+ ( once(mi_call(Goal, StateIn, StateOut0))
4772
+ -> StateOut = StateOut0
4773
+ ; StateOut = StateIn
4774
+ ).
4775
+
4776
+ %% mi_call(not(Goal), +StateIn, -StateOut) (alias for \+/1)
4777
+ mi_call(not(Goal), StateIn, StateIn) :-
4778
+ !,
4779
+ mi_call(\+(Goal), StateIn, StateIn).
4780
+
4781
+ %% Refuse reserved predicates invoked directly in non-meta frames. This is the
4782
+ %% second of the three-place reserved guard (execute, mi_call, assert/load).
4783
+ mi_call(Goal, StateIn, _StateOut) :-
4784
+ guard_reserved_direct(Goal, StateIn),
4785
+ !,
4786
+ throw(error(permission_error(execute, reserved_predicate, Goal),
4787
+ context(mi_call/3, 'cannot invoke a reserved REPL predicate'))).
4788
+
4789
+ %% guard_reserved_goal(+Goal, +StateIn)
4790
+ %% Throws when Goal names a reserved predicate and the frame is not meta.
4791
+ guard_reserved_goal(Goal, StateIn) :-
4792
+ ( is_dict(StateIn), get_dict(frame_kind, StateIn, meta)
4793
+ -> true
4794
+ ; nonvar(Goal),
4795
+ callable(Goal),
4796
+ functor(Goal, Name, _),
4797
+ repl_reserved_name(Name)
4798
+ -> throw(error(permission_error(execute, reserved_predicate, Name),
4799
+ context(mi_call/3, 'cannot invoke a reserved REPL predicate')))
4800
+ ; true
4801
+ ).
4802
+
4803
+ guard_reserved_direct(Goal, StateIn) :-
4804
+ ( is_dict(StateIn), get_dict(frame_kind, StateIn, meta)
4805
+ -> fail
4806
+ ; nonvar(Goal),
4807
+ callable(Goal),
4808
+ functor(Goal, Name, _),
4809
+ repl_reserved_name(Name)
4810
+ ).
4811
+
4812
+ %% Untransformed task/N and prompt/N (N >= 2) are source-level sugar rewritten
4813
+ %% to task_named/3 / prompt_named/3 by transform_task_calls/3 at load time.
4814
+ %% There is no runtime handler for the untransformed form, so it would match no
4815
+ %% clause and fail silently (looking like an inexplicable "0 solutions"). Fail
4816
+ %% loudly instead: reaching here means a clause was asserted/run without the
4817
+ %% load-time preprocessing (assert/assert_many now apply it, so this is a guard).
4818
+ mi_call(Goal, _StateIn, _StateOut) :-
4819
+ compound(Goal),
4820
+ functor(Goal, Name, Arity),
4821
+ Arity >= 2,
4822
+ ( Name == task ; Name == prompt ),
4823
+ !,
4824
+ throw(error(domain_error(unrewritten_task_form, Goal),
4825
+ context(mi_call/3, 'task/N and prompt/N are rewritten at load time (task_named/3, prompt_named/3); preprocess the clause before running it'))).
4826
+
2475
4827
  %% mi_call(Goal, +StateIn, -StateOut)
2476
4828
  %% Catch-all for built-in and user-defined predicates
2477
4829
  %% MUST NOT match special predicates that have dedicated handlers
@@ -2689,6 +5041,17 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
2689
5041
  rebuild_goal(Functor, StringArg, ExpandedRestArgs, ExpandedGoal)
2690
5042
  ).
2691
5043
 
5044
+ % Goals whose interpolated string is NOT the first argument (verify/3, judge/2,
5045
+ % choose/4, rate/4, probability/3, holds/2,3, and any user predicate): hoist the
5046
+ % interpolation into a preceding goal.
5047
+ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
5048
+ compound(Goal),
5049
+ Goal =.. [Functor|Args],
5050
+ expand_args_hoist(Args, Bindings, PreGoals, ExpandedArgs, true),
5051
+ !,
5052
+ Bare =.. [Functor|ExpandedArgs],
5053
+ prepend_goals(PreGoals, Bare, ExpandedGoal).
5054
+
2692
5055
  % Other goals - expand arguments
2693
5056
  expand_body_interp(Goal, Bindings, ExpandedGoal) :-
2694
5057
  compound(Goal), !,
@@ -2753,6 +5116,34 @@ expand_rest_args([A|As], Bindings, [EA|EAs]) :-
2753
5116
  expand_body_interp(A, Bindings, EA),
2754
5117
  expand_rest_args(As, Bindings, EAs).
2755
5118
 
5119
+ %% expand_args_hoist(+Args, +Bindings, -PreGoals, -Args2, -Hoisted)
5120
+ %% Expand each argument; any interpolated string becomes a goal in PreGoals and
5121
+ %% its argument becomes the temp variable that goal binds.
5122
+ expand_args_hoist([], _, [], [], false).
5123
+ expand_args_hoist([Arg|Args], Bindings, PreGoals, [Expanded|EArgs], Hoisted) :-
5124
+ expand_arg_hoist(Arg, Bindings, Pre, Expanded, ThisHoisted),
5125
+ expand_args_hoist(Args, Bindings, Rest, EArgs, RestHoisted),
5126
+ append(Pre, Rest, PreGoals),
5127
+ ( ThisHoisted == true
5128
+ -> Hoisted = true
5129
+ ; Hoisted = RestHoisted
5130
+ ).
5131
+
5132
+ expand_arg_hoist(Arg, Bindings, [FormatGoal], TempVar, true) :-
5133
+ ( string(Arg)
5134
+ ; atom(Arg)
5135
+ ),
5136
+ string_needs_interpolation(Arg),
5137
+ !,
5138
+ build_format_call(Arg, Bindings, TempVar, FormatGoal).
5139
+ expand_arg_hoist(Arg, _, [], Arg, false).
5140
+
5141
+ %% prepend_goals(+PreGoals, +Goal, -Expanded)
5142
+ prepend_goals([], Goal, Goal) :- !.
5143
+ prepend_goals(Goals, Goal, Expanded) :-
5144
+ list_to_conjunction(Goals, Conj),
5145
+ Expanded = (Conj, Goal).
5146
+
2756
5147
  %% flatten_conjunction(+A, +B, -Conj)
2757
5148
  %% Flatten nested conjunctions properly
2758
5149
  flatten_conjunction((A1, A2), B, Result) :- !,