@konneal/engine 0.2.56 → 0.2.57

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,11 +1,12 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-ZVMQHCNL.js";
3
+ } from "./chunk-MNEUEHO7.js";
4
4
  import "./chunk-TPV5GPY2.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
7
7
  import "./chunk-LVZTWUVJ.js";
8
8
  import "./chunk-2PCUAJJO.js";
9
+ import "./chunk-Z6JAGZAM.js";
9
10
  import "./chunk-3FYJM7LH.js";
10
11
  export {
11
12
  handleAsk
@@ -0,0 +1,17 @@
1
+ import {
2
+ buildRegisterQuery,
3
+ isRegisterShaped,
4
+ queryFamily,
5
+ registerNote,
6
+ registerTokens,
7
+ searchRegister
8
+ } from "./chunk-Z6JAGZAM.js";
9
+ import "./chunk-3FYJM7LH.js";
10
+ export {
11
+ buildRegisterQuery,
12
+ isRegisterShaped,
13
+ queryFamily,
14
+ registerNote,
15
+ registerTokens,
16
+ searchRegister
17
+ };
@@ -64,6 +64,10 @@ import {
64
64
  roleModel,
65
65
  sha256Hex
66
66
  } from "./chunk-2PCUAJJO.js";
67
+ import {
68
+ registerNote,
69
+ searchRegister
70
+ } from "./chunk-Z6JAGZAM.js";
67
71
  import {
68
72
  P
69
73
  } from "./chunk-3FYJM7LH.js";
@@ -1864,119 +1868,37 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
1864
1868
  return `a:${indexVersion}:g${gen}:${ns}:${queryHash}`;
1865
1869
  }
1866
1870
 
