deepclause-sdk 0.0.89 → 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 (292) hide show
  1. package/README.md +417 -948
  2. package/dist/agent.d.ts +18 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +74 -21
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +323 -176
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +146 -188
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.d.ts +3 -2
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +61 -425
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/login.d.ts +29 -0
  15. package/dist/cli/login.d.ts.map +1 -0
  16. package/dist/cli/login.js +184 -0
  17. package/dist/cli/login.js.map +1 -0
  18. package/dist/cli/readline.d.ts +6 -0
  19. package/dist/cli/readline.d.ts.map +1 -0
  20. package/dist/cli/readline.js +15 -0
  21. package/dist/cli/readline.js.map +1 -0
  22. package/dist/cli/repl-bin.d.ts +9 -0
  23. package/dist/cli/repl-bin.d.ts.map +1 -0
  24. package/dist/cli/repl-bin.js +16 -0
  25. package/dist/cli/repl-bin.js.map +1 -0
  26. package/dist/cli/repl.d.ts +191 -0
  27. package/dist/cli/repl.d.ts.map +1 -0
  28. package/dist/cli/repl.js +1232 -0
  29. package/dist/cli/repl.js.map +1 -0
  30. package/dist/cli/run.d.ts +17 -18
  31. package/dist/cli/run.d.ts.map +1 -1
  32. package/dist/cli/run.js +196 -183
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/cli/set-backend.d.ts +53 -0
  35. package/dist/cli/set-backend.d.ts.map +1 -0
  36. package/dist/cli/set-backend.js +259 -0
  37. package/dist/cli/set-backend.js.map +1 -0
  38. package/dist/cli/visualize.d.ts +38 -0
  39. package/dist/cli/visualize.d.ts.map +1 -0
  40. package/dist/cli/visualize.js +125 -0
  41. package/dist/cli/visualize.js.map +1 -0
  42. package/dist/compiler.d.ts +5 -52
  43. package/dist/compiler.d.ts.map +1 -1
  44. package/dist/compiler.js +4 -768
  45. package/dist/compiler.js.map +1 -1
  46. package/dist/index.d.ts +27 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +18 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/prolog/bridge.d.ts +3 -1
  51. package/dist/prolog/bridge.d.ts.map +1 -1
  52. package/dist/prolog/bridge.js +8 -1
  53. package/dist/prolog/bridge.js.map +1 -1
  54. package/dist/prolog-src/deepclause_analysis.pl +10 -2
  55. package/dist/prolog-src/deepclause_mi.pl +2245 -41
  56. package/dist/repl/agent.d.ts +72 -0
  57. package/dist/repl/agent.d.ts.map +1 -0
  58. package/dist/repl/agent.js +366 -0
  59. package/dist/repl/agent.js.map +1 -0
  60. package/dist/repl/auth.d.ts +45 -0
  61. package/dist/repl/auth.d.ts.map +1 -0
  62. package/dist/repl/auth.js +116 -0
  63. package/dist/repl/auth.js.map +1 -0
  64. package/dist/repl/backend.d.ts +290 -0
  65. package/dist/repl/backend.d.ts.map +1 -0
  66. package/dist/repl/backend.js +370 -0
  67. package/dist/repl/backend.js.map +1 -0
  68. package/dist/repl/compaction.d.ts +122 -0
  69. package/dist/repl/compaction.d.ts.map +1 -0
  70. package/dist/repl/compaction.js +318 -0
  71. package/dist/repl/compaction.js.map +1 -0
  72. package/dist/repl/export.d.ts +30 -0
  73. package/dist/repl/export.d.ts.map +1 -0
  74. package/dist/repl/export.js +23 -0
  75. package/dist/repl/export.js.map +1 -0
  76. package/dist/repl/frames.d.ts +21 -0
  77. package/dist/repl/frames.d.ts.map +1 -0
  78. package/dist/repl/frames.js +79 -0
  79. package/dist/repl/frames.js.map +1 -0
  80. package/dist/repl/harness-tools.d.ts +74 -0
  81. package/dist/repl/harness-tools.d.ts.map +1 -0
  82. package/dist/repl/harness-tools.js +121 -0
  83. package/dist/repl/harness-tools.js.map +1 -0
  84. package/dist/repl/highlight.d.ts +30 -0
  85. package/dist/repl/highlight.d.ts.map +1 -0
  86. package/dist/repl/highlight.js +285 -0
  87. package/dist/repl/highlight.js.map +1 -0
  88. package/dist/repl/history.d.ts +234 -0
  89. package/dist/repl/history.d.ts.map +1 -0
  90. package/dist/repl/history.js +661 -0
  91. package/dist/repl/history.js.map +1 -0
  92. package/dist/repl/index.d.ts +120 -0
  93. package/dist/repl/index.d.ts.map +1 -0
  94. package/dist/repl/index.js +107 -0
  95. package/dist/repl/index.js.map +1 -0
  96. package/dist/repl/judge.d.ts +51 -0
  97. package/dist/repl/judge.d.ts.map +1 -0
  98. package/dist/repl/judge.js +58 -0
  99. package/dist/repl/judge.js.map +1 -0
  100. package/dist/repl/markdown.d.ts +25 -0
  101. package/dist/repl/markdown.d.ts.map +1 -0
  102. package/dist/repl/markdown.js +103 -0
  103. package/dist/repl/markdown.js.map +1 -0
  104. package/dist/repl/picker.d.ts +34 -0
  105. package/dist/repl/picker.d.ts.map +1 -0
  106. package/dist/repl/picker.js +81 -0
  107. package/dist/repl/picker.js.map +1 -0
  108. package/dist/repl/project-context.d.ts +16 -0
  109. package/dist/repl/project-context.d.ts.map +1 -0
  110. package/dist/repl/project-context.js +23 -0
  111. package/dist/repl/project-context.js.map +1 -0
  112. package/dist/repl/render.d.ts +160 -0
  113. package/dist/repl/render.d.ts.map +1 -0
  114. package/dist/repl/render.js +715 -0
  115. package/dist/repl/render.js.map +1 -0
  116. package/dist/repl/session.d.ts +356 -0
  117. package/dist/repl/session.d.ts.map +1 -0
  118. package/dist/repl/session.js +1275 -0
  119. package/dist/repl/session.js.map +1 -0
  120. package/dist/repl/status.d.ts +100 -0
  121. package/dist/repl/status.d.ts.map +1 -0
  122. package/dist/repl/status.js +218 -0
  123. package/dist/repl/status.js.map +1 -0
  124. package/dist/repl/tools.d.ts +53 -0
  125. package/dist/repl/tools.d.ts.map +1 -0
  126. package/dist/repl/tools.js +307 -0
  127. package/dist/repl/tools.js.map +1 -0
  128. package/dist/repl/toolset.d.ts +32 -0
  129. package/dist/repl/toolset.d.ts.map +1 -0
  130. package/dist/repl/toolset.js +82 -0
  131. package/dist/repl/toolset.js.map +1 -0
  132. package/dist/repl/tui/editor.d.ts +17 -0
  133. package/dist/repl/tui/editor.d.ts.map +1 -0
  134. package/dist/repl/tui/editor.js +39 -0
  135. package/dist/repl/tui/editor.js.map +1 -0
  136. package/dist/repl/tui/status-bar.d.ts +26 -0
  137. package/dist/repl/tui/status-bar.d.ts.map +1 -0
  138. package/dist/repl/tui/status-bar.js +47 -0
  139. package/dist/repl/tui/status-bar.js.map +1 -0
  140. package/dist/repl/tui/theme.d.ts +30 -0
  141. package/dist/repl/tui/theme.d.ts.map +1 -0
  142. package/dist/repl/tui/theme.js +81 -0
  143. package/dist/repl/tui/theme.js.map +1 -0
  144. package/dist/repl/tui/transcript.d.ts +39 -0
  145. package/dist/repl/tui/transcript.d.ts.map +1 -0
  146. package/dist/repl/tui/transcript.js +123 -0
  147. package/dist/repl/tui/transcript.js.map +1 -0
  148. package/dist/runner.d.ts +132 -13
  149. package/dist/runner.d.ts.map +1 -1
  150. package/dist/runner.js +434 -277
  151. package/dist/runner.js.map +1 -1
  152. package/dist/sdk.d.ts.map +1 -1
  153. package/dist/sdk.js +0 -32
  154. package/dist/sdk.js.map +1 -1
  155. package/dist/system/assets/docs/AGENT_PROMPT.md +1373 -0
  156. package/dist/system/assets/docs/DML_AUTHORING.md +195 -0
  157. package/dist/system/assets/docs/DML_REFERENCE.md +312 -72
  158. package/dist/system/assets/docs/HARNESS_EXAMPLES.md +172 -0
  159. package/dist/system/assets/docs/HARNESS_PATTERNS.md +562 -0
  160. package/dist/system/assets/docs/META_PROMPT.md +68 -0
  161. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +117 -0
  162. package/dist/system/assets/index.d.ts +9 -17
  163. package/dist/system/assets/index.d.ts.map +1 -1
  164. package/dist/system/assets/index.js +26 -85
  165. package/dist/system/assets/index.js.map +1 -1
  166. package/dist/system/runtime/agentvm-manager.d.ts +9 -2
  167. package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
  168. package/dist/system/runtime/agentvm-manager.js +70 -44
  169. package/dist/system/runtime/agentvm-manager.js.map +1 -1
  170. package/dist/system/runtime/media.d.ts +23 -0
  171. package/dist/system/runtime/media.d.ts.map +1 -0
  172. package/dist/system/runtime/media.js +55 -0
  173. package/dist/system/runtime/media.js.map +1 -0
  174. package/dist/system/runtime/shell-limits.d.ts +21 -0
  175. package/dist/system/runtime/shell-limits.d.ts.map +1 -0
  176. package/dist/system/runtime/shell-limits.js +39 -0
  177. package/dist/system/runtime/shell-limits.js.map +1 -0
  178. package/dist/system/runtime/shell-manager.d.ts +8 -3
  179. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  180. package/dist/system/runtime/shell-manager.js +56 -39
  181. package/dist/system/runtime/shell-manager.js.map +1 -1
  182. package/dist/system/runtime/shell-tool-events.js +5 -0
  183. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  184. package/dist/system/runtime/token-usage.d.ts +2 -0
  185. package/dist/system/runtime/token-usage.d.ts.map +1 -1
  186. package/dist/system/runtime/token-usage.js +7 -0
  187. package/dist/system/runtime/token-usage.js.map +1 -1
  188. package/dist/tools.d.ts +3 -21
  189. package/dist/tools.d.ts.map +1 -1
  190. package/dist/tools.js +14 -168
  191. package/dist/tools.js.map +1 -1
  192. package/dist/types.d.ts +80 -118
  193. package/dist/types.d.ts.map +1 -1
  194. package/dist/validate.d.ts +19 -0
  195. package/dist/validate.d.ts.map +1 -0
  196. package/dist/validate.js +338 -0
  197. package/dist/validate.js.map +1 -0
  198. package/dist/visualize/index.d.ts +52 -0
  199. package/dist/visualize/index.d.ts.map +1 -0
  200. package/dist/visualize/index.js +189 -0
  201. package/dist/visualize/index.js.map +1 -0
  202. package/package.json +17 -15
  203. package/src/prolog-src/deepclause_analysis.pl +10 -2
  204. package/src/prolog-src/deepclause_mi.pl +2245 -41
  205. package/dist/cli/commands.d.ts +0 -42
  206. package/dist/cli/commands.d.ts.map +0 -1
  207. package/dist/cli/commands.js +0 -246
  208. package/dist/cli/commands.js.map +0 -1
  209. package/dist/cli/compile.d.ts +0 -99
  210. package/dist/cli/compile.d.ts.map +0 -1
  211. package/dist/cli/compile.js +0 -363
  212. package/dist/cli/compile.js.map +0 -1
  213. package/dist/cli/default-skills.d.ts +0 -10
  214. package/dist/cli/default-skills.d.ts.map +0 -1
  215. package/dist/cli/default-skills.js +0 -206
  216. package/dist/cli/default-skills.js.map +0 -1
  217. package/dist/cli/interactive.d.ts +0 -2
  218. package/dist/cli/interactive.d.ts.map +0 -1
  219. package/dist/cli/interactive.js +0 -20
  220. package/dist/cli/interactive.js.map +0 -1
  221. package/dist/cli/mcp.d.ts +0 -56
  222. package/dist/cli/mcp.d.ts.map +0 -1
  223. package/dist/cli/mcp.js +0 -138
  224. package/dist/cli/mcp.js.map +0 -1
  225. package/dist/cli/prompt.d.ts +0 -2
  226. package/dist/cli/prompt.d.ts.map +0 -1
  227. package/dist/cli/prompt.js +0 -2
  228. package/dist/cli/prompt.js.map +0 -1
  229. package/dist/cli/search.d.ts +0 -19
  230. package/dist/cli/search.d.ts.map +0 -1
  231. package/dist/cli/search.js +0 -189
  232. package/dist/cli/search.js.map +0 -1
  233. package/dist/cli/tool-args.d.ts +0 -2
  234. package/dist/cli/tool-args.d.ts.map +0 -1
  235. package/dist/cli/tool-args.js +0 -39
  236. package/dist/cli/tool-args.js.map +0 -1
  237. package/dist/cli/tools.d.ts +0 -36
  238. package/dist/cli/tools.d.ts.map +0 -1
  239. package/dist/cli/tools.js +0 -229
  240. package/dist/cli/tools.js.map +0 -1
  241. package/dist/cli/tui/index.d.ts +0 -22
  242. package/dist/cli/tui/index.d.ts.map +0 -1
  243. package/dist/cli/tui/index.js +0 -29
  244. package/dist/cli/tui/index.js.map +0 -1
  245. package/dist/cli/tui.d.ts +0 -167
  246. package/dist/cli/tui.d.ts.map +0 -1
  247. package/dist/cli/tui.js +0 -5228
  248. package/dist/cli/tui.js.map +0 -1
  249. package/dist/compiler_prompt.d.ts +0 -20
  250. package/dist/compiler_prompt.d.ts.map +0 -1
  251. package/dist/compiler_prompt.js +0 -870
  252. package/dist/compiler_prompt.js.map +0 -1
  253. package/dist/deepclause-cli.skill +0 -0
  254. package/dist/prolog-src/deepclause_mi.pl.bak +0 -570
  255. package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +0 -105
  256. package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +0 -700
  257. package/dist/system/assets/docs/TUI.md +0 -150
  258. package/dist/system/assets/recipes/deepclause-coding-workflow/SKILL.md +0 -100
  259. package/dist/system/assets/skills/conductor.dml +0 -76
  260. package/dist/system/assets/skills/deep-planner.dml +0 -412
  261. package/dist/system/assets/skills/plan.dml +0 -429
  262. package/dist/system/assets/skills/security-planner.dml +0 -337
  263. package/dist/system/assets/skills/skill-creator.dml +0 -187
  264. package/dist/system/runtime/catalog-recipes.d.ts +0 -24
  265. package/dist/system/runtime/catalog-recipes.d.ts.map +0 -1
  266. package/dist/system/runtime/catalog-recipes.js +0 -273
  267. package/dist/system/runtime/catalog-recipes.js.map +0 -1
  268. package/dist/system/runtime/catalog-skills.d.ts +0 -50
  269. package/dist/system/runtime/catalog-skills.d.ts.map +0 -1
  270. package/dist/system/runtime/catalog-skills.js +0 -152
  271. package/dist/system/runtime/catalog-skills.js.map +0 -1
  272. package/dist/system/runtime/conductor.d.ts +0 -105
  273. package/dist/system/runtime/conductor.d.ts.map +0 -1
  274. package/dist/system/runtime/conductor.js +0 -732
  275. package/dist/system/runtime/conductor.js.map +0 -1
  276. package/dist/system/runtime/console-capture.d.ts +0 -9
  277. package/dist/system/runtime/console-capture.d.ts.map +0 -1
  278. package/dist/system/runtime/console-capture.js +0 -67
  279. package/dist/system/runtime/console-capture.js.map +0 -1
  280. package/dist/system/runtime/dml-executor.d.ts +0 -52
  281. package/dist/system/runtime/dml-executor.d.ts.map +0 -1
  282. package/dist/system/runtime/dml-executor.js +0 -203
  283. package/dist/system/runtime/dml-executor.js.map +0 -1
  284. package/dist/system/runtime/runtime-tools.d.ts +0 -31
  285. package/dist/system/runtime/runtime-tools.d.ts.map +0 -1
  286. package/dist/system/runtime/runtime-tools.js +0 -312
  287. package/dist/system/runtime/runtime-tools.js.map +0 -1
  288. package/dist/system/runtime/skill-creator.d.ts +0 -38
  289. package/dist/system/runtime/skill-creator.d.ts.map +0 -1
  290. package/dist/system/runtime/skill-creator.js +0 -763
  291. package/dist/system/runtime/skill-creator.js.map +0 -1
  292. package/src/prolog-src/deepclause_mi.pl.bak +0 -570
