@polycode-projects/the-mechanical-code-talker 2.11.6 → 2.11.9
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/bin/tmct.mjs +20 -8
- package/package.json +2 -1
- package/src/adapters/toml-config.mjs +2 -0
- package/src/domain/ask-vocab.mjs +32 -0
- package/src/domain/ask.mjs +89 -4
- package/src/domain/domain.mjs +14 -0
- package/src/domain/interpret/strategies/keywords.mjs +18 -2
- package/src/domain/reference-pack.mjs +15 -3
- package/src/services/adventure-viz.mjs +77 -23
- package/src/services/chat-page-viz.mjs +149 -3
- package/src/services/chat.mjs +207 -47
- package/src/services/extensions.mjs +9 -2
- package/src/services/extract-facts.mjs +74 -7
- package/src/services/init.mjs +21 -2
- package/src/services/ledger-viz.mjs +1 -3
- package/src/services/research-viz.mjs +672 -0
- package/src/surfaces/http/server-http.mjs +172 -3
- package/src/surfaces/web/chat-browser-entry.mjs +21 -1
- package/src/surfaces/web/memory-ask-browser.bundle.js +119 -119
- package/src/surfaces/web/research-browser-entry.mjs +319 -0
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
// HTTP surface.
|
|
16
16
|
|
|
17
17
|
import { createServer } from "node:http";
|
|
18
|
-
import { runTurn, selectTool } from "../../services/chat.mjs";
|
|
19
|
-
import { TOOLS } from "../../tools/server.mjs";
|
|
18
|
+
import { runTurn, selectTool, capabilityPlanDeps } from "../../services/chat.mjs";
|
|
19
|
+
import { TOOLS, dispatchTool } from "../../tools/server.mjs";
|
|
20
|
+
import { runCapabilityPlan, buildCapabilityPlanCtx, declaredCapabilityNames } from "../../domain/router/drive.mjs";
|
|
21
|
+
import { isCapability } from "../../domain/router/registry.mjs";
|
|
22
|
+
import { hallucinationsIn } from "../../domain/router/call-validator.mjs";
|
|
20
23
|
import { parseEntities } from "../../domain/codegraph.mjs";
|
|
24
|
+
import { ToolError } from "../../adapters/config.mjs";
|
|
21
25
|
import { uuidv7 } from "../../adapters/uuid.mjs";
|
|
22
26
|
import * as defaultSource from "../../adapters/source.mjs";
|
|
23
27
|
|
|
@@ -83,9 +87,58 @@ function assistantMessage(model, content, stopReason) {
|
|
|
83
87
|
};
|
|
84
88
|
}
|
|
85
89
|
|
|
90
|
+
/** The first tool_use block of a transcript that ENDS on an assistant message —
|
|
91
|
+
* a caller PROPOSING a call for tmct to validate and (if clean) run. Returns
|
|
92
|
+
* { name, input, id, rest } (rest = any further tool_use blocks in the same
|
|
93
|
+
* message, noted in the reply but not executed) or null. A transcript that ends
|
|
94
|
+
* user-role with a tool_result is the loop-closing shape, not a proposal, so
|
|
95
|
+
* this returns null for it — its final message is not an assistant message. */
|
|
96
|
+
function proposedToolUse(messages) {
|
|
97
|
+
if (!Array.isArray(messages) || messages.length === 0) return null;
|
|
98
|
+
const last = messages[messages.length - 1];
|
|
99
|
+
if (!last || last.role !== "assistant" || !Array.isArray(last.content)) return null;
|
|
100
|
+
const uses = last.content.filter((b) => b && b.type === "tool_use" && typeof b.name === "string");
|
|
101
|
+
if (!uses.length) return null;
|
|
102
|
+
const [first, ...rest] = uses;
|
|
103
|
+
return {
|
|
104
|
+
name: first.name,
|
|
105
|
+
input: first.input && typeof first.input === "object" ? first.input : {},
|
|
106
|
+
id: first.id,
|
|
107
|
+
rest,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One human line for a hallucination-taxonomy problem, its reason in parentheses
|
|
112
|
+
* so a caller can grep the machine reason off the prose. */
|
|
113
|
+
function describeProblem(p, name) {
|
|
114
|
+
switch (p.reason) {
|
|
115
|
+
case "unknown-tool": return `"${p.detail}" is not a capability I know (unknown-tool)`;
|
|
116
|
+
case "undeclared": return `"${p.detail}" is a real capability but was not declared in this request (undeclared)`;
|
|
117
|
+
case "unknown-arg": return `${p.detail} is not an argument ${name} accepts (unknown-arg)`;
|
|
118
|
+
case "missing-arg": return `${p.detail} (missing-arg)`;
|
|
119
|
+
default: return `${p.detail} (${p.reason})`;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The tmct capabilities a request declared — what tmct will actually validate
|
|
124
|
+
* and execute, as named in a hand-back / refusal so the caller can see the set. */
|
|
125
|
+
function declaredCapabilityList(declaredNames) {
|
|
126
|
+
const caps = [...declaredNames].filter(isCapability);
|
|
127
|
+
return caps.length ? caps.join(", ") : "(none)";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Append the "N further calls not processed" note (when a proposal carried more
|
|
131
|
+
* than one tool_use block) to a reply text. */
|
|
132
|
+
function withRestNote(text, rest) {
|
|
133
|
+
if (!rest.length) return text;
|
|
134
|
+
return `${text}\n(${rest.length} further proposed call(s) in the same message were not processed: ${rest.map((b) => b.name).join(", ")}.)`;
|
|
135
|
+
}
|
|
136
|
+
|
|
86
137
|
/**
|
|
87
138
|
* Produce the Messages-API response for one request body. Pure over its inputs
|
|
88
139
|
* (the loaded graph + config), so it is unit-testable without a socket.
|
|
140
|
+
* - a caller-PROPOSED tool_use (transcript ends on an assistant tool_use) →
|
|
141
|
+
* validate with hallucinationsIn, then execute (end_turn) or refuse
|
|
89
142
|
* - a returned tool_result → end_turn text (relay the tool's output)
|
|
90
143
|
* - a mapped, declared graph tool → tool_use
|
|
91
144
|
* - otherwise → end_turn text via runTurn
|
|
@@ -96,6 +149,57 @@ export async function respondToMessages(body, { config, graph, source = defaultS
|
|
|
96
149
|
(Array.isArray(tools) ? tools : []).map((t) => t && t.name).filter(Boolean),
|
|
97
150
|
);
|
|
98
151
|
|
|
152
|
+
// A caller-proposed call: validate it against the registry + the declared set
|
|
153
|
+
// before anything runs, then execute a clean call or refuse a hallucinated one
|
|
154
|
+
// with the taxonomy's reason. Checked FIRST — a proposal ends on an assistant
|
|
155
|
+
// tool_use, so the loop-closing tool_result branch below (final user turn)
|
|
156
|
+
// never fires for it, and this never fires for a loop close.
|
|
157
|
+
const proposal = proposedToolUse(messages);
|
|
158
|
+
if (proposal) {
|
|
159
|
+
const { name, input, rest } = proposal;
|
|
160
|
+
|
|
161
|
+
// A tool the caller declared that is NOT a tmct capability is the caller's
|
|
162
|
+
// OWN tool — hand it back honestly rather than burning the taxonomy's
|
|
163
|
+
// unknown-tool (which stays reserved for a genuinely invented name).
|
|
164
|
+
if (declaredNames.has(name) && !isCapability(name)) {
|
|
165
|
+
const text = `"${name}" is your own tool, not a tmct capability — tmct validates and executes only tmct capabilities (${declaredCapabilityList(declaredNames)}). Nothing was executed.`;
|
|
166
|
+
return assistantMessage(model, [{ type: "text", text: withRestNote(text, rest) }], "end_turn");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const problems = hallucinationsIn({ name, input }, [...declaredNames]);
|
|
170
|
+
if (problems.length) {
|
|
171
|
+
const lines = problems.map((p) => `refusing the proposed call — ${describeProblem(p, name)}.`);
|
|
172
|
+
lines.push(`Nothing was executed; declared capabilities: ${declaredCapabilityList(declaredNames)}.`);
|
|
173
|
+
const msg = assistantMessage(model, [{ type: "text", text: withRestNote(lines.join("\n"), rest) }], "refusal");
|
|
174
|
+
msg.tmct_checked_call = { name, input, problems };
|
|
175
|
+
return msg;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Clean. A taught: record is simulated over the taught rules only — never a
|
|
179
|
+
// dispatchable tool — so decline honestly rather than dispatch an "unknown
|
|
180
|
+
// tool" error under a call that actually validated.
|
|
181
|
+
if (name.startsWith("taught:")) {
|
|
182
|
+
const text = `"${name}" is a taught action — simulated over the taught rules, not a dispatchable tool — so nothing was executed. Run it in chat with "next".`;
|
|
183
|
+
const msg = assistantMessage(model, [{ type: "text", text: withRestNote(text, rest) }], "end_turn");
|
|
184
|
+
msg.tmct_checked_call = { name, input, problems: [] };
|
|
185
|
+
return msg;
|
|
186
|
+
}
|
|
187
|
+
try {
|
|
188
|
+
const out = await dispatchTool(name, input, { config, source });
|
|
189
|
+
const msg = assistantMessage(model, [{ type: "text", text: withRestNote(out, rest) }], "end_turn");
|
|
190
|
+
msg.tmct_checked_call = { name, input, problems: [] };
|
|
191
|
+
return msg;
|
|
192
|
+
} catch (e) {
|
|
193
|
+
if (!(e instanceof ToolError)) throw e;
|
|
194
|
+
// A well-formed call that grounded nothing is an honest MISS, not a
|
|
195
|
+
// refusal — the taxonomy is clean; the graph simply had no answer.
|
|
196
|
+
const text = `the proposed call was well-formed but grounded nothing: ${e.message}`;
|
|
197
|
+
const msg = assistantMessage(model, [{ type: "text", text: withRestNote(text, rest) }], "end_turn");
|
|
198
|
+
msg.tmct_checked_call = { name, input, problems: [] };
|
|
199
|
+
return msg;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
99
203
|
// Closing the loop: the caller executed our tool_use and returned a
|
|
100
204
|
// tool_result. Relay it as the final, cited answer with end_turn.
|
|
101
205
|
const lastUser = lastMessageOfRole(messages, "user");
|
|
@@ -127,6 +231,44 @@ export async function respondToMessages(body, { config, graph, source = defaultS
|
|
|
127
231
|
return assistantMessage(model, [{ type: "text", text: answer }], "end_turn");
|
|
128
232
|
}
|
|
129
233
|
|
|
234
|
+
/** The capability names a /v1/plan request restricts its plan to (its `tools`
|
|
235
|
+
* array of names or `{name}` objects), and any that are not registered
|
|
236
|
+
* capabilities — the route rejects an `unknown` list with a 400 before the loop
|
|
237
|
+
* ever runs. A request with no `tools` plans over every registered capability. */
|
|
238
|
+
function planToolNames(body) {
|
|
239
|
+
const declared = declaredCapabilityNames();
|
|
240
|
+
if (!Array.isArray(body?.tools)) return { tools: declared, declared, unknown: [] };
|
|
241
|
+
const tools = body.tools.map((t) => (typeof t === "string" ? t : t && t.name)).filter(Boolean);
|
|
242
|
+
const unknown = tools.filter((t) => !declared.includes(t));
|
|
243
|
+
return { tools, declared, unknown };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Produce the /v1/plan response for one request body — the capability router
|
|
248
|
+
* (runCapabilityPlan) over HTTP, the same loop result the library export and the
|
|
249
|
+
* `tmct plan --json` CLI already return. Assumes a validated body (the route
|
|
250
|
+
* rejects a missing `request` or an unknown tool name with a 400 first).
|
|
251
|
+
* - refused → { request, ...loopResult } — an in-band honest "no plan found",
|
|
252
|
+
* not a protocol error (still HTTP 200)
|
|
253
|
+
* - grounded → { request, ...loopResult, usage } with $0 usage
|
|
254
|
+
*
|
|
255
|
+
* Per-request taught-action registrations ride ctx.disposers and are unregistered
|
|
256
|
+
* in the finally, so a second request re-reads the store instead of colliding on
|
|
257
|
+
* an already-registered taught capability name.
|
|
258
|
+
*/
|
|
259
|
+
export async function respondToPlan(body, { config, graph, memoryDir = null, source = defaultSource } = {}) {
|
|
260
|
+
const request = typeof body?.request === "string" ? body.request.trim() : "";
|
|
261
|
+
const { tools } = planToolNames(body);
|
|
262
|
+
const ctx = await buildCapabilityPlanCtx({ ...capabilityPlanDeps(), config, source, graph, memoryDir });
|
|
263
|
+
try {
|
|
264
|
+
const result = await runCapabilityPlan(request, tools, ctx);
|
|
265
|
+
if (result.refused) return { request, ...result };
|
|
266
|
+
return { request, ...result, usage: { ...ZERO_USAGE } };
|
|
267
|
+
} finally {
|
|
268
|
+
for (const dispose of ctx.disposers || []) dispose();
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
130
272
|
/** Self-description payload (GET /) — lets a routing target discover the endpoint
|
|
131
273
|
* and the tools tmct can back with just an HTTP GET. */
|
|
132
274
|
function describe(config) {
|
|
@@ -134,6 +276,7 @@ function describe(config) {
|
|
|
134
276
|
service: "tmct",
|
|
135
277
|
description: "Anthropic Messages API-compatible, deterministic, no-LLM graph router (the $0 floor).",
|
|
136
278
|
endpoint: { method: "POST", path: "/v1/messages" },
|
|
279
|
+
plan_endpoint: { method: "POST", path: "/v1/plan" },
|
|
137
280
|
graph: config && config.graphFile,
|
|
138
281
|
tools: TOOLS.map((t) => ({ name: t.name, description: t.description, input_schema: t.inputSchema })),
|
|
139
282
|
usage_pricing: ZERO_USAGE,
|
|
@@ -174,7 +317,7 @@ function sendError(res, status, type, message) {
|
|
|
174
317
|
* host — bind address (default 127.0.0.1)
|
|
175
318
|
* port — TCP port; 0 picks an ephemeral port (tests)
|
|
176
319
|
*/
|
|
177
|
-
export async function startServer({ config, host = "127.0.0.1", port = 0, source = defaultSource } = {}) {
|
|
320
|
+
export async function startServer({ config, host = "127.0.0.1", port = 0, source = defaultSource, memoryDir = null } = {}) {
|
|
178
321
|
if (!config || !config.graphFile) throw new Error("startServer requires config.graphFile");
|
|
179
322
|
// Load the graph once, up front. A missing artifact loads as the empty
|
|
180
323
|
// bootstrap graph — runTurn tolerates it (an honest empty/orienting answer).
|
|
@@ -187,6 +330,32 @@ export async function startServer({ config, host = "127.0.0.1", port = 0, source
|
|
|
187
330
|
sendJson(res, 200, describe(config));
|
|
188
331
|
return;
|
|
189
332
|
}
|
|
333
|
+
if (url.pathname === "/v1/plan") {
|
|
334
|
+
if (req.method !== "POST") {
|
|
335
|
+
sendError(res, 405, "invalid_request_error", "POST /v1/plan");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
let body;
|
|
339
|
+
try {
|
|
340
|
+
body = JSON.parse((await readBody(req)) || "{}");
|
|
341
|
+
} catch {
|
|
342
|
+
sendError(res, 400, "invalid_request_error", "request body is not valid JSON");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const request = typeof body?.request === "string" ? body.request.trim() : "";
|
|
346
|
+
if (!request) {
|
|
347
|
+
sendError(res, 400, "invalid_request_error", "`request` is required and must be a non-empty string");
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const { unknown, declared } = planToolNames(body);
|
|
351
|
+
if (unknown.length) {
|
|
352
|
+
sendError(res, 400, "invalid_request_error", `unknown tools name(s): ${unknown.join(", ")}; registered capabilities: ${declared.join(", ")}`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const out = await respondToPlan(body, { config, graph, memoryDir, source });
|
|
356
|
+
sendJson(res, 200, out);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
190
359
|
if (url.pathname !== "/v1/messages") {
|
|
191
360
|
sendError(res, 404, "not_found_error", `no route ${req.method} ${url.pathname}`);
|
|
192
361
|
return;
|
|
@@ -179,4 +179,24 @@ export async function exportFactsJsonl(memoryDir) {
|
|
|
179
179
|
return serializeFactsJsonl(await loadMemory(memoryDir));
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Every fact a "research <topic>" run has stored so far, in storage order —
|
|
184
|
+
* the exposure the "researched this session" panel needs and runTurn's
|
|
185
|
+
* result doesn't otherwise carry (a research turn's own record reports a
|
|
186
|
+
* per-topic FACT COUNT, research.mjs's own researchSnapshot, never the
|
|
187
|
+
* triples themselves). Reads the same provenance tags memoryStats already
|
|
188
|
+
* folds (readFactRows' `provenance`, the ' | '-joined compat string),
|
|
189
|
+
* filtered to the `research:<topicKey>@<depth>` prefix research.mjs's own
|
|
190
|
+
* `researchProvenanceTag` stamps every research-sourced fact with, rather
|
|
191
|
+
* than a second ingest-path computation — so this can never list a fact
|
|
192
|
+
* research didn't actually store.
|
|
193
|
+
*/
|
|
194
|
+
export async function researchedFactRows(memoryDir) {
|
|
195
|
+
const memory = await loadMemory(memoryDir);
|
|
196
|
+
const rows = readFactRows(memory);
|
|
197
|
+
return rows
|
|
198
|
+
.filter((row) => String(row.provenance || "").split(" | ").some((tag) => tag.startsWith("research:")))
|
|
199
|
+
.map((row) => ({ subject: row.subject, predicate: row.predicate, object: row.object }));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
globalThis.tmctChat = { createChatSession, registerWinkModel, registerReferencePackProvider, registerLiveReferenceProvider, registerResearchProvider, normFactTerm, vocabExampleHint, memoryStats, openPersistedStore, exportFactsJsonl, researchedFactRows, splitSentences: splitSentencesPreservingPaths };
|