1867
- // workers/worker_public/src/certificates.ts
1868
- function isRegisterShaped(query) {
1869
- return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were|have|has|hold|holds|possess|carry|got)\b/i.test(query);
1870
- }
1871
- function queryFamily(query) {
1872
- const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
1873
- return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
1874
- }
1875
- function registerTokens(query) {
1876
- const stop = /* @__PURE__ */ new Set([
1877
- "the",
1878
- "a",
1879
- "an",
1880
- "is",
1881
- "are",
1882
- "was",
1883
- "were",
1884
- "still",
1885
- "currently",
1886
- "in",
1887
- "on",
1888
- "for",
1889
- "of",
1890
- "and",
1891
- "or",
1892
- "certificate",
1893
- "certificates",
1894
- "certified",
1895
- "certification",
1896
- "register",
1897
- "status",
1898
- "valid",
1899
- "suspended",
1900
- "revoked",
1901
- "recommendation",
1902
- "per",
1903
- "under",
1904
- "by",
1905
- "with",
1906
- "what",
1907
- "which",
1908
- "who",
1909
- "does",
1910
- "do",
1911
- "load",
1912
- "cell",
1913
- "model",
1914
- "manufacturer",
1915
- "holder",
1916
- "company",
1917
- "r",
1918
- "how",
1919
- "to",
1920
- "list",
1921
- "show"
1922
- ]);
1923
- const tokens2 = [];
1924
- const publisherWord = P().publisher.name.toLowerCase();
1925
- for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
1926
- if (w.length < 2) continue;
1927
- if (stop.has(w.toLowerCase())) continue;
1928
- if (w.toLowerCase() === publisherWord) continue;
1929
- if (/^\d+$/.test(w) && w.length < 2) continue;
1930
- if (!tokens2.includes(w)) tokens2.push(w);
1931
- }
1932
- return tokens2.slice(0, 6);
1933
- }
1934
- function buildRegisterQuery(tokens2) {
1935
- const params = [];
1936
- const clauses = tokens2.map((t) => {
1937
- if (/^\d+$/.test(t)) {
1938
- const padded = `% ${t.toLowerCase()} %`;
1939
- params.push(t, t, padded);
1940
- const n1 = params.length - 2;
1941
- const n2 = params.length;
1942
- return `(model = ?${n1} OR num = ?${n1} OR (' ' || REPLACE(REPLACE(REPLACE(LOWER(model), '-', ' '), ',', ' '), '.', ' ') || ' ') LIKE ?${n2})`;
1943
- }
1944
- params.push(`%${t}%`);
1945
- const n = params.length;
1946
- return `(holder LIKE ?${n} OR model LIKE ?${n} OR num LIKE ?${n})`;
1947
- });
1948
- return {
1949
- sql: `SELECT num, family, holder, model, year, status FROM certificates WHERE ${clauses.join(" OR ")} LIMIT 6`,
1950
- params
1951
- };
1952
- }
1953
- async function searchRegister(db, query) {
1954
- if (!isRegisterShaped(query)) return null;
1955
- const tokens2 = registerTokens(query);
1956
- if (!tokens2.length) return null;
1957
- const built = buildRegisterQuery(tokens2);
1958
- let { sql, params } = built;
1959
- const family = queryFamily(query);
1960
- if (family) {
1961
- sql = sql.replace(" WHERE ", " WHERE family = ?0 AND ");
1962
- params.unshift(family);
1963
- }
1964
- try {
1965
- const res = await db.prepare(sql).bind(...params).all();
1966
- return { rows: res.results ?? [], tokens: tokens2 };
1967
- } catch {
1968
- return { rows: [], tokens: tokens2 };
1871
+ // workers/worker_public/src/tools.ts
1872
+ function parseToolCall(text) {
1873
+ const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
1874
+ if (!m) return null;
1875
+ let args = {};
1876
+ if (m[2]) {
1877
+ try {
1878
+ args = JSON.parse(m[2]);
1879
+ } catch {
1880
+ return null;
1881
+ }
1882
+ } else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
1883
+ return null;
1969
1884
  }
1885
+ return { name: m[1].toLowerCase(), args };
1970
1886
  }
1971
- function registerNote(rows) {
1972
- if (!rows.length) {
1973
- return `Certificate register: NO certificate matching the asked holder or model appears in the ${P().publisher.name}-CS register snapshot. State plainly that no such certificate is in this register, and that the register is a snapshot rather than the live certification status.`;
1974
- }
1975
- const lines = rows.map((r) => `- ${r.num}: holder ${r.holder}, model "${r.model}"${r.year ? `, issued ${r.year}` : ""} \u2014 status ${r.status}`);
1976
- return [
1977
- "Certificate register \u2014 the following certificates matched the asked holder or model. Quote the certificate number, the holder and the status verbatim; the register is a snapshot, so qualify any statement about current certification accordingly.",
1978
- ...lines
1979
- ].join("\n");
1887
+ var TOOL_DECLARATION = `You may use one tool before answering, by writing a single line:
1888
+ TOOL register_search {"query": "manufacturer and model to look up"}
1889
+ 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.`;
1890
+ async function runTool(db, call) {
1891
+ if (call.name !== "register_search") return null;
1892
+ const query = String(call.args?.query ?? "").trim().slice(0, 160);
1893
+ if (!query) return null;
1894
+ const { searchRegister: searchRegister2, registerNote: registerNote2 } = await import("./certificates-DZJJLRGU.js");
1895
+ const reg = await searchRegister2(db, query);
1896
+ 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.`;
1897
+ return { name: call.name, query, output };
1898
+ }
1899
+ function toolNote(r) {
1900
+ return `The ${r.name} tool returned, for the query "${r.query}":
1901
+ ${r.output}`;
1980
1902
  }
1981
1903
 
1982
1904
  // workers/worker_public/prompts/nameplate.md
@@ -2743,6 +2665,27 @@ Answer account questions from these records ONLY: name the record when you use i
2743
2665
  budget
2744
2666
  );
2745
2667
  await attachFigureImages(env, messages, usedHits, q.query);
2668
+ if (/certificate|certified|certification/i.test(q.query) && env.DB) {
2669
+ try {
2670
+ const probe = await env.AI.run(model, {
2671
+ messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}
2672
+
2673
+ ${q.query}` }],
2674
+ max_tokens: 300,
2675
+ reasoning_effort: "low"
2676
+ });
2677
+ const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
2678
+ const call = parseToolCall(probeText);
2679
+ if (call) {
2680
+ const result = await runTool(env.DB, call);
2681
+ if (result) {
2682
+ messages.push({ role: "system", content: toolNote(result) });
2683
+ console.log("agent-tool:", result.name, JSON.stringify(result.query), "->", result.output.slice(0, 80));
2684
+ }
2685
+ }
2686
+ } catch {
2687
+ }
2688
+ }
2746
2689
  if (withImage) {
2747
2690
  const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
2748
2691
  const last = messages[messages.length - 1];
@@ -0,0 +1,127 @@
1
+ import {
2
+ P
3
+ } from "./chunk-3FYJM7LH.js";
4
+
5
+ // workers/worker_public/src/certificates.ts
6
+ function isRegisterShaped(query) {
7
+ return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were|have|has|hold|holds|possess|carry|got)\b/i.test(query);
8
+ }
9
+ function queryFamily(query) {
10
+ const m = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(query);
11
+ return m ? `${m[1].toUpperCase()}${m[2].padStart(2, "0")}` : null;
12
+ }
13
+ function registerTokens(query) {
14
+ const stop = /* @__PURE__ */ new Set([
15
+ "the",
16
+ "a",
17
+ "an",
18
+ "is",
19
+ "are",
20
+ "was",
21
+ "were",
22
+ "still",
23
+ "currently",
24
+ "in",
25
+ "on",
26
+ "for",
27
+ "of",
28
+ "and",
29
+ "or",
30
+ "certificate",
31
+ "certificates",
32
+ "certified",
33
+ "certification",
34
+ "register",
35
+ "status",
36
+ "valid",
37
+ "suspended",
38
+ "revoked",
39
+ "recommendation",
40
+ "per",
41
+ "under",
42
+ "by",
43
+ "with",
44
+ "what",
45
+ "which",
46
+ "who",
47
+ "does",
48
+ "do",
49
+ "load",
50
+ "cell",
51
+ "model",
52
+ "manufacturer",
53
+ "holder",
54
+ "company",
55
+ "r",
56
+ "how",
57
+ "to",
58
+ "list",
59
+ "show"
60
+ ]);
61
+ const tokens = [];
62
+ const publisherWord = P().publisher.name.toLowerCase();
63
+ for (const w of query.split(/[^A-Za-z0-9&-]+/)) {
64
+ if (w.length < 2) continue;
65
+ if (stop.has(w.toLowerCase())) continue;
66
+ if (w.toLowerCase() === publisherWord) continue;
67
+ if (/^\d+$/.test(w) && w.length < 2) continue;
68
+ if (!tokens.includes(w)) tokens.push(w);
69
+ }
70
+ return tokens.slice(0, 6);
71
+ }
72
+ function buildRegisterQuery(tokens) {
73
+ const params = [];
74
+ const clauses = tokens.map((t) => {
75
+ if (/^\d+$/.test(t)) {
76
+ const padded = `% ${t.toLowerCase()} %`;
77
+ params.push(t, t, padded);
78
+ const n1 = params.length - 2;
79
+ const n2 = params.length;
80
+ return `(model = ?${n1} OR num = ?${n1} OR (' ' || REPLACE(REPLACE(REPLACE(LOWER(model), '-', ' '), ',', ' '), '.', ' ') || ' ') LIKE ?${n2})`;
81
+ }
82
+ params.push(`%${t}%`);
83
+ const n = params.length;
84
+ return `(holder LIKE ?${n} OR model LIKE ?${n} OR num LIKE ?${n})`;
85
+ });
86
+ return {
87
+ sql: `SELECT num, family, holder, model, year, status FROM certificates WHERE ${clauses.join(" OR ")} LIMIT 6`,
88
+ params
89
+ };
90
+ }
91
+ async function searchRegister(db, query) {
92
+ if (!isRegisterShaped(query)) return null;
93
+ const tokens = registerTokens(query);
94
+ if (!tokens.length) return null;
95
+ const built = buildRegisterQuery(tokens);
96
+ let { sql, params } = built;
97
+ const family = queryFamily(query);
98
+ if (family) {
99
+ sql = sql.replace(" WHERE ", " WHERE family = ?0 AND ");
100
+ params.unshift(family);
101
+ }
102
+ try {
103
+ const res = await db.prepare(sql).bind(...params).all();
104
+ return { rows: res.results ?? [], tokens };
105
+ } catch {
106
+ return { rows: [], tokens };
107
+ }
108
+ }
109
+ function registerNote(rows) {
110
+ if (!rows.length) {
111
+ return `Certificate register: NO certificate matching the asked holder or model appears in the ${P().publisher.name}-CS register snapshot. State plainly that no such certificate is in this register, and that the register is a snapshot rather than the live certification status.`;
112
+ }
113
+ const lines = rows.map((r) => `- ${r.num}: holder ${r.holder}, model "${r.model}"${r.year ? `, issued ${r.year}` : ""} \u2014 status ${r.status}`);
114
+ return [
115
+ "Certificate register \u2014 the following certificates matched the asked holder or model. Quote the certificate number, the holder and the status verbatim; the register is a snapshot, so qualify any statement about current certification accordingly.",
116
+ ...lines
117
+ ].join("\n");
118
+ }
119
+
120
+ export {
121
+ isRegisterShaped,
122
+ queryFamily,
123
+ registerTokens,
124
+ buildRegisterQuery,
125
+ searchRegister,
126
+ registerNote
127
+ };
@@ -0,0 +1,18 @@
1
+ export interface ToolCall {
2
+ name: string;
3
+ args: Record<string, unknown>;
4
+ }
5
+ export interface ToolResult {
6
+ name: string;
7
+ query: string;
8
+ output: string;
9
+ }
10
+ /** The strict protocol: a line `TOOL register_search {"query": "…"}`.
11
+ * Plain regex parse — no JSON mode needed, tolerant of surrounding
12
+ * prose (the model may talk around the call). */
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.";
15
+ export declare function runTool(db: any, call: ToolCall): Promise<ToolResult | null>;
16
+ /** The attributed injection: the answer model sees what the tool
17
+ * returned, for the string it asked — never a standing claim. */
18
+ export declare function toolNote(r: ToolResult): string;
@@ -1,6 +1,3 @@
1
- import {
2
- handleSearch
3
- } from "../../chunk-FLNX4O7C.js";
4
1
  import {
5
2
  bindModelNode,
6
3
  checkQuoteAnchors,
@@ -9,7 +6,10 @@ import {
9
6
  modelGroundingBlock,
10
7
  scoreFaithfulness,
11
8
  scoreJudge
12
- } from "../../chunk-ZVMQHCNL.js";
9
+ } from "../../chunk-MNEUEHO7.js";
10
+ import {
11
+ handleSearch
12
+ } from "../../chunk-FLNX4O7C.js";
13
13
  import {
14
14
  buildMessages,
15
15
  cfBlobs,
@@ -63,6 +63,7 @@ import {
63
63
  sha256Hex,
64
64
  today
65
65
  } from "../../chunk-2PCUAJJO.js";
66
+ import "../../chunk-Z6JAGZAM.js";
66
67
  import {
67
68
  P,
68
69
  setProfile
@@ -958,7 +959,7 @@ async function handleMcp(env, ctx, req, tier, key) {
958
959
  // stream:false forces the JSON lane (anon defaults to SSE)
959
960
  body: JSON.stringify({ ...args, stream: false })
960
961
  });
961
- const res = name === "ask" ? await (await import("../../ask-5XMD22SJ.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
962
+ const res = name === "ask" ? await (await import("../../ask-26DGSVFW.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-PBOODCMQ.js")).handleSearch(env, ctx, inner, tier, key);
962
963
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
963
964
  });
964
965
  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.56",
30
+ "version": "0.2.57",
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",
@@ -40,6 +40,7 @@ import { json, err, corsHeaders, readJson, validateQuery, type ApiKey } from "./
40
40
  import { clientIp, checkQuota, telemetry } from "./quota";
41
41
  import { graphExpand, editionNote } from "./graph";
42
42
  import { registerNote, searchRegister } from "./certificates";
43
+ import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
43
44
  import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
44
45
  import { P } from "./profile.ts";
45
46
 
@@ -1147,6 +1148,31 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1147
1148
  budget,
1148
1149
  );
