deepclause-sdk 0.1.3 → 0.2.2

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 (92) hide show
  1. package/README.md +45 -6
  2. package/dist/cli/config.d.ts +2 -2
  3. package/dist/cli/repl.d.ts +36 -0
  4. package/dist/cli/repl.d.ts.map +1 -1
  5. package/dist/cli/repl.js +414 -14
  6. package/dist/cli/repl.js.map +1 -1
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +3 -0
  10. package/dist/index.js.map +1 -1
  11. package/dist/prolog-src/deepclause_mi.pl +547 -2
  12. package/dist/repl/backend.d.ts +9 -0
  13. package/dist/repl/backend.d.ts.map +1 -1
  14. package/dist/repl/backend.js +44 -0
  15. package/dist/repl/backend.js.map +1 -1
  16. package/dist/repl/harness-tools.d.ts +36 -0
  17. package/dist/repl/harness-tools.d.ts.map +1 -1
  18. package/dist/repl/harness-tools.js +173 -0
  19. package/dist/repl/harness-tools.js.map +1 -1
  20. package/dist/repl/history.d.ts +29 -0
  21. package/dist/repl/history.d.ts.map +1 -1
  22. package/dist/repl/history.js +71 -0
  23. package/dist/repl/history.js.map +1 -1
  24. package/dist/repl/index.d.ts +6 -0
  25. package/dist/repl/index.d.ts.map +1 -1
  26. package/dist/repl/index.js +9 -1
  27. package/dist/repl/index.js.map +1 -1
  28. package/dist/repl/picker.d.ts +3 -0
  29. package/dist/repl/picker.d.ts.map +1 -1
  30. package/dist/repl/picker.js +132 -0
  31. package/dist/repl/picker.js.map +1 -1
  32. package/dist/repl/process-backend.d.ts +31 -0
  33. package/dist/repl/process-backend.d.ts.map +1 -0
  34. package/dist/repl/process-backend.js +10 -0
  35. package/dist/repl/process-backend.js.map +1 -0
  36. package/dist/repl/process-executor.d.ts +67 -0
  37. package/dist/repl/process-executor.d.ts.map +1 -0
  38. package/dist/repl/process-executor.js +371 -0
  39. package/dist/repl/process-executor.js.map +1 -0
  40. package/dist/repl/process-worker.d.ts +10 -0
  41. package/dist/repl/process-worker.d.ts.map +1 -0
  42. package/dist/repl/process-worker.js +210 -0
  43. package/dist/repl/process-worker.js.map +1 -0
  44. package/dist/repl/processes.d.ts +282 -0
  45. package/dist/repl/processes.d.ts.map +1 -0
  46. package/dist/repl/processes.js +557 -0
  47. package/dist/repl/processes.js.map +1 -0
  48. package/dist/repl/render.d.ts.map +1 -1
  49. package/dist/repl/render.js +19 -5
  50. package/dist/repl/render.js.map +1 -1
  51. package/dist/repl/session.d.ts +142 -3
  52. package/dist/repl/session.d.ts.map +1 -1
  53. package/dist/repl/session.js +435 -4
  54. package/dist/repl/session.js.map +1 -1
  55. package/dist/repl/tui/autocomplete.d.ts.map +1 -1
  56. package/dist/repl/tui/autocomplete.js +7 -0
  57. package/dist/repl/tui/autocomplete.js.map +1 -1
  58. package/dist/repl/tui/harness-view.d.ts +102 -0
  59. package/dist/repl/tui/harness-view.d.ts.map +1 -0
  60. package/dist/repl/tui/harness-view.js +323 -0
  61. package/dist/repl/tui/harness-view.js.map +1 -0
  62. package/dist/repl/tui/renderer.d.ts.map +1 -1
  63. package/dist/repl/tui/renderer.js +6 -1
  64. package/dist/repl/tui/renderer.js.map +1 -1
  65. package/dist/repl/tui/services-panel.d.ts +35 -0
  66. package/dist/repl/tui/services-panel.d.ts.map +1 -0
  67. package/dist/repl/tui/services-panel.js +108 -0
  68. package/dist/repl/tui/services-panel.js.map +1 -0
  69. package/dist/repl/tui/status-bar.d.ts +2 -0
  70. package/dist/repl/tui/status-bar.d.ts.map +1 -1
  71. package/dist/repl/tui/status-bar.js +2 -0
  72. package/dist/repl/tui/status-bar.js.map +1 -1
  73. package/dist/repl/tui/tool-pane.d.ts.map +1 -1
  74. package/dist/repl/tui/tool-pane.js +6 -2
  75. package/dist/repl/tui/tool-pane.js.map +1 -1
  76. package/dist/repl/worker-host.d.ts +80 -0
  77. package/dist/repl/worker-host.d.ts.map +1 -0
  78. package/dist/repl/worker-host.js +305 -0
  79. package/dist/repl/worker-host.js.map +1 -0
  80. package/dist/repl/worker-protocol.d.ts +123 -0
  81. package/dist/repl/worker-protocol.d.ts.map +1 -0
  82. package/dist/repl/worker-protocol.js +9 -0
  83. package/dist/repl/worker-protocol.js.map +1 -0
  84. package/dist/runner.d.ts +24 -0
  85. package/dist/runner.d.ts.map +1 -1
  86. package/dist/runner.js +58 -0
  87. package/dist/runner.js.map +1 -1
  88. package/dist/system/assets/docs/AGENT_PROMPT.md +247 -0
  89. package/dist/system/assets/docs/DML_AUTHORING.md +21 -0
  90. package/dist/system/assets/docs/DML_REFERENCE.md +199 -0
  91. package/package.json +7 -6
  92. package/src/prolog-src/deepclause_mi.pl +547 -2
