@konneal/engine 0.2.83 → 0.2.85
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/dist/{ask-IAJDX2YY.js → ask-52JUHGUG.js} +1 -1
- package/dist/{chunk-TWK46PU2.js → chunk-R3KA6S3E.js} +44 -12
- package/dist/tools.d.ts +19 -2
- package/dist/worker_public/src/index.js +13 -3
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +21 -3
- package/workers/worker_public/src/mcp-proto.ts +14 -0
- package/workers/worker_public/src/tools.ts +52 -15
|
@@ -1875,7 +1875,7 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
|
|
|
1875
1875
|
|
|
1876
1876
|
// workers/worker_public/src/tools.ts
|
|
1877
1877
|
function parseToolCall(text) {
|
|
1878
|
-
const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
|
|
1878
|
+
const m = /^TOOL\s+([a-z_.]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
|
|
1879
1879
|
if (!m) return null;
|
|
1880
1880
|
let args = {};
|
|
1881
1881
|
if (m[2]) {
|
|
@@ -1884,22 +1884,38 @@ function parseToolCall(text) {
|
|
|
1884
1884
|
} catch {
|
|
1885
1885
|
return null;
|
|
1886
1886
|
}
|
|
1887
|
-
} else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
|
|
1887
|
+
} else if (/^TOOL\s+[a-z_.]+\s*\{/m.test(String(text ?? ""))) {
|
|
1888
1888
|
return null;
|
|
1889
1889
|
}
|
|
1890
1890
|
return { name: m[1].toLowerCase(), args };
|
|
1891
1891
|
}
|
|
1892
|
-
var
|
|
1893
|
-
|
|
1894
|
-
|
|
1892
|
+
var certificatesSearch = {
|
|
1893
|
+
name: "certificates.search",
|
|
1894
|
+
description: "Search the certificate register (a snapshot) by holder name, model designation, or a printed certificate number. Returns the matching rows \u2014 number, holder, model, issue year, status, and document links where on file \u2014 or the exact no-match statement for the string asked.",
|
|
1895
|
+
params: [{ key: "query", required: true, description: "the holder, model, or printed certificate number to look up" }],
|
|
1896
|
+
audiences: ["agent", "mcp"],
|
|
1897
|
+
handler: async (db, args) => {
|
|
1898
|
+
const query = String(args?.query ?? "").trim().slice(0, 160);
|
|
1899
|
+
if (!query) return null;
|
|
1900
|
+
const { searchRegister: searchRegister2, registerNote: registerNote2, certificateLinks: certificateLinks2 } = await import("./certificates-CO7KX2S2.js");
|
|
1901
|
+
const reg = await searchRegister2(db, query, true);
|
|
1902
|
+
const output = reg?.rows?.length ? [registerNote2(reg.rows), certificateLinks2(reg.rows)].filter(Boolean).join("\n") : `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
1903
|
+
return { name: "certificates.search", query, output };
|
|
1904
|
+
}
|
|
1905
|
+
};
|
|
1906
|
+
var TOOLS_REGISTRY = [certificatesSearch];
|
|
1907
|
+
var TOOL_DECLARATION = [
|
|
1908
|
+
"You may use one tool before answering, by writing a single line:",
|
|
1909
|
+
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
|
|
1910
|
+
const shape = `{${t.params.map((p) => `"${p.key}": "<${p.description}>"`).join(", ")}}`;
|
|
1911
|
+
return `TOOL ${t.name} ${shape}`;
|
|
1912
|
+
}),
|
|
1913
|
+
"The worker runs it and returns the result attributed \u2014 phrase the tool's result as what it returned, with the searched string visible in your answer. Use a tool when the question turns on what it answers (including from a photograph). If you do not need it, answer directly without the line."
|
|
1914
|
+
].join("\n");
|
|
1895
1915
|
async function runTool(db, call) {
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-CO7KX2S2.js");
|
|
1900
|
-
const reg = await searchRegister2(db, query, true);
|
|
1901
|
-
const output = reg?.rows?.length ? registerNote2(reg.rows) : `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
1902
|
-
return { name: call.name, query, output };
|
|
1916
|
+
const spec = TOOLS_REGISTRY.find((t) => t.name === call.name && t.audiences.includes("agent"));
|
|
1917
|
+
if (!spec) return null;
|
|
1918
|
+
return spec.handler(db, call.args ?? {});
|
|
1903
1919
|
}
|
|
1904
1920
|
function toolNote(r) {
|
|
1905
1921
|
return `The ${r.name} tool returned, for the query "${r.query}":
|
|
@@ -2310,6 +2326,11 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2310
2326
|
if (await env.CACHE.get("sys:generation") === "off") {
|
|
2311
2327
|
return err(503, "generation_disabled", "Generation is temporarily paused; search remains available.");
|
|
2312
2328
|
}
|
|
2329
|
+
const agentSteps = [];
|
|
2330
|
+
const mark = (step) => {
|
|
2331
|
+
agentSteps.push(step);
|
|
2332
|
+
console.log(`agent-step: ${step}`);
|
|
2333
|
+
};
|
|
2313
2334
|
const service = env.INTERNAL_SERVICE;
|
|
2314
2335
|
const fedAuth = {
|
|
2315
2336
|
cookie: req.headers.get("cookie") ?? "",
|
|
@@ -2488,6 +2509,7 @@ async function handleAsk(env, ctx, req, tier, key) {
|
|
|
2488
2509
|
let register = await searchRegister(env.DB, q.query);
|
|
2489
2510
|
const bridgeImage = userImage ?? storedImage;
|
|
2490
2511
|
if (bridgeImage && /certificate|certified|certification/i.test(q.query)) {
|
|
2512
|
+
mark("Reading the nameplate photograph");
|
|
2491
2513
|
const np = await extractNameplate(env.AI, MODELS.member, bridgeImage);
|
|
2492
2514
|
if (np?.manufacturer) {
|
|
2493
2515
|
const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query), true);
|
|
@@ -2878,6 +2900,7 @@ Answer account questions from these records ONLY: name the record when you use i
|
|
|
2878
2900
|
if (/certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
2879
2901
|
try {
|
|
2880
2902
|
const imageTurn = typeof withImage === "string";
|
|
2903
|
+
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
2881
2904
|
const probeMessages = [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}
|
|
2882
2905
|
|
|
2883
2906
|
${imageTurn ? "A photograph of the instrument is attached after this message. Transcribe the maker, model and any printed certificate number you can read FROM THE PHOTOGRAPH ITSELF, then emit the TOOL line with those tokens as the query \u2014 never invented, and never tokens taken from any register content elsewhere in the context (a register row's holder is not the plate's maker; observed live 2026-10-04)." : ""}
|
|
@@ -2892,6 +2915,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2892
2915
|
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
2893
2916
|
const call = parseToolCall(probeText);
|
|
2894
2917
|
if (call) {
|
|
2918
|
+
mark(`Searching the certificate register for \u201C${String(call.args?.query ?? "")}\u201D`);
|
|
2895
2919
|
const result = await runTool(env.DB, call);
|
|
2896
2920
|
if (result) {
|
|
2897
2921
|
messages.push({ role: "system", content: toolNote(result) });
|
|
@@ -2926,6 +2950,12 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2926
2950
|
const send = (obj) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}
|
|
2927
2951
|
|
|
2928
2952
|
`));
|
|
2953
|
+
for (const step of agentSteps) {
|
|
2954
|
+
try {
|
|
2955
|
+
send({ type: "agent", step });
|
|
2956
|
+
} catch {
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2929
2959
|
for (const t of pendingTools) {
|
|
2930
2960
|
try {
|
|
2931
2961
|
send({ type: "tool", name: t.name, query: t.query });
|
|
@@ -3085,6 +3115,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
3085
3115
|
completionBlocks.push(...await completeFigures(env.DB, answer, [...c2ns.blocks, ...completionBlocks], used));
|
|
3086
3116
|
if (withImage && register?.rows?.length && answer.includes(refusalAnswer())) {
|
|
3087
3117
|
console.log("blind-answer: the register note carried rows but the answer refused \u2014 regenerating once");
|
|
3118
|
+
mark("Re-answering from the register results");
|
|
3088
3119
|
const regen = await generateOnce(env, model, [
|
|
3089
3120
|
...messages.slice(0, -1),
|
|
3090
3121
|
{ role: "user", content: "The photograph was attached and the register note lists actual matching certificate rows. Present those rows \u2014 number, holder, model, status, and every document link \u2014 as the answer. Do not claim that nothing was provided." },
|
|
@@ -3148,5 +3179,6 @@ export {
|
|
|
3148
3179
|
bindModelNode,
|
|
3149
3180
|
modelGroundingBlock,
|
|
3150
3181
|
scoreJudge,
|
|
3182
|
+
TOOLS_REGISTRY,
|
|
3151
3183
|
handleAsk
|
|
3152
3184
|
};
|
package/dist/tools.d.ts
CHANGED
|
@@ -7,11 +7,28 @@ export interface ToolResult {
|
|
|
7
7
|
query: string;
|
|
8
8
|
output: string;
|
|
9
9
|
}
|
|
10
|
-
/** The strict protocol: a line `TOOL
|
|
10
|
+
/** The strict protocol: a line `TOOL certificates.search {"query": "…"}`.
|
|
11
11
|
* Plain regex parse — no JSON mode needed, tolerant of surrounding
|
|
12
12
|
* prose (the model may talk around the call). */
|
|
13
13
|
export declare function parseToolCall(text: string): ToolCall | null;
|
|
14
|
-
export
|
|
14
|
+
export interface ToolSpec {
|
|
15
|
+
/** canonical on every surface */
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
params: {
|
|
19
|
+
key: string;
|
|
20
|
+
required: boolean;
|
|
21
|
+
description: string;
|
|
22
|
+
}[];
|
|
23
|
+
audiences: ("agent" | "mcp")[];
|
|
24
|
+
handler: (db: any, args: Record<string, unknown>) => Promise<ToolResult | null>;
|
|
25
|
+
}
|
|
26
|
+
export declare const TOOLS_REGISTRY: ToolSpec[];
|
|
27
|
+
/** The agent loop's protocol declaration, GENERATED from the registry —
|
|
28
|
+
* the model's tool vocabulary is always the registry's, never a
|
|
29
|
+
* hand-maintained literal. */
|
|
30
|
+
export declare const TOOL_DECLARATION: string;
|
|
31
|
+
/** Dispatch by LOOKUP — a tool name is never hard-coded at a call site. */
|
|
15
32
|
export declare function runTool(db: any, call: ToolCall): Promise<ToolResult | null>;
|
|
16
33
|
/** The attributed injection: the answer model sees what the tool
|
|
17
34
|
* returned, for the string it asked — never a standing claim. */
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
TOOLS_REGISTRY,
|
|
2
3
|
bindModelNode,
|
|
3
4
|
checkQuoteAnchors,
|
|
4
5
|
handleAsk,
|
|
@@ -6,7 +7,7 @@ import {
|
|
|
6
7
|
modelGroundingBlock,
|
|
7
8
|
scoreFaithfulness,
|
|
8
9
|
scoreJudge
|
|
9
|
-
} from "../../chunk-
|
|
10
|
+
} from "../../chunk-R3KA6S3E.js";
|
|
10
11
|
import {
|
|
11
12
|
handleSearch
|
|
12
13
|
} from "../../chunk-KO6DRI7C.js";
|
|
@@ -922,7 +923,16 @@ var TOOLS = [
|
|
|
922
923
|
},
|
|
923
924
|
required: ["query"]
|
|
924
925
|
}
|
|
925
|
-
}
|
|
926
|
+
},
|
|
927
|
+
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("mcp")).map((t) => ({
|
|
928
|
+
name: t.name,
|
|
929
|
+
description: t.description,
|
|
930
|
+
inputSchema: {
|
|
931
|
+
type: "object",
|
|
932
|
+
properties: Object.fromEntries(t.params.map((p) => [p.key, { type: "string", description: p.description }])),
|
|
933
|
+
required: t.params.filter((p) => p.required).map((p) => p.key)
|
|
934
|
+
}
|
|
935
|
+
}))
|
|
926
936
|
];
|
|
927
937
|
function dispatch(method, params, callTool) {
|
|
928
938
|
switch (method) {
|
|
@@ -959,7 +969,7 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
959
969
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
960
970
|
body: JSON.stringify({ ...args, stream: false })
|
|
961
971
|
});
|
|
962
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
972
|
+
const res = name === "ask" ? await (await import("../../ask-52JUHGUG.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
|
|
963
973
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
964
974
|
});
|
|
965
975
|
if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.85",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
|
@@ -364,6 +364,16 @@ async function handleAsk(
|
|
|
364
364
|
return err(503, "generation_disabled", "Generation is temporarily paused; search remains available.");
|
|
365
365
|
}
|
|
366
366
|
|
|
367
|
+
// the agent's rounds, visible to the user as they happen: each step the
|
|
368
|
+
// assistant takes before and during the answer streams as an event (the
|
|
369
|
+
// owner's 2026-10-04 direction — the user must see the system-to-model
|
|
370
|
+
// questioning sessions, not stare at a spinner)
|
|
371
|
+
const agentSteps: string[] = [];
|
|
372
|
+
const mark = (step: string) => {
|
|
373
|
+
agentSteps.push(step);
|
|
374
|
+
console.log(`agent-step: ${step}`);
|
|
375
|
+
};
|
|
376
|
+
|
|
367
377
|
// Members get federated retrieval (OIML + ISO/IEC) merged into the same
|
|
368
378
|
// pipeline via the service binding; rag-public never touches the
|
|
369
379
|
// internal index itself, and generation/rerank stay in ONE pipeline.
|
|
@@ -666,6 +676,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
666
676
|
// holder, and the photograph's own reading is authoritative over them.
|
|
667
677
|
const bridgeImage = userImage ?? storedImage;
|
|
668
678
|
if (bridgeImage && /certificate|certified|certification/i.test(q.query)) {
|
|
679
|
+
mark("Reading the nameplate photograph");
|
|
669
680
|
const np = await extractNameplate((env as any).AI, MODELS.member, bridgeImage);
|
|
670
681
|
if (np?.manufacturer) {
|
|
671
682
|
const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query), true);
|
|
@@ -1216,7 +1227,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1216
1227
|
await attachFigureImages(env, messages, usedHits, q.query);
|
|
1217
1228
|
// the agent loop (the owner's 2026-09-28 direction): ONE bounded tool
|
|
1218
1229
|
// round, armed only where a tool can help. The model may call
|
|
1219
|
-
//
|
|
1230
|
+
// the registry tools through the strict line protocol; the worker
|
|
1220
1231
|
// executes it deterministically and the ATTRIBUTED result joins the
|
|
1221
1232
|
// messages — what the tool returned, for the string it asked. The
|
|
1222
1233
|
// invocations record into pendingTools; the stream emits them as its
|
|
@@ -1230,6 +1241,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1230
1241
|
if (/certificate|certified|certification/i.test(q.query) && env.DB) {
|
|
1231
1242
|
try {
|
|
1232
1243
|
const imageTurn = typeof withImage === "string";
|
|
1244
|
+
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
1233
1245
|
const probeMessages: any[] = [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}\n\n${imageTurn ? "A photograph of the instrument is attached after this message. Transcribe the maker, model and any printed certificate number you can read FROM THE PHOTOGRAPH ITSELF, then emit the TOOL line with those tokens as the query — never invented, and never tokens taken from any register content elsewhere in the context (a register row's holder is not the plate's maker; observed live 2026-10-04)." : ""}\n\n${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
1234
1246
|
if (imageTurn) probeMessages.push({ role: "user", content: [{ type: "image_url", image_url: { url: withImage } }] });
|
|
1235
1247
|
const probe = await (env as any).AI.run(model, {
|
|
@@ -1240,6 +1252,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1240
1252
|
const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
1241
1253
|
const call = parseToolCall(probeText);
|
|
1242
1254
|
if (call) {
|
|
1255
|
+
mark(`Searching the certificate register for “${String(call.args?.query ?? "")}”`);
|
|
1243
1256
|
const result = await runTool(env.DB, call);
|
|
1244
1257
|
if (result) {
|
|
1245
1258
|
messages.push({ role: "system", content: toolNote(result) });
|
|
@@ -1292,8 +1305,12 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1292
1305
|
const sse = new ReadableStream({
|
|
1293
1306
|
async start(controller) {
|
|
1294
1307
|
const send = (obj: unknown) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
1295
|
-
// the
|
|
1296
|
-
//
|
|
1308
|
+
// the agent trace arrives first: every questioning round the
|
|
1309
|
+
// assistant ran — the photograph read, the register search, any
|
|
1310
|
+
// re-answer — so the user watched what happened, not a spinner
|
|
1311
|
+
for (const step of agentSteps) {
|
|
1312
|
+
try { send({ type: "agent", step }); } catch { /* never block the answer */ }
|
|
1313
|
+
}
|
|
1297
1314
|
for (const t of pendingTools) {
|
|
1298
1315
|
try { send({ type: "tool", name: t.name, query: t.query }); } catch { /* never block the answer */ }
|
|
1299
1316
|
}
|
|
@@ -1522,6 +1539,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1522
1539
|
// that exact signature; the retry carries a sterner ground instruction.
|
|
1523
1540
|
if (withImage && register?.rows?.length && answer.includes(refusalAnswer())) {
|
|
1524
1541
|
console.log("blind-answer: the register note carried rows but the answer refused — regenerating once");
|
|
1542
|
+
mark("Re-answering from the register results");
|
|
1525
1543
|
const regen = await generateOnce(env, model, [
|
|
1526
1544
|
...messages.slice(0, -1),
|
|
1527
1545
|
{ role: "user", content: "The photograph was attached and the register note lists actual matching certificate rows. Present those rows — number, holder, model, status, and every document link — as the answer. Do not claim that nothing was provided." },
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// The MCP protocol surface (JSON-RPC 2.0, streamable HTTP 2025-06-18),
|
|
2
2
|
// dependency-free so plain node can load it for unit tests — the
|
|
3
3
|
// route adapter (mcp.ts) owns the handler wiring.
|
|
4
|
+
import { TOOLS_REGISTRY } from "./tools.ts";
|
|
4
5
|
export const PROTOCOL_VERSION = "2025-06-18";
|
|
5
6
|
|
|
6
7
|
export interface McpTool {
|
|
@@ -9,6 +10,8 @@ export interface McpTool {
|
|
|
9
10
|
inputSchema: { type: "object"; properties: Record<string, unknown>; required: string[] };
|
|
10
11
|
}
|
|
11
12
|
|
|
13
|
+
// the registry's MCP-audience tools are GENERATED here (OCP: adding a
|
|
14
|
+
// tool registers it; this list is never hand-extended for them)
|
|
12
15
|
export const TOOLS: McpTool[] = [
|
|
13
16
|
{
|
|
14
17
|
name: "ask",
|
|
@@ -34,6 +37,17 @@ export const TOOLS: McpTool[] = [
|
|
|
34
37
|
required: ["query"],
|
|
35
38
|
},
|
|
36
39
|
},
|
|
40
|
+
...TOOLS_REGISTRY
|
|
41
|
+
.filter((t) => t.audiences.includes("mcp"))
|
|
42
|
+
.map((t): McpTool => ({
|
|
43
|
+
name: t.name,
|
|
44
|
+
description: t.description,
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: "object",
|
|
47
|
+
properties: Object.fromEntries(t.params.map((p) => [p.key, { type: "string", description: p.description }])),
|
|
48
|
+
required: t.params.filter((p) => p.required).map((p) => p.key),
|
|
49
|
+
},
|
|
50
|
+
})),
|
|
37
51
|
];
|
|
38
52
|
|
|
39
53
|
export type McpResult =
|
|
@@ -17,35 +17,72 @@ export interface ToolResult {
|
|
|
17
17
|
output: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** The strict protocol: a line `TOOL
|
|
20
|
+
/** The strict protocol: a line `TOOL certificates.search {"query": "…"}`.
|
|
21
21
|
* Plain regex parse — no JSON mode needed, tolerant of surrounding
|
|
22
22
|
* prose (the model may talk around the call). */
|
|
23
23
|
export function parseToolCall(text: string): ToolCall | null {
|
|
24
|
-
const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? "")); // line-anchored, case-sensitive: prose never trips it
|
|
24
|
+
const m = /^TOOL\s+([a-z_.]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? "")); // line-anchored, case-sensitive: prose never trips it
|
|
25
25
|
if (!m) return null;
|
|
26
26
|
let args: Record<string, unknown> = {};
|
|
27
27
|
if (m[2]) {
|
|
28
28
|
try { args = JSON.parse(m[2]); } catch { return null; }
|
|
29
|
-
} else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
|
|
29
|
+
} else if (/^TOOL\s+[a-z_.]+\s*\{/m.test(String(text ?? ""))) {
|
|
30
30
|
return null; // a malformed call (opened, never closed) is not a call
|
|
31
31
|
}
|
|
32
32
|
return { name: m[1].toLowerCase(), args };
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
// ── the tool registry (OCP, the owner's 2026-10-04 direction) ────────
|
|
36
|
+
// ONE declared table is the single source of the tool surface: the agent
|
|
37
|
+
// loop's declaration is GENERATED from it, runTool dispatches by lookup,
|
|
38
|
+
// and the MCP server's tools/list is generated from the same entries.
|
|
39
|
+
// Adding a tool = adding one registration; no call site is edited.
|
|
40
|
+
export interface ToolSpec {
|
|
41
|
+
/** canonical on every surface */
|
|
42
|
+
name: string;
|
|
43
|
+
description: string;
|
|
44
|
+
params: { key: string; required: boolean; description: string }[];
|
|
45
|
+
audiences: ("agent" | "mcp")[];
|
|
46
|
+
handler: (db: any, args: Record<string, unknown>) => Promise<ToolResult | null>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const certificatesSearch: ToolSpec = {
|
|
50
|
+
name: "certificates.search",
|
|
51
|
+
description:
|
|
52
|
+
"Search the certificate register (a snapshot) by holder name, model designation, or a printed certificate number. Returns the matching rows — number, holder, model, issue year, status, and document links where on file — or the exact no-match statement for the string asked.",
|
|
53
|
+
params: [{ key: "query", required: true, description: "the holder, model, or printed certificate number to look up" }],
|
|
54
|
+
audiences: ["agent", "mcp"],
|
|
55
|
+
handler: async (db, args) => {
|
|
56
|
+
const query = String(args?.query ?? "").trim().slice(0, 160);
|
|
57
|
+
if (!query) return null;
|
|
58
|
+
const { searchRegister, registerNote, certificateLinks } = await import("./certificates");
|
|
59
|
+
const reg = await searchRegister(db, query, true); // the tool decided — no shaping gate
|
|
60
|
+
const output = reg?.rows?.length
|
|
61
|
+
? [registerNote(reg.rows), certificateLinks(reg.rows)].filter(Boolean).join("\n")
|
|
62
|
+
: `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
63
|
+
return { name: "certificates.search", query, output };
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch];
|
|
68
|
+
|
|
69
|
+
/** The agent loop's protocol declaration, GENERATED from the registry —
|
|
70
|
+
* the model's tool vocabulary is always the registry's, never a
|
|
71
|
+
* hand-maintained literal. */
|
|
72
|
+
export const TOOL_DECLARATION = [
|
|
73
|
+
"You may use one tool before answering, by writing a single line:",
|
|
74
|
+
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
|
|
75
|
+
const shape = `{${t.params.map((p) => `"${p.key}": "<${p.description}>"`).join(", ")}}`;
|
|
76
|
+
return `TOOL ${t.name} ${shape}`;
|
|
77
|
+
}),
|
|
78
|
+
"The worker runs it and returns the result attributed — phrase the tool's result as what it returned, with the searched string visible in your answer. Use a tool when the question turns on what it answers (including from a photograph). If you do not need it, answer directly without the line.",
|
|
79
|
+
].join("\n");
|
|
38
80
|
|
|
81
|
+
/** Dispatch by LOOKUP — a tool name is never hard-coded at a call site. */
|
|
39
82
|
export async function runTool(db: any, call: ToolCall): Promise<ToolResult | null> {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const { searchRegister, registerNote } = await import("./certificates");
|
|
44
|
-
const reg = await searchRegister(db, query, true); // the tool decided — no shaping gate
|
|
45
|
-
const output = reg?.rows?.length
|
|
46
|
-
? registerNote(reg.rows)
|
|
47
|
-
: `No certificate was found for "${query}" in the certificates database (the register snapshot). State this as the search's result, with the searched string visible.`;
|
|
48
|
-
return { name: call.name, query, output };
|
|
83
|
+
const spec = TOOLS_REGISTRY.find((t) => t.name === call.name && t.audiences.includes("agent"));
|
|
84
|
+
if (!spec) return null;
|
|
85
|
+
return spec.handler(db, call.args ?? {});
|
|
49
86
|
}
|
|
50
87
|
|
|
51
88
|
/** The attributed injection: the answer model sees what the tool
|