@polycode-projects/the-mechanical-code-talker 1.10.13 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -101
- package/ROADMAP.md +17 -4
- package/bin/tmct.mjs +115 -9
- package/data/games/crates.txt +24 -0
- package/data/games/hanoi-3.txt +30 -0
- package/package.json +1 -1
- package/src/ask-browser.bundle.js +129 -398
- package/src/chat.mjs +613 -13
- package/src/domain.mjs +271 -0
- package/src/import-file.mjs +86 -0
- package/src/init.mjs +46 -1
- package/src/ledger-viz.mjs +613 -0
- package/src/memory/core.mjs +80 -16
- package/src/memory/shacl.mjs +17 -7
- package/src/memory-ask-browser-entry.mjs +4 -2
- package/src/memory-ask-browser.bundle.js +5157 -1165
- package/src/plan-viz.mjs +410 -0
- package/src/router/guardrail.mjs +5 -0
- package/src/router/registry.mjs +55 -11
- package/src/router/taught.mjs +73 -0
- package/src/sentences.mjs +19 -0
- package/src/viz-theme.mjs +50 -0
- package/src/viz.mjs +2 -2
- package/src/wink-model.mjs +12 -6
package/src/chat.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import { loadTemplates, render as renderTemplate } from "./corpus/templates.mjs"
|
|
|
34
34
|
import { resolveExtensions, mergedLexiconExtra } from "./extensions.mjs";
|
|
35
35
|
import { rankByBiasThenTrust } from "./memory/bias.mjs";
|
|
36
36
|
import { finish, beginsWithVowelSound, grammarRules } from "./finish.mjs";
|
|
37
|
+
import { splitSentences } from "./sentences.mjs";
|
|
37
38
|
import {
|
|
38
39
|
VERB_TO_KIND, WHERE_MARKERS, MENTION_MARKERS, ENTITY_TO_TYPE, PASSIVE_PARTICIPLE_TO_KIND,
|
|
39
40
|
stripTrailingScopeFiller, stripTrailingDiscourseTag, EDGE_NOUN_TO_METRIC, RELATIONS,
|
|
@@ -1533,6 +1534,16 @@ const WALL_MISS_ANYWHERE_RE = /couldn't parse this as a graph question\. Try:/;
|
|
|
1533
1534
|
// instead of the grammar wall or a silent data loss.
|
|
1534
1535
|
const TEACH_RE = /^(?:please\s+)?(?:i\s+(?:want|wanted)\s+you\s+to\s+|i(?:'d|\s+would)\s+like\s+you\s+to\s+)?(?:remember|note|keep in mind|jot down|for the record|fyi|learn)\b(?:\s+(?:this|that|also))?[:,]?\s*(?:that\s+)?(.+?)[.?!]*$/i;
|
|
1535
1536
|
const BARE_DECLARATIVE_RE = /^(?:every |each |all |a |an )?[\w-]+(?: [\w-]+)? (?:is|are) (?:a |an )?[\w-]+$/i;
|
|
1537
|
+
/** "X is <comparative> than Y" — the comparative teach/ask surface. The
|
|
1538
|
+
* comparative slot is closed by SHAPE (-er word, better/worse, or a
|
|
1539
|
+
* more/less + adjective pair), never a hand-list of adjectives. */
|
|
1540
|
+
const COMPARATIVE_SRC = "(?:[a-z]+er|better|worse|(?:more|less)\\s+[a-z]+)";
|
|
1541
|
+
const COMPARATIVE_TEACH_RE = new RegExp(`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(?:is|are)\\s+(${COMPARATIVE_SRC})\\s+than\\s+(.+)$`, "i");
|
|
1542
|
+
const COMPARATIVE_ASK_RE = new RegExp(`^(?:is|are)\\s+(.+?)\\s+(${COMPARATIVE_SRC})\\s+than\\s+(.+?)[?.!\\s]*$`, "i");
|
|
1543
|
+
/** The one closed preposition set shared by every frame that folds a
|
|
1544
|
+
* preposition into a minted predicate (the general-verb teach/query lanes
|
|
1545
|
+
* and the action-rule frames) — a single source so the set never forks. */
|
|
1546
|
+
const PREP_SRC = "on|in|at|onto|upon|under|over|beside|near|behind|above|below|inside|outside";
|
|
1536
1547
|
/** Interrogative / auxiliary leads that make an "X is a Y"-shaped line a QUESTION
|
|
1537
1548
|
* ("what is a cache", "is a module a component"), never a teach declarative. */
|
|
1538
1549
|
const QUESTION_LEAD_RE = /^(?:what|who|which|where|when|why|how|is|are|do|does|did|can|could|should|would|will|has|have)\b/i;
|
|
@@ -1749,6 +1760,86 @@ const FILTER_RULE_TEACH_RE =
|
|
|
1749
1760
|
const RECURSIVE_RULE_TEACH_RE =
|
|
1750
1761
|
/^an?\s+([a-z][\w-]*)\s+(?:is|are)\s+an?\s+([a-z][\w-]*),?\s+or\s+an?\s+([a-z][\w-]*)\s+of\s+an?\s+\1[.!?]*$/i;
|
|
1751
1762
|
|
|
1763
|
+
/** ACTION-RULE TEACH FRAMES — a world-mutating action taught one sentence at
|
|
1764
|
+
* a time, each sentence its own Rule individual (kind action-signature /
|
|
1765
|
+
* action-precond / action-effect) sharing one rule name ("<verb> <prep>",
|
|
1766
|
+
* e.g. "move onto"). src/domain.mjs collects the family by name
|
|
1767
|
+
* (findRulesByName) and grounds it over class members at plan time; nothing
|
|
1768
|
+
* in the teach lane executes an action. Predicate slot values are stored
|
|
1769
|
+
* BARE ("rest-on") because normFactTerm strips a mgx: prefix from slot
|
|
1770
|
+
* values; readers re-attach it. The class/role words are single tokens, the
|
|
1771
|
+
* preposition set is PREP_SRC, the comparative slot is COMPARATIVE_SRC. */
|
|
1772
|
+
const ACTION_SIGNATURE_TEACH_RE = new RegExp(
|
|
1773
|
+
`^you\\s+can\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1774
|
+
const ACTION_PRECOND_NOTHING_RE = new RegExp(
|
|
1775
|
+
`^to\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s*,?\\s*nothing\\s+may\\s+([a-z]+)\\s+(${PREP_SRC})\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1776
|
+
const ACTION_PRECOND_COMPARATIVE_RE = new RegExp(
|
|
1777
|
+
`^to\\s+([a-z]+)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s*,?\\s*the\\s+([a-z][\\w-]*)\\s+must\\s+be\\s+(${COMPARATIVE_SRC})\\s+than\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1778
|
+
const ACTION_EFFECT_TEACH_RE = new RegExp(
|
|
1779
|
+
`^([a-z]+ing)\\s+an?\\s+([a-z][\\w-]*)\\s+(${PREP_SRC})\\s+an?\\s+([a-z][\\w-]*)\\s+makes\\s+the\\s+([a-z][\\w-]*)\\s+([a-z]+)\\s+(${PREP_SRC})\\s+the\\s+([a-z][\\w-]*)[.!?]*$`, "i");
|
|
1780
|
+
/** "a disk renders as a block" — the render-template binding, an ordinary
|
|
1781
|
+
* Fact on the curated mgx:rendersAs predicate (camelCase, so the
|
|
1782
|
+
* general-verb preposition fold can never suffix it). */
|
|
1783
|
+
const RENDERS_AS_TEACH_RE = /^an?\s+([a-z][\w-]*)\s+renders\s+as\s+an?\s+([a-z][\w-]*)[.!?]*$/i;
|
|
1784
|
+
// Bare-copula instance membership: the subject MUST contain a hyphen
|
|
1785
|
+
// (disk-1, peg-a) — see bareTaxonomyTeach's reasoning.
|
|
1786
|
+
const INSTANCE_TYPE_TEACH_RE = /^([a-z][\w]*(?:-[\w]+)+)\s+is\s+an?\s+([a-z][\w-]+)[.!?]*$/i;
|
|
1787
|
+
// Bare article-led kind-of taxonomy: "a disk is a kind of game piece".
|
|
1788
|
+
const BARE_KINDOF_TEACH_RE = /^an?\s+([a-z][\w-]+)\s+is\s+a\s+kind\s+of\s+(?:an?\s+)?([a-z][\w-]+(?:\s+[a-z][\w-]+)?)([.!?]*)$/i;
|
|
1789
|
+
|
|
1790
|
+
/** Verb → lemma via the prose adapter, degrading to the word itself. */
|
|
1791
|
+
async function verbLemma(word) {
|
|
1792
|
+
const w = String(word || "").toLowerCase();
|
|
1793
|
+
try {
|
|
1794
|
+
const { proseLemma } = await import("./prose-nlp.mjs");
|
|
1795
|
+
const lemma = proseLemma();
|
|
1796
|
+
return lemma ? lemma(w) : w;
|
|
1797
|
+
} catch { return w; }
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
/** Pre-ask declarative taxonomy teaches. Checked BEFORE the ask engine: "a
|
|
1801
|
+
* disk is a kind of game piece." otherwise parses as an inherits QUESTION
|
|
1802
|
+
* and dies on term resolution, even though an article-led declarative with
|
|
1803
|
+
* no question lead is a statement. Two closed shapes only:
|
|
1804
|
+
* - instance membership with a HYPHENATED subject ("disk-1 is a disk") —
|
|
1805
|
+
* hyphenated/numbered coinages are unambiguous individual names, so this
|
|
1806
|
+
* stays clear of the plain-word bare "X is a Y" declines the tier-5
|
|
1807
|
+
* fabrication fixes deliberately preserve;
|
|
1808
|
+
* - article-led "is a kind of" taxonomy with a multi-word object — the
|
|
1809
|
+
* infix is unambiguous taxonomy-teach intent and the ACE path can't parse
|
|
1810
|
+
* the two-word object; single-word objects stay with the ACE path. */
|
|
1811
|
+
async function bareTaxonomyTeach(line, { memoryDir, sessionId }) {
|
|
1812
|
+
if (!memoryDir || QUESTION_LEAD_RE.test(line)) return null;
|
|
1813
|
+
const inst = line.match(INSTANCE_TYPE_TEACH_RE);
|
|
1814
|
+
if (inst) {
|
|
1815
|
+
return teachFact(memoryDir, sessionId, {
|
|
1816
|
+
subject: inst[1], predicate: "rdfs:subClassOf", object: inst[2],
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
const kindOf = line.match(BARE_KINDOF_TEACH_RE);
|
|
1820
|
+
if (kindOf) {
|
|
1821
|
+
// Defer to the ACE assert path exactly where it succeeds: a single-word
|
|
1822
|
+
// object with no trailing punctuation ("a father is a kind of parent" —
|
|
1823
|
+
// the pinned README transcript's shape, with its richer receipt). The ACE
|
|
1824
|
+
// path dies on multi-word objects and on trailing punctuation (the
|
|
1825
|
+
// period rides into term resolution), so those store here.
|
|
1826
|
+
const singleWordObject = !/\s/.test(kindOf[2]);
|
|
1827
|
+
const noTrailingPunct = kindOf[3] === "";
|
|
1828
|
+
if (singleWordObject && noTrailingPunct) return null;
|
|
1829
|
+
return teachFact(memoryDir, sessionId, {
|
|
1830
|
+
subject: kindOf[1], predicate: "rdfs:subClassOf", object: kindOf[2],
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
return null;
|
|
1834
|
+
}
|
|
1835
|
+
// The plan lane's closed recognizer set. The goal frame is plan-lane state,
|
|
1836
|
+
// not a Rule — goals accumulate on the session's planState slot.
|
|
1837
|
+
const GOAL_TEACH_RE = new RegExp(
|
|
1838
|
+
`^the\\s+goal\\s+is\\s+that\\s+(?:(every|each|all)\\s+)?([\\w-]+)\\s+([a-z]+s)\\s+(${PREP_SRC})\\s+([\\w-]+)[.!?]*$`, "i");
|
|
1839
|
+
const PLAN_SOLVE_RE = /^(?:solve\s+it|plan\s+the\s+moves|how\s+do\s+i\s+get(?:\s+from\s+here)?\s+to\s+the\s+goal)[?.!\s]*$/i;
|
|
1840
|
+
const LEGAL_MOVES_RE = /^what\s+moves\s+are\s+legal(?:\s+now)?[?.!\s]*$/i;
|
|
1841
|
+
const PLAN_NEXT_RE = /^(?:next|next\s+move|go\s+on|continue)[.!?\s]*$/i;
|
|
1842
|
+
|
|
1752
1843
|
/** "<X> is <adjective>" — the property teach payload (wrapper-REQUIRED): a lazy
|
|
1753
1844
|
* subject and a single bare complement word. Never matches the "is a <noun>"
|
|
1754
1845
|
* membership shape (that stays the ACE grammar's), so "remember that cache is
|
|
@@ -2391,7 +2482,7 @@ async function subjectIsNounOrPropn(word) {
|
|
|
2391
2482
|
// query is never shadowed. Reuses generalVerbTeach's own exclude guards and
|
|
2392
2483
|
// generalVerbPredicate, plus the SAME adverb-skip, so the two never disagree. ----
|
|
2393
2484
|
const GENERAL_VERB_YESNO_RE = new RegExp(`^(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[?.!\\s]*$`, "i");
|
|
2394
|
-
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+(?:\\s+(
|
|
2485
|
+
const GENERAL_VERB_OPEN_RE = new RegExp(`^what\\s+(?:does|did)\\s+([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+(?:\\s+(?:${PREP_SRC}))?)[?.!\\s]*$`, "i");
|
|
2395
2486
|
/** GENERAL_VERB_EXCLUDE_RE was written for generalVerbTeach's fully-conjugated
|
|
2396
2487
|
* declarative verb ("X OWNS Y", "X MAINTAINS Y") — but "does/did X <verb> Y"
|
|
2397
2488
|
* captures the BARE INFINITIVE after do-support ("does X OWN Y", never "does X
|
|
@@ -2407,7 +2498,7 @@ const GENERAL_VERB_QUERY_EXCLUDE_RE = /^(?:be|own|maintain)$/i;
|
|
|
2407
2498
|
* minted predicate: "disk-1 rests on peg-a" stores mgx:rest-on with object
|
|
2408
2499
|
* "peg-a", never mgx:rest with the meaning-bearing "on" buried inside the
|
|
2409
2500
|
* object where no read-back can match it. */
|
|
2410
|
-
const GENERAL_VERB_PREP_RE =
|
|
2501
|
+
const GENERAL_VERB_PREP_RE = new RegExp(`^(${PREP_SRC})\\s+(.+)$`, "i");
|
|
2411
2502
|
/** Fold a leading preposition from `objectRaw` into a minted mgx:<lemma>
|
|
2412
2503
|
* predicate. Curated predicates (mgx:hasA, mgx:capableOf — anything not the
|
|
2413
2504
|
* plain lowercase mint shape) are never suffixed. Returns {predicate,
|
|
@@ -2567,7 +2658,11 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2567
2658
|
// `are` payload — see the payload-construction block below), which leaves
|
|
2568
2659
|
// whatever the structural grammar's own honest miss already said standing,
|
|
2569
2660
|
// rather than overwriting it with a wrong-reason refusal.
|
|
2570
|
-
|
|
2661
|
+
// The action-signature frame is the ONE pronoun-led teach shape ("you can
|
|
2662
|
+
// move a disk onto a peg") — the full-shape test keeps "you can fly"
|
|
2663
|
+
// declining right here.
|
|
2664
|
+
if (pronounMatch && !ACTION_SIGNATURE_TEACH_RE.test(pronounSrc)
|
|
2665
|
+
&& !(await hasMidSentenceInterrogative(pronounSrc))) {
|
|
2571
2666
|
const pronoun = pronounMatch[1];
|
|
2572
2667
|
return {
|
|
2573
2668
|
text: `I can't store a fact about "${pronoun}" as a class — pronouns aren't things I can classify. `
|
|
@@ -2780,6 +2875,167 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2780
2875
|
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2781
2876
|
}
|
|
2782
2877
|
|
|
2878
|
+
// ACTION-RULE TEACH — the four action frames plus the render binding (see
|
|
2879
|
+
// the ACTION_*_TEACH_RE docblock). Each sentence stores its own Rule
|
|
2880
|
+
// individual under a shared "<verb> <prep>" name. A role word that names
|
|
2881
|
+
// neither the taught subject class nor the literal "target" is an honest
|
|
2882
|
+
// decline that RETURNS here — falling through would hand these shapes to
|
|
2883
|
+
// the general-verb lane below, which would mint a garbage predicate from
|
|
2884
|
+
// them (the silent-garble case this lane exists to prevent).
|
|
2885
|
+
const actionLemma = verbLemma;
|
|
2886
|
+
const actionRoleFor = (word, subjectClass) => {
|
|
2887
|
+
const w = String(word || "").toLowerCase();
|
|
2888
|
+
if (w === "target") return "target";
|
|
2889
|
+
if (w === String(subjectClass || "").toLowerCase()) return "subject";
|
|
2890
|
+
return null;
|
|
2891
|
+
};
|
|
2892
|
+
|
|
2893
|
+
const actionSig = ownSrc.match(ACTION_SIGNATURE_TEACH_RE);
|
|
2894
|
+
if (actionSig && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
2895
|
+
try {
|
|
2896
|
+
const verb = await actionLemma(actionSig[1]);
|
|
2897
|
+
const prep = actionSig[3].toLowerCase();
|
|
2898
|
+
const { appendRule, RULE_KIND_ACTION_SIGNATURE } = await import("./memory/core.mjs");
|
|
2899
|
+
const { id } = await appendRule(memoryDir, {
|
|
2900
|
+
name: `${verb} ${prep}`,
|
|
2901
|
+
kind: RULE_KIND_ACTION_SIGNATURE,
|
|
2902
|
+
slots: { subjectClass: actionSig[2], targetClass: actionSig[4] },
|
|
2903
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
2904
|
+
});
|
|
2905
|
+
if (id) {
|
|
2906
|
+
return {
|
|
2907
|
+
text: `noted — remembered: you can ${verb} a ${actionSig[2].toLowerCase()} ${prep} a ${actionSig[4].toLowerCase()}`,
|
|
2908
|
+
via: "assert", miss: false,
|
|
2909
|
+
};
|
|
2910
|
+
}
|
|
2911
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2912
|
+
}
|
|
2913
|
+
|
|
2914
|
+
const precondNothing = ownSrc.match(ACTION_PRECOND_NOTHING_RE);
|
|
2915
|
+
if (precondNothing && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
2916
|
+
const role = actionRoleFor(precondNothing[7], precondNothing[2]);
|
|
2917
|
+
if (!role) {
|
|
2918
|
+
return {
|
|
2919
|
+
text: `I can't place "${precondNothing[7]}" in that rule — the last word must be "target" or the ${precondNothing[2]} itself (e.g. "nothing may ${precondNothing[5].toLowerCase()} ${precondNothing[6].toLowerCase()} the ${precondNothing[2]}").`,
|
|
2920
|
+
via: "teach-miss", miss: true,
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
try {
|
|
2924
|
+
const verb = await actionLemma(precondNothing[1]);
|
|
2925
|
+
const prep = precondNothing[3].toLowerCase();
|
|
2926
|
+
const innerVerb = await actionLemma(precondNothing[5]);
|
|
2927
|
+
const scopeWord = precondNothing[4].toLowerCase();
|
|
2928
|
+
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("./memory/core.mjs");
|
|
2929
|
+
const { id } = await appendRule(memoryDir, {
|
|
2930
|
+
name: `${verb} ${prep}`,
|
|
2931
|
+
kind: RULE_KIND_ACTION_PRECOND,
|
|
2932
|
+
slots: {
|
|
2933
|
+
shape: "no-incoming",
|
|
2934
|
+
predicate: `${innerVerb}-${precondNothing[6].toLowerCase()}`,
|
|
2935
|
+
role,
|
|
2936
|
+
scope: scopeWord === "target" ? "any" : scopeWord,
|
|
2937
|
+
},
|
|
2938
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
2939
|
+
});
|
|
2940
|
+
if (id) {
|
|
2941
|
+
return {
|
|
2942
|
+
text: `noted — remembered: to ${verb} ${prep}, nothing may ${precondNothing[5].toLowerCase()} ${precondNothing[6].toLowerCase()} the ${role === "target" ? "target" : precondNothing[2].toLowerCase()}`,
|
|
2943
|
+
via: "assert", miss: false,
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
const precondComp = ownSrc.match(ACTION_PRECOND_COMPARATIVE_RE);
|
|
2950
|
+
if (precondComp && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
2951
|
+
const role = actionRoleFor(precondComp[5], precondComp[2]);
|
|
2952
|
+
const rightWord = precondComp[7].toLowerCase();
|
|
2953
|
+
const otherOk = role === "subject"
|
|
2954
|
+
? (rightWord === "target" || rightWord === precondComp[4].toLowerCase())
|
|
2955
|
+
: (role === "target" && rightWord === precondComp[2].toLowerCase());
|
|
2956
|
+
if (!role || !otherOk) {
|
|
2957
|
+
return {
|
|
2958
|
+
text: `I can't place "${!role ? precondComp[5] : precondComp[7]}" in that rule — the compared words must be the ${precondComp[2]} and the target (e.g. "the ${precondComp[2]} must be smaller than the target").`,
|
|
2959
|
+
via: "teach-miss", miss: true,
|
|
2960
|
+
};
|
|
2961
|
+
}
|
|
2962
|
+
try {
|
|
2963
|
+
const verb = await actionLemma(precondComp[1]);
|
|
2964
|
+
const prep = precondComp[3].toLowerCase();
|
|
2965
|
+
const scopeWord = precondComp[4].toLowerCase();
|
|
2966
|
+
const { appendRule, RULE_KIND_ACTION_PRECOND } = await import("./memory/core.mjs");
|
|
2967
|
+
const { id } = await appendRule(memoryDir, {
|
|
2968
|
+
name: `${verb} ${prep}`,
|
|
2969
|
+
kind: RULE_KIND_ACTION_PRECOND,
|
|
2970
|
+
slots: {
|
|
2971
|
+
shape: "comparator",
|
|
2972
|
+
predicate: `${precondComp[6].toLowerCase().replace(/\s+/g, "-")}-than`,
|
|
2973
|
+
role,
|
|
2974
|
+
scope: scopeWord === "target" ? "any" : scopeWord,
|
|
2975
|
+
},
|
|
2976
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
2977
|
+
});
|
|
2978
|
+
if (id) {
|
|
2979
|
+
return {
|
|
2980
|
+
text: `noted — remembered: to ${verb} ${prep}, the ${precondComp[5].toLowerCase()} must be ${precondComp[6].toLowerCase()} than the ${rightWord}`,
|
|
2981
|
+
via: "assert", miss: false,
|
|
2982
|
+
};
|
|
2983
|
+
}
|
|
2984
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
const actionEffect = ownSrc.match(ACTION_EFFECT_TEACH_RE);
|
|
2988
|
+
if (actionEffect && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
2989
|
+
const gerund = actionEffect[1].toLowerCase();
|
|
2990
|
+
const verb = await actionLemma(gerund);
|
|
2991
|
+
// An unreduced -ing form would mint a name ("moving onto") that can never
|
|
2992
|
+
// match the signature's ("move onto") — decline rather than store a rule
|
|
2993
|
+
// the interpreter can't collect.
|
|
2994
|
+
if (verb === gerund || !gerund.startsWith(verb.slice(0, Math.min(3, verb.length)))) {
|
|
2995
|
+
return {
|
|
2996
|
+
text: `I can't reduce "${actionEffect[1]}" to its verb right now — the lemmatizer isn't available. Retry later, or teach the other rule sentences first.`,
|
|
2997
|
+
via: "teach-miss", miss: true,
|
|
2998
|
+
};
|
|
2999
|
+
}
|
|
3000
|
+
const subjectRole = actionRoleFor(actionEffect[5], actionEffect[2]);
|
|
3001
|
+
const objectRole = actionRoleFor(actionEffect[8], actionEffect[2]);
|
|
3002
|
+
if (!subjectRole || !objectRole || subjectRole === objectRole) {
|
|
3003
|
+
return {
|
|
3004
|
+
text: `I can't place "${!subjectRole ? actionEffect[5] : actionEffect[8]}" in that rule — the effect must relate the ${actionEffect[2]} and the target, once each (e.g. "makes the ${actionEffect[2]} rest on the target").`,
|
|
3005
|
+
via: "teach-miss", miss: true,
|
|
3006
|
+
};
|
|
3007
|
+
}
|
|
3008
|
+
try {
|
|
3009
|
+
const prep = actionEffect[3].toLowerCase();
|
|
3010
|
+
const effVerb = await actionLemma(actionEffect[6]);
|
|
3011
|
+
const { appendRule, RULE_KIND_ACTION_EFFECT } = await import("./memory/core.mjs");
|
|
3012
|
+
const { id } = await appendRule(memoryDir, {
|
|
3013
|
+
name: `${verb} ${prep}`,
|
|
3014
|
+
kind: RULE_KIND_ACTION_EFFECT,
|
|
3015
|
+
slots: {
|
|
3016
|
+
predicate: `${effVerb}-${actionEffect[7].toLowerCase()}`,
|
|
3017
|
+
subjectRole,
|
|
3018
|
+
objectRole,
|
|
3019
|
+
},
|
|
3020
|
+
provenance: teachProvenanceTag(sessionId, new Date().toISOString()),
|
|
3021
|
+
});
|
|
3022
|
+
if (id) {
|
|
3023
|
+
return {
|
|
3024
|
+
text: `noted — remembered: ${gerund} a ${actionEffect[2].toLowerCase()} ${prep} a ${actionEffect[4].toLowerCase()} makes the ${actionEffect[5].toLowerCase()} ${actionEffect[6].toLowerCase()} ${actionEffect[7].toLowerCase()} the ${actionEffect[8].toLowerCase()}`,
|
|
3025
|
+
via: "assert", miss: false,
|
|
3026
|
+
};
|
|
3027
|
+
}
|
|
3028
|
+
} catch { /* malformed slots — fall through to the ordinary honest-miss cascade */ }
|
|
3029
|
+
}
|
|
3030
|
+
|
|
3031
|
+
const rendersAs = ownSrc.match(RENDERS_AS_TEACH_RE);
|
|
3032
|
+
if (rendersAs && memoryDir && !QUESTION_LEAD_RE.test(ownSrc) && !ownSrcMidQuestion) {
|
|
3033
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3034
|
+
subject: rendersAs[1], predicate: "mgx:rendersAs", object: rendersAs[2],
|
|
3035
|
+
});
|
|
3036
|
+
if (stored) return stored;
|
|
3037
|
+
}
|
|
3038
|
+
|
|
2783
3039
|
// "some Xs are Ys" / "a few Xs are Ys" — the plural class-
|
|
2784
3040
|
// membership quantifier shape. ACE has no quantifier-phrase pattern at all
|
|
2785
3041
|
// (parseAce never even attempts a fit), so this is ALWAYS a direct write,
|
|
@@ -2879,7 +3135,7 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2879
3135
|
|
|
2880
3136
|
let payload = null;
|
|
2881
3137
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
2882
|
-
else if (BARE_DECLARATIVE_RE.test(raw) && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
3138
|
+
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)) && !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
2883
3139
|
if (!payload) {
|
|
2884
3140
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
2885
3141
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -2897,6 +3153,20 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
2897
3153
|
// Try to store it (a live session provides the write target). assertTurn returns
|
|
2898
3154
|
// the "noted — remembered …" confirmation or null (grammar miss / unknown words).
|
|
2899
3155
|
if (memoryDir) {
|
|
3156
|
+
// COMPARATIVE frame — "disk-1 is smaller than disk-2" → mgx:smaller-than.
|
|
3157
|
+
// Checked ahead of the ACE candidates: the copula plus "than" is not in
|
|
3158
|
+
// the ACE fragment at all, and letting it fall through produced the
|
|
3159
|
+
// both-sides-ungrounded decline (honest but unactionable — no phrasing
|
|
3160
|
+
// it could suggest would have stored a comparison).
|
|
3161
|
+
const comp = String(payload).trim().match(COMPARATIVE_TEACH_RE);
|
|
3162
|
+
if (comp) {
|
|
3163
|
+
const compPredicate = `mgx:${comp[2].toLowerCase().replace(/\s+/g, "-")}-than`;
|
|
3164
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
3165
|
+
subject: comp[1].trim(), predicate: compPredicate,
|
|
3166
|
+
object: comp[3].trim().replace(/[.!?]+$/, ""),
|
|
3167
|
+
});
|
|
3168
|
+
if (stored) return stored;
|
|
3169
|
+
}
|
|
2900
3170
|
for (const cand of assertCandidates(payload)) {
|
|
2901
3171
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
2902
3172
|
// universal success, so every caller (this loop AND the top-level
|
|
@@ -3714,6 +3984,7 @@ const FACT_PREDICATE_PHRASES = {
|
|
|
3714
3984
|
"mgx:hasLastSubevent": "ends with",
|
|
3715
3985
|
"mgx:hasPrerequisite": "requires",
|
|
3716
3986
|
"mgx:ownedBy": "is owned by", // the teach lane's ownership frame ("Priya owns tasks.mjs")
|
|
3987
|
+
"mgx:rendersAs": "renders as", // the render-template binding ("a disk renders as a block")
|
|
3717
3988
|
"mgx:synonym": "means the same as",
|
|
3718
3989
|
"mgx:antonym": "is the opposite of",
|
|
3719
3990
|
"mgx:similarTo": "is similar to",
|
|
@@ -3746,7 +4017,12 @@ function thirdPersonSingularSurface(lemma) {
|
|
|
3746
4017
|
}
|
|
3747
4018
|
function predicatePhrase(predicate) {
|
|
3748
4019
|
if (FACT_PREDICATE_PHRASES[predicate]) return FACT_PREDICATE_PHRASES[predicate];
|
|
3749
|
-
const
|
|
4020
|
+
const p = String(predicate || "");
|
|
4021
|
+
// a comparative renders as its copula surface: mgx:smaller-than ->
|
|
4022
|
+
// "is smaller than" (never a 3sg fold — "smallers" isn't a word)
|
|
4023
|
+
const comp = /^mgx:([a-z]+(?:-[a-z]+)*)-than$/i.exec(p);
|
|
4024
|
+
if (comp) return `is ${comp[1].replace(/-/g, " ")} than`;
|
|
4025
|
+
const m = /^mgx:([a-z]+)(?:-([a-z]+))?$/i.exec(p);
|
|
3750
4026
|
if (!m) return predicate;
|
|
3751
4027
|
// a folded preposition renders back naturally: mgx:rest-on -> "rests on"
|
|
3752
4028
|
return `${thirdPersonSingularSurface(m[1])}${m[2] ? ` ${m[2]}` : ""}`;
|
|
@@ -4385,6 +4661,30 @@ export async function factAnswer(memoryDir, query, envelope, miss, biasByBundle
|
|
|
4385
4661
|
}
|
|
4386
4662
|
if (!miss) return null;
|
|
4387
4663
|
|
|
4664
|
+
// (b0-comp) "is disk-1 smaller than disk-2" — yes iff the exact taught
|
|
4665
|
+
// comparative fact exists; otherwise an honest, specific miss whose teach
|
|
4666
|
+
// hint is the EXACT phrasing the comparative teach frame accepts. Never an
|
|
4667
|
+
// inverted guess: "disk-1 is smaller than disk-2" proves nothing here
|
|
4668
|
+
// about "is disk-2 smaller than disk-1" (the frame stores no
|
|
4669
|
+
// antisymmetry), so the reverse question stays a can't-confirm.
|
|
4670
|
+
const compAsk = q.match(COMPARATIVE_ASK_RE);
|
|
4671
|
+
if (compAsk) {
|
|
4672
|
+
const compWord = compAsk[2].toLowerCase().replace(/\s+/g, "-");
|
|
4673
|
+
const compPredicate = `mgx:${compWord}-than`;
|
|
4674
|
+
const facts = await memoryFacts(memoryDir);
|
|
4675
|
+
const subj = factTermVariants(normFactTerm, compAsk[1].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
4676
|
+
const obj = factTermVariants(normFactTerm, compAsk[3].replace(/^(?:an?|the)\s+/i, "").trim());
|
|
4677
|
+
const hit = facts.find((f) => f.predicate === compPredicate && subj.has(f.subject) && obj.has(f.object));
|
|
4678
|
+
if (hit) return { text: `yes — ${renderFactLine(hit)}`, replace: true };
|
|
4679
|
+
const known = facts.filter((f) => f.predicate === compPredicate && (subj.has(f.subject) || subj.has(f.object)));
|
|
4680
|
+
const shown = known.length ? ` I do know: ${known.slice(0, 3).map(renderFactLine).join("; ")}.` : "";
|
|
4681
|
+
return {
|
|
4682
|
+
text: `I can't confirm that — nothing I remember compares them that way.${shown} If it's true, teach me: "${compAsk[1].trim()} is ${compAsk[2].toLowerCase()} than ${compAsk[3].trim()}".`,
|
|
4683
|
+
replace: true,
|
|
4684
|
+
miss: true,
|
|
4685
|
+
};
|
|
4686
|
+
}
|
|
4687
|
+
|
|
4388
4688
|
// (b0) Derived forward yes/no readers — FORWARD_YESNO_MARKERS, one per
|
|
4389
4689
|
// renderable relation. Runs BEFORE the isa lane because ISA_ASK_RE's lazy
|
|
4390
4690
|
// subject otherwise swallows these shapes whole ("is a wheel part of a
|
|
@@ -4981,7 +5281,7 @@ function inheritsChain(graph, startId) {
|
|
|
4981
5281
|
* "what kind of thing is an X" reports X's own type (subject-side first).
|
|
4982
5282
|
* Miss-only and run AFTER factAnswer returns null, so it never shadows the
|
|
4983
5283
|
* subject-side answer or a schema hit. Returns { text, replace:true } or null. */
|
|
4984
|
-
async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}, cache = null) {
|
|
5284
|
+
export async function factReadBack(memoryDir, query, envelope, miss, graph = null, focusLabel = null, biasByBundle = {}, cache = null) {
|
|
4985
5285
|
if (!miss) return null;
|
|
4986
5286
|
let normFactTerm;
|
|
4987
5287
|
try { ({ normFactTerm } = await import("./memory/core.mjs")); } catch { return null; }
|
|
@@ -7023,7 +7323,222 @@ async function entityOfKindInText(graph, expectedClass, answerText) {
|
|
|
7023
7323
|
* otherwise the unchanged dispatchTool path (which also yields the no-graph error).
|
|
7024
7324
|
* A hit updates the focus to the resolved object. Grammar miss / ToolError → a
|
|
7025
7325
|
* normal answer, never a crash. */
|
|
7026
|
-
|
|
7326
|
+
/** Load the taught domain for the plan lane: fact rows + rule rows compiled
|
|
7327
|
+
* through src/domain.mjs. Fresh-loads memory (never the turn cache) because
|
|
7328
|
+
* the caller may have just written snapshot rows this same turn. */
|
|
7329
|
+
async function loadPlanContext(memoryDir) {
|
|
7330
|
+
const { loadMemory, readFactRows, readRuleRows } = await import("./memory/core.mjs");
|
|
7331
|
+
const { compileDomain, stateFromFacts } = await import("./domain.mjs");
|
|
7332
|
+
const payload = await loadMemory(memoryDir);
|
|
7333
|
+
const factRows = readFactRows(payload);
|
|
7334
|
+
const ruleRows = readRuleRows(payload);
|
|
7335
|
+
const domain = compileDomain(factRows, ruleRows);
|
|
7336
|
+
const state = stateFromFacts(factRows, domain);
|
|
7337
|
+
return { factRows, ruleRows, domain, state };
|
|
7338
|
+
}
|
|
7339
|
+
|
|
7340
|
+
/** Human label for a grounded action: name "move onto" + disk-1 + peg-c →
|
|
7341
|
+
* "move disk-1 onto peg-c". */
|
|
7342
|
+
function actionLabel(name, subject, target) {
|
|
7343
|
+
const sp = String(name).split(/\s+/);
|
|
7344
|
+
const verb = sp[0] || "move";
|
|
7345
|
+
const prep = sp.slice(1).join(" ") || "onto";
|
|
7346
|
+
return `${verb} ${subject} ${prep} ${target}`;
|
|
7347
|
+
}
|
|
7348
|
+
|
|
7349
|
+
/** THE PLAN LANE — the closed goal/solve/legal-moves recognizers over the
|
|
7350
|
+
* taught action rules (PLAN_HANOI's chat surface). Returns
|
|
7351
|
+
* { text, via, deduced, note, plan? } or null when the query is none of the
|
|
7352
|
+
* three shapes. Mutates planHolder.state (the session's plan slot). */
|
|
7353
|
+
async function planLaneAnswer(query, { memoryDir, planHolder, sessionId = "", }) {
|
|
7354
|
+
const q = String(query).trim();
|
|
7355
|
+
|
|
7356
|
+
const goalMatch = q.match(GOAL_TEACH_RE);
|
|
7357
|
+
if (goalMatch) {
|
|
7358
|
+
const { normFactTerm } = await import("./memory/core.mjs");
|
|
7359
|
+
const verb = await verbLemma(goalMatch[3]);
|
|
7360
|
+
if (!verb) {
|
|
7361
|
+
return {
|
|
7362
|
+
text: `I can't reduce "${goalMatch[3]}" to a verb for that goal — try the plain form (e.g. "rests").`,
|
|
7363
|
+
via: "plan", deduced: "record the goal state for a later plan", note: "GOAL frame — verb lemma unavailable, honest decline",
|
|
7364
|
+
};
|
|
7365
|
+
}
|
|
7366
|
+
const spec = {
|
|
7367
|
+
universal: !!goalMatch[1],
|
|
7368
|
+
term: normFactTerm(goalMatch[2]),
|
|
7369
|
+
predicate: `${verb}-${goalMatch[4].toLowerCase()}`,
|
|
7370
|
+
object: normFactTerm(goalMatch[5]),
|
|
7371
|
+
};
|
|
7372
|
+
const tail = q.replace(/^the\s+goal\s+is\s+that\s+/i, "").replace(/[.!?]+$/, "");
|
|
7373
|
+
const prev = planHolder.state && Array.isArray(planHolder.state.goals) && !planHolder.state.done ? planHolder.state : null;
|
|
7374
|
+
planHolder.state = {
|
|
7375
|
+
goals: [...(prev?.goals ?? []), spec],
|
|
7376
|
+
goalTexts: [...(prev?.goalTexts ?? []), tail],
|
|
7377
|
+
actions: null, states: null, stepGoals: null, cursor: 0, done: false,
|
|
7378
|
+
};
|
|
7379
|
+
const n = planHolder.state.goals.length;
|
|
7380
|
+
return {
|
|
7381
|
+
text: `noted — the goal is that ${tail}.${n > 1 ? ` (${n} goals held)` : ""} Say "solve it" when the state is taught.`,
|
|
7382
|
+
via: "plan", deduced: "record the goal state for a later plan",
|
|
7383
|
+
note: "GOAL frame — goal spec accumulated on the session plan slot",
|
|
7384
|
+
};
|
|
7385
|
+
}
|
|
7386
|
+
|
|
7387
|
+
const wantsSolve = PLAN_SOLVE_RE.test(q);
|
|
7388
|
+
const wantsLegal = LEGAL_MOVES_RE.test(q);
|
|
7389
|
+
if (!wantsSolve && !wantsLegal) return null;
|
|
7390
|
+
|
|
7391
|
+
let ctx;
|
|
7392
|
+
try {
|
|
7393
|
+
ctx = await loadPlanContext(memoryDir);
|
|
7394
|
+
} catch (err) {
|
|
7395
|
+
return { text: `I can't read the taught domain: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence", note: "plan lane — domain load failed" };
|
|
7396
|
+
}
|
|
7397
|
+
const { domain, state, factRows } = ctx;
|
|
7398
|
+
if (!domain.actions.length) {
|
|
7399
|
+
return {
|
|
7400
|
+
text: `no action rules taught yet — teach the game first (e.g. "you can move a disk onto a peg").`,
|
|
7401
|
+
via: "plan", deduced: "plan a move sequence (no action rules yet)", note: "plan lane — honest decline: no action rules",
|
|
7402
|
+
};
|
|
7403
|
+
}
|
|
7404
|
+
if (!state.length) {
|
|
7405
|
+
return {
|
|
7406
|
+
text: `no current state taught yet — state the board first (e.g. "disk-1 rests on peg-a").`,
|
|
7407
|
+
via: "plan", deduced: "plan a move sequence (no state yet)", note: "plan lane — honest decline: empty state",
|
|
7408
|
+
};
|
|
7409
|
+
}
|
|
7410
|
+
const { movesFromRules, stateKeyFor, compileGoal, PlanBudgetError } = await import("./domain.mjs");
|
|
7411
|
+
|
|
7412
|
+
if (wantsLegal) {
|
|
7413
|
+
let moves;
|
|
7414
|
+
try {
|
|
7415
|
+
moves = movesFromRules(state, domain);
|
|
7416
|
+
} catch (err) {
|
|
7417
|
+
if (err instanceof PlanBudgetError) {
|
|
7418
|
+
return { text: `too many possible moves to enumerate here (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "list the legal moves (budget exceeded)", note: "plan lane — budget decline" };
|
|
7419
|
+
}
|
|
7420
|
+
throw err;
|
|
7421
|
+
}
|
|
7422
|
+
if (!moves.length) {
|
|
7423
|
+
return { text: "no legal moves from the current state.", via: "plan", deduced: "list the legal moves (none)", note: "plan lane — legal moves: none" };
|
|
7424
|
+
}
|
|
7425
|
+
const lines = moves.map((m, i) => ` ${i + 1}. ${actionLabel(m.action.name, m.action.subject, m.action.target)}`);
|
|
7426
|
+
return {
|
|
7427
|
+
text: `${moves.length} legal move${moves.length === 1 ? "" : "s"} from here:\n${lines.join("\n")}`,
|
|
7428
|
+
via: "plan", deduced: "list the legal moves from the current state",
|
|
7429
|
+
note: "plan lane — movesFromRules over the current snapshot, one ply, no search",
|
|
7430
|
+
};
|
|
7431
|
+
}
|
|
7432
|
+
|
|
7433
|
+
// "solve it" — the full search.
|
|
7434
|
+
if (!planHolder.state?.goals?.length) {
|
|
7435
|
+
return {
|
|
7436
|
+
text: `no goal set yet — teach one first (e.g. "the goal is that every disk rests on peg-c").`,
|
|
7437
|
+
via: "plan", deduced: "plan a move sequence (no goal yet)", note: "plan lane — honest decline: no goal",
|
|
7438
|
+
};
|
|
7439
|
+
}
|
|
7440
|
+
const goals = planHolder.state.goals;
|
|
7441
|
+
const goalText = planHolder.state.goalTexts.join("; ");
|
|
7442
|
+
let isGoal;
|
|
7443
|
+
try {
|
|
7444
|
+
isGoal = compileGoal(goals, domain);
|
|
7445
|
+
} catch (err) {
|
|
7446
|
+
return { text: `I can't compile that goal: ${err?.message ?? err}`, via: "plan", deduced: "plan a move sequence (uncompilable goal)", note: "plan lane — goal compile decline" };
|
|
7447
|
+
}
|
|
7448
|
+
const { findActionPath } = await import("./planning.mjs");
|
|
7449
|
+
let found;
|
|
7450
|
+
try {
|
|
7451
|
+
found = findActionPath(state, isGoal, (s) => movesFromRules(s, domain), { maxDepth: 300, stateKey: stateKeyFor });
|
|
7452
|
+
} catch (err) {
|
|
7453
|
+
if (err instanceof PlanBudgetError) {
|
|
7454
|
+
return { text: `the search space is too large (${err.message}) — narrow the classes involved.`, via: "plan", deduced: "plan a move sequence (budget exceeded)", note: "plan lane — budget decline" };
|
|
7455
|
+
}
|
|
7456
|
+
throw err;
|
|
7457
|
+
}
|
|
7458
|
+
if (!found) {
|
|
7459
|
+
return {
|
|
7460
|
+
text: `no plan found within 300 moves from the current state to: ${goalText}.`,
|
|
7461
|
+
via: "plan", deduced: "plan a move sequence (no path)", note: "plan lane — honest miss: findActionPath returned null",
|
|
7462
|
+
};
|
|
7463
|
+
}
|
|
7464
|
+
const n = found.actions.length;
|
|
7465
|
+
const actions = found.actions.map((a) => ({
|
|
7466
|
+
name: a.name, subject: a.subject, target: a.target,
|
|
7467
|
+
label: actionLabel(a.name, a.subject, a.target),
|
|
7468
|
+
}));
|
|
7469
|
+
const stepGoals = actions.map((a, i) =>
|
|
7470
|
+
`${a.label} (step ${i + 1} of ${n}, working toward: ${goalText})`);
|
|
7471
|
+
const renderHints = {};
|
|
7472
|
+
const ordering = [];
|
|
7473
|
+
for (const r of factRows) {
|
|
7474
|
+
if (r.predicate === "mgx:rendersAs") renderHints[r.subject] = r.object;
|
|
7475
|
+
else if (/-than$/.test(r.predicate)) ordering.push({ subject: r.subject, predicate: r.predicate, object: r.object });
|
|
7476
|
+
}
|
|
7477
|
+
const plan = {
|
|
7478
|
+
actions, states: found.states, stepGoals,
|
|
7479
|
+
goal: { text: goalText, specs: goals },
|
|
7480
|
+
domain: { classMembers: domain.classMembers, ordering, renderHints },
|
|
7481
|
+
};
|
|
7482
|
+
planHolder.state = {
|
|
7483
|
+
...planHolder.state, actions, states: found.states, stepGoals, cursor: 0, done: false, goalText,
|
|
7484
|
+
};
|
|
7485
|
+
const ruleNames = [...new Set(domain.actions.map((a) => a.name))].join('", "');
|
|
7486
|
+
const moveLines = actions.map((a, i) => ` ${i + 1}. ${a.label}`);
|
|
7487
|
+
const text = n === 0
|
|
7488
|
+
? `the goal already holds — nothing to do.`
|
|
7489
|
+
: `plan found — ${n} move${n === 1 ? "" : "s"} (shortest):\n${moveLines.join("\n")}\n\n` +
|
|
7490
|
+
`because — you taught me the "${ruleNames}" rule${domain.actions.length === 1 ? "" : "s"}` +
|
|
7491
|
+
`${ordering.length ? ` and ${ordering.length} ordering fact${ordering.length === 1 ? "" : "s"}` : ""}. ` +
|
|
7492
|
+
`Say "next" to make move 1, or ask "what moves are legal now".`;
|
|
7493
|
+
return {
|
|
7494
|
+
text, via: "plan",
|
|
7495
|
+
deduced: `plan a move sequence from the current state to the goal (${n} move${n === 1 ? "" : "s"})`,
|
|
7496
|
+
note: "plan lane — compileDomain + findActionPath over the taught rules; plan held on the session slot",
|
|
7497
|
+
plan,
|
|
7498
|
+
};
|
|
7499
|
+
}
|
|
7500
|
+
|
|
7501
|
+
/** Execute the active plan's next move: append the successor snapshot's rows
|
|
7502
|
+
* as @stepK facts, advance the cursor, and on the final step re-read the
|
|
7503
|
+
* store and confirm the goal from the WRITTEN facts (never assumed). */
|
|
7504
|
+
async function executePlanStep(planHolder, { memoryDir, sessionId = "" }) {
|
|
7505
|
+
const ps = planHolder.state;
|
|
7506
|
+
const k = ps.cursor + 1;
|
|
7507
|
+
const action = ps.actions[ps.cursor];
|
|
7508
|
+
const rows = ps.states[k];
|
|
7509
|
+
const { appendFact, loadMemory, readFactRows } = await import("./memory/core.mjs");
|
|
7510
|
+
for (const row of rows) {
|
|
7511
|
+
await appendFact(memoryDir, {
|
|
7512
|
+
subject: `${row.subject}@step${k}`, predicate: row.predicate, object: row.object,
|
|
7513
|
+
provenance: `plan:${sessionId || "chat"}:step${k}`,
|
|
7514
|
+
});
|
|
7515
|
+
}
|
|
7516
|
+
planHolder.state = { ...ps, cursor: k };
|
|
7517
|
+
const boardLine = rows.map((r) => `${r.subject} ${predicatePhrase(r.predicate)} ${r.object}`).join("; ");
|
|
7518
|
+
if (k < ps.actions.length) {
|
|
7519
|
+
return {
|
|
7520
|
+
text: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}`,
|
|
7521
|
+
deduced: ps.stepGoals[k] ? ps.stepGoals[k] : `continue the plan (step ${k + 1} of ${ps.actions.length})`,
|
|
7522
|
+
};
|
|
7523
|
+
}
|
|
7524
|
+
// Final step: confirm the goal against the store, from the written facts.
|
|
7525
|
+
const { compileDomain, stateFromFacts, compileGoal } = await import("./domain.mjs");
|
|
7526
|
+
const { readRuleRows } = await import("./memory/core.mjs");
|
|
7527
|
+
const payload = await loadMemory(memoryDir);
|
|
7528
|
+
const factRows = readFactRows(payload);
|
|
7529
|
+
const domain = compileDomain(factRows, readRuleRows(payload));
|
|
7530
|
+
const finalState = stateFromFacts(factRows, domain);
|
|
7531
|
+
const holds = compileGoal(ps.goals, domain)(finalState);
|
|
7532
|
+
planHolder.state = { ...planHolder.state, done: true };
|
|
7533
|
+
return {
|
|
7534
|
+
text: holds
|
|
7535
|
+
? `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}\n\ndone — ${ps.goalText} (checked against board@step${k}'s written facts, not assumed).`
|
|
7536
|
+
: `moved — ${action.label} (step ${k} of ${ps.actions.length}). board@step${k}: ${boardLine}\n\nBUT the goal does NOT hold against the written facts — the plan or the state drifted; re-teach the state and solve again.`,
|
|
7537
|
+
deduced: holds ? `goal reached — ${ps.goalText} (${k} of ${k} steps)` : "plan finished but the goal check failed",
|
|
7538
|
+
};
|
|
7539
|
+
}
|
|
7540
|
+
|
|
7541
|
+
async function runAsk(query, { config, source, graph, focus, last, templates, memoryDir, sessionId = "", lexicon = null, env, trace, vocabHint = null, tel = null, biasByBundle = {}, cache = null, vocabAntecedent = null, planHolder = null }) {
|
|
7027
7542
|
const ts = new Date().toISOString();
|
|
7028
7543
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
7029
7544
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
@@ -7252,6 +7767,23 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7252
7767
|
note(trace, `lane: (1) META/SELF — bare self/session question recognized, answered via="${meta.via}"`);
|
|
7253
7768
|
}
|
|
7254
7769
|
}
|
|
7770
|
+
// (1p) PLAN — the goal/solve/legal-moves recognizers over taught action
|
|
7771
|
+
// rules. Sits ABOVE the conversational catch-all: "solve it" is three
|
|
7772
|
+
// short words and isConversational() would otherwise claim it into the
|
|
7773
|
+
// orientation card before this lane ever ran.
|
|
7774
|
+
let planResult = null;
|
|
7775
|
+
if (!handled && miss && memoryDir && planHolder) {
|
|
7776
|
+
const planLane = await planLaneAnswer(query, { memoryDir, planHolder, sessionId });
|
|
7777
|
+
if (planLane) {
|
|
7778
|
+
answer = planLane.text; via = planLane.via; recordMiss = false; handled = true;
|
|
7779
|
+
if (planLane.plan) planResult = planLane.plan;
|
|
7780
|
+
if (planLane.deduced) {
|
|
7781
|
+
deduced = planLane.deduced;
|
|
7782
|
+
note(trace, `goal: ${deduced} (revised — the plan lane answered)`);
|
|
7783
|
+
}
|
|
7784
|
+
note(trace, `lane: (1p) PLAN — ${planLane.note}`);
|
|
7785
|
+
}
|
|
7786
|
+
}
|
|
7255
7787
|
// "what about X" with a genuine PRIOR turn to continue is exempt from the
|
|
7256
7788
|
// conversational catch-all even when short/non-codeish: isConversational()
|
|
7257
7789
|
// can't see that discourseRewrite/describeWrapperAnswer haven't had their
|
|
@@ -7785,7 +8317,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
7785
8317
|
// `goal`: the SAME deduced string the debug trace's own "goal:" line
|
|
7786
8318
|
// carries. Only runAsk ever sets this field, so the always-on goal line is
|
|
7787
8319
|
// scoped to real ask-engine turns by construction.
|
|
7788
|
-
return { answer, logLines, record, focus: newFocus, detail, effectiveQuery, goal: deduced };
|
|
8320
|
+
return { answer, logLines, record, focus: newFocus, detail, effectiveQuery, goal: deduced, ...(planResult ? { plan: planResult } : {}) };
|
|
7789
8321
|
}
|
|
7790
8322
|
|
|
7791
8323
|
/** A non-ask, non-dispatch chat turn (count answer, /stats) — the same
|
|
@@ -8207,7 +8739,7 @@ function vocabAntecedentFrom(last) {
|
|
|
8207
8739
|
return m ? m[1] : null;
|
|
8208
8740
|
}
|
|
8209
8741
|
|
|
8210
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null } = {}) {
|
|
8742
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, _noSplit = false } = {}) {
|
|
8211
8743
|
const line = String(input ?? "").trim();
|
|
8212
8744
|
// ONE fresh, empty cache for this turn only — every factRows() reader
|
|
8213
8745
|
// reached from this call shares it, so the first reader computes
|
|
@@ -8249,7 +8781,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8249
8781
|
// vocabHint: createSession computes this ONCE per session; a direct
|
|
8250
8782
|
// runTurn() caller that doesn't pass one gets it computed here instead.
|
|
8251
8783
|
const resolvedVocabHint = vocabHint ?? vocabExampleHint(await hasSeededVocabulary(memoryDir));
|
|
8252
|
-
|
|
8784
|
+
// The session's in-progress plan rides a mutable holder: the plan lane and
|
|
8785
|
+
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
8786
|
+
// it untouched, and the caller re-threads whatever comes back.
|
|
8787
|
+
const planHolder = { state: planState };
|
|
8788
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder };
|
|
8253
8789
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
8254
8790
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
8255
8791
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -8290,6 +8826,21 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8290
8826
|
const convo = vocabAntecedent ? null : conversationalTurn(workingLine, ctx);
|
|
8291
8827
|
if (convo) return withNarration(convo, trace, "casual/social — no graph intent");
|
|
8292
8828
|
|
|
8829
|
+
// PLAN NEXT — "next"/"continue" with an ACTIVE plan executes the plan's
|
|
8830
|
+
// next move as a snapshot write. Checked BEFORE the MORE_RE pager because
|
|
8831
|
+
// MORE_RE owns the same words; with no active plan this block never fires
|
|
8832
|
+
// and paging behaves exactly as before.
|
|
8833
|
+
if (memoryDir && PLAN_NEXT_RE.test(workingLine)
|
|
8834
|
+
&& planHolder.state && !planHolder.state.done
|
|
8835
|
+
&& Array.isArray(planHolder.state.actions) && planHolder.state.cursor < planHolder.state.actions.length) {
|
|
8836
|
+
const step = await executePlanStep(planHolder, { memoryDir, sessionId });
|
|
8837
|
+
note(trace, `goal: ${step.deduced}`);
|
|
8838
|
+
note(trace, "lane: PLAN NEXT — executed the active plan's next move as an @stepK snapshot write");
|
|
8839
|
+
const rec = withLast(plainTurn(workingLine, step.text, { via: "plan", focus }), step.deduced);
|
|
8840
|
+
rec.planState = planHolder.state;
|
|
8841
|
+
return rec;
|
|
8842
|
+
}
|
|
8843
|
+
|
|
8293
8844
|
// "more" — page the remainder of a previous long listing, if one is held. Gated on
|
|
8294
8845
|
// an actual pending remainder so a bare "more" with nothing to continue falls through
|
|
8295
8846
|
// to the ordinary path (an honest miss), never a pretend page.
|
|
@@ -8299,6 +8850,39 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8299
8850
|
return withLast(morePage(workingLine, ctx), "continue viewing a previous long listing");
|
|
8300
8851
|
}
|
|
8301
8852
|
|
|
8853
|
+
// Multi-sentence PLAN pre-split — one message carrying state sentences plus
|
|
8854
|
+
// a goal/trigger ("disk-1 rests on disk-2. … the goal is that …. solve it.")
|
|
8855
|
+
// runs each sentence as its own nested turn, threading focus/last/planState
|
|
8856
|
+
// through, and answers with the final turn's result behind brief receipts.
|
|
8857
|
+
if (!_noSplit && memoryDir) {
|
|
8858
|
+
const sentences = splitSentences(workingLine);
|
|
8859
|
+
if (sentences.length > 1) {
|
|
8860
|
+
const lastSentence = sentences[sentences.length - 1];
|
|
8861
|
+
if (PLAN_SOLVE_RE.test(lastSentence) || GOAL_TEACH_RE.test(lastSentence) || LEGAL_MOVES_RE.test(lastSentence)) {
|
|
8862
|
+
let f = focus; let l = last; let ps = planHolder.state;
|
|
8863
|
+
const receipts = [];
|
|
8864
|
+
let finalRec = null;
|
|
8865
|
+
for (const sentence of sentences) {
|
|
8866
|
+
const r = await runTurn(sentence, {
|
|
8867
|
+
config, source, graph, focus: f, last: l, memoryDir, sessionId, env, lexicon,
|
|
8868
|
+
narrate: false, vocabHint, tel, biasByBundle, planState: ps, _noSplit: true,
|
|
8869
|
+
});
|
|
8870
|
+
f = r.focus ?? f;
|
|
8871
|
+
l = r.last ?? l;
|
|
8872
|
+
if ("planState" in r) ps = r.planState;
|
|
8873
|
+
finalRec = r;
|
|
8874
|
+
receipts.push(String(r.answer ?? "").split("\n")[0]);
|
|
8875
|
+
}
|
|
8876
|
+
const receiptLines = receipts.slice(0, -1).map((t) => `• ${t}`).join("\n");
|
|
8877
|
+
const combined = { ...finalRec, answer: receiptLines ? `${receiptLines}\n\n${finalRec.answer}` : finalRec.answer };
|
|
8878
|
+
combined.planState = ps;
|
|
8879
|
+
combined.focus = f;
|
|
8880
|
+
combined.last = l;
|
|
8881
|
+
return combined;
|
|
8882
|
+
}
|
|
8883
|
+
}
|
|
8884
|
+
}
|
|
8885
|
+
|
|
8302
8886
|
if (workingLine.startsWith("/")) return withLast(await runCommand(workingLine, ctx), "use a specific tool/command directly");
|
|
8303
8887
|
// Declarative ACE sentences ("every module is a artifact") ASSERT into tmct's
|
|
8304
8888
|
// own memory and confirm — they are statements to remember, not graph queries.
|
|
@@ -8311,6 +8895,15 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8311
8895
|
note(trace, "lane: assertTurn — grammar/ace.mjs parseAce matched a full triple with no residue");
|
|
8312
8896
|
return withLast(asserted, "teach/remember a new fact");
|
|
8313
8897
|
}
|
|
8898
|
+
// Bare declarative taxonomy (hyphenated-instance membership, article-led
|
|
8899
|
+
// kind-of) — see bareTaxonomyTeach. Checked here because the ask engine
|
|
8900
|
+
// would otherwise parse these statements as inherits QUESTIONS.
|
|
8901
|
+
const taxonomy = await bareTaxonomyTeach(workingLine, ctx);
|
|
8902
|
+
if (taxonomy) {
|
|
8903
|
+
note(trace, "goal: teach/remember a new fact (bare declarative taxonomy)");
|
|
8904
|
+
note(trace, "lane: bareTaxonomyTeach — hyphenated-instance or article-led kind-of declarative, stored before the ask engine could parse it as a question");
|
|
8905
|
+
return withLast(plainTurn(workingLine, taxonomy.text, { via: taxonomy.via, miss: taxonomy.miss, focus }), "teach/remember a new fact");
|
|
8906
|
+
}
|
|
8314
8907
|
}
|
|
8315
8908
|
// MEMORY-STORE counts first ("how many facts / utterances do you know") — the
|
|
8316
8909
|
// memory graph owns Facts + Utterances, so these are answerable and consistent
|
|
@@ -8367,7 +8960,11 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
8367
8960
|
note(trace, "lane: answerCount — a header-count aggregate question, answered mechanically off the graph header, never dispatched to the ask engine");
|
|
8368
8961
|
return withLast(plainTurn(workingLine, count, { via: "count", focus }), "get a count of a graph kind");
|
|
8369
8962
|
}
|
|
8370
|
-
|
|
8963
|
+
{
|
|
8964
|
+
const rec = withLast(await runAsk(workingLine, ctx), "unclear — no goal signal computed by the ask engine");
|
|
8965
|
+
rec.planState = planHolder.state;
|
|
8966
|
+
return rec;
|
|
8967
|
+
}
|
|
8371
8968
|
}
|
|
8372
8969
|
|
|
8373
8970
|
// ---- W3: seedMemory → bootstrap (first run in a graph-less repo) ----
|
|
@@ -8680,6 +9277,7 @@ export async function createSession({
|
|
|
8680
9277
|
let turns = 0;
|
|
8681
9278
|
let focus = null; // the current focus entity ({id,label}) — threaded turn to turn
|
|
8682
9279
|
let last = null; // the last dispatched answer ({query,answer,detail}) — why/say-more re-renders it
|
|
9280
|
+
let planState = null; // the in-progress plan (goals/moves/cursor) — cleared by completion or a fresh goal, never by an aside
|
|
8683
9281
|
let closed = false;
|
|
8684
9282
|
|
|
8685
9283
|
return {
|
|
@@ -8689,6 +9287,7 @@ export async function createSession({
|
|
|
8689
9287
|
// prompt/expand-hint without reaching into runTurn's threading.
|
|
8690
9288
|
get focus() { return focus; },
|
|
8691
9289
|
get lastAnswer() { return last; },
|
|
9290
|
+
get planState() { return planState; },
|
|
8692
9291
|
get turns() { return turns; },
|
|
8693
9292
|
get narrate() { return narrateOn; },
|
|
8694
9293
|
promptFor: () => promptFor(focus),
|
|
@@ -8700,7 +9299,7 @@ export async function createSession({
|
|
|
8700
9299
|
async turn(line) {
|
|
8701
9300
|
let result;
|
|
8702
9301
|
try {
|
|
8703
|
-
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle });
|
|
9302
|
+
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, vocabHint, tel, biasByBundle, planState });
|
|
8704
9303
|
} catch (e) {
|
|
8705
9304
|
const ts = new Date().toISOString();
|
|
8706
9305
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -8714,6 +9313,7 @@ export async function createSession({
|
|
|
8714
9313
|
const { answer, logLines, record, focus: nextFocus, last: nextLast, end, narrate: nextNarrate } = result;
|
|
8715
9314
|
focus = nextFocus;
|
|
8716
9315
|
last = nextLast;
|
|
9316
|
+
if ("planState" in result) planState = result.planState;
|
|
8717
9317
|
// /narrate on|off (runCommand) rides the turn RESULT the same way a focus
|
|
8718
9318
|
// update does — apply it to this handle's session-scoped state.
|
|
8719
9319
|
if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
|
|
@@ -8729,7 +9329,7 @@ export async function createSession({
|
|
|
8729
9329
|
});
|
|
8730
9330
|
await upsertGraph(record.ts);
|
|
8731
9331
|
turns += 1;
|
|
8732
|
-
return { answer, end: Boolean(end), prompt: promptFor(focus) };
|
|
9332
|
+
return { answer, end: Boolean(end), prompt: promptFor(focus), plan: result.plan ?? null };
|
|
8733
9333
|
},
|
|
8734
9334
|
|
|
8735
9335
|
/** End-of-session close: end lines in both artifacts, the final graph upsert
|