deepclause-sdk 0.0.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 (111) hide show
  1. package/README.md +446 -0
  2. package/dist/agent.d.ts +44 -0
  3. package/dist/agent.d.ts.map +1 -0
  4. package/dist/agent.js +518 -0
  5. package/dist/agent.js.map +1 -0
  6. package/dist/cli/commands.d.ts +37 -0
  7. package/dist/cli/commands.d.ts.map +1 -0
  8. package/dist/cli/commands.js +105 -0
  9. package/dist/cli/commands.js.map +1 -0
  10. package/dist/cli/compile.d.ts +88 -0
  11. package/dist/cli/compile.d.ts.map +1 -0
  12. package/dist/cli/compile.js +362 -0
  13. package/dist/cli/compile.js.map +1 -0
  14. package/dist/cli/config.d.ts +265 -0
  15. package/dist/cli/config.d.ts.map +1 -0
  16. package/dist/cli/config.js +272 -0
  17. package/dist/cli/config.js.map +1 -0
  18. package/dist/cli/index.d.ts +8 -0
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +287 -0
  21. package/dist/cli/index.js.map +1 -0
  22. package/dist/cli/mcp.d.ts +56 -0
  23. package/dist/cli/mcp.d.ts.map +1 -0
  24. package/dist/cli/mcp.js +138 -0
  25. package/dist/cli/mcp.js.map +1 -0
  26. package/dist/cli/prompt.d.ts +20 -0
  27. package/dist/cli/prompt.d.ts.map +1 -0
  28. package/dist/cli/prompt.js +669 -0
  29. package/dist/cli/prompt.js.map +1 -0
  30. package/dist/cli/run.d.ts +33 -0
  31. package/dist/cli/run.d.ts.map +1 -0
  32. package/dist/cli/run.js +429 -0
  33. package/dist/cli/run.js.map +1 -0
  34. package/dist/cli/search.d.ts +25 -0
  35. package/dist/cli/search.d.ts.map +1 -0
  36. package/dist/cli/search.js +125 -0
  37. package/dist/cli/search.js.map +1 -0
  38. package/dist/cli/tools.d.ts +36 -0
  39. package/dist/cli/tools.d.ts.map +1 -0
  40. package/dist/cli/tools.js +204 -0
  41. package/dist/cli/tools.js.map +1 -0
  42. package/dist/cli/tui/index.d.ts +22 -0
  43. package/dist/cli/tui/index.d.ts.map +1 -0
  44. package/dist/cli/tui/index.js +29 -0
  45. package/dist/cli/tui/index.js.map +1 -0
  46. package/dist/index.d.ts +9 -0
  47. package/dist/index.d.ts.map +1 -0
  48. package/dist/index.js +8 -0
  49. package/dist/index.js.map +1 -0
  50. package/dist/prolog/bridge.d.ts +21 -0
  51. package/dist/prolog/bridge.d.ts.map +1 -0
  52. package/dist/prolog/bridge.js +226 -0
  53. package/dist/prolog/bridge.js.map +1 -0
  54. package/dist/prolog/loader.d.ts +40 -0
  55. package/dist/prolog/loader.d.ts.map +1 -0
  56. package/dist/prolog/loader.js +133 -0
  57. package/dist/prolog/loader.js.map +1 -0
  58. package/dist/prolog-src/deepclause_memory.pl +45 -0
  59. package/dist/prolog-src/deepclause_mi.pl +1978 -0
  60. package/dist/prolog-src/deepclause_mi.pl.bak +570 -0
  61. package/dist/prolog-src/deepclause_strings.pl +89 -0
  62. package/dist/runner.d.ts +143 -0
  63. package/dist/runner.d.ts.map +1 -0
  64. package/dist/runner.js +1095 -0
  65. package/dist/runner.js.map +1 -0
  66. package/dist/sdk.d.ts +9 -0
  67. package/dist/sdk.d.ts.map +1 -0
  68. package/dist/sdk.js +131 -0
  69. package/dist/sdk.js.map +1 -0
  70. package/dist/tools.d.ts +22 -0
  71. package/dist/tools.d.ts.map +1 -0
  72. package/dist/tools.js +138 -0
  73. package/dist/tools.js.map +1 -0
  74. package/dist/types.d.ts +186 -0
  75. package/dist/types.d.ts.map +1 -0
  76. package/dist/types.js +5 -0
  77. package/dist/types.js.map +1 -0
  78. package/package.json +79 -0
  79. package/src/prolog-src/deepclause_memory.pl +45 -0
  80. package/src/prolog-src/deepclause_mi.pl +1978 -0
  81. package/src/prolog-src/deepclause_mi.pl.bak +570 -0
  82. package/src/prolog-src/deepclause_strings.pl +89 -0
  83. package/vendor/swipl-wasm/LICENSE.txt +41 -0
  84. package/vendor/swipl-wasm/dist/bin/index.js +25 -0
  85. package/vendor/swipl-wasm/dist/common.d.ts +88 -0
  86. package/vendor/swipl-wasm/dist/generateImage.d.ts +6 -0
  87. package/vendor/swipl-wasm/dist/generateImage.js +76 -0
  88. package/vendor/swipl-wasm/dist/index.d.ts +2 -0
  89. package/vendor/swipl-wasm/dist/index.js +1 -0
  90. package/vendor/swipl-wasm/dist/loadImage.d.ts +2 -0
  91. package/vendor/swipl-wasm/dist/loadImage.js +10 -0
  92. package/vendor/swipl-wasm/dist/loadImageDefault.d.ts +2 -0
  93. package/vendor/swipl-wasm/dist/loadImageDefault.js +11 -0
  94. package/vendor/swipl-wasm/dist/strToBuffer.d.ts +8 -0
  95. package/vendor/swipl-wasm/dist/strToBuffer.js +41 -0
  96. package/vendor/swipl-wasm/dist/swipl/swipl-bundle-no-data.d.ts +2 -0
  97. package/vendor/swipl-wasm/dist/swipl/swipl-bundle-no-data.js +2 -0
  98. package/vendor/swipl-wasm/dist/swipl/swipl-bundle.d.ts +2 -0
  99. package/vendor/swipl-wasm/dist/swipl/swipl-bundle.js +2 -0
  100. package/vendor/swipl-wasm/dist/swipl/swipl-web.d.ts +2 -0
  101. package/vendor/swipl-wasm/dist/swipl/swipl-web.data +0 -0
  102. package/vendor/swipl-wasm/dist/swipl/swipl-web.js +2 -0
  103. package/vendor/swipl-wasm/dist/swipl/swipl-web.wasm +0 -0
  104. package/vendor/swipl-wasm/dist/swipl/swipl-win.js +1 -0
  105. package/vendor/swipl-wasm/dist/swipl/swipl-win.wasm +0 -0
  106. package/vendor/swipl-wasm/dist/swipl/swipl.d.ts +2 -0
  107. package/vendor/swipl-wasm/dist/swipl/swipl.js +1 -0
  108. package/vendor/swipl-wasm/dist/swipl/swipl.wasm +0 -0
  109. package/vendor/swipl-wasm/dist/swipl-node.d.ts +2 -0
  110. package/vendor/swipl-wasm/dist/swipl-node.js +17 -0
  111. package/vendor/swipl-wasm/package.json +129 -0
