@konneal/engine 0.1.4 → 0.2.1

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.
Files changed (58) hide show
  1. package/dist/admin.d.ts +1 -0
  2. package/dist/ask-47RNGK2R.js +12 -0
  3. package/dist/chunk-3OXSQH7Y.js +1852 -0
  4. package/dist/chunk-6GOSMLRH.js +2781 -0
  5. package/dist/{chunk-EHJEELVB.js → chunk-LNSDBEKS.js} +1 -1
  6. package/dist/{chunk-35ODH64W.js → chunk-Q327B27J.js} +33 -0
  7. package/dist/{chunk-OCNLV7Q7.js → chunk-Q6LI4T7M.js} +6 -1
  8. package/dist/{chunk-ROF3Q7UC.js → chunk-SN3ANQ3Y.js} +2 -2
  9. package/dist/chunk-VJZLVU3S.js +64 -0
  10. package/dist/{chunk-CAEHIVG5.js → chunk-WGXATDXY.js} +1 -1
  11. package/dist/codecs.d.ts +3 -3
  12. package/dist/completion.d.ts +1 -1
  13. package/dist/config.d.ts +9 -0
  14. package/dist/faithfulness.d.ts +1 -0
  15. package/dist/mcp-proto.d.ts +25 -0
  16. package/dist/mcp.d.ts +4 -0
  17. package/dist/openapi-surface.gen.d.ts +9 -0
  18. package/dist/openapi-types.d.ts +2141 -0
  19. package/dist/profile.gen.d.ts +1 -0
  20. package/dist/prompts/system.md +1 -0
  21. package/dist/quota.d.ts +4 -1
  22. package/dist/search-OMPBMZT4.js +11 -0
  23. package/dist/tablecontext.d.ts +7 -0
  24. package/dist/verdict-parse.d.ts +5 -0
  25. package/dist/worker_mcp/src/index.js +3 -3
  26. package/dist/worker_public/src/config.js +4 -2
  27. package/dist/worker_public/src/index.js +1193 -4932
  28. package/dist/worker_public/src/profile.js +1 -1
  29. package/dist/worker_public/src/refusal.js +2 -2
  30. package/dist/worker_public/src/requestScope.js +3 -3
  31. package/docs/spec-api.md +27 -13
  32. package/package.json +12 -3
  33. package/profile/prompts.yaml +3 -0
  34. package/workers/shared/router.ts +23 -16
  35. package/workers/worker_public/migrations/0014_usage_cache.sql +5 -0
  36. package/workers/worker_public/openapi.yaml +1169 -0
  37. package/workers/worker_public/prompts/system.md +1 -0
  38. package/workers/worker_public/schema.sql +3 -1
  39. package/workers/worker_public/src/admin.ts +51 -7
  40. package/workers/worker_public/src/ai.ts +10 -2
  41. package/workers/worker_public/src/ask.ts +94 -22
  42. package/workers/worker_public/src/codecs.ts +35 -10
  43. package/workers/worker_public/src/completion.ts +24 -1
  44. package/workers/worker_public/src/config.ts +10 -0
  45. package/workers/worker_public/src/faithfulness.ts +10 -17
  46. package/workers/worker_public/src/grader.ts +2 -2
  47. package/workers/worker_public/src/index.ts +106 -58
  48. package/workers/worker_public/src/lib/router.ts +1 -1
  49. package/workers/worker_public/src/mcp-proto.ts +71 -0
  50. package/workers/worker_public/src/mcp.ts +47 -0
  51. package/workers/worker_public/src/openapi-surface.gen.ts +318 -0
  52. package/workers/worker_public/src/pipeline.ts +4 -2
  53. package/workers/worker_public/src/profile.gen.ts +1 -0
  54. package/workers/worker_public/src/projects.ts +4 -2
  55. package/workers/worker_public/src/quota.ts +4 -2
  56. package/workers/worker_public/src/research.ts +55 -3
  57. package/workers/worker_public/src/tablecontext.ts +13 -2
  58. package/workers/worker_public/src/verdict-parse.ts +60 -0
@@ -4,7 +4,7 @@ import { LIMITS, MODELS, DATASETS, THRESHOLDS, processExpansion } from "./config
4
4
  import systemPromptText from "../prompts/system.md";
5
5
  import conversationalPromptText from "../prompts/conversational.md";
6
6
  import listwisePromptText from "../prompts/listwise.md";
7
- import { tableContext } from "./tablecontext";
7
+ import { tableSelection } from "./tablecontext";
8
8
 