@@ -20,12 +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,
27
92
  post_judgment_result/3,
28
93
  provide_input/2,
94
+ provide_input_cancelled/1,
29
95
  mi/3,
30
96
  % Tool engine predicates for isolated tool execution
31
97
  create_tool_engine/4,
@@ -42,7 +108,25 @@
42
108
  :- discontiguous var_to_name/3.
43
109
 
44
110
  %% Dynamic predicates for session state (engine management only, not memory!)
45
- :- 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
46
130
  :- dynamic session_params/2. % session_params(SessionId, ParamsDict)
47
131
  :- dynamic session_user_tools/2. % session_user_tools(SessionId, ToolName)
48
132
  :- dynamic session_user_tool_schema/3. % session_user_tool_schema(SessionId, ToolName, Schema)
@@ -56,6 +140,7 @@
56
140
  :- dynamic session_trace_enabled/2. % session_trace_enabled(SessionId, true/false)
57
141
  :- dynamic session_pending_signal/2. % session_pending_signal(SessionId, Signal) - fallback for engine_fetch
58
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)
59
144
 
60
145
  %% ============================================================
61
146
  %% DML Parsing
@@ -260,6 +345,182 @@ wrapper_type(list, array).
260
345
  wrapper_type(object, object).
261
346
  wrapper_type(dict, object).