@@ -93,6 +93,7 @@
93
93
  repl_memory_term/3,
94
94
  repl_get_memory/3,
95
95
  repl_set_memory/3,
96
+ repl_note_resumed/2,
96
97
  post_agent_result/4,
97
98
  post_exec_result/3,
98
99
  post_sample_token_result/3,
@@ -114,6 +115,9 @@
114
115
  :- discontiguous mi_call/3.
115
116
  :- discontiguous transform_task_calls/3.
116
117
  :- discontiguous var_to_name/3.
118
+ %% `proc_request_payload/2` sits between the process-primitive clauses of
119
+ %% `process_engine_result/4`, which is intentional.
120
+ :- discontiguous process_engine_result/4.
117
121
 
118
122
  %% Dynamic predicates for session state (engine management only, not memory!)
119
123
  :- dynamic session_engine/2. % session_engine(SessionId, Engine) - legacy default engine handle
@@ -148,6 +152,7 @@
148
152
  :- dynamic session_trace_log/2. % session_trace_log(SessionId, TraceLog) - accumulated trace entries
149
153
  :- dynamic session_trace_enabled/2. % session_trace_enabled(SessionId, true/false)
150
154
  :- dynamic session_pending_signal/2. % session_pending_signal(SessionId, Signal) - fallback for engine_fetch
155
+ :- dynamic session_proc_result/3. % session_proc_result(SessionId, Key, Value) - process primitive results
151
156
  :- dynamic session_user_input/2. % session_user_input(SessionId, Input) - for ask_user tool
152
157
  :- dynamic session_input_cancelled/1. % session_input_cancelled(SessionId) - input/2 should fail (non-interactive)
153
158
 
@@ -957,7 +962,11 @@ step_engine_handle(SessionId, Engine, Status, Content, Payload) :-
957
962
  )
958
963
  ),
959
964
  Error,
960
- ( format(atom(ErrMsg), 'Engine error: ~w', [Error]),
965
+ ( Error = process_exit(_)
966
+ -> Status = finished,
967
+ Content = '',
968
+ Payload = none
969
+ ; format(atom(ErrMsg), 'Engine error: ~w', [Error]),
961
970
  Status = error,
962
971
  Content = ErrMsg,
963
972
  ( session_trace_enabled(SessionId, true),
@@ -1304,6 +1313,13 @@ repl_listing_output(SessionId, Filter) :-
1304
1313
  )
1305
1314
  ).
1306
1315
 
1316
+ %% A filter either names a predicate (`name/arity`), names a prefix, or is `all`.
1317
+ %% Anything else matches **nothing**.
1318
+ %%
1319
+ %% There used to be a final `repl_listing_match(_, _, _)` catch-all, which turned any
1320
+ %% filter it did not understand into "the whole module" — silently. `listing('alpha/1')`
1321
+ %% therefore listed every predicate, so the side view's drill-down showed the module
1322
+ %% instead of the predicate under the cursor.
1307
1323
  repl_listing_match(all, _, _) :- !.
1308
1324
  repl_listing_match(Name/Arity, Name, Arity) :- !.
1309
1325
  repl_listing_match(Filter, Name, _) :-
@@ -1312,7 +1328,8 @@ repl_listing_match(Filter, Name, _) :-
1312
1328
  ( sub_atom(Name, 0, _, _, Filter)
1313
1329
  ; sub_atom(Filter, 0, _, _, Name)
1314
1330
  ).
1315
- repl_listing_match(_, _, _).
1331
+ repl_listing_match(_, _, _) :-
1332
+ fail.
1316
1333
 
1317
1334
  %% repl_user_predicate(+SessionId, +Name, +Arity)
1318
1335
  %% Excludes reserved and internal predicates from listing/export.
@@ -2308,6 +2325,46 @@ repl_rollback(SessionId, Source, Result) :-
2308
2325
  format(atom(Result), 'error(~w)', [RollbackError])
2309
2326
  ).
2310
2327
 
2328
+ %% repl_harness_view(+SessionId, -Rows)
2329
+ %% One row per predicate of the harness, for the live side view:
2330
+ %% "Name/Arity|ClauseCount|flag,flag"
2331
+ %% Text rather than terms, so the host does not have to decode compound terms through
2332
+ %% the bridge — the view needs identity (to diff) and a little detail, not structure.
2333
+ repl_harness_view(SessionId, Rows) :-
2334
+ repl_user_predicates(SessionId, Preds),
2335
+ findall(Row,
2336
+ ( member(Name/Arity, Preds),
2337
+ repl_predicate_clause_count(SessionId, Name, Arity, Clauses),
2338
+ repl_harness_flags(SessionId, Name, Arity, Flags),
2339
+ ( Flags == []
2340
+ -> FlagText = ""
2341
+ ; atomic_list_concat(Flags, ',', FlagText)
2342
+ ),
2343
+ format(atom(Row), "~w/~w|~w|~w", [Name, Arity, Clauses, FlagText])
2344
+ ),
2345
+ Rows).
2346
+
2347
+ repl_predicate_clause_count(SessionId, Name, Arity, Count) :-
2348
+ functor(Head, Name, Arity),
2349
+ findall(x, clause(SessionId:Head, _), All),
2350
+ length(All, Count).
2351
+
2352
+ repl_harness_flags(SessionId, Name, Arity, Flags) :-
2353
+ findall(Flag,
2354
+ ( member(Flag, [entry, tool, doc, touched]),
2355
+ repl_harness_flag(SessionId, Name, Arity, Flag)
2356
+ ),
2357
+ Flags).
2358
+
2359
+ repl_harness_flag(_SessionId, agent_main, _Arity, entry).
2360
+ repl_harness_flag(SessionId, Name, _Arity, tool) :-
2361
+ session_user_tools(SessionId, Name).
2362
+ repl_harness_flag(SessionId, Name, Arity, doc) :-
2363
+ current_predicate(SessionId:doc/2),
2364
+ catch(SessionId:doc(Name/Arity, _), _, fail).
2365
+ repl_harness_flag(SessionId, Name, Arity, touched) :-
2366
+ session_edit_touched(SessionId, Name/Arity).
2367
+
2311
2368
  %% repl_user_predicates(+SessionId, -Preds)
