@konneal/engine 0.2.84 → 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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-D24PQPDK.js";
3
+ } from "./chunk-R3KA6S3E.js";
4
4
  import "./chunk-7HWJOFTQ.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
@@ -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 TOOL_DECLARATION = `You may use one tool before answering, by writing a single line:
1893
- TOOL register_search {"query": "manufacturer and model to look up"}
1894
- The worker runs it against the certificate register (a snapshot) and returns the matching rows, or the exact no-match statement for the string you asked. Use it when the question turns on certification standing and the holder is known \u2014 including from a photograph. Then answer, phrasing the tool's result as what it returned (the searched string stays visible in your answer). If you do not need the tool, answer directly without the line.`;
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
- if (call.name !== "register_search") return null;
1897
- const query = String(call.args?.query ?? "").trim().slice(0, 160);
1898
- if (!query) return null;
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}":
@@ -3163,5 +3179,6 @@ export {
3163
3179
  bindModelNode,
3164
3180
  modelGroundingBlock,
3165
3181
  scoreJudge,
3182
+ TOOLS_REGISTRY,
3166
3183
  handleAsk
3167
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 register_search {"query": "…"}`.
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 declare const TOOL_DECLARATION = "You may use one tool before answering, by writing a single line:\nTOOL register_search {\"query\": \"manufacturer and model to look up\"}\nThe worker runs it against the certificate register (a snapshot) and returns the matching rows, or the exact no-match statement for the string you asked. Use it when the question turns on certification standing and the holder is known \u2014 including from a photograph. Then answer, phrasing the tool's result as what it returned (the searched string stays visible in your answer). If you do not need the tool, answer directly without the line.";
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-D24PQPDK.js";
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-GVU5PYC4.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
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.84",
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",
@@ -1227,7 +1227,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1227
1227
  await attachFigureImages(env, messages, usedHits, q.query);
1228
1228
  // the agent loop (the owner's 2026-09-28 direction): ONE bounded tool
1229
1229
  // round, armed only where a tool can help. The model may call
1230
- // register_search through the strict line protocol; the worker
1230
+ // the registry tools through the strict line protocol; the worker
1231
1231
  // executes it deterministically and the ATTRIBUTED result joins the
1232
1232
  // messages — what the tool returned, for the string it asked. The
1233
1233
  // invocations record into pendingTools; the stream emits them as its
@@ -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 register_search {"query": "…"}`.
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
- export const TOOL_DECLARATION = `You may use one tool before answering, by writing a single line:
36
- TOOL register_search {"query": "manufacturer and model to look up"}
37
- The worker runs it against the certificate register (a snapshot) and returns the matching rows, or the exact no-match statement for the string you asked. Use it when the question turns on certification standing and the holder is known — including from a photograph. Then answer, phrasing the tool's result as what it returned (the searched string stays visible in your answer). If you do not need the tool, answer directly without the line.`;
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
- if (call.name !== "register_search") return null;
41
- const query = String(call.args?.query ?? "").trim().slice(0, 160);
42
- if (!query) return null;
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