262
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
+
263
524
  %% var_to_name(+Bindings, +Var, -Name)
264
525
  %% Look up a variable's name from the bindings list
265
526
  var_to_name(Bindings, Var, Name) :-
@@ -269,7 +530,15 @@ var_to_name(Bindings, Var, Name) :-
269
530
  ).
270
531
 
271
532
  %% process_clause(+Term, +SessionId)
272
- %% 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
+
273
542
  process_clause((:- Directive), SessionId) :-
274
543
  !,
275
544
  process_directive(Directive, SessionId).
@@ -281,8 +550,9 @@ process_clause((tool(ToolHead, Description) :- Body), SessionId) :-
281
550
  extract_tool_schema(ToolHead, Description, ToolName, Schema),
282
551
  assertz(session_user_tools(SessionId, ToolName)),
283
552
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
284
- % Store the source code for the tool description
285
- format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
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),
286
556
  assertz(SessionId:tool_source(ToolName, SourceCode)),
287
557
  % Assert the tool implementation (use just ToolHead for execution)
288
558
  assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
@@ -294,8 +564,8 @@ process_clause((tool(ToolHead) :- Body), SessionId) :-
294
564
  extract_tool_schema(ToolHead, none, ToolName, Schema),
295
565
  assertz(session_user_tools(SessionId, ToolName)),