2312
2369
  %% Sorted Name/Arity list of the harness's own predicates.
2313
2370
  repl_user_predicates(SessionId, Preds) :-
@@ -2414,6 +2471,9 @@ repl_validate_docs(SessionId, Warnings) :-
2414
2471
  list_to_set(Raw, Warnings).
2415
2472
 
2416
2473
  doc_without_clauses(SessionId, Warning) :-
2474
+ % A harness with no `doc/2` at all would otherwise make this call an
2475
+ % undefined-procedure warning (`Unknown procedure: repl_x:doc/2`).
2476
+ current_predicate(SessionId:doc/2),
2417
2477
  SessionId:doc(Name/Arity, _),
2418
2478
  ( \+ current_predicate(SessionId:Name/Arity)
2419
2479
  ; functor(Head, Name, Arity),
@@ -2666,6 +2726,33 @@ repl_set_memory(SessionId, Slot, Messages) :-
2666
2726
  assertz(session_repl_memory(SessionId, Messages))
2667
2727
  ).
2668
2728
 
2729
+ %% repl_note_resumed(+SessionId, +Text)
2730
+ %% Record that the session was restored from a saved snapshot. Added to both
2731
+ %% memory slots so the authoring agent and every task/N see it. Any earlier
2732
+ %% note is replaced, so a session resumed many times carries one line, not a
2733
+ %% growing stack.
2734
+ repl_note_resumed(SessionId, Text) :-
2735
+ atom_string(TextAtom, Text),
2736
+ Note = message{role: system, content: TextAtom},
2737
+ forall(member(Slot, [harness, meta]), (
2738
+ repl_get_memory(SessionId, Slot, Messages),
2739
+ exclude(is_resume_note, Messages, Kept),
2740
+ append(Kept, [Note], Next),
2741
+ repl_set_memory(SessionId, Slot, Next)
2742
+ )).
2743
+
2744
+ %% is_resume_note(+Message)
2745
+ %% True for a prior `repl_note_resumed/2` message, matched by its opening words.
2746
+ %% The content may be an atom (as first written) or a string (after a task/N
2747
+ %% round-trips the memory through the runner), so accept either.
2748
+ is_resume_note(message{role: system, content: Content}) :-
2749
+ ( atom(Content)
2750
+ -> sub_atom(Content, 0, _, _, 'This session was reloaded')
2751
+ ; string(Content)
2752
+ -> sub_string(Content, 0, _, _, "This session was reloaded")
2753
+ ; fail
2754
+ ).
2755
+
2669
2756
  %% ---- Edit transactions ----
2670
2757
 
2671
2758
  %% repl_snapshot_save(+SessionId)
@@ -2922,6 +3009,25 @@ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), request_age
2922
3009
  payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: none, reasoningEffort: none, recipeContext: ""}) :- !.
2923
3010
  process_engine_result(request_exec(Tool, Args), request_exec, '',
2924
3011
  payload{toolName: Tool, args: Args}) :- !.
3012
+ %% Process primitives (spawn/send/receive/...) are yielded to the host, which
3013
+ %% owns identity, mailboxes, links, monitors, and naming.
3014
+ process_engine_result(request_proc(Request), request_proc, '', Payload) :-
3015
+ proc_request_payload(Request, Payload).
3016
+
3017
+ %% proc_request_payload(+Request, -Payload)
3018
+ proc_request_payload(self, payload{op: self}) :- !.
3019
+ proc_request_payload(spawn(Goal), payload{op: spawn, goal: Goal}) :- !.
3020
+ proc_request_payload(spawn(Goal, Options), payload{op: spawn, goal: Goal, options: Options}) :- !.
3021
+ proc_request_payload(send(To, Message), payload{op: send, to: To, message: Message}) :- !.
3022
+ proc_request_payload(receive(Timeout), payload{op: receive, timeout: Timeout}) :- !.
3023
+ proc_request_payload(requeue(Message), payload{op: requeue, message: Message}) :- !.
3024
+ proc_request_payload(wait(Pid), payload{op: wait, pid: Pid}) :- !.
3025
+ proc_request_payload(exit(Reason), payload{op: exit, reason: Reason}) :- !.
3026
+ proc_request_payload(exit(Pid, Reason), payload{op: exit, pid: Pid, reason: Reason}) :- !.
3027
+ proc_request_payload(process_info(Pid), payload{op: process_info, pid: Pid}) :- !.
3028
+ proc_request_payload(register(Name, Pid), payload{op: register, name: Name, pid: Pid}) :- !.
3029
+ proc_request_payload(whereis(Name), payload{op: whereis, name: Name}) :- !.
3030
+ proc_request_payload(sleep(Milliseconds), payload{op: sleep, ms: Milliseconds}) :- !.
2925
3031
  process_engine_result(request_sample_token(Prompt, Allowed, ModelSpec), request_sample_token, '',
2926
3032
  payload{prompt: Prompt, allowedTokens: Allowed, modelSpec: ModelSpec}) :- !.
