@polycode-projects/the-mechanical-code-talker 5.0.5 → 5.0.7

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.
Files changed (46) hide show
  1. package/README.md +78 -19
  2. package/bin/tmct.mjs +63 -2
  3. package/package.json +1 -1
  4. package/src/adapters/memory/core.mjs +23 -0
  5. package/src/domain/ask-vocab.mjs +19 -0
  6. package/src/domain/ask.mjs +10 -5
  7. package/src/domain/codegraph.mjs +23 -9
  8. package/src/domain/game-config.mjs +12 -0
  9. package/src/domain/interpret/strategies/keywords.mjs +30 -1
  10. package/src/domain/memory/capability.mjs +15 -11
  11. package/src/domain/router/drive.mjs +36 -17
  12. package/src/domain/router/resolver.mjs +63 -17
  13. package/src/domain/spider-fly-world.mjs +2 -2
  14. package/src/domain/sprite-templates.mjs +19 -7
  15. package/src/domain/syllogise.mjs +16 -6
  16. package/src/domain/town-square-world.mjs +1 -1
  17. package/src/services/adventure-viz.mjs +5 -2
  18. package/src/services/adventure.mjs +8 -1
  19. package/src/services/chat-page-viz.mjs +123 -25
  20. package/src/services/chat-session.mjs +60 -10
  21. package/src/services/chat.mjs +328 -36
  22. package/src/services/code-explorer-viz.mjs +3 -2
  23. package/src/services/extract-facts.mjs +47 -7
  24. package/src/services/ingest-viz.mjs +113 -29
  25. package/src/services/ledger-viz.mjs +9 -4
  26. package/src/services/memory-panel-viz.mjs +44 -0
  27. package/src/services/mud-viz.mjs +21 -3
  28. package/src/services/mudiii-scene.mjs +407 -36
  29. package/src/services/mudiii-turn.mjs +65 -9
  30. package/src/services/mudiii-viz.mjs +810 -157
  31. package/src/services/p2p-room.mjs +1 -1
  32. package/src/services/plan-viz.mjs +26 -4
  33. package/src/services/predator-prey.mjs +141 -37
  34. package/src/services/research-viz.mjs +17 -23
  35. package/src/services/spider-fly-turn.mjs +7 -1
  36. package/src/services/spider-fly-viz.mjs +13 -5
  37. package/src/services/sprite-catalog-viz.mjs +3 -2
  38. package/src/services/viz-theme.mjs +20 -0
  39. package/src/services/viz-ticker.mjs +15 -2
  40. package/src/surfaces/http/server-http.mjs +90 -13
  41. package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
  42. package/src/surfaces/web/mud-browser-entry.mjs +33 -1
  43. package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
  44. package/src/surfaces/web/tmct-surface.mjs +18 -6
  45. package/src/tools/handlers/tmct-ask.mjs +15 -2
  46. package/src/tools/server.mjs +31 -2
@@ -36,7 +36,7 @@ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
36
36
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
37
37
  import {
38
38
  foldWorldState, worldActionRows, worldDigestRows, roomAffordances,
39
- personKnowledgeLines, personKnownFoodLines,
39
+ personKnowledgeLines, personKnownFoodLines, objectClassChain, recordExamined,
40
40
  diggableDirections, castInRoom, displayNameOf, isOutOfPlay, outOfPlayReasonOf, outOfPlayPhrase,
41
41
  roomKindOf, isMudStatePredicate, worldEpochFact, snapshotSubject,
42
42
  } from "../../services/adventure.mjs";
@@ -52,6 +52,12 @@ import { createTurnSession } from "./turn-session.mjs";
52
52
  import { publishTmctSurface } from "./tmct-surface.mjs";
53
53
  import { graphAsk, enginePlan } from "./engine-surface.mjs";
54
54
 