296
566
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
297
- % Store the source code for the tool description
298
- format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
567
+ % Store the source code for the tool description (reverses task rewrites).
568
+ format_tool_source(ToolHead, none, Body, SourceCode),
299
569
  assertz(SessionId:tool_source(ToolName, SourceCode)),
300
570
  % Assert the tool implementation
301
571
  assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
@@ -309,6 +579,15 @@ process_clause((Head :- Body), SessionId) :-
309
579
  ),
310
580
  assertz(SessionId:(Head :- ExpandedBody)).
311
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
+
312
591
  process_clause(Fact, SessionId) :-
313
592
  % Simple fact
314
593
  assertz(SessionId:Fact).
@@ -426,15 +705,27 @@ workspace_module_reference(Module) :-
426
705
  process_use_module_directive(Module, SessionId) :-
427
706
  ( workspace_module_reference(Module)
428
707
  -> process_directive(consult(Module), SessionId)
429
- ; SessionId:use_module(Module)
708
+ ; SessionId:use_module(Module),
709
+ import_library_into_mi(Module)
430
710
  ).
431
711
 
432
712
  process_use_module_directive(Module, Exports, SessionId) :-
433
713
  ( workspace_module_reference(Module)
434
714
  -> process_directive(consult(Module), SessionId)
435
- ; SessionId:use_module(Module, Exports)
715
+ ; SessionId:use_module(Module, Exports),
716
+ import_library_into_mi(Module)
436
717
  ).