2927
3033
  process_engine_result(request_sample_token(Prompt, Allowed), request_sample_token, '',
@@ -3010,6 +3116,13 @@ post_exec_result(SessionId, Status, Result) :-
3010
3116
  assertz(session_exec_result(SessionId, result{status: Status, result: Result})),
3011
3117
  post_signal_to_engine(SessionId, exec_done).
3012
3118
 
3119
+ %% post_proc_result(+SessionId, +Key, +Value)
3120
+ %% Reply to a process primitive (spawn/send/receive/...). Key names the result.
3121
+ post_proc_result(SessionId, Key, Value) :-
3122
+ retractall(session_proc_result(SessionId, Key, _)),
3123
+ assertz(session_proc_result(SessionId, Key, Value)),
3124
+ post_signal_to_engine(SessionId, proc_done).
3125
+
3013
3126
  %% post_sample_token_result(+SessionId, +Status, +TokenOrError)
3014
3127
  post_sample_token_result(SessionId, Status, TokenOrError) :-
3015
3128
  retractall(session_sample_token_result(SessionId, _)),
@@ -3459,6 +3572,10 @@ process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolSco
3459
3572
  %% Legacy 4-arg format (no tool scope)
3460
3573
  process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), request_agent_loop, '',
3461
3574
  payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: none, reasoningEffort: none, recipeContext: ""}) :- !.
3575
+ %% Process primitives (self/send/receive/spawn/...) reached inside a DML tool
3576
+ %% are yielded to the host exactly as in the main engine.
3577
+ process_tool_engine_result(request_proc(Request), request_proc, '', Payload) :-
3578
+ proc_request_payload(Request, Payload).
3462
3579
  process_tool_engine_result(output(Text), output, Text, none) :- !.
3463
3580
  process_tool_engine_result(log(Text), log, Text, none) :- !.
3464
3581
  process_tool_engine_result(Other, error, Msg, none) :-
@@ -4144,6 +4261,410 @@ mi_call(exec(ToolCall, Output), StateIn, StateOut) :-
4144
4261
  fail
4145
4262
  ).
4146
4263
 
