@konneal/engine 0.2.136 → 0.2.138

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.
@@ -17,19 +17,21 @@ export interface ToolResult {
17
17
  output: string;
18
18
  }
19
19
 
20
- /** The strict protocol: a line `TOOL certificates.search {"query": "…"}`.
21
- * Plain regex parse — no JSON mode needed, tolerant of surrounding
22
- * prose (the model may talk around the call). */
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_.]+)\s*(\{[^\n}]*\})?/m.exec(String(text ?? "")); // line-anchored, case-sensitive: prose never trips it
25
+ const m = /^TOOL\s+([a-z_.]+)[ \t]*(\{.*)?$/m.exec(String(text ?? ""));
25
26
  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
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 use one tool before answering, by writing a single line:",
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}": "<${p.description}>"`).join(", ")}}`;
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 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.",
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.