55
+ // The class chain a room-mate object needs to reach for a starting character
56
+ // to be seeded as already knowing about it — mirrors mud-turn.mjs's own
57
+ // private FOOD_CLASS, kept as this file's own copy for the same reason that
58
+ // file gives: nothing here reaches into another module's private constant.
59
+ const MUD_STARTING_FOOD_CLASS = "food";
60
+
55
61
  /** A live, shared mud world several characters can each act in. `worldPayload`
56
62
  * is `{ name, facts, rules, opening }` — the same shape adventure-browser-
57
63
  * entry.mjs's own worldPayload takes, read once at build time through the
@@ -131,6 +137,32 @@ export async function createMudSession(worldPayload, { characters = [], epoch =
131
137
  return state.placements.get(character)?.object ?? null;
132
138
  }
133
139
 
140
+ // Every character already stands somewhere in the world this store was
141
+ // just seeded with, and food sitting loose in ITS OWN starting room is
142
+ // something it would see just by being there — the room's own "You can:"
143
+ // affordances and the pouch already read straight off this same seeded
144
+ // state, no play button required. Without this, personKnownFoodLines
145
+ // reads pure testimony (the mgx:knows-about facts recordTold/recordExamined
146
+ // write from the scripted tick loop), so "what food do you know about"
147
+ // stayed empty until the sim had ticked at least once, even with a carrot
148
+ // sitting in plain view. One recordExamined per character per food item
149
+ // already in its own room, written once here at open, closes that gap —
150
+ // a character standing three rooms away from every carrot on the board
151
+ // still starts knowing nothing, the honest answer for it.
152
+ {
153
+ const { rows, state } = await readWorld();
154
+ for (const character of characters) {
155
+ const room = state.placements.get(character)?.object ?? null;
156
+ if (!room) continue;
157
+ for (const [thing, place] of state.placements) {
158
+ if (thing === character) continue;
159
+ if (place.predicate !== "mgx:located-in" || place.object !== room) continue;
160
+ if (!objectClassChain(rows, thing).includes(MUD_STARTING_FOOD_CLASS)) continue;
161
+ await recordExamined(memoryDir, { observer: character, thing, k: 0, epoch });
162
+ }
163
+ }
164
+ }
165
+
134
166
  const windows = {};
135
167
  for (const character of characters) {
136
168
  // Deliberately per-closure, never on a shared object: a window's own
@@ -6,14 +6,6 @@
6
6
  // on `{ as: character }` — the addressee is in the sentence, "@fox-1 look"),
7
7
  // and the simulation itself advances as ONE whole-world tick
8
8
  // (`runTownSquareTick`) rather than mud's per-character `autoplayTick`.
9
- //
10
- // `src/services/predator-prey.mjs` — the engine this file drives — does not
11
- // exist in every worktree yet; a concurrent track owns it. The import below
12
- // is guarded (dynamic, try/catch) so this module still loads, and every
13
- // test that imports mudiii-viz.mjs (which never imports this file) is
14
- // unaffected either way. `createMudiiiSession` throws a clear "the engine
15
- // isn't built yet" error if actually called before that track lands, rather
16
- // than a bare ERR_MODULE_NOT_FOUND with no context.
17
9
  import {
18
10
  createInMemoryStore, appendFacts, appendRule, loadMemory, readFactRows, removeFacts,
19
11
  } from "../../adapters/memory/core.mjs";
@@ -22,26 +14,21 @@ import { memoryFactGraphPayload } from "../../domain/memory-facts.mjs";
22
14
  import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
23
15
  import { worldProvenanceTag } from "../../domain/worlds-pack.mjs";
24
16
  import {
25
- COMPASS_POINTS, DEFAULT_FACING, layoutNamed, reverseFacing, stepCellFrom, turnedFacing,
17
+ COMPASS_POINTS, DEFAULT_FACING, cellId, layoutNamed, parseCellId, reverseFacing, stepCellFrom, turnedFacing,
26
18
  } from "../../domain/town-square-world.mjs";
19
+ import { findActionPath } from "../../domain/planning.mjs";
20
+ import { DEFAULT_GAME_CONFIG } from "../../domain/game-config.mjs";
27
21
  import { parseMudEditorText, planMudEditorSync, gridWorldEditorState } from "../../services/mud-editor.mjs";
22
+ import { pillsForMudiii } from "../../services/mudiii-turn.mjs";
23
+ import { relatedForTerm } from "../../domain/skos-view.mjs";
24
+ import { classAncestorChain } from "../../domain/sprite-map.mjs";
28
25
  import { createTurnSession } from "./turn-session.mjs";
29
26
  import { publishTmctSurface } from "./tmct-surface.mjs";
30
27
  import { graphAsk, enginePlan } from "./engine-surface.mjs";
31
-
32
- let engine = null;
33
- async function loadEngine() {
34
- if (engine) return engine;
35
- try {
36
- engine = await import("../../services/predator-prey.mjs");
37
- } catch (err) {
38
- throw new Error(
39
- "mudiii's engine (src/services/predator-prey.mjs) is not built in this worktree yet "
40
- + `(${err && err.message ? err.message : err})`,
41
- );
42
- }
43
- return engine;
44
- }
28
+ import {
29
+ foldTownSquareState, gridApplyActions, pathStateKey, placeFood as engPlaceFood, recastTownSquare,
30
+ roleOfId, runTownSquareTick, startTownSquareGame, townSquareBoard,
31
+ } from "../../services/predator-prey.mjs";
45
32
 
46
33
  /** `count` entries drawn at random from `roster`, in random order, without
47
34
  * repeats — mirrors mud-browser-entry.mjs's own `pickMudRoster`. `random` is
@@ -127,6 +114,29 @@ export function driveRequest(direction, { cell, facing = DEFAULT_FACING } = {})
127
114
  return COMPASS_POINTS.includes(press) ? { facing: press } : null;
128
115
  }
129
116
 
117
+ /** The shortest route from `fromCell` to `toCell` over `factRows`' own exit
118
+ * facts, as `{ cells, directions }` — `cells` runs from one end to the other
119
+ * inclusive, `directions` names one hop each. Null when nothing connects
120
+ * them, so a caller declines visibly rather than drawing a line through a
121
+ * building the board would never let anyone walk through.
122
+ *
123
+ * The exit table is the one legality answer, the same rows the engine's own
124
+ * chase and forage searches read, so a route drawn here is a route the world
125
+ * agrees with. */
126
+ export function routeBetweenCells(factRows, fromCell, toCell) {
127
+ const from = parseCellId(fromCell);
128
+ const to = parseCellId(toCell);
129
+ if (!from || !to) return null;
130
+ const found = findActionPath(
131
+ from,
132
+ (searchState) => searchState.x === to.x && searchState.y === to.y,
133
+ gridApplyActions(factRows),
134
+ { stateKey: pathStateKey },
135
+ );
136
+ if (!found) return null;
137
+ return { cells: found.states.map((s) => cellId(s.x, s.y)), directions: found.actions };
138
+ }
139
+
130
140
  /** A live, shared town-square world one visitor watches and talks over.
131
141
  * `worldPayload` is `{ name, facts, rules, opening }`, read once at build
132
142
  * time the same way every other viz page's world payload is. `agents` is
@@ -137,15 +147,14 @@ export function driveRequest(direction, { cell, facing = DEFAULT_FACING } = {})
137
147
  * (`runTownSquareTick`) and its conversation is a single shared dock.
138
148
  *
139
149
  * Returns `{ memoryDir, codeGraph, graph, refreshGraph, turn, tick, board,
140
- * snapshot, applyEdit, placeFood, driveAgent }`. A reset is not a method here: the
141
- * page re-opens a whole session for it, which is what a reset means when the
142
- * store is in memory and belongs to one visitor. */
143
- export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0 } = {}) {
144
- const {
145
- startTownSquareGame, runTownSquareTick, townSquareBoard, foldTownSquareState,
146
- placeFood: engPlaceFood, roleOfId,
147
- } = await loadEngine();
148
-
150
+ * snapshot, applyEdit, placeFood, driveAgent, recast }`.
151
+ *
152
+ * `getTeachEnabled` is read fresh on every turn, so a visitor can tick the
153
+ * page's teach box mid-session and have the very next line read as a fact to
154
+ * store rather than a command to run. */
155
+ export async function createMudiiiSession(
156
+ worldPayload, { agents = [], epoch = 0, getTeachEnabled = () => false } = {},
157
+ ) {
149
158
  // Every engine call is layout-scoped: the world pack ships the BOARD, and
150
159
  // the layout carries the geometry plus the cast counts the engine mints the
151
160
  // animals from. Resolved once here so no call site has to remember it.
@@ -184,8 +193,11 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
184
193
 
185
194
  const turnSession = createTurnSession({
186
195
  memoryDir, graph: codeGraph, lexicon, sessionId: "town-square",
187
- vocabHint: 'Try "@fox-1 look", or "what does fox-1 believe".',
188
- buildExtraOptions: () => ({ planState: planHolder.state }),
196
+ vocabHint: 'Try "@fox the goblin is east", or "what does the fox see".',
197
+ buildExtraOptions: () => ({
198
+ planState: planHolder.state,
199
+ gameConfig: { ...DEFAULT_GAME_CONFIG, mudiii: { ...DEFAULT_GAME_CONFIG.mudiii, teach: getTeachEnabled() } },
200
+ }),
189
201
  captureExtraState: async (result, state) => {
190
202
  if ("planState" in result) planHolder.state = state.planState;
191
203
  },
@@ -233,6 +245,22 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
233
245
  };
234
246
  }
235
247
 
248
+ /** Re-cast this same store onto a fresh epoch and hand back the opening
249
+ * board. This is what a Reset means once a store is live: the world's own
250
+ * facts, everything taught into it and everything the editor changed all
251
+ * stand, and only the animals are minted again. Re-opening a whole session
252
+ * instead would throw away the taught facts along with the cast, which is
253
+ * not what "reset the board" says.
254
+ *
255
+ * `agents` sizes the new cast the same way `createMudiiiSession`'s own
256
+ * roster does; `epoch` defaults to one past whatever the store is on. */
257
+ async function recast({ agents: nextAgents = [], epoch: nextEpoch = null } = {}) {
258
+ await recastTownSquare(memoryDir, {
259
+ layout, epoch: nextEpoch, ...townSquareRosterArgs(nextAgents, (id) => roleOfId(id)),
260
+ });
261
+ return townSquareBoard(memoryDir, { layout });
262
+ }
263
+
236
264
  /** The board as it stands, in the same payload shape `tick` returns, with no
237
265
  * turn spent. A page opens a session and then draws THIS — otherwise its
238
266
  * first sight of where anything stands is the first tick, and every mesh
@@ -285,6 +313,7 @@ export async function createMudiiiSession(worldPayload, { agents = [], epoch = 0
285
313
  turn: turnSession.turn,
286
314
  tick,
287
315
  driveAgent,
316
+ recast,
288
317
  board,
289
318
  snapshot,
290
319
  applyEdit,
@@ -304,5 +333,12 @@ publishTmctSurface({
304
333
  plan: enginePlan,
305
334
  page: {
306
335
  pickMudiiiRoster,
336
+ // pillsForMudiii closes over two other modules' bindings, so the page
337
+ // reaches it through this bag rather than a `.toString()` splice.
338
+ pillsForMudiii,
339
+ // What the editor's own suggestion rail reads for the word under the
340
+ // cursor: the lateral SKOS neighbourhood and the vertical is-a chain.
341
+ relatedForTerm, classAncestorChain,
342
+ routeBetweenCells,
307
343
  },
308
344
  });
@@ -25,13 +25,25 @@
25
25
  // them apart at a glance, which is exactly what eleven flat bags made
26
26
  // impossible.
27
27
  //
28
- // Two more members show up on some pages, by convention rather than by
28
+ // Three more members show up on some pages, by convention rather than by
29
29
  // anything this function returns: `tmct.ready` is the page's own boot
30
- // promise (chat, ingest), and `tmct.lastSave` is its last background-save
31
- // record, `{at, ms}` (chat, adventure). Neither can be threaded through
32
- // `publishTmctSurface(...)` itselfboot is still running when this
33
- // function returns, so the page's own script sets each once it reaches
34
- // that point (see chat-page-viz.mjs, ingest-viz.mjs, adventure-viz.mjs).
30
+ // promise (chat, ingest), `tmct.lastSave` is its last background-save
31
+ // record, `{at, ms}` (chat, adventure), and `tmct.seed` is what the starter
32
+ // memory is doing `{state, facts}` through "loading", "indexing", "ready",
33
+ // "failed" (with `error`) and "skipped" (chat, ingest, research).
34
+ //
35
+ // `tmct.seed` is deliberately not the same question as `tmct.ready`. Boot
36
+ // finishing and the memory arriving are separate events, and a page whose
37
+ // seed failed still boots, still opens, and still answers — from a smaller
38
+ // store, which is the open-world assumption behaving correctly rather than a
39
+ // degraded mode. What the seed record adds is the ability to SAY so: a pill
40
+ // that reports real load progress, and a test that can tell a failed download
41
+ // apart from a store that is empty on purpose. Both look like zero facts.
42
+ //
43
+ // None can be threaded through `publishTmctSurface(...)` itself — boot is
44
+ // still running when this function returns, so the page's own script sets
45
+ // each once it reaches that point (see chat-page-viz.mjs, ingest-viz.mjs,
46
+ // adventure-viz.mjs).
35
47
  // Each page's older bare global — window.tmctChatReady, tmctIngestReady,
36
48
  // tmctChatLastSave, tmctAdventureLastSave — keeps working unchanged;
37
49
  // `tmct.ready` / `tmct.lastSave` just reach the same value under the one
@@ -1,5 +1,11 @@
1
1
  // tmct_ask — a plain-English structural question answered from the graph in one
2
2
  // mechanical, zero-model-call round-trip. See src/domain/ask.mjs.
3
+ //
4
+ // SYNCHRONOUS, and callers rely on it: the browser's code explorer imports this
5
+ // function directly and reads the envelope off the return, twice per relation
6
+ // kind, to build its sidebar. Anything that has to await — the memory fallback a
7
+ // cold surface supplies — composes AROUND this at dispatch, never inside it.
8
+ // See askWithMemoryFallback in ../server.mjs.
3
9
 
4
10
  import { ask } from "../../domain/ask.mjs";
5
11
  import { requiredArg, toolResult } from "./kit.mjs";
@@ -9,11 +15,18 @@ import { requiredArg, toolResult } from "./kit.mjs";
9
15
  * that still read that string split it on the one constant rather than their own copy. */
10
16
  export const ASK_ENVELOPE_DELIM = "\n\n---tmct_ask---\n";
11
17
 
12
- export function tmct_ask(args, { graph }) {
13
- const { content, tmct_ask: envelope } = ask(graph, requiredArg(args, "query"));
18
+ /** One answer + envelope as a tool result. Shared with the memory fallback, so
19
+ * both spellings of an answered ask carry the envelope in the same three
20
+ * places (prose, structured data, and the in-band string). */
21
+ export function askToolResult(content, envelope) {
14
22
  return toolResult({
15
23
  content,
16
24
  data: envelope,
17
25
  text: `${content}${ASK_ENVELOPE_DELIM}${JSON.stringify(envelope, null, 2)}`,
18
26
  });
19
27
  }
28
+
29
+ export function tmct_ask(args, { graph }) {
30
+ const { content, tmct_ask: envelope } = ask(graph, requiredArg(args, "query"));
31
+ return askToolResult(content, envelope);
32
+ }
@@ -23,6 +23,7 @@ import { ask } from "../domain/ask.mjs";
23
23
  import { createGraphService } from "../adapters/providers/graph-service.mjs";
24
24
  import { loadGraph } from "./graph-load.mjs";
25
25
  import { HANDLERS } from "./handlers/index.mjs";
26
+ import { askToolResult } from "./handlers/tmct-ask.mjs";
26
27
  import { isToolResult } from "./handlers/kit.mjs";
27
28
  import { setDefaultNlpAdapter } from "../domain/interpret/nlp-registry.mjs";
28
29
  import { setConstructionBanks } from "../domain/interpret/strategies/constructions.mjs";
@@ -50,7 +51,7 @@ export const TOOLS = HOT_TOOLS.map(({ name, agentDescription, inputSchema }) =>
50
51
  inputSchema,
51
52
  }));
52
53
 
53
- async function runHandler(name, args, { config, source = defaultSource, tel = null, ingest = null, memoryBackend = null, graph: suppliedGraph = null } = {}) {
54
+ async function runHandler(name, args, { config, source = defaultSource, tel = null, ingest = null, memoryBackend = null, factLookup = null, graph: suppliedGraph = null } = {}) {
54
55
  // Reject an unknown tool BEFORE touching the graph — an unknown name never
55
56
  // triggers a load. hasOwn, so an inherited name ("constructor", "toString")
56
57
  // is unknown rather than a callable found on the prototype chain.
@@ -78,7 +79,35 @@ async function runHandler(name, args, { config, source = defaultSource, tel = nu
78
79
  // isn't there.
79
80
  const repoRoot = config?.graphFile ? dirname(dirname(config.graphFile)) : null;
80
81
  const svc = createGraphService(graph, { sourceAccess: Boolean(repoRoot), repoRoot, readFile, tel, ask });
81
- return handle(args, { graph, svc, config, repoRoot, memoryBackend });
82
+ const out = handle(args, { graph, svc, config, repoRoot, memoryBackend });
83
+ return name === "tmct_ask" ? askWithMemoryFallback(out, args, factLookup) : out;
84
+ }
85
+
86
+ /** Offer a graph miss to the caller's own memory reader before the miss stands.
87
+ * `factLookup` is a seam of the same kind as `ingest`: the conversational
88
+ * store's vocabulary reader lives in the SERVICE layer, which this layer sits
89
+ * under and must not import, so a cold caller holding a repo's memory hands it
90
+ * in. A caller that supplies none keeps the graph-only answer, byte for byte.
91
+ *
92
+ * Composed HERE rather than inside the handler because the handler is a
93
+ * synchronous entry the browser's code explorer calls directly — an `async`
94
+ * handler hands that caller a promise whose `.data` is undefined, and its
95
+ * sidebar silently draws nothing. Dispatch already awaits, so the awaiting
96
+ * belongs here. */
97
+ async function askWithMemoryFallback(out, args, factLookup) {
98
+ const envelope = out?.data;
99
+ if (!envelope?.miss || typeof factLookup !== "function") return out;
100
+ let fromMemory = null;
101
+ try { fromMemory = await factLookup(String(args?.query ?? ""), envelope); } catch { fromMemory = null; }
102
+ if (!fromMemory?.text) return out;
103
+ // A reader hit flagged `miss` is the same refusal in better words, so the
104
+ // envelope keeps its miss and only the prose improves.
105
+ const content = fromMemory.replace ? fromMemory.text : `${out.content}\n${fromMemory.text}`;
106
+ return askToolResult(content, {
107
+ ...envelope,
108
+ miss: Boolean(fromMemory.miss),
109
+ matchedVia: fromMemory.miss ? envelope.matchedVia : "memory",
110
+ });
82
111
  }
83
112
 
84
113
  /** The caller-facing string for one tool call. A handler that returns a structured