@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.
- package/README.md +78 -19
- package/bin/tmct.mjs +63 -2
- package/package.json +1 -1
- package/src/adapters/memory/core.mjs +23 -0
- package/src/domain/ask-vocab.mjs +19 -0
- package/src/domain/ask.mjs +10 -5
- package/src/domain/codegraph.mjs +23 -9
- package/src/domain/game-config.mjs +12 -0
- package/src/domain/interpret/strategies/keywords.mjs +30 -1
- package/src/domain/memory/capability.mjs +15 -11
- package/src/domain/router/drive.mjs +36 -17
- package/src/domain/router/resolver.mjs +63 -17
- package/src/domain/spider-fly-world.mjs +2 -2
- package/src/domain/sprite-templates.mjs +19 -7
- package/src/domain/syllogise.mjs +16 -6
- package/src/domain/town-square-world.mjs +1 -1
- package/src/services/adventure-viz.mjs +5 -2
- package/src/services/adventure.mjs +8 -1
- package/src/services/chat-page-viz.mjs +123 -25
- package/src/services/chat-session.mjs +60 -10
- package/src/services/chat.mjs +328 -36
- package/src/services/code-explorer-viz.mjs +3 -2
- package/src/services/extract-facts.mjs +47 -7
- package/src/services/ingest-viz.mjs +113 -29
- package/src/services/ledger-viz.mjs +9 -4
- package/src/services/memory-panel-viz.mjs +44 -0
- package/src/services/mud-viz.mjs +21 -3
- package/src/services/mudiii-scene.mjs +407 -36
- package/src/services/mudiii-turn.mjs +65 -9
- package/src/services/mudiii-viz.mjs +810 -157
- package/src/services/p2p-room.mjs +1 -1
- package/src/services/plan-viz.mjs +26 -4
- package/src/services/predator-prey.mjs +141 -37
- package/src/services/research-viz.mjs +17 -23
- package/src/services/spider-fly-turn.mjs +7 -1
- package/src/services/spider-fly-viz.mjs +13 -5
- package/src/services/sprite-catalog-viz.mjs +3 -2
- package/src/services/viz-theme.mjs +20 -0
- package/src/services/viz-ticker.mjs +15 -2
- package/src/surfaces/http/server-http.mjs +90 -13
- package/src/surfaces/web/memory-ask-browser.bundle.js +125 -125
- package/src/surfaces/web/mud-browser-entry.mjs +33 -1
- package/src/surfaces/web/mudiii-browser-entry.mjs +70 -34
- package/src/surfaces/web/tmct-surface.mjs +18 -6
- package/src/tools/handlers/tmct-ask.mjs +15 -2
- 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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
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 }`.
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
188
|
-
buildExtraOptions: () => ({
|
|
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
|
-
//
|
|
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),
|
|
31
|
-
// record, `{at, ms}` (chat, adventure).
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
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
|
-
|
|
13
|
-
|
|
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
|
+
}
|
package/src/tools/server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|