1149
1150
  await attachFigureImages(env, messages, usedHits, q.query);
1151
+ // the agent loop (the owner's 2026-09-28 direction): ONE bounded tool
1152
+ // round, armed only where a tool can help. The model may call
1153
+ // register_search through the strict line protocol; the worker
1154
+ // executes it deterministically and the ATTRIBUTED result joins the
1155
+ // messages — what the tool returned, for the string it asked.
1156
+ if (/certificate|certified|certification/i.test(q.query) && env.DB) {
1157
+ try {
1158
+ const probe = await (env as any).AI.run(model, {
1159
+ messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}\n\n${q.query}` }],
1160
+ max_tokens: 300,
1161
+ reasoning_effort: "low",
1162
+ });
1163
+ const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
1164
+ const call = parseToolCall(probeText);
1165
+ if (call) {
1166
+ const result = await runTool(env.DB, call);
1167
+ if (result) {
1168
+ messages.push({ role: "system", content: toolNote(result) });
1169
+ console.log("agent-tool:", result.name, JSON.stringify(result.query), "->", result.output.slice(0, 80));
1170
+ }
1171
+ }
1172
+ } catch {
1173
+ /* the loop is optional — the answer proceeds without the tool */
1174
+ }
1175
+ }
1150
1176
  if (withImage) {
1151
1177
  // the user's own image rides on the question message — retrieval stays
1152
1178
  // text-driven; the answer model reads the image as question context.
@@ -0,0 +1,55 @@
1
+ // The agent interface's tool plane (the owner's 2026-09-28 direction): a
2
+ // bounded tool loop on the serving path. The model may invoke named
3
+ // tools through a strict line protocol; the worker executes each
4
+ // deterministically and the result rides back ATTRIBUTED — every tool
5
+ // output is phrased as what the tool returned, and the searched string
6
+ // is always visible in the answer. One round per ask, armed only where
7
+ // a tool can help (the certificate intent, v1).
8
+
9
+ export interface ToolCall {
10
+ name: string;
11
+ args: Record<string, unknown>;
12
+ }
13
+
14
+ export interface ToolResult {
15
+ name: string;
16
+ query: string;
17
+ output: string;
18
+ }
19
+
20
+ /** The strict protocol: a line `TOOL register_search {"query": "…"}`.
21
+ * Plain regex parse — no JSON mode needed, tolerant of surrounding
22
+ * prose (the model may talk around the call). */
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
25
+ if (!m) return null;
26
+ let args: Record<string, unknown> = {};
27
+ if (m[2]) {
28
+ try { args = JSON.parse(m[2]); } catch { return null; }
29
+ } else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
30
+ return null; // a malformed call (opened, never closed) is not a call
31
+ }
32
+ return { name: m[1].toLowerCase(), args };
33
+ }
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.`;
38
+
39
+ 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);
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 };
49
+ }
50
+
51
+ /** The attributed injection: the answer model sees what the tool
52
+ * returned, for the string it asked — never a standing claim. */
53
+ export function toolNote(r: ToolResult): string {
54
+ return `The ${r.name} tool returned, for the query "${r.query}":\n${r.output}`;
55
+ }