4264
+ %% ============================================================
4265
+ %% Process Primitives (session-scoped subagents)
4266
+ %% ============================================================
4267
+ %% These yield to the TypeScript host, which owns process identity, mailboxes,
4268
+ %% links, monitors, and naming. See docs/DML_REPL_PROCESSES_SPEC.md.
4269
+ %% Goals and messages cross the boundary as canonical term strings, so a
4270
+ %% process never shares a variable with its spawner (invariant A).
4271
+
4272
+ %% mi_call(self(?Pid), +StateIn, -StateOut)
4273
+ mi_call(self(Pid), StateIn, StateIn) :-
4274
+ !,
4275
+ get_session_id(SessionId),
4276
+ engine_yield(request_proc(self)),
4277
+ get_next_signal(SessionId, _),
4278
+ session_proc_result(SessionId, self, Pid),
4279
+ retract(session_proc_result(SessionId, self, _)).
4280
+
4281
+ %% mi_call(spawn(+Goal, -Pid), +StateIn, -StateOut)
4282
+ mi_call(spawn(Goal, Pid), StateIn, StateIn) :-
4283
+ !,
4284
+ get_session_id(SessionId),
4285
+ copy_term(Goal, GoalCopy),
4286
+ term_string(GoalCopy, GoalText),
4287
+ engine_yield(request_proc(spawn(GoalText))),
4288
+ get_next_signal(SessionId, _),
4289
+ session_proc_result(SessionId, spawn, SpawnResult),
4290
+ retract(session_proc_result(SessionId, spawn, _)),
4291
+ ( SpawnResult = ok(Pid)
4292
+ -> true
4293
+ ; throw(error(process_spawn_failed(SpawnResult), spawn/2))
4294
+ ).
4295
+
4296
+ %% mi_call(spawn_opt(+Goal, -Pid, +Options), +StateIn, -StateOut)
4297
+ mi_call(spawn_opt(Goal, Pid, Options), StateIn, StateIn) :-
4298
+ !,
4299
+ get_session_id(SessionId),
4300
+ copy_term(Goal, GoalCopy),
4301
+ term_string(GoalCopy, GoalText),
4302
+ ( is_dict(Options) -> term_string(Options, OptionsText) ; OptionsText = "{}" ),
4303
+ engine_yield(request_proc(spawn(GoalText, OptionsText))),
4304
+ get_next_signal(SessionId, _),
4305
+ session_proc_result(SessionId, spawn, SpawnResult),
4306
+ retract(session_proc_result(SessionId, spawn, _)),
4307
+ ( SpawnResult = ok(Pid)
4308
+ -> true
4309
+ ; throw(error(process_spawn_failed(SpawnResult), spawn_opt/3))
4310
+ ).
4311
+
4312
+ %% mi_call(send(+Target, +Message), +StateIn, -StateOut)
4313
+ mi_call(send(To, Message), StateIn, StateIn) :-
4314
+ !,
4315
+ get_session_id(SessionId),
4316
+ term_string(To, ToText),
4317
+ term_string(Message, MessageText),
4318
+ engine_yield(request_proc(send(ToText, MessageText))),
4319
+ get_next_signal(SessionId, _),
4320
+ session_proc_result(SessionId, send, SendResult),
4321
+ retract(session_proc_result(SessionId, send, _)),
4322
+ ( SendResult == ok
4323
+ -> true
4324
+ ; throw(error(process_send_failed(SendResult), send/2))
4325
+ ).
4326
+
4327
+ %% mi_call(receive(?Message), +StateIn, -StateOut)
4328
+ mi_call(receive(Message), StateIn, StateIn) :-
4329
+ !,
4330
+ mi_call(receive(Message, 0), StateIn, StateIn).
4331
+
4332
+ %% mi_call(receive(?Message, +TimeoutMs), +StateIn, -StateOut)
4333
+ %% Fails on timeout, so a caller can branch on it.
4334
+ mi_call(receive(Message, TimeoutMs), StateIn, StateIn) :-
4335
+ !,
4336
+ get_session_id(SessionId),
4337
+ engine_yield(request_proc(receive(TimeoutMs))),
4338
+ get_next_signal(SessionId, _),
4339
+ session_proc_result(SessionId, receive, ReceiveResult),
4340
+ retract(session_proc_result(SessionId, receive, _)),
4341
+ ReceiveResult = ok(Message).
4342
+
4343
+ %% mi_call(receive_match(+Template, ?Message, +TimeoutMs), +StateIn, -StateOut)
4344
+ %% Selective receive: skip messages that do not unify with Template, requeue
4345
+ %% them in order, and fail on timeout. The template unifies in this process.
4346
+ mi_call(receive_match(Template, Message, TimeoutMs), StateIn, StateIn) :-
4347
+ !,
4348
+ get_session_id(SessionId),
4349
+ proc_now_ms(Start),
4350
+ receive_match_loop(Template, Message, TimeoutMs, Start, SessionId, []).
4351
+
4352
+ %% A plain monotonic clock for helpers (now_ms/1 is a mi_call special predicate).
4353
+ proc_now_ms(Milliseconds) :-
4354
+ get_time(Seconds),
4355
+ Milliseconds is round(Seconds * 1000).
4356
+
4357
+ receive_match_loop(Template, Message, TimeoutMs, Start, SessionId, Skipped) :-
4358
+ proc_now_ms(Now),
4359
+ ( TimeoutMs > 0
4360
+ -> Left is TimeoutMs - (Now - Start)
4361
+ ; Left = 0
4362
+ ),
4363
+ ( TimeoutMs > 0, Left =< 0
4364
+ -> requeue_skipped(Skipped, SessionId),
4365
+ fail
4366
+ ; engine_yield(request_proc(receive(Left))),
4367
+ get_next_signal(SessionId, _),
4368
+ session_proc_result(SessionId, receive, Result),
4369
+ retract(session_proc_result(SessionId, receive, _)),
4370
+ ( Result = ok(Term),
4371
+ ( copy_term(Template, Copy), Copy = Term
4372
+ -> requeue_skipped(Skipped, SessionId),
4373
+ Template = Term,
4374
+ Message = Term
4375
+ ; receive_match_loop(Template, Message, TimeoutMs, Start, SessionId, [Term|Skipped])
4376
+ )
4377
+ ; requeue_skipped(Skipped, SessionId),
4378
+ fail
4379
+ )
4380
+ ).
4381
+
4382
+ requeue_skipped(Skipped, SessionId) :-
4383
+ requeue_reversed(Skipped, [], Reversed),
4384
+ requeue_all(Reversed, SessionId).
4385
+
4386
+ %% Build the reversed list without library(lists) (deepclause_mi may not have it).
4387
+ requeue_reversed([], Acc, Acc).
4388
+ requeue_reversed([X|Xs], Acc, Out) :- requeue_reversed(Xs, [X|Acc], Out).
4389
+
4390
+ requeue_all([], _).
4391
+ requeue_all([Message|Messages], SessionId) :-
4392
+ term_string(Message, Text),
4393
+ engine_yield(request_proc(requeue(Text))),
4394
+ get_next_signal(SessionId, _),
4395
+ session_proc_result(SessionId, requeue, _),
4396
+ retract(session_proc_result(SessionId, requeue, _)),
4397
+ requeue_all(Messages, SessionId).
4398
+
4399
+ %% mi_call(exit(+Reason), +StateIn, -StateOut)
4400
+ %% Terminate this process. The host marks it killed; the frame is stopped by
4401
+ %% the throw below, which the runner maps to a clean termination.
4402
+ mi_call(exit(Reason), StateIn, StateIn) :-
4403
+ !,
4404
+ get_session_id(SessionId),
4405
+ term_string(Reason, ReasonText),
4406
+ engine_yield(request_proc(exit(ReasonText))),
4407
+ get_next_signal(SessionId, _),
4408
+ session_proc_result(SessionId, exit, _),
4409
+ retract(session_proc_result(SessionId, exit, _)),
4410
+ throw(process_exit(Reason)).
4411
+
4412
+ %% mi_call(exit(+Pid, +Reason), +StateIn, -StateOut)
4413
+ %% Terminate another process. Unlike exit/1 this returns; the target is killed.
4414
+ mi_call(exit(Pid, Reason), StateIn, StateIn) :-
4415
+ !,
4416
+ get_session_id(SessionId),
4417
+ term_string(Pid, PidText),
4418
+ term_string(Reason, ReasonText),
4419
+ engine_yield(request_proc(exit(PidText, ReasonText))),
4420
+ get_next_signal(SessionId, _),
4421
+ session_proc_result(SessionId, exit, ExitResult),
4422
+ retract(session_proc_result(SessionId, exit, _)),
4423
+ ( ExitResult == ok
4424
+ -> true
4425
+ ; throw(error(process_exit_failed(ExitResult), exit/2))
4426
+ ).
4427
+
4428
+ %% mi_call(process_info(+Pid, -Info), +StateIn, -StateOut)
4429
+ mi_call(process_info(Pid, Info), StateIn, StateIn) :-
4430
+ !,
4431
+ get_session_id(SessionId),
4432
+ term_string(Pid, PidText),
4433
+ engine_yield(request_proc(process_info(PidText))),
4434
+ get_next_signal(SessionId, _),
4435
+ session_proc_result(SessionId, process_info, Info),
4436
+ retract(session_proc_result(SessionId, process_info, _)).
4437
+
4438
+ %% mi_call(register(+Name, +Pid), +StateIn, -StateOut)
4439
+ mi_call(register(Name, Pid), StateIn, StateIn) :-
4440
+ !,
4441
+ get_session_id(SessionId),
4442
+ term_string(Name, NameText),
4443
+ term_string(Pid, PidText),
4444
+ engine_yield(request_proc(register(NameText, PidText))),
4445
+ get_next_signal(SessionId, _),
4446
+ session_proc_result(SessionId, register, RegisterResult),
4447
+ retract(session_proc_result(SessionId, register, _)),
4448
+ ( RegisterResult == ok
4449
+ -> true
4450
+ ; throw(error(process_register_failed(RegisterResult), register/2))
4451
+ ).
4452
+
4453
+ %% mi_call(whereis(+Name, ?Pid), +StateIn, -StateOut)
4454
+ mi_call(whereis(Name, Pid), StateIn, StateIn) :-
4455
+ !,
4456
+ get_session_id(SessionId),
4457
+ term_string(Name, NameText),
4458
+ engine_yield(request_proc(whereis(NameText))),
4459
+ get_next_signal(SessionId, _),
4460
+ session_proc_result(SessionId, whereis, WhereResult),
4461
+ retract(session_proc_result(SessionId, whereis, _)),
4462
+ WhereResult = ok(Pid).
4463
+
4464
+ %% mi_call(sleep(+Milliseconds), +StateIn, -StateOut)
4465
+ mi_call(sleep(Milliseconds), StateIn, StateIn) :-
4466
+ !,
4467
+ get_session_id(SessionId),
4468
+ engine_yield(request_proc(sleep(Milliseconds))),
4469
+ get_next_signal(SessionId, _),
4470
+ session_proc_result(SessionId, sleep, _),
4471
+ retract(session_proc_result(SessionId, sleep, _)).
4472
+
4473
+ %% mi_call(make_ref(-Ref), +StateIn, -StateOut)
4474
+ mi_call(make_ref(Ref), StateIn, StateIn) :-
4475
+ !,
4476
+ gensym(ref_, Ref).
4477
+
4478
+ %% mi_call(now_ms(-Milliseconds), +StateIn, -StateOut)
4479
+ mi_call(now_ms(Milliseconds), StateIn, StateIn) :-
4480
+ !,
4481
+ get_time(Seconds),
4482
+ Milliseconds is round(Seconds * 1000).
4483
+
4484
+ %% mi_call(serve(+Name, +Goal), +StateIn, -StateOut)
4485
+ %% Spawn a session-scoped process and register it under Name. Sugar over
4486
+ %% spawn/2 + register/2; the goal decides the process's shape.
4487
+ mi_call(serve(Name, Goal), StateIn, StateIn) :-
4488
+ !,
4489
+ get_session_id(SessionId),
4490
+ copy_term(Goal, GoalCopy),
4491
+ term_string(GoalCopy, GoalText),
4492
+ engine_yield(request_proc(spawn(GoalText))),
4493
+ get_next_signal(SessionId, _),
4494
+ session_proc_result(SessionId, spawn, SpawnResult),
4495
+ retract(session_proc_result(SessionId, spawn, _)),
4496
+ SpawnResult = ok(Pid),
4497
+ term_string(Name, NameText),
4498
+ term_string(Pid, PidText),
4499
+ engine_yield(request_proc(register(NameText, PidText))),
4500
+ get_next_signal(SessionId, _),
4501
+ session_proc_result(SessionId, register, _),
4502
+ retract(session_proc_result(SessionId, register, _)).
4503
+
4504
+ %% mi_call(service_cast(+Target, +Message), +StateIn, -StateOut)
4505
+ mi_call(service_cast(Target, Message), StateIn, StateIn) :-
4506
+ !,
4507
+ get_session_id(SessionId),
4508
+ term_string(Target, TargetText),
4509
+ term_string(Message, MessageText),
4510
+ engine_yield(request_proc(send(TargetText, MessageText))),
4511
+ get_next_signal(SessionId, _),
4512
+ session_proc_result(SessionId, send, _),
4513
+ retract(session_proc_result(SessionId, send, _)).
4514
+
4515
+ %% mi_call(service_call(+Target, +Request, -Reply), +StateIn, -StateOut)
4516
+ mi_call(service_call(Target, Request, Reply), StateIn, StateOut) :-
4517
+ !,
4518
+ mi_call(service_call(Target, Request, Reply, 300000), StateIn, StateOut).
4519
+
4520
+ %% mi_call(service_call(+Target, +Request, -Reply, +TimeoutMs), ...)
4521
+ %% Request/reply against a process using the request(From, Ref, Request) /
4522
+ %% reply(Ref, Value) protocol. Unrelated messages are ignored while waiting.
4523
+ mi_call(service_call(Target, Request, Reply, Timeout), StateIn, StateIn) :-
4524
+ !,
4525
+ get_session_id(SessionId),
4526
+ gensym(reply_ref_, Ref),
4527
+ engine_yield(request_proc(self)),
4528
+ get_next_signal(SessionId, _),
4529
+ session_proc_result(SessionId, self, Me),
4530
+ retract(session_proc_result(SessionId, self, _)),
4531
+ term_string(Target, TargetText),
4532
+ term_string(request(Me, Ref, Request), RequestText),
4533
+ engine_yield(request_proc(send(TargetText, RequestText))),
4534
+ get_next_signal(SessionId, _),
4535
+ session_proc_result(SessionId, send, _),
4536
+ retract(session_proc_result(SessionId, send, _)),
4537
+ service_await_reply(Ref, Reply, Timeout).
4538
+
4539
+ %% service_await_reply(+Ref, -Reply, +TimeoutMs)
4540
+ service_await_reply(Ref, Reply, Timeout) :-
4541
+ get_session_id(SessionId),
4542
+ engine_yield(request_proc(receive(Timeout))),
4543
+ get_next_signal(SessionId, _),
4544
+ session_proc_result(SessionId, receive, Result),
4545
+ retract(session_proc_result(SessionId, receive, _)),
4546
+ ( Result = ok(reply(Ref, Reply))
4547
+ -> true
4548
+ ; Result = ok(_)
4549
+ -> service_await_reply(Ref, Reply, Timeout)
4550
+ ; fail
4551
+ ).
4552
+
4553
+ %% mi_call(wait(+Pid[, -Outcome]), +StateIn, -StateOut)
4554
+ %% Block until a process terminates. Outcome is ok(Result), failed(Error), or
4555
+ %% killed(Reason); Result is the process's terminal text as a Prolog string.
4556
+ %% `wait/1` is semidet: it succeeds only on a successful finish.
4557
+ mi_call(wait(Pid), StateIn, StateIn) :-
4558
+ !,
4559
+ mi_call(wait(Pid, Outcome), StateIn, _),
4560
+ Outcome = ok(_).
4561
+
4562
+ mi_call(wait(Pid, Outcome), StateIn, StateIn) :-
4563
+ !,
4564
+ get_session_id(SessionId),
4565
+ term_string(Pid, PidText),
4566
+ engine_yield(request_proc(wait(PidText))),
4567
+ get_next_signal(SessionId, _),
4568
+ session_proc_result(SessionId, wait, Outcome),
4569
+ retract(session_proc_result(SessionId, wait, _)).
4570
+
4571
+ %% ============================================================
4572
+ %% Swarm / pmap: fan a worker over a list concurrently
4573
+ %% ============================================================
4574
+ %% Each item runs in its own process (spawned through the host), calling
4575
+ %% call(Worker, Item, Value). Results preserve input order. `Concurrency` is
4576
+ %% accepted for compatibility and not yet enforced (the host's process limit is).
4577
+
4578
+ proc_self(SessionId, Me) :-
4579
+ engine_yield(request_proc(self)),
4580
+ get_next_signal(SessionId, _),
4581
+ session_proc_result(SessionId, self, Me),
4582
+ retract(session_proc_result(SessionId, self, _)).
4583
+
4584
+ %% mi_call(swarm(+Worker, +Items, -Results), ...)
4585
+ mi_call(swarm(Worker, Items, Results), StateIn, StateOut) :-
4586
+ !,
4587
+ mi_call(swarm(Worker, Items, Results, 4), StateIn, StateOut).
4588
+
4589
+ %% mi_call(swarm(+Worker, +Items, -Results, +Concurrency), ...)
4590
+ %% Windowed fan-out: at most Concurrency items run at once.
4591
+ mi_call(swarm(Worker, Items, Results, Concurrency), StateIn, StateIn) :-
4592
+ !,
4593
+ get_session_id(SessionId),
4594
+ proc_self(SessionId, Me),
4595
+ ( integer(Concurrency), Concurrency > 0 -> Conc = Concurrency ; Conc = 4 ),
4596
+ swarm_loop(Items, 1, [], Conc, Worker, Me, SessionId, [], Pairs),
4597
+ keysort(Pairs, Sorted),
4598
+ swarm_values(Sorted, Results).
4599
+
4600
+ %% Accumulate `Acc`, bind the collected pairs to `Pairs` at the base.
4601
+ swarm_loop(Pending, NextIndex, InFlight, Conc, Worker, Me, SessionId, Acc, Pairs) :-
4602
+ ( Pending = [Item|Rest],
4603
+ length(InFlight, K), K < Conc
4604
+ -> swarm_spawn_one(Item, NextIndex, Worker, Me, SessionId, SpawnResult),
4605
+ Next is NextIndex + 1,
4606
+ ( SpawnResult = ok(_Pid)
4607
+ -> swarm_loop(Rest, Next, [NextIndex|InFlight], Conc, Worker, Me, SessionId, Acc, Pairs)
4608
+ ; % The host refused the spawn (e.g. the live-process limit): fail only
4609
+ % this item, so the swarm still completes and the reason is visible
4610
+ % instead of the whole call failing silently.
4611
+ swarm_loop(Rest, Next, InFlight, Conc, Worker, Me, SessionId,
4612
+ [NextIndex-failed(SpawnResult)|Acc], Pairs)
4613
+ )
4614
+ ; InFlight = [_|_]
4615
+ -> swarm_receive(Index, Value, SessionId),
4616
+ selectchk(Index, InFlight, Rest),
4617
+ swarm_loop(Pending, NextIndex, Rest, Conc, Worker, Me, SessionId, [Index-Value|Acc], Pairs)
4618
+ ; Pairs = Acc
4619
+ ).
4620
+
4621
+ swarm_spawn_one(Item, Index, Worker, Me, SessionId, SpawnResult) :-
4622
+ term_string(deepclause_mi:dc_swarm_item(Worker, Item, Me, Index), GoalText),
4623
+ engine_yield(request_proc(spawn(GoalText))),
4624
+ get_next_signal(SessionId, _),
4625
+ session_proc_result(SessionId, spawn, SpawnResult),
4626
+ retract(session_proc_result(SessionId, spawn, _)).
4627
+
4628
+ swarm_receive(Index, Value, SessionId) :-
4629
+ engine_yield(request_proc(receive(0))),
4630
+ get_next_signal(SessionId, _),
4631
+ session_proc_result(SessionId, receive, R),
4632
+ retract(session_proc_result(SessionId, receive, _)),
4633
+ R = ok(swarm_result(Index, Value)).
4634
+
4635
+ swarm_values([], []).
4636
+ swarm_values([_-Value|Rest], [Value|Values]) :- swarm_values(Rest, Values).
4637
+
4638
+ %% dc_swarm_item(+Worker, +Item, +Parent, +Index) — the child goal.
4639
+ %% Catch a thrown error (an undefined worker, a budget, a tool failure) and
4640
+ %% report it as `failed(Error)`. Without the catch the child process crashes and
4641
+ %% never sends a result, so the parent's `swarm_receive` waits forever.
4642
+ %%
4643
+ %% The `call` must stay inline in this clause: a helper predicate defined in
4644
+ %% `deepclause_mi` is reached via the meta-interpreter's raw-Prolog fallback, and
4645
+ %% then `call(Worker, ...)` resolves in `deepclause_mi` instead of the session
4646
+ %% module (so every worker looked undefined).
4647
+ dc_swarm_item(Worker, Item, Parent, Index) :-
4648
+ catch(
4649
+ ( call(Worker, Item, Result)
4650
+ -> Outcome = ok(Result)
4651
+ ; Outcome = failed
4652
+ ),
4653
+ Error,
4654
+ Outcome = failed(Error)
4655
+ ),
4656
+ send(Parent, swarm_result(Index, Outcome)).
4657
+
4658
+ %% mi_call(pmap(+Worker, +Items, -Values), ...)
4659
+ %% Like swarm/3, but fails if any item failed.
4660
+ mi_call(pmap(Worker, Items, Values), StateIn, StateIn) :-
4661
+ !,
4662
+ mi_call(swarm(Worker, Items, Results), StateIn, _),
4663
+ pmap_ok(Results, Values).
4664
+
4665
+ pmap_ok([], []).
4666
+ pmap_ok([ok(Value)|Rest], [Value|Values]) :- pmap_ok(Rest, Values).
4667
+
4147
4668
  %% ============================================================
4148
4669
  %% Tool Scoping Predicates
4149
4670
  %% ============================================================
@@ -5477,6 +5998,30 @@ is_mi_special_predicate(task_named(_,_,_)). %% Transformed form of task/N
5477
5998
  is_mi_special_predicate(sample_token(_,_)).
5478
5999
  is_mi_special_predicate(sample_token(_,_,_)).
5479
6000
  is_mi_special_predicate(exec(_,_)).
6001
+ is_mi_special_predicate(spawn(_,_)).
6002
+ is_mi_special_predicate(spawn_opt(_,_,_)).
6003
+ is_mi_special_predicate(self(_)).
6004
+ is_mi_special_predicate(send(_,_)).
6005
+ is_mi_special_predicate(receive(_)).
6006
+ is_mi_special_predicate(receive(_,_)).
6007
+ is_mi_special_predicate(receive_match(_,_,_)).
6008
+ is_mi_special_predicate(exit(_)).
6009
+ is_mi_special_predicate(exit(_,_)).
6010
+ is_mi_special_predicate(process_info(_,_)).
6011
+ is_mi_special_predicate(register(_,_)).
6012
+ is_mi_special_predicate(whereis(_,_)).
6013
+ is_mi_special_predicate(sleep(_)).
6014
+ is_mi_special_predicate(make_ref(_)).
6015
+ is_mi_special_predicate(now_ms(_)).
6016
+ is_mi_special_predicate(serve(_,_)).
6017
+ is_mi_special_predicate(service_cast(_,_)).
6018
+ is_mi_special_predicate(service_call(_,_,_)).
6019
+ is_mi_special_predicate(service_call(_,_,_,_)).
6020
+ is_mi_special_predicate(swarm(_,_,_)).
6021
+ is_mi_special_predicate(swarm(_,_,_,_)).
6022
+ is_mi_special_predicate(pmap(_,_,_)).
6023
+ is_mi_special_predicate(wait(_)).
6024
+ is_mi_special_predicate(wait(_,_)).
5480
6025
  is_mi_special_predicate(param(_,_)).
5481
6026
  is_mi_special_predicate(param(_,_,_)).
5482
6027
  %% Prompt predicates (like task but with fresh memory)
@@ -318,4 +318,13 @@ export interface ResolvedBackend {
318
318
  * provider/model so the runner builds the SDK provider backend.
319
319
  */
320
320
  export declare function resolveLlmBackend(options?: ResolveBackendOptions): Promise<ResolvedBackend>;
321
+ /**
322
+ * Wrap a backend so at most `maxConcurrent` model calls are in flight at once.
323
+ *
324
+ * A `swarm`/fan-out otherwise fires one provider call per item simultaneously,
325
+ * which trips rate limits and bursts cost. The wrapper is created once (in
326
+ * `createHarnessRepl`) and shared by every child session, so the limit is per
327
+ * session tree. `maxConcurrent <= 0` returns the backend unchanged.
328
+ */
329
+ export declare function createLimitedBackend(backend: LLMBackend, maxConcurrent: number): LLMBackend;
321
330
  //# sourceMappingURL=backend.d.ts.map