@konneal/engine 0.2.55 → 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-DKQXJN6G.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,84 @@ 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)\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);
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;
1963
1884
  }
1885
+ return { name: m[1].toLowerCase(), args };
1886
+ }
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}`;
1902
+ }
1903
+
1904
+ // workers/worker_public/prompts/nameplate.md
1905
+ var nameplate_default = `You read instrument nameplates. From the photograph, extract the
1906
+ manufacturer's name exactly as printed and the model designation exactly
1907
+ as printed. Answer with ONLY a JSON object on one line:
1908
+
1909
+ {"manufacturer": "\u2026", "model": "\u2026"}
1910
+
1911
+ If the photograph shows no manufacturer or no model, use null for the
1912
+ missing value. Never guess beyond what is printed.
1913
+ `;
1914
+
1915
+ // workers/worker_public/src/nameplate-parse.ts
1916
+ function parseNameplate(text) {
1917
+ const m = /\{[^{}]*\}/.exec(String(text ?? ""));
1918
+ if (!m) return null;
1964
1919
  try {
1965
- const res = await db.prepare(sql).bind(...params).all();
1966
- return { rows: res.results ?? [], tokens: tokens2 };
1920
+ const j = JSON.parse(m[0]);
1921
+ const clean = (v) => typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null;
1922
+ const out = { manufacturer: clean(j.manufacturer), model: clean(j.model) };
1923
+ return out.manufacturer || out.model ? out : null;
1967
1924
  } catch {
1968
- return { rows: [], tokens: tokens2 };
1925
+ return null;
1969
1926
  }
1970
1927
  }
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.`;
1928
+ function nameplateRegisterQuery(np, question) {
1929
+ const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
1930
+ return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
1931
+ }
1932
+
1933
+ // workers/worker_public/src/nameplate.ts
1934
+ async function extractNameplate(ai, model, image) {
1935
+ try {
1936
+ const res = await ai.run(model, {
1937
+ messages: [
1938
+ { role: "system", content: nameplate_default.trimEnd() },
1939
+ { role: "user", content: [{ type: "image_url", image_url: { url: image } }] }
1940
+ ],
1941
+ max_tokens: 512,
1942
+ reasoning_effort: "low"
1943
+ });
1944
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
1945
+ return parseNameplate(text ?? "");
1946
+ } catch {
1947
+ return null;
1974
1948
  }
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");
1980
1949
  }
1981
1950
 
1982
1951
  // workers/worker_public/src/ask.ts
@@ -2332,7 +2301,17 @@ async function handleAsk(env, ctx, req, tier, key) {
2332
2301
  console.log("understand:", understanding?.intent ?? "null", understanding?.doc_number ? `doc#${understanding.doc_number}${understanding.edition ? "@" + understanding.edition : ""}` : "nodoc", "|", q.query.slice(0, 50));
2333
2302
  const graphDocNumbers = await graphExpand(env, understanding);
2334
2303
  const eNote = await editionNote(env, understanding);
2335
- const register = await searchRegister(env.DB, q.query);
2304
+ let register = await searchRegister(env.DB, q.query);
2305
+ if (withImage && !register?.rows?.length && /certificate|certified|certification/i.test(q.query)) {
2306
+ const np = await extractNameplate(env.AI, MODELS.member, withImage);
2307
+ if (np?.manufacturer) {
2308
+ const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query));
2309
+ if (bridged?.rows?.length) {
2310
+ register = bridged;
2311
+ console.log("nameplate-bridge:", np.manufacturer, np.model ?? "", "\u2192", bridged.rows.length, "register rows");
2312
+ }
2313
+ }
2314
+ }
2336
2315
  console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
2337
2316
  const regNote = register ? registerNote(register.rows) : void 0;
2338
2317
  if (understanding?.intent === "conversational") {
@@ -2686,6 +2665,27 @@ Answer account questions from these records ONLY: name the record when you use i
2686
2665
  budget
2687
2666
  );