437
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
+
438
729
  %% process_directive(+Directive, +SessionId)
439
730
  process_directive(param(Key, Desc), SessionId) :-
440
731
  !,
@@ -454,9 +745,17 @@ process_directive(consult(File), SessionId) :-
454
745
  %% Handle use_module directive
455
746
  process_directive(use_module(Module), SessionId) :-
456
747
  !,
748
+ ( workspace_module_reference(Module)
749
+ -> true
750
+ ; record_directive(SessionId, use_module(Module))
751
+ ),
457
752
  process_use_module_directive(Module, SessionId).
458
753
  process_directive(use_module(Module, Exports), SessionId) :-
459
754
  !,
755
+ ( workspace_module_reference(Module)
756
+ -> true
757
+ ; record_directive(SessionId, use_module(Module, Exports))
758
+ ),
460
759
  process_use_module_directive(Module, Exports, SessionId).
461
760
  %% Handle list-style consult: :- [File] or :- [File1, File2, ...]
462
761
  process_directive([File|Files], SessionId) :-
@@ -473,6 +772,9 @@ process_directive(_, _).
473
772
  %% create_engine(+SessionId, +MemoryId, +Args, +Params, +InitialMemory, -Engine)
474
773
  %% Args is a list of positional arguments for agent_main
475
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.
476
778
  create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