9
9
  // the pinned refusal sentence lives with the canonicalizer in ./refusal
10
10
  // (refusals are never cached: a refusal says "retrieval found nothing",
@@ -337,7 +337,9 @@ export function buildMessages(
337
337
  const head = `[${usedHits.length + 1}] ${label}${unitTag} ${h.metadata.clause_title ? "— " + h.metadata.clause_title : ""}\n`;
338
338
  // tables: schema-aware pruning from the producer payload; the
339
339
  // stored text is the fallback (pruning never goes below baseline)
340
- const pruned = (h.metadata as any).block === "table" ? tableContext(h.metadata, query) : null;
340
+ const tableSel = (h.metadata as any).block === "table" ? tableSelection(h.metadata, query) : null;
341
+ const pruned = tableSel?.text ?? null;
342
+ if (tableSel) (h.metadata as any).table_selection = { cols: tableSel.cols, rowsShown: tableSel.rowsShown, rowsTotal: tableSel.rowsTotal };
341
343
  const body = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
342
344
  const t = estTokens(head) + estTokens(body);
343
345
  if (t <= remain) {
@@ -104,6 +104,7 @@ export const PROFILE = {
104
104
  },
105
105
  "prompts": {
106
106
  "vars": {
107
+ "publisher_identity": "the Fixture Organization — a worldwide organization that publishes the fixture corpus",
107
108
  "assistant_identity": "the fixture assistant — a public service answering questions about the fixture publisher's documents",
108
109
  "refusal_sentence": "I don't have information on this in the indexed fixture documents.",
109
110
  "account_note_source": "the user's own fixture account",
@@ -30,8 +30,10 @@ export async function handleProjects(env: any, sub: string, req: Request, route:
30
30
  }
31
31
  if (method === "POST") {
32
32
  const body: any = await req.json().catch(() => null);
33
- if (typeof body?.project_id === "string") {
34
- // move a conversation in/out of a project (membership-as-move)
33
+ if (body && (typeof body.project_id === "string" || body.project_id === null)) {
34
+ // move a conversation in/out of a project (membership-as-move) —
35
+ // null UNFILES (the 2026-09-16 bug: the string-only guard let the
36
+ // null case fall through to create-project and 400)
35
37
  const convId = String(body.conversation_id ?? "");
36
38
  if (!/^[a-zA-Z0-9_-]{8,64}$/.test(convId)) return err(400, "invalid_input", "bad conversation id");
37
39
  const target = body.project_id === null ? null : String(body.project_id);
@@ -39,13 +39,15 @@ export function telemetry(
39
39
  answerChars: number,
40
40
  queryHash: string,
41
41
  lang?: string,
42
+ cache?: "exact" | "semantic",
43
+ meta?: { durationMs?: number; keyId?: string | null },
42
44
  ) {
43
45
  const day = today();
44
46
  ctx.waitUntil(
45
47
  env.DB.batch([
46
48
  env.DB.prepare(
47
- "INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9)",
48
- ).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null),
49
+ "INSERT INTO queries (ts, day, tier, route, model, ok, answer_chars, query_hash, lang, cache, duration_ms, key_id) VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12)",
50
+ ).bind(new Date().toISOString(), day, tier, route, model, ok ? 1 : 0, answerChars, queryHash, lang ?? null, cache ?? null, meta?.durationMs ?? null, meta?.keyId ?? null),
49
51
  env.DB.prepare(
50
52
  "INSERT INTO spend (day, tier, model, requests) VALUES (?1,?2,?3,1) ON CONFLICT(day, tier, model) DO UPDATE SET requests = requests + 1",
51
53
  ).bind(day, tier, model ?? "none"),
@@ -31,6 +31,47 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
31
31
  if (!q) return err(400, "invalid_input", `query is required (1-${LIMITS.maxInputChars} chars)`);
32
32
  const maxIters = Math.min(Math.max(Number(body?.max_iterations) || 3, 1), 3);
33
33
 
34
+ // the passes, streamed when the caller asks: each retrieval and each
35
+ // sufficiency judgement is an event, so the reader watches the loop
36
+ // work instead of watching a spinner — the final event carries the
37
+ // same result object the JSON path returns
38
+ if (body?.stream === true) {
39
+ const enc = new TextEncoder();
40
+ const started = Date.now();
41
+ const stream = new ReadableStream({
42
+ async start(ctrl) {
43
+ const send = (e: unknown) => ctrl.enqueue(enc.encode(`data: ${JSON.stringify(e)}\n\n`));
44
+ try {
45
+ const out = await run(env, ctx, q, maxIters, send);
46
+ send({ type: "done", ...out });
47
+ } catch (e: any) {
48
+ send({ type: "error", message: String(e?.message ?? e).slice(0, 200) });
49
+ } finally {
50
+ ctrl.close();
51
+ }
52
+ },
53
+ });
54
+ return new Response(stream, {
55
+ headers: {
56
+ "content-type": "text/event-stream; charset=utf-8",
57
+ "cache-control": "no-cache",
58
+ "x-accel-buffering": "no",
59
+ ...corsHeaders(req),
60
+ },
61
+ });
62
+ void started;
63
+ }
64
+ const out = await run(env, ctx, q, maxIters);
65
+ return json({ ...out, ...corsHeaders(req) });
66
+ }
67
+
68
+ async function run(
69
+ env: Env,
70
+ ctx: ExecutionContext,
71
+ q: { query: string; lang?: string },
72
+ maxIters: number,
73
+ emit?: (e: unknown) => void,
74
+ ): Promise<{ answer: string; citations: unknown[]; model: string; query_hash: string; research: { iterations: number; passages: number; elapsed_ms: number; sufficient: boolean | null } }> {
34
75
  const started = Date.now();
35
76
  const queryHash = await sha256Hex(q.query);
36
77
  const understanding = await understandQuery(portModelRunner(env), MODELS.understand, q.query, [], []);
@@ -44,6 +85,7 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
44
85
 
45
86
  for (let i = 0; i < maxIters; i++) {
46
87
  iterations = i + 1;
88
+ emit?.({ type: "pass", n: iterations, of: maxIters, phase: "retrieving" });
47
89
  let retrieved: { hits: Hit[] };
48
90
  try {
49
91
  retrieved = await retrieve(env, q.query, {
@@ -95,6 +137,15 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
95
137
  }
96
138
  })();
97
139
  console.log("research iter", iterations, "passages", passages.length, "sufficient:", judge?.sufficient);
140
+ emit?.({
141
+ type: "pass",
142
+ n: iterations,
143
+ of: maxIters,
144
+ phase: "judged",
145
+ passages: passages.length,
146
+ sufficient: judge?.sufficient ?? null,
147
+ ...(judge && !judge.sufficient && judge.missing ? { missing: judge.missing.slice(0, 300) } : {}),
148
+ });
98
149
  if (!judge || judge.sufficient || !judge.missing) break;
99
150
  // fold, don't accumulate: appending every round's `missing` compounds
100
151
  // stale wants; the next retrieval focuses on the ORIGINAL question plus
@@ -104,14 +155,14 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
104
155
 
105
156
  const used = [...accumulated.values()];
106
157
  if (!used.length) {
107
- return err(503, "retrieval_unavailable", "Search is briefly busy — please retry in a moment.");
158
+ throw new Error("Search is briefly busy — please retry in a moment.");
108
159
  }
109
160
  const { messages, usedHits } = buildMessages(q.query, used, q.lang, [], eNote || undefined, undefined, LIMITS.inputTokenBudget);
110
161
  let answer = await generateOnce(env, MODELS.research, messages);
111
162
  if (answer === null) answer = await generateOnce(env, MODELS.fallback, messages);
112
163
  if (answer === null) {
113
164
  telemetry(env, ctx, "member", "research", MODELS.research, false, 0, queryHash, q.lang);
114
- return err(502, "generation_failed", "The generation model is unavailable; please retry.");
165
+ throw new Error("The generation model is unavailable; please retry.");
115
166
  }
116
167
  answer = canonicalRefusal(answer);
117
168
  const anchors = checkQuoteAnchors(answer, used.map((h: Hit) => h.text));
@@ -124,5 +175,6 @@ export async function handleResearch(env: Env, ctx: ExecutionContext, req: Reque
124
175
  research: { iterations, passages: used.length, elapsed_ms: Date.now() - started, sufficient: judge?.sufficient ?? null },
125
176
  };
126
177
  telemetry(env, ctx, "member", "research", MODELS.research, true, answer.length, queryHash, q.lang);
127
- return json({ ...out, ...corsHeaders(req) });
178
+ emit?.({ type: "pass", n: iterations, of: maxIters, phase: "writing", passages: used.length });
179
+ return out;
128
180
  }
@@ -8,7 +8,14 @@
8
8
  * selected column labels. Full table stays available for rendering;
9
9
  * this only shapes the prompt context, and falls back to the stored
10
10
  * text when pruning matches nothing (never worse than baseline). */
11
- export function tableContext(meta: any, query: string): string | null {
11
+ export interface TableSelection {
12
+ text: string;
13
+ cols: string[];
14
+ rowsShown: number;
15
+ rowsTotal: number;
16
+ }
17
+
18
+ export function tableSelection(meta: any, query: string): TableSelection | null {
12
19
  const t: any = meta?.table;
13
20
  if (!t || !Array.isArray(t.columns) || !Array.isArray(t.rows) || !t.rows.length) return null;
14
21
  const terms = new Set(
@@ -37,5 +44,9 @@ export function tableContext(meta: any, query: string): string | null {
37
44
  rowHits.length > CAP || rowHits.length < t.rows.length
38
45
  ? `\n(${shown.length} of ${t.rows.length} rows shown; ${t.rows.length - rowHits.length} rows did not match the question terms)`
39
46
  : "";
40
- return `${header}\n${lines.join("\n")}${elided}`;
47
+ return { text: `${header}\n${lines.join("\n")}${elided}`, cols: colKeep.map((i: number) => `${t.columns[i]?.label ?? ""}${t.columns[i]?.unit ? ` [${t.columns[i].unit}]` : ""}`), rowsShown: shown.length, rowsTotal: t.rows.length };
48
+ }
49
+
50
+ export function tableContext(meta: any, query: string): string | null {
51
+ return tableSelection(meta, query)?.text ?? null;
41
52
  }
@@ -0,0 +1,60 @@
1
+ // Judge-verdict extraction, kept free of the prompt import so plain
2
+ // node can load it for unit tests.
3
+ export interface Verdict {
4
+ score: number; // 0-1, clamped
5
+ ungrounded_claims: string[];
6
+ }
7
+
8
+ function coerceVerdict(obj: unknown): Verdict | null {
9
+ if (obj === null || typeof obj !== "object" || Array.isArray(obj)) return null;
10
+ const raw = (obj as { score?: unknown }).score;
11
+ const score = typeof raw === "number" ? raw : typeof raw === "string" && raw.trim() !== "" ? Number(raw) : NaN;
12
+ if (!Number.isFinite(score)) return null;
13
+ const claims = (obj as { ungrounded_claims?: unknown }).ungrounded_claims;
14
+ return {
15
+ score: Math.max(0, Math.min(1, score)),
16
+ ungrounded_claims: Array.isArray(claims) ? claims.map(String).slice(0, 5) : [],
17
+ };
18
+ }
19
+
20
+ // Reasoning models emit drafts, prose, fenced blocks, and several {...}
21
+ // fragments before the verdict; the verdict is the LAST well-formed
22
+ // object carrying a numeric score. Braces can also appear INSIDE claim
23
+ // strings, so the scan tracks string state instead of regexing flat
24
+ // objects (the flat scan silently missed those shapes live).
25
+ export function parseVerdict(text: string): Verdict | null {
26
+ const stripped = text.replace(/```[a-zA-Z]*\n?/g, "").replace(/```/g, "").trim();
27
+ try {
28
+ const whole = coerceVerdict(JSON.parse(stripped));
29
+ if (whole) return whole;
30
+ } catch {
31
+ // not one JSON document — scan candidates
32
+ }
33
+ let verdict: Verdict | null = null;
34
+ let depth = 0;
35
+ let start = -1;
36
+ let inString = false;
37
+ let escaped = false;
38
+ for (let i = 0; i < stripped.length; i++) {
39
+ const ch = stripped[i];
40
+ if (escaped) { escaped = false; continue; }
41
+ if (inString && ch === "\\") { escaped = true; continue; }
42
+ if (ch === '"') { inString = !inString; continue; }
43
+ if (inString) continue;
44
+ if (ch === "{") {
45
+ if (depth === 0) start = i;
46
+ depth++;
47
+ } else if (ch === "}" && depth > 0) {
48
+ depth--;
49
+ if (depth === 0 && start >= 0) {
50
+ try {
51
+ const v = coerceVerdict(JSON.parse(stripped.slice(start, i + 1)));
52
+ if (v) verdict = v;
53
+ } catch {
54
+ // malformed candidate — keep scanning
55
+ }
56
+ }
57
+ }
58
+ }
59
+ return verdict;
60
+ }