2688
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
+ }
2689
2689
  if (withImage) {
2690
2690
  const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
2691
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,8 @@
1
+ export interface Nameplate {
2
+ manufacturer: string | null;
3
+ model: string | null;
4
+ }
5
+ export declare function parseNameplate(text: string): Nameplate | null;
6
+ /** The register query the extraction feeds: the nameplate's tokens lead,
7
+ * the question's family (if named) scopes. */
8
+ export declare function nameplateRegisterQuery(np: Nameplate, question: string): string;
@@ -0,0 +1,5 @@
1
+ import { nameplateRegisterQuery, type Nameplate } from "./nameplate-parse.ts";
2
+ export type { Nameplate };
3
+ export { nameplateRegisterQuery };
4
+ export declare function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null>;
5
+ export declare const nameplateModel: () => "@cf/zai-org/glm-5.3-flash";
@@ -0,0 +1,8 @@
1
+ You read instrument nameplates. From the photograph, extract the
2
+ manufacturer's name exactly as printed and the model designation exactly
3
+ as printed. Answer with ONLY a JSON object on one line:
4
+
5
+ {"manufacturer": "…", "model": "…"}
6
+
7
+ If the photograph shows no manufacturer or no model, use null for the
8
+ missing value. Never guess beyond what is printed.
@@ -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-DKQXJN6G.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-PJBGJIXF.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.55",
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",
@@ -0,0 +1,8 @@
1
+ You read instrument nameplates. From the photograph, extract the
2
+ manufacturer's name exactly as printed and the model designation exactly
3
+ as printed. Answer with ONLY a JSON object on one line:
4
+
5
+ {"manufacturer": "…", "model": "…"}
6
+
7
+ If the photograph shows no manufacturer or no model, use null for the
8
+ missing value. Never guess beyond what is printed.
@@ -40,6 +40,8 @@ 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";
44
+ import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
43
45
  import { P } from "./profile.ts";
44
46
 
45
47
  /** User-uploaded image for multimodal questions: a data URL
@@ -619,7 +621,21 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
619
621
  // register-shaped question queries the D1 table directly and the rows
620
622
  // join the prompt as an authoritative note — exact matches, never a
621
623
  // similarity guess about certification standing
622
- const register = await searchRegister(env.DB, q.query);
624
+ let register = await searchRegister(env.DB, q.query);
625
+ // the nameplate bridge (TODO.new-era/6's image lane): a certificate
626
+ // question WITH a photograph carries its manufacturer in the pixels —
627
+ // one vision call extracts it, and those tokens re-query the register
628
+ // so the answer cites the actual certificate rows
629
+ if (withImage && !register?.rows?.length && /certificate|certified|certification/i.test(q.query)) {
630
+ const np = await extractNameplate((env as any).AI, MODELS.member, withImage);
631
+ if (np?.manufacturer) {
632
+ const bridged = await searchRegister(env.DB, nameplateRegisterQuery(np, q.query));
633
+ if (bridged?.rows?.length) {
634
+ register = bridged;
635
+ console.log("nameplate-bridge:", np.manufacturer, np.model ?? "", "→", bridged.rows.length, "register rows");
636
+ }
637
+ }
638
+ }
623
639
  console.log("register-search:", JSON.stringify({ shaped: !!register, tokens: register?.tokens ?? null, rows: register?.rows?.length ?? null, first: register?.rows?.[0]?.num ?? null }));
624
640
  const regNote = register ? registerNote(register.rows) : undefined;
625
641
 
@@ -1132,6 +1148,31 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1132
1148
  budget,
1133
1149
  );
1134
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
+ }
1135
1176
  if (withImage) {
1136
1177
  // the user's own image rides on the question message — retrieval stays
1137
1178
  // text-driven; the answer model reads the image as question context.
@@ -22,7 +22,7 @@ export interface RegisterRow {
22
22
  * trigger the citation-graph notes already use (query-shaped notes,
23
23
  * never a router). */
24
24
  export function isRegisterShaped(query: string): boolean {
25
- return /\bcertif(ied|icates?|ication)s?\b/i.test(query) && /\b(is|are|still|currently|valid|status|suspended|revoked|was|were)\b/i.test(query);
25
+ 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);
26
26
  }
27
27
 
28
28
  /** Search tokens: words that can identify a holder or a model — words of
@@ -0,0 +1,29 @@
1
+ // The nameplate bridge's pure half (TODO.new-era/6's image lane): parse
2
+ // the vision call's answer into {manufacturer, model}. Lives apart from
3
+ // the AI-call module so the plain-node suites can exercise it (the
4
+ // prompt is a bundled .md, which only esbuild can load).
5
+
6
+ export interface Nameplate {
7
+ manufacturer: string | null;
8
+ model: string | null;
9
+ }
10
+
11
+ export function parseNameplate(text: string): Nameplate | null {
12
+ const m = /\{[^{}]*\}/.exec(String(text ?? ""));
13
+ if (!m) return null;
14
+ try {
15
+ const j = JSON.parse(m[0]) as { manufacturer?: unknown; model?: unknown };
16
+ const clean = (v: unknown) => (typeof v === "string" && v.trim() ? v.trim().slice(0, 80) : null);
17
+ const out = { manufacturer: clean(j.manufacturer), model: clean(j.model) };
18
+ return out.manufacturer || out.model ? out : null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /** The register query the extraction feeds: the nameplate's tokens lead,
25
+ * the question's family (if named) scopes. */
26
+ export function nameplateRegisterQuery(np: Nameplate, question: string): string {
27
+ const fam = /\b([RDMB])\s?-?(\d{2,3})\b/i.exec(question);
28
+ return [np.manufacturer, np.model, fam ? `${fam[1]} ${fam[2]}` : null].filter(Boolean).join(" ");
29
+ }
@@ -0,0 +1,33 @@
1
+ // The nameplate bridge (TODO.new-era/6, the image lane): a certificate
2
+ // question that arrives WITH a photograph carries its manufacturer only
3
+ // in the pixels. A single low-effort vision call extracts the printed
4
+ // manufacturer and model, and those tokens feed the certificate
5
+ // register's D1 lane — so "does this load cell's manufacturer hold the
6
+ // certificate" answers with the actual register rows instead of advice.
7
+ // The shape rules are the 2026-09-09 multimodal findings: the image
8
+ // rides its OWN short user message, never a long passage.
9
+ import nameplatePrompt from "../prompts/nameplate.md";
10
+ import { parseNameplate, nameplateRegisterQuery, type Nameplate } from "./nameplate-parse.ts";
11
+ import { MODELS } from "./config.ts";
12
+
13
+ export type { Nameplate };
14
+ export { nameplateRegisterQuery };
15
+
16
+ export async function extractNameplate(ai: any, model: string, image: string): Promise<Nameplate | null> {
17
+ try {
18
+ const res: any = await ai.run(model, {
19
+ messages: [
20
+ { role: "system", content: nameplatePrompt.trimEnd() },
21
+ { role: "user", content: [{ type: "image_url", image_url: { url: image } }] },
22
+ ],
23
+ max_tokens: 512,
24
+ reasoning_effort: "low",
25
+ });
26
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
27
+ return parseNameplate(text ?? "");
28
+ } catch {
29
+ return null; // the bridge is optional — the ask never depends on it
30
+ }
31
+ }
32
+
33
+ export const nameplateModel = () => MODELS.member;
@@ -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
+ }