477
779
  assertz(session_params(SessionId, Params)),
478
780
  determine_agent_goal(SessionId, Args, Goal),
@@ -484,12 +786,1631 @@ create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
484
786
  (get_dict(gas_limit, Params, GasLimit) -> true ; GasLimit = -1),
485
787
  % Create initial state with provided memory, params, context stack, trace depth and gas
486
788
  InitialState = state{memory: InitialMemory, params: Params, context_stack: [], depth: 0, gas_remaining: GasLimit},
487
- % Create the engine - pass SessionId and initial state to mi/3
488
- engine_create(_,
489
- deepclause_mi:mi(Goal, InitialState, SessionId),
490
- Engine),
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, _)),
491
794
  assertz(session_engine(SessionId, Engine)).
492
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))
2228
+ ).
2229
+
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
+ ).
2295
+
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])
2312
+ ).
2313
+
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)
2336
+ ).
2337
+
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])
2347
+ ).
2348
+
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).
2353
+
2354
+ repl_commit_from_parse(_SessionId, _Source, _Preds, ParseError, Result) :-
2355
+ ParseError \== none,
2356
+ !,
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]) :-
2379
+ !,
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,
2387
+ !,
2388
+ commit_error(LoadError, Result).
2389
+ repl_commit_from_load(none, ok).
2390
+
2391
+ commit_error(Message, Result) :-
2392
+ format(atom(Result), 'error(~w)', [Message]).
2393
+
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
+ ).
2413
+
493
2414
  %% determine_agent_goal(+SessionId, +Args, -Goal)
494
2415
  %% Args is a list of positional arguments - passed directly to agent_main
495
2416
  %% Prefers arity that matches the number of arguments provided
@@ -529,31 +2450,12 @@ determine_agent_goal(SessionId, Args, Goal) :-
529
2450
  determine_agent_goal(SessionId, _Args, SessionId:agent_main).
530
2451
 
531
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.
532
2456
  step_engine(SessionId, Status, Content, Payload) :-
533
- ( session_engine(SessionId, Engine)
534
- -> catch(
535
- ( engine_next(Engine, Result)
536
- -> process_engine_result(Result, Status, Content, Payload)
537
- ; % Engine finished - return trace if enabled
538
- Status = finished,
539
- Content = '',
540
- ( session_trace_enabled(SessionId, true),
541
- session_trace_log(SessionId, TraceLog)
542
- -> Payload = payload{trace: TraceLog}
543
- ; Payload = none
544
- )
545
- ),
546
- Error,
547
- ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
548
- Status = error,
549
- Content = ErrMsg,
550
- ( session_trace_enabled(SessionId, true),
551
- session_trace_log(SessionId, TraceLog)
552
- -> Payload = payload{trace: TraceLog}
553
- ; Payload = none
554
- )
555
- )
556
- )
2457
+ ( session_default_frame(SessionId, FrameId)
2458
+ -> step_frame(FrameId, Status, Content, Payload)
557
2459
  ; Status = error,
558
2460
  Content = 'No engine found for session',
559
2461
  Payload = none
@@ -582,6 +2484,13 @@ process_engine_result(request_judgment(Payload), request_judgment, '', Payload)
582
2484
  %% Tool result from inline tool execution
583
2485
  process_engine_result(tool_result(Result), tool_result, '',
584
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}) :- !.
585
2494
  process_engine_result(wait_input(Prompt), wait_input, Prompt, none) :- !.
586
2495
  process_engine_result(error(Msg), error, Msg, none) :- !.
587
2496
  process_engine_result(Other, error, Msg, none) :-
@@ -589,15 +2498,23 @@ process_engine_result(Other, error, Msg, none) :-
589
2498
 
590
2499
  %% destroy_engine(+SessionId)
591
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
+ ),
592
2507
  ( session_engine(SessionId, Engine)
593
2508
  -> catch(engine_destroy(Engine), _, true)
594
2509
  ; true
595
2510
  ),
2511
+ retractall(session_default_frame(SessionId, _)),
596
2512
  retractall(session_engine(SessionId, _)),
597
2513
  retractall(session_params(SessionId, _)),
