@konneal/engine 0.2.158 → 0.2.162

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,9 +1,9 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-STDXSVLX.js";
3
+ } from "./chunk-AEVDR7DA.js";
4
4
  import "./chunk-ECOYO7EX.js";
5
5
  import "./chunk-PBHMQZKA.js";
6
- import "./chunk-G2RE4XRV.js";
6
+ import "./chunk-MTIAHAOK.js";
7
7
  import "./chunk-NI4CIK6W.js";
8
8
  import "./chunk-W7J5SFXJ.js";
9
9
  import "./chunk-ETLSWSZ7.js";
@@ -75,7 +75,7 @@ import {
75
75
  tokenBudget,
76
76
  understandQuery,
77
77
  usageTotal
78
- } from "./chunk-G2RE4XRV.js";
78
+ } from "./chunk-MTIAHAOK.js";
79
79
  import {
80
80
  embed,
81
81
  generateOnce
@@ -144,6 +144,38 @@ async function retrieveInternal(service, auth, query) {
144
144
  // workers/worker_public/prompts/grader.md
145
145
  var grader_default = 'You grade retrieval quality for a legal-metrology Q&A system.\nGiven the question and the retrieved passage summaries, reply with ONLY:\n{"grade": "good"} \u2014 passages clearly contain the material to answer\n{"grade": "weak"} \u2014 passages are on the right publication/topic but lack the specific material (a broader or differently-worded retrieval might find it)\n{"grade": "bad"} \u2014 passages are unrelated to the question\n';
146
146
 
147
+ // workers/worker_public/src/crag.ts
148
+ async function gradePassage(ai, model, systemPrompt, query, passage, timeoutMs = 6e3) {
149
+ try {
150
+ const timeout = new Promise((r) => setTimeout(() => r(null), timeoutMs));
151
+ const call = (async () => {
152
+ const res = await ai.run(model, {
153
+ messages: [
154
+ { role: "system", content: systemPrompt },
155
+ { role: "user", content: `Question: ${query}
156
+
157
+ Passage:
158
+ ${passage.replace(/\s+/g, " ").slice(0, 1200)}` }
159
+ ],
160
+ max_tokens: 6144,
161
+ reasoning_effort: "low",
162
+ // a measurement: greedy decoding, no sampling
163
+ temperature: 0,
164
+ top_p: 1
165
+ });
166
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
167
+ const m = (text ?? "").match(/"grade"\s*:\s*"(correct|incorrect|ambiguous)"/);
168
+ return m ? m[1] : null;
169
+ })();
170
+ return await Promise.race([call, timeout]);
171
+ } catch {
172
+ return null;
173
+ }
174
+ }
175
+
176
+ // workers/worker_public/prompts/crag-passage.md
177
+ var crag_passage_default = 'You judge the RELEVANCE of one retrieved passage to a question, for a\nlegal-metrology retrieval system (the CRAG per-document evaluator).\nVerdicts:\n- "correct": the passage contains information useful for answering the question.\n- "incorrect": the passage is unrelated to the question or would mislead.\n- "ambiguous": partially related \u2014 relevant subject, unclear usefulness.\nReply with ONLY: {"grade": "correct" | "incorrect" | "ambiguous"}\n';
178
+
147
179
  // workers/worker_public/src/grader.ts
148
180
  async function gradeRetrieval(ai, model, query, passages) {
149
181
  if (!passages.length) return "bad";
@@ -210,6 +242,12 @@ async function scoreJudge(ai, model, systemPrompt, userPrompt) {
210
242
  return null;
211
243
  }
212
244
  }
245
+ function gradePassage2(ai, model, query, passage) {
246
+ return gradePassage(ai, model, crag_passage_default.trimEnd(), query, passage);
247
+ }
248
+ function gradePassages(ai, model, query, passages) {
249
+ return Promise.all(passages.map((p) => gradePassage2(ai, model, query, p)));
250
+ }
213
251
 
214
252
  // workers/worker_public/prompts/summarize.md
215
253
  var summarize_default = "Summarize this conversation so a Q&A assistant can continue it with full continuity. Capture: documents and editions discussed, questions asked, answers given (key values and definitions), terminology established, unresolved threads. Under 150 words, plain text, no preamble. The conversation may be any length and in any language \u2014 summarize it in English.\n";
@@ -1722,6 +1760,7 @@ async function attachFigureImages(env, messages, usedHits, query) {
1722
1760
  const payload = row ? JSON.parse(String(row.payload)) : {};
1723
1761
  const uri = typeof payload.uri === "string" ? payload.uri : "";
1724
1762
  if (typeof payload.caption === "string" && payload.caption.trim()) figCaption = payload.caption.trim();
1763
+ else if (typeof payload.description === "string" && payload.description.trim()) figCaption = payload.description.trim();
1725
1764
  const m = typeof uri === "string" ? uri.match(/^\/assets\/(.+)/) : null;
1726
1765
  if (!m) continue;
1727
1766
  const obj = await env.UNIT_ASSETS.get(m[1]);
@@ -2460,6 +2499,7 @@ ${summary}` }] : [],
2460
2499
  const routed = routeMode === "adaptive" ? routeFor(understanding, q2.query) : { route: routeMode, features: [`forced-${routeMode}`] };
2461
2500
  const routeSubset = ablate?.stages ?? (routed.route === "fast" ? fastRouteStages(STAGE_NAMES) : null);
2462
2501
  if (routeSubset) console.log("route:", routed.route, routed.features.join("+"), "\u2014", STAGE_NAMES.length - routeSubset.length, "lanes dropped");
2502
+ const crag = { grade: null, corrective: null };
2463
2503
  try {
2464
2504
  const tR = Date.now();
2465
2505
  if (declaredCtx?.kind === "account") {
@@ -2517,6 +2557,7 @@ Answer account questions from these records ONLY: name the record when you use i
2517
2557
  }
2518
2558
  }
2519
2559
  const grade = await gradePromise;
2560
+ crag.grade = grade;
2520
2561
  stageTiming.retrieve = Date.now() - tR;
2521
2562
  console.log("stage: grade+listwise", Date.now() - tR, "ms since retrieve start | grade:", grade);
2522
2563
  if (grade === "weak" && understanding?.docidentifier) {
@@ -2526,12 +2567,31 @@ Answer account questions from these records ONLY: name the record when you use i
2526
2567
  const grade2 = await gradeRetrieval(env.AI, roleModel(env, "grader"), q2.query, second.hits.map((h) => h.text));
2527
2568
  stageTiming.corrective = Date.now() - tc;
2528
2569
  if (grade2 === "good") retrieved = second;
2570
+ crag.corrective = { fired: true, accepted: grade2 === "good", grade: grade2 };
2529
2571
  }
2530
2572
  } catch (e) {
2531
2573
  console.log("ask: retrieval failed:", String(e).slice(0, 300));
2532
2574
  telemetry(env, ctx, tier, "ask", MODELS.embed, false, 0, await sha256Hex(q2.query), q2.lang, void 0, telemetryMeta());
2533
2575
  return finishLine("Search is briefly busy \u2014 please retry in a moment.");
2534
2576
  }
2577
+ {
2578
+ const figs = retrieved.hits.filter((h) => h.metadata.block === "figure" && h.metadata.unit_id);
2579
+ if (figs.length) {
2580
+ try {
2581
+ const ids = [...new Set(figs.map((h) => String(h.metadata.unit_id)))].slice(0, 20);
2582
+ const ph = ids.map((_, i) => `?${i + 1}`).join(",");
2583
+ const res = await env.DB.prepare(
2584
+ `SELECT unit_id, json_extract(payload,'$.description') AS d FROM unit_payloads WHERE unit_id IN (${ph})`
2585
+ ).bind(...ids).all();
2586
+ const byId = new Map((res.results ?? []).map((r) => [r.unit_id, r.d]));
2587
+ for (const h of figs) {
2588
+ const d = byId.get(String(h.metadata.unit_id));
2589
+ if (d) h.metadata.figure_description = String(d).slice(0, 600);
2590
+ }
2591
+ } catch {
2592
+ }
2593
+ }
2594
+ }
2535
2595
  const { hits } = retrieved;
2536
2596
  if (hits.length === 0 && !liveRecords?.length && (!boundModel || boundModel.gated)) {
2537
2597
  const answer2 = licenseBoundaryRefusal(modelDocHint?.doc_number ?? understanding?.doc_number ?? null, standardKeys) ?? refusalAnswer();
@@ -2654,6 +2714,7 @@ Answer account questions from these records ONLY: name the record when you use i
2654
2714
  send({ type: "citations", citations: cites, context_applied: ctxApplied, ...sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {} } : {}, ...liveRecords ? { records: liveRecords } : {}, quota });
2655
2715
  const stats = {};
2656
2716
  let full = "";
2717
+ let streamRestarts = 0;
2657
2718
  try {
2658
2719
  let headSettled = false;
2659
2720
  for await (const tok of sseTokens(stream, stats)) {
@@ -2666,6 +2727,7 @@ Answer account questions from these records ONLY: name the record when you use i
2666
2727
  headSettled = true;
2667
2728
  if (toolNarration(full) || intentPreamble(full)) {
2668
2729
  console.log(toolNarration(full) ? "tool-narration(stream): regenerating once" : "intent-preamble(stream): regenerating once");
2730
+ streamRestarts += 1;
2669
2731
  send({ type: "retry" });
2670
2732
  const regen = await generateStream(env, model, [
2671
2733
  ...messages,
@@ -2694,6 +2756,7 @@ Answer account questions from these records ONLY: name the record when you use i
2694
2756
  send({ type: "quota", quota: { ...quota, ...tq2, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
2695
2757
  if (withImage && register?.rows?.length && full.includes(refusalAnswer())) {
2696
2758
  console.log("blind-answer(stream): regenerating once");
2759
+ streamRestarts += 1;
2697
2760
  mark("Re-answering from the register results");
2698
2761
  send({ type: "retry" });
2699
2762
  const regen = await generateStream(env, model, [
@@ -2749,6 +2812,43 @@ Answer account questions from these records ONLY: name the record when you use i
2749
2812
  const streamed = { total: streamedAnchors.total, violations: streamedRetyped ? ["table-retyped"] : streamedAnchors.violations };
2750
2813
  if (streamed.violations.length > 0) {
2751
2814
  console.log("anchors:", streamed.violations.length, "of", streamed.total, "unverified \u2014 not caching");
2815
+ try {
2816
+ live2.send({
2817
+ type: "contract",
2818
+ verified: streamed.total - streamed.violations.length,
2819
+ violations: streamed.violations.slice(0, 5),
2820
+ note: "the streamed answer carried unverified quote anchors \u2014 the json lane enforces this contract by regeneration; treat the quoted spans as unverified"
2821
+ });
2822
+ } catch {
2823
+ }
2824
+ }
2825
+ if (body?.manifest === true) {
2826
+ try {
2827
+ live2.send({ type: "manifest", manifest: {
2828
+ index_version: env.INDEX_VERSION,
2829
+ corpus_gen: gen,
2830
+ model,
2831
+ effort: answerEffort(env),
2832
+ fresh,
2833
+ lane: "stream",
2834
+ citations: usedHits.length,
2835
+ retrieval: usedHits.map((h, i) => ({
2836
+ rank: i + 1,
2837
+ doc_id: h.metadata.doc_id,
2838
+ clause_anchor: h.metadata.clause_anchor ?? null,
2839
+ score: h.score,
2840
+ rerank_score: h.rerank_score ?? null
2841
+ })),
2842
+ stages: stageTiming,
2843
+ generate_retries: generateRetries,
2844
+ restarts: streamRestarts,
2845
+ crag: { retrieval_grade: crag.grade, corrective: crag.corrective, passages: null, note: "per-passage grades ride the json lane's manifest" },
2846
+ entailment: streamedEntailment,
2847
+ contract: { verified: streamed.total - streamed.violations.length, violations: streamed.violations.slice(0, 5) },
2848
+ total_ms: Date.now() - tStart
2849
+ } });
2850
+ } catch {
2851
+ }
2752
2852
  }
2753
2853
  if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && cacheableAnswer(canonical, refusalScope(answerCites(canonical))) && !ablate) {
2754
2854
  ctx.waitUntil(
@@ -2914,6 +3014,11 @@ Answer account questions from these records ONLY: name the record when you use i
2914
3014
  ...h.metadata.table_selection ? { sel: h.metadata.table_selection } : {}
2915
3015
  }));
2916
3016
  const tq = await chargeAnswerTokens(env, bucketId, void 0, messageChars(messages), answer.length, probeTokensUsed);
3017
+ let cragPassages = null;
3018
+ if (body?.manifest === true && used.length) {
3019
+ const grades = await gradePassages(env.AI, roleModel(env, "grader"), q2.query, used.map((h) => h.text));
3020
+ cragPassages = used.map((h, i) => ({ rank: i + 1, doc_id: h.metadata.doc_id, clause_anchor: h.metadata.clause_anchor ?? null, grade: grades[i] }));
3021
+ }
2917
3022
  const manifestOut = body?.manifest === true ? { manifest: {
2918
3023
  index_version: env.INDEX_VERSION,
2919
3024
  corpus_gen: gen,
@@ -2930,6 +3035,8 @@ Answer account questions from these records ONLY: name the record when you use i
2930
3035
  })),
2931
3036
  stages: stageTiming,
2932
3037
  generate_retries: generateRetries,
3038
+ crag: { retrieval_grade: crag.grade, corrective: crag.corrective, passages: cragPassages ?? [] },
3039
+ entailment,
2933
3040
  total_ms: Date.now() - tStart
2934
3041
  } } : {};
2935
3042
  return json({ ...out, context: contextOut, read: readAs(), ...manifestOut, quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
@@ -6,7 +6,7 @@ import {
6
6
  sessionFrom,
7
7
  telemetry,
8
8
  understandQuery
9
- } from "./chunk-G2RE4XRV.js";
9
+ } from "./chunk-MTIAHAOK.js";
10
10
  import {
11
11
  corsHeaders,
12
12
  err,
@@ -23640,7 +23640,10 @@ Context passages:
23640
23640
  const tableSel = h.metadata.block === "table" ? tableSelection(h.metadata, query) : null;
23641
23641
  const pruned = tableSel?.text ?? null;
23642
23642
  if (tableSel) h.metadata.table_selection = { cols: tableSel.cols, rowsShown: tableSel.rowsShown, rowsTotal: tableSel.rowsTotal };
23643
- const body = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
23643
+ const figDesc = h.metadata.block === "figure" ? String(h.metadata.figure_description ?? "").trim() : "";
23644
+ const raw = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
23645
+ const body = figDesc ? `${raw}
23646
+ Caption: ${figDesc}` : raw;
23644
23647
  const t = estTokens(head) + estTokens(body);
23645
23648
  if (t <= remain) {
23646
23649
  passageParts.push(head + body);
@@ -0,0 +1,11 @@
1
+ export declare function tokens(text: string): string[];
2
+ export declare function splitSentences(text: string): string[];
3
+ /** Query-term density, with a numeric sentence boosted: a sentence that
4
+ * states values, tolerances or clause numbers is evidence even when it
5
+ * repeats few query words. */
6
+ export declare function scoreSentence(sentence: string, qTerms: Set<string>): number;
7
+ /** Compress to at most `budgetChars` characters: the lead sentence is
8
+ * always kept (it carries the subject), the rest by score, emitted in
9
+ * reading order. No sentence scores above zero → plain truncation of
10
+ * the lead at a word boundary (never empty when input is non-empty). */
11
+ export declare function compressText(text: string, query: string, budgetChars: number): string;
package/dist/crag.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ export type PassageGrade = "correct" | "incorrect" | "ambiguous";
2
+ export declare function gradePassage(ai: any, model: string, systemPrompt: string, query: string, passage: string, timeoutMs?: number): Promise<PassageGrade | null>;
3
+ export declare function gradePassages(ai: any, model: string, systemPrompt: string, query: string, passages: string[]): Promise<(PassageGrade | null)[]>;
package/dist/grader.d.ts CHANGED
@@ -1,3 +1,7 @@
1
1
  export type RetrievalGrade = "good" | "weak" | "bad";
2
2
  export declare function gradeRetrieval(ai: any, model: string, query: string, passages: string[]): Promise<RetrievalGrade>;
3
3
  export declare function scoreJudge(ai: any, model: string, systemPrompt: string, userPrompt: string): Promise<number | null>;
4
+ import { type PassageGrade } from "./crag";
5
+ export type { PassageGrade };
6
+ export declare function gradePassage(ai: any, model: string, query: string, passage: string): Promise<PassageGrade | null>;
7
+ export declare function gradePassages(ai: any, model: string, query: string, passages: string[]): Promise<(PassageGrade | null)[]>;
@@ -0,0 +1,7 @@
1
+ You judge the RELEVANCE of one retrieved passage to a question, for a
2
+ legal-metrology retrieval system (the CRAG per-document evaluator).
3
+ Verdicts:
4
+ - "correct": the passage contains information useful for answering the question.
5
+ - "incorrect": the passage is unrelated to the question or would mislead.
6
+ - "ambiguous": partially related — relevant subject, unclear usefulness.
7
+ Reply with ONLY: {"grade": "correct" | "incorrect" | "ambiguous"}
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  handleSearch
3
- } from "./chunk-OL7KKGEO.js";
4
- import "./chunk-G2RE4XRV.js";
3
+ } from "./chunk-IGAGXQHV.js";
4
+ import "./chunk-MTIAHAOK.js";
5
5
  import "./chunk-NI4CIK6W.js";
6
6
  import "./chunk-W7J5SFXJ.js";
7
7
  import "./chunk-ETLSWSZ7.js";
@@ -4,7 +4,7 @@ import {
4
4
  handleMemories,
5
5
  scoreFaithfulness,
6
6
  scoreJudge
7
- } from "../../chunk-STDXSVLX.js";
7
+ } from "../../chunk-AEVDR7DA.js";
8
8
  import "../../chunk-ECOYO7EX.js";
9
9
  import {
10
10
  TOOLS_REGISTRY,
@@ -13,7 +13,7 @@ import {
13
13
  } from "../../chunk-PBHMQZKA.js";
14
14
  import {
15
15
  handleSearch
16
- } from "../../chunk-OL7KKGEO.js";
16
+ } from "../../chunk-IGAGXQHV.js";
17
17
  import {
18
18
  buildMessages,
19
19
  citations,
@@ -37,7 +37,7 @@ import {
37
37
  sessionFrom,
38
38
  telemetry,
39
39
  understandQuery
40
- } from "../../chunk-G2RE4XRV.js";
40
+ } from "../../chunk-MTIAHAOK.js";
41
41
  import {
42
42
  embed,
43
43
  generateOnce
@@ -757,6 +757,53 @@ async function handleRevokeKey(env, req, id) {
757
757
  return json({ ok: true, updated: r.meta?.changes ?? 0 });
758
758
  }
759
759
 
760
+ // workers/worker_public/src/compress.ts
761
+ var STOP = new Set(
762
+ "the a an and or of to in on for with by as at is are was were be been from that this these those it its their there which who what when where how why not no than then also such may must shall can could should would per within under over into about between during before after each any all more most less least other only same both few many some own very one two".split(
763
+ " "
764
+ )
765
+ );
766
+ function tokens(text) {
767
+ return (text.toLowerCase().match(/[a-z0-9]+(?:[.][0-9]+)?/g) ?? []).filter(
768
+ (t) => /^[0-9]/.test(t) || t.length >= 3 && !STOP.has(t)
769
+ );
770
+ }
771
+ function splitSentences(text) {
772
+ const parts = text.replace(/\r/g, "").split(/(?<=[.!?;])\s+|\n+/).map((s) => s.trim()).filter(Boolean);
773
+ return parts;
774
+ }
775
+ function scoreSentence(sentence, qTerms) {
776
+ const toks = tokens(sentence);
777
+ if (!toks.length) return 0;
778
+ let hits = 0;
779
+ for (const t of toks) if (qTerms.has(t)) hits += 1;
780
+ const density = hits / Math.sqrt(toks.length);
781
+ const numeric = /\d/.test(sentence) ? 0.35 : 0;
782
+ return density + numeric;
783
+ }
784
+ function compressText(text, query, budgetChars) {
785
+ const clean = text.replace(/\s+/g, " ").trim();
786
+ if (!clean || clean.length <= budgetChars) return clean;
787
+ const qTerms = new Set(tokens(query));
788
+ const sentences = splitSentences(clean);
789
+ if (sentences.length <= 1) {
790
+ const cut = clean.slice(0, Math.max(0, budgetChars));
791
+ const end = cut.lastIndexOf(" ");
792
+ return (end > 40 ? cut.slice(0, end) : cut).trim();
793
+ }
794
+ const scored = sentences.map((s, i) => ({ s, i, score: scoreSentence(s, qTerms) }));
795
+ const keep = /* @__PURE__ */ new Set([0]);
796
+ const ranked = scored.slice(1).sort((a, b) => b.score - a.score || a.i - b.i);
797
+ let used = sentences[0].length + 1;
798
+ for (const r of ranked) {
799
+ if (used + r.s.length + 1 > budgetChars) continue;
800
+ keep.add(r.i);
801
+ used += r.s.length + 1;
802
+ }
803
+ const out = scored.filter((r) => keep.has(r.i)).map((r) => r.s).join(" ").trim();
804
+ return out.slice(0, budgetChars);
805
+ }
806
+
760
807
  // workers/worker_public/prompts/research.md
761
808
  var research_default = `You are a sufficiency judge for a research loop over {{PUBLISHER_NAME}} publications. Given the research question and the passages collected so far (across iterations), decide whether the collected evidence is SUFFICIENT to write a complete, well-grounded answer.
762
809
 
@@ -840,7 +887,7 @@ async function run(env, ctx, q, maxIters, emit) {
840
887
  const older = passages.slice(0, Math.max(0, passages.length - KEEP_RECENT));
841
888
  const recent = passages.slice(-KEEP_RECENT);
842
889
  const digest = older.length ? `Earlier evidence (digest, ${older.length} passages):
843
- ${older.map((h) => `- ${h.metadata.docidentifier ?? ""} \xA7${h.metadata.clause_anchor ?? ""}: ${h.text.replace(/\s+/g, " ").slice(0, 160)}`).join("\n")}
890
+ ${older.map((h) => `- ${h.metadata.docidentifier ?? ""} \xA7${h.metadata.clause_anchor ?? ""}: ${compressText(h.text, q.query, 160)}`).join("\n")}
844
891
 
845
892
  ` : "";
846
893
  judge = await (async () => {
@@ -1124,7 +1171,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
1124
1171
  const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
1125
1172
  payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
1126
1173
  } else {
1127
- const res = name === "ask" ? await (await import("../../ask-7GRQQEBN.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-GAU5SEZI.js")).handleSearch(env, ctx, inner, tier, key);
1174
+ const res = name === "ask" ? await (await import("../../ask-ND63B3CF.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-FW5O32BY.js")).handleSearch(env, ctx, inner, tier, key);
1128
1175
  payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
1129
1176
  }
1130
1177
  telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
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.158",
30
+ "version": "0.2.162",
31
31
  "description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
32
32
  "license": "BSD-3-Clause",
33
33
  "type": "module",
@@ -0,0 +1,7 @@
1
+ You judge the RELEVANCE of one retrieved passage to a question, for a
2
+ legal-metrology retrieval system (the CRAG per-document evaluator).
3
+ Verdicts:
4
+ - "correct": the passage contains information useful for answering the question.
5
+ - "incorrect": the passage is unrelated to the question or would mislead.
6
+ - "ambiguous": partially related — relevant subject, unclear usefulness.
7
+ Reply with ONLY: {"grade": "correct" | "incorrect" | "ambiguous"}
@@ -10,7 +10,7 @@ import { buildMessages, citations, fill, promptVars, retrieve, retrievalQuery, i
10
10
  import { sessionFrom } from "./auth";
11
11
  import { retrieveInternal } from "./internal_gateway";
12
12
  import { understandQuery } from "./understand";
13
- import { gradeRetrieval } from "./grader";
13
+ import { gradePassages, gradeRetrieval } from "./grader";
14
14
  import summarizePrompt from "../prompts/summarize.md";
15
15
  import { embed, generateOnce } from "./ai";
16
16
  import { reflect } from "./reflect";
@@ -173,7 +173,11 @@ async function attachFigureImages(env: Env, messages: { role: string; content: a
173
173
  const row = await env.DB.prepare("SELECT payload FROM unit_payloads WHERE unit_id = ?1").bind(h.metadata.unit_id!).first<any>();
174
174
  const payload = row ? JSON.parse(String(row.payload)) : {};
175
175
  const uri = typeof payload.uri === "string" ? payload.uri : "";
176
+ // the producer's caption first; the caption LANE's output (a
177
+ // generated description) second — the field drift that kept
178
+ // generated captions out of this context
176
179
  if (typeof payload.caption === "string" && payload.caption.trim()) figCaption = payload.caption.trim();
180
+ else if (typeof payload.description === "string" && payload.description.trim()) figCaption = payload.description.trim();
177
181
  const m = typeof uri === "string" ? uri.match(/^\/assets\/(.+)/) : null;
178
182
  if (!m) continue;
179
183
  const obj = await env.UNIT_ASSETS.get(m[1]);
@@ -1295,6 +1299,10 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1295
1299
  : { route: routeMode, features: [`forced-${routeMode}`] };
1296
1300
  const routeSubset = ablate?.stages ?? (routed.route === "fast" ? fastRouteStages(STAGE_NAMES) : null);
1297
1301
  if (routeSubset) console.log("route:", routed.route, routed.features.join("+"), "—", STAGE_NAMES.length - routeSubset.length, "lanes dropped");
1302
+ // the CRAG state, hoisted out of the retrieval try so the manifest can
1303
+ // report it (TODO.sota/14.6 v2) — the coarse retrieval grade and the
1304
+ // corrective retry's firing/acceptance
1305
+ const crag: { grade: string | null; corrective: { fired: boolean; accepted: boolean; grade: string | null } | null } = { grade: null, corrective: null };
1298
1306
  try {
1299
1307
  const tR = Date.now();
1300
1308
  // ── The "my account" live read (TODO.ai-platform/03) — resolved
@@ -1381,6 +1389,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1381
1389
  }
1382
1390
  }
1383
1391
  const grade = await gradePromise;
1392
+ crag.grade = grade;
1384
1393
  stageTiming.retrieve = Date.now() - tR;
1385
1394
  console.log("stage: grade+listwise", Date.now() - tR, "ms since retrieve start | grade:", grade);
1386
1395
  if (grade === "weak" && understanding?.docidentifier) {
@@ -1390,12 +1399,34 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1390
1399
  const grade2 = await gradeRetrieval(env.AI, roleModel(env, "grader"), q.query, second.hits.map((h: Hit) => h.text));
1391
1400
  stageTiming.corrective = Date.now() - tc;
1392
1401
  if (grade2 === "good") retrieved = second; // corrective retry must be strictly better
1402
+ crag.corrective = { fired: true, accepted: grade2 === "good", grade: grade2 };
1393
1403
  }
1394
1404
  } catch (e) {
1395
1405
  console.log("ask: retrieval failed:", String(e).slice(0, 300));
1396
1406
  telemetry(env, ctx, tier, "ask", MODELS.embed, false, 0, await sha256Hex(q.query), q.lang, undefined, telemetryMeta());
1397
1407
  return finishLine("Search is briefly busy — please retry in a moment.");
1398
1408
  }
1409
+ // the figure stratum (TODO.sota/14.10): figure payloads' generated
1410
+ // captions ride their passages (buildMessages appends them) so the
1411
+ // model has words for the drawing and a reason to cite the unit —
1412
+ // one batched D1 read per turn that retrieved figures, fail-open
1413
+ {
1414
+ const figs = retrieved.hits.filter((h: Hit) => (h.metadata as any).block === "figure" && (h.metadata as any).unit_id);
1415
+ if (figs.length) {
1416
+ try {
1417
+ const ids = [...new Set(figs.map((h: Hit) => String((h.metadata as any).unit_id)))].slice(0, 20);
1418
+ const ph = ids.map((_, i) => `?${i + 1}`).join(",");
1419
+ const res = await env.DB.prepare(
1420
+ `SELECT unit_id, json_extract(payload,'$.description') AS d FROM unit_payloads WHERE unit_id IN (${ph})`,
1421
+ ).bind(...ids).all<{ unit_id: string; d: string | null }>();
1422
+ const byId = new Map((res.results ?? []).map((r) => [r.unit_id, r.d]));
1423
+ for (const h of figs) {
1424
+ const d = byId.get(String((h.metadata as any).unit_id));
1425
+ if (d) (h.metadata as any).figure_description = String(d).slice(0, 600);
1426
+ }
1427
+ } catch { /* the caption is additive — the passage serves without it */ }
1428
+ }
1429
+ }
1399
1430
  const { hits } = retrieved;
1400
1431
  if (hits.length === 0 && !liveRecords?.length && (!boundModel || boundModel.gated)) {
1401
1432
  // the license boundary refusal (TODO.external-refs/08) outranks the
@@ -1590,6 +1621,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1590
1621
  send({ type: "citations", citations: cites, context_applied: ctxApplied, ...(sourceQuality ? { source_quality: sourceQuality, confidence_note: qualityNote(sourceQuality), ...(sourceQuality === "ocr" ? { experimental_sources: experimentalSourceLabels(cites) } : {}) } : {}), ...(liveRecords ? { records: liveRecords } : {}), quota, });
1591
1622
  const stats: { usage?: unknown } = {};
1592
1623
  let full = "";
1624
+ let streamRestarts = 0; // the head restart + the blind-answer restart — the streamed manifest reports them
1593
1625
  try {
1594
1626
  // the head buffers until it can rule out the tool-narration
1595
1627
  // leak (the JSON lane's contract retry cannot run here — the
@@ -1605,6 +1637,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1605
1637
  headSettled = true;
1606
1638
  if (toolNarration(full) || intentPreamble(full)) {
1607
1639
  console.log(toolNarration(full) ? "tool-narration(stream): regenerating once" : "intent-preamble(stream): regenerating once");
1640
+ streamRestarts += 1;
1608
1641
  send({ type: "retry" });
1609
1642
  const regen = await generateStream(env, model, [
1610
1643
  ...messages,
@@ -1643,6 +1676,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1643
1676
  // tokens stream as the answer.
1644
1677
  if (withImage && register?.rows?.length && full.includes(refusalAnswer())) {
1645
1678
  console.log("blind-answer(stream): regenerating once");
1679
+ streamRestarts += 1;
1646
1680
  mark("Re-answering from the register results");
1647
1681
  send({ type: "retry" });
1648
1682
  const regen = await generateStream(env, model, [
@@ -1702,6 +1736,54 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1702
1736
  const streamed = { total: streamedAnchors.total, violations: streamedRetyped ? ["table-retyped"] : streamedAnchors.violations };
1703
1737
  if (streamed.violations.length > 0) {
1704
1738
  console.log("anchors:", streamed.violations.length, "of", streamed.total, "unverified — not caching");
1739
+ // the streaming-contract event (TODO.sota/14.5, v1 — the
1740
+ // detection half): a mid-stream answer cannot regenerate, so
1741
+ // the violations are reported to the client as they are —
1742
+ // measured, never hidden — and the unverified answer never
1743
+ // enters the cache. The bounded restart still covers the
1744
+ // head class (narration, preamble).
1745
+ try {
1746
+ live!.send({
1747
+ type: "contract",
1748
+ verified: streamed.total - streamed.violations.length,
1749
+ violations: streamed.violations.slice(0, 5),
1750
+ note: "the streamed answer carried unverified quote anchors — the json lane enforces this contract by regeneration; treat the quoted spans as unverified",
1751
+ });
1752
+ } catch { /* the client may have closed; never block the close */ }
1753
+ }
1754
+ // the streamed lane's manifest (TODO.sota/14.5: the post-answer
1755
+ // manifest reconciles what shipped): everything here is already
1756
+ // computed — the derivation identity, the retrieval anatomy, the
1757
+ // restart count (the restart cost's per-answer record) and the
1758
+ // contract state. The per-passage CRAG grades stay the JSON
1759
+ // lane's — post-answer dead air never justifies k grader calls
1760
+ // on the stream.
1761
+ if (body?.manifest === true) {
1762
+ try {
1763
+ live!.send({ type: "manifest", manifest: {
1764
+ index_version: env.INDEX_VERSION,
1765
+ corpus_gen: gen,
1766
+ model,
1767
+ effort: answerEffort(env),
1768
+ fresh,
1769
+ lane: "stream",
1770
+ citations: usedHits.length,
1771
+ retrieval: usedHits.map((h: Hit, i: number) => ({
1772
+ rank: i + 1,
1773
+ doc_id: h.metadata.doc_id,
1774
+ clause_anchor: h.metadata.clause_anchor ?? null,
1775
+ score: h.score,
1776
+ rerank_score: h.rerank_score ?? null,
1777
+ })),
1778
+ stages: stageTiming,
1779
+ generate_retries: generateRetries,
1780
+ restarts: streamRestarts,
1781
+ crag: { retrieval_grade: crag.grade, corrective: crag.corrective, passages: null, note: "per-passage grades ride the json lane's manifest" },
1782
+ entailment: streamedEntailment,
1783
+ contract: { verified: streamed.total - streamed.violations.length, violations: streamed.violations.slice(0, 5) },
1784
+ total_ms: Date.now() - tStart,
1785
+ } });
1786
+ } catch { /* the client may have closed; never block the close */ }
1705
1787
  }
1706
1788
  if (streamed.violations.length === 0 && canonical.length > 0 && !contextual && !declaredCtx && cacheableAnswer(canonical, refusalScope(answerCites(canonical))) && !ablate) {
1707
1789
  ctx.waitUntil(
@@ -1974,7 +2056,17 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1974
2056
  // retrieval anatomy (rank, corpus score, rerank score per used passage),
1975
2057
  // the stage timings, and the enforcement counters. Everything here is
1976
2058
  // already computed; the flag only ASSEMBLES it for the harness and for
1977
- // external auditors. v2 names the CRAG grades per passage.
2059
+ // external auditors. v2 added the CRAG grades: the retrieval grade and
2060
+ // corrective-retry outcome (hoisted state) plus the per-passage grades
2061
+ // (opt-in calls) and the entailment measurement.
2062
+ // the per-passage CRAG grades run ONLY on the manifest path — the
2063
+ // harness opted into the derivation, so it pays the k grader calls;
2064
+ // the default serving path never waits on them
2065
+ let cragPassages: { rank: number; doc_id: string; clause_anchor: string | null; grade: string | null }[] | null = null;
2066
+ if (body?.manifest === true && used.length) {
2067
+ const grades = await gradePassages(env.AI, roleModel(env, "grader"), q.query, (used as Hit[]).map((h: Hit) => h.text));
2068
+ cragPassages = (used as Hit[]).map((h: Hit, i: number) => ({ rank: i + 1, doc_id: h.metadata.doc_id, clause_anchor: h.metadata.clause_anchor ?? null, grade: grades[i] }));
2069
+ }
1978
2070
  const manifestOut = body?.manifest === true
1979
2071
  ? { manifest: {
1980
2072
  index_version: env.INDEX_VERSION,
@@ -1992,6 +2084,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1992
2084
  })),
1993
2085
  stages: stageTiming,
1994
2086
  generate_retries: generateRetries,
2087
+ crag: { retrieval_grade: crag.grade, corrective: crag.corrective, passages: cragPassages ?? [] },
2088
+ entailment,
1995
2089
  total_ms: Date.now() - tStart,
1996
2090
  } }
1997
2091
  : {};
@@ -0,0 +1,74 @@
1
+ // Extractive prompt compression (TODO.sota/02 row 5, LLMLingua-class
2
+ // adapted to the edge runtime): no second model, fully deterministic —
3
+ // sentences are scored by query-term density with a numeric-evidence
4
+ // bump (metrology answers live or die on numbers) and kept in reading
5
+ // order within a character budget. The consumer picks the budget; the
6
+ // compressor never grows text, never reorders it, and never invents.
7
+
8
+ const STOP = new Set(
9
+ "the a an and or of to in on for with by as at is are was were be been from that this these those it its their there which who what when where how why not no than then also such may must shall can could should would per within under over into about between during before after each any all more most less least other only same both few many some own very one two".split(
10
+ " ",
11
+ ),
12
+ );
13
+
14
+ export function tokens(text: string): string[] {
15
+ // numeric tokens survive at ANY length: "R 60", "15", "50 000" are the
16
+ // domain's own vocabulary, and the 3-char floor exists to drop prose
17
+ // fragments ("of", "a"), not identifiers
18
+ return (text.toLowerCase().match(/[a-z0-9]+(?:[.][0-9]+)?/g) ?? []).filter(
19
+ (t) => /^[0-9]/.test(t) || (t.length >= 3 && !STOP.has(t)),
20
+ );
21
+ }
22
+
23
+ export function splitSentences(text: string): string[] {
24
+ const parts = text
25
+ .replace(/\r/g, "")
26
+ .split(/(?<=[.!?;])\s+|\n+/)
27
+ .map((s) => s.trim())
28
+ .filter(Boolean);
29
+ return parts;
30
+ }
31
+
32
+ /** Query-term density, with a numeric sentence boosted: a sentence that
33
+ * states values, tolerances or clause numbers is evidence even when it
34
+ * repeats few query words. */
35
+ export function scoreSentence(sentence: string, qTerms: Set<string>): number {
36
+ const toks = tokens(sentence);
37
+ if (!toks.length) return 0;
38
+ let hits = 0;
39
+ for (const t of toks) if (qTerms.has(t)) hits += 1;
40
+ const density = hits / Math.sqrt(toks.length);
41
+ const numeric = /\d/.test(sentence) ? 0.35 : 0;
42
+ return density + numeric;
43
+ }
44
+
45
+ /** Compress to at most `budgetChars` characters: the lead sentence is
46
+ * always kept (it carries the subject), the rest by score, emitted in
47
+ * reading order. No sentence scores above zero → plain truncation of
48
+ * the lead at a word boundary (never empty when input is non-empty). */
49
+ export function compressText(text: string, query: string, budgetChars: number): string {
50
+ const clean = text.replace(/\s+/g, " ").trim();
51
+ if (!clean || clean.length <= budgetChars) return clean;
52
+ const qTerms = new Set(tokens(query));
53
+ const sentences = splitSentences(clean);
54
+ if (sentences.length <= 1) {
55
+ const cut = clean.slice(0, Math.max(0, budgetChars));
56
+ const end = cut.lastIndexOf(" ");
57
+ return (end > 40 ? cut.slice(0, end) : cut).trim();
58
+ }
59
+ const scored = sentences.map((s, i) => ({ s, i, score: scoreSentence(s, qTerms) }));
60
+ const keep = new Set<number>([0]);
61
+ const ranked = scored.slice(1).sort((a, b) => b.score - a.score || a.i - b.i);
62
+ let used = sentences[0].length + 1;
63
+ for (const r of ranked) {
64
+ if (used + r.s.length + 1 > budgetChars) continue;
65
+ keep.add(r.i);
66
+ used += r.s.length + 1;
67
+ }
68
+ const out = scored
69
+ .filter((r) => keep.has(r.i))
70
+ .map((r) => r.s)
71
+ .join(" ")
72
+ .trim();
73
+ return out.slice(0, budgetChars);
74
+ }
@@ -0,0 +1,52 @@
1
+ // The CRAG per-passage evaluator core (TODO.sota/14.6 v2): grade EACH used
2
+ // passage's relevance on the CRAG vocabulary. Dependency-free on purpose —
3
+ // the system prompt arrives as a parameter (the md-importing binding lives
4
+ // in grader.ts), the ai binding is injected, every failure mode is
5
+ // fail-open: a lost race or an unparseable reply grades null, because
6
+ // absence is honest and a guess is not. Opt-in — only the reproducibility
7
+ // manifest requests these grades, so the k calls never sit on the default
8
+ // serving path.
9
+
10
+ export type PassageGrade = "correct" | "incorrect" | "ambiguous";
11
+
12
+ export async function gradePassage(
13
+ ai: any,
14
+ model: string,
15
+ systemPrompt: string,
16
+ query: string,
17
+ passage: string,
18
+ timeoutMs = 6000,
19
+ ): Promise<PassageGrade | null> {
20
+ try {
21
+ const timeout = new Promise<null>((r) => setTimeout(() => r(null), timeoutMs));
22
+ const call = (async () => {
23
+ const res: any = await ai.run(model, {
24
+ messages: [
25
+ { role: "system", content: systemPrompt },
26
+ { role: "user", content: `Question: ${query}\n\nPassage:\n${passage.replace(/\s+/g, " ").slice(0, 1200)}` },
27
+ ],
28
+ max_tokens: 6144,
29
+ reasoning_effort: "low",
30
+ // a measurement: greedy decoding, no sampling
31
+ temperature: 0,
32
+ top_p: 1,
33
+ });
34
+ const text = typeof res?.response === "string" ? res.response : res?.choices?.[0]?.message?.content;
35
+ const m = (text ?? "").match(/"grade"\s*:\s*"(correct|incorrect|ambiguous)"/);
36
+ return m ? (m[1] as PassageGrade) : null;
37
+ })();
38
+ return await Promise.race([call, timeout]);
39
+ } catch {
40
+ return null;
41
+ }
42
+ }
43
+
44
+ export function gradePassages(
45
+ ai: any,
46
+ model: string,
47
+ systemPrompt: string,
48
+ query: string,
49
+ passages: string[],
50
+ ): Promise<(PassageGrade | null)[]> {
51
+ return Promise.all(passages.map((p) => gradePassage(ai, model, systemPrompt, query, p)));
52
+ }
@@ -90,3 +90,30 @@ export async function scoreJudge(
90
90
  return null;
91
91
  }
92
92
  }
93
+
94
+ // The CRAG per-passage binding (TODO.sota/14.6 v2): the dependency-free
95
+ // core (crag.ts) takes the system prompt as a parameter; this module binds
96
+ // the bundled prompt. Opt-in — only the reproducibility manifest requests
97
+ // these grades, so the k calls never sit on the default serving path.
98
+ import { gradePassage as gradePassageCore, type PassageGrade } from "./crag";
99
+ import CRAG_PASSAGE from "../prompts/crag-passage.md";
100
+
101
+ export type { PassageGrade };
102
+
103
+ export function gradePassage(
104
+ ai: any,
105
+ model: string,
106
+ query: string,
107
+ passage: string,
108
+ ): Promise<PassageGrade | null> {
109
+ return gradePassageCore(ai, model, CRAG_PASSAGE.trimEnd(), query, passage);
110
+ }
111
+
112
+ export function gradePassages(
113
+ ai: any,
114
+ model: string,
115
+ query: string,
116
+ passages: string[],
117
+ ): Promise<(PassageGrade | null)[]> {
118
+ return Promise.all(passages.map((p) => gradePassage(ai, model, query, p)));
119
+ }
@@ -360,7 +360,13 @@ export function buildMessages(
360
360
  const tableSel = (h.metadata as any).block === "table" ? tableSelection(h.metadata, query) : null;
361
361
  const pruned = tableSel?.text ?? null;
362
362
  if (tableSel) (h.metadata as any).table_selection = { cols: tableSel.cols, rowsShown: tableSel.rowsShown, rowsTotal: tableSel.rowsTotal };
363
- const body = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
363
+ // the figure stratum (TODO.sota/14.10): a caption-less figure's
364
+ // generated description rides its passage — the model gets words for
365
+ // the drawing and a reason to cite the unit; absent for every other
366
+ // block type and for uncaptioned figures
367
+ const figDesc = (h.metadata as any).block === "figure" ? String((h.metadata as any).figure_description ?? "").trim() : "";
368
+ const raw = clipToTokens(pruned ?? h.text, LIMITS.maxPassageTokens);
369
+ const body = figDesc ? `${raw}\nCaption: ${figDesc}` : raw;
364
370
  const t = estTokens(head) + estTokens(body);
365
371
  if (t <= remain) {
366
372
  passageParts.push(head + body);
@@ -8,6 +8,7 @@ import { retrieve, buildMessages, citations } from "./pipeline";
8
8
  import { understandQuery } from "./understand";
9
9
  import { generateOnce } from "./ai";
10
10
  import { canonicalRefusal } from "./refusal";
11
+ import { compressText } from "./compress";
11
12
  import { checkQuoteAnchors } from "./anchors";
12
13
  import { graphExpand, editionNote } from "./graph";
13
14
  import researchPromptText from "../prompts/research.md";
@@ -109,7 +110,7 @@ async function run(
109
110
  const older = passages.slice(0, Math.max(0, passages.length - KEEP_RECENT));
110
111
  const recent = passages.slice(-KEEP_RECENT);
111
112
  const digest = older.length
112
- ? `Earlier evidence (digest, ${older.length} passages):\n${older.map((h) => `- ${h.metadata.docidentifier ?? ""} §${h.metadata.clause_anchor ?? ""}: ${h.text.replace(/\s+/g, " ").slice(0, 160)}`).join("\n")}\n\n`
113
+ ? `Earlier evidence (digest, ${older.length} passages):\n${older.map((h) => `- ${h.metadata.docidentifier ?? ""} §${h.metadata.clause_anchor ?? ""}: ${compressText(h.text, q.query, 160)}`).join("\n")}\n\n`
113
114
  : "";
114
115
  judge = await (async () => {
115
116
  try {