deepclause-sdk 0.0.88 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +418 -914
  3. package/dist/agent.d.ts +19 -2
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +221 -4
  6. package/dist/agent.js.map +1 -1
  7. package/dist/cli/config.d.ts +323 -176
  8. package/dist/cli/config.d.ts.map +1 -1
  9. package/dist/cli/config.js +146 -188
  10. package/dist/cli/config.js.map +1 -1
  11. package/dist/cli/index.d.ts +3 -2
  12. package/dist/cli/index.d.ts.map +1 -1
  13. package/dist/cli/index.js +61 -425
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/login.d.ts +29 -0
  16. package/dist/cli/login.d.ts.map +1 -0
  17. package/dist/cli/login.js +184 -0
  18. package/dist/cli/login.js.map +1 -0
  19. package/dist/cli/readline.d.ts +6 -0
  20. package/dist/cli/readline.d.ts.map +1 -0
  21. package/dist/cli/readline.js +15 -0
  22. package/dist/cli/readline.js.map +1 -0
  23. package/dist/cli/repl-bin.d.ts +9 -0
  24. package/dist/cli/repl-bin.d.ts.map +1 -0
  25. package/dist/cli/repl-bin.js +16 -0
  26. package/dist/cli/repl-bin.js.map +1 -0
  27. package/dist/cli/repl.d.ts +191 -0
  28. package/dist/cli/repl.d.ts.map +1 -0
  29. package/dist/cli/repl.js +1232 -0
  30. package/dist/cli/repl.js.map +1 -0
  31. package/dist/cli/run.d.ts +17 -18
  32. package/dist/cli/run.d.ts.map +1 -1
  33. package/dist/cli/run.js +196 -183
  34. package/dist/cli/run.js.map +1 -1
  35. package/dist/cli/set-backend.d.ts +53 -0
  36. package/dist/cli/set-backend.d.ts.map +1 -0
  37. package/dist/cli/set-backend.js +259 -0
  38. package/dist/cli/set-backend.js.map +1 -0
  39. package/dist/cli/visualize.d.ts +38 -0
  40. package/dist/cli/visualize.d.ts.map +1 -0
  41. package/dist/cli/visualize.js +125 -0
  42. package/dist/cli/visualize.js.map +1 -0
  43. package/dist/compiler.d.ts +5 -52
  44. package/dist/compiler.d.ts.map +1 -1
  45. package/dist/compiler.js +4 -768
  46. package/dist/compiler.js.map +1 -1
  47. package/dist/index.d.ts +29 -2
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +20 -2
  50. package/dist/index.js.map +1 -1
  51. package/dist/judge/index.d.ts +9 -0
  52. package/dist/judge/index.d.ts.map +1 -0
  53. package/dist/judge/index.js +6 -0
  54. package/dist/judge/index.js.map +1 -0
  55. package/dist/judge/jev.d.ts +21 -0
  56. package/dist/judge/jev.d.ts.map +1 -0
  57. package/dist/judge/jev.js +218 -0
  58. package/dist/judge/jev.js.map +1 -0
  59. package/dist/judge/llm.d.ts +23 -0
  60. package/dist/judge/llm.d.ts.map +1 -0
  61. package/dist/judge/llm.js +120 -0
  62. package/dist/judge/llm.js.map +1 -0
  63. package/dist/judge/marshal.d.ts +26 -0
  64. package/dist/judge/marshal.d.ts.map +1 -0
  65. package/dist/judge/marshal.js +266 -0
  66. package/dist/judge/marshal.js.map +1 -0
  67. package/dist/judge/mock.d.ts +28 -0
  68. package/dist/judge/mock.d.ts.map +1 -0
  69. package/dist/judge/mock.js +98 -0
  70. package/dist/judge/mock.js.map +1 -0
  71. package/dist/judge/types.d.ts +95 -0
  72. package/dist/judge/types.d.ts.map +1 -0
  73. package/dist/judge/types.js +23 -0
  74. package/dist/judge/types.js.map +1 -0
  75. package/dist/prolog/bridge.d.ts +5 -1
  76. package/dist/prolog/bridge.d.ts.map +1 -1
  77. package/dist/prolog/bridge.js +29 -1
  78. package/dist/prolog/bridge.js.map +1 -1
  79. package/dist/prolog-src/deepclause_analysis.pl +10 -2
  80. package/dist/prolog-src/deepclause_mi.pl +2522 -118
  81. package/dist/repl/agent.d.ts +72 -0
  82. package/dist/repl/agent.d.ts.map +1 -0
  83. package/dist/repl/agent.js +366 -0
  84. package/dist/repl/agent.js.map +1 -0
  85. package/dist/repl/auth.d.ts +45 -0
  86. package/dist/repl/auth.d.ts.map +1 -0
  87. package/dist/repl/auth.js +116 -0
  88. package/dist/repl/auth.js.map +1 -0
  89. package/dist/repl/backend.d.ts +290 -0
  90. package/dist/repl/backend.d.ts.map +1 -0
  91. package/dist/repl/backend.js +370 -0
  92. package/dist/repl/backend.js.map +1 -0
  93. package/dist/repl/compaction.d.ts +122 -0
  94. package/dist/repl/compaction.d.ts.map +1 -0
  95. package/dist/repl/compaction.js +318 -0
  96. package/dist/repl/compaction.js.map +1 -0
  97. package/dist/repl/export.d.ts +30 -0
  98. package/dist/repl/export.d.ts.map +1 -0
  99. package/dist/repl/export.js +23 -0
  100. package/dist/repl/export.js.map +1 -0
  101. package/dist/repl/frames.d.ts +21 -0
  102. package/dist/repl/frames.d.ts.map +1 -0
  103. package/dist/repl/frames.js +79 -0
  104. package/dist/repl/frames.js.map +1 -0
  105. package/dist/repl/harness-tools.d.ts +74 -0
  106. package/dist/repl/harness-tools.d.ts.map +1 -0
  107. package/dist/repl/harness-tools.js +121 -0
  108. package/dist/repl/harness-tools.js.map +1 -0
  109. package/dist/repl/highlight.d.ts +30 -0
  110. package/dist/repl/highlight.d.ts.map +1 -0
  111. package/dist/repl/highlight.js +285 -0
  112. package/dist/repl/highlight.js.map +1 -0
  113. package/dist/repl/history.d.ts +234 -0
  114. package/dist/repl/history.d.ts.map +1 -0
  115. package/dist/repl/history.js +661 -0
  116. package/dist/repl/history.js.map +1 -0
  117. package/dist/repl/index.d.ts +120 -0
  118. package/dist/repl/index.d.ts.map +1 -0
  119. package/dist/repl/index.js +107 -0
  120. package/dist/repl/index.js.map +1 -0
  121. package/dist/repl/judge.d.ts +51 -0
  122. package/dist/repl/judge.d.ts.map +1 -0
  123. package/dist/repl/judge.js +58 -0
  124. package/dist/repl/judge.js.map +1 -0
  125. package/dist/repl/markdown.d.ts +25 -0
  126. package/dist/repl/markdown.d.ts.map +1 -0
  127. package/dist/repl/markdown.js +103 -0
  128. package/dist/repl/markdown.js.map +1 -0
  129. package/dist/repl/picker.d.ts +34 -0
  130. package/dist/repl/picker.d.ts.map +1 -0
  131. package/dist/repl/picker.js +81 -0
  132. package/dist/repl/picker.js.map +1 -0
  133. package/dist/repl/project-context.d.ts +16 -0
  134. package/dist/repl/project-context.d.ts.map +1 -0
  135. package/dist/repl/project-context.js +23 -0
  136. package/dist/repl/project-context.js.map +1 -0
  137. package/dist/repl/render.d.ts +160 -0
  138. package/dist/repl/render.d.ts.map +1 -0
  139. package/dist/repl/render.js +715 -0
  140. package/dist/repl/render.js.map +1 -0
  141. package/dist/repl/session.d.ts +356 -0
  142. package/dist/repl/session.d.ts.map +1 -0
  143. package/dist/repl/session.js +1275 -0
  144. package/dist/repl/session.js.map +1 -0
  145. package/dist/repl/status.d.ts +100 -0
  146. package/dist/repl/status.d.ts.map +1 -0
  147. package/dist/repl/status.js +218 -0
  148. package/dist/repl/status.js.map +1 -0
  149. package/dist/repl/tools.d.ts +53 -0
  150. package/dist/repl/tools.d.ts.map +1 -0
  151. package/dist/repl/tools.js +307 -0
  152. package/dist/repl/tools.js.map +1 -0
  153. package/dist/repl/toolset.d.ts +32 -0
  154. package/dist/repl/toolset.d.ts.map +1 -0
  155. package/dist/repl/toolset.js +82 -0
  156. package/dist/repl/toolset.js.map +1 -0
  157. package/dist/repl/tui/editor.d.ts +17 -0
  158. package/dist/repl/tui/editor.d.ts.map +1 -0
  159. package/dist/repl/tui/editor.js +39 -0
  160. package/dist/repl/tui/editor.js.map +1 -0
  161. package/dist/repl/tui/status-bar.d.ts +26 -0
  162. package/dist/repl/tui/status-bar.d.ts.map +1 -0
  163. package/dist/repl/tui/status-bar.js +47 -0
  164. package/dist/repl/tui/status-bar.js.map +1 -0
  165. package/dist/repl/tui/theme.d.ts +30 -0
  166. package/dist/repl/tui/theme.d.ts.map +1 -0
  167. package/dist/repl/tui/theme.js +81 -0
  168. package/dist/repl/tui/theme.js.map +1 -0
  169. package/dist/repl/tui/transcript.d.ts +39 -0
  170. package/dist/repl/tui/transcript.d.ts.map +1 -0
  171. package/dist/repl/tui/transcript.js +123 -0
  172. package/dist/repl/tui/transcript.js.map +1 -0
  173. package/dist/runner.d.ts +145 -13
  174. package/dist/runner.d.ts.map +1 -1
  175. package/dist/runner.js +600 -282
  176. package/dist/runner.js.map +1 -1
  177. package/dist/sdk.d.ts.map +1 -1
  178. package/dist/sdk.js +56 -32
  179. package/dist/sdk.js.map +1 -1
  180. package/dist/system/assets/docs/AGENT_PROMPT.md +1373 -0
  181. package/dist/system/assets/docs/DML_AUTHORING.md +195 -0
  182. package/dist/system/assets/docs/DML_REFERENCE.md +393 -72
  183. package/dist/system/assets/docs/HARNESS_EXAMPLES.md +172 -0
  184. package/dist/system/assets/docs/HARNESS_PATTERNS.md +562 -0
  185. package/dist/system/assets/docs/META_PROMPT.md +68 -0
  186. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +117 -0
  187. package/dist/system/assets/index.d.ts +9 -17
  188. package/dist/system/assets/index.d.ts.map +1 -1
  189. package/dist/system/assets/index.js +26 -85
  190. package/dist/system/assets/index.js.map +1 -1
  191. package/dist/system/runtime/agentvm-manager.d.ts +9 -2
  192. package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
  193. package/dist/system/runtime/agentvm-manager.js +70 -44
  194. package/dist/system/runtime/agentvm-manager.js.map +1 -1
  195. package/dist/system/runtime/media.d.ts +23 -0
  196. package/dist/system/runtime/media.d.ts.map +1 -0
  197. package/dist/system/runtime/media.js +55 -0
  198. package/dist/system/runtime/media.js.map +1 -0
  199. package/dist/system/runtime/shell-limits.d.ts +21 -0
  200. package/dist/system/runtime/shell-limits.d.ts.map +1 -0
  201. package/dist/system/runtime/shell-limits.js +39 -0
  202. package/dist/system/runtime/shell-limits.js.map +1 -0
  203. package/dist/system/runtime/shell-manager.d.ts +8 -3
  204. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  205. package/dist/system/runtime/shell-manager.js +56 -39
  206. package/dist/system/runtime/shell-manager.js.map +1 -1
  207. package/dist/system/runtime/shell-tool-events.js +5 -0
  208. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  209. package/dist/system/runtime/token-usage.d.ts +2 -0
  210. package/dist/system/runtime/token-usage.d.ts.map +1 -1
  211. package/dist/system/runtime/token-usage.js +7 -0
  212. package/dist/system/runtime/token-usage.js.map +1 -1
  213. package/dist/tools.d.ts +3 -21
  214. package/dist/tools.d.ts.map +1 -1
  215. package/dist/tools.js +14 -168
  216. package/dist/tools.js.map +1 -1
  217. package/dist/types.d.ts +149 -118
  218. package/dist/types.d.ts.map +1 -1
  219. package/dist/validate.d.ts +19 -0
  220. package/dist/validate.d.ts.map +1 -0
  221. package/dist/validate.js +338 -0
  222. package/dist/validate.js.map +1 -0
  223. package/dist/visualize/index.d.ts +52 -0
  224. package/dist/visualize/index.d.ts.map +1 -0
  225. package/dist/visualize/index.js +189 -0
  226. package/dist/visualize/index.js.map +1 -0
  227. package/package.json +17 -15
  228. package/src/prolog-src/deepclause_analysis.pl +10 -2
  229. package/src/prolog-src/deepclause_mi.pl +2522 -118
  230. package/dist/cli/commands.d.ts +0 -42
  231. package/dist/cli/commands.d.ts.map +0 -1
  232. package/dist/cli/commands.js +0 -246
  233. package/dist/cli/commands.js.map +0 -1
  234. package/dist/cli/compile.d.ts +0 -99
  235. package/dist/cli/compile.d.ts.map +0 -1
  236. package/dist/cli/compile.js +0 -363
  237. package/dist/cli/compile.js.map +0 -1
  238. package/dist/cli/default-skills.d.ts +0 -10
  239. package/dist/cli/default-skills.d.ts.map +0 -1
  240. package/dist/cli/default-skills.js +0 -206
  241. package/dist/cli/default-skills.js.map +0 -1
  242. package/dist/cli/interactive.d.ts +0 -2
  243. package/dist/cli/interactive.d.ts.map +0 -1
  244. package/dist/cli/interactive.js +0 -20
  245. package/dist/cli/interactive.js.map +0 -1
  246. package/dist/cli/mcp.d.ts +0 -56
  247. package/dist/cli/mcp.d.ts.map +0 -1
  248. package/dist/cli/mcp.js +0 -138
  249. package/dist/cli/mcp.js.map +0 -1
  250. package/dist/cli/prompt.d.ts +0 -2
  251. package/dist/cli/prompt.d.ts.map +0 -1
  252. package/dist/cli/prompt.js +0 -2
  253. package/dist/cli/prompt.js.map +0 -1
  254. package/dist/cli/search.d.ts +0 -19
  255. package/dist/cli/search.d.ts.map +0 -1
  256. package/dist/cli/search.js +0 -189
  257. package/dist/cli/search.js.map +0 -1
  258. package/dist/cli/tool-args.d.ts +0 -2
  259. package/dist/cli/tool-args.d.ts.map +0 -1
  260. package/dist/cli/tool-args.js +0 -39
  261. package/dist/cli/tool-args.js.map +0 -1
  262. package/dist/cli/tools.d.ts +0 -36
  263. package/dist/cli/tools.d.ts.map +0 -1
  264. package/dist/cli/tools.js +0 -229
  265. package/dist/cli/tools.js.map +0 -1
  266. package/dist/cli/tui/index.d.ts +0 -22
  267. package/dist/cli/tui/index.d.ts.map +0 -1
  268. package/dist/cli/tui/index.js +0 -29
  269. package/dist/cli/tui/index.js.map +0 -1
  270. package/dist/cli/tui.d.ts +0 -167
  271. package/dist/cli/tui.d.ts.map +0 -1
  272. package/dist/cli/tui.js +0 -5228
  273. package/dist/cli/tui.js.map +0 -1
  274. package/dist/compiler_prompt.d.ts +0 -20
  275. package/dist/compiler_prompt.d.ts.map +0 -1
  276. package/dist/compiler_prompt.js +0 -870
  277. package/dist/compiler_prompt.js.map +0 -1
  278. package/dist/deepclause-cli.skill +0 -0
  279. package/dist/prolog-src/deepclause_mi.pl.bak +0 -570
  280. package/dist/system/assets/compactors/default-memory-compactor.dml +0 -6
  281. package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +0 -105
  282. package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +0 -700
  283. package/dist/system/assets/docs/TUI.md +0 -150
  284. package/dist/system/assets/recipes/deepclause-coding-workflow/SKILL.md +0 -100
  285. package/dist/system/assets/skills/conductor.dml +0 -76
  286. package/dist/system/assets/skills/deep-planner.dml +0 -412
  287. package/dist/system/assets/skills/plan.dml +0 -429
  288. package/dist/system/assets/skills/security-planner.dml +0 -337
  289. package/dist/system/assets/skills/skill-creator.dml +0 -187
  290. package/dist/system/runtime/catalog-recipes.d.ts +0 -24
  291. package/dist/system/runtime/catalog-recipes.d.ts.map +0 -1
  292. package/dist/system/runtime/catalog-recipes.js +0 -273
  293. package/dist/system/runtime/catalog-recipes.js.map +0 -1
  294. package/dist/system/runtime/catalog-skills.d.ts +0 -50
  295. package/dist/system/runtime/catalog-skills.d.ts.map +0 -1
  296. package/dist/system/runtime/catalog-skills.js +0 -152
  297. package/dist/system/runtime/catalog-skills.js.map +0 -1
  298. package/dist/system/runtime/conductor.d.ts +0 -105
  299. package/dist/system/runtime/conductor.d.ts.map +0 -1
  300. package/dist/system/runtime/conductor.js +0 -732
  301. package/dist/system/runtime/conductor.js.map +0 -1
  302. package/dist/system/runtime/console-capture.d.ts +0 -9
  303. package/dist/system/runtime/console-capture.d.ts.map +0 -1
  304. package/dist/system/runtime/console-capture.js +0 -67
  305. package/dist/system/runtime/console-capture.js.map +0 -1
  306. package/dist/system/runtime/dml-executor.d.ts +0 -52
  307. package/dist/system/runtime/dml-executor.d.ts.map +0 -1
  308. package/dist/system/runtime/dml-executor.js +0 -203
  309. package/dist/system/runtime/dml-executor.js.map +0 -1
  310. package/dist/system/runtime/runtime-tools.d.ts +0 -31
  311. package/dist/system/runtime/runtime-tools.d.ts.map +0 -1
  312. package/dist/system/runtime/runtime-tools.js +0 -312
  313. package/dist/system/runtime/runtime-tools.js.map +0 -1
  314. package/dist/system/runtime/skill-creator.d.ts +0 -38
  315. package/dist/system/runtime/skill-creator.d.ts.map +0 -1
  316. package/dist/system/runtime/skill-creator.js +0 -763
  317. package/dist/system/runtime/skill-creator.js.map +0 -1
  318. package/src/prolog-src/deepclause_mi.pl.bak +0 -570
@@ -20,11 +20,78 @@
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_set_meta_catalog/2,
75
+ repl_set_meta_template/2,
76
+ repl_set_harness_examples/2,
77
+ repl_set_prolog_snippets/2,
78
+ repl_run_meta/4,
79
+ repl_snapshot_save/1,
80
+ repl_snapshot_restore/1,
81
+ repl_commit/3,
82
+ repl_last_commit_summary/2,
83
+ repl_execute_frame/4,
84
+ repl_record_meta/3,
85
+ repl_memory_term/3,
86
+ repl_get_memory/3,
87
+ repl_set_memory/3,
23
88
  post_agent_result/4,
24
89
  post_exec_result/3,
25
90
  post_sample_token_result/3,
26
91
  post_llm_result/3,
92
+ post_judgment_result/3,
27
93
  provide_input/2,
94
+ provide_input_cancelled/1,
28
95
  mi/3,
29
96
  % Tool engine predicates for isolated tool execution
30
97
  create_tool_engine/4,
@@ -41,7 +108,25 @@
41
108
  :- discontiguous var_to_name/3.
42
109
 
43
110
  %% Dynamic predicates for session state (engine management only, not memory!)
44
- :- dynamic session_engine/2. % session_engine(SessionId, Engine)
111
+ :- dynamic session_engine/2. % session_engine(SessionId, Engine) - legacy default engine handle
112
+ :- dynamic session_frame/4. % session_frame(FrameId, SessionId, Kind, Depth)
113
+ :- dynamic session_frame_engine/2. % session_frame_engine(FrameId, Engine)
114
+ :- dynamic session_default_frame/2. % session_default_frame(SessionId, FrameId)
115
+ :- dynamic session_repl_memory/2. % session_repl_memory(SessionId, HarnessMemory) - object/entry memory
116
+ :- dynamic session_meta_memory/2. % session_meta_memory(SessionId, MetaMemory) - code-writing task memory
117
+ :- dynamic session_repl_mode/2. % session_repl_mode(SessionId, true/false) - gates format/writeln output shims
118
+ :- dynamic session_frozen/2. % session_frozen(SessionId, true/false) - read-only lock
119
+ :- dynamic session_locked/2. % session_locked(SessionId, true/false) - irreversibly frozen (--locked)
120
+ :- dynamic session_directive/2. % session_directive(SessionId, Directive) - recorded :- directives for export
121
+ :- dynamic session_meta_catalog/2. % session_meta_catalog(SessionId, CatalogText) - host tool catalog for the meta prompt
122
+ :- dynamic session_dml_reference/2. % session_dml_reference(SessionId, Text) - bundled DML reference for the meta prompt
123
+ :- dynamic session_authoring_examples/2. % session_authoring_examples(SessionId, Text) - curated authoring patterns
124
+ :- dynamic session_meta_template/2. % session_meta_template(SessionId, Template) - bundled META_PROMPT.md template
125
+ :- dynamic session_harness_examples/2. % session_harness_examples(SessionId, Text) - bundled HARNESS_EXAMPLES.md
126
+ :- dynamic session_prolog_snippets/2. % session_prolog_snippets(SessionId, Text) - bundled PROLOG_SNIPPETS.md
127
+ :- dynamic session_last_commit/2. % session_last_commit(SessionId, [Name/Arity]) - predicates the last commit replaced
128
+ :- dynamic session_edit_touched/2. % session_edit_touched(SessionId, Name/Arity) - agent-mode edit transactions
129
+ :- dynamic repl_snapshot/2. % repl_snapshot(SessionId, Snapshot) - transactional edit snapshot
45
130
  :- dynamic session_params/2. % session_params(SessionId, ParamsDict)
