@polycode-projects/the-mechanical-code-talker 4.0.1 → 4.1.1
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 -1
- package/corpus/sprites/src/sprite-facts.jsonl +375 -8
- package/package.json +1 -1
- 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/mud-facts.mjs +15 -0
- 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 +36 -0
- package/src/domain/sprite-facts.mjs +0 -0
- package/src/domain/sprite-request.mjs +156 -0
- package/src/domain/sprite-templates.mjs +161 -14
- package/src/services/adventure-editor.mjs +8 -14
- package/src/services/adventure-viz.mjs +119 -150
- package/src/services/adventure.mjs +97 -35
- package/src/services/chat-page-viz.mjs +64 -48
- package/src/services/chat.mjs +102 -34
- package/src/services/code-explorer-viz.mjs +52 -50
- package/src/services/ingest-viz.mjs +32 -74
- package/src/services/ledger-viz.mjs +87 -70
- package/src/services/memory-panel-viz.mjs +38 -0
- package/src/services/mud-editor.mjs +10 -15
- package/src/services/mud-turn.mjs +6 -6
- package/src/services/mud-viz.mjs +119 -225
- package/src/services/p2p-room.mjs +90 -23
- package/src/services/plan-pddl.mjs +3 -1
- package/src/services/plan-viz.mjs +13 -12
- package/src/services/research-viz.mjs +25 -67
- package/src/services/spider-fly-turn.mjs +14 -22
- package/src/services/spider-fly-viz.mjs +97 -136
- package/src/services/spider-fly.mjs +69 -11
- package/src/services/sprite-catalog-viz.mjs +274 -224
- package/src/services/viz-boot.mjs +71 -0
- package/src/services/viz-room-graph.mjs +203 -0
- package/src/services/viz-theme.mjs +75 -1
- package/src/services/viz-ticker.mjs +22 -0
- package/src/surfaces/web/adventure-browser-entry.mjs +62 -47
- package/src/surfaces/web/chat-browser-entry.mjs +51 -107
- package/src/surfaces/web/code-explorer-browser-entry.mjs +192 -35
- package/src/surfaces/web/engine-surface.mjs +82 -0
- package/src/surfaces/web/ingest-browser-entry.mjs +16 -17
- package/src/surfaces/web/ledger-browser-entry.mjs +24 -56
- package/src/surfaces/web/memory-ask-browser-entry.mjs +55 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +128 -125
- package/src/surfaces/web/memory-stats.mjs +11 -0
- package/src/surfaces/web/mud-browser-entry.mjs +70 -49
- package/src/surfaces/web/plan-browser-entry.mjs +39 -50
- package/src/surfaces/web/research-browser-entry.mjs +48 -46
- package/src/surfaces/web/spider-fly-browser-entry.mjs +76 -40
- package/src/surfaces/web/sprites-browser-entry.mjs +28 -32
- package/src/surfaces/web/tmct-surface.mjs +147 -0
- package/src/surfaces/web/turn-session.mjs +124 -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 +37 -6
|
@@ -43,14 +43,19 @@
|
|
|
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 { publishTmctSurface } from "./tmct-surface.mjs";
|
|
48
|
+
import { graphAsk, enginePlan } from "./engine-surface.mjs";
|
|
49
|
+
import { registerWinkModel } from "../../adapters/wink-model.mjs";
|
|
47
50
|
import {
|
|
48
51
|
createInMemoryStore, normFactTerm, appendFacts, loadMemory, readFactRows,
|
|
49
52
|
} from "../../adapters/memory/core.mjs";
|
|
50
53
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
54
|
+
import { worldRelationGraphPayload } from "../../domain/ask.mjs";
|
|
51
55
|
import { loadLexicon } from "../../domain/grammar/lexicon.mjs";
|
|
52
56
|
import {
|
|
53
57
|
worldFactRows, WORLD_NAME, WORLD_OPENING, cellId, parseCellId, DIRECTION_DELTA, visibleCells,
|
|
58
|
+
isLiveRenderableAgent, agentKindOf,
|
|
54
59
|
} from "../../domain/spider-fly-world.mjs";
|
|
55
60
|
import { foldSpiderFlyState, runSpiderFlyTick, startSpiderFlyGame, liveWebs, DEFAULT_VISION_RADIUS } from "../../services/spider-fly.mjs";
|
|
56
61
|
import { pillsForSpiderFly, oneStepDirectionBetween } from "../../services/spider-fly-turn.mjs";
|
|
@@ -84,13 +89,27 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
84
89
|
else if (f.predicate === "mgx:mass") initialAgents[f.subject] = { ...initialAgents[f.subject], mass: Number(f.object) };
|
|
85
90
|
}
|
|
86
91
|
|
|
87
|
-
|
|
92
|
+
// The graph the chat dock's own ask() traverses. There is no code graph
|
|
93
|
+
// here, so it holds the LIVE BOARD instead: one individual per agent, classed
|
|
94
|
+
// by its id, carrying its current cell, mass and mood. That is what makes
|
|
95
|
+
// "list the locations of flies and spiders" a real ask() capability call
|
|
96
|
+
// rather than another hand-written filter over the same rows. Rebuilt before
|
|
97
|
+
// every chat turn, since every tick moves the pieces.
|
|
98
|
+
let graph = parseEntities({ individuals: [], objectProperties: [] });
|
|
99
|
+
const readBoard = async () => {
|
|
100
|
+
const rows = readFactRows(await loadMemory(memoryDir));
|
|
101
|
+
return { rows, state: foldSpiderFlyState(rows) };
|
|
102
|
+
};
|
|
103
|
+
async function refreshWorldGraph() {
|
|
104
|
+
const { rows, state } = await readBoard();
|
|
105
|
+
graph = parseEntities(worldRelationGraphPayload(rows, {
|
|
106
|
+
classOf: (id) => (isLiveRenderableAgent(id, state) ? agentKindOf(id) : null),
|
|
107
|
+
}));
|
|
108
|
+
}
|
|
109
|
+
await refreshWorldGraph();
|
|
88
110
|
const lexicon = loadLexicon();
|
|
89
111
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
90
112
|
|
|
91
|
-
let focus = null;
|
|
92
|
-
let last = null;
|
|
93
|
-
let planState = { spiderFly: { turn: 0 } };
|
|
94
113
|
// The live, in-page-slider-adjustable knobs (mass-loss-rate/spawn-rate/
|
|
95
114
|
// vision-radius per class, and every other spiderFly tunable) — starts at
|
|
96
115
|
// the shipped defaults, mutated only through setConfig() below, and
|
|
@@ -100,6 +119,19 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
100
119
|
// change only changes what happens FROM HERE ON, same as tmct.toml would.
|
|
101
120
|
let config = { ...DEFAULT_GAME_CONFIG.spiderFly };
|
|
102
121
|
|
|
122
|
+
// The chat dock's turn dispatch — createTurnSession owns the focus/last/
|
|
123
|
+
// planState fold and the throw-safe catch fallback every browser entry
|
|
124
|
+
// needs; tick() below reaches into the SAME planState (via setPlanState)
|
|
125
|
+
// so a raw tick and a chat-driven tick can never disagree about the turn
|
|
126
|
+
// count either lane sees next.
|
|
127
|
+
const turnSession = createTurnSession({
|
|
128
|
+
memoryDir, graph, lexicon, sessionId, vocabHint: "",
|
|
129
|
+
// `graph` is re-read here, not captured above: createTurnSession binds its
|
|
130
|
+
// own once at creation, and this board is rebuilt every turn.
|
|
131
|
+
buildExtraOptions: () => ({ graph, gameConfig: { ...DEFAULT_GAME_CONFIG, spiderFly: config } }),
|
|
132
|
+
});
|
|
133
|
+
turnSession.setPlanState({ spiderFly: { turn: 0 } });
|
|
134
|
+
|
|
103
135
|
return {
|
|
104
136
|
memoryDir,
|
|
105
137
|
sessionId,
|
|
@@ -107,34 +139,32 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
107
139
|
initial: { turn: 0, agents: initialAgents, activeWebs: [] },
|
|
108
140
|
taxonomyRows,
|
|
109
141
|
|
|
142
|
+
/** The board as a graph, as of the last refresh. Every tick moves the
|
|
143
|
+
* pieces, so a caller putting a question to it calls `refreshGraph()`
|
|
144
|
+
* first — which is exactly what `turn()` below already does. */
|
|
145
|
+
get graph() { return graph; },
|
|
146
|
+
|
|
147
|
+
/** Rebuild the board graph from the store's current rows. Exposed so a
|
|
148
|
+
* question asked outside the chat dock (tmct.ask) reads this turn's
|
|
149
|
+
* positions and not last turn's, the same way a typed one does. */
|
|
150
|
+
refreshGraph: refreshWorldGraph,
|
|
151
|
+
|
|
110
152
|
/** Run one real engine turn directly. Returns spider-fly.mjs's own
|
|
111
153
|
* { turn, agents, ecology } shape unmodified. */
|
|
112
154
|
async tick() {
|
|
113
155
|
const result = await runSpiderFlyTick(memoryDir, { config });
|
|
114
|
-
|
|
156
|
+
turnSession.setPlanState({ spiderFly: { turn: result.turn } });
|
|
115
157
|
return result;
|
|
116
158
|
},
|
|
117
159
|
|
|
118
160
|
/** 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
|
-
*
|
|
161
|
+
* page's own chat run, over this session's own memoryDir, via the
|
|
162
|
+
* shared turn-dispatch wrapper (createTurnSession above) every browser
|
|
163
|
+
* entry now uses. The board graph is rebuilt first, so a question about
|
|
164
|
+
* where the pieces are reads this turn's positions and not last turn's. */
|
|
122
165
|
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 };
|
|
166
|
+
await refreshWorldGraph();
|
|
167
|
+
return turnSession.turn(line);
|
|
138
168
|
},
|
|
139
169
|
|
|
140
170
|
/** A read-only fold of the CURRENT board — no engine advance, no goal
|
|
@@ -143,11 +173,10 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
143
173
|
* chat-driven tick. Web individuals are never listed as agents (that's
|
|
144
174
|
* spider-1/fly-1/... only) — they surface only through activeWebs. */
|
|
145
175
|
async snapshot() {
|
|
146
|
-
const
|
|
147
|
-
const state = foldSpiderFlyState(rows);
|
|
176
|
+
const { state } = await readBoard();
|
|
148
177
|
const agents = {};
|
|
149
178
|
for (const [id, place] of state.placements) {
|
|
150
|
-
if (
|
|
179
|
+
if (!isLiveRenderableAgent(id, state)) continue;
|
|
151
180
|
agents[id] = { cell: place.cell, mass: state.mass.get(id)?.value ?? null };
|
|
152
181
|
}
|
|
153
182
|
return { turn: state.turnCount, agents, activeWebs: liveWebs(state.webs, state.turnCount) };
|
|
@@ -172,16 +201,23 @@ export async function createSpiderFlySession({ flyCount = 1 } = {}) {
|
|
|
172
201
|
};
|
|
173
202
|
}
|
|
174
203
|
|
|
175
|
-
//
|
|
176
|
-
//
|
|
177
|
-
//
|
|
178
|
-
//
|
|
179
|
-
//
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
//
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
}
|
|
204
|
+
// `tmct.page` is grid geometry and sprite resolution — the two things on this
|
|
205
|
+
// page the engine has no plain-English form for. Reconstructing a spider's
|
|
206
|
+
// remaining silk-thread path from its returned direction list and masking the
|
|
207
|
+
// POV overlay's visible cells both need the raw cell math; the deception pills
|
|
208
|
+
// need spider-fly-turn.mjs's own pill logic.
|
|
209
|
+
publishTmctSurface({
|
|
210
|
+
open: createSpiderFlySession,
|
|
211
|
+
// The board moves every tick, so a question rebuilds the graph first —
|
|
212
|
+
// the same refresh a typed turn does before it dispatches.
|
|
213
|
+
ask: async (request, options, session) => {
|
|
214
|
+
await session.refreshGraph();
|
|
215
|
+
return graphAsk(request, options, session);
|
|
216
|
+
},
|
|
217
|
+
plan: enginePlan,
|
|
218
|
+
page: {
|
|
219
|
+
normFactTerm, resolveSpriteForClass, SPRITE_REGISTRY, resolveSpriteAsset,
|
|
220
|
+
cellId, parseCellId, DIRECTION_DELTA, visibleCells, DEFAULT_VISION_RADIUS,
|
|
221
|
+
pillsForSpiderFly, oneStepDirectionBetween, DEFAULT_GAME_CONFIG, registerWinkModel,
|
|
222
|
+
},
|
|
223
|
+
});
|
|
@@ -9,19 +9,27 @@
|
|
|
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";
|
|
28
|
+
import { publishTmctSurface } from "./tmct-surface.mjs";
|
|
29
|
+
import { graphAsk, enginePlan } from "./engine-surface.mjs";
|
|
22
30
|
|
|
23
31
|
/** A live in-memory chat session seeded with the embedded sprite-facts rows.
|
|
24
|
-
* Returns { memoryDir, sessionId, factCount, turn }. */
|
|
32
|
+
* Returns { memoryDir, sessionId, graph, factCount, turn }. */
|
|
25
33
|
export async function createSpriteCatalogSession({ factRows = [] } = {}) {
|
|
26
34
|
const memoryDir = createInMemoryStore();
|
|
27
35
|
await appendFacts(memoryDir, factRows.map((f) => ({
|
|
@@ -32,37 +40,25 @@ export async function createSpriteCatalogSession({ factRows = [] } = {}) {
|
|
|
32
40
|
const lexicon = loadLexicon();
|
|
33
41
|
const sessionId = globalThis.crypto?.randomUUID?.() ?? String(Date.now());
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
let last = null;
|
|
43
|
+
const session = createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint: "" });
|
|
37
44
|
|
|
38
45
|
return {
|
|
39
46
|
memoryDir,
|
|
40
47
|
sessionId,
|
|
48
|
+
graph,
|
|
41
49
|
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
|
-
},
|
|
50
|
+
turn: session.turn,
|
|
61
51
|
};
|
|
62
52
|
}
|
|
63
53
|
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
54
|
+
// `tmct.page` keeps the wink seam (the page hands in the self-hosted pair from
|
|
55
|
+
// ./vendor/wink.js, exactly the way chat.html/ledger.html/plan.html register
|
|
56
|
+
// theirs — the bundle itself never imports wink-nlp), the term normalizer, and
|
|
57
|
+
// the scene composer's parser, which reads a typed line into drawable items
|
|
58
|
+
// rather than answering anything.
|
|
59
|
+
publishTmctSurface({
|
|
60
|
+
open: createSpriteCatalogSession,
|
|
61
|
+
ask: graphAsk,
|
|
62
|
+
plan: enginePlan,
|
|
63
|
+
page: { registerWinkModel, normFactTerm, extractSceneItems },
|
|
64
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
// tmct-surface.mjs — the one contract every demo page has with the engine.
|
|
2
|
+
//
|
|
3
|
+
// Each browser entry used to publish its own bag of primitives under its own
|
|
4
|
+
// name (tmctChat, tmctMud, tmctLedger, tmctSpiderFly, ...), so a visitor
|
|
5
|
+
// reading one page's source met a different wall of raw functions on every
|
|
6
|
+
// page, wired together by hand. mud-browser-entry.mjs's own header stated the
|
|
7
|
+
// reason for its 25: "so mud-viz.mjs's own inlined script never duplicates
|
|
8
|
+
// sprite resolution or the digest/affordance/knowledge readers". Right
|
|
9
|
+
// instinct, wrong unit — the page still had to know 25 names to say one
|
|
10
|
+
// sentence.
|
|
11
|
+
//
|
|
12
|
+
// This publishes ONE `globalThis.tmct` instead, and every page reaches the
|
|
13
|
+
// engine the same way:
|
|
14
|
+
//
|
|
15
|
+
// await tmct.open(...) open this page's session
|
|
16
|
+
// tmct.session the live session, once opened
|
|
17
|
+
// await tmct.turn(line) one conversational turn, the dock's entry point
|
|
18
|
+
// await tmct.ask(request) one grounded question, prose plus its evidence
|
|
19
|
+
// await tmct.plan(request) one compound request, planned over what's loaded
|
|
20
|
+
//
|
|
21
|
+
// Whatever a page still needs that has NO plain-English form lives on
|
|
22
|
+
// `tmct.page` — canvas geometry, sprite templates, a wink model to register, a
|
|
23
|
+
// digest structure table. The split is the point: `tmct.ask(...)` is tmct
|
|
24
|
+
// answering, `tmct.page.cellId(...)` is this page drawing. A reader can tell
|
|
25
|
+
// them apart at a glance, which is exactly what eleven flat bags made
|
|
26
|
+
// impossible.
|
|
27
|
+
//
|
|
28
|
+
// `ask` and `plan` are supplied per page rather than fixed here, because what
|
|
29
|
+
// a page can ground a question against genuinely differs: code-explorer holds
|
|
30
|
+
// a code graph, spider-fly holds a live board projected into one, the ledger
|
|
31
|
+
// dock holds a memory store and no graph at all. A page that supplies neither
|
|
32
|
+
// gets an honest refusal naming why, never a guess — the same promise the
|
|
33
|
+
// engine itself makes.
|
|
34
|
+
|
|
35
|
+
const NO_SESSION = "no session is open on this page yet — await tmct.open() first";
|
|
36
|
+
|
|
37
|
+
// The demo ledger page carries TWO bundles at once: the committed
|
|
38
|
+
// question-only one inlined into the HTML (memory-ask-browser-entry.mjs) and
|
|
39
|
+
// the demo site's own full turn engine as a separate script tag
|
|
40
|
+
// (ledger-browser-entry.mjs). Both publish this surface, and which one the
|
|
41
|
+
// browser runs first is not something either can promise. So the
|
|
42
|
+
// question-only one publishes as a `fallback` and the full engine always
|
|
43
|
+
// wins, whichever order they load in. Registered globally rather than
|
|
44
|
+
// module-locally because the two bundles carry their own copy of this module.
|
|
45
|
+
const FALLBACK = Symbol.for("tmct.surface.fallback");
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* `publishTmctSurface({ open, ask, plan, turn, page, target })` installs
|
|
49
|
+
* `target.tmct` (default `globalThis`) and returns it.
|
|
50
|
+
*
|
|
51
|
+
* `open(...args)` is the page's own session factory and is the only required
|
|
52
|
+
* argument — `createChatSession`, `createMudSession`, `createPlanSession` and
|
|
53
|
+
* the rest, called with whatever arguments they already take. Its result
|
|
54
|
+
* becomes `tmct.session`.
|
|
55
|
+
*
|
|
56
|
+
* `turn(line, options, session)` (optional) overrides how a line reaches the
|
|
57
|
+
* session. The default calls `session.turn(line, options)`, which is every
|
|
58
|
+
* page but mud — mud runs several characters over one world, so it routes on
|
|
59
|
+
* `{ as: character }` to that character's own window.
|
|
60
|
+
*
|
|
61
|
+
* `ask(request, options, session)` and `plan(request, options, session)`
|
|
62
|
+
* (optional) are the page's grounded-question and capability-planner routes.
|
|
63
|
+
* Left out, the matching verb refuses and says which route the page does have.
|
|
64
|
+
* Every verb takes the same `(what, options)` shape, so the research page's
|
|
65
|
+
* source-scoped `tmct.ask(q, { sources })` reads like the plan page's
|
|
66
|
+
* `tmct.turn("solve it", { maxDepth })`.
|
|
67
|
+
*
|
|
68
|
+
* `page` is the residual bag: this page's own rendering helpers, reachable as
|
|
69
|
+
* `tmct.page.*`.
|
|
70
|
+
*
|
|
71
|
+
* `fallback: true` publishes only where no fuller surface is standing, and
|
|
72
|
+
* yields to one that arrives later — the ledger page's two bundles.
|
|
73
|
+
*/
|
|
74
|
+
export function publishTmctSurface({
|
|
75
|
+
open, ask = null, plan = null, turn = null, page = {}, fallback = false, target = globalThis,
|
|
76
|
+
} = {}) {
|
|
77
|
+
if (typeof open !== "function") {
|
|
78
|
+
throw new Error("publishTmctSurface: `open` must be this page's session factory");
|
|
79
|
+
}
|
|
80
|
+
const standing = target.tmct;
|
|
81
|
+
if (fallback && standing && !standing[FALLBACK]) return standing;
|
|
82
|
+
let session = null;
|
|
83
|
+
|
|
84
|
+
const liveSession = () => {
|
|
85
|
+
if (!session) throw new Error(NO_SESSION);
|
|
86
|
+
return session;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const surface = {
|
|
90
|
+
// Variadic, so each page's factory keeps the signature it already has —
|
|
91
|
+
// adventure and mud both take the world payload first, then their options.
|
|
92
|
+
async open(...args) {
|
|
93
|
+
session = await open(...args);
|
|
94
|
+
return session;
|
|
95
|
+
},
|
|
96
|
+
|
|
97
|
+
get session() { return session; },
|
|
98
|
+
|
|
99
|
+
async turn(line, options) {
|
|
100
|
+
const live = liveSession();
|
|
101
|
+
if (turn) return turn(line, options, live);
|
|
102
|
+
// Not every page holds a conversation. The ingest page grounds pasted
|
|
103
|
+
// prose into facts and answers nothing, so it says so rather than
|
|
104
|
+
// failing on a method it never had.
|
|
105
|
+
if (typeof live.turn !== "function") {
|
|
106
|
+
return {
|
|
107
|
+
answer: "this page runs no conversational turn — see tmct.session for what it does run",
|
|
108
|
+
end: false, record: null, plan: null, research: null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return live.turn(line, options);
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
async ask(request, options) {
|
|
115
|
+
if (!ask) {
|
|
116
|
+
return {
|
|
117
|
+
answer: "this page has no separate ask route — it answers questions through tmct.turn()",
|
|
118
|
+
data: undefined,
|
|
119
|
+
miss: true,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
return ask(request, options, liveSession());
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
async plan(request, options) {
|
|
126
|
+
if (!plan) {
|
|
127
|
+
return {
|
|
128
|
+
refused: true,
|
|
129
|
+
why: "this page's bundle carries no capability planner",
|
|
130
|
+
driver: null, calls: [], composed: null, observed: null,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
return plan(request, options, liveSession());
|
|
134
|
+
},
|
|
135
|
+
|
|
136
|
+
page,
|
|
137
|
+
|
|
138
|
+
/** True where this is the question-only stand-in rather than a page's full
|
|
139
|
+
* engine. The ledger page reads it to tell its two docks apart: the live
|
|
140
|
+
* one teaches, the stand-in only answers. */
|
|
141
|
+
fallback,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
surface[FALLBACK] = fallback;
|
|
145
|
+
target.tmct = surface;
|
|
146
|
+
return surface;
|
|
147
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// turn-session.mjs — the turn-dispatch wrapper nine near-identical browser
|
|
2
|
+
// sessions each hand-rolled (chat-browser-entry.mjs, code-explorer-browser-
|
|
3
|
+
// entry.mjs, ledger-browser-entry.mjs, adventure-browser-entry.mjs, spider-
|
|
4
|
+
// fly-browser-entry.mjs, plan-browser-entry.mjs, research-browser-entry.mjs,
|
|
5
|
+
// mud-browser-entry.mjs, sprites-browser-entry.mjs): a closure over
|
|
6
|
+
// focus/last/planState/researchState, a `try { runTurn(...) } catch` that
|
|
7
|
+
// turns a throw into a safe answer instead of ending the session (the page
|
|
8
|
+
// has no other chance to show that turn's reply), the post-call state
|
|
9
|
+
// capture, and a return shape covering every field one caller or another
|
|
10
|
+
// reads back.
|
|
11
|
+
//
|
|
12
|
+
// The nine differ in what they hand `runTurn` beyond the common core
|
|
13
|
+
// (`uiContext`, `synthesisBudget`, `gameConfig`, `researchConfig`,
|
|
14
|
+
// `actingSubject`, a per-character `sessionId`...) and in what they do with a
|
|
15
|
+
// turn's result besides the standard focus/last/planState/researchState fold
|
|
16
|
+
// (sync an externally-held plan holder, grow a visited-rooms set, bump a
|
|
17
|
+
// per-character turn tally). `buildExtraOptions`/`captureExtraState` below
|
|
18
|
+
// are exactly those two seams — everything else is common enough to live
|
|
19
|
+
// here once.
|
|
20
|
+
//
|
|
21
|
+
// The catch fallback text converges on the FULLEST of the nine: several
|
|
22
|
+
// callers had already drifted to a shorter one (mud-browser-entry.mjs drops
|
|
23
|
+
// "or /help"; research-browser-entry.mjs drops "or /help" AND "answering
|
|
24
|
+
// that") — proof, not just a risk, that duplicating this string invites
|
|
25
|
+
// exactly this drift.
|
|
26
|
+
import { runTurn as defaultRunTurn } from "../../services/chat.mjs";
|
|
27
|
+
|
|
28
|
+
function turnErrorFallback(message) {
|
|
29
|
+
return `Something went wrong answering that (${message}). Try rephrasing, or /help.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* `createTurnSession({ memoryDir, graph, lexicon, sessionId, vocabHint,
|
|
34
|
+
* buildExtraOptions, captureExtraState })` — a live turn-dispatch session
|
|
35
|
+
* over the real engine (`runTurn`), threading focus/last/planState/
|
|
36
|
+
* researchState across calls the way every browser chat dock does.
|
|
37
|
+
*
|
|
38
|
+
* `vocabHint` is the session-lifetime hint every caller computes once at
|
|
39
|
+
* creation (a fixed string, or `vocabExampleHint(vocabSeeded)`'s result) —
|
|
40
|
+
* never recomputed per turn.
|
|
41
|
+
*
|
|
42
|
+
* `buildExtraOptions(state, callArgs)` (optional) returns extra `runTurn`
|
|
43
|
+
* options to merge OVER the defaults below — anything a specific page needs
|
|
44
|
+
* (`uiContext: "browser"`, `synthesisBudget`, `gameConfig`, `researchConfig`,
|
|
45
|
+
* a per-character `actingSubject`/`sessionId`, or a `planState` read from an
|
|
46
|
+
* external holder instead of this closure's own). `state` is
|
|
47
|
+
* `{ focus, last, planState, researchState }` as this turn is about to run;
|
|
48
|
+
* `callArgs` is whatever extra argument `turn(line, callArgs)` was given
|
|
49
|
+
* (plan-browser-entry.mjs's own per-call `{ maxDepth }` override, for one).
|
|
50
|
+
*
|
|
51
|
+
* `captureExtraState(result, state, callArgs)` (optional, may be async) runs
|
|
52
|
+
* once after a SUCCESSFUL turn, with `state` already folded forward
|
|
53
|
+
* (focus/last/planState/researchState updated) — the caller's own chance to
|
|
54
|
+
* do further bookkeeping (sync an external plan holder, reload memory to
|
|
55
|
+
* grow a visited-rooms set, bump a turn tally). Never runs on a throw: none
|
|
56
|
+
* of the nine originals touch their own state when `runTurn` itself failed.
|
|
57
|
+
*
|
|
58
|
+
* Returns `{ memoryDir, sessionId, graph, turn, focus, last, planState,
|
|
59
|
+
* setPlanState, researchState, setResearchState }`. `graph` is the one this
|
|
60
|
+
* session was built over, exposed so a caller can put a question to the same
|
|
61
|
+
* graph its turns run against (engine-surface.mjs's `graphAsk`) without
|
|
62
|
+
* holding a second reference to it. `turn(line, callArgs)`
|
|
63
|
+
* resolves to `{ answer, end, record, plan, research }` — the union of every
|
|
64
|
+
* field any of the nine originals reads; a caller ignores what it doesn't
|
|
65
|
+
* need. `research` is passed through undefined/null/a queue snapshot exactly
|
|
66
|
+
* as `runTurn` returned it (a caller may distinguish "not a research turn"
|
|
67
|
+
* from "a research turn that just ended").
|
|
68
|
+
*
|
|
69
|
+
* `runTurn` (default: the real engine) is injectable, purely so a test can
|
|
70
|
+
* force the catch path deterministically without needing an input that
|
|
71
|
+
* happens to make the real, deliberately crash-resistant engine throw.
|
|
72
|
+
*/
|
|
73
|
+
export function createTurnSession({
|
|
74
|
+
memoryDir, graph, lexicon, sessionId, vocabHint = "",
|
|
75
|
+
buildExtraOptions = () => ({}),
|
|
76
|
+
captureExtraState = async () => {},
|
|
77
|
+
runTurn = defaultRunTurn,
|
|
78
|
+
} = {}) {
|
|
79
|
+
let focus = null;
|
|
80
|
+
let last = null;
|
|
81
|
+
let planState = null;
|
|
82
|
+
let researchState = null;
|
|
83
|
+
|
|
84
|
+
async function turn(line, callArgs) {
|
|
85
|
+
const before = { focus, last, planState, researchState };
|
|
86
|
+
const extra = buildExtraOptions(before, callArgs) || {};
|
|
87
|
+
let result;
|
|
88
|
+
try {
|
|
89
|
+
result = await runTurn(line, {
|
|
90
|
+
config: null, source: null, graph, focus, last, memoryDir, sessionId,
|
|
91
|
+
env: {}, lexicon, vocabHint, planState, researchState,
|
|
92
|
+
...extra,
|
|
93
|
+
});
|
|
94
|
+
} catch (e) {
|
|
95
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
96
|
+
return { answer: turnErrorFallback(message), end: false, record: null, plan: null, research: null };
|
|
97
|
+
}
|
|
98
|
+
focus = result.focus;
|
|
99
|
+
last = result.last;
|
|
100
|
+
if ("planState" in result) planState = result.planState;
|
|
101
|
+
if ("researchState" in result) researchState = result.researchState;
|
|
102
|
+
await captureExtraState(result, { focus, last, planState, researchState }, callArgs);
|
|
103
|
+
return {
|
|
104
|
+
answer: result.answer,
|
|
105
|
+
end: Boolean(result.end),
|
|
106
|
+
record: result.record ?? null,
|
|
107
|
+
plan: result.plan ?? null,
|
|
108
|
+
research: result.research,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
memoryDir,
|
|
114
|
+
sessionId,
|
|
115
|
+
graph,
|
|
116
|
+
get focus() { return focus; },
|
|
117
|
+
get last() { return last; },
|
|
118
|
+
get planState() { return planState; },
|
|
119
|
+
setPlanState(next) { planState = next; },
|
|
120
|
+
get researchState() { return researchState; },
|
|
121
|
+
setResearchState(next) { researchState = next; },
|
|
122
|
+
turn,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
@@ -20,12 +20,19 @@
|
|
|
20
20
|
// restated here, so a verb the parser learns is a verb the documentation gains.
|
|
21
21
|
|
|
22
22
|
import { RELATIONS, WHERE_MARKERS } from "../domain/ask-vocab.mjs";
|
|
23
|
+
import { EXPRESSION_PALETTE } from "../domain/sprite-expressions.mjs";
|
|
23
24
|
|
|
24
25
|
/** The relation kinds tmct_ask traverses, each with the opening verbs a question can
|
|
25
26
|
* use for it. Read from the parser's RELATIONS table — never a hand-kept copy. */
|
|
26
27
|
export const askLexicon = () =>
|
|
27
28
|
Object.entries(RELATIONS).map(([kind, { verbs }]) => ({ kind, verbs: verbs.slice(0, 4) }));
|
|
28
29
|
|
|
30
|
+
/** The expression words tmct_sprite's schema offers. Read from the face-fragment
|
|
31
|
+
* palette sprite-expressions.mjs already ships — the same derivation askLexicon
|
|
32
|
+
* does for the ask verbs, so an expression the palette gains is an expression
|
|
33
|
+
* the schema gains. */
|
|
34
|
+
export const spriteExpressionEnum = () => Object.keys(EXPRESSION_PALETTE).sort();
|
|
35
|
+
|
|
29
36
|
const symbolArg = (description) => ({
|
|
30
37
|
type: "object",
|
|
31
38
|
required: ["symbol"],
|
|
@@ -193,6 +200,29 @@ export const TOOL_DEFINITIONS = Object.freeze([
|
|
|
193
200
|
},
|
|
194
201
|
example: { term: "sofa" },
|
|
195
202
|
},
|
|
203
|
+
{
|
|
204
|
+
name: "tmct_sprite",
|
|
205
|
+
tier: "cold",
|
|
206
|
+
summary:
|
|
207
|
+
"The sprite markup for a class, resolved up the taught rdfs:subClassOf chain — with the expression and size asked for, and the ancestor chain the resolver walked to find it.",
|
|
208
|
+
inputSchema: {
|
|
209
|
+
type: "object",
|
|
210
|
+
required: ["class"],
|
|
211
|
+
properties: {
|
|
212
|
+
class: { type: "string", description: "The class to draw, e.g. spider. Resolved through the memory graph's own rdfs:subClassOf taxonomy." },
|
|
213
|
+
expression: {
|
|
214
|
+
type: "string",
|
|
215
|
+
enum: spriteExpressionEnum(),
|
|
216
|
+
description: "The face the sprite wears, as an mgx:feels value. A class whose template takes no expression parameter misses rather than dropping it.",
|
|
217
|
+
},
|
|
218
|
+
size: {
|
|
219
|
+
type: "string",
|
|
220
|
+
description: "A taught size property word (mgx:hasProperty), resolved to a numeric render SCALE — the size of the thing drawn, not a choice of template tier.",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
example: { class: "spider", expression: "happy", size: "large" },
|
|
225
|
+
},
|
|
196
226
|
{
|
|
197
227
|
name: "tmct_architecture",
|
|
198
228
|
tier: "cold",
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// The tool-name → handler registry dispatchTool maps over. One module per tool, so a
|
|
2
2
|
// tool's behaviour has exactly one home and this file only wires names to it.
|
|
3
3
|
//
|
|
4
|
-
// A handler takes (args, ctx) and returns the caller-facing string
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// A handler takes (args, ctx) and returns the caller-facing string, or kit.mjs's
|
|
5
|
+
// toolResult({ content, data }) when it already holds the structured form of what that
|
|
6
|
+
// string says. ctx carries the already-loaded { graph, svc, config, repoRoot }. A handler
|
|
7
|
+
// marked `ownsGraphLoad` gets { config, source, tel } instead and loads what it needs.
|
|
7
8
|
|
|
8
9
|
import { tmct_context } from "./tmct-context.mjs";
|
|
9
10
|
import { tmct_context_more } from "./tmct-context-more.mjs";
|
|
@@ -15,6 +16,7 @@ import { tmct_search } from "./tmct-search.mjs";
|
|
|
15
16
|
import { tmct_members } from "./tmct-members.mjs";
|
|
16
17
|
import { tmct_subclasses } from "./tmct-subclasses.mjs";
|
|
17
18
|
import { tmct_related } from "./tmct-related.mjs";
|
|
19
|
+
import { tmct_sprite } from "./tmct-sprite.mjs";
|
|
18
20
|
import { tmct_architecture } from "./tmct-architecture.mjs";
|
|
19
21
|
import { tmct_exports } from "./tmct-exports.mjs";
|
|
20
22
|
import { tmct_untested } from "./tmct-untested.mjs";
|
|
@@ -42,6 +44,7 @@ export const HANDLERS = Object.freeze({
|
|
|
42
44
|
tmct_members,
|
|
43
45
|
tmct_subclasses,
|
|
44
46
|
tmct_related,
|
|
47
|
+
tmct_sprite,
|
|
45
48
|
tmct_architecture,
|
|
46
49
|
tmct_exports,
|
|
47
50
|
tmct_untested,
|
|
@@ -1,11 +1,28 @@
|
|
|
1
|
-
// What every per-tool handler module shares: the argument contract, the
|
|
2
|
-
// contract, and the one shape most cold tools have (take a symbol,
|
|
1
|
+
// What every per-tool handler module shares: the argument contract, the result contract,
|
|
2
|
+
// the resolve-or-miss contract, and the one shape most cold tools have (take a symbol,
|
|
3
|
+
// resolve it, render it).
|
|
3
4
|
|
|
4
5
|
import { ToolError } from "../../adapters/config.mjs";
|
|
5
6
|
import { resolveSymbol } from "../../domain/codegraph.mjs";
|
|
6
7
|
|
|
7
8
|
export const SNIPPET_MAX_LINES = 200;
|
|
8
9
|
|
|
10
|
+
/** A handler's answer when it already holds the structured form of what its prose says.
|
|
11
|
+
* `content` is the prose. `data` is that structure, handed to dispatchToolStructured
|
|
12
|
+
* callers as-is so a page renders it instead of parsing the sentence back apart.
|
|
13
|
+
* `text` is what the string-only callers (dispatchTool, the CLI `cli <tool>` route)
|
|
14
|
+
* receive; it defaults to `content`, and a tool sets it only where its flat string
|
|
15
|
+
* already carried the structure in-band. A handler with nothing structured to add
|
|
16
|
+
* keeps returning a bare string, and `data` is absent for it. */
|
|
17
|
+
export function toolResult({ content, data, text = null }) {
|
|
18
|
+
return { content, data, text: text ?? content };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** A handler may return either shape, so both dispatch paths have to tell them apart. */
|
|
22
|
+
export const isToolResult = (value) =>
|
|
23
|
+
!!value && typeof value === "object"
|
|
24
|
+
&& typeof value.content === "string" && typeof value.text === "string";
|
|
25
|
+
|
|
9
26
|
/** A required string argument, trimmed. Missing or blank raises "<key> is required". */
|
|
10
27
|
export function requiredArg(args, key) {
|
|
11
28
|
const value = String(args?.[key] || "").trim();
|