@polycode-projects/the-mechanical-code-talker 4.0.0 → 4.1.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 +178 -9
- package/corpus/reference/index.json.gz +0 -0
- package/corpus/reference/manifest.json +8 -8
- package/corpus/reference/shards/ref-00.jsonl.gz +0 -0
- package/corpus/sprites/src/sprite-facts.jsonl +401 -0
- package/corpus/worlds/index.json.gz +0 -0
- package/corpus/worlds/manifest.json +49 -9
- package/corpus/worlds/shards/greyvale-museum.jsonl.gz +0 -0
- package/corpus/worlds/shards/lantern-cottage.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-hollow.jsonl.gz +0 -0
- package/corpus/worlds/shards/mud-warren.jsonl.gz +0 -0
- package/corpus/worlds/shards/spider-fly.jsonl.gz +0 -0
- package/corpus/worlds/src/greyvale-museum.jsonl +136 -0
- package/corpus/worlds/src/lantern-cottage.jsonl +60 -0
- package/corpus/worlds/src/mud-hollow.jsonl +82 -0
- package/corpus/worlds/src/mud-warren.jsonl +124 -0
- package/corpus/worlds/src/spider-fly.jsonl +1 -1
- package/data/sprites/animal-icon.toml +11 -5
- package/data/sprites/book-icon.toml +9 -5
- package/data/sprites/cabinet-icon.toml +10 -5
- package/data/sprites/cellar-icon.toml +16 -6
- package/data/sprites/container-icon.toml +7 -3
- package/data/sprites/desk-icon.toml +8 -5
- package/data/sprites/dog-icon.toml +8 -5
- package/data/sprites/dog-with-colour-icon.toml +13 -12
- package/data/sprites/drawing-room-icon.toml +14 -5
- package/data/sprites/egg-icon.toml +6 -3
- package/data/sprites/fly-icon.toml +10 -5
- package/data/sprites/furniture-icon.toml +5 -3
- package/data/sprites/garden-icon.toml +10 -3
- package/data/sprites/key-icon.toml +3 -1
- package/data/sprites/kitchen-icon.toml +15 -6
- package/data/sprites/lamp-icon.toml +9 -4
- package/data/sprites/letter-icon.toml +7 -4
- package/data/sprites/library-icon.toml +14 -3
- package/data/sprites/pan-icon.toml +7 -3
- package/data/sprites/person-icon.toml +6 -2
- package/data/sprites/poodle-icon.toml +1 -1
- package/data/sprites/portable-icon.toml +6 -4
- package/data/sprites/portrait-icon.toml +7 -4
- package/data/sprites/room-icon.toml +11 -2
- package/data/sprites/spider-icon.toml +9 -3
- package/data/sprites/study-icon.toml +10 -2
- package/package.json +5 -4
- package/src/adapters/memory/core.mjs +20 -0
- package/src/domain/ask-vocab.mjs +71 -0
- package/src/domain/ask.mjs +168 -0
- package/src/domain/game-config.mjs +11 -0
- package/src/domain/grammar/ace.mjs +40 -4
- package/src/domain/grammar/lexicon-core.json +2 -1
- package/src/domain/grammar/lexicon.mjs +18 -0
- package/src/domain/mud-facts.mjs +15 -0
- package/src/domain/reference-pack.mjs +31 -7
- package/src/domain/router/drive.mjs +35 -9
- package/src/domain/router/registry.mjs +24 -4
- package/src/domain/router/resolver.mjs +102 -40
- package/src/domain/scene-compose.mjs +117 -0
- package/src/domain/spider-fly-world.mjs +37 -1
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +169 -20
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +209 -157
- package/src/services/adventure.mjs +526 -391
- package/src/services/chat-page-viz.mjs +69 -25
- package/src/services/chat.mjs +200 -51
- package/src/services/code-explorer-viz.mjs +102 -62
- package/src/services/extract-facts.mjs +4 -7
- package/src/services/ingest-viz.mjs +68 -82
- package/src/services/ledger-viz.mjs +136 -67
- package/src/services/memory-panel-viz.mjs +62 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +1016 -208
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +123 -64
- package/src/services/research-viz.mjs +160 -108
- package/src/services/spider-fly-turn.mjs +15 -23
- package/src/services/spider-fly-viz.mjs +146 -161
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +414 -240
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +90 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +49 -33
- package/src/surfaces/web/chat-browser-entry.mjs +30 -105
- package/src/surfaces/web/code-explorer-browser-entry.mjs +168 -24
- package/src/surfaces/web/ingest-browser-entry.mjs +3 -13
- package/src/surfaces/web/ledger-browser-entry.mjs +7 -47
- package/src/surfaces/web/memory-ask-browser.bundle.js +127 -124
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +71 -29
- package/src/surfaces/web/plan-browser-entry.mjs +22 -40
- package/src/surfaces/web/research-browser-entry.mjs +26 -41
- package/src/surfaces/web/spider-fly-browser-entry.mjs +45 -28
- package/src/surfaces/web/sprites-browser-entry.mjs +14 -27
- package/src/surfaces/web/turn-session.mjs +120 -0
- package/src/tools/definitions.mjs +30 -0
- package/src/tools/handlers/index.mjs +6 -3
- package/src/tools/handlers/kit.mjs +19 -2
- package/src/tools/handlers/tmct-ask.mjs +11 -6
- package/src/tools/handlers/tmct-ingest.mjs +5 -1
- package/src/tools/handlers/tmct-related.mjs +4 -4
- package/src/tools/handlers/tmct-sprite.mjs +147 -0
- package/src/tools/memory-fallthrough.mjs +9 -2
- package/src/tools/server.mjs +25 -1
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// store shape.
|
|
5
5
|
import { loadMemory, readFactRows } from "../../adapters/memory/core.mjs";
|
|
6
6
|
import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
|
|
7
|
+
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* The memory a running session holds, broken down by where each fact came
|
|
@@ -51,3 +52,13 @@ export async function memoryStats(memoryDir) {
|
|
|
51
52
|
}
|
|
52
53
|
return { total: rows.length, bandCounts, taught };
|
|
53
54
|
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The session's whole triple store as JSONL — one
|
|
58
|
+
* { subject, predicate, object, provenance } object per line, the same shape
|
|
59
|
+
* `tmct extract` and `tmct memory --export` emit. Reads the live memory the
|
|
60
|
+
* same way memoryStats does; a page offers this as a download.
|
|
61
|
+
*/
|
|
62
|
+
export async function exportFactsJsonl(memoryDir) {
|
|
63
|
+
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
64
|
+
}
|
|
@@ -28,7 +28,6 @@
|
|
|
28
28
|
// auto-playing at once — this file makes no ordering promise between two
|
|
29
29
|
// concurrent calls into the same memoryDir, the same way two callers writing
|
|
30
30
|
// into any shared store concurrently would need their own queue.
|
|
31
|
-
import { runTurn } from "../../services/chat.mjs";
|
|
32
31
|
import {
|
|
33
32
|
createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
|
|
34
33
|
} from "../../adapters/memory/core.mjs";
|
|
@@ -38,8 +37,9 @@ import {
|
|
|
38
37
|
foldWorldState, worldActionRows, worldDigestRows, roomAffordances,
|
|
39
38
|
personKnowledgeLines, personKnownFoodLines,
|
|
40
39
|
diggableDirections, castInRoom, displayNameOf, isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase,
|
|
41
|
-
roomKindOf,
|
|
40
|
+
roomKindOf, isMudStatePredicate, worldEpochFact, snapshotSubject,
|
|
42
41
|
} from "../../services/adventure.mjs";
|
|
42
|
+
import { waveFact, playedByFact, P2P_PREDICATES } from "../../domain/p2p/facts.mjs";
|
|
43
43
|
import { relatedForTerm } from "../../domain/skos-view.mjs";
|
|
44
44
|
import { runMudTurn } from "../../services/mud-turn.mjs";
|
|
45
45
|
import { parseMudEditorText, planMudEditorSync } from "../../services/mud-editor.mjs";
|
|
@@ -47,6 +47,7 @@ import { mudSpeciesOf } from "../../domain/game-config.mjs";
|
|
|
47
47
|
import { worldProvenanceTag } from "../../domain/worlds-pack.mjs";
|
|
48
48
|
import { resolveSpriteForClass, SPRITE_REGISTRY, classAncestorChain } from "../../domain/sprite-map.mjs";
|
|
49
49
|
import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
|
|
50
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
50
51
|
|
|
51
52
|
/** A live, shared mud world several characters can each act in. `worldPayload`
|
|
52
53
|
* is `{ name, facts, rules, opening }` — the same shape adventure-browser-
|
|
@@ -64,13 +65,18 @@ import { resolveSpriteAsset } from "../../domain/sprite-templates.mjs";
|
|
|
64
65
|
* visitedRoomIds, turnsTaken, isOutOfPlay, outOfPlayReason }`. `snapshot()` is
|
|
65
66
|
* the one OMNISCIENT read this module exposes — the central world map's own
|
|
66
67
|
* data source, never a per-window one. */
|
|
67
|
-
export async function createMudSession(worldPayload, { characters = [] } = {}) {
|
|
68
|
+
export async function createMudSession(worldPayload, { characters = [], epoch = 0 } = {}) {
|
|
68
69
|
const memoryDir = createInMemoryStore();
|
|
69
70
|
const tag = worldProvenanceTag(worldPayload.name);
|
|
70
71
|
const seedFacts = worldFactsForCast(worldPayload.facts, characters);
|
|
71
72
|
await appendFacts(memoryDir, seedFacts.map((f) => ({
|
|
72
73
|
subject: f.subject, predicate: f.predicate, object: f.object, provenance: tag,
|
|
73
74
|
})));
|
|
75
|
+
// A recast opens the same deterministic ids over a fresh store while peers
|
|
76
|
+
// may still hold the old run's snapshots. The epoch marker is what makes
|
|
77
|
+
// every fold — local and merged — treat this seed as the newer state. An
|
|
78
|
+
// unrecast boot writes nothing, so a solo session's store is unchanged.
|
|
79
|
+
if (epoch > 0) await appendFacts(memoryDir, [{ ...worldEpochFact(epoch), provenance: tag }]);
|
|
74
80
|
for (const rule of worldPayload.rules) {
|
|
75
81
|
await appendRule(memoryDir, { name: rule.name, kind: rule.ruleKind, slots: rule.slots, provenance: tag });
|
|
76
82
|
}
|
|
@@ -104,9 +110,10 @@ export async function createMudSession(worldPayload, { characters = [] } = {}) {
|
|
|
104
110
|
// "it"/"there" belongs to that window's own conversation, and its own
|
|
105
111
|
// discovered-room history is the real fog of war this page promises —
|
|
106
112
|
// sharing either across characters would leak one window's state into
|
|
107
|
-
// another's.
|
|
108
|
-
|
|
109
|
-
|
|
113
|
+
// another's. createTurnSession owns focus/last for us; planState is the
|
|
114
|
+
// one piece that is NOT per-character (see planHolder above), so it is
|
|
115
|
+
// threaded through buildExtraOptions/captureExtraState instead of living
|
|
116
|
+
// in the session's own internal slot.
|
|
110
117
|
const visitedRoomIds = new Set();
|
|
111
118
|
// This character's OWN turns, not the page's shared tick counter: two
|
|
112
119
|
// windows playing at different speeds, or one paused while the other
|
|
@@ -116,6 +123,20 @@ export async function createMudSession(worldPayload, { characters = [] } = {}) {
|
|
|
116
123
|
const startRoom = await roomOf(character);
|
|
117
124
|
if (startRoom) visitedRoomIds.add(startRoom);
|
|
118
125
|
|
|
126
|
+
const turnSession = createTurnSession({
|
|
127
|
+
memoryDir, graph, lexicon, sessionId: character,
|
|
128
|
+
vocabHint: 'Try a world command ("dig north", "eat the carrot-1"), or ask "what food do you know about".',
|
|
129
|
+
buildExtraOptions: () => ({
|
|
130
|
+
uiContext: "browser", actingSubject: character, planState: planHolder.state,
|
|
131
|
+
}),
|
|
132
|
+
captureExtraState: async (result, state) => {
|
|
133
|
+
if ("planState" in result) planHolder.state = state.planState;
|
|
134
|
+
turnsTaken += 1;
|
|
135
|
+
const here = await roomOf(character);
|
|
136
|
+
if (here) visitedRoomIds.add(here);
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
119
140
|
windows[character] = {
|
|
120
141
|
character,
|
|
121
142
|
|
|
@@ -124,27 +145,7 @@ export async function createMudSession(worldPayload, { characters = [] } = {}) {
|
|
|
124
145
|
* character via actingSubject. A throwing runTurn must never end the
|
|
125
146
|
* session; this window has no other chance to show this turn's
|
|
126
147
|
* answer. */
|
|
127
|
-
|
|
128
|
-
let result;
|
|
129
|
-
try {
|
|
130
|
-
result = await runTurn(line, {
|
|
131
|
-
config: null, source: null, graph, focus, last, memoryDir,
|
|
132
|
-
sessionId: character, env: {}, lexicon, uiContext: "browser",
|
|
133
|
-
actingSubject: character, planState: planHolder.state,
|
|
134
|
-
vocabHint: 'Try a world command ("dig north", "eat the carrot-1"), or ask "what food do you know about".',
|
|
135
|
-
});
|
|
136
|
-
} catch (e) {
|
|
137
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
138
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing.`, end: false };
|
|
139
|
-
}
|
|
140
|
-
focus = result.focus;
|
|
141
|
-
last = result.last;
|
|
142
|
-
if ("planState" in result) planHolder.state = result.planState;
|
|
143
|
-
turnsTaken += 1;
|
|
144
|
-
const here = await roomOf(character);
|
|
145
|
-
if (here) visitedRoomIds.add(here);
|
|
146
|
-
return { answer: result.answer, end: Boolean(result.end) };
|
|
147
|
-
},
|
|
148
|
+
turn: turnSession.turn,
|
|
148
149
|
|
|
149
150
|
/** One whole scripted turn (mud-turn.mjs's runMudTurn): investigate,
|
|
150
151
|
* walk toward known food, or roll at the edge (dig). `k` is the turn
|
|
@@ -220,7 +221,7 @@ export async function createMudSession(worldPayload, { characters = [] } = {}) {
|
|
|
220
221
|
const editTurn = state.turnCount + 1;
|
|
221
222
|
if (toAppend.length) {
|
|
222
223
|
await appendFacts(memoryDir, toAppend.map((f) => ({
|
|
223
|
-
subject: f.kind === "other" ? f.subject :
|
|
224
|
+
subject: f.kind === "other" ? f.subject : snapshotSubject(f.subject, editTurn, state.epoch),
|
|
224
225
|
predicate: f.predicate,
|
|
225
226
|
object: f.object,
|
|
226
227
|
provenance: f.kind === "other" ? tag : `${tag}:turn${editTurn}`,
|
|
@@ -232,7 +233,44 @@ export async function createMudSession(worldPayload, { characters = [] } = {}) {
|
|
|
232
233
|
return { unrecognized, added: toAppend.length, removed };
|
|
233
234
|
}
|
|
234
235
|
|
|
235
|
-
|
|
236
|
+
// Wall-clock resolution is 1ms, and both writers below are content-addressed
|
|
237
|
+
// by (subject, predicate, object) — a second wave from the same character in
|
|
238
|
+
// the same room is the SAME fact id, so it only registers as a change if its
|
|
239
|
+
// provenance tag differs. Two writes inside one tick would share a timestamp,
|
|
240
|
+
// share a tag, and the second would silently vanish. p2p-room.mjs nudges its
|
|
241
|
+
// own clock for exactly this reason; this is the same nudge for the writes
|
|
242
|
+
// that happen before a room exists.
|
|
243
|
+
let lastWriteMs = -Infinity;
|
|
244
|
+
function stampNow() {
|
|
245
|
+
const nudged = Math.max(Date.now(), lastWriteMs + 1);
|
|
246
|
+
lastWriteMs = nudged;
|
|
247
|
+
return new Date(nudged).toISOString();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** `character` waves in whichever room it currently stands in — an ordinary
|
|
251
|
+
* add-only fact, so a page with no network renders it exactly like a page
|
|
252
|
+
* sharing the world with three others. Returns the room waved in, or null
|
|
253
|
+
* when the character stands nowhere (out of play). Nothing is ever
|
|
254
|
+
* retracted: "currently waving" is a recency read over this fact's own
|
|
255
|
+
* provenance timestamp. */
|
|
256
|
+
async function wave(character) {
|
|
257
|
+
const here = await roomOf(character);
|
|
258
|
+
if (!here) return null;
|
|
259
|
+
await appendFacts(memoryDir, [waveFact(character, here, stampNow())]);
|
|
260
|
+
return here;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Claim `characters` for `peerId` — one add-only `mgx:playedBy` fact each.
|
|
264
|
+
* Claims never overwrite: two peers claiming the same animal both write,
|
|
265
|
+
* and every reader settles it the same way by taking the oldest claim. */
|
|
266
|
+
async function claimCharacters(characters, peerId) {
|
|
267
|
+
const at = stampNow();
|
|
268
|
+
const claims = (characters || []).map((character) => playedByFact(character, peerId, at));
|
|
269
|
+
if (claims.length) await appendFacts(memoryDir, claims);
|
|
270
|
+
return claims.length;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
return { memoryDir, windows, snapshot, applyEdit, wave, claimCharacters };
|
|
236
274
|
}
|
|
237
275
|
|
|
238
276
|
/** `count` entries drawn at random from `roster`, in random order, without
|
|
@@ -324,6 +362,10 @@ globalThis.tmctMud = {
|
|
|
324
362
|
personKnowledgeLines, personKnownFoodLines,
|
|
325
363
|
diggableDirections, castInRoom, displayNameOf, isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase,
|
|
326
364
|
roomKindOf,
|
|
365
|
+
// The shared-world reach-throughs: which predicates carry live world state,
|
|
366
|
+
// and the P2P layer's own four. mud.html hands both to `mudSyncableFacts`,
|
|
367
|
+
// which is written to take the check rather than import the engine itself.
|
|
368
|
+
isMudStatePredicate, P2P_PREDICATES,
|
|
327
369
|
// The edit mode's own reach-throughs: the SKOS neighbourhood and the is-a
|
|
328
370
|
// chain its cursor-suggestion pills read, neither of which is splice-safe.
|
|
329
371
|
relatedForTerm,
|
|
@@ -24,7 +24,6 @@
|
|
|
24
24
|
// engine. `maxDepth` is overridable PER CALL (not just at session creation)
|
|
25
25
|
// so the page's own max-search-depth control can re-run "solve it" on the
|
|
26
26
|
// CURRENT board without tearing down and re-teaching the whole puzzle.
|
|
27
|
-
import { runTurn } from "../../services/chat.mjs";
|
|
28
27
|
import { createInMemoryStore } from "../../adapters/memory/core.mjs";
|
|
29
28
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
30
29
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
@@ -32,6 +31,7 @@ import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
|
|
|
32
31
|
import { hanoiLessonSentences } from "../../domain/hanoi-lesson.mjs";
|
|
33
32
|
import { computeBlocksLayout, planToPageData, renderInputsFromPlan } from "../../services/plan-viz.mjs";
|
|
34
33
|
import { planToPddl } from "../../services/plan-pddl.mjs";
|
|
34
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
35
35
|
// Re-exported so the page can register a CDN-loaded wink-nlp pair before the
|
|
36
36
|
// first teach, the same seam chat-browser-entry.mjs exposes as
|
|
37
37
|
// tmctChat.registerWinkModel — see wink-model.mjs's own header. The hanoi
|
|
@@ -57,52 +57,34 @@ export async function createPlanSession({ diskCount = 3, maxDepth = DEFAULT_GAME
|
|
|
57
57
|
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
58
58
|
const lexicon = loadLexicon();
|
|
59
59
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
60
|
-
const planHolder = { state: null };
|
|
61
|
-
let focus = null;
|
|
62
|
-
let last = null;
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
84
|
-
env: {}, lexicon, vocabHint: "", planState: planHolder.state, gameConfig,
|
|
85
|
-
});
|
|
86
|
-
} catch (e) {
|
|
87
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
88
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, plan: null };
|
|
89
|
-
}
|
|
90
|
-
focus = result.focus;
|
|
91
|
-
last = result.last;
|
|
92
|
-
if ("planState" in result) planHolder.state = result.planState;
|
|
93
|
-
const plan = result.plan
|
|
94
|
-
? { ...result.plan, becauseText: planHolder.state?.becauseText ?? null }
|
|
95
|
-
: null;
|
|
96
|
-
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan };
|
|
97
|
-
}
|
|
61
|
+
// `maxDepth` overrides the session's own default for just this one call
|
|
62
|
+
// (the page's own max-search-depth control threads it on every call,
|
|
63
|
+
// including a plain typed "solve it"), so raising or lowering it never
|
|
64
|
+
// requires re-teaching the board. `captureExtraState` folds `becauseText`
|
|
65
|
+
// onto the returned `plan` from the session's own plan slot — the
|
|
66
|
+
// plan-lane contract's returned object never carries it itself (only
|
|
67
|
+
// planState does), and the PDDL panel's own "because —" line needs it.
|
|
68
|
+
const session = createTurnSession({
|
|
69
|
+
memoryDir, graph, lexicon, sessionId, vocabHint: "",
|
|
70
|
+
buildExtraOptions: (state, callArgs) => ({
|
|
71
|
+
gameConfig: {
|
|
72
|
+
...DEFAULT_GAME_CONFIG,
|
|
73
|
+
planning: { ...DEFAULT_GAME_CONFIG.planning, maxDepth: callArgs?.maxDepth ?? maxDepth },
|
|
74
|
+
},
|
|
75
|
+
}),
|
|
76
|
+
captureExtraState: (result, state) => {
|
|
77
|
+
if (result.plan) result.plan = { ...result.plan, becauseText: state.planState?.becauseText ?? null };
|
|
78
|
+
},
|
|
79
|
+
});
|
|
98
80
|
|
|
99
81
|
let plan = null;
|
|
100
82
|
for (const sentence of hanoiLessonSentences(diskCount)) {
|
|
101
|
-
const r = await turn(sentence);
|
|
83
|
+
const r = await session.turn(sentence);
|
|
102
84
|
if (r.plan) plan = r.plan;
|
|
103
85
|
}
|
|
104
86
|
|
|
105
|
-
return { memoryDir, sessionId, diskCount, maxDepth, plan, turn };
|
|
87
|
+
return { memoryDir, sessionId, diskCount, maxDepth, plan, turn: session.turn };
|
|
106
88
|
}
|
|
107
89
|
|
|
108
90
|
// Re-exported so the page's own rendering script (plan-viz.mjs's inlined
|
|
@@ -26,10 +26,9 @@
|
|
|
26
26
|
// Gitignored, Pages-demo-site-only output — scripts/build-demo-site.mjs builds
|
|
27
27
|
// it fresh on every deploy, never committed, the same posture the chat/ingest
|
|
28
28
|
// bundles document for their own output.
|
|
29
|
-
import {
|
|
29
|
+
import { vocabExampleHint, factAnswer, factReadBack } from "../../services/chat.mjs";
|
|
30
30
|
import { clampResearchConfig } from "../../services/research.mjs";
|
|
31
|
-
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, removeFacts } from "../../adapters/memory/core.mjs";
|
|
32
|
-
import { serializeFactsJsonl } from "../../adapters/memory/export-jsonl.mjs";
|
|
31
|
+
import { createInMemoryStore, normFactTerm, loadMemory, readFactRows, removeFacts, applySeedPayload } from "../../adapters/memory/core.mjs";
|
|
33
32
|
import { provenanceTagToSource } from "../../domain/memory/trust.mjs";
|
|
34
33
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
35
34
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
@@ -39,6 +38,8 @@ import { registerLiveReferenceProvider, registerResearchProvider } from "../../a
|
|
|
39
38
|
import { groundTextToFacts } from "./ingest-browser-entry.mjs";
|
|
40
39
|
import { openPersistedStore } from "./idb-persist.mjs";
|
|
41
40
|
import { digestTermFromPayloadBrowser } from "./digest-client.mjs";
|
|
41
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
42
|
+
import { exportFactsJsonl } from "./memory-stats.mjs";
|
|
42
43
|
|
|
43
44
|
// The Fact individual's first-write-wins timestamp, read straight off the
|
|
44
45
|
// stored attribute (mgx:createdAt) so a "recently learned" ordering never
|
|
@@ -205,22 +206,16 @@ export async function researchSnapshot(memoryDir, sessionIds = {}, { recentCap =
|
|
|
205
206
|
*/
|
|
206
207
|
export function createResearchSession({ seedPayload = null, vocabSeeded = false, liveReference = false, onLiveLookup = null, synthesisBudget = 12, digestStructures = null } = {}) {
|
|
207
208
|
const memoryDir = createInMemoryStore();
|
|
208
|
-
|
|
209
|
+
applySeedPayload(memoryDir, seedPayload);
|
|
209
210
|
|
|
210
211
|
const graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
211
212
|
const lexicon = loadLexicon();
|
|
212
213
|
const vocabHint = vocabExampleHint(vocabSeeded);
|
|
213
|
-
const chatSessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
214
214
|
// A DISTINCT id so an ingested fact's teach tag is told apart from a typed
|
|
215
215
|
// teach turn's by session id alone — the whole reason the two growth paths
|
|
216
216
|
// stay separable in the source panel.
|
|
217
217
|
const ingestSessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now() + 1);
|
|
218
|
-
const sessionIds = { chatSessionId, ingestSessionId };
|
|
219
218
|
|
|
220
|
-
let focus = null;
|
|
221
|
-
let last = null;
|
|
222
|
-
let planState = null;
|
|
223
|
-
let researchState = null;
|
|
224
219
|
const normLive = (v) => (v === "always" ? "always" : v === "supplement" ? "supplement" : Boolean(v));
|
|
225
220
|
let liveReferenceOn = normLive(liveReference);
|
|
226
221
|
let synthesisBudgetOn = Number.isFinite(synthesisBudget) ? synthesisBudget : 12;
|
|
@@ -230,6 +225,26 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
|
|
|
230
225
|
// (they ride its persisted state), so its queue stays internally consistent.
|
|
231
226
|
let researchConfig = clampResearchConfig();
|
|
232
227
|
|
|
228
|
+
// The chat-engine turn (research/teach/ask), threading focus/last/planState/
|
|
229
|
+
// researchState across calls the way every browser chat dock does. A throw
|
|
230
|
+
// never kills the session — the page has no other chance to show this
|
|
231
|
+
// turn's answer.
|
|
232
|
+
const turnSession = createTurnSession({
|
|
233
|
+
memoryDir, graph, lexicon, vocabHint,
|
|
234
|
+
sessionId: globalThis.crypto?.randomUUID?.() ?? String(Date.now()),
|
|
235
|
+
buildExtraOptions: () => ({
|
|
236
|
+
researchConfig, liveReference: liveReferenceOn, onLiveLookup,
|
|
237
|
+
uiContext: "browser", synthesisBudget: synthesisBudgetOn,
|
|
238
|
+
}),
|
|
239
|
+
captureExtraState: (result) => {
|
|
240
|
+
if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") {
|
|
241
|
+
liveReferenceOn = result.liveReference;
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const chatSessionId = turnSession.sessionId;
|
|
246
|
+
const sessionIds = { chatSessionId, ingestSessionId };
|
|
247
|
+
|
|
233
248
|
return {
|
|
234
249
|
memoryDir,
|
|
235
250
|
chatSessionId,
|
|
@@ -245,28 +260,7 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
|
|
|
245
260
|
* unset keys keep their current value. */
|
|
246
261
|
setResearchConfig(partial) { researchConfig = clampResearchConfig({ ...researchConfig, ...(partial || {}) }); },
|
|
247
262
|
|
|
248
|
-
|
|
249
|
-
* session — the page has no other chance to show this turn's answer. */
|
|
250
|
-
async turn(line) {
|
|
251
|
-
let result;
|
|
252
|
-
try {
|
|
253
|
-
result = await runTurn(line, {
|
|
254
|
-
config: null, source: null, graph, focus, last, memoryDir, sessionId: chatSessionId,
|
|
255
|
-
env: {}, lexicon, vocabHint, planState, researchState, researchConfig,
|
|
256
|
-
liveReference: liveReferenceOn, onLiveLookup,
|
|
257
|
-
uiContext: "browser", synthesisBudget: synthesisBudgetOn,
|
|
258
|
-
});
|
|
259
|
-
} catch (e) {
|
|
260
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
261
|
-
return { answer: `Something went wrong with that (${message}). Try rephrasing.`, end: false, record: null, plan: null, research: undefined };
|
|
262
|
-
}
|
|
263
|
-
focus = result.focus;
|
|
264
|
-
last = result.last;
|
|
265
|
-
if ("planState" in result) planState = result.planState;
|
|
266
|
-
if ("researchState" in result) researchState = result.researchState;
|
|
267
|
-
if (typeof result.liveReference === "boolean" || result.liveReference === "supplement" || result.liveReference === "always") liveReferenceOn = result.liveReference;
|
|
268
|
-
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null, research: result.research };
|
|
269
|
-
},
|
|
263
|
+
turn: (line) => turnSession.turn(line),
|
|
270
264
|
|
|
271
265
|
/** Ingest a document into the SAME store, under the ingest session id so
|
|
272
266
|
* its facts carry the "ingest" source rather than "taught". */
|
|
@@ -333,15 +327,6 @@ export function createResearchSession({ seedPayload = null, vocabSeeded = false,
|
|
|
333
327
|
};
|
|
334
328
|
}
|
|
335
329
|
|
|
336
|
-
/**
|
|
337
|
-
* The session's whole triple store as JSONL — the same
|
|
338
|
-
* { subject, predicate, object, provenance } shape `tmct extract` and
|
|
339
|
-
* `tmct memory --export` emit, offered to the page as the canonical download.
|
|
340
|
-
*/
|
|
341
|
-
export async function exportFactsJsonl(memoryDir) {
|
|
342
|
-
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
343
|
-
}
|
|
344
|
-
|
|
345
330
|
globalThis.tmctResearch = {
|
|
346
331
|
createResearchSession, researchSnapshot, exportFactsJsonl,
|
|
347
332
|
registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, registerResearchProvider,
|
|
@@ -43,14 +43,17 @@
|
|
|
43
43
|
// spider-fly.mjs's own header comment confirms grid movement never reads
|
|
44
44
|
// them back (hand-written pathfinding over has-exit-* facts, not the taught
|
|
45
45
|
// action-rule DSL), so nothing here depends on them being loaded.
|
|
46
|
-
import {
|
|
46
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
47
|
+
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
47
48
|
import {
|
|
48
49
|
createInMemoryStore, normFactTerm, appendFacts, loadMemory, readFactRows,
|
|
49
50
|
} from "../../adapters/memory/core.mjs";
|
|
50
51
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
52
|
+
import { worldRelationGraphPayload } from "../../domain/ask.mjs";
|
|
51
53
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
52
54
|
import {
|
|
53
55
|
worldFactRows, WORLD_NAME, WORLD_OPENING, cellId, parseCellId, DIRECTION_DELTA, visibleCells,
|
|
56
|
+
isLiveRenderableAgent, agentKindOf,
|
|
54
57
|
} from "../../domain/spider-fly-world.mjs";
|
|
55
58
|
import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame, liveWebs, DEFAULT_VISION_RADIUS } from "../../services/spider-fly.mjs";
|
|
56
59
|
import { pillsForSpiderFly, oneStepDirectionBetween } from "../../services/spider-fly-turn.mjs";
|
|
@@ -84,13 +87,27 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
84
87
|
else if (f.predicate === "mgx:mass") initialAgents[f.subject] = { ...initialAgents[f.subject], mass: Number(f.object) };
|
|
85
88
|
}
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
// The graph the chat dock's own ask() traverses. There is no code graph
|
|
91
|
+
// here, so it holds the LIVE BOARD instead: one individual per agent, classed
|
|
92
|
+
// by its id, carrying its current cell, mass and mood. That is what makes
|
|
93
|
+
// "list the locations of flies and spiders" a real ask() capability call
|
|
94
|
+
// rather than another hand-written filter over the same rows. Rebuilt before
|
|
95
|
+
// every chat turn, since every tick moves the pieces.
|
|
96
|
+
let graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
97
|
+
const readBoard = async () => {
|
|
98
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
99
|
+
return { rows, state: foldSpiderFlyState(rows) };
|
|
100
|
+
};
|
|
101
|
+
async function refreshWorldGraph() {
|
|
102
|
+
const { rows, state } = await readBoard();
|
|
103
|
+
graph = parseEntities(worldRelationGraphPayload(rows, {
|
|
104
|
+
classOf: (id) => (isLiveRenderableAgent(id, state) ? agentKindOf(id) : null),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
await refreshWorldGraph();
|
|
88
108
|
const lexicon = loadLexicon();
|
|
89
109
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
90
110
|
|
|
91
|
-
let focus = null;
|
|
92
|
-
let last = null;
|
|
93
|
-
let planState = { spiderFly: { turn: 0 } };
|
|
94
111
|
// The live, in-page-slider-adjustable knobs (mass-loss-rate/spawn-rate/
|
|
95
112
|
// vision-radius per class, and every other spiderFly tunable) — starts at
|
|
96
113
|
// the shipped defaults, mutated only through setConfig() below, and
|
|
@@ -100,6 +117,19 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
100
117
|
// change only changes what happens FROM HERE ON, same as tmct.toml would.
|
|
101
118
|
let config = { ...DEFAULT_GAME_CONFIG.spiderFly };
|
|
102
119
|
|
|
120
|
+
// The chat dock's turn dispatch — createTurnSession owns the focus/last/
|
|
121
|
+
// planState fold and the throw-safe catch fallback every browser entry
|
|
122
|
+
// needs; tick() below reaches into the SAME planState (via setPlanState)
|
|
123
|
+
// so a raw tick and a chat-driven tick can never disagree about the turn
|
|
124
|
+
// count either lane sees next.
|
|
125
|
+
const turnSession = createTurnSession({
|
|
126
|
+
memoryDir, graph, lexicon, sessionId, vocabHint: "",
|
|
127
|
+
// `graph` is re-read here, not captured above: createTurnSession binds its
|
|
128
|
+
// own once at creation, and this board is rebuilt every turn.
|
|
129
|
+
buildExtraOptions: () => ({ graph, gameConfig: { ...DEFAULT_GAME_CONFIG, spiderFly: config } }),
|
|
130
|
+
});
|
|
131
|
+
turnSession.setPlanState({ spiderFly: { turn: 0 } });
|
|
132
|
+
|
|
103
133
|
return {
|
|
104
134
|
memoryDir,
|
|
105
135
|
sessionId,
|
|
@@ -111,30 +141,18 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
111
141
|
* { turn, agents, ecology } shape unmodified. */
|
|
112
142
|
async tick() {
|
|
113
143
|
const result = await runSpiderFlyTick(memoryDir, { config });
|
|
114
|
-
|
|
144
|
+
turnSession.setPlanState({ spiderFly: { turn: result.turn } });
|
|
115
145
|
return result;
|
|
116
146
|
},
|
|
117
147
|
|
|
118
148
|
/** One dispatched chat turn — the SAME runTurn the CLI and the home
|
|
119
|
-
* page's own chat run, over this session's own memoryDir
|
|
120
|
-
*
|
|
121
|
-
*
|
|
149
|
+
* page's own chat run, over this session's own memoryDir, via the
|
|
150
|
+
* shared turn-dispatch wrapper (createTurnSession above) every browser
|
|
151
|
+
* entry now uses. The board graph is rebuilt first, so a question about
|
|
152
|
+
* where the pieces are reads this turn's positions and not last turn's. */
|
|
122
153
|
async turn(line) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
result = await runTurn(line, {
|
|
126
|
-
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
127
|
-
env: {}, lexicon, vocabHint: "", planState,
|
|
128
|
-
gameConfig: { ...DEFAULT_GAME_CONFIG, spiderFly: config },
|
|
129
|
-
});
|
|
130
|
-
} catch (e) {
|
|
131
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
132
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null, plan: null };
|
|
133
|
-
}
|
|
134
|
-
focus = result.focus;
|
|
135
|
-
last = result.last;
|
|
136
|
-
if ("planState" in result) planState = result.planState;
|
|
137
|
-
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null, plan: result.plan ?? null };
|
|
154
|
+
await refreshWorldGraph();
|
|
155
|
+
return turnSession.turn(line);
|
|
138
156
|
},
|
|
139
157
|
|
|
140
158
|
/** A read-only fold of the CURRENT board — no engine advance, no goal
|
|
@@ -143,11 +161,10 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
143
161
|
* chat-driven tick. Web individuals are never listed as agents (that's
|
|
144
162
|
* spider-1/fly-1/... only) — they surface only through activeWebs. */
|
|
145
163
|
async snapshot() {
|
|
146
|
-
const
|
|
147
|
-
const state = foldSpiderFlyState(rows);
|
|
164
|
+
const { state } = await readBoard();
|
|
148
165
|
const agents = {};
|
|
149
166
|
for (const [id, place] of state.placements) {
|
|
150
|
-
if (
|
|
167
|
+
if (!isLiveRenderableAgent(id, state)) continue;
|
|
151
168
|
agents[id] = { cell: place.cell, mass: state.mass.get(id)?.value ?? null };
|
|
152
169
|
}
|
|
153
170
|
return { turn: state.turnCount, agents, activeWebs: liveWebs(state.webs, state.turnCount) };
|
|
@@ -183,5 +200,5 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
183
200
|
globalThis.tmctSpiderFly = {
|
|
184
201
|
createSpiderFlySession, normFactTerm, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
|
|
185
202
|
cellId, parseCellId, DIRECTION_DELTA, visibleCells, DEFAULT_VISION_RADIUS,
|
|
186
|
-
pillsForSpiderFly, oneStepDirectionBetween, DEFAULT_GAME_CONFIG,
|
|
203
|
+
pillsForSpiderFly, oneStepDirectionBetween, DEFAULT_GAME_CONFIG, registerWinkModel,
|
|
187
204
|
};
|
|
@@ -9,16 +9,22 @@
|
|
|
9
9
|
// (src/domain/sprite-facts.mjs's rows). A question the engine can't ground in
|
|
10
10
|
// those rows gets the same refusal the CLI gives — never a guess.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
12
|
+
// Every line the dock takes goes to that session — the page intercepts
|
|
13
|
+
// nothing, so a catalog question is answered by the same membership, count and
|
|
14
|
+
// property lanes chat.mjs runs for any other caller, reading the sprite-facts
|
|
15
|
+
// predicates straight.
|
|
16
|
+
//
|
|
17
|
+
// The scene composer's parser rides along here too: extractSceneItems resolves
|
|
18
|
+
// a typed class name through ask.mjs's resolveObject, so it needs the real
|
|
19
|
+
// resolver in the page rather than a self-contained function the page could
|
|
20
|
+
// splice in as text.
|
|
17
21
|
import { createInMemoryStore, appendFacts, normFactTerm } from "../../adapters/memory/core.mjs";
|
|
22
|
+
import { extractSceneItems } from "../../domain/scene-compose.mjs";
|
|
18
23
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
19
24
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
20
25
|
import { SPRITE_FACTS_PROVENANCE } from "../../domain/sprite-facts.mjs";
|
|
21
26
|
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
27
|
+
import { createTurnSession } from "./turn-session.mjs";
|
|
22
28
|
|
|
23
29
|
/** A live in-memory chat session seeded with the embedded sprite-facts rows.
|
|
24
30
|
* Returns { memoryDir, sessionId, factCount, turn }. */
|
|
@@ -32,32 +38,13 @@ export async function createSpriteCatalogSession({ factRows = [] } = {}) {
|
|
|
32
38
|
const lexicon = loadLexicon();
|
|
33
39
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
34
40
|
|
|
35
|
-
|
|
36
|
-
let last = null;
|
|
41
|
+
const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint: "" });
|
|
37
42
|
|
|
38
43
|
return {
|
|
39
44
|
memoryDir,
|
|
40
45
|
sessionId,
|
|
41
46
|
factCount: factRows.length,
|
|
42
|
-
|
|
43
|
-
/** One dispatched chat turn — the SAME runTurn the CLI runs, over this
|
|
44
|
-
* session's own memoryDir. A throwing runTurn must never kill the
|
|
45
|
-
* session — the page has no other chance to show this turn's answer. */
|
|
46
|
-
async turn(line) {
|
|
47
|
-
let result;
|
|
48
|
-
try {
|
|
49
|
-
result = await runTurn(line, {
|
|
50
|
-
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
51
|
-
env: {}, lexicon, vocabHint: "",
|
|
52
|
-
});
|
|
53
|
-
} catch (e) {
|
|
54
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
55
|
-
return { answer: `Something went wrong answering that (${message}). Try rephrasing, or /help.`, end: false, record: null };
|
|
56
|
-
}
|
|
57
|
-
focus = result.focus;
|
|
58
|
-
last = result.last;
|
|
59
|
-
return { answer: result.answer, end: Boolean(result.end), record: result.record ?? null };
|
|
60
|
-
},
|
|
47
|
+
turn: session.turn,
|
|
61
48
|
};
|
|
62
49
|
}
|
|
63
50
|
|
|
@@ -65,4 +52,4 @@ export async function createSpriteCatalogSession({ factRows = [] } = {}) {
|
|
|
65
52
|
// the self-hosted wink pair (./vendor/wink.js) exactly the way chat.html/
|
|
66
53
|
// ledger.html/plan.html register theirs — the bundle itself never imports
|
|
67
54
|
// wink-nlp (wink-model.mjs's own header explains why).
|
|
68
|
-
globalThis.tmctSprites = { createSpriteCatalogSession, registerWinkModel, normFactTerm };
|
|
55
|
+
globalThis.tmctSprites = { createSpriteCatalogSession, registerWinkModel, normFactTerm, extractSceneItems };
|