46
131
  :- dynamic session_user_tools/2. % session_user_tools(SessionId, ToolName)
47
132
  :- dynamic session_user_tool_schema/3. % session_user_tool_schema(SessionId, ToolName, Schema)
@@ -50,10 +135,12 @@
50
135
  :- dynamic session_exec_result/2. % session_exec_result(SessionId, Result)
51
136
  :- dynamic session_sample_token_result/2. % session_sample_token_result(SessionId, Result)
52
137
  :- dynamic session_llm_result/2. % session_llm_result(SessionId, Result)
138
+ :- dynamic session_judgment_result/2. % session_judgment_result(SessionId, Result)
53
139
  :- dynamic session_trace_log/2. % session_trace_log(SessionId, TraceLog) - accumulated trace entries
54
140
  :- dynamic session_trace_enabled/2. % session_trace_enabled(SessionId, true/false)
55
141
  :- dynamic session_pending_signal/2. % session_pending_signal(SessionId, Signal) - fallback for engine_fetch
56
142
  :- dynamic session_user_input/2. % session_user_input(SessionId, Input) - for ask_user tool
143
+ :- dynamic session_input_cancelled/1. % session_input_cancelled(SessionId) - input/2 should fail (non-interactive)
57
144
 
58
145
  %% ============================================================
59
146
  %% DML Parsing
@@ -258,6 +345,182 @@ wrapper_type(list, array).
258
345
  wrapper_type(object, object).
259
346
  wrapper_type(dict, object).
260
347
 
348
+ %% ============================================================
349
+ %% Source-facing view of user clauses
350
+ %%
351
+ %% At load time task/N and prompt/N are rewritten to task_named/3 and
352
+ %% prompt_named/3, and interpolated descriptions are hoisted into a preceding
353
+ %% format/3. Those internal forms must not leak into listing/export — the meta
354
+ %% task only knows task/N — so reverse them for display only.
355
+ %% ============================================================
356
+
357
+ %% clean_clause_body(+Body, -CleanBody)
358
+ clean_clause_body(Body, Clean) :-
359
+ conj_list(Body, Goals),
360
+ clean_goal_list(Goals, CleanGoals),
361
+ ( CleanGoals = [Single]
362
+ -> Clean = Single
363
+ ; list_to_conjunction(CleanGoals, Clean)
364
+ ).
365
+
366
+ conj_list((A, B), List) :-
367
+ !,
368
+ conj_list(A, LA),
369
+ conj_list(B, LB),
370
+ append(LA, LB, List).
371
+ conj_list(Goal, [Goal]).
372
+
373
+ clean_goal_list([], []) :- !.
374
+ clean_goal_list([Goal], [Clean]) :-
375
+ !,
376
+ clean_atomic(Goal, Clean).
377
+ clean_goal_list([First, Second | Rest], [CleanFirst | CleanRest]) :-
378
+ ( merge_interpolation(First, Second, CleanFirst)
379
+ -> clean_goal_list(Rest, CleanRest)
380
+ ; clean_atomic(First, CleanFirst),
381
+ clean_goal_list([Second | Rest], CleanRest)
382
+ ).
383
+
384
+ %% merge_interpolation(+FormatGoal, +TaskGoal, -Merged)
385
+ %% Fold `format(string(D), T, Args), task_named(D, ...)` back into
386
+ %% `task("…{Var}…", ...)`.
387
+ merge_interpolation(FormatGoal, TaskGoal, Merged) :-
388
+ FormatGoal = format(string(D), Template, Args),
389
+ ( TaskGoal = task_named(D, Vars, Infos)
390
+ ; TaskGoal = prompt_named(D, Vars, Infos)
391
+ ),
392
+ task_functor(TaskGoal, Functor),
393
+ interpolate_template(Template, Args, Description),
394
+ clean_task(Functor, Description, Vars, Infos, Merged).
395
+
396
+ task_functor(task_named(_, _, _), task).
397
+ task_functor(prompt_named(_, _, _), prompt).
398
+
399
+ clean_atomic(task_named(D, Vars, Infos), Clean) :-
400
+ !,
401
+ clean_task(task, D, Vars, Infos, Clean).
402
+ clean_atomic(prompt_named(D, Vars, Infos), Clean) :-
403
+ !,
404
+ clean_task(prompt, D, Vars, Infos, Clean).
405
+ clean_atomic((A ; B), (CA ; CB)) :-
406
+ !,
407
+ clean_clause_body(A, CA),
408
+ clean_clause_body(B, CB).
409
+ clean_atomic((A -> B), (CA -> CB)) :-
410
+ !,
411
+ clean_clause_body(A, CA),
412
+ clean_clause_body(B, CB).
413
+ clean_atomic((A -> B ; C), (CA -> CB ; CC)) :-
414
+ !,
415
+ clean_clause_body(A, CA),
416
+ clean_clause_body(B, CB),
417
+ clean_clause_body(C, CC).
418
+ clean_atomic(\+(A), \+ CA) :-
419
+ !,
420
+ clean_clause_body(A, CA).
421
+ clean_atomic(not(A), not(CA)) :-
422
+ !,
423
+ clean_clause_body(A, CA).
424
+ % Wrappers: recurse into their goal arguments so a task/prompt inside is also
425
+ % reversed (the load-time rewrite descends into these via transform_task_calls).
426
+ clean_atomic(with_tools(Tools, Goal), with_tools(Tools, CleanGoal)) :-
427
+ !,
428
+ clean_clause_body(Goal, CleanGoal).
429
+ clean_atomic(without_tools(Tools, Goal), without_tools(Tools, CleanGoal)) :-
430
+ !,
431
+ clean_clause_body(Goal, CleanGoal).
432
+ clean_atomic(with_judgment(Backend, Goal), with_judgment(Backend, CleanGoal)) :-
433
+ !,
434
+ clean_clause_body(Goal, CleanGoal).
435
+ clean_atomic(require_judgment(Backend, Goal), require_judgment(Backend, CleanGoal)) :-
436
+ !,
437
+ clean_clause_body(Goal, CleanGoal).
438
+ clean_atomic(once(Goal), once(CleanGoal)) :-
439
+ !,
440
+ clean_clause_body(Goal, CleanGoal).
441
+ clean_atomic(ignore(Goal), ignore(CleanGoal)) :-
442
+ !,
443
+ clean_clause_body(Goal, CleanGoal).
444
+ clean_atomic(catch(Goal, Catcher, Recovery), catch(CleanGoal, Catcher, CleanRecovery)) :-
445
+ !,
446
+ clean_clause_body(Goal, CleanGoal),
447
+ clean_clause_body(Recovery, CleanRecovery).
448
+ clean_atomic(forall(Generator, Action), forall(CleanGenerator, CleanAction)) :-
449
+ !,
450
+ clean_clause_body(Generator, CleanGenerator),
451
+ clean_clause_body(Action, CleanAction).
452
+ clean_atomic(findall(Template, Goal, List), findall(Template, CleanGoal, List)) :-
453
+ !,
454
+ clean_clause_body(Goal, CleanGoal).
455
+ clean_atomic(Module:Goal, Module:CleanGoal) :-
456
+ !,
457
+ clean_clause_body(Goal, CleanGoal).
458
+ clean_atomic(Goal, Goal).
459
+
460
+ %% clean_task(+Functor, +Description, +Vars, +Infos, -Clean)
461
+ clean_task(Functor, Description, Vars, Infos, Clean) :-
462
+ clean_typed_args(Vars, Infos, CleanVars),
463
+ Clean =.. [Functor, Description | CleanVars].
464
+
465
+ clean_typed_args([], [], []) :- !.
466
+ clean_typed_args([Var | Vars], [Info | Infos], [Wrapped | Wrappeds]) :-
467
+ !,
468
+ typed_arg(Var, Info, Wrapped),
469
+ clean_typed_args(Vars, Infos, Wrappeds).
470
+ clean_typed_args(Vars, _, Vars).
471
+
472
+ %% typed_arg(+Var, +Info, -Wrapped) — reverse typed_var{...} to its wrapper.
473
+ typed_arg(Var, Info, Wrapped) :-
474
+ is_dict(Info),
475
+ get_dict(type, Info, Type),
476
+ !,
477
+ typed_arg_type(Type, Var, Info, Wrapped).
478
+ typed_arg(Var, _, Var).
479
+
480
+ typed_arg_type(string, Var, _, Var) :- !.
481
+ typed_arg_type(integer, Var, _, integer(Var)) :- !.
482
+ typed_arg_type(number, Var, _, number(Var)) :- !.
483
+ typed_arg_type(boolean, Var, _, boolean(Var)) :- !.
484
+ typed_arg_type(object, Var, _, object(Var)) :- !.
485
+ typed_arg_type(array, Var, Info, list(Wrapped)) :-
486
+ !,
487
+ ( get_dict(itemType, Info, ItemType)
488
+ -> true
489
+ ; ItemType = string
490
+ ),
491
+ Wrapped =.. [ItemType, Var].
492
+ typed_arg_type(_, Var, _, Var).
493
+
494
+ %% interpolate_template(+Template, +Args, -Description)
495
+ %% Reverse of the {Var} -> ~w hoist. Succeeds only when every placeholder is a
496
+ %% plain variable, so the caller can fall back to keeping the format/3 goal.
497
+ interpolate_template(Template, Args, Description) :-
498
+ string(Template),
499
+ string_codes(Template, Codes),
500
+ interpolate_codes(Codes, Args, OutCodes),
501
+ string_codes(Description, OutCodes).
502
+
503
+ interpolate_codes([], [], []) :- !.
504
+ %% A {Var} placeholder consumes one argument; require Args to be exhausted
505
+ %% exactly (the []/[] base case above fails if either side has leftovers).
506
+ interpolate_codes([0'~, 0'w | Rest], [Arg | Args], Out) :-
507
+ !,
508
+ named_var(Arg),
509
+ term_string(Arg, Name, [numbervars(true)]),
510
+ string_codes(Name, NameCodes),
511
+ interpolate_codes(Rest, Args, Tail),
512
+ append([0'{|NameCodes], [0'}|Tail], Out).
513
+ interpolate_codes([0'~, 0'~ | Rest], Args, [0'~ | Out]) :-
514
+ !,
515
+ interpolate_codes(Rest, Args, Out).
516
+ interpolate_codes([Code | Rest], Args, [Code | Out]) :-
517
+ interpolate_codes(Rest, Args, Out).
518
+
519
+ %% An interpolation argument is a variable or a `$VAR(N)` term (after
520
+ %% numbervars), both of which print as a stable name (A, B, ...).
521
+ named_var(Arg) :- var(Arg), !.
522
+ named_var('$VAR'(_)).
523
+
261
524
  %% var_to_name(+Bindings, +Var, -Name)
262
525
  %% Look up a variable's name from the bindings list
263
526
  var_to_name(Bindings, Var, Name) :-
@@ -267,7 +530,15 @@ var_to_name(Bindings, Var, Name) :-
267
530
  ).
268
531
 
269
532
  %% process_clause(+Term, +SessionId)
270
- %% Process a single clause and add to session
533
+ %% Process a single clause and add to session.
534
+ %% Reject reserved-name definitions (third place of the reserved guard, so
535
+ %% consulted/generated code cannot shadow the REPL control surface).
536
+ process_clause(Term, _SessionId) :-
537
+ repl_reserved_clause(Term),
538
+ !,
539
+ throw(error(permission_error(modify, reserved_name, Term),
540
+ context(process_clause/2, 'reserved predicate'))).
541
+
271
542
  process_clause((:- Directive), SessionId) :-
272
543
  !,
273
544
  process_directive(Directive, SessionId).
@@ -279,8 +550,9 @@ process_clause((tool(ToolHead, Description) :- Body), SessionId) :-
279
550
  extract_tool_schema(ToolHead, Description, ToolName, Schema),
280
551
  assertz(session_user_tools(SessionId, ToolName)),
281
552
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
282
- % Store the source code for the tool description
283
- format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
553
+ % Store the source code for the tool description. Reverses the load-time
554
+ % task/prompt rewrites and normalizes variables for deterministic export.
555
+ format_tool_source(ToolHead, Description, Body, SourceCode),
284
556
  assertz(SessionId:tool_source(ToolName, SourceCode)),
285
557
  % Assert the tool implementation (use just ToolHead for execution)
286
558
  assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
@@ -292,8 +564,8 @@ process_clause((tool(ToolHead) :- Body), SessionId) :-
292
564
  extract_tool_schema(ToolHead, none, ToolName, Schema),
293
565
  assertz(session_user_tools(SessionId, ToolName)),
294
566
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
295
- % Store the source code for the tool description
296
- format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
567
+ % Store the source code for the tool description (reverses task rewrites).
568
+ format_tool_source(ToolHead, none, Body, SourceCode),
297
569
  assertz(SessionId:tool_source(ToolName, SourceCode)),
298
570
  % Assert the tool implementation
299
571
  assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
@@ -307,6 +579,15 @@ process_clause((Head :- Body), SessionId) :-
307
579
  ),
308
580
  assertz(SessionId:(Head :- ExpandedBody)).
309
581
 
582
+ %% repl_delete/1 is a load-time directive that removes a predicate. It must
583
+ %% be handled before the generic fact clause so it is never asserted itself.
584
+ process_clause(repl_delete(Name/Arity), SessionId) :-
585
+ !,
586
+ ( atom(Name), integer(Arity)
587
+ -> catch(abolish(SessionId:Name/Arity), _, true)
588
+ ; true
589
+ ).
590
+
310
591
  process_clause(Fact, SessionId) :-
311
592
  % Simple fact
312
593
  assertz(SessionId:Fact).
@@ -393,100 +674,1742 @@ extract_tool_schema(ToolHead, Description, ToolName, Schema) :-
393
674
  outputs: Outputs
394
675
  }.
395
676
 
