@konneal/engine 0.2.135 → 0.2.137
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-JX55WMFR.js → ask-UL5NL5O7.js} +6 -5
- package/dist/{chunk-27NZR4HF.js → chunk-4GNKOTIK.js} +56 -45
- package/dist/{chunk-MWOY24B6.js → chunk-5WITTJQD.js} +4 -4
- package/dist/{chunk-TH5XGPD5.js → chunk-LSZTJI7V.js} +28 -56
- package/dist/chunk-NI4CIK6W.js +57 -0
- package/dist/{chunk-J6LBR6GI.js → chunk-RKNN3WAV.js} +316 -20
- package/dist/{chunk-7AXXR7GD.js → chunk-XH5QGX2O.js} +7 -5
- package/dist/dbtools.d.ts +67 -0
- package/dist/env-LMHGNJTN.js +18 -0
- package/dist/{search-LFS5HHMZ.js → search-VEGQBEFC.js} +4 -3
- package/dist/{tools-O4JYGVSR.js → tools-EZRLYLTM.js} +3 -2
- package/dist/tools.d.ts +4 -3
- package/dist/worker_public/src/index.js +14 -12
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +55 -26
- package/workers/worker_public/src/dbtools.ts +343 -0
- package/workers/worker_public/src/research.ts +1 -1
- package/workers/worker_public/src/tools.ts +60 -14
|
@@ -627,7 +627,12 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
627
627
|
let understanding: any = null;
|
|
628
628
|
// the loose redirect-offer family's gate (refusal.ts): knowledge-seek
|
|
629
629
|
// questions only, uncited answers only — a conversational turn's
|
|
630
|
-
// scope vocabulary is the answer itself
|
|
630
|
+
// scope vocabulary is the answer itself. "Cited" is the ANSWER TEXT
|
|
631
|
+
// carrying citation markers, NEVER the prompt's hit count: retrieval
|
|
632
|
+
// always returns passages (the model just declines to cite them on a
|
|
633
|
+
// refusal) — gating on usedHits left every refusal "cited" and the
|
|
634
|
+
// wall dark through three deploys (2026-10-11)
|
|
635
|
+
const answerCites = (t: string) => /\[[^\]]+\]/.test(t);
|
|
631
636
|
const refusalScope = (cited: boolean) => ({ cited, conversational: understanding?.intent === "conversational" });
|
|
632
637
|
// a query naming a model node (/req/…, /term/…) is node-SCOPED: its
|
|
633
638
|
// ── Option C: optimistic parallel retrieval ──
|
|
@@ -1421,28 +1426,52 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1421
1426
|
// the tool call — one reader, one tool, no parallel extraction racing a
|
|
1422
1427
|
// second vision pass that drifts hour to hour. The old probe dropped the
|
|
1423
1428
|
// image-bearing message, which is why the private bridge had to exist.
|
|
1424
|
-
if (multiTurn &&
|
|
1429
|
+
if (multiTurn && env.DB) {
|
|
1425
1430
|
try {
|
|
1426
1431
|
const imageTurn = typeof withImage === "string";
|
|
1427
1432
|
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1433
|
+
// the loop v2 (the owner's 2026-10-12 direction): the MODEL decides
|
|
1434
|
+
// whether and what to query — no intent regex arms it. Three
|
|
1435
|
+
// bounded rounds; each result joins the messages ATTRIBUTED and the
|
|
1436
|
+
// next probe re-reads them (the model composes db.find predicates
|
|
1437
|
+
// over the published schema and corrects from schema-named
|
|
1438
|
+
// errors). Cross-turn memory: the conversation history above
|
|
1439
|
+
// already carries the prior answers (which quote their tool
|
|
1440
|
+
// results) — a follow-up reuses them instead of re-querying.
|
|
1441
|
+
// Native function-calling rides alongside the line protocol: when
|
|
1442
|
+
// the binding returns tool_calls, they dispatch identically.
|
|
1443
|
+
for (let round = 0; round < 3; round++) {
|
|
1444
|
+
const imageNote = imageTurn && round === 0
|
|
1445
|
+
? "A photograph of the instrument is attached after this message. Transcribe the maker, model and any printed certificate number you can read FROM THE PHOTOGRAPH ITSELF, then emit the TOOL line with those tokens as the query — never invented, and never tokens taken from any register content elsewhere in the context (a register row's holder is not the plate's maker; observed live 2026-10-04)."
|
|
1446
|
+
: "";
|
|
1447
|
+
const continueNote = round > 0
|
|
1448
|
+
? "The tool results above are in hand. Answer now without a TOOL line, or issue exactly one more TOOL line if one more query is genuinely needed."
|
|
1449
|
+
: "";
|
|
1450
|
+
const probeMessages: any[] = [...messages.slice(0, -1), { role: "user", content: [TOOL_DECLARATION, imageNote, continueNote, q.query].filter(Boolean).join("\n\n") }];
|
|
1451
|
+
if (imageTurn && round === 0) probeMessages.push({ role: "user", content: [{ type: "image_url", image_url: { url: withImage } }] });
|
|
1452
|
+
const probe = await (env as any).AI.run(model, {
|
|
1453
|
+
messages: probeMessages,
|
|
1454
|
+
max_tokens: 700,
|
|
1455
|
+
reasoning_effort: imageTurn && round === 0 ? "high" : "low",
|
|
1456
|
+
});
|
|
1457
|
+
const probeText = typeof probe?.response === "string" ? probe?.response : probe?.choices?.[0]?.message?.content ?? "";
|
|
1458
|
+
probeTokensUsed += usageTotal((probe as any)?.usage) ?? 0;
|
|
1459
|
+
const nativeCall = Array.isArray((probe as any)?.tool_calls) ? (probe as any).tool_calls[0]?.function : null;
|
|
1460
|
+
const call = nativeCall
|
|
1461
|
+
? {
|
|
1462
|
+
name: String(nativeCall.name ?? ""),
|
|
1463
|
+
args: (() => {
|
|
1464
|
+
try { return typeof nativeCall.arguments === "string" ? JSON.parse(nativeCall.arguments) : (nativeCall.arguments ?? {}); } catch { return {}; }
|
|
1465
|
+
})(),
|
|
1466
|
+
}
|
|
1467
|
+
: parseToolCall(probeText);
|
|
1468
|
+
if (!call || !call.name) break; // the model chose to answer
|
|
1440
1469
|
const result = await runTool(env, call);
|
|
1441
|
-
if (result)
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1470
|
+
if (!result) break;
|
|
1471
|
+
mark(`${result.name} — ${result.query}`.slice(0, 140));
|
|
1472
|
+
messages.push({ role: "system", content: toolNote(result) });
|
|
1473
|
+
pendingTools.push(result);
|
|
1474
|
+
console.log("agent-tool:", result.name, JSON.stringify(result.query), "->", result.output.slice(0, 80));
|
|
1446
1475
|
}
|
|
1447
1476
|
} catch {
|
|
1448
1477
|
/* the loop is optional — the answer proceeds without the tool */
|
|
@@ -1541,7 +1570,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1541
1570
|
}
|
|
1542
1571
|
// the misplaced-pin wall rides the streamed path too: a
|
|
1543
1572
|
// CITED stream is not a refusal
|
|
1544
|
-
const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(
|
|
1573
|
+
const canonical0 = usedHits.length > 0 ? exciseMisplacedPin(canonicalRefusal(full, refusalScope(answerCites(full))), true) : canonicalRefusal(full, refusalScope(false));
|
|
1545
1574
|
// answer contract v2: validate [[u:]] refs, resolve typed blocks
|
|
1546
1575
|
const c2 = canonical0.includes(refusalAnswer())
|
|
1547
1576
|
? { text: canonical0, blocks: [], dropped: [] as string[] }
|
|
@@ -1582,7 +1611,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1582
1611
|
if (streamed.violations.length > 0) {
|
|
1583
1612
|
console.log("anchors:", streamed.violations.length, "of", streamed.total, "unverified — not caching");
|
|
1584
1613
|
}
|
|
1585
|
-
if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && cacheableAnswer(canonical, refusalScope(
|
|
1614
|
+
if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && cacheableAnswer(canonical, refusalScope(answerCites(canonical))) && !ablate) {
|
|
1586
1615
|
ctx.waitUntil(
|
|
1587
1616
|
env.CACHE.put(exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt))), JSON.stringify({ answer: canonical, citations: cites, model, query_hash: queryHash, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: streamedNote || qualityNote(sourceQuality) } : {}), ...(streamedEntailment ? { entailment: streamedEntailment } : {}) }), { expirationTtl: LIMITS.cacheTtlSec }),
|
|
1588
1617
|
);
|
|
@@ -1645,7 +1674,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1645
1674
|
);
|
|
1646
1675
|
answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
|
|
1647
1676
|
}
|
|
1648
|
-
if (answer) answer = canonicalRefusal(answer, refusalScope(
|
|
1677
|
+
if (answer) answer = canonicalRefusal(answer, refusalScope(answerCites(answer)));
|
|
1649
1678
|
// the misplaced-pin wall: a CITED answer is not a refusal — excise
|
|
1650
1679
|
// the pinned sentence if the model dropped it in as a disclaimer
|
|
1651
1680
|
// (measured on chip-scoped asks: correct cited answers carrying the
|
|
@@ -1688,7 +1717,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1688
1717
|
generateRetries += 1;
|
|
1689
1718
|
const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
|
|
1690
1719
|
if (corrected) {
|
|
1691
|
-
const correctedAnswer = canonicalRefusal(corrected, refusalScope(
|
|
1720
|
+
const correctedAnswer = canonicalRefusal(corrected, refusalScope(answerCites(corrected)));
|
|
1692
1721
|
const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h: Hit) => h.text));
|
|
1693
1722
|
const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
|
|
1694
1723
|
if (retryAnchors.violations.length < anchors.violations.length || (!retryRetyped && retyped) || (unreferenced && correctedAnswer.includes("[[u:"))) {
|
|
@@ -1733,7 +1762,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1733
1762
|
const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
|
|
1734
1763
|
// the answer now comes from the retry passages — citations must follow
|
|
1735
1764
|
if (retryAnswer) {
|
|
1736
|
-
answer = canonicalRefusal(retryAnswer, refusalScope(
|
|
1765
|
+
answer = canonicalRefusal(retryAnswer, refusalScope(answerCites(retryAnswer)));
|
|
1737
1766
|
used = retryUsed;
|
|
1738
1767
|
}
|
|
1739
1768
|
}
|
|
@@ -1785,7 +1814,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1785
1814
|
messages[messages.length - 1],
|
|
1786
1815
|
], effort);
|
|
1787
1816
|
if (regen && !regen.includes(refusalAnswer())) {
|
|
1788
|
-
answer = canonicalRefusal(regen, refusalScope(
|
|
1817
|
+
answer = canonicalRefusal(regen, refusalScope(answerCites(regen)));
|
|
1789
1818
|
console.log("blind-answer: regeneration grounded");
|
|
1790
1819
|
}
|
|
1791
1820
|
}
|
|
@@ -1811,7 +1840,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1811
1840
|
}
|
|
1812
1841
|
}
|
|
1813
1842
|
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality, confidence_note: entailmentNote || qualityNote(jsonQuality), ...(entailment ? { entailment } : {}), ...(jsonQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(finalCites) } : {}) } : {}), model: MODELS.member, query_hash: queryHash, follow_ups: understanding?.follow_ups ?? [], blocks: [...c2ns.blocks, ...(verdictBlock ? [verdictBlock] : []), ...(conditionBlock ? [conditionBlock] : []), ...(aggregationBlock ? [aggregationBlock] : []), ...(registerBlock ? [registerBlock] : []), ...completionBlocks], context_applied: ctxApplied, ...(liveRecords ? { records: liveRecords } : {}), ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
|
|
1814
|
-
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer, refusalScope(
|
|
1843
|
+
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer, refusalScope(answerCites(answer))) && finalAnchors.violations.length === 0;
|
|
1815
1844
|
if (cacheable) {
|
|
1816
1845
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
1817
1846
|
ctx.waitUntil(env.CACHE.put(ck, JSON.stringify(out), { expirationTtl: LIMITS.cacheTtlSec }));
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
// The data-plane grammar (the owner's 2026-10-12 direction, "the model
|
|
2
|
+
// decides what and how to look for data"): instead of pre-enumerated
|
|
3
|
+
// operation menus, the agent gets the PUBLISHED SCHEMA and composes
|
|
4
|
+
// typed predicates over it. db.schema answers "what is queryable";
|
|
5
|
+
// db.find answers the question. The engine compiles predicates to
|
|
6
|
+
// parameterized SQL — the model never writes SQL, identifiers never
|
|
7
|
+
// interpolate from model input, values always bind. BIRD-INTERACT
|
|
8
|
+
// (2026) grounding: interactive data questions collapse without schema
|
|
9
|
+
// linkage, so every validation error NAMES the valid columns.
|
|
10
|
+
|
|
11
|
+
export interface CatalogColumn {
|
|
12
|
+
name: string;
|
|
13
|
+
type: "text" | "number";
|
|
14
|
+
description: string;
|
|
15
|
+
/** ordering/comparison compiles CAST(col AS INTEGER) — textual columns
|
|
16
|
+
* carrying numeric content (certificates.year is TEXT in D1) */
|
|
17
|
+
numeric_cast?: boolean;
|
|
18
|
+
/** excluded from the default select (heavy bodies); still selectable by name */
|
|
19
|
+
wide?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CatalogTable {
|
|
23
|
+
name: string;
|
|
24
|
+
description: string;
|
|
25
|
+
columns: CatalogColumn[];
|
|
26
|
+
default_select: string[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// PUBLISHED tables only — public corpus data. Everything holding user
|
|
30
|
+
// or operational state (api_keys, queries, spend, feedback, memories,
|
|
31
|
+
// projects, project_files, conversations, messages, shared_conversations,
|
|
32
|
+
// conversation_entities, attachments, model_plane_meta, ask retries and
|
|
33
|
+
// usage caches) is deliberately absent: db.find cannot reach it.
|
|
34
|
+
export const DB_CATALOG: CatalogTable[] = [
|
|
35
|
+
{
|
|
36
|
+
name: "certificates",
|
|
37
|
+
description:
|
|
38
|
+
"The certificate register (a snapshot, not live certification status): one row per issued certificate across the recommendation families.",
|
|
39
|
+
columns: [
|
|
40
|
+
{ name: "num", type: "text", description: "the printed certificate number, e.g. R60/2021-A-NL1-26.12 (family/prefix per recommendation)" },
|
|
41
|
+
{ name: "family", type: "text", description: "the recommendation family number, e.g. '60' for R 60" },
|
|
42
|
+
{ name: "holder", type: "text", description: "the certificate holder (manufacturer)" },
|
|
43
|
+
{ name: "model", type: "text", description: "the instrument model designation" },
|
|
44
|
+
{ name: "year", type: "number", description: "the issue year", numeric_cast: true },
|
|
45
|
+
{ name: "status", type: "text", description: "Valid | Suspended | Withdrawn" },
|
|
46
|
+
{ name: "pdf_key", type: "text", description: "the /cert-pdf path suffix when the PDF is on file, else empty" },
|
|
47
|
+
],
|
|
48
|
+
default_select: ["num", "family", "holder", "model", "year", "status", "pdf_key"],
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
name: "documents",
|
|
52
|
+
description:
|
|
53
|
+
"The document identity registry: every publication edition with its family, part, and derived edition status (an edition with a successor is superseded regardless of any copied status field).",
|
|
54
|
+
columns: [
|
|
55
|
+
{ name: "canonical_id", type: "text", description: "doc:<family>-<edition> slug" },
|
|
56
|
+
{ name: "docidentifier", type: "text", description: "the full identifier with edition" },
|
|
57
|
+
{ name: "family", type: "text", description: "the family key, e.g. R-60" },
|
|
58
|
+
{ name: "part", type: "text", description: "1 | 2 | 3 | A | annexes | sup | empty" },
|
|
59
|
+
{ name: "edition", type: "number", description: "the edition year", numeric_cast: true },
|
|
60
|
+
{ name: "status", type: "text", description: "in-force | superseded | withdrawn (as recorded)" },
|
|
61
|
+
{ name: "derived_status", type: "text", description: "the successor-edge derivation result" },
|
|
62
|
+
{ name: "active", type: "number", description: "1 = the active edition of its family+part" },
|
|
63
|
+
{ name: "superseded_by", type: "text", description: "the successor's canonical_id, else empty" },
|
|
64
|
+
{ name: "title", type: "text", description: "the publication title" },
|
|
65
|
+
],
|
|
66
|
+
default_select: ["canonical_id", "docidentifier", "family", "part", "edition", "derived_status", "active", "superseded_by", "title"],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "graph_nodes",
|
|
70
|
+
description: "The public citation-graph nodes: documents, families, and vocabulary concepts.",
|
|
71
|
+
columns: [
|
|
72
|
+
{ name: "id", type: "text", description: "doc:<identifier-slug> | family:<number> | concept:<id>" },
|
|
73
|
+
{ name: "kind", type: "text", description: "doc | family | concept" },
|
|
74
|
+
{ name: "label", type: "text", description: "the display label" },
|
|
75
|
+
],
|
|
76
|
+
default_select: ["id", "kind", "label"],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
name: "graph_edges",
|
|
80
|
+
description: "The public citation-graph edges between nodes.",
|
|
81
|
+
columns: [
|
|
82
|
+
{ name: "src", type: "text", description: "the source node id" },
|
|
83
|
+
{ name: "dst", type: "text", description: "the destination node id" },
|
|
84
|
+
{ name: "kind", type: "text", description: "part_of | variant_of | successor | amends | defines" },
|
|
85
|
+
],
|
|
86
|
+
default_select: ["src", "kind", "dst"],
|
|
87
|
+
},
|
|
88
|
+
{
|
|
89
|
+
name: "model_nodes",
|
|
90
|
+
description:
|
|
91
|
+
"The Recommendation model plane (per-standard requirement/conformance graphs): the machine-readable nodes behind the verdict engine.",
|
|
92
|
+
columns: [
|
|
93
|
+
{ name: "standard", type: "text", description: "the standard key, e.g. r60" },
|
|
94
|
+
{ name: "node_id", type: "text", description: "/req/metrological/mpe" },
|
|
95
|
+
{ name: "kind", type: "text", description: "requirement | conformance_test | term | constraint | characteristic | state_machine | dimension" },
|
|
96
|
+
{ name: "name", type: "text", description: "the node's display name" },
|
|
97
|
+
{ name: "clause_doc", type: "text", description: "the provenance document urn" },
|
|
98
|
+
{ name: "clause_ref", type: "text", description: "the clause inside it, e.g. 5.3.2" },
|
|
99
|
+
{ name: "content", type: "text", description: "the node's JSON projection (the checks, thresholds, parameters)", wide: true },
|
|
100
|
+
{ name: "content_hash", type: "text", description: "sha256 of content" },
|
|
101
|
+
],
|
|
102
|
+
default_select: ["standard", "node_id", "kind", "name", "clause_doc", "clause_ref"],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "unit_payloads",
|
|
106
|
+
description: "Typed retrievable units (tables, formulas, figures, terms) with their producer payloads.",
|
|
107
|
+
columns: [
|
|
108
|
+
{ name: "unit_id", type: "text", description: "u:table-1" },
|
|
109
|
+
{ name: "doc_id", type: "text", description: "the owning document id" },
|
|
110
|
+
{ name: "docidentifier", type: "text", description: "the publication identifier with part" },
|
|
111
|
+
{ name: "edition", type: "text", description: "the edition year" },
|
|
112
|
+
{ name: "clause_anchor", type: "text", description: "the clause the unit lives in" },
|
|
113
|
+
{ name: "type", type: "text", description: "table | formula | figure | term | requirement" },
|
|
114
|
+
{ name: "payload", type: "text", description: "the producer payload JSON (the table's rows, the formula's math)", wide: true },
|
|
115
|
+
],
|
|
116
|
+
default_select: ["unit_id", "doc_id", "docidentifier", "edition", "clause_anchor", "type"],
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "chunks",
|
|
120
|
+
description:
|
|
121
|
+
"The indexed corpus chunks (one row per clause-level passage) — use for exact metadata questions; semantic search is NOT this tool's job.",
|
|
122
|
+
columns: [
|
|
123
|
+
{ name: "id", type: "text", description: "the chunk id" },
|
|
124
|
+
{ name: "doc_id", type: "text", description: "the source document slug" },
|
|
125
|
+
{ name: "docidentifier", type: "text", description: "the publication identifier with part" },
|
|
126
|
+
{ name: "doctype", type: "text", description: "R | D | B | G | E" },
|
|
127
|
+
{ name: "doc_number", type: "text", description: "60" },
|
|
128
|
+
{ name: "edition", type: "text", description: "the edition year" },
|
|
129
|
+
{ name: "language", type: "text", description: "en | fr | …" },
|
|
130
|
+
{ name: "clause_anchor", type: "text", description: "the clause anchor" },
|
|
131
|
+
{ name: "clause_title", type: "text", description: "the clause title" },
|
|
132
|
+
{ name: "status", type: "text", description: "in-force | superseded" },
|
|
133
|
+
{ name: "superseded_by", type: "text", description: "the superseding docidentifier, else empty" },
|
|
134
|
+
{ name: "corpus", type: "text", description: "clean | dirty" },
|
|
135
|
+
{ name: "tier", type: "text", description: "the quality tier" },
|
|
136
|
+
{ name: "text", type: "text", description: "the passage body (wide — select only when needed)", wide: true },
|
|
137
|
+
{ name: "fts_text", type: "text", description: "context preamble + body (wide)", wide: true },
|
|
138
|
+
],
|
|
139
|
+
default_select: ["id", "doc_id", "docidentifier", "doctype", "doc_number", "edition", "language", "clause_anchor", "clause_title", "status", "corpus"],
|
|
140
|
+
},
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
export function publishedTable(name: string): CatalogTable | undefined {
|
|
144
|
+
return DB_CATALOG.find((t) => t.name === name);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// ── the predicate compiler (pure; unit-tested) ────────────────────────
|
|
148
|
+
|
|
149
|
+
export const FIND_OPS = ["eq", "ne", "lt", "le", "gt", "ge", "in", "like", "between"] as const;
|
|
150
|
+
export type FindOp = (typeof FIND_OPS)[number];
|
|
151
|
+
|
|
152
|
+
export interface FindPredicate {
|
|
153
|
+
col: string;
|
|
154
|
+
op: FindOp;
|
|
155
|
+
value: unknown;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export interface FindArgs {
|
|
159
|
+
table: string;
|
|
160
|
+
where?: FindPredicate[];
|
|
161
|
+
select?: string[];
|
|
162
|
+
order_by?: { col: string; dir?: "asc" | "desc" };
|
|
163
|
+
limit?: number;
|
|
164
|
+
group_by?: string[];
|
|
165
|
+
agg?: { fn: "count" | "sum" | "avg" | "min" | "max"; col?: string };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export type Compiled =
|
|
169
|
+
| { ok: true; sql: string; params: unknown[]; echo: string }
|
|
170
|
+
| { ok: false; error: string };
|
|
171
|
+
|
|
172
|
+
const MAX_LIMIT = 25;
|
|
173
|
+
const DEFAULT_LIMIT = 10;
|
|
174
|
+
|
|
175
|
+
const NUMERIC_OPS: FindOp[] = ["lt", "le", "gt", "ge", "between"];
|
|
176
|
+
|
|
177
|
+
/** Compile validated identifiers + bound parameters. Returns a
|
|
178
|
+
* schema-linked error (naming the valid columns) on any mismatch —
|
|
179
|
+
* the model corrects from the error, the loop continues. */
|
|
180
|
+
export function compileFind(args: FindArgs): Compiled {
|
|
181
|
+
const table = publishedTable(String(args?.table ?? ""));
|
|
182
|
+
if (!table) {
|
|
183
|
+
return {
|
|
184
|
+
ok: false,
|
|
185
|
+
error: `unknown table "${String(args?.table ?? "")}". Published tables: ${DB_CATALOG.map((t) => t.name).join(", ")}.`,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const colNames = table.columns.map((c) => c.name);
|
|
189
|
+
const colByName = new Map(table.columns.map((c) => [c.name, c]));
|
|
190
|
+
const params: unknown[] = [];
|
|
191
|
+
const phrases: string[] = [];
|
|
192
|
+
const p = (n: number) => `?${n}`;
|
|
193
|
+
|
|
194
|
+
const colRef = (col: string): string => {
|
|
195
|
+
const c = colByName.get(col)!;
|
|
196
|
+
return c.numeric_cast ? `CAST(${col} AS INTEGER)` : col;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
for (const w of Array.isArray(args?.where) ? args.where : []) {
|
|
200
|
+
const col = String(w?.col ?? "");
|
|
201
|
+
const colDef = colByName.get(col);
|
|
202
|
+
if (!colDef) {
|
|
203
|
+
return {
|
|
204
|
+
ok: false,
|
|
205
|
+
error: `table "${table.name}" has no column "${col}". Valid columns: ${colNames.join(", ")}.`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const op = String(w?.op ?? "") as FindOp;
|
|
209
|
+
if (!FIND_OPS.includes(op)) {
|
|
210
|
+
return { ok: false, error: `unknown op "${op}". Valid ops: ${FIND_OPS.join(", ")}.` };
|
|
211
|
+
}
|
|
212
|
+
const v = w?.value;
|
|
213
|
+
if (NUMERIC_OPS.includes(op) && !colDef.numeric_cast && colDef.type !== "number") {
|
|
214
|
+
return { ok: false, error: `op "${op}" needs a numeric column; "${col}" is ${colDef.type}.` };
|
|
215
|
+
}
|
|
216
|
+
const ref = colRef(col);
|
|
217
|
+
if (op === "in") {
|
|
218
|
+
const list = Array.isArray(v) ? v.map((x) => String(x)) : [String(v)];
|
|
219
|
+
if (!list.length) return { ok: false, error: `op "in" needs a non-empty value list.` };
|
|
220
|
+
const holders = list.map((x) => p(params.push(x))).join(", ");
|
|
221
|
+
phrases.push(`${ref} IN (${holders})`);
|
|
222
|
+
} else if (op === "between") {
|
|
223
|
+
const pair = Array.isArray(v) ? v : [undefined, undefined];
|
|
224
|
+
if (pair.length !== 2) return { ok: false, error: `op "between" needs a two-element value [lo, hi].` };
|
|
225
|
+
phrases.push(`${ref} BETWEEN ${p(params.push(pair[0]))} AND ${p(params.push(pair[1]))}`);
|
|
226
|
+
} else if (op === "like") {
|
|
227
|
+
phrases.push(`${ref} LIKE ${p(params.push(String(v)))}`);
|
|
228
|
+
} else {
|
|
229
|
+
const sqlOp = { eq: "=", ne: "!=", lt: "<", le: "<=", gt: ">", ge: ">=" }[op as "eq" | "ne" | "lt" | "le" | "gt" | "ge"];
|
|
230
|
+
phrases.push(`${ref} ${sqlOp} ${p(params.push(v))}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const select = (Array.isArray(args?.select) && args.select.length ? args.select : table.default_select).map(String);
|
|
235
|
+
for (const s of select) {
|
|
236
|
+
if (!colNames.includes(s)) {
|
|
237
|
+
return { ok: false, error: `table "${table.name}" has no column "${s}". Valid columns: ${colNames.join(", ")}.` };
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
let head: string;
|
|
242
|
+
if (args?.agg?.fn) {
|
|
243
|
+
const fn = String(args.agg.fn) as "count" | "sum" | "avg" | "min" | "max";
|
|
244
|
+
if (!["count", "sum", "avg", "min", "max"].includes(fn)) {
|
|
245
|
+
return { ok: false, error: `unknown agg fn "${fn}". Valid: count, sum, avg, min, max.` };
|
|
246
|
+
}
|
|
247
|
+
let aggExpr = "COUNT(*)";
|
|
248
|
+
if (fn !== "count") {
|
|
249
|
+
const acol = String(args.agg.col ?? "");
|
|
250
|
+
const adef = colByName.get(acol);
|
|
251
|
+
if (!adef) return { ok: false, error: `agg column "${acol}" is not a column of "${table.name}". Valid columns: ${colNames.join(", ")}.` };
|
|
252
|
+
aggExpr = `${fn.toUpperCase()}(${colRef(acol)})`;
|
|
253
|
+
}
|
|
254
|
+
head = `SELECT ${args?.group_by?.length ? `${(args.group_by as string[]).map(String).join(", ")}, ` : ""}${aggExpr} AS agg FROM ${table.name}`;
|
|
255
|
+
for (const g of args?.group_by ?? []) {
|
|
256
|
+
if (!colNames.includes(String(g))) {
|
|
257
|
+
return { ok: false, error: `table "${table.name}" has no column "${String(g)}". Valid columns: ${colNames.join(", ")}.` };
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
head = `SELECT ${select.join(", ")} FROM ${table.name}`;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const where = phrases.length ? ` WHERE ${phrases.join(" AND ")}` : "";
|
|
265
|
+
let tail = "";
|
|
266
|
+
if (args?.group_by?.length) tail += ` GROUP BY ${(args.group_by as string[]).map(String).join(", ")}`;
|
|
267
|
+
if (args?.order_by?.col) {
|
|
268
|
+
const ocol = String(args.order_by.col);
|
|
269
|
+
if (!colNames.includes(ocol)) {
|
|
270
|
+
return { ok: false, error: `table "${table.name}" has no column "${ocol}". Valid columns: ${colNames.join(", ")}.` };
|
|
271
|
+
}
|
|
272
|
+
const dir = String(args.order_by.dir ?? "desc").toLowerCase() === "asc" ? "ASC" : "DESC";
|
|
273
|
+
tail += ` ORDER BY ${colRef(ocol)} ${dir}`;
|
|
274
|
+
}
|
|
275
|
+
const limit = Math.max(1, Math.min(MAX_LIMIT, Number(args?.limit ?? DEFAULT_LIMIT) || DEFAULT_LIMIT));
|
|
276
|
+
tail += ` LIMIT ${limit}`;
|
|
277
|
+
|
|
278
|
+
const echo = [
|
|
279
|
+
`db.find ${table.name}`,
|
|
280
|
+
args?.where?.length ? `where ${args.where.map((w) => `${w.col} ${w.op} ${JSON.stringify(w.value ?? null)}`).join(" AND ")}` : "all rows",
|
|
281
|
+
args?.agg?.fn ? `${args.agg.fn}${args.agg.col ? ` ${args.agg.col}` : ""}` : "",
|
|
282
|
+
]
|
|
283
|
+
.filter(Boolean)
|
|
284
|
+
.join(" · ");
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
ok: true,
|
|
288
|
+
sql: `${head}${where}${tail}`,
|
|
289
|
+
params,
|
|
290
|
+
echo,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** The db.find handler: compile, run on D1, format deterministically.
|
|
295
|
+
* Every output carries the predicate echo — the answer attributes the
|
|
296
|
+
* rows to the query that produced them. */
|
|
297
|
+
export async function runFind(env: any, args: FindArgs): Promise<{ name: string; query: string; output: string } | null> {
|
|
298
|
+
const compiled = compileFind(args);
|
|
299
|
+
if (!compiled.ok) {
|
|
300
|
+
return {
|
|
301
|
+
name: "db.find",
|
|
302
|
+
query: String(args?.table ?? ""),
|
|
303
|
+
output: `The query was rejected: ${compiled.error} Correct the predicates and call again.`,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
try {
|
|
307
|
+
const { portStore } = await import("./env.ts");
|
|
308
|
+
const res = await portStore(env).prepare(compiled.sql).bind(...compiled.params).all();
|
|
309
|
+
const rows = (res?.results ?? []) as Record<string, unknown>[];
|
|
310
|
+
const body = rows.length
|
|
311
|
+
? rows.map((r) => JSON.stringify(r)).join("\n")
|
|
312
|
+
: "No rows matched. Either the predicates are too narrow or the data is absent — state which, and consider widening.";
|
|
313
|
+
const aggNote = args?.agg?.fn ? " (aggregate)" : "";
|
|
314
|
+
return { name: "db.find", query: compiled.echo, output: `${compiled.echo}${aggNote} → ${rows.length} row(s):\n${body}` };
|
|
315
|
+
} catch (e: any) {
|
|
316
|
+
return { name: "db.find", query: compiled.echo, output: `The query failed to run: ${String(e?.message ?? e).slice(0, 200)}` };
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The db.schema handler: the published map, one table or the catalog. */
|
|
321
|
+
export function runSchema(table?: string): { name: string; query: string; output: string } {
|
|
322
|
+
const tables = table ? [publishedTable(String(table))].filter(Boolean) : DB_CATALOG;
|
|
323
|
+
if (!tables.length) {
|
|
324
|
+
return {
|
|
325
|
+
name: "db.schema",
|
|
326
|
+
query: String(table),
|
|
327
|
+
output: `Unknown table "${table}". Published tables: ${DB_CATALOG.map((t) => t.name).join(", ")}.`,
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
const output = (tables as CatalogTable[])
|
|
331
|
+
.map((t) => {
|
|
332
|
+
const cols = t.columns
|
|
333
|
+
.map((c) => ` ${c.name} (${c.type}${c.numeric_cast ? ", numeric-cast" : ""}${c.wide ? ", wide" : ""}) — ${c.description}`)
|
|
334
|
+
.join("\n");
|
|
335
|
+
return `${t.name} — ${t.description}\n${cols}\n default select: ${t.default_select.join(", ")}`;
|
|
336
|
+
})
|
|
337
|
+
.join("\n\n");
|
|
338
|
+
return {
|
|
339
|
+
name: "db.schema",
|
|
340
|
+
query: table ? String(table) : "catalog",
|
|
341
|
+
output: `Published tables (compose TOOL db.find with where: [{col, op, value}]; ops: ${FIND_OPS.join(" ")}; values bind safely; like accepts % and _ wildcards):\n\n${output}`,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
@@ -164,7 +164,7 @@ async function run(
|
|
|
164
164
|
telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
|
|
165
165
|
throw new Error("The generation model is unavailable; please retry.");
|
|
166
166
|
}
|
|
167
|
-
answer = canonicalRefusal(answer, { cited:
|
|
167
|
+
answer = canonicalRefusal(answer, { cited: /\[[^\]]+\]/.test(answer), conversational: false });
|
|
168
168
|
const anchors = checkQuoteAnchors(answer, used.map((h: Hit) => h.text));
|
|
169
169
|
if (anchors.violations.length) console.log("research anchors:", anchors.violations.length, "unverified");
|
|
170
170
|
const out = {
|
|
@@ -17,19 +17,21 @@ export interface ToolResult {
|
|
|
17
17
|
output: string;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** The strict protocol: a line `TOOL
|
|
21
|
-
*
|
|
22
|
-
*
|
|
20
|
+
/** The strict protocol: a line `TOOL <name> {…json…}`. Line-anchored,
|
|
21
|
+
* case-sensitive (prose never trips it); the args are the WHOLE rest
|
|
22
|
+
* of the line parsed as JSON — nested objects (db.find predicates)
|
|
23
|
+
* are legal, and a malformed or unterminated object is not a call. */
|
|
23
24
|
export function parseToolCall(text: string): ToolCall | null {
|
|
24
|
-
const m = /^TOOL\s+([a-z_.]+)\
|
|
25
|
+
const m = /^TOOL\s+([a-z_.]+)[ \t]*(\{.*)?$/m.exec(String(text ?? ""));
|
|
25
26
|
if (!m) return null;
|
|
26
|
-
|
|
27
|
-
if (m[
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
27
|
+
const rest = (m[2] ?? "").trim();
|
|
28
|
+
if (!rest) return { name: m[1].toLowerCase(), args: {} };
|
|
29
|
+
if (!rest.startsWith("{")) return null;
|
|
30
|
+
try {
|
|
31
|
+
return { name: m[1].toLowerCase(), args: JSON.parse(rest) };
|
|
32
|
+
} catch {
|
|
33
|
+
return null;
|
|
31
34
|
}
|
|
32
|
-
return { name: m[1].toLowerCase(), args };
|
|
33
35
|
}
|
|
34
36
|
|
|
35
37
|
// ── the tool registry (OCP, the owner's 2026-10-04 direction) ────────
|
|
@@ -65,19 +67,63 @@ const certificatesSearch: ToolSpec = {
|
|
|
65
67
|
};
|
|
66
68
|
|
|
67
69
|
import { unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry, unitsLookup, unitsConvert } from "./tools-content.ts";
|
|
70
|
+
import { runFind, runSchema } from "./dbtools.ts";
|
|
71
|
+
|
|
72
|
+
// the data-plane grammar (dbtools.ts): the model composes typed
|
|
73
|
+
// predicates over the PUBLISHED schema — no operation menus, no raw SQL
|
|
74
|
+
const dbSchema: ToolSpec = {
|
|
75
|
+
name: "db.schema",
|
|
76
|
+
description:
|
|
77
|
+
"The published data map: every queryable table (the certificate register, the document registry, the citation graph, the model plane, typed units, the corpus chunks) with its columns, types, and meanings. Call this first whenever a question turns on structured data; then compose db.find.",
|
|
78
|
+
params: [{ key: "table", required: false, description: "one table's detail; omit for the whole catalog" }],
|
|
79
|
+
audiences: ["agent", "mcp"],
|
|
80
|
+
handler: async (_env, args) => {
|
|
81
|
+
const t = args?.table ? String(args.table) : undefined;
|
|
82
|
+
return runSchema(t);
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const dbFind: ToolSpec = {
|
|
87
|
+
name: "db.find",
|
|
88
|
+
description:
|
|
89
|
+
"Query a published table with typed predicates you compose: where: [{col, op, value}] with ops eq|ne|lt|le|gt|ge|in|like|between, optional select, order_by {col, dir}, limit (≤25), group_by + agg {fn: count|sum|avg|min|max, col}. Values bind safely; identifiers validate against the schema (errors name the valid columns). Returns the matching rows or aggregate with the query echoed.",
|
|
90
|
+
params: [
|
|
91
|
+
{ key: "table", required: true, description: "a published table (db.schema lists them)" },
|
|
92
|
+
{ key: "where", required: false, description: "[{col, op, value}] predicates, ANDed" },
|
|
93
|
+
{ key: "select", required: false, description: "columns to return; default per table" },
|
|
94
|
+
{ key: "order_by", required: false, description: "{col, dir: asc|desc}" },
|
|
95
|
+
{ key: "limit", required: false, description: "row cap, ≤25" },
|
|
96
|
+
{ key: "agg", required: false, description: "{fn, col?} aggregate instead of rows; with group_by: [cols]" },
|
|
97
|
+
],
|
|
98
|
+
audiences: ["agent", "mcp"],
|
|
99
|
+
handler: async (env, args) => {
|
|
100
|
+
const findArgs = {
|
|
101
|
+
table: String(args?.table ?? ""),
|
|
102
|
+
where: Array.isArray(args?.where) ? args.where : [],
|
|
103
|
+
select: Array.isArray(args?.select) ? args.select : undefined,
|
|
104
|
+
order_by: args?.order_by && typeof args.order_by === "object" ? (args.order_by as { col: string; dir?: "asc" | "desc" }) : undefined,
|
|
105
|
+
limit: args?.limit != null ? Number(args.limit) : undefined,
|
|
106
|
+
group_by: Array.isArray(args?.group_by) ? args.group_by : undefined,
|
|
107
|
+
agg: args?.agg && typeof args.agg === "object" ? (args.agg as { fn: "count" | "sum" | "avg" | "min" | "max"; col?: string }) : undefined,
|
|
108
|
+
};
|
|
109
|
+
if (!findArgs.table) return null;
|
|
110
|
+
return runFind(env, findArgs);
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const TOOLS_REGISTRY: ToolSpec[] = [dbSchema, dbFind, certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry, unitsLookup, unitsConvert];
|
|
68
115
|
|
|
69
|
-
export const TOOLS_REGISTRY: ToolSpec[] = [certificatesSearch, unitsGet, graphCites, docsSection, glossaryLookup, documentsFamily, licensedSection, verdictEvaluate, conditionsCheck, bibEntry, unitsLookup, unitsConvert];
|
|
70
116
|
|
|
71
117
|
/** The agent loop's protocol declaration, GENERATED from the registry —
|
|
72
118
|
* the model's tool vocabulary is always the registry's, never a
|
|
73
119
|
* hand-maintained literal. */
|
|
74
120
|
export const TOOL_DECLARATION = [
|
|
75
|
-
"You may
|
|
121
|
+
"You may query data before answering, by writing ONE line per call:",
|
|
76
122
|
...TOOLS_REGISTRY.filter((t) => t.audiences.includes("agent")).map((t) => {
|
|
77
|
-
const shape = `{${t.params.map((p) => `"${p.key}":
|
|
123
|
+
const shape = `{${t.params.map((p) => `"${p.key}": <${p.description}>`).join(", ")}}`;
|
|
78
124
|
return `TOOL ${t.name} ${shape}`;
|
|
79
125
|
}),
|
|
80
|
-
"The worker runs
|
|
126
|
+
"The worker runs each call and returns the result attributed; you may issue up to three calls across the turns, one per turn, each on its own single line (compact JSON, no line breaks). Structured questions — counts, latest, filters, per-holder or per-year breakdowns over the register, document editions, model-plane requirements — start from TOOL db.schema (it lists the queryable tables and their columns), then compose TOOL db.find predicates. Each result arrives attributed: phrase it as what the tool returned, with your query visible in the answer. When you have enough, answer WITHOUT a TOOL line; if no tool helps, answer directly.",
|
|
81
127
|
].join("\n");
|
|
82
128
|
|
|
83
129
|
/** Dispatch by LOOKUP — a tool name is never hard-coded at a call site.
|