@@ -0,0 +1,1978 @@
1
+ /**
2
+ * deepclause_mi.pl - Meta-interpreter for DeepClause SDK
3
+ *
4
+ * The simplified meta-interpreter that handles:
5
+ * - task/1 and task/N predicates (agent loops)
6
+ * - exec/2 predicate (external tool calls)
7
+ * - Memory predicates (system, user) - now backtrackable via state
8
+ * - Output predicates (answer, yield, log)
9
+ * - Parameter handling
10
+ *
11
+ * Key design decisions:
12
+ * - No @-predicates - all LLM interaction via task()
13
+ * - Cooperative execution via engine yields
14
+ * - BACKTRACKABLE MEMORY via state threading (no external dynamic predicates)
15
+ * - State = state{memory: [...], params: {...}}
16
+ */
17
+
18
+ :- module(deepclause_mi, [
19
+ parse_dml/5,
20
+ create_engine/5,
21
+ step_engine/4,
22
+ destroy_engine/1,
23
+ post_agent_result/4,
24
+ post_exec_result/3,
25
+ provide_input/2,
26
+ mi/3,
27
+ % Tool engine predicates for isolated tool execution
28
+ create_tool_engine/4,
29
+ step_tool_engine/4,
30
+ destroy_tool_engine/1,
31
+ post_tool_agent_result/5
32
+ ]).
33
+
34
+ :- use_module(deepclause_strings).
35
+
36
+ %% Allow mi_call/3 clauses to be non-contiguous
37
+ :- discontiguous mi_call/3.
38
+ :- discontiguous transform_task_calls/3.
39
+
40
+ %% Dynamic predicates for session state (engine management only, not memory!)
41
+ :- dynamic session_engine/2. % session_engine(SessionId, Engine)
42
+ :- dynamic session_params/2. % session_params(SessionId, ParamsDict)
43
+ :- dynamic session_user_tools/2. % session_user_tools(SessionId, ToolName)
44
+ :- dynamic session_user_tool_schema/3. % session_user_tool_schema(SessionId, ToolName, Schema)
45
+ :- dynamic session_pending_input/2. % session_pending_input(SessionId, Input)
46
+ :- dynamic session_agent_result/2. % session_agent_result(SessionId, Result)
47
+ :- dynamic session_exec_result/2. % session_exec_result(SessionId, Result)
48
+ :- dynamic session_trace_log/2. % session_trace_log(SessionId, TraceLog) - accumulated trace entries
49
+ :- dynamic session_trace_enabled/2. % session_trace_enabled(SessionId, true/false)
50
+ :- dynamic session_pending_signal/2. % session_pending_signal(SessionId, Signal) - fallback for engine_fetch
51
+ :- dynamic session_user_input/2. % session_user_input(SessionId, Input) - for ask_user tool
52
+
53
+ %% ============================================================
54
+ %% DML Parsing
55
+ %% ============================================================
56
+
57
+ %% parse_dml(+FilePath, +SessionId, +MemoryId, +Params, -Error)
58
+ %% Parse a DML file and load its clauses into the session module
59
+ %% Also asserts param/2 facts from the Params dict
60
+ parse_dml(FilePath, SessionId, _MemoryId, Params, Error) :-
61
+ catch(
62
+ (
63
+ read_file_to_string(FilePath, Code, []),
64
+ assert_params(SessionId, Params),
65
+ parse_dml_string(Code, SessionId),
66
+ Error = none
67
+ ),
68
+ ParseError,
69
+ format(atom(Error), '~w', [ParseError])
70
+ ).
71
+
72
+ %% assert_params(+SessionId, +Params)
73
+ %% Assert param/2 facts from the Params dict into the session module
74
+ assert_params(SessionId, Params) :-
75
+ ( is_dict(Params)
76
+ -> dict_pairs(Params, _, Pairs),
77
+ forall(
78
+ member(Key-Value, Pairs),
79
+ assertz(SessionId:param(Key, Value))
80
+ )
81
+ ; true
82
+ ).
83
+
84
+ %% parse_dml_string(+Code, +SessionId)
85
+ %% Parse DML code from a string
86
+ parse_dml_string(Code, SessionId) :-
87
+ open_string(Code, Stream),
88
+ parse_clauses(Stream, SessionId),
89
+ close(Stream).
90
+
91
+ %% parse_clauses(+Stream, +SessionId)
92
+ %% Read and process all clauses from a stream
93
+ %% Uses variable_names to enable compile-time string interpolation
94
+ parse_clauses(Stream, SessionId) :-
95
+ read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
96
+ ( Term == end_of_file
97
+ -> true
98
+ ; % First, transform task/N calls to include variable names
99
+ transform_task_calls(Term, Bindings, TransformedTerm),
100
+ % Then expand string interpolations
101
+ expand_interpolations(TransformedTerm, Bindings, ExpandedTerm),
102
+ process_clause(ExpandedTerm, SessionId),
103
+ parse_clauses(Stream, SessionId)
104
+ ).
105
+
106
+ %% ============================================================
107
+ %% Task Call Transformation (compile-time)
108
+ %% ============================================================
109
+
110
+ %% transform_task_calls(+Term, +Bindings, -TransformedTerm)
111
+ %% Transform task(Desc, Var1, ...) to task_named(Desc, [Var1, ...], ['Name1', ...])
112
+ transform_task_calls(Var, _, Var) :- var(Var), !.
113
+ transform_task_calls((Head :- Body), Bindings, (Head :- TransformedBody)) :- !,
114
+ transform_task_calls(Body, Bindings, TransformedBody).
115
+ transform_task_calls((A, B), Bindings, (TA, TB)) :- !,
116
+ transform_task_calls(A, Bindings, TA),
117
+ transform_task_calls(B, Bindings, TB).
118
+ transform_task_calls((A ; B), Bindings, (TA ; TB)) :- !,
119
+ transform_task_calls(A, Bindings, TA),
120
+ transform_task_calls(B, Bindings, TB).
121
+ transform_task_calls((A -> B), Bindings, (TA -> TB)) :- !,
122
+ transform_task_calls(A, Bindings, TA),
123
+ transform_task_calls(B, Bindings, TB).
124
+ transform_task_calls(\+ A, Bindings, \+ TA) :- !,
125
+ transform_task_calls(A, Bindings, TA).
126
+
127
+ %% Transform task/2 to task_named/3
128
+ transform_task_calls(task(Desc, V1), Bindings, task_named(Desc, [V1], Names)) :- !,
129
+ maplist(var_to_name(Bindings), [V1], Names).
130
+
131
+ %% Transform task/3 to task_named/3
132
+ transform_task_calls(task(Desc, V1, V2), Bindings, task_named(Desc, [V1, V2], Names)) :- !,
133
+ maplist(var_to_name(Bindings), [V1, V2], Names).
134
+
135
+ %% Transform task/4 to task_named/3
136
+ transform_task_calls(task(Desc, V1, V2, V3), Bindings, task_named(Desc, [V1, V2, V3], Names)) :- !,
137
+ maplist(var_to_name(Bindings), [V1, V2, V3], Names).
138
+
139
+ %% Transform task/5 to task_named/3
140
+ transform_task_calls(task(Desc, V1, V2, V3, V4), Bindings, task_named(Desc, [V1, V2, V3, V4], Names)) :- !,
141
+ maplist(var_to_name(Bindings), [V1, V2, V3, V4], Names).
142
+
143
+ %% Transform prompt/2 to prompt_named/3
144
+ transform_task_calls(prompt(Desc, V1), Bindings, prompt_named(Desc, [V1], Names)) :- !,
145
+ maplist(var_to_name(Bindings), [V1], Names).
146
+
147
+ %% Transform prompt/3 to prompt_named/3
148
+ transform_task_calls(prompt(Desc, V1, V2), Bindings, prompt_named(Desc, [V1, V2], Names)) :- !,
149
+ maplist(var_to_name(Bindings), [V1, V2], Names).
150
+
151
+ %% Transform prompt/4 to prompt_named/3
152
+ transform_task_calls(prompt(Desc, V1, V2, V3), Bindings, prompt_named(Desc, [V1, V2, V3], Names)) :- !,
153
+ maplist(var_to_name(Bindings), [V1, V2, V3], Names).
154
+
155
+ %% Recurse into arbitrary compound terms (like with_tools/2, without_tools/2, etc.)
156
+ transform_task_calls(Term, Bindings, TransformedTerm) :-
157
+ compound(Term),
158
+ \+ is_list(Term), % Don't decompose lists
159
+ Term =.. [Functor|Args],
160
+ Args \= [], % Has at least one argument
161
+ !,
162
+ maplist(transform_task_calls_arg(Bindings), Args, TransformedArgs),
163
+ TransformedTerm =.. [Functor|TransformedArgs].
164
+
165
+ %% Helper to transform each argument
166
+ transform_task_calls_arg(Bindings, Arg, TransformedArg) :-
167
+ transform_task_calls(Arg, Bindings, TransformedArg).
168
+
169
+ %% Default: atoms and numbers pass through unchanged
170
+ transform_task_calls(Term, _, Term).
171
+
172
+ %% var_to_name(+Bindings, +Var, -Name)
173
+ %% Look up a variable's name from the bindings list
174
+ var_to_name(Bindings, Var, Name) :-
175
+ ( member(N=V, Bindings), V == Var
176
+ -> Name = N
177
+ ; Name = 'Var' % Fallback if not found
178
+ ).
179
+
180
+ %% process_clause(+Term, +SessionId)
181
+ %% Process a single clause and add to session
182
+ process_clause((:- Directive), SessionId) :-
183
+ !,
184
+ process_directive(Directive, SessionId).
185
+
186
+ %% Handle tool/2 with description: tool(Head, Description) :- Body
187
+ process_clause((tool(ToolHead, Description) :- Body), SessionId) :-
188
+ !,
189
+ extract_tool_schema(ToolHead, Description, ToolName, Schema),
190
+ assertz(session_user_tools(SessionId, ToolName)),
191
+ assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
192
+ % Store the source code for the tool description
193
+ format(string(SourceCode), "tool(~w, ~q) :-~n ~w.", [ToolHead, Description, Body]),
194
+ assertz(SessionId:tool_source(ToolName, SourceCode)),
195
+ % Assert the tool implementation (use just ToolHead for execution)
196
+ assertz(SessionId:(tool(ToolHead) :- Body)).
197
+
198
+ %% Handle tool/1 without description: tool(Head) :- Body
199
+ process_clause((tool(ToolHead) :- Body), SessionId) :-
200
+ !,
201
+ extract_tool_schema(ToolHead, none, ToolName, Schema),
202
+ assertz(session_user_tools(SessionId, ToolName)),
203
+ assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
204
+ % Store the source code for the tool description
205
+ format(string(SourceCode), "tool(~w) :-~n ~w.", [ToolHead, Body]),
206
+ assertz(SessionId:tool_source(ToolName, SourceCode)),
207
+ % Assert the tool implementation
208
+ assertz(SessionId:(tool(ToolHead) :- Body)).
209
+
210
+ process_clause((Head :- Body), SessionId) :-
211
+ !,
212
+ % Regular clause - assert it
213
+ assertz(SessionId:(Head :- Body)).
214
+
215
+ process_clause(Fact, SessionId) :-
216
+ % Simple fact
217
+ assertz(SessionId:Fact).
218
+
219
+ %% ============================================================
220
+ %% Tool Schema Extraction
221
+ %% ============================================================
222
+
223
+ %% extract_tool_schema(+ToolHead, +Description, -ToolName, -Schema)
224
+ extract_tool_schema(ToolHead, Description, ToolName, Schema) :-
225
+ ToolHead =.. [ToolName|Args],
226
+ length(Args, Arity),
227
+ extract_params(Args, 1, Arity, Inputs, Outputs),
228
+ (Description == none -> Desc = "" ; Desc = Description),
229
+ Schema = schema{
230
+ name: ToolName,
231
+ description: Desc,
232
+ inputs: Inputs,
233
+ outputs: Outputs
234
+ }.
235
+
236
+ %% extract_params(+Args, +Index, +Arity, -Inputs, -Outputs)
237
+ extract_params([], _, _, [], []) :- !.
238
+ extract_params([_Arg|Rest], Index, Arity, Inputs, Outputs) :-
239
+ format(atom(Name), 'arg~w', [Index]),
240
+ Type = string,
241
+ NextIndex is Index + 1,
242
+ extract_params(Rest, NextIndex, Arity, RestInputs, RestOutputs),
243
+ Param = param{name: Name, type: Type},
244
+ ( Index == Arity
245
+ -> Inputs = RestInputs, Outputs = [Param|RestOutputs]
246
+ ; Inputs = [Param|RestInputs], Outputs = RestOutputs
247
+ ).
248
+
249
+ %% process_directive(+Directive, +SessionId)
250
+ process_directive(param(Key, Desc), SessionId) :-
251
+ !,
252
+ assertz(SessionId:param_decl(Key, Desc)).
253
+ process_directive(param(Key, Desc, Default), SessionId) :-
254
+ !,
255
+ assertz(SessionId:param_decl(Key, Desc, Default)).
256
+ process_directive(_, _).
257
+
258
+ %% ============================================================
259
+ %% Engine Management
260
+ %% ============================================================
261
+
262
+ %% create_engine(+SessionId, +MemoryId, +Args, +Params, -Engine)
263
+ %% Args is a list of positional arguments for agent_main
264
+ %% Params is a dict of named parameters (already asserted as param/2 facts)
265
+ create_engine(SessionId, _MemoryId, Args, Params, Engine) :-
266
+ assertz(session_params(SessionId, Params)),
267
+ determine_agent_goal(SessionId, Args, Goal),
268
+ % Check if tracing is enabled and store in session state
269
+ (get_dict(trace, Params, true) -> TraceEnabled = true ; TraceEnabled = false),
270
+ assertz(session_trace_enabled(SessionId, TraceEnabled)),
271
+ assertz(session_trace_log(SessionId, [])),
272
+ % Create initial state with empty memory, params, context stack, and trace depth
273
+ InitialState = state{memory: [], params: Params, context_stack: [], depth: 0},
274
+ % Create the engine - pass SessionId and initial state to mi/3
275
+ engine_create(_,
276
+ deepclause_mi:mi(Goal, InitialState, SessionId),
277
+ Engine),
278
+ assertz(session_engine(SessionId, Engine)).
279
+
280
+ %% determine_agent_goal(+SessionId, +Args, -Goal)
281
+ %% Args is a list of positional arguments - passed directly to agent_main
282
+ %% Prefers arity that matches the number of arguments provided
283
+ determine_agent_goal(SessionId, [], SessionId:agent_main) :-
284
+ current_predicate(SessionId:agent_main/0), !.
285
+ determine_agent_goal(SessionId, [Arg1], SessionId:agent_main(Arg1)) :-
286
+ current_predicate(SessionId:agent_main/1), !.
287
+ determine_agent_goal(SessionId, [Arg1, Arg2], SessionId:agent_main(Arg1, Arg2)) :-
288
+ current_predicate(SessionId:agent_main/2), !.
289
+ determine_agent_goal(SessionId, [Arg1, Arg2, Arg3], SessionId:agent_main(Arg1, Arg2, Arg3)) :-
290
+ current_predicate(SessionId:agent_main/3), !.
291
+ % Fallback: if no exact arity match, try best fit
292
+ determine_agent_goal(SessionId, Args, Goal) :-
293
+ current_predicate(SessionId:agent_main/1), !,
294
+ ( Args = [Arg1|_]
295
+ -> Goal = SessionId:agent_main(Arg1)
296
+ ; Goal = SessionId:agent_main(_)
297
+ ).
298
+ determine_agent_goal(SessionId, Args, Goal) :-
299
+ current_predicate(SessionId:agent_main/2), !,
300
+ ( Args = [Arg1, Arg2|_]
301
+ -> Goal = SessionId:agent_main(Arg1, Arg2)
302
+ ; Args = [Arg1|_]
303
+ -> Goal = SessionId:agent_main(Arg1, _)
304
+ ; Goal = SessionId:agent_main(_, _)
305
+ ).
306
+ determine_agent_goal(SessionId, Args, Goal) :-
307
+ current_predicate(SessionId:agent_main/3), !,
308
+ ( Args = [Arg1, Arg2, Arg3|_]
309
+ -> Goal = SessionId:agent_main(Arg1, Arg2, Arg3)
310
+ ; Args = [Arg1, Arg2|_]
311
+ -> Goal = SessionId:agent_main(Arg1, Arg2, _)
312
+ ; Args = [Arg1|_]
313
+ -> Goal = SessionId:agent_main(Arg1, _, _)
314
+ ; Goal = SessionId:agent_main(_, _, _)
315
+ ).
316
+ determine_agent_goal(SessionId, _Args, SessionId:agent_main).
317
+
318
+ %% step_engine(+SessionId, -Status, -Content, -Payload)
319
+ step_engine(SessionId, Status, Content, Payload) :-
320
+ ( session_engine(SessionId, Engine)
321
+ -> catch(
322
+ ( engine_next(Engine, Result)
323
+ -> process_engine_result(Result, Status, Content, Payload)
324
+ ; % Engine finished - return trace if enabled
325
+ Status = finished,
326
+ Content = '',
327
+ ( session_trace_enabled(SessionId, true),
328
+ session_trace_log(SessionId, TraceLog)
329
+ -> Payload = payload{trace: TraceLog}
330
+ ; Payload = none
331
+ )
332
+ ),
333
+ Error,
334
+ ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
335
+ Status = error,
336
+ Content = ErrMsg,
337
+ Payload = none
338
+ )
339
+ )
340
+ ; Status = error,
341
+ Content = 'No engine found for session',
342
+ Payload = none
343
+ ).
344
+
345
+ %% process_engine_result(+Result, -Status, -Content, -Payload)
346
+ process_engine_result(output(Text), output, Text, none) :- !.
347
+ process_engine_result(log(Text), log, Text, none) :- !.
348
+ process_engine_result(answer(Text), answer, Text, none) :- !.
349
+ %% Note: Memory and tool scope are now passed in the payload for the agent loop
350
+ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope), request_agent_loop, '',
351
+ payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope}) :- !.
352
+ %% Legacy 4-arg format (no tool scope)
353
+ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), request_agent_loop, '',
354
+ payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: none}) :- !.
355
+ process_engine_result(request_exec(Tool, Args), request_exec, '',
356
+ payload{toolName: Tool, args: Args}) :- !.
357
+ %% Tool result from inline tool execution
358
+ process_engine_result(tool_result(Result), tool_result, '',
359
+ payload{result: Result}) :- !.
360
+ process_engine_result(wait_input(Prompt), wait_input, Prompt, none) :- !.
361
+ process_engine_result(error(Msg), error, Msg, none) :- !.
362
+ process_engine_result(Other, error, Msg, none) :-
363
+ format(atom(Msg), 'Unknown engine result: ~w', [Other]).
364
+
365
+ %% destroy_engine(+SessionId)
366
+ destroy_engine(SessionId) :-
367
+ ( session_engine(SessionId, Engine)
368
+ -> catch(engine_destroy(Engine), _, true)
369
+ ; true
370
+ ),
371
+ retractall(session_engine(SessionId, _)),
372
+ retractall(session_params(SessionId, _)),
373
+ retractall(session_user_tools(SessionId, _)),
374
+ retractall(session_user_tool_schema(SessionId, _, _)),
375
+ retractall(session_pending_input(SessionId, _)),
376
+ retractall(session_agent_result(SessionId, _)),
377
+ retractall(session_exec_result(SessionId, _)),
378
+ retractall(session_trace_log(SessionId, _)),
379
+ retractall(session_trace_enabled(SessionId, _)),
380
+ ( current_module(SessionId)
381
+ -> catch(
382
+ ( findall(Head,
383
+ (current_predicate(SessionId:Name/Arity),
384
+ functor(Head, Name, Arity),
385
+ clause(SessionId:Head, _)),
386
+ Heads),
387
+ forall(member(H, Heads), retractall(SessionId:H))
388
+ ),
389
+ _,
390
+ true
391
+ )
392
+ ; true
393
+ ).
394
+
395
+ %% ============================================================
396
+ %% Result Posting (from JavaScript)
397
+ %% ============================================================
398
+
399
+ %% post_agent_result(+SessionId, +Success, +Variables, +Messages)
400
+ %% Messages is a list of message{role: Role, content: Content} dicts
401
+ post_agent_result(SessionId, Success, Variables, Messages) :-
402
+ assertz(session_agent_result(SessionId, result{success: Success, variables: Variables, messages: Messages})),
403
+ post_signal_to_engine(SessionId, agent_done).
404
+
405
+ %% post_exec_result(+SessionId, +Status, +Result)
406
+ post_exec_result(SessionId, Status, Result) :-
407
+ retractall(session_exec_result(SessionId, _)),
408
+ assertz(session_exec_result(SessionId, result{status: Status, result: Result})),
409
+ post_signal_to_engine(SessionId, exec_done).
410
+
411
+ %% provide_input(+SessionId, +Input)
412
+ provide_input(SessionId, Input) :-
413
+ assertz(session_pending_input(SessionId, Input)),
414
+ post_signal_to_engine(SessionId, input_provided).
415
+
416
+ %% post_signal_to_engine(+SessionId, +Signal)
417
+ %% Posts a signal via dynamic predicate (most reliable for WASM engines)
418
+ %% Falls back to engine_post only if dynamic assert fails
419
+ post_signal_to_engine(SessionId, Signal) :-
420
+ % Always use dynamic predicate - more reliable than engine_post in WASM
421
+ assertz(session_pending_signal(SessionId, Signal)).
422
+
423
+ %% ============================================================
424
+ %% Meta-Interpreter Core - STATE THREADED VERSION
425
+ %% ============================================================
426
+
427
+ %% mi(+Goal, +StateIn, +SessionId)
428
+ %% Main meta-interpreter entry point
429
+ %% State = state{memory: [...], params: {...}}
430
+ mi(Goal, StateIn, SessionId) :-
431
+ nb_setval(current_session_id, SessionId),
432
+ catch(
433
+ mi_call(Goal, StateIn, _StateOut),
434
+ Error,
435
+ ( Error == '$answer_commit'
436
+ -> true % answer/1 threw to commit - success, no backtracking
437
+ ; format(atom(ErrMsg), 'Runtime error: ~w', [Error]),
438
+ engine_yield(error(ErrMsg)),
439
+ fail
440
+ )
441
+ ),
442
+ !. % Cut to prevent backtracking after goal completes
443
+
444
+ %% ============================================================
445
+ %% State Helpers
446
+ %% ============================================================
447
+
448
+ %% add_memory(+StateIn, +Role, +Content, -StateOut)
449
+ add_memory(StateIn, Role, Content, StateOut) :-
450
+ Message = message{role: Role, content: Content},
451
+ OldMemory = StateIn.memory,
452
+ append(OldMemory, [Message], NewMemory),
453
+ StateOut = StateIn.put(memory, NewMemory).
454
+
455
+ %% get_memory(+State, -Memory)
456
+ get_memory(State, Memory) :-
457
+ Memory = State.memory.
458
+
459
+ %% get_params(+State, -Params)
460
+ get_params(State, Params) :-
461
+ Params = State.params.
462
+
463
+ %% get_context_stack(+State, -Stack)
464
+ get_context_stack(State, Stack) :-
465
+ ( get_dict(context_stack, State, Stack)
466
+ -> true
467
+ ; Stack = []
468
+ ).
469
+
470
+ %% set_context_stack(+StateIn, +Stack, -StateOut)
471
+ set_context_stack(StateIn, Stack, StateOut) :-
472
+ StateOut = StateIn.put(context_stack, Stack).
473
+
474
+ %% set_memory(+StateIn, +Memory, -StateOut)
475
+ set_memory(StateIn, Memory, StateOut) :-
476
+ StateOut = StateIn.put(memory, Memory).
477
+
478
+ %% ============================================================
479
+ %% Tool Scope Helpers
480
+ %% ============================================================
481
+ %% Tool scoping allows manual control over which tools are available
482
+ %% in nested tasks. The scope is stored in state and passed through
483
+ %% the request_agent_loop payload.
484
+
485
+ %% get_tool_scope(+State, -Scope)
486
+ %% Returns the current tool scope, or 'none' if not set
487
+ get_tool_scope(State, Scope) :-
488
+ ( get_dict(tool_scope, State, Scope)
489
+ -> true
490
+ ; Scope = none
491
+ ).
492
+
493
+ %% set_tool_scope(+StateIn, +Scope, -StateOut)
494
+ set_tool_scope(StateIn, Scope, StateOut) :-
495
+ StateOut = StateIn.put(tool_scope, Scope).
496
+
497
+ %% clear_tool_scope(+StateIn, -StateOut)
498
+ clear_tool_scope(StateIn, StateOut) :-
499
+ ( get_dict(tool_scope, StateIn, _)
500
+ -> del_dict(tool_scope, StateIn, _, StateOut)
501
+ ; StateOut = StateIn
502
+ ).
503
+
504
+ %% ============================================================
505
+ %% Trace Helpers
506
+ %% ============================================================
507
+
508
+ %% get_depth(+State, -Depth)
509
+ get_depth(State, Depth) :-
510
+ ( get_dict(depth, State, Depth)
511
+ -> true
512
+ ; Depth = 0
513
+ ).
514
+
515
+ %% set_depth(+StateIn, +Depth, -StateOut)
516
+ set_depth(StateIn, Depth, StateOut) :-
517
+ StateOut = StateIn.put(depth, Depth).
518
+
519
+ %% add_trace_entry(+SessionId, +Type, +Predicate, +Args, +Depth)
520
+ %% Add a trace entry if tracing is enabled
521
+ add_trace_entry(SessionId, Type, Predicate, Args, Depth) :-
522
+ ( session_trace_enabled(SessionId, true)
523
+ -> get_time(Now),
524
+ Timestamp is round(Now * 1000), % Convert to milliseconds
525
+ Entry = trace{timestamp: Timestamp, type: Type, predicate: Predicate, args: Args, depth: Depth},
526
+ session_trace_log(SessionId, OldLog),
527
+ retract(session_trace_log(SessionId, OldLog)),
528
+ append(OldLog, [Entry], NewLog),
529
+ assertz(session_trace_log(SessionId, NewLog))
530
+ ; true
531
+ ).
532
+
533
+ %% add_trace_with_result(+SessionId, +Type, +Predicate, +Args, +Result, +Depth)
534
+ %% Add a trace entry with a result field
535
+ add_trace_with_result(SessionId, Type, Predicate, Args, Result, Depth) :-
536
+ ( session_trace_enabled(SessionId, true)
537
+ -> get_time(Now),
538
+ Timestamp is round(Now * 1000),
539
+ Entry = trace{timestamp: Timestamp, type: Type, predicate: Predicate, args: Args, result: Result, depth: Depth},
540
+ session_trace_log(SessionId, OldLog),
541
+ retract(session_trace_log(SessionId, OldLog)),
542
+ append(OldLog, [Entry], NewLog),
543
+ assertz(session_trace_log(SessionId, NewLog))
544
+ ; true
545
+ ).
546
+
547
+ %% ============================================================
548
+ %% Agent Signal Loop - SIMPLIFIED
549
+ %% ============================================================
550
+ %%
551
+ %% With isolated tool execution, the agent signal loop is much simpler.
552
+ %% It only needs to wait for agent_done from the host after a task() completes.
553
+ %% Tool calls are handled separately via execute_tool_isolated/4.
554
+
555
+ %% handle_agent_signals(+SessionId, +StateIn, -StateOut)
556
+ %% Wait for agent_done signal from host after task() request.
557
+ %% State passes through unchanged (tool results don't modify task state).
558
+ handle_agent_signals(SessionId, StateIn, StateOut) :-
559
+ get_next_signal(SessionId, Signal),
560
+ handle_signal(SessionId, Signal, StateIn, StateOut).
561
+
562
+ %% get_next_signal(+SessionId, -Signal)
563
+ %% Retrieves the next signal, preferring dynamic predicate over engine_fetch.
564
+ get_next_signal(SessionId, Signal) :-
565
+ ( retract(session_pending_signal(SessionId, Signal))
566
+ -> true
567
+ ; catch(
568
+ engine_fetch(Signal),
569
+ _FetchError,
570
+ ( retract(session_pending_signal(SessionId, Signal))
571
+ -> true
572
+ ; Signal = error_signal(no_signal_available)
573
+ )
574
+ )
575
+ ).
576
+
577
+ %% handle_signal(+SessionId, +Signal, +StateIn, -StateOut)
578
+ handle_signal(_SessionId, agent_done, State, State) :- !.
579
+
580
+ handle_signal(_SessionId, error_signal(Error), State, State) :-
581
+ !,
582
+ format(atom(ErrMsg), 'Engine fetch error: ~w', [Error]),
583
+ engine_yield(error(ErrMsg)).
584
+
585
+ handle_signal(SessionId, exec_done, StateIn, StateOut) :-
586
+ % Stale signal - ignore and continue waiting
587
+ !,
588
+ handle_agent_signals(SessionId, StateIn, StateOut).
589
+
590
+ handle_signal(SessionId, _Unknown, StateIn, StateOut) :-
591
+ % Unknown signal - ignore and continue waiting
592
+ !,
593
+ handle_agent_signals(SessionId, StateIn, StateOut).
594
+
595
+ %% call_tool_inline/6 is DEPRECATED - kept for backwards compatibility only
596
+ %% New code should use execute_tool_isolated/4 instead.
597
+ %% call_tool_inline(+SessionId, +ToolName, +Args, +StateIn, -StateOut, -Result)
598
+ %% Executes a DML-defined tool inline in the current engine.
599
+ %% The tool body runs through the meta-interpreter, sharing state.
600
+ %% Args is a list of input argument values from the tool call.
601
+ %% The tool may have additional output arguments which become unbound variables.
602
+ call_tool_inline(SessionId, ToolName, Args, StateIn, StateOut, Result) :-
603
+ % Convert tool name to atom
604
+ atom_string(ToolNameAtom, ToolName),
605
+ % Find the tool clause to determine its actual arity
606
+ % Tools are stored as: SessionId:(tool(Head) :- Body)
607
+ ( clause(SessionId:tool(ToolPattern), Body),
608
+ ToolPattern =.. [ToolNameAtom|PatternArgs]
609
+ -> % Found the tool - create head args with inputs filled in
610
+ length(PatternArgs, TotalArity),
611
+ length(Args, InputArity),
612
+ % Create the head with input args + unbound output variables
613
+ length(HeadArgs, TotalArity),
614
+ % Fill in the input args
615
+ fill_args(HeadArgs, Args, InputArity),
616
+ ToolHead =.. [ToolNameAtom|HeadArgs],
617
+ % Unify with the pattern to bind variables in Body
618
+ ToolHead = ToolPattern,
619
+ % Execute the tool body through MI
620
+ % Wrap in catch to handle answer() commits - answer() yields and then throws
621
+ catch(
622
+ ( mi_call(Body, StateIn, StateOut)
623
+ -> % Collect result from result/1 facts or output args
624
+ % Use catch to handle case where result/2 doesn't exist
625
+ ( catch(SessionId:result(ToolNameAtom, R), _, fail)
626
+ -> Result = R,
627
+ catch(retract(SessionId:result(ToolNameAtom, R)), _, true)
628
+ ; % No explicit result - return the last output arg if bound
629
+ ( TotalArity > InputArity,
630
+ last(HeadArgs, LastArg),
631
+ nonvar(LastArg)
632
+ -> Result = LastArg
633
+ ; Result = true
634
+ )
635
+ )
636
+ ; % Tool body failed
637
+ StateOut = StateIn,
638
+ Result = false
639
+ ),
640
+ '$answer_commit',
641
+ % answer() was called - it already yielded the answer text
642
+ % Just set success result and state
643
+ (StateOut = StateIn, Result = answered)
644
+ )
645
+ ; % No DML body - should not happen if host checked
646
+ StateOut = StateIn,
647
+ format(atom(Result), 'Tool ~w not found', [ToolName])
648
+ ).
649
+
650
+ %% ============================================================
651
+ %% ISOLATED Tool Execution (Engine-based for exec() support)
652
+ %% ============================================================
653
+ %%
654
+ %% Tools run in their own engine to support exec() calls.
655
+ %% TypeScript creates the engine, steps it, handles request_exec,
656
+ %% and collects the result when the engine finishes.
657
+ %% exec() results are posted via the SESSION's exec_result mechanism.
658
+
659
+ %% Tool engine dynamic predicates
660
+ :- dynamic tool_engine/2. % tool_engine(ToolEngineId, Engine)
661
+
662
+ %% create_tool_engine(+SessionId, +ToolName, +Args, -ToolEngineId)
663
+ %% Creates an engine to execute a DML tool. Returns a unique engine ID.
664
+ create_tool_engine(SessionId, ToolName, Args, ToolEngineId) :-
665
+ atom_string(ToolNameAtom, ToolName),
666
+ % Generate unique tool engine ID
667
+ gensym(tool_engine_, ToolEngineId),
668
+ ( clause(SessionId:tool(ToolPattern), Body),
669
+ ToolPattern =.. [ToolNameAtom|PatternArgs]
670
+ -> length(PatternArgs, TotalArity),
671
+ length(Args, InputArity),
672
+ length(HeadArgs, TotalArity),
673
+ fill_args(HeadArgs, Args, InputArity),
674
+ ToolHead =.. [ToolNameAtom|HeadArgs],
675
+ ToolHead = ToolPattern,
676
+ (session_params(SessionId, Params) -> true ; Params = params{}),
677
+ InitialState = state{memory: [], params: Params, context_stack: [], depth: 0},
678
+ % Create goal that will yield the result
679
+ Goal = (
680
+ nb_setval(current_session_id, SessionId),
681
+ catch(
682
+ (mi_call(Body, InitialState, _FinalState),
683
+ extract_tool_result_simple(TotalArity, InputArity, HeadArgs, ToolResult),
684
+ engine_yield(tool_finished(ToolResult))),
685
+ Error,
686
+ (handle_tool_engine_error(Error, ErrResult),
687
+ engine_yield(tool_finished(ErrResult)))
688
+ )
689
+ ),
690
+ engine_create(_, Goal, Engine),
691
+ assertz(tool_engine(ToolEngineId, Engine))
692
+ ; % Tool not found - create engine that just yields error
693
+ engine_create(_, engine_yield(tool_finished(error{message: "Tool not found"})), Engine),
694
+ assertz(tool_engine(ToolEngineId, Engine))
695
+ ).
696
+
697
+ %% handle_tool_engine_error(+Error, -Result)
698
+ handle_tool_engine_error('$answer_commit', true) :- !.
699
+ handle_tool_engine_error(Error, error{message: ErrMsg}) :-
700
+ format(atom(ErrMsg), '~w', [Error]).
701
+
702
+ %% step_tool_engine(+ToolEngineId, -Status, -Content, -Payload)
703
+ %% Steps the tool engine, similar to step_engine but for tools.
704
+ step_tool_engine(ToolEngineId, Status, Content, Payload) :-
705
+ ( tool_engine(ToolEngineId, Engine)
706
+ -> catch(
707
+ ( engine_next(Engine, Result)
708
+ -> process_tool_engine_result(Result, Status, Content, Payload)
709
+ ; % Engine exhausted without yielding - treat as failure
710
+ Status = finished,
711
+ Content = '',
712
+ Payload = payload{result: false}
713
+ ),
714
+ Error,
715
+ ( format(atom(ErrMsg), 'Tool engine error: ~w', [Error]),
716
+ Status = error,
717
+ Content = ErrMsg,
718
+ Payload = none
719
+ )
720
+ )
721
+ ; Status = error,
722
+ Content = 'No tool engine found',
723
+ Payload = none
724
+ ).
725
+
726
+ %% process_tool_engine_result(+Result, -Status, -Content, -Payload)
727
+ process_tool_engine_result(tool_finished(ToolResult), finished, '', payload{result: ToolResult}) :- !.
728
+ process_tool_engine_result(request_exec(Tool, Args), request_exec, '',
729
+ payload{toolName: Tool, args: Args}) :- !.
730
+ process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope), request_agent_loop, '',
731
+ payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope}) :- !.
732
+ %% Legacy 4-arg format (no tool scope)
733
+ process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), request_agent_loop, '',
734
+ payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: none}) :- !.
735
+ process_tool_engine_result(output(Text), output, Text, none) :- !.
736
+ process_tool_engine_result(log(Text), log, Text, none) :- !.
737
+ process_tool_engine_result(Other, error, Msg, none) :-
738
+ format(atom(Msg), 'Unknown tool engine result: ~w', [Other]).
739
+
740
+ %% destroy_tool_engine(+ToolEngineId)
741
+ destroy_tool_engine(ToolEngineId) :-
742
+ ( tool_engine(ToolEngineId, Engine)
743
+ -> catch(engine_destroy(Engine), _, true)
744
+ ; true
745
+ ),
746
+ retractall(tool_engine(ToolEngineId, _)).
747
+
748
+ %% post_tool_agent_result(+SessionId, +ToolEngineId, +Success, +Variables, +Messages)
749
+ %% Posts the result of a nested agent loop back to the tool engine.
750
+ %% Also stores in session_agent_result so the meta-interpreter can retrieve it.
751
+ :- dynamic tool_engine_agent_result/2. % tool_engine_agent_result(ToolEngineId, Result)
752
+
753
+ post_tool_agent_result(SessionId, ToolEngineId, Success, Variables, Messages) :-
754
+ Result = result{success: Success, variables: Variables, messages: Messages},
755
+ % Store in tool_engine_agent_result (for future use)
756
+ retractall(tool_engine_agent_result(ToolEngineId, _)),
757
+ assertz(tool_engine_agent_result(ToolEngineId, Result)),
758
+ % Also store in session_agent_result so task() in the meta-interpreter can read it
759
+ retractall(session_agent_result(SessionId, _)),
760
+ assertz(session_agent_result(SessionId, Result)),
761
+ % Signal the tool engine that agent result is ready
762
+ ( tool_engine(ToolEngineId, Engine)
763
+ -> catch(engine_post(Engine, agent_done), _, true)
764
+ ; true
765
+ ).
766
+
767
+ %% Helper predicates (shared with old isolated execution)
768
+
769
+ %% extract_tool_result_simple(+TotalArity, +InputArity, +HeadArgs, -Result)
770
+ extract_tool_result_simple(TotalArity, InputArity, HeadArgs, Result) :-
771
+ ( TotalArity > InputArity,
772
+ last(HeadArgs, LastArg),
773
+ nonvar(LastArg)
774
+ -> Result = LastArg
775
+ ; Result = true
776
+ ).
777
+
778
+ %% fill_args(+HeadArgs, +InputArgs, +Count)
779
+ %% Fill the first Count elements of HeadArgs with InputArgs
780
+ fill_args(_, [], 0) :- !.
781
+ fill_args([H|T], [A|As], N) :-
782
+ N > 0,
783
+ H = A,
784
+ N1 is N - 1,
785
+ fill_args(T, As, N1).
786
+
787
+ %% bind_tool_params(+ParamsList, +ArgsDict)
788
+ %% Binds tool parameters from the arguments dictionary
789
+ bind_tool_params([], _) :- !.
790
+ bind_tool_params([Param|Rest], Args) :-
791
+ ( atom(Param)
792
+ -> ParamName = Param, ParamVar = Param
793
+ ; Param = (ParamName = ParamVar)
794
+ ),
795
+ ( get_dict(ParamName, Args, Value)
796
+ -> ParamVar = Value
797
+ ; true % Leave unbound if not provided
798
+ ),
799
+ bind_tool_params(Rest, Args).
800
+
801
+ %% ============================================================
802
+ %% Task Handling
803
+ %% ============================================================
804
+
805
+ %% collect_user_tools(+SessionId, -ToolSchemas)
806
+ collect_user_tools(SessionId, ToolSchemas) :-
807
+ findall(
808
+ tool_info{name: Name, schema: Schema, source: Source},
809
+ (
810
+ session_user_tool_schema(SessionId, Name, Schema),
811
+ (SessionId:tool_source(Name, Source) -> true ; Source = "")
812
+ ),
813
+ ToolSchemas
814
+ ).
815
+
816
+ %% mi_call(task(Desc), +StateIn, -StateOut)
817
+ mi_call(task(Desc), StateIn, StateOut) :-
818
+ !,
819
+ get_params(StateIn, Params),
820
+ interpolate_desc(Desc, Params, InterpDesc),
821
+ get_session_id(SessionId),
822
+ get_depth(StateIn, Depth),
823
+ add_trace_entry(SessionId, llm_call, task, [InterpDesc], Depth),
824
+ collect_user_tools(SessionId, UserTools),
825
+ get_memory(StateIn, Memory),
826
+ get_tool_scope(StateIn, ToolScope),
827
+ % Yield request with current memory and tool scope
828
+ engine_yield(request_agent_loop(InterpDesc, [], UserTools, Memory, ToolScope)),
829
+ % Handle signals (tool calls) until agent_done
830
+ handle_agent_signals(SessionId, StateIn, StateAfterAgent),
831
+ session_agent_result(SessionId, Result),
832
+ retract(session_agent_result(SessionId, _)),
833
+ % Check success and warn on failure
834
+ ( Result.success == true
835
+ -> true
836
+ ; ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
837
+ -> format(atom(WarnMsg), 'Warning: task/1 failed: ~w', [ErrorMsg])
838
+ ; format(atom(WarnMsg), 'Warning: task/1 failed (success=false)', [])
839
+ ),
840
+ engine_yield(output(WarnMsg)),
841
+ fail
842
+ ),
843
+ % Use full messages from agent loop result
844
+ ( get_dict(messages, Result, Messages), Messages \= []
845
+ -> % Replace memory with all messages from agent (includes system, history, and new messages)
846
+ set_memory(StateAfterAgent, Messages, StateOut)
847
+ ; % Fallback: just add task description and response (old behavior)
848
+ add_memory(StateAfterAgent, user, InterpDesc, State1),
849
+ ( get_dict(response, Result, Response), Response \= ""
850
+ -> add_trace_with_result(SessionId, exit, task, [InterpDesc], Response, Depth),
851
+ add_memory(State1, assistant, Response, StateOut)
852
+ ; add_trace_entry(SessionId, exit, task, [InterpDesc], Depth),
853
+ StateOut = State1
854
+ )
855
+ ).
856
+
857
+ %% mi_call(task_named(Desc, Vars, VarNames), +StateIn, -StateOut)
858
+ %% New unified handler for task/N with embedded variable names
859
+ mi_call(task_named(Desc, Vars, VarNames), StateIn, StateOut) :-
860
+ !,
861
+ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut).
862
+
863
+ %% ============================================================
864
+ %% Prompt Handling - Fresh LLM call without existing memory
865
+ %% ============================================================
866
+
867
+ %% mi_call(prompt(Desc), +StateIn, -StateOut)
868
+ %% Like task/1 but starts with empty memory
869
+ mi_call(prompt(Desc), StateIn, StateOut) :-
870
+ !,
871
+ get_params(StateIn, Params),
872
+ interpolate_desc(Desc, Params, InterpDesc),
873
+ get_session_id(SessionId),
874
+ collect_user_tools(SessionId, UserTools),
875
+ get_tool_scope(StateIn, ToolScope),
876
+ % Use empty memory instead of current memory
877
+ engine_yield(request_agent_loop(InterpDesc, [], UserTools, [], ToolScope)),
878
+ % Handle signals (tool calls) until agent_done - still need for tool execution
879
+ handle_agent_signals(SessionId, StateIn, StateOut),
880
+ session_agent_result(SessionId, Result),
881
+ retract(session_agent_result(SessionId, _)),
882
+ % Check success and warn on failure
883
+ ( Result.success == true
884
+ -> true
885
+ ; ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
886
+ -> format(atom(WarnMsg), 'Warning: prompt/1 failed: ~w', [ErrorMsg])
887
+ ; format(atom(WarnMsg), 'Warning: prompt/1 failed (success=false)', [])
888
+ ),
889
+ engine_yield(output(WarnMsg)),
890
+ fail
891
+ ).
892
+ % Don't modify memory - but state may have changed from tool calls
893
+
894
+ %% mi_call(prompt_named(Desc, Vars, VarNames), +StateIn, -StateOut)
895
+ %% New unified handler for prompt/N with embedded variable names
896
+ mi_call(prompt_named(Desc, Vars, VarNames), StateIn, StateOut) :-
897
+ !,
898
+ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut).
899
+
900
+ %% mi_call_prompt_n(+Desc, +Vars, +VarNames, +StateIn, -StateOut)
901
+ %% Like mi_call_task_n but uses empty memory and doesn't update memory
902
+ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
903
+ !, % Cut to make this predicate deterministic - no backtracking once started
904
+ get_params(StateIn, Params),
905
+ interpolate_desc(Desc, Params, InterpDesc),
906
+ get_session_id(SessionId),
907
+ collect_user_tools(SessionId, UserTools),
908
+ get_tool_scope(StateIn, ToolScope),
909
+ % Use empty memory instead of current memory
910
+ engine_yield(request_agent_loop(InterpDesc, VarNames, UserTools, [], ToolScope)),
911
+ % Handle signals (tool calls) until agent_done - still need for tool execution
912
+ handle_agent_signals(SessionId, StateIn, StateOut),
913
+ session_agent_result(SessionId, Result),
914
+ retract(session_agent_result(SessionId, _)),
915
+ % Check success and warn on failure
916
+ ( Result.success == true
917
+ -> true
918
+ ; length(VarNames, Arity),
919
+ ActualArity is Arity + 1,
920
+ ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
921
+ -> format(atom(WarnMsg), 'Warning: prompt/~w failed: ~w', [ActualArity, ErrorMsg])
922
+ ; format(atom(WarnMsg), 'Warning: prompt/~w failed (success=false)', [ActualArity])
923
+ ),
924
+ engine_yield(output(WarnMsg)),
925
+ fail
926
+ ),
927
+ bind_task_variables(Result.variables, VarNames, Vars).
928
+ % Don't modify memory - but state may have changed from tool calls
929
+
930
+ %% mi_call_task_n(+Desc, +Vars, +VarNames, +StateIn, -StateOut)
931
+ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
932
+ !, % Cut to make this predicate deterministic - no backtracking once started
933
+ get_params(StateIn, Params),
934
+ interpolate_desc(Desc, Params, InterpDesc),
935
+ get_session_id(SessionId),
936
+ collect_user_tools(SessionId, UserTools),
937
+ get_memory(StateIn, Memory),
938
+ get_tool_scope(StateIn, ToolScope),
939
+ engine_yield(request_agent_loop(InterpDesc, VarNames, UserTools, Memory, ToolScope)),
940
+ % Handle signals (tool calls) until agent_done
941
+ handle_agent_signals(SessionId, StateIn, StateAfterAgent),
942
+ session_agent_result(SessionId, Result),
943
+ retract(session_agent_result(SessionId, _)),
944
+ % Check success and warn on failure
945
+ ( Result.success == true
946
+ -> true
947
+ ; length(VarNames, Arity),
948
+ ActualArity is Arity + 1,
949
+ ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
950
+ -> format(atom(WarnMsg), 'Warning: task/~w failed: ~w', [ActualArity, ErrorMsg])
951
+ ; format(atom(WarnMsg), 'Warning: task/~w failed (success=false)', [ActualArity])
952
+ ),
953
+ engine_yield(output(WarnMsg)),
954
+ fail
955
+ ),
956
+ bind_task_variables(Result.variables, VarNames, Vars),
957
+ % Use full messages from agent loop result
958
+ ( get_dict(messages, Result, Messages), Messages \= []
959
+ -> % Replace memory with all messages from agent
960
+ set_memory(StateAfterAgent, Messages, StateOut)
961
+ ; % Fallback: just add task description and response (old behavior)
962
+ add_memory(StateAfterAgent, user, InterpDesc, State1),
963
+ ( get_dict(response, Result, Response), Response \= ""
964
+ -> add_memory(State1, assistant, Response, StateOut)
965
+ ; StateOut = State1
966
+ )
967
+ ).
968
+
969
+ %% bind_task_variables(+VarsDict, +Names, -Values)
970
+ bind_task_variables(_, [], []) :- !.
971
+ bind_task_variables(VarsDict, [Name|Names], [Value|Values]) :-
972
+ ( get_dict(Name, VarsDict, Value)
973
+ -> true
974
+ ; true
975
+ ),
976
+ bind_task_variables(VarsDict, Names, Values).
977
+
978
+ %% ============================================================
979
+ %% Exec Handling
980
+ %% ============================================================
981
+
982
+ %% mi_call(exec(ToolCall, Output), +StateIn, -StateOut)
983
+ mi_call(exec(ToolCall, Output), StateIn, StateIn) :-
984
+ !,
985
+ ToolCall =.. [ToolName|Args],
986
+ get_session_id(SessionId),
987
+ get_depth(StateIn, Depth),
988
+ add_trace_entry(SessionId, exec, ToolName, Args, Depth),
989
+ engine_yield(request_exec(ToolName, Args)),
990
+ % Use get_next_signal helper which handles both dynamic predicate and engine_fetch
991
+ get_next_signal(SessionId, _Signal),
992
+ session_exec_result(SessionId, Result),
993
+ retract(session_exec_result(SessionId, _)),
994
+ ( Result.status == success
995
+ -> Output = Result.result,
996
+ add_trace_with_result(SessionId, exit, ToolName, Args, Output, Depth)
997
+ ; add_trace_entry(SessionId, fail, ToolName, Args, Depth),
998
+ fail
999
+ ).
1000
+
1001
+ %% ============================================================
1002
+ %% Tool Scoping Predicates
1003
+ %% ============================================================
1004
+ %% These predicates allow manual control over which tools are available
1005
+ %% to nested task() calls. They set a scope in the state which is passed
1006
+ %% to the TypeScript runner and used to filter available tools.
1007
+ %%
1008
+ %% Example:
1009
+ %% tool(smart_format(Items, Output)) :-
1010
+ %% with_tools([summarize, calculate], (
1011
+ %% task("Format this nicely", Output)
1012
+ %% )).
1013
+
1014
+ %% mi_call(with_tools(ToolList, Goal), +StateIn, -StateOut)
1015
+ %% Run Goal with only the specified tools available to nested tasks
1016
+ mi_call(with_tools(ToolList, Goal), StateIn, StateOut) :-
1017
+ !,
1018
+ set_tool_scope(StateIn, whitelist(ToolList), ScopedState),
1019
+ mi_call(Goal, ScopedState, StateWithScope),
1020
+ clear_tool_scope(StateWithScope, StateOut).
1021
+
1022
+ %% mi_call(without_tools(ToolList, Goal), +StateIn, -StateOut)
1023
+ %% Run Goal with the specified tools excluded from nested tasks
1024
+ mi_call(without_tools(ToolList, Goal), StateIn, StateOut) :-
1025
+ !,
1026
+ set_tool_scope(StateIn, blacklist(ToolList), ScopedState),
1027
+ mi_call(Goal, ScopedState, StateWithScope),
1028
+ clear_tool_scope(StateWithScope, StateOut).
1029
+
1030
+ %% ============================================================
1031
+ %% Memory Predicates - NOW BACKTRACKABLE VIA STATE!
1032
+ %% ============================================================
1033
+
1034
+ %% mi_call(system(Text), +StateIn, -StateOut)
1035
+ mi_call(system(Text), StateIn, StateOut) :-
1036
+ !,
1037
+ get_params(StateIn, Params),
1038
+ interpolate_desc(Text, Params, InterpText),
1039
+ add_memory(StateIn, system, InterpText, StateOut).
1040
+
1041
+ %% mi_call(user(Text), +StateIn, -StateOut)
1042
+ mi_call(user(Text), StateIn, StateOut) :-
1043
+ !,
1044
+ get_params(StateIn, Params),
1045
+ interpolate_desc(Text, Params, InterpText),
1046
+ add_memory(StateIn, user, InterpText, StateOut).
1047
+
1048
+ %% ============================================================
1049
+ %% Output Predicates
1050
+ %% ============================================================
1051
+
1052
+ %% mi_call(answer(Text), +StateIn, -StateOut)
1053
+ %% answer/1 commits by throwing - prevents backtracking to other clauses
1054
+ mi_call(answer(Text), StateIn, StateIn) :-
1055
+ get_params(StateIn, Params),
1056
+ interpolate_desc(Text, Params, InterpText),
1057
+ engine_yield(answer(InterpText)),
1058
+ throw('$answer_commit').
1059
+
1060
+ %% mi_call(output(Text), +StateIn, -StateOut)
1061
+ mi_call(output(Text), StateIn, StateIn) :-
1062
+ get_params(StateIn, Params),
1063
+ interpolate_desc(Text, Params, InterpText),
1064
+ get_session_id(SessionId),
1065
+ get_depth(StateIn, Depth),
1066
+ add_trace_entry(SessionId, output, output, [InterpText], Depth),
1067
+ engine_yield(output(InterpText)).
1068
+
1069
+ %% mi_call(input(Prompt, Input), +StateIn, -StateOut)
1070
+ %% Request input from the user with a prompt
1071
+ mi_call(input(Prompt, Input), StateIn, StateIn) :-
1072
+ get_params(StateIn, Params),
1073
+ interpolate_desc(Prompt, Params, InterpPrompt),
1074
+ get_session_id(SessionId),
1075
+ get_depth(StateIn, Depth),
1076
+ add_trace_entry(SessionId, input, input, [InterpPrompt], Depth),
1077
+ engine_yield(wait_input(InterpPrompt)),
1078
+ % Use get_next_signal helper which handles both dynamic predicate and engine_fetch
1079
+ get_next_signal(SessionId, _Signal),
1080
+ % Retrieve the input provided via provide_input/2
1081
+ ( session_pending_input(SessionId, Input)
1082
+ -> retract(session_pending_input(SessionId, Input)),
1083
+ add_trace_with_result(SessionId, exit, input, [InterpPrompt], Input, Depth)
1084
+ ; Input = "", % Default to empty if no input provided
1085
+ add_trace_with_result(SessionId, exit, input, [InterpPrompt], "", Depth)
1086
+ ).
1087
+
1088
+ %% mi_call(yield(Text), +StateIn, -StateOut)
1089
+ mi_call(yield(Text), StateIn, StateIn) :-
1090
+ get_params(StateIn, Params),
1091
+ interpolate_desc(Text, Params, InterpText),
1092
+ get_session_id(SessionId),
1093
+ get_depth(StateIn, Depth),
1094
+ add_trace_entry(SessionId, output, yield, [InterpText], Depth),
1095
+ engine_yield(output(InterpText)).
1096
+
1097
+ %% mi_call(log(Text), +StateIn, -StateOut)
1098
+ mi_call(log(Text), StateIn, StateIn) :-
1099
+ get_params(StateIn, Params),
1100
+ interpolate_desc(Text, Params, InterpText),
1101
+ engine_yield(log(InterpText)).
1102
+
1103
+ %% ============================================================
1104
+ %% Context Stack Management - for memory save/restore
1105
+ %% ============================================================
1106
+
1107
+ %% mi_call(push_context, +StateIn, -StateOut)
1108
+ %% Save current memory to stack, keep memory active
1109
+ mi_call(push_context, StateIn, StateOut) :-
1110
+ !,
1111
+ get_memory(StateIn, Memory),
1112
+ get_context_stack(StateIn, Stack),
1113
+ set_context_stack(StateIn, [Memory|Stack], StateOut).
1114
+
1115
+ %% mi_call(push_context(clear), +StateIn, -StateOut)
1116
+ %% Save current memory to stack AND clear memory
1117
+ mi_call(push_context(clear), StateIn, StateOut) :-
1118
+ !,
1119
+ get_memory(StateIn, Memory),
1120
+ get_context_stack(StateIn, Stack),
1121
+ set_context_stack(StateIn, [Memory|Stack], State1),
1122
+ set_memory(State1, [], StateOut).
1123
+
1124
+ %% mi_call(pop_context, +StateIn, -StateOut)
1125
+ %% Restore memory from stack
1126
+ mi_call(pop_context, StateIn, StateOut) :-
1127
+ !,
1128
+ get_context_stack(StateIn, Stack),
1129
+ ( Stack = [SavedMemory|RestStack]
1130
+ -> set_memory(StateIn, SavedMemory, State1),
1131
+ set_context_stack(State1, RestStack, StateOut)
1132
+ ; % Empty stack - do nothing
1133
+ StateOut = StateIn
1134
+ ).
1135
+
1136
+ %% mi_call(clear_memory, +StateIn, -StateOut)
1137
+ %% Clear current memory without saving
1138
+ mi_call(clear_memory, StateIn, StateOut) :-
1139
+ !,
1140
+ set_memory(StateIn, [], StateOut).
1141
+
1142
+ %% ============================================================
1143
+ %% Parameter Handling
1144
+ %% ============================================================
1145
+
1146
+ %% mi_call(param(Key, Value), +StateIn, -StateOut)
1147
+ %% Simple param/2 lookup from session module (asserted facts)
1148
+ mi_call(param(Key, Value), StateIn, StateIn) :-
1149
+ !,
1150
+ get_session_id(SessionId),
1151
+ ( atom(Key)
1152
+ -> KeyAtom = Key
1153
+ ; atom_string(KeyAtom, Key)
1154
+ ),
1155
+ SessionId:param(KeyAtom, Value).
1156
+
1157
+ %% mi_call(param(Key, Desc, Value), +StateIn, -StateOut)
1158
+ %% Legacy param/3 - still queries from state params dict
1159
+ mi_call(param(Key, _Desc, Value), StateIn, StateIn) :-
1160
+ !,
1161
+ get_params(StateIn, Params),
1162
+ ( atom(Key)
1163
+ -> KeyAtom = Key
1164
+ ; atom_string(KeyAtom, Key)
1165
+ ),
1166
+ get_dict(KeyAtom, Params, Value).
1167
+
1168
+ %% ============================================================
1169
+ %% Control Flow - STATE THREADED
1170
+ %% No cuts - rely on is_mi_special_predicate guard in catch-all
1171
+ %% ============================================================
1172
+
1173
+ %% mi_call((A, B), +StateIn, -StateOut)
1174
+ mi_call((A, B), StateIn, StateOut) :-
1175
+ mi_call(A, StateIn, State1),
1176
+ mi_call(B, State1, StateOut).
1177
+
1178
+ %% mi_call((Cond -> Then ; Else), +StateIn, -StateOut)
1179
+ %% MUST come before disjunction clause - if-then-else is syntactically a disjunction!
1180
+ mi_call((Cond -> Then ; Else), StateIn, StateOut) :-
1181
+ !, % Commit to this clause for if-then-else patterns
1182
+ ( mi_call(Cond, StateIn, State1)
1183
+ -> mi_call(Then, State1, StateOut)
1184
+ ; mi_call(Else, StateIn, StateOut)
1185
+ ).
1186
+
1187
+ %% mi_call((Cond -> Then), +StateIn, -StateOut)
1188
+ %% Soft cut without else branch
1189
+ mi_call((Cond -> Then), StateIn, StateOut) :-
1190
+ !, % Commit to this clause
1191
+ ( mi_call(Cond, StateIn, State1)
1192
+ -> mi_call(Then, State1, StateOut)
1193
+ ).
1194
+
1195
+ %% mi_call((A ; B), +StateIn, -StateOut)
1196
+ %% Disjunction - BACKTRACKABLE! On failure, StateIn is restored
1197
+ %% NOTE: This clause must come AFTER if-then-else clauses
1198
+ mi_call((A ; B), StateIn, StateOut) :-
1199
+ ( mi_call(A, StateIn, StateOut)
1200
+ ; mi_call(B, StateIn, StateOut)
1201
+ ).
1202
+
1203
+ %% mi_call(\+(Goal), +StateIn, -StateOut)
1204
+ mi_call(\+(Goal), StateIn, StateIn) :-
1205
+ \+ mi_call(Goal, StateIn, _).
1206
+
1207
+ %% mi_call(!, +StateIn, -StateOut)
1208
+ %% Cut DOES need to actually cut
1209
+ mi_call(!, StateIn, StateIn) :-
1210
+ !.
1211
+
1212
+ %% mi_call(true, +StateIn, -StateOut)
1213
+ mi_call(true, StateIn, StateIn).
1214
+
1215
+ %% mi_call(fail, +StateIn, -StateOut)
1216
+ mi_call(fail, _StateIn, _StateOut) :-
1217
+ fail.
1218
+
1219
+ %% mi_call(false, +StateIn, -StateOut)
1220
+ mi_call(false, _StateIn, _StateOut) :-
1221
+ fail.
1222
+
1223
+ %% ============================================================
1224
+ %% List Predicates - Interpreted for proper state threading
1225
+ %% ============================================================
1226
+
1227
+ %% mi_call(member(X, List), +StateIn, -StateOut)
1228
+ %% List membership - allows backtracking through list elements
1229
+ mi_call(member(X, List), StateIn, StateIn) :-
1230
+ member(X, List).
1231
+
1232
+ %% mi_call(append(A, B, C), +StateIn, -StateOut)
1233
+ mi_call(append(A, B, C), StateIn, StateIn) :-
1234
+ append(A, B, C).
1235
+
1236
+ %% mi_call(length(List, Len), +StateIn, -StateOut)
1237
+ mi_call(length(List, Len), StateIn, StateIn) :-
1238
+ length(List, Len).
1239
+
1240
+ %% mi_call(nth0(Index, List, Elem), +StateIn, -StateOut)
1241
+ mi_call(nth0(Index, List, Elem), StateIn, StateIn) :-
1242
+ nth0(Index, List, Elem).
1243
+
1244
+ %% mi_call(nth1(Index, List, Elem), +StateIn, -StateOut)
1245
+ mi_call(nth1(Index, List, Elem), StateIn, StateIn) :-
1246
+ nth1(Index, List, Elem).
1247
+
1248
+ %% mi_call(last(List, Elem), +StateIn, -StateOut)
1249
+ mi_call(last(List, Elem), StateIn, StateIn) :-
1250
+ last(List, Elem).
1251
+
1252
+ %% mi_call(reverse(List, Reversed), +StateIn, -StateOut)
1253
+ mi_call(reverse(List, Reversed), StateIn, StateIn) :-
1254
+ reverse(List, Reversed).
1255
+
1256
+ %% mi_call(sort(List, Sorted), +StateIn, -StateOut)
1257
+ mi_call(sort(List, Sorted), StateIn, StateIn) :-
1258
+ sort(List, Sorted).
1259
+
1260
+ %% mi_call(msort(List, Sorted), +StateIn, -StateOut)
1261
+ mi_call(msort(List, Sorted), StateIn, StateIn) :-
1262
+ msort(List, Sorted).
1263
+
1264
+ %% ============================================================
1265
+ %% Meta-Predicates - Interpreted with proper state threading
1266
+ %% ============================================================
1267
+
1268
+ %% mi_call(include(Goal, List, Included), +StateIn, -StateOut)
1269
+ %% Filter list keeping elements where Goal succeeds
1270
+ mi_call(include(Goal, List, Included), StateIn, StateOut) :-
1271
+ mi_include(Goal, List, Included, StateIn, StateOut).
1272
+
1273
+ mi_include(_, [], [], State, State).
1274
+ mi_include(Goal, [H|T], Result, StateIn, StateOut) :-
1275
+ copy_term(Goal, GoalCopy),
1276
+ GoalCopy =.. GoalList,
1277
+ append(GoalList, [H], GoalWithArg),
1278
+ TestGoal =.. GoalWithArg,
1279
+ ( mi_call(TestGoal, StateIn, State1)
1280
+ -> Result = [H|Rest],
1281
+ mi_include(Goal, T, Rest, State1, StateOut)
1282
+ ; mi_include(Goal, T, Result, StateIn, StateOut)
1283
+ ).
1284
+
1285
+ %% mi_call(exclude(Goal, List, Excluded), +StateIn, -StateOut)
1286
+ %% Filter list removing elements where Goal succeeds
1287
+ mi_call(exclude(Goal, List, Excluded), StateIn, StateOut) :-
1288
+ mi_exclude(Goal, List, Excluded, StateIn, StateOut).
1289
+
1290
+ mi_exclude(_, [], [], State, State).
1291
+ mi_exclude(Goal, [H|T], Result, StateIn, StateOut) :-
1292
+ copy_term(Goal, GoalCopy),
1293
+ GoalCopy =.. GoalList,
1294
+ append(GoalList, [H], GoalWithArg),
1295
+ TestGoal =.. GoalWithArg,
1296
+ ( mi_call(TestGoal, StateIn, State1)
1297
+ -> mi_exclude(Goal, T, Result, State1, StateOut)
1298
+ ; Result = [H|Rest],
1299
+ mi_exclude(Goal, T, Rest, StateIn, StateOut)
1300
+ ).
1301
+
1302
+ %% mi_call(partition(Goal, List, Included, Excluded), +StateIn, -StateOut)
1303
+ %% Partition list into elements where Goal succeeds and fails
1304
+ mi_call(partition(Goal, List, Included, Excluded), StateIn, StateOut) :-
1305
+ mi_partition(Goal, List, Included, Excluded, StateIn, StateOut).
1306
+
1307
+ mi_partition(_, [], [], [], State, State).
1308
+ mi_partition(Goal, [H|T], Inc, Exc, StateIn, StateOut) :-
1309
+ copy_term(Goal, GoalCopy),
1310
+ GoalCopy =.. GoalList,
1311
+ append(GoalList, [H], GoalWithArg),
1312
+ TestGoal =.. GoalWithArg,
1313
+ ( mi_call(TestGoal, StateIn, State1)
1314
+ -> Inc = [H|IncRest],
1315
+ mi_partition(Goal, T, IncRest, Exc, State1, StateOut)
1316
+ ; Exc = [H|ExcRest],
1317
+ mi_partition(Goal, T, Inc, ExcRest, StateIn, StateOut)
1318
+ ).
1319
+
1320
+ %% mi_call(forall(Cond, Action), +StateIn, -StateOut)
1321
+ %% Succeed if for all solutions of Cond, Action succeeds
1322
+ %% Note: State changes in Action are NOT preserved (bagof semantics)
1323
+ mi_call(forall(Cond, Action), StateIn, StateIn) :-
1324
+ \+ (mi_call(Cond, StateIn, State1), \+ mi_call(Action, State1, _)).
1325
+
1326
+ %% mi_call(findall(Template, Goal, List), +StateIn, -StateOut)
1327
+ %% Collect all solutions - state changes are NOT preserved
1328
+ mi_call(findall(Template, Goal, List), StateIn, StateIn) :-
1329
+ findall(Template, mi_call(Goal, StateIn, _), List).
1330
+
1331
+ %% mi_call(bagof(Template, Goal, List), +StateIn, -StateOut)
1332
+ mi_call(bagof(Template, Goal, List), StateIn, StateIn) :-
1333
+ bagof(Template, mi_call(Goal, StateIn, _), List).
1334
+
1335
+ %% mi_call(setof(Template, Goal, List), +StateIn, -StateOut)
1336
+ mi_call(setof(Template, Goal, List), StateIn, StateIn) :-
1337
+ setof(Template, mi_call(Goal, StateIn, _), List).
1338
+
1339
+ %% mi_call(aggregate_all(Template, Goal, Result), +StateIn, -StateOut)
1340
+ mi_call(aggregate_all(Template, Goal, Result), StateIn, StateIn) :-
1341
+ aggregate_all(Template, mi_call(Goal, StateIn, _), Result).
1342
+
1343
+ %% mi_call(maplist(Goal, List), +StateIn, -StateOut)
1344
+ %% Apply Goal to each element (Goal/1)
1345
+ mi_call(maplist(Goal, List), StateIn, StateOut) :-
1346
+ mi_maplist1(Goal, List, StateIn, StateOut).
1347
+
1348
+ mi_maplist1(_, [], State, State).
1349
+ mi_maplist1(Goal, [H|T], StateIn, StateOut) :-
1350
+ copy_term(Goal, GoalCopy),
1351
+ GoalCopy =.. GoalList,
1352
+ append(GoalList, [H], GoalWithArg),
1353
+ CallGoal =.. GoalWithArg,
1354
+ mi_call(CallGoal, StateIn, State1),
1355
+ mi_maplist1(Goal, T, State1, StateOut).
1356
+
1357
+ %% mi_call(maplist(Goal, List1, List2), +StateIn, -StateOut)
1358
+ %% Apply Goal to pairs of elements (Goal/2)
1359
+ mi_call(maplist(Goal, List1, List2), StateIn, StateOut) :-
1360
+ mi_maplist2(Goal, List1, List2, StateIn, StateOut).
1361
+
1362
+ mi_maplist2(_, [], [], State, State).
1363
+ mi_maplist2(Goal, [H1|T1], [H2|T2], StateIn, StateOut) :-
1364
+ copy_term(Goal, GoalCopy),
1365
+ GoalCopy =.. GoalList,
1366
+ append(GoalList, [H1, H2], GoalWithArgs),
1367
+ CallGoal =.. GoalWithArgs,
1368
+ mi_call(CallGoal, StateIn, State1),
1369
+ mi_maplist2(Goal, T1, T2, State1, StateOut).
1370
+
1371
+ %% mi_call(maplist(Goal, L1, L2, L3), +StateIn, -StateOut)
1372
+ %% Apply Goal to triples of elements (Goal/3)
1373
+ mi_call(maplist(Goal, L1, L2, L3), StateIn, StateOut) :-
1374
+ mi_maplist3(Goal, L1, L2, L3, StateIn, StateOut).
1375
+
1376
+ mi_maplist3(_, [], [], [], State, State).
1377
+ mi_maplist3(Goal, [H1|T1], [H2|T2], [H3|T3], StateIn, StateOut) :-
1378
+ copy_term(Goal, GoalCopy),
1379
+ GoalCopy =.. GoalList,
1380
+ append(GoalList, [H1, H2, H3], GoalWithArgs),
1381
+ CallGoal =.. GoalWithArgs,
1382
+ mi_call(CallGoal, StateIn, State1),
1383
+ mi_maplist3(Goal, T1, T2, T3, State1, StateOut).
1384
+
1385
+ %% mi_call(foldl(Goal, List, V0, V), +StateIn, -StateOut)
1386
+ %% Left fold over list
1387
+ mi_call(foldl(Goal, List, V0, V), StateIn, StateOut) :-
1388
+ mi_foldl(Goal, List, V0, V, StateIn, StateOut).
1389
+
1390
+ mi_foldl(_, [], V, V, State, State).
1391
+ mi_foldl(Goal, [H|T], V0, V, StateIn, StateOut) :-
1392
+ copy_term(Goal, GoalCopy),
1393
+ GoalCopy =.. GoalList,
1394
+ append(GoalList, [H, V0, V1], GoalWithArgs),
1395
+ CallGoal =.. GoalWithArgs,
1396
+ mi_call(CallGoal, StateIn, State1),
1397
+ mi_foldl(Goal, T, V1, V, State1, StateOut).
1398
+
1399
+ %% ============================================================
1400
+ %% File I/O - All paths relative to /workspace
1401
+ %% ============================================================
1402
+
1403
+ %% Helper: resolve path relative to /workspace
1404
+ %% Prevents directory traversal outside workspace
1405
+ resolve_workspace_path(RelPath, FullPath) :-
1406
+ ( atom(RelPath) -> atom_string(RelPath, RelPathStr)
1407
+ ; RelPathStr = RelPath
1408
+ ),
1409
+ % Remove leading slashes to make relative
1410
+ ( sub_string(RelPathStr, 0, 1, _, "/")
1411
+ -> sub_string(RelPathStr, 1, _, 0, CleanPath)
1412
+ ; CleanPath = RelPathStr
1413
+ ),
1414
+ % Prevent directory traversal
1415
+ ( sub_string(CleanPath, _, _, _, "..")
1416
+ -> throw(error(permission_error(access, directory, RelPath),
1417
+ context(resolve_workspace_path/2, 'Directory traversal not allowed')))
1418
+ ; true
1419
+ ),
1420
+ % Build full path
1421
+ atom_concat('/workspace/', CleanPath, FullPath).
1422
+
1423
+ %% mi_call(read_file_to_string(File, Content, Options), +StateIn, -StateOut)
1424
+ mi_call(read_file_to_string(File, Content, Options), StateIn, StateIn) :-
1425
+ !,
1426
+ resolve_workspace_path(File, FullPath),
1427
+ readutil:read_file_to_string(FullPath, Content, Options).
1428
+
1429
+ %% mi_call(read_file(File, Content), +StateIn, -StateOut)
1430
+ %% Simplified version without options
1431
+ mi_call(read_file(File, Content), StateIn, StateIn) :-
1432
+ !,
1433
+ resolve_workspace_path(File, FullPath),
1434
+ readutil:read_file_to_string(FullPath, Content, []).
1435
+
1436
+ %% mi_call(write_file(File, Content), +StateIn, -StateOut)
1437
+ %% Write string content to a file
1438
+ mi_call(write_file(File, Content), StateIn, StateIn) :-
1439
+ !,
1440
+ resolve_workspace_path(File, FullPath),
1441
+ open(FullPath, write, Stream),
1442
+ write(Stream, Content),
1443
+ close(Stream).
1444
+
1445
+ %% mi_call(append_file(File, Content), +StateIn, -StateOut)
1446
+ %% Append string content to a file
1447
+ mi_call(append_file(File, Content), StateIn, StateIn) :-
1448
+ !,
1449
+ resolve_workspace_path(File, FullPath),
1450
+ open(FullPath, append, Stream),
1451
+ write(Stream, Content),
1452
+ close(Stream).
1453
+
1454
+ %% mi_call(open(File, Mode, Stream), +StateIn, -StateOut)
1455
+ %% Open a file for reading/writing
1456
+ mi_call(open(File, Mode, Stream), StateIn, StateIn) :-
1457
+ !,
1458
+ resolve_workspace_path(File, FullPath),
1459
+ open(FullPath, Mode, Stream).
1460
+
1461
+ %% mi_call(close(Stream), +StateIn, -StateOut)
1462
+ mi_call(close(Stream), StateIn, StateIn) :-
1463
+ !,
1464
+ close(Stream).
1465
+
1466
+ %% mi_call(read_string(Stream, Length, Content), +StateIn, -StateOut)
1467
+ mi_call(read_string(Stream, Length, Content), StateIn, StateIn) :-
1468
+ !,
1469
+ read_string(Stream, Length, Content).
1470
+
1471
+ %% mi_call(write(Stream, Content), +StateIn, -StateOut)
1472
+ mi_call(write(Stream, Content), StateIn, StateIn) :-
1473
+ !,
1474
+ write(Stream, Content).
1475
+
1476
+ %% mi_call(exists_file(File), +StateIn, -StateOut)
1477
+ mi_call(exists_file(File), StateIn, StateIn) :-
1478
+ !,
1479
+ resolve_workspace_path(File, FullPath),
1480
+ exists_file(FullPath).
1481
+
1482
+ %% mi_call(exists_directory(Dir), +StateIn, -StateOut)
1483
+ mi_call(exists_directory(Dir), StateIn, StateIn) :-
1484
+ !,
1485
+ resolve_workspace_path(Dir, FullPath),
1486
+ exists_directory(FullPath).
1487
+
1488
+ %% mi_call(directory_files(Dir, Files), +StateIn, -StateOut)
1489
+ mi_call(directory_files(Dir, Files), StateIn, StateIn) :-
1490
+ !,
1491
+ resolve_workspace_path(Dir, FullPath),
1492
+ directory_files(FullPath, Files).
1493
+
1494
+ %% mi_call(make_directory(Dir), +StateIn, -StateOut)
1495
+ mi_call(make_directory(Dir), StateIn, StateIn) :-
1496
+ !,
1497
+ resolve_workspace_path(Dir, FullPath),
1498
+ make_directory(FullPath).
1499
+
1500
+ %% mi_call(delete_file(File), +StateIn, -StateOut)
1501
+ mi_call(delete_file(File), StateIn, StateIn) :-
1502
+ !,
1503
+ resolve_workspace_path(File, FullPath),
1504
+ delete_file(FullPath).
1505
+
1506
+ %% mi_call(delete_directory(Dir), +StateIn, -StateOut)
1507
+ mi_call(delete_directory(Dir), StateIn, StateIn) :-
1508
+ !,
1509
+ resolve_workspace_path(Dir, FullPath),
1510
+ delete_directory(FullPath).
1511
+
1512
+ %% ============================================================
1513
+ %% Catch/Throw - STATE THREADED
1514
+ %% ============================================================
1515
+
1516
+ %% mi_call(catch(Goal, Catcher, Recovery), +StateIn, -StateOut)
1517
+ mi_call(catch(Goal, Catcher, Recovery), StateIn, StateOut) :-
1518
+ catch(
1519
+ mi_call(Goal, StateIn, StateOut),
1520
+ Catcher,
1521
+ mi_call(Recovery, StateIn, StateOut)
1522
+ ).
1523
+
1524
+ %% mi_call(throw(Error), +StateIn, -StateOut)
1525
+ mi_call(throw(Error), _StateIn, _StateOut) :-
1526
+ !,
1527
+ throw(Error).
1528
+
1529
+ %% ============================================================
1530
+ %% Module-Qualified Goals
1531
+ %% ============================================================
1532
+
1533
+ %% mi_call(Module:Goal, +StateIn, -StateOut)
1534
+ %% Module-qualified goals - handle special predicates and user-defined predicates
1535
+
1536
+ % First check if the unqualified Goal is a special MI predicate - if so, handle it directly
1537
+ mi_call(_Module:Goal, StateIn, StateOut) :-
1538
+ is_mi_special_predicate(Goal),
1539
+ !,
1540
+ mi_call(Goal, StateIn, StateOut).
1541
+
1542
+ % For regular module-qualified goals, use clause/2 for backtracking
1543
+ mi_call(Module:Goal, StateIn, StateOut) :-
1544
+ predicate_property(Module:Goal, defined),
1545
+ !, % CUT: If predicate is defined, don't fallback to call/1
1546
+ % Now try clause/2 for rules, allowing backtracking through clauses
1547
+ clause(Module:Goal, Body),
1548
+ ( Body == true
1549
+ -> true % Fact - succeed with no body to interpret
1550
+ ; mi_call(Body, StateIn, StateOut)
1551
+ ).
1552
+
1553
+ mi_call(Module:Goal, StateIn, StateIn) :-
1554
+ % Fallback for built-ins only (predicate is NOT defined in module)
1555
+ call(Module:Goal).
1556
+
1557
+ %% ============================================================
1558
+ %% Built-in and User-Defined Predicates
1559
+ %% ============================================================
1560
+
1561
+ %% is_mi_special_predicate(+Goal)
1562
+ %% Check if Goal is a special predicate handled by dedicated mi_call clauses
1563
+ is_mi_special_predicate(answer(_)).
1564
+ is_mi_special_predicate(output(_)).
1565
+ is_mi_special_predicate(yield(_)).
1566
+ is_mi_special_predicate(log(_)).
1567
+ is_mi_special_predicate(system(_)).
1568
+ is_mi_special_predicate(user(_)). %% Add user message to context
1569
+ is_mi_special_predicate(task(_)).
1570
+ is_mi_special_predicate(task(_,_)).
1571
+ is_mi_special_predicate(task(_,_,_)).
1572
+ is_mi_special_predicate(task(_,_,_,_)).
1573
+ is_mi_special_predicate(task(_,_,_,_,_)).
1574
+ is_mi_special_predicate(task_named(_,_,_)). %% Transformed form of task/N
1575
+ is_mi_special_predicate(exec(_,_)).
1576
+ is_mi_special_predicate(param(_,_)).
1577
+ is_mi_special_predicate(param(_,_,_)).
1578
+ %% Prompt predicates (like task but with fresh memory)
1579
+ is_mi_special_predicate(prompt(_)).
1580
+ is_mi_special_predicate(prompt(_,_)).
1581
+ is_mi_special_predicate(prompt(_,_,_)).
1582
+ is_mi_special_predicate(prompt(_,_,_,_)).
1583
+ is_mi_special_predicate(prompt_named(_,_,_)). %% Transformed form of prompt/N
1584
+ %% Tool scoping predicates
1585
+ is_mi_special_predicate(with_tools(_,_)).
1586
+ is_mi_special_predicate(without_tools(_,_)).
1587
+ %% Context stack management
1588
+ is_mi_special_predicate(push_context).
1589
+ is_mi_special_predicate(push_context(_)).
1590
+ is_mi_special_predicate(pop_context).
1591
+ is_mi_special_predicate(clear_memory).
1592
+ %% Control flow
1593
+ is_mi_special_predicate((_,_)).
1594
+ is_mi_special_predicate((_;_)).
1595
+ is_mi_special_predicate((_->_)).
1596
+ is_mi_special_predicate((_->_;_)).
1597
+ is_mi_special_predicate(\+(_)).
1598
+ is_mi_special_predicate(catch(_,_,_)).
1599
+ is_mi_special_predicate(throw(_)).
1600
+ is_mi_special_predicate(!).
1601
+ is_mi_special_predicate(true).
1602
+ is_mi_special_predicate(fail).
1603
+ is_mi_special_predicate(false).
1604
+ is_mi_special_predicate(_:_).
1605
+ %% List predicates
1606
+ is_mi_special_predicate(member(_,_)).
1607
+ is_mi_special_predicate(append(_,_,_)).
1608
+ is_mi_special_predicate(length(_,_)).
1609
+ is_mi_special_predicate(nth0(_,_,_)).
1610
+ is_mi_special_predicate(nth1(_,_,_)).
1611
+ is_mi_special_predicate(last(_,_)).
1612
+ is_mi_special_predicate(reverse(_,_)).
1613
+ is_mi_special_predicate(sort(_,_)).
1614
+ is_mi_special_predicate(msort(_,_)).
1615
+ %% Meta-predicates
1616
+ is_mi_special_predicate(include(_,_,_)).
1617
+ is_mi_special_predicate(exclude(_,_,_)).
1618
+ is_mi_special_predicate(partition(_,_,_,_)).
1619
+ is_mi_special_predicate(forall(_,_)).
1620
+ is_mi_special_predicate(findall(_,_,_)).
1621
+ is_mi_special_predicate(bagof(_,_,_)).
1622
+ is_mi_special_predicate(setof(_,_,_)).
1623
+ is_mi_special_predicate(aggregate_all(_,_,_)).
1624
+ is_mi_special_predicate(maplist(_,_)).
1625
+ is_mi_special_predicate(maplist(_,_,_)).
1626
+ is_mi_special_predicate(maplist(_,_,_,_)).
1627
+ is_mi_special_predicate(foldl(_,_,_,_)).
1628
+ %% File I/O predicates
1629
+ is_mi_special_predicate(read_file_to_string(_,_,_)).
1630
+ is_mi_special_predicate(read_file(_,_)).
1631
+ is_mi_special_predicate(write_file(_,_)).
1632
+ is_mi_special_predicate(append_file(_,_)).
1633
+ is_mi_special_predicate(open(_,_,_)).
1634
+ is_mi_special_predicate(close(_)).
1635
+ is_mi_special_predicate(read_string(_,_,_)).
1636
+ is_mi_special_predicate(write(_,_)).
1637
+ is_mi_special_predicate(exists_file(_)).
1638
+ is_mi_special_predicate(exists_directory(_)).
1639
+ is_mi_special_predicate(directory_files(_,_)).
1640
+ is_mi_special_predicate(make_directory(_)).
1641
+ is_mi_special_predicate(delete_file(_)).
1642
+ is_mi_special_predicate(delete_directory(_)).
1643
+
1644
+ %% mi_call(Goal, +StateIn, -StateOut)
1645
+ %% Catch-all for built-in and user-defined predicates
1646
+ %% MUST NOT match special predicates that have dedicated handlers
1647
+ mi_call(Goal, StateIn, StateOut) :-
1648
+ ( var(Goal)
1649
+ -> throw(error(instantiation_error, mi_call/3))
1650
+ ; true
1651
+ ),
1652
+ % Skip if this is a special predicate (has dedicated mi_call clause)
1653
+ \+ is_mi_special_predicate(Goal),
1654
+ mi_call_dispatch(Goal, StateIn, StateOut).
1655
+
1656
+ %% mi_call_dispatch(+Goal, +StateIn, -StateOut)
1657
+ %% Dispatch user-defined vs built-in predicates
1658
+ %% Uses separate clauses instead of if-then-else to preserve backtracking
1659
+ mi_call_dispatch(Goal, StateIn, StateIn) :-
1660
+ predicate_property(Goal, built_in),
1661
+ !, % Commit: it's built-in, no backtracking needed
1662
+ call(Goal).
1663
+
1664
+ mi_call_dispatch(Goal, StateIn, StateOut) :-
1665
+ get_session_id(SessionId),
1666
+ callable(Goal),
1667
+ predicate_property(SessionId:Goal, defined),
1668
+ % User-defined predicate - use clause/2 for backtracking
1669
+ % Add trace entry for call
1670
+ get_depth(StateIn, Depth),
1671
+ Goal =.. [Functor|Args],
1672
+ add_trace_entry(SessionId, call, Functor, Args, Depth),
1673
+ % Find a matching clause - allows backtracking to try multiple clauses
1674
+ clause(SessionId:Goal, Body),
1675
+ NewDepth is Depth + 1,
1676
+ set_depth(StateIn, NewDepth, State1),
1677
+ ( mi_call(Body, State1, State2)
1678
+ -> add_trace_entry(SessionId, exit, Functor, Args, Depth),
1679
+ set_depth(State2, Depth, StateOut)
1680
+ ; add_trace_entry(SessionId, fail, Functor, Args, Depth),
1681
+ fail
1682
+ ).
1683
+
1684
+ mi_call_dispatch(Goal, StateIn, StateIn) :-
1685
+ % Fallback for external/library predicates
1686
+ get_session_id(SessionId),
1687
+ % Check if this predicate exists in the session module but not as user-defined
1688
+ \+ predicate_property(SessionId:Goal, defined),
1689
+ catch(call(SessionId:Goal), _, fail),
1690
+ !.
1691
+
1692
+ mi_call_dispatch(Goal, StateIn, StateIn) :-
1693
+ % Final fallback: call without module
1694
+ % Only for predicates not defined in session
1695
+ get_session_id(SessionId),
1696
+ \+ predicate_property(SessionId:Goal, defined),
1697
+ call(Goal).
1698
+
1699
+ %% ============================================================
1700
+ %% Helper Predicates
1701
+ %% ============================================================
1702
+
1703
+ %% interpolate_desc(+Template, +Params, -Result)
1704
+ interpolate_desc(Template, Params, Result) :-
1705
+ ( is_dict(Params)
1706
+ -> dict_pairs(Params, _, Pairs),
1707
+ maplist([K-V, K=V]>>true, Pairs, Bindings),
1708
+ deepclause_strings:interpolate_string(Template, Bindings, Result)
1709
+ ; Result = Template
1710
+ ).
1711
+
1712
+ %% get_session_id(-SessionId)
1713
+ get_session_id(SessionId) :-
1714
+ nb_current(current_session_id, SessionId), !.
1715
+ get_session_id(default_session).
1716
+
1717
+ %% ============================================================
1718
+ %% Compile-Time String Interpolation Expansion
1719
+ %% ============================================================
1720
+
1721
+ %% expand_interpolations(+Term, +Bindings, -ExpandedTerm)
1722
+ %% Walk a term and expand strings containing {VarName} patterns
1723
+ %% Bindings is a list of 'VarName'=Variable pairs from read_term
1724
+ expand_interpolations(Term, Bindings, ExpandedTerm) :-
1725
+ expand_term_interp(Term, Bindings, ExpandedTerm).
1726
+
1727
+ %% expand_term_interp(+Term, +Bindings, -Expanded)
1728
+ %% Main term expansion predicate
1729
+
1730
+ % Variables pass through unchanged
1731
+ expand_term_interp(Var, _, Var) :-
1732
+ var(Var), !.
1733
+
1734
+ % Strings - check if they need interpolation
1735
+ expand_term_interp(String, Bindings, Expanded) :-
1736
+ string(String), !,
1737
+ ( string_needs_interpolation(String)
1738
+ -> build_format_goal(String, Bindings, Expanded)
1739
+ ; Expanded = String
1740
+ ).
1741
+
1742
+ % Atoms - check if they need interpolation (for atom strings)
1743
+ expand_term_interp(Atom, Bindings, Expanded) :-
1744
+ atom(Atom),
1745
+ \+ Atom = [], % Not empty list
1746
+ atom_string(Atom, String),
1747
+ string_needs_interpolation(String), !,
1748
+ build_format_goal(String, Bindings, ExpandedGoal),
1749
+ % Wrap in atom conversion if original was atom
1750
+ Expanded = ExpandedGoal.
1751
+
1752
+ % Regular atoms pass through
1753
+ expand_term_interp(Atom, _, Atom) :-
1754
+ atom(Atom), !.
1755
+
1756
+ % Numbers pass through
1757
+ expand_term_interp(Num, _, Num) :-
1758
+ number(Num), !.
1759
+
1760
+ % Empty list
1761
+ expand_term_interp([], _, []) :- !.
1762
+
1763
+ % Lists - expand each element
1764
+ expand_term_interp([H|T], Bindings, [EH|ET]) :- !,
1765
+ expand_term_interp(H, Bindings, EH),
1766
+ expand_term_interp(T, Bindings, ET).
1767
+
1768
+ % Clause with body - special handling for string interpolation in goals
1769
+ expand_term_interp((Head :- Body), Bindings, (Head :- ExpandedBody)) :- !,
1770
+ expand_body_interp(Body, Bindings, ExpandedBody).
1771
+
1772
+ % Other compound terms - expand arguments
1773
+ expand_term_interp(Term, Bindings, ExpandedTerm) :-
1774
+ compound(Term), !,
1775
+ Term =.. [Functor|Args],
1776
+ maplist({Bindings}/[A, EA]>>expand_term_interp(A, Bindings, EA), Args, ExpandedArgs),
1777
+ ExpandedTerm =.. [Functor|ExpandedArgs].
1778
+
1779
+ %% expand_body_interp(+Body, +Bindings, -ExpandedBody)
1780
+ %% Expand goals in a clause body, handling control structures
1781
+
1782
+ % Variable goal
1783
+ expand_body_interp(Var, _, Var) :-
1784
+ var(Var), !.
1785
+
1786
+ % Conjunction
1787
+ expand_body_interp((A, B), Bindings, ExpandedConj) :- !,
1788
+ expand_body_interp(A, Bindings, EA),
1789
+ expand_body_interp(B, Bindings, EB),
1790
+ flatten_conjunction(EA, EB, ExpandedConj).
1791
+
1792
+ % Disjunction
1793
+ expand_body_interp((A ; B), Bindings, (EA ; EB)) :- !,
1794
+ expand_body_interp(A, Bindings, EA),
1795
+ expand_body_interp(B, Bindings, EB).
1796
+
1797
+ % If-then-else
1798
+ expand_body_interp((A -> B ; C), Bindings, (EA -> EB ; EC)) :- !,
1799
+ expand_body_interp(A, Bindings, EA),
1800
+ expand_body_interp(B, Bindings, EB),
1801
+ expand_body_interp(C, Bindings, EC).
1802
+
1803
+ % If-then
1804
+ expand_body_interp((A -> B), Bindings, (EA -> EB)) :- !,
1805
+ expand_body_interp(A, Bindings, EA),
1806
+ expand_body_interp(B, Bindings, EB).
1807
+
1808
+ % Soft cut
1809
+ expand_body_interp((A *-> B), Bindings, (EA *-> EB)) :- !,
1810
+ expand_body_interp(A, Bindings, EA),
1811
+ expand_body_interp(B, Bindings, EB).
1812
+
1813
+ % Negation
1814
+ expand_body_interp(\+(A), Bindings, \+(EA)) :- !,
1815
+ expand_body_interp(A, Bindings, EA).
1816
+
1817
+ % Goals that take string arguments - expand the string arg specially
1818
+ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
1819
+ goal_with_string_arg(Goal, Functor, StringArg, RestArgs), !,
1820
+ ( string_needs_interpolation(StringArg)
1821
+ -> build_format_call(StringArg, Bindings, TempVar, FormatGoal),
1822
+ rebuild_goal(Functor, TempVar, RestArgs, NewGoal),
1823
+ ExpandedGoal = (FormatGoal, NewGoal)
1824
+ ; expand_rest_args(RestArgs, Bindings, ExpandedRestArgs),
1825
+ rebuild_goal(Functor, StringArg, ExpandedRestArgs, ExpandedGoal)
1826
+ ).
1827
+
1828
+ % Other goals - expand arguments
1829
+ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
1830
+ compound(Goal), !,
1831
+ Goal =.. [Functor|Args],
1832
+ maplist({Bindings}/[A, EA]>>expand_body_interp(A, Bindings, EA), Args, ExpandedArgs),
1833
+ ExpandedGoal =.. [Functor|ExpandedArgs].
1834
+
1835
+ % Atoms/other
1836
+ expand_body_interp(Goal, _, Goal).
1837
+
1838
+ %% goal_with_string_arg(+Goal, -Functor, -StringArg, -RestArgs)
1839
+ %% Match goals that take a string as their first argument
1840
+ goal_with_string_arg(answer(S), answer, S, []) :- string(S).
1841
+ goal_with_string_arg(answer(S), answer, S, []) :- atom(S), \+ S = [].
1842
+ goal_with_string_arg(system(S), system, S, []) :- string(S).
1843
+ goal_with_string_arg(system(S), system, S, []) :- atom(S), \+ S = [].
1844
+ goal_with_string_arg(user(S), user, S, []) :- string(S).
1845
+ goal_with_string_arg(user(S), user, S, []) :- atom(S), \+ S = [].
1846
+ goal_with_string_arg(output(S), output, S, []) :- string(S).
1847
+ goal_with_string_arg(output(S), output, S, []) :- atom(S), \+ S = [].
1848
+ goal_with_string_arg(log(S), log, S, []) :- string(S).
1849
+ goal_with_string_arg(log(S), log, S, []) :- atom(S), \+ S = [].
1850
+ goal_with_string_arg(yield(S), yield, S, []) :- string(S).
1851
+ goal_with_string_arg(yield(S), yield, S, []) :- atom(S), \+ S = [].
1852
+ goal_with_string_arg(task(S), task, S, []) :- string(S).
1853
+ goal_with_string_arg(task(S), task, S, []) :- atom(S), \+ S = [].
1854
+ goal_with_string_arg(task(S, V1), task, S, [V1]) :- string(S).
1855
+ goal_with_string_arg(task(S, V1), task, S, [V1]) :- atom(S), \+ S = [].
1856
+ goal_with_string_arg(task(S, V1, V2), task, S, [V1, V2]) :- string(S).
1857
+ goal_with_string_arg(task(S, V1, V2), task, S, [V1, V2]) :- atom(S), \+ S = [].
1858
+ goal_with_string_arg(task(S, V1, V2, V3), task, S, [V1, V2, V3]) :- string(S).
1859
+ goal_with_string_arg(task(S, V1, V2, V3), task, S, [V1, V2, V3]) :- atom(S), \+ S = [].
1860
+ %% task_named/3 - transformed form with embedded variable names
1861
+ goal_with_string_arg(task_named(S, Vars, Names), task_named, S, [Vars, Names]) :- string(S).
1862
+ goal_with_string_arg(task_named(S, Vars, Names), task_named, S, [Vars, Names]) :- atom(S), \+ S = [].
1863
+ goal_with_string_arg(prompt(S), prompt, S, []) :- string(S).
1864
+ goal_with_string_arg(prompt(S), prompt, S, []) :- atom(S), \+ S = [].
1865
+ goal_with_string_arg(prompt(S, V1), prompt, S, [V1]) :- string(S).
1866
+ goal_with_string_arg(prompt(S, V1), prompt, S, [V1]) :- atom(S), \+ S = [].
1867
+ goal_with_string_arg(prompt(S, V1, V2), prompt, S, [V1, V2]) :- string(S).
1868
+ goal_with_string_arg(prompt(S, V1, V2), prompt, S, [V1, V2]) :- atom(S), \+ S = [].
1869
+ goal_with_string_arg(prompt(S, V1, V2, V3), prompt, S, [V1, V2, V3]) :- string(S).
1870
+ goal_with_string_arg(prompt(S, V1, V2, V3), prompt, S, [V1, V2, V3]) :- atom(S), \+ S = [].
1871
+ %% prompt_named/3 - transformed form with embedded variable names
1872
+ goal_with_string_arg(prompt_named(S, Vars, Names), prompt_named, S, [Vars, Names]) :- string(S).
1873
+ goal_with_string_arg(prompt_named(S, Vars, Names), prompt_named, S, [Vars, Names]) :- atom(S), \+ S = [].
1874
+
1875
+ %% rebuild_goal(+Functor, +StringArg, +RestArgs, -Goal)
1876
+ %% Rebuild a goal with the string argument and rest args
1877
+ rebuild_goal(Functor, StringArg, [], Goal) :- !,
1878
+ Goal =.. [Functor, StringArg].
1879
+ rebuild_goal(Functor, StringArg, RestArgs, Goal) :-
1880
+ Goal =.. [Functor, StringArg | RestArgs].
1881
+
1882
+ %% expand_rest_args(+Args, +Bindings, -ExpandedArgs)
1883
+ expand_rest_args([], _, []).
1884
+ expand_rest_args([A|As], Bindings, [EA|EAs]) :-
1885
+ expand_body_interp(A, Bindings, EA),
1886
+ expand_rest_args(As, Bindings, EAs).
1887
+
1888
+ %% flatten_conjunction(+A, +B, -Conj)
1889
+ %% Flatten nested conjunctions properly
1890
+ flatten_conjunction((A1, A2), B, Result) :- !,
1891
+ flatten_conjunction(A2, B, Rest),
1892
+ Result = (A1, Rest).
1893
+ flatten_conjunction(A, B, (A, B)).
1894
+
1895
+ %% string_needs_interpolation(+String)
1896
+ %% Check if a string contains {VarName} patterns
1897
+ string_needs_interpolation(String) :-
1898
+ ( string(String) -> S = String
1899
+ ; atom(String) -> atom_string(String, S)
1900
+ ; fail
1901
+ ),
1902
+ sub_string(S, _, _, _, "{"),
1903
+ sub_string(S, _, _, _, "}").
1904
+
1905
+ %% build_format_call(+Template, +Bindings, -TempVar, -FormatGoal)
1906
+ %% Build a format/3 call that produces the interpolated string
1907
+ %% For variables in scope: use the variable directly
1908
+ %% For params (not in scope): generate param(name, Var) lookup
1909
+ build_format_call(Template, Bindings, TempVar, FullGoal) :-
1910
+ ( string(Template) -> T = Template
1911
+ ; atom_string(Template, T)
1912
+ ),
1913
+ extract_interpolation_vars(T, VarNames, FormatString),
1914
+ lookup_vars_with_params(VarNames, Bindings, VarList, ParamGoals),
1915
+ FormatGoal = format(string(TempVar), FormatString, VarList),
1916
+ ( ParamGoals == []
1917
+ -> FullGoal = FormatGoal
1918
+ ; list_to_conjunction(ParamGoals, ParamConj),
1919
+ FullGoal = (ParamConj, FormatGoal)
1920
+ ).
1921
+
1922
+ %% list_to_conjunction(+List, -Conjunction)
1923
+ list_to_conjunction([G], G) :- !.
1924
+ list_to_conjunction([G|Gs], (G, Rest)) :-
1925
+ list_to_conjunction(Gs, Rest).
1926
+
1927
+ %% build_format_goal(+String, +Bindings, -Goal)
1928
+ %% For direct string expansion (not in a goal context)
1929
+ build_format_goal(String, Bindings, Goal) :-
1930
+ build_format_call(String, Bindings, TempVar, FormatGoal),
1931
+ Goal = (FormatGoal, TempVar).
1932
+
1933
+ %% extract_interpolation_vars(+Template, -VarNames, -FormatString)
1934
+ %% Parse template to extract variable names and build format string
1935
+ extract_interpolation_vars(Template, VarNames, FormatString) :-
1936
+ string_codes(Template, Codes),
1937
+ extract_vars_from_codes(Codes, VarNames, FormatCodes),
1938
+ string_codes(FormatString, FormatCodes).
1939
+
1940
+ %% extract_vars_from_codes(+Codes, -VarNames, -FormatCodes)
1941
+ extract_vars_from_codes([], [], []) :- !.
1942
+
1943
+ % Found opening brace
1944
+ extract_vars_from_codes([0'{|Rest], [VarName|VarNames], [0'~, 0'w|FormatRest]) :- !,
1945
+ extract_var_until_close(Rest, VarNameCodes, AfterClose),
1946
+ atom_codes(VarName, VarNameCodes),
1947
+ extract_vars_from_codes(AfterClose, VarNames, FormatRest).
1948
+
1949
+ % Escape tilde for format/3
1950
+ extract_vars_from_codes([0'~|Rest], VarNames, [0'~, 0'~|FormatRest]) :- !,
1951
+ extract_vars_from_codes(Rest, VarNames, FormatRest).
1952
+
1953
+ % Regular character
1954
+ extract_vars_from_codes([C|Rest], VarNames, [C|FormatRest]) :-
1955
+ extract_vars_from_codes(Rest, VarNames, FormatRest).
1956
+
1957
+ %% extract_var_until_close(+Codes, -VarNameCodes, -Rest)
1958
+ extract_var_until_close([0'}|Rest], [], Rest) :- !.
1959
+ extract_var_until_close([C|Rest], [C|VarRest], Final) :-
1960
+ extract_var_until_close(Rest, VarRest, Final).
1961
+ extract_var_until_close([], [], []).
1962
+
1963
+ %% lookup_vars_with_params(+VarNames, +Bindings, -VarList, -ParamGoals)
1964
+ %% Look up each variable name:
1965
+ %% - If in Bindings (local Prolog variable) → use directly
1966
+ %% - If not in Bindings → generate param(name, Var) lookup
1967
+ lookup_vars_with_params([], _, [], []).
1968
+ lookup_vars_with_params([Name|Names], Bindings, [Var|Vars], ParamGoals) :-
1969
+ ( member(Name=Var, Bindings)
1970
+ -> % Found as local variable
1971
+ ParamGoals = RestGoals
1972
+ ; % Not a local variable - treat as param lookup
1973
+ % Convert to lowercase atom for param key
1974
+ downcase_atom(Name, LowerName),
1975
+ ParamGoals = [param(LowerName, Var)|RestGoals]
1976
+ ),
1977
+ lookup_vars_with_params(Names, Bindings, Vars, RestGoals).
1978
+