598
2514
  retractall(session_user_tools(SessionId, _)),
599
2515
  retractall(session_user_tool_schema(SessionId, _, _)),
600
2516
  retractall(session_pending_input(SessionId, _)),
2517
+ retractall(session_input_cancelled(SessionId)),
601
2518
  retractall(session_agent_result(SessionId, _)),
602
2519
  retractall(session_exec_result(SessionId, _)),
603
2520
  retractall(session_sample_token_result(SessionId, _)),
@@ -671,6 +2588,13 @@ provide_input(SessionId, Input) :-
671
2588
  assertz(session_pending_input(SessionId, Input)),
672
2589
  post_signal_to_engine(SessionId, input_provided).
673
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
+
674
2598
  %% post_signal_to_engine(+SessionId, +Signal)
675
2599
  %% Posts a signal via dynamic predicate (most reliable for WASM engines)
676
2600
  %% Falls back to engine_post only if dynamic assert fails
@@ -1697,6 +3621,49 @@ mi_call(get_memory(Messages), StateIn, StateIn) :-
1697
3621
  !,
1698
3622
  get_memory(StateIn, Messages).
1699
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
+
1700
3667
  %% ============================================================
1701
3668
  %% Output Predicates
1702
3669
  %% ============================================================
@@ -1729,8 +3696,11 @@ mi_call(input(Prompt, Input), StateIn, StateIn) :-
1729
3696
  engine_yield(wait_input(InterpPrompt)),
1730
3697
  % Use get_next_signal helper which handles both dynamic predicate and engine_fetch
1731
3698
  get_next_signal(SessionId, _Signal),
1732
- % Retrieve the input provided via provide_input/2
1733
- ( 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)
1734
3704
  -> retract(session_pending_input(SessionId, Input)),
1735
3705
  add_trace_with_result(SessionId, exit, input, [InterpPrompt], Input, Depth)
1736
3706
  ; Input = "", % Default to empty if no input provided
@@ -1752,6 +3722,27 @@ mi_call(log(Text), StateIn, StateIn) :-
1752
3722
  interpolate_desc(Text, Params, InterpText),
1753
3723
  engine_yield(log(InterpText)).
1754
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
+
1755
3746
  %% ============================================================
1756
3747
  %% Context Stack Management - for memory save/restore
1757
3748
  %% ============================================================
@@ -1878,20 +3869,57 @@ mi_call(false, _StateIn, _StateOut) :-
1878
3869
  %% mi_call(retractall(Head), +StateIn, -StateOut)
1879
3870
  %% Database predicates must operate on the session module so DML-side facts
1880
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
+
1881
3896
  mi_call(assertz(Head), StateIn, StateIn) :-
1882
3897
  get_session_id(SessionId),
3898
+ guard_not_frozen(SessionId),
3899
+ guard_reserved_clause(Head),
1883
3900
  assertz(SessionId:Head).
1884
3901
 
1885
3902
  mi_call(asserta(Head), StateIn, StateIn) :-
1886
3903
  get_session_id(SessionId),
3904
+ guard_not_frozen(SessionId),
3905
+ guard_reserved_clause(Head),
1887
3906
  asserta(SessionId:Head).
1888
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
+
1889
3915
  mi_call(retract(Head), StateIn, StateIn) :-
1890
3916
  get_session_id(SessionId),
3917
+ guard_not_frozen(SessionId),
1891
3918
  retract(SessionId:Head).
1892
3919
 
1893
3920
  mi_call(retractall(Head), StateIn, StateIn) :-
1894
3921
  get_session_id(SessionId),
3922
+ guard_not_frozen(SessionId),
1895
3923
  retractall(SessionId:Head).
1896
3924
 
1897
3925
  %% ============================================================
@@ -2100,6 +4128,7 @@ resolve_workspace_path(RelPath, FullPath) :-
2100
4128
  mi_call(consult(File), StateIn, StateIn) :-
2101
4129
  !,
2102
4130
  get_session_id(SessionId),
4131
+ guard_not_frozen(SessionId),
2103
4132
  resolve_workspace_path(File, FullPath),
2104
4133
  readutil:read_file_to_string(FullPath, Code, []),
2105
4134
  consult_string(Code, SessionId).
@@ -2146,7 +4175,14 @@ consult_clauses(Stream, SessionId) :-
2146
4175
  ).
2147
4176
 
2148
4177
  %% consult_process_term(+Term, +SessionId)
2149
- %% 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
+
2150
4186
  consult_process_term(:-(Directive), SessionId) :-
