deepclause-sdk 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -3
- package/dist/agent.d.ts +17 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +148 -42
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +12 -12
- package/dist/cli/repl.d.ts +13 -1
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +146 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/forms/index.d.ts +3 -0
- package/dist/forms/index.d.ts.map +1 -0
- package/dist/forms/index.js +3 -0
- package/dist/forms/index.js.map +1 -0
- package/dist/forms/registry.d.ts +119 -0
- package/dist/forms/registry.d.ts.map +1 -0
- package/dist/forms/registry.js +256 -0
- package/dist/forms/registry.js.map +1 -0
- package/dist/forms/spec.d.ts +193 -0
- package/dist/forms/spec.d.ts.map +1 -0
- package/dist/forms/spec.js +295 -0
- package/dist/forms/spec.js.map +1 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/prolog/loader.d.ts +18 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +26 -1
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +650 -28
- package/dist/repl/agent.d.ts +4 -0
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +2 -0
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/form-widget.d.ts +56 -0
- package/dist/repl/form-widget.d.ts.map +1 -0
- package/dist/repl/form-widget.js +289 -0
- package/dist/repl/form-widget.js.map +1 -0
- package/dist/repl/harness-tools.d.ts +3 -1
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +13 -56
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +2 -1
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +34 -8
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +14 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +6 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/process-backend.d.ts +2 -2
- package/dist/repl/process-backend.d.ts.map +1 -1
- package/dist/repl/process-executor.d.ts +11 -2
- package/dist/repl/process-executor.d.ts.map +1 -1
- package/dist/repl/process-executor.js +67 -18
- package/dist/repl/process-executor.js.map +1 -1
- package/dist/repl/process-worker.d.ts +35 -4
- package/dist/repl/process-worker.d.ts.map +1 -1
- package/dist/repl/process-worker.js +78 -38
- package/dist/repl/process-worker.js.map +1 -1
- package/dist/repl/processes.d.ts +4 -0
- package/dist/repl/processes.d.ts.map +1 -1
- package/dist/repl/processes.js +20 -0
- package/dist/repl/processes.js.map +1 -1
- package/dist/repl/render.d.ts +17 -0
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +50 -0
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +112 -7
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +241 -15
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tui/form-view.d.ts +17 -0
- package/dist/repl/tui/form-view.d.ts.map +1 -0
- package/dist/repl/tui/form-view.js +25 -0
- package/dist/repl/tui/form-view.js.map +1 -0
- package/dist/repl/tui/renderer.d.ts +4 -0
- package/dist/repl/tui/renderer.d.ts.map +1 -1
- package/dist/repl/tui/renderer.js +12 -0
- package/dist/repl/tui/renderer.js.map +1 -1
- package/dist/repl/worker-host.d.ts +16 -2
- package/dist/repl/worker-host.d.ts.map +1 -1
- package/dist/repl/worker-host.js +38 -6
- package/dist/repl/worker-host.js.map +1 -1
- package/dist/repl/worker-protocol.d.ts +14 -0
- package/dist/repl/worker-protocol.d.ts.map +1 -1
- package/dist/repl/worker-protocol.js +8 -1
- package/dist/repl/worker-protocol.js.map +1 -1
- package/dist/runner.d.ts +63 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +361 -40
- package/dist/runner.js.map +1 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +12 -0
- package/dist/sdk.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +441 -18
- package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +1 -1
- package/dist/system/assets/docs/DML_FORMS.md +740 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +234 -4
- package/dist/system/assets/docs/HARNESS_EXAMPLES.md +94 -0
- package/dist/system/assets/index.d.ts +2 -2
- package/dist/system/assets/index.d.ts.map +1 -1
- package/dist/system/assets/index.js +4 -0
- package/dist/system/assets/index.js.map +1 -1
- package/dist/system/environment.d.ts +6 -0
- package/dist/system/environment.d.ts.map +1 -1
- package/dist/system/environment.js +5 -4
- package/dist/system/environment.js.map +1 -1
- package/dist/types.d.ts +35 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +9 -7
- package/src/prolog-src/deepclause_mi.pl +650 -28
|
@@ -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,
|
|
@@ -105,7 +106,13 @@
|
|
|
105
106
|
create_tool_engine/4,
|
|
106
107
|
step_tool_engine/4,
|
|
107
108
|
destroy_tool_engine/1,
|
|
108
|
-
post_tool_agent_result/5
|
|
109
|
+
post_tool_agent_result/5,
|
|
110
|
+
% Forms (docs/DML_FORMS.md)
|
|
111
|
+
post_form_result/2,
|
|
112
|
+
post_next_result/2,
|
|
113
|
+
form_validate_values/3,
|
|
114
|
+
form_advisory_values/3,
|
|
115
|
+
repl_normalize_message/2
|
|
109
116
|
]).
|
|
110
117
|
|
|
111
118
|
:- use_module(deepclause_strings).
|
|
@@ -114,6 +121,10 @@
|
|
|
114
121
|
:- discontiguous mi_call/3.
|
|
115
122
|
:- discontiguous transform_task_calls/3.
|
|
116
123
|
:- discontiguous var_to_name/3.
|
|
124
|
+
%% `proc_request_payload/2` sits between the process-primitive clauses of
|
|
125
|
+
%% `process_engine_result/4`, which is intentional.
|
|
126
|
+
:- discontiguous process_engine_result/4.
|
|
127
|
+
:- discontiguous process_directive/2.
|
|
117
128
|
|
|
118
129
|
%% Dynamic predicates for session state (engine management only, not memory!)
|
|
119
130
|
:- dynamic session_engine/2. % session_engine(SessionId, Engine) - legacy default engine handle
|
|
@@ -151,6 +162,9 @@
|
|
|
151
162
|
:- dynamic session_proc_result/3. % session_proc_result(SessionId, Key, Value) - process primitive results
|
|
152
163
|
:- dynamic session_user_input/2. % session_user_input(SessionId, Input) - for ask_user tool
|
|
153
164
|
:- dynamic session_input_cancelled/1. % session_input_cancelled(SessionId) - input/2 should fail (non-interactive)
|
|
165
|
+
:- dynamic session_form_result/2. % session_form_result(SessionId, Result) - forms primitive results
|
|
166
|
+
:- dynamic session_builtin_tool/2. % session_builtin_tool(SessionId, ToolName) - runtime-installed tools
|
|
167
|
+
:- dynamic session_next_result/2. % session_next_result(SessionId, Item) - conversation inbox item
|
|
154
168
|
|
|
155
169
|
%% ============================================================
|
|
156
170
|
%% DML Parsing
|
|
@@ -197,7 +211,7 @@ parse_dml_string(Code, SessionId) :-
|
|
|
197
211
|
parse_clauses(Stream, SessionId) :-
|
|
198
212
|
read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
|
|
199
213
|
( Term == end_of_file
|
|
200
|
-
->
|
|
214
|
+
-> install_builtin_tools(SessionId)
|
|
201
215
|
; preprocess_loaded_term(Term, Bindings, PreparedTerms),
|
|
202
216
|
forall(member(PreparedTerm, PreparedTerms), process_clause(PreparedTerm, SessionId)),
|
|
203
217
|
parse_clauses(Stream, SessionId)
|
|
@@ -569,6 +583,19 @@ process_clause((:- Directive), SessionId) :-
|
|
|
569
583
|
!,
|
|
570
584
|
process_directive(Directive, SessionId).
|
|
571
585
|
|
|
586
|
+
%% Handle tool/3: description plus per-input types.
|
|
587
|
+
%% tool(Head, Description, [object, number]) :- Body.
|
|
588
|
+
%% The leading args are inputs (typed from the list); the last arg is the output.
|
|
589
|
+
process_clause((tool(ToolHead, Description, Types) :- Body), SessionId) :-
|
|
590
|
+
!,
|
|
591
|
+
(catch(expand_dict_dots(Body, ExpandedBody), _, fail) -> true ; ExpandedBody = Body),
|
|
592
|
+
extract_tool_schema(ToolHead, Description, Types, ToolName, Schema),
|
|
593
|
+
assertz(session_user_tools(SessionId, ToolName)),
|
|
594
|
+
assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
|
|
595
|
+
format_tool_source(ToolHead, Description, Types, Body, SourceCode),
|
|
596
|
+
assertz(SessionId:tool_source(ToolName, SourceCode)),
|
|
597
|
+
assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
|
|
598
|
+
|
|
572
599
|
%% Handle tool/2 with description: tool(Head, Description) :- Body
|
|
573
600
|
process_clause((tool(ToolHead, Description) :- Body), SessionId) :-
|
|
574
601
|
!,
|
|
@@ -689,9 +716,17 @@ expand_dict_expr(Expr, Expr).
|
|
|
689
716
|
|
|
690
717
|
%% extract_tool_schema(+ToolHead, +Description, -ToolName, -Schema)
|
|
691
718
|
extract_tool_schema(ToolHead, Description, ToolName, Schema) :-
|
|
719
|
+
extract_tool_schema(ToolHead, Description, [], ToolName, Schema).
|
|
720
|
+
|
|
721
|
+
%% extract_tool_schema(+Head, +Description, +Types, -ToolName, -Schema)
|
|
722
|
+
%% `Types` is an optional list giving the type of each input (the leading args,
|
|
723
|
+
%% in order); a missing entry defaults to `string`. This is what lets a tool take
|
|
724
|
+
%% an object, an array or a number rather than only a string.
|
|
725
|
+
extract_tool_schema(ToolHead, Description, Types, ToolName, Schema) :-
|
|
692
726
|
ToolHead =.. [ToolName|Args],
|
|
693
727
|
length(Args, Arity),
|
|
694
|
-
|
|
728
|
+
tool_type_warning(ToolName, Arity, Types),
|
|
729
|
+
extract_params(Args, 1, Arity, Types, Inputs, Outputs),
|
|
695
730
|
(Description == none -> Desc = "" ; Desc = Description),
|
|
696
731
|
Schema = schema{
|
|
697
732
|
name: ToolName,
|
|
@@ -700,19 +735,45 @@ extract_tool_schema(ToolHead, Description, ToolName, Schema) :-
|
|
|
700
735
|
outputs: Outputs
|
|
701
736
|
}.
|
|
702
737
|
|
|
703
|
-
%% extract_params(+Args, +Index, +Arity, -Inputs, -Outputs)
|
|
704
|
-
|
|
705
|
-
extract_params([
|
|
738
|
+
%% extract_params(+Args, +Index, +Arity, +Types, -Inputs, -Outputs)
|
|
739
|
+
%% The last argument is the output; the rest are inputs, typed from `Types`.
|
|
740
|
+
extract_params([], _, _, _, [], []) :- !.
|
|
741
|
+
extract_params([_Arg|Rest], Index, Arity, Types, Inputs, Outputs) :-
|
|
706
742
|
format(atom(Name), 'arg~w', [Index]),
|
|
707
|
-
Type = string,
|
|
708
743
|
NextIndex is Index + 1,
|
|
709
|
-
extract_params(Rest, NextIndex, Arity, RestInputs, RestOutputs),
|
|
710
|
-
Param = param{name: Name, type: Type},
|
|
744
|
+
extract_params(Rest, NextIndex, Arity, Types, RestInputs, RestOutputs),
|
|
711
745
|
( Index == Arity
|
|
746
|
+
-> IsOutput = true, Type = string
|
|
747
|
+
; IsOutput = false, input_type(Index, Types, Type)
|
|
748
|
+
),
|
|
749
|
+
Param = param{name: Name, type: Type},
|
|
750
|
+
( IsOutput == true
|
|
712
751
|
-> Inputs = RestInputs, Outputs = [Param|RestOutputs]
|
|
713
752
|
; Inputs = [Param|RestInputs], Outputs = RestOutputs
|
|
714
753
|
).
|
|
715
754
|
|
|
755
|
+
%% input_type(+Index, +Types, -Type): the Index-th input's declared type.
|
|
756
|
+
input_type(1, [T|_], T) :- !.
|
|
757
|
+
input_type(N, [_|Rest], T) :- N > 1, N1 is N - 1, input_type(N1, Rest, T).
|
|
758
|
+
input_type(_, [], string).
|
|
759
|
+
|
|
760
|
+
%% tool_type_warning(+Name, +Arity, +Types)
|
|
761
|
+
%% The last argument of a tool head is the output, so a type list should label
|
|
762
|
+
%% the other (Arity - 1) inputs. When it does not, the author has almost
|
|
763
|
+
%% certainly written a tool whose second input was taken for its output — the
|
|
764
|
+
%% `tool(fs_write(Path, Content), …)` shape, which silently writes nothing.
|
|
765
|
+
tool_type_warning(ToolName, Arity, Types) :-
|
|
766
|
+
is_list(Types),
|
|
767
|
+
Types \== [],
|
|
768
|
+
length(Types, Declared),
|
|
769
|
+
Inputs is Arity - 1,
|
|
770
|
+
Declared =\= Inputs,
|
|
771
|
+
!,
|
|
772
|
+
format(user_error,
|
|
773
|
+
'Warning: tool ~w/~w declares ~w input type(s) but has ~w input argument(s). The last argument of a tool head is the output; a tool with another input needs a result argument (e.g. fs_write(Path, Content, ok)).~n',
|
|
774
|
+
[ToolName, Arity, Declared, Inputs]).
|
|
775
|
+
tool_type_warning(_, _, _).
|
|
776
|
+
|
|
716
777
|
module_reference_string(Module, ModuleStr) :-
|
|
717
778
|
atom(Module),
|
|
718
779
|
!,
|
|
@@ -798,6 +859,113 @@ process_directive(repl_delete(Name/Arity), SessionId) :-
|
|
|
798
859
|
Arity >= 0,
|
|
799
860
|
catch(abolish(SessionId:Name/Arity), _, true).
|
|
800
861
|
|
|
862
|
+
%% ============================================================
|
|
863
|
+
%% Built-in tools (docs/DML_FORMS.md)
|
|
864
|
+
%% ============================================================
|
|
865
|
+
%%
|
|
866
|
+
%% These `tool/2` wrappers are installed into every session, so a harness can
|
|
867
|
+
%% hand the model the form tools without declaring anything. They are ordinary
|
|
868
|
+
%% `tool/N` clauses: assert them through `process_clause/2` (never into a
|
|
869
|
+
%% harness's own clause set) and `task/N` offers them, `with_tools/2` scopes
|
|
870
|
+
%% them, and an export carries the source so a standalone run works.
|
|
871
|
+
%%
|
|
872
|
+
%% The gate that used to guard this — `:- capability(forms).` — was removed: it
|
|
873
|
+
%% was file-scoped (processed at load), so it was invisible to a module built
|
|
874
|
+
%% through the edit tools, and a harness could not discover it, because a gated
|
|
875
|
+
%% primitive is a handler and `current_predicate/1` reports `no` for handlers.
|
|
876
|
+
|
|
877
|
+
install_builtin_tools(SessionId) :-
|
|
878
|
+
% The marker keeps these out of the harness's own surface — `listing`, the
|
|
879
|
+
% manifest's predicates, the export, and the end_turn probes — while they
|
|
880
|
+
% stay in `session_user_tool_schema/3`, so `task/N` and `with_tools/2` offer
|
|
881
|
+
% them. They are built in, not authored.
|
|
882
|
+
forall(builtin_tool_name(Name),
|
|
883
|
+
( session_builtin_tool(SessionId, Name)
|
|
884
|
+
-> true
|
|
885
|
+
; assertz(session_builtin_tool(SessionId, Name))
|
|
886
|
+
)),
|
|
887
|
+
% Idempotent: `parse_clauses/2` runs on every load, and a duplicate schema
|
|
888
|
+
% would show the model the same tool twice.
|
|
889
|
+
( session_user_tool_schema(SessionId, render_form, _)
|
|
890
|
+
-> true
|
|
891
|
+
; forall(builtin_tool_clause(Clause), process_clause(Clause, SessionId))
|
|
892
|
+
).
|
|
893
|
+
|
|
894
|
+
builtin_tool_name(render_form).
|
|
895
|
+
builtin_tool_name(update_form).
|
|
896
|
+
builtin_tool_name(set_form_issues).
|
|
897
|
+
builtin_tool_name(wait_for_form_change).
|
|
898
|
+
builtin_tool_name(validate_form).
|
|
899
|
+
builtin_tool_name(advisories).
|
|
900
|
+
builtin_tool_name(close_form).
|
|
901
|
+
|
|
902
|
+
builtin_tool_clause(
|
|
903
|
+
(tool(render_form(Spec, FormId),
|
|
904
|
+
"Open (or replace) a form. `Spec` is the form spec (version, title, fields, actions). Returns the form id.",
|
|
905
|
+
[object])
|
|
906
|
+
:- form(Spec, FormId))).
|
|
907
|
+
builtin_tool_clause(
|
|
908
|
+
(tool(update_form(FormId, Patch, Result),
|
|
909
|
+
"Update form values with a JSON merge patch.",
|
|
910
|
+
[string, object])
|
|
911
|
+
:- form_update(FormId, Patch), Result = ok)).
|
|
912
|
+
builtin_tool_clause(
|
|
913
|
+
(tool(set_form_issues(FormId, Issues, Result),
|
|
914
|
+
"Attach per-field errors/warnings. Issues is a list of {path, level, message}.",
|
|
915
|
+
[string, array])
|
|
916
|
+
:- form_errors(FormId, Issues), Result = ok)).
|
|
917
|
+
builtin_tool_clause(
|
|
918
|
+
(tool(wait_for_form_change(FormId, Change),
|
|
919
|
+
"Wait for the user to act on a form. Change is a dict with action, values, revision and changed.",
|
|
920
|
+
[string])
|
|
921
|
+
:- await_form(FormId, Change))).
|
|
922
|
+
builtin_tool_clause(
|
|
923
|
+
(tool(validate_form(Values, Result),
|
|
924
|
+
"Hard-validate values without submitting. Result is a list of issues.",
|
|
925
|
+
[object])
|
|
926
|
+
:- get_session_id(SessionId),
|
|
927
|
+
form_validate_values(SessionId, Values, Result))).
|
|
928
|
+
builtin_tool_clause(
|
|
929
|
+
(tool(advisories(Values, Result),
|
|
930
|
+
"Advisory check of values (never blocks). Result is a list of issues.",
|
|
931
|
+
[object])
|
|
932
|
+
:- get_session_id(SessionId),
|
|
933
|
+
form_advisory_values(SessionId, Values, Result))).
|
|
934
|
+
builtin_tool_clause(
|
|
935
|
+
(tool(close_form(FormId, Result), "Close a form.", [string])
|
|
936
|
+
:- close_form(FormId), Result = ok)).
|
|
937
|
+
|
|
938
|
+
%% form_validate_values(+SessionId, +Values, -IssueDicts)
|
|
939
|
+
%% Runs the harness's hard rules if it defines `form_validate/2`, and normalises
|
|
940
|
+
%% whatever it returns (`error/2`, `warning/2`, or an issue dict) into issue
|
|
941
|
+
%% dicts. With no such predicate, there are no hard errors.
|
|
942
|
+
form_validate_values(SessionId, Values, IssueDicts) :-
|
|
943
|
+
( catch((call(SessionId:form_validate(Values, Issues)), is_list(Issues)), _, fail)
|
|
944
|
+
-> form_issue_dicts(Issues, IssueDicts)
|
|
945
|
+
; IssueDicts = []
|
|
946
|
+
).
|
|
947
|
+
|
|
948
|
+
%% repl_normalize_message(+Text, -TermText)
|
|
949
|
+
%% The host `call`/`/call` entry takes the request as a term *as text*. Prose is
|
|
950
|
+
%% not a term, and interpolating it into `request(From, Ref, Message)` produced a
|
|
951
|
+
%% goal the service could not parse — so it never replied and the caller waited
|
|
952
|
+
%% out the timeout. Anything that does not parse is quoted as a string; a valid
|
|
953
|
+
%% term passes through unchanged.
|
|
954
|
+
repl_normalize_message(Text, TermText) :-
|
|
955
|
+
( catch((term_string(_Term, Text, []), true), _, fail)
|
|
956
|
+
-> TermText = Text
|
|
957
|
+
; format(string(TermText), "~q", [Text])
|
|
958
|
+
).
|
|
959
|
+
|
|
960
|
+
%% form_advisory_values(+SessionId, +Values, -IssueDicts)
|
|
961
|
+
%% The advisory twin of `form_validate_values/3`: reads the harness's
|
|
962
|
+
%% `form_advisories/2` when it defines one. Never blocks anything.
|
|
963
|
+
form_advisory_values(SessionId, Values, IssueDicts) :-
|
|
964
|
+
( catch((call(SessionId:form_advisories(Values, Issues)), is_list(Issues)), _, fail)
|
|
965
|
+
-> form_issue_dicts(Issues, IssueDicts)
|
|
966
|
+
; IssueDicts = []
|
|
967
|
+
).
|
|
968
|
+
|
|
801
969
|
%% process_directive(require_sandbox, +SessionId)
|
|
802
970
|
%% The harness declares that it must run in an execution sandbox, and the load
|
|
803
971
|
%% fails if it cannot. Checked against the **effective** backend rather than
|
|
@@ -835,6 +1003,10 @@ repl_benign_directive(discontiguous(_)).
|
|
|
835
1003
|
repl_benign_directive(discontiguous(_, _)).
|
|
836
1004
|
repl_benign_directive(multifile(_)).
|
|
837
1005
|
repl_benign_directive(multifile(_, _)).
|
|
1006
|
+
%% The capability gate was removed — every host feature is always available. An
|
|
1007
|
+
%% older file that still declares one loads unchanged; the directive is ignored
|
|
1008
|
+
%% rather than reported as a mystery "unhandled directive".
|
|
1009
|
+
repl_benign_directive(capability(_)).
|
|
838
1010
|
|
|
839
1011
|
%% sandbox_kind_atom(+Raw, -Atom)
|
|
840
1012
|
%% A JS string or an atom in, an atom out.
|
|
@@ -1028,7 +1200,8 @@ create_session(SessionId, Params, HarnessMemory, MetaMemory) :-
|
|
|
1028
1200
|
),
|
|
1029
1201
|
retractall(session_sandbox_backend(SessionId, _)),
|
|
1030
1202
|
assertz(session_sandbox_backend(SessionId, SandboxKind)),
|
|
1031
|
-
repl_inject_workspace_predicates(SessionId)
|
|
1203
|
+
repl_inject_workspace_predicates(SessionId),
|
|
1204
|
+
install_builtin_tools(SessionId).
|
|
1032
1205
|
|
|
1033
1206
|
%% repl_abolish_user_predicates(+SessionId)
|
|
1034
1207
|
%% Removes only the harness's own predicates; imported builtins are untouched.
|
|
@@ -1066,7 +1239,9 @@ clear_session(SessionId) :-
|
|
|
1066
1239
|
retractall(repl_snapshot(SessionId, _)),
|
|
1067
1240
|
retractall(session_user_tools(SessionId, _)),
|
|
1068
1241
|
retractall(session_user_tool_schema(SessionId, _, _)),
|
|
1069
|
-
|
|
1242
|
+
retractall(session_builtin_tool(SessionId, _)),
|
|
1243
|
+
repl_abolish_user_predicates(SessionId),
|
|
1244
|
+
install_builtin_tools(SessionId).
|
|
1070
1245
|
|
|
1071
1246
|
%% destroy_session(+SessionId)
|
|
1072
1247
|
%% Tears a REPL session down completely: clears the module/frames and removes
|
|
@@ -1341,6 +1516,13 @@ repl_user_predicate(_SessionId, Name, _Arity) :-
|
|
|
1341
1516
|
history_predicate(Name),
|
|
1342
1517
|
!,
|
|
1343
1518
|
fail.
|
|
1519
|
+
%% Runtime-installed tools (the built-in form tools) are visible to `task/N` but
|
|
1520
|
+
%% are not part of the harness: they do not appear in `listing`, the manifest's
|
|
1521
|
+
%% predicates, the exported clauses, or the counts.
|
|
1522
|
+
repl_user_predicate(SessionId, Name, _Arity) :-
|
|
1523
|
+
session_builtin_tool(SessionId, Name),
|
|
1524
|
+
!,
|
|
1525
|
+
fail.
|
|
1344
1526
|
repl_user_predicate(SessionId, Name, Arity) :-
|
|
1345
1527
|
% User DML clauses are asserted (interpreted) into the session module.
|
|
1346
1528
|
% Exclude predicates imported via use_module (e.g. library(clpfd)) and
|
|
@@ -1353,6 +1535,7 @@ repl_user_predicate(SessionId, Name, Arity) :-
|
|
|
1353
1535
|
|
|
1354
1536
|
internal_session_predicate(param).
|
|
1355
1537
|
internal_session_predicate(param_decl).
|
|
1538
|
+
internal_session_predicate(tool).
|
|
1356
1539
|
internal_session_predicate(tool_source).
|
|
1357
1540
|
internal_session_predicate('__session__').
|
|
1358
1541
|
internal_session_predicate(Name) :- workspace_predicate_name(Name).
|
|
@@ -1509,7 +1692,14 @@ export_directives(SessionId) :-
|
|
|
1509
1692
|
).
|
|
1510
1693
|
|
|
1511
1694
|
export_tools(SessionId) :-
|
|
1512
|
-
|
|
1695
|
+
% Built-in tools are re-installed on load (install_builtin_tools/1), so they
|
|
1696
|
+
% are not written into the harness — an export carries the harness, not the
|
|
1697
|
+
% runtime.
|
|
1698
|
+
findall(Name,
|
|
1699
|
+
( session_user_tool_schema(SessionId, Name, _),
|
|
1700
|
+
\+ session_builtin_tool(SessionId, Name)
|
|
1701
|
+
),
|
|
1702
|
+
Names0),
|
|
1513
1703
|
sort(Names0, Names),
|
|
1514
1704
|
forall(member(Name, Names),
|
|
1515
1705
|
( SessionId:tool_source(Name, Source)
|
|
@@ -1551,7 +1741,11 @@ export_stats(SessionId, Clauses, Tools, Scripts) :-
|
|
|
1551
1741
|
),
|
|
1552
1742
|
PredCounts),
|
|
1553
1743
|
sum_list(PredCounts, Clauses),
|
|
1554
|
-
findall(Name,
|
|
1744
|
+
findall(Name,
|
|
1745
|
+
( session_user_tool_schema(SessionId, Name, _),
|
|
1746
|
+
\+ session_builtin_tool(SessionId, Name)
|
|
1747
|
+
),
|
|
1748
|
+
ToolNames0),
|
|
1555
1749
|
sort(ToolNames0, ToolNames),
|
|
1556
1750
|
length(ToolNames, Tools),
|
|
1557
1751
|
( current_predicate(SessionId:script_file/2)
|
|
@@ -1568,7 +1762,22 @@ export_clause(Head, Body) :-
|
|
|
1568
1762
|
%% format_tool_source(+Head, +Description, +Body, -SourceCode)
|
|
1569
1763
|
%% Source text for a tool, used by `listing(tools)` and export. Reverses the
|
|
1570
1764
|
%% load-time task/prompt rewrites so readers see `task/N`, not `task_named/3`.
|
|
1571
|
-
|
|
1765
|
+
%% Render a tool clause back to source for `listing`/`export`.
|
|
1766
|
+
format_tool_source(Head, Description, Body, SourceCode) :-
|
|
1767
|
+
format_tool_source(Head, Description, none, Body, SourceCode).
|
|
1768
|
+
|
|
1769
|
+
%% With an explicit input-type list: `tool(Head, Desc, [object]) :- Body.`
|
|
1770
|
+
format_tool_source(Head, Description, Types, Body, SourceCode) :-
|
|
1771
|
+
Types \== none,
|
|
1772
|
+
!,
|
|
1773
|
+
copy_term(Head-Description-Types-Body, NH-ND-NT-NB),
|
|
1774
|
+
numbervars(NH-ND-NT-NB, 0, _),
|
|
1775
|
+
clean_clause_body(NB, CleanBody),
|
|
1776
|
+
( CleanBody == true
|
|
1777
|
+
-> format(string(SourceCode), "tool(~q, ~q, ~q).", [NH, ND, NT])
|
|
1778
|
+
; format(string(SourceCode), "tool(~q, ~q, ~q) :-~n ~q.", [NH, ND, NT, CleanBody])
|
|
1779
|
+
).
|
|
1780
|
+
format_tool_source(Head, none, _Types, Body, SourceCode) :-
|
|
1572
1781
|
!,
|
|
1573
1782
|
copy_term(Head-Body, NH-NB),
|
|
1574
1783
|
numbervars(NH-NB, 0, _),
|
|
@@ -1577,7 +1786,7 @@ format_tool_source(Head, none, Body, SourceCode) :-
|
|
|
1577
1786
|
-> format(string(SourceCode), "tool(~q).", [NH])
|
|
1578
1787
|
; format(string(SourceCode), "tool(~q) :-~n ~q.", [NH, CleanBody])
|
|
1579
1788
|
).
|
|
1580
|
-
format_tool_source(Head, Description, Body, SourceCode) :-
|
|
1789
|
+
format_tool_source(Head, Description, _Types, Body, SourceCode) :-
|
|
1581
1790
|
copy_term(Head-Description-Body, NH-ND-NB),
|
|
1582
1791
|
numbervars(NH-ND-NB, 0, _),
|
|
1583
1792
|
clean_clause_body(NB, CleanBody),
|
|
@@ -2484,6 +2693,9 @@ tool_contract_violation(SessionId, Warning) :-
|
|
|
2484
2693
|
clause(SessionId:tool(Head), Body),
|
|
2485
2694
|
compound(Head),
|
|
2486
2695
|
Head =.. [Name|HeadArgs],
|
|
2696
|
+
% Built-in tools call host handlers (`form/2`, …), which are not session
|
|
2697
|
+
% predicates by design — they are not the author's to define.
|
|
2698
|
+
\+ session_builtin_tool(SessionId, Name),
|
|
2487
2699
|
length(HeadArgs, Arity),
|
|
2488
2700
|
tool_body_goal(Body, Call),
|
|
2489
2701
|
Call =.. [Callee|Args],
|
|
@@ -2496,12 +2708,23 @@ tool_contract_violation(SessionId, Warning) :-
|
|
|
2496
2708
|
-> format(string(Warning),
|
|
2497
2709
|
'tool ~w/~w declares a different arity than its body calls (~w/~w). A tool head and its delegate must match, with the output argument last.',
|
|
2498
2710
|
[Name, Arity, Callee, CalleeArity])
|
|
2499
|
-
; \+ ( SessionId:current_predicate(Callee/CalleeArity) )
|
|
2711
|
+
; \+ ( SessionId:current_predicate(Callee/CalleeArity) ; tool_host_predicate(Callee, CalleeArity) )
|
|
2500
2712
|
-> format(string(Warning),
|
|
2501
2713
|
'tool ~w/~w calls ~w/~w, which is not defined, so a nested agent gets "Tool not found". Define ~w/~w and delegate to it.',
|
|
2502
2714
|
[Name, Arity, Callee, CalleeArity, Callee, CalleeArity])
|
|
2503
2715
|
).
|
|
2504
2716
|
|
|
2717
|
+
%% Host handlers (service_call/3, form/2, exec/2, …) are mi_call specials, not
|
|
2718
|
+
%% session clauses. A tool that wraps one is valid even though
|
|
2719
|
+
%% current_predicate/1 reports false.
|
|
2720
|
+
tool_host_predicate(Name, Arity) :-
|
|
2721
|
+
atom(Name),
|
|
2722
|
+
Name \== (:),
|
|
2723
|
+
integer(Arity),
|
|
2724
|
+
Arity >= 0,
|
|
2725
|
+
functor(Goal, Name, Arity),
|
|
2726
|
+
is_mi_special_predicate(Goal).
|
|
2727
|
+
|
|
2505
2728
|
%% tool_body_goal(+Body, -FirstCall): a tool body must reach a call, not be empty.
|
|
2506
2729
|
tool_body_goal(Body, Call) :-
|
|
2507
2730
|
Body \== true,
|
|
@@ -2722,6 +2945,33 @@ repl_set_memory(SessionId, Slot, Messages) :-
|
|
|
2722
2945
|
assertz(session_repl_memory(SessionId, Messages))
|
|
2723
2946
|
).
|
|
2724
2947
|
|
|
2948
|
+
%% repl_note_resumed(+SessionId, +Text)
|
|
2949
|
+
%% Record that the session was restored from a saved snapshot. Added to both
|
|
2950
|
+
%% memory slots so the authoring agent and every task/N see it. Any earlier
|
|
2951
|
+
%% note is replaced, so a session resumed many times carries one line, not a
|
|
2952
|
+
%% growing stack.
|
|
2953
|
+
repl_note_resumed(SessionId, Text) :-
|
|
2954
|
+
atom_string(TextAtom, Text),
|
|
2955
|
+
Note = message{role: system, content: TextAtom},
|
|
2956
|
+
forall(member(Slot, [harness, meta]), (
|
|
2957
|
+
repl_get_memory(SessionId, Slot, Messages),
|
|
2958
|
+
exclude(is_resume_note, Messages, Kept),
|
|
2959
|
+
append(Kept, [Note], Next),
|
|
2960
|
+
repl_set_memory(SessionId, Slot, Next)
|
|
2961
|
+
)).
|
|
2962
|
+
|
|
2963
|
+
%% is_resume_note(+Message)
|
|
2964
|
+
%% True for a prior `repl_note_resumed/2` message, matched by its opening words.
|
|
2965
|
+
%% The content may be an atom (as first written) or a string (after a task/N
|
|
2966
|
+
%% round-trips the memory through the runner), so accept either.
|
|
2967
|
+
is_resume_note(message{role: system, content: Content}) :-
|
|
2968
|
+
( atom(Content)
|
|
2969
|
+
-> sub_atom(Content, 0, _, _, 'This session was reloaded')
|
|
2970
|
+
; string(Content)
|
|
2971
|
+
-> sub_string(Content, 0, _, _, "This session was reloaded")
|
|
2972
|
+
; fail
|
|
2973
|
+
).
|
|
2974
|
+
|
|
2725
2975
|
%% ---- Edit transactions ----
|
|
2726
2976
|
|
|
2727
2977
|
%% repl_snapshot_save(+SessionId)
|
|
@@ -2962,6 +3212,9 @@ process_engine_result(media(Payload), media, '', Payload) :- is_dict(Payload), !
|
|
|
2962
3212
|
process_engine_result(log(Text), log, Text, none) :- !.
|
|
2963
3213
|
process_engine_result(answer(Text), answer, Text, none) :- !.
|
|
2964
3214
|
%% Current 8-arg format (with model spec from with_model/2)
|
|
3215
|
+
%% Conversation: the same loop with `mode: chat` (no finish, ends when the model stops).
|
|
3216
|
+
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec, Mode), request_agent_loop, '',
|
|
3217
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec, mode: Mode}) :- !.
|
|
2965
3218
|
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec), request_agent_loop, '',
|
|
2966
3219
|
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec}) :- !.
|
|
2967
3220
|
%% Prompt-family 6-arg format (tool scope + model spec; no reasoning/recipe)
|
|
@@ -2983,6 +3236,20 @@ process_engine_result(request_exec(Tool, Args), request_exec, '',
|
|
|
2983
3236
|
process_engine_result(request_proc(Request), request_proc, '', Payload) :-
|
|
2984
3237
|
proc_request_payload(Request, Payload).
|
|
2985
3238
|
|
|
3239
|
+
%% Forms (docs/DML_FORMS.md). The host owns the registry; these carry the
|
|
3240
|
+
%% operation and, for `wait_form`, the change to deliver.
|
|
3241
|
+
%% Conversation inbox: the host answers with the next item (see wait_next_item/3).
|
|
3242
|
+
process_engine_result(wait_next(Accept), wait_next, '', payload{accept: Accept}) :- !.
|
|
3243
|
+
process_engine_result(request_form(Op, Spec), request_form, '', payload{op: Op, spec: Spec}) :- !.
|
|
3244
|
+
process_engine_result(request_form(update, FormId, Patch), request_form, '',
|
|
3245
|
+
payload{op: update, formId: FormId, patch: Patch}) :- !.
|
|
3246
|
+
process_engine_result(request_form(errors, FormId, Issues), request_form, '',
|
|
3247
|
+
payload{op: errors, formId: FormId, issues: Issues}) :- !.
|
|
3248
|
+
process_engine_result(request_form(close, FormId), request_form, '',
|
|
3249
|
+
payload{op: close, formId: FormId}) :- !.
|
|
3250
|
+
process_engine_result(wait_form(FormId, Since, Timeout), wait_form, '',
|
|
3251
|
+
payload{formId: FormId, since: Since, timeout: Timeout}) :- !.
|
|
3252
|
+
|
|
2986
3253
|
%% proc_request_payload(+Request, -Payload)
|
|
2987
3254
|
proc_request_payload(self, payload{op: self}) :- !.
|
|
2988
3255
|
proc_request_payload(spawn(Goal), payload{op: spawn, goal: Goal}) :- !.
|
|
@@ -3071,12 +3338,17 @@ destroy_engine(SessionId) :-
|
|
|
3071
3338
|
post_agent_result(SessionId, Success, Variables, Messages) :-
|
|
3072
3339
|
post_agent_result(SessionId, Success, Variables, Messages, none).
|
|
3073
3340
|
|
|
3074
|
-
|
|
3341
|
+
post_agent_result(SessionId, Success, Variables, Messages, FullMemory) :-
|
|
3342
|
+
post_agent_result(SessionId, Success, Variables, Messages, FullMemory, "").
|
|
3343
|
+
|
|
3344
|
+
%% post_agent_result(+SessionId, +Success, +Variables, +Messages, +FullMemory, +Error)
|
|
3075
3345
|
%% Messages is a list of message{role: Role, content: Content} dicts.
|
|
3076
3346
|
%% FullMemory optionally replaces the entire state memory, including compacted system summaries.
|
|
3077
|
-
|
|
3347
|
+
%% Error is non-empty when the turn failed before producing a result (a model
|
|
3348
|
+
%% error), so a caller can say what went wrong instead of guessing.
|
|
3349
|
+
post_agent_result(SessionId, Success, Variables, Messages, FullMemory, Error) :-
|
|
3078
3350
|
retractall(session_agent_result(SessionId, _)),
|
|
3079
|
-
assertz(session_agent_result(SessionId, result{success: Success, variables: Variables, messages: Messages, full_memory: FullMemory})),
|
|
3351
|
+
assertz(session_agent_result(SessionId, result{success: Success, variables: Variables, messages: Messages, full_memory: FullMemory, error: Error})),
|
|
3080
3352
|
post_signal_to_engine(SessionId, agent_done).
|
|
3081
3353
|
|
|
3082
3354
|
%% post_exec_result(+SessionId, +Status, +Result)
|
|
@@ -3092,6 +3364,22 @@ post_proc_result(SessionId, Key, Value) :-
|
|
|
3092
3364
|
assertz(session_proc_result(SessionId, Key, Value)),
|
|
3093
3365
|
post_signal_to_engine(SessionId, proc_done).
|
|
3094
3366
|
|
|
3367
|
+
%% post_form_result(+SessionId, +Result)
|
|
3368
|
+
%% Reply to a forms primitive (`form/2`, `form_update/2`, `form_errors/2`,
|
|
3369
|
+
%% `await_form/2,3`, `close_form/1`). Result is `ok(...)` or `error(...)`.
|
|
3370
|
+
%% post_next_result(+SessionId, +Item)
|
|
3371
|
+
%% The host answers `wait_next` with `user("...")`, `message(Term)`, or `closed`
|
|
3372
|
+
%% when there is nobody left to talk to.
|
|
3373
|
+
post_next_result(SessionId, Item) :-
|
|
3374
|
+
retractall(session_next_result(SessionId, _)),
|
|
3375
|
+
assertz(session_next_result(SessionId, Item)),
|
|
3376
|
+
post_signal_to_engine(SessionId, next_done).
|
|
3377
|
+
|
|
3378
|
+
post_form_result(SessionId, Result) :-
|
|
3379
|
+
retractall(session_form_result(SessionId, _)),
|
|
3380
|
+
assertz(session_form_result(SessionId, Result)),
|
|
3381
|
+
post_signal_to_engine(SessionId, form_done).
|
|
3382
|
+
|
|
3095
3383
|
%% post_sample_token_result(+SessionId, +Status, +TokenOrError)
|
|
3096
3384
|
post_sample_token_result(SessionId, Status, TokenOrError) :-
|
|
3097
3385
|
retractall(session_sample_token_result(SessionId, _)),
|
|
@@ -3480,9 +3768,22 @@ create_tool_engine(SessionId, ToolName, Args, ToolEngineId) :-
|
|
|
3480
3768
|
Goal = (
|
|
3481
3769
|
nb_setval(current_session_id, SessionId),
|
|
3482
3770
|
catch(
|
|
3483
|
-
(mi_call(Body, InitialState, _FinalState),
|
|
3484
|
-
|
|
3485
|
-
|
|
3771
|
+
( mi_call(Body, InitialState, _FinalState),
|
|
3772
|
+
% A tool that declares an output but leaves it unbound has no
|
|
3773
|
+
% result. `extract_tool_result_simple/4` maps that to `true`, which
|
|
3774
|
+
% is how a `tool(fs_write(Path, Content), …)` (content taken for the
|
|
3775
|
+
% output) reported success while writing nothing.
|
|
3776
|
+
( TotalArity > InputArity,
|
|
3777
|
+
last(HeadArgs, LastArg),
|
|
3778
|
+
var(LastArg)
|
|
3779
|
+
-> format(atom(NoResult),
|
|
3780
|
+
'tool ~w returned no result: the last argument of the tool head is its output. To pass another input, give the predicate a result argument (e.g. fs_write(Path, Content, ok)) and declare its types with tool/3.',
|
|
3781
|
+
[ToolNameAtom]),
|
|
3782
|
+
engine_yield(tool_no_result(NoResult))
|
|
3783
|
+
; extract_tool_result_simple(TotalArity, InputArity, HeadArgs, ToolResult),
|
|
3784
|
+
engine_yield(tool_finished(ToolResult))
|
|
3785
|
+
)
|
|
3786
|
+
),
|
|
3486
3787
|
Error,
|
|
3487
3788
|
(handle_tool_engine_error(Error, ErrResult),
|
|
3488
3789
|
engine_yield(tool_finished(ErrResult)))
|
|
@@ -3526,6 +3827,9 @@ step_tool_engine(ToolEngineId, Status, Content, Payload) :-
|
|
|
3526
3827
|
|
|
3527
3828
|
%% process_tool_engine_result(+Result, -Status, -Content, -Payload)
|
|
3528
3829
|
process_tool_engine_result(tool_finished(ToolResult), finished, '', payload{result: ToolResult}) :- !.
|
|
3830
|
+
%% A tool whose output argument was never bound: surface it as an error, not a
|
|
3831
|
+
%% silent success (see create_tool_engine/4).
|
|
3832
|
+
process_tool_engine_result(tool_no_result(Message), error, Message, none) :- !.
|
|
3529
3833
|
process_tool_engine_result(request_exec(Tool, Args), request_exec, '',
|
|
3530
3834
|
payload{toolName: Tool, args: Args}) :- !.
|
|
3531
3835
|
process_tool_engine_result(request_sample_token(Prompt, Allowed), request_sample_token, '',
|
|
@@ -3533,6 +3837,8 @@ process_tool_engine_result(request_sample_token(Prompt, Allowed), request_sample
|
|
|
3533
3837
|
process_tool_engine_result(request_llm(Messages), request_llm, '',
|
|
3534
3838
|
payload{messages: Messages}) :- !.
|
|
3535
3839
|
process_tool_engine_result(request_judgment(Payload), request_judgment, '', Payload) :- !.
|
|
3840
|
+
process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec, Mode), request_agent_loop, '',
|
|
3841
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec, mode: Mode}) :- !.
|
|
3536
3842
|
process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
|
|
3537
3843
|
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext}) :- !.
|
|
3538
3844
|
%% Legacy 5-arg format (no reasoning/recipe)
|
|
@@ -3545,6 +3851,17 @@ process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), reques
|
|
|
3545
3851
|
%% are yielded to the host exactly as in the main engine.
|
|
3546
3852
|
process_tool_engine_result(request_proc(Request), request_proc, '', Payload) :-
|
|
3547
3853
|
proc_request_payload(Request, Payload).
|
|
3854
|
+
%% Form primitives reached inside a DML tool (docs/DML_FORMS.md).
|
|
3855
|
+
process_tool_engine_result(wait_next(Accept), wait_next, '', payload{accept: Accept}) :- !.
|
|
3856
|
+
process_tool_engine_result(request_form(Op, Spec), request_form, '', payload{op: Op, spec: Spec}) :- !.
|
|
3857
|
+
process_tool_engine_result(request_form(update, FormId, Patch), request_form, '',
|
|
3858
|
+
payload{op: update, formId: FormId, patch: Patch}) :- !.
|
|
3859
|
+
process_tool_engine_result(request_form(errors, FormId, Issues), request_form, '',
|
|
3860
|
+
payload{op: errors, formId: FormId, issues: Issues}) :- !.
|
|
3861
|
+
process_tool_engine_result(request_form(close, FormId), request_form, '',
|
|
3862
|
+
payload{op: close, formId: FormId}) :- !.
|
|
3863
|
+
process_tool_engine_result(wait_form(FormId, Since, Timeout), wait_form, '',
|
|
3864
|
+
payload{formId: FormId, since: Since, timeout: Timeout}) :- !.
|
|
3548
3865
|
process_tool_engine_result(output(Text), output, Text, none) :- !.
|
|
3549
3866
|
process_tool_engine_result(log(Text), log, Text, none) :- !.
|
|
3550
3867
|
process_tool_engine_result(Other, error, Msg, none) :-
|
|
@@ -3743,6 +4060,112 @@ mi_call(prompt_named(Desc, Vars, VarNames), StateIn, StateOut) :-
|
|
|
3743
4060
|
consume_gas(StateIn, State1),
|
|
3744
4061
|
mi_call_prompt_n(Desc, Vars, VarNames, State1, StateOut).
|
|
3745
4062
|
|
|
4063
|
+
%% ============================================================
|
|
4064
|
+
%% Conversation (docs/DML_CONVERSATION.md)
|
|
4065
|
+
%% ============================================================
|
|
4066
|
+
|
|
4067
|
+
%% mi_call(input(-Message), +StateIn, -StateOut)
|
|
4068
|
+
%% Wait for the person's next message. No prompt: the embedding UI is a chat
|
|
4069
|
+
%% interface and is already asking. This is `next/1` restricted to the person —
|
|
4070
|
+
%% a worker's message that arrives first stays queued for `next/1`. FAILS when
|
|
4071
|
+
%% there is nobody to talk to, which is what ends
|
|
4072
|
+
%% `loop :- input(A), chat(A).`
|
|
4073
|
+
mi_call(input(Message), StateIn, StateIn) :-
|
|
4074
|
+
!,
|
|
4075
|
+
get_session_id(SessionId),
|
|
4076
|
+
get_depth(StateIn, Depth),
|
|
4077
|
+
wait_next_item(SessionId, user, Item),
|
|
4078
|
+
( Item = user(Message)
|
|
4079
|
+
-> add_trace_with_result(SessionId, exit, input, [""], Message, Depth)
|
|
4080
|
+
; fail
|
|
4081
|
+
).
|
|
4082
|
+
|
|
4083
|
+
%% mi_call(next(-Item), +StateIn, -StateOut)
|
|
4084
|
+
%% The next item from the conversation inbox: `user(Text)` from the person, or a
|
|
4085
|
+
%% message a worker sent to this session. Fails when nothing is left, so
|
|
4086
|
+
%% `loop :- next(Item), handle(Item), loop.` ends cleanly.
|
|
4087
|
+
mi_call(next(Item), StateIn, StateIn) :-
|
|
4088
|
+
!,
|
|
4089
|
+
get_session_id(SessionId),
|
|
4090
|
+
wait_next_item(SessionId, any, Item),
|
|
4091
|
+
Item \== closed.
|
|
4092
|
+
|
|
4093
|
+
%% wait_next_item(+SessionId, +Accept, -Item)
|
|
4094
|
+
%% One host round trip: the runner owns the inbox (it is the only place that can
|
|
4095
|
+
%% see both the person and the mailbox), and answers with the next item.
|
|
4096
|
+
wait_next_item(SessionId, Accept, Item) :-
|
|
4097
|
+
engine_yield(wait_next(Accept)),
|
|
4098
|
+
get_next_signal(SessionId, _),
|
|
4099
|
+
session_next_result(SessionId, Item),
|
|
4100
|
+
retractall(session_next_result(SessionId, _)).
|
|
4101
|
+
|
|
4102
|
+
%% mi_call(chat(+Message), +StateIn, -StateOut) / chat(+Message, -Reply)
|
|
4103
|
+
%%
|
|
4104
|
+
%% One conversational turn: `Message` is the person's message, the agent loop runs
|
|
4105
|
+
%% with the harness's tools, and the turn ends when the model stops calling tools.
|
|
4106
|
+
%% No `finish`, no `set_result` and no nudging — the reply *is* the answer
|
|
4107
|
+
%% (agent.ts `conversational`). Memory carries the conversation forward, so
|
|
4108
|
+
%% `loop :- input(A), chat(A).` is a conversation in one run.
|
|
4109
|
+
mi_call(chat(Message), StateIn, StateOut) :-
|
|
4110
|
+
!,
|
|
4111
|
+
mi_call(chat(Message, _Reply), StateIn, StateOut).
|
|
4112
|
+
|
|
4113
|
+
mi_call(chat(Message, Reply), StateIn, StateOut) :-
|
|
4114
|
+
!,
|
|
4115
|
+
consume_gas(StateIn, State0),
|
|
4116
|
+
get_params(State0, Params),
|
|
4117
|
+
interpolate_desc(Message, Params, InterpMessage),
|
|
4118
|
+
get_session_id(SessionId),
|
|
4119
|
+
get_depth(StateIn, Depth),
|
|
4120
|
+
add_trace_entry(SessionId, llm_call, chat, [InterpMessage], Depth),
|
|
4121
|
+
collect_user_tools(SessionId, UserTools),
|
|
4122
|
+
% The message is NOT added to memory here: the loop appends it as the turn's
|
|
4123
|
+
% user message, and returns the whole conversation as `full_memory`.
|
|
4124
|
+
get_memory(State0, Memory0),
|
|
4125
|
+
get_tool_scope(State0, ToolScope),
|
|
4126
|
+
get_reasoning_effort(State0, ReasoningEffort),
|
|
4127
|
+
get_recipe_context(State0, RecipeContext),
|
|
4128
|
+
get_model_spec(State0, ModelSpec),
|
|
4129
|
+
engine_yield(request_agent_loop(InterpMessage, [], UserTools, Memory0, ToolScope,
|
|
4130
|
+
ReasoningEffort, RecipeContext, ModelSpec, chat)),
|
|
4131
|
+
handle_agent_signals(SessionId, State0, StateAfterAgent),
|
|
4132
|
+
session_agent_result(SessionId, Result),
|
|
4133
|
+
retract(session_agent_result(SessionId, _)),
|
|
4134
|
+
% A conversation must survive a failed turn: say so, leave the memory as it
|
|
4135
|
+
% was (the failed turn is not part of the conversation), and let the person
|
|
4136
|
+
% type again. Failing here would end `loop :- input(A), chat(A), loop.` and
|
|
4137
|
+
% drop them out of the chat.
|
|
4138
|
+
( get_dict(success, Result, true)
|
|
4139
|
+
-> ( get_dict(full_memory, Result, FullMemory), is_list(FullMemory)
|
|
4140
|
+
-> set_memory(StateAfterAgent, FullMemory, StateOut)
|
|
4141
|
+
; set_memory(StateAfterAgent, Memory0, StateOut)
|
|
4142
|
+
),
|
|
4143
|
+
last_assistant_message(Result, Reply),
|
|
4144
|
+
add_trace_with_result(SessionId, exit, chat, [InterpMessage], Reply, Depth)
|
|
4145
|
+
; ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
|
|
4146
|
+
-> format(string(WarnMsg),
|
|
4147
|
+
'The model call failed (~w). Your message was not answered — try again.', [ErrorMsg])
|
|
4148
|
+
; WarnMsg = 'The model call failed. Your message was not answered — try again.'
|
|
4149
|
+
),
|
|
4150
|
+
engine_yield(output(WarnMsg)),
|
|
4151
|
+
Reply = "",
|
|
4152
|
+
set_memory(StateAfterAgent, Memory0, StateOut),
|
|
4153
|
+
add_trace_entry(SessionId, fail, chat, [InterpMessage], Depth)
|
|
4154
|
+
).
|
|
4155
|
+
|
|
4156
|
+
%% last_assistant_message(+AgentResult, -Text)
|
|
4157
|
+
%% The reply is also streamed to the UI; this binds it for a harness that wants it.
|
|
4158
|
+
last_assistant_message(Result, Text) :-
|
|
4159
|
+
( get_dict(full_memory, Result, Messages), is_list(Messages)
|
|
4160
|
+
-> true
|
|
4161
|
+
; Messages = []
|
|
4162
|
+
),
|
|
4163
|
+
findall(Content, member(message{role: assistant, content: Content}, Messages), Texts),
|
|
4164
|
+
( last(Texts, Text)
|
|
4165
|
+
-> true
|
|
4166
|
+
; Text = ""
|
|
4167
|
+
).
|
|
4168
|
+
|
|
3746
4169
|
%% mi_call_prompt_n(+Desc, +Vars, +VarNames, +StateIn, -StateOut)
|
|
3747
4170
|
%% Simple LLM call with empty memory and NO tools (pure text completion with variable binding)
|
|
3748
4171
|
mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
@@ -4268,7 +4691,7 @@ mi_call(spawn_opt(Goal, Pid, Options), StateIn, StateIn) :-
|
|
|
4268
4691
|
get_session_id(SessionId),
|
|
4269
4692
|
copy_term(Goal, GoalCopy),
|
|
4270
4693
|
term_string(GoalCopy, GoalText),
|
|
4271
|
-
(
|
|
4694
|
+
spawn_options_text(Options, OptionsText),
|
|
4272
4695
|
engine_yield(request_proc(spawn(GoalText, OptionsText))),
|
|
4273
4696
|
get_next_signal(SessionId, _),
|
|
4274
4697
|
session_proc_result(SessionId, spawn, SpawnResult),
|
|
@@ -4278,6 +4701,39 @@ mi_call(spawn_opt(Goal, Pid, Options), StateIn, StateIn) :-
|
|
|
4278
4701
|
; throw(error(process_spawn_failed(SpawnResult), spawn_opt/3))
|
|
4279
4702
|
).
|
|
4280
4703
|
|
|
4704
|
+
%% spawn_options_text(+Options, -Text)
|
|
4705
|
+
%% `spawn_opt/3` takes an options **dict** (`_{name: worker}`), but the obvious
|
|
4706
|
+
%% Prolog spelling is a list (`[name(worker)]`) or a single option
|
|
4707
|
+
%% (`name(worker)`). A non-dict used to be silently ignored, so the process kept
|
|
4708
|
+
%% its default label and nothing said so — the agent's `name(tstsvc)` did nothing,
|
|
4709
|
+
%% and two of this repo's own tests pass a list and believe they named a worker.
|
|
4710
|
+
spawn_options_text(Options, Text) :-
|
|
4711
|
+
( Options == []
|
|
4712
|
+
-> Text = "{}"
|
|
4713
|
+
; is_dict(Options)
|
|
4714
|
+
-> term_string(Options, Text)
|
|
4715
|
+
; ( is_list(Options) -> List = Options ; List = [Options] ),
|
|
4716
|
+
spawn_option_pairs(List, Pairs),
|
|
4717
|
+
dict_pairs(Dict, _, Pairs),
|
|
4718
|
+
term_string(Dict, Text)
|
|
4719
|
+
).
|
|
4720
|
+
|
|
4721
|
+
spawn_option_pairs([], []).
|
|
4722
|
+
spawn_option_pairs([Option|Rest], [Key-Value|Pairs]) :-
|
|
4723
|
+
spawn_option(Option, Key, Value),
|
|
4724
|
+
spawn_option_pairs(Rest, Pairs).
|
|
4725
|
+
|
|
4726
|
+
spawn_option(name(V), name, V) :- !.
|
|
4727
|
+
spawn_option(priority(V), priority, V) :- !.
|
|
4728
|
+
spawn_option(timeout(V), timeout, V) :- !.
|
|
4729
|
+
spawn_option(code(V), code, V) :- !.
|
|
4730
|
+
spawn_option(Key-V, Key, V) :- atom(Key), !.
|
|
4731
|
+
spawn_option(Option, _, _) :-
|
|
4732
|
+
format(atom(Message),
|
|
4733
|
+
'spawn_opt/3 does not understand the option ~w (use name(...), priority(...), timeout(...), code(...), or a dict)',
|
|
4734
|
+
[Option]),
|
|
4735
|
+
throw(error(domain_error(spawn_option, Option), Message)).
|
|
4736
|
+
|
|
4281
4737
|
%% mi_call(send(+Target, +Message), +StateIn, -StateOut)
|
|
4282
4738
|
mi_call(send(To, Message), StateIn, StateIn) :-
|
|
4283
4739
|
!,
|
|
@@ -4453,22 +4909,53 @@ mi_call(now_ms(Milliseconds), StateIn, StateIn) :-
|
|
|
4453
4909
|
%% mi_call(serve(+Name, +Goal), +StateIn, -StateOut)
|
|
4454
4910
|
%% Spawn a session-scoped process and register it under Name. Sugar over
|
|
4455
4911
|
%% spawn/2 + register/2; the goal decides the process's shape.
|
|
4912
|
+
%%
|
|
4913
|
+
%% The process is *labelled* with the service name. A spawn labels a process with
|
|
4914
|
+
%% its goal functor, so `serve(test_service, test_service_loop)` used to appear in
|
|
4915
|
+
%% `ps` as "test_service_loop" — the label of an anonymous loop — and a live
|
|
4916
|
+
%% service looked like something nobody could talk to. `process_info/2` keeps the
|
|
4917
|
+
%% goal, so nothing is lost.
|
|
4456
4918
|
mi_call(serve(Name, Goal), StateIn, StateIn) :-
|
|
4457
4919
|
!,
|
|
4458
4920
|
get_session_id(SessionId),
|
|
4459
4921
|
copy_term(Goal, GoalCopy),
|
|
4460
4922
|
term_string(GoalCopy, GoalText),
|
|
4461
|
-
|
|
4923
|
+
( atom(Name)
|
|
4924
|
+
-> Label = Name
|
|
4925
|
+
; atom_string(Label, Name) % a string name -> atom
|
|
4926
|
+
),
|
|
4927
|
+
% Spawn options are read as a dict (`parseSpawnOptions`), so `name:` not `name(...)`.
|
|
4928
|
+
format(string(OptionsText), "{name: ~q}", [Label]),
|
|
4929
|
+
engine_yield(request_proc(spawn(GoalText, OptionsText))),
|
|
4462
4930
|
get_next_signal(SessionId, _),
|
|
4463
4931
|
session_proc_result(SessionId, spawn, SpawnResult),
|
|
4464
4932
|
retract(session_proc_result(SessionId, spawn, _)),
|
|
4465
|
-
|
|
4933
|
+
% A failed spawn must be an error, not a silent failure: unifying with ok/1
|
|
4934
|
+
% used to fail, so `serve/2` looked like a predicate with no solutions.
|
|
4935
|
+
( SpawnResult = ok(Pid)
|
|
4936
|
+
-> true
|
|
4937
|
+
; throw(error(process_spawn_failed(SpawnResult), serve/2))
|
|
4938
|
+
),
|
|
4466
4939
|
term_string(Name, NameText),
|
|
4467
4940
|
term_string(Pid, PidText),
|
|
4468
4941
|
engine_yield(request_proc(register(NameText, PidText))),
|
|
4469
4942
|
get_next_signal(SessionId, _),
|
|
4470
|
-
session_proc_result(SessionId, register,
|
|
4471
|
-
retract(session_proc_result(SessionId, register, _))
|
|
4943
|
+
session_proc_result(SessionId, register, RegisterResult),
|
|
4944
|
+
retract(session_proc_result(SessionId, register, _)),
|
|
4945
|
+
( RegisterResult == ok
|
|
4946
|
+
-> true
|
|
4947
|
+
; throw(error(process_register_failed(RegisterResult), serve/2))
|
|
4948
|
+
).
|
|
4949
|
+
|
|
4950
|
+
%% mi_call(serve(+Name, +Goal, -Pid), +StateIn, -StateOut)
|
|
4951
|
+
%% `serve/2` registers and binds nothing; the pid is what a caller needs next (to
|
|
4952
|
+
%% `wait`, `stop` or link it), and `spawn/2` already binds one. So this is the same
|
|
4953
|
+
%% call with the pid out — not a third "options" argument (`serve/3` did not exist,
|
|
4954
|
+
%% which is a guess the argument shapes invite).
|
|
4955
|
+
mi_call(serve(Name, Goal, Pid), StateIn, StateOut) :-
|
|
4956
|
+
!,
|
|
4957
|
+
mi_call(serve(Name, Goal), StateIn, State1),
|
|
4958
|
+
mi_call(whereis(Name, Pid), State1, StateOut).
|
|
4472
4959
|
|
|
4473
4960
|
%% mi_call(service_cast(+Target, +Message), +StateIn, -StateOut)
|
|
4474
4961
|
mi_call(service_cast(Target, Message), StateIn, StateIn) :-
|
|
@@ -4865,6 +5352,111 @@ mi_call(input(Prompt, Input), StateIn, StateIn) :-
|
|
|
4865
5352
|
add_trace_with_result(SessionId, exit, input, [InterpPrompt], "", Depth)
|
|
4866
5353
|
).
|
|
4867
5354
|
|
|
5355
|
+
%% ============================================================
|
|
5356
|
+
%% Forms (docs/DML_FORMS.md)
|
|
5357
|
+
%% ============================================================
|
|
5358
|
+
|
|
5359
|
+
%% mi_call(form(+Spec, -FormId), +StateIn, -StateOut)
|
|
5360
|
+
mi_call(form(Spec, FormId), StateIn, StateIn) :-
|
|
5361
|
+
!,
|
|
5362
|
+
get_session_id(SessionId),
|
|
5363
|
+
engine_yield(request_form(open, Spec)),
|
|
5364
|
+
get_next_signal(SessionId, _),
|
|
5365
|
+
session_form_result(SessionId, Result),
|
|
5366
|
+
retract(session_form_result(SessionId, _)),
|
|
5367
|
+
( Result = ok(Dict)
|
|
5368
|
+
-> get_dict(formId, Dict, FormId)
|
|
5369
|
+
; throw(error(form_open_failed(Result), form/2))
|
|
5370
|
+
).
|
|
5371
|
+
|
|
5372
|
+
%% mi_call(form_update(+FormId, +Patch), +StateIn, -StateOut)
|
|
5373
|
+
mi_call(form_update(FormId, Patch), StateIn, StateIn) :-
|
|
5374
|
+
!,
|
|
5375
|
+
get_session_id(SessionId),
|
|
5376
|
+
engine_yield(request_form(update, FormId, Patch)),
|
|
5377
|
+
get_next_signal(SessionId, _),
|
|
5378
|
+
session_form_result(SessionId, _), retract(session_form_result(SessionId, _)).
|
|
5379
|
+
|
|
5380
|
+
%% mi_call(form_errors(+FormId, +Issues), +StateIn, -StateOut)
|
|
5381
|
+
%% Issues are `error(Path, Message)` / `warning(Path, Message)` terms.
|
|
5382
|
+
mi_call(form_errors(FormId, Issues), StateIn, StateIn) :-
|
|
5383
|
+
!,
|
|
5384
|
+
form_issue_dicts(Issues, Dicts),
|
|
5385
|
+
get_session_id(SessionId),
|
|
5386
|
+
engine_yield(request_form(errors, FormId, Dicts)),
|
|
5387
|
+
get_next_signal(SessionId, _),
|
|
5388
|
+
session_form_result(SessionId, _), retract(session_form_result(SessionId, _)).
|
|
5389
|
+
|
|
5390
|
+
%% mi_call(close_form(+FormId), +StateIn, -StateOut)
|
|
5391
|
+
mi_call(close_form(FormId), StateIn, StateIn) :-
|
|
5392
|
+
!,
|
|
5393
|
+
get_session_id(SessionId),
|
|
5394
|
+
engine_yield(request_form(close, FormId)),
|
|
5395
|
+
get_next_signal(SessionId, _),
|
|
5396
|
+
session_form_result(SessionId, _), retract(session_form_result(SessionId, _)).
|
|
5397
|
+
|
|
5398
|
+
%% mi_call(await_form(+FormId, -Change) / await_form(+FormId, +Since, -Change))
|
|
5399
|
+
%% The 2-arity form waits for the next change (the registry's cursor).
|
|
5400
|
+
%%
|
|
5401
|
+
%% A submit the host's rules reject never reaches the caller: the host returns
|
|
5402
|
+
%% `action: invalid` with the errors, and this loops — attaching them to the form
|
|
5403
|
+
%% so the user sees what is wrong, then waiting for the next attempt. So a harness
|
|
5404
|
+
%% (or a model's `wait_for_form_change`) only ever sees an action the rules
|
|
5405
|
+
%% accepted, and neither has to write the reject-and-re-show loop.
|
|
5406
|
+
mi_call(await_form(FormId, Change), StateIn, StateIn) :-
|
|
5407
|
+
!,
|
|
5408
|
+
mi_call(await_form(FormId, -1, Change), StateIn, StateIn).
|
|
5409
|
+
mi_call(await_form(FormId, Since, Change), StateIn, StateIn) :-
|
|
5410
|
+
!,
|
|
5411
|
+
get_session_id(SessionId),
|
|
5412
|
+
await_form_loop(SessionId, FormId, Since, StateIn, Change).
|
|
5413
|
+
|
|
5414
|
+
await_form_loop(SessionId, FormId, Since, StateIn, Change) :-
|
|
5415
|
+
engine_yield(wait_form(FormId, Since, 0)),
|
|
5416
|
+
get_next_signal(SessionId, _),
|
|
5417
|
+
session_form_result(SessionId, Result),
|
|
5418
|
+
retract(session_form_result(SessionId, _)),
|
|
5419
|
+
( Result = ok(Dict0)
|
|
5420
|
+
-> form_change_atom_action(Dict0, Dict),
|
|
5421
|
+
( get_dict(action, Dict, invalid)
|
|
5422
|
+
-> ( get_dict(errors, Dict, Errors)
|
|
5423
|
+
-> mi_call(form_errors(FormId, Errors), StateIn, _)
|
|
5424
|
+
; true
|
|
5425
|
+
),
|
|
5426
|
+
% Follow the registry's cursor, not the caller's original `Since`,
|
|
5427
|
+
% so the retry waits for a new attempt instead of re-reading the
|
|
5428
|
+
% submit that was just refused.
|
|
5429
|
+
await_form_loop(SessionId, FormId, -1, StateIn, Change)
|
|
5430
|
+
; Change = Dict
|
|
5431
|
+
)
|
|
5432
|
+
; throw(error(form_wait_failed(Result), await_form/3))
|
|
5433
|
+
).
|
|
5434
|
+
|
|
5435
|
+
%% form_change_atom_action(+Change0, -Change)
|
|
5436
|
+
%% The host builds the change from JSON, so `action` arrives as a *string*. DML
|
|
5437
|
+
%% compares it to an atom — `Change.action == submit` (docs/DML_FORMS.md §3) — and
|
|
5438
|
+
%% `"submit" == submit` is false, which silently skipped every action branch.
|
|
5439
|
+
%% Convert here: the atom is the structural tag, and the model-facing tools
|
|
5440
|
+
%% re-serialize the dict to JSON, where an atom is a string again.
|
|
5441
|
+
form_change_atom_action(Change0, Change) :-
|
|
5442
|
+
( get_dict(action, Change0, Action0), string(Action0)
|
|
5443
|
+
-> atom_string(Action, Action0),
|
|
5444
|
+
put_dict(action, Change0, Action, Change)
|
|
5445
|
+
; Change = Change0
|
|
5446
|
+
).
|
|
5447
|
+
|
|
5448
|
+
%% form_issue_dicts(+Issues, -Dicts)
|
|
5449
|
+
form_issue_dicts([], []).
|
|
5450
|
+
form_issue_dicts([Issue|Rest], [Dict|Dicts]) :-
|
|
5451
|
+
issue_dict(Issue, Dict),
|
|
5452
|
+
form_issue_dicts(Rest, Dicts).
|
|
5453
|
+
|
|
5454
|
+
issue_dict(error(Path, Message), issue{path: Path, level: error, message: Message}) :- !.
|
|
5455
|
+
issue_dict(warning(Path, Message), issue{path: Path, level: warning, message: Message}) :- !.
|
|
5456
|
+
issue_dict(issue{path: Path, level: Level, message: Message}, issue{path: Path, level: Level, message: Message}) :- !.
|
|
5457
|
+
%% A dict already shaped like an issue (e.g. parsed from JSON) passes through.
|
|
5458
|
+
issue_dict(Issue, Issue) :- is_dict(Issue), !.
|
|
5459
|
+
|
|
4868
5460
|
%% mi_call(yield(Text), +StateIn, -StateOut)
|
|
4869
5461
|
mi_call(yield(Text), StateIn, StateIn) :-
|
|
4870
5462
|
get_params(StateIn, Params),
|
|
@@ -4892,6 +5484,22 @@ mi_call(format(Format, Args), StateIn, StateIn) :-
|
|
|
4892
5484
|
; format(Format, Args)
|
|
4893
5485
|
).
|
|
4894
5486
|
|
|
5487
|
+
%% mi_call(write(Value), +StateIn, -StateOut)
|
|
5488
|
+
%% `write/1` had no handler, so it fell through to SWI's current_output and its
|
|
5489
|
+
%% text vanished in the REPL — only `format/2` and `writeln/1` were captured. A
|
|
5490
|
+
%% debugging `write(step)` then looks exactly like a goal that produced nothing,
|
|
5491
|
+
%% which is the worst possible signal.
|
|
5492
|
+
mi_call(write(Value), StateIn, StateIn) :-
|
|
5493
|
+
get_session_id(SessionId),
|
|
5494
|
+
( session_repl_mode(SessionId, true)
|
|
5495
|
+
-> with_output_to(string(Text), write(Value)),
|
|
5496
|
+
( Text == ""
|
|
5497
|
+
-> true
|
|
5498
|
+
; mi_call(output(Text), StateIn, StateIn)
|
|
5499
|
+
)
|
|
5500
|
+
; write(Value)
|
|
5501
|
+
).
|
|
5502
|
+
|
|
4895
5503
|
%% mi_call(writeln(Value), +StateIn, -StateOut)
|
|
4896
5504
|
mi_call(writeln(Value), StateIn, StateIn) :-
|
|
4897
5505
|
get_session_id(SessionId),
|
|
@@ -5725,7 +6333,7 @@ consult_string(Code, SessionId) :-
|
|
|
5725
6333
|
consult_clauses(Stream, SessionId) :-
|
|
5726
6334
|
read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
|
|
5727
6335
|
( Term == end_of_file
|
|
5728
|
-
->
|
|
6336
|
+
-> install_builtin_tools(SessionId)
|
|
5729
6337
|
; preprocess_loaded_term(Term, Bindings, PreparedTerms),
|
|
5730
6338
|
forall(member(PreparedTerm, PreparedTerms), consult_process_term(PreparedTerm, SessionId)),
|
|
5731
6339
|
consult_clauses(Stream, SessionId)
|
|
@@ -5983,6 +6591,7 @@ is_mi_special_predicate(sleep(_)).
|
|
|
5983
6591
|
is_mi_special_predicate(make_ref(_)).
|
|
5984
6592
|
is_mi_special_predicate(now_ms(_)).
|
|
5985
6593
|
is_mi_special_predicate(serve(_,_)).
|
|
6594
|
+
is_mi_special_predicate(serve(_,_,_)).
|
|
5986
6595
|
is_mi_special_predicate(service_cast(_,_)).
|
|
5987
6596
|
is_mi_special_predicate(service_call(_,_,_)).
|
|
5988
6597
|
is_mi_special_predicate(service_call(_,_,_,_)).
|
|
@@ -5993,6 +6602,13 @@ is_mi_special_predicate(wait(_)).
|
|
|
5993
6602
|
is_mi_special_predicate(wait(_,_)).
|
|
5994
6603
|
is_mi_special_predicate(param(_,_)).
|
|
5995
6604
|
is_mi_special_predicate(param(_,_,_)).
|
|
6605
|
+
%% Forms (docs/DML_FORMS.md)
|
|
6606
|
+
is_mi_special_predicate(form(_,_)).
|
|
6607
|
+
is_mi_special_predicate(form_update(_,_)).
|
|
6608
|
+
is_mi_special_predicate(form_errors(_,_)).
|
|
6609
|
+
is_mi_special_predicate(await_form(_,_)).
|
|
6610
|
+
is_mi_special_predicate(await_form(_,_,_)).
|
|
6611
|
+
is_mi_special_predicate(close_form(_)).
|
|
5996
6612
|
%% Prompt predicates (like task but with fresh memory)
|
|
5997
6613
|
is_mi_special_predicate(prompt(_)).
|
|
5998
6614
|
is_mi_special_predicate(prompt(_,_)).
|
|
@@ -6023,7 +6639,13 @@ is_mi_special_predicate(require_judgment(_,_)).
|
|
|
6023
6639
|
is_mi_special_predicate(get_memory(_)).
|
|
6024
6640
|
is_mi_special_predicate(format(_,_)).
|
|
6025
6641
|
is_mi_special_predicate(writeln(_)).
|
|
6642
|
+
is_mi_special_predicate(write(_)).
|
|
6026
6643
|
is_mi_special_predicate(input(_,_)).
|
|
6644
|
+
%% Conversation (docs/DML_CONVERSATION.md)
|
|
6645
|
+
is_mi_special_predicate(input(_)).
|
|
6646
|
+
is_mi_special_predicate(chat(_)).
|
|
6647
|
+
is_mi_special_predicate(chat(_,_)).
|
|
6648
|
+
is_mi_special_predicate(next(_)).
|
|
6027
6649
|
is_mi_special_predicate(call(_)).
|
|
6028
6650
|
is_mi_special_predicate(call(_,_)).
|
|
6029
6651
|
is_mi_special_predicate(call(_,_,_)).
|