@polycode-projects/the-mechanical-code-talker 2.10.5 → 2.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 +2 -2
- package/corpus/sprites/src/sprite-facts.jsonl +18 -0
- package/corpus/worlds/manifest.json +5 -5
- package/corpus/worlds/shards/ashcombe-hall.jsonl.gz +0 -0
- package/corpus/worlds/src/ashcombe-hall.jsonl +27 -0
- package/data/sprites/book-icon.toml +12 -0
- package/data/sprites/cellar-icon.toml +12 -0
- package/data/sprites/drawing-room-icon.toml +13 -0
- package/data/sprites/garden-icon.toml +12 -0
- package/data/sprites/kitchen-icon.toml +13 -0
- package/data/sprites/library-icon.toml +12 -0
- package/data/sprites/pan-icon.toml +11 -0
- package/data/sprites/study-icon.toml +12 -0
- package/package.json +5 -2
- package/src/adapters/corpus/wikipedia-live.mjs +182 -26
- package/src/adapters/corpus/worlds-pack.mjs +8 -2
- package/src/adapters/toml-config.mjs +6 -0
- package/src/domain/memory/trust.mjs +11 -0
- package/src/domain/worlds-pack.mjs +50 -0
- package/src/services/adventure-autoplay.mjs +5 -2
- package/src/services/adventure-viz.mjs +301 -33
- package/src/services/adventure.mjs +162 -14
- package/src/services/chat-page-viz.mjs +265 -189
- package/src/services/chat-session.mjs +15 -5
- package/src/services/chat.mjs +286 -43
- package/src/services/code-explorer-viz.mjs +183 -75
- package/src/services/extract-facts.mjs +118 -28
- package/src/services/ingest-viz.mjs +328 -79
- package/src/services/ledger-viz.mjs +99 -0
- package/src/services/memory-panel-viz.mjs +159 -0
- package/src/services/research.mjs +266 -0
- package/src/services/sentences.mjs +19 -0
- package/src/services/spider-fly-viz.mjs +21 -5
- package/src/surfaces/web/adventure-browser-entry.mjs +9 -5
- package/src/surfaces/web/chat-browser-entry.mjs +28 -11
- package/src/surfaces/web/code-explorer-browser-entry.mjs +27 -11
- package/src/surfaces/web/ingest-browser-entry.mjs +123 -41
- package/src/surfaces/web/ledger-browser-entry.mjs +10 -4
- package/src/surfaces/web/memory-ask-browser.bundle.js +112 -112
- package/src/surfaces/web/memory-stats.mjs +53 -0
|
@@ -28,6 +28,7 @@ import * as defaultSource from "../adapters/source.mjs";
|
|
|
28
28
|
import { resolveExtensions, mergedLexiconExtra } from "./extensions.mjs";
|
|
29
29
|
import { runTurn, hasSeededVocabulary, vocabExampleHint } from "./chat.mjs";
|
|
30
30
|
import { resolveGameConfig } from "../domain/game-config.mjs";
|
|
31
|
+
import { resolveResearchConfig } from "./research.mjs";
|
|
31
32
|
import { sessionLogHeaderMarkdown, sessionLogTurnMarkdown, sessionLogEndMarkdown } from "./session-log-format.mjs";
|
|
32
33
|
|
|
33
34
|
/** Where session logs live, relative to the target repo. `.tmct/` is the repo's
|
|
@@ -199,6 +200,11 @@ export async function createSession({
|
|
|
199
200
|
// to the shipped defaults for every key.
|
|
200
201
|
const gameConfig = resolveGameConfig(toml);
|
|
201
202
|
|
|
203
|
+
// The research lane's knobs (fan-out cap, depth, the polite interval) —
|
|
204
|
+
// resolved once per session from the same tmct.toml, defaults filling
|
|
205
|
+
// every unset key exactly as resolveGameConfig does above.
|
|
206
|
+
const researchConfig = resolveResearchConfig(toml);
|
|
207
|
+
|
|
202
208
|
// tmct.toml's corpus tier3 opts the session into the live supplement too —
|
|
203
209
|
// the flag/env tiers above stay authoritative when set.
|
|
204
210
|
if (toml?.corpus?.tier === "tier3") liveReferenceOn = true;
|
|
@@ -337,6 +343,7 @@ export async function createSession({
|
|
|
337
343
|
let focus = null; // the current focus entity ({id,label}) — threaded turn to turn
|
|
338
344
|
let last = null; // the last dispatched answer ({query,answer,detail}) — why/say-more re-renders it
|
|
339
345
|
let planState = null; // the in-progress plan (goals/moves/cursor) — cleared by completion or a fresh goal, never by an aside
|
|
346
|
+
let researchState = null; // the in-progress research queue — advanced by "research next", cleared by completion or "research stop"
|
|
340
347
|
let closed = false;
|
|
341
348
|
|
|
342
349
|
return {
|
|
@@ -347,6 +354,7 @@ export async function createSession({
|
|
|
347
354
|
get focus() { return focus; },
|
|
348
355
|
get lastAnswer() { return last; },
|
|
349
356
|
get planState() { return planState; },
|
|
357
|
+
get researchState() { return researchState; },
|
|
350
358
|
get turns() { return turns; },
|
|
351
359
|
get narrate() { return narrateOn; },
|
|
352
360
|
get liveReference() { return liveReferenceOn; },
|
|
@@ -360,7 +368,7 @@ export async function createSession({
|
|
|
360
368
|
async turn(line) {
|
|
361
369
|
let result;
|
|
362
370
|
try {
|
|
363
|
-
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, liveReference: liveReferenceOn, vocabHint, tel, biasByBundle, planState, gameConfig });
|
|
371
|
+
result = await runTurn(line, { config, source, graph, focus, last, memoryDir, sessionId, env, lexicon, narrate: narrateOn, liveReference: liveReferenceOn, vocabHint, tel, biasByBundle, planState, gameConfig, researchState, researchConfig });
|
|
364
372
|
} catch (e) {
|
|
365
373
|
const ts = new Date().toISOString();
|
|
366
374
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -375,13 +383,15 @@ export async function createSession({
|
|
|
375
383
|
focus = nextFocus;
|
|
376
384
|
last = nextLast;
|
|
377
385
|
if ("planState" in result) planState = result.planState;
|
|
386
|
+
if ("researchState" in result) researchState = result.researchState;
|
|
378
387
|
// /narrate on|off and /wiki on|off (runCommand) ride the turn RESULT the
|
|
379
388
|
// same way a focus update does — apply them to this handle's
|
|
380
389
|
// session-scoped state.
|
|
381
390
|
if (typeof nextNarrate === "boolean") narrateOn = nextNarrate;
|
|
382
|
-
//
|
|
383
|
-
// append a cited read-out under every grounded answer)
|
|
384
|
-
|
|
391
|
+
// four-state: false (off), true (rescue on a miss), "supplement" (also
|
|
392
|
+
// append a cited read-out under every grounded vocabulary answer), or
|
|
393
|
+
// "always" (widen that to every grounded answer).
|
|
394
|
+
if (typeof nextLiveReference === "boolean" || nextLiveReference === "supplement" || nextLiveReference === "always") liveReferenceOn = nextLiveReference;
|
|
385
395
|
await writeLog(sessionLogTurnMarkdown({ startedAt: record.ts, turnNumber: turns + 1, query: line, answer }));
|
|
386
396
|
await writeSidecar(record);
|
|
387
397
|
turnRecords.push(record);
|
|
@@ -394,7 +404,7 @@ export async function createSession({
|
|
|
394
404
|
});
|
|
395
405
|
await upsertGraph(record.ts);
|
|
396
406
|
turns += 1;
|
|
397
|
-
return { answer, end: Boolean(end), prompt: promptFor(focus), plan: result.plan ?? null, record };
|
|
407
|
+
return { answer, end: Boolean(end), prompt: promptFor(focus), plan: result.plan ?? null, research: result.research, record };
|
|
398
408
|
},
|
|
399
409
|
|
|
400
410
|
/** End-of-session close: end lines in both artifacts, the final graph upsert
|
package/src/services/chat.mjs
CHANGED
|
@@ -51,13 +51,14 @@ import {
|
|
|
51
51
|
LIVE_PACK_NAME, cleanMissLiveTerm, renderLiveReferenceAnswer, liveProvenanceTag,
|
|
52
52
|
} from "../domain/reference-pack.mjs";
|
|
53
53
|
import { getReferencePackProvider } from "../adapters/corpus/reference-pack.mjs";
|
|
54
|
-
import { getLiveReferenceProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
54
|
+
import { getLiveReferenceProvider, getResearchProvider } from "../adapters/corpus/wikipedia-live.mjs";
|
|
55
|
+
import { researchTurn, researchSnapshot, resolveResearchConfig, RESEARCH_DEFAULTS } from "./research.mjs";
|
|
55
56
|
import { CHILD_PACK_NAME, childProvenanceTag } from "../domain/child-pack.mjs";
|
|
56
57
|
import { getChildPackProvider } from "../adapters/corpus/child-pack.mjs";
|
|
57
58
|
import { dialogueActForLane } from "../domain/dialogue-acts.mjs";
|
|
58
59
|
import { subClassParents, ancestryChain, clusterSenses } from "../domain/sense-split.mjs";
|
|
59
60
|
import { relatedForTerm } from "../domain/skos-view.mjs";
|
|
60
|
-
import { adventureTurn, unclaimedAdventureOpening } from "./adventure.mjs";
|
|
61
|
+
import { adventureTurn, unclaimedAdventureOpening, foldWorldState } from "./adventure.mjs";
|
|
61
62
|
import { spiderFlyTurn } from "./spider-fly-turn.mjs";
|
|
62
63
|
import { DEFAULT_GAME_CONFIG } from "../domain/game-config.mjs";
|
|
63
64
|
|
|
@@ -3138,7 +3139,8 @@ async function unknownAdjectiveFallback(payload, { memoryDir, sessionId, lexicon
|
|
|
3138
3139
|
// sentence said ("tony never eats ribs" -> tony eats ribs) — a truthful teach
|
|
3139
3140
|
// read back as a confident lie. It belongs to NEG_MARKER_SRC below.
|
|
3140
3141
|
const TEACH_ADVERB_SKIP_SRC = "(?:(?:usually|often|sometimes|rarely|always|typically|generally|"
|
|
3141
|
-
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually
|
|
3142
|
+
+ "occasionally|frequently|normally|regularly|commonly|mostly|currently|still|also|really|actually|"
|
|
3143
|
+
+ "closely|strongly|directly)\\s+)?";
|
|
3142
3144
|
/** The negation markers a teach/query frame recognizes, in ONE place so the
|
|
3143
3145
|
* teach side and the query side can never disagree about what negates a
|
|
3144
3146
|
* sentence — the same discipline TEACH_ADVERB_SKIP_SRC is shared under. */
|
|
@@ -3163,6 +3165,96 @@ function splitTeachNegation(payload) {
|
|
|
3163
3165
|
return { payload: `${m[1]} ${canFamily ? "can " : ""}${m[3]}`.trim(), negated: true };
|
|
3164
3166
|
}
|
|
3165
3167
|
const GENERAL_VERB_TEACH_RE = new RegExp(`^([\\w'-]+)\\s+${TEACH_ADVERB_SKIP_SRC}([a-z]+)\\s+(.+?)[.!?]*$`, "i");
|
|
3168
|
+
/** The closed participle set the relational teach frames read as "X is
|
|
3169
|
+
* <participle> <prep> Y" — a past participle whose own form is the word, so it
|
|
3170
|
+
* reads back with no morphology. Closed by list (templates over general
|
|
3171
|
+
* grammar), so the frame can never widen onto an arbitrary "-ed" adjective. */
|
|
3172
|
+
const TEACH_PARTICIPLE_SRC = "connected|related|associated|linked|based|derived|composed|made|used|known|located|found|involved|concerned";
|
|
3173
|
+
/** The prepositions those participles take. A closed set, folded into the
|
|
3174
|
+
* minted predicate (mgx:<participle>-<prep>) the same way PREP_SRC folds into
|
|
3175
|
+
* the general-verb frame's. */
|
|
3176
|
+
const TEACH_PARTICIPLE_PREP_SRC = "with|to|from|by|of|in|on|for|as|about|into";
|
|
3177
|
+
/** "X is <participle> <prep> Y" — "sales are closely connected with marketing"
|
|
3178
|
+
* → sales mgx:connected-with marketing. The subject is one or two tokens (the
|
|
3179
|
+
* same bound the comparative/unknown-subject frames use), an optional adverb
|
|
3180
|
+
* is skipped, and the object is captured for a determiner-strip + 3-token cap
|
|
3181
|
+
* by its handler. */
|
|
3182
|
+
const PARTICIPLE_PREP_TEACH_RE = new RegExp(
|
|
3183
|
+
`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(?:is|are|was|were)\\s+${TEACH_ADVERB_SKIP_SRC}(${TEACH_PARTICIPLE_SRC})\\s+(${TEACH_PARTICIPLE_PREP_SRC})\\s+(.+)$`,
|
|
3184
|
+
"i",
|
|
3185
|
+
);
|
|
3186
|
+
/** "X is a <noun> <participle> <prep> Y" — a copula-NP with a trailing
|
|
3187
|
+
* participle clause: "sales are activities related to selling" decomposes into
|
|
3188
|
+
* the class-membership half (sales ⊑ activity, through the ordinary mint/assert
|
|
3189
|
+
* path) AND the relational half (sales mgx:related-to selling). The NP head is
|
|
3190
|
+
* a single token, followed by a closed participle — which keeps this disjoint
|
|
3191
|
+
* from PARTICIPLE_PREP_TEACH_RE, where the participle sits right after the
|
|
3192
|
+
* copula. */
|
|
3193
|
+
const COPULA_NP_PARTICIPLE_TEACH_RE = new RegExp(
|
|
3194
|
+
`^(?:the\\s+|an?\\s+)?([\\w'-]+(?:\\s+[\\w'-]+)?)\\s+(is|are|was|were)\\s+(?:an?\\s+)?([\\w'-]+)\\s+(${TEACH_PARTICIPLE_SRC})\\s+(${TEACH_PARTICIPLE_PREP_SRC})\\s+(.+)$`,
|
|
3195
|
+
"i",
|
|
3196
|
+
);
|
|
3197
|
+
/** "A and B have/share the same <noun>" — "sales and marketing have the same
|
|
3198
|
+
* goal" → sales mgx:same-goal-as marketing. A closed shape; the conjunction
|
|
3199
|
+
* pre-pass leaves it alone (its second clause never opens with is/are/has/
|
|
3200
|
+
* have/can), so it reaches the teach dispatch whole. */
|
|
3201
|
+
const SAME_NOUN_TEACH_RE = /^(?:the\s+)?([\w'-]+)\s+and\s+(?:the\s+)?([\w'-]+)\s+(?:have|has|share|shares)\s+(?:the\s+)?same\s+([\w'-]+)[.!?]*$/i;
|
|
3202
|
+
/** "the letter is in the garden" — a locative teach whose subject the running
|
|
3203
|
+
* adventure world already places somewhere. Group 1 is the subject; the world
|
|
3204
|
+
* place is left to the fold, since the sentence is stored as a note either
|
|
3205
|
+
* way. */
|
|
3206
|
+
const LOCATIVE_TEACH_RE = /^(?:the\s+)?([\w'-]+)\s+(?:is|are|was|were)\s+(?:in|on|under|inside|at|near|behind|above|below)\s+(?:the\s+)?[\w'-]+/i;
|
|
3207
|
+
/** Fold a relational object down to its head phrase: cut at the first clause
|
|
3208
|
+
* boundary (a comma, semicolon, or a coordinating "or"/"and"), strip a leading
|
|
3209
|
+
* determiner, then cap at 3 tokens — "selling or the number of goods sold in a
|
|
3210
|
+
* period" folds to "selling", "the number of goods" to "number of goods".
|
|
3211
|
+
* Keeps a minted relational object bounded, the same discipline the general-
|
|
3212
|
+
* verb frame's own object fold uses. */
|
|
3213
|
+
function participleObject(raw) {
|
|
3214
|
+
const cleaned = String(raw).trim()
|
|
3215
|
+
.replace(/[.!?]+$/, "")
|
|
3216
|
+
.split(/\s*[,;]\s*|\s+(?:or|and)\s+/i)[0]
|
|
3217
|
+
.trim()
|
|
3218
|
+
.replace(/^(?:the|an?|its|his|her|their|our|my|your|some|any)\s+/i, "");
|
|
3219
|
+
return cleaned.split(/\s+/).slice(0, 3).join(" ");
|
|
3220
|
+
}
|
|
3221
|
+
/** Does a bare (unwrapped) sentence fit one of the relational teach frames —
|
|
3222
|
+
* including its negated twin, read through splitTeachNegation the same way the
|
|
3223
|
+
* general-verb and capability frames read theirs? Used only to admit the
|
|
3224
|
+
* sentence as a teach payload; the dispatch below re-matches and stores. */
|
|
3225
|
+
function matchesRelationalTeachFrame(sentence) {
|
|
3226
|
+
const { payload } = splitTeachNegation(String(sentence || "").trim());
|
|
3227
|
+
return PARTICIPLE_PREP_TEACH_RE.test(payload)
|
|
3228
|
+
|| COPULA_NP_PARTICIPLE_TEACH_RE.test(payload)
|
|
3229
|
+
|| SAME_NOUN_TEACH_RE.test(payload);
|
|
3230
|
+
}
|
|
3231
|
+
/** Does a relational-frame sentence name a code-graph entity? "the Router is
|
|
3232
|
+
* used by every handler" reads as a passive uses-CLAIM the ask engine verifies
|
|
3233
|
+
* against the graph ("No — no uses edge found…"), not a fact to store — the
|
|
3234
|
+
* participle+preposition frame would otherwise intercept it. When any term the
|
|
3235
|
+
* frame would store resolves to a graph entity, the frame yields so the graph
|
|
3236
|
+
* lane keeps the sentence. No graph (a bare/browser/ingest turn) means nothing
|
|
3237
|
+
* to yield to, so the frame proceeds. */
|
|
3238
|
+
async function relationalFrameNamesGraphEntity(sentence, graph) {
|
|
3239
|
+
if (!graph) return false;
|
|
3240
|
+
const { payload } = splitTeachNegation(String(sentence || "").trim());
|
|
3241
|
+
let terms = null;
|
|
3242
|
+
const pp = payload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
3243
|
+
if (pp) terms = [pp[1], participleObject(pp[4])];
|
|
3244
|
+
else {
|
|
3245
|
+
const np = payload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
3246
|
+
if (np) terms = [np[1], np[3], participleObject(np[6])];
|
|
3247
|
+
else {
|
|
3248
|
+
const same = payload.match(SAME_NOUN_TEACH_RE);
|
|
3249
|
+
if (same) terms = [same[1], same[2]];
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
if (!terms) return false;
|
|
3253
|
+
for (const t of terms) {
|
|
3254
|
+
try { if (await resolveEntity(graph, String(t).trim())) return true; } catch { /* unresolved is fine */ }
|
|
3255
|
+
}
|
|
3256
|
+
return false;
|
|
3257
|
+
}
|
|
3166
3258
|
/** Determiners/quantifiers that make the FIRST token an article, not a real
|
|
3167
3259
|
* bare-name subject ("every controller…", "the cache…") — GENERAL_VERB_TEACH_RE
|
|
3168
3260
|
* would otherwise happily bind them as a 1-token subject and misread the
|
|
@@ -3993,7 +4085,7 @@ async function teachExclusionReason(sentence) {
|
|
|
3993
4085
|
}
|
|
3994
4086
|
export { teachExclusionReason };
|
|
3995
4087
|
|
|
3996
|
-
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null }) {
|
|
4088
|
+
async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cache = null, planHolder = null, graph = null }) {
|
|
3997
4089
|
// A closed discourse-marker preamble ahead of a teach sentence ("howdy
|
|
3998
4090
|
// pardner, remember that TaskController is fragile") would otherwise
|
|
3999
4091
|
// corrupt TEACH_RE's own match, so strip it first. applyPreambleFrames is
|
|
@@ -4869,7 +4961,10 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4869
4961
|
|
|
4870
4962
|
let payload = null;
|
|
4871
4963
|
if (wrapped && /\b(?:is|are)\b/i.test(wrapped)) payload = wrapped;
|
|
4872
|
-
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
|
|
4964
|
+
else if ((BARE_DECLARATIVE_RE.test(raw) || COMPARATIVE_TEACH_RE.test(raw)
|
|
4965
|
+
|| (matchesRelationalTeachFrame(raw) && !(await relationalFrameNamesGraphEntity(raw, graph)))
|
|
4966
|
+
|| matchBareHabitualTeach(raw) || matchBareCanTeach(raw))
|
|
4967
|
+
&& !QUESTION_LEAD_RE.test(raw) && !(await hasMidSentenceInterrogative(raw))) payload = raw;
|
|
4873
4968
|
if (!payload) {
|
|
4874
4969
|
// "remember margo eats ribs", re-escaping here through a combination
|
|
4875
4970
|
// that mechanism's own deliberate subject-shape restriction doesn't
|
|
@@ -4901,6 +4996,58 @@ async function teachLane(query, { memoryDir, sessionId = "", lexicon = null, cac
|
|
|
4901
4996
|
});
|
|
4902
4997
|
if (stored) return stored;
|
|
4903
4998
|
}
|
|
4999
|
+
// RELATIONAL teach frames — a participle+preposition claim
|
|
5000
|
+
// ("sales are closely connected with marketing"), a copula-NP with a
|
|
5001
|
+
// trailing participle ("sales are activities related to selling"), or a
|
|
5002
|
+
// shared-attribute claim ("sales and marketing have the same goal"). Each
|
|
5003
|
+
// reads through splitTeachNegation like its comparative/general-verb
|
|
5004
|
+
// siblings, so a negated form stores the mgxneg: twin.
|
|
5005
|
+
{
|
|
5006
|
+
const { payload: posPayload, negated } = splitTeachNegation(String(payload).trim());
|
|
5007
|
+
// (D) participle + preposition, checked ahead of the copula-NP form so a
|
|
5008
|
+
// bare participle right after the copula is never misread as a noun.
|
|
5009
|
+
const pp = posPayload.match(PARTICIPLE_PREP_TEACH_RE);
|
|
5010
|
+
if (pp) {
|
|
5011
|
+
const pred = `mgx:${pp[2].toLowerCase()}-${pp[3].toLowerCase()}`;
|
|
5012
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
5013
|
+
subject: pp[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5014
|
+
object: participleObject(pp[4]),
|
|
5015
|
+
});
|
|
5016
|
+
if (stored) return stored;
|
|
5017
|
+
}
|
|
5018
|
+
// (E) copula-NP + trailing participle — decomposed into the
|
|
5019
|
+
// class-membership half (subject ⊑ singular(NP head)) and the relational
|
|
5020
|
+
// half. The copula-NP shape is a deliberate declarative, strong enough to
|
|
5021
|
+
// mint the membership directly, so it lands even when neither term is in
|
|
5022
|
+
// the lexicon (the acceptance the ingest pipeline needs). Either half may
|
|
5023
|
+
// stand on its own if the other's write fails.
|
|
5024
|
+
const np = posPayload.match(COPULA_NP_PARTICIPLE_TEACH_RE);
|
|
5025
|
+
if (np) {
|
|
5026
|
+
const subject = np[1].trim();
|
|
5027
|
+
const relPred = `mgx:${np[4].toLowerCase()}-${np[5].toLowerCase()}`;
|
|
5028
|
+
const isaStored = await teachFact(memoryDir, sessionId, {
|
|
5029
|
+
subject, predicate: SUBCLASS_PREDICATE, object: singularizeSurface(np[3]),
|
|
5030
|
+
});
|
|
5031
|
+
const relStored = await teachFact(memoryDir, sessionId, {
|
|
5032
|
+
subject, predicate: negated ? negatedPredicate(relPred) : relPred,
|
|
5033
|
+
object: participleObject(np[6]),
|
|
5034
|
+
});
|
|
5035
|
+
const stripNoted = (t) => String(t).replace(/^noted — remembered(?:\s+\d+\s+facts?)?:\s*/i, "").trim();
|
|
5036
|
+
if (isaStored && relStored) return { text: `noted — remembered both: ${stripNoted(isaStored.text)}; and ${stripNoted(relStored.text)}`, via: "assert", miss: false };
|
|
5037
|
+
if (relStored) return relStored;
|
|
5038
|
+
if (isaStored) return isaStored;
|
|
5039
|
+
}
|
|
5040
|
+
// (F) shared attribute — "A and B have the same <noun>".
|
|
5041
|
+
const same = posPayload.match(SAME_NOUN_TEACH_RE);
|
|
5042
|
+
if (same) {
|
|
5043
|
+
const pred = `mgx:same-${same[3].toLowerCase()}-as`;
|
|
5044
|
+
const stored = await teachFact(memoryDir, sessionId, {
|
|
5045
|
+
subject: same[1].trim(), predicate: negated ? negatedPredicate(pred) : pred,
|
|
5046
|
+
object: same[2].trim(),
|
|
5047
|
+
});
|
|
5048
|
+
if (stored) return stored;
|
|
5049
|
+
}
|
|
5050
|
+
}
|
|
4904
5051
|
for (const cand of assertCandidates(payload)) {
|
|
4905
5052
|
// assertTurn ITSELF records the "every" quantifier (point 3) on a plain
|
|
4906
5053
|
// universal success, so every caller (this loop AND the top-level
|
|
@@ -5624,7 +5771,8 @@ export async function helpText() {
|
|
|
5624
5771
|
["/export <path>", "write the memory store to a file, as JSONL (the same shape `tmct memory --export` writes)"],
|
|
5625
5772
|
["/ingest <path>", "read a local text file and store every fact the recognizer grounds from it (same recognizer as `tmct extract`)"],
|
|
5626
5773
|
["/narrate on|off", "verbose developer/debug mode: decision points, matched pattern, results+sources, goal per turn"],
|
|
5627
|
-
["/wiki on|off|supplement", "live Wikipedia (default off): on tries en.wikipedia.org when I can't answer (network), cited; supplement also adds a read-out under every grounded answer"],
|
|
5774
|
+
["/wiki on|off|supplement|always", "live Wikipedia (default off): on tries en.wikipedia.org when I can't answer (network), cited; supplement also adds a read-out under every grounded vocabulary answer; always widens that to every grounded answer"],
|
|
5775
|
+
["research <topic> [limit N]", "fetch the topic from Simple English Wikipedia (the explicit ask is the network consent), store what it grounds, and queue its linked topics — \"research next\" steps the queue; also status/stop"],
|
|
5628
5776
|
["/help", "this list"],
|
|
5629
5777
|
["/exit", "leave the session (also Ctrl+C / Ctrl+D)"],
|
|
5630
5778
|
];
|
|
@@ -5904,6 +6052,14 @@ function predicatePhrase(predicate) {
|
|
|
5904
6052
|
// "is smaller than" (never a 3sg fold — "smallers" isn't a word)
|
|
5905
6053
|
const comp = /^mgx:([a-z]+(?:-[a-z]+)*)-than$/i.exec(p);
|
|
5906
6054
|
if (comp) return `is ${comp[1].replace(/-/g, " ")} than`;
|
|
6055
|
+
// a participle + preposition renders as its copula surface: mgx:connected-with
|
|
6056
|
+
// -> "is connected with" (the participle is already a participle, so no 3sg
|
|
6057
|
+
// fold — "connecteds" isn't a word)
|
|
6058
|
+
const part = new RegExp(`^mgx:(${TEACH_PARTICIPLE_SRC})-([a-z]+)$`, "i").exec(p);
|
|
6059
|
+
if (part) return `is ${part[1].toLowerCase()} ${part[2].toLowerCase()}`;
|
|
6060
|
+
// a shared-attribute predicate: mgx:same-goal-as -> "has the same goal as"
|
|
6061
|
+
const same = /^mgx:same-([a-z]+)-as$/i.exec(p);
|
|
6062
|
+
if (same) return `has the same ${same[1].toLowerCase()} as`;
|
|
5907
6063
|
const m = /^mgx:([a-z]+)(?:-([a-z]+))?$/i.exec(p);
|
|
5908
6064
|
if (!m) return predicate;
|
|
5909
6065
|
// a folded preposition renders back naturally: mgx:rest-on -> "rests on"
|
|
@@ -10012,7 +10168,7 @@ async function liveReferenceAnswerForKey(key, onLiveLookup) {
|
|
|
10012
10168
|
* key up in the shipped child triples pack and append every fact under child
|
|
10013
10169
|
* provenance, so the SAME question can be re-asked from the store. Null on a
|
|
10014
10170
|
* pack miss or any failure — the turn then proceeds byte-identically. */
|
|
10015
|
-
async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
10171
|
+
async function childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
10016
10172
|
let row = null;
|
|
10017
10173
|
try { row = await getChildPackProvider(env).lookup(key); } catch { row = null; }
|
|
10018
10174
|
if (!row?.facts?.length) return null;
|
|
@@ -10023,7 +10179,7 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
10023
10179
|
})));
|
|
10024
10180
|
} catch { return null; }
|
|
10025
10181
|
if (cache) cache.rows = null;
|
|
10026
|
-
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
10182
|
+
await synthesiseAroundTerm(memoryDir, key, cache, synthesisBudget);
|
|
10027
10183
|
return { key, count: row.facts.length };
|
|
10028
10184
|
}
|
|
10029
10185
|
|
|
@@ -10034,7 +10190,7 @@ async function childPackFactsForKey(key, { memoryDir, env, cache }) {
|
|
|
10034
10190
|
* and is failure-tolerated: the answer stands whether or not the facts land.
|
|
10035
10191
|
* The optimistic tier is pure (no recognizer re-entry), so this stays cheap on
|
|
10036
10192
|
* the chat turn. Returns the count stored. */
|
|
10037
|
-
async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = referenceProvenanceTag, lexicon = null) {
|
|
10193
|
+
async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = referenceProvenanceTag, lexicon = null, synthesisBudget = AUTO_SYNTHESIS_BUDGET) {
|
|
10038
10194
|
if (!article) return 0;
|
|
10039
10195
|
const provenance = tagFor(article);
|
|
10040
10196
|
const facts = [];
|
|
@@ -10056,7 +10212,7 @@ async function ingestReferenceArticle(memoryDir, key, article, cache, tagFor = r
|
|
|
10056
10212
|
await appendFacts(memoryDir, facts);
|
|
10057
10213
|
if (cache) cache.rows = null;
|
|
10058
10214
|
} catch { return 0; }
|
|
10059
|
-
await synthesiseAroundTerm(memoryDir, key, cache);
|
|
10215
|
+
await synthesiseAroundTerm(memoryDir, key, cache, synthesisBudget);
|
|
10060
10216
|
return facts.length;
|
|
10061
10217
|
}
|
|
10062
10218
|
|
|
@@ -10072,15 +10228,15 @@ const AUTO_SYNTHESIS_BUDGET = 12;
|
|
|
10072
10228
|
* facts carry entailed:* provenance at their discounted trust and are
|
|
10073
10229
|
* retractable. Failure-tolerated: a synthesis miss never disturbs the answer
|
|
10074
10230
|
* the load already composed. Returns the count derived. */
|
|
10075
|
-
async function synthesiseAroundTerm(memoryDir, term, cache) {
|
|
10076
|
-
if (!memoryDir || !term) return 0;
|
|
10231
|
+
async function synthesiseAroundTerm(memoryDir, term, cache, budget = AUTO_SYNTHESIS_BUDGET) {
|
|
10232
|
+
if (!memoryDir || !term || budget <= 0) return 0;
|
|
10077
10233
|
try {
|
|
10078
10234
|
const { syllogise } = await import("../domain/syllogise.mjs");
|
|
10079
10235
|
const { loadMemory, readFactRows, appendFacts, normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
10080
10236
|
const res = await syllogise(memoryDir, {
|
|
10081
10237
|
focus: [...factTermVariants(normFactTerm, term)],
|
|
10082
10238
|
expandFocus: true,
|
|
10083
|
-
budget
|
|
10239
|
+
budget,
|
|
10084
10240
|
store: { loadMemory, readFactRows, appendFacts },
|
|
10085
10241
|
});
|
|
10086
10242
|
if (res?.count && cache) cache.rows = null;
|
|
@@ -11280,8 +11436,12 @@ const DECISION_RECALL_RE = /^(?:remind\s+me\s+)?what\s+(?:did\s+)?(?:we|i|you)\s
|
|
|
11280
11436
|
* than silently accepted alongside the current location. */
|
|
11281
11437
|
const MOVE_HISTORY_RE = /^where\s+did\s+(.+?)\s+(?:move|get\s+moved|go)(?:\s+to)?[?.!\s]*$/i;
|
|
11282
11438
|
|
|
11283
|
-
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, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null }) {
|
|
11439
|
+
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, gameConfig = DEFAULT_GAME_CONFIG, liveReference = false, onLiveLookup = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET }) {
|
|
11284
11440
|
const ts = new Date().toISOString();
|
|
11441
|
+
// The surface this turn runs on ("cli" default; "browser" from a web entry) —
|
|
11442
|
+
// the honest-miss tail below points a browser/adventure miss at the teach
|
|
11443
|
+
// lane instead of the CLI-only --repo/tmct-init remedy.
|
|
11444
|
+
const browser = uiContext === "browser";
|
|
11285
11445
|
// DISCOURSE ANAPHORA: a follow-up like "which of those are tested" / "count
|
|
11286
11446
|
// them" filters or counts the PREVIOUS answer's entity set, threaded as
|
|
11287
11447
|
// ask()'s `prev`. Prefers the FULL id set (`allIds`) over `matches`, since a
|
|
@@ -11340,7 +11500,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11340
11500
|
try { liveKey = cleanMissLiveTerm(wikiTerm, lexicon ?? undefined); } catch { liveKey = null; }
|
|
11341
11501
|
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
11342
11502
|
if (live) {
|
|
11343
|
-
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
11503
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
11344
11504
|
note(trace, `lane: WIKIPEDIA ASK — answered from a live en.wikipedia.org lookup, cited (article "${live.article.title}", revid ${live.article.revid})`);
|
|
11345
11505
|
return plainTurn(query, live.text, { via: "reference", miss: false, focus });
|
|
11346
11506
|
}
|
|
@@ -11503,8 +11663,12 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11503
11663
|
// examples are the wrong audience here), an unseeded one at the seed/
|
|
11504
11664
|
// teach pair — vocabHint already carries exactly that split.
|
|
11505
11665
|
answer = (!graph || noCodeGraph(graph)) && (!config || e?.emptyGraph || /^cannot read graph artifact\b/.test(thrown))
|
|
11666
|
+
// A browser session has no `tmct init in a repo` to reach for, so its
|
|
11667
|
+
// fallback drops that CLI-only remedy and keeps just the teach pointer.
|
|
11506
11668
|
? `I can't answer that as a code question — no code graph is loaded in this session. ${vocabHint
|
|
11507
|
-
||
|
|
11669
|
+
|| (browser
|
|
11670
|
+
? "I can still remember and answer taught facts (try \"every bug is an issue\")."
|
|
11671
|
+
: "I can still remember and answer taught facts (try \"every bug is an issue\"), or run `tmct init` in a repo to index one.")}`
|
|
11508
11672
|
: thrown;
|
|
11509
11673
|
note(trace, `intermediate: the ask engine threw — ${thrown}`);
|
|
11510
11674
|
}
|
|
@@ -11903,7 +12067,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11903
12067
|
if (!bareMetaHit) {
|
|
11904
12068
|
const refTerm = metaTermOf(factQuery, envelope);
|
|
11905
12069
|
const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
|
|
11906
|
-
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
|
|
12070
|
+
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
|
|
11907
12071
|
if (learned) {
|
|
11908
12072
|
const fact = (await factAnswer(memoryDir, factQuery, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
11909
12073
|
?? (await factReadBack(memoryDir, factQuery, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
@@ -11967,7 +12131,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11967
12131
|
handled = true;
|
|
11968
12132
|
note(trace, "lane: (2b) REFERENCE PACK — a bare \"what is X\" clean miss answered from the shipped reference pack, cited");
|
|
11969
12133
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${bareMetaHit.reference.article.title}" (revid ${bareMetaHit.reference.article.revid})`);
|
|
11970
|
-
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon);
|
|
12134
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.reference.key, bareMetaHit.reference.article, cache, referenceProvenanceTag, lexicon, synthesisBudget);
|
|
11971
12135
|
} else if (bareMetaHit?.live) {
|
|
11972
12136
|
// The bare-form LIVE hit settles the same way, under live provenance.
|
|
11973
12137
|
answer = bareMetaHit.text;
|
|
@@ -11976,7 +12140,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
11976
12140
|
handled = true;
|
|
11977
12141
|
note(trace, "lane: (2b) LIVE WIKIPEDIA — a bare \"what is X\" clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
11978
12142
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${bareMetaHit.live.article.title}" (revid ${bareMetaHit.live.article.revid})`);
|
|
11979
|
-
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon);
|
|
12143
|
+
await ingestReferenceArticle(memoryDir, bareMetaHit.live.key, bareMetaHit.live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
11980
12144
|
} else if (bareMetaHit) {
|
|
11981
12145
|
answer = bareMetaHit.replace ? bareMetaHit.text : `${answer}\n${bareMetaHit.text}`;
|
|
11982
12146
|
// Same discipline as lane (3): a fact-lane return flagged `miss` is an
|
|
@@ -12203,7 +12367,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12203
12367
|
// (4) #2 TEACH lane — a teach-shaped would-miss nothing above answered: route to
|
|
12204
12368
|
// memory, or say what CAN be remembered (LOUD), never the wall / a silent drop.
|
|
12205
12369
|
if (miss && recordMiss && via === "composed") {
|
|
12206
|
-
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder });
|
|
12370
|
+
const taught = await teachLane(query, { memoryDir, sessionId, lexicon, cache, planHolder, graph });
|
|
12207
12371
|
if (taught) {
|
|
12208
12372
|
answer = taught.text; via = taught.via; recordMiss = taught.miss;
|
|
12209
12373
|
if (!taught.miss) dialogueLaneOverride = "teach";
|
|
@@ -12222,6 +12386,26 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12222
12386
|
// kind of parent" as inherits) keeps its canonical: it genuinely
|
|
12223
12387
|
// restates the relation the teach stored.
|
|
12224
12388
|
if (envelope?.parsed?.fuzzyVerb) canonical = null;
|
|
12389
|
+
// A mid-game locative teach is accepted and stored as the player's own
|
|
12390
|
+
// note, but a running adventure's world only moves through actions — so
|
|
12391
|
+
// when the taught subject is a term the world already places, the
|
|
12392
|
+
// confirmation says so plainly. (The fold itself keeps taught rows out of
|
|
12393
|
+
// the world state; this is the matching UX.)
|
|
12394
|
+
if (!taught.miss && planHolder?.state?.adventure) {
|
|
12395
|
+
const loc = String(query).trim().match(LOCATIVE_TEACH_RE);
|
|
12396
|
+
if (loc) {
|
|
12397
|
+
let placed = false;
|
|
12398
|
+
try {
|
|
12399
|
+
const { normFactTerm } = await import("../adapters/memory/core.mjs");
|
|
12400
|
+
const world = foldWorldState(await factRows(memoryDir, cache));
|
|
12401
|
+
placed = world.placements.has(normFactTerm(loc[1]));
|
|
12402
|
+
} catch { placed = false; }
|
|
12403
|
+
if (placed) {
|
|
12404
|
+
answer = `${answer}\n(noted as your note — the game world itself only changes through actions like go, take and open.)`;
|
|
12405
|
+
note(trace, "intermediate: mid-game locative teach — stored as a note; the adventure fold only moves through actions");
|
|
12406
|
+
}
|
|
12407
|
+
}
|
|
12408
|
+
}
|
|
12225
12409
|
}
|
|
12226
12410
|
}
|
|
12227
12411
|
// (4b) #4 AUTHOR lane — "who is <Name>", "what did <Name> touch",
|
|
@@ -12362,7 +12546,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12362
12546
|
if (miss && recordMiss && via === "composed" && memoryDir) {
|
|
12363
12547
|
const refTerm = metaTermOf(query, envelope);
|
|
12364
12548
|
const key = refTerm ? await cleanMissPackKey(refTerm, { graph, memoryDir, lexicon, cache }) : null;
|
|
12365
|
-
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache }) : null;
|
|
12549
|
+
const learned = key ? await childPackFactsForKey(key, { memoryDir, env, cache, synthesisBudget }) : null;
|
|
12366
12550
|
if (learned) {
|
|
12367
12551
|
const fact = (await factAnswer(memoryDir, query, envelope, miss, biasByBundle, cache, newFocus?.label))
|
|
12368
12552
|
?? (await factReadBack(memoryDir, query, envelope, miss, graph, newFocus?.label, biasByBundle, cache));
|
|
@@ -12383,7 +12567,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12383
12567
|
recordMiss = false;
|
|
12384
12568
|
note(trace, "lane: (4h) REFERENCE PACK — a clean miss on a lexicon term answered from the shipped reference pack, cited");
|
|
12385
12569
|
note(trace, `source: reference pack ${REFERENCE_PACK_NAME} — article "${ref.article.title}" (revid ${ref.article.revid})`);
|
|
12386
|
-
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon);
|
|
12570
|
+
await ingestReferenceArticle(memoryDir, ref.key, ref.article, cache, referenceProvenanceTag, lexicon, synthesisBudget);
|
|
12387
12571
|
}
|
|
12388
12572
|
}
|
|
12389
12573
|
// The live Wikipedia supplement (opt-in), strictly AFTER both shipped
|
|
@@ -12399,7 +12583,7 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12399
12583
|
recordMiss = false;
|
|
12400
12584
|
note(trace, "lane: (4h) LIVE WIKIPEDIA — a clean miss answered from a live en.wikipedia.org lookup (opt-in), cited");
|
|
12401
12585
|
note(trace, `source: live reference ${LIVE_PACK_NAME} — article "${live.article.title}" (revid ${live.article.revid})`);
|
|
12402
|
-
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12586
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12403
12587
|
}
|
|
12404
12588
|
}
|
|
12405
12589
|
}
|
|
@@ -12421,12 +12605,25 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12421
12605
|
note(trace, `lane: (5) SHORT TAILORED MISS — every lane above declined; ${repeat ? "REPEAT collapsed to one-liner (wall kindness)" : "the full grammar wall was shortened + tailored to the query's keywords"}`);
|
|
12422
12606
|
}
|
|
12423
12607
|
// #4 HONEST-EMPTY POLISH — an empty CODE graph: any still-standing engine
|
|
12424
|
-
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
12425
|
-
// toward a real graph, unless it already points there. Only when
|
|
12608
|
+
// dead-end (an honest empty, the short miss, the bootstrap note) carries the
|
|
12609
|
+
// exit toward a real graph, unless it already points there. Only when
|
|
12610
|
+
// genuinely empty. The CLI keeps the --repo/example pointer verbatim; a
|
|
12611
|
+
// browser or a live adventure has no such command to reach for, so each gets
|
|
12612
|
+
// a teach-forward pointer (and the adventure also names the world asides that
|
|
12613
|
+
// are guaranteed to hit).
|
|
12614
|
+
const adventureLive = !!planHolder?.state?.adventure;
|
|
12426
12615
|
if (recordMiss && (via === "composed" || via === "miss")
|
|
12427
12616
|
&& noCodeGraph(graph) && !/--repo|tmct init|no code graph/i.test(answer)) {
|
|
12428
|
-
|
|
12429
|
-
|
|
12617
|
+
if (adventureLive) {
|
|
12618
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>". Or ask the world: "look", "where is the key", "talk to the butler".)`;
|
|
12619
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a live adventure miss points at the teach lane and the world asides, not the --repo remedy");
|
|
12620
|
+
} else if (browser) {
|
|
12621
|
+
answer = `${answer}\n(I don't know that yet — you can teach me: say "remember: <thing> is a <kind>".)`;
|
|
12622
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — a browser miss points at the teach lane, not the CLI-only --repo remedy");
|
|
12623
|
+
} else {
|
|
12624
|
+
answer = `${answer}\n(this repo has no code graph — for structure, point me at a \`.tmct/graph.json\` with \`--repo <path>\` or run \`npm run example:mini\`; tmct doesn't index code itself.)`;
|
|
12625
|
+
note(trace, "intermediate: HONEST-EMPTY POLISH — the loaded graph has 0 modules, so the dead-end got a --repo/tmct init pointer appended");
|
|
12626
|
+
}
|
|
12430
12627
|
}
|
|
12431
12628
|
// TEACH-OFFER: a "what is X" miss where X is genuinely unknown EVERYWHERE —
|
|
12432
12629
|
// not a real graph entity, not a schema/vocab term, and not already in
|
|
@@ -12497,20 +12694,24 @@ async function runAsk(query, { config, source, graph, focus, last, templates, me
|
|
|
12497
12694
|
answer = `hypothetically, if ${counterfactualSubject[1].trim()} were removed: ${answer}`;
|
|
12498
12695
|
note(trace, `intermediate: COUNTERFACTUAL_RE matched — compiled to a real traversal, wrapped as hypothetical ("${counterfactualSubject[1].trim()}" removed)`);
|
|
12499
12696
|
}
|
|
12500
|
-
// LIVE SUPPLEMENT (/wiki supplement
|
|
12501
|
-
// Wikipedia says about its subject —
|
|
12502
|
-
//
|
|
12503
|
-
//
|
|
12504
|
-
// Wikipedia read-out. Failure-tolerated, and network-gated by
|
|
12505
|
-
//
|
|
12506
|
-
|
|
12507
|
-
|
|
12697
|
+
// LIVE SUPPLEMENT (/wiki supplement, and its superset /wiki always): a
|
|
12698
|
+
// grounded answer also carries what Wikipedia says about its subject —
|
|
12699
|
+
// corroboration, not rescue. Scoped to a clean vocabulary subject (a
|
|
12700
|
+
// "what is X" / "tell me about X" term), never doubled onto an answer that
|
|
12701
|
+
// already IS a Wikipedia read-out. Failure-tolerated, and network-gated by
|
|
12702
|
+
// the same toggle (both values are truthy, so the rescue lanes above already
|
|
12703
|
+
// ran). "always" widens the term fallback to an ordinary grounded ask's own
|
|
12704
|
+
// parsed object, so a plain code/fact answer also gets corroborated; the
|
|
12705
|
+
// adapter's throttle bounds the request rate.
|
|
12706
|
+
if ((liveReference === "supplement" || liveReference === "always") && !recordMiss && via !== "reference") {
|
|
12707
|
+
const supplementTerm = metaTermOf(query, envelope) || vagueTouchTermOf(query)
|
|
12708
|
+
|| (liveReference === "always" ? envelope?.parsed?.object : null);
|
|
12508
12709
|
let liveKey = null;
|
|
12509
12710
|
try { liveKey = supplementTerm ? cleanMissLiveTerm(supplementTerm, lexicon ?? undefined) : null; } catch { liveKey = null; }
|
|
12510
12711
|
const live = liveKey ? await liveReferenceAnswerForKey(liveKey, onLiveLookup) : null;
|
|
12511
12712
|
if (live) {
|
|
12512
12713
|
answer = `${answer}\nWikipedia adds: ${live.text}`;
|
|
12513
|
-
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon);
|
|
12714
|
+
await ingestReferenceArticle(memoryDir, live.key, live.article, cache, liveProvenanceTag, lexicon, synthesisBudget);
|
|
12514
12715
|
note(trace, `intermediate: LIVE SUPPLEMENT — appended a cited en.wikipedia.org read-out for "${supplementTerm}" (supplement mode)`);
|
|
12515
12716
|
}
|
|
12516
12717
|
}
|
|
@@ -12653,13 +12854,14 @@ async function runCommand(line, { config, source, graph, focus, memoryDir, trace
|
|
|
12653
12854
|
// state). A bare "/wiki" reports the CURRENT state and changes nothing.
|
|
12654
12855
|
if (name === "wiki") {
|
|
12655
12856
|
const arg = argText.toLowerCase();
|
|
12656
|
-
const stateWord = (v) => (v === "supplement" ? "supplement" : v ? "on" : "off");
|
|
12657
|
-
if (arg !== "on" && arg !== "off" && arg !== "supplement") {
|
|
12658
|
-
return mk(`live Wikipedia supplement is ${stateWord(liveReference)} — /wiki on, /wiki off, or /wiki
|
|
12857
|
+
const stateWord = (v) => (v === "always" ? "always" : v === "supplement" ? "supplement" : v ? "on" : "off");
|
|
12858
|
+
if (arg !== "on" && arg !== "off" && arg !== "supplement" && arg !== "always") {
|
|
12859
|
+
return mk(`live Wikipedia supplement is ${stateWord(liveReference)} — /wiki on, /wiki off, /wiki supplement, or /wiki always. `
|
|
12659
12860
|
+ "When on, a question I can't answer also tries en.wikipedia.org (network); "
|
|
12660
|
-
+ "supplement adds a cited Wikipedia read-out under every grounded answer too
|
|
12861
|
+
+ "supplement adds a cited Wikipedia read-out under every grounded vocabulary answer too; "
|
|
12862
|
+
+ "always widens that to every grounded answer.");
|
|
12661
12863
|
}
|
|
12662
|
-
const next = arg === "supplement" ? "supplement" : arg === "on";
|
|
12864
|
+
const next = arg === "always" ? "always" : arg === "supplement" ? "supplement" : arg === "on";
|
|
12663
12865
|
return mk(`live Wikipedia supplement ${stateWord(next)}.`, { liveReferenceNext: next });
|
|
12664
12866
|
}
|
|
12665
12867
|
|
|
@@ -13747,7 +13949,7 @@ function vocabAntecedentFrom(last) {
|
|
|
13747
13949
|
return m[1];
|
|
13748
13950
|
}
|
|
13749
13951
|
|
|
13750
|
-
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, _noSplit = false } = {}) {
|
|
13952
|
+
export async function runTurn(input, { config, source = defaultSource, graph = null, focus = null, last = null, memoryDir = null, sessionId = "", env = process.env, lexicon = null, narrate = false, liveReference = false, onLiveLookup = null, vocabHint = null, tel = null, biasByBundle = {}, factRowsCache: injectedFactRowsCache = null, planState = null, gameConfig = null, uiContext = "cli", synthesisBudget = AUTO_SYNTHESIS_BUDGET, researchState = null, researchConfig = null, _noSplit = false } = {}) {
|
|
13751
13953
|
// Every game's tuning knobs (spider-fly's mass economy, guess-the-number's
|
|
13752
13954
|
// bounds, the shared plan lane's search-depth cap) — a caller's own
|
|
13753
13955
|
// gameConfig (chat-session.mjs resolves one per session from tmct.toml)
|
|
@@ -13812,7 +14014,7 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13812
14014
|
// the PLAN NEXT block below write planHolder.state; every other path leaves
|
|
13813
14015
|
// it untouched, and the caller re-threads whatever comes back.
|
|
13814
14016
|
const planHolder = { state: planState };
|
|
13815
|
-
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig };
|
|
14017
|
+
const ctx = { config, source, graph, focus, last, memoryDir, sessionId, templates, env, lexicon, trace, narrate, liveReference, onLiveLookup, vocabHint: resolvedVocabHint, tel, biasByBundle, cache: factRowsCache, vocabAntecedent, planHolder, gameConfig: resolvedGameConfig, uiContext, synthesisBudget };
|
|
13816
14018
|
// A DISPATCHED turn (count / slash-command / ask) becomes the new "last
|
|
13817
14019
|
// answer" that why/say-more re-renders; a conversational turn does not.
|
|
13818
14020
|
// Every dispatched turn's result passes through finish() here — the LAST
|
|
@@ -13950,6 +14152,47 @@ export async function runTurn(input, { config, source = defaultSource, graph = n
|
|
|
13950
14152
|
}
|
|
13951
14153
|
}
|
|
13952
14154
|
|
|
14155
|
+
// RESEARCH — "research <topic>[, limit N]" runs a Simple English Wikipedia
|
|
14156
|
+
// queue through the same ingest path a live-Wikipedia rescue uses: depth 0
|
|
14157
|
+
// now, the lead section's linked topics queued for "research next" (which
|
|
14158
|
+
// the web pages' auto-play button submits turn by turn). The explicit
|
|
14159
|
+
// request is the network consent for its own fetches — unlike the
|
|
14160
|
+
// clean-miss rescue, which fires on an ordinary question and so stays
|
|
14161
|
+
// behind /wiki on. Queue state threads turn-to-turn as researchState, the
|
|
14162
|
+
// same way planState does.
|
|
14163
|
+
{
|
|
14164
|
+
const researchHolder = { state: researchState };
|
|
14165
|
+
const resolvedResearchConfig = researchConfig ?? RESEARCH_DEFAULTS;
|
|
14166
|
+
const rTurn = await researchTurn(workingLine, {
|
|
14167
|
+
holder: researchHolder,
|
|
14168
|
+
memoryDir,
|
|
14169
|
+
lexicon,
|
|
14170
|
+
provider: getResearchProvider({ minIntervalMs: resolvedResearchConfig.minIntervalMs }),
|
|
14171
|
+
config: resolvedResearchConfig,
|
|
14172
|
+
planActive: Boolean(planHolder.state && !planHolder.state.done),
|
|
14173
|
+
pagerActive: Boolean(Array.isArray(last?.detail?.pending?.items) && last.detail.pending.items.length),
|
|
14174
|
+
notify: onLiveLookup,
|
|
14175
|
+
ingest: (key, article, tag) =>
|
|
14176
|
+
ingestReferenceArticle(memoryDir, key, article, factRowsCache, () => tag, lexicon, synthesisBudget),
|
|
14177
|
+
});
|
|
14178
|
+
if (rTurn) {
|
|
14179
|
+
note(trace, `lane: ${rTurn.note}`);
|
|
14180
|
+
note(trace, `goal: ${rTurn.goal}`);
|
|
14181
|
+
const result = plainTurn(workingLine, rTurn.text, { via: "research", miss: !!rTurn.miss, focus });
|
|
14182
|
+
result.lane = "research";
|
|
14183
|
+
const snapshot = researchSnapshot(researchHolder.state);
|
|
14184
|
+
if (snapshot) result.record.research = snapshot;
|
|
14185
|
+
const rec = withLast(result, rTurn.goal);
|
|
14186
|
+
rec.planState = planHolder.state;
|
|
14187
|
+
rec.researchState = researchHolder.state;
|
|
14188
|
+
// Present on EVERY research turn — null when the run ended or never
|
|
14189
|
+
// started — so a UI can tell "queue cleared" from "not a research
|
|
14190
|
+
// turn" (where the field is absent entirely).
|
|
14191
|
+
rec.research = snapshot;
|
|
14192
|
+
return rec;
|
|
14193
|
+
}
|
|
14194
|
+
}
|
|
14195
|
+
|
|
13953
14196
|
// Conversational layer next (greetings, thanks, help, bye, why/say-more) — these
|
|
13954
14197
|
// resolve no entity and carry their own preserved `last`. Bypasses withLast (a
|
|
13955
14198
|
// conversational turn is never finish()'d / never becomes a new `last`), so the
|