@konneal/engine 0.2.84 → 0.2.86
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-GVU5PYC4.js → ask-XM5FO6LR.js} +2 -1
- package/dist/chunk-GCK6DW7E.js +234 -0
- package/dist/{chunk-D24PQPDK.js → chunk-PIEUN5B2.js} +10 -99
- package/dist/tools-3AMOGBJG.js +14 -0
- package/dist/tools-content.d.ts +5 -0
- package/dist/tools.d.ts +20 -3
- package/dist/worker_public/src/index.js +20 -3
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +2 -2
- package/workers/worker_public/src/mcp-proto.ts +14 -0
- package/workers/worker_public/src/mcp.ts +7 -0
- package/workers/worker_public/src/tools-content.ts +139 -0
- package/workers/worker_public/src/tools.ts +56 -17
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// workers/worker_public/src/refs.ts
|
|
2
|
+
var REF = /\[\[(u:[A-Za-z0-9_-]+)\]\]/g;
|
|
3
|
+
function availableUnitIds(hits) {
|
|
4
|
+
const ids = /* @__PURE__ */ new Set();
|
|
5
|
+
for (const h of hits) {
|
|
6
|
+
const u = h.metadata?.unit_id;
|
|
7
|
+
if (u) ids.add(u);
|
|
8
|
+
}
|
|
9
|
+
return ids;
|
|
10
|
+
}
|
|
11
|
+
function parseRefs(text) {
|
|
12
|
+
return [...text.matchAll(REF)].map((m) => m[1]);
|
|
13
|
+
}
|
|
14
|
+
function sanitizeRefs(text, available) {
|
|
15
|
+
const dropped = [];
|
|
16
|
+
const out = text.replace(REF, (full, id) => {
|
|
17
|
+
if (available.has(id)) return full;
|
|
18
|
+
dropped.push(id);
|
|
19
|
+
return "";
|
|
20
|
+
});
|
|
21
|
+
return { text: out, dropped };
|
|
22
|
+
}
|
|
23
|
+
async function resolveBlocks(db, refs) {
|
|
24
|
+
if (!refs.length) return [];
|
|
25
|
+
const uniq = [...new Set(refs)].slice(0, 12);
|
|
26
|
+
const blocks = [];
|
|
27
|
+
for (let i = 0; i < uniq.length; i += 20) {
|
|
28
|
+
const batch = uniq.slice(i, i + 20);
|
|
29
|
+
const placeholders = batch.map((_, n) => `?${n + 1}`).join(",");
|
|
30
|
+
try {
|
|
31
|
+
const res = await db.prepare(`SELECT unit_id, type, docidentifier, edition, payload FROM unit_payloads WHERE unit_id IN (${placeholders})`).bind(...batch).all();
|
|
32
|
+
for (const r of res.results) {
|
|
33
|
+
let payload = {};
|
|
34
|
+
try {
|
|
35
|
+
payload = JSON.parse(String(r.payload));
|
|
36
|
+
} catch {
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
blocks.push({
|
|
40
|
+
unit_id: String(r.unit_id),
|
|
41
|
+
type: String(r.type),
|
|
42
|
+
docidentifier: String(r.docidentifier ?? ""),
|
|
43
|
+
edition: r.edition ? String(r.edition) : void 0,
|
|
44
|
+
payload
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
} catch (e) {
|
|
48
|
+
console.log("resolveBlocks failed:", String(e).slice(0, 150));
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return blocks;
|
|
52
|
+
}
|
|
53
|
+
async function contractV2(db, answer, usedHits) {
|
|
54
|
+
const available = availableUnitIds(usedHits);
|
|
55
|
+
const { text, dropped } = sanitizeRefs(answer, available);
|
|
56
|
+
if (dropped.length) console.log("refs: dropped", dropped.length, "invalid (not in passages)");
|
|
57
|
+
const refs = parseRefs(text);
|
|
58
|
+
const blocks = await resolveBlocks(db, refs);
|
|
59
|
+
return { text, blocks, dropped };
|
|
60
|
+
}
|
|
61
|
+
function tableRetyped(text, availableTable) {
|
|
62
|
+
if (!availableTable) return false;
|
|
63
|
+
return /(^|\n)\s*\|[^\n]+\|\s*(\n\s*\|[-: |]+\|\s*)?(\n|$)/.test(text) && (text.match(/\|/g) ?? []).length >= 6;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// workers/worker_public/src/tools-content.ts
|
|
67
|
+
var slug = (docidentifier) => docidentifier.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
68
|
+
var unitsGet = {
|
|
69
|
+
name: "units.get",
|
|
70
|
+
description: "Fetch typed Recommendation content \u2014 a table, formula or figure as structured data (columns, rows, MathML, caption), by its unit id. The machine-consumable form of a Recommendation's content, for computing with rather than reading.",
|
|
71
|
+
params: [{ key: "unit_id", required: true, description: "the unit id, e.g. u:table-1 or u:form-3 (ids appear in answers' [[u:\u2026]] references)" }],
|
|
72
|
+
audiences: ["mcp"],
|
|
73
|
+
handler: async (env, args) => {
|
|
74
|
+
const unit_id = String(args?.unit_id ?? "").trim().replace(/^\[?\[?u:/, "u:").replace(/]?\]?$/, "");
|
|
75
|
+
if (!/^u:[\w.-]+$/.test(unit_id)) return null;
|
|
76
|
+
const blocks = await resolveBlocks(env.DB, [unit_id]);
|
|
77
|
+
if (!blocks.length) {
|
|
78
|
+
return { name: "units.get", query: unit_id, output: `No typed unit "${unit_id}" exists in the indexed payloads. State this plainly.` };
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
name: "units.get",
|
|
82
|
+
query: unit_id,
|
|
83
|
+
output: JSON.stringify(
|
|
84
|
+
blocks.map((b) => ({ unit_id: b.unit_id, type: b.type, docidentifier: b.docidentifier, payload: b.payload }))
|
|
85
|
+
)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
var graphCites = {
|
|
90
|
+
name: "graph.cites",
|
|
91
|
+
description: "The bibliography edges: what a publication's own bibliography actually cites, extracted from its indexed bibliography sections. Returns the cited works' labels for a document (optionally scoped to an edition).",
|
|
92
|
+
params: [
|
|
93
|
+
{ key: "doc", required: true, description: "the publication identifier, e.g. R 60-1:2021" },
|
|
94
|
+
{ key: "edition", required: false, description: "an edition year to scope, e.g. 2021" }
|
|
95
|
+
],
|
|
96
|
+
audiences: ["mcp"],
|
|
97
|
+
handler: async (env, args) => {
|
|
98
|
+
const doc = String(args?.doc ?? "").trim();
|
|
99
|
+
if (doc.length < 3) return null;
|
|
100
|
+
const edition = args?.edition ? String(args.edition).trim() : null;
|
|
101
|
+
const pat = `%${doc.replace(/[:\s]+/g, "%")}%${edition ? edition : ""}%`;
|
|
102
|
+
try {
|
|
103
|
+
const rows = await env.DB.prepare(
|
|
104
|
+
"SELECT DISTINCT n.label AS label FROM graph_edges e JOIN documents d ON e.src = d.canonical_id JOIN graph_nodes n ON e.dst = n.id WHERE d.docidentifier LIKE ?1 AND e.kind = 'cites' ORDER BY label LIMIT 60"
|
|
105
|
+
).bind(pat).all();
|
|
106
|
+
const labels = (rows.results ?? []).map((r) => r.label).filter(Boolean);
|
|
107
|
+
return {
|
|
108
|
+
name: "graph.cites",
|
|
109
|
+
query: `${doc}${edition ? ` (${edition})` : ""}`,
|
|
110
|
+
output: labels.length ? `The publication's bibliography cites:
|
|
111
|
+
${labels.map((l) => `- ${l}`).join("\n")}` : `No bibliography citations are indexed for "${doc}"${edition ? ` (${edition})` : ""}. State this plainly.`
|
|
112
|
+
};
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
var docsSection = {
|
|
119
|
+
name: "docs.section",
|
|
120
|
+
description: "Fetch one clause of a publication's rendered document \u2014 its text and its permanent deep-link URL \u2014 for agents that need the human-formatted passage with its anchor.",
|
|
121
|
+
params: [
|
|
122
|
+
{ key: "doc", required: true, description: "the publication identifier, e.g. R 60-1:2021" },
|
|
123
|
+
{ key: "clause", required: true, description: "the clause anchor, e.g. 5.1.1 or annex-b" }
|
|
124
|
+
],
|
|
125
|
+
audiences: ["mcp"],
|
|
126
|
+
handler: async (env, args) => {
|
|
127
|
+
const doc = String(args?.doc ?? "").trim();
|
|
128
|
+
const clause = String(args?.clause ?? "").trim();
|
|
129
|
+
if (doc.length < 3 || !clause) return null;
|
|
130
|
+
try {
|
|
131
|
+
const rows = await env.DB.prepare(
|
|
132
|
+
"SELECT docidentifier, clause_anchor, clause_title, text FROM chunks WHERE docidentifier LIKE ?1 AND REPLACE(clause_anchor, ' ', '') = ?2 LIMIT 1"
|
|
133
|
+
).bind(`%${doc.replace(/[:\s]+/g, "%")}%`, clause).all();
|
|
134
|
+
const r = (rows.results ?? [])[0];
|
|
135
|
+
if (!r) {
|
|
136
|
+
return { name: "docs.section", query: `${doc} \xA7${clause}`, output: `No clause "${clause}" is indexed for "${doc}". State this plainly.` };
|
|
137
|
+
}
|
|
138
|
+
const url = `https://www.ommisa.org/docs/${slug(r.docidentifier)}.html#${r.clause_anchor}`;
|
|
139
|
+
return {
|
|
140
|
+
name: "docs.section",
|
|
141
|
+
query: `${r.docidentifier} \xA7${r.clause_anchor}`,
|
|
142
|
+
output: JSON.stringify({ docidentifier: r.docidentifier, clause: r.clause_anchor, title: r.clause_title, url, text: (r.text ?? "").slice(0, 4e3) })
|
|
143
|
+
};
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
var glossaryLookup = {
|
|
150
|
+
name: "glossary.lookup",
|
|
151
|
+
description: "Look up a defined term in the terminology datasets \u2014 the concept's definition and its defining publication. Bind everyday words to the defined term before interpreting a question.",
|
|
152
|
+
params: [{ key: "term", required: true, description: "the term or everyday phrase to bind, e.g. creep or load cell" }],
|
|
153
|
+
audiences: ["mcp"],
|
|
154
|
+
handler: async (env, args) => {
|
|
155
|
+
const term = String(args?.term ?? "").trim();
|
|
156
|
+
if (term.length < 2) return null;
|
|
157
|
+
try {
|
|
158
|
+
const rows = await env.DB.prepare(
|
|
159
|
+
"SELECT clause_title AS term, chunk_text AS text, docidentifier FROM chunks WHERE corpus = 'glossary' AND (clause_title = ?1 OR clause_title LIKE ?2) LIMIT 3"
|
|
160
|
+
).bind(term, `%${term}%`).all();
|
|
161
|
+
const hits = (rows.results ?? []).filter((r) => r.term);
|
|
162
|
+
if (!hits.length) {
|
|
163
|
+
return { name: "glossary.lookup", query: term, output: `No defined term matches "${term}" in the terminology datasets. State this plainly.` };
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
name: "glossary.lookup",
|
|
167
|
+
query: term,
|
|
168
|
+
output: hits.map((r) => `- ${r.term} (${r.docidentifier}): ${String(r.text ?? "").split(" \u2014 ").slice(1).join(" \u2014 ").slice(0, 400)}`).join("\n")
|
|
169
|
+
};
|
|
170
|
+
} catch {
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// workers/worker_public/src/tools.ts
|
|
177
|
+
function parseToolCall(text) {
|
|
178
|
+
const m = /^TOOL\s+([a-z_.]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
|
|
179
|
+
if (!m) return null;
|
|
180
|
+
let args = {};
|
|
181
|
+
if (m[2]) {
|
|
182
|
+
try {
|
|
183
|
+
args = JSON.parse(m[2]);
|
|
184
|
+
} catch {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
} else if (/^TOOL\s+[a-z_.]+\s*\{/m.test(String(text ?? ""))) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
return { name: m[1].toLowerCase(), args };
|
|
191
|
+
}
|
|
192
|
+
var certificatesSearch = {
|
|
193
|
+
name: "certificates.search",
|
|
194
|
+
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.",
|
|
195
|
+
params: [{ key: "query", required: true, description: "the holder, model, or printed certificate number to look up" }],
|
|
196
|
+
audiences: ["agent", "mcp"],
|
|
197
|
+
handler: async (env, args) => {
|
|
198
|
+
const query = String(args?.query ?? "").trim().slice(0, 160);
|
|
199
|
+
if (!query) return null;
|
|
200
|
+
const { searchRegister, registerNote, certificateLinks } = await import("./certificates-CO7KX2S2.js");
|
|
201
|
+
const reg = await searchRegister(env.DB, query, true);
|
|
202
|
+
const output = reg?.rows?.length ? [registerNote(reg.rows), certificateLinks(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.`;
|
|
203
|
+
return { name: "certificates.search", query, output };
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
var TOOLS_REGISTRY = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup];
|
|
207
|
+
var TOOL_DECLARATION = [
|
|
208
|
+
"You may use one tool before answering, by writing a single line:",
|
|
209
|
+
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
|
|
210
|
+
const shape = `{${t.params.map((p) => `"${p.key}": "<${p.description}>"`).join(", ")}}`;
|
|
211
|
+
return `TOOL ${t.name} ${shape}`;
|
|
212
|
+
}),
|
|
213
|
+
"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."
|
|
214
|
+
].join("\n");
|
|
215
|
+
async function runTool(env, call) {
|
|
216
|
+
const spec = TOOLS_REGISTRY.find((t) => t.name === call.name && t.audiences.includes("agent"));
|
|
217
|
+
if (!spec) return null;
|
|
218
|
+
return spec.handler(env, call.args ?? {});
|
|
219
|
+
}
|
|
220
|
+
function toolNote(r) {
|
|
221
|
+
return `The ${r.name} tool returned, for the query "${r.query}":
|
|
222
|
+
${r.output}`;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export {
|
|
226
|
+
resolveBlocks,
|
|
227
|
+
contractV2,
|
|
228
|
+
tableRetyped,
|
|
229
|
+
parseToolCall,
|
|
230
|
+
TOOLS_REGISTRY,
|
|
231
|
+
TOOL_DECLARATION,
|
|
232
|
+
runTool,
|
|
233
|
+
toolNote
|
|
234
|
+
};
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import {
|
|
2
|
+
TOOL_DECLARATION,
|
|
3
|
+
contractV2,
|
|
4
|
+
parseToolCall,
|
|
5
|
+
resolveBlocks,
|
|
6
|
+
runTool,
|
|
7
|
+
tableRetyped,
|
|
8
|
+
toolNote
|
|
9
|
+
} from "./chunk-GCK6DW7E.js";
|
|
1
10
|
import {
|
|
2
11
|
NO_CONTEXT,
|
|
3
12
|
answerQuality,
|
|
@@ -363,71 +372,6 @@ function checkQuoteAnchors(answer, passages) {
|
|
|
363
372
|
}
|
|
364
373
|
var ANCHOR_CORRECTION_NOTE = "Correction notice: your draft quoted text that does not appear verbatim in the provided passages. Rewrite the answer \u2014 every quoted phrase must be an exact copy from a passage, or cite the clause without quoting.";
|
|
365
374
|
|
|
366
|
-
// workers/worker_public/src/refs.ts
|
|
367
|
-
var REF = /\[\[(u:[A-Za-z0-9_-]+)\]\]/g;
|
|
368
|
-
function availableUnitIds(hits) {
|
|
369
|
-
const ids = /* @__PURE__ */ new Set();
|
|
370
|
-
for (const h of hits) {
|
|
371
|
-
const u = h.metadata?.unit_id;
|
|
372
|
-
if (u) ids.add(u);
|
|
373
|
-
}
|
|
374
|
-
return ids;
|
|
375
|
-
}
|
|
376
|
-
function parseRefs(text) {
|
|
377
|
-
return [...text.matchAll(REF)].map((m) => m[1]);
|
|
378
|
-
}
|
|
379
|
-
function sanitizeRefs(text, available) {
|
|
380
|
-
const dropped = [];
|
|
381
|
-
const out = text.replace(REF, (full, id) => {
|
|
382
|
-
if (available.has(id)) return full;
|
|
383
|
-
dropped.push(id);
|
|
384
|
-
return "";
|
|
385
|
-
});
|
|
386
|
-
return { text: out, dropped };
|
|
387
|
-
}
|
|
388
|
-
async function resolveBlocks(db, refs) {
|
|
389
|
-
if (!refs.length) return [];
|
|
390
|
-
const uniq = [...new Set(refs)].slice(0, 12);
|
|
391
|
-
const blocks = [];
|
|
392
|
-
for (let i = 0; i < uniq.length; i += 20) {
|
|
393
|
-
const batch = uniq.slice(i, i + 20);
|
|
394
|
-
const placeholders = batch.map((_, n) => `?${n + 1}`).join(",");
|
|
395
|
-
try {
|
|
396
|
-
const res = await db.prepare(`SELECT unit_id, type, docidentifier, edition, payload FROM unit_payloads WHERE unit_id IN (${placeholders})`).bind(...batch).all();
|
|
397
|
-
for (const r of res.results) {
|
|
398
|
-
let payload = {};
|
|
399
|
-
try {
|
|
400
|
-
payload = JSON.parse(String(r.payload));
|
|
401
|
-
} catch {
|
|
402
|
-
continue;
|
|
403
|
-
}
|
|
404
|
-
blocks.push({
|
|
405
|
-
unit_id: String(r.unit_id),
|
|
406
|
-
type: String(r.type),
|
|
407
|
-
docidentifier: String(r.docidentifier ?? ""),
|
|
408
|
-
edition: r.edition ? String(r.edition) : void 0,
|
|
409
|
-
payload
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
} catch (e) {
|
|
413
|
-
console.log("resolveBlocks failed:", String(e).slice(0, 150));
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
return blocks;
|
|
417
|
-
}
|
|
418
|
-
async function contractV2(db, answer, usedHits) {
|
|
419
|
-
const available = availableUnitIds(usedHits);
|
|
420
|
-
const { text, dropped } = sanitizeRefs(answer, available);
|
|
421
|
-
if (dropped.length) console.log("refs: dropped", dropped.length, "invalid (not in passages)");
|
|
422
|
-
const refs = parseRefs(text);
|
|
423
|
-
const blocks = await resolveBlocks(db, refs);
|
|
424
|
-
return { text, blocks, dropped };
|
|
425
|
-
}
|
|
426
|
-
function tableRetyped(text, availableTable) {
|
|
427
|
-
if (!availableTable) return false;
|
|
428
|
-
return /(^|\n)\s*\|[^\n]+\|\s*(\n\s*\|[-: |]+\|\s*)?(\n|$)/.test(text) && (text.match(/\|/g) ?? []).length >= 6;
|
|
429
|
-
}
|
|
430
|
-
|
|
431
375
|
// workers/worker_public/src/completion.ts
|
|
432
376
|
async function completeTables(db, answer, used) {
|
|
433
377
|
const blocks = [];
|
|
@@ -1873,39 +1817,6 @@ function exactCacheKey(indexVersion, gen, ns, queryHash) {
|
|
|
1873
1817
|
return `a:${indexVersion}:g${gen}:${ns}:${queryHash}`;
|
|
1874
1818
|
}
|
|
1875
1819
|
|
|
1876
|
-
// workers/worker_public/src/tools.ts
|
|
1877
|
-
function parseToolCall(text) {
|
|
1878
|
-
const m = /^TOOL\s+([a-z_]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? ""));
|
|
1879
|
-
if (!m) return null;
|
|
1880
|
-
let args = {};
|
|
1881
|
-
if (m[2]) {
|
|
1882
|
-
try {
|
|
1883
|
-
args = JSON.parse(m[2]);
|
|
1884
|
-
} catch {
|
|
1885
|
-
return null;
|
|
1886
|
-
}
|
|
1887
|
-
} else if (/^TOOL\s+[a-z_]+\s*\{/m.test(String(text ?? ""))) {
|
|
1888
|
-
return null;
|
|
1889
|
-
}
|
|
1890
|
-
return { name: m[1].toLowerCase(), args };
|
|
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.`;
|
|
1895
|
-
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 };
|
|
1903
|
-
}
|
|
1904
|
-
function toolNote(r) {
|
|
1905
|
-
return `The ${r.name} tool returned, for the query "${r.query}":
|
|
1906
|
-
${r.output}`;
|
|
1907
|
-
}
|
|
1908
|
-
|
|
1909
1820
|
// workers/worker_public/src/operations.ts
|
|
1910
1821
|
function isOperationIntent(query) {
|
|
1911
1822
|
return /\b(operations?|endpoints?)\b/i.test(query);
|
|
@@ -2900,7 +2811,7 @@ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
|
|
|
2900
2811
|
const call = parseToolCall(probeText);
|
|
2901
2812
|
if (call) {
|
|
2902
2813
|
mark(`Searching the certificate register for \u201C${String(call.args?.query ?? "")}\u201D`);
|
|
2903
|
-
const result = await runTool(env
|
|
2814
|
+
const result = await runTool(env, call);
|
|
2904
2815
|
if (result) {
|
|
2905
2816
|
messages.push({ role: "system", content: toolNote(result) });
|
|
2906
2817
|
pendingTools.push(result);
|
package/dist/tools.d.ts
CHANGED
|
@@ -7,12 +7,29 @@ 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
|
|
15
|
-
|
|
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: (env: 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. */
|
|
32
|
+
export declare function runTool(env: 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. */
|
|
18
35
|
export declare function toolNote(r: ToolResult): string;
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
modelGroundingBlock,
|
|
7
7
|
scoreFaithfulness,
|
|
8
8
|
scoreJudge
|
|
9
|
-
} from "../../chunk-
|
|
9
|
+
} from "../../chunk-PIEUN5B2.js";
|
|
10
|
+
import {
|
|
11
|
+
TOOLS_REGISTRY
|
|
12
|
+
} from "../../chunk-GCK6DW7E.js";
|
|
10
13
|
import {
|
|
11
14
|
handleSearch
|
|
12
15
|
} from "../../chunk-KO6DRI7C.js";
|
|
@@ -922,7 +925,16 @@ var TOOLS = [
|
|
|
922
925
|
},
|
|
923
926
|
required: ["query"]
|
|
924
927
|
}
|
|
925
|
-
}
|
|
928
|
+
},
|
|
929
|
+
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("mcp")).map((t) => ({
|
|
930
|
+
name: t.name,
|
|
931
|
+
description: t.description,
|
|
932
|
+
inputSchema: {
|
|
933
|
+
type: "object",
|
|
934
|
+
properties: Object.fromEntries(t.params.map((p) => [p.key, { type: "string", description: p.description }])),
|
|
935
|
+
required: t.params.filter((p) => p.required).map((p) => p.key)
|
|
936
|
+
}
|
|
937
|
+
}))
|
|
926
938
|
];
|
|
927
939
|
function dispatch(method, params, callTool) {
|
|
928
940
|
switch (method) {
|
|
@@ -959,7 +971,12 @@ async function handleMcp(env, ctx, req, tier, key) {
|
|
|
959
971
|
// stream:false forces the JSON lane (anon defaults to SSE)
|
|
960
972
|
body: JSON.stringify({ ...args, stream: false })
|
|
961
973
|
});
|
|
962
|
-
const
|
|
974
|
+
const { runTool, TOOLS_REGISTRY: TOOLS_REGISTRY2 } = await import("../../tools-3AMOGBJG.js");
|
|
975
|
+
if (TOOLS_REGISTRY2.some((t) => t.name === name && t.audiences.includes("mcp"))) {
|
|
976
|
+
const r = await runTool({ ...env, DB: env.DB }, { name, args });
|
|
977
|
+
return r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
978
|
+
}
|
|
979
|
+
const res = name === "ask" ? await (await import("../../ask-XM5FO6LR.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
|
|
963
980
|
return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
964
981
|
});
|
|
965
982
|
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.86",
|
|
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
|
-
//
|
|
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
|
|
@@ -1253,7 +1253,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1253
1253
|
const call = parseToolCall(probeText);
|
|
1254
1254
|
if (call) {
|
|
1255
1255
|
mark(`Searching the certificate register for “${String(call.args?.query ?? "")}”`);
|
|
1256
|
-
const result = await runTool(env
|
|
1256
|
+
const result = await runTool(env, call);
|
|
1257
1257
|
if (result) {
|
|
1258
1258
|
messages.push({ role: "system", content: toolNote(result) });
|
|
1259
1259
|
pendingTools.push(result);
|
|
@@ -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 =
|
|
@@ -32,6 +32,13 @@ export async function handleMcp(
|
|
|
32
32
|
});
|
|
33
33
|
// deferred so plain node can load mcp-proto without the handlers'
|
|
34
34
|
// .md prompt imports, which only the bundler resolves
|
|
35
|
+
// registry tools (certificates.search, units.get, …) dispatch through
|
|
36
|
+
// the SAME handlers the API would — the no-drift rule, now structural
|
|
37
|
+
const { runTool, TOOLS_REGISTRY } = await import("./tools.ts");
|
|
38
|
+
if (TOOLS_REGISTRY.some((t) => t.name === name && t.audiences.includes("mcp"))) {
|
|
39
|
+
const r = await runTool({ ...env, DB: (env as any).DB }, { name, args });
|
|
40
|
+
return r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
41
|
+
}
|
|
35
42
|
const res = name === "ask"
|
|
36
43
|
? await (await import("./ask")).handleAsk(env, ctx as any, inner, tier, key)
|
|
37
44
|
: await (await import("./search")).handleSearch(env, ctx as any, inner, tier, key);
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// The content-plane MCP tools (the owner's 2026-10-04 direction): a
|
|
2
|
+
// content plane earns a tool when its answer is MACHINE-SHAPED —
|
|
3
|
+
// structured data an agent computes with, not prose it re-reads. Every
|
|
4
|
+
// handler wraps the SAME query surface the API serves; entitlement
|
|
5
|
+
// scope rides the metadata the serving path already enforces. These are
|
|
6
|
+
// MCP-audience only: the agent path's declaration stays small because
|
|
7
|
+
// tool-selection quality degrades with menu size.
|
|
8
|
+
import type { ToolSpec } from "./tools.ts";
|
|
9
|
+
import { resolveBlocks } from "./refs.ts";
|
|
10
|
+
|
|
11
|
+
const slug = (docidentifier: string) =>
|
|
12
|
+
docidentifier
|
|
13
|
+
.toLowerCase()
|
|
14
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
15
|
+
.replace(/^-+|-+$/g, "");
|
|
16
|
+
|
|
17
|
+
export const unitsGet: ToolSpec = {
|
|
18
|
+
name: "units.get",
|
|
19
|
+
description:
|
|
20
|
+
"Fetch typed Recommendation content — a table, formula or figure as structured data (columns, rows, MathML, caption), by its unit id. The machine-consumable form of a Recommendation's content, for computing with rather than reading.",
|
|
21
|
+
params: [{ key: "unit_id", required: true, description: "the unit id, e.g. u:table-1 or u:form-3 (ids appear in answers' [[u:…]] references)" }],
|
|
22
|
+
audiences: ["mcp"],
|
|
23
|
+
handler: async (env, args) => {
|
|
24
|
+
const unit_id = String(args?.unit_id ?? "").trim().replace(/^\[?\[?u:/, "u:").replace(/]?\]?$/, "");
|
|
25
|
+
if (!/^u:[\w.-]+$/.test(unit_id)) return null;
|
|
26
|
+
const blocks = await resolveBlocks(env.DB, [unit_id]);
|
|
27
|
+
if (!blocks.length) {
|
|
28
|
+
return { name: "units.get", query: unit_id, output: `No typed unit "${unit_id}" exists in the indexed payloads. State this plainly.` };
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
name: "units.get",
|
|
32
|
+
query: unit_id,
|
|
33
|
+
output: JSON.stringify(
|
|
34
|
+
blocks.map((b: any) => ({ unit_id: b.unit_id, type: b.type, docidentifier: b.docidentifier, payload: b.payload })),
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const graphCites: ToolSpec = {
|
|
41
|
+
name: "graph.cites",
|
|
42
|
+
description:
|
|
43
|
+
"The bibliography edges: what a publication's own bibliography actually cites, extracted from its indexed bibliography sections. Returns the cited works' labels for a document (optionally scoped to an edition).",
|
|
44
|
+
params: [
|
|
45
|
+
{ key: "doc", required: true, description: "the publication identifier, e.g. R 60-1:2021" },
|
|
46
|
+
{ key: "edition", required: false, description: "an edition year to scope, e.g. 2021" },
|
|
47
|
+
],
|
|
48
|
+
audiences: ["mcp"],
|
|
49
|
+
handler: async (env, args) => {
|
|
50
|
+
const doc = String(args?.doc ?? "").trim();
|
|
51
|
+
if (doc.length < 3) return null;
|
|
52
|
+
const edition = args?.edition ? String(args.edition).trim() : null;
|
|
53
|
+
const pat = `%${doc.replace(/[:\s]+/g, "%")}%${edition ? edition : ""}%`;
|
|
54
|
+
try {
|
|
55
|
+
const rows = await env.DB.prepare(
|
|
56
|
+
"SELECT DISTINCT n.label AS label FROM graph_edges e JOIN documents d ON e.src = d.canonical_id JOIN graph_nodes n ON e.dst = n.id WHERE d.docidentifier LIKE ?1 AND e.kind = 'cites' ORDER BY label LIMIT 60",
|
|
57
|
+
)
|
|
58
|
+
.bind(pat)
|
|
59
|
+
.all();
|
|
60
|
+
const labels = ((rows.results ?? []) as { label: string }[]).map((r) => r.label).filter(Boolean);
|
|
61
|
+
return {
|
|
62
|
+
name: "graph.cites",
|
|
63
|
+
query: `${doc}${edition ? ` (${edition})` : ""}`,
|
|
64
|
+
output: labels.length
|
|
65
|
+
? `The publication's bibliography cites:\n${labels.map((l) => `- ${l}`).join("\n")}`
|
|
66
|
+
: `No bibliography citations are indexed for "${doc}"${edition ? ` (${edition})` : ""}. State this plainly.`,
|
|
67
|
+
};
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const docsSection: ToolSpec = {
|
|
75
|
+
name: "docs.section",
|
|
76
|
+
description:
|
|
77
|
+
"Fetch one clause of a publication's rendered document — its text and its permanent deep-link URL — for agents that need the human-formatted passage with its anchor.",
|
|
78
|
+
params: [
|
|
79
|
+
{ key: "doc", required: true, description: "the publication identifier, e.g. R 60-1:2021" },
|
|
80
|
+
{ key: "clause", required: true, description: "the clause anchor, e.g. 5.1.1 or annex-b" },
|
|
81
|
+
],
|
|
82
|
+
audiences: ["mcp"],
|
|
83
|
+
handler: async (env, args) => {
|
|
84
|
+
const doc = String(args?.doc ?? "").trim();
|
|
85
|
+
const clause = String(args?.clause ?? "").trim();
|
|
86
|
+
if (doc.length < 3 || !clause) return null;
|
|
87
|
+
try {
|
|
88
|
+
const rows = await env.DB.prepare(
|
|
89
|
+
"SELECT docidentifier, clause_anchor, clause_title, text FROM chunks WHERE docidentifier LIKE ?1 AND REPLACE(clause_anchor, ' ', '') = ?2 LIMIT 1",
|
|
90
|
+
)
|
|
91
|
+
.bind(`%${doc.replace(/[:\s]+/g, "%")}%`, clause)
|
|
92
|
+
.all();
|
|
93
|
+
const r = (rows.results ?? [])[0] as { docidentifier: string; clause_anchor: string; clause_title: string; text: string } | undefined;
|
|
94
|
+
if (!r) {
|
|
95
|
+
return { name: "docs.section", query: `${doc} §${clause}`, output: `No clause "${clause}" is indexed for "${doc}". State this plainly.` };
|
|
96
|
+
}
|
|
97
|
+
const url = `https://www.ommisa.org/docs/${slug(r.docidentifier)}.html#${r.clause_anchor}`;
|
|
98
|
+
return {
|
|
99
|
+
name: "docs.section",
|
|
100
|
+
query: `${r.docidentifier} §${r.clause_anchor}`,
|
|
101
|
+
output: JSON.stringify({ docidentifier: r.docidentifier, clause: r.clause_anchor, title: r.clause_title, url, text: (r.text ?? "").slice(0, 4000) }),
|
|
102
|
+
};
|
|
103
|
+
} catch {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
export const glossaryLookup: ToolSpec = {
|
|
110
|
+
name: "glossary.lookup",
|
|
111
|
+
description:
|
|
112
|
+
"Look up a defined term in the terminology datasets — the concept's definition and its defining publication. Bind everyday words to the defined term before interpreting a question.",
|
|
113
|
+
params: [{ key: "term", required: true, description: "the term or everyday phrase to bind, e.g. creep or load cell" }],
|
|
114
|
+
audiences: ["mcp"],
|
|
115
|
+
handler: async (env, args) => {
|
|
116
|
+
const term = String(args?.term ?? "").trim();
|
|
117
|
+
if (term.length < 2) return null;
|
|
118
|
+
try {
|
|
119
|
+
const rows = await env.DB.prepare(
|
|
120
|
+
"SELECT clause_title AS term, chunk_text AS text, docidentifier FROM chunks WHERE corpus = 'glossary' AND (clause_title = ?1 OR clause_title LIKE ?2) LIMIT 3",
|
|
121
|
+
)
|
|
122
|
+
.bind(term, `%${term}%`)
|
|
123
|
+
.all();
|
|
124
|
+
const hits = ((rows.results ?? []) as { term: string; text: string; docidentifier: string }[]).filter((r) => r.term);
|
|
125
|
+
if (!hits.length) {
|
|
126
|
+
return { name: "glossary.lookup", query: term, output: `No defined term matches "${term}" in the terminology datasets. State this plainly.` };
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
name: "glossary.lookup",
|
|
130
|
+
query: term,
|
|
131
|
+
output: hits
|
|
132
|
+
.map((r) => `- ${r.term} (${r.docidentifier}): ${String(r.text ?? "").split(" — ").slice(1).join(" — ").slice(0, 400)}`)
|
|
133
|
+
.join("\n"),
|
|
134
|
+
};
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
};
|
|
@@ -17,35 +17,74 @@ 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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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: (env: 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 (env, 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(env.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
|
+
import { unitsGet, graphCites, docsSection, glossaryLookup } from "./tools-content.ts";
|
|
68
|
+
|
|
69
|
+
export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup];
|
|
70
|
+
|
|
71
|
+
/** The agent loop's protocol declaration, GENERATED from the registry —
|
|
72
|
+
* the model's tool vocabulary is always the registry's, never a
|
|
73
|
+
* hand-maintained literal. */
|
|
74
|
+
export const TOOL_DECLARATION = [
|
|
75
|
+
"You may use one tool before answering, by writing a single line:",
|
|
76
|
+
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
|
|
77
|
+
const shape = `{${t.params.map((p) => `"${p.key}": "<${p.description}>"`).join(", ")}}`;
|
|
78
|
+
return `TOOL ${t.name} ${shape}`;
|
|
79
|
+
}),
|
|
80
|
+
"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.",
|
|
81
|
+
].join("\n");
|
|
82
|
+
|
|
83
|
+
/** Dispatch by LOOKUP — a tool name is never hard-coded at a call site. */
|
|
84
|
+
export async function runTool(env: any, call: ToolCall): Promise<ToolResult | null> {
|
|
85
|
+
const spec = TOOLS_REGISTRY.find((t) => t.name === call.name && t.audiences.includes("agent"));
|
|
86
|
+
if (!spec) return null;
|
|
87
|
+
return spec.handler(env, call.args ?? {});
|
|
49
88
|
}
|
|
50
89
|
|
|
51
90
|
/** The attributed injection: the answer model sees what the tool
|