396
- %% extract_params(+Args, +Index, +Arity, -Inputs, -Outputs)
397
- extract_params([], _, _, [], []) :- !.
398
- extract_params([_Arg|Rest], Index, Arity, Inputs, Outputs) :-
399
- format(atom(Name), 'arg~w', [Index]),
400
- Type = string,
401
- NextIndex is Index + 1,
402
- extract_params(Rest, NextIndex, Arity, RestInputs, RestOutputs),
403
- Param = param{name: Name, type: Type},
404
- ( Index == Arity
405
- -> Inputs = RestInputs, Outputs = [Param|RestOutputs]
406
- ; Inputs = [Param|RestInputs], Outputs = RestOutputs
677
+ %% extract_params(+Args, +Index, +Arity, -Inputs, -Outputs)
678
+ extract_params([], _, _, [], []) :- !.
679
+ extract_params([_Arg|Rest], Index, Arity, Inputs, Outputs) :-
680
+ format(atom(Name), 'arg~w', [Index]),
681
+ Type = string,
682
+ NextIndex is Index + 1,
683
+ extract_params(Rest, NextIndex, Arity, RestInputs, RestOutputs),
684
+ Param = param{name: Name, type: Type},
685
+ ( Index == Arity
686
+ -> Inputs = RestInputs, Outputs = [Param|RestOutputs]
687
+ ; Inputs = [Param|RestInputs], Outputs = RestOutputs
688
+ ).
689
+
690
+ module_reference_string(Module, ModuleStr) :-
691
+ atom(Module),
692
+ !,
693
+ atom_string(Module, ModuleStr).
694
+ module_reference_string(Module, Module) :-
695
+ string(Module).
696
+
697
+ workspace_module_reference(Module) :-
698
+ module_reference_string(Module, ModuleStr),
699
+ ( sub_string(ModuleStr, _, _, _, "/")
700
+ ; sub_string(ModuleStr, _, _, 0, ".dml")
701
+ ; sub_string(ModuleStr, _, _, 0, ".pl")
702
+ ; sub_string(ModuleStr, 0, 1, _, ".")
703
+ ).
704
+
705
+ process_use_module_directive(Module, SessionId) :-
706
+ ( workspace_module_reference(Module)
707
+ -> process_directive(consult(Module), SessionId)
708
+ ; SessionId:use_module(Module),
709
+ import_library_into_mi(Module)
710
+ ).
711
+
712
+ process_use_module_directive(Module, Exports, SessionId) :-
713
+ ( workspace_module_reference(Module)
714
+ -> process_directive(consult(Module), SessionId)
715
+ ; SessionId:use_module(Module, Exports),
716
+ import_library_into_mi(Module)
717
+ ).
718
+
719
+ %% System libraries are also imported into the meta-interpreter module itself,
720
+ %% so meta-calls (`call_with_inference_limit/3`, `once/1`, `catch/3`, ...) can
721
+ %% resolve them. Without this, `call_with_inference_limit(label(Xs), ...)` fails
722
+ %% with `existence_error(deepclause_mi:label/1)` because clpfd was imported only
723
+ %% into the session module.
724
+ import_library_into_mi(library(Name)) :-
725
+ !,
726
+ catch(deepclause_mi:use_module(library(Name)), _, true).
727
+ import_library_into_mi(_).
728
+
729
+ %% process_directive(+Directive, +SessionId)
730
+ process_directive(param(Key, Desc), SessionId) :-
731
+ !,
732
+ assertz(SessionId:param_decl(Key, Desc)).
733
+ process_directive(param(Key, Desc, Default), SessionId) :-
734
+ !,
735
+ assertz(SessionId:param_decl(Key, Desc, Default)).
736
+
737
+ %% Handle consult directive: :- consult(File)
738
+ process_directive(consult(File), SessionId) :-
739
+ !,
740
+ % Set session ID for get_session_id to find, then call our custom consult
741
+ nb_setval(current_session_id, SessionId),
742
+ resolve_workspace_path(File, FullPath),
743
+ readutil:read_file_to_string(FullPath, Code, []),
744
+ consult_string(Code, SessionId).
745
+ %% Handle use_module directive
746
+ process_directive(use_module(Module), SessionId) :-
747
+ !,
748
+ ( workspace_module_reference(Module)
749
+ -> true
750
+ ; record_directive(SessionId, use_module(Module))
751
+ ),
752
+ process_use_module_directive(Module, SessionId).
753
+ process_directive(use_module(Module, Exports), SessionId) :-
754
+ !,
755
+ ( workspace_module_reference(Module)
756
+ -> true
757
+ ; record_directive(SessionId, use_module(Module, Exports))
758
+ ),
759
+ process_use_module_directive(Module, Exports, SessionId).
760
+ %% Handle list-style consult: :- [File] or :- [File1, File2, ...]
761
+ process_directive([File|Files], SessionId) :-
762
+ !,
763
+ nb_setval(current_session_id, SessionId),
764
+ process_directive(consult(File), SessionId),
765
+ (Files == [] -> true ; process_directive(Files, SessionId)).
766
+ process_directive(_, _).
767
+
768
+ %% ============================================================
769
+ %% Engine Management
770
+ %% ============================================================
771
+
772
+ %% create_engine(+SessionId, +MemoryId, +Args, +Params, +InitialMemory, -Engine)
773
+ %% Args is a list of positional arguments for agent_main
774
+ %% Params is a dict of named parameters (already asserted as param/2 facts)
775
+ %%
776
+ %% The engine is registered as the session's default `query` frame so that
777
+ %% step_engine/4 keeps working as a thin wrapper over step_frame/4.
778
+ create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
779
+ assertz(session_params(SessionId, Params)),
780
+ determine_agent_goal(SessionId, Args, Goal),
781
+ % Check if tracing is enabled and store in session state
782
+ (get_dict(trace, Params, true) -> TraceEnabled = true ; TraceEnabled = false),
783
+ assertz(session_trace_enabled(SessionId, TraceEnabled)),
784
+ assertz(session_trace_log(SessionId, [])),
785
+ % Check for gas limit in params
786
+ (get_dict(gas_limit, Params, GasLimit) -> true ; GasLimit = -1),
787
+ % Create initial state with provided memory, params, context stack, trace depth and gas
788
+ InitialState = state{memory: InitialMemory, params: Params, context_stack: [], depth: 0, gas_remaining: GasLimit},
789
+ create_frame(SessionId, query, 0, Goal, InitialState, FrameId),
790
+ retractall(session_default_frame(SessionId, _)),
791
+ assertz(session_default_frame(SessionId, FrameId)),
792
+ session_frame_engine(FrameId, Engine),
793
+ retractall(session_engine(SessionId, _)),
794
+ assertz(session_engine(SessionId, Engine)).
795
+
796
+ %% ============================================================
797
+ %% Frames (re-entrant execution)
798
+ %% ============================================================
799
+ %%
800
+ %% Historically a session owned exactly one engine (session_engine/2).
801
+ %% Prompt mode requires nested engines: a meta frame that calls `execute`
802
+ %% which runs a child frame, which may itself call task() and spawn a
803
+ %% further frame. A frame is therefore (FrameId, SessionId, Kind, Depth)
804
+ %% with its engine stored in session_frame_engine/2.
805
+ %%
806
+ %% The legacy step_engine/4 resolves the session's default query frame and
807
+ %% delegates to step_frame/4, so single-engine callers are unaffected.
808
+
809
+ default_frame(SessionId, FrameId) :-
810
+ session_default_frame(SessionId, FrameId).
811
+
812
+ %% frame_info(+FrameId, -SessionId, -Kind, -Depth)
813
+ frame_info(FrameId, SessionId, Kind, Depth) :-
814
+ session_frame(FrameId, SessionId, Kind, Depth).
815
+
816
+ %% create_frame_raw(+SessionId, +Kind, +Depth, +EngineGoal, -FrameId)
817
+ %% Registers a frame whose engine runs EngineGoal verbatim. Most callers want
818
+ %% create_frame/6, which wraps the goal in the mi/3 meta-interpreter; goal mode
819
+ %% uses this to run repl_solve/4 directly (mi/3 cuts after the first solution).
820
+ create_frame_raw(SessionId, Kind, Depth, EngineGoal, FrameId) :-
821
+ gensym(frame_, FrameId),
822
+ engine_create(_, EngineGoal, Engine),
823
+ assertz(session_frame(FrameId, SessionId, Kind, Depth)),
824
+ assertz(session_frame_engine(FrameId, Engine)).
825
+
826
+ %% create_frame(+SessionId, +Kind, +Depth, +Goal, +InitialState, -FrameId)
827
+ %% Kind = query | meta | nested | object
828
+ %% Creates an engine running deepclause_mi:mi(Goal, InitialState, SessionId)
829
+ %% and registers it as a frame. The caller is responsible for depth-first
830
+ %% stepping: a parent frame is suspended while a child frame runs, so the
831
+ %% per-session result/signal slots are never concurrently pending.
832
+ create_frame(SessionId, Kind, Depth, Goal, InitialState, FrameId) :-
833
+ create_frame_raw(SessionId, Kind, Depth,
834
+ deepclause_mi:mi(Goal, InitialState, SessionId),
835
+ FrameId).
836
+
837
+ %% step_frame(+FrameId, -Status, -Content, -Payload)
838
+ step_frame(FrameId, Status, Content, Payload) :-
839
+ ( session_frame(FrameId, SessionId, _Kind, _Depth),
840
+ session_frame_engine(FrameId, Engine)
841
+ -> step_engine_handle(SessionId, Engine, Status, Content, Payload)
842
+ ; Status = error,
843
+ Content = 'No engine found for frame',
844
+ Payload = none
845
+ ).
846
+
847
+ %% step_engine_handle(+SessionId, +Engine, -Status, -Content, -Payload)
848
+ %% Shared stepping logic for both step_frame/4 and the legacy step_engine/4.
849
+ step_engine_handle(SessionId, Engine, Status, Content, Payload) :-
850
+ catch(
851
+ ( engine_next(Engine, Result)
852
+ -> process_engine_result(Result, Status, Content, Payload)
853
+ ; % Engine finished - return trace if enabled
854
+ Status = finished,
855
+ Content = '',
856
+ ( session_trace_enabled(SessionId, true),
857
+ session_trace_log(SessionId, TraceLog)
858
+ -> Payload = payload{trace: TraceLog}
859
+ ; Payload = none
860
+ )
861
+ ),
862
+ Error,
863
+ ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
864
+ Status = error,
865
+ Content = ErrMsg,
866
+ ( session_trace_enabled(SessionId, true),
867
+ session_trace_log(SessionId, TraceLog)
868
+ -> Payload = payload{trace: TraceLog}
869
+ ; Payload = none
870
+ )
871
+ )
872
+ ).
873
+
874
+ %% end_frame(+FrameId)
875
+ %% Destroys the frame's engine and unregisters the frame. Also clears the
876
+ %% session's default-frame pointer if this frame was the default.
877
+ end_frame(FrameId) :-
878
+ ( session_frame_engine(FrameId, Engine)
879
+ -> catch(engine_destroy(Engine), _, true)
880
+ ; true
881
+ ),
882
+ ( session_frame(FrameId, SessionId, _, _)
883
+ -> retractall(session_default_frame(SessionId, FrameId))
884
+ ; true
885
+ ),
886
+ retractall(session_frame_engine(FrameId, _)),
887
+ retractall(session_frame(FrameId, _, _, _)).
888
+
889
+ %% ============================================================
890
+ %% REPL Session and Goal Mode (M1)
891
+ %% ============================================================
892
+ %%
893
+ %% A REPL session owns a persistent living module. Unlike create_engine/6,
894
+ %% which is tied to a single run and destroyed by destroy_engine/1, a session
895
+ %% lives across inputs; reset/clear_session empties it without tearing down the
896
+ %% process-level SWI instance.
897
+
898
+ %% create_session(+SessionId, +Params, +HarnessMemory, +MetaMemory)
899
+ create_session(SessionId, Params, HarnessMemory, MetaMemory) :-
900
+ retractall(session_params(SessionId, _)),
901
+ assertz(session_params(SessionId, Params)),
902
+ ( get_dict(trace, Params, true)
903
+ -> TraceEnabled = true
904
+ ; TraceEnabled = false
905
+ ),
906
+ retractall(session_trace_enabled(SessionId, _)),
907
+ assertz(session_trace_enabled(SessionId, TraceEnabled)),
908
+ retractall(session_trace_log(SessionId, _)),
909
+ assertz(session_trace_log(SessionId, [])),
910
+ retractall(session_repl_memory(SessionId, _)),
911
+ assertz(session_repl_memory(SessionId, HarnessMemory)),
912
+ retractall(session_meta_memory(SessionId, _)),
913
+ assertz(session_meta_memory(SessionId, MetaMemory)),
914
+ retractall(session_repl_mode(SessionId, _)),
915
+ assertz(session_repl_mode(SessionId, true)),
916
+ retractall(session_frozen(SessionId, _)),
917
+ assertz(session_frozen(SessionId, false)),
918
+ retractall(session_locked(SessionId, _)),
919
+ assertz(session_locked(SessionId, false)).
920
+
921
+ %% repl_abolish_user_predicates(+SessionId)
922
+ %% Removes only the harness's own predicates; imported builtins are untouched.
923
+ repl_abolish_user_predicates(SessionId) :-
924
+ ( current_module(SessionId)
925
+ -> findall(Name/Arity,
926
+ ( current_predicate(SessionId:Name/Arity),
927
+ repl_user_predicate(SessionId, Name, Arity)
928
+ ),
929
+ UserPreds0),
930
+ sort(UserPreds0, UserPreds),
931
+ forall(member(Name/Arity, UserPreds),
932
+ catch(abolish(SessionId:Name/Arity), _, true))
933
+ ; true
934
+ ).
935
+
936
+ %% clear_session(+SessionId)
937
+ %% Empties the living module, ends all frames, and resets memories. Session
938
+ %% params (workspace/gas limit) are preserved so the REPL stays configured.
939
+ clear_session(SessionId) :-
940
+ ( findall(FrameId, session_frame(FrameId, SessionId, _, _), FrameIds)
941
+ -> forall(member(FrameId, FrameIds), end_frame(FrameId))
942
+ ; true
943
+ ),
944
+ ( session_engine(SessionId, Engine)
945
+ -> catch(engine_destroy(Engine), _, true)
946
+ ; true
947
+ ),
948
+ retractall(session_engine(SessionId, _)),
949
+ retractall(session_default_frame(SessionId, _)),
950
+ retractall(session_repl_memory(SessionId, _)),
951
+ assertz(session_repl_memory(SessionId, [])),
952
+ retractall(session_meta_memory(SessionId, _)),
953
+ assertz(session_meta_memory(SessionId, [])),
954
+ retractall(repl_snapshot(SessionId, _)),
955
+ retractall(session_user_tools(SessionId, _)),
956
+ retractall(session_user_tool_schema(SessionId, _, _)),
957
+ repl_abolish_user_predicates(SessionId).
958
+
959
+ %% destroy_session(+SessionId)
960
+ %% Tears a REPL session down completely: clears the module/frames and removes
961
+ %% the session's dynamic bookkeeping so the process can host another session.
962
+ destroy_session(SessionId) :-
963
+ clear_session(SessionId),
964
+ retractall(session_repl_memory(SessionId, _)),
965
+ retractall(session_meta_memory(SessionId, _)),
966
+ retractall(session_repl_mode(SessionId, _)),
967
+ retractall(session_frozen(SessionId, _)),
968
+ retractall(session_locked(SessionId, _)),
969
+ retractall(session_directive(SessionId, _)),
970
+ retractall(session_meta_catalog(SessionId, _)),
971
+ retractall(session_dml_reference(SessionId, _)),
972
+ retractall(session_authoring_examples(SessionId, _)),
973
+ retractall(session_meta_template(SessionId, _)),
974
+ retractall(session_harness_examples(SessionId, _)),
975
+ retractall(session_prolog_snippets(SessionId, _)),
976
+ retractall(session_last_commit(SessionId, _)),
977
+ retractall(repl_snapshot(SessionId, _)),
978
+ retractall(session_params(SessionId, _)),
979
+ retractall(session_trace_enabled(SessionId, _)),
980
+ retractall(session_trace_log(SessionId, _)).
981
+
982
+ %% load_code(+SessionId, +Code, -Error)
983
+ %% Parses DML source into the session module, running the normal source
984
+ %% preprocessing (task/prompt rewrite, interpolation, expand_term).
985
+ load_code(SessionId, Code, Error) :-
986
+ catch(
987
+ ( guard_not_frozen(SessionId),
988
+ parse_dml_string(Code, SessionId),
989
+ Error = none
990
+ ),
991
+ LoadError,
992
+ format(atom(Error), '~w', [LoadError])
993
+ ).
994
+
995
+ %% repl_initial_state(+SessionId, +FrameKind, -InitialState)
996
+ %% Builds the state{} threaded through a REPL frame. M1 seeds harness memory
997
+ %% from session_repl_memory/2; M4 adds the meta/scratch slots.
998
+ repl_initial_state(SessionId, FrameKind, InitialState) :-
999
+ session_params(SessionId, Params),
1000
+ ( get_dict(gas_limit, Params, GasLimit)
1001
+ -> true
1002
+ ; GasLimit = -1
1003
+ ),
1004
+ % Two-level memory: meta (code-writing) and harness (runtime) are kept
1005
+ % apart. The state carries the slot so write-back goes to the right one.
1006
+ ( FrameKind == meta
1007
+ -> MemorySlot = meta,
1008
+ ( session_meta_memory(SessionId, Memory)
1009
+ -> true
1010
+ ; Memory = []
1011
+ )
1012
+ ; MemorySlot = harness,
1013
+ ( session_repl_memory(SessionId, Memory)
1014
+ -> true
1015
+ ; Memory = []
1016
+ )
1017
+ ),
1018
+ InitialState = state{
1019
+ memory: Memory,
1020
+ params: Params,
1021
+ context_stack: [],
1022
+ depth: 0,
1023
+ gas_remaining: GasLimit,
1024
+ memory_slot: MemorySlot,
1025
+ frame_kind: FrameKind
1026
+ }.
1027
+
1028
+ %% query_start(+SessionId, +Text, +FrameKind, +InitialState, -Error)
1029
+ %% Parses and preprocesses a goal string, then creates a frame running
1030
+ %% repl_solve/4. The frame becomes the session's default frame so that
1031
+ %% step_engine/4 and the runner can drive it.
1032
+ query_start(SessionId, Text, FrameKind, InitialState, Error) :-
1033
+ catch(
1034
+ ( read_repl_goal(SessionId, Text, Goal, Names),
1035
+ create_frame_raw(SessionId, FrameKind, 0,
1036
+ deepclause_mi:repl_solve(Goal, Names, SessionId, InitialState),
1037
+ FrameId),
1038
+ retractall(session_default_frame(SessionId, _)),
1039
+ assertz(session_default_frame(SessionId, FrameId)),
1040
+ session_frame_engine(FrameId, Engine),
1041
+ retractall(session_engine(SessionId, _)),
1042
+ assertz(session_engine(SessionId, Engine)),
1043
+ Error = none
1044
+ ),
1045
+ QueryError,
1046
+ format(atom(Error), '~w', [QueryError])
1047
+ ).
1048
+
1049
+ %% ensure_full_stop(+Text, -TextWithStop)
1050
+ %% Appends a terminating full stop + newline when the goal text lacks one, so
1051
+ %% both `goal: p(X).` and `-g "p(X)"` read cleanly.
1052
+ ensure_full_stop(Text, Text1) :-
1053
+ atom_string(TextAtom, Text),
1054
+ ( atom_ends_with_stop(TextAtom)
1055
+ -> atom_concat(TextAtom, '\n', Text1)
1056
+ ; atom_concat(TextAtom, '.\n', Text1)
1057
+ ).
1058
+
1059
+ %% atom_ends_with_stop(+Atom)
1060
+ %% True when the last non-whitespace character is a full stop. A naive
1061
+ %% "ends with ." test would append an extra full stop to any block that ends
1062
+ %% with a newline, and read_term/3 then fails on the lone '.' (end_of_clause).
1063
+ atom_ends_with_stop(TextAtom) :-
1064
+ atom_string(TextAtom, TextStr),
1065
+ string_length(TextStr, Len0),
1066
+ trim_trailing_ws(TextStr, Len0, Len),
1067
+ Len > 0,
1068
+ Pos is Len - 1,
1069
+ sub_string(TextStr, Pos, 1, _, ".").
1070
+
1071
+ trim_trailing_ws(Str, Len0, Len) :-
1072
+ ( Len0 > 0,
1073
+ Pos is Len0 - 1,
1074
+ sub_string(Str, Pos, 1, _, Ch),
1075
+ ( Ch == " " ; Ch == "\t" ; Ch == "\n" ; Ch == "\r" )
1076
+ -> Len1 is Len0 - 1,
1077
+ trim_trailing_ws(Str, Len1, Len)
1078
+ ; Len = Len0
1079
+ ).
1080
+
1081
+ %% read_repl_goal(+SessionId, +Text, -Goal, -Names)
1082
+ %% Names is the read_term variable_names list (Name=Var), used to render
1083
+ %% bindings for solution events.
1084
+ read_repl_goal(SessionId, Text, Goal, Names) :-
1085
+ % read_term/3 requires a full stop; REPL callers may omit it (`-g "p(X)"`).
1086
+ ensure_full_stop(Text, Text1),
1087
+ open_string(Text1, Stream),
1088
+ read_term(Stream, RawGoal, [module(SessionId), variable_names(Names0)]),
1089
+ close(Stream),
1090
+ ( RawGoal == end_of_file
1091
+ -> throw(error(syntax_error('empty goal'), _))
1092
+ ; true
1093
+ ),
1094
+ % Apply only the goal-relevant source transforms. Do NOT run
1095
+ % preprocess_loaded_term/3 here: its file-load term expansion flattens a
1096
+ % list goal such as ["file.dml"] into its elements.
1097
+ transform_task_calls(RawGoal, Names0, Transformed),
1098
+ % Use the body expander (not expand_term_interp/3) so a `{Var}` string is
1099
+ % hoisted into a goal *before* the goal that uses it. expand_term_interp
1100
+ % embeds the interpolation term in the argument position instead, which is
1101
+ % wrong for a raw goal such as verify(X, "Is {X} > 0?", V).
1102
+ expand_body_interp(Transformed, Names0, Goal),
1103
+ Names = Names0.
1104
+
1105
+ %% repl_solve(+Goal, +Names, +SessionId, +State)
1106
+ %% Enumerating goal-mode solver. Yields one solution(Bindings, Deterministic)
1107
+ %% per answer and fails so engine_next/2 backtracks for the next solution.
1108
+ %% answer/1 commits (throws '$answer_commit') and stops enumeration.
1109
+ %% mi/3 is deliberately not used: it cuts after the first solution.
1110
+ repl_solve(Goal, Names, SessionId, State) :-
1111
+ nb_setval(current_session_id, SessionId),
1112
+ catch(
1113
+ repl_solve_loop(Goal, Names, SessionId, State),
1114
+ '$answer_commit',
1115
+ true
1116
+ ).
1117
+
1118
+ repl_solve_loop(Goal, Names, SessionId, State) :-
1119
+ mi_call(Goal, State, StateOut),
1120
+ collect_bindings(Names, Bindings),
1121
+ store_memory_for_state(SessionId, StateOut),
1122
+ engine_yield(solution(Bindings, false)),
1123
+ fail.
1124
+
1125
+ %% collect_bindings(+Names, -Bindings)
1126
+ %% Bindings is a list of Name-RenderedText. Unbound variables render as '_'.
1127
+ collect_bindings(Names, Bindings) :-
1128
+ findall(Name-Text,
1129
+ ( member(Name=Var, Names),
1130
+ render_binding(Var, Text)
1131
+ ),
1132
+ Bindings).
1133
+
1134
+ render_binding(Var, '_') :-
1135
+ var(Var),
1136
+ !.
1137
+ render_binding(Value, Text) :-
1138
+ with_output_to(string(Text), write_term(Value, [quoted(true), max_depth(20)])).
1139
+
1140
+ %% store_memory_for_state(+SessionId, +State)
1141
+ %% Writes the threaded memory back to the slot named by the state.
1142
+ store_memory_for_state(SessionId, State) :-
1143
+ ( is_dict(State),
1144
+ get_dict(memory, State, Memory)
1145
+ -> ( get_dict(memory_slot, State, meta)
1146
+ -> retractall(session_meta_memory(SessionId, _)),
1147
+ assertz(session_meta_memory(SessionId, Memory))
1148
+ ; retractall(session_repl_memory(SessionId, _)),
1149
+ assertz(session_repl_memory(SessionId, Memory))
1150
+ )
1151
+ ; true
1152
+ ).
1153
+
1154
+ %% repl_listing(+SessionId, +Filter, -Text)
1155
+ %% Read-only listing used by the meta tool and the `listing.` REPL command.
1156
+ repl_listing(SessionId, Filter, Text) :-
1157
+ with_output_to(string(Text), repl_listing_output(SessionId, Filter)).
1158
+
1159
+ %% repl_listing(+Filter, -Text) uses the current session id.
1160
+ repl_listing(Filter, Text) :-
1161
+ get_session_id(SessionId),
1162
+ repl_listing(SessionId, Filter, Text).
1163
+
1164
+ repl_listing_output(SessionId, Filter) :-
1165
+ ( Filter == tools
1166
+ -> forall(session_user_tool_schema(SessionId, Name, Schema),
1167
+ ( SessionId:tool_source(Name, Source)
1168
+ -> format("~w - ~w~n~w~n~n", [Name, Schema, Source])
1169
+ ; format("~w - ~w~n~n", [Name, Schema])
1170
+ )
1171
+ )
1172
+ ; Filter == history
1173
+ -> repl_history_listing(SessionId)
1174
+ ; ( current_predicate(SessionId:module_doc/1),
1175
+ SessionId:module_doc(Skill)
1176
+ -> format("skill: ~w~n~n", [Skill])
1177
+ ; true
1178
+ ),
1179
+ findall(Name/Arity,
1180
+ ( current_predicate(SessionId:Name/Arity),
1181
+ repl_user_predicate(SessionId, Name, Arity)
1182
+ ),
1183
+ Preds0),
1184
+ sort(Preds0, Preds),
1185
+ forall(member(Name/Arity, Preds),
1186
+ ( repl_listing_match(Filter, Name, Arity)
1187
+ -> format("~w/~w~n", [Name, Arity]),
1188
+ ( current_predicate(SessionId:doc/2),
1189
+ SessionId:doc(Name/Arity, DocText)
1190
+ -> format(" % ~w~n", [DocText])
1191
+ ; true
1192
+ ),
1193
+ functor(Head, Name, Arity),
1194
+ forall(clause(SessionId:Head, ClauseBody), print_user_clause(Head, ClauseBody))
1195
+ ; true
1196
+ )
1197
+ )
1198
+ ).
1199
+
1200
+ repl_listing_match(all, _, _) :- !.
1201
+ repl_listing_match(Name/Arity, Name, Arity) :- !.
1202
+ repl_listing_match(Filter, Name, _) :-
1203
+ atom(Filter),
1204
+ !,
1205
+ ( sub_atom(Name, 0, _, _, Filter)
1206
+ ; sub_atom(Filter, 0, _, _, Name)
1207
+ ).
1208
+ repl_listing_match(_, _, _).
1209
+
1210
+ %% repl_user_predicate(+SessionId, +Name, +Arity)
1211
+ %% Excludes reserved and internal predicates from listing/export.
1212
+ repl_user_predicate(_SessionId, Name, _Arity) :-
1213
+ repl_reserved_name(Name),
1214
+ !,
1215
+ fail.
1216
+ repl_user_predicate(_SessionId, Name, _Arity) :-
1217
+ internal_session_predicate(Name),
1218
+ !,
1219
+ fail.
1220
+ repl_user_predicate(_SessionId, Name, _Arity) :-
1221
+ history_predicate(Name),
1222
+ !,
1223
+ fail.
1224
+ repl_user_predicate(SessionId, Name, Arity) :-
1225
+ % User DML clauses are asserted (interpreted) into the session module.
1226
+ % Exclude predicates imported via use_module (e.g. library(clpfd)) and
1227
+ % foreign/builtin predicates. Build a callable head rather than a predicate
1228
+ % indicator: predicate_property/2 wants a goal (Module:Goal).
1229
+ functor(Head, Name, Arity),
1230
+ predicate_property(SessionId:Head, interpreted),
1231
+ \+ predicate_property(SessionId:Head, imported_from(_)),
1232
+ \+ predicate_property(SessionId:Head, foreign).
1233
+
1234
+ internal_session_predicate(param).
1235
+ internal_session_predicate(param_decl).
1236
+ internal_session_predicate(tool_source).
1237
+ internal_session_predicate('__session__').
1238
+
1239
+ %% history_predicate(+Name)
1240
+ %% Runtime-owned harness-history facts. Excluded from the user predicate set (so
1241
+ %% they are neither listed as harness predicates nor exported nor replaced by a
1242
+ %% commit) and protected from user definition -- but CALLABLE from DML, so the
1243
+ %% harness can query what happened in previous sessions.
1244
+ history_predicate(Name) :-
1245
+ atom(Name),
1246
+ sub_atom(Name, 0, _, _, harness_).
1247
+
1248
+ %% repl_reserved_name(+Name)
1249
+ %% Names owned by the REPL runtime. M4 uses repl_reserved/1 to refuse goals
1250
+ %% that try to re-enter or shadow the REPL control surface.
1251
+ %% Any repl_*/meta_* name is reserved (namespace guard), plus the explicit
1252
+ %% control names below. repl_delete is a protected exception for definition
1253
+ %% purposes (see repl_protected_name/1).
1254
+ repl_reserved_name(Name) :-
1255
+ atom(Name),
1256
+ ( sub_atom(Name, 0, _, _, repl_)
1257
+ ; sub_atom(Name, 0, _, _, meta_)
1258
+ ).
1259
+
1260
+ repl_reserved_name(repl_meta_prompt).
1261
+ repl_reserved_name(repl_extract_code).
1262
+ repl_reserved_name(repl_reserved).
1263
+ repl_reserved_name(repl_reserved_name).
1264
+ repl_reserved_name(repl_tool_catalog).
1265
+ repl_reserved_name(repl_listing).
1266
+ repl_reserved_name(repl_assert).
1267
+ repl_reserved_name(repl_retract).
1268
+ repl_reserved_name(repl_delete).
1269
+ repl_reserved_name(repl_freeze).
1270
+ repl_reserved_name(repl_unfreeze).
1271
+ repl_reserved_name(repl_is_frozen).
1272
+ repl_reserved_name(repl_solve).
1273
+ repl_reserved_name(repl_initial_state).
1274
+ repl_reserved_name(repl_user_predicate).
1275
+ repl_reserved_name(query_start).
1276
+ repl_reserved_name(read_repl_goal).
1277
+ repl_reserved_name(create_session).
1278
+ repl_reserved_name(clear_session).
1279
+ repl_reserved_name(destroy_session).
1280
+ repl_reserved_name(create_frame).
1281
+ repl_reserved_name(create_frame_raw).
1282
+ repl_reserved_name(step_frame).
1283
+ repl_reserved_name(end_frame).
1284
+ repl_reserved_name(frame_info).
1285
+ repl_reserved_name(default_frame).
1286
+ repl_reserved_name(load_code).
1287
+ repl_reserved_name(freeze).
1288
+ repl_reserved_name(unfreeze).
1289
+ repl_reserved_name(reset).
1290
+ repl_reserved_name(export).
1291
+
1292
+ %% repl_reserved(+TermOrNameArity)
1293
+ repl_reserved(Name) :-
1294
+ atom(Name),
1295
+ repl_reserved_name(Name).
1296
+ repl_reserved(Name/Arity) :-
1297
+ atom(Name),
1298
+ integer(Arity),
1299
+ repl_reserved_name(Name).
1300
+ repl_reserved(Term) :-
1301
+ compound(Term),
1302
+ functor(Term, Name, _),
1303
+ repl_reserved_name(Name).
1304
+
1305
+ %% ============================================================
1306
+ %% Freeze Lock and Export (M2)
1307
+ %% ============================================================
1308
+
1309
+ %% repl_freeze(+SessionId)
1310
+ %% Makes the living module read-only. Queries, `execute`, and nested task()
1311
+ %% calls still work; only mutation is refused.
1312
+ repl_freeze(SessionId) :-
1313
+ retractall(session_frozen(SessionId, _)),
1314
+ assertz(session_frozen(SessionId, true)).
1315
+
1316
+ %% repl_lock(+SessionId)
1317
+ %% Irreversibly freezes: repl_unfreeze/1 refuses. Used by --locked.
1318
+ repl_lock(SessionId) :-
1319
+ repl_freeze(SessionId),
1320
+ retractall(session_locked(SessionId, _)),
1321
+ assertz(session_locked(SessionId, true)).
1322
+
1323
+ %% repl_unfreeze(+SessionId)
1324
+ repl_unfreeze(SessionId) :-
1325
+ ( session_locked(SessionId, true)
1326
+ -> throw(error(permission_error(modify, locked_harness, SessionId),
1327
+ context(repl_unfreeze/1, 'harness is locked; unfreeze is disabled')))
1328
+ ; retractall(session_frozen(SessionId, _)),
1329
+ assertz(session_frozen(SessionId, false))
1330
+ ).
1331
+
1332
+ %% repl_is_frozen(+SessionId)
1333
+ repl_is_frozen(SessionId) :-
1334
+ session_frozen(SessionId, true),
1335
+ !.
1336
+
1337
+ %% repl_try_unfreeze(+SessionId, -Error)
1338
+ %% Non-throwing wrapper so the host can report a locked harness cleanly.
1339
+ repl_try_unfreeze(SessionId, Error) :-
1340
+ catch(repl_unfreeze(SessionId), UnfreezeError,
1341
+ format(atom(Error), '~w', [UnfreezeError])),
1342
+ ( var(Error)
1343
+ -> Error = none
1344
+ ; true
1345
+ ).
1346
+
1347
+ %% guard_not_frozen(+SessionId)
1348
+ %% Shared mutation guard for assert/retract/consult/load.
1349
+ guard_not_frozen(SessionId) :-
1350
+ ( session_frozen(SessionId, true)
1351
+ -> throw(error(permission_error(modify, frozen_harness, SessionId),
1352
+ context(mi_call/3, 'harness is frozen; assert/retract/load are disabled')))
1353
+ ; true
1354
+ ).
1355
+
1356
+ %% record_directive(+SessionId, +Directive)
1357
+ record_directive(SessionId, Directive) :-
1358
+ ( session_directive(SessionId, Directive)
1359
+ -> true
1360
+ ; assertz(session_directive(SessionId, Directive))
1361
+ ).
1362
+
1363
+ %% ---- Export (serialization) ----
1364
+
1365
+ %% export_harness(+SessionId, +Header, -Text)
1366
+ %% Deterministically serialize the living module into self-contained DML:
1367
+ %% directives, tools, embedded scripts, then user predicates sorted name/arity.
1368
+ export_harness(SessionId, Header, Text) :-
1369
+ with_output_to(string(Text), export_harness_output(SessionId, Header)).
1370
+
1371
+ export_harness_output(SessionId, Header) :-
1372
+ ( Header == none
1373
+ -> true
1374
+ ; format("% ~w~n", [Header])
1375
+ ),
1376
+ export_directives(SessionId),
1377
+ export_tools(SessionId),
1378
+ export_clauses(SessionId).
1379
+
1380
+ export_directives(SessionId) :-
1381
+ findall(Directive, session_directive(SessionId, Directive), Directives0),
1382
+ sort(Directives0, Directives),
1383
+ forall(member(Directive, Directives), format(":- ~q.~n", [Directive])),
1384
+ ( Directives == []
1385
+ -> true
1386
+ ; nl
1387
+ ).
1388
+
1389
+ export_tools(SessionId) :-
1390
+ findall(Name, session_user_tool_schema(SessionId, Name, _), Names0),
1391
+ sort(Names0, Names),
1392
+ forall(member(Name, Names),
1393
+ ( SessionId:tool_source(Name, Source)
1394
+ -> format("~w~n~n", [Source])
1395
+ ; true
1396
+ )).
1397
+
1398
+ export_clauses(SessionId) :-
1399
+ findall(Name/Arity,
1400
+ ( current_predicate(SessionId:Name/Arity),
1401
+ export_user_predicate(SessionId, Name, Arity)
1402
+ ),
1403
+ Preds0),
1404
+ sort(Preds0, Preds),
1405
+ forall(member(Name/Arity, Preds),
1406
+ ( functor(Head, Name, Arity),
1407
+ forall(clause(SessionId:Head, Body), export_clause(Head, Body))
1408
+ )).
1409
+
1410
+ export_user_predicate(SessionId, Name, Arity) :-
1411
+ repl_user_predicate(SessionId, Name, Arity),
1412
+ \+ tool_predicate_name(Name).
1413
+
1414
+ %% export_clause(+Head, +Body)
1415
+ %% Copy + numbervars the clause so variable names are deterministic (A, B, …),
1416
+ %% which is required for byte-stable export → reload → export.
1417
+ %% export_stats(+SessionId, -Clauses, -Tools, -Scripts)
1418
+ export_stats(SessionId, Clauses, Tools, Scripts) :-
1419
+ findall(Name/Arity,
1420
+ ( current_predicate(SessionId:Name/Arity),
1421
+ export_user_predicate(SessionId, Name, Arity)
1422
+ ),
1423
+ Preds0),
1424
+ sort(Preds0, Preds),
1425
+ findall(Count,
1426
+ ( member(PredName/PredArity, Preds),
1427
+ functor(PredHead, PredName, PredArity),
1428
+ aggregate_all(count, clause(SessionId:PredHead, _), Count)
1429
+ ),
1430
+ PredCounts),
1431
+ sum_list(PredCounts, Clauses),
1432
+ findall(Name, session_user_tool_schema(SessionId, Name, _), ToolNames0),
1433
+ sort(ToolNames0, ToolNames),
1434
+ length(ToolNames, Tools),
1435
+ ( current_predicate(SessionId:script_file/2)
1436
+ -> findall(Path, SessionId:script_file(_, Path), ScriptPaths0),
1437
+ sort(ScriptPaths0, ScriptPaths),
1438
+ length(ScriptPaths, Scripts)
1439
+ ; Scripts = 0
1440
+ ).
1441
+
1442
+ export_clause(Head, Body) :-
1443
+ print_user_clause(Head, Body),
1444
+ nl.
1445
+
1446
+ %% format_tool_source(+Head, +Description, +Body, -SourceCode)
1447
+ %% Source text for a tool, used by `listing(tools)` and export. Reverses the
1448
+ %% load-time task/prompt rewrites so readers see `task/N`, not `task_named/3`.
1449
+ format_tool_source(Head, none, Body, SourceCode) :-
1450
+ !,
1451
+ copy_term(Head-Body, NH-NB),
1452
+ numbervars(NH-NB, 0, _),
1453
+ clean_clause_body(NB, CleanBody),
1454
+ ( CleanBody == true
1455
+ -> format(string(SourceCode), "tool(~q).", [NH])
1456
+ ; format(string(SourceCode), "tool(~q) :-~n ~q.", [NH, CleanBody])
1457
+ ).
1458
+ format_tool_source(Head, Description, Body, SourceCode) :-
1459
+ copy_term(Head-Description-Body, NH-ND-NB),
1460
+ numbervars(NH-ND-NB, 0, _),
1461
+ clean_clause_body(NB, CleanBody),
1462
+ ( CleanBody == true
1463
+ -> format(string(SourceCode), "tool(~q, ~q).", [NH, ND])
1464
+ ; format(string(SourceCode), "tool(~q, ~q) :-~n ~q.", [NH, ND, CleanBody])
1465
+ ).
1466
+
1467
+ %% print_user_clause(+Head, +Body)
1468
+ %% Copy + numbervars for deterministic variable names, reverse the load-time
1469
+ %% task/prompt rewrites (see clean_clause_body/2), then print in source form.
1470
+ print_user_clause(Head, Body) :-
1471
+ copy_term(Head-Body, NumberedHead-NumberedBody),
1472
+ numbervars(NumberedHead-NumberedBody, 0, _),
1473
+ clean_clause_body(NumberedBody, CleanBody),
1474
+ ( CleanBody == true
1475
+ -> format("~q.~n", [NumberedHead])
1476
+ ; format("~q :-~n ~q.~n", [NumberedHead, CleanBody])
1477
+ ).
1478
+
1479
+ tool_predicate_name(tool).
1480
+ tool_predicate_name(tool_source).
1481
+
1482
+ %% repl_script_files(+SessionId, -Paths)
1483
+ %% Workspace-relative files linked by script_file/2 facts; the host bundles them.
1484
+ repl_script_files(SessionId, Paths) :-
1485
+ ( current_predicate(SessionId:script_file/2)
1486
+ -> % Only literal paths count; a stray variable path is ignored.
1487
+ findall(Path,
1488
+ ( SessionId:script_file(_, Path),
1489
+ (atom(Path) ; string(Path))
1490
+ ),
1491
+ Paths0),
1492
+ sort(Paths0, Paths)
1493
+ ; Paths = []
1494
+ ).
1495
+
1496
+ %% repl_materialize_embedded(+SessionId)
1497
+ %% Writes every embedded_file(Path, Content) fact to its workspace path. Used
1498
+ %% after loading a single-file .dml export (where linked scripts are inlined).
1499
+ repl_materialize_embedded(SessionId) :-
1500
+ ( current_predicate(SessionId:embedded_file/2)
1501
+ -> forall(SessionId:embedded_file(Path, Content),
1502
+ ( resolve_workspace_path(Path, Full),
1503
+ catch(
1504
+ ( file_directory_name(Full, Dir),
1505
+ make_directory_path(Dir),
1506
+ setup_call_cleanup(
1507
+ open(Full, write, Stream),
1508
+ format(Stream, "~s", [Content]),
1509
+ close(Stream))
1510
+ ),
1511
+ _,
1512
+ true
1513
+ )
1514
+ ))
1515
+ ; true
1516
+ ).
1517
+
1518
+ %% ============================================================
1519
+ %% Prompt Mode (M3)
1520
+ %% ============================================================
1521
+
1522
+ %% repl_protected_name(+Name)
1523
+ %% Reserved names that generated code may not (re)define. repl_delete is a
1524
+ %% legitimate harness directive and is allowed through.
1525
+ repl_protected_name(Name) :-
1526
+ history_predicate(Name).
1527
+ repl_protected_name(Name) :-
1528
+ repl_reserved_name(Name),
1529
+ Name \== repl_delete.
1530
+
1531
+ repl_protected_clause((Head :- _)) :-
1532
+ callable(Head),
1533
+ functor(Head, Name, _),
1534
+ repl_protected_name(Name).
1535
+ repl_protected_clause(Head) :-
1536
+ callable(Head),
1537
+ functor(Head, Name, _),
1538
+ repl_protected_name(Name).
1539
+
1540
+ %% repl_set_meta_catalog(+SessionId, +CatalogText)
1541
+ repl_set_meta_catalog(SessionId, CatalogText) :-
1542
+ retractall(session_meta_catalog(SessionId, _)),
1543
+ assertz(session_meta_catalog(SessionId, CatalogText)).
1544
+
1545
+ %% repl_prepare_single(+SessionId, +ClauseText, -PreparedTerms)
1546
+ %% Reads exactly ONE source term and runs the SAME load-time preprocessing as
1547
+ %% parse_dml_string/2: transform_task_calls/3 (task/N, prompt/N ->
1548
+ %% task_named/3, prompt_named/3), expand_interpolations/3 ({Var} strings), and
1549
+ %% expand_term/2 (e.g. DCG rules). Scratch asserts must be preprocessed exactly
1550
+ %% like a file load, or a clause asserted mid-turn keeps task/N, which has no
1551
+ %% runtime handler and fails silently.
1552
+ %%
1553
+ %% Trailing content is an error so `assert` can never silently drop clauses that
1554
+ %% `assert_many` would have kept.
1555
+ repl_prepare_single(SessionId, ClauseText, PreparedTerms) :-
1556
+ ensure_full_stop(ClauseText, Text1),
1557
+ open_string(Text1, Stream),
1558
+ read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
1559
+ ( read_term(Stream, Extra, [module(SessionId)]),
1560
+ Extra \== end_of_file
1561
+ -> close(Stream),
1562
+ throw(error(syntax_error('assert expects a single clause; use assert_many for multiple'), _))
1563
+ ; close(Stream)
1564
+ ),
1565
+ preprocess_loaded_term(Term, Bindings, PreparedTerms).
1566
+
1567
+ %% repl_read_clause(+SessionId, +ClauseText, -Clause)
1568
+ %% Reads exactly one term without preprocessing. Used by retract, which only
1569
+ %% needs the predicate's name/arity. Trailing content is an error.
1570
+ repl_read_clause(SessionId, ClauseText, Clause) :-
1571
+ ensure_full_stop(ClauseText, Text1),
1572
+ open_string(Text1, Stream),
1573
+ read_term(Stream, Clause, [module(SessionId)]),
1574
+ ( read_term(Stream, Extra, [module(SessionId)]),
1575
+ Extra \== end_of_file
1576
+ -> close(Stream),
1577
+ throw(error(syntax_error('retract expects a single clause pattern'), _))
1578
+ ; close(Stream)
1579
+ ).
1580
+
1581
+ %% repl_head_of(+Term, -Head)
1582
+ %% Strips a rule body, so retracting with a full clause pattern targets the
1583
+ %% predicate instead of accidentally matching every rule ((:-)/2).
1584
+ repl_head_of((Head :- _Body), Head) :- !.
1585
+ repl_head_of(Head, Head).
1586
+
1587
+ %% repl_assert_clause(+PreparedTerm, +SessionId)
1588
+ %% Asserts one already-preprocessed term, rejecting reserved names.
1589
+ repl_assert_clause(PreparedTerm, SessionId) :-
1590
+ ( repl_protected_clause(PreparedTerm)
1591
+ -> throw(error(permission_error(modify, reserved_name, PreparedTerm),
1592
+ context(repl_assert/3, 'cannot define a reserved predicate')))
1593
+ ; process_clause(PreparedTerm, SessionId)
1594
+ ).
1595
+
1596
+ %% repl_assert(+SessionId, +ClauseText, -Result)
1597
+ %% Parses and asserts generated DML with the load-time preprocessing applied,
1598
+ %% so scratch-asserted clauses behave exactly like loaded ones.
1599
+ %% Refused when frozen; reserved names are rejected.
1600
+ repl_assert(SessionId, ClauseText, Result) :-
1601
+ catch(
1602
+ ( guard_not_frozen(SessionId),
1603
+ repl_prepare_single(SessionId, ClauseText, PreparedTerms),
1604
+ forall(member(PreparedTerm, PreparedTerms),
1605
+ repl_assert_clause(PreparedTerm, SessionId)),
1606
+ Result = ok
1607
+ ),
1608
+ AssertError,
1609
+ format(atom(Result), 'error(~w)', [AssertError])
1610
+ ).
1611
+
1612
+ %% repl_set_dml_reference(+SessionId, +Text)
1613
+ %% Stores the bundled DML reference so the meta prompt can include it.
1614
+ repl_set_dml_reference(SessionId, Text) :-
1615
+ retractall(session_dml_reference(SessionId, _)),
1616
+ assertz(session_dml_reference(SessionId, Text)).
1617
+
1618
+ %% repl_set_authoring_examples(+SessionId, +Text)
1619
+ %% Stores the curated authoring patterns for the meta prompt.
1620
+ repl_set_authoring_examples(SessionId, Text) :-
1621
+ retractall(session_authoring_examples(SessionId, _)),
1622
+ assertz(session_authoring_examples(SessionId, Text)).
1623
+
1624
+ %% repl_set_meta_template(+SessionId, +Template)
1625
+ %% Stores the meta-prompt template (the bundled META_PROMPT.md). Placeholders
1626
+ %% {{CATALOG}}, {{REFERENCE}}, {{EXAMPLES}}, {{MANIFEST}}, {{MEMORY}}, {{REQUEST}}
1627
+ %% are filled by repl_meta_prompt/3.
1628
+ repl_set_meta_template(SessionId, Template) :-
1629
+ retractall(session_meta_template(SessionId, _)),
1630
+ assertz(session_meta_template(SessionId, Template)).
1631
+
1632
+ %% repl_set_harness_examples(+SessionId, +Text)
1633
+ %% Stores the bundled HARNESS_EXAMPLES.md ({{EXAMPLES}}).
1634
+ repl_set_harness_examples(SessionId, Text) :-
1635
+ retractall(session_harness_examples(SessionId, _)),
1636
+ assertz(session_harness_examples(SessionId, Text)).
1637
+
1638
+ %% repl_set_prolog_snippets(+SessionId, +Text)
1639
+ %% Stores the bundled PROLOG_SNIPPETS.md ({{SNIPPETS}}).
1640
+ repl_set_prolog_snippets(SessionId, Text) :-
1641
+ retractall(session_prolog_snippets(SessionId, _)),
1642
+ assertz(session_prolog_snippets(SessionId, Text)).
1643
+
1644
+ %% repl_parse_clauses(+SessionId, +ClausesText, -Terms)
1645
+ %% Parse and preprocess a block of DML clauses without asserting them.
1646
+ repl_parse_clauses(SessionId, ClausesText, Terms) :-
1647
+ ensure_full_stop(ClausesText, Text1),
1648
+ open_string(Text1, Stream),
1649
+ repl_read_prepared_terms(Stream, SessionId, Terms),
1650
+ close(Stream).
1651
+
1652
+ repl_read_prepared_terms(Stream, SessionId, Terms) :-
1653
+ read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
1654
+ ( Term == end_of_file
1655
+ -> Terms = []
1656
+ ; % Apply the same preprocessing as parse_dml_string/2 so scratch clauses
1657
+ % match loaded ones (task/prompt rewrite, interpolation, expand_term).
1658
+ preprocess_loaded_term(Term, Bindings, Prepared),
1659
+ % Directives (e.g. use_module(library(clpfd))) must run while parsing so
1660
+ % their operators are visible to the terms that follow; keep them out of
1661
+ % the returned list so repl_commit_terms does not process them twice.
1662
+ partition_directives(Prepared, Rules, Directives),
1663
+ forall(member(Directive, Directives), process_clause(Directive, SessionId)),
1664
+ append(Rules, Rest, Terms),
1665
+ repl_read_prepared_terms(Stream, SessionId, Rest)
1666
+ ).
1667
+
1668
+ partition_directives([], [], []).
1669
+ partition_directives([Term|Terms], Rules, Directives) :-
1670
+ ( Term = (:- _)
1671
+ -> Rules = Rules0,
1672
+ Directives = [Term|Directives0]
1673
+ ; Rules = [Term|Rules0],
1674
+ Directives = Directives0
1675
+ ),
1676
+ partition_directives(Terms, Rules0, Directives0).
1677
+
1678
+ %% repl_commit_terms(+SessionId, +Terms)
1679
+ %% Assert already-preprocessed terms (the shared tail of assert_many/define).
1680
+ repl_commit_terms(SessionId, Terms) :-
1681
+ forall(member(PreparedTerm, Terms),
1682
+ ( repl_protected_clause(PreparedTerm)
1683
+ -> throw(error(permission_error(modify, reserved_name, PreparedTerm),
1684
+ context(repl_assert_many/3, 'cannot define a reserved predicate')))
1685
+ ; process_clause(PreparedTerm, SessionId)
1686
+ )).
1687
+
1688
+ %% repl_assert_many(+SessionId, +ClausesText, -Result)
1689
+ %% Parses a block of clauses and asserts them in the order written. Prefer this
1690
+ %% for defining a predicate: clause order matters for backtracking, so asserting
1691
+ %% one clause at a time can produce a wrong program.
1692
+ repl_assert_many(SessionId, ClausesText, Result) :-
1693
+ catch(
1694
+ ( guard_not_frozen(SessionId),
1695
+ repl_parse_clauses(SessionId, ClausesText, Terms),
1696
+ repl_commit_terms(SessionId, Terms),
1697
+ repl_note_touched(SessionId, Terms),
1698
+ Result = ok
1699
+ ),
1700
+ AssertError,
1701
+ format(atom(Result), 'error(~w)', [AssertError])
1702
+ ).
1703
+
1704
+ %% repl_term_indicator(+Term, -Name/Arity)
1705
+ %% The plain predicate a parsed clause defines. Directives and tool/2 wrappers
1706
+ %% have no plain indicator here (process_clause owns them).
1707
+ repl_term_indicator((:- _), _) :- !, fail.
1708
+ repl_term_indicator((tool(_, _) :- _), _) :- !, fail.
1709
+ repl_term_indicator((tool(_) :- _), _) :- !, fail.
1710
+ repl_term_indicator(Term, Name/Arity) :-
1711
+ repl_head_of(Term, Head),
1712
+ callable(Head),
1713
+ functor(Head, Name, Arity).
1714
+
1715
+ %% repl_note_touched(+SessionId, +Terms)
1716
+ %% Record the predicate indicators a term list defines, for history recording.
1717
+ repl_note_touched(SessionId, Terms) :-
1718
+ forall(( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
1719
+ ( session_edit_touched(SessionId, Name/Arity)
1720
+ -> true
1721
+ ; assertz(session_edit_touched(SessionId, Name/Arity))
1722
+ )).
1723
+
1724
+ %% repl_edit_touched(+SessionId, -Indicators)
1725
+ %% Predicates touched by edit tools since the last clear (sorted, unique).
1726
+ repl_edit_touched(SessionId, Indicators) :-
1727
+ findall(Name/Arity, session_edit_touched(SessionId, Name/Arity), Raw),
1728
+ sort(Raw, Indicators).
1729
+
1730
+ %% repl_edit_touched_text(+SessionId, -Text)
1731
+ %% "Name/Arity, Name/Arity" of the predicates an agent turn touched.
1732
+ repl_edit_touched_text(SessionId, Text) :-
1733
+ repl_edit_touched(SessionId, Indicators),
1734
+ with_output_to(string(Text), print_pred_list(Indicators)).
1735
+
1736
+ repl_edit_touched_clear(SessionId) :-
1737
+ retractall(session_edit_touched(SessionId, _)).
1738
+
1739
+ %% repl_callable_indicator(+Head, -Name, -Arity)
1740
+ %% Validate a predicate head: an ordinary (non-operator, non-system) functor.
1741
+ repl_callable_indicator(Head, Name, Arity) :-
1742
+ callable(Head),
1743
+ functor(Head, Name, Arity),
1744
+ atom(Name),
1745
+ Name \== '[]',
1746
+ Name \== '{}',
1747
+ \+ repl_system_functor(Name/Arity).
1748
+
1749
+ %% Functors that are list/operator/system structure, never user predicates.
1750
+ repl_system_functor(Name/_) :-
1751
+ member(Name, ['.', ',', ';', ':-', '->', ':', '|', '!', '\\+', '=', 'is']).
1752
+
1753
+ %% repl_retract(+SessionId, +ClausePattern, -Result)
1754
+ %% Retracts matching user clauses. Reserved names are protected.
1755
+ repl_retract(SessionId, ClauseText, Result) :-
1756
+ catch(
1757
+ ( guard_not_frozen(SessionId),
1758
+ repl_read_clause(SessionId, ClauseText, Pattern),
1759
+ repl_head_of(Pattern, Clause),
1760
+ repl_callable_indicator(Clause, Name, Arity),
1761
+ ( repl_protected_clause(Clause)
1762
+ -> throw(error(permission_error(modify, reserved_name, Clause),
1763
+ context(repl_retract/3, 'cannot retract a reserved predicate')))
1764
+ ; true
1765
+ ),
1766
+ functor(Head, Name, Arity),
1767
+ retractall(SessionId:Head),
1768
+ ( session_user_tools(SessionId, Name)
1769
+ -> retractall(session_user_tools(SessionId, Name)),
1770
+ retractall(session_user_tool_schema(SessionId, Name, _)),
1771
+ retractall(SessionId:tool_source(Name, _))
1772
+ ; true
1773
+ ),
1774
+ ( session_edit_touched(SessionId, Name/Arity)
1775
+ -> true
1776
+ ; assertz(session_edit_touched(SessionId, Name/Arity))
1777
+ ),
1778
+ Result = ok
1779
+ ),
1780
+ RetractError,
1781
+ format(atom(Result), 'error(~w)', [RetractError])
1782
+ ).
1783
+
1784
+ %% repl_delete_predicate(+SessionId, +Name, +Arity, -Result)
1785
+ %% Fully remove a predicate. Unlike retract (which only removes matching
1786
+ %% clauses and can leave an empty predicate behind), this abolishes it.
1787
+ repl_delete_predicate(SessionId, Name, Arity, Result) :-
1788
+ catch(
1789
+ ( guard_not_frozen(SessionId),
1790
+ atom(Name), integer(Arity), Arity >= 0,
1791
+ ( repl_protected_name(Name)
1792
+ -> throw(error(permission_error(modify, reserved_name, Name/Arity),
1793
+ context(repl_delete_predicate/4, 'cannot delete a reserved predicate')))
1794
+ ; true
1795
+ ),
1796
+ catch(abolish(SessionId:Name/Arity), _, true),
1797
+ ( session_user_tools(SessionId, Name)
1798
+ -> retractall(session_user_tools(SessionId, Name)),
1799
+ retractall(session_user_tool_schema(SessionId, Name, _)),
1800
+ retractall(SessionId:tool_source(Name, _))
1801
+ ; true
1802
+ ),
1803
+ ( session_edit_touched(SessionId, Name/Arity)
1804
+ -> true
1805
+ ; assertz(session_edit_touched(SessionId, Name/Arity))
1806
+ ),
1807
+ Result = ok
1808
+ ),
1809
+ DeleteError,
1810
+ format(atom(Result), 'error(~w)', [DeleteError])
1811
+ ).
1812
+
1813
+ %% repl_define(+SessionId, +HeadPatternText, +ClausesText, -Result)
1814
+ %% Replace every predicate the clause block defines. It parses the block,
1815
+ %% computes its indicators (plus the head pattern's), abolishes each, then
1816
+ %% asserts the block. This makes `define` safe no matter how many predicates a
1817
+ %% block spans, so a multi-predicate block cannot append duplicate clauses.
1818
+ repl_define(SessionId, HeadPatternText, ClausesText, Result) :-
1819
+ catch(
1820
+ ( guard_not_frozen(SessionId),
1821
+ repl_read_clause(SessionId, HeadPatternText, Pattern),
1822
+ repl_head_of(Pattern, HeadClause),
1823
+ repl_callable_indicator(HeadClause, HeadName, HeadArity),
1824
+ repl_parse_clauses(SessionId, ClausesText, Terms),
1825
+ findall(Name/Arity,
1826
+ ( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
1827
+ BlockIndicators0),
1828
+ sort([HeadName/HeadArity|BlockIndicators0], Indicators),
1829
+ forall(member(Name/Arity, Indicators),
1830
+ ( repl_protected_name(Name)
1831
+ -> throw(error(permission_error(modify, reserved_name, Name/Arity),
1832
+ context(repl_define/4, 'cannot define a reserved predicate')))
1833
+ ; catch(abolish(SessionId:Name/Arity), _, true),
1834
+ ( session_user_tools(SessionId, Name)
1835
+ -> retractall(session_user_tools(SessionId, Name)),
1836
+ retractall(session_user_tool_schema(SessionId, Name, _)),
1837
+ retractall(SessionId:tool_source(Name, _))
1838
+ ; true
1839
+ )
1840
+ )),
1841
+ repl_commit_terms(SessionId, Terms),
1842
+ repl_note_touched(SessionId, Terms),
1843
+ Result = ok
1844
+ ),
1845
+ DefineError,
1846
+ format(atom(Result), 'error(~w)', [DefineError])
1847
+ ).
1848
+
1849
+ %% repl_extract_code(+Text, -Source)
1850
+ %% Extracts a ```dml fenced block; falls back to the whole answer.
1851
+ repl_extract_code(Text, Source) :-
1852
+ atom_string(TextAtom, Text),
1853
+ ( sub_string(TextAtom, Start0, _, _, "```dml")
1854
+ -> Start is Start0 + 6,
1855
+ % After = 0 takes the remainder of the text (an unbound After would
1856
+ % bind Rest to the empty prefix first).
1857
+ sub_string(TextAtom, Start, _, 0, Rest),
1858
+ % Search for the closing fence by offset: binding Before=0 here would
1859
+ % require the fence to be at the very start of Rest.
1860
+ ( sub_string(Rest, FenceEnd, _, _, "```")
1861
+ -> sub_string(Rest, 0, FenceEnd, _, Source)
1862
+ ; Source = Rest
1863
+ )
1864
+ ; Source = TextAtom
1865
+ ).
1866
+
1867
+ %% repl_manifest(+SessionId, -ManifestText)
1868
+ %% Compact signatures + tools + entries + directives for the meta prompt.
1869
+ repl_manifest(SessionId, Manifest) :-
1870
+ findall(Name/Arity,
1871
+ ( current_predicate(SessionId:Name/Arity),
1872
+ repl_user_predicate(SessionId, Name, Arity)
1873
+ ),
1874
+ Preds0),
1875
+ sort(Preds0, Preds),
1876
+ findall(Name, session_user_tool_schema(SessionId, Name, _), Tools0),
1877
+ sort(Tools0, Tools),
1878
+ findall(Directive, session_directive(SessionId, Directive), Directives0),
1879
+ sort(Directives0, Directives),
1880
+ ( current_predicate(SessionId:module_doc/1),
1881
+ SessionId:module_doc(Summary0)
1882
+ -> Summary = Summary0
1883
+ ; Summary = ""
1884
+ ),
1885
+ ( current_predicate(SessionId:doc/2)
1886
+ -> findall(PI-DocText, SessionId:doc(PI, DocText), Docs0)
1887
+ ; Docs0 = []
1888
+ ),
1889
+ repl_history_summary(SessionId, HistorySummary),
1890
+ with_output_to(string(Manifest),
1891
+ ( ( Summary == ""
1892
+ -> format("- skill: (none; add module_doc/1 so the harness describes itself and can be routed)~n")
1893
+ ; format("- skill: ~w~n", [Summary])
1894
+ ),
1895
+ format("- predicates: ~w~n", [Preds]),
1896
+ format("- tools: ~w~n", [Tools]),
1897
+ format("- directives: ~w~n", [Directives]),
1898
+ ( Docs0 == []
1899
+ -> format("- docs: (none; add doc(Name/Arity, Text) facts)~n")
1900
+ ; forall(member(PI-DocText, Docs0), format("- doc(~w): ~w~n", [PI, DocText]))
1901
+ ),
1902
+ format("- history: ~w~n", [HistorySummary])
1903
+ )).
1904
+
1905
+ %% repl_meta_memory_text(+SessionId, -Text)
1906
+ repl_meta_memory_text(SessionId, Text) :-
1907
+ ( session_meta_memory(SessionId, Memory)
1908
+ -> true
1909
+ ; Memory = []
1910
+ ),
1911
+ last_n(Memory, 10, Recent),
1912
+ with_output_to(string(Text),
1913
+ forall(member(message{role: Role, content: Content}, Recent),
1914
+ format("~w: ~w~n", [Role, Content]))).
1915
+
1916
+ %% repl_memory_term(+SessionId, +Slot, -TermText)
1917
+ %% Serializes a live memory slot (harness|meta) as a Prolog term so a session
1918
+ %% can persist it and restore it on `--session` resume.
1919
+ repl_memory_term(SessionId, Slot, Text) :-
1920
+ ( Slot == meta
1921
+ -> ( session_meta_memory(SessionId, Memory)
1922
+ -> true
1923
+ ; Memory = []
1924
+ )
1925
+ ; ( session_repl_memory(SessionId, Memory)
1926
+ -> true
1927
+ ; Memory = []
1928
+ )
1929
+ ),
1930
+ with_output_to(string(Text),
1931
+ write_term(Memory, [quoted(true), max_depth(0)])).
1932
+
1933
+ last_n(List, N, Tail) :-
1934
+ length(List, Len),
1935
+ ( Len =< N
1936
+ -> Tail = List
1937
+ ; Skip is Len - N,
1938
+ length(Prefix, Skip),
1939
+ append(Prefix, Tail, List)
1940
+ ).
1941
+
1942
+ %% ============================================================
1943
+ %% Harness history (see docs/DML_REPL_SESSION_HISTORY.md)
1944
+ %% ============================================================
1945
+
1946
+ %% repl_history_load(+SessionId, +Text)
1947
+ %% Asserts harness_* history facts (the persisted .deepclause/repl/history.dml)
1948
+ %% into the session module so the harness can query prior sessions.
1949
+ repl_history_load(SessionId, Text) :-
1950
+ ( var(Text)
1951
+ -> true
1952
+ ; open_string(Text, Stream),
1953
+ repl_history_read_stream(Stream, SessionId),
1954
+ close(Stream)
1955
+ ).
1956
+
1957
+ repl_history_read_stream(Stream, SessionId) :-
1958
+ catch(read_term(Stream, Term, [module(SessionId)]), _, Term = end_of_file),
1959
+ ( Term == end_of_file
1960
+ -> true
1961
+ ; repl_history_assert_term(Term, SessionId),
1962
+ repl_history_read_stream(Stream, SessionId)
1963
+ ).
1964
+
1965
+ %% repl_history_record(+SessionId, +FactText)
1966
+ %% Asserts one or more history facts into the live module. The recorder persists
1967
+ %% the same facts to history.dml; here they become queryable.
1968
+ repl_history_record(SessionId, FactText) :-
1969
+ ( var(FactText)
1970
+ -> true
1971
+ ; ensure_full_stop(FactText, Text1),
1972
+ open_string(Text1, Stream),
1973
+ repl_history_read_stream(Stream, SessionId),
1974
+ close(Stream)
1975
+ ).
1976
+
1977
+ %% Only harness_* facts may be asserted through this path.
1978
+ repl_history_assert_term(Term, SessionId) :-
1979
+ ( nonvar(Term),
1980
+ Term \== end_of_file,
1981
+ Term \== none,
1982
+ callable(Term),
1983
+ functor(Term, Name, _),
1984
+ history_predicate(Name)
1985
+ -> % harness_head is a singleton: keep the latest (rollback moves it back).
1986
+ ( Name == harness_head
1987
+ -> retractall(SessionId:harness_head(_, _, _, _))
1988
+ ; true
1989
+ ),
1990
+ % Migrate pre-/5 harness_change facts (they carried an unused Entry arg).
1991
+ ( Term = harness_change(V, Ts, Kind, Sid, Turn, _Entry)
1992
+ -> assertz(SessionId:harness_change(V, Ts, Kind, Sid, Turn))
1993
+ ; assertz(SessionId:Term)
1994
+ )
1995
+ ; true
1996
+ ).
1997
+
1998
+ %% repl_rollback(+SessionId, +Source, -Result)
1999
+ %% Restores the living module from a history version's serialized source:
2000
+ %% abolishes the user predicates and reloads the source (directives, tools and
2001
+ %% entries come from the source too). Refused when frozen. Runtime conversation
2002
+ %% memory is preserved.
2003
+ repl_rollback(SessionId, Source, Result) :-
2004
+ catch(
2005
+ ( guard_not_frozen(SessionId),
2006
+ repl_abolish_user_predicates(SessionId),
2007
+ retractall(session_directive(SessionId, _)),
2008
+ retractall(session_user_tools(SessionId, _)),
2009
+ retractall(session_user_tool_schema(SessionId, _, _)),
2010
+ ( current_predicate(SessionId:tool_source/2)
2011
+ -> retractall(SessionId:tool_source(_, _))
2012
+ ; true
2013
+ ),
2014
+ ( Source == ""
2015
+ -> Result = ok
2016
+ ; load_code(SessionId, Source, LoadError),
2017
+ ( LoadError == none
2018
+ -> Result = ok
2019
+ ; format(atom(Result), 'error(~w)', [LoadError])
2020
+ )
2021
+ )
2022
+ ),
2023
+ RollbackError,
2024
+ format(atom(Result), 'error(~w)', [RollbackError])
2025
+ ).
2026
+
2027
+ %% repl_user_predicates(+SessionId, -Preds)
2028
+ %% Sorted Name/Arity list of the harness's own predicates.
2029
+ repl_user_predicates(SessionId, Preds) :-
2030
+ findall(Name/Arity,
2031
+ ( current_predicate(SessionId:Name/Arity),
2032
+ repl_user_predicate(SessionId, Name, Arity)
2033
+ ),
2034
+ Preds0),
2035
+ sort(Preds0, Preds).
2036
+
2037
+ %% repl_user_predicates_text(+SessionId, -Text)
2038
+ %% Rendered "name/arity, name/arity" for change diffing on the host side.
2039
+ repl_user_predicates_text(SessionId, Text) :-
2040
+ repl_user_predicates(SessionId, Preds),
2041
+ with_output_to(string(Text), print_pred_list(Preds)).
2042
+
2043
+ %% repl_predicate_arities(+SessionId, +Name, -Arities)
2044
+ %% Sorted arities of a user predicate, e.g. agent_main -> [0,1,2]. Used by
2045
+ %% `deepclause run` to pick the agent_main/N to call for the given arguments.
2046
+ repl_predicate_arities(SessionId, Name, Arities) :-
2047
+ atom(Name),
2048
+ findall(Arity,
2049
+ ( current_predicate(SessionId:Name/Arity),
2050
+ repl_user_predicate(SessionId, Name, Arity)
2051
+ ),
2052
+ Arities0),
2053
+ sort(Arities0, Arities).
2054
+
2055
+ %% repl_history_listing(+SessionId)
2056
+ %% Human-readable tail for `listing(history)`.
2057
+ repl_history_listing(SessionId) :-
2058
+ ( current_predicate(SessionId:harness_head/4),
2059
+ SessionId:harness_head(Version, Timestamp, HeadSession, _Turn)
2060
+ -> format("harness_head(~w, ~w, ~w)~n", [Version, Timestamp, HeadSession])
2061
+ ; format("harness_head(none)~n")
2062
+ ),
2063
+ ( current_predicate(SessionId:harness_session/5)
2064
+ -> forall(SessionId:harness_session(Sid, Started, Model, Backend, Dir),
2065
+ format("harness_session(~w, ~w, ~w, ~w, ~w)~n", [Sid, Started, Model, Backend, Dir]))
2066
+ ; true
2067
+ ),
2068
+ ( current_predicate(SessionId:harness_change/5)
2069
+ -> forall(SessionId:harness_change(V, Ts, Kind, Sid, Turn),
2070
+ format("harness_change(~w, ~w, ~w, ~w, ~w)~n", [V, Ts, Kind, Sid, Turn]))
2071
+ ; true
2072
+ ).
2073
+
2074
+ %% repl_history_summary(+SessionId, -Text)
2075
+ %% One-line history for the meta prompt manifest.
2076
+ repl_history_summary(SessionId, Text) :-
2077
+ ( current_predicate(SessionId:harness_head/4),
2078
+ SessionId:harness_head(Version, Timestamp, HeadSession, _Turn)
2079
+ -> true
2080
+ ; Version = none, Timestamp = none, HeadSession = none
2081
+ ),
2082
+ ( current_predicate(SessionId:harness_session/5)
2083
+ -> findall(s, SessionId:harness_session(_, _, _, _, _), Sessions0),
2084
+ length(Sessions0, Sessions)
2085
+ ; Sessions = 0
2086
+ ),
2087
+ ( current_predicate(SessionId:harness_change/5)
2088
+ -> findall(c, SessionId:harness_change(_, _, _, _, _), Changes0),
2089
+ length(Changes0, Changes)
2090
+ ; Changes = 0
2091
+ ),
2092
+ format(atom(Text), 'version ~w (updated ~w by ~w), ~w session(s), ~w change(s)',
2093
+ [Version, Timestamp, HeadSession, Sessions, Changes]).
2094
+
2095
+ %% repl_meta_prompt(+SessionId, +Request, -PromptText)
2096
+ %% Assembles the deterministic code-writing prompt: authoring contract, host
2097
+ %% tool catalog, harness manifest, recent meta memory, the request, and the
2098
+ %% output contract.
2099
+ repl_meta_prompt(SessionId, Request, Prompt) :-
2100
+ ( session_meta_catalog(SessionId, Catalog) -> true ; Catalog = "" ),
2101
+ ( session_dml_reference(SessionId, Reference) -> true ; Reference = "" ),
2102
+ ( session_authoring_examples(SessionId, Authoring) -> true ; Authoring = "" ),
2103
+ ( session_harness_examples(SessionId, Examples) -> true ; Examples = "" ),
2104
+ ( session_prolog_snippets(SessionId, Snippets) -> true ; Snippets = "" ),
2105
+ repl_manifest(SessionId, Manifest),
2106
+ repl_meta_memory_text(SessionId, MemoryText),
2107
+ % The template is the bundled META_PROMPT.md, injected by the host via
2108
+ % repl_set_meta_template/2. The fallback keeps the prompt usable when no
2109
+ % template was set (e.g. a direct repl_meta_prompt/3 call).
2110
+ ( session_meta_template(SessionId, Template)
2111
+ -> true
2112
+ ; Template = "You are the DeepClause REPL harness author.~n~nHOST TOOL CATALOG~n{{CATALOG}}~n~nUSER REQUEST~n{{REQUEST}}~n"
2113
+ ),
2114
+ replace_all(Template, "{{CATALOG}}", Catalog, P1),
2115
+ replace_all(P1, "{{REFERENCE}}", Reference, P2),
2116
+ replace_all(P2, "{{AUTHORING}}", Authoring, P3),
2117
+ replace_all(P3, "{{EXAMPLES}}", Examples, P4),
2118
+ replace_all(P4, "{{SNIPPETS}}", Snippets, P5),
2119
+ replace_all(P5, "{{MANIFEST}}", Manifest, P6),
2120
+ replace_all(P6, "{{MEMORY}}", MemoryText, P7),
2121
+ replace_all(P7, "{{REQUEST}}", Request, Prompt).
2122
+
2123
+ %% replace_all(+Text, +From, +To, -Out)
2124
+ %% Literal, all-occurrences substring replacement. Fills the meta-prompt
2125
+ %% template's {{PLACEHOLDER}} sections.
2126
+ replace_all(Text, From, To, Out) :-
2127
+ string_codes(Text, TextCodes),
2128
+ string_codes(From, FromCodes),
2129
+ string_codes(To, ToCodes),
2130
+ replace_all_codes(TextCodes, FromCodes, ToCodes, OutCodes),
2131
+ string_codes(Out, OutCodes).
2132
+
2133
+ replace_all_codes([], _, _, []) :- !.
2134
+ replace_all_codes(Codes, From, To, Out) :-
2135
+ From \== [],
2136
+ ( append(From, Rest, Codes)
2137
+ -> append(To, Tail, Out),
2138
+ replace_all_codes(Rest, From, To, Tail)
2139
+ ; Codes = [C|Cs],
2140
+ Out = [C|Tail],
2141
+ replace_all_codes(Cs, From, To, Tail)
2142
+ ).
2143
+
2144
+ repl_meta_prompt(Request, Prompt) :-
2145
+ get_session_id(SessionId),
2146
+ repl_meta_prompt(SessionId, Request, Prompt).
2147
+
2148
+ %% repl_run_meta(+SessionId, +PromptText, -FrameId, -Error)
2149
+ %% Creates the meta frame running the code-writing task.
2150
+ repl_run_meta(SessionId, PromptText, FrameId, Error) :-
2151
+ catch(
2152
+ ( repl_initial_state(SessionId, meta, State),
2153
+ create_frame_raw(SessionId, meta, 0,
2154
+ deepclause_mi:repl_meta_solve(PromptText, SessionId, State),
2155
+ FrameId),
2156
+ Error = none
2157
+ ),
2158
+ CreateError,
2159
+ format(atom(Error), '~w', [CreateError])
2160
+ ).
2161
+
2162
+ repl_meta_solve(PromptText, SessionId, State) :-
2163
+ nb_setval(current_session_id, SessionId),
2164
+ catch(
2165
+ ( mi_call(
2166
+ task_named(PromptText, [DMLCode, Answer],
2167
+ [typed_var{name:'DMLCode', type:string},
2168
+ typed_var{name:'Answer', type:string}]),
2169
+ State, _StateOut),
2170
+ % The task's own memory (incl. the assembled prompt and tool results)
2171
+ % is ephemeral. The host records a clean user/assistant pair with
2172
+ % repl_record_meta/3 after the turn; that becomes the next turn's
2173
+ % conversation history (a normal messages array).
2174
+ engine_yield(meta_result(DMLCode, Answer))
2175
+ ),
2176
+ MetaError,
2177
+ ( format(atom(Msg), '~w', [MetaError]),
2178
+ engine_yield(error(Msg))
2179
+ )
2180
+ ).
2181
+
2182
+ %% repl_record_meta(+SessionId, +Request, +Summary)
2183
+ %% Appends a short outcome note to meta memory (bounded).
2184
+ repl_record_meta(SessionId, Request, Summary) :-
2185
+ ( session_meta_memory(SessionId, Memory)
2186
+ -> true
2187
+ ; Memory = []
2188
+ ),
2189
+ append(Memory,
2190
+ [message{role: user, content: Request},
2191
+ message{role: assistant, content: Summary}],
2192
+ Combined),
2193
+ % Keep every system message (compaction checkpoints) and the last 20
2194
+ % conversational messages; naive last_n/3 would evict old checkpoints.
2195
+ findall(M, (member(M, Combined), repl_system_message(M)), SystemMessages),
2196
+ findall(M, (member(M, Combined), \+ repl_system_message(M)), Conversational),
2197
+ last_n(Conversational, 20, Recent),
2198
+ append(SystemMessages, Recent, Bounded),
2199
+ retractall(session_meta_memory(SessionId, _)),
2200
+ assertz(session_meta_memory(SessionId, Bounded)).
2201
+
2202
+ %% repl_system_message(+Message)
2203
+ repl_system_message(message{role: system, content: _}).
2204
+
2205
+ %% repl_get_memory(+SessionId, +Slot, -Messages)
2206
+ %% Read a live memory slot as the message list stored in the session.
2207
+ repl_get_memory(SessionId, Slot, Messages) :-
2208
+ ( Slot == meta
2209
+ -> ( session_meta_memory(SessionId, Memory)
2210
+ -> true
2211
+ ; Memory = []
2212
+ )
2213
+ ; ( session_repl_memory(SessionId, Memory)
2214
+ -> true
2215
+ ; Memory = []
2216
+ )
2217
+ ),
2218
+ Messages = Memory.
2219
+
2220
+ %% repl_set_memory(+SessionId, +Slot, +Messages)
2221
+ %% Replace a live memory slot (used by host-side TypeScript compaction).
2222
+ repl_set_memory(SessionId, Slot, Messages) :-
2223
+ ( Slot == meta
2224
+ -> retractall(session_meta_memory(SessionId, _)),
2225
+ assertz(session_meta_memory(SessionId, Messages))
2226
+ ; retractall(session_repl_memory(SessionId, _)),
2227
+ assertz(session_repl_memory(SessionId, Messages))
407
2228
  ).
408
2229
 
409
- module_reference_string(Module, ModuleStr) :-
410
- atom(Module),
411
- !,
412
- atom_string(Module, ModuleStr).
413
- module_reference_string(Module, Module) :-
414
- string(Module).
2230
+ %% ---- Edit transactions ----
2231
+
2232
+ %% repl_snapshot_save(+SessionId)
2233
+ %% Snapshots the living module, tool bookkeeping, directives, and harness
2234
+ %% memory so a failed prompt leaves the harness untouched.
2235
+ repl_snapshot_save(SessionId) :-
2236
+ retractall(repl_snapshot(SessionId, _)),
2237
+ findall(Clause,
2238
+ ( current_predicate(SessionId:Name/Arity),
2239
+ repl_user_predicate(SessionId, Name, Arity),
2240
+ functor(Head, Name, Arity),
2241
+ clause(SessionId:Head, Body),
2242
+ ( Body == true
2243
+ -> Clause = Head
2244
+ ; Clause = (Head :- Body)
2245
+ )
2246
+ ),
2247
+ Clauses),
2248
+ findall(Name, session_user_tools(SessionId, Name), Tools),
2249
+ findall(Name-Schema, session_user_tool_schema(SessionId, Name, Schema), Schemas),
2250
+ ( current_predicate(SessionId:tool_source/2)
2251
+ -> findall(Name-Source, SessionId:tool_source(Name, Source), ToolSources)
2252
+ ; ToolSources = []
2253
+ ),
2254
+ findall(Directive, session_directive(SessionId, Directive), Directives),
2255
+ ( session_repl_memory(SessionId, Memory) -> true ; Memory = [] ),
2256
+ assertz(repl_snapshot(SessionId, snapshot{
2257
+ clauses: Clauses,
2258
+ tools: Tools,
2259
+ schemas: Schemas,
2260
+ tool_sources: ToolSources,
2261
+ directives: Directives,
2262
+ memory: Memory
2263
+ })).
2264
+
2265
+ %% repl_snapshot_restore(+SessionId)
2266
+ repl_snapshot_restore(SessionId) :-
2267
+ ( repl_snapshot(SessionId, Snapshot)
2268
+ -> retract(repl_snapshot(SessionId, Snapshot)),
2269
+ findall(Name/Arity,
2270
+ ( current_predicate(SessionId:Name/Arity),
2271
+ repl_user_predicate(SessionId, Name, Arity)
2272
+ ),
2273
+ Preds),
2274
+ forall(member(AbName/AbArity, Preds),
2275
+ catch(abolish(SessionId:AbName/AbArity), _, true)),
2276
+ forall(member(Clause, Snapshot.clauses), assertz(SessionId:Clause)),
2277
+ retractall(session_user_tools(SessionId, _)),
2278
+ forall(member(Tool, Snapshot.tools), assertz(session_user_tools(SessionId, Tool))),
2279
+ retractall(session_user_tool_schema(SessionId, _, _)),
2280
+ forall(member(SchemaName-Schema, Snapshot.schemas),
2281
+ assertz(session_user_tool_schema(SessionId, SchemaName, Schema))),
2282
+ ( current_predicate(SessionId:tool_source/2)
2283
+ -> retractall(SessionId:tool_source(_, _))
2284
+ ; true
2285
+ ),
2286
+ forall(member(SourceName-Source, Snapshot.tool_sources),
2287
+ assertz(SessionId:tool_source(SourceName, Source))),
2288
+ retractall(session_directive(SessionId, _)),
2289
+ forall(member(Directive, Snapshot.directives),
2290
+ assertz(session_directive(SessionId, Directive))),
2291
+ retractall(session_repl_memory(SessionId, _)),
2292
+ assertz(session_repl_memory(SessionId, Snapshot.memory))
2293
+ ; true
2294
+ ).
415
2295
 
416
- workspace_module_reference(Module) :-
417
- module_reference_string(Module, ModuleStr),
418
- ( sub_string(ModuleStr, _, _, _, "/")
419
- ; sub_string(ModuleStr, _, _, 0, ".dml")
420
- ; sub_string(ModuleStr, _, _, 0, ".pl")
421
- ; sub_string(ModuleStr, 0, 1, _, ".")
2296
+ %% repl_parse_source(+SessionId, +Source, -Preds, -Error)
2297
+ %% Validates generated DML and collects the predicates it defines.
2298
+ repl_parse_source(SessionId, Source, Preds, Error) :-
2299
+ catch(
2300
+ ( open_string(Source, Stream),
2301
+ repl_collect_source_predicates(Stream, SessionId, Preds0),
2302
+ close(Stream),
2303
+ ( member(Name0/_, Preds0),
2304
+ repl_protected_name(Name0)
2305
+ -> format(atom(Error), 'generated code defines reserved predicate ~w', [Name0])
2306
+ ; sort(Preds0, Preds),
2307
+ Error = none
2308
+ )
2309
+ ),
2310
+ ParseError,
2311
+ format(atom(Error), '~w', [ParseError])
422
2312
  ).
423
2313
 
424
- process_use_module_directive(Module, SessionId) :-
425
- ( workspace_module_reference(Module)
426
- -> process_directive(consult(Module), SessionId)
427
- ; SessionId:use_module(Module)
2314
+ %% repl_collect_source_predicates(+Stream, +SessionId, -Preds)
2315
+ %% Reads with the session's operator table and executes `:- use_module(...)`
2316
+ %% directives as it goes, exactly like the load path. This is what lets library
2317
+ %% operators (CLP(FD)'s #=/ins, etc.) be used in committed source: without it,
2318
+ %% validation parsed the source with an empty operator table and rejected code
2319
+ %% that loads fine.
2320
+ repl_collect_source_predicates(Stream, SessionId, Preds) :-
2321
+ read_term(Stream, Term, [module(SessionId)]),
2322
+ ( Term == end_of_file
2323
+ -> Preds = []
2324
+ ; ( Term = (:- Directive)
2325
+ -> catch(process_directive(Directive, SessionId), _, true),
2326
+ Preds = Rest
2327
+ ; Term = repl_delete(_)
2328
+ -> Preds = Rest
2329
+ ; Term = (Head :- _)
2330
+ -> functor(Head, Name, Arity),
2331
+ Preds = [Name/Arity|Rest]
2332
+ ; functor(Term, Name, Arity),
2333
+ Preds = [Name/Arity|Rest]
2334
+ ),
2335
+ repl_collect_source_predicates(Stream, SessionId, Rest)
428
2336
  ).
429
2337
 
430
- process_use_module_directive(Module, Exports, SessionId) :-
431
- ( workspace_module_reference(Module)
432
- -> process_directive(consult(Module), SessionId)
433
- ; SessionId:use_module(Module, Exports)
2338
+ %% repl_commit(+SessionId, +Source, -Result)
2339
+ %% Transactional commit: validate, replace-by-predicate, then load the returned
2340
+ %% source. Result = ok | 'error(Message)'. Never throws; failures are reported
2341
+ %% through Result so the caller can decide whether to repair.
2342
+ repl_commit(SessionId, Source, Result) :-
2343
+ catch(
2344
+ repl_commit_unsafe(SessionId, Source, Result),
2345
+ CommitError,
2346
+ format(atom(Result), 'error(~w)', [CommitError])
434
2347
  ).
435
2348
 
436
- %% process_directive(+Directive, +SessionId)
437
- process_directive(param(Key, Desc), SessionId) :-
438
- !,
439
- assertz(SessionId:param_decl(Key, Desc)).
440
- process_directive(param(Key, Desc, Default), SessionId) :-
441
- !,
442
- assertz(SessionId:param_decl(Key, Desc, Default)).
2349
+ repl_commit_unsafe(SessionId, Source, Result) :-
2350
+ guard_not_frozen(SessionId),
2351
+ repl_parse_source(SessionId, Source, Preds, ParseError),
2352
+ repl_commit_from_parse(SessionId, Source, Preds, ParseError, Result).
443
2353
 
444
- %% Handle consult directive: :- consult(File)
445
- process_directive(consult(File), SessionId) :-
446
- !,
447
- % Set session ID for get_session_id to find, then call our custom consult
448
- nb_setval(current_session_id, SessionId),
449
- resolve_workspace_path(File, FullPath),
450
- readutil:read_file_to_string(FullPath, Code, []),
451
- consult_string(Code, SessionId).
452
- %% Handle use_module directive
453
- process_directive(use_module(Module), SessionId) :-
2354
+ repl_commit_from_parse(_SessionId, _Source, _Preds, ParseError, Result) :-
2355
+ ParseError \== none,
454
2356
  !,
455
- process_use_module_directive(Module, SessionId).
456
- process_directive(use_module(Module, Exports), SessionId) :-
2357
+ commit_error(ParseError, Result).
2358
+ repl_commit_from_parse(SessionId, Source, Preds, none, Result) :-
2359
+ forall(member(Name/Arity, Preds),
2360
+ catch(abolish(SessionId:Name/Arity), _, true)),
2361
+ load_code(SessionId, Source, LoadError),
2362
+ ( LoadError == none
2363
+ -> retractall(session_last_commit(SessionId, _)),
2364
+ assertz(session_last_commit(SessionId, Preds))
2365
+ ; true
2366
+ ),
2367
+ repl_commit_from_load(LoadError, Result).
2368
+
2369
+ %% repl_last_commit_summary(+SessionId, -Text)
2370
+ %% Rendered "name/arity, name/arity" of the predicates the last commit replaced.
2371
+ repl_last_commit_summary(SessionId, Text) :-
2372
+ ( session_last_commit(SessionId, Preds), Preds \= []
2373
+ -> with_output_to(string(Text), print_pred_list(Preds))
2374
+ ; Text = ""
2375
+ ).
2376
+
2377
+ print_pred_list([]).
2378
+ print_pred_list([Name/Arity]) :-
457
2379
  !,
458
- process_use_module_directive(Module, Exports, SessionId).
459
- %% Handle list-style consult: :- [File] or :- [File1, File2, ...]
460
- process_directive([File|Files], SessionId) :-
2380
+ format("~w/~w", [Name, Arity]).
2381
+ print_pred_list([Name/Arity|Rest]) :-
2382
+ format("~w/~w, ", [Name, Arity]),
2383
+ print_pred_list(Rest).
2384
+
2385
+ repl_commit_from_load(LoadError, Result) :-
2386
+ LoadError \== none,
461
2387
  !,
462
- nb_setval(current_session_id, SessionId),
463
- process_directive(consult(File), SessionId),
464
- (Files == [] -> true ; process_directive(Files, SessionId)).
465
- process_directive(_, _).
2388
+ commit_error(LoadError, Result).
2389
+ repl_commit_from_load(none, ok).
466
2390
 
467
- %% ============================================================
468
- %% Engine Management
469
- %% ============================================================
2391
+ commit_error(Message, Result) :-
2392
+ format(atom(Result), 'error(~w)', [Message]).
470
2393
 
471
- %% create_engine(+SessionId, +MemoryId, +Args, +Params, +InitialMemory, -Engine)
472
- %% Args is a list of positional arguments for agent_main
473
- %% Params is a dict of named parameters (already asserted as param/2 facts)
474
- create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
475
- assertz(session_params(SessionId, Params)),
476
- determine_agent_goal(SessionId, Args, Goal),
477
- % Check if tracing is enabled and store in session state
478
- (get_dict(trace, Params, true) -> TraceEnabled = true ; TraceEnabled = false),
479
- assertz(session_trace_enabled(SessionId, TraceEnabled)),
480
- assertz(session_trace_log(SessionId, [])),
481
- % Check for gas limit in params
482
- (get_dict(gas_limit, Params, GasLimit) -> true ; GasLimit = -1),
483
- % Create initial state with provided memory, params, context stack, trace depth and gas
484
- InitialState = state{memory: InitialMemory, params: Params, context_stack: [], depth: 0, gas_remaining: GasLimit},
485
- % Create the engine - pass SessionId and initial state to mi/3
486
- engine_create(_,
487
- deepclause_mi:mi(Goal, InitialState, SessionId),
488
- Engine),
489
- assertz(session_engine(SessionId, Engine)).
2394
+ %% repl_execute_frame(+SessionId, +GoalText, -FrameId, -Error)
2395
+ %% Creates a child frame for the meta `execute` tool.
2396
+ repl_execute_frame(SessionId, GoalText, FrameId, Error) :-
2397
+ catch(
2398
+ ( read_repl_goal(SessionId, GoalText, Goal, Names),
2399
+ ( repl_reserved(Goal)
2400
+ -> throw(error(permission_error(execute, reserved_predicate, Goal),
2401
+ context(repl_execute_frame/4, 'cannot execute a reserved predicate')))
2402
+ ; true
2403
+ ),
2404
+ repl_initial_state(SessionId, nested, State),
2405
+ create_frame_raw(SessionId, nested, 1,
2406
+ deepclause_mi:repl_solve(Goal, Names, SessionId, State),
2407
+ FrameId),
2408
+ Error = none
2409
+ ),
2410
+ ExecuteError,
2411
+ format(atom(Error), '~w', [ExecuteError])
2412
+ ).
490
2413
 
491
2414
  %% determine_agent_goal(+SessionId, +Args, -Goal)
492
2415
  %% Args is a list of positional arguments - passed directly to agent_main
@@ -527,31 +2450,12 @@ determine_agent_goal(SessionId, Args, Goal) :-
527
2450
  determine_agent_goal(SessionId, _Args, SessionId:agent_main).
528
2451
 
529
2452
  %% step_engine(+SessionId, -Status, -Content, -Payload)
2453
+ %% Legacy single-engine entry point: resolves the session's default frame and
2454
+ %% delegates to step_frame/4. Kept as a thin wrapper so existing callers are
2455
+ %% unaffected by the frame refactor.
530
2456
  step_engine(SessionId, Status, Content, Payload) :-
531
- ( session_engine(SessionId, Engine)
532
- -> catch(
533
- ( engine_next(Engine, Result)
534
- -> process_engine_result(Result, Status, Content, Payload)
535
- ; % Engine finished - return trace if enabled
536
- Status = finished,
537
- Content = '',
538
- ( session_trace_enabled(SessionId, true),
539
- session_trace_log(SessionId, TraceLog)
540
- -> Payload = payload{trace: TraceLog}
541
- ; Payload = none
542
- )
543
- ),
544
- Error,
545
- ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
546
- Status = error,
547
- Content = ErrMsg,
548
- ( session_trace_enabled(SessionId, true),
549
- session_trace_log(SessionId, TraceLog)
550
- -> Payload = payload{trace: TraceLog}
551
- ; Payload = none
552
- )
553
- )
554
- )
2457
+ ( session_default_frame(SessionId, FrameId)
2458
+ -> step_frame(FrameId, Status, Content, Payload)
555
2459
  ; Status = error,
556
2460
  Content = 'No engine found for session',
557
2461
  Payload = none
@@ -562,7 +2466,7 @@ process_engine_result(output(Text), output, Text, none) :- !.
562
2466
  process_engine_result(log(Text), log, Text, none) :- !.
563
2467
  process_engine_result(answer(Text), answer, Text, none) :- !.
564
2468
  %% Note: Memory and tool scope are now passed in the payload for the agent loop
565
- process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
2469
+ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
566
2470
  payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext}) :- !.
567
2471
  %% Legacy 5-arg format (no reasoning/recipe)
568
2472
  process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope), request_agent_loop, '',
@@ -576,9 +2480,17 @@ process_engine_result(request_sample_token(Prompt, Allowed), request_sample_toke
576
2480
  payload{prompt: Prompt, allowedTokens: Allowed}) :- !.
577
2481
  process_engine_result(request_llm(Messages), request_llm, '',
578
2482
  payload{messages: Messages}) :- !.
2483
+ process_engine_result(request_judgment(Payload), request_judgment, '', Payload) :- !.
579
2484
  %% Tool result from inline tool execution
580
2485
  process_engine_result(tool_result(Result), tool_result, '',
581
2486
  payload{result: Result}) :- !.
2487
+ %% Goal-mode solution: bindings is a list of Name-RenderedText pairs.
2488
+ process_engine_result(solution(Bindings, Deterministic), solution, '',
2489
+ payload{bindings: Bindings, deterministic: Deterministic}) :- !.
2490
+ %% Prompt-mode meta task result: generated DML source and an optional direct
2491
+ %% answer for simple questions.
2492
+ process_engine_result(meta_result(Code, Answer), meta_result, '',
2493
+ payload{code: Code, answer: Answer}) :- !.
582
2494
  process_engine_result(wait_input(Prompt), wait_input, Prompt, none) :- !.
583
2495
  process_engine_result(error(Msg), error, Msg, none) :- !.
584
2496
  process_engine_result(Other, error, Msg, none) :-
@@ -586,19 +2498,28 @@ process_engine_result(Other, error, Msg, none) :-
586
2498
 
587
2499
  %% destroy_engine(+SessionId)
588
2500
  destroy_engine(SessionId) :-
2501
+ % Destroy every frame belonging to this session (the default query frame and
2502
+ % any nested frames still alive). end_frame/1 also clears the default pointer.
2503
+ ( findall(FrameId, session_frame(FrameId, SessionId, _, _), FrameIds)
2504
+ -> forall(member(FrameId, FrameIds), end_frame(FrameId))
2505
+ ; true
2506
+ ),
589
2507
  ( session_engine(SessionId, Engine)
590
2508
  -> catch(engine_destroy(Engine), _, true)
591
2509
  ; true
592
2510
  ),
2511
+ retractall(session_default_frame(SessionId, _)),
593
2512
  retractall(session_engine(SessionId, _)),
594
2513
  retractall(session_params(SessionId, _)),
595
2514
  retractall(session_user_tools(SessionId, _)),
596
2515
  retractall(session_user_tool_schema(SessionId, _, _)),
597
2516
  retractall(session_pending_input(SessionId, _)),
2517
+ retractall(session_input_cancelled(SessionId)),
598
2518
  retractall(session_agent_result(SessionId, _)),
599
2519
  retractall(session_exec_result(SessionId, _)),
600
2520
  retractall(session_sample_token_result(SessionId, _)),
601
2521
  retractall(session_llm_result(SessionId, _)),
2522
+ retractall(session_judgment_result(SessionId, _)),
602
2523
  retractall(session_trace_log(SessionId, _)),
603
2524
  retractall(session_trace_enabled(SessionId, _)),
604
2525
  ( current_module(SessionId)
@@ -645,6 +2566,17 @@ post_sample_token_result(SessionId, Status, TokenOrError) :-
645
2566
  assertz(session_sample_token_result(SessionId, result{status: Status, token: TokenOrError})),
646
2567
  post_signal_to_engine(SessionId, sample_token_done).
647
2568
 
2569
+ %% post_judgment_result(+SessionId, +Status, +ValuesOrError)
2570
+ %% On success Values is the list of answer values in question order.
2571
+ post_judgment_result(SessionId, success, Values) :-
2572
+ retractall(session_judgment_result(SessionId, _)),
2573
+ assertz(session_judgment_result(SessionId, result{status: success, values: Values})),
2574
+ post_signal_to_engine(SessionId, judgment_done).
2575
+ post_judgment_result(SessionId, failure, Error) :-
2576
+ retractall(session_judgment_result(SessionId, _)),
2577
+ assertz(session_judgment_result(SessionId, result{status: failure, error: Error})),
2578
+ post_signal_to_engine(SessionId, judgment_done).
2579
+
648
2580
  %% post_llm_result(+SessionId, +Status, +TextOrError)
649
2581
  post_llm_result(SessionId, Status, TextOrError) :-
650
2582
  retractall(session_llm_result(SessionId, _)),
@@ -656,6 +2588,13 @@ provide_input(SessionId, Input) :-
656
2588
  assertz(session_pending_input(SessionId, Input)),
657
2589
  post_signal_to_engine(SessionId, input_provided).
658
2590
 
2591
+ %% provide_input_cancelled(+SessionId)
2592
+ %% Non-interactive runs have no human. Signal the waiting input/2 to fail
2593
+ %% instead of blocking.
2594
+ provide_input_cancelled(SessionId) :-
2595
+ assertz(session_input_cancelled(SessionId)),
2596
+ post_signal_to_engine(SessionId, input_provided).
2597
+
659
2598
  %% post_signal_to_engine(+SessionId, +Signal)
660
2599
  %% Posts a signal via dynamic predicate (most reliable for WASM engines)
661
2600
  %% Falls back to engine_post only if dynamic assert fails
@@ -1042,6 +2981,7 @@ process_tool_engine_result(request_sample_token(Prompt, Allowed), request_sample
1042
2981
  payload{prompt: Prompt, allowedTokens: Allowed}) :- !.
1043
2982
  process_tool_engine_result(request_llm(Messages), request_llm, '',
1044
2983
  payload{messages: Messages}) :- !.
2984
+ process_tool_engine_result(request_judgment(Payload), request_judgment, '', Payload) :- !.
1045
2985
  process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
1046
2986
  payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext}) :- !.
1047
2987
  %% Legacy 5-arg format (no reasoning/recipe)
@@ -1313,6 +3253,175 @@ wait_sample_token_signal(SessionId) :-
1313
3253
  ; wait_sample_token_signal(SessionId)
1314
3254
  ).
1315
3255
 
3256
+ %% ============================================================
3257
+ %% Semantic Judgment Predicates
3258
+ %% ============================================================
3259
+ %% judge/2 asks one batch of typed questions about an explicit state and binds
3260
+ %% the answers into output variables carried by the spec terms. The one-off
3261
+ %% predicates are sugar over the same request.
3262
+ %%
3263
+ %% judge(Message, [
3264
+ %% choose("Which team?", [billing, orders, account]) - Team,
3265
+ %% rate("How frustrated?", [calm, frustrated, angry]) - Frustration,
3266
+ %% verify("Does it ask for a refund?") - Refund
3267
+ %% ]).
3268
+ %%
3269
+ %% Backends (llm, jev, mock) are selected with with_judgment/2, the workspace
3270
+ %% configuration, or a per-run override. require_judgment/2 gates a goal on a
3271
+ %% backend capability such as calibrated.
3272
+
3273
+ mi_call(judge(State, Specs), StateIn, StateOut) :-
3274
+ !,
3275
+ mi_call_judgment(State, Specs, StateIn, StateOut).
3276
+
3277
+ mi_call(choose(State, Question, Options, Choice), StateIn, StateOut) :-
3278
+ !,
3279
+ mi_call_judgment(State, [choose(Question, Options) - Choice], StateIn, StateOut).
3280
+
3281
+ mi_call(rate(State, Question, Levels, Level), StateIn, StateOut) :-
3282
+ !,
3283
+ mi_call_judgment(State, [rate(Question, Levels) - Level], StateIn, StateOut).
3284
+
3285
+ mi_call(verify(State, Question, Truth), StateIn, StateOut) :-
3286
+ !,
3287
+ mi_call_judgment(State, [verify(Question) - Truth], StateIn, StateOut).
3288
+
3289
+ mi_call(probability(State, Question, Probability), StateIn, StateOut) :-
3290
+ !,
3291
+ mi_call_judgment(State, [probability(Question) - Probability], StateIn, StateOut).
3292
+
3293
+ mi_call(holds(State, Question), StateIn, StateOut) :-
3294
+ !,
3295
+ mi_call_judgment(State, [verify(Question) - Truth], StateIn, StateOut),
3296
+ ( Truth == yes
3297
+ -> true
3298
+ ; fail
3299
+ ).
3300
+
3301
+ mi_call(holds(State, Question, Threshold), StateIn, StateOut) :-
3302
+ !,
3303
+ mi_call_judgment(State, [probability(Question) - Probability], StateIn, StateOut),
3304
+ ( number(Probability), Probability >= Threshold
3305
+ -> true
3306
+ ; fail
3307
+ ).
3308
+
3309
+ mi_call(with_judgment(Backend, Goal), StateIn, StateOut) :-
3310
+ !,
3311
+ set_judge_backend(StateIn, Backend, ScopedState),
3312
+ mi_call(Goal, ScopedState, StateWithScope),
3313
+ clear_judge_backend(StateWithScope, StateOut).
3314
+
3315
+ mi_call(require_judgment(Capability, Goal), StateIn, StateOut) :-
3316
+ !,
3317
+ push_required_judgment(StateIn, Capability, ScopedState),
3318
+ mi_call(Goal, ScopedState, StateWithScope),
3319
+ pop_required_judgment(StateWithScope, StateOut).
3320
+
3321
+ %% mi_call_judgment(+State, +Specs, +StateIn, -StateOut)
3322
+ mi_call_judgment(State, Specs, StateIn, StateOut) :-
3323
+ consume_gas(StateIn, State1),
3324
+ get_session_id(SessionId),
3325
+ get_depth(State1, Depth),
3326
+ split_specs(Specs, Questions, Outputs),
3327
+ length(Questions, Count),
3328
+ add_trace_entry(SessionId, llm_call, judgment, [Count], Depth),
3329
+ get_judge_backend(State1, Backend),
3330
+ get_required_judgments(State1, Requires),
3331
+ engine_yield(request_judgment(payload{
3332
+ state: State,
3333
+ questions: Questions,
3334
+ backend: Backend,
3335
+ requires: Requires
3336
+ })),
3337
+ wait_judgment_signal(SessionId),
3338
+ ( session_judgment_result(SessionId, Result)
3339
+ -> retract(session_judgment_result(SessionId, _)),
3340
+ ( Result.status == success
3341
+ -> add_trace_with_result(SessionId, exit, judgment, [Count], Result.values, Depth),
3342
+ bind_judgment_outputs(Outputs, Result.values),
3343
+ StateOut = State1
3344
+ ; ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
3345
+ -> format(atom(WarnMsg), 'Warning: judgment failed: ~w', [ErrorMsg])
3346
+ ; format(atom(WarnMsg), 'Warning: judgment failed (status=~w)', [Result.status])
3347
+ ),
3348
+ add_trace_entry(SessionId, fail, judgment, [Count], Depth),
3349
+ engine_yield(output(WarnMsg)),
3350
+ fail
3351
+ )
3352
+ ; add_trace_entry(SessionId, fail, judgment, [Count], Depth),
3353
+ engine_yield(output('Warning: judgment failed (no result returned)')),
3354
+ fail
3355
+ ).
3356
+
3357
+ wait_judgment_signal(SessionId) :-
3358
+ get_next_signal(SessionId, Signal),
3359
+ ( Signal == judgment_done
3360
+ -> true
3361
+ ; Signal = error_signal(_)
3362
+ -> true
3363
+ ; wait_judgment_signal(SessionId)
3364
+ ).
3365
+
3366
+ %% split_specs(+Specs, -Questions, -Outputs)
3367
+ %% Each spec is `Question - Output`. Outputs are bound after the batch.
3368
+ split_specs([], [], []).
3369
+ split_specs([Spec-Output|Rest], [Question|Questions], [Output|Outputs]) :-
3370
+ spec_question(Spec, Question),
3371
+ split_specs(Rest, Questions, Outputs).
3372
+
3373
+ spec_question(choose(Instruction, Options),
3374
+ question{kind: choose, instruction: Instruction, options: Options}).
3375
+ spec_question(rate(Instruction, Levels),
3376
+ question{kind: rate, instruction: Instruction, levels: Levels}).
3377
+ spec_question(verify(Instruction),
3378
+ question{kind: verify, instruction: Instruction}).
3379
+ spec_question(verify(Instruction, Criteria),
3380
+ question{kind: verify, instruction: Instruction, criteria: Criteria}).
3381
+ spec_question(probability(Instruction),
3382
+ question{kind: probability, instruction: Instruction}).
3383
+
3384
+ bind_judgment_outputs([], []).
3385
+ bind_judgment_outputs([Output|Outputs], [Value|Values]) :-
3386
+ Output = Value,
3387
+ bind_judgment_outputs(Outputs, Values).
3388
+
3389
+ %% get_judge_backend(+State, -Backend)
3390
+ get_judge_backend(State, Backend) :-
3391
+ ( get_dict(judge_backend, State, Scoped)
3392
+ -> Backend = Scoped
3393
+ ; Backend = default
3394
+ ).
3395
+
3396
+ set_judge_backend(StateIn, Backend, StateOut) :-
3397
+ StateOut = StateIn.put(judge_backend, Backend).
3398
+
3399
+ clear_judge_backend(StateIn, StateOut) :-
3400
+ ( get_dict(judge_backend, StateIn, _)
3401
+ -> del_dict(judge_backend, StateIn, _, StateOut)
3402
+ ; StateOut = StateIn
3403
+ ).
3404
+
3405
+ get_required_judgments(State, Requires) :-
3406
+ ( get_dict(judgment_requires, State, Requires)
3407
+ -> true
3408
+ ; Requires = []
3409
+ ).
3410
+
3411
+ push_required_judgment(StateIn, Capability, StateOut) :-
3412
+ get_required_judgments(StateIn, Existing),
3413
+ ( member(Capability, Existing)
3414
+ -> StateOut = StateIn
3415
+ ; append(Existing, [Capability], Updated),
3416
+ StateOut = StateIn.put(judgment_requires, Updated)
3417
+ ).
3418
+
3419
+ pop_required_judgment(StateIn, StateOut) :-
3420
+ ( get_dict(judgment_requires, StateIn, _)
3421
+ -> del_dict(judgment_requires, StateIn, _, StateOut)
3422
+ ; StateOut = StateIn
3423
+ ).
3424
+
1316
3425
  mi_call(llm(Messages, Reply), StateIn, StateOut) :-
1317
3426
  !,
1318
3427
  consume_gas(StateIn, State1),
@@ -1512,6 +3621,49 @@ mi_call(get_memory(Messages), StateIn, StateIn) :-
1512
3621
  !,
1513
3622
  get_memory(StateIn, Messages).
1514
3623
 
3624
+ %% ============================================================
3625
+ %% Media (see docs/DML_MEDIA.md)
3626
+ %% ============================================================
3627
+
3628
+ %% mi_call(read_media(+Path, -Media), +StateIn, -StateOut)
3629
+ %% Binds Media = media(Path, Mime) for a workspace-relative image. The bytes
3630
+ %% stay on disk; only the reference enters memory.
3631
+ mi_call(read_media(Path, Media), StateIn, StateIn) :-
3632
+ !,
3633
+ get_params(StateIn, Params),
3634
+ interpolate_desc(Path, Params, P),
3635
+ media_mime(P, Mime),
3636
+ Media = media(P, Mime).
3637
+
3638
+ %% mi_call(attach_media(+Media), +StateIn, -StateOut)
3639
+ %% Appends a user memory message carrying a reference-only media entry, so the
3640
+ %% next task/N can see the image.
3641
+ mi_call(attach_media(Media), StateIn, StateOut) :-
3642
+ !,
3643
+ media_dict(Media, media{path: Path, mime: Mime}),
3644
+ format(atom(Note), '[attached ~w: ~w]', [Mime, Path]),
3645
+ Message = message{role: user, content: Note, media: [media{path: Path, mime: Mime}]},
3646
+ get_memory(StateIn, Memory),
3647
+ append(Memory, [Message], NewMemory),
3648
+ set_memory(StateIn, NewMemory, StateOut).
3649
+
3650
+ %% media_dict(+Media, -Dict)
3651
+ %% Accepts media(Path, Mime) or a bare path; normalizes to the storage dict.
3652
+ media_dict(media(Path, Mime), media{path: Path, mime: Mime}) :- !.
3653
+ media_dict(Path, media{path: Path, mime: Mime}) :- media_mime(Path, Mime).
3654
+
3655
+ %% media_mime(+Path, -Mime)
3656
+ %% Allowed media types, matching pi image content blocks.
3657
+ media_mime(Path, Mime) :-
3658
+ ( string(Path) -> atom_string(Atom, Path) ; Atom = Path ),
3659
+ ( sub_atom(Atom, _, _, 0, '.png') -> Mime = "image/png"
3660
+ ; sub_atom(Atom, _, _, 0, '.jpg') -> Mime = "image/jpeg"
3661
+ ; sub_atom(Atom, _, _, 0, '.jpeg') -> Mime = "image/jpeg"
3662
+ ; sub_atom(Atom, _, _, 0, '.gif') -> Mime = "image/gif"
3663
+ ; sub_atom(Atom, _, _, 0, '.webp') -> Mime = "image/webp"
3664
+ ; fail
3665
+ ).
3666
+
1515
3667
  %% ============================================================
1516
3668
  %% Output Predicates
1517
3669
  %% ============================================================
@@ -1544,8 +3696,11 @@ mi_call(input(Prompt, Input), StateIn, StateIn) :-
1544
3696
  engine_yield(wait_input(InterpPrompt)),
1545
3697
  % Use get_next_signal helper which handles both dynamic predicate and engine_fetch
1546
3698
  get_next_signal(SessionId, _Signal),
1547
- % Retrieve the input provided via provide_input/2
1548
- ( session_pending_input(SessionId, Input)
3699
+ % A cancelled input request fails the goal (non-interactive policy).
3700
+ ( session_input_cancelled(SessionId)
3701
+ -> retractall(session_input_cancelled(SessionId)),
3702
+ fail
3703
+ ; session_pending_input(SessionId, Input)
1549
3704
  -> retract(session_pending_input(SessionId, Input)),
1550
3705
  add_trace_with_result(SessionId, exit, input, [InterpPrompt], Input, Depth)
1551
3706
  ; Input = "", % Default to empty if no input provided
@@ -1567,6 +3722,27 @@ mi_call(log(Text), StateIn, StateIn) :-
1567
3722
  interpolate_desc(Text, Params, InterpText),
1568
3723
  engine_yield(log(InterpText)).
1569
3724
 
3725
+ %% mi_call(format(Format, Args), +StateIn, -StateOut)
3726
+ %% In REPL mode, capture format/2 output as an output event rather than
3727
+ %% writing to the process stdout. Outside the REPL (runDML), keep normal
3728
+ %% behavior so existing traces/prompts are unaffected.
3729
+ mi_call(format(Format, Args), StateIn, StateIn) :-
3730
+ get_session_id(SessionId),
3731
+ ( session_repl_mode(SessionId, true)
3732
+ -> with_output_to(string(Text), format(Format, Args)),
3733
+ mi_call(output(Text), StateIn, StateIn)
3734
+ ; format(Format, Args)
3735
+ ).
3736
+
3737
+ %% mi_call(writeln(Value), +StateIn, -StateOut)
3738
+ mi_call(writeln(Value), StateIn, StateIn) :-
3739
+ get_session_id(SessionId),
3740
+ ( session_repl_mode(SessionId, true)
3741
+ -> with_output_to(string(Text), writeln(Value)),
3742
+ mi_call(output(Text), StateIn, StateIn)
3743
+ ; writeln(Value)
3744
+ ).
3745
+
1570
3746
  %% ============================================================
1571
3747
  %% Context Stack Management - for memory save/restore
1572
3748
  %% ============================================================
@@ -1693,20 +3869,57 @@ mi_call(false, _StateIn, _StateOut) :-
1693
3869
  %% mi_call(retractall(Head), +StateIn, -StateOut)
1694
3870
  %% Database predicates must operate on the session module so DML-side facts
1695
3871
  %% (draft/4, claim/3, ...) are asserted/retracted in the right module.
3872
+ %% repl_reserved_clause(+Term)
3873
+ %% True when Term defines a reserved predicate. directives and the legitimate
3874
+ %% repl_delete harness form are exempt.
3875
+ repl_reserved_clause((:- _)) :-
3876
+ !,
3877
+ fail.
3878
+ repl_reserved_clause(repl_delete(_)) :-
3879
+ !,
3880
+ fail.
3881
+ repl_reserved_clause((tool(ToolHead, _) :- _)) :-
3882
+ !,
3883
+ functor(ToolHead, Name, _),
3884
+ repl_reserved_name(Name).
3885
+ repl_reserved_clause((tool(ToolHead) :- _)) :-
3886
+ !,
3887
+ functor(ToolHead, Name, _),
3888
+ repl_reserved_name(Name).
3889
+ repl_reserved_clause((Head :- _)) :-
3890
+ functor(Head, Name, _),
3891
+ repl_protected_name(Name).
3892
+ repl_reserved_clause(Head) :-
3893
+ functor(Head, Name, _),
3894
+ repl_protected_name(Name).
3895
+
1696
3896
  mi_call(assertz(Head), StateIn, StateIn) :-
1697
3897
  get_session_id(SessionId),
3898
+ guard_not_frozen(SessionId),
3899
+ guard_reserved_clause(Head),
1698
3900
  assertz(SessionId:Head).
1699
3901
 
1700
3902
  mi_call(asserta(Head), StateIn, StateIn) :-
1701
3903
  get_session_id(SessionId),
3904
+ guard_not_frozen(SessionId),
3905
+ guard_reserved_clause(Head),
1702
3906
  asserta(SessionId:Head).
1703
3907
 
3908
+ guard_reserved_clause(Head) :-
3909
+ ( repl_reserved_clause(Head)
3910
+ -> throw(error(permission_error(modify, reserved_name, Head),
3911
+ context(mi_call/3, 'cannot define a reserved predicate')))
3912
+ ; true
3913
+ ).
3914
+
1704
3915
  mi_call(retract(Head), StateIn, StateIn) :-
1705
3916
  get_session_id(SessionId),
3917
+ guard_not_frozen(SessionId),
1706
3918
  retract(SessionId:Head).
1707
3919
 
1708
3920
  mi_call(retractall(Head), StateIn, StateIn) :-
1709
3921
  get_session_id(SessionId),
3922
+ guard_not_frozen(SessionId),
1710
3923
  retractall(SessionId:Head).
1711
3924
 
1712
3925
  %% ============================================================
@@ -1915,6 +4128,7 @@ resolve_workspace_path(RelPath, FullPath) :-
1915
4128
  mi_call(consult(File), StateIn, StateIn) :-
1916
4129
  !,
1917
4130
  get_session_id(SessionId),
4131
+ guard_not_frozen(SessionId),
1918
4132
  resolve_workspace_path(File, FullPath),
1919
4133
  readutil:read_file_to_string(FullPath, Code, []),
1920
4134
  consult_string(Code, SessionId).
@@ -1961,7 +4175,14 @@ consult_clauses(Stream, SessionId) :-
1961
4175
  ).
1962
4176
 
1963
4177
  %% consult_process_term(+Term, +SessionId)
1964
- %% Process a single term read during consult
4178
+ %% Process a single term read during consult. Reserved-name definitions are
4179
+ %% refused so consulted files cannot shadow the REPL control surface.
4180
+ consult_process_term(Term, _SessionId) :-
4181
+ repl_reserved_clause(Term),
4182
+ !,
4183
+ throw(error(permission_error(modify, reserved_name, Term),
4184
+ context(consult_process_term/2, 'reserved predicate'))).
4185
+
1965
4186
  consult_process_term(:-(Directive), SessionId) :-
1966
4187
  % Handle directives - route through mi_call so custom predicates like consult/1 work
1967
4188
  !,
@@ -1980,7 +4201,7 @@ consult_process_term((tool(ToolHead, Description) :- Body), SessionId) :-
1980
4201
  extract_tool_schema(ToolHead, Description, ToolName, Schema),
1981
4202
  assertz(session_user_tools(SessionId, ToolName)),
1982
4203
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
1983
- format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
4204
+ format_tool_source(ToolHead, Description, Body, SourceCode),
1984
4205
  assertz(SessionId:tool_source(ToolName, SourceCode)),
1985
4206
  assertz(SessionId:(tool(ToolHead) :- Body)).
1986
4207
 
@@ -1990,7 +4211,7 @@ consult_process_term((tool(ToolHead) :- Body), SessionId) :-
1990
4211
  extract_tool_schema(ToolHead, none, ToolName, Schema),
1991
4212
  assertz(session_user_tools(SessionId, ToolName)),
1992
4213
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
1993
- format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
4214
+ format_tool_source(ToolHead, none, Body, SourceCode),
1994
4215
  assertz(SessionId:tool_source(ToolName, SourceCode)),
1995
4216
  assertz(SessionId:(tool(ToolHead) :- Body)).
1996
4217
 
@@ -2099,6 +4320,22 @@ mi_call(delete_directory(Dir), StateIn, StateIn) :-
2099
4320
  resolve_workspace_path(Dir, FullPath),
2100
4321
  delete_directory(FullPath).
2101
4322
 
4323
+ %% mi_call(size_file(File, Size), +StateIn, -StateOut)
4324
+ %% size_file/2 is a plain SWI builtin, so it would otherwise resolve `File`
4325
+ %% against the WASM process cwd (not the workspace) and demand `/workspace/...`.
4326
+ mi_call(size_file(File, Size), StateIn, StateIn) :-
4327
+ !,
4328
+ resolve_workspace_path(File, FullPath),
4329
+ size_file(FullPath, Size).
4330
+
4331
+ %% mi_call(time_file(File, Time), +StateIn, -StateOut)
4332
+ %% Modifications time (seconds since epoch); same workspace-relative rule as
4333
+ %% size_file/2.
4334
+ mi_call(time_file(File, Time), StateIn, StateIn) :-
4335
+ !,
4336
+ resolve_workspace_path(File, FullPath),
4337
+ time_file(FullPath, Time).
4338
+
2102
4339
  %% ============================================================
2103
4340
  %% Catch/Throw - STATE THREADED
2104
4341
  %% ============================================================
@@ -2180,6 +4417,8 @@ is_mi_special_predicate(yield(_)).
2180
4417
  is_mi_special_predicate(log(_)).
2181
4418
  is_mi_special_predicate(system(_)).
2182
4419
  is_mi_special_predicate(user(_)). %% Add user message to context
4420
+ is_mi_special_predicate(read_media(_,_)).
4421
+ is_mi_special_predicate(attach_media(_)).
2183
4422
  is_mi_special_predicate(task(_)).
2184
4423
  is_mi_special_predicate(task(_,_)).
2185
4424
  is_mi_special_predicate(task(_,_,_)).
@@ -2205,8 +4444,28 @@ is_mi_special_predicate(without_tools(_,_)).
2205
4444
  is_mi_special_predicate(with_reasoning(_,_)).
2206
4445
  %% Recipe context scoping
2207
4446
  is_mi_special_predicate(with_recipe(_,_)).
4447
+ %% Semantic judgment predicates
4448
+ is_mi_special_predicate(judge(_,_)).
4449
+ is_mi_special_predicate(choose(_,_,_,_)).
4450
+ is_mi_special_predicate(rate(_,_,_,_)).
4451
+ is_mi_special_predicate(verify(_,_,_)).
4452
+ is_mi_special_predicate(probability(_,_,_)).
4453
+ is_mi_special_predicate(holds(_,_)).
4454
+ is_mi_special_predicate(holds(_,_,_)).
4455
+ is_mi_special_predicate(with_judgment(_,_)).
4456
+ is_mi_special_predicate(require_judgment(_,_)).
2208
4457
  %% Context stack management
2209
4458
  is_mi_special_predicate(get_memory(_)).
4459
+ is_mi_special_predicate(format(_,_)).
4460
+ is_mi_special_predicate(writeln(_)).
4461
+ is_mi_special_predicate(input(_,_)).
4462
+ is_mi_special_predicate(call(_)).
4463
+ is_mi_special_predicate(call(_,_)).
4464
+ is_mi_special_predicate(call(_,_,_)).
4465
+ is_mi_special_predicate(call(_,_,_,_)).
4466
+ is_mi_special_predicate(once(_)).
4467
+ is_mi_special_predicate(ignore(_)).
4468
+ is_mi_special_predicate(not(_)).
2210
4469
  is_mi_special_predicate(push_context).
2211
4470
  is_mi_special_predicate(push_context(_)).
2212
4471
  is_mi_special_predicate(pop_context).
@@ -2264,6 +4523,8 @@ is_mi_special_predicate(close(_)).
2264
4523
  is_mi_special_predicate(read_string(_,_,_)).
2265
4524
  is_mi_special_predicate(write(_,_)).
2266
4525
  is_mi_special_predicate(exists_file(_)).
4526
+ is_mi_special_predicate(size_file(_,_)).
4527
+ is_mi_special_predicate(time_file(_,_)).
2267
4528
  is_mi_special_predicate(exists_directory(_)).
2268
4529
  is_mi_special_predicate(directory_files(_,_)).
2269
4530
  is_mi_special_predicate(make_directory(_)).
@@ -2277,6 +4538,105 @@ is_mi_special_predicate(use_module(_,_)).
2277
4538
  is_mi_special_predicate([_]).
2278
4539
  is_mi_special_predicate([_|_]).
2279
4540
 
4541
+ %% mi_call(call(Goal), +StateIn, -StateOut)
4542
+ %% Route call/1 through the meta-interpreter so backtracking and the
4543
+ %% reserved-predicate guard apply (a bare call/repl_* would bypass them).
4544
+ mi_call(call(Goal), StateIn, StateOut) :-
4545
+ guard_reserved_goal(Goal, StateIn),
4546
+ !,
4547
+ mi_call(Goal, StateIn, StateOut).
4548
+
4549
+ %% mi_call(call(Closure, Extra...), +StateIn, -StateOut)
4550
+ %% call/2..4 must also route through the meta-interpreter; otherwise a wrapped
4551
+ %% task/prompt/exec runs in raw Prolog, where task_named/3 does not exist.
4552
+ mi_call(call(Goal, A), StateIn, StateOut) :-
4553
+ !,
4554
+ extend_callable(Goal, [A], Callable),
4555
+ mi_call(call(Callable), StateIn, StateOut).
4556
+
4557
+ mi_call(call(Goal, A, B), StateIn, StateOut) :-
4558
+ !,
4559
+ extend_callable(Goal, [A, B], Callable),
4560
+ mi_call(call(Callable), StateIn, StateOut).
4561
+
4562
+ mi_call(call(Goal, A, B, C), StateIn, StateOut) :-
4563
+ !,
4564
+ extend_callable(Goal, [A, B, C], Callable),
4565
+ mi_call(call(Callable), StateIn, StateOut).
4566
+
4567
+ %% extend_callable(+Closure, +ExtraArgs, -Callable)
4568
+ extend_callable(Closure, Extra, Callable) :-
4569
+ Closure =.. List,
4570
+ append(List, Extra, Extended),
4571
+ Callable =.. Extended.
4572
+
4573
+ %% mi_call(once(Goal), +StateIn, -StateOut)
4574
+ %% once/1 must route through the meta-interpreter. As a plain builtin it would
4575
+ %% run Goal in raw Prolog, so a wrapped task/prompt/exec fails with
4576
+ %% task_named/3 undefined. Commits to the first solution of Goal, like once/1.
4577
+ mi_call(once(Goal), StateIn, StateOut) :-
4578
+ !,
4579
+ once(mi_call(Goal, StateIn, StateOut)).
4580
+
4581
+ %% mi_call(ignore(Goal), +StateIn, -StateOut)
4582
+ mi_call(ignore(Goal), StateIn, StateOut) :-
4583
+ !,
4584
+ ( once(mi_call(Goal, StateIn, StateOut0))
4585
+ -> StateOut = StateOut0
4586
+ ; StateOut = StateIn
4587
+ ).
4588
+
4589
+ %% mi_call(not(Goal), +StateIn, -StateOut) (alias for \+/1)
4590
+ mi_call(not(Goal), StateIn, StateIn) :-
4591
+ !,
4592
+ mi_call(\+(Goal), StateIn, StateIn).
4593
+
4594
+ %% Refuse reserved predicates invoked directly in non-meta frames. This is the
4595
+ %% second of the three-place reserved guard (execute, mi_call, assert/load).
4596
+ mi_call(Goal, StateIn, _StateOut) :-
4597
+ guard_reserved_direct(Goal, StateIn),
4598
+ !,
4599
+ throw(error(permission_error(execute, reserved_predicate, Goal),
4600
+ context(mi_call/3, 'cannot invoke a reserved REPL predicate'))).
4601
+
4602
+ %% guard_reserved_goal(+Goal, +StateIn)
4603
+ %% Throws when Goal names a reserved predicate and the frame is not meta.
4604
+ guard_reserved_goal(Goal, StateIn) :-
4605
+ ( is_dict(StateIn), get_dict(frame_kind, StateIn, meta)
4606
+ -> true
4607
+ ; nonvar(Goal),
4608
+ callable(Goal),
4609
+ functor(Goal, Name, _),
4610
+ repl_reserved_name(Name)
4611
+ -> throw(error(permission_error(execute, reserved_predicate, Name),
4612
+ context(mi_call/3, 'cannot invoke a reserved REPL predicate')))
4613
+ ; true
4614
+ ).
4615
+
4616
+ guard_reserved_direct(Goal, StateIn) :-
4617
+ ( is_dict(StateIn), get_dict(frame_kind, StateIn, meta)
4618
+ -> fail
4619
+ ; nonvar(Goal),
4620
+ callable(Goal),
4621
+ functor(Goal, Name, _),
4622
+ repl_reserved_name(Name)
4623
+ ).
4624
+
4625
+ %% Untransformed task/N and prompt/N (N >= 2) are source-level sugar rewritten
4626
+ %% to task_named/3 / prompt_named/3 by transform_task_calls/3 at load time.
4627
+ %% There is no runtime handler for the untransformed form, so it would match no
4628
+ %% clause and fail silently (looking like an inexplicable "0 solutions"). Fail
4629
+ %% loudly instead: reaching here means a clause was asserted/run without the
4630
+ %% load-time preprocessing (assert/assert_many now apply it, so this is a guard).
4631
+ mi_call(Goal, _StateIn, _StateOut) :-
4632
+ compound(Goal),
4633
+ functor(Goal, Name, Arity),
4634
+ Arity >= 2,
4635
+ ( Name == task ; Name == prompt ),
4636
+ !,
4637
+ throw(error(domain_error(unrewritten_task_form, Goal),
4638
+ 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'))).
4639
+
2280
4640
  %% mi_call(Goal, +StateIn, -StateOut)
2281
4641
  %% Catch-all for built-in and user-defined predicates
2282
4642
  %% MUST NOT match special predicates that have dedicated handlers
@@ -2331,9 +4691,14 @@ mi_call_dispatch(Goal, StateIn, StateOut) :-
2331
4691
  clause(SessionId:Goal, Body),
2332
4692
  NewDepth is Depth + 1,
2333
4693
  set_depth(StateIn, NewDepth, State1),
2334
- mi_call(Body, State1, State2),
2335
- add_trace_entry(SessionId, exit, Functor, Args, Depth),
2336
- set_depth(State2, Depth, StateOut)
4694
+ % Soft cut: execute the body for every solution (so findall/backtracking
4695
+ % works), but still record a fail trace when the body has none.
4696
+ ( mi_call(Body, State1, State2)
4697
+ *-> add_trace_entry(SessionId, exit, Functor, Args, Depth),
4698
+ set_depth(State2, Depth, StateOut)
4699
+ ; add_trace_entry(SessionId, fail, Functor, Args, Depth),
4700
+ fail
4701
+ )
2337
4702
  ).
2338
4703
 
2339
4704
  mi_call_dispatch(Goal, StateIn, StateIn) :-
@@ -2489,6 +4854,17 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
2489
4854
  rebuild_goal(Functor, StringArg, ExpandedRestArgs, ExpandedGoal)
2490
4855
  ).
2491
4856
 
4857
+ % Goals whose interpolated string is NOT the first argument (verify/3, judge/2,
4858
+ % choose/4, rate/4, probability/3, holds/2,3, and any user predicate): hoist the
4859
+ % interpolation into a preceding goal.
4860
+ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
4861
+ compound(Goal),
4862
+ Goal =.. [Functor|Args],
4863
+ expand_args_hoist(Args, Bindings, PreGoals, ExpandedArgs, true),
4864
+ !,
4865
+ Bare =.. [Functor|ExpandedArgs],
4866
+ prepend_goals(PreGoals, Bare, ExpandedGoal).
4867
+
2492
4868
  % Other goals - expand arguments
2493
4869
  expand_body_interp(Goal, Bindings, ExpandedGoal) :-
2494
4870
  compound(Goal), !,
@@ -2553,6 +4929,34 @@ expand_rest_args([A|As], Bindings, [EA|EAs]) :-
2553
4929
  expand_body_interp(A, Bindings, EA),
2554
4930
  expand_rest_args(As, Bindings, EAs).
2555
4931
 
4932
+ %% expand_args_hoist(+Args, +Bindings, -PreGoals, -Args2, -Hoisted)
4933
+ %% Expand each argument; any interpolated string becomes a goal in PreGoals and
4934
+ %% its argument becomes the temp variable that goal binds.
4935
+ expand_args_hoist([], _, [], [], false).
4936
+ expand_args_hoist([Arg|Args], Bindings, PreGoals, [Expanded|EArgs], Hoisted) :-
4937
+ expand_arg_hoist(Arg, Bindings, Pre, Expanded, ThisHoisted),
4938
+ expand_args_hoist(Args, Bindings, Rest, EArgs, RestHoisted),
4939
+ append(Pre, Rest, PreGoals),
4940
+ ( ThisHoisted == true
4941
+ -> Hoisted = true
4942
+ ; Hoisted = RestHoisted
4943
+ ).
4944
+
4945
+ expand_arg_hoist(Arg, Bindings, [FormatGoal], TempVar, true) :-
4946
+ ( string(Arg)
4947
+ ; atom(Arg)
4948
+ ),
4949
+ string_needs_interpolation(Arg),
4950
+ !,
4951
+ build_format_call(Arg, Bindings, TempVar, FormatGoal).
4952
+ expand_arg_hoist(Arg, _, [], Arg, false).
4953
+
4954
+ %% prepend_goals(+PreGoals, +Goal, -Expanded)
4955
+ prepend_goals([], Goal, Goal) :- !.
4956
+ prepend_goals(Goals, Goal, Expanded) :-
4957
+ list_to_conjunction(Goals, Conj),
4958
+ Expanded = (Conj, Goal).
4959
+
2556
4960
  %% flatten_conjunction(+A, +B, -Conj)
2557
4961
  %% Flatten nested conjunctions properly
2558
4962
  flatten_conjunction((A1, A2), B, Result) :- !,