deepclause-sdk 0.0.89 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (329) hide show
  1. package/README.md +481 -941
  2. package/dist/agent.d.ts +19 -2
  3. package/dist/agent.d.ts.map +1 -1
  4. package/dist/agent.js +75 -21
  5. package/dist/agent.js.map +1 -1
  6. package/dist/cli/config.d.ts +385 -178
  7. package/dist/cli/config.d.ts.map +1 -1
  8. package/dist/cli/config.js +161 -188
  9. package/dist/cli/config.js.map +1 -1
  10. package/dist/cli/index.d.ts +3 -2
  11. package/dist/cli/index.d.ts.map +1 -1
  12. package/dist/cli/index.js +85 -411
  13. package/dist/cli/index.js.map +1 -1
  14. package/dist/cli/login.d.ts +29 -0
  15. package/dist/cli/login.d.ts.map +1 -0
  16. package/dist/cli/login.js +184 -0
  17. package/dist/cli/login.js.map +1 -0
  18. package/dist/cli/readline.d.ts +6 -0
  19. package/dist/cli/readline.d.ts.map +1 -0
  20. package/dist/cli/readline.js +15 -0
  21. package/dist/cli/readline.js.map +1 -0
  22. package/dist/cli/repl-bin.d.ts +9 -0
  23. package/dist/cli/repl-bin.d.ts.map +1 -0
  24. package/dist/cli/repl-bin.js +16 -0
  25. package/dist/cli/repl-bin.js.map +1 -0
  26. package/dist/cli/repl.d.ts +216 -0
  27. package/dist/cli/repl.d.ts.map +1 -0
  28. package/dist/cli/repl.js +1392 -0
  29. package/dist/cli/repl.js.map +1 -0
  30. package/dist/cli/run.d.ts +17 -18
  31. package/dist/cli/run.d.ts.map +1 -1
  32. package/dist/cli/run.js +202 -183
  33. package/dist/cli/run.js.map +1 -1
  34. package/dist/cli/set-backend.d.ts +53 -0
  35. package/dist/cli/set-backend.d.ts.map +1 -0
  36. package/dist/cli/set-backend.js +259 -0
  37. package/dist/cli/set-backend.js.map +1 -0
  38. package/dist/cli/visualize.d.ts +42 -0
  39. package/dist/cli/visualize.d.ts.map +1 -0
  40. package/dist/cli/visualize.js +150 -0
  41. package/dist/cli/visualize.js.map +1 -0
  42. package/dist/compiler.d.ts +5 -52
  43. package/dist/compiler.d.ts.map +1 -1
  44. package/dist/compiler.js +4 -768
  45. package/dist/compiler.js.map +1 -1
  46. package/dist/index.d.ts +27 -2
  47. package/dist/index.d.ts.map +1 -1
  48. package/dist/index.js +18 -2
  49. package/dist/index.js.map +1 -1
  50. package/dist/prolog/bridge.d.ts +5 -1
  51. package/dist/prolog/bridge.d.ts.map +1 -1
  52. package/dist/prolog/bridge.js +10 -1
  53. package/dist/prolog/bridge.js.map +1 -1
  54. package/dist/prolog-src/deepclause_analysis.pl +10 -2
  55. package/dist/prolog-src/deepclause_mi.pl +2440 -49
  56. package/dist/repl/agent.d.ts +90 -0
  57. package/dist/repl/agent.d.ts.map +1 -0
  58. package/dist/repl/agent.js +410 -0
  59. package/dist/repl/agent.js.map +1 -0
  60. package/dist/repl/auth.d.ts +45 -0
  61. package/dist/repl/auth.d.ts.map +1 -0
  62. package/dist/repl/auth.js +116 -0
  63. package/dist/repl/auth.js.map +1 -0
  64. package/dist/repl/backend.d.ts +321 -0
  65. package/dist/repl/backend.d.ts.map +1 -0
  66. package/dist/repl/backend.js +415 -0
  67. package/dist/repl/backend.js.map +1 -0
  68. package/dist/repl/compaction.d.ts +122 -0
  69. package/dist/repl/compaction.d.ts.map +1 -0
  70. package/dist/repl/compaction.js +318 -0
  71. package/dist/repl/compaction.js.map +1 -0
  72. package/dist/repl/export.d.ts +30 -0
  73. package/dist/repl/export.d.ts.map +1 -0
  74. package/dist/repl/export.js +23 -0
  75. package/dist/repl/export.js.map +1 -0
  76. package/dist/repl/frames.d.ts +21 -0
  77. package/dist/repl/frames.d.ts.map +1 -0
  78. package/dist/repl/frames.js +79 -0
  79. package/dist/repl/frames.js.map +1 -0
  80. package/dist/repl/harness-tools.d.ts +99 -0
  81. package/dist/repl/harness-tools.d.ts.map +1 -0
  82. package/dist/repl/harness-tools.js +192 -0
  83. package/dist/repl/harness-tools.js.map +1 -0
  84. package/dist/repl/highlight.d.ts +30 -0
  85. package/dist/repl/highlight.d.ts.map +1 -0
  86. package/dist/repl/highlight.js +285 -0
  87. package/dist/repl/highlight.js.map +1 -0
  88. package/dist/repl/history.d.ts +234 -0
  89. package/dist/repl/history.d.ts.map +1 -0
  90. package/dist/repl/history.js +661 -0
  91. package/dist/repl/history.js.map +1 -0
  92. package/dist/repl/imagegen.d.ts +29 -0
  93. package/dist/repl/imagegen.d.ts.map +1 -0
  94. package/dist/repl/imagegen.js +84 -0
  95. package/dist/repl/imagegen.js.map +1 -0
  96. package/dist/repl/index.d.ts +130 -0
  97. package/dist/repl/index.d.ts.map +1 -0
  98. package/dist/repl/index.js +112 -0
  99. package/dist/repl/index.js.map +1 -0
  100. package/dist/repl/judge.d.ts +51 -0
  101. package/dist/repl/judge.d.ts.map +1 -0
  102. package/dist/repl/judge.js +58 -0
  103. package/dist/repl/judge.js.map +1 -0
  104. package/dist/repl/markdown.d.ts +25 -0
  105. package/dist/repl/markdown.d.ts.map +1 -0
  106. package/dist/repl/markdown.js +103 -0
  107. package/dist/repl/markdown.js.map +1 -0
  108. package/dist/repl/model-resolver.d.ts +59 -0
  109. package/dist/repl/model-resolver.d.ts.map +1 -0
  110. package/dist/repl/model-resolver.js +156 -0
  111. package/dist/repl/model-resolver.js.map +1 -0
  112. package/dist/repl/picker.d.ts +34 -0
  113. package/dist/repl/picker.d.ts.map +1 -0
  114. package/dist/repl/picker.js +81 -0
  115. package/dist/repl/picker.js.map +1 -0
  116. package/dist/repl/project-context.d.ts +16 -0
  117. package/dist/repl/project-context.d.ts.map +1 -0
  118. package/dist/repl/project-context.js +23 -0
  119. package/dist/repl/project-context.js.map +1 -0
  120. package/dist/repl/render.d.ts +187 -0
  121. package/dist/repl/render.d.ts.map +1 -0
  122. package/dist/repl/render.js +808 -0
  123. package/dist/repl/render.js.map +1 -0
  124. package/dist/repl/session.d.ts +424 -0
  125. package/dist/repl/session.d.ts.map +1 -0
  126. package/dist/repl/session.js +1473 -0
  127. package/dist/repl/session.js.map +1 -0
  128. package/dist/repl/status.d.ts +100 -0
  129. package/dist/repl/status.d.ts.map +1 -0
  130. package/dist/repl/status.js +218 -0
  131. package/dist/repl/status.js.map +1 -0
  132. package/dist/repl/tools.d.ts +53 -0
  133. package/dist/repl/tools.d.ts.map +1 -0
  134. package/dist/repl/tools.js +307 -0
  135. package/dist/repl/tools.js.map +1 -0
  136. package/dist/repl/toolset.d.ts +32 -0
  137. package/dist/repl/toolset.d.ts.map +1 -0
  138. package/dist/repl/toolset.js +82 -0
  139. package/dist/repl/toolset.js.map +1 -0
  140. package/dist/repl/tui/editor.d.ts +17 -0
  141. package/dist/repl/tui/editor.d.ts.map +1 -0
  142. package/dist/repl/tui/editor.js +39 -0
  143. package/dist/repl/tui/editor.js.map +1 -0
  144. package/dist/repl/tui/status-bar.d.ts +26 -0
  145. package/dist/repl/tui/status-bar.d.ts.map +1 -0
  146. package/dist/repl/tui/status-bar.js +47 -0
  147. package/dist/repl/tui/status-bar.js.map +1 -0
  148. package/dist/repl/tui/theme.d.ts +30 -0
  149. package/dist/repl/tui/theme.d.ts.map +1 -0
  150. package/dist/repl/tui/theme.js +81 -0
  151. package/dist/repl/tui/theme.js.map +1 -0
  152. package/dist/repl/tui/transcript.d.ts +41 -0
  153. package/dist/repl/tui/transcript.d.ts.map +1 -0
  154. package/dist/repl/tui/transcript.js +139 -0
  155. package/dist/repl/tui/transcript.js.map +1 -0
  156. package/dist/runner.d.ts +143 -13
  157. package/dist/runner.d.ts.map +1 -1
  158. package/dist/runner.js +532 -290
  159. package/dist/runner.js.map +1 -1
  160. package/dist/sdk.d.ts.map +1 -1
  161. package/dist/sdk.js +0 -32
  162. package/dist/sdk.js.map +1 -1
  163. package/dist/system/assets/docs/AGENT_PROMPT.md +1447 -0
  164. package/dist/system/assets/docs/DML_AUTHORING.md +195 -0
  165. package/dist/system/assets/docs/DML_REFERENCE.md +344 -72
  166. package/dist/system/assets/docs/HARNESS_EXAMPLES.md +172 -0
  167. package/dist/system/assets/docs/HARNESS_PATTERNS.md +562 -0
  168. package/dist/system/assets/docs/META_PROMPT.md +68 -0
  169. package/dist/system/assets/docs/PROLOG_SNIPPETS.md +117 -0
  170. package/dist/system/assets/index.d.ts +9 -17
  171. package/dist/system/assets/index.d.ts.map +1 -1
  172. package/dist/system/assets/index.js +26 -85
  173. package/dist/system/assets/index.js.map +1 -1
  174. package/dist/system/runtime/agentvm-manager.d.ts +9 -2
  175. package/dist/system/runtime/agentvm-manager.d.ts.map +1 -1
  176. package/dist/system/runtime/agentvm-manager.js +70 -44
  177. package/dist/system/runtime/agentvm-manager.js.map +1 -1
  178. package/dist/system/runtime/media.d.ts +23 -0
  179. package/dist/system/runtime/media.d.ts.map +1 -0
  180. package/dist/system/runtime/media.js +55 -0
  181. package/dist/system/runtime/media.js.map +1 -0
  182. package/dist/system/runtime/shell-limits.d.ts +21 -0
  183. package/dist/system/runtime/shell-limits.d.ts.map +1 -0
  184. package/dist/system/runtime/shell-limits.js +39 -0
  185. package/dist/system/runtime/shell-limits.js.map +1 -0
  186. package/dist/system/runtime/shell-manager.d.ts +8 -3
  187. package/dist/system/runtime/shell-manager.d.ts.map +1 -1
  188. package/dist/system/runtime/shell-manager.js +56 -39
  189. package/dist/system/runtime/shell-manager.js.map +1 -1
  190. package/dist/system/runtime/shell-tool-events.js +5 -0
  191. package/dist/system/runtime/shell-tool-events.js.map +1 -1
  192. package/dist/system/runtime/token-usage.d.ts +2 -0
  193. package/dist/system/runtime/token-usage.d.ts.map +1 -1
  194. package/dist/system/runtime/token-usage.js +7 -0
  195. package/dist/system/runtime/token-usage.js.map +1 -1
  196. package/dist/tools.d.ts +3 -21
  197. package/dist/tools.d.ts.map +1 -1
  198. package/dist/tools.js +14 -168
  199. package/dist/tools.js.map +1 -1
  200. package/dist/types.d.ts +136 -119
  201. package/dist/types.d.ts.map +1 -1
  202. package/dist/validate.d.ts +19 -0
  203. package/dist/validate.d.ts.map +1 -0
  204. package/dist/validate.js +338 -0
  205. package/dist/validate.js.map +1 -0
  206. package/dist/visualize/ascii.d.ts +55 -0
  207. package/dist/visualize/ascii.d.ts.map +1 -0
  208. package/dist/visualize/ascii.js +242 -0
  209. package/dist/visualize/ascii.js.map +1 -0
  210. package/dist/visualize/display.d.ts +24 -0
  211. package/dist/visualize/display.d.ts.map +1 -0
  212. package/dist/visualize/display.js +29 -0
  213. package/dist/visualize/display.js.map +1 -0
  214. package/dist/visualize/graphics.d.ts +35 -0
  215. package/dist/visualize/graphics.d.ts.map +1 -0
  216. package/dist/visualize/graphics.js +96 -0
  217. package/dist/visualize/graphics.js.map +1 -0
  218. package/dist/visualize/image.d.ts +38 -0
  219. package/dist/visualize/image.d.ts.map +1 -0
  220. package/dist/visualize/image.js +95 -0
  221. package/dist/visualize/image.js.map +1 -0
  222. package/dist/visualize/index.d.ts +59 -0
  223. package/dist/visualize/index.d.ts.map +1 -0
  224. package/dist/visualize/index.js +198 -0
  225. package/dist/visualize/index.js.map +1 -0
  226. package/dist/visualize/raster.d.ts +23 -0
  227. package/dist/visualize/raster.d.ts.map +1 -0
  228. package/dist/visualize/raster.js +47 -0
  229. package/dist/visualize/raster.js.map +1 -0
  230. package/dist/visualize/svg.d.ts +39 -0
  231. package/dist/visualize/svg.d.ts.map +1 -0
  232. package/dist/visualize/svg.js +148 -0
  233. package/dist/visualize/svg.js.map +1 -0
  234. package/dist/visualize/transcode.d.ts +13 -0
  235. package/dist/visualize/transcode.d.ts.map +1 -0
  236. package/dist/visualize/transcode.js +40 -0
  237. package/dist/visualize/transcode.js.map +1 -0
  238. package/docs/images/repl.png +0 -0
  239. package/package.json +19 -15
  240. package/src/prolog-src/deepclause_analysis.pl +10 -2
  241. package/src/prolog-src/deepclause_mi.pl +2440 -49
  242. package/dist/cli/commands.d.ts +0 -42
  243. package/dist/cli/commands.d.ts.map +0 -1
  244. package/dist/cli/commands.js +0 -246
  245. package/dist/cli/commands.js.map +0 -1
  246. package/dist/cli/compile.d.ts +0 -99
  247. package/dist/cli/compile.d.ts.map +0 -1
  248. package/dist/cli/compile.js +0 -363
  249. package/dist/cli/compile.js.map +0 -1
  250. package/dist/cli/default-skills.d.ts +0 -10
  251. package/dist/cli/default-skills.d.ts.map +0 -1
  252. package/dist/cli/default-skills.js +0 -206
  253. package/dist/cli/default-skills.js.map +0 -1
  254. package/dist/cli/interactive.d.ts +0 -2
  255. package/dist/cli/interactive.d.ts.map +0 -1
  256. package/dist/cli/interactive.js +0 -20
  257. package/dist/cli/interactive.js.map +0 -1
  258. package/dist/cli/mcp.d.ts +0 -56
  259. package/dist/cli/mcp.d.ts.map +0 -1
  260. package/dist/cli/mcp.js +0 -138
  261. package/dist/cli/mcp.js.map +0 -1
  262. package/dist/cli/prompt.d.ts +0 -2
  263. package/dist/cli/prompt.d.ts.map +0 -1
  264. package/dist/cli/prompt.js +0 -2
  265. package/dist/cli/prompt.js.map +0 -1
  266. package/dist/cli/search.d.ts +0 -19
  267. package/dist/cli/search.d.ts.map +0 -1
  268. package/dist/cli/search.js +0 -189
  269. package/dist/cli/search.js.map +0 -1
  270. package/dist/cli/tool-args.d.ts +0 -2
  271. package/dist/cli/tool-args.d.ts.map +0 -1
  272. package/dist/cli/tool-args.js +0 -39
  273. package/dist/cli/tool-args.js.map +0 -1
  274. package/dist/cli/tools.d.ts +0 -36
  275. package/dist/cli/tools.d.ts.map +0 -1
  276. package/dist/cli/tools.js +0 -229
  277. package/dist/cli/tools.js.map +0 -1
  278. package/dist/cli/tui/index.d.ts +0 -22
  279. package/dist/cli/tui/index.d.ts.map +0 -1
  280. package/dist/cli/tui/index.js +0 -29
  281. package/dist/cli/tui/index.js.map +0 -1
  282. package/dist/cli/tui.d.ts +0 -167
  283. package/dist/cli/tui.d.ts.map +0 -1
  284. package/dist/cli/tui.js +0 -5228
  285. package/dist/cli/tui.js.map +0 -1
  286. package/dist/compiler_prompt.d.ts +0 -20
  287. package/dist/compiler_prompt.d.ts.map +0 -1
  288. package/dist/compiler_prompt.js +0 -870
  289. package/dist/compiler_prompt.js.map +0 -1
  290. package/dist/deepclause-cli.skill +0 -0
  291. package/dist/prolog-src/deepclause_mi.pl.bak +0 -570
  292. package/dist/system/assets/docs/CONDUCTOR_PROMPT.md +0 -105
  293. package/dist/system/assets/docs/DML_COMPILER_PROMPT.md +0 -700
  294. package/dist/system/assets/docs/TUI.md +0 -150
  295. package/dist/system/assets/recipes/deepclause-coding-workflow/SKILL.md +0 -100
  296. package/dist/system/assets/skills/conductor.dml +0 -76
  297. package/dist/system/assets/skills/deep-planner.dml +0 -412
  298. package/dist/system/assets/skills/plan.dml +0 -429
  299. package/dist/system/assets/skills/security-planner.dml +0 -337
  300. package/dist/system/assets/skills/skill-creator.dml +0 -187
  301. package/dist/system/runtime/catalog-recipes.d.ts +0 -24
  302. package/dist/system/runtime/catalog-recipes.d.ts.map +0 -1
  303. package/dist/system/runtime/catalog-recipes.js +0 -273
  304. package/dist/system/runtime/catalog-recipes.js.map +0 -1
  305. package/dist/system/runtime/catalog-skills.d.ts +0 -50
  306. package/dist/system/runtime/catalog-skills.d.ts.map +0 -1
  307. package/dist/system/runtime/catalog-skills.js +0 -152
  308. package/dist/system/runtime/catalog-skills.js.map +0 -1
  309. package/dist/system/runtime/conductor.d.ts +0 -105
  310. package/dist/system/runtime/conductor.d.ts.map +0 -1
  311. package/dist/system/runtime/conductor.js +0 -732
  312. package/dist/system/runtime/conductor.js.map +0 -1
  313. package/dist/system/runtime/console-capture.d.ts +0 -9
  314. package/dist/system/runtime/console-capture.d.ts.map +0 -1
  315. package/dist/system/runtime/console-capture.js +0 -67
  316. package/dist/system/runtime/console-capture.js.map +0 -1
  317. package/dist/system/runtime/dml-executor.d.ts +0 -52
  318. package/dist/system/runtime/dml-executor.d.ts.map +0 -1
  319. package/dist/system/runtime/dml-executor.js +0 -203
  320. package/dist/system/runtime/dml-executor.js.map +0 -1
  321. package/dist/system/runtime/runtime-tools.d.ts +0 -31
  322. package/dist/system/runtime/runtime-tools.d.ts.map +0 -1
  323. package/dist/system/runtime/runtime-tools.js +0 -312
  324. package/dist/system/runtime/runtime-tools.js.map +0 -1
  325. package/dist/system/runtime/skill-creator.d.ts +0 -38
  326. package/dist/system/runtime/skill-creator.d.ts.map +0 -1
  327. package/dist/system/runtime/skill-creator.js +0 -763
  328. package/dist/system/runtime/skill-creator.js.map +0 -1
  329. package/src/prolog-src/deepclause_mi.pl.bak +0 -570
