@konneal/engine 0.2.152 → 0.2.155
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/answerGuards.d.ts +4 -0
- package/dist/{ask-LP6M4QNS.js → ask-MRVXR7CY.js} +1 -1
- package/dist/{chunk-VSK2RGUF.js → chunk-TXF5VMNM.js} +906 -875
- package/dist/worker_public/src/index.js +7 -3
- package/docs/AGENT-MODE-STREAM-FIRST.md +84 -0
- package/package.json +1 -1
- package/workers/worker_public/src/admin.ts +14 -1
- package/workers/worker_public/src/answerGuards.ts +36 -0
- package/workers/worker_public/src/ask.ts +207 -188
|
@@ -49,37 +49,8 @@ import { TOOL_DECLARATION, parseToolCall, runTool, toolNote } from "./tools";
|
|
|
49
49
|
import { operationsLane } from "./operations.ts";
|
|
50
50
|
import { extractNameplate, nameplateRegisterQuery } from "./nameplate.ts";
|
|
51
51
|
import { P } from "./profile.ts";
|
|
52
|
+
import { toolNarration, intentPreamble, TOOL_NARRATION_NOTICE, INTENT_PREAMBLE_NOTICE } from "./answerGuards.ts";
|
|
52
53
|
|
|
53
|
-
// the tool-narration leak (2026-10-14, the R 134-1 probe): the answer
|
|
54
|
-
// model sometimes NARRATES the agent loop's call in prose — 'The
|
|
55
|
-
// db.find tool returned, for the query "…": {…json…}' — instead of
|
|
56
|
-
// answering. The TOOL-line scrub cannot catch prose. The JSON lane
|
|
57
|
-
// regenerates through the contract retry; the streamed lane buffers
|
|
58
|
-
// the head and sends the retry sentinel (the blind-answer guard's
|
|
59
|
-
// client contract) before streaming the fresh answer. One bounded
|
|
60
|
-
// restart per lane.
|
|
61
|
-
function toolNarration(t: string): boolean {
|
|
62
|
-
return /^(?:the\s+)?[a-z][a-z_.]*\.[a-z_]+ tool returned\b/i.test(t.slice(0, 200)) ||
|
|
63
|
-
/tool returned,? for the query\b/i.test(t.slice(0, 300));
|
|
64
|
-
}
|
|
65
|
-
const TOOL_NARRATION_NOTICE =
|
|
66
|
-
"Correction notice: your draft narrated an internal tool call ('the … tool returned …') instead of answering. Rewrite: answer the question directly from the retrieved passages, cite the clauses, and never mention tools, queries, wire formats, or JSON.";
|
|
67
|
-
|
|
68
|
-
// The intent preamble (2026-10-19, the ~200-word technical-file
|
|
69
|
-
// question): the draft is a PLAN, not an answer — "I'll look up the R
|
|
70
|
-
// 76-1 MPE clause and classification details in the corpus." — shipped
|
|
71
|
-
// while ten citations rode the same response. Narrow by construction:
|
|
72
|
-
// it must OPEN with a first-person intent verb AND carry no substance —
|
|
73
|
-
// short, no clause citation, no normative numbers (a real answer's
|
|
74
|
-
// opening names values or clauses).
|
|
75
|
-
function intentPreamble(t: string): boolean {
|
|
76
|
-
const s = (t ?? "").trim();
|
|
77
|
-
if (s.length > 240) return false;
|
|
78
|
-
if (!/^(?:i'll|i will|let me|let's|i'm going to|i am going to)\b/i.test(s)) return false;
|
|
79
|
-
return !/\[\[u:|\[\^?\d+\]|\b\d+\.\d+\b/.test(s);
|
|
80
|
-
}
|
|
81
|
-
const INTENT_PREAMBLE_NOTICE =
|
|
82
|
-
"Correction notice: your draft announced a plan ('I'll look up …') instead of answering. Do not narrate intent — answer the question NOW from the retrieved passages: give the values, clause references and citations the question asks for.";
|
|
83
54
|
|
|
84
55
|
/** User-uploaded image for multimodal questions: a data URL
|
|
85
56
|
* (data:image/(png|jpeg|webp|gif);base64,…) up to 6 MB of payload. The
|
|
@@ -366,17 +337,6 @@ async function handleAsk(
|
|
|
366
337
|
let generateRetries = 0;
|
|
367
338
|
// how the question was read, for the reader: the interpretation that
|
|
368
339
|
// steered retrieval — a wrong read is visible before it costs trust
|
|
369
|
-
const readAs = () =>
|
|
370
|
-
understanding
|
|
371
|
-
? {
|
|
372
|
-
intent: understanding.intent,
|
|
373
|
-
doc: understanding.docidentifier,
|
|
374
|
-
edition: understanding.edition ?? null,
|
|
375
|
-
term: understanding.term,
|
|
376
|
-
terms: (understanding.defined_terms ?? []).slice(0, 4),
|
|
377
|
-
lang: q?.lang ?? null,
|
|
378
|
-
}
|
|
379
|
-
: undefined;
|
|
380
340
|
const serverTiming = () =>
|
|
381
341
|
Object.entries(stageTiming)
|
|
382
342
|
.map(([k, v]) => `${k};dur=${v}`)
|
|
@@ -482,11 +442,39 @@ async function handleAsk(
|
|
|
482
442
|
// owner's 2026-10-04 direction — the user must see the system-to-model
|
|
483
443
|
// questioning sessions, not stare at a spinner)
|
|
484
444
|
const agentSteps: string[] = [];
|
|
445
|
+
// THE AGENT-MODE CHANNEL (the owner's directive: users see every call
|
|
446
|
+
// as it runs): the streamed lane's SSE opens IMMEDIATELY — the channel
|
|
447
|
+
// exists before the pipeline starts, and every step (and the answer
|
|
448
|
+
// tokens) enqueues into it live. The json lane leaves it null and the
|
|
449
|
+
// steps fall back to the agentSteps buffer for its single response.
|
|
450
|
+
let live: { send: (ev: unknown) => void; close: () => void } | null = null;
|
|
451
|
+
let liveResponse: Response | null = null;
|
|
485
452
|
const mark = (step: string) => {
|
|
486
|
-
|
|
453
|
+
if (live) live.send({ type: "agent", step });
|
|
454
|
+
else agentSteps.push(step);
|
|
487
455
|
console.log(`agent-step: ${step}`);
|
|
488
456
|
};
|
|
489
457
|
|
|
458
|
+
// THE STREAM-FIRST HOIST (the owner's directive: users see every call as
|
|
459
|
+
// it runs): the SSE response returns NOW — before the vision step, the
|
|
460
|
+
// understanding, the register bridge — so every agent step streams to
|
|
461
|
+
// the user at the moment it runs, never replayed from a buffer. The json
|
|
462
|
+
// lane leaves the channel null and the buffer still collects for its
|
|
463
|
+
// single response.
|
|
464
|
+
const wantsStream = body?.stream === true || (tier === "anon" && body?.stream !== false);
|
|
465
|
+
if (wantsStream) {
|
|
466
|
+
const encoder = new TextEncoder();
|
|
467
|
+
let ctrl: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
468
|
+
const sse = new ReadableStream({
|
|
469
|
+
start(c) { ctrl = c; },
|
|
470
|
+
});
|
|
471
|
+
live = {
|
|
472
|
+
send: (ev: unknown) => { try { ctrl!.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`)); } catch { /* the client went away */ } },
|
|
473
|
+
close: () => { try { ctrl!.close(); } catch { /* closed */ } },
|
|
474
|
+
};
|
|
475
|
+
liveResponse = new Response(sse, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-accel-buffering": "no", ...corsHeaders(req) } });
|
|
476
|
+
}
|
|
477
|
+
|
|
490
478
|
// Members get federated retrieval (OIML + ISO/IEC) merged into the same
|
|
491
479
|
// pipeline via the service binding; rag-public never touches the
|
|
492
480
|
// internal index itself, and generation/rerank stay in ONE pipeline.
|
|
@@ -601,7 +589,42 @@ async function handleAsk(
|
|
|
601
589
|
const gen = await corpusGen(env.CACHE);
|
|
602
590
|
const withImage = effectiveImage || storedImage;
|
|
603
591
|
const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage || ablate ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
|
|
604
|
-
|
|
592
|
+
let probeTokensUsed = 0;
|
|
593
|
+
|
|
594
|
+
// THE PIPELINE: one body, two deliveries. Every `return` inside is an
|
|
595
|
+
// exit — for the streamed lane each exit converts to its live events
|
|
596
|
+
// via `finish` (and null, meaning the SSE already carried everything);
|
|
597
|
+
// for the json lane the same payload returns as the response.
|
|
598
|
+
const pipeline = async (
|
|
599
|
+
channel: { send: (ev: unknown) => void; close: () => void } | null,
|
|
600
|
+
qq: { query: string; lang?: string },
|
|
601
|
+
): Promise<Response | null> => {
|
|
602
|
+
// the channel is a parameter: effectively const inside, so every
|
|
603
|
+
// guard on it narrows for the whole body
|
|
604
|
+
const live = channel;
|
|
605
|
+
// the streamed lane's graceful degradation: a serving failure reaches
|
|
606
|
+
// the user as a line, never as a dropped stream
|
|
607
|
+
const q = qq;
|
|
608
|
+
const finishLine = (message: string) => {
|
|
609
|
+
if (live) {
|
|
610
|
+
live!.send({ type: "token", v: message });
|
|
611
|
+
live!.send({ type: "done", model: MODELS.member, query_hash: "", context_applied: NO_CONTEXT });
|
|
612
|
+
live!.close();
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
return err(503, "serving_busy", message);
|
|
616
|
+
};
|
|
617
|
+
const readAs = () =>
|
|
618
|
+
understanding
|
|
619
|
+
? {
|
|
620
|
+
intent: understanding.intent,
|
|
621
|
+
doc: understanding.docidentifier,
|
|
622
|
+
edition: understanding.edition ?? null,
|
|
623
|
+
term: understanding.term,
|
|
624
|
+
terms: (understanding.defined_terms ?? []).slice(0, 4),
|
|
625
|
+
lang: q?.lang ?? null,
|
|
626
|
+
}
|
|
627
|
+
: undefined;
|
|
605
628
|
|
|
606
629
|
if (cached) {
|
|
607
630
|
telemetry(env, ctx, tier, "ask", null, true, (cached.value.answer ?? "").length, cached.value.query_hash, q.lang, "exact", telemetryMeta());
|
|
@@ -610,13 +633,12 @@ async function handleAsk(
|
|
|
610
633
|
// node named in the question binds WITHOUT a chip and its echo must
|
|
611
634
|
// survive the cache, not silently flatten to "none")
|
|
612
635
|
const cctx = cached.value.context_applied ?? NO_CONTEXT;
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
], corsHeaders(req));
|
|
636
|
+
if (live) {
|
|
637
|
+
live!.send({ type: "citations", citations: cached.value.citations ?? [], quota, context_applied: cctx, ...(cached.value.source_quality ? { source_quality: cached.value.source_quality, confidence_note: cached.value.confidence_note ?? null, ...(cached.value.experimental_sources ? { experimental_sources: cached.value.experimental_sources } : {}), ...(cached.value.entailment ? { entailment: cached.value.entailment } : {}) } : {}) });
|
|
638
|
+
live!.send({ type: "token", v: cached.value.answer ?? "" });
|
|
639
|
+
live!.send({ type: "done", model: cached.value.model ?? MODELS.member, query_hash: cached.value.query_hash, served_from: "cache", context_applied: cctx });
|
|
640
|
+
live!.close();
|
|
641
|
+
return null;
|
|
620
642
|
}
|
|
621
643
|
return json({ ...cached.value, cached: true, quota, context_applied: cctx });
|
|
622
644
|
}
|
|
@@ -813,6 +835,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
813
835
|
// own reading is authoritative over the text lane's tokens ("this",
|
|
814
836
|
// "have" — stopwords that can match an unrelated holder)
|
|
815
837
|
if (nameplate?.manufacturer) {
|
|
838
|
+
mark(`certificates.search — ${nameplate.manufacturer}${nameplate.model ? ` ${nameplate.model}` : ""}`.slice(0, 140));
|
|
816
839
|
const bridged = await searchRegister(env.DB, nameplateRegisterQuery(nameplate, q.query), true);
|
|
817
840
|
if (bridged?.rows?.length) {
|
|
818
841
|
register = bridged;
|
|
@@ -855,20 +878,29 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
855
878
|
registerBlock = block;
|
|
856
879
|
aggregateNoteText = aggregateNote(agg);
|
|
857
880
|
const queryHash = await sha256Hex(q.query);
|
|
858
|
-
telemetry(env, ctx, tier, "ask", MODELS.member, true, answer
|
|
859
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, answer
|
|
860
|
-
|
|
881
|
+
telemetry(env, ctx, tier, "ask", MODELS.member, true, answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
882
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, answer!.length);
|
|
883
|
+
const payload = {
|
|
861
884
|
answer,
|
|
862
885
|
citations: [{ docidentifier: `${P().publisher.name}-CS certificate register (family ${agg.family})`, corpus: "register" }],
|
|
863
886
|
model: MODELS.member,
|
|
864
887
|
query_hash: queryHash,
|
|
865
|
-
follow_ups: [],
|
|
888
|
+
follow_ups: [] as string[],
|
|
866
889
|
blocks: [block],
|
|
867
890
|
context_applied: NO_CONTEXT,
|
|
868
891
|
read: readAs(),
|
|
869
892
|
...(ablate ? { ablate } : {}),
|
|
870
893
|
quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview },
|
|
871
|
-
}
|
|
894
|
+
};
|
|
895
|
+
if (live) {
|
|
896
|
+
live!.send({ type: "citations", citations: payload.citations, quota: payload.quota, context_applied: NO_CONTEXT });
|
|
897
|
+
live!.send({ type: "token", v: answer });
|
|
898
|
+
live!.send({ type: "done", model: MODELS.member, query_hash: queryHash, blocks: [block], follow_ups: [], context_applied: NO_CONTEXT, read: readAs() });
|
|
899
|
+
live!.send({ type: "quota", quota: payload.quota });
|
|
900
|
+
live!.close();
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
return json(payload);
|
|
872
904
|
}
|
|
873
905
|
}
|
|
874
906
|
}
|
|
@@ -890,37 +922,31 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
890
922
|
...keptHistory.slice(-6),
|
|
891
923
|
{ role: "user", content: q.query },
|
|
892
924
|
];
|
|
893
|
-
if (
|
|
925
|
+
if (live) {
|
|
926
|
+
// the reading arrives first — a conversational turn is still a reading
|
|
927
|
+
if (readAs()) live!.send({ type: "read", read: readAs() });
|
|
928
|
+
live!.send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota, });
|
|
929
|
+
const stats: { usage?: unknown } = {};
|
|
930
|
+
let full = "";
|
|
894
931
|
const stream = await generateStream(env, model, messages, effort);
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
for await (const tok of sseTokens(stream, stats)) {
|
|
907
|
-
full += tok;
|
|
908
|
-
send({ type: "token", v: tok });
|
|
909
|
-
}
|
|
910
|
-
} catch {
|
|
911
|
-
// stream ended prematurely — deliver what we have
|
|
912
|
-
}
|
|
913
|
-
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
|
|
914
|
-
send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
915
|
-
send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
|
|
916
|
-
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
917
|
-
controller.close();
|
|
918
|
-
},
|
|
919
|
-
});
|
|
920
|
-
return new Response(sse, {
|
|
921
|
-
headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-accel-buffering": "no", ...corsHeaders(req) },
|
|
922
|
-
});
|
|
932
|
+
try {
|
|
933
|
+
if (stream) {
|
|
934
|
+
for await (const tok of sseTokens(stream, stats)) {
|
|
935
|
+
full += tok;
|
|
936
|
+
live!.send({ type: "token", v: tok });
|
|
937
|
+
}
|
|
938
|
+
} else {
|
|
939
|
+
live!.send({ type: "token", v: "The generation model is unavailable; please retry." });
|
|
940
|
+
}
|
|
941
|
+
} catch {
|
|
942
|
+
// stream ended prematurely — deliver what we have
|
|
923
943
|
}
|
|
944
|
+
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages!), full.length, probeTokensUsed);
|
|
945
|
+
live!.send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
946
|
+
live!.send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
|
|
947
|
+
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
948
|
+
live!.close();
|
|
949
|
+
return null;
|
|
924
950
|
}
|
|
925
951
|
let answer = await generateOnce(env, model, messages, effort);
|
|
926
952
|
if (answer === null) answer = await generateOnce(env, MODELS.fallback, messages, effort);
|
|
@@ -928,7 +954,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
928
954
|
telemetry(env, ctx, tier, "ask", model, false, 0, queryHash, q.lang, undefined, telemetryMeta());
|
|
929
955
|
return err(502, "generation_failed", "The generation model is unavailable; please retry.");
|
|
930
956
|
}
|
|
931
|
-
telemetry(env, ctx, tier, "ask", model, true, answer
|
|
957
|
+
telemetry(env, ctx, tier, "ask", model, true, answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
932
958
|
return json({ answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: NO_CONTEXT, read: readAs(), quota, });
|
|
933
959
|
}
|
|
934
960
|
|
|
@@ -971,19 +997,16 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
971
997
|
console.log("draft act:", draftAct, "→", verdict.status === "draft" ? `draft (${Object.keys(verdict.draft.fields).length} fields)` : `refused (${verdict.reason})`);
|
|
972
998
|
const citations = verdict.citation ? [{ ...verdict.citation, corpus: P().publisher.id }] : [];
|
|
973
999
|
const draftPayload = verdict.status === "draft" ? verdict.draft : undefined;
|
|
974
|
-
telemetry(env, ctx, tier, "ask", model, true, verdict.answer
|
|
975
|
-
if (
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
],
|
|
983
|
-
corsHeaders(req),
|
|
984
|
-
);
|
|
1000
|
+
telemetry(env, ctx, tier, "ask", model, true, verdict.answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
1001
|
+
if (live) {
|
|
1002
|
+
if (readAs()) live!.send({ type: "read", read: readAs() });
|
|
1003
|
+
live!.send({ type: "citations", citations, context_applied: draftCtxApplied, ...(draftPayload ? { draft: draftPayload } : {}), quota, });
|
|
1004
|
+
live!.send({ type: "token", v: verdict.answer });
|
|
1005
|
+
live!.send({ type: "done", model, query_hash: queryHash, context_applied: draftCtxApplied, read: readAs(), ...(ablate ? { ablate } : {}) });
|
|
1006
|
+
live!.close();
|
|
1007
|
+
return null;
|
|
985
1008
|
}
|
|
986
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer
|
|
1009
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer!.length);
|
|
987
1010
|
return json({ answer: verdict.answer, citations, model, query_hash: queryHash, follow_ups: [], context_applied: draftCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), ...(ablate ? { ablate } : {}), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
988
1011
|
}
|
|
989
1012
|
|
|
@@ -1009,19 +1032,16 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1009
1032
|
});
|
|
1010
1033
|
console.log("api_call draft:", apiCallIntent, "→", verdict.status === "draft" ? `draft (${verdict.draft.call.method} ${verdict.draft.call.path})` : `refused (${verdict.reason})`);
|
|
1011
1034
|
const draftPayload = verdict.status === "draft" ? verdict.draft : undefined;
|
|
1012
|
-
telemetry(env, ctx, tier, "ask", model, true, verdict.answer
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
],
|
|
1021
|
-
corsHeaders(req),
|
|
1022
|
-
);
|
|
1035
|
+
telemetry(env, ctx, tier, "ask", model, true, verdict.answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
1036
|
+
if (live) {
|
|
1037
|
+
if (readAs()) live!.send({ type: "read", read: readAs() });
|
|
1038
|
+
live!.send({ type: "citations", citations: [], context_applied: callCtxApplied, ...(draftPayload ? { draft: draftPayload } : {}), quota, });
|
|
1039
|
+
live!.send({ type: "token", v: verdict.answer });
|
|
1040
|
+
live!.send({ type: "done", model, query_hash: queryHash, context_applied: callCtxApplied, read: readAs(), ...(ablate ? { ablate } : {}) });
|
|
1041
|
+
live!.close();
|
|
1042
|
+
return null;
|
|
1023
1043
|
}
|
|
1024
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer
|
|
1044
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer!.length);
|
|
1025
1045
|
return json({ answer: verdict.answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: callCtxApplied, read: readAs(), ...(draftPayload ? { draft: draftPayload } : {}), ...(ablate ? { ablate } : {}), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
1026
1046
|
}
|
|
1027
1047
|
|
|
@@ -1374,7 +1394,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1374
1394
|
} catch (e) {
|
|
1375
1395
|
console.log("ask: retrieval failed:", String(e).slice(0, 300));
|
|
1376
1396
|
telemetry(env, ctx, tier, "ask", MODELS.embed, false, 0, await sha256Hex(q.query), q.lang, undefined, telemetryMeta());
|
|
1377
|
-
return
|
|
1397
|
+
return finishLine("Search is briefly busy — please retry in a moment.");
|
|
1378
1398
|
}
|
|
1379
1399
|
const { hits } = retrieved;
|
|
1380
1400
|
if (hits.length === 0 && !liveRecords?.length && (!boundModel || boundModel.gated)) {
|
|
@@ -1385,7 +1405,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1385
1405
|
const answer =
|
|
1386
1406
|
licenseBoundaryRefusal(modelDocHint?.doc_number ?? understanding?.doc_number ?? null, standardKeys) ?? refusalAnswer();
|
|
1387
1407
|
const out = { answer, citations: [], model, query_hash: await sha256Hex(q.query), context_applied: ctxApplied, ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
|
|
1388
|
-
telemetry(env, ctx, tier, "ask", model, true, answer
|
|
1408
|
+
telemetry(env, ctx, tier, "ask", model, true, answer!.length, out.query_hash, q.lang, undefined, telemetryMeta());
|
|
1389
1409
|
return json({ ...out, quota, });
|
|
1390
1410
|
}
|
|
1391
1411
|
|
|
@@ -1461,13 +1481,13 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1461
1481
|
const pendingTools: { name: string; query: string; output: string }[] = [];
|
|
1462
1482
|
// the agent's own consumption (2026-10-11): every tool probe is a real
|
|
1463
1483
|
// model call — its usage joins the charge so credits price the true cost
|
|
1464
|
-
|
|
1484
|
+
|
|
1465
1485
|
// On an IMAGE turn the probe CARRIES THE IMAGE (2026-10-03, the owner's
|
|
1466
1486
|
// architecture): the model that reads the plate is the model that issues
|
|
1467
1487
|
// the tool call — one reader, one tool, no parallel extraction racing a
|
|
1468
1488
|
// second vision pass that drifts hour to hour. The old probe dropped the
|
|
1469
1489
|
// image-bearing message, which is why the private bridge had to exist.
|
|
1470
|
-
if (multiTurn && env.DB) {
|
|
1490
|
+
if (multiTurn && env.DB && !nameplate?.manufacturer) {
|
|
1471
1491
|
try {
|
|
1472
1492
|
const imageTurn = typeof withImage === "string";
|
|
1473
1493
|
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
@@ -1552,13 +1572,10 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1552
1572
|
// provenance — ahead of the prose passages.
|
|
1553
1573
|
const cites = boundModel ? [modelCitation(boundModel), ...citations(usedHits)] : citations(usedHits);
|
|
1554
1574
|
|
|
1555
|
-
if (
|
|
1575
|
+
if (live) {
|
|
1556
1576
|
const stream = await generateStream(env, model, messages, routeEffort(routed.route));
|
|
1557
1577
|
if (stream) {
|
|
1558
|
-
const
|
|
1559
|
-
const sse = new ReadableStream({
|
|
1560
|
-
async start(controller) {
|
|
1561
|
-
const send = (obj: unknown) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
1578
|
+
const send = (obj: unknown) => live!.send(obj);
|
|
1562
1579
|
// the agent trace arrives first: every questioning round the
|
|
1563
1580
|
// assistant ran — the photograph read, the register search, any
|
|
1564
1581
|
// re-answer — so the user watched what happened, not a spinner
|
|
@@ -1615,7 +1632,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1615
1632
|
} catch {
|
|
1616
1633
|
// stream ended prematurely — deliver what we have
|
|
1617
1634
|
}
|
|
1618
|
-
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
|
|
1635
|
+
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages!), full.length, probeTokensUsed);
|
|
1619
1636
|
send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
1620
1637
|
// the blind-answer guard ON THE STREAM PATH (the JSON path has
|
|
1621
1638
|
// had it since 0.2.80): an image rode the turn, the register
|
|
@@ -1671,10 +1688,10 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1671
1688
|
new Promise<null>((r) => setTimeout(() => r(null), THRESHOLDS.entailmentBudgetMs)),
|
|
1672
1689
|
]);
|
|
1673
1690
|
if (raced) {
|
|
1674
|
-
const v = entailmentVerdict(raced
|
|
1675
|
-
streamedEntailment = { support: v.support, score: Math.round(raced
|
|
1691
|
+
const v = entailmentVerdict(raced!.score, THRESHOLDS.entailmentSupportedFloor, THRESHOLDS.entailmentPartialFloor);
|
|
1692
|
+
streamedEntailment = { support: v.support, score: Math.round(raced!.score * 100) / 100, ...(raced!.ungrounded_claims?.length ? { ungrounded: raced!.ungrounded_claims.slice(0, 3) } : {}) };
|
|
1676
1693
|
streamedNote = v.note;
|
|
1677
|
-
if (v.support !== "supported") console.log("entailment(stream):", v.support, raced
|
|
1694
|
+
if (v.support !== "supported") console.log("entailment(stream):", v.support, raced!.score);
|
|
1678
1695
|
send({ type: "confidence", source_quality: sourceQuality, confidence_note: streamedNote || qualityNote(sourceQuality), ...(streamedEntailment ? { entailment: streamedEntailment } : {}) });
|
|
1679
1696
|
}
|
|
1680
1697
|
}
|
|
@@ -1691,19 +1708,17 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1691
1708
|
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 }),
|
|
1692
1709
|
);
|
|
1693
1710
|
}
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
});
|
|
1711
|
+
live!.close();
|
|
1712
|
+
} else {
|
|
1713
|
+
// the streamed lane's generation is unavailable — the line goes
|
|
1714
|
+
// out on the already-open SSE; the json lane never enters this block
|
|
1715
|
+
live!.send({ type: "token", v: "The generation model is unavailable; please retry." });
|
|
1716
|
+
live!.send({ type: "done", model: MODELS.member, query_hash: "", context_applied: NO_CONTEXT });
|
|
1717
|
+
live!.close();
|
|
1718
|
+
return null;
|
|
1719
|
+
}
|
|
1720
|
+
return null;
|
|
1705
1721
|
}
|
|
1706
|
-
}
|
|
1707
1722
|
|
|
1708
1723
|
const tGen = Date.now();
|
|
1709
1724
|
// ── speculative draft-verify (TODO.sota/02 row 2): when the ablation
|
|
@@ -1722,9 +1737,9 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1722
1737
|
return null;
|
|
1723
1738
|
});
|
|
1724
1739
|
if (spec) {
|
|
1725
|
-
answer = spec
|
|
1740
|
+
answer = spec!.answer;
|
|
1726
1741
|
stageTiming.speculative = Date.now() - tGen;
|
|
1727
|
-
console.log("speculative:", spec
|
|
1742
|
+
console.log("speculative:", spec!.nDrafts, "drafts,", spec!.chosen, "viable — verified");
|
|
1728
1743
|
}
|
|
1729
1744
|
}
|
|
1730
1745
|
if (!answer) answer = await generateOnce(env, model, messages, routeEffort(routed.route));
|
|
@@ -1749,19 +1764,25 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1749
1764
|
);
|
|
1750
1765
|
answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
|
|
1751
1766
|
}
|
|
1767
|
+
if (answer === null) {
|
|
1768
|
+
// both generations failed — the json lane returns the error shape; the
|
|
1769
|
+
// streamed lane's equivalent lives inside the streamed block above
|
|
1770
|
+
telemetry(env, ctx, tier, "ask", model, false, 0, queryHash, q.lang, undefined, telemetryMeta());
|
|
1771
|
+
return err(502, "generation_failed", "The generation model is unavailable; please retry.");
|
|
1772
|
+
}
|
|
1752
1773
|
// the protocol scrub: a TOOL line is the agent loop's wire protocol,
|
|
1753
1774
|
// never user-facing prose — the answer model occasionally echoes the
|
|
1754
1775
|
// line it just composed (observed live: 'TOOL db.find …' opened a
|
|
1755
1776
|
// served answer). Stripped here and on the streamed path.
|
|
1756
1777
|
const scrubToolLines = (t: string) => t.replace(/^TOOL [a-z_.]+.*$\n?/gm, "").replace(/^\n+/, "");
|
|
1757
|
-
if (answer) answer = scrubToolLines(answer);
|
|
1758
|
-
if (answer) answer = canonicalRefusal(answer
|
|
1759
|
-
if (answer) answer = scrubToolLines(answer);
|
|
1778
|
+
if (answer) answer = scrubToolLines(answer!);
|
|
1779
|
+
if (answer) answer = canonicalRefusal(answer!, refusalScope(answerCites(answer!)));
|
|
1780
|
+
if (answer) answer = scrubToolLines(answer!);
|
|
1760
1781
|
// the misplaced-pin wall: a CITED answer is not a refusal — excise
|
|
1761
1782
|
// the pinned sentence if the model dropped it in as a disclaimer
|
|
1762
1783
|
// (measured on chip-scoped asks: correct cited answers carrying the
|
|
1763
1784
|
// pin mid-text, read by users and graders as total refusals)
|
|
1764
|
-
if (answer) answer = exciseMisplacedPin(answer
|
|
1785
|
+
if (answer) answer = exciseMisplacedPin(answer!, answerCites(answer!));
|
|
1765
1786
|
// the tool-narration leak (2026-10-14, the R 134-1 probe): the answer
|
|
1766
1787
|
// model sometimes NARRATES the agent loop's call in prose — 'The
|
|
1767
1788
|
// db.find tool returned, for the query "…": {…json…}' — instead of
|
|
@@ -1775,31 +1796,31 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1775
1796
|
// the passages or a typed table was retyped as markdown; the retry
|
|
1776
1797
|
// wins only if it verifies better.
|
|
1777
1798
|
let used = usedHits;
|
|
1778
|
-
if (answer && !answer
|
|
1779
|
-
const anchors = checkQuoteAnchors(answer
|
|
1799
|
+
if (answer && !answer!.includes(refusalAnswer())) {
|
|
1800
|
+
const anchors = checkQuoteAnchors(answer!, used.map((h: Hit) => h.text));
|
|
1780
1801
|
const hasTableUnit = used.some((h: Hit) => h.metadata.unit_id && h.metadata.block === "table");
|
|
1781
|
-
const retyped = tableRetyped(answer
|
|
1802
|
+
const retyped = tableRetyped(answer!, hasTableUnit);
|
|
1782
1803
|
// presenting a served table's DATA without its unit reference is the
|
|
1783
1804
|
// same contract violation as retyping it — the HARD RULE wants the
|
|
1784
1805
|
// token wherever the table's values carry the answer
|
|
1785
1806
|
const unreferenced = (() => {
|
|
1786
|
-
if (!hasTableUnit || answer
|
|
1807
|
+
if (!hasTableUnit || answer!.includes("[[u:")) return false;
|
|
1787
1808
|
const norm = (s: string) => (s.match(/\d[\d ,.]{1,8}\d/g) ?? []).map((x) => x.replace(/[ ,.]/g, ""));
|
|
1788
|
-
const nums = norm(answer);
|
|
1809
|
+
const nums = norm(answer!);
|
|
1789
1810
|
if (nums.length < 2) return false;
|
|
1790
1811
|
const tableNums = new Set(
|
|
1791
1812
|
norm(used.filter((h: Hit) => h.metadata.unit_id && h.metadata.block === "table").map((h: Hit) => h.text).join(" ")),
|
|
1792
1813
|
);
|
|
1793
1814
|
return nums.filter((n) => tableNums.has(n)).length >= 2;
|
|
1794
1815
|
})();
|
|
1795
|
-
if (anchors.violations.length > 0 || retyped || unreferenced || toolNarration(answer) || intentPreamble(answer)) {
|
|
1796
|
-
console.log("contract check:", anchors.violations.length, "anchor violations; tableRetyped:", retyped, "; tableDataUnreferenced:", unreferenced, "; toolNarration:", toolNarration(answer), "; intentPreamble:", intentPreamble(answer), "— regenerating");
|
|
1816
|
+
if (anchors.violations.length > 0 || retyped || unreferenced || toolNarration(answer!) || intentPreamble(answer!)) {
|
|
1817
|
+
console.log("contract check:", anchors.violations.length, "anchor violations; tableRetyped:", retyped, "; tableDataUnreferenced:", unreferenced, "; toolNarration:", toolNarration(answer!), "; intentPreamble:", intentPreamble(answer!), "— regenerating");
|
|
1797
1818
|
// name the EXACT unit the token must reference — a generic note
|
|
1798
1819
|
// leaves the model guessing which id to write
|
|
1799
1820
|
const tableUnitId = unreferenced
|
|
1800
1821
|
? used.find((h: Hit) => h.metadata.unit_id && h.metadata.block === "table")?.metadata.unit_id
|
|
1801
1822
|
: undefined;
|
|
1802
|
-
const note = toolNarration(answer)
|
|
1823
|
+
const note = toolNarration(answer!)
|
|
1803
1824
|
? TOOL_NARRATION_NOTICE
|
|
1804
1825
|
: intentPreamble(answer)
|
|
1805
1826
|
? INTENT_PREAMBLE_NOTICE
|
|
@@ -1809,7 +1830,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1809
1830
|
generateRetries += 1;
|
|
1810
1831
|
const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
|
|
1811
1832
|
if (corrected) {
|
|
1812
|
-
const correctedAnswer = canonicalRefusal(corrected
|
|
1833
|
+
const correctedAnswer = canonicalRefusal(corrected!, refusalScope(answerCites(corrected!)));
|
|
1813
1834
|
const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h: Hit) => h.text));
|
|
1814
1835
|
const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
|
|
1815
1836
|
if (retryAnchors.violations.length < anchors.violations.length || (!retryRetyped && retyped) || (unreferenced && correctedAnswer.includes("[[u:")) || !toolNarration(correctedAnswer) || !intentPreamble(correctedAnswer)) {
|
|
@@ -1834,27 +1855,27 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1834
1855
|
// The model critiques its own answer; if claims are ungrounded, retry
|
|
1835
1856
|
// retrieval with the missing-info hint (max one retry).
|
|
1836
1857
|
// Ref: selfrag.github.io; arXiv 2606.05658 bounded reflection
|
|
1837
|
-
if (answer && !answer
|
|
1838
|
-
const reflection = await reflect(env.AI, MODELS.grader, q.query, answer
|
|
1839
|
-
console.log("reflection:", reflection ? (reflection
|
|
1840
|
-
if (reflection && !reflection
|
|
1858
|
+
if (answer && !answer!.includes(refusalAnswer())) {
|
|
1859
|
+
const reflection = await reflect(env.AI, MODELS.grader, q.query, answer!, hits.map((h: Hit) => h.text));
|
|
1860
|
+
console.log("reflection:", reflection ? (reflection!.grounded ? "grounded" : "ungrounded") : "null");
|
|
1861
|
+
if (reflection && !reflection!.grounded && reflection!.missing_info) {
|
|
1841
1862
|
// re-retrieve targeting what was missing — the declared context's
|
|
1842
1863
|
// hard seal binds the retry exactly as the first pass
|
|
1843
1864
|
// (TODO.ai-platform/02)
|
|
1844
1865
|
const retryRetrieve = await retrieve(env, q.query, {
|
|
1845
1866
|
prev,
|
|
1846
|
-
understanding: { ...understanding, standalone_query: `${understanding?.standalone_query || q.query} ${reflection
|
|
1867
|
+
understanding: { ...understanding, standalone_query: `${understanding?.standalone_query || q.query} ${reflection!.missing_info}` } as any,
|
|
1847
1868
|
sealScope: declaredScoped ? docScope : null,
|
|
1848
1869
|
datasetScope: narrowed ? corpora : null,
|
|
1849
1870
|
standardKeys,
|
|
1850
|
-
...(routeSubset ? { stageSubset: routeSubset } : {}),
|
|
1871
|
+
...(routeSubset ? { stageSubset: routeSubset ?? undefined } : {}),
|
|
1851
1872
|
});
|
|
1852
1873
|
if (retryRetrieve.hits.length > 0) {
|
|
1853
1874
|
const { messages: retryMessages, usedHits: retryUsed } = buildMessages(q.query, retryRetrieve.hits, q.lang, keptHistory, undefined, summary, budget);
|
|
1854
1875
|
const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
|
|
1855
1876
|
// the answer now comes from the retry passages — citations must follow
|
|
1856
1877
|
if (retryAnswer) {
|
|
1857
|
-
answer = canonicalRefusal(retryAnswer
|
|
1878
|
+
answer = canonicalRefusal(retryAnswer!, refusalScope(answerCites(retryAnswer!)));
|
|
1858
1879
|
used = retryUsed;
|
|
1859
1880
|
}
|
|
1860
1881
|
}
|
|
@@ -1865,14 +1886,14 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1865
1886
|
telemetry(env, ctx, tier, "ask", model, false, 0, queryHash, q.lang, undefined, telemetryMeta());
|
|
1866
1887
|
return err(502, "generation_failed", "The generation model is unavailable; please retry.");
|
|
1867
1888
|
}
|
|
1868
|
-
const finalCites = boundModel ? [modelCitation(boundModel), ...citations(used)] : citations(used);
|
|
1869
|
-
const c2ns = answer
|
|
1889
|
+
const finalCites = boundModel ? [modelCitation(boundModel!), ...citations(used)] : citations(used);
|
|
1890
|
+
const c2ns = answer!.includes(refusalAnswer())
|
|
1870
1891
|
? { text: answer, blocks: [] as Awaited<ReturnType<typeof contractV2>>["blocks"], dropped: [] as string[] }
|
|
1871
|
-
: await contractV2(env.DB, answer
|
|
1892
|
+
: await contractV2(env.DB, answer!, used);
|
|
1872
1893
|
answer = c2ns.text;
|
|
1873
|
-
const finalAnchors = answer
|
|
1894
|
+
const finalAnchors = answer!.includes(refusalAnswer())
|
|
1874
1895
|
? { total: 0, violations: [] as string[] }
|
|
1875
|
-
: checkQuoteAnchors(answer
|
|
1896
|
+
: checkQuoteAnchors(answer!, used.map((h: Hit) => h.text));
|
|
1876
1897
|
if (finalAnchors.violations.length > 0) {
|
|
1877
1898
|
console.log("anchors:", finalAnchors.violations.length, "of", finalAnchors.total, "unverified — not caching");
|
|
1878
1899
|
}
|
|
@@ -1884,20 +1905,20 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1884
1905
|
// fallback) but contractV2 then dropped the reference because the unit
|
|
1885
1906
|
// wasn't in the used passages — leaving no block and no token.
|
|
1886
1907
|
let completionBlocks: Awaited<ReturnType<typeof completeTables>> = [];
|
|
1887
|
-
if (!answer
|
|
1888
|
-
completionBlocks = await completeTables(env.DB, answer
|
|
1908
|
+
if (!answer!.includes(refusalAnswer()) && !c2ns.blocks.some((b: any) => b.type === "table")) {
|
|
1909
|
+
completionBlocks = await completeTables(env.DB, answer!, used);
|
|
1889
1910
|
if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
|
|
1890
1911
|
}
|
|
1891
1912
|
|
|
1892
1913
|
// figure completion (#172) — see ./completion for the rationale
|
|
1893
|
-
completionBlocks.push(...(await completeFigures(env.DB, answer
|
|
1914
|
+
completionBlocks.push(...(await completeFigures(env.DB, answer!, [...c2ns.blocks, ...completionBlocks], used)));
|
|
1894
1915
|
|
|
1895
1916
|
// the blind-answer regeneration (2026-10-03, live on the photo case):
|
|
1896
1917
|
// the multimodal call can answer AROUND an attached image — a 200 whose
|
|
1897
1918
|
// text claims nothing was provided — while the bridge succeeded and the
|
|
1898
1919
|
// register note carries actual rows. One bounded regeneration, decided on
|
|
1899
1920
|
// that exact signature; the retry carries a sterner ground instruction.
|
|
1900
|
-
if (withImage && register?.rows?.length && answer
|
|
1921
|
+
if (withImage && register?.rows?.length && answer!.includes(refusalAnswer())) {
|
|
1901
1922
|
console.log("blind-answer: the register note carried rows but the answer refused — regenerating once");
|
|
1902
1923
|
mark("Re-answering from the register results");
|
|
1903
1924
|
const regen = await generateOnce(env, model, [
|
|
@@ -1905,8 +1926,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1905
1926
|
{ role: "user", content: "The photograph was attached and the register note lists actual matching certificate rows. Present those rows — number, holder, model, status, and every document link — as the answer. Do not claim that nothing was provided." },
|
|
1906
1927
|
messages[messages.length - 1],
|
|
1907
1928
|
], effort);
|
|
1908
|
-
if (regen && !regen
|
|
1909
|
-
answer = canonicalRefusal(regen
|
|
1929
|
+
if (regen && !regen!.includes(refusalAnswer())) {
|
|
1930
|
+
answer = canonicalRefusal(regen!, refusalScope(answerCites(regen!)));
|
|
1910
1931
|
console.log("blind-answer: regeneration grounded");
|
|
1911
1932
|
}
|
|
1912
1933
|
}
|
|
@@ -1919,25 +1940,25 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1919
1940
|
// rung note untouched; refusals and uncited answers have nothing to gate.
|
|
1920
1941
|
let entailment: { support: string; score: number; ungrounded?: string[] } | null = null;
|
|
1921
1942
|
let entailmentNote = "";
|
|
1922
|
-
if (THRESHOLDS.entailmentGate && jsonQuality && used.length && !answer
|
|
1943
|
+
if (THRESHOLDS.entailmentGate && jsonQuality && used.length && !answer!.includes(refusalAnswer())) {
|
|
1923
1944
|
const raced = await Promise.race([
|
|
1924
|
-
scoreFaithfulness(env.AI, roleModel(env, "grader"), answer
|
|
1945
|
+
scoreFaithfulness(env.AI, roleModel(env, "grader"), answer!, (used as Hit[]).map((h: Hit) => h.text)),
|
|
1925
1946
|
new Promise<null>((r) => setTimeout(() => r(null), THRESHOLDS.entailmentBudgetMs)),
|
|
1926
1947
|
]);
|
|
1927
1948
|
if (raced) {
|
|
1928
|
-
const v = entailmentVerdict(raced
|
|
1929
|
-
entailment = { support: v.support, score: Math.round(raced
|
|
1949
|
+
const v = entailmentVerdict(raced!.score, THRESHOLDS.entailmentSupportedFloor, THRESHOLDS.entailmentPartialFloor);
|
|
1950
|
+
entailment = { support: v.support, score: Math.round(raced!.score * 100) / 100, ...(raced!.ungrounded_claims?.length ? { ungrounded: raced!.ungrounded_claims.slice(0, 3) } : {}) };
|
|
1930
1951
|
entailmentNote = v.note;
|
|
1931
|
-
if (v.support !== "supported") console.log("entailment:", v.support, raced
|
|
1952
|
+
if (v.support !== "supported") console.log("entailment:", v.support, raced!.score, raced!.ungrounded_claims?.slice(0, 2));
|
|
1932
1953
|
}
|
|
1933
1954
|
}
|
|
1934
|
-
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality
|
|
1935
|
-
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer
|
|
1955
|
+
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: refusalShaped(answer!, refusalScope(answerCites(answer!))) ? [] : (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 } };
|
|
1956
|
+
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer!, refusalScope(answerCites(answer!))) && finalAnchors.violations.length === 0;
|
|
1936
1957
|
if (cacheable) {
|
|
1937
1958
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
1938
1959
|
ctx.waitUntil(env.CACHE.put(ck, JSON.stringify(out), { expirationTtl: LIMITS.cacheTtlSec }));
|
|
1939
1960
|
}
|
|
1940
|
-
telemetry(env, ctx, tier, "ask", model, true, answer
|
|
1961
|
+
telemetry(env, ctx, tier, "ask", model, true, answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
1941
1962
|
// grounding transparency for integrators (and the eval battery): the
|
|
1942
1963
|
// passages the answer was actually built from — response-only, never
|
|
1943
1964
|
// stored in the answer cache
|
|
@@ -1947,25 +1968,23 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1947
1968
|
text: h.text.slice(0, 1200),
|
|
1948
1969
|
...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
|
|
1949
1970
|
}));
|
|
1950
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer
|
|
1971
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages!), answer!.length, probeTokensUsed);
|
|
1951
1972
|
return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
|
|
1952
|
-
}
|
|
1973
|
+
}
|
|
1953
1974
|
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
...cors,
|
|
1963
|
-
},
|
|
1964
|
-
});
|
|
1975
|
+
// THE DISPATCH: the streamed lane's response went out at the top — the
|
|
1976
|
+
// pipeline has been feeding it live. The json lane awaits the pipeline
|
|
1977
|
+
// and returns its aggregate.
|
|
1978
|
+
if (liveResponse) {
|
|
1979
|
+
void pipeline(live!, q).catch(() => { try { live!.close(); } catch { /* the client went away */ } });
|
|
1980
|
+
return liveResponse;
|
|
1981
|
+
}
|
|
1982
|
+
return (await pipeline(null, q))!;
|
|
1965
1983
|
}
|
|
1966
1984
|
|
|
1967
1985
|
|
|
1968
1986
|
|
|
1987
|
+
|
|
1969
1988
|
/** RAGAS-style metric battery (G13): judge an (question, answer, passages)
|
|
1970
1989
|
* triple — faithfulness, answer relevancy, context precision. Driven by
|
|
1971
1990
|
* tests/eval-suite.mjs; prompts are data; refuses nothing, judges only. */
|