2151
4187
  % Handle directives - route through mi_call so custom predicates like consult/1 work
2152
4188
  !,
@@ -2165,7 +4201,7 @@ consult_process_term((tool(ToolHead, Description) :- Body), SessionId) :-
2165
4201
  extract_tool_schema(ToolHead, Description, ToolName, Schema),
2166
4202
  assertz(session_user_tools(SessionId, ToolName)),
2167
4203
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
2168
- format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
4204
+ format_tool_source(ToolHead, Description, Body, SourceCode),
2169
4205
  assertz(SessionId:tool_source(ToolName, SourceCode)),
2170
4206
  assertz(SessionId:(tool(ToolHead) :- Body)).
2171
4207
 
@@ -2175,7 +4211,7 @@ consult_process_term((tool(ToolHead) :- Body), SessionId) :-
2175
4211
  extract_tool_schema(ToolHead, none, ToolName, Schema),
2176
4212
  assertz(session_user_tools(SessionId, ToolName)),
2177
4213
  assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
2178
- format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
4214
+ format_tool_source(ToolHead, none, Body, SourceCode),
2179
4215
  assertz(SessionId:tool_source(ToolName, SourceCode)),
2180
4216
  assertz(SessionId:(tool(ToolHead) :- Body)).
2181
4217
 
@@ -2284,6 +4320,22 @@ mi_call(delete_directory(Dir), StateIn, StateIn) :-
2284
4320
  resolve_workspace_path(Dir, FullPath),
2285
4321
  delete_directory(FullPath).
2286
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
+
2287
4339
  %% ============================================================
2288
4340
  %% Catch/Throw - STATE THREADED
2289
4341
  %% ============================================================
@@ -2365,6 +4417,8 @@ is_mi_special_predicate(yield(_)).
2365
4417
  is_mi_special_predicate(log(_)).
2366
4418
  is_mi_special_predicate(system(_)).
2367
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(_)).
2368
4422
  is_mi_special_predicate(task(_)).
2369
4423
  is_mi_special_predicate(task(_,_)).
2370
4424
  is_mi_special_predicate(task(_,_,_)).
@@ -2402,6 +4456,16 @@ is_mi_special_predicate(with_judgment(_,_)).
2402
4456
  is_mi_special_predicate(require_judgment(_,_)).
2403
4457
  %% Context stack management
2404
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(_)).
2405
4469
  is_mi_special_predicate(push_context).
2406
4470
  is_mi_special_predicate(push_context(_)).
2407
4471
  is_mi_special_predicate(pop_context).
@@ -2459,6 +4523,8 @@ is_mi_special_predicate(close(_)).
2459
4523
  is_mi_special_predicate(read_string(_,_,_)).
2460
4524
  is_mi_special_predicate(write(_,_)).
2461
4525
  is_mi_special_predicate(exists_file(_)).
4526
+ is_mi_special_predicate(size_file(_,_)).
4527
+ is_mi_special_predicate(time_file(_,_)).
2462
4528
  is_mi_special_predicate(exists_directory(_)).
2463
4529
  is_mi_special_predicate(directory_files(_,_)).
2464
4530
  is_mi_special_predicate(make_directory(_)).
@@ -2472,6 +4538,105 @@ is_mi_special_predicate(use_module(_,_)).
2472
4538
  is_mi_special_predicate([_]).
2473
4539
  is_mi_special_predicate([_|_]).
2474
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
+
2475
4640
  %% mi_call(Goal, +StateIn, -StateOut)
2476
4641
  %% Catch-all for built-in and user-defined predicates
2477
4642
  %% MUST NOT match special predicates that have dedicated handlers
@@ -2689,6 +4854,17 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
2689
4854
  rebuild_goal(Functor, StringArg, ExpandedRestArgs, ExpandedGoal)
2690
4855
  ).
2691
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
+
2692
4868
  % Other goals - expand arguments
2693
4869
  expand_body_interp(Goal, Bindings, ExpandedGoal) :-
2694
4870
  compound(Goal), !,
@@ -2753,6 +4929,34 @@ expand_rest_args([A|As], Bindings, [EA|EAs]) :-
2753
4929
  expand_body_interp(A, Bindings, EA),
2754
4930
  expand_rest_args(As, Bindings, EAs).
2755
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
+
2756
4960
  %% flatten_conjunction(+A, +B, -Conj)
2757
4961
  %% Flatten nested conjunctions properly
2758
4962
  flatten_conjunction((A1, A2), B, Result) :- !,