package/dist/runner.js CHANGED
@@ -1,22 +1,21 @@
1
1
  /**
2
2
  * DML Runner - Executes DML code using SWI-Prolog WASM
3
3
  */
4
- import * as fs from 'fs/promises';
5
- import * as path from 'path';
6
4
  import { mountWorkspace } from './prolog/loader.js';
7
5
  import { runAgentLoop } from './agent.js';
8
6
  import { judgeAnswersToValues, judgeCacheKey, normalizeJudgeState, parseJudgeQuestions, validateJudgeAnswers, valuesToPrologList } from './judge/marshal.js';
9
- import { executeCompactor, getCompactionBindings, resolveCompactionOptions, } from './compaction.js';
10
7
  import { buildToolCompletionEvent, buildToolFailureEvent, buildToolStartEvent, } from './system/runtime/shell-tool-events.js';
11
8
  import { checkToolPolicy } from './tools.js';
12
9
  import { generateLlmReply, sampleSingleToken } from './prolog/bridge.js';
13
- import { recordTokenUsage } from './system/runtime/token-usage.js';
14
10
  import { buildReasoningProviderOptions } from './system/config/model-database.js';
11
+ import { resolveMediaRefs } from './system/runtime/media.js';
12
+ import { resolveMedia } from './visualize/image.js';
13
+ import { parseModelSpec } from './repl/model-resolver.js';
15
14
  /**
16
15
  * Convert swipl-wasm value to plain JavaScript value
17
16
  * Handles PrologString, PrologAtom, PrologList, etc.
18
17
  */
