deepclause-sdk 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -12
- package/dist/agent.d.ts +14 -1
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +117 -9
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +86 -26
- package/dist/cli/config.d.ts.map +1 -1
- package/dist/cli/config.js +15 -0
- package/dist/cli/config.js.map +1 -1
- package/dist/cli/index.js +38 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/repl.d.ts +27 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +191 -5
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +16 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/visualize.d.ts +4 -0
- package/dist/cli/visualize.d.ts.map +1 -1
- package/dist/cli/visualize.js +29 -4
- package/dist/cli/visualize.js.map +1 -1
- package/dist/prolog/bridge.d.ts +3 -1
- package/dist/prolog/bridge.d.ts.map +1 -1
- package/dist/prolog/bridge.js +2 -0
- package/dist/prolog/bridge.js.map +1 -1
- package/dist/prolog/loader.d.ts +32 -0
- package/dist/prolog/loader.d.ts.map +1 -1
- package/dist/prolog/loader.js +87 -0
- package/dist/prolog/loader.js.map +1 -1
- package/dist/prolog-src/deepclause_mi.pl +1217 -120
- package/dist/repl/agent.d.ts +33 -1
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +91 -8
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/backend.d.ts +31 -0
- package/dist/repl/backend.d.ts.map +1 -1
- package/dist/repl/backend.js +122 -77
- package/dist/repl/backend.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +49 -2
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +87 -7
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/imagegen.d.ts +29 -0
- package/dist/repl/imagegen.d.ts.map +1 -0
- package/dist/repl/imagegen.js +84 -0
- package/dist/repl/imagegen.js.map +1 -0
- package/dist/repl/index.d.ts +20 -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/model-resolver.d.ts +59 -0
- package/dist/repl/model-resolver.d.ts.map +1 -0
- package/dist/repl/model-resolver.js +156 -0
- package/dist/repl/model-resolver.js.map +1 -0
- package/dist/repl/render.d.ts +36 -2
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +175 -17
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +129 -4
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +380 -17
- package/dist/repl/session.js.map +1 -1
- package/dist/repl/tool-arguments.d.ts +36 -0
- package/dist/repl/tool-arguments.d.ts.map +1 -0
- package/dist/repl/tool-arguments.js +123 -0
- package/dist/repl/tool-arguments.js.map +1 -0
- package/dist/repl/toolset.d.ts +28 -1
- package/dist/repl/toolset.d.ts.map +1 -1
- package/dist/repl/toolset.js +52 -4
- package/dist/repl/toolset.js.map +1 -1
- package/dist/repl/tui/transcript.d.ts +2 -0
- package/dist/repl/tui/transcript.d.ts.map +1 -1
- package/dist/repl/tui/transcript.js +16 -0
- package/dist/repl/tui/transcript.js.map +1 -1
- package/dist/runner.d.ts +44 -2
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +293 -33
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +1000 -44
- package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
- package/dist/system/assets/docs/DML_REFERENCE.md +652 -39
- package/dist/system/assets/docs/PROLOG_SNIPPETS.md +45 -1
- package/dist/system/assets/docs/TASK_PROMPT.md +2 -0
- package/dist/system/environment.d.ts +36 -0
- package/dist/system/environment.d.ts.map +1 -0
- package/dist/system/environment.js +81 -0
- package/dist/system/environment.js.map +1 -0
- package/dist/system/runtime/shell-manager.d.ts +40 -2
- package/dist/system/runtime/shell-manager.d.ts.map +1 -1
- package/dist/system/runtime/shell-manager.js +82 -16
- package/dist/system/runtime/shell-manager.js.map +1 -1
- package/dist/system/runtime/shell-tool-events.js +25 -1
- package/dist/system/runtime/shell-tool-events.js.map +1 -1
- package/dist/types.d.ts +76 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/visualize/ascii.d.ts +55 -0
- package/dist/visualize/ascii.d.ts.map +1 -0
- package/dist/visualize/ascii.js +242 -0
- package/dist/visualize/ascii.js.map +1 -0
- package/dist/visualize/display.d.ts +24 -0
- package/dist/visualize/display.d.ts.map +1 -0
- package/dist/visualize/display.js +29 -0
- package/dist/visualize/display.js.map +1 -0
- package/dist/visualize/graphics.d.ts +35 -0
- package/dist/visualize/graphics.d.ts.map +1 -0
- package/dist/visualize/graphics.js +96 -0
- package/dist/visualize/graphics.js.map +1 -0
- package/dist/visualize/image.d.ts +38 -0
- package/dist/visualize/image.d.ts.map +1 -0
- package/dist/visualize/image.js +95 -0
- package/dist/visualize/image.js.map +1 -0
- package/dist/visualize/index.d.ts +7 -0
- package/dist/visualize/index.d.ts.map +1 -1
- package/dist/visualize/index.js +9 -0
- package/dist/visualize/index.js.map +1 -1
- package/dist/visualize/raster.d.ts +23 -0
- package/dist/visualize/raster.d.ts.map +1 -0
- package/dist/visualize/raster.js +47 -0
- package/dist/visualize/raster.js.map +1 -0
- package/dist/visualize/svg.d.ts +39 -0
- package/dist/visualize/svg.d.ts.map +1 -0
- package/dist/visualize/svg.js +148 -0
- package/dist/visualize/svg.js.map +1 -0
- package/dist/visualize/transcode.d.ts +13 -0
- package/dist/visualize/transcode.d.ts.map +1 -0
- package/dist/visualize/transcode.js +40 -0
- package/dist/visualize/transcode.js.map +1 -0
- package/docs/images/repl.png +0 -0
- package/package.json +10 -6
- package/src/prolog-src/deepclause_mi.pl +1217 -120
- package/dist/compaction.d.ts +0 -98
- package/dist/compaction.d.ts.map +0 -1
- package/dist/compaction.js +0 -471
- package/dist/compaction.js.map +0 -1
- package/dist/repl/tools.d.ts +0 -53
- package/dist/repl/tools.d.ts.map +0 -1
- package/dist/repl/tools.js +0 -307
- package/dist/repl/tools.js.map +0 -1
- package/dist/system/assets/compactors/default-loop-compactor.dml +0 -43
- package/dist/system/assets/compactors/default-session-compactor.dml +0 -30
- package/dist/system/assets/docs/META_PROMPT.md +0 -68
|
@@ -61,6 +61,13 @@
|
|
|
61
61
|
repl_edit_touched/2,
|
|
62
62
|
repl_edit_touched_text/2,
|
|
63
63
|
repl_edit_touched_clear/1,
|
|
64
|
+
repl_validate_tools/2,
|
|
65
|
+
repl_validate_docs/2,
|
|
66
|
+
schema_fill_fields/3,
|
|
67
|
+
schema_fields_of/3,
|
|
68
|
+
repl_unhandled_directives/2,
|
|
69
|
+
repl_inject_workspace_predicates/1,
|
|
70
|
+
workspace_predicate_name/1,
|
|
64
71
|
repl_set_dml_reference/2,
|
|
65
72
|
repl_set_authoring_examples/2,
|
|
66
73
|
repl_manifest/2,
|
|
@@ -71,6 +78,7 @@
|
|
|
71
78
|
repl_user_predicates/2,
|
|
72
79
|
repl_user_predicates_text/2,
|
|
73
80
|
repl_predicate_arities/3,
|
|
81
|
+
repl_probe_nondeterminism/6,
|
|
74
82
|
repl_set_meta_catalog/2,
|
|
75
83
|
repl_set_meta_template/2,
|
|
76
84
|
repl_set_harness_examples/2,
|
|
@@ -115,6 +123,7 @@
|
|
|
115
123
|
:- dynamic session_repl_memory/2. % session_repl_memory(SessionId, HarnessMemory) - object/entry memory
|
|
116
124
|
:- dynamic session_meta_memory/2. % session_meta_memory(SessionId, MetaMemory) - code-writing task memory
|
|
117
125
|
:- dynamic session_repl_mode/2. % session_repl_mode(SessionId, true/false) - gates format/writeln output shims
|
|
126
|
+
:- dynamic session_unhandled_directive/2. % session_unhandled_directive(SessionId, Directive)
|
|
118
127
|
:- dynamic session_frozen/2. % session_frozen(SessionId, true/false) - read-only lock
|
|
119
128
|
:- dynamic session_locked/2. % session_locked(SessionId, true/false) - irreversibly frozen (--locked)
|
|
120
129
|
:- dynamic session_directive/2. % session_directive(SessionId, Directive) - recorded :- directives for export
|
|
@@ -152,7 +161,8 @@
|
|
|
152
161
|
parse_dml(FilePath, SessionId, _MemoryId, Params, Error) :-
|
|
153
162
|
catch(
|
|
154
163
|
(
|
|
155
|
-
|
|
164
|
+
% FilePath is an absolute host temp path, not workspace-relative.
|
|
165
|
+
system:read_file_to_string(FilePath, Code, []),
|
|
156
166
|
assert_params(SessionId, Params),
|
|
157
167
|
parse_dml_string(Code, SessionId),
|
|
158
168
|
Error = none
|
|
@@ -309,6 +319,11 @@ extract_var_info(Bindings, Term, BareVar, Info) :-
|
|
|
309
319
|
BareVar = Term,
|
|
310
320
|
var_to_name(Bindings, Term, Name),
|
|
311
321
|
Info = typed_var{name: Name, type: string}
|
|
322
|
+
; compound(Term), Term =.. [schema, SchemaName, Inner], var(Inner)
|
|
323
|
+
-> % A declared shape: schema(name, Var)
|
|
324
|
+
BareVar = Inner,
|
|
325
|
+
var_to_name(Bindings, Inner, Name),
|
|
326
|
+
Info = typed_var{name: Name, type: object, schema: SchemaName}
|
|
312
327
|
; compound(Term), Term =.. [Wrapper, Inner]
|
|
313
328
|
-> % Wrapped variable
|
|
314
329
|
( wrapper_type(Wrapper, Type)
|
|
@@ -316,6 +331,11 @@ extract_var_info(Bindings, Term, BareVar, Info) :-
|
|
|
316
331
|
-> BareVar = Inner,
|
|
317
332
|
var_to_name(Bindings, Inner, Name),
|
|
318
333
|
Info = typed_var{name: Name, type: Type}
|
|
334
|
+
; compound(Inner), Inner =.. [schema, SchemaName, InnerVar], var(InnerVar), Wrapper == list
|
|
335
|
+
-> % List of declared shapes: list(schema(name, X))
|
|
336
|
+
BareVar = InnerVar,
|
|
337
|
+
var_to_name(Bindings, InnerVar, Name),
|
|
338
|
+
Info = typed_var{name: Name, type: array, itemType: object, schema: SchemaName}
|
|
319
339
|
; compound(Inner), Inner =.. [InnerWrapper, InnerVar], var(InnerVar), Wrapper == list
|
|
320
340
|
-> % List of typed items: list(string(X))
|
|
321
341
|
BareVar = InnerVar,
|
|
@@ -336,6 +356,7 @@ extract_var_info(Bindings, Term, BareVar, Info) :-
|
|
|
336
356
|
).
|
|
337
357
|
|
|
338
358
|
%% wrapper_type(?Wrapper, ?Type)
|
|
359
|
+
wrapper_type(media, media).
|
|
339
360
|
wrapper_type(string, string).
|
|
340
361
|
wrapper_type(integer, integer).
|
|
341
362
|
wrapper_type(float, number).
|
|
@@ -429,6 +450,9 @@ clean_atomic(with_tools(Tools, Goal), with_tools(Tools, CleanGoal)) :-
|
|
|
429
450
|
clean_atomic(without_tools(Tools, Goal), without_tools(Tools, CleanGoal)) :-
|
|
430
451
|
!,
|
|
431
452
|
clean_clause_body(Goal, CleanGoal).
|
|
453
|
+
clean_atomic(with_fresh_memory(Goal), with_fresh_memory(CleanGoal)) :-
|
|
454
|
+
!,
|
|
455
|
+
clean_clause_body(Goal, CleanGoal).
|
|
432
456
|
clean_atomic(with_judgment(Backend, Goal), with_judgment(Backend, CleanGoal)) :-
|
|
433
457
|
!,
|
|
434
458
|
clean_clause_body(Goal, CleanGoal).
|
|
@@ -477,6 +501,7 @@ typed_arg(Var, Info, Wrapped) :-
|
|
|
477
501
|
typed_arg_type(Type, Var, Info, Wrapped).
|
|
478
502
|
typed_arg(Var, _, Var).
|
|
479
503
|
|
|
504
|
+
typed_arg_type(media, Var, _, media(Var)) :- !.
|
|
480
505
|
typed_arg_type(string, Var, _, Var) :- !.
|
|
481
506
|
typed_arg_type(integer, Var, _, integer(Var)) :- !.
|
|
482
507
|
typed_arg_type(number, Var, _, number(Var)) :- !.
|
|
@@ -739,8 +764,7 @@ process_directive(consult(File), SessionId) :-
|
|
|
739
764
|
!,
|
|
740
765
|
% Set session ID for get_session_id to find, then call our custom consult
|
|
741
766
|
nb_setval(current_session_id, SessionId),
|
|
742
|
-
|
|
743
|
-
readutil:read_file_to_string(FullPath, Code, []),
|
|
767
|
+
system:read_file_to_string(File, Code, []),
|
|
744
768
|
consult_string(Code, SessionId).
|
|
745
769
|
%% Handle use_module directive
|
|
746
770
|
process_directive(use_module(Module), SessionId) :-
|
|
@@ -763,7 +787,80 @@ process_directive([File|Files], SessionId) :-
|
|
|
763
787
|
nb_setval(current_session_id, SessionId),
|
|
764
788
|
process_directive(consult(File), SessionId),
|
|
765
789
|
(Files == [] -> true ; process_directive(Files, SessionId)).
|
|
766
|
-
|
|
790
|
+
%% `:- repl_delete(Name/Arity).` — the directive form of the fact handled by
|
|
791
|
+
%% process_clause/2 below. It used to fall through to the catch-all and be
|
|
792
|
+
%% discarded, so a predicate the author asked to remove stayed in the module.
|
|
793
|
+
process_directive(repl_delete(Name/Arity), SessionId) :-
|
|
794
|
+
!,
|
|
795
|
+
atom(Name),
|
|
796
|
+
integer(Arity),
|
|
797
|
+
Arity >= 0,
|
|
798
|
+
catch(abolish(SessionId:Name/Arity), _, true).
|
|
799
|
+
|
|
800
|
+
%% process_directive(require_sandbox, +SessionId)
|
|
801
|
+
%% The harness declares that it must run in an execution sandbox, and the load
|
|
802
|
+
%% fails if it cannot. Checked against the **effective** backend rather than
|
|
803
|
+
%% mere availability: bwrap being installed while the session resolves to
|
|
804
|
+
%% clean-room means the code would run unsandboxed, which is what the directive
|
|
805
|
+
%% exists to prevent. Only the user sets this (see repl_commit_directive/2).
|
|
806
|
+
process_directive(require_sandbox, SessionId) :-
|
|
807
|
+
( session_sandbox_backend(SessionId, Kind)
|
|
808
|
+
-> true
|
|
809
|
+
; Kind = 'clean-room'
|
|
810
|
+
),
|
|
811
|
+
( sandbox_backend_acceptable(Kind)
|
|
812
|
+
-> true
|
|
813
|
+
; format(atom(Message),
|
|
814
|
+
'harness requires an execution sandbox, but the effective backend is ~w; bwrap, AgentVM or sandbox-exec is required. Install bubblewrap, run with --sandbox (AgentVM), or remove the :- require_sandbox. directive.',
|
|
815
|
+
[Kind]),
|
|
816
|
+
throw(error(permission_error(load, harness, require_sandbox),
|
|
817
|
+
context(require_sandbox/0, Message)))
|
|
818
|
+
).
|
|
819
|
+
|
|
820
|
+
%% Anything else is recorded rather than discarded. An unrecognised directive
|
|
821
|
+
%% (`:- dynamic(f/1).`, a `:- op(...)` without use_module, a typo) does nothing,
|
|
822
|
+
%% and silently doing nothing is how an edit that never took effect goes
|
|
823
|
+
%% unnoticed. The host reports these after a load.
|
|
824
|
+
process_directive(Directive, SessionId) :-
|
|
825
|
+
( repl_benign_directive(Directive)
|
|
826
|
+
-> true
|
|
827
|
+
; assertz(session_unhandled_directive(SessionId, Directive))
|
|
828
|
+
).
|
|
829
|
+
|
|
830
|
+
%% Directives DML deliberately accepts and ignores (SWI state it manages itself).
|
|
831
|
+
repl_benign_directive(dynamic(_)).
|
|
832
|
+
repl_benign_directive(dynamic(_, _)).
|
|
833
|
+
repl_benign_directive(discontiguous(_)).
|
|
834
|
+
repl_benign_directive(discontiguous(_, _)).
|
|
835
|
+
repl_benign_directive(multifile(_)).
|
|
836
|
+
repl_benign_directive(multifile(_, _)).
|
|
837
|
+
|
|
838
|
+
%% sandbox_kind_atom(+Raw, -Atom)
|
|
839
|
+
%% A JS string or an atom in, an atom out.
|
|
840
|
+
%%
|
|
841
|
+
%% Uses term_string/3, not atom_string/2: in this WASM build `atom_string/2`
|
|
842
|
+
%% returns the string unchanged, so an "atom" produced by it still fails `atom/1`
|
|
843
|
+
%% and compares unequal to every atom literal. That silently made every backend
|
|
844
|
+
%% look unacceptable. `term_string(Atom, "bwrap", [])` does produce the atom.
|
|
845
|
+
sandbox_kind_atom(Raw, Atom) :-
|
|
846
|
+
( atom(Raw)
|
|
847
|
+
-> Atom = Raw
|
|
848
|
+
; catch(term_string(Atom, Raw, []), _, Atom = Raw)
|
|
849
|
+
).
|
|
850
|
+
|
|
851
|
+
%% sandbox_backend_acceptable(+Kind)
|
|
852
|
+
%% The backends that provide filesystem isolation for `exec(bash(...))`.
|
|
853
|
+
%% `clean-room` is the absence of a wrapper and does not qualify. The host
|
|
854
|
+
%% normalizes `sandbox-exec` to `sandbox_exec` (see DMLSession.open) because a
|
|
855
|
+
%% hyphen makes the token a subtraction expression to a Prolog parser.
|
|
856
|
+
sandbox_backend_acceptable(agentvm).
|
|
857
|
+
sandbox_backend_acceptable(bwrap).
|
|
858
|
+
sandbox_backend_acceptable(sandbox_exec).
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
%% repl_unhandled_directives(+SessionId, -Directives)
|
|
862
|
+
repl_unhandled_directives(SessionId, Directives) :-
|
|
863
|
+
findall(D, session_unhandled_directive(SessionId, D), Directives).
|
|
767
864
|
|
|
768
865
|
%% ============================================================
|
|
769
866
|
%% Engine Management
|
|
@@ -916,7 +1013,17 @@ create_session(SessionId, Params, HarnessMemory, MetaMemory) :-
|
|
|
916
1013
|
retractall(session_frozen(SessionId, _)),
|
|
917
1014
|
assertz(session_frozen(SessionId, false)),
|
|
918
1015
|
retractall(session_locked(SessionId, _)),
|
|
919
|
-
assertz(session_locked(SessionId, false))
|
|
1016
|
+
assertz(session_locked(SessionId, false)),
|
|
1017
|
+
% Stored as an atom. The host sends a JS string, and an atom and a string
|
|
1018
|
+
% that print identically are not `==`, so normalize once here rather than at
|
|
1019
|
+
% every comparison.
|
|
1020
|
+
( get_dict(sandbox, Params, SandboxRaw)
|
|
1021
|
+
-> sandbox_kind_atom(SandboxRaw, SandboxKind)
|
|
1022
|
+
; SandboxKind = 'clean-room'
|
|
1023
|
+
),
|
|
1024
|
+
retractall(session_sandbox_backend(SessionId, _)),
|
|
1025
|
+
assertz(session_sandbox_backend(SessionId, SandboxKind)),
|
|
1026
|
+
repl_inject_workspace_predicates(SessionId).
|
|
920
1027
|
|
|
921
1028
|
%% repl_abolish_user_predicates(+SessionId)
|
|
922
1029
|
%% Removes only the harness's own predicates; imported builtins are untouched.
|
|
@@ -1235,6 +1342,7 @@ internal_session_predicate(param).
|
|
|
1235
1342
|
internal_session_predicate(param_decl).
|
|
1236
1343
|
internal_session_predicate(tool_source).
|
|
1237
1344
|
internal_session_predicate('__session__').
|
|
1345
|
+
internal_session_predicate(Name) :- workspace_predicate_name(Name).
|
|
1238
1346
|
|
|
1239
1347
|
%% history_predicate(+Name)
|
|
1240
1348
|
%% Runtime-owned harness-history facts. Excluded from the user predicate set (so
|
|
@@ -1264,6 +1372,7 @@ repl_reserved_name(repl_reserved_name).
|
|
|
1264
1372
|
repl_reserved_name(repl_tool_catalog).
|
|
1265
1373
|
repl_reserved_name(repl_listing).
|
|
1266
1374
|
repl_reserved_name(repl_assert).
|
|
1375
|
+
repl_reserved_name(require_sandbox).
|
|
1267
1376
|
repl_reserved_name(repl_retract).
|
|
1268
1377
|
repl_reserved_name(repl_delete).
|
|
1269
1378
|
repl_reserved_name(repl_freeze).
|
|
@@ -1499,12 +1608,11 @@ repl_script_files(SessionId, Paths) :-
|
|
|
1499
1608
|
repl_materialize_embedded(SessionId) :-
|
|
1500
1609
|
( current_predicate(SessionId:embedded_file/2)
|
|
1501
1610
|
-> forall(SessionId:embedded_file(Path, Content),
|
|
1502
|
-
(
|
|
1503
|
-
|
|
1504
|
-
( file_directory_name(Full, Dir),
|
|
1611
|
+
( catch(
|
|
1612
|
+
( file_directory_name(Path, Dir),
|
|
1505
1613
|
make_directory_path(Dir),
|
|
1506
1614
|
setup_call_cleanup(
|
|
1507
|
-
open(
|
|
1615
|
+
open(Path, write, Stream),
|
|
1508
1616
|
format(Stream, "~s", [Content]),
|
|
1509
1617
|
close(Stream))
|
|
1510
1618
|
),
|
|
@@ -1601,6 +1709,8 @@ repl_assert(SessionId, ClauseText, Result) :-
|
|
|
1601
1709
|
catch(
|
|
1602
1710
|
( guard_not_frozen(SessionId),
|
|
1603
1711
|
repl_prepare_single(SessionId, ClauseText, PreparedTerms),
|
|
1712
|
+
forall(member(PreparedTerm, PreparedTerms),
|
|
1713
|
+
repl_require_new_predicate(SessionId, PreparedTerm)),
|
|
1604
1714
|
forall(member(PreparedTerm, PreparedTerms),
|
|
1605
1715
|
repl_assert_clause(PreparedTerm, SessionId)),
|
|
1606
1716
|
Result = ok
|
|
@@ -1660,11 +1770,25 @@ repl_read_prepared_terms(Stream, SessionId, Terms) :-
|
|
|
1660
1770
|
% their operators are visible to the terms that follow; keep them out of
|
|
1661
1771
|
% the returned list so repl_commit_terms does not process them twice.
|
|
1662
1772
|
partition_directives(Prepared, Rules, Directives),
|
|
1663
|
-
forall(member(Directive, Directives),
|
|
1773
|
+
forall(member(Directive, Directives), repl_commit_directive(Directive, SessionId)),
|
|
1664
1774
|
append(Rules, Rest, Terms),
|
|
1665
1775
|
repl_read_prepared_terms(Stream, SessionId, Rest)
|
|
1666
1776
|
).
|
|
1667
1777
|
|
|
1778
|
+
%% repl_commit_directive(+Directive, +SessionId)
|
|
1779
|
+
%% Directives written by the *agent* (assert_many/define) run here. `require_sandbox`
|
|
1780
|
+
%% is the user's to set — through `/set-require-sandbox` — so an agent cannot
|
|
1781
|
+
%% declare a requirement it was not asked for, nor lift one by re-asserting the
|
|
1782
|
+
%% harness. The file-load path (parse_clauses/2) is deliberately not guarded:
|
|
1783
|
+
%% that is what the user's own harness file goes through.
|
|
1784
|
+
repl_commit_directive((:- require_sandbox), _SessionId) :-
|
|
1785
|
+
!,
|
|
1786
|
+
throw(error(permission_error(modify, reserved_name, require_sandbox),
|
|
1787
|
+
context(repl_commit_directive/2,
|
|
1788
|
+
'only the user can set the sandbox requirement'))).
|
|
1789
|
+
repl_commit_directive(Directive, SessionId) :-
|
|
1790
|
+
process_clause(Directive, SessionId).
|
|
1791
|
+
|
|
1668
1792
|
partition_directives([], [], []).
|
|
1669
1793
|
partition_directives([Term|Terms], Rules, Directives) :-
|
|
1670
1794
|
( Term = (:- _)
|
|
@@ -1693,6 +1817,9 @@ repl_assert_many(SessionId, ClausesText, Result) :-
|
|
|
1693
1817
|
catch(
|
|
1694
1818
|
( guard_not_frozen(SessionId),
|
|
1695
1819
|
repl_parse_clauses(SessionId, ClausesText, Terms),
|
|
1820
|
+
% assert appends; against an existing predicate that leaves both
|
|
1821
|
+
% versions live with their clauses interleaved.
|
|
1822
|
+
forall(member(Term, Terms), repl_require_new_predicate(SessionId, Term)),
|
|
1696
1823
|
repl_commit_terms(SessionId, Terms),
|
|
1697
1824
|
repl_note_touched(SessionId, Terms),
|
|
1698
1825
|
Result = ok
|
|
@@ -1716,11 +1843,105 @@ repl_term_indicator(Term, Name/Arity) :-
|
|
|
1716
1843
|
%% Record the predicate indicators a term list defines, for history recording.
|
|
1717
1844
|
repl_note_touched(SessionId, Terms) :-
|
|
1718
1845
|
forall(( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
|
|
1719
|
-
(
|
|
1720
|
-
|
|
1721
|
-
|
|
1846
|
+
repl_note_indicator(SessionId, Name/Arity)).
|
|
1847
|
+
|
|
1848
|
+
repl_note_indicator(SessionId, Name/Arity) :-
|
|
1849
|
+
( session_edit_touched(SessionId, Name/Arity)
|
|
1850
|
+
-> true
|
|
1851
|
+
; assertz(session_edit_touched(SessionId, Name/Arity))
|
|
1852
|
+
).
|
|
1853
|
+
|
|
1854
|
+
%% repl_require_new_predicate(+SessionId, +Term)
|
|
1855
|
+
%% `assert`/`assert_many` append. Against a predicate that already has clauses
|
|
1856
|
+
%% that mixes the old and the new version — both stay live, clause order
|
|
1857
|
+
%% interleaves them, and nothing says so. Refuse instead, and name the remedy.
|
|
1858
|
+
repl_require_new_predicate(SessionId, Term) :-
|
|
1859
|
+
repl_term_indicator(Term, Name/Arity),
|
|
1860
|
+
findall(x,
|
|
1861
|
+
( functor(Head, Name, Arity),
|
|
1862
|
+
clause(SessionId:Head, _)
|
|
1863
|
+
),
|
|
1864
|
+
Existing),
|
|
1865
|
+
Existing \== [],
|
|
1866
|
+
!,
|
|
1867
|
+
length(Existing, Count),
|
|
1868
|
+
format(atom(Message),
|
|
1869
|
+
'~w/~w already has ~w clause(s). assert appends, so the old and the new clauses would both be present and clause order would interleave them. Use define("~w/~w", "...") to replace the predicate, or delete("~w/~w") first.',
|
|
1870
|
+
[Name, Arity, Count, Name, Arity, Name, Arity]),
|
|
1871
|
+
throw(error(permission_error(modify, existing_predicate, Name/Arity),
|
|
1872
|
+
context(repl_require_new_predicate/2, Message))).
|
|
1873
|
+
repl_require_new_predicate(_SessionId, _Term).
|
|
1874
|
+
|
|
1875
|
+
%% repl_forget_tool(+SessionId, +Name)
|
|
1876
|
+
%% Drop the tool annotation that belonged to a predicate being removed.
|
|
1877
|
+
repl_forget_tool(SessionId, Name) :-
|
|
1878
|
+
( session_user_tools(SessionId, Name)
|
|
1879
|
+
-> retractall(session_user_tools(SessionId, Name)),
|
|
1880
|
+
retractall(session_user_tool_schema(SessionId, Name, _)),
|
|
1881
|
+
catch(retractall(SessionId:tool_source(Name, _)), _, true)
|
|
1882
|
+
; true
|
|
1883
|
+
).
|
|
1884
|
+
|
|
1885
|
+
%% repl_capture_predicates(+SessionId, +Indicators, -Captured)
|
|
1886
|
+
%% Copy the clauses and tool bookkeeping a replacement is about to destroy, so
|
|
1887
|
+
%% a failure part-way through can put them back.
|
|
1888
|
+
repl_capture_predicates(SessionId, Indicators, Captured) :-
|
|
1889
|
+
findall(indicator(Name, Arity, Clauses, tool(IsTool, Schema, Source)),
|
|
1890
|
+
( member(Name/Arity, Indicators),
|
|
1891
|
+
findall(Stored,
|
|
1892
|
+
( functor(Head, Name, Arity),
|
|
1893
|
+
clause(SessionId:Head, Body),
|
|
1894
|
+
( Body == true
|
|
1895
|
+
-> Stored = Head
|
|
1896
|
+
; Stored = (Head :- Body)
|
|
1897
|
+
)
|
|
1898
|
+
),
|
|
1899
|
+
Clauses),
|
|
1900
|
+
( session_user_tools(SessionId, Name)
|
|
1901
|
+
-> IsTool = true
|
|
1902
|
+
; IsTool = false
|
|
1903
|
+
),
|
|
1904
|
+
( session_user_tool_schema(SessionId, Name, Found)
|
|
1905
|
+
-> Schema = Found
|
|
1906
|
+
; Schema = none
|
|
1907
|
+
),
|
|
1908
|
+
( current_predicate(SessionId:tool_source/2),
|
|
1909
|
+
SessionId:tool_source(Name, FoundSource)
|
|
1910
|
+
-> Source = FoundSource
|
|
1911
|
+
; Source = none
|
|
1912
|
+
)
|
|
1913
|
+
),
|
|
1914
|
+
Captured).
|
|
1915
|
+
|
|
1916
|
+
%% repl_restore_captured(+SessionId, +Captured)
|
|
1917
|
+
repl_restore_captured(SessionId, Captured) :-
|
|
1918
|
+
forall(member(indicator(Name, Arity, Clauses, tool(IsTool, Schema, Source)), Captured),
|
|
1919
|
+
( catch(abolish(SessionId:Name/Arity), _, true),
|
|
1920
|
+
repl_forget_tool(SessionId, Name),
|
|
1921
|
+
forall(member(Clause, Clauses), assertz(SessionId:Clause)),
|
|
1922
|
+
( IsTool == true
|
|
1923
|
+
-> assertz(session_user_tools(SessionId, Name))
|
|
1924
|
+
; true
|
|
1925
|
+
),
|
|
1926
|
+
( Schema == none
|
|
1927
|
+
-> true
|
|
1928
|
+
; assertz(session_user_tool_schema(SessionId, Name, Schema))
|
|
1929
|
+
),
|
|
1930
|
+
( Source == none
|
|
1931
|
+
-> true
|
|
1932
|
+
; assertz(SessionId:tool_source(Name, Source))
|
|
1933
|
+
)
|
|
1722
1934
|
)).
|
|
1723
1935
|
|
|
1936
|
+
%% repl_replace_predicates(+SessionId, +Indicators, +Terms)
|
|
1937
|
+
%% The destructive half of define: abolish the targets, then assert the block.
|
|
1938
|
+
repl_replace_predicates(SessionId, Indicators, Terms) :-
|
|
1939
|
+
forall(member(Name/Arity, Indicators),
|
|
1940
|
+
( catch(abolish(SessionId:Name/Arity), _, true),
|
|
1941
|
+
repl_forget_tool(SessionId, Name)
|
|
1942
|
+
)),
|
|
1943
|
+
repl_commit_terms(SessionId, Terms).
|
|
1944
|
+
|
|
1724
1945
|
%% repl_edit_touched(+SessionId, -Indicators)
|
|
1725
1946
|
%% Predicates touched by edit tools since the last clear (sorted, unique).
|
|
1726
1947
|
repl_edit_touched(SessionId, Indicators) :-
|
|
@@ -1751,7 +1972,13 @@ repl_system_functor(Name/_) :-
|
|
|
1751
1972
|
member(Name, ['.', ',', ';', ':-', '->', ':', '|', '!', '\\+', '=', 'is']).
|
|
1752
1973
|
|
|
1753
1974
|
%% repl_retract(+SessionId, +ClausePattern, -Result)
|
|
1754
|
-
%%
|
|
1975
|
+
%% Removes the clauses that match the pattern, and says how many.
|
|
1976
|
+
%%
|
|
1977
|
+
%% This used to build a fresh head with `functor/3` and call `retractall/1`, so
|
|
1978
|
+
%% the pattern's *arguments were discarded*: `retract("g(1)")` removed every
|
|
1979
|
+
%% g/1 clause and reported `ok`. It now retracts what was asked for, and fails
|
|
1980
|
+
%% loudly when nothing matched — a silent no-op is how an agent concludes a
|
|
1981
|
+
%% clause is gone when it never was.
|
|
1755
1982
|
repl_retract(SessionId, ClauseText, Result) :-
|
|
1756
1983
|
catch(
|
|
1757
1984
|
( guard_not_frozen(SessionId),
|
|
@@ -1763,19 +1990,29 @@ repl_retract(SessionId, ClauseText, Result) :-
|
|
|
1763
1990
|
context(repl_retract/3, 'cannot retract a reserved predicate')))
|
|
1764
1991
|
; true
|
|
1765
1992
|
),
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
(
|
|
1769
|
-
->
|
|
1770
|
-
|
|
1771
|
-
|
|
1993
|
+
findall(x, retract(SessionId:Pattern), Removed),
|
|
1994
|
+
length(Removed, RemovedCount),
|
|
1995
|
+
( RemovedCount =:= 0
|
|
1996
|
+
-> format(atom(Message),
|
|
1997
|
+
'no clause of ~w/~w matched ~w, so nothing was removed',
|
|
1998
|
+
[Name, Arity, Clause]),
|
|
1999
|
+
throw(error(existence_error(clause, Clause),
|
|
2000
|
+
context(repl_retract/3, Message)))
|
|
1772
2001
|
; true
|
|
1773
2002
|
),
|
|
1774
|
-
|
|
2003
|
+
% Only drop the tool annotation when the predicate is really gone;
|
|
2004
|
+
% other clauses of it may still be live.
|
|
2005
|
+
( functor(Head, Name, Arity),
|
|
2006
|
+
clause(SessionId:Head, _)
|
|
1775
2007
|
-> true
|
|
1776
|
-
;
|
|
2008
|
+
; repl_forget_tool(SessionId, Name)
|
|
1777
2009
|
),
|
|
1778
|
-
|
|
2010
|
+
repl_note_indicator(SessionId, Name/Arity),
|
|
2011
|
+
( RemovedCount =:= 1
|
|
2012
|
+
-> Suffix = ''
|
|
2013
|
+
; Suffix = 's'
|
|
2014
|
+
),
|
|
2015
|
+
format(atom(Result), 'ok(removed ~w clause~w)', [RemovedCount, Suffix])
|
|
1779
2016
|
),
|
|
1780
2017
|
RetractError,
|
|
1781
2018
|
format(atom(Result), 'error(~w)', [RetractError])
|
|
@@ -1811,34 +2048,45 @@ repl_delete_predicate(SessionId, Name, Arity, Result) :-
|
|
|
1811
2048
|
).
|
|
1812
2049
|
|
|
1813
2050
|
%% repl_define(+SessionId, +HeadPatternText, +ClausesText, -Result)
|
|
1814
|
-
%% Replace every predicate the clause block defines
|
|
1815
|
-
%%
|
|
1816
|
-
%%
|
|
1817
|
-
%%
|
|
2051
|
+
%% Replace every predicate the clause block defines, all or nothing.
|
|
2052
|
+
%%
|
|
2053
|
+
%% The order matters and used to be wrong. Abolishing first and asserting after
|
|
2054
|
+
%% means any failure in between leaves the target *destroyed*: a block with one
|
|
2055
|
+
%% reserved name in it removed the predicate it was replacing and reported an
|
|
2056
|
+
%% error, and a following `end_turn` then committed the deletion as a version.
|
|
2057
|
+
%% So everything that can fail is checked before anything is touched, and the
|
|
2058
|
+
%% clauses being replaced are copied first so a failure can still put them back.
|
|
1818
2059
|
repl_define(SessionId, HeadPatternText, ClausesText, Result) :-
|
|
1819
2060
|
catch(
|
|
1820
2061
|
( guard_not_frozen(SessionId),
|
|
1821
|
-
|
|
1822
|
-
repl_head_of(Pattern, HeadClause),
|
|
1823
|
-
repl_callable_indicator(HeadClause, HeadName, HeadArity),
|
|
2062
|
+
repl_define_target(SessionId, HeadPatternText, HeadName, HeadArity),
|
|
1824
2063
|
repl_parse_clauses(SessionId, ClausesText, Terms),
|
|
1825
2064
|
findall(Name/Arity,
|
|
1826
2065
|
( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
|
|
1827
2066
|
BlockIndicators0),
|
|
1828
|
-
sort(
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
2067
|
+
sort(BlockIndicators0, BlockIndicators),
|
|
2068
|
+
% The head names the predicate being replaced. If it is not one of
|
|
2069
|
+
% them the target is almost certainly a typo, and silently replacing
|
|
2070
|
+
% a *different* predicate is worse than refusing.
|
|
2071
|
+
( BlockIndicators \== [],
|
|
2072
|
+
\+ member(HeadName/HeadArity, BlockIndicators)
|
|
2073
|
+
-> format(atom(TargetMessage),
|
|
2074
|
+
'define was told to replace ~w/~w, but the clause block defines ~w. Pass the predicate you are replacing as the head.',
|
|
2075
|
+
[HeadName, HeadArity, BlockIndicators]),
|
|
2076
|
+
throw(error(domain_error(define_target, HeadName/HeadArity),
|
|
2077
|
+
context(repl_define/4, TargetMessage)))
|
|
2078
|
+
; true
|
|
2079
|
+
),
|
|
2080
|
+
sort([HeadName/HeadArity|BlockIndicators], Indicators),
|
|
2081
|
+
% Validate everything that can fail, before anything is removed.
|
|
2082
|
+
forall(member(Term, Terms), repl_require_committable(SessionId, Term)),
|
|
2083
|
+
forall(member(Name/Arity, Indicators), repl_require_not_reserved(SessionId, Name/Arity)),
|
|
2084
|
+
repl_capture_predicates(SessionId, Indicators, Captured),
|
|
2085
|
+
( catch(repl_replace_predicates(SessionId, Indicators, Terms), ReplaceError,
|
|
2086
|
+
( repl_restore_captured(SessionId, Captured),
|
|
2087
|
+
throw(ReplaceError)
|
|
2088
|
+
))
|
|
2089
|
+
),
|
|
1842
2090
|
repl_note_touched(SessionId, Terms),
|
|
1843
2091
|
Result = ok
|
|
1844
2092
|
),
|
|
@@ -1846,6 +2094,42 @@ repl_define(SessionId, HeadPatternText, ClausesText, Result) :-
|
|
|
1846
2094
|
format(atom(Result), 'error(~w)', [DefineError])
|
|
1847
2095
|
).
|
|
1848
2096
|
|
|
2097
|
+
%% repl_define_target(+SessionId, +HeadText, -Name, -Arity)
|
|
2098
|
+
%% The first argument of `define` names the predicate being replaced, written as
|
|
2099
|
+
%% an indicator (`f/1`) or as a clause head (`f(_)`).
|
|
2100
|
+
%%
|
|
2101
|
+
%% It has to be decomposed explicitly: `f/1` *reads* as the operator term
|
|
2102
|
+
%% `/(f, 1)`, so treating it as a clause head yields the functor `//2`. That is
|
|
2103
|
+
%% what used to happen — the target was silently ignored and the clause block
|
|
2104
|
+
%% alone decided what got replaced, so `define` accepted any name at all.
|
|
2105
|
+
repl_define_target(SessionId, HeadText, Name, Arity) :-
|
|
2106
|
+
repl_read_clause(SessionId, HeadText, Pattern),
|
|
2107
|
+
( Pattern = IndicatorName/IndicatorArity,
|
|
2108
|
+
atom(IndicatorName),
|
|
2109
|
+
integer(IndicatorArity),
|
|
2110
|
+
IndicatorArity >= 0
|
|
2111
|
+
-> Name = IndicatorName,
|
|
2112
|
+
Arity = IndicatorArity
|
|
2113
|
+
; repl_head_of(Pattern, HeadClause),
|
|
2114
|
+
repl_callable_indicator(HeadClause, Name, Arity)
|
|
2115
|
+
).
|
|
2116
|
+
|
|
2117
|
+
%% repl_require_committable(+SessionId, +Term)
|
|
2118
|
+
repl_require_committable(_SessionId, Term) :-
|
|
2119
|
+
( repl_protected_clause(Term)
|
|
2120
|
+
-> throw(error(permission_error(modify, reserved_name, Term),
|
|
2121
|
+
context(repl_define/4, 'cannot define a reserved predicate')))
|
|
2122
|
+
; true
|
|
2123
|
+
).
|
|
2124
|
+
|
|
2125
|
+
%% repl_require_not_reserved(+SessionId, +Indicator)
|
|
2126
|
+
repl_require_not_reserved(_SessionId, Name/Arity) :-
|
|
2127
|
+
( repl_protected_name(Name)
|
|
2128
|
+
-> throw(error(permission_error(modify, reserved_name, Name/Arity),
|
|
2129
|
+
context(repl_define/4, 'cannot define a reserved predicate')))
|
|
2130
|
+
; true
|
|
2131
|
+
).
|
|
2132
|
+
|
|
1849
2133
|
%% repl_extract_code(+Text, -Source)
|
|
1850
2134
|
%% Extracts a ```dml fenced block; falls back to the whole answer.
|
|
1851
2135
|
repl_extract_code(Text, Source) :-
|
|
@@ -2052,6 +2336,161 @@ repl_predicate_arities(SessionId, Name, Arities) :-
|
|
|
2052
2336
|
Arities0),
|
|
2053
2337
|
sort(Arities0, Arities).
|
|
2054
2338
|
|
|
2339
|
+
%% ============================================================
|
|
2340
|
+
%% Nondeterminism probe (end_turn advisory)
|
|
2341
|
+
%%
|
|
2342
|
+
%% After a commit, the host may probe each changed pure predicate: if calling it
|
|
2343
|
+
%% with fresh variables yields more than a threshold of solutions, the predicate
|
|
2344
|
+
%% is probably missing a commit (`once/1` / `!`) and would flood an `execute`.
|
|
2345
|
+
%% Side-effecting predicates are never run (see repl_impure_name/1), fact tables
|
|
2346
|
+
%% are skipped, and the search is bounded by an inference limit.
|
|
2347
|
+
%% ============================================================
|
|
2348
|
+
|
|
2349
|
+
%% Functors with side effects: model calls, host tools, file IO, DB mutation.
|
|
2350
|
+
repl_impure_name(task). repl_impure_name(prompt). repl_impure_name(exec).
|
|
2351
|
+
repl_impure_name(system). repl_impure_name(user). repl_impure_name(answer).
|
|
2352
|
+
repl_impure_name(input). repl_impure_name(output). repl_impure_name(read_media).
|
|
2353
|
+
repl_impure_name(attach_media). repl_impure_name(save_image). repl_impure_name(gen_image).
|
|
2354
|
+
repl_impure_name(judge). repl_impure_name(choose). repl_impure_name(rate).
|
|
2355
|
+
repl_impure_name(verify). repl_impure_name(probability).
|
|
2356
|
+
repl_impure_name(write_file). repl_impure_name(append_file). repl_impure_name(delete_file).
|
|
2357
|
+
repl_impure_name(make_directory_path). repl_impure_name(open). repl_impure_name(close).
|
|
2358
|
+
repl_impure_name(assert). repl_impure_name(assertz). repl_impure_name(asserta).
|
|
2359
|
+
repl_impure_name(retract). repl_impure_name(retractall).
|
|
2360
|
+
repl_impure_name(consult). repl_impure_name(use_module). repl_impure_name(load_files).
|
|
2361
|
+
|
|
2362
|
+
%% repl_body_impure(+Goal) — true when the goal tree calls a side-effecting name.
|
|
2363
|
+
repl_body_impure(Goal) :-
|
|
2364
|
+
callable(Goal),
|
|
2365
|
+
functor(Goal, Name, _),
|
|
2366
|
+
repl_impure_name(Name),
|
|
2367
|
+
!.
|
|
2368
|
+
repl_body_impure((A, _)) :- repl_body_impure(A).
|
|
2369
|
+
repl_body_impure((_, B)) :- repl_body_impure(B).
|
|
2370
|
+
repl_body_impure((A ; B)) :- ( repl_body_impure(A) ; repl_body_impure(B) ).
|
|
2371
|
+
repl_body_impure((A -> B)) :- ( repl_body_impure(A) ; repl_body_impure(B) ).
|
|
2372
|
+
repl_body_impure(\+ Goal) :- repl_body_impure(Goal).
|
|
2373
|
+
repl_body_impure(once(Goal)) :- repl_body_impure(Goal).
|
|
2374
|
+
repl_body_impure(ignore(Goal)) :- repl_body_impure(Goal).
|
|
2375
|
+
repl_body_impure(forall(Gen, Action)) :- ( repl_body_impure(Gen) ; repl_body_impure(Action) ).
|
|
2376
|
+
repl_body_impure(catch(Goal, _, Recovery)) :- ( repl_body_impure(Goal) ; repl_body_impure(Recovery) ).
|
|
2377
|
+
repl_body_impure(findall(_, Goal, _)) :- repl_body_impure(Goal).
|
|
2378
|
+
repl_body_impure(findall(_, Goal, _, _)) :- repl_body_impure(Goal).
|
|
2379
|
+
repl_body_impure(call_with_inference_limit(Goal, _, _)) :- repl_body_impure(Goal).
|
|
2380
|
+
repl_body_impure(with_output_to(_, Goal)) :- repl_body_impure(Goal).
|
|
2381
|
+
repl_body_impure(_:Goal) :- repl_body_impure(Goal).
|
|
2382
|
+
|
|
2383
|
+
repl_predicate_pure(SessionId, Name, Arity) :-
|
|
2384
|
+
functor(Head, Name, Arity),
|
|
2385
|
+
\+ ( clause(SessionId:Head, Body), repl_body_impure(Body) ).
|
|
2386
|
+
|
|
2387
|
+
%% ============================================================
|
|
2388
|
+
%% Tool contract validation (end_turn advisory)
|
|
2389
|
+
%% ============================================================
|
|
2390
|
+
%%
|
|
2391
|
+
%% A tool head is what the nested agent calls; the tool body is what runs. So a
|
|
2392
|
+
%% well-formed tool is `tool(foo(A,B), "…") :- foo(A,B)` with foo/2 defined
|
|
2393
|
+
%% separately. The declared head arrives structurally through `clause/2` (its
|
|
2394
|
+
%% body is NOT executed), so a declared-but-undefined tool is caught here rather
|
|
2395
|
+
%% than at call time deep inside a nested agent.
|
|
2396
|
+
%%
|
|
2397
|
+
%% Known limitation: a module-qualified skill delegate (e.g. `other:solve(A,B)`)
|
|
2398
|
+
%% is not a session predicate and is reported; use `with_tools/2` for those.
|
|
2399
|
+
|
|
2400
|
+
%% repl_validate_tools(+SessionId, -Warnings)
|
|
2401
|
+
repl_validate_tools(SessionId, Warnings) :-
|
|
2402
|
+
findall(W, tool_contract_violation(SessionId, W), Raw),
|
|
2403
|
+
list_to_set(Raw, Warnings).
|
|
2404
|
+
|
|
2405
|
+
%% repl_validate_docs(+SessionId, -Warnings)
|
|
2406
|
+
%%
|
|
2407
|
+
%% `doc(Name/Arity, Text)` is an annotation: it documents a predicate, it does not
|
|
2408
|
+
%% create one. A `doc/2` whose predicate has no clauses therefore names a goal that
|
|
2409
|
+
%% fails silently when called — the failure surfaces far away as an empty result,
|
|
2410
|
+
%% and the name still appears in the manifest, so the listing looks healthy.
|
|
2411
|
+
%% Reported at `end_turn`, where the author can still see what they just wrote.
|
|
2412
|
+
repl_validate_docs(SessionId, Warnings) :-
|
|
2413
|
+
findall(W, doc_without_clauses(SessionId, W), Raw),
|
|
2414
|
+
list_to_set(Raw, Warnings).
|
|
2415
|
+
|
|
2416
|
+
doc_without_clauses(SessionId, Warning) :-
|
|
2417
|
+
SessionId:doc(Name/Arity, _),
|
|
2418
|
+
( \+ current_predicate(SessionId:Name/Arity)
|
|
2419
|
+
; functor(Head, Name, Arity),
|
|
2420
|
+
\+ clause(SessionId:Head, _)
|
|
2421
|
+
),
|
|
2422
|
+
!,
|
|
2423
|
+
format(string(Warning),
|
|
2424
|
+
'doc(~w/~w) documents a predicate with no clauses, so calling it fails silently and yields an empty result. Define ~w/~w, or remove the doc/2.',
|
|
2425
|
+
[Name, Arity, Name, Arity]).
|
|
2426
|
+
|
|
2427
|
+
tool_contract_violation(SessionId, Warning) :-
|
|
2428
|
+
clause(SessionId:tool(Head), Body),
|
|
2429
|
+
compound(Head),
|
|
2430
|
+
Head =.. [Name|HeadArgs],
|
|
2431
|
+
length(HeadArgs, Arity),
|
|
2432
|
+
tool_body_goal(Body, Call),
|
|
2433
|
+
Call =.. [Callee|Args],
|
|
2434
|
+
length(Args, CalleeArity),
|
|
2435
|
+
( Callee == Name, CalleeArity == Arity, \+ repl_user_predicate(SessionId, Name, Arity)
|
|
2436
|
+
-> format(string(Warning),
|
|
2437
|
+
'tool ~w/~w calls the tool itself, so a nested agent recurses forever. Define ~w/~w separately and delegate to it: tool(~w(A,B), "…") :- ~w(A,B).',
|
|
2438
|
+
[Name, Arity, Name, Name, Name, Name])
|
|
2439
|
+
; Callee == Name, CalleeArity \== Arity
|
|
2440
|
+
-> format(string(Warning),
|
|
2441
|
+
'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.',
|
|
2442
|
+
[Name, Arity, Callee, CalleeArity])
|
|
2443
|
+
; \+ ( SessionId:current_predicate(Callee/CalleeArity) )
|
|
2444
|
+
-> format(string(Warning),
|
|
2445
|
+
'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.',
|
|
2446
|
+
[Name, Arity, Callee, CalleeArity, Callee, CalleeArity])
|
|
2447
|
+
).
|
|
2448
|
+
|
|
2449
|
+
%% tool_body_goal(+Body, -FirstCall): a tool body must reach a call, not be empty.
|
|
2450
|
+
tool_body_goal(Body, Call) :-
|
|
2451
|
+
Body \== true,
|
|
2452
|
+
tool_first_goal(Body, Call),
|
|
2453
|
+
compound(Call),
|
|
2454
|
+
functor(Call, F, _),
|
|
2455
|
+
F \== (',').
|
|
2456
|
+
|
|
2457
|
+
%% tool_first_goal(+Body, -Call): the first call in a conjunction body.
|
|
2458
|
+
tool_first_goal((A, _), Call) :- !, tool_first_goal(A, Call).
|
|
2459
|
+
tool_first_goal(Call, Call).
|
|
2460
|
+
|
|
2461
|
+
%% True when the predicate has at least one clause with a real body (not a fact).
|
|
2462
|
+
repl_predicate_has_rule(SessionId, Name, Arity) :-
|
|
2463
|
+
functor(Head, Name, Arity),
|
|
2464
|
+
clause(SessionId:Head, Body),
|
|
2465
|
+
Body \== true,
|
|
2466
|
+
!.
|
|
2467
|
+
|
|
2468
|
+
%% repl_probe_nondeterminism(+SessionId, +Name, +Arity, +Threshold, +Bound, -Result)
|
|
2469
|
+
%% Result: one | many | timeout | data | unsafe | removed | error.
|
|
2470
|
+
repl_probe_nondeterminism(SessionId, Name, Arity, Threshold, Bound, Result) :-
|
|
2471
|
+
( \+ current_predicate(SessionId:Name/Arity)
|
|
2472
|
+
-> Result = removed
|
|
2473
|
+
; \+ repl_predicate_has_rule(SessionId, Name, Arity)
|
|
2474
|
+
-> Result = data
|
|
2475
|
+
; \+ repl_predicate_pure(SessionId, Name, Arity)
|
|
2476
|
+
-> Result = unsafe
|
|
2477
|
+
; functor(Head, Name, Arity),
|
|
2478
|
+
N is Threshold + 1,
|
|
2479
|
+
catch(
|
|
2480
|
+
( with_output_to(string(_),
|
|
2481
|
+
call_with_inference_limit(call_nth(SessionId:Head, N), Bound, R)),
|
|
2482
|
+
( R == false -> Result = one
|
|
2483
|
+
; R == inference_limit_exceeded -> Result = timeout
|
|
2484
|
+
; R == true -> Result = many
|
|
2485
|
+
; R == (!) -> Result = many % success is returned as `!` in this build
|
|
2486
|
+
; Result = error
|
|
2487
|
+
)
|
|
2488
|
+
),
|
|
2489
|
+
_,
|
|
2490
|
+
Result = error
|
|
2491
|
+
)
|
|
2492
|
+
).
|
|
2493
|
+
|
|
2055
2494
|
%% repl_history_listing(+SessionId)
|
|
2056
2495
|
%% Human-readable tail for `listing(history)`.
|
|
2057
2496
|
repl_history_listing(SessionId) :-
|
|
@@ -2463,8 +2902,15 @@ step_engine(SessionId, Status, Content, Payload) :-
|
|
|
2463
2902
|
|
|
2464
2903
|
%% process_engine_result(+Result, -Status, -Content, -Payload)
|
|
2465
2904
|
process_engine_result(output(Text), output, Text, none) :- !.
|
|
2905
|
+
process_engine_result(media(Payload), media, '', Payload) :- is_dict(Payload), !.
|
|
2466
2906
|
process_engine_result(log(Text), log, Text, none) :- !.
|
|
2467
2907
|
process_engine_result(answer(Text), answer, Text, none) :- !.
|
|
2908
|
+
%% Current 8-arg format (with model spec from with_model/2)
|
|
2909
|
+
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec), request_agent_loop, '',
|
|
2910
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext, modelSpec: ModelSpec}) :- !.
|
|
2911
|
+
%% Prompt-family 6-arg format (tool scope + model spec; no reasoning/recipe)
|
|
2912
|
+
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ModelSpec), request_agent_loop, '',
|
|
2913
|
+
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: none, recipeContext: "", modelSpec: ModelSpec}) :- !.
|
|
2468
2914
|
%% Note: Memory and tool scope are now passed in the payload for the agent loop
|
|
2469
2915
|
process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory, ToolScope, ReasoningEffort, RecipeContext), request_agent_loop, '',
|
|
2470
2916
|
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: ToolScope, reasoningEffort: ReasoningEffort, recipeContext: RecipeContext}) :- !.
|
|
@@ -2476,10 +2922,14 @@ process_engine_result(request_agent_loop(Desc, Vars, Tools, Memory), request_age
|
|
|
2476
2922
|
payload{taskDescription: Desc, outputVars: Vars, userTools: Tools, memory: Memory, toolScope: none, reasoningEffort: none, recipeContext: ""}) :- !.
|
|
2477
2923
|
process_engine_result(request_exec(Tool, Args), request_exec, '',
|
|
2478
2924
|
payload{toolName: Tool, args: Args}) :- !.
|
|
2925
|
+
process_engine_result(request_sample_token(Prompt, Allowed, ModelSpec), request_sample_token, '',
|
|
2926
|
+
payload{prompt: Prompt, allowedTokens: Allowed, modelSpec: ModelSpec}) :- !.
|
|
2479
2927
|
process_engine_result(request_sample_token(Prompt, Allowed), request_sample_token, '',
|
|
2480
|
-
payload{prompt: Prompt, allowedTokens: Allowed}) :- !.
|
|
2928
|
+
payload{prompt: Prompt, allowedTokens: Allowed, modelSpec: none}) :- !.
|
|
2929
|
+
process_engine_result(request_llm(Messages, ModelSpec), request_llm, '',
|
|
2930
|
+
payload{messages: Messages, modelSpec: ModelSpec}) :- !.
|
|
2481
2931
|
process_engine_result(request_llm(Messages), request_llm, '',
|
|
2482
|
-
payload{messages: Messages}) :- !.
|
|
2932
|
+
payload{messages: Messages, modelSpec: none}) :- !.
|
|
2483
2933
|
process_engine_result(request_judgment(Payload), request_judgment, '', Payload) :- !.
|
|
2484
2934
|
%% Tool result from inline tool execution
|
|
2485
2935
|
process_engine_result(tool_result(Result), tool_result, '',
|
|
@@ -2714,6 +3164,25 @@ get_reasoning_effort(State, Effort) :-
|
|
|
2714
3164
|
; Effort = none
|
|
2715
3165
|
).
|
|
2716
3166
|
|
|
3167
|
+
%% set_model_state(+StateIn, +Spec, -StateOut)
|
|
3168
|
+
set_model_state(StateIn, Spec, StateOut) :-
|
|
3169
|
+
StateOut = StateIn.put(model_spec, Spec).
|
|
3170
|
+
|
|
3171
|
+
%% clear_model_state(+StateIn, -StateOut)
|
|
3172
|
+
clear_model_state(StateIn, StateOut) :-
|
|
3173
|
+
( get_dict(model_spec, StateIn, _)
|
|
3174
|
+
-> del_dict(model_spec, StateIn, _, StateOut)
|
|
3175
|
+
; StateOut = StateIn
|
|
3176
|
+
).
|
|
3177
|
+
|
|
3178
|
+
%% get_model_spec(+State, -Spec)
|
|
3179
|
+
%% `none` means "no override" (use the session model).
|
|
3180
|
+
get_model_spec(State, Spec) :-
|
|
3181
|
+
( get_dict(model_spec, State, Spec)
|
|
3182
|
+
-> true
|
|
3183
|
+
; Spec = none
|
|
3184
|
+
).
|
|
3185
|
+
|
|
2717
3186
|
%% ============================================================
|
|
2718
3187
|
%% Recipe Context Helpers
|
|
2719
3188
|
%% ============================================================
|
|
@@ -3090,8 +3559,9 @@ mi_call(task(Desc), StateIn, StateOut) :-
|
|
|
3090
3559
|
get_tool_scope(StateIn, ToolScope),
|
|
3091
3560
|
get_reasoning_effort(StateIn, ReasoningEffort),
|
|
3092
3561
|
get_recipe_context(StateIn, RecipeContext),
|
|
3562
|
+
get_model_spec(StateIn, ModelSpec),
|
|
3093
3563
|
% Yield request with current memory and tool scope
|
|
3094
|
-
engine_yield(request_agent_loop(InterpDesc, [], UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext)),
|
|
3564
|
+
engine_yield(request_agent_loop(InterpDesc, [], UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec)),
|
|
3095
3565
|
% Handle signals (tool calls) until agent_done
|
|
3096
3566
|
handle_agent_signals(SessionId, StateIn, StateAfterAgent),
|
|
3097
3567
|
session_agent_result(SessionId, Result),
|
|
@@ -3161,7 +3631,8 @@ mi_call(prompt(Desc), StateIn, StateOut) :-
|
|
|
3161
3631
|
get_depth(StateIn, Depth),
|
|
3162
3632
|
add_trace_entry(SessionId, llm_call, prompt, [InterpDesc], Depth),
|
|
3163
3633
|
% prompt/N is a simple LLM call - no tools, no tool scope
|
|
3164
|
-
|
|
3634
|
+
get_model_spec(StateIn, ModelSpec),
|
|
3635
|
+
engine_yield(request_agent_loop(InterpDesc, [], [], [], none, ModelSpec)),
|
|
3165
3636
|
% Wait for agent_done (no tool calls expected since no tools provided)
|
|
3166
3637
|
handle_agent_signals(SessionId, StateIn, StateOut),
|
|
3167
3638
|
session_agent_result(SessionId, Result),
|
|
@@ -3196,7 +3667,9 @@ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3196
3667
|
get_depth(StateIn, Depth),
|
|
3197
3668
|
add_trace_entry(SessionId, llm_call, prompt, [InterpDesc], Depth),
|
|
3198
3669
|
% prompt/N is a simple LLM call - no tools, no tool scope
|
|
3199
|
-
|
|
3670
|
+
get_model_spec(StateIn, ModelSpec),
|
|
3671
|
+
resolve_schema_fields(SessionId, VarNames, ResolvedVarNames),
|
|
3672
|
+
engine_yield(request_agent_loop(InterpDesc, ResolvedVarNames, [], [], none, ModelSpec)),
|
|
3200
3673
|
% Wait for agent_done (no tool calls expected since no tools provided)
|
|
3201
3674
|
handle_agent_signals(SessionId, StateIn, StateOut),
|
|
3202
3675
|
session_agent_result(SessionId, Result),
|
|
@@ -3214,7 +3687,7 @@ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3214
3687
|
engine_yield(output(WarnMsg)),
|
|
3215
3688
|
fail
|
|
3216
3689
|
),
|
|
3217
|
-
bind_task_variables(Result.variables, VarNames, Vars).
|
|
3690
|
+
bind_task_variables(SessionId, Result.variables, VarNames, Vars).
|
|
3218
3691
|
% Don't modify memory - but state may have changed from tool calls
|
|
3219
3692
|
|
|
3220
3693
|
mi_call_sample_token(Desc, Allowed, Token, StateIn, StateOut) :-
|
|
@@ -3223,7 +3696,8 @@ mi_call_sample_token(Desc, Allowed, Token, StateIn, StateOut) :-
|
|
|
3223
3696
|
get_session_id(SessionId),
|
|
3224
3697
|
get_depth(StateIn, Depth),
|
|
3225
3698
|
add_trace_entry(SessionId, llm_call, sample_token, [InterpDesc, Allowed], Depth),
|
|
3226
|
-
|
|
3699
|
+
get_model_spec(StateIn, ModelSpec),
|
|
3700
|
+
engine_yield(request_sample_token(InterpDesc, Allowed, ModelSpec)),
|
|
3227
3701
|
wait_sample_token_signal(SessionId),
|
|
3228
3702
|
( session_sample_token_result(SessionId, Result)
|
|
3229
3703
|
-> retract(session_sample_token_result(SessionId, _)),
|
|
@@ -3431,7 +3905,8 @@ mi_call_llm(Messages, Reply, StateIn, StateOut) :-
|
|
|
3431
3905
|
get_session_id(SessionId),
|
|
3432
3906
|
get_depth(StateIn, Depth),
|
|
3433
3907
|
add_trace_entry(SessionId, llm_call, llm, [Messages], Depth),
|
|
3434
|
-
|
|
3908
|
+
get_model_spec(StateIn, ModelSpec),
|
|
3909
|
+
engine_yield(request_llm(Messages, ModelSpec)),
|
|
3435
3910
|
wait_llm_signal(SessionId),
|
|
3436
3911
|
( session_llm_result(SessionId, Result)
|
|
3437
3912
|
-> retract(session_llm_result(SessionId, _)),
|
|
@@ -3474,7 +3949,9 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3474
3949
|
get_tool_scope(StateIn, ToolScope),
|
|
3475
3950
|
get_reasoning_effort(StateIn, ReasoningEffort),
|
|
3476
3951
|
get_recipe_context(StateIn, RecipeContext),
|
|
3477
|
-
|
|
3952
|
+
get_model_spec(StateIn, ModelSpec),
|
|
3953
|
+
resolve_schema_fields(SessionId, VarNames, ResolvedVarNames),
|
|
3954
|
+
engine_yield(request_agent_loop(InterpDesc, ResolvedVarNames, UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec)),
|
|
3478
3955
|
% Handle signals (tool calls) until agent_done
|
|
3479
3956
|
handle_agent_signals(SessionId, StateIn, StateAfterAgent),
|
|
3480
3957
|
session_agent_result(SessionId, Result),
|
|
@@ -3491,7 +3968,7 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3491
3968
|
engine_yield(output(WarnMsg)),
|
|
3492
3969
|
fail
|
|
3493
3970
|
),
|
|
3494
|
-
bind_task_variables(Result.variables, VarNames, Vars),
|
|
3971
|
+
bind_task_variables(SessionId, Result.variables, VarNames, Vars),
|
|
3495
3972
|
% Use full memory from agent loop result when available
|
|
3496
3973
|
( get_dict(full_memory, Result, FullMemory), is_list(FullMemory)
|
|
3497
3974
|
-> set_memory(StateAfterAgent, FullMemory, StateOut),
|
|
@@ -3513,18 +3990,134 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3513
3990
|
)
|
|
3514
3991
|
).
|
|
3515
3992
|
|
|
3993
|
+
%% ---------------------------------------------------------------
|
|
3994
|
+
%% Output schemas: schema(Name, [field(Field, Type, Option), ...])
|
|
3995
|
+
%%
|
|
3996
|
+
%% `list(object(B))` says "a list of dicts" but not what is in them, so every
|
|
3997
|
+
%% harness re-derives defaults and tolerates missing fields by hand. A schema
|
|
3998
|
+
%% declares the shape once, and `schema_fill/3` makes the value match it: absent
|
|
3999
|
+
%% fields with a default are filled, unknown fields are kept (so a model mistake
|
|
4000
|
+
%% stays visible rather than disappearing), and a `required` field that is absent
|
|
4001
|
+
%% or a field of the wrong type **fails the step** rather than yielding an empty
|
|
4002
|
+
%% result.
|
|
4003
|
+
%%
|
|
4004
|
+
%% schema(author, [
|
|
4005
|
+
%% field(name, string, required),
|
|
4006
|
+
%% field(affiliation, string, default("not stated")),
|
|
4007
|
+
%% field(email, string, default(""))
|
|
4008
|
+
%% ]).
|
|
4009
|
+
%%
|
|
4010
|
+
%% task("From the front matter extract every author: {Text}",
|
|
4011
|
+
%% list(schema(author, Authors))).
|
|
4012
|
+
%% schema_fill(+Fields, +Value, -Filled): the pure part, so it is testable and
|
|
4013
|
+
%% usable without a session. The session predicate schema_fill/3 looks the fields
|
|
4014
|
+
%% up from `schema/2`.
|
|
4015
|
+
schema_fill_fields(Fields, Value, Filled) :-
|
|
4016
|
+
is_dict(Value),
|
|
4017
|
+
( member(field(Field, Type, required), Fields),
|
|
4018
|
+
\+ dict_field(Value, Field, _),
|
|
4019
|
+
!,
|
|
4020
|
+
format(atom(Msg), 'schema: required field "~w" is missing', [Field]),
|
|
4021
|
+
throw(error(schema_error(Msg), schema_fill/3))
|
|
4022
|
+
; true
|
|
4023
|
+
),
|
|
4024
|
+
( member(field(Field, Type, _), Fields),
|
|
4025
|
+
dict_field(Value, Field, Found),
|
|
4026
|
+
\+ schema_value_ok(Type, Found),
|
|
4027
|
+
!,
|
|
4028
|
+
format(atom(Msg), 'schema: field "~w" is not ~w (~w)', [Field, Type, Found]),
|
|
4029
|
+
throw(error(schema_error(Msg), schema_fill/3))
|
|
4030
|
+
; true
|
|
4031
|
+
),
|
|
4032
|
+
dict_pairs(Value, _, Pairs),
|
|
4033
|
+
findall(Key-NewValue,
|
|
4034
|
+
( member(Key-Old, Pairs),
|
|
4035
|
+
( member(field(Key, _, Option), Fields)
|
|
4036
|
+
-> ( Option = default(Default), Old == "" -> NewValue = Default
|
|
4037
|
+
; NewValue = Old
|
|
4038
|
+
)
|
|
4039
|
+
; NewValue = Old % undeclared: kept, so it stays visible
|
|
4040
|
+
)
|
|
4041
|
+
),
|
|
4042
|
+
Updated),
|
|
4043
|
+
findall(Field-Default,
|
|
4044
|
+
( member(field(Field, _, default(Default)), Fields),
|
|
4045
|
+
\+ dict_field(Value, Field, _)
|
|
4046
|
+
),
|
|
4047
|
+
Missing),
|
|
4048
|
+
append(Updated, Missing, All),
|
|
4049
|
+
dict_pairs(Filled, _, All).
|
|
4050
|
+
|
|
4051
|
+
%% A declared type accepts its own shape; anything coarser (a number for a string
|
|
4052
|
+
%% field, for instance) fails, because silently coercing hides the model's error.
|
|
4053
|
+
schema_value_ok(string, V) :- ( string(V) ; atom(V) ), !.
|
|
4054
|
+
schema_value_ok(integer, V) :- integer(V), !.
|
|
4055
|
+
schema_value_ok(number, V) :- number(V), !.
|
|
4056
|
+
schema_value_ok(boolean, V) :- ( V == true ; V == false ; V == yes ; V == no ), !.
|
|
4057
|
+
schema_value_ok(object, V) :- is_dict(V), !.
|
|
4058
|
+
schema_value_ok(array, V) :- is_list(V), !.
|
|
4059
|
+
schema_value_ok(_, _) :- fail.
|
|
4060
|
+
|
|
3516
4061
|
%% bind_task_variables(+VarsDict, +Names, -Values)
|
|
3517
|
-
bind_task_variables(_, [], []) :- !.
|
|
3518
|
-
bind_task_variables(VarsDict, [N|Names], [Value|Values]) :-
|
|
4062
|
+
bind_task_variables(_, _, [], []) :- !.
|
|
4063
|
+
bind_task_variables(SessionId, VarsDict, [N|Names], [Value|Values]) :-
|
|
3519
4064
|
( is_dict(N)
|
|
3520
4065
|
-> Key = N.name
|
|
3521
4066
|
; Key = N
|
|
3522
4067
|
),
|
|
3523
|
-
( get_dict(Key, VarsDict,
|
|
4068
|
+
( get_dict(Key, VarsDict, Raw)
|
|
3524
4069
|
-> true
|
|
3525
|
-
;
|
|
4070
|
+
; Raw = _Missing
|
|
3526
4071
|
),
|
|
3527
|
-
|
|
4072
|
+
( is_dict(N), get_dict(schema, N, SchemaName), nonvar(Raw)
|
|
4073
|
+
-> schema_fields_of(SessionId, SchemaName, Fields),
|
|
4074
|
+
( is_list(Raw)
|
|
4075
|
+
-> maplist(schema_filled(Fields), Raw, Value)
|
|
4076
|
+
; schema_fill_fields(Fields, Raw, Value)
|
|
4077
|
+
)
|
|
4078
|
+
; Value = Raw
|
|
4079
|
+
),
|
|
4080
|
+
bind_task_variables(SessionId, VarsDict, Names, Values).
|
|
4081
|
+
|
|
4082
|
+
schema_fields_of(SessionId, Name, Fields) :-
|
|
4083
|
+
( catch(clause(SessionId:schema(Name, Fields), true), _, fail)
|
|
4084
|
+
-> true
|
|
4085
|
+
; format(atom(Msg), 'schema "~w" is not declared', [Name]),
|
|
4086
|
+
throw(error(schema_error(Msg), schema_fields_of/3))
|
|
4087
|
+
).
|
|
4088
|
+
|
|
4089
|
+
%% resolve_schema_fields(+SessionId, +VarNames, -Resolved)
|
|
4090
|
+
%%
|
|
4091
|
+
%% Attach a schema's field list to the descriptor for each output variable that
|
|
4092
|
+
%% names one, so the agent building the prompt can tell the model the shape to
|
|
4093
|
+
%% produce. Done here rather than in extract_var_info/4 (which runs at load time)
|
|
4094
|
+
%% because the `schema/2` fact is only certainly present once everything is loaded.
|
|
4095
|
+
resolve_schema_fields(SessionId, VarNames, Resolved) :-
|
|
4096
|
+
maplist(resolve_schema_field(SessionId), VarNames, Resolved).
|
|
4097
|
+
|
|
4098
|
+
resolve_schema_field(SessionId, VarName, Resolved) :-
|
|
4099
|
+
( is_dict(VarName), get_dict(schema, VarName, SchemaName)
|
|
4100
|
+
-> ( schema_field_dicts(SessionId, SchemaName, Fields)
|
|
4101
|
+
-> put_dict(fields, VarName, Fields, Resolved)
|
|
4102
|
+
; Resolved = VarName
|
|
4103
|
+
)
|
|
4104
|
+
; Resolved = VarName
|
|
4105
|
+
).
|
|
4106
|
+
|
|
4107
|
+
%% Field descriptors as dicts, so they cross the bridge without a decoder.
|
|
4108
|
+
%% `required` is 1/0 rather than an atom: the atom `false` arrives as the string
|
|
4109
|
+
%% "false", which is truthy in JavaScript.
|
|
4110
|
+
schema_field_dicts(SessionId, SchemaName, Dicts) :-
|
|
4111
|
+
catch(clause(SessionId:schema(SchemaName, Fields), true), _, fail),
|
|
4112
|
+
findall(x{name: Field, type: Type, required: Required, default: Default},
|
|
4113
|
+
( member(field(Field, Type, Option), Fields),
|
|
4114
|
+
( Option == required -> Required = 1 ; Required = 0 ),
|
|
4115
|
+
( Option = default(D) -> Default = D ; Default = no_default )
|
|
4116
|
+
),
|
|
4117
|
+
Dicts).
|
|
4118
|
+
|
|
4119
|
+
schema_filled(Fields, Raw, Filled) :-
|
|
4120
|
+
( is_dict(Raw) -> schema_fill_fields(Fields, Raw, Filled) ; Filled = Raw ).
|
|
3528
4121
|
|
|
3529
4122
|
%% ============================================================
|
|
3530
4123
|
%% Exec Handling
|
|
@@ -3572,6 +4165,19 @@ mi_call(with_tools(ToolList, Goal), StateIn, StateOut) :-
|
|
|
3572
4165
|
mi_call(Goal, ScopedState, StateWithScope),
|
|
3573
4166
|
clear_tool_scope(StateWithScope, StateOut).
|
|
3574
4167
|
|
|
4168
|
+
%% mi_call(with_fresh_memory(Goal), +StateIn, -StateOut)
|
|
4169
|
+
%% Run Goal with an empty memory, and discard whatever it accumulates. The steps
|
|
4170
|
+
%% inside see nothing from the caller, and the caller sees nothing from them.
|
|
4171
|
+
%% Use it to scope a group of task/N calls that must not be influenced by the
|
|
4172
|
+
%% conversation so far, or to keep a long history from growing through a
|
|
4173
|
+
%% sub-computation. `prompt/N` is the single-call equivalent.
|
|
4174
|
+
mi_call(with_fresh_memory(Goal), StateIn, StateOut) :-
|
|
4175
|
+
!,
|
|
4176
|
+
get_memory(StateIn, SavedMemory),
|
|
4177
|
+
set_memory(StateIn, [], FreshState),
|
|
4178
|
+
mi_call(Goal, FreshState, AfterGoal),
|
|
4179
|
+
set_memory(AfterGoal, SavedMemory, StateOut).
|
|
4180
|
+
|
|
3575
4181
|
%% mi_call(without_tools(ToolList, Goal), +StateIn, -StateOut)
|
|
3576
4182
|
%% Run Goal with the specified tools excluded from nested tasks
|
|
3577
4183
|
mi_call(without_tools(ToolList, Goal), StateIn, StateOut) :-
|
|
@@ -3589,6 +4195,17 @@ mi_call(with_reasoning(Goal, Effort), StateIn, StateOut) :-
|
|
|
3589
4195
|
mi_call(Goal, ScopedState, StateWithScope),
|
|
3590
4196
|
clear_reasoning_effort_state(StateWithScope, StateOut).
|
|
3591
4197
|
|
|
4198
|
+
%% mi_call(with_model(Spec, Goal), +StateIn, -StateOut)
|
|
4199
|
+
%% Run Goal with a specific model, and optionally a reasoning level. `Spec` is a
|
|
4200
|
+
%% string — "provider:model", "provider:model#reasoning", "model",
|
|
4201
|
+
%% "model#reasoning" — or a model{provider, id, temperature, maxTokens, reasoning}
|
|
4202
|
+
%% dict. It applies to task/N, prompt/N and sample_token/N (not judgments).
|
|
4203
|
+
mi_call(with_model(Spec, Goal), StateIn, StateOut) :-
|
|
4204
|
+
!,
|
|
4205
|
+
set_model_state(StateIn, Spec, ScopedState),
|
|
4206
|
+
mi_call(Goal, ScopedState, StateWithScope),
|
|
4207
|
+
clear_model_state(StateWithScope, StateOut).
|
|
4208
|
+
|
|
3592
4209
|
%% mi_call(with_recipe(Goal, Query), +StateIn, -StateOut)
|
|
3593
4210
|
%% Run Goal with a recipe loaded as transient context (not persisted to memory).
|
|
3594
4211
|
mi_call(with_recipe(Goal, Query), StateIn, StateOut) :-
|
|
@@ -3647,9 +4264,36 @@ mi_call(attach_media(Media), StateIn, StateOut) :-
|
|
|
3647
4264
|
append(Memory, [Message], NewMemory),
|
|
3648
4265
|
set_memory(StateIn, NewMemory, StateOut).
|
|
3649
4266
|
|
|
4267
|
+
%% mi_call(save_image(+Media, +OutPath), +StateIn, -StateOut)
|
|
4268
|
+
%% Write a media reference to a workspace file: `save_image(media(Path, Mime), "out.png")`,
|
|
4269
|
+
%% `save_image(media{svg: Svg}, "out.svg")`. Parent directories must exist.
|
|
4270
|
+
mi_call(save_image(Media, Out), StateIn, StateIn) :-
|
|
4271
|
+
!,
|
|
4272
|
+
media_dict(Media, Dict),
|
|
4273
|
+
( get_dict(kind, Dict, inline_svg),
|
|
4274
|
+
get_dict(data, Dict, Svg)
|
|
4275
|
+
-> open(Out, write, Stream),
|
|
4276
|
+
write(Stream, Svg),
|
|
4277
|
+
close(Stream)
|
|
4278
|
+
; get_dict(path, Dict, Path),
|
|
4279
|
+
Path \== ""
|
|
4280
|
+
-> copy_file(Path, Out)
|
|
4281
|
+
; throw(error(domain_error(save_image, Media), save_image/2))
|
|
4282
|
+
).
|
|
4283
|
+
|
|
3650
4284
|
%% media_dict(+Media, -Dict)
|
|
3651
|
-
%%
|
|
4285
|
+
%% Normalizes an image reference for display/attachment:
|
|
4286
|
+
%% media(Path, Mime) | media(Path) | a bare Path string
|
|
4287
|
+
%% media{path: Path} | media{path: Path, mime: Mime}
|
|
4288
|
+
%% media{svg: SvgString} (inline SVG)
|
|
4289
|
+
%% media{data: Base64} | media{data: Base64, mime: Mime} (inline bytes)
|
|
3652
4290
|
media_dict(media(Path, Mime), media{path: Path, mime: Mime}) :- !.
|
|
4291
|
+
media_dict(media(Path), media{path: Path, mime: Mime}) :- !, media_mime(Path, Mime).
|
|
4292
|
+
media_dict(media{path: Path, mime: Mime}, media{path: Path, mime: Mime}) :- !.
|
|
4293
|
+
media_dict(media{path: Path}, media{path: Path, mime: Mime}) :- !, media_mime(Path, Mime).
|
|
4294
|
+
media_dict(media{svg: Svg}, media{path: "", mime: "image/svg+xml", kind: inline_svg, data: Svg}) :- !.
|
|
4295
|
+
media_dict(media{data: Data, mime: Mime}, media{path: "", mime: Mime, kind: inline_data, data: Data}) :- !.
|
|
4296
|
+
media_dict(media{data: Data}, media{path: "", mime: "image/png", kind: inline_data, data: Data}) :- !.
|
|
3653
4297
|
media_dict(Path, media{path: Path, mime: Mime}) :- media_mime(Path, Mime).
|
|
3654
4298
|
|
|
3655
4299
|
%% media_mime(+Path, -Mime)
|
|
@@ -3657,6 +4301,7 @@ media_dict(Path, media{path: Path, mime: Mime}) :- media_mime(Path, Mime).
|
|
|
3657
4301
|
media_mime(Path, Mime) :-
|
|
3658
4302
|
( string(Path) -> atom_string(Atom, Path) ; Atom = Path ),
|
|
3659
4303
|
( sub_atom(Atom, _, _, 0, '.png') -> Mime = "image/png"
|
|
4304
|
+
; sub_atom(Atom, _, _, 0, '.svg') -> Mime = "image/svg+xml"
|
|
3660
4305
|
; sub_atom(Atom, _, _, 0, '.jpg') -> Mime = "image/jpeg"
|
|
3661
4306
|
; sub_atom(Atom, _, _, 0, '.jpeg') -> Mime = "image/jpeg"
|
|
3662
4307
|
; sub_atom(Atom, _, _, 0, '.gif') -> Mime = "image/gif"
|
|
@@ -3676,6 +4321,29 @@ mi_call(answer(Text), StateIn, StateIn) :-
|
|
|
3676
4321
|
engine_yield(answer(InterpText)),
|
|
3677
4322
|
throw('$answer_commit').
|
|
3678
4323
|
|
|
4324
|
+
%% mi_call(output(Media), +StateIn, -StateOut)
|
|
4325
|
+
%% Present an image instead of text: `output(media(Path, Mime))`, `output(media(Path))`,
|
|
4326
|
+
%% or a dict form (`media{svg: Svg}`, `media{data: Base64, mime: Mime}`). The host
|
|
4327
|
+
%% renders it inline (kitty/iTerm2) or reports the path.
|
|
4328
|
+
%% This clause must come before the generic `output(Text)` clause; it fails
|
|
4329
|
+
%% through for anything that is not a media reference.
|
|
4330
|
+
mi_call(output(Media), StateIn, StateIn) :-
|
|
4331
|
+
nonvar(Media),
|
|
4332
|
+
( Media = media(_, _)
|
|
4333
|
+
; Media = media(_)
|
|
4334
|
+
; is_dict(Media)
|
|
4335
|
+
),
|
|
4336
|
+
media_dict(Media, Dict),
|
|
4337
|
+
!,
|
|
4338
|
+
get_dict(mime, Dict, Mime),
|
|
4339
|
+
get_dict(path, Dict, Path),
|
|
4340
|
+
( get_dict(data, Dict, Data) -> true ; Data = "" ),
|
|
4341
|
+
( get_dict(kind, Dict, Kind) -> true ; Kind = file ),
|
|
4342
|
+
get_session_id(SessionId),
|
|
4343
|
+
get_depth(StateIn, Depth),
|
|
4344
|
+
add_trace_entry(SessionId, media, media, [Path], Depth),
|
|
4345
|
+
engine_yield(media(media{path: Path, mimeType: Mime, kind: Kind, data: Data})).
|
|
4346
|
+
|
|
3679
4347
|
%% mi_call(output(Text), +StateIn, -StateOut)
|
|
3680
4348
|
mi_call(output(Text), StateIn, StateIn) :-
|
|
3681
4349
|
get_params(StateIn, Params),
|
|
@@ -4099,28 +4767,428 @@ mi_foldl(Goal, [H|T], V0, V, StateIn, StateOut) :-
|
|
|
4099
4767
|
mi_foldl(Goal, T, V1, V, State1, StateOut).
|
|
4100
4768
|
|
|
4101
4769
|
%% ============================================================
|
|
4102
|
-
%%
|
|
4770
|
+
%% Workspace predicates (one definition, every surface)
|
|
4103
4771
|
%% ============================================================
|
|
4772
|
+
%%
|
|
4773
|
+
%% These are **real clauses in the session module**, not only meta-interpreter
|
|
4774
|
+
%% handlers. `read_file/2` in particular has no other definition in this build:
|
|
4775
|
+
%% without these clauses it exists only as an `mi_call/3` body, so it works from
|
|
4776
|
+
%% DML but fails from a host query. Installing them makes a host query, `execute`,
|
|
4777
|
+
%% `/goal` and a nested `task/N` resolve the same predicate.
|
|
4778
|
+
%%
|
|
4779
|
+
%% The file-stream primitives live in `system:` (NOT `readutil`, which has no
|
|
4780
|
+
%% `read_file_to_string/3`). They are module-qualified so a wrapper cannot
|
|
4781
|
+
%% recurse into itself.
|
|
4782
|
+
%%
|
|
4783
|
+
%% Deliberately not here: `open/3`, `close/1`, `read_string/3`, `write/2`. They
|
|
4784
|
+
%% are imported builtins in the session module and SWI refuses to shadow an
|
|
4785
|
+
%% imported private predicate, so a workspace-resolving version cannot be
|
|
4786
|
+
%% installed. `read_file/2` and `write_file/2` are the whole-file API instead.
|
|
4787
|
+
%%
|
|
4788
|
+
%% `workspace_predicate_name/1` is the single source of truth; it also keeps the
|
|
4789
|
+
%% set out of `listing` / `export` / history.
|
|
4790
|
+
|
|
4791
|
+
workspace_predicate_name(read_file).
|
|
4792
|
+
workspace_predicate_name(read_file_to_string).
|
|
4793
|
+
workspace_predicate_name(write_file).
|
|
4794
|
+
workspace_predicate_name(append_file).
|
|
4795
|
+
workspace_predicate_name(exists_file).
|
|
4796
|
+
workspace_predicate_name(exists_directory).
|
|
4797
|
+
workspace_predicate_name(directory_files).
|
|
4798
|
+
workspace_predicate_name(make_directory).
|
|
4799
|
+
workspace_predicate_name(make_directory_path).
|
|
4800
|
+
workspace_predicate_name(delete_file).
|
|
4801
|
+
workspace_predicate_name(delete_directory).
|
|
4802
|
+
workspace_predicate_name(size_file).
|
|
4803
|
+
workspace_predicate_name(time_file).
|
|
4804
|
+
workspace_predicate_name(dict_field).
|
|
4805
|
+
workspace_predicate_name(dict_field_key).
|
|
4806
|
+
workspace_predicate_name(json_get).
|
|
4807
|
+
workspace_predicate_name(json_gets).
|
|
4808
|
+
workspace_predicate_name(json_put).
|
|
4809
|
+
workspace_predicate_name(json_text).
|
|
4810
|
+
workspace_predicate_name(schema_fill).
|
|
4811
|
+
workspace_predicate_name(pattern).
|
|
4812
|
+
|
|
4813
|
+
%% dict_field(+Dict, +Key, -Value) and dict_field(+Dict, +Key, +Default, -Value)
|
|
4814
|
+
%%
|
|
4815
|
+
%% Look up a field of a model-produced dict. SWI dicts have *atom* keys and
|
|
4816
|
+
%% `get_dict/3` is strict: `get_dict("affiliation", D)` raises
|
|
4817
|
+
%% `type_error(dict-key, affiliation)` rather than failing, and the model varies
|
|
4818
|
+
%% the spelling (`Name` / `name` / `full_name`). This matches on a normalised
|
|
4819
|
+
%% atom — any of atom/string, any case, `_`/`-`/space ignored — so
|
|
4820
|
+
%% `dict_field(D, "affiliation", A)` finds the `affiliation` key.
|
|
4821
|
+
dict_field(Dict, Key, Value) :-
|
|
4822
|
+
is_dict(Dict),
|
|
4823
|
+
dict_field_key(Key, Wanted),
|
|
4824
|
+
dict_pairs(Dict, _, Pairs),
|
|
4825
|
+
member(K-V, Pairs),
|
|
4826
|
+
dict_field_key(K, Wanted),
|
|
4827
|
+
Value = V.
|
|
4828
|
+
|
|
4829
|
+
dict_field(Dict, Key, Default, Value) :-
|
|
4830
|
+
( dict_field(Dict, Key, Found)
|
|
4831
|
+
-> Value = Found
|
|
4832
|
+
; Value = Default
|
|
4833
|
+
).
|
|
4834
|
+
|
|
4835
|
+
%% dict_field_key(+Key, -Normalised): lowercase atom with separators removed.
|
|
4836
|
+
dict_field_key(Key, Normalised) :-
|
|
4837
|
+
( atom(Key) -> atom_string(Key, Text) ; Text = Key ),
|
|
4838
|
+
string_lower(Text, Lower),
|
|
4839
|
+
split_string(Lower, " _-", "", Parts),
|
|
4840
|
+
atomic_list_concat(Parts, '', Normalised).
|
|
4841
|
+
|
|
4842
|
+
%% json_get/3,4, json_gets/3, json_put/4, json_text/2
|
|
4843
|
+
%%
|
|
4844
|
+
%% Path access into a model-produced structure, so a nested lookup is one call
|
|
4845
|
+
%% rather than a chain of dict_field/3. A path is a list of keys and indices, or a
|
|
4846
|
+
%% string/atom with `.` separators (`"results.0.authors.1.name"`), which is how a
|
|
4847
|
+
%% JSON path is normally written.
|
|
4848
|
+
%%
|
|
4849
|
+
%% A missing path **fails** — it does not throw and does not yield "" — so absence
|
|
4850
|
+
%% is visible at the call site; json_get/4 supplies a default instead. Key matching
|
|
4851
|
+
%% is the same tolerant match as dict_field/3 (atom or string, any case,
|
|
4852
|
+
%% `_`/`-`/space ignored).
|
|
4853
|
+
json_path_segments(Path, Segments) :-
|
|
4854
|
+
( is_list(Path)
|
|
4855
|
+
-> Segments = Path
|
|
4856
|
+
; ( atom(Path) -> atom_string(Path, Text) ; Text = Path ),
|
|
4857
|
+
split_string(Text, ".", "", RawParts),
|
|
4858
|
+
exclude(==(""), RawParts, Parts),
|
|
4859
|
+
maplist(json_path_segment, Parts, Segments)
|
|
4860
|
+
).
|
|
4861
|
+
|
|
4862
|
+
json_path_segment("*", star) :- !.
|
|
4863
|
+
json_path_segment(Part, Index) :-
|
|
4864
|
+
catch(number_string(N, Part), _, fail),
|
|
4865
|
+
integer(N),
|
|
4866
|
+
!,
|
|
4867
|
+
Index = N.
|
|
4868
|
+
json_path_segment(Part, Part).
|
|
4869
|
+
|
|
4870
|
+
%% json_walk(+Value, +Segments, -Found)
|
|
4871
|
+
json_walk(Value, [], Value) :- !.
|
|
4872
|
+
json_walk(Doc, [Index|Rest], Found) :-
|
|
4873
|
+
integer(Index),
|
|
4874
|
+
is_list(Doc),
|
|
4875
|
+
!,
|
|
4876
|
+
nth0(Index, Doc, Next),
|
|
4877
|
+
json_walk(Next, Rest, Found).
|
|
4878
|
+
json_walk(Doc, [Key|Rest], Found) :-
|
|
4879
|
+
is_dict(Doc),
|
|
4880
|
+
dict_field_key(Key, Wanted),
|
|
4881
|
+
dict_pairs(Doc, _, Pairs),
|
|
4882
|
+
member(K-V, Pairs),
|
|
4883
|
+
dict_field_key(K, Wanted),
|
|
4884
|
+
json_walk(V, Rest, Found).
|
|
4885
|
+
|
|
4886
|
+
json_get(Doc, Path, Value) :-
|
|
4887
|
+
json_path_segments(Path, Segments),
|
|
4888
|
+
\+ member(star, Segments),
|
|
4889
|
+
json_walk(Doc, Segments, Value).
|
|
4890
|
+
|
|
4891
|
+
json_get(Doc, Path, Default, Value) :-
|
|
4892
|
+
( json_get(Doc, Path, Found)
|
|
4893
|
+
-> Value = Found
|
|
4894
|
+
; Value = Default
|
|
4895
|
+
).
|
|
4896
|
+
|
|
4897
|
+
%% json_gets/3: `*` maps over a list, collecting the remainder for every element.
|
|
4898
|
+
%% Nested `*`s flatten, so "results.*.authors.*.name" is a flat list of names.
|
|
4899
|
+
json_gets(Doc, Path, Values) :-
|
|
4900
|
+
json_path_segments(Path, Segments),
|
|
4901
|
+
json_collect(Doc, Segments, Values).
|
|
4902
|
+
|
|
4903
|
+
json_collect(Value, [], [Value]) :- !.
|
|
4904
|
+
json_collect(Doc, [star|Rest], Values) :-
|
|
4905
|
+
is_list(Doc),
|
|
4906
|
+
findall(Collected,
|
|
4907
|
+
( member(Item, Doc), json_collect(Item, Rest, Collected) ),
|
|
4908
|
+
Nested),
|
|
4909
|
+
append(Nested, Values).
|
|
4910
|
+
json_collect(Doc, [Seg|Rest], Values) :-
|
|
4911
|
+
json_walk_1(Doc, Seg, Next),
|
|
4912
|
+
json_collect(Next, Rest, Values).
|
|
4913
|
+
|
|
4914
|
+
%% One step of a walk, for the collector (where `star` is handled by the caller).
|
|
4915
|
+
json_walk_1(Doc, Index, Value) :- integer(Index), is_list(Doc), !, nth0(Index, Doc, Value).
|
|
4916
|
+
json_walk_1(Doc, Key, Value) :-
|
|
4917
|
+
is_dict(Doc),
|
|
4918
|
+
dict_field_key(Key, Wanted),
|
|
4919
|
+
dict_pairs(Doc, _, Pairs),
|
|
4920
|
+
member(K-V, Pairs),
|
|
4921
|
+
dict_field_key(K, Wanted),
|
|
4922
|
+
Value = V.
|
|
4923
|
+
|
|
4924
|
+
%% json_put/4: set a key path. Lists are read-only here — an index path is
|
|
4925
|
+
%% rejected, because building output is about setting fields, not splicing lists.
|
|
4926
|
+
json_put(Doc, Path, Value, Updated) :-
|
|
4927
|
+
json_path_segments(Path, Segments),
|
|
4928
|
+
Segments \== [],
|
|
4929
|
+
\+ member(star, Segments),
|
|
4930
|
+
% NB: `\+ ( member(S, Segments), integer(S) )`. Writing this as
|
|
4931
|
+
% `\+ member(S, Segments), integer(S)` binds S inside the negation, so the
|
|
4932
|
+
% whole guard fails for any non-empty path.
|
|
4933
|
+
\+ ( member(Seg, Segments), integer(Seg) ),
|
|
4934
|
+
json_put_walk(Doc, Segments, Value, Updated).
|
|
4935
|
+
|
|
4936
|
+
json_put_walk(_Doc, [], Value, Value) :- !.
|
|
4937
|
+
json_put_walk(Doc, [Key|Rest], Value, Updated) :-
|
|
4938
|
+
is_dict(Doc),
|
|
4939
|
+
( json_walk_1(Doc, Key, Old)
|
|
4940
|
+
-> json_put_walk(Old, Rest, Value, NewValue)
|
|
4941
|
+
; json_put_walk(_{}, Rest, Value, NewValue)
|
|
4942
|
+
),
|
|
4943
|
+
dict_field_key(Key, Key0),
|
|
4944
|
+
put_dict(Key0, Doc, NewValue, Updated).
|
|
4945
|
+
|
|
4946
|
+
%% json_text(+Dict, -Text): compact JSON. For layout or streaming use
|
|
4947
|
+
%% json_write_dict/3 from library(http/json).
|
|
4948
|
+
json_text(Dict, Text) :- atom_json_dict(Text, Dict, []).
|
|
4949
|
+
|
|
4950
|
+
%% pattern(+Template, ?Text, ?Bindings)
|
|
4951
|
+
%%
|
|
4952
|
+
%% One syntax for building a string and for parsing it. Literal text matches
|
|
4953
|
+
%% exactly, and `$Name` marks a slot; the direction follows from what is bound:
|
|
4954
|
+
%%
|
|
4955
|
+
%% pattern("Name: $Name, age: $Age", Text, [name="Ada", age=36]) % build
|
|
4956
|
+
%% pattern("Name: $Name, age: $Age", "Name: Ada, age: 36", B) % parse
|
|
4957
|
+
%% pattern("DOI: $Doi:doi", Text, [doi=D]) % typed slot
|
|
4958
|
+
%%
|
|
4959
|
+
%% `$` is the marker rather than `{…}` so a template can contain JSON or any other
|
|
4960
|
+
%% brace-bearing text verbatim, and so it never collides with the `{Var}`
|
|
4961
|
+
%% interpolation used in task/N descriptions. `$$` is a literal dollar.
|
|
4962
|
+
%%
|
|
4963
|
+
%% A template must be anchored by literal text: two slots separated only by nothing
|
|
4964
|
+
%% (`"$A$B"`) is rejected, rather than resolved arbitrarily. On parse a slot
|
|
4965
|
+
%% consumes up to the next literal, so the literals decide the split.
|
|
4966
|
+
pattern(Template, Text, Bindings) :-
|
|
4967
|
+
pattern_tokens(Template, Tokens),
|
|
4968
|
+
( var(Text)
|
|
4969
|
+
-> pattern_build(Tokens, Bindings, Text)
|
|
4970
|
+
; pattern_parse(Tokens, Text, Bindings)
|
|
4971
|
+
).
|
|
4972
|
+
|
|
4973
|
+
%% pattern_tokens(+Template, -Tokens): [lit(CharList)|slot(NameChars, TypeChars)]
|
|
4974
|
+
pattern_tokens(Template, Tokens) :-
|
|
4975
|
+
( atom(Template) -> atom_string(Template, Text) ; Text = Template ),
|
|
4976
|
+
string_chars(Text, Chars),
|
|
4977
|
+
pattern_scan(Chars, [], Tokens),
|
|
4978
|
+
pattern_check_anchored(Tokens).
|
|
4979
|
+
|
|
4980
|
+
pattern_scan([], Acc, [lit(Text)]) :-
|
|
4981
|
+
!,
|
|
4982
|
+
reverse(Acc, Text).
|
|
4983
|
+
pattern_scan(['$', '$' | Rest], Acc, Tokens) :-
|
|
4984
|
+
!,
|
|
4985
|
+
pattern_scan(Rest, ['$' | Acc], Tokens).
|
|
4986
|
+
pattern_scan(['$' | Rest], Acc, Tokens) :-
|
|
4987
|
+
!,
|
|
4988
|
+
pattern_take_name(Rest, NameChars, Rest1),
|
|
4989
|
+
NameChars \== [],
|
|
4990
|
+
atom_chars(Name, NameChars),
|
|
4991
|
+
( Rest1 = [':' | Rest2],
|
|
4992
|
+
pattern_take_name(Rest2, TypeChars, Rest3),
|
|
4993
|
+
TypeChars \== []
|
|
4994
|
+
-> atom_chars(Type, TypeChars)
|
|
4995
|
+
; Rest3 = Rest1, Type = none
|
|
4996
|
+
),
|
|
4997
|
+
reverse(Acc, Text),
|
|
4998
|
+
Tokens = [lit(Text), slot(Name, Type) | More],
|
|
4999
|
+
pattern_scan(Rest3, [], More).
|
|
5000
|
+
pattern_scan([C | Rest], Acc, Tokens) :-
|
|
5001
|
+
pattern_scan(Rest, [C | Acc], Tokens).
|
|
5002
|
+
|
|
5003
|
+
%% A slot name is a run of characters that are not a template delimiter. Scanning by
|
|
5004
|
+
%% delimiter rather than by `char_type(C, alnum)` avoids depending on a ctypes
|
|
5005
|
+
%% predicate that is not guaranteed in this build.
|
|
5006
|
+
pattern_take_name([C | Rest], [C | More], Left) :-
|
|
5007
|
+
\+ pattern_delimiter(C),
|
|
5008
|
+
!,
|
|
5009
|
+
pattern_take_name(Rest, More, Left).
|
|
5010
|
+
pattern_take_name(Rest, [], Rest).
|
|
5011
|
+
|
|
5012
|
+
pattern_delimiter(' '). pattern_delimiter('\t'). pattern_delimiter('\n').
|
|
5013
|
+
pattern_delimiter(':'). pattern_delimiter(','). pattern_delimiter('.').
|
|
5014
|
+
pattern_delimiter(')'). pattern_delimiter(']'). pattern_delimiter('}').
|
|
5015
|
+
pattern_delimiter('$'). pattern_delimiter('-'). pattern_delimiter('/').
|
|
5016
|
+
pattern_delimiter('='). pattern_delimiter('\'').
|
|
5017
|
+
|
|
5018
|
+
%% Two slots with nothing between them cannot be told apart on parse.
|
|
5019
|
+
pattern_check_anchored(Tokens) :-
|
|
5020
|
+
pattern_adjacent_slots(Tokens, A, B),
|
|
5021
|
+
!,
|
|
5022
|
+
format(atom(Msg),
|
|
5023
|
+
'pattern: slots "~w" and "~w" are adjacent with no literal between them, so the split is ambiguous',
|
|
5024
|
+
[A, B]),
|
|
5025
|
+
throw(error(domain_error(pattern_unanchored, Msg), pattern_tokens/2)).
|
|
5026
|
+
pattern_check_anchored(_).
|
|
5027
|
+
|
|
5028
|
+
pattern_adjacent_slots([slot(A, _), lit([]), slot(B, _) | _], A, B) :- !.
|
|
5029
|
+
pattern_adjacent_slots([_ | Rest], A, B) :- pattern_adjacent_slots(Rest, A, B).
|
|
5030
|
+
|
|
5031
|
+
%% pattern_build(+Tokens, +Bindings, -Text)
|
|
5032
|
+
pattern_build(Tokens, Bindings, Text) :-
|
|
5033
|
+
findall(Chars,
|
|
5034
|
+
( member(Token, Tokens),
|
|
5035
|
+
pattern_build_token(Token, Bindings, Chars)
|
|
5036
|
+
),
|
|
5037
|
+
Parts),
|
|
5038
|
+
append(Parts, All),
|
|
5039
|
+
string_chars(Text, All).
|
|
4104
5040
|
|
|
4105
|
-
|
|
4106
|
-
|
|
4107
|
-
|
|
4108
|
-
|
|
4109
|
-
;
|
|
5041
|
+
pattern_build_token(lit(Chars), _, Chars) :- !.
|
|
5042
|
+
pattern_build_token(slot(Name, Type), Bindings, Chars) :-
|
|
5043
|
+
( member(Name=Value, Bindings)
|
|
5044
|
+
-> true
|
|
5045
|
+
; member(Key=Value, Bindings), pattern_name(Key, Name)
|
|
5046
|
+
-> true
|
|
5047
|
+
; format(atom(Msg), 'pattern: no value bound for slot "~w"', [Name]),
|
|
5048
|
+
throw(error(uninstantiation_error(Msg), pattern/3))
|
|
4110
5049
|
),
|
|
4111
|
-
|
|
4112
|
-
|
|
4113
|
-
|
|
4114
|
-
|
|
5050
|
+
pattern_render(Type, Value, Chars).
|
|
5051
|
+
|
|
5052
|
+
pattern_render(none, Value, Chars) :- !, pattern_text_chars(Value, Chars).
|
|
5053
|
+
pattern_render(Type, Value, Chars) :-
|
|
5054
|
+
( memberchk(Type, [string, text])
|
|
5055
|
+
-> pattern_text_chars(Value, Chars)
|
|
5056
|
+
; memberchk(Type, [int, integer, number])
|
|
5057
|
+
-> ( number(Value) -> format(string(S), "~w", [Value]) ; S = Value ),
|
|
5058
|
+
string_chars(S, Chars)
|
|
5059
|
+
; format(atom(Msg), 'pattern: unknown slot type "~w"', [Type]),
|
|
5060
|
+
throw(error(domain_error(pattern_type, Msg), pattern/3))
|
|
5061
|
+
).
|
|
5062
|
+
|
|
5063
|
+
%% Always a char LIST. Returning the string itself here silently corrupted the
|
|
5064
|
+
%% build: the parts list then held a string among char lists, and the final
|
|
5065
|
+
%% string_chars/2 failed on a mixed list.
|
|
5066
|
+
pattern_text_chars(Value, Chars) :-
|
|
5067
|
+
( string(Value) -> string_chars(Value, Chars)
|
|
5068
|
+
; atom(Value) -> atom_chars(Value, Chars)
|
|
5069
|
+
; number(Value) -> format(string(S), "~w", [Value]), string_chars(S, Chars)
|
|
5070
|
+
; format(atom(Msg), 'pattern: cannot render ~w as text', [Value]),
|
|
5071
|
+
throw(error(type_error(text, Msg), pattern/3))
|
|
5072
|
+
).
|
|
5073
|
+
|
|
5074
|
+
%% pattern_parse(+Tokens, +Text, -Bindings)
|
|
5075
|
+
pattern_parse(Tokens, Text, Bindings) :-
|
|
5076
|
+
% Always a char list: a string here would be one element and the literal
|
|
5077
|
+
% matching below would never line up.
|
|
5078
|
+
( string(Text) -> string_chars(Text, Chars)
|
|
5079
|
+
; atom(Text) -> atom_string(Text, S), string_chars(S, Chars)
|
|
5080
|
+
; Chars = Text
|
|
4115
5081
|
),
|
|
4116
|
-
|
|
4117
|
-
(
|
|
4118
|
-
|
|
4119
|
-
|
|
4120
|
-
|
|
5082
|
+
is_list(Chars),
|
|
5083
|
+
pattern_match(Tokens, Chars, Bindings).
|
|
5084
|
+
|
|
5085
|
+
pattern_match([], [], []) :- !.
|
|
5086
|
+
pattern_match([lit(Lit)], Rest, []) :-
|
|
5087
|
+
!,
|
|
5088
|
+
append(Lit, [], Rest).
|
|
5089
|
+
pattern_match([lit(Lit), slot(Name, Type) | More], Chars, [Name=Value | Bindings]) :-
|
|
5090
|
+
append(Lit, Rest0, Chars),
|
|
5091
|
+
!,
|
|
5092
|
+
( More = [lit(Next) | _], Next \== []
|
|
5093
|
+
-> append(Taken, After, Rest0),
|
|
5094
|
+
append(Next, _, After)
|
|
5095
|
+
; Taken = Rest0,
|
|
5096
|
+
After = []
|
|
4121
5097
|
),
|
|
4122
|
-
|
|
4123
|
-
|
|
5098
|
+
pattern_capture(Type, Taken, Value),
|
|
5099
|
+
pattern_match(More, After, Bindings).
|
|
5100
|
+
|
|
5101
|
+
pattern_capture(none, Chars, Value) :- !, string_chars(Value, Chars).
|
|
5102
|
+
pattern_capture(Type, Chars, Value) :-
|
|
5103
|
+
( memberchk(Type, [string, text])
|
|
5104
|
+
-> string_chars(Value, Chars)
|
|
5105
|
+
; memberchk(Type, [int, integer])
|
|
5106
|
+
-> ( catch(number_codes(N, Chars), _, fail), integer(N)
|
|
5107
|
+
-> Value = N
|
|
5108
|
+
; format(atom(Msg), 'pattern: "~s" is not an integer', [Chars]),
|
|
5109
|
+
throw(error(syntax_error(Msg), pattern/3))
|
|
5110
|
+
)
|
|
5111
|
+
; memberchk(Type, [number])
|
|
5112
|
+
-> ( catch(number_codes(N, Chars), _, fail)
|
|
5113
|
+
-> Value = N
|
|
5114
|
+
; format(atom(Msg), 'pattern: "~s" is not a number', [Chars]),
|
|
5115
|
+
throw(error(syntax_error(Msg), pattern/3))
|
|
5116
|
+
)
|
|
5117
|
+
; format(atom(Msg), 'pattern: unknown slot type "~w"', [Type]),
|
|
5118
|
+
throw(error(domain_error(pattern_type, Msg), pattern/3))
|
|
5119
|
+
).
|
|
5120
|
+
|
|
5121
|
+
%% pattern_name(+Key, +Name): compare a template slot name with a binding key the
|
|
5122
|
+
%% way dict_field does — atom or string, any case, separators ignored.
|
|
5123
|
+
pattern_name(Key, Name) :-
|
|
5124
|
+
pattern_normalise(Key, A),
|
|
5125
|
+
pattern_normalise(Name, B),
|
|
5126
|
+
A == B.
|
|
5127
|
+
|
|
5128
|
+
pattern_normalise(Value, Normalised) :-
|
|
5129
|
+
( atom(Value) -> atom_string(Value, Text) ; Text = Value ),
|
|
5130
|
+
string_lower(Text, Lower),
|
|
5131
|
+
split_string(Lower, " _-", "", Parts),
|
|
5132
|
+
atomic_list_concat(Parts, '', Normalised).
|
|
5133
|
+
|
|
5134
|
+
%% repl_inject_workspace_predicates(+SessionId)
|
|
5135
|
+
%% Idempotent: assertz/1 the same clauses twice is harmless (a duplicate clause
|
|
5136
|
+
%% yields the same solution once? no - it yields twice), so clear first.
|
|
5137
|
+
repl_inject_workspace_predicates(SessionId) :-
|
|
5138
|
+
forall(( workspace_predicate_name(Name), between(0, 5, Arity) ),
|
|
5139
|
+
catch(abolish(SessionId:Name/Arity), _, true)),
|
|
5140
|
+
forall(workspace_predicate_clause(SessionId, Clause),
|
|
5141
|
+
catch(assertz(Clause), _, true)).
|
|
5142
|
+
|
|
5143
|
+
workspace_predicate_clause(S, (S:read_file(F, C) :-
|
|
5144
|
+
system:read_file_to_string(F, C, []))).
|
|
5145
|
+
workspace_predicate_clause(S, (S:read_file_to_string(F, C, O) :-
|
|
5146
|
+
system:read_file_to_string(F, C, O))).
|
|
5147
|
+
workspace_predicate_clause(S, (S:write_file(F, C) :-
|
|
5148
|
+
system:open(F, write, St), system:write(St, C), system:close(St))).
|
|
5149
|
+
workspace_predicate_clause(S, (S:append_file(F, C) :-
|
|
5150
|
+
system:open(F, append, St), system:write(St, C), system:close(St))).
|
|
5151
|
+
workspace_predicate_clause(S, (S:exists_file(F) :-
|
|
5152
|
+
system:exists_file(F))).
|
|
5153
|
+
workspace_predicate_clause(S, (S:exists_directory(D) :-
|
|
5154
|
+
system:exists_directory(D))).
|
|
5155
|
+
workspace_predicate_clause(S, (S:directory_files(D, L) :-
|
|
5156
|
+
system:directory_files(D, L))).
|
|
5157
|
+
workspace_predicate_clause(S, (S:make_directory(D) :-
|
|
5158
|
+
system:make_directory(D))).
|
|
5159
|
+
workspace_predicate_clause(S, (S:make_directory_path(D) :-
|
|
5160
|
+
system:make_directory_path(D))).
|
|
5161
|
+
workspace_predicate_clause(S, (S:delete_file(F) :-
|
|
5162
|
+
system:delete_file(F))).
|
|
5163
|
+
workspace_predicate_clause(S, (S:delete_directory(D) :-
|
|
5164
|
+
system:delete_directory(D))).
|
|
5165
|
+
workspace_predicate_clause(S, (S:size_file(F, Sz) :-
|
|
5166
|
+
system:size_file(F, Sz))).
|
|
5167
|
+
workspace_predicate_clause(S, (S:dict_field(D, K, V) :- deepclause_mi:dict_field(D, K, V))).
|
|
5168
|
+
workspace_predicate_clause(S, (S:dict_field(D, K, Def, V) :- deepclause_mi:dict_field(D, K, Def, V))).
|
|
5169
|
+
workspace_predicate_clause(S, (S:dict_field_key(K, N) :- deepclause_mi:dict_field_key(K, N))).
|
|
5170
|
+
workspace_predicate_clause(S, (S:json_get(D, P, V) :- deepclause_mi:json_get(D, P, V))).
|
|
5171
|
+
workspace_predicate_clause(S, (S:json_get(D, P, Def, V) :- deepclause_mi:json_get(D, P, Def, V))).
|
|
5172
|
+
workspace_predicate_clause(S, (S:json_gets(D, P, Vs) :- deepclause_mi:json_gets(D, P, Vs))).
|
|
5173
|
+
workspace_predicate_clause(S, (S:json_put(D, P, V, D2) :- deepclause_mi:json_put(D, P, V, D2))).
|
|
5174
|
+
workspace_predicate_clause(S, (S:json_text(D, T) :- deepclause_mi:json_text(D, T))).
|
|
5175
|
+
workspace_predicate_clause(S, (S:schema_fill(N, V, F) :-
|
|
5176
|
+
deepclause_mi:schema_fields_of(S, N, Fields), deepclause_mi:schema_fill_fields(Fields, V, F))).
|
|
5177
|
+
workspace_predicate_clause(S, (S:pattern(T, Text, B) :- deepclause_mi:pattern(T, Text, B))).
|
|
5178
|
+
|
|
5179
|
+
workspace_predicate_clause(S, (S:time_file(F, T) :-
|
|
5180
|
+
system:time_file(F, T))).
|
|
5181
|
+
|
|
5182
|
+
%% ============================================================
|
|
5183
|
+
%% File I/O - paths resolve against /workspace (see the note above)
|
|
5184
|
+
%% ============================================================
|
|
5185
|
+
|
|
5186
|
+
%% File I/O paths
|
|
5187
|
+
%% `/workspace` is the host project, mounted by the WASM filesystem (NODEFS);
|
|
5188
|
+
%% its working directory is set to /workspace, so a relative path and a
|
|
5189
|
+
%% `/workspace/...` path name the same file. Every other absolute path is the
|
|
5190
|
+
%% ephemeral in-memory VM filesystem, not the host. There is no rewriting
|
|
5191
|
+
%% layer any more: the mount is the boundary.
|
|
4124
5192
|
|
|
4125
5193
|
%% mi_call(consult(File), +StateIn, -StateOut)
|
|
4126
5194
|
%% Load Prolog clauses from a file into the current session module
|
|
@@ -4129,8 +5197,7 @@ mi_call(consult(File), StateIn, StateIn) :-
|
|
|
4129
5197
|
!,
|
|
4130
5198
|
get_session_id(SessionId),
|
|
4131
5199
|
guard_not_frozen(SessionId),
|
|
4132
|
-
|
|
4133
|
-
readutil:read_file_to_string(FullPath, Code, []),
|
|
5200
|
+
system:read_file_to_string(File, Code, []),
|
|
4134
5201
|
consult_string(Code, SessionId).
|
|
4135
5202
|
|
|
4136
5203
|
mi_call(use_module(Module), StateIn, StateIn) :-
|
|
@@ -4228,22 +5295,19 @@ consult_process_term(Head, SessionId) :-
|
|
|
4228
5295
|
%% mi_call(read_file_to_string(File, Content, Options), +StateIn, -StateOut)
|
|
4229
5296
|
mi_call(read_file_to_string(File, Content, Options), StateIn, StateIn) :-
|
|
4230
5297
|
!,
|
|
4231
|
-
|
|
4232
|
-
readutil:read_file_to_string(FullPath, Content, Options).
|
|
5298
|
+
system:read_file_to_string(File, Content, Options).
|
|
4233
5299
|
|
|
4234
5300
|
%% mi_call(read_file(File, Content), +StateIn, -StateOut)
|
|
4235
5301
|
%% Simplified version without options
|
|
4236
5302
|
mi_call(read_file(File, Content), StateIn, StateIn) :-
|
|
4237
5303
|
!,
|
|
4238
|
-
|
|
4239
|
-
readutil:read_file_to_string(FullPath, Content, []).
|
|
5304
|
+
system:read_file_to_string(File, Content, []).
|
|
4240
5305
|
|
|
4241
5306
|
%% mi_call(write_file(File, Content), +StateIn, -StateOut)
|
|
4242
5307
|
%% Write string content to a file
|
|
4243
5308
|
mi_call(write_file(File, Content), StateIn, StateIn) :-
|
|
4244
5309
|
!,
|
|
4245
|
-
|
|
4246
|
-
open(FullPath, write, Stream),
|
|
5310
|
+
open(File, write, Stream),
|
|
4247
5311
|
write(Stream, Content),
|
|
4248
5312
|
close(Stream).
|
|
4249
5313
|
|
|
@@ -4251,8 +5315,7 @@ mi_call(write_file(File, Content), StateIn, StateIn) :-
|
|
|
4251
5315
|
%% Append string content to a file
|
|
4252
5316
|
mi_call(append_file(File, Content), StateIn, StateIn) :-
|
|
4253
5317
|
!,
|
|
4254
|
-
|
|
4255
|
-
open(FullPath, append, Stream),
|
|
5318
|
+
open(File, append, Stream),
|
|
4256
5319
|
write(Stream, Content),
|
|
4257
5320
|
close(Stream).
|
|
4258
5321
|
|
|
@@ -4260,19 +5323,13 @@ mi_call(append_file(File, Content), StateIn, StateIn) :-
|
|
|
4260
5323
|
%% Open a file for reading/writing
|
|
4261
5324
|
mi_call(open(File, Mode, Stream), StateIn, StateIn) :-
|
|
4262
5325
|
!,
|
|
4263
|
-
|
|
4264
|
-
open(FullPath, Mode, Stream).
|
|
5326
|
+
open(File, Mode, Stream).
|
|
4265
5327
|
|
|
4266
5328
|
%% mi_call(close(Stream), +StateIn, -StateOut)
|
|
4267
5329
|
mi_call(close(Stream), StateIn, StateIn) :-
|
|
4268
5330
|
!,
|
|
4269
5331
|
close(Stream).
|
|
4270
5332
|
|
|
4271
|
-
%% mi_call(read_string(Stream, Length, Content), +StateIn, -StateOut)
|
|
4272
|
-
mi_call(read_string(Stream, Length, Content), StateIn, StateIn) :-
|
|
4273
|
-
!,
|
|
4274
|
-
read_string(Stream, Length, Content).
|
|
4275
|
-
|
|
4276
5333
|
%% mi_call(write(Stream, Content), +StateIn, -StateOut)
|
|
4277
5334
|
mi_call(write(Stream, Content), StateIn, StateIn) :-
|
|
4278
5335
|
!,
|
|
@@ -4281,60 +5338,51 @@ mi_call(write(Stream, Content), StateIn, StateIn) :-
|
|
|
4281
5338
|
%% mi_call(exists_file(File), +StateIn, -StateOut)
|
|
4282
5339
|
mi_call(exists_file(File), StateIn, StateIn) :-
|
|
4283
5340
|
!,
|
|
4284
|
-
|
|
4285
|
-
exists_file(FullPath).
|
|
5341
|
+
exists_file(File).
|
|
4286
5342
|
|
|
4287
5343
|
%% mi_call(exists_directory(Dir), +StateIn, -StateOut)
|
|
4288
5344
|
mi_call(exists_directory(Dir), StateIn, StateIn) :-
|
|
4289
5345
|
!,
|
|
4290
|
-
|
|
4291
|
-
exists_directory(FullPath).
|
|
5346
|
+
exists_directory(Dir).
|
|
4292
5347
|
|
|
4293
5348
|
%% mi_call(directory_files(Dir, Files), +StateIn, -StateOut)
|
|
4294
5349
|
mi_call(directory_files(Dir, Files), StateIn, StateIn) :-
|
|
4295
5350
|
!,
|
|
4296
|
-
|
|
4297
|
-
directory_files(FullPath, Files).
|
|
5351
|
+
directory_files(Dir, Files).
|
|
4298
5352
|
|
|
4299
5353
|
%% mi_call(make_directory(Dir), +StateIn, -StateOut)
|
|
4300
5354
|
mi_call(make_directory(Dir), StateIn, StateIn) :-
|
|
4301
5355
|
!,
|
|
4302
|
-
|
|
4303
|
-
make_directory(FullPath).
|
|
5356
|
+
make_directory(Dir).
|
|
4304
5357
|
|
|
4305
5358
|
%% mi_call(make_directory_path(Dir), +StateIn, -StateOut)
|
|
4306
5359
|
mi_call(make_directory_path(Dir), StateIn, StateIn) :-
|
|
4307
5360
|
!,
|
|
4308
|
-
|
|
4309
|
-
make_directory_path(FullPath).
|
|
5361
|
+
make_directory_path(Dir).
|
|
4310
5362
|
|
|
4311
5363
|
%% mi_call(delete_file(File), +StateIn, -StateOut)
|
|
4312
5364
|
mi_call(delete_file(File), StateIn, StateIn) :-
|
|
4313
5365
|
!,
|
|
4314
|
-
|
|
4315
|
-
delete_file(FullPath).
|
|
5366
|
+
delete_file(File).
|
|
4316
5367
|
|
|
4317
5368
|
%% mi_call(delete_directory(Dir), +StateIn, -StateOut)
|
|
4318
5369
|
mi_call(delete_directory(Dir), StateIn, StateIn) :-
|
|
4319
5370
|
!,
|
|
4320
|
-
|
|
4321
|
-
delete_directory(FullPath).
|
|
5371
|
+
delete_directory(Dir).
|
|
4322
5372
|
|
|
4323
5373
|
%% mi_call(size_file(File, Size), +StateIn, -StateOut)
|
|
4324
5374
|
%% size_file/2 is a plain SWI builtin, so it would otherwise resolve `File`
|
|
4325
5375
|
%% against the WASM process cwd (not the workspace) and demand `/workspace/...`.
|
|
4326
5376
|
mi_call(size_file(File, Size), StateIn, StateIn) :-
|
|
4327
5377
|
!,
|
|
4328
|
-
|
|
4329
|
-
size_file(FullPath, Size).
|
|
5378
|
+
size_file(File, Size).
|
|
4330
5379
|
|
|
4331
5380
|
%% mi_call(time_file(File, Time), +StateIn, -StateOut)
|
|
4332
5381
|
%% Modifications time (seconds since epoch); same workspace-relative rule as
|
|
4333
5382
|
%% size_file/2.
|
|
4334
5383
|
mi_call(time_file(File, Time), StateIn, StateIn) :-
|
|
4335
5384
|
!,
|
|
4336
|
-
|
|
4337
|
-
time_file(FullPath, Time).
|
|
5385
|
+
time_file(File, Time).
|
|
4338
5386
|
|
|
4339
5387
|
%% ============================================================
|
|
4340
5388
|
%% Catch/Throw - STATE THREADED
|
|
@@ -4419,6 +5467,7 @@ is_mi_special_predicate(system(_)).
|
|
|
4419
5467
|
is_mi_special_predicate(user(_)). %% Add user message to context
|
|
4420
5468
|
is_mi_special_predicate(read_media(_,_)).
|
|
4421
5469
|
is_mi_special_predicate(attach_media(_)).
|
|
5470
|
+
is_mi_special_predicate(save_image(_,_)).
|
|
4422
5471
|
is_mi_special_predicate(task(_)).
|
|
4423
5472
|
is_mi_special_predicate(task(_,_)).
|
|
4424
5473
|
is_mi_special_predicate(task(_,_,_)).
|
|
@@ -4439,9 +5488,11 @@ is_mi_special_predicate(prompt_named(_,_,_)). %% Transformed form of prompt/N
|
|
|
4439
5488
|
is_mi_special_predicate(llm(_,_)).
|
|
4440
5489
|
%% Tool scoping predicates
|
|
4441
5490
|
is_mi_special_predicate(with_tools(_,_)).
|
|
5491
|
+
is_mi_special_predicate(with_fresh_memory(_)).
|
|
4442
5492
|
is_mi_special_predicate(without_tools(_,_)).
|
|
4443
5493
|
%% Reasoning effort scoping
|
|
4444
5494
|
is_mi_special_predicate(with_reasoning(_,_)).
|
|
5495
|
+
is_mi_special_predicate(with_model(_,_)).
|
|
4445
5496
|
%% Recipe context scoping
|
|
4446
5497
|
is_mi_special_predicate(with_recipe(_,_)).
|
|
4447
5498
|
%% Semantic judgment predicates
|
|
@@ -4669,6 +5720,14 @@ consume_gas(StateIn, StateOut) :-
|
|
|
4669
5720
|
%% mi_call_dispatch(+Goal, +StateIn, -StateOut)
|
|
4670
5721
|
%% Dispatch user-defined vs built-in predicates
|
|
4671
5722
|
%% Uses separate clauses instead of if-then-else to preserve backtracking
|
|
5723
|
+
mi_call_dispatch(Goal, StateIn, StateIn) :-
|
|
5724
|
+
get_session_id(SessionId),
|
|
5725
|
+
callable(Goal),
|
|
5726
|
+
predicate_property(SessionId:Goal, defined),
|
|
5727
|
+
predicate_property(Goal, built_in),
|
|
5728
|
+
!,
|
|
5729
|
+
call(SessionId:Goal).
|
|
5730
|
+
|
|
4672
5731
|
mi_call_dispatch(Goal, StateIn, StateIn) :-
|
|
4673
5732
|
predicate_property(Goal, built_in),
|
|
4674
5733
|
!, % Commit: it's built-in, no backtracking needed
|
|
@@ -4750,8 +5809,14 @@ var_to_name(Bindings, Var, Name) :-
|
|
|
4750
5809
|
%% expand_interpolations(+Term, +Bindings, -ExpandedTerm)
|
|
4751
5810
|
%% Walk a term and expand strings containing {VarName} patterns
|
|
4752
5811
|
%% Bindings is a list of 'VarName'=Variable pairs from read_term
|
|
5812
|
+
%% Interpolation happens in goal position only: `task/N`/`prompt/N`/`output/1`/… and
|
|
5813
|
+
%% the arguments of a user goal, never inside a list, a dict, or another compound
|
|
5814
|
+
%% passed as data. See the note on expand_term_interp/3.
|
|
5815
|
+
expand_interpolations((Head :- Body), Bindings, (Head :- ExpandedBody)) :-
|
|
5816
|
+
!,
|
|
5817
|
+
expand_body_interp(Body, Bindings, ExpandedBody).
|
|
4753
5818
|
expand_interpolations(Term, Bindings, ExpandedTerm) :-
|
|
4754
|
-
|
|
5819
|
+
expand_body_interp(Term, Bindings, ExpandedTerm).
|
|
4755
5820
|
|
|
4756
5821
|
%% expand_term_interp(+Term, +Bindings, -Expanded)
|
|
4757
5822
|
%% Main term expansion predicate
|
|
@@ -4760,13 +5825,16 @@ expand_interpolations(Term, Bindings, ExpandedTerm) :-
|
|
|
4760
5825
|
expand_term_interp(Var, _, Var) :-
|
|
4761
5826
|
var(Var), !.
|
|
4762
5827
|
|
|
4763
|
-
% Strings
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
5828
|
+
% Strings are LITERAL here. Interpolation is decided by expand_body_interp/3, which
|
|
5829
|
+
% only rewrites a string that is a *direct argument of a goal*. Rewriting every
|
|
5830
|
+
% string, nested ones included, corrupted data: the shell command
|
|
5831
|
+
% exec(bash(argv: [..., "-w", "%{http_code}", Url]), R)
|
|
5832
|
+
% had its `%{http_code}` element replaced by a format goal inside the argv list, so
|
|
5833
|
+
% the list arrived truncated at `-w`. A JSON document, a regex, an `awk` program and
|
|
5834
|
+
% a `jq` filter all contain braces and must survive verbatim.
|
|
5835
|
+
expand_term_interp(String, _, String) :-
|
|
5836
|
+
string(String),
|
|
5837
|
+
!.
|
|
4770
5838
|
|
|
4771
5839
|
% Atoms - check if they need interpolation (for atom strings)
|
|
4772
5840
|
expand_term_interp(Atom, Bindings, Expanded) :-
|
|
@@ -4812,6 +5880,19 @@ expand_term_interp(Term, Bindings, ExpandedTerm) :-
|
|
|
4812
5880
|
expand_body_interp(Var, _, Var) :-
|
|
4813
5881
|
var(Var), !.
|
|
4814
5882
|
|
|
5883
|
+
% A list or a dict is DATA, not a sequence of goals. Descending into one treats its
|
|
5884
|
+
% elements as goal arguments and hoists any `{...}` string it contains, which
|
|
5885
|
+
% rewrote data into a goal term:
|
|
5886
|
+
% argv: ["curl","-w","%{http_code}","u"] became
|
|
5887
|
+
% argv: [curl,-w|((param(http_code,_),format(...)),[Temp,u])]
|
|
5888
|
+
% so `exec(bash(argv: [...]))` reached the shell with a truncated argument list.
|
|
5889
|
+
expand_body_interp(Data, _, Data) :-
|
|
5890
|
+
( Data == []
|
|
5891
|
+
; Data = [_|_]
|
|
5892
|
+
; is_dict(Data)
|
|
5893
|
+
),
|
|
5894
|
+
!.
|
|
5895
|
+
|
|
4815
5896
|
% Conjunction
|
|
4816
5897
|
expand_body_interp((A, B), Bindings, ExpandedConj) :- !,
|
|
4817
5898
|
expand_body_interp(A, Bindings, EA),
|
|
@@ -4860,6 +5941,7 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
|
|
|
4860
5941
|
expand_body_interp(Goal, Bindings, ExpandedGoal) :-
|
|
4861
5942
|
compound(Goal),
|
|
4862
5943
|
Goal =.. [Functor|Args],
|
|
5944
|
+
interp_arg_predicate(Functor),
|
|
4863
5945
|
expand_args_hoist(Args, Bindings, PreGoals, ExpandedArgs, true),
|
|
4864
5946
|
!,
|
|
4865
5947
|
Bare =.. [Functor|ExpandedArgs],
|
|
@@ -4875,6 +5957,21 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
|
|
|
4875
5957
|
% Atoms/other
|
|
4876
5958
|
expand_body_interp(Goal, _, Goal).
|
|
4877
5959
|
|
|
5960
|
+
%% Predicates whose string arguments interpolate `{Var}`. Interpolation lives in
|
|
5961
|
+
%% the *description* of an LLM step and in the question of a judgment; anywhere else
|
|
5962
|
+
%% a string is data and must survive verbatim. Hoisting for every goal made a string
|
|
5963
|
+
%% of data into a goal argument: `X = "%{code}"` became an interpolation of the
|
|
5964
|
+
%% variable `code`, and a shell argv containing `%{http_code}` was rewritten.
|
|
5965
|
+
%%
|
|
5966
|
+
%% (task/N, prompt/N, answer/1, system/1, user/1, output/1, log/1 and yield/1 are
|
|
5967
|
+
%% handled by goal_with_string_arg/4 above, for their first argument.)
|
|
5968
|
+
interp_arg_predicate(judge).
|
|
5969
|
+
interp_arg_predicate(choose).
|
|
5970
|
+
interp_arg_predicate(rate).
|
|
5971
|
+
interp_arg_predicate(verify).
|
|
5972
|
+
interp_arg_predicate(probability).
|
|
5973
|
+
interp_arg_predicate(holds).
|
|
5974
|
+
|
|
4878
5975
|
%% goal_with_string_arg(+Goal, -Functor, -StringArg, -RestArgs)
|
|
4879
5976
|
%% Match goals that take a string as their first argument
|
|
4880
5977
|
goal_with_string_arg(answer(S), answer, S, []) :- string(S).
|