deepclause-sdk 0.2.2 → 0.4.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 +86 -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 +177 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +4 -0
- package/dist/cli/run.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 +786 -40
- package/dist/repl/agent.d.ts +11 -0
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +23 -9
- 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 +4 -1
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +15 -58
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/history.d.ts +24 -1
- package/dist/repl/history.d.ts.map +1 -1
- package/dist/repl/history.js +76 -8
- package/dist/repl/history.js.map +1 -1
- package/dist/repl/index.d.ts +20 -2
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +9 -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 +60 -0
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +143 -7
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +317 -20
- 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 +69 -1
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +381 -44
- 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 +452 -18
- package/dist/system/assets/docs/CHAT_PROMPT.md +28 -0
- package/dist/system/assets/docs/DML_AUTHORING.md +8 -4
- package/dist/system/assets/docs/DML_FORMS.md +740 -0
- package/dist/system/assets/docs/DML_REFERENCE.md +239 -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 +48 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/prolog-src/deepclause_mi.pl +786 -40
|
@@ -89,6 +89,9 @@
|
|
|
89
89
|
repl_commit/3,
|
|
90
90
|
repl_last_commit_summary/2,
|
|
91
91
|
repl_execute_frame/4,
|
|
92
|
+
read_workspace_file/3,
|
|
93
|
+
workspace_read_path/3,
|
|
94
|
+
read_workspace_chars/2,
|
|
92
95
|
repl_record_meta/3,
|
|
93
96
|
repl_memory_term/3,
|
|
94
97
|
repl_get_memory/3,
|
|
@@ -106,7 +109,13 @@
|
|
|
106
109
|
create_tool_engine/4,
|
|
107
110
|
step_tool_engine/4,
|
|
108
111
|
destroy_tool_engine/1,
|
|
109
|
-
post_tool_agent_result/5
|
|
112
|
+
post_tool_agent_result/5,
|
|
113
|
+
% Forms (docs/DML_FORMS.md)
|
|
114
|
+
post_form_result/2,
|
|
115
|
+
post_next_result/2,
|
|
116
|
+
form_validate_values/3,
|
|
117
|
+
form_advisory_values/3,
|
|
118
|
+
repl_normalize_message/2
|
|
110
119
|
]).
|
|
111
120
|
|
|
112
121
|
:- use_module(deepclause_strings).
|
|
@@ -118,6 +127,7 @@
|
|
|
118
127
|
%% `proc_request_payload/2` sits between the process-primitive clauses of
|
|
119
128
|
%% `process_engine_result/4`, which is intentional.
|
|
120
129
|
:- discontiguous process_engine_result/4.
|
|
130
|
+
:- discontiguous process_directive/2.
|
|
121
131
|
|
|
122
132
|
%% Dynamic predicates for session state (engine management only, not memory!)
|
|
123
133
|
:- dynamic session_engine/2. % session_engine(SessionId, Engine) - legacy default engine handle
|
|
@@ -155,6 +165,10 @@
|
|
|
155
165
|
:- dynamic session_proc_result/3. % session_proc_result(SessionId, Key, Value) - process primitive results
|
|
156
166
|
:- dynamic session_user_input/2. % session_user_input(SessionId, Input) - for ask_user tool
|
|
157
167
|
:- dynamic session_input_cancelled/1. % session_input_cancelled(SessionId) - input/2 should fail (non-interactive)
|
|
168
|
+
:- dynamic session_form_result/2. % session_form_result(SessionId, Result) - forms primitive results
|
|
169
|
+
:- dynamic session_builtin_tool/2. % session_builtin_tool(SessionId, ToolName) - runtime-installed tools
|
|
170
|
+
:- dynamic session_next_result/2. % session_next_result(SessionId, Item) - conversation inbox item
|
|
171
|
+
:- dynamic session_resource/2. % session_resource(SessionId, Resource) - held stream/form (docs/DML_REPL_SUSPEND_RESUME.md)
|
|
158
172
|
|
|
159
173
|
%% ============================================================
|
|
160
174
|
%% DML Parsing
|
|
@@ -201,7 +215,7 @@ parse_dml_string(Code, SessionId) :-
|
|
|
201
215
|
parse_clauses(Stream, SessionId) :-
|
|
202
216
|
read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
|
|
203
217
|
( Term == end_of_file
|
|
204
|
-
->
|
|
218
|
+
-> install_builtin_tools(SessionId)
|
|
205
219
|
; preprocess_loaded_term(Term, Bindings, PreparedTerms),
|
|
206
220
|
forall(member(PreparedTerm, PreparedTerms), process_clause(PreparedTerm, SessionId)),
|
|
207
221
|
parse_clauses(Stream, SessionId)
|
|
@@ -573,6 +587,19 @@ process_clause((:- Directive), SessionId) :-
|
|
|
573
587
|
!,
|
|
574
588
|
process_directive(Directive, SessionId).
|
|
575
589
|
|
|
590
|
+
%% Handle tool/3: description plus per-input types.
|
|
591
|
+
%% tool(Head, Description, [object, number]) :- Body.
|
|
592
|
+
%% The leading args are inputs (typed from the list); the last arg is the output.
|
|
593
|
+
process_clause((tool(ToolHead, Description, Types) :- Body), SessionId) :-
|
|
594
|
+
!,
|
|
595
|
+
(catch(expand_dict_dots(Body, ExpandedBody), _, fail) -> true ; ExpandedBody = Body),
|
|
596
|
+
extract_tool_schema(ToolHead, Description, Types, ToolName, Schema),
|
|
597
|
+
assertz(session_user_tools(SessionId, ToolName)),
|
|
598
|
+
assertz(session_user_tool_schema(SessionId, ToolName, Schema)),
|
|
599
|
+
format_tool_source(ToolHead, Description, Types, Body, SourceCode),
|
|
600
|
+
assertz(SessionId:tool_source(ToolName, SourceCode)),
|
|
601
|
+
assertz(SessionId:(tool(ToolHead) :- ExpandedBody)).
|
|
602
|
+
|
|
576
603
|
%% Handle tool/2 with description: tool(Head, Description) :- Body
|
|
577
604
|
process_clause((tool(ToolHead, Description) :- Body), SessionId) :-
|
|
578
605
|
!,
|
|
@@ -693,9 +720,17 @@ expand_dict_expr(Expr, Expr).
|
|
|
693
720
|
|
|
694
721
|
%% extract_tool_schema(+ToolHead, +Description, -ToolName, -Schema)
|
|
695
722
|
extract_tool_schema(ToolHead, Description, ToolName, Schema) :-
|
|
723
|
+
extract_tool_schema(ToolHead, Description, [], ToolName, Schema).
|
|
724
|
+
|
|
725
|
+
%% extract_tool_schema(+Head, +Description, +Types, -ToolName, -Schema)
|
|
726
|
+
%% `Types` is an optional list giving the type of each input (the leading args,
|
|
727
|
+
%% in order); a missing entry defaults to `string`. This is what lets a tool take
|
|
728
|
+
%% an object, an array or a number rather than only a string.
|
|
729
|
+
extract_tool_schema(ToolHead, Description, Types, ToolName, Schema) :-
|
|
696
730
|
ToolHead =.. [ToolName|Args],
|
|
697
731
|
length(Args, Arity),
|
|
698
|
-
|
|
732
|
+
tool_type_warning(ToolName, Arity, Types),
|
|
733
|
+
extract_params(Args, 1, Arity, Types, Inputs, Outputs),
|
|
699
734
|
(Description == none -> Desc = "" ; Desc = Description),
|
|
700
735
|
Schema = schema{
|
|
701
736
|
name: ToolName,
|
|
@@ -704,19 +739,45 @@ extract_tool_schema(ToolHead, Description, ToolName, Schema) :-
|
|
|
704
739
|
outputs: Outputs
|
|
705
740
|
}.
|
|
706
741
|
|
|
707
|
-
%% extract_params(+Args, +Index, +Arity, -Inputs, -Outputs)
|
|
708
|
-
|
|
709
|
-
extract_params([
|
|
742
|
+
%% extract_params(+Args, +Index, +Arity, +Types, -Inputs, -Outputs)
|
|
743
|
+
%% The last argument is the output; the rest are inputs, typed from `Types`.
|
|
744
|
+
extract_params([], _, _, _, [], []) :- !.
|
|
745
|
+
extract_params([_Arg|Rest], Index, Arity, Types, Inputs, Outputs) :-
|
|
710
746
|
format(atom(Name), 'arg~w', [Index]),
|
|
711
|
-
Type = string,
|
|
712
747
|
NextIndex is Index + 1,
|
|
713
|
-
extract_params(Rest, NextIndex, Arity, RestInputs, RestOutputs),
|
|
714
|
-
Param = param{name: Name, type: Type},
|
|
748
|
+
extract_params(Rest, NextIndex, Arity, Types, RestInputs, RestOutputs),
|
|
715
749
|
( Index == Arity
|
|
750
|
+
-> IsOutput = true, Type = string
|
|
751
|
+
; IsOutput = false, input_type(Index, Types, Type)
|
|
752
|
+
),
|
|
753
|
+
Param = param{name: Name, type: Type},
|
|
754
|
+
( IsOutput == true
|
|
716
755
|
-> Inputs = RestInputs, Outputs = [Param|RestOutputs]
|
|
717
756
|
; Inputs = [Param|RestInputs], Outputs = RestOutputs
|
|
718
757
|
).
|
|
719
758
|
|
|
759
|
+
%% input_type(+Index, +Types, -Type): the Index-th input's declared type.
|
|
760
|
+
input_type(1, [T|_], T) :- !.
|
|
761
|
+
input_type(N, [_|Rest], T) :- N > 1, N1 is N - 1, input_type(N1, Rest, T).
|
|
762
|
+
input_type(_, [], string).
|
|
763
|
+
|
|
764
|
+
%% tool_type_warning(+Name, +Arity, +Types)
|
|
765
|
+
%% The last argument of a tool head is the output, so a type list should label
|
|
766
|
+
%% the other (Arity - 1) inputs. When it does not, the author has almost
|
|
767
|
+
%% certainly written a tool whose second input was taken for its output — the
|
|
768
|
+
%% `tool(fs_write(Path, Content), …)` shape, which silently writes nothing.
|
|
769
|
+
tool_type_warning(ToolName, Arity, Types) :-
|
|
770
|
+
is_list(Types),
|
|
771
|
+
Types \== [],
|
|
772
|
+
length(Types, Declared),
|
|
773
|
+
Inputs is Arity - 1,
|
|
774
|
+
Declared =\= Inputs,
|
|
775
|
+
!,
|
|
776
|
+
format(user_error,
|
|
777
|
+
'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',
|
|
778
|
+
[ToolName, Arity, Declared, Inputs]).
|
|
779
|
+
tool_type_warning(_, _, _).
|
|
780
|
+
|
|
720
781
|
module_reference_string(Module, ModuleStr) :-
|
|
721
782
|
atom(Module),
|
|
722
783
|
!,
|
|
@@ -769,7 +830,7 @@ process_directive(consult(File), SessionId) :-
|
|
|
769
830
|
!,
|
|
770
831
|
% Set session ID for get_session_id to find, then call our custom consult
|
|
771
832
|
nb_setval(current_session_id, SessionId),
|
|
772
|
-
|
|
833
|
+
read_workspace_file(SessionId, File, Code),
|
|
773
834
|
consult_string(Code, SessionId).
|
|
774
835
|
%% Handle use_module directive
|
|
775
836
|
process_directive(use_module(Module), SessionId) :-
|
|
@@ -802,6 +863,113 @@ process_directive(repl_delete(Name/Arity), SessionId) :-
|
|
|
802
863
|
Arity >= 0,
|
|
803
864
|
catch(abolish(SessionId:Name/Arity), _, true).
|
|
804
865
|
|
|
866
|
+
%% ============================================================
|
|
867
|
+
%% Built-in tools (docs/DML_FORMS.md)
|
|
868
|
+
%% ============================================================
|
|
869
|
+
%%
|
|
870
|
+
%% These `tool/2` wrappers are installed into every session, so a harness can
|
|
871
|
+
%% hand the model the form tools without declaring anything. They are ordinary
|
|
872
|
+
%% `tool/N` clauses: assert them through `process_clause/2` (never into a
|
|
873
|
+
%% harness's own clause set) and `task/N` offers them, `with_tools/2` scopes
|
|
874
|
+
%% them, and an export carries the source so a standalone run works.
|
|
875
|
+
%%
|
|
876
|
+
%% The gate that used to guard this — `:- capability(forms).` — was removed: it
|
|
877
|
+
%% was file-scoped (processed at load), so it was invisible to a module built
|
|
878
|
+
%% through the edit tools, and a harness could not discover it, because a gated
|
|
879
|
+
%% primitive is a handler and `current_predicate/1` reports `no` for handlers.
|
|
880
|
+
|
|
881
|
+
install_builtin_tools(SessionId) :-
|
|
882
|
+
% The marker keeps these out of the harness's own surface — `listing`, the
|
|
883
|
+
% manifest's predicates, the export, and the end_turn probes — while they
|
|
884
|
+
% stay in `session_user_tool_schema/3`, so `task/N` and `with_tools/2` offer
|
|
885
|
+
% them. They are built in, not authored.
|
|
886
|
+
forall(builtin_tool_name(Name),
|
|
887
|
+
( session_builtin_tool(SessionId, Name)
|
|
888
|
+
-> true
|
|
889
|
+
; assertz(session_builtin_tool(SessionId, Name))
|
|
890
|
+
)),
|
|
891
|
+
% Idempotent: `parse_clauses/2` runs on every load, and a duplicate schema
|
|
892
|
+
% would show the model the same tool twice.
|
|
893
|
+
( session_user_tool_schema(SessionId, render_form, _)
|
|
894
|
+
-> true
|
|
895
|
+
; forall(builtin_tool_clause(Clause), process_clause(Clause, SessionId))
|
|
896
|
+
).
|
|
897
|
+
|
|
898
|
+
builtin_tool_name(render_form).
|
|
899
|
+
builtin_tool_name(update_form).
|
|
900
|
+
builtin_tool_name(set_form_issues).
|
|
901
|
+
builtin_tool_name(wait_for_form_change).
|
|
902
|
+
builtin_tool_name(validate_form).
|
|
903
|
+
builtin_tool_name(advisories).
|
|
904
|
+
builtin_tool_name(close_form).
|
|
905
|
+
|
|
906
|
+
builtin_tool_clause(
|
|
907
|
+
(tool(render_form(Spec, FormId),
|
|
908
|
+
"Open (or replace) a form. `Spec` is the form spec (version, title, fields, actions). Returns the form id.",
|
|
909
|
+
[object])
|
|
910
|
+
:- form(Spec, FormId))).
|
|
911
|
+
builtin_tool_clause(
|
|
912
|
+
(tool(update_form(FormId, Patch, Result),
|
|
913
|
+
"Update form values with a JSON merge patch.",
|
|
914
|
+
[string, object])
|
|
915
|
+
:- form_update(FormId, Patch), Result = ok)).
|
|
916
|
+
builtin_tool_clause(
|
|
917
|
+
(tool(set_form_issues(FormId, Issues, Result),
|
|
918
|
+
"Attach per-field errors/warnings. Issues is a list of {path, level, message}.",
|
|
919
|
+
[string, array])
|
|
920
|
+
:- form_errors(FormId, Issues), Result = ok)).
|
|
921
|
+
builtin_tool_clause(
|
|
922
|
+
(tool(wait_for_form_change(FormId, Change),
|
|
923
|
+
"Wait for the user to act on a form. Change is a dict with action, values, revision and changed.",
|
|
924
|
+
[string])
|
|
925
|
+
:- await_form(FormId, Change))).
|
|
926
|
+
builtin_tool_clause(
|
|
927
|
+
(tool(validate_form(Values, Result),
|
|
928
|
+
"Hard-validate values without submitting. Result is a list of issues.",
|
|
929
|
+
[object])
|
|
930
|
+
:- get_session_id(SessionId),
|
|
931
|
+
form_validate_values(SessionId, Values, Result))).
|
|
932
|
+
builtin_tool_clause(
|
|
933
|
+
(tool(advisories(Values, Result),
|
|
934
|
+
"Advisory check of values (never blocks). Result is a list of issues.",
|
|
935
|
+
[object])
|
|
936
|
+
:- get_session_id(SessionId),
|
|
937
|
+
form_advisory_values(SessionId, Values, Result))).
|
|
938
|
+
builtin_tool_clause(
|
|
939
|
+
(tool(close_form(FormId, Result), "Close a form.", [string])
|
|
940
|
+
:- close_form(FormId), Result = ok)).
|
|
941
|
+
|
|
942
|
+
%% form_validate_values(+SessionId, +Values, -IssueDicts)
|
|
943
|
+
%% Runs the harness's hard rules if it defines `form_validate/2`, and normalises
|
|
944
|
+
%% whatever it returns (`error/2`, `warning/2`, or an issue dict) into issue
|
|
945
|
+
%% dicts. With no such predicate, there are no hard errors.
|
|
946
|
+
form_validate_values(SessionId, Values, IssueDicts) :-
|
|
947
|
+
( catch((call(SessionId:form_validate(Values, Issues)), is_list(Issues)), _, fail)
|
|
948
|
+
-> form_issue_dicts(Issues, IssueDicts)
|
|
949
|
+
; IssueDicts = []
|
|
950
|
+
).
|
|
951
|
+
|
|
952
|
+
%% repl_normalize_message(+Text, -TermText)
|
|
953
|
+
%% The host `call`/`/call` entry takes the request as a term *as text*. Prose is
|
|
954
|
+
%% not a term, and interpolating it into `request(From, Ref, Message)` produced a
|
|
955
|
+
%% goal the service could not parse — so it never replied and the caller waited
|
|
956
|
+
%% out the timeout. Anything that does not parse is quoted as a string; a valid
|
|
957
|
+
%% term passes through unchanged.
|
|
958
|
+
repl_normalize_message(Text, TermText) :-
|
|
959
|
+
( catch((term_string(_Term, Text, []), true), _, fail)
|
|
960
|
+
-> TermText = Text
|
|
961
|
+
; format(string(TermText), "~q", [Text])
|
|
962
|
+
).
|
|
963
|
+
|
|
964
|
+
%% form_advisory_values(+SessionId, +Values, -IssueDicts)
|
|
965
|
+
%% The advisory twin of `form_validate_values/3`: reads the harness's
|
|
966
|
+
%% `form_advisories/2` when it defines one. Never blocks anything.
|
|
967
|
+
form_advisory_values(SessionId, Values, IssueDicts) :-
|
|
968
|
+
( catch((call(SessionId:form_advisories(Values, Issues)), is_list(Issues)), _, fail)
|
|
969
|
+
-> form_issue_dicts(Issues, IssueDicts)
|
|
970
|
+
; IssueDicts = []
|
|
971
|
+
).
|
|
972
|
+
|
|
805
973
|
%% process_directive(require_sandbox, +SessionId)
|
|
806
974
|
%% The harness declares that it must run in an execution sandbox, and the load
|
|
807
975
|
%% fails if it cannot. Checked against the **effective** backend rather than
|
|
@@ -839,6 +1007,10 @@ repl_benign_directive(discontiguous(_)).
|
|
|
839
1007
|
repl_benign_directive(discontiguous(_, _)).
|
|
840
1008
|
repl_benign_directive(multifile(_)).
|
|
841
1009
|
repl_benign_directive(multifile(_, _)).
|
|
1010
|
+
%% The capability gate was removed — every host feature is always available. An
|
|
1011
|
+
%% older file that still declares one loads unchanged; the directive is ignored
|
|
1012
|
+
%% rather than reported as a mystery "unhandled directive".
|
|
1013
|
+
repl_benign_directive(capability(_)).
|
|
842
1014
|
|
|
843
1015
|
%% sandbox_kind_atom(+Raw, -Atom)
|
|
844
1016
|
%% A JS string or an atom in, an atom out.
|
|
@@ -878,6 +1050,7 @@ repl_unhandled_directives(SessionId, Directives) :-
|
|
|
878
1050
|
%% The engine is registered as the session's default `query` frame so that
|
|
879
1051
|
%% step_engine/4 keeps working as a thin wrapper over step_frame/4.
|
|
880
1052
|
create_engine(SessionId, _MemoryId, Args, Params, InitialMemory, Engine) :-
|
|
1053
|
+
release_stale_resources(SessionId),
|
|
881
1054
|
assertz(session_params(SessionId, Params)),
|
|
882
1055
|
determine_agent_goal(SessionId, Args, Goal),
|
|
883
1056
|
% Check if tracing is enabled and store in session state
|
|
@@ -1032,7 +1205,8 @@ create_session(SessionId, Params, HarnessMemory, MetaMemory) :-
|
|
|
1032
1205
|
),
|
|
1033
1206
|
retractall(session_sandbox_backend(SessionId, _)),
|
|
1034
1207
|
assertz(session_sandbox_backend(SessionId, SandboxKind)),
|
|
1035
|
-
repl_inject_workspace_predicates(SessionId)
|
|
1208
|
+
repl_inject_workspace_predicates(SessionId),
|
|
1209
|
+
install_builtin_tools(SessionId).
|
|
1036
1210
|
|
|
1037
1211
|
%% repl_abolish_user_predicates(+SessionId)
|
|
1038
1212
|
%% Removes only the harness's own predicates; imported builtins are untouched.
|
|
@@ -1070,7 +1244,9 @@ clear_session(SessionId) :-
|
|
|
1070
1244
|
retractall(repl_snapshot(SessionId, _)),
|
|
1071
1245
|
retractall(session_user_tools(SessionId, _)),
|
|
1072
1246
|
retractall(session_user_tool_schema(SessionId, _, _)),
|
|
1073
|
-
|
|
1247
|
+
retractall(session_builtin_tool(SessionId, _)),
|
|
1248
|
+
repl_abolish_user_predicates(SessionId),
|
|
1249
|
+
install_builtin_tools(SessionId).
|
|
1074
1250
|
|
|
1075
1251
|
%% destroy_session(+SessionId)
|
|
1076
1252
|
%% Tears a REPL session down completely: clears the module/frames and removes
|
|
@@ -1147,7 +1323,8 @@ repl_initial_state(SessionId, FrameKind, InitialState) :-
|
|
|
1147
1323
|
%% step_engine/4 and the runner can drive it.
|
|
1148
1324
|
query_start(SessionId, Text, FrameKind, InitialState, Error) :-
|
|
1149
1325
|
catch(
|
|
1150
|
-
(
|
|
1326
|
+
( release_stale_resources(SessionId),
|
|
1327
|
+
read_repl_goal(SessionId, Text, Goal, Names),
|
|
1151
1328
|
create_frame_raw(SessionId, FrameKind, 0,
|
|
1152
1329
|
deepclause_mi:repl_solve(Goal, Names, SessionId, InitialState),
|
|
1153
1330
|
FrameId),
|
|
@@ -1227,8 +1404,16 @@ repl_solve(Goal, Names, SessionId, State) :-
|
|
|
1227
1404
|
nb_setval(current_session_id, SessionId),
|
|
1228
1405
|
catch(
|
|
1229
1406
|
repl_solve_loop(Goal, Names, SessionId, State),
|
|
1230
|
-
|
|
1231
|
-
|
|
1407
|
+
Error,
|
|
1408
|
+
% `answer/1` and `suspend/1` both stop enumeration, and both have already
|
|
1409
|
+
% yielded to the host (the answer text, or the continuation). Anything else
|
|
1410
|
+
% is a real error.
|
|
1411
|
+
( Error == '$answer_commit'
|
|
1412
|
+
-> true
|
|
1413
|
+
; Error == '$suspend_commit'
|
|
1414
|
+
-> true
|
|
1415
|
+
; throw(Error)
|
|
1416
|
+
)
|
|
1232
1417
|
).
|
|
1233
1418
|
|
|
1234
1419
|
repl_solve_loop(Goal, Names, SessionId, State) :-
|
|
@@ -1345,6 +1530,13 @@ repl_user_predicate(_SessionId, Name, _Arity) :-
|
|
|
1345
1530
|
history_predicate(Name),
|
|
1346
1531
|
!,
|
|
1347
1532
|
fail.
|
|
1533
|
+
%% Runtime-installed tools (the built-in form tools) are visible to `task/N` but
|
|
1534
|
+
%% are not part of the harness: they do not appear in `listing`, the manifest's
|
|
1535
|
+
%% predicates, the exported clauses, or the counts.
|
|
1536
|
+
repl_user_predicate(SessionId, Name, _Arity) :-
|
|
1537
|
+
session_builtin_tool(SessionId, Name),
|
|
1538
|
+
!,
|
|
1539
|
+
fail.
|
|
1348
1540
|
repl_user_predicate(SessionId, Name, Arity) :-
|
|
1349
1541
|
% User DML clauses are asserted (interpreted) into the session module.
|
|
1350
1542
|
% Exclude predicates imported via use_module (e.g. library(clpfd)) and
|
|
@@ -1357,6 +1549,7 @@ repl_user_predicate(SessionId, Name, Arity) :-
|
|
|
1357
1549
|
|
|
1358
1550
|
internal_session_predicate(param).
|
|
1359
1551
|
internal_session_predicate(param_decl).
|
|
1552
|
+
internal_session_predicate(tool).
|
|
1360
1553
|
internal_session_predicate(tool_source).
|
|
1361
1554
|
internal_session_predicate('__session__').
|
|
1362
1555
|
internal_session_predicate(Name) :- workspace_predicate_name(Name).
|
|
@@ -1513,7 +1706,14 @@ export_directives(SessionId) :-
|
|
|
1513
1706
|
).
|
|
1514
1707
|
|
|
1515
1708
|
export_tools(SessionId) :-
|
|
1516
|
-
|
|
1709
|
+
% Built-in tools are re-installed on load (install_builtin_tools/1), so they
|
|
1710
|
+
% are not written into the harness — an export carries the harness, not the
|
|
1711
|
+
% runtime.
|
|
1712
|
+
findall(Name,
|
|
1713
|
+
( session_user_tool_schema(SessionId, Name, _),
|
|
1714
|
+
\+ session_builtin_tool(SessionId, Name)
|
|
1715
|
+
),
|
|
1716
|
+
Names0),
|
|
1517
1717
|
sort(Names0, Names),
|
|
1518
1718
|
forall(member(Name, Names),
|
|
1519
1719
|
( SessionId:tool_source(Name, Source)
|
|
@@ -1555,7 +1755,11 @@ export_stats(SessionId, Clauses, Tools, Scripts) :-
|
|
|
1555
1755
|
),
|
|
1556
1756
|
PredCounts),
|
|
1557
1757
|
sum_list(PredCounts, Clauses),
|
|
1558
|
-
findall(Name,
|
|
1758
|
+
findall(Name,
|
|
1759
|
+
( session_user_tool_schema(SessionId, Name, _),
|
|
1760
|
+
\+ session_builtin_tool(SessionId, Name)
|
|
1761
|
+
),
|
|
1762
|
+
ToolNames0),
|
|
1559
1763
|
sort(ToolNames0, ToolNames),
|
|
1560
1764
|
length(ToolNames, Tools),
|
|
1561
1765
|
( current_predicate(SessionId:script_file/2)
|
|
@@ -1572,7 +1776,22 @@ export_clause(Head, Body) :-
|
|
|
1572
1776
|
%% format_tool_source(+Head, +Description, +Body, -SourceCode)
|
|
1573
1777
|
%% Source text for a tool, used by `listing(tools)` and export. Reverses the
|
|
1574
1778
|
%% load-time task/prompt rewrites so readers see `task/N`, not `task_named/3`.
|
|
1575
|
-
|
|
1779
|
+
%% Render a tool clause back to source for `listing`/`export`.
|
|
1780
|
+
format_tool_source(Head, Description, Body, SourceCode) :-
|
|
1781
|
+
format_tool_source(Head, Description, none, Body, SourceCode).
|
|
1782
|
+
|
|
1783
|
+
%% With an explicit input-type list: `tool(Head, Desc, [object]) :- Body.`
|
|
1784
|
+
format_tool_source(Head, Description, Types, Body, SourceCode) :-
|
|
1785
|
+
Types \== none,
|
|
1786
|
+
!,
|
|
1787
|
+
copy_term(Head-Description-Types-Body, NH-ND-NT-NB),
|
|
1788
|
+
numbervars(NH-ND-NT-NB, 0, _),
|
|
1789
|
+
clean_clause_body(NB, CleanBody),
|
|
1790
|
+
( CleanBody == true
|
|
1791
|
+
-> format(string(SourceCode), "tool(~q, ~q, ~q).", [NH, ND, NT])
|
|
1792
|
+
; format(string(SourceCode), "tool(~q, ~q, ~q) :-~n ~q.", [NH, ND, NT, CleanBody])
|
|
1793
|
+
).
|
|
1794
|
+
format_tool_source(Head, none, _Types, Body, SourceCode) :-
|
|
1576
1795
|
!,
|
|
1577
1796
|
copy_term(Head-Body, NH-NB),
|
|
1578
1797
|
numbervars(NH-NB, 0, _),
|
|
@@ -1581,7 +1800,7 @@ format_tool_source(Head, none, Body, SourceCode) :-
|
|
|
1581
1800
|
-> format(string(SourceCode), "tool(~q).", [NH])
|
|
1582
1801
|
; format(string(SourceCode), "tool(~q) :-~n ~q.", [NH, CleanBody])
|
|
1583
1802
|
).
|
|
1584
|
-
format_tool_source(Head, Description, Body, SourceCode) :-
|
|
1803
|
+
format_tool_source(Head, Description, _Types, Body, SourceCode) :-
|
|
1585
1804
|
copy_term(Head-Description-Body, NH-ND-NB),
|
|
1586
1805
|
numbervars(NH-ND-NB, 0, _),
|
|
1587
1806
|
clean_clause_body(NB, CleanBody),
|
|
@@ -1850,6 +2069,7 @@ repl_assert_many(SessionId, ClausesText, Result) :-
|
|
|
1850
2069
|
%% have no plain indicator here (process_clause owns them).
|
|
1851
2070
|
repl_term_indicator((:- _), _) :- !, fail.
|
|
1852
2071
|
repl_term_indicator((tool(_, _) :- _), _) :- !, fail.
|
|
2072
|
+
repl_term_indicator((tool(_, _, _) :- _), _) :- !, fail.
|
|
1853
2073
|
repl_term_indicator((tool(_) :- _), _) :- !, fail.
|
|
1854
2074
|
repl_term_indicator(Term, Name/Arity) :-
|
|
1855
2075
|
repl_head_of(Term, Head),
|
|
@@ -2488,6 +2708,9 @@ tool_contract_violation(SessionId, Warning) :-
|
|
|
2488
2708
|
clause(SessionId:tool(Head), Body),
|
|
2489
2709
|
compound(Head),
|
|
2490
2710
|
Head =.. [Name|HeadArgs],
|
|
2711
|
+
% Built-in tools call host handlers (`form/2`, …), which are not session
|
|
2712
|
+
% predicates by design — they are not the author's to define.
|
|
2713
|
+
\+ session_builtin_tool(SessionId, Name),
|
|
2491
2714
|
length(HeadArgs, Arity),
|
|
2492
2715
|
tool_body_goal(Body, Call),
|
|
2493
2716
|
Call =.. [Callee|Args],
|
|
@@ -2500,12 +2723,23 @@ tool_contract_violation(SessionId, Warning) :-
|
|
|
2500
2723
|
-> format(string(Warning),
|
|
2501
2724
|
'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.',
|
|
2502
2725
|
[Name, Arity, Callee, CalleeArity])
|
|
2503
|
-
; \+ ( SessionId:current_predicate(Callee/CalleeArity) )
|
|
2726
|
+
; \+ ( SessionId:current_predicate(Callee/CalleeArity) ; tool_host_predicate(Callee, CalleeArity) )
|
|
2504
2727
|
-> format(string(Warning),
|
|
2505
2728
|
'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.',
|
|
2506
2729
|
[Name, Arity, Callee, CalleeArity, Callee, CalleeArity])
|
|
2507
2730
|
).
|
|
2508
2731
|
|
|
2732
|
+
%% Host handlers (service_call/3, form/2, exec/2, …) are mi_call specials, not
|
|
2733
|
+
%% session clauses. A tool that wraps one is valid even though
|
|
2734
|
+
%% current_predicate/1 reports false.
|
|
2735
|
+
tool_host_predicate(Name, Arity) :-
|
|
2736
|
+
atom(Name),
|
|
2737
|
+
Name \== (:),
|
|
2738
|
+
integer(Arity),
|
|
2739
|
+
Arity >= 0,
|
|
2740
|
+
functor(Goal, Name, Arity),
|
|
2741
|
+
is_mi_special_predicate(Goal).
|
|
2742
|
+
|
|
2509
2743
|
%% tool_body_goal(+Body, -FirstCall): a tool body must reach a call, not be empty.
|
|
2510
2744
|
tool_body_goal(Body, Call) :-
|
|
2511
2745
|
Body \== true,
|
|
@@ -2990,9 +3224,14 @@ step_engine(SessionId, Status, Content, Payload) :-
|
|
|
2990
3224
|
%% process_engine_result(+Result, -Status, -Content, -Payload)
|
|
2991
3225
|
process_engine_result(output(Text), output, Text, none) :- !.
|
|
2992
3226
|
process_engine_result(media(Payload), media, '', Payload) :- is_dict(Payload), !.
|
|
3227
|
+
process_engine_result(html(Payload), html, '', Payload) :- is_dict(Payload), !.
|
|
2993
3228
|
process_engine_result(log(Text), log, Text, none) :- !.
|
|
3229
|
+
process_engine_result(suspend(Continuation, Memory), suspend, '', payload{continuation: Continuation, memory: Memory}) :- !.
|
|
2994
3230
|
process_engine_result(answer(Text), answer, Text, none) :- !.
|
|
2995
3231
|
%% Current 8-arg format (with model spec from with_model/2)
|
|
3232
|
+
%% Conversation: the same loop with `mode: chat` (no finish, ends when the model stops).
|
|
3233
|
+
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec, Mode), request_agent_loop, '',
|
|
3234
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec, mode: Mode}) :- !.
|
|
2996
3235
|
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec), request_agent_loop, '',
|
|
2997
3236
|
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec}) :- !.
|
|
2998
3237
|
%% Prompt-family 6-arg format (tool scope + model spec; no reasoning/recipe)
|
|
@@ -3014,6 +3253,20 @@ process_engine_result(request_exec(Tool, Args), request_exec, '',
|
|
|
3014
3253
|
process_engine_result(request_proc(Request), request_proc, '', Payload) :-
|
|
3015
3254
|
proc_request_payload(Request, Payload).
|
|
3016
3255
|
|
|
3256
|
+
%% Forms (docs/DML_FORMS.md). The host owns the registry; these carry the
|
|
3257
|
+
%% operation and, for `wait_form`, the change to deliver.
|
|
3258
|
+
%% Conversation inbox: the host answers with the next item (see wait_next_item/3).
|
|
3259
|
+
process_engine_result(wait_next(Accept), wait_next, '', payload{accept: Accept}) :- !.
|
|
3260
|
+
process_engine_result(request_form(Op, Spec), request_form, '', payload{op: Op, spec: Spec}) :- !.
|
|
3261
|
+
process_engine_result(request_form(update, FormId, Patch), request_form, '',
|
|
3262
|
+
payload{op: update, formId: FormId, patch: Patch}) :- !.
|
|
3263
|
+
process_engine_result(request_form(errors, FormId, Issues), request_form, '',
|
|
3264
|
+
payload{op: errors, formId: FormId, issues: Issues}) :- !.
|
|
3265
|
+
process_engine_result(request_form(close, FormId), request_form, '',
|
|
3266
|
+
payload{op: close, formId: FormId}) :- !.
|
|
3267
|
+
process_engine_result(wait_form(FormId, Since, Timeout), wait_form, '',
|
|
3268
|
+
payload{formId: FormId, since: Since, timeout: Timeout}) :- !.
|
|
3269
|
+
|
|
3017
3270
|
%% proc_request_payload(+Request, -Payload)
|
|
3018
3271
|
proc_request_payload(self, payload{op: self}) :- !.
|
|
3019
3272
|
proc_request_payload(spawn(Goal), payload{op: spawn, goal: Goal}) :- !.
|
|
@@ -3102,12 +3355,17 @@ destroy_engine(SessionId) :-
|
|
|
3102
3355
|
post_agent_result(SessionId, Success, Variables, Messages) :-
|
|
3103
3356
|
post_agent_result(SessionId, Success, Variables, Messages, none).
|
|
3104
3357
|
|
|
3105
|
-
|
|
3358
|
+
post_agent_result(SessionId, Success, Variables, Messages, FullMemory) :-
|
|
3359
|
+
post_agent_result(SessionId, Success, Variables, Messages, FullMemory, "").
|
|
3360
|
+
|
|
3361
|
+
%% post_agent_result(+SessionId, +Success, +Variables, +Messages, +FullMemory, +Error)
|
|
3106
3362
|
%% Messages is a list of message{role: Role, content: Content} dicts.
|
|
3107
3363
|
%% FullMemory optionally replaces the entire state memory, including compacted system summaries.
|
|
3108
|
-
|
|
3364
|
+
%% Error is non-empty when the turn failed before producing a result (a model
|
|
3365
|
+
%% error), so a caller can say what went wrong instead of guessing.
|
|
3366
|
+
post_agent_result(SessionId, Success, Variables, Messages, FullMemory, Error) :-
|
|
3109
3367
|
retractall(session_agent_result(SessionId, _)),
|
|
3110
|
-
assertz(session_agent_result(SessionId, result{success: Success, variables: Variables, messages: Messages, full_memory: FullMemory})),
|
|
3368
|
+
assertz(session_agent_result(SessionId, result{success: Success, variables: Variables, messages: Messages, full_memory: FullMemory, error: Error})),
|
|
3111
3369
|
post_signal_to_engine(SessionId, agent_done).
|
|
3112
3370
|
|
|
3113
3371
|
%% post_exec_result(+SessionId, +Status, +Result)
|
|
@@ -3123,6 +3381,22 @@ post_proc_result(SessionId, Key, Value) :-
|
|
|
3123
3381
|
assertz(session_proc_result(SessionId, Key, Value)),
|
|
3124
3382
|
post_signal_to_engine(SessionId, proc_done).
|
|
3125
3383
|
|
|
3384
|
+
%% post_form_result(+SessionId, +Result)
|
|
3385
|
+
%% Reply to a forms primitive (`form/2`, `form_update/2`, `form_errors/2`,
|
|
3386
|
+
%% `await_form/2,3`, `close_form/1`). Result is `ok(...)` or `error(...)`.
|
|
3387
|
+
%% post_next_result(+SessionId, +Item)
|
|
3388
|
+
%% The host answers `wait_next` with `user("...")`, `message(Term)`, or `closed`
|
|
3389
|
+
%% when there is nobody left to talk to.
|
|
3390
|
+
post_next_result(SessionId, Item) :-
|
|
3391
|
+
retractall(session_next_result(SessionId, _)),
|
|
3392
|
+
assertz(session_next_result(SessionId, Item)),
|
|
3393
|
+
post_signal_to_engine(SessionId, next_done).
|
|
3394
|
+
|
|
3395
|
+
post_form_result(SessionId, Result) :-
|
|
3396
|
+
retractall(session_form_result(SessionId, _)),
|
|
3397
|
+
assertz(session_form_result(SessionId, Result)),
|
|
3398
|
+
post_signal_to_engine(SessionId, form_done).
|
|
3399
|
+
|
|
3126
3400
|
%% post_sample_token_result(+SessionId, +Status, +TokenOrError)
|
|
3127
3401
|
post_sample_token_result(SessionId, Status, TokenOrError) :-
|
|
3128
3402
|
retractall(session_sample_token_result(SessionId, _)),
|
|
@@ -3179,6 +3453,8 @@ mi(Goal, StateIn, SessionId) :-
|
|
|
3179
3453
|
Error,
|
|
3180
3454
|
( Error == '$answer_commit'
|
|
3181
3455
|
-> true % answer/1 threw to commit - success, no backtracking
|
|
3456
|
+
; Error == '$suspend_commit'
|
|
3457
|
+
-> true % suspend/1 threw to stop here; the host holds the continuation
|
|
3182
3458
|
; Error == gas_exhausted
|
|
3183
3459
|
-> engine_yield(error("Gas exhausted: execution exceeded iteration limit")),
|
|
3184
3460
|
fail
|
|
@@ -3511,9 +3787,22 @@ create_tool_engine(SessionId, ToolName, Args, ToolEngineId) :-
|
|
|
3511
3787
|
Goal = (
|
|
3512
3788
|
nb_setval(current_session_id, SessionId),
|
|
3513
3789
|
catch(
|
|
3514
|
-
(mi_call(Body, InitialState, _FinalState),
|
|
3515
|
-
|
|
3516
|
-
|
|
3790
|
+
( mi_call(Body, InitialState, _FinalState),
|
|
3791
|
+
% A tool that declares an output but leaves it unbound has no
|
|
3792
|
+
% result. `extract_tool_result_simple/4` maps that to `true`, which
|
|
3793
|
+
% is how a `tool(fs_write(Path, Content), …)` (content taken for the
|
|
3794
|
+
% output) reported success while writing nothing.
|
|
3795
|
+
( TotalArity > InputArity,
|
|
3796
|
+
last(HeadArgs, LastArg),
|
|
3797
|
+
var(LastArg)
|
|
3798
|
+
-> format(atom(NoResult),
|
|
3799
|
+
'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.',
|
|
3800
|
+
[ToolNameAtom]),
|
|
3801
|
+
engine_yield(tool_no_result(NoResult))
|
|
3802
|
+
; extract_tool_result_simple(TotalArity, InputArity, HeadArgs, ToolResult),
|
|
3803
|
+
engine_yield(tool_finished(ToolResult))
|
|
3804
|
+
)
|
|
3805
|
+
),
|
|
3517
3806
|
Error,
|
|
3518
3807
|
(handle_tool_engine_error(Error, ErrResult),
|
|
3519
3808
|
engine_yield(tool_finished(ErrResult)))
|
|
@@ -3528,6 +3817,9 @@ create_tool_engine(SessionId, ToolName, Args, ToolEngineId) :-
|
|
|
3528
3817
|
|
|
3529
3818
|
%% handle_tool_engine_error(+Error, -Result)
|
|
3530
3819
|
handle_tool_engine_error('$answer_commit', true) :- !.
|
|
3820
|
+
%% A tool body that suspends has already handed its continuation to the host; the
|
|
3821
|
+
%% tool engine stops with it rather than turning it into a tool failure.
|
|
3822
|
+
handle_tool_engine_error('$suspend_commit', suspended) :- !.
|
|
3531
3823
|
handle_tool_engine_error(Error, error{message: ErrMsg}) :-
|
|
3532
3824
|
format(atom(ErrMsg), '~w', [Error]).
|
|
3533
3825
|
|
|
@@ -3557,6 +3849,9 @@ step_tool_engine(ToolEngineId, Status, Content, Payload) :-
|
|
|
3557
3849
|
|
|
3558
3850
|
%% process_tool_engine_result(+Result, -Status, -Content, -Payload)
|
|
3559
3851
|
process_tool_engine_result(tool_finished(ToolResult), finished, '', payload{result: ToolResult}) :- !.
|
|
3852
|
+
%% A tool whose output argument was never bound: surface it as an error, not a
|
|
3853
|
+
%% silent success (see create_tool_engine/4).
|
|
3854
|
+
process_tool_engine_result(tool_no_result(Message), error, Message, none) :- !.
|
|
3560
3855
|
process_tool_engine_result(request_exec(Tool, Args), request_exec, '',
|
|
3561
3856
|
payload{toolName: Tool, args: Args}) :- !.
|
|
3562
3857
|
process_tool_engine_result(request_sample_token(Prompt, Allowed), request_sample_token, '',
|
|
@@ -3564,6 +3859,14 @@ process_tool_engine_result(request_sample_token(Prompt, Allowed), request_sample
|
|
|
3564
3859
|
process_tool_engine_result(request_llm(Messages), request_llm, '',
|
|
3565
3860
|
payload{messages: Messages}) :- !.
|
|
3566
3861
|
process_tool_engine_result(request_judgment(Payload), request_judgment, '', Payload) :- !.
|
|
3862
|
+
process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec, Mode), request_agent_loop, '',
|
|
3863
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec, mode: Mode}) :- !.
|
|
3864
|
+
%% task/N from inside a tool: includes the model spec.
|
|
3865
|
+
process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec), request_agent_loop, '',
|
|
3866
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec}) :- !.
|
|
3867
|
+
%% prompt/N from inside a tool: no tools and no memory, but carries the model spec.
|
|
3868
|
+
process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ModelSpec), request_agent_loop, '',
|
|
3869
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: none, recipeContext: "", modelSpec: ModelSpec}) :- !.
|
|
3567
3870
|
process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
|
|
3568
3871
|
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext}) :- !.
|
|
3569
3872
|
%% Legacy 5-arg format (no reasoning/recipe)
|
|
@@ -3576,6 +3879,17 @@ process_tool_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), reques
|
|
|
3576
3879
|
%% are yielded to the host exactly as in the main engine.
|
|
3577
3880
|
process_tool_engine_result(request_proc(Request), request_proc, '', Payload) :-
|
|
3578
3881
|
proc_request_payload(Request, Payload).
|
|
3882
|
+
%% Form primitives reached inside a DML tool (docs/DML_FORMS.md).
|
|
3883
|
+
process_tool_engine_result(wait_next(Accept), wait_next, '', payload{accept: Accept}) :- !.
|
|
3884
|
+
process_tool_engine_result(request_form(Op, Spec), request_form, '', payload{op: Op, spec: Spec}) :- !.
|
|
3885
|
+
process_tool_engine_result(request_form(update, FormId, Patch), request_form, '',
|
|
3886
|
+
payload{op: update, formId: FormId, patch: Patch}) :- !.
|
|
3887
|
+
process_tool_engine_result(request_form(errors, FormId, Issues), request_form, '',
|
|
3888
|
+
payload{op: errors, formId: FormId, issues: Issues}) :- !.
|
|
3889
|
+
process_tool_engine_result(request_form(close, FormId), request_form, '',
|
|
3890
|
+
payload{op: close, formId: FormId}) :- !.
|
|
3891
|
+
process_tool_engine_result(wait_form(FormId, Since, Timeout), wait_form, '',
|
|
3892
|
+
payload{formId: FormId, since: Since, timeout: Timeout}) :- !.
|
|
3579
3893
|
process_tool_engine_result(output(Text), output, Text, none) :- !.
|
|
3580
3894
|
process_tool_engine_result(log(Text), log, Text, none) :- !.
|
|
3581
3895
|
process_tool_engine_result(Other, error, Msg, none) :-
|
|
@@ -3774,6 +4088,112 @@ mi_call(prompt_named(Desc, Vars, VarNames), StateIn, StateOut) :-
|
|
|
3774
4088
|
consume_gas(StateIn, State1),
|
|
3775
4089
|
mi_call_prompt_n(Desc, Vars, VarNames, State1, StateOut).
|
|
3776
4090
|
|
|
4091
|
+
%% ============================================================
|
|
4092
|
+
%% Conversation (docs/DML_CONVERSATION.md)
|
|
4093
|
+
%% ============================================================
|
|
4094
|
+
|
|
4095
|
+
%% mi_call(input(-Message), +StateIn, -StateOut)
|
|
4096
|
+
%% Wait for the person's next message. No prompt: the embedding UI is a chat
|
|
4097
|
+
%% interface and is already asking. This is `next/1` restricted to the person —
|
|
4098
|
+
%% a worker's message that arrives first stays queued for `next/1`. FAILS when
|
|
4099
|
+
%% there is nobody to talk to, which is what ends
|
|
4100
|
+
%% `loop :- input(A), chat(A).`
|
|
4101
|
+
mi_call(input(Message), StateIn, StateIn) :-
|
|
4102
|
+
!,
|
|
4103
|
+
get_session_id(SessionId),
|
|
4104
|
+
get_depth(StateIn, Depth),
|
|
4105
|
+
wait_next_item(SessionId, user, Item),
|
|
4106
|
+
( Item = user(Message)
|
|
4107
|
+
-> add_trace_with_result(SessionId, exit, input, [""], Message, Depth)
|
|
4108
|
+
; fail
|
|
4109
|
+
).
|
|
4110
|
+
|
|
4111
|
+
%% mi_call(next(-Item), +StateIn, -StateOut)
|
|
4112
|
+
%% The next item from the conversation inbox: `user(Text)` from the person, or a
|
|
4113
|
+
%% message a worker sent to this session. Fails when nothing is left, so
|
|
4114
|
+
%% `loop :- next(Item), handle(Item), loop.` ends cleanly.
|
|
4115
|
+
mi_call(next(Item), StateIn, StateIn) :-
|
|
4116
|
+
!,
|
|
4117
|
+
get_session_id(SessionId),
|
|
4118
|
+
wait_next_item(SessionId, any, Item),
|
|
4119
|
+
Item \== closed.
|
|
4120
|
+
|
|
4121
|
+
%% wait_next_item(+SessionId, +Accept, -Item)
|
|
4122
|
+
%% One host round trip: the runner owns the inbox (it is the only place that can
|
|
4123
|
+
%% see both the person and the mailbox), and answers with the next item.
|
|
4124
|
+
wait_next_item(SessionId, Accept, Item) :-
|
|
4125
|
+
engine_yield(wait_next(Accept)),
|
|
4126
|
+
get_next_signal(SessionId, _),
|
|
4127
|
+
session_next_result(SessionId, Item),
|
|
4128
|
+
retractall(session_next_result(SessionId, _)).
|
|
4129
|
+
|
|
4130
|
+
%% mi_call(chat(+Message), +StateIn, -StateOut) / chat(+Message, -Reply)
|
|
4131
|
+
%%
|
|
4132
|
+
%% One conversational turn: `Message` is the person's message, the agent loop runs
|
|
4133
|
+
%% with the harness's tools, and the turn ends when the model stops calling tools.
|
|
4134
|
+
%% No `finish`, no `set_result` and no nudging — the reply *is* the answer
|
|
4135
|
+
%% (agent.ts `conversational`). Memory carries the conversation forward, so
|
|
4136
|
+
%% `loop :- input(A), chat(A).` is a conversation in one run.
|
|
4137
|
+
mi_call(chat(Message), StateIn, StateOut) :-
|
|
4138
|
+
!,
|
|
4139
|
+
mi_call(chat(Message, _Reply), StateIn, StateOut).
|
|
4140
|
+
|
|
4141
|
+
mi_call(chat(Message, Reply), StateIn, StateOut) :-
|
|
4142
|
+
!,
|
|
4143
|
+
consume_gas(StateIn, State0),
|
|
4144
|
+
get_params(State0, Params),
|
|
4145
|
+
interpolate_desc(Message, Params, InterpMessage),
|
|
4146
|
+
get_session_id(SessionId),
|
|
4147
|
+
get_depth(StateIn, Depth),
|
|
4148
|
+
add_trace_entry(SessionId, llm_call, chat, [InterpMessage], Depth),
|
|
4149
|
+
collect_user_tools(SessionId, UserTools),
|
|
4150
|
+
% The message is NOT added to memory here: the loop appends it as the turn's
|
|
4151
|
+
% user message, and returns the whole conversation as `full_memory`.
|
|
4152
|
+
get_memory(State0, Memory0),
|
|
4153
|
+
get_tool_scope(State0, ToolScope),
|
|
4154
|
+
get_reasoning_effort(State0, ReasoningEffort),
|
|
4155
|
+
get_recipe_context(State0, RecipeContext),
|
|
4156
|
+
get_model_spec(State0, ModelSpec),
|
|
4157
|
+
engine_yield(request_agent_loop(InterpMessage, [], UserTools, Memory0, ToolScope,
|
|
4158
|
+
ReasoningEffort, RecipeContext, ModelSpec, chat)),
|
|
4159
|
+
handle_agent_signals(SessionId, State0, StateAfterAgent),
|
|
4160
|
+
session_agent_result(SessionId, Result),
|
|
4161
|
+
retract(session_agent_result(SessionId, _)),
|
|
4162
|
+
% A conversation must survive a failed turn: say so, leave the memory as it
|
|
4163
|
+
% was (the failed turn is not part of the conversation), and let the person
|
|
4164
|
+
% type again. Failing here would end `loop :- input(A), chat(A), loop.` and
|
|
4165
|
+
% drop them out of the chat.
|
|
4166
|
+
( get_dict(success, Result, true)
|
|
4167
|
+
-> ( get_dict(full_memory, Result, FullMemory), is_list(FullMemory)
|
|
4168
|
+
-> set_memory(StateAfterAgent, FullMemory, StateOut)
|
|
4169
|
+
; set_memory(StateAfterAgent, Memory0, StateOut)
|
|
4170
|
+
),
|
|
4171
|
+
last_assistant_message(Result, Reply),
|
|
4172
|
+
add_trace_with_result(SessionId, exit, chat, [InterpMessage], Reply, Depth)
|
|
4173
|
+
; ( get_dict(error, Result, ErrorMsg), ErrorMsg \= ""
|
|
4174
|
+
-> format(string(WarnMsg),
|
|
4175
|
+
'The model call failed (~w). Your message was not answered — try again.', [ErrorMsg])
|
|
4176
|
+
; WarnMsg = 'The model call failed. Your message was not answered — try again.'
|
|
4177
|
+
),
|
|
4178
|
+
engine_yield(output(WarnMsg)),
|
|
4179
|
+
Reply = "",
|
|
4180
|
+
set_memory(StateAfterAgent, Memory0, StateOut),
|
|
4181
|
+
add_trace_entry(SessionId, fail, chat, [InterpMessage], Depth)
|
|
4182
|
+
).
|
|
4183
|
+
|
|
4184
|
+
%% last_assistant_message(+AgentResult, -Text)
|
|
4185
|
+
%% The reply is also streamed to the UI; this binds it for a harness that wants it.
|
|
4186
|
+
last_assistant_message(Result, Text) :-
|
|
4187
|
+
( get_dict(full_memory, Result, Messages), is_list(Messages)
|
|
4188
|
+
-> true
|
|
4189
|
+
; Messages = []
|
|
4190
|
+
),
|
|
4191
|
+
findall(Content, member(message{role: assistant, content: Content}, Messages), Texts),
|
|
4192
|
+
( last(Texts, Text)
|
|
4193
|
+
-> true
|
|
4194
|
+
; Text = ""
|
|
4195
|
+
).
|
|
4196
|
+
|
|
3777
4197
|
%% mi_call_prompt_n(+Desc, +Vars, +VarNames, +StateIn, -StateOut)
|
|
3778
4198
|
%% Simple LLM call with empty memory and NO tools (pure text completion with variable binding)
|
|
3779
4199
|
mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
@@ -4299,7 +4719,7 @@ mi_call(spawn_opt(Goal, Pid, Options), StateIn, StateIn) :-
|
|
|
4299
4719
|
get_session_id(SessionId),
|
|
4300
4720
|
copy_term(Goal, GoalCopy),
|
|
4301
4721
|
term_string(GoalCopy, GoalText),
|
|
4302
|
-
(
|
|
4722
|
+
spawn_options_text(Options, OptionsText),
|
|
4303
4723
|
engine_yield(request_proc(spawn(GoalText, OptionsText))),
|
|
4304
4724
|
get_next_signal(SessionId, _),
|
|
4305
4725
|
session_proc_result(SessionId, spawn, SpawnResult),
|
|
@@ -4309,6 +4729,39 @@ mi_call(spawn_opt(Goal, Pid, Options), StateIn, StateIn) :-
|
|
|
4309
4729
|
; throw(error(process_spawn_failed(SpawnResult), spawn_opt/3))
|
|
4310
4730
|
).
|
|
4311
4731
|
|
|
4732
|
+
%% spawn_options_text(+Options, -Text)
|
|
4733
|
+
%% `spawn_opt/3` takes an options **dict** (`_{name: worker}`), but the obvious
|
|
4734
|
+
%% Prolog spelling is a list (`[name(worker)]`) or a single option
|
|
4735
|
+
%% (`name(worker)`). A non-dict used to be silently ignored, so the process kept
|
|
4736
|
+
%% its default label and nothing said so — the agent's `name(tstsvc)` did nothing,
|
|
4737
|
+
%% and two of this repo's own tests pass a list and believe they named a worker.
|
|
4738
|
+
spawn_options_text(Options, Text) :-
|
|
4739
|
+
( Options == []
|
|
4740
|
+
-> Text = "{}"
|
|
4741
|
+
; is_dict(Options)
|
|
4742
|
+
-> term_string(Options, Text)
|
|
4743
|
+
; ( is_list(Options) -> List = Options ; List = [Options] ),
|
|
4744
|
+
spawn_option_pairs(List, Pairs),
|
|
4745
|
+
dict_pairs(Dict, _, Pairs),
|
|
4746
|
+
term_string(Dict, Text)
|
|
4747
|
+
).
|
|
4748
|
+
|
|
4749
|
+
spawn_option_pairs([], []).
|
|
4750
|
+
spawn_option_pairs([Option|Rest], [Key-Value|Pairs]) :-
|
|
4751
|
+
spawn_option(Option, Key, Value),
|
|
4752
|
+
spawn_option_pairs(Rest, Pairs).
|
|
4753
|
+
|
|
4754
|
+
spawn_option(name(V), name, V) :- !.
|
|
4755
|
+
spawn_option(priority(V), priority, V) :- !.
|
|
4756
|
+
spawn_option(timeout(V), timeout, V) :- !.
|
|
4757
|
+
spawn_option(code(V), code, V) :- !.
|
|
4758
|
+
spawn_option(Key-V, Key, V) :- atom(Key), !.
|
|
4759
|
+
spawn_option(Option, _, _) :-
|
|
4760
|
+
format(atom(Message),
|
|
4761
|
+
'spawn_opt/3 does not understand the option ~w (use name(...), priority(...), timeout(...), code(...), or a dict)',
|
|
4762
|
+
[Option]),
|
|
4763
|
+
throw(error(domain_error(spawn_option, Option), Message)).
|
|
4764
|
+
|
|
4312
4765
|
%% mi_call(send(+Target, +Message), +StateIn, -StateOut)
|
|
4313
4766
|
mi_call(send(To, Message), StateIn, StateIn) :-
|
|
4314
4767
|
!,
|
|
@@ -4484,22 +4937,53 @@ mi_call(now_ms(Milliseconds), StateIn, StateIn) :-
|
|
|
4484
4937
|
%% mi_call(serve(+Name, +Goal), +StateIn, -StateOut)
|
|
4485
4938
|
%% Spawn a session-scoped process and register it under Name. Sugar over
|
|
4486
4939
|
%% spawn/2 + register/2; the goal decides the process's shape.
|
|
4940
|
+
%%
|
|
4941
|
+
%% The process is *labelled* with the service name. A spawn labels a process with
|
|
4942
|
+
%% its goal functor, so `serve(test_service, test_service_loop)` used to appear in
|
|
4943
|
+
%% `ps` as "test_service_loop" — the label of an anonymous loop — and a live
|
|
4944
|
+
%% service looked like something nobody could talk to. `process_info/2` keeps the
|
|
4945
|
+
%% goal, so nothing is lost.
|
|
4487
4946
|
mi_call(serve(Name, Goal), StateIn, StateIn) :-
|
|
4488
4947
|
!,
|
|
4489
4948
|
get_session_id(SessionId),
|
|
4490
4949
|
copy_term(Goal, GoalCopy),
|
|
4491
4950
|
term_string(GoalCopy, GoalText),
|
|
4492
|
-
|
|
4951
|
+
( atom(Name)
|
|
4952
|
+
-> Label = Name
|
|
4953
|
+
; atom_string(Label, Name) % a string name -> atom
|
|
4954
|
+
),
|
|
4955
|
+
% Spawn options are read as a dict (`parseSpawnOptions`), so `name:` not `name(...)`.
|
|
4956
|
+
format(string(OptionsText), "{name: ~q}", [Label]),
|
|
4957
|
+
engine_yield(request_proc(spawn(GoalText, OptionsText))),
|
|
4493
4958
|
get_next_signal(SessionId, _),
|
|
4494
4959
|
session_proc_result(SessionId, spawn, SpawnResult),
|
|
4495
4960
|
retract(session_proc_result(SessionId, spawn, _)),
|
|
4496
|
-
|
|
4961
|
+
% A failed spawn must be an error, not a silent failure: unifying with ok/1
|
|
4962
|
+
% used to fail, so `serve/2` looked like a predicate with no solutions.
|
|
4963
|
+
( SpawnResult = ok(Pid)
|
|
4964
|
+
-> true
|
|
4965
|
+
; throw(error(process_spawn_failed(SpawnResult), serve/2))
|
|
4966
|
+
),
|
|
4497
4967
|
term_string(Name, NameText),
|
|
4498
4968
|
term_string(Pid, PidText),
|
|
4499
4969
|
engine_yield(request_proc(register(NameText, PidText))),
|
|
4500
4970
|
get_next_signal(SessionId, _),
|
|
4501
|
-
session_proc_result(SessionId, register,
|
|
4502
|
-
retract(session_proc_result(SessionId, register, _))
|
|
4971
|
+
session_proc_result(SessionId, register, RegisterResult),
|
|
4972
|
+
retract(session_proc_result(SessionId, register, _)),
|
|
4973
|
+
( RegisterResult == ok
|
|
4974
|
+
-> true
|
|
4975
|
+
; throw(error(process_register_failed(RegisterResult), serve/2))
|
|
4976
|
+
).
|
|
4977
|
+
|
|
4978
|
+
%% mi_call(serve(+Name, +Goal, -Pid), +StateIn, -StateOut)
|
|
4979
|
+
%% `serve/2` registers and binds nothing; the pid is what a caller needs next (to
|
|
4980
|
+
%% `wait`, `stop` or link it), and `spawn/2` already binds one. So this is the same
|
|
4981
|
+
%% call with the pid out — not a third "options" argument (`serve/3` did not exist,
|
|
4982
|
+
%% which is a guess the argument shapes invite).
|
|
4983
|
+
mi_call(serve(Name, Goal, Pid), StateIn, StateOut) :-
|
|
4984
|
+
!,
|
|
4985
|
+
mi_call(serve(Name, Goal), StateIn, State1),
|
|
4986
|
+
mi_call(whereis(Name, Pid), State1, StateOut).
|
|
4503
4987
|
|
|
4504
4988
|
%% mi_call(service_cast(+Target, +Message), +StateIn, -StateOut)
|
|
4505
4989
|
mi_call(service_cast(Target, Message), StateIn, StateIn) :-
|
|
@@ -4842,6 +5326,61 @@ mi_call(answer(Text), StateIn, StateIn) :-
|
|
|
4842
5326
|
engine_yield(answer(InterpText)),
|
|
4843
5327
|
throw('$answer_commit').
|
|
4844
5328
|
|
|
5329
|
+
%% mi_call(suspend(+Continuation), +StateIn, -StateOut)
|
|
5330
|
+
%% Stop the turn at a **safe point** and let the host persist a resumable
|
|
5331
|
+
%% continuation (docs/DML_REPL_SUSPEND_RESUME.md).
|
|
5332
|
+
%%
|
|
5333
|
+
%% `Continuation` is the goal that resumes where this left off — the entry point
|
|
5334
|
+
%% of the loop whose state is the argument, e.g.
|
|
5335
|
+
%%
|
|
5336
|
+
%% loop(State) :- ..., checkpoint(State), suspend(loop(State)).
|
|
5337
|
+
%%
|
|
5338
|
+
%% A turn is suspended *between* host calls, never inside one: a model call, a
|
|
5339
|
+
%% shell command or a form wait has already handed control to the host by the time
|
|
5340
|
+
%% this runs. Non-serializable state it did not hand over — an open stream, an open
|
|
5341
|
+
%% form — makes it refuse, so a resumed session can never inherit a half-written
|
|
5342
|
+
%% file.
|
|
5343
|
+
mi_call(suspend(Continuation), StateIn, StateIn) :-
|
|
5344
|
+
!,
|
|
5345
|
+
get_session_id(SessionId),
|
|
5346
|
+
( session_resource(SessionId, Resource)
|
|
5347
|
+
-> format(atom(Message),
|
|
5348
|
+
'cannot suspend while holding ~w: close it first', [Resource]),
|
|
5349
|
+
throw(error(suspend_refused(Resource), Message))
|
|
5350
|
+
; true
|
|
5351
|
+
),
|
|
5352
|
+
term_string(Continuation, ContinuationText),
|
|
5353
|
+
get_memory(StateIn, Memory),
|
|
5354
|
+
engine_yield(suspend(ContinuationText, Memory)),
|
|
5355
|
+
throw('$suspend_commit').
|
|
5356
|
+
|
|
5357
|
+
%% release_stale_resources(+SessionId)
|
|
5358
|
+
%% A turn that was aborted while holding a stream leaves the handle open and the
|
|
5359
|
+
%% ledger non-empty, which would block every later `suspend/1`. Runs start clean:
|
|
5360
|
+
%% close whatever the previous turn left open and forget it. (A suspension is a
|
|
5361
|
+
%% clean stop, so it never leaves anything behind — `suspend/1` refuses while the
|
|
5362
|
+
%% ledger is non-empty.)
|
|
5363
|
+
release_stale_resources(SessionId) :-
|
|
5364
|
+
forall(session_resource(SessionId, Resource),
|
|
5365
|
+
( Resource = file(_, Stream)
|
|
5366
|
+
-> catch(close(Stream), _, true)
|
|
5367
|
+
; true
|
|
5368
|
+
)),
|
|
5369
|
+
retractall(session_resource(SessionId, _)).
|
|
5370
|
+
|
|
5371
|
+
%% acquire_resource(+Resource) / release_resources(+Pattern)
|
|
5372
|
+
%% What a turn holds that a snapshot cannot: a stream or an open form.
|
|
5373
|
+
acquire_resource(Resource) :-
|
|
5374
|
+
get_session_id(SessionId),
|
|
5375
|
+
( session_resource(SessionId, Resource)
|
|
5376
|
+
-> true
|
|
5377
|
+
; assertz(session_resource(SessionId, Resource))
|
|
5378
|
+
).
|
|
5379
|
+
|
|
5380
|
+
release_resources(Pattern) :-
|
|
5381
|
+
get_session_id(SessionId),
|
|
5382
|
+
retractall(session_resource(SessionId, Pattern)).
|
|
5383
|
+
|
|
4845
5384
|
%% mi_call(output(Media), +StateIn, -StateOut)
|
|
4846
5385
|
%% Present an image instead of text: `output(media(Path, Mime))`, `output(media(Path))`,
|
|
4847
5386
|
%% or a dict form (`media{svg: Svg}`, `media{data: Base64, mime: Mime}`). The host
|
|
@@ -4865,6 +5404,22 @@ mi_call(output(Media), StateIn, StateIn) :-
|
|
|
4865
5404
|
add_trace_entry(SessionId, media, media, [Path], Depth),
|
|
4866
5405
|
engine_yield(media(media{path: Path, mimeType: Mime, kind: Kind, data: Data})).
|
|
4867
5406
|
|
|
5407
|
+
%% mi_call(output(html(Path)), +StateIn, -StateOut)
|
|
5408
|
+
%% Present a workspace HTML file as an interactive miniapp.
|
|
5409
|
+
mi_call(output(html(Path)), StateIn, StateIn) :-
|
|
5410
|
+
nonvar(Path),
|
|
5411
|
+
( string(Path)
|
|
5412
|
+
-> HtmlPath = Path
|
|
5413
|
+
; atom(Path)
|
|
5414
|
+
-> atom_string(Path, HtmlPath)
|
|
5415
|
+
; fail
|
|
5416
|
+
),
|
|
5417
|
+
get_session_id(SessionId),
|
|
5418
|
+
get_depth(StateIn, Depth),
|
|
5419
|
+
add_trace_entry(SessionId, html, html, [HtmlPath], Depth),
|
|
5420
|
+
!,
|
|
5421
|
+
engine_yield(html(html{path: HtmlPath})).
|
|
5422
|
+
|
|
4868
5423
|
%% mi_call(output(Text), +StateIn, -StateOut)
|
|
4869
5424
|
mi_call(output(Text), StateIn, StateIn) :-
|
|
4870
5425
|
get_params(StateIn, Params),
|
|
@@ -4896,6 +5451,115 @@ mi_call(input(Prompt, Input), StateIn, StateIn) :-
|
|
|
4896
5451
|
add_trace_with_result(SessionId, exit, input, [InterpPrompt], "", Depth)
|
|
4897
5452
|
).
|
|
4898
5453
|
|
|
5454
|
+
%% ============================================================
|
|
5455
|
+
%% Forms (docs/DML_FORMS.md)
|
|
5456
|
+
%% ============================================================
|
|
5457
|
+
|
|
5458
|
+
%% mi_call(form(+Spec, -FormId), +StateIn, -StateOut)
|
|
5459
|
+
mi_call(form(Spec, FormId), StateIn, StateIn) :-
|
|
5460
|
+
!,
|
|
5461
|
+
get_session_id(SessionId),
|
|
5462
|
+
engine_yield(request_form(open, Spec)),
|
|
5463
|
+
get_next_signal(SessionId, _),
|
|
5464
|
+
session_form_result(SessionId, Result),
|
|
5465
|
+
retract(session_form_result(SessionId, _)),
|
|
5466
|
+
( Result = ok(Dict)
|
|
5467
|
+
-> get_dict(formId, Dict, FormId),
|
|
5468
|
+
% An open form is host state a snapshot cannot carry, so `suspend/1`
|
|
5469
|
+
% refuses until it is closed (docs/DML_REPL_SUSPEND_RESUME.md).
|
|
5470
|
+
acquire_resource(form(FormId))
|
|
5471
|
+
; throw(error(form_open_failed(Result), form/2))
|
|
5472
|
+
).
|
|
5473
|
+
|
|
5474
|
+
%% mi_call(form_update(+FormId, +Patch), +StateIn, -StateOut)
|
|
5475
|
+
mi_call(form_update(FormId, Patch), StateIn, StateIn) :-
|
|
5476
|
+
!,
|
|
5477
|
+
get_session_id(SessionId),
|
|
5478
|
+
engine_yield(request_form(update, FormId, Patch)),
|
|
5479
|
+
get_next_signal(SessionId, _),
|
|
5480
|
+
session_form_result(SessionId, _), retract(session_form_result(SessionId, _)).
|
|
5481
|
+
|
|
5482
|
+
%% mi_call(form_errors(+FormId, +Issues), +StateIn, -StateOut)
|
|
5483
|
+
%% Issues are `error(Path, Message)` / `warning(Path, Message)` terms.
|
|
5484
|
+
mi_call(form_errors(FormId, Issues), StateIn, StateIn) :-
|
|
5485
|
+
!,
|
|
5486
|
+
form_issue_dicts(Issues, Dicts),
|
|
5487
|
+
get_session_id(SessionId),
|
|
5488
|
+
engine_yield(request_form(errors, FormId, Dicts)),
|
|
5489
|
+
get_next_signal(SessionId, _),
|
|
5490
|
+
session_form_result(SessionId, _), retract(session_form_result(SessionId, _)).
|
|
5491
|
+
|
|
5492
|
+
%% mi_call(close_form(+FormId), +StateIn, -StateOut)
|
|
5493
|
+
mi_call(close_form(FormId), StateIn, StateIn) :-
|
|
5494
|
+
!,
|
|
5495
|
+
release_resources(form(FormId)),
|
|
5496
|
+
get_session_id(SessionId),
|
|
5497
|
+
engine_yield(request_form(close, FormId)),
|
|
5498
|
+
get_next_signal(SessionId, _),
|
|
5499
|
+
session_form_result(SessionId, _), retract(session_form_result(SessionId, _)).
|
|
5500
|
+
|
|
5501
|
+
%% mi_call(await_form(+FormId, -Change) / await_form(+FormId, +Since, -Change))
|
|
5502
|
+
%% The 2-arity form waits for the next change (the registry's cursor).
|
|
5503
|
+
%%
|
|
5504
|
+
%% A submit the host's rules reject never reaches the caller: the host returns
|
|
5505
|
+
%% `action: invalid` with the errors, and this loops — attaching them to the form
|
|
5506
|
+
%% so the user sees what is wrong, then waiting for the next attempt. So a harness
|
|
5507
|
+
%% (or a model's `wait_for_form_change`) only ever sees an action the rules
|
|
5508
|
+
%% accepted, and neither has to write the reject-and-re-show loop.
|
|
5509
|
+
mi_call(await_form(FormId, Change), StateIn, StateIn) :-
|
|
5510
|
+
!,
|
|
5511
|
+
mi_call(await_form(FormId, -1, Change), StateIn, StateIn).
|
|
5512
|
+
mi_call(await_form(FormId, Since, Change), StateIn, StateIn) :-
|
|
5513
|
+
!,
|
|
5514
|
+
get_session_id(SessionId),
|
|
5515
|
+
await_form_loop(SessionId, FormId, Since, StateIn, Change).
|
|
5516
|
+
|
|
5517
|
+
await_form_loop(SessionId, FormId, Since, StateIn, Change) :-
|
|
5518
|
+
engine_yield(wait_form(FormId, Since, 0)),
|
|
5519
|
+
get_next_signal(SessionId, _),
|
|
5520
|
+
session_form_result(SessionId, Result),
|
|
5521
|
+
retract(session_form_result(SessionId, _)),
|
|
5522
|
+
( Result = ok(Dict0)
|
|
5523
|
+
-> form_change_atom_action(Dict0, Dict),
|
|
5524
|
+
( get_dict(action, Dict, invalid)
|
|
5525
|
+
-> ( get_dict(errors, Dict, Errors)
|
|
5526
|
+
-> mi_call(form_errors(FormId, Errors), StateIn, _)
|
|
5527
|
+
; true
|
|
5528
|
+
),
|
|
5529
|
+
% Follow the registry's cursor, not the caller's original `Since`,
|
|
5530
|
+
% so the retry waits for a new attempt instead of re-reading the
|
|
5531
|
+
% submit that was just refused.
|
|
5532
|
+
await_form_loop(SessionId, FormId, -1, StateIn, Change)
|
|
5533
|
+
; Change = Dict
|
|
5534
|
+
)
|
|
5535
|
+
; throw(error(form_wait_failed(Result), await_form/3))
|
|
5536
|
+
).
|
|
5537
|
+
|
|
5538
|
+
%% form_change_atom_action(+Change0, -Change)
|
|
5539
|
+
%% The host builds the change from JSON, so `action` arrives as a *string*. DML
|
|
5540
|
+
%% compares it to an atom — `Change.action == submit` (docs/DML_FORMS.md §3) — and
|
|
5541
|
+
%% `"submit" == submit` is false, which silently skipped every action branch.
|
|
5542
|
+
%% Convert here: the atom is the structural tag, and the model-facing tools
|
|
5543
|
+
%% re-serialize the dict to JSON, where an atom is a string again.
|
|
5544
|
+
form_change_atom_action(Change0, Change) :-
|
|
5545
|
+
( get_dict(action, Change0, Action0), string(Action0)
|
|
5546
|
+
-> atom_string(Action, Action0),
|
|
5547
|
+
put_dict(action, Change0, Action, Change)
|
|
5548
|
+
; Change = Change0
|
|
5549
|
+
).
|
|
5550
|
+
|
|
5551
|
+
%% form_issue_dicts(+Issues, -Dicts)
|
|
5552
|
+
form_issue_dicts([], []).
|
|
5553
|
+
form_issue_dicts([Issue|Rest], [Dict|Dicts]) :-
|
|
5554
|
+
issue_dict(Issue, Dict),
|
|
5555
|
+
form_issue_dicts(Rest, Dicts).
|
|
5556
|
+
|
|
5557
|
+
issue_dict(error(Path, Message), issue{path: Path, level: error, message: Message}) :- !.
|
|
5558
|
+
issue_dict(warning(Path, Message), issue{path: Path, level: warning, message: Message}) :- !.
|
|
5559
|
+
issue_dict(issue{path: Path, level: Level, message: Message}, issue{path: Path, level: Level, message: Message}) :- !.
|
|
5560
|
+
%% A dict already shaped like an issue (e.g. parsed from JSON) passes through.
|
|
5561
|
+
issue_dict(Issue, Issue) :- is_dict(Issue), !.
|
|
5562
|
+
|
|
4899
5563
|
%% mi_call(yield(Text), +StateIn, -StateOut)
|
|
4900
5564
|
mi_call(yield(Text), StateIn, StateIn) :-
|
|
4901
5565
|
get_params(StateIn, Params),
|
|
@@ -4923,6 +5587,22 @@ mi_call(format(Format, Args), StateIn, StateIn) :-
|
|
|
4923
5587
|
; format(Format, Args)
|
|
4924
5588
|
).
|
|
4925
5589
|
|
|
5590
|
+
%% mi_call(write(Value), +StateIn, -StateOut)
|
|
5591
|
+
%% `write/1` had no handler, so it fell through to SWI's current_output and its
|
|
5592
|
+
%% text vanished in the REPL — only `format/2` and `writeln/1` were captured. A
|
|
5593
|
+
%% debugging `write(step)` then looks exactly like a goal that produced nothing,
|
|
5594
|
+
%% which is the worst possible signal.
|
|
5595
|
+
mi_call(write(Value), StateIn, StateIn) :-
|
|
5596
|
+
get_session_id(SessionId),
|
|
5597
|
+
( session_repl_mode(SessionId, true)
|
|
5598
|
+
-> with_output_to(string(Text), write(Value)),
|
|
5599
|
+
( Text == ""
|
|
5600
|
+
-> true
|
|
5601
|
+
; mi_call(output(Text), StateIn, StateIn)
|
|
5602
|
+
)
|
|
5603
|
+
; write(Value)
|
|
5604
|
+
).
|
|
5605
|
+
|
|
4926
5606
|
%% mi_call(writeln(Value), +StateIn, -StateOut)
|
|
4927
5607
|
mi_call(writeln(Value), StateIn, StateIn) :-
|
|
4928
5608
|
get_session_id(SessionId),
|
|
@@ -5071,6 +5751,10 @@ repl_reserved_clause((tool(ToolHead, _) :- _)) :-
|
|
|
5071
5751
|
!,
|
|
5072
5752
|
functor(ToolHead, Name, _),
|
|
5073
5753
|
repl_reserved_name(Name).
|
|
5754
|
+
repl_reserved_clause((tool(ToolHead, _, _) :- _)) :-
|
|
5755
|
+
!,
|
|
5756
|
+
functor(ToolHead, Name, _),
|
|
5757
|
+
repl_reserved_name(Name).
|
|
5074
5758
|
repl_reserved_clause((tool(ToolHead) :- _)) :-
|
|
5075
5759
|
!,
|
|
5076
5760
|
functor(ToolHead, Name, _),
|
|
@@ -5652,6 +6336,41 @@ pattern_normalise(Value, Normalised) :-
|
|
|
5652
6336
|
split_string(Lower, " _-", "", Parts),
|
|
5653
6337
|
atomic_list_concat(Parts, '', Normalised).
|
|
5654
6338
|
|
|
6339
|
+
%% read_workspace_file(+SessionId, +File, -Content)
|
|
6340
|
+
%%
|
|
6341
|
+
%% `read_file_to_string/3` goes through SWI's `absolute_file_name/2`, which
|
|
6342
|
+
%% resolves differently inside an `execute` frame and can turn
|
|
6343
|
+
%% `/workspace/foo` into `/home/foo` (or a basename under another cwd). The
|
|
6344
|
+
%% mounted FS accepts the absolute path through open/4, so read it explicitly
|
|
6345
|
+
%% and join the lines. Relative paths are anchored at the session workspace.
|
|
6346
|
+
read_workspace_file(SessionId, File, Content) :-
|
|
6347
|
+
deepclause_mi:workspace_read_path(SessionId, File, Path),
|
|
6348
|
+
setup_call_cleanup(
|
|
6349
|
+
open(Path, read, Stream, [encoding(utf8)]),
|
|
6350
|
+
deepclause_mi:read_workspace_chars(Stream, Chars),
|
|
6351
|
+
close(Stream)
|
|
6352
|
+
),
|
|
6353
|
+
string_chars(Content, Chars).
|
|
6354
|
+
|
|
6355
|
+
workspace_read_path(_SessionId, File, Path) :-
|
|
6356
|
+
( string(File)
|
|
6357
|
+
-> Text = File
|
|
6358
|
+
; atom(File)
|
|
6359
|
+
-> atom_string(File, Text)
|
|
6360
|
+
; throw(error(type_error(file_name, File), context(read_workspace_file/3, _)))
|
|
6361
|
+
),
|
|
6362
|
+
( sub_string(Text, 0, 1, _, "/")
|
|
6363
|
+
-> Path = Text
|
|
6364
|
+
; atomic_list_concat(['/workspace/', Text], '', Path)
|
|
6365
|
+
).
|
|
6366
|
+
|
|
6367
|
+
read_workspace_chars(Stream, [Char|Chars]) :-
|
|
6368
|
+
get_char(Stream, Char),
|
|
6369
|
+
Char \== end_of_file,
|
|
6370
|
+
!,
|
|
6371
|
+
deepclause_mi:read_workspace_chars(Stream, Chars).
|
|
6372
|
+
read_workspace_chars(_, []).
|
|
6373
|
+
|
|
5655
6374
|
%% repl_inject_workspace_predicates(+SessionId)
|
|
5656
6375
|
%% Idempotent: assertz/1 the same clauses twice is harmless (a duplicate clause
|
|
5657
6376
|
%% yields the same solution once? no - it yields twice), so clear first.
|
|
@@ -5662,9 +6381,9 @@ repl_inject_workspace_predicates(SessionId) :-
|
|
|
5662
6381
|
catch(assertz(Clause), _, true)).
|
|
5663
6382
|
|
|
5664
6383
|
workspace_predicate_clause(S, (S:read_file(F, C) :-
|
|
5665
|
-
|
|
5666
|
-
workspace_predicate_clause(S, (S:read_file_to_string(F, C,
|
|
5667
|
-
|
|
6384
|
+
deepclause_mi:read_workspace_file(S, F, C))).
|
|
6385
|
+
workspace_predicate_clause(S, (S:read_file_to_string(F, C, _O) :-
|
|
6386
|
+
deepclause_mi:read_workspace_file(S, F, C))).
|
|
5668
6387
|
workspace_predicate_clause(S, (S:write_file(F, C) :-
|
|
5669
6388
|
system:open(F, write, St), system:write(St, C), system:close(St))).
|
|
5670
6389
|
workspace_predicate_clause(S, (S:append_file(F, C) :-
|
|
@@ -5718,7 +6437,7 @@ mi_call(consult(File), StateIn, StateIn) :-
|
|
|
5718
6437
|
!,
|
|
5719
6438
|
get_session_id(SessionId),
|
|
5720
6439
|
guard_not_frozen(SessionId),
|
|
5721
|
-
|
|
6440
|
+
read_workspace_file(SessionId, File, Code),
|
|
5722
6441
|
consult_string(Code, SessionId).
|
|
5723
6442
|
|
|
5724
6443
|
mi_call(use_module(Module), StateIn, StateIn) :-
|
|
@@ -5756,7 +6475,7 @@ consult_string(Code, SessionId) :-
|
|
|
5756
6475
|
consult_clauses(Stream, SessionId) :-
|
|
5757
6476
|
read_term(Stream, Term, [module(SessionId), variable_names(Bindings)]),
|
|
5758
6477
|
( Term == end_of_file
|
|
5759
|
-
->
|
|
6478
|
+
-> install_builtin_tools(SessionId)
|
|
5760
6479
|
; preprocess_loaded_term(Term, Bindings, PreparedTerms),
|
|
5761
6480
|
forall(member(PreparedTerm, PreparedTerms), consult_process_term(PreparedTerm, SessionId)),
|
|
5762
6481
|
consult_clauses(Stream, SessionId)
|
|
@@ -5814,15 +6533,17 @@ consult_process_term(Head, SessionId) :-
|
|
|
5814
6533
|
assertz(SessionId:Head).
|
|
5815
6534
|
|
|
5816
6535
|
%% mi_call(read_file_to_string(File, Content, Options), +StateIn, -StateOut)
|
|
5817
|
-
mi_call(read_file_to_string(File, Content,
|
|
6536
|
+
mi_call(read_file_to_string(File, Content, _Options), StateIn, StateIn) :-
|
|
5818
6537
|
!,
|
|
5819
|
-
|
|
6538
|
+
get_session_id(SessionId),
|
|
6539
|
+
read_workspace_file(SessionId, File, Content).
|
|
5820
6540
|
|
|
5821
6541
|
%% mi_call(read_file(File, Content), +StateIn, -StateOut)
|
|
5822
6542
|
%% Simplified version without options
|
|
5823
6543
|
mi_call(read_file(File, Content), StateIn, StateIn) :-
|
|
5824
6544
|
!,
|
|
5825
|
-
|
|
6545
|
+
get_session_id(SessionId),
|
|
6546
|
+
read_workspace_file(SessionId, File, Content).
|
|
5826
6547
|
|
|
5827
6548
|
%% mi_call(write_file(File, Content), +StateIn, -StateOut)
|
|
5828
6549
|
%% Write string content to a file
|
|
@@ -5844,11 +6565,21 @@ mi_call(append_file(File, Content), StateIn, StateIn) :-
|
|
|
5844
6565
|
%% Open a file for reading/writing
|
|
5845
6566
|
mi_call(open(File, Mode, Stream), StateIn, StateIn) :-
|
|
5846
6567
|
!,
|
|
5847
|
-
open(File, Mode, Stream)
|
|
6568
|
+
open(File, Mode, Stream),
|
|
6569
|
+
% A stream is not serializable, so a suspended session must not be holding
|
|
6570
|
+
% one. Track it here and `suspend/1` refuses while the ledger is non-empty.
|
|
6571
|
+
acquire_resource(file(File, Stream)).
|
|
6572
|
+
|
|
6573
|
+
%% mi_call(open(File, Mode, Stream, Options), +StateIn, -StateOut)
|
|
6574
|
+
mi_call(open(File, Mode, Stream, Options), StateIn, StateIn) :-
|
|
6575
|
+
!,
|
|
6576
|
+
open(File, Mode, Stream, Options),
|
|
6577
|
+
acquire_resource(file(File, Stream)).
|
|
5848
6578
|
|
|
5849
6579
|
%% mi_call(close(Stream), +StateIn, -StateOut)
|
|
5850
6580
|
mi_call(close(Stream), StateIn, StateIn) :-
|
|
5851
6581
|
!,
|
|
6582
|
+
release_resources(file(_, Stream)),
|
|
5852
6583
|
close(Stream).
|
|
5853
6584
|
|
|
5854
6585
|
%% mi_call(write(Stream, Content), +StateIn, -StateOut)
|
|
@@ -6014,6 +6745,7 @@ is_mi_special_predicate(sleep(_)).
|
|
|
6014
6745
|
is_mi_special_predicate(make_ref(_)).
|
|
6015
6746
|
is_mi_special_predicate(now_ms(_)).
|
|
6016
6747
|
is_mi_special_predicate(serve(_,_)).
|
|
6748
|
+
is_mi_special_predicate(serve(_,_,_)).
|
|
6017
6749
|
is_mi_special_predicate(service_cast(_,_)).
|
|
6018
6750
|
is_mi_special_predicate(service_call(_,_,_)).
|
|
6019
6751
|
is_mi_special_predicate(service_call(_,_,_,_)).
|
|
@@ -6024,6 +6756,13 @@ is_mi_special_predicate(wait(_)).
|
|
|
6024
6756
|
is_mi_special_predicate(wait(_,_)).
|
|
6025
6757
|
is_mi_special_predicate(param(_,_)).
|
|
6026
6758
|
is_mi_special_predicate(param(_,_,_)).
|
|
6759
|
+
%% Forms (docs/DML_FORMS.md)
|
|
6760
|
+
is_mi_special_predicate(form(_,_)).
|
|
6761
|
+
is_mi_special_predicate(form_update(_,_)).
|
|
6762
|
+
is_mi_special_predicate(form_errors(_,_)).
|
|
6763
|
+
is_mi_special_predicate(await_form(_,_)).
|
|
6764
|
+
is_mi_special_predicate(await_form(_,_,_)).
|
|
6765
|
+
is_mi_special_predicate(close_form(_)).
|
|
6027
6766
|
%% Prompt predicates (like task but with fresh memory)
|
|
6028
6767
|
is_mi_special_predicate(prompt(_)).
|
|
6029
6768
|
is_mi_special_predicate(prompt(_,_)).
|
|
@@ -6054,7 +6793,14 @@ is_mi_special_predicate(require_judgment(_,_)).
|
|
|
6054
6793
|
is_mi_special_predicate(get_memory(_)).
|
|
6055
6794
|
is_mi_special_predicate(format(_,_)).
|
|
6056
6795
|
is_mi_special_predicate(writeln(_)).
|
|
6796
|
+
is_mi_special_predicate(write(_)).
|
|
6797
|
+
is_mi_special_predicate(suspend(_)).
|
|
6057
6798
|
is_mi_special_predicate(input(_,_)).
|
|
6799
|
+
%% Conversation (docs/DML_CONVERSATION.md)
|
|
6800
|
+
is_mi_special_predicate(input(_)).
|
|
6801
|
+
is_mi_special_predicate(chat(_)).
|
|
6802
|
+
is_mi_special_predicate(chat(_,_)).
|
|
6803
|
+
is_mi_special_predicate(next(_)).
|
|
6058
6804
|
is_mi_special_predicate(call(_)).
|
|
6059
6805
|
is_mi_special_predicate(call(_,_)).
|
|
6060
6806
|
is_mi_special_predicate(call(_,_,_)).
|