19
- function toJsValue(value) {
18
+ export function toJsValue(value) {
20
19
  if (value === null || value === undefined) {
21
20
  return value;
22
21
  }
@@ -60,45 +59,45 @@ function toJsValue(value) {
60
59
  }
61
60
  return value;
62
61
  }
63
- function detectProvider(model) {
64
- const lower = model.toLowerCase();
65
- if (lower.includes('gpt') || lower.includes('o1') || lower.includes('o3')) {
66
- return 'openai';
67
- }
68
- if (lower.includes('claude')) {
69
- return 'anthropic';
70
- }
71
- if (lower.includes('gemini') || lower.includes('palm')) {
72
- return 'google';
62
+ /**
63
+ * Decode the `bindings` payload of a goal-mode `solution` event.
64
+ *
65
+ * `collect_bindings/2` yields a list of `Name-Text` pairs. swipl-wasm encodes
66
+ * that compound as `{ '$t': 't', '-': [[Name, Text]] }`; a dict/array form is
67
+ * also accepted for forward compatibility.
68
+ */
69
+ export function decodeSolutionBindings(raw) {
70
+ const normalized = toJsValue(raw);
71
+ if (!Array.isArray(normalized)) {
72
+ return [];
73
73
  }
74
- return 'openrouter';
75
- }
76
- function createChildAbortController(parentSignal, timeoutMs) {
77
- const controller = new AbortController();
78
- const disposers = [];
79
- if (parentSignal) {
80
- if (parentSignal.aborted) {
81
- controller.abort(parentSignal.reason);
74
+ const bindings = [];
75
+ for (const item of normalized) {
76
+ const decoded = toJsValue(item);
77
+ if (decoded && typeof decoded === 'object') {
78
+ const compound = decoded;
79
+ const args = compound['-'];
80
+ if (Array.isArray(args) && Array.isArray(args[0]) && args[0].length >= 2) {
81
+ const [name, text] = args[0];
82
+ bindings.push({ name: String(toJsValue(name) ?? ''), value: String(toJsValue(text) ?? '') });
83
+ continue;
84
+ }
85
+ if ('name' in compound || 'value' in compound) {
86
+ bindings.push({
87
+ name: String(toJsValue(compound.name) ?? ''),
88
+ value: String(toJsValue(compound.value) ?? ''),
89
+ });
90
+ continue;
91
+ }
82
92
  }
83
- else {
84
- const onAbort = () => controller.abort(parentSignal.reason);
85
- parentSignal.addEventListener('abort', onAbort, { once: true });
86
- disposers.push(() => parentSignal.removeEventListener('abort', onAbort));
93
+ if (Array.isArray(decoded) && decoded.length >= 2) {
94
+ bindings.push({
95
+ name: String(toJsValue(decoded[0]) ?? ''),
96
+ value: String(toJsValue(decoded[1]) ?? ''),
97
+ });
87
98
  }
88
99
  }
89
- let timeout;
90
- if (timeoutMs > 0) {
91
- timeout = setTimeout(() => controller.abort(new Error(`Compactor timed out after ${timeoutMs}ms`)), timeoutMs);
92
- disposers.push(() => clearTimeout(timeout));
93
- }
94
- return {
95
- signal: controller.signal,
96
- dispose: () => {
97
- for (const dispose of disposers) {
98
- dispose();
99
- }
100
- },
101
- };
100
+ return bindings;
102
101
  }
103
102
  export function decodeAgentOutputVars(rawOutputVars) {
104
103
  const normalized = toJsValue(rawOutputVars);
@@ -142,11 +141,18 @@ function prologTypeToJsonType(prologType) {
142
141
  export class DMLRunner {
143
142
  swipl;
144
143
  options;
145
- engine = null;
146
144
  sessionId = '';
147
145
  currentMemory = [];
148
146
  stepCounter = 0;
149
147
  judgeCache = new Map();
148
+ /**
149
+ * Runner-owned frame table: FrameId -> { frame, engine }.
150
+ * The Prolog session owns the authoritative engine handles
151
+ * (session_frame_engine/2); this map mirrors them for the driver.
152
+ */
153
+ frames = new Map();
154
+ /** FrameId of the session's default query frame (created by createEngine). */
155
+ defaultFrameId = null;
150
156
  constructor(swipl, options) {
151
157
  this.swipl = swipl;
152
158
  this.options = options;
@@ -191,94 +197,18 @@ export class DMLRunner {
191
197
  yield { type: 'error', content: parseResult.error };
192
198
  return;
193
199
  }
194
- // Create cooperative engine with args
200
+ // Create cooperative engine with args (registers the default query frame)
195
201
  const engineResult = this.createEngine(memoryId, args, params);
196
202
  if (engineResult.error) {
197
203
  yield { type: 'error', content: engineResult.error };
198
204
  return;
199
205
  }
200
- // Cooperative execution loop
201
- while (true) {
202
- // Check for abort
203
- if (options.signal?.aborted) {
204
- const reason = options.signal.reason;
205
- const abortMessage = reason instanceof Error
206
- ? reason.message
207
- : typeof reason === 'string' && reason.trim().length > 0
208
- ? reason
209
- : 'Execution aborted';
210
- yield { type: 'error', content: abortMessage };
211
- break;
212
- }
213
- // Step the engine
214
- const step = this.stepEngine();
215
- if (process.env.DEBUG_STEPS) {
216
- console.log('[MAIN_LOOP] step:', step.status, step.content?.substring(0, 50));
217
- }
218
- switch (step.status) {
219
- case 'output':
220
- if (step.content) {
221
- if (process.env.DEBUG_STEPS) {
222
- console.log('[MAIN_LOOP] yielding output:', step.content.substring(0, 50));
223
- }
224
- yield { type: 'output', content: step.content };
225
- }
226
- break;
227
- case 'log':
228
- if (step.content) {
229
- yield { type: 'log', content: step.content };
230
- }
231
- break;
232
- case 'answer':
233
- yield { type: 'answer', content: step.content };
234
- break;
235
- case 'request_agent_loop':
236
- // Run agent loop for task() predicate
237
- yield* this.handleAgentLoop(step.payload, memoryId, options);
238
- break;
239
- case 'request_exec':
240
- // Execute external tool
241
- yield* this.handleExec(step.payload, options);
242
- break;
243
- case 'request_sample_token':
244
- yield* this.handleSampleToken(step.payload, options);
245
- break;
246
- case 'request_llm':
247
- yield* this.handleLlm(step.payload, options);
248
- break;
249
- case 'request_judgment':
250
- yield* this.handleJudgment(step.payload, options);
251
- break;
252
- case 'wait_input':
253
- // Request user input
254
- yield { type: 'input_required', prompt: step.prompt };
255
- const input = await options.onInputRequired(step.prompt ?? '');
256
- this.provideInput(input);
257
- break;
258
- case 'finished':
259
- // Include trace data if trace mode was enabled
260
- if (this.options.trace && step.payload) {
261
- const traceData = toJsValue(step.payload.trace);
262
- yield { type: 'finished', trace: Array.isArray(traceData) ? traceData : undefined };
263
- }
264
- else {
265
- yield { type: 'finished' };
266
- }
267
- return;
268
- case 'error':
269
- if (this.options.trace && step.payload) {
270
- const traceData = toJsValue(step.payload.trace);
271
- yield { type: 'error', content: step.content, trace: Array.isArray(traceData) ? traceData : undefined };
272
- }
273
- else {
274
- yield { type: 'error', content: step.content };
275
- }
276
- return;
277
- default:
278
- yield { type: 'error', content: `Unknown step status: ${step.status}` };
279
- return;
280
- }
206
+ const frame = this.defaultFrameId ? this.frames.get(this.defaultFrameId)?.frame : undefined;
207
+ if (!frame) {
208
+ yield { type: 'error', content: 'Failed to register default query frame' };
209
+ return;
281
210
  }
211
+ yield* this.stepLoop(frame, options);
282
212
  }
283
213
  catch (error) {
284
214
  const message = error instanceof Error ? error.message : String(error);
@@ -289,6 +219,185 @@ export class DMLRunner {
289
219
  this.cleanup();
290
220
  }
291
221
  }
222
+ /**
223
+ * Drive a single frame until it finishes or errors, yielding its events.
224
+ *
225
+ * This is the extracted, reentrant form of the original `while(true)` loop.
226
+ * `run()` calls it for the default query frame; `runFrame` calls it for
227
+ * nested frames. Because frames are read from the Prolog frame table via
228
+ * `step_frame/4`, stepping a different frame never disturbs a suspended
229
+ * parent's engine.
230
+ */
231
+ async *stepLoop(frame, options) {
232
+ for await (const event of this.stepLoopInner(frame, options)) {
233
+ // Preserve frame metadata already attached by a nested frame; otherwise
234
+ // stamp this frame so the renderer can attribute output correctly.
235
+ yield {
236
+ ...event,
237
+ frameId: event.frameId ?? frame.id,
238
+ depth: event.depth ?? frame.depth,
239
+ frameKind: event.frameKind ?? frame.kind,
240
+ };
241
+ }
242
+ }
243
+ async *stepLoopInner(frame, options) {
244
+ // Cooperative execution loop
245
+ while (true) {
246
+ // Check for abort
247
+ if (options.signal?.aborted) {
248
+ const reason = options.signal.reason;
249
+ const abortMessage = reason instanceof Error
250
+ ? reason.message
251
+ : typeof reason === 'string' && reason.trim().length > 0
252
+ ? reason
253
+ : 'Execution aborted';
254
+ yield { type: 'error', content: abortMessage };
255
+ break;
256
+ }
257
+ // Step the frame
258
+ const step = this.stepFrame(frame.id);
259
+ if (process.env.DEBUG_STEPS) {
260
+ console.log('[MAIN_LOOP] step:', step.status, step.content?.substring(0, 50));
261
+ }
262
+ switch (step.status) {
263
+ case 'output':
264
+ if (step.content) {
265
+ if (process.env.DEBUG_STEPS) {
266
+ console.log('[MAIN_LOOP] yielding output:', step.content.substring(0, 50));
267
+ }
268
+ yield { type: 'output', content: step.content };
269
+ }
270
+ break;
271
+ case 'media': {
272
+ // `output(media(...))` — resolve to a base64 PNG for inline display.
273
+ const payload = (step.payload ?? {});
274
+ const media = {
275
+ path: typeof payload.path === 'string' && payload.path ? payload.path : undefined,
276
+ mimeType: typeof payload.mimeType === 'string' ? payload.mimeType : 'image/png',
277
+ kind: typeof payload.kind === 'string' ? payload.kind : undefined,
278
+ data: typeof payload.data === 'string' && payload.data ? payload.data : undefined,
279
+ };
280
+ const resolved = await resolveMedia(options.workspacePath ?? process.cwd(), media);
281
+ yield {
282
+ type: 'media',
283
+ media: {
284
+ path: media.path,
285
+ mimeType: media.mimeType,
286
+ kind: media.kind,
287
+ data: resolved?.pngBase64,
288
+ },
289
+ };
290
+ break;
291
+ }
292
+ case 'log':
293
+ if (step.content) {
294
+ yield { type: 'log', content: step.content };
295
+ }
296
+ break;
297
+ case 'answer':
298
+ yield { type: 'answer', content: step.content };
299
+ break;
300
+ case 'solution': {
301
+ const payload = (step.payload ?? {});
302
+ yield {
303
+ type: 'solution',
304
+ solution: {
305
+ bindings: decodeSolutionBindings(payload.bindings),
306
+ deterministic: String(toJsValue(payload.deterministic) ?? 'false') === 'true',
307
+ },
308
+ };
309
+ break;
310
+ }
311
+ case 'request_agent_loop':
312
+ // Run agent loop for task() predicate. The frame kind decides the
313
+ // tool set: `meta` frames get the host meta tools and hide the
314
+ // harness's own DML tools (proposal §9.2). Budget exhaustion (and any
315
+ // other agent-loop error) is surfaced as an error event.
316
+ try {
317
+ yield* this.handleAgentLoop(step.payload, '', options, frame);
318
+ }
319
+ catch (error) {
320
+ yield { type: 'error', content: error instanceof Error ? error.message : String(error) };
321
+ return;
322
+ }
323
+ break;
324
+ case 'meta_result': {
325
+ const payload = (step.payload ?? {});
326
+ yield {
327
+ type: 'meta_result',
328
+ metaResult: {
329
+ code: String(toJsValue(payload.code) ?? ''),
330
+ answer: String(toJsValue(payload.answer) ?? ''),
331
+ },
332
+ };
333
+ break;
334
+ }
335
+ case 'request_exec':
336
+ // Execute external tool
337
+ yield* this.handleExec(step.payload, options);
338
+ break;
339
+ case 'request_sample_token':
340
+ yield* this.handleSampleToken(step.payload, options);
341
+ break;
342
+ case 'request_llm':
343
+ yield* this.handleLlm(step.payload, options);
344
+ break;
345
+ case 'request_judgment':
346
+ yield* this.handleJudgment(step.payload, options);
347
+ break;
348
+ case 'wait_input': {
349
+ // Request user input
350
+ yield { type: 'input_required', prompt: step.prompt };
351
+ const input = await options.onInputRequired(step.prompt ?? '');
352
+ if (input === undefined || input === null) {
353
+ this.cancelInput();
354
+ }
355
+ else {
356
+ this.provideInput(input);
357
+ }
358
+ break;
359
+ }
360
+ case 'finished':
361
+ // Include trace data if trace mode was enabled
362
+ if (this.options.trace && step.payload) {
363
+ const traceData = toJsValue(step.payload.trace);
364
+ yield { type: 'finished', trace: Array.isArray(traceData) ? traceData : undefined };
365
+ }
366
+ else {
367
+ yield { type: 'finished' };
368
+ }
369
+ return;
370
+ case 'error':
371
+ if (this.options.trace && step.payload) {
372
+ const traceData = toJsValue(step.payload.trace);
373
+ yield { type: 'error', content: step.content, trace: Array.isArray(traceData) ? traceData : undefined };
374
+ }
375
+ else {
376
+ yield { type: 'error', content: step.content };
377
+ }
378
+ return;
379
+ default:
380
+ yield { type: 'error', content: `Unknown step status: ${step.status}` };
381
+ return;
382
+ }
383
+ }
384
+ }
385
+ /**
386
+ * Run a specific frame to completion, yielding its events.
387
+ *
388
+ * This is the reentrant entry point used by nested execution (the REPL's
389
+ * `execute` meta tool): the caller builds a child frame with
390
+ * `createNestedFrame`, runs it with this method while the parent frame is
391
+ * suspended, then posts the result back to the parent.
392
+ */
393
+ async *runFrame(frameId, options) {
394
+ const frame = this.frames.get(frameId)?.frame;
395
+ if (!frame) {
396
+ yield { type: 'error', content: `Unknown frame: ${frameId}` };
397
+ return;
398
+ }
399
+ yield* this.stepLoop(frame, options);
400
+ }
292
401
  /**
293
402
  * Parse DML code into Prolog clauses
294
403
  */
@@ -381,10 +490,15 @@ export class DMLRunner {
381
490
  createEngine(memoryId, args, params) {
382
491
  try {
383
492
  const initialMemory = this.serializeMemoryMessages(this.currentMemory);
384
- const result = this.query(`deepclause_mi:create_engine('${this.sessionId}', '${memoryId}', ${args}, ${params}, [${initialMemory}], Engine)`);
385
- if (result && result.Engine) {
386
- this.engine = result.Engine;
387
- return {};
493
+ // create_engine/6 registers the default `query` frame; default_frame/2
494
+ // returns its FrameId so the runner can address it explicitly.
495
+ const result = this.query(`deepclause_mi:create_engine('${this.sessionId}', '${memoryId}', ${args}, ${params}, [${initialMemory}], Engine), deepclause_mi:default_frame('${this.sessionId}', FrameId)`);
496
+ if (result && result.Engine && result.FrameId) {
497
+ const frameId = String(toJsValue(result.FrameId));
498
+ const frame = { id: frameId, kind: 'query', depth: 0 };
499
+ this.frames.set(frameId, { frame, engine: result.Engine });
500
+ this.defaultFrameId = frameId;
501
+ return { frameId };
388
502
  }
389
503
  return { error: 'Failed to create engine' };
390
504
  }
@@ -394,11 +508,93 @@ export class DMLRunner {
394
508
  }
395
509
  }
396
510
  /**
397
- * Step the cooperative engine
511
+ * Create a nested frame that runs a Prolog goal in this session.
512
+ *
513
+ * `goalTerm` is inserted verbatim into create_frame/6, so callers MUST pass
514
+ * a term built with the runner's `toPrologTerm` escaping or parsed from DML
515
+ * via `read_term` — never raw user text (foot gun #18). `stateTerm` is an
516
+ * optional Prolog `state{...}` term; the default is an empty state with no
517
+ * gas limit.
518
+ *
519
+ * Returns the registered frame, or null on failure. The caller is
520
+ * responsible for running (`runFrame`) and ending (`endFrame`) it.
398
521
  */
399
- stepEngine() {
522
+ createNestedFrame(params) {
523
+ const depth = params.depth ?? 1;
524
+ const stateTerm = params.stateTerm
525
+ ?? 'state{memory: [], params: params{}, context_stack: [], depth: 0}';
526
+ const result = this.query(`deepclause_mi:create_frame('${this.sessionId}', ${params.kind}, ${depth}, ${params.goalTerm}, ${stateTerm}, FrameId)`);
527
+ if (!result || !result.FrameId) {
528
+ return null;
529
+ }
530
+ const id = String(toJsValue(result.FrameId));
531
+ const frame = { id, kind: params.kind, depth };
532
+ this.frames.set(id, { frame, engine: null });
533
+ return frame;
534
+ }
535
+ /** End a frame, destroying its engine in the Prolog session. */
536
+ endFrame(frameId) {
537
+ this.query(`deepclause_mi:end_frame('${frameId}')`);
538
+ this.frames.delete(frameId);
539
+ if (this.defaultFrameId === frameId) {
540
+ this.defaultFrameId = null;
541
+ }
542
+ }
543
+ /**
544
+ * Bind this runner to an existing session id. Used by the REPL's
545
+ * DMLSession, whose session outlives a single `run()`.
546
+ */
547
+ bindSession(sessionId) {
548
+ this.sessionId = sessionId;
549
+ }
550
+ /**
551
+ * Adopt the session's default frame (for example one created by the REPL's
552
+ * `query_start/5`) into the runner's frame table so `runFrame` can drive it.
553
+ */
554
+ registerDefaultFrame(kind, depth = 0) {
555
+ const result = this.query(`deepclause_mi:default_frame('${this.sessionId}', FrameId)`);
556
+ if (!result || !result.FrameId) {
557
+ return null;
558
+ }
559
+ const id = String(toJsValue(result.FrameId));
560
+ const frame = { id, kind, depth };
561
+ this.frames.set(id, { frame, engine: null });
562
+ this.defaultFrameId = id;
563
+ return frame;
564
+ }
565
+ /**
566
+ * Adopt a Prolog-created frame into the runner's frame table. Used by the
567
+ * REPL for `execute` child frames (`repl_execute_frame/4`).
568
+ */
569
+ registerFrame(frameId, kind, depth = 0) {
570
+ const frame = { id: frameId, kind, depth };
571
+ this.frames.set(frameId, { frame, engine: null });
572
+ return frame;
573
+ }
574
+ /**
575
+ * Drop all runner-side frame bookkeeping without touching Prolog. Used after
576
+ * the REPL clears a session (the Prolog side already ended every frame).
577
+ */
578
+ clearFrames() {
579
+ this.frames.clear();
580
+ this.defaultFrameId = null;
581
+ }
582
+ /**
583
+ * Step a specific frame exactly once and return its raw step result.
584
+ * Used by the reentrant driver and nested-execution tests.
585
+ */
586
+ stepFrameOnce(frameId) {
587
+ if (!this.frames.has(frameId)) {
588
+ return null;
589
+ }
590
+ return this.stepFrame(frameId);
591
+ }
592
+ /**
593
+ * Step the cooperative engine for a frame
594
+ */
595
+ stepFrame(frameId) {
400
596
  try {
401
- const result = this.query(`deepclause_mi:step_engine('${this.sessionId}', Status, Content, Payload)`);
597
+ const result = this.query(`deepclause_mi:step_frame('${frameId}', Status, Content, Payload)`);
402
598
  // Convert swipl-wasm values to plain JS
403
599
  const status = toJsValue(result?.Status);
404
600
  const content = toJsValue(result?.Content);
@@ -419,13 +615,15 @@ export class DMLRunner {
419
615
  * Handle agent loop request (task() predicate)
420
616
  */
421
617
  async *handleAgentLoop(payload, _memoryId, // No longer used - memory comes from payload
422
- options) {
618
+ options, _frame) {
423
619
  const rawPayload = payload;
424
620
  const taskDescription = String(toJsValue(rawPayload.taskDescription) ?? '');
425
621
  const stepId = `step_${this.stepCounter++}`;
426
622
  yield { type: 'task_activity', taskState: 'started', taskDescription, taskId: stepId };
427
623
  let stepSucceeded = null;
428
624
  try {
625
+ // The per-model-call budget is enforced inside the agent loop (before each
626
+ // model call) so it also covers multi-iteration turns.
429
627
  // Ensure outputVars is an array of either strings or TypedVar objects
430
628
  const outputVars = decodeAgentOutputVars(rawPayload.outputVars);
431
629
  // Parse userTools - now contains schema info as array of tool_info dicts
@@ -455,14 +653,48 @@ export class DMLRunner {
455
653
  };
456
654
  // Extract memory from payload (now passed via state threading)
457
655
  const preparedMemory = this.extractMemoryFromPayload(rawPayload);
656
+ // A task with a `media(X)` typed output generates an image instead of running
657
+ // a text agent turn; the harness displays it only when it calls output(media(X)).
658
+ const mediaOutputs = outputVars.filter((v) => typeof v !== 'string' && v.type === 'media');
659
+ if (mediaOutputs.length > 0) {
660
+ const imageBackend = this.options.imageBackend;
661
+ if (!imageBackend) {
662
+ yield { type: 'error', content: 'image generation is unavailable: no image backend is configured' };
663
+ this.postAgentResult({ success: false, outputs: [], variables: {}, messages: preparedMemory }, preparedMemory);
664
+ stepSucceeded = false;
665
+ return;
666
+ }
667
+ const refs = [];
668
+ for (const message of preparedMemory) {
669
+ for (const ref of message.media ?? []) {
670
+ refs.push(ref);
671
+ }
672
+ }
673
+ const generated = await imageBackend.generate({
674
+ prompt: taskDescription,
675
+ refs: refs.length > 0 ? refs : undefined,
676
+ signal: options.signal,
677
+ });
678
+ const variables = {};
679
+ for (const output of mediaOutputs) {
680
+ variables[output.name] = generated.path;
681
+ }
682
+ this.postAgentResult({ success: true, outputs: [], variables, messages: preparedMemory }, preparedMemory);
683
+ stepSucceeded = true;
684
+ return;
685
+ }
458
686
  // Extract reasoning effort and recipe context (from with_reasoning/with_recipe scoping)
459
687
  const reasoningEffort = String(toJsValue(rawPayload.reasoningEffort) ?? 'none');
460
688
  const recipeContext = String(toJsValue(rawPayload.recipeContext) ?? '');
689
+ // Per-call model override from with_model/2 (undefined = the session model).
690
+ const scopedModel = await this.resolveScope(rawPayload.modelSpec);
461
691
  // Build provider options — merge reasoning effort if set
462
692
  const baseProviderOptions = this.options.providerOptions ?? {};
463
693
  let effectiveProviderOptions = baseProviderOptions;
464
- if (reasoningEffort !== 'none' && this.options.reasoningType && this.options.reasoningType !== 'none') {
465
- const reasoningOpts = buildReasoningProviderOptions(reasoningEffort, this.options.reasoningType, this.options.reasoningBudgetMap);
694
+ const reasoningType = scopedModel?.reasoningType ?? this.options.reasoningType;
695
+ const reasoningBudgetMap = scopedModel?.reasoningBudgetMap ?? this.options.reasoningBudgetMap;
696
+ if (reasoningEffort !== 'none' && reasoningType && reasoningType !== 'none') {
697
+ const reasoningOpts = buildReasoningProviderOptions(reasoningEffort, reasoningType, reasoningBudgetMap);
466
698
  effectiveProviderOptions = { ...baseProviderOptions, ...reasoningOpts };
467
699
  }
468
700
  // Prepend recipe context as a system message if present
@@ -503,6 +735,9 @@ export class DMLRunner {
503
735
  try {
504
736
  emitToolEvent({ type: 'input_required', prompt });
505
737
  const response = await options.onInputRequired(prompt);
738
+ if (response === undefined || response === null) {
739
+ return { cancelled: true };
740
+ }
506
741
  return { user_response: response };
507
742
  }
508
743
  catch (err) {
@@ -515,14 +750,15 @@ export class DMLRunner {
515
750
  const emitToolCall = (toolName, args) => {
516
751
  emitToolEvent({ type: 'tool_call', toolName, toolArgs: args });
517
752
  };
518
- // Build available tools for agent - SIMPLIFIED: tools run in isolated engines
519
- // The callback runs DML tools in isolated engines with no shared state.
753
+ // Build available tools for the agent: the harness's DML tool/2 predicates
754
+ // plus registered TS tools (reachable via exec/2).
755
+ const selfReportingTools = undefined;
520
756
  const availableTools = this.buildAgentTools(userTools, options.toolPolicy, async (toolName, args) => {
521
757
  return this.executeToolIsolated(toolName, args, augmentedTools, onToolOutput, {
522
758
  workspacePath: options.workspacePath,
523
759
  signal: options.signal,
524
- compaction: options.compaction,
525
760
  toolPolicy: options.toolPolicy,
761
+ onModelCall: options.onModelCall,
526
762
  }, [], emitToolCall, emitToolEvent);
527
763
  });
528
764
  // Run agent loop with streaming support
@@ -532,13 +768,15 @@ export class DMLRunner {
532
768
  memory: effectiveMemory,
533
769
  tools: availableTools,
534
770
  workspacePath: options.workspacePath,
771
+ resolveMedia: (refs) => this.readMediaRefs(options.workspacePath, refs),
535
772
  modelOptions: {
536
- model: this.options.model,
537
- provider: this.options.provider,
538
- temperature: this.options.temperature,
539
- maxOutputTokens: this.options.maxTokens,
773
+ model: scopedModel?.id ?? this.options.model,
774
+ provider: scopedModel?.provider ?? this.options.provider,
775
+ temperature: scopedModel?.temperature ?? this.options.temperature,
776
+ maxOutputTokens: scopedModel?.maxTokens ?? this.options.maxTokens,
540
777
  baseUrl: this.options.baseUrl,
541
778
  providerOptions: effectiveProviderOptions,
779
+ target: scopedModel,
542
780
  },
543
781
  streaming: this.options.streaming,
544
782
  debug: this.options.debug,
@@ -550,6 +788,13 @@ export class DMLRunner {
550
788
  streamResolve = null;
551
789
  }
552
790
  },
791
+ onReasoning: (chunk, done) => {
792
+ streamQueue.push({ type: 'reasoning', content: chunk, done });
793
+ if (streamResolve) {
794
+ streamResolve();
795
+ streamResolve = null;
796
+ }
797
+ },
553
798
  onToolCall: (toolName, args) => {
554
799
  streamQueue.push({ type: 'tool_call', toolName, toolArgs: args });
555
800
  if (streamResolve) {
@@ -564,22 +809,27 @@ export class DMLRunner {
564
809
  streamResolve = null;
565
810
  }
566
811
  },
567
- onBeforeModelCall: async (messages, lastInputTokens) => this.applyCompactionBindings({
568
- messages,
569
- lastInputTokens,
570
- scope: 'loop',
571
- trigger: 'before_model_call',
572
- options,
573
- executionContext: {
574
- workspacePath: options.workspacePath,
575
- signal: options.signal,
576
- compaction: options.compaction,
577
- },
578
- registeredTools: options.tools,
579
- toolPolicy: options.toolPolicy,
580
- emitEvent: emitRunnerEvent,
581
- }),
812
+ onModelCallPhase: (phase) => {
813
+ streamQueue.push({ type: 'model_call', modelCallState: phase, modelId: this.options.model });
814
+ if (streamResolve) {
815
+ streamResolve();
816
+ streamResolve = null;
817
+ }
818
+ },
819
+ selfReportingTools,
820
+ onToolResult: (toolName, toolArgs, toolResult) => {
821
+ streamQueue.push(buildToolCompletionEvent(toolName, toolArgs, toolResult));
822
+ if (streamResolve) {
823
+ streamResolve();
824
+ streamResolve = null;
825
+ }
826
+ },
827
+ onBeforeModelCall: async (messages, lastInputTokens) => {
828
+ const hostCompacted = await this.applyHostCompaction(messages, lastInputTokens, emitRunnerEvent, scopedModel?.contextWindow);
829
+ return hostCompacted ?? messages;
830
+ },
582
831
  onAskUser: options.onInputRequired,
832
+ onModelCall: options.onModelCall,
583
833
  signal: options.signal,
584
834
  llmBackend: this.options.llmBackend,
585
835
  });
@@ -678,15 +928,17 @@ export class DMLRunner {
678
928
  yield { type: 'task_activity', taskState: 'started', taskDescription, taskId: stepId };
679
929
  let succeeded = false;
680
930
  try {
931
+ const scopedModel = await this.resolveScope(data.modelSpec);
681
932
  const { token, usage } = await sampleSingleToken({
682
933
  prompt,
683
934
  allowedTokens,
684
935
  modelOptions: {
685
- provider: this.options.provider,
686
- model: this.options.model,
687
- temperature: this.options.temperature,
936
+ provider: scopedModel?.provider ?? this.options.provider,
937
+ model: scopedModel?.id ?? this.options.model,
938
+ temperature: scopedModel?.temperature ?? this.options.temperature,
688
939
  baseUrl: this.options.baseUrl,
689
940
  providerOptions: this.options.providerOptions,
941
+ target: scopedModel,
690
942
  },
691
943
  signal: options.signal,
692
944
  llmBackend: this.options.llmBackend,
@@ -717,18 +969,21 @@ export class DMLRunner {
717
969
  yield { type: 'task_activity', taskState: 'started', taskDescription, taskId: stepId };
718
970
  let succeeded = false;
719
971
  try {
972
+ const scopedModel = await this.resolveScope(data.modelSpec);
720
973
  const { text, usage } = await generateLlmReply({
721
974
  messages,
722
975
  modelOptions: {
723
- provider: this.options.provider,
724
- model: this.options.model,
725
- temperature: this.options.temperature,
726
- maxOutputTokens: this.options.maxTokens,
976
+ provider: scopedModel?.provider ?? this.options.provider,
977
+ model: scopedModel?.id ?? this.options.model,
978
+ temperature: scopedModel?.temperature ?? this.options.temperature,
979
+ maxOutputTokens: scopedModel?.maxTokens ?? this.options.maxTokens,
727
980
  baseUrl: this.options.baseUrl,
728
981
  providerOptions: this.options.providerOptions,
982
+ target: scopedModel,
729
983
  },
730
984
  signal: options.signal,
731
985
  llmBackend: this.options.llmBackend,
986
+ resolveMedia: (refs) => this.readMediaRefs(options.workspacePath, refs),
732
987
  });
733
988
  succeeded = true;
734
989
  this.postLlmResult({ success: true, text });
@@ -859,15 +1114,18 @@ export class DMLRunner {
859
1114
  */
860
1115
  async *handleExec(payload, options) {
861
1116
  const { toolName, args } = payload;
862
- // Check tool policy
1117
+ // Check tool policy (ask_user is the human channel and is exempt).
863
1118
  const policyCheck = checkToolPolicy(toolName, options.toolPolicy);
864
1119
  if (!policyCheck.allowed) {
865
1120
  yield { type: 'log', content: `Tool '${toolName}' blocked by policy: ${policyCheck.reason}` };
866
1121
  this.postExecResult({ success: false, error: policyCheck.reason });
867
1122
  return;
868
1123
  }
869
- // Get tool definition
870
- const tool = options.tools.get(toolName);
1124
+ // Get tool definition. `ask_user` is SDK-internal so exec(ask_user(...))
1125
+ // works in DML even when no capability tools are registered.
1126
+ const tool = toolName === 'ask_user'
1127
+ ? this.internalAskUserTool(options)
1128
+ : options.tools.get(toolName);
871
1129
  if (!tool) {
872
1130
  yield { type: 'log', content: `Tool '${toolName}' not found` };
873
1131
  this.postExecResult({ success: false, error: `Tool not found: ${toolName}` });
@@ -879,7 +1137,7 @@ export class DMLRunner {
879
1137
  argsObj = this.argsToObject(args, tool);
880
1138
  // Emit tool_call event before execution
881
1139
  yield buildToolStartEvent(toolName, argsObj);
882
- const result = await tool.execute(argsObj);
1140
+ const result = await tool.execute(argsObj, { signal: options.signal });
883
1141
  yield buildToolCompletionEvent(toolName, argsObj, result);
884
1142
  // Post result back to Prolog
885
1143
  this.postExecResult({ success: true, result });
@@ -1172,7 +1430,7 @@ export class DMLRunner {
1172
1430
  try {
1173
1431
  argsObj = this.argsToObject(execArgs, tool);
1174
1432
  onToolEvent?.(buildToolStartEvent(execToolName, argsObj));
1175
- const execResult = await tool.execute(argsObj);
1433
+ const execResult = await tool.execute(argsObj, { signal: runContext.signal });
1176
1434
  onToolEvent?.(buildToolCompletionEvent(execToolName, argsObj, execResult));
1177
1435
  const postGoal = `deepclause_mi:post_exec_result('${this.sessionId}', success, ${this.toPrologTerm(execResult)})`;
1178
1436
  this.query(postGoal);
@@ -1243,6 +1501,7 @@ export class DMLRunner {
1243
1501
  },
1244
1502
  signal: runContext.signal,
1245
1503
  llmBackend: this.options.llmBackend,
1504
+ resolveMedia: (refs) => this.readMediaRefs(runContext.workspacePath, refs),
1246
1505
  });
1247
1506
  this.postLlmResult({ success: true, text });
1248
1507
  if (usage) {
@@ -1436,6 +1695,7 @@ export class DMLRunner {
1436
1695
  streaming: this.options.streaming,
1437
1696
  debug: this.options.debug,
1438
1697
  llmBackend: this.options.llmBackend,
1698
+ resolveMedia: (refs) => this.readMediaRefs(runContext.workspacePath, refs),
1439
1699
  onOutput: (text) => {
1440
1700
  outputs.push(text);
1441
1701
  onOutput(text);
@@ -1443,26 +1703,26 @@ export class DMLRunner {
1443
1703
  onStream: (chunk, done) => {
1444
1704
  onToolEvent?.({ type: 'stream', content: chunk, done });
1445
1705
  },
1706
+ onReasoning: (chunk, done) => {
1707
+ onToolEvent?.({ type: 'reasoning', content: chunk, done });
1708
+ },
1446
1709
  onUsage: (usage) => {
1447
1710
  onToolEvent?.({ type: 'usage', usage });
1448
1711
  },
1712
+ onModelCallPhase: (phase) => {
1713
+ onToolEvent?.({ type: 'model_call', modelCallState: phase, modelId: this.options.model });
1714
+ },
1449
1715
  onToolCall: onToolCall ?? (() => { }), // Forward tool call events to parent
1450
- onBeforeModelCall: async (messages, lastInputTokens) => this.applyCompactionBindings({
1451
- messages,
1452
- lastInputTokens,
1453
- scope: 'loop',
1454
- trigger: 'before_model_call',
1455
- options: { compaction: runContext.compaction },
1456
- executionContext: runContext,
1457
- registeredTools,
1458
- toolPolicy: runContext.toolPolicy ?? null,
1459
- emitEvent: onToolEvent,
1460
- }),
1716
+ onBeforeModelCall: async (messages, lastInputTokens) => {
1717
+ const hostCompacted = await this.applyHostCompaction(messages, lastInputTokens, onToolEvent);
1718
+ return hostCompacted ?? messages;
1719
+ },
1461
1720
  onAskUser: async () => {
1462
1721
  // Nested agent loops don't support ask_user directly
1463
1722
  // If needed, the tool should use exec(ask_user, ...) instead
1464
1723
  return '';
1465
1724
  },
1725
+ onModelCall: runContext.onModelCall,
1466
1726
  signal: runContext.signal,
1467
1727
  });
1468
1728
  if (process.env.DEBUG_RUNNER) {
@@ -1524,116 +1784,17 @@ export class DMLRunner {
1524
1784
  }
1525
1785
  }
1526
1786
  }
1527
- async applyCompactionBindings(params) {
1528
- const resolved = resolveCompactionOptions(this.options.compaction, params.options.compaction);
1529
- const bindings = getCompactionBindings(resolved, params.scope, params.trigger);
1530
- if (bindings.length === 0) {
1531
- return params.messages;
1532
- }
1533
- let messages = params.messages;
1534
- for (const binding of bindings) {
1535
- const result = await executeCompactor({
1536
- binding,
1537
- messages,
1538
- knownInputTokens: params.lastInputTokens,
1539
- maxContextTokens: this.options.contextWindow,
1540
- emitEvent: params.emitEvent,
1541
- execute: (request) => this.runBoundCompactor({
1542
- request,
1543
- executionContext: params.executionContext,
1544
- registeredTools: params.registeredTools,
1545
- toolPolicy: params.toolPolicy,
1546
- }),
1547
- });
1548
- params.emitEvent?.(result.event);
1549
- if (result.usageByModel) {
1550
- for (const totals of Object.values(result.usageByModel)) {
1551
- params.emitEvent?.({ type: 'usage', usage: {
1552
- inputTokens: totals.inputTokens,
1553
- outputTokens: totals.outputTokens,
1554
- totalTokens: totals.totalTokens,
1555
- cacheReadTokens: totals.cacheReadTokens || undefined,
1556
- cacheWriteTokens: totals.cacheWriteTokens || undefined,
1557
- reasoningTokens: totals.reasoningTokens || undefined,
1558
- } });
1559
- }
1560
- }
1561
- messages = result.messages;
1562
- }
1563
- return messages;
1564
- }
1565
- async runBoundCompactor(params) {
1566
- const { binding, params: compactorParams } = params.request;
1567
- const code = await this.resolveCompactorSource(binding.compactor.source, binding.compactor.sourceType, params.executionContext.workspacePath);
1568
- const model = binding.compactor.model ?? this.options.model;
1569
- const provider = binding.compactor.provider
1570
- ?? (binding.compactor.model ? detectProvider(model) : this.options.provider);
1571
- const compactorRunner = new DMLRunner(this.swipl, {
1572
- ...this.options,
1573
- model,
1574
- provider,
1575
- streaming: false,
1576
- compaction: { enabled: false },
1577
- });
1578
- const { signal, dispose } = createChildAbortController(params.executionContext.signal, binding.compactor.timeoutMs);
1579
- try {
1580
- const tools = binding.compactor.inheritTools ? params.registeredTools : new Map();
1581
- let answer = '';
1582
- let errorContent = '';
1583
- const usageByModel = {};
1584
- for await (const event of compactorRunner.run(code, {
1585
- workspacePath: params.executionContext.workspacePath,
1586
- gasLimit: binding.compactor.gasLimit,
1587
- params: compactorParams,
1588
- initialMessages: params.request.messages,
1589
- tools,
1590
- toolPolicy: binding.compactor.toolPolicy ?? (binding.compactor.inheritTools ? params.toolPolicy : null) ?? null,
1591
- onInputRequired: async (prompt) => {
1592
- throw new Error(`Compactor requested unexpected input: ${prompt}`);
1593
- },
1594
- signal,
1595
- compaction: { enabled: false },
1596
- })) {
1597
- if (event.type === 'answer' && event.content) {
1598
- answer = event.content;
1599
- }
1600
- else if (event.type === 'error' && event.content) {
1601
- errorContent = event.content;
1602
- }
1603
- else if (event.type === 'usage' && event.usage) {
1604
- recordTokenUsage(usageByModel, this.options.model, event.usage);
1605
- }
1606
- }
1607
- return {
1608
- answer,
1609
- error: errorContent || undefined,
1610
- usageByModel: Object.keys(usageByModel).length > 0 ? usageByModel : undefined,
1611
- };
1612
- }
1613
- finally {
1614
- dispose();
1615
- await compactorRunner.dispose();
1616
- }
1617
- }
1618
- async resolveCompactorSource(source, sourceType, workspacePath) {
1619
- if (sourceType === 'inline') {
1620
- return source;
1621
- }
1622
- const basePath = workspacePath ?? process.cwd();
1623
- const candidatePath = path.isAbsolute(source) ? source : path.resolve(basePath, source);
1624
- try {
1625
- return await fs.readFile(candidatePath, 'utf8');
1787
+ async applyHostCompaction(messages, lastInputTokens, emit, contextWindow) {
1788
+ const host = this.options.compactMessages;
1789
+ if (!host) {
1790
+ return null;
1626
1791
  }
1627
- catch (error) {
1628
- if (sourceType === 'file') {
1629
- throw error;
1630
- }
1631
- return source;
1792
+ const result = await host(messages, lastInputTokens, contextWindow);
1793
+ if (result.event) {
1794
+ emit?.(result.event);
1632
1795
  }
1796
+ return result.messages;
1633
1797
  }
1634
- /**
1635
- * Extract memory from payload (now passed via state threading)
1636
- */
1637
1798
  extractMessagesFromValue(rawMessages) {
1638
1799
  const normalized = toJsValue(rawMessages);
1639
1800
  if (!Array.isArray(normalized)) {
@@ -1643,19 +1804,46 @@ export class DMLRunner {
1643
1804
  const msg = toJsValue(messageValue);
1644
1805
  const role = String(toJsValue(msg.role) ?? 'user');
1645
1806
  const content = String(toJsValue(msg.content) ?? '');
1807
+ const media = this.extractMediaRefs(msg.media);
1646
1808
  if (role !== 'system' && role !== 'user' && role !== 'assistant') {
1647
- return { role: 'user', content };
1809
+ return { role: 'user', content, media: media.length ? media : undefined };
1648
1810
  }
1649
- return { role: role, content };
1811
+ return { role: role, content, media: media.length ? media : undefined };
1650
1812
  });
1651
1813
  }
1814
+ /** Decode `media{path:..., mime:...}` dicts into reference-only refs. */
1815
+ extractMediaRefs(raw) {
1816
+ const value = toJsValue(raw);
1817
+ if (!Array.isArray(value))
1818
+ return [];
1819
+ const refs = [];
1820
+ for (const entry of value) {
1821
+ const obj = toJsValue(entry);
1822
+ const mediaPath = String(obj?.path ?? '');
1823
+ const mimeType = String(obj?.mime ?? obj?.mimeType ?? '');
1824
+ if (mediaPath && mimeType)
1825
+ refs.push({ path: mediaPath, mimeType });
1826
+ }
1827
+ return refs;
1828
+ }
1829
+ /** Resolve a message's reference-only media to base64 bytes (workspace files). */
1830
+ async readMediaRefs(workspacePath, refs) {
1831
+ return resolveMediaRefs(workspacePath ?? process.cwd(), refs);
1832
+ }
1652
1833
  extractMemoryFromPayload(rawPayload) {
1653
1834
  const rawMemory = toJsValue(rawPayload.memory);
1654
1835
  return this.extractMessagesFromValue(rawMemory);
1655
1836
  }
1656
1837
  serializeMemoryMessages(messages) {
1657
1838
  return messages
1658
- .map((message) => `message{role: ${message.role}, content: ${this.toPrologTerm(message.content)}}`)
1839
+ .map((message) => {
1840
+ const media = message.media && message.media.length > 0
1841
+ ? `, media: [${message.media
1842
+ .map((ref) => `media{path: ${this.toPrologTerm(ref.path)}, mime: ${this.toPrologTerm(ref.mimeType)}}`)
1843
+ .join(', ')}]`
1844
+ : '';
1845
+ return `message{role: ${message.role}, content: ${this.toPrologTerm(message.content)}${media}}`;
1846
+ })
1659
1847
  .join(', ');
1660
1848
  }
1661
1849
  /**
@@ -1723,6 +1911,30 @@ export class DMLRunner {
1723
1911
  const fullMemoryStr = this.serializeMemoryMessages(fullMemory ?? []);
1724
1912
  this.query(`deepclause_mi:post_tool_agent_result('${this.sessionId}', '${toolEngineId}', ${result.success}, vars{${varsStr}}, [${messagesStr}], [${fullMemoryStr}])`);
1725
1913
  }
1914
+ /** SDK-internal ask_user tool used by exec(ask_user(...)) in DML. */
1915
+ internalAskUserTool(options) {
1916
+ return {
1917
+ description: 'Ask the user for input or clarification',
1918
+ parameters: {
1919
+ type: 'object',
1920
+ properties: {
1921
+ prompt: { type: 'string', description: 'The question or prompt to show the user' },
1922
+ },
1923
+ required: ['prompt'],
1924
+ },
1925
+ execute: async (args) => {
1926
+ const prompt = String(args.prompt ?? args.arg1 ?? '');
1927
+ if (!prompt) {
1928
+ return { error: 'No prompt provided to ask_user' };
1929
+ }
1930
+ const response = await options.onInputRequired(prompt);
1931
+ if (response === undefined || response === null) {
1932
+ return { cancelled: true };
1933
+ }
1934
+ return { user_response: response };
1935
+ },
1936
+ };
1937
+ }
1726
1938
  /**
1727
1939
  * Provide user input to waiting Prolog
1728
1940
  */
@@ -1730,9 +1942,36 @@ export class DMLRunner {
1730
1942
  const escaped = input.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
1731
1943
  this.query(`deepclause_mi:provide_input('${this.sessionId}', "${escaped}")`);
1732
1944
  }
1945
+ /** Signal a waiting input/2 that no human is available. */
1946
+ cancelInput() {
1947
+ this.query(`deepclause_mi:provide_input_cancelled('${this.sessionId}')`);
1948
+ }
1733
1949
  /**
1734
1950
  * Execute a Prolog query
1735
1951
  */
1952
+ scopedModels = new Map();
1953
+ /**
1954
+ * Resolve a `with_model/2` spec to a model profile (cached). Returns undefined
1955
+ * for a `default`/absent scope (use the session model).
1956
+ */
1957
+ async resolveScope(rawSpec) {
1958
+ const spec = parseModelSpec(rawSpec);
1959
+ if (spec === 'default') {
1960
+ return undefined;
1961
+ }
1962
+ const key = JSON.stringify(spec);
1963
+ const cached = this.scopedModels.get(key);
1964
+ if (cached) {
1965
+ return cached;
1966
+ }
1967
+ const resolver = this.options.modelResolver;
1968
+ if (!resolver) {
1969
+ throw new Error('with_model is unavailable: no model resolver is configured');
1970
+ }
1971
+ const resolved = await resolver.resolve(spec);
1972
+ this.scopedModels.set(key, resolved);
1973
+ return resolved;
1974
+ }
1736
1975
  query(goal) {
1737
1976
  try {
1738
1977
  const q = this.swipl.prolog.query(goal);
@@ -1748,14 +1987,17 @@ export class DMLRunner {
1748
1987
  * Cleanup resources
1749
1988
  */
1750
1989
  cleanup() {
1751
- if (this.engine) {
1990
+ if (this.frames.size > 0) {
1752
1991
  try {
1992
+ // destroy_engine/1 ends every frame registered for the session and
1993
+ // retracts the session's dynamic state.
1753
1994
  this.query(`deepclause_mi:destroy_engine('${this.sessionId}')`);
1754
1995
  }
1755
1996
  catch {
1756
1997
  // Ignore cleanup errors
1757
1998
  }
1758
- this.engine = null;
1999
+ this.frames.clear();
2000
+ this.defaultFrameId = null;
1759
2001
  }
1760
2002
  }
1761
2003
  /**