deepclause-sdk 0.1.1 → 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/dist/agent.d.ts +12 -0
- package/dist/agent.d.ts.map +1 -1
- package/dist/agent.js +116 -9
- package/dist/agent.js.map +1 -1
- package/dist/cli/config.d.ts +24 -24
- package/dist/cli/repl.d.ts +2 -0
- package/dist/cli/repl.d.ts.map +1 -1
- package/dist/cli/repl.js +26 -0
- package/dist/cli/repl.js.map +1 -1
- package/dist/cli/run.d.ts.map +1 -1
- package/dist/cli/run.js +10 -0
- package/dist/cli/run.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 +1028 -118
- package/dist/repl/agent.d.ts +15 -1
- package/dist/repl/agent.d.ts.map +1 -1
- package/dist/repl/agent.js +44 -5
- package/dist/repl/agent.js.map +1 -1
- package/dist/repl/harness-tools.d.ts +24 -2
- package/dist/repl/harness-tools.d.ts.map +1 -1
- package/dist/repl/harness-tools.js +14 -5
- package/dist/repl/harness-tools.js.map +1 -1
- package/dist/repl/index.d.ts +8 -0
- package/dist/repl/index.d.ts.map +1 -1
- package/dist/repl/index.js +1 -0
- package/dist/repl/index.js.map +1 -1
- package/dist/repl/render.d.ts +8 -1
- package/dist/repl/render.d.ts.map +1 -1
- package/dist/repl/render.js +81 -16
- package/dist/repl/render.js.map +1 -1
- package/dist/repl/session.d.ts +61 -4
- package/dist/repl/session.d.ts.map +1 -1
- package/dist/repl/session.js +174 -9
- 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/runner.d.ts +31 -0
- package/dist/runner.d.ts.map +1 -1
- package/dist/runner.js +192 -17
- package/dist/runner.js.map +1 -1
- package/dist/system/assets/docs/AGENT_PROMPT.md +925 -43
- package/dist/system/assets/docs/DML_AUTHORING.md +96 -17
- package/dist/system/assets/docs/DML_REFERENCE.md +620 -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 +20 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +4 -2
- package/src/prolog-src/deepclause_mi.pl +1028 -118
- 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,
|
|
@@ -116,6 +123,7 @@
|
|
|
116
123
|
:- dynamic session_repl_memory/2. % session_repl_memory(SessionId, HarnessMemory) - object/entry memory
|
|
117
124
|
:- dynamic session_meta_memory/2. % session_meta_memory(SessionId, MetaMemory) - code-writing task memory
|
|
118
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)
|
|
119
127
|
:- dynamic session_frozen/2. % session_frozen(SessionId, true/false) - read-only lock
|
|
120
128
|
:- dynamic session_locked/2. % session_locked(SessionId, true/false) - irreversibly frozen (--locked)
|
|
121
129
|
:- dynamic session_directive/2. % session_directive(SessionId, Directive) - recorded :- directives for export
|
|
@@ -153,7 +161,8 @@
|
|
|
153
161
|
parse_dml(FilePath, SessionId, _MemoryId, Params, Error) :-
|
|
154
162
|
catch(
|
|
155
163
|
(
|
|
156
|
-
|
|
164
|
+
% FilePath is an absolute host temp path, not workspace-relative.
|
|
165
|
+
system:read_file_to_string(FilePath, Code, []),
|
|
157
166
|
assert_params(SessionId, Params),
|
|
158
167
|
parse_dml_string(Code, SessionId),
|
|
159
168
|
Error = none
|
|
@@ -310,6 +319,11 @@ extract_var_info(Bindings, Term, BareVar, Info) :-
|
|
|
310
319
|
BareVar = Term,
|
|
311
320
|
var_to_name(Bindings, Term, Name),
|
|
312
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}
|
|
313
327
|
; compound(Term), Term =.. [Wrapper, Inner]
|
|
314
328
|
-> % Wrapped variable
|
|
315
329
|
( wrapper_type(Wrapper, Type)
|
|
@@ -317,6 +331,11 @@ extract_var_info(Bindings, Term, BareVar, Info) :-
|
|
|
317
331
|
-> BareVar = Inner,
|
|
318
332
|
var_to_name(Bindings, Inner, Name),
|
|
319
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}
|
|
320
339
|
; compound(Inner), Inner =.. [InnerWrapper, InnerVar], var(InnerVar), Wrapper == list
|
|
321
340
|
-> % List of typed items: list(string(X))
|
|
322
341
|
BareVar = InnerVar,
|
|
@@ -431,6 +450,9 @@ clean_atomic(with_tools(Tools, Goal), with_tools(Tools, CleanGoal)) :-
|
|
|
431
450
|
clean_atomic(without_tools(Tools, Goal), without_tools(Tools, CleanGoal)) :-
|
|
432
451
|
!,
|
|
433
452
|
clean_clause_body(Goal, CleanGoal).
|
|
453
|
+
clean_atomic(with_fresh_memory(Goal), with_fresh_memory(CleanGoal)) :-
|
|
454
|
+
!,
|
|
455
|
+
clean_clause_body(Goal, CleanGoal).
|
|
434
456
|
clean_atomic(with_judgment(Backend, Goal), with_judgment(Backend, CleanGoal)) :-
|
|
435
457
|
!,
|
|
436
458
|
clean_clause_body(Goal, CleanGoal).
|
|
@@ -742,8 +764,7 @@ process_directive(consult(File), SessionId) :-
|
|
|
742
764
|
!,
|
|
743
765
|
% Set session ID for get_session_id to find, then call our custom consult
|
|
744
766
|
nb_setval(current_session_id, SessionId),
|
|
745
|
-
|
|
746
|
-
readutil:read_file_to_string(FullPath, Code, []),
|
|
767
|
+
system:read_file_to_string(File, Code, []),
|
|
747
768
|
consult_string(Code, SessionId).
|
|
748
769
|
%% Handle use_module directive
|
|
749
770
|
process_directive(use_module(Module), SessionId) :-
|
|
@@ -766,7 +787,80 @@ process_directive([File|Files], SessionId) :-
|
|
|
766
787
|
nb_setval(current_session_id, SessionId),
|
|
767
788
|
process_directive(consult(File), SessionId),
|
|
768
789
|
(Files == [] -> true ; process_directive(Files, SessionId)).
|
|
769
|
-
|
|
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).
|
|
770
864
|
|
|
771
865
|
%% ============================================================
|
|
772
866
|
%% Engine Management
|
|
@@ -919,7 +1013,17 @@ create_session(SessionId, Params, HarnessMemory, MetaMemory) :-
|
|
|
919
1013
|
retractall(session_frozen(SessionId, _)),
|
|
920
1014
|
assertz(session_frozen(SessionId, false)),
|
|
921
1015
|
retractall(session_locked(SessionId, _)),
|
|
922
|
-
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).
|
|
923
1027
|
|
|
924
1028
|
%% repl_abolish_user_predicates(+SessionId)
|
|
925
1029
|
%% Removes only the harness's own predicates; imported builtins are untouched.
|
|
@@ -1238,6 +1342,7 @@ internal_session_predicate(param).
|
|
|
1238
1342
|
internal_session_predicate(param_decl).
|
|
1239
1343
|
internal_session_predicate(tool_source).
|
|
1240
1344
|
internal_session_predicate('__session__').
|
|
1345
|
+
internal_session_predicate(Name) :- workspace_predicate_name(Name).
|
|
1241
1346
|
|
|
1242
1347
|
%% history_predicate(+Name)
|
|
1243
1348
|
%% Runtime-owned harness-history facts. Excluded from the user predicate set (so
|
|
@@ -1267,6 +1372,7 @@ repl_reserved_name(repl_reserved_name).
|
|
|
1267
1372
|
repl_reserved_name(repl_tool_catalog).
|
|
1268
1373
|
repl_reserved_name(repl_listing).
|
|
1269
1374
|
repl_reserved_name(repl_assert).
|
|
1375
|
+
repl_reserved_name(require_sandbox).
|
|
1270
1376
|
repl_reserved_name(repl_retract).
|
|
1271
1377
|
repl_reserved_name(repl_delete).
|
|
1272
1378
|
repl_reserved_name(repl_freeze).
|
|
@@ -1502,12 +1608,11 @@ repl_script_files(SessionId, Paths) :-
|
|
|
1502
1608
|
repl_materialize_embedded(SessionId) :-
|
|
1503
1609
|
( current_predicate(SessionId:embedded_file/2)
|
|
1504
1610
|
-> forall(SessionId:embedded_file(Path, Content),
|
|
1505
|
-
(
|
|
1506
|
-
|
|
1507
|
-
( file_directory_name(Full, Dir),
|
|
1611
|
+
( catch(
|
|
1612
|
+
( file_directory_name(Path, Dir),
|
|
1508
1613
|
make_directory_path(Dir),
|
|
1509
1614
|
setup_call_cleanup(
|
|
1510
|
-
open(
|
|
1615
|
+
open(Path, write, Stream),
|
|
1511
1616
|
format(Stream, "~s", [Content]),
|
|
1512
1617
|
close(Stream))
|
|
1513
1618
|
),
|
|
@@ -1604,6 +1709,8 @@ repl_assert(SessionId, ClauseText, Result) :-
|
|
|
1604
1709
|
catch(
|
|
1605
1710
|
( guard_not_frozen(SessionId),
|
|
1606
1711
|
repl_prepare_single(SessionId, ClauseText, PreparedTerms),
|
|
1712
|
+
forall(member(PreparedTerm, PreparedTerms),
|
|
1713
|
+
repl_require_new_predicate(SessionId, PreparedTerm)),
|
|
1607
1714
|
forall(member(PreparedTerm, PreparedTerms),
|
|
1608
1715
|
repl_assert_clause(PreparedTerm, SessionId)),
|
|
1609
1716
|
Result = ok
|
|
@@ -1663,11 +1770,25 @@ repl_read_prepared_terms(Stream, SessionId, Terms) :-
|
|
|
1663
1770
|
% their operators are visible to the terms that follow; keep them out of
|
|
1664
1771
|
% the returned list so repl_commit_terms does not process them twice.
|
|
1665
1772
|
partition_directives(Prepared, Rules, Directives),
|
|
1666
|
-
forall(member(Directive, Directives),
|
|
1773
|
+
forall(member(Directive, Directives), repl_commit_directive(Directive, SessionId)),
|
|
1667
1774
|
append(Rules, Rest, Terms),
|
|
1668
1775
|
repl_read_prepared_terms(Stream, SessionId, Rest)
|
|
1669
1776
|
).
|
|
1670
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
|
+
|
|
1671
1792
|
partition_directives([], [], []).
|
|
1672
1793
|
partition_directives([Term|Terms], Rules, Directives) :-
|
|
1673
1794
|
( Term = (:- _)
|
|
@@ -1696,6 +1817,9 @@ repl_assert_many(SessionId, ClausesText, Result) :-
|
|
|
1696
1817
|
catch(
|
|
1697
1818
|
( guard_not_frozen(SessionId),
|
|
1698
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)),
|
|
1699
1823
|
repl_commit_terms(SessionId, Terms),
|
|
1700
1824
|
repl_note_touched(SessionId, Terms),
|
|
1701
1825
|
Result = ok
|
|
@@ -1719,11 +1843,105 @@ repl_term_indicator(Term, Name/Arity) :-
|
|
|
1719
1843
|
%% Record the predicate indicators a term list defines, for history recording.
|
|
1720
1844
|
repl_note_touched(SessionId, Terms) :-
|
|
1721
1845
|
forall(( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
|
|
1722
|
-
(
|
|
1723
|
-
|
|
1724
|
-
|
|
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
|
+
)
|
|
1725
1934
|
)).
|
|
1726
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
|
+
|
|
1727
1945
|
%% repl_edit_touched(+SessionId, -Indicators)
|
|
1728
1946
|
%% Predicates touched by edit tools since the last clear (sorted, unique).
|
|
1729
1947
|
repl_edit_touched(SessionId, Indicators) :-
|
|
@@ -1754,7 +1972,13 @@ repl_system_functor(Name/_) :-
|
|
|
1754
1972
|
member(Name, ['.', ',', ';', ':-', '->', ':', '|', '!', '\\+', '=', 'is']).
|
|
1755
1973
|
|
|
1756
1974
|
%% repl_retract(+SessionId, +ClausePattern, -Result)
|
|
1757
|
-
%%
|
|
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.
|
|
1758
1982
|
repl_retract(SessionId, ClauseText, Result) :-
|
|
1759
1983
|
catch(
|
|
1760
1984
|
( guard_not_frozen(SessionId),
|
|
@@ -1766,19 +1990,29 @@ repl_retract(SessionId, ClauseText, Result) :-
|
|
|
1766
1990
|
context(repl_retract/3, 'cannot retract a reserved predicate')))
|
|
1767
1991
|
; true
|
|
1768
1992
|
),
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
(
|
|
1772
|
-
->
|
|
1773
|
-
|
|
1774
|
-
|
|
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)))
|
|
1775
2001
|
; true
|
|
1776
2002
|
),
|
|
1777
|
-
|
|
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, _)
|
|
1778
2007
|
-> true
|
|
1779
|
-
;
|
|
2008
|
+
; repl_forget_tool(SessionId, Name)
|
|
1780
2009
|
),
|
|
1781
|
-
|
|
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])
|
|
1782
2016
|
),
|
|
1783
2017
|
RetractError,
|
|
1784
2018
|
format(atom(Result), 'error(~w)', [RetractError])
|
|
@@ -1814,34 +2048,45 @@ repl_delete_predicate(SessionId, Name, Arity, Result) :-
|
|
|
1814
2048
|
).
|
|
1815
2049
|
|
|
1816
2050
|
%% repl_define(+SessionId, +HeadPatternText, +ClausesText, -Result)
|
|
1817
|
-
%% Replace every predicate the clause block defines
|
|
1818
|
-
%%
|
|
1819
|
-
%%
|
|
1820
|
-
%%
|
|
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.
|
|
1821
2059
|
repl_define(SessionId, HeadPatternText, ClausesText, Result) :-
|
|
1822
2060
|
catch(
|
|
1823
2061
|
( guard_not_frozen(SessionId),
|
|
1824
|
-
|
|
1825
|
-
repl_head_of(Pattern, HeadClause),
|
|
1826
|
-
repl_callable_indicator(HeadClause, HeadName, HeadArity),
|
|
2062
|
+
repl_define_target(SessionId, HeadPatternText, HeadName, HeadArity),
|
|
1827
2063
|
repl_parse_clauses(SessionId, ClausesText, Terms),
|
|
1828
2064
|
findall(Name/Arity,
|
|
1829
2065
|
( member(Term, Terms), repl_term_indicator(Term, Name/Arity) ),
|
|
1830
2066
|
BlockIndicators0),
|
|
1831
|
-
sort(
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
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
|
+
),
|
|
1845
2090
|
repl_note_touched(SessionId, Terms),
|
|
1846
2091
|
Result = ok
|
|
1847
2092
|
),
|
|
@@ -1849,6 +2094,42 @@ repl_define(SessionId, HeadPatternText, ClausesText, Result) :-
|
|
|
1849
2094
|
format(atom(Result), 'error(~w)', [DefineError])
|
|
1850
2095
|
).
|
|
1851
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
|
+
|
|
1852
2133
|
%% repl_extract_code(+Text, -Source)
|
|
1853
2134
|
%% Extracts a ```dml fenced block; falls back to the whole answer.
|
|
1854
2135
|
repl_extract_code(Text, Source) :-
|
|
@@ -2103,6 +2384,80 @@ repl_predicate_pure(SessionId, Name, Arity) :-
|
|
|
2103
2384
|
functor(Head, Name, Arity),
|
|
2104
2385
|
\+ ( clause(SessionId:Head, Body), repl_body_impure(Body) ).
|
|
2105
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
|
+
|
|
2106
2461
|
%% True when the predicate has at least one clause with a real body (not a fact).
|
|
2107
2462
|
repl_predicate_has_rule(SessionId, Name, Arity) :-
|
|
2108
2463
|
functor(Head, Name, Arity),
|
|
@@ -3313,7 +3668,8 @@ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3313
3668
|
add_trace_entry(SessionId, llm_call, prompt, [InterpDesc], Depth),
|
|
3314
3669
|
% prompt/N is a simple LLM call - no tools, no tool scope
|
|
3315
3670
|
get_model_spec(StateIn, ModelSpec),
|
|
3316
|
-
|
|
3671
|
+
resolve_schema_fields(SessionId, VarNames, ResolvedVarNames),
|
|
3672
|
+
engine_yield(request_agent_loop(InterpDesc, ResolvedVarNames, [], [], none, ModelSpec)),
|
|
3317
3673
|
% Wait for agent_done (no tool calls expected since no tools provided)
|
|
3318
3674
|
handle_agent_signals(SessionId, StateIn, StateOut),
|
|
3319
3675
|
session_agent_result(SessionId, Result),
|
|
@@ -3331,7 +3687,7 @@ mi_call_prompt_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3331
3687
|
engine_yield(output(WarnMsg)),
|
|
3332
3688
|
fail
|
|
3333
3689
|
),
|
|
3334
|
-
bind_task_variables(Result.variables, VarNames, Vars).
|
|
3690
|
+
bind_task_variables(SessionId, Result.variables, VarNames, Vars).
|
|
3335
3691
|
% Don't modify memory - but state may have changed from tool calls
|
|
3336
3692
|
|
|
3337
3693
|
mi_call_sample_token(Desc, Allowed, Token, StateIn, StateOut) :-
|
|
@@ -3594,7 +3950,8 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3594
3950
|
get_reasoning_effort(StateIn, ReasoningEffort),
|
|
3595
3951
|
get_recipe_context(StateIn, RecipeContext),
|
|
3596
3952
|
get_model_spec(StateIn, ModelSpec),
|
|
3597
|
-
|
|
3953
|
+
resolve_schema_fields(SessionId, VarNames, ResolvedVarNames),
|
|
3954
|
+
engine_yield(request_agent_loop(InterpDesc, ResolvedVarNames, UserTools, Memory, ToolScope, ReasoningEffort, RecipeContext, ModelSpec)),
|
|
3598
3955
|
% Handle signals (tool calls) until agent_done
|
|
3599
3956
|
handle_agent_signals(SessionId, StateIn, StateAfterAgent),
|
|
3600
3957
|
session_agent_result(SessionId, Result),
|
|
@@ -3611,7 +3968,7 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3611
3968
|
engine_yield(output(WarnMsg)),
|
|
3612
3969
|
fail
|
|
3613
3970
|
),
|
|
3614
|
-
bind_task_variables(Result.variables, VarNames, Vars),
|
|
3971
|
+
bind_task_variables(SessionId, Result.variables, VarNames, Vars),
|
|
3615
3972
|
% Use full memory from agent loop result when available
|
|
3616
3973
|
( get_dict(full_memory, Result, FullMemory), is_list(FullMemory)
|
|
3617
3974
|
-> set_memory(StateAfterAgent, FullMemory, StateOut),
|
|
@@ -3633,18 +3990,134 @@ mi_call_task_n(Desc, Vars, VarNames, StateIn, StateOut) :-
|
|
|
3633
3990
|
)
|
|
3634
3991
|
).
|
|
3635
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
|
+
|
|
3636
4061
|
%% bind_task_variables(+VarsDict, +Names, -Values)
|
|
3637
|
-
bind_task_variables(_, [], []) :- !.
|
|
3638
|
-
bind_task_variables(VarsDict, [N|Names], [Value|Values]) :-
|
|
4062
|
+
bind_task_variables(_, _, [], []) :- !.
|
|
4063
|
+
bind_task_variables(SessionId, VarsDict, [N|Names], [Value|Values]) :-
|
|
3639
4064
|
( is_dict(N)
|
|
3640
4065
|
-> Key = N.name
|
|
3641
4066
|
; Key = N
|
|
3642
4067
|
),
|
|
3643
|
-
( get_dict(Key, VarsDict,
|
|
4068
|
+
( get_dict(Key, VarsDict, Raw)
|
|
3644
4069
|
-> true
|
|
3645
|
-
;
|
|
4070
|
+
; Raw = _Missing
|
|
4071
|
+
),
|
|
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
|
|
3646
4079
|
),
|
|
3647
|
-
bind_task_variables(VarsDict, Names, Values).
|
|
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 ).
|
|
3648
4121
|
|
|
3649
4122
|
%% ============================================================
|
|
3650
4123
|
%% Exec Handling
|
|
@@ -3692,6 +4165,19 @@ mi_call(with_tools(ToolList, Goal), StateIn, StateOut) :-
|
|
|
3692
4165
|
mi_call(Goal, ScopedState, StateWithScope),
|
|
3693
4166
|
clear_tool_scope(StateWithScope, StateOut).
|
|
3694
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
|
+
|
|
3695
4181
|
%% mi_call(without_tools(ToolList, Goal), +StateIn, -StateOut)
|
|
3696
4182
|
%% Run Goal with the specified tools excluded from nested tasks
|
|
3697
4183
|
mi_call(without_tools(ToolList, Goal), StateIn, StateOut) :-
|
|
@@ -3786,15 +4272,12 @@ mi_call(save_image(Media, Out), StateIn, StateIn) :-
|
|
|
3786
4272
|
media_dict(Media, Dict),
|
|
3787
4273
|
( get_dict(kind, Dict, inline_svg),
|
|
3788
4274
|
get_dict(data, Dict, Svg)
|
|
3789
|
-
->
|
|
3790
|
-
open(OutAbs, write, Stream),
|
|
4275
|
+
-> open(Out, write, Stream),
|
|
3791
4276
|
write(Stream, Svg),
|
|
3792
4277
|
close(Stream)
|
|
3793
4278
|
; get_dict(path, Dict, Path),
|
|
3794
4279
|
Path \== ""
|
|
3795
|
-
->
|
|
3796
|
-
resolve_workspace_path(Out, OutAbs),
|
|
3797
|
-
copy_file(InAbs, OutAbs)
|
|
4280
|
+
-> copy_file(Path, Out)
|
|
3798
4281
|
; throw(error(domain_error(save_image, Media), save_image/2))
|
|
3799
4282
|
).
|
|
3800
4283
|
|
|
@@ -4284,28 +4767,428 @@ mi_foldl(Goal, [H|T], V0, V, StateIn, StateOut) :-
|
|
|
4284
4767
|
mi_foldl(Goal, T, V1, V, State1, StateOut).
|
|
4285
4768
|
|
|
4286
4769
|
%% ============================================================
|
|
4287
|
-
%%
|
|
4770
|
+
%% Workspace predicates (one definition, every surface)
|
|
4288
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).
|
|
4289
5040
|
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
;
|
|
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))
|
|
4295
5049
|
),
|
|
4296
|
-
|
|
4297
|
-
|
|
4298
|
-
|
|
4299
|
-
|
|
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
|
|
4300
5081
|
),
|
|
4301
|
-
|
|
4302
|
-
(
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
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 = []
|
|
4306
5097
|
),
|
|
4307
|
-
|
|
4308
|
-
|
|
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.
|
|
4309
5192
|
|
|
4310
5193
|
%% mi_call(consult(File), +StateIn, -StateOut)
|
|
4311
5194
|
%% Load Prolog clauses from a file into the current session module
|
|
@@ -4314,8 +5197,7 @@ mi_call(consult(File), StateIn, StateIn) :-
|
|
|
4314
5197
|
!,
|
|
4315
5198
|
get_session_id(SessionId),
|
|
4316
5199
|
guard_not_frozen(SessionId),
|
|
4317
|
-
|
|
4318
|
-
readutil:read_file_to_string(FullPath, Code, []),
|
|
5200
|
+
system:read_file_to_string(File, Code, []),
|
|
4319
5201
|
consult_string(Code, SessionId).
|
|
4320
5202
|
|
|
4321
5203
|
mi_call(use_module(Module), StateIn, StateIn) :-
|
|
@@ -4413,22 +5295,19 @@ consult_process_term(Head, SessionId) :-
|
|
|
4413
5295
|
%% mi_call(read_file_to_string(File, Content, Options), +StateIn, -StateOut)
|
|
4414
5296
|
mi_call(read_file_to_string(File, Content, Options), StateIn, StateIn) :-
|
|
4415
5297
|
!,
|
|
4416
|
-
|
|
4417
|
-
readutil:read_file_to_string(FullPath, Content, Options).
|
|
5298
|
+
system:read_file_to_string(File, Content, Options).
|
|
4418
5299
|
|
|
4419
5300
|
%% mi_call(read_file(File, Content), +StateIn, -StateOut)
|
|
4420
5301
|
%% Simplified version without options
|
|
4421
5302
|
mi_call(read_file(File, Content), StateIn, StateIn) :-
|
|
4422
5303
|
!,
|
|
4423
|
-
|
|
4424
|
-
readutil:read_file_to_string(FullPath, Content, []).
|
|
5304
|
+
system:read_file_to_string(File, Content, []).
|
|
4425
5305
|
|
|
4426
5306
|
%% mi_call(write_file(File, Content), +StateIn, -StateOut)
|
|
4427
5307
|
%% Write string content to a file
|
|
4428
5308
|
mi_call(write_file(File, Content), StateIn, StateIn) :-
|
|
4429
5309
|
!,
|
|
4430
|
-
|
|
4431
|
-
open(FullPath, write, Stream),
|
|
5310
|
+
open(File, write, Stream),
|
|
4432
5311
|
write(Stream, Content),
|
|
4433
5312
|
close(Stream).
|
|
4434
5313
|
|
|
@@ -4436,8 +5315,7 @@ mi_call(write_file(File, Content), StateIn, StateIn) :-
|
|
|
4436
5315
|
%% Append string content to a file
|
|
4437
5316
|
mi_call(append_file(File, Content), StateIn, StateIn) :-
|
|
4438
5317
|
!,
|
|
4439
|
-
|
|
4440
|
-
open(FullPath, append, Stream),
|
|
5318
|
+
open(File, append, Stream),
|
|
4441
5319
|
write(Stream, Content),
|
|
4442
5320
|
close(Stream).
|
|
4443
5321
|
|
|
@@ -4445,19 +5323,13 @@ mi_call(append_file(File, Content), StateIn, StateIn) :-
|
|
|
4445
5323
|
%% Open a file for reading/writing
|
|
4446
5324
|
mi_call(open(File, Mode, Stream), StateIn, StateIn) :-
|
|
4447
5325
|
!,
|
|
4448
|
-
|
|
4449
|
-
open(FullPath, Mode, Stream).
|
|
5326
|
+
open(File, Mode, Stream).
|
|
4450
5327
|
|
|
4451
5328
|
%% mi_call(close(Stream), +StateIn, -StateOut)
|
|
4452
5329
|
mi_call(close(Stream), StateIn, StateIn) :-
|
|
4453
5330
|
!,
|
|
4454
5331
|
close(Stream).
|
|
4455
5332
|
|
|
4456
|
-
%% mi_call(read_string(Stream, Length, Content), +StateIn, -StateOut)
|
|
4457
|
-
mi_call(read_string(Stream, Length, Content), StateIn, StateIn) :-
|
|
4458
|
-
!,
|
|
4459
|
-
read_string(Stream, Length, Content).
|
|
4460
|
-
|
|
4461
5333
|
%% mi_call(write(Stream, Content), +StateIn, -StateOut)
|
|
4462
5334
|
mi_call(write(Stream, Content), StateIn, StateIn) :-
|
|
4463
5335
|
!,
|
|
@@ -4466,60 +5338,51 @@ mi_call(write(Stream, Content), StateIn, StateIn) :-
|
|
|
4466
5338
|
%% mi_call(exists_file(File), +StateIn, -StateOut)
|
|
4467
5339
|
mi_call(exists_file(File), StateIn, StateIn) :-
|
|
4468
5340
|
!,
|
|
4469
|
-
|
|
4470
|
-
exists_file(FullPath).
|
|
5341
|
+
exists_file(File).
|
|
4471
5342
|
|
|
4472
5343
|
%% mi_call(exists_directory(Dir), +StateIn, -StateOut)
|
|
4473
5344
|
mi_call(exists_directory(Dir), StateIn, StateIn) :-
|
|
4474
5345
|
!,
|
|
4475
|
-
|
|
4476
|
-
exists_directory(FullPath).
|
|
5346
|
+
exists_directory(Dir).
|
|
4477
5347
|
|
|
4478
5348
|
%% mi_call(directory_files(Dir, Files), +StateIn, -StateOut)
|
|
4479
5349
|
mi_call(directory_files(Dir, Files), StateIn, StateIn) :-
|
|
4480
5350
|
!,
|
|
4481
|
-
|
|
4482
|
-
directory_files(FullPath, Files).
|
|
5351
|
+
directory_files(Dir, Files).
|
|
4483
5352
|
|
|
4484
5353
|
%% mi_call(make_directory(Dir), +StateIn, -StateOut)
|
|
4485
5354
|
mi_call(make_directory(Dir), StateIn, StateIn) :-
|
|
4486
5355
|
!,
|
|
4487
|
-
|
|
4488
|
-
make_directory(FullPath).
|
|
5356
|
+
make_directory(Dir).
|
|
4489
5357
|
|
|
4490
5358
|
%% mi_call(make_directory_path(Dir), +StateIn, -StateOut)
|
|
4491
5359
|
mi_call(make_directory_path(Dir), StateIn, StateIn) :-
|
|
4492
5360
|
!,
|
|
4493
|
-
|
|
4494
|
-
make_directory_path(FullPath).
|
|
5361
|
+
make_directory_path(Dir).
|
|
4495
5362
|
|
|
4496
5363
|
%% mi_call(delete_file(File), +StateIn, -StateOut)
|
|
4497
5364
|
mi_call(delete_file(File), StateIn, StateIn) :-
|
|
4498
5365
|
!,
|
|
4499
|
-
|
|
4500
|
-
delete_file(FullPath).
|
|
5366
|
+
delete_file(File).
|
|
4501
5367
|
|
|
4502
5368
|
%% mi_call(delete_directory(Dir), +StateIn, -StateOut)
|
|
4503
5369
|
mi_call(delete_directory(Dir), StateIn, StateIn) :-
|
|
4504
5370
|
!,
|
|
4505
|
-
|
|
4506
|
-
delete_directory(FullPath).
|
|
5371
|
+
delete_directory(Dir).
|
|
4507
5372
|
|
|
4508
5373
|
%% mi_call(size_file(File, Size), +StateIn, -StateOut)
|
|
4509
5374
|
%% size_file/2 is a plain SWI builtin, so it would otherwise resolve `File`
|
|
4510
5375
|
%% against the WASM process cwd (not the workspace) and demand `/workspace/...`.
|
|
4511
5376
|
mi_call(size_file(File, Size), StateIn, StateIn) :-
|
|
4512
5377
|
!,
|
|
4513
|
-
|
|
4514
|
-
size_file(FullPath, Size).
|
|
5378
|
+
size_file(File, Size).
|
|
4515
5379
|
|
|
4516
5380
|
%% mi_call(time_file(File, Time), +StateIn, -StateOut)
|
|
4517
5381
|
%% Modifications time (seconds since epoch); same workspace-relative rule as
|
|
4518
5382
|
%% size_file/2.
|
|
4519
5383
|
mi_call(time_file(File, Time), StateIn, StateIn) :-
|
|
4520
5384
|
!,
|
|
4521
|
-
|
|
4522
|
-
time_file(FullPath, Time).
|
|
5385
|
+
time_file(File, Time).
|
|
4523
5386
|
|
|
4524
5387
|
%% ============================================================
|
|
4525
5388
|
%% Catch/Throw - STATE THREADED
|
|
@@ -4625,6 +5488,7 @@ is_mi_special_predicate(prompt_named(_,_,_)). %% Transformed form of prompt/N
|
|
|
4625
5488
|
is_mi_special_predicate(llm(_,_)).
|
|
4626
5489
|
%% Tool scoping predicates
|
|
4627
5490
|
is_mi_special_predicate(with_tools(_,_)).
|
|
5491
|
+
is_mi_special_predicate(with_fresh_memory(_)).
|
|
4628
5492
|
is_mi_special_predicate(without_tools(_,_)).
|
|
4629
5493
|
%% Reasoning effort scoping
|
|
4630
5494
|
is_mi_special_predicate(with_reasoning(_,_)).
|
|
@@ -4856,6 +5720,14 @@ consume_gas(StateIn, StateOut) :-
|
|
|
4856
5720
|
%% mi_call_dispatch(+Goal, +StateIn, -StateOut)
|
|
4857
5721
|
%% Dispatch user-defined vs built-in predicates
|
|
4858
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
|
+
|
|
4859
5731
|
mi_call_dispatch(Goal, StateIn, StateIn) :-
|
|
4860
5732
|
predicate_property(Goal, built_in),
|
|
4861
5733
|
!, % Commit: it's built-in, no backtracking needed
|
|
@@ -4937,8 +5809,14 @@ var_to_name(Bindings, Var, Name) :-
|
|
|
4937
5809
|
%% expand_interpolations(+Term, +Bindings, -ExpandedTerm)
|
|
4938
5810
|
%% Walk a term and expand strings containing {VarName} patterns
|
|
4939
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).
|
|
4940
5818
|
expand_interpolations(Term, Bindings, ExpandedTerm) :-
|
|
4941
|
-
|
|
5819
|
+
expand_body_interp(Term, Bindings, ExpandedTerm).
|
|
4942
5820
|
|
|
4943
5821
|
%% expand_term_interp(+Term, +Bindings, -Expanded)
|
|
4944
5822
|
%% Main term expansion predicate
|
|
@@ -4947,13 +5825,16 @@ expand_interpolations(Term, Bindings, ExpandedTerm) :-
|
|
|
4947
5825
|
expand_term_interp(Var, _, Var) :-
|
|
4948
5826
|
var(Var), !.
|
|
4949
5827
|
|
|
4950
|
-
% Strings
|
|
4951
|
-
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
4956
|
-
|
|
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
|
+
!.
|
|
4957
5838
|
|
|
4958
5839
|
% Atoms - check if they need interpolation (for atom strings)
|
|
4959
5840
|
expand_term_interp(Atom, Bindings, Expanded) :-
|
|
@@ -4999,6 +5880,19 @@ expand_term_interp(Term, Bindings, ExpandedTerm) :-
|
|
|
4999
5880
|
expand_body_interp(Var, _, Var) :-
|
|
5000
5881
|
var(Var), !.
|
|
5001
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
|
+
|
|
5002
5896
|
% Conjunction
|
|
5003
5897
|
expand_body_interp((A, B), Bindings, ExpandedConj) :- !,
|
|
5004
5898
|
expand_body_interp(A, Bindings, EA),
|
|
@@ -5047,6 +5941,7 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
|
|
|
5047
5941
|
expand_body_interp(Goal, Bindings, ExpandedGoal) :-
|
|
5048
5942
|
compound(Goal),
|
|
5049
5943
|
Goal =.. [Functor|Args],
|
|
5944
|
+
interp_arg_predicate(Functor),
|
|
5050
5945
|
expand_args_hoist(Args, Bindings, PreGoals, ExpandedArgs, true),
|
|
5051
5946
|
!,
|
|
5052
5947
|
Bare =.. [Functor|ExpandedArgs],
|
|
@@ -5062,6 +5957,21 @@ expand_body_interp(Goal, Bindings, ExpandedGoal) :-
|
|
|
5062
5957
|
% Atoms/other
|
|
5063
5958
|
expand_body_interp(Goal, _, Goal).
|
|
5064
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
|
+
|
|
5065
5975
|
%% goal_with_string_arg(+Goal, -Functor, -StringArg, -RestArgs)
|
|
5066
5976
|
%% Match goals that take a string as their first argument
|
|
5067
5977
|
goal_with_string_arg(answer(S), answer, S, []) :- string(S).
|