@konneal/engine 0.2.150 → 0.2.153
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{ask-KVGDQTF7.js → ask-UIIE5LD2.js} +1 -1
- package/dist/{chunk-CPPIKASJ.js → chunk-I6HU3EM6.js} +910 -874
- package/dist/mcp-proto.d.ts +3 -0
- package/dist/worker_public/src/index.js +31 -3
- package/docs/AGENT-MODE-STREAM-FIRST.md +84 -0
- package/package.json +1 -1
- package/workers/worker_public/src/ask.ts +229 -163
- package/workers/worker_public/src/mcp-proto.ts +39 -1
|
@@ -65,6 +65,22 @@ function toolNarration(t: string): boolean {
|
|
|
65
65
|
const TOOL_NARRATION_NOTICE =
|
|
66
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
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
|
+
|
|
68
84
|
/** User-uploaded image for multimodal questions: a data URL
|
|
69
85
|
* (data:image/(png|jpeg|webp|gif);base64,…) up to 6 MB of payload. The
|
|
70
86
|
* question text still drives retrieval; the image is CONTEXT for the
|
|
@@ -350,17 +366,6 @@ async function handleAsk(
|
|
|
350
366
|
let generateRetries = 0;
|
|
351
367
|
// how the question was read, for the reader: the interpretation that
|
|
352
368
|
// steered retrieval — a wrong read is visible before it costs trust
|
|
353
|
-
const readAs = () =>
|
|
354
|
-
understanding
|
|
355
|
-
? {
|
|
356
|
-
intent: understanding.intent,
|
|
357
|
-
doc: understanding.docidentifier,
|
|
358
|
-
edition: understanding.edition ?? null,
|
|
359
|
-
term: understanding.term,
|
|
360
|
-
terms: (understanding.defined_terms ?? []).slice(0, 4),
|
|
361
|
-
lang: q?.lang ?? null,
|
|
362
|
-
}
|
|
363
|
-
: undefined;
|
|
364
369
|
const serverTiming = () =>
|
|
365
370
|
Object.entries(stageTiming)
|
|
366
371
|
.map(([k, v]) => `${k};dur=${v}`)
|
|
@@ -466,11 +471,39 @@ async function handleAsk(
|
|
|
466
471
|
// owner's 2026-10-04 direction — the user must see the system-to-model
|
|
467
472
|
// questioning sessions, not stare at a spinner)
|
|
468
473
|
const agentSteps: string[] = [];
|
|
474
|
+
// THE AGENT-MODE CHANNEL (the owner's directive: users see every call
|
|
475
|
+
// as it runs): the streamed lane's SSE opens IMMEDIATELY — the channel
|
|
476
|
+
// exists before the pipeline starts, and every step (and the answer
|
|
477
|
+
// tokens) enqueues into it live. The json lane leaves it null and the
|
|
478
|
+
// steps fall back to the agentSteps buffer for its single response.
|
|
479
|
+
let live: { send: (ev: unknown) => void; close: () => void } | null = null;
|
|
480
|
+
let liveResponse: Response | null = null;
|
|
469
481
|
const mark = (step: string) => {
|
|
470
|
-
|
|
482
|
+
if (live) live.send({ type: "agent", step });
|
|
483
|
+
else agentSteps.push(step);
|
|
471
484
|
console.log(`agent-step: ${step}`);
|
|
472
485
|
};
|
|
473
486
|
|
|
487
|
+
// THE STREAM-FIRST HOIST (the owner's directive: users see every call as
|
|
488
|
+
// it runs): the SSE response returns NOW — before the vision step, the
|
|
489
|
+
// understanding, the register bridge — so every agent step streams to
|
|
490
|
+
// the user at the moment it runs, never replayed from a buffer. The json
|
|
491
|
+
// lane leaves the channel null and the buffer still collects for its
|
|
492
|
+
// single response.
|
|
493
|
+
const wantsStream = body?.stream === true || (tier === "anon" && body?.stream !== false);
|
|
494
|
+
if (wantsStream) {
|
|
495
|
+
const encoder = new TextEncoder();
|
|
496
|
+
let ctrl: ReadableStreamDefaultController<Uint8Array> | null = null;
|
|
497
|
+
const sse = new ReadableStream({
|
|
498
|
+
start(c) { ctrl = c; },
|
|
499
|
+
});
|
|
500
|
+
live = {
|
|
501
|
+
send: (ev: unknown) => { try { ctrl!.enqueue(encoder.encode(`data: ${JSON.stringify(ev)}\n\n`)); } catch { /* the client went away */ } },
|
|
502
|
+
close: () => { try { ctrl!.close(); } catch { /* closed */ } },
|
|
503
|
+
};
|
|
504
|
+
liveResponse = new Response(sse, { headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-accel-buffering": "no", ...corsHeaders(req) } });
|
|
505
|
+
}
|
|
506
|
+
|
|
474
507
|
// Members get federated retrieval (OIML + ISO/IEC) merged into the same
|
|
475
508
|
// pipeline via the service binding; rag-public never touches the
|
|
476
509
|
// internal index itself, and generation/rerank stay in ONE pipeline.
|
|
@@ -585,7 +618,42 @@ async function handleAsk(
|
|
|
585
618
|
const gen = await corpusGen(env.CACHE);
|
|
586
619
|
const withImage = effectiveImage || storedImage;
|
|
587
620
|
const cached = fresh || contextual || declaredCtx || draftAct || apiCallIntent || withImage || ablate ? null : await cacheGet(env, gen, ns, q.query, q.lang, salt);
|
|
588
|
-
|
|
621
|
+
let probeTokensUsed = 0;
|
|
622
|
+
|
|
623
|
+
// THE PIPELINE: one body, two deliveries. Every `return` inside is an
|
|
624
|
+
// exit — for the streamed lane each exit converts to its live events
|
|
625
|
+
// via `finish` (and null, meaning the SSE already carried everything);
|
|
626
|
+
// for the json lane the same payload returns as the response.
|
|
627
|
+
const pipeline = async (
|
|
628
|
+
channel: { send: (ev: unknown) => void; close: () => void } | null,
|
|
629
|
+
qq: { query: string; lang?: string },
|
|
630
|
+
): Promise<Response | null> => {
|
|
631
|
+
// the channel is a parameter: effectively const inside, so every
|
|
632
|
+
// guard on it narrows for the whole body
|
|
633
|
+
const live = channel;
|
|
634
|
+
// the streamed lane's graceful degradation: a serving failure reaches
|
|
635
|
+
// the user as a line, never as a dropped stream
|
|
636
|
+
const q = qq;
|
|
637
|
+
const finishLine = (message: string) => {
|
|
638
|
+
if (live) {
|
|
639
|
+
live!.send({ type: "token", v: message });
|
|
640
|
+
live!.send({ type: "done", model: MODELS.member, query_hash: "", context_applied: NO_CONTEXT });
|
|
641
|
+
live!.close();
|
|
642
|
+
return null;
|
|
643
|
+
}
|
|
644
|
+
return err(503, "serving_busy", message);
|
|
645
|
+
};
|
|
646
|
+
const readAs = () =>
|
|
647
|
+
understanding
|
|
648
|
+
? {
|
|
649
|
+
intent: understanding.intent,
|
|
650
|
+
doc: understanding.docidentifier,
|
|
651
|
+
edition: understanding.edition ?? null,
|
|
652
|
+
term: understanding.term,
|
|
653
|
+
terms: (understanding.defined_terms ?? []).slice(0, 4),
|
|
654
|
+
lang: q?.lang ?? null,
|
|
655
|
+
}
|
|
656
|
+
: undefined;
|
|
589
657
|
|
|
590
658
|
if (cached) {
|
|
591
659
|
telemetry(env, ctx, tier, "ask", null, true, (cached.value.answer ?? "").length, cached.value.query_hash, q.lang, "exact", telemetryMeta());
|
|
@@ -594,13 +662,12 @@ async function handleAsk(
|
|
|
594
662
|
// node named in the question binds WITHOUT a chip and its echo must
|
|
595
663
|
// survive the cache, not silently flatten to "none")
|
|
596
664
|
const cctx = cached.value.context_applied ?? NO_CONTEXT;
|
|
597
|
-
if (
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
], corsHeaders(req));
|
|
665
|
+
if (live) {
|
|
666
|
+
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 } : {}) } : {}) });
|
|
667
|
+
live!.send({ type: "token", v: cached.value.answer ?? "" });
|
|
668
|
+
live!.send({ type: "done", model: cached.value.model ?? MODELS.member, query_hash: cached.value.query_hash, served_from: "cache", context_applied: cctx });
|
|
669
|
+
live!.close();
|
|
670
|
+
return null;
|
|
604
671
|
}
|
|
605
672
|
return json({ ...cached.value, cached: true, quota, context_applied: cctx });
|
|
606
673
|
}
|
|
@@ -797,6 +864,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
797
864
|
// own reading is authoritative over the text lane's tokens ("this",
|
|
798
865
|
// "have" — stopwords that can match an unrelated holder)
|
|
799
866
|
if (nameplate?.manufacturer) {
|
|
867
|
+
mark(`certificates.search — ${nameplate.manufacturer}${nameplate.model ? ` ${nameplate.model}` : ""}`.slice(0, 140));
|
|
800
868
|
const bridged = await searchRegister(env.DB, nameplateRegisterQuery(nameplate, q.query), true);
|
|
801
869
|
if (bridged?.rows?.length) {
|
|
802
870
|
register = bridged;
|
|
@@ -839,20 +907,29 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
839
907
|
registerBlock = block;
|
|
840
908
|
aggregateNoteText = aggregateNote(agg);
|
|
841
909
|
const queryHash = await sha256Hex(q.query);
|
|
842
|
-
telemetry(env, ctx, tier, "ask", MODELS.member, true, answer
|
|
843
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, answer
|
|
844
|
-
|
|
910
|
+
telemetry(env, ctx, tier, "ask", MODELS.member, true, answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
911
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, answer!.length);
|
|
912
|
+
const payload = {
|
|
845
913
|
answer,
|
|
846
914
|
citations: [{ docidentifier: `${P().publisher.name}-CS certificate register (family ${agg.family})`, corpus: "register" }],
|
|
847
915
|
model: MODELS.member,
|
|
848
916
|
query_hash: queryHash,
|
|
849
|
-
follow_ups: [],
|
|
917
|
+
follow_ups: [] as string[],
|
|
850
918
|
blocks: [block],
|
|
851
919
|
context_applied: NO_CONTEXT,
|
|
852
920
|
read: readAs(),
|
|
853
921
|
...(ablate ? { ablate } : {}),
|
|
854
922
|
quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview },
|
|
855
|
-
}
|
|
923
|
+
};
|
|
924
|
+
if (live) {
|
|
925
|
+
live!.send({ type: "citations", citations: payload.citations, quota: payload.quota, context_applied: NO_CONTEXT });
|
|
926
|
+
live!.send({ type: "token", v: answer });
|
|
927
|
+
live!.send({ type: "done", model: MODELS.member, query_hash: queryHash, blocks: [block], follow_ups: [], context_applied: NO_CONTEXT, read: readAs() });
|
|
928
|
+
live!.send({ type: "quota", quota: payload.quota });
|
|
929
|
+
live!.close();
|
|
930
|
+
return null;
|
|
931
|
+
}
|
|
932
|
+
return json(payload);
|
|
856
933
|
}
|
|
857
934
|
}
|
|
858
935
|
}
|
|
@@ -874,37 +951,31 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
874
951
|
...keptHistory.slice(-6),
|
|
875
952
|
{ role: "user", content: q.query },
|
|
876
953
|
];
|
|
877
|
-
if (
|
|
954
|
+
if (live) {
|
|
955
|
+
// the reading arrives first — a conversational turn is still a reading
|
|
956
|
+
if (readAs()) live!.send({ type: "read", read: readAs() });
|
|
957
|
+
live!.send({ type: "citations", citations: [], context_applied: NO_CONTEXT, quota, });
|
|
958
|
+
const stats: { usage?: unknown } = {};
|
|
959
|
+
let full = "";
|
|
878
960
|
const stream = await generateStream(env, model, messages, effort);
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
for await (const tok of sseTokens(stream, stats)) {
|
|
891
|
-
full += tok;
|
|
892
|
-
send({ type: "token", v: tok });
|
|
893
|
-
}
|
|
894
|
-
} catch {
|
|
895
|
-
// stream ended prematurely — deliver what we have
|
|
896
|
-
}
|
|
897
|
-
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
|
|
898
|
-
send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
899
|
-
send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
|
|
900
|
-
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
901
|
-
controller.close();
|
|
902
|
-
},
|
|
903
|
-
});
|
|
904
|
-
return new Response(sse, {
|
|
905
|
-
headers: { "content-type": "text/event-stream", "cache-control": "no-cache", "x-accel-buffering": "no", ...corsHeaders(req) },
|
|
906
|
-
});
|
|
961
|
+
try {
|
|
962
|
+
if (stream) {
|
|
963
|
+
for await (const tok of sseTokens(stream, stats)) {
|
|
964
|
+
full += tok;
|
|
965
|
+
live!.send({ type: "token", v: tok });
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
live!.send({ type: "token", v: "The generation model is unavailable; please retry." });
|
|
969
|
+
}
|
|
970
|
+
} catch {
|
|
971
|
+
// stream ended prematurely — deliver what we have
|
|
907
972
|
}
|
|
973
|
+
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages!), full.length, probeTokensUsed);
|
|
974
|
+
live!.send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
975
|
+
live!.send({ type: "done", model, query_hash: queryHash, context_applied: NO_CONTEXT, read: readAs() });
|
|
976
|
+
telemetry(env, ctx, tier, "ask", model, true, full.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
977
|
+
live!.close();
|
|
978
|
+
return null;
|
|
908
979
|
}
|
|
909
980
|
let answer = await generateOnce(env, model, messages, effort);
|
|
910
981
|
if (answer === null) answer = await generateOnce(env, MODELS.fallback, messages, effort);
|
|
@@ -912,7 +983,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
912
983
|
telemetry(env, ctx, tier, "ask", model, false, 0, queryHash, q.lang, undefined, telemetryMeta());
|
|
913
984
|
return err(502, "generation_failed", "The generation model is unavailable; please retry.");
|
|
914
985
|
}
|
|
915
|
-
telemetry(env, ctx, tier, "ask", model, true, answer
|
|
986
|
+
telemetry(env, ctx, tier, "ask", model, true, answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
916
987
|
return json({ answer, citations: [], model, query_hash: queryHash, follow_ups: [], context_applied: NO_CONTEXT, read: readAs(), quota, });
|
|
917
988
|
}
|
|
918
989
|
|
|
@@ -955,19 +1026,16 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
955
1026
|
console.log("draft act:", draftAct, "→", verdict.status === "draft" ? `draft (${Object.keys(verdict.draft.fields).length} fields)` : `refused (${verdict.reason})`);
|
|
956
1027
|
const citations = verdict.citation ? [{ ...verdict.citation, corpus: P().publisher.id }] : [];
|
|
957
1028
|
const draftPayload = verdict.status === "draft" ? verdict.draft : undefined;
|
|
958
|
-
telemetry(env, ctx, tier, "ask", model, true, verdict.answer
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
],
|
|
967
|
-
corsHeaders(req),
|
|
968
|
-
);
|
|
1029
|
+
telemetry(env, ctx, tier, "ask", model, true, verdict.answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
1030
|
+
if (live) {
|
|
1031
|
+
if (readAs()) live!.send({ type: "read", read: readAs() });
|
|
1032
|
+
live!.send({ type: "citations", citations, context_applied: draftCtxApplied, ...(draftPayload ? { draft: draftPayload } : {}), quota, });
|
|
1033
|
+
live!.send({ type: "token", v: verdict.answer });
|
|
1034
|
+
live!.send({ type: "done", model, query_hash: queryHash, context_applied: draftCtxApplied, read: readAs(), ...(ablate ? { ablate } : {}) });
|
|
1035
|
+
live!.close();
|
|
1036
|
+
return null;
|
|
969
1037
|
}
|
|
970
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer
|
|
1038
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer!.length);
|
|
971
1039
|
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 } });
|
|
972
1040
|
}
|
|
973
1041
|
|
|
@@ -993,19 +1061,16 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
993
1061
|
});
|
|
994
1062
|
console.log("api_call draft:", apiCallIntent, "→", verdict.status === "draft" ? `draft (${verdict.draft.call.method} ${verdict.draft.call.path})` : `refused (${verdict.reason})`);
|
|
995
1063
|
const draftPayload = verdict.status === "draft" ? verdict.draft : undefined;
|
|
996
|
-
telemetry(env, ctx, tier, "ask", model, true, verdict.answer
|
|
997
|
-
if (
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
],
|
|
1005
|
-
corsHeaders(req),
|
|
1006
|
-
);
|
|
1064
|
+
telemetry(env, ctx, tier, "ask", model, true, verdict.answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
1065
|
+
if (live) {
|
|
1066
|
+
if (readAs()) live!.send({ type: "read", read: readAs() });
|
|
1067
|
+
live!.send({ type: "citations", citations: [], context_applied: callCtxApplied, ...(draftPayload ? { draft: draftPayload } : {}), quota, });
|
|
1068
|
+
live!.send({ type: "token", v: verdict.answer });
|
|
1069
|
+
live!.send({ type: "done", model, query_hash: queryHash, context_applied: callCtxApplied, read: readAs(), ...(ablate ? { ablate } : {}) });
|
|
1070
|
+
live!.close();
|
|
1071
|
+
return null;
|
|
1007
1072
|
}
|
|
1008
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer
|
|
1073
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, q.query.length, verdict.answer!.length);
|
|
1009
1074
|
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 } });
|
|
1010
1075
|
}
|
|
1011
1076
|
|
|
@@ -1358,7 +1423,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1358
1423
|
} catch (e) {
|
|
1359
1424
|
console.log("ask: retrieval failed:", String(e).slice(0, 300));
|
|
1360
1425
|
telemetry(env, ctx, tier, "ask", MODELS.embed, false, 0, await sha256Hex(q.query), q.lang, undefined, telemetryMeta());
|
|
1361
|
-
return
|
|
1426
|
+
return finishLine("Search is briefly busy — please retry in a moment.");
|
|
1362
1427
|
}
|
|
1363
1428
|
const { hits } = retrieved;
|
|
1364
1429
|
if (hits.length === 0 && !liveRecords?.length && (!boundModel || boundModel.gated)) {
|
|
@@ -1369,7 +1434,7 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1369
1434
|
const answer =
|
|
1370
1435
|
licenseBoundaryRefusal(modelDocHint?.doc_number ?? understanding?.doc_number ?? null, standardKeys) ?? refusalAnswer();
|
|
1371
1436
|
const out = { answer, citations: [], model, query_hash: await sha256Hex(q.query), context_applied: ctxApplied, ...(ablate ? { ablate } : {}), route: { route: routed.route, features: routed.features } };
|
|
1372
|
-
telemetry(env, ctx, tier, "ask", model, true, answer
|
|
1437
|
+
telemetry(env, ctx, tier, "ask", model, true, answer!.length, out.query_hash, q.lang, undefined, telemetryMeta());
|
|
1373
1438
|
return json({ ...out, quota, });
|
|
1374
1439
|
}
|
|
1375
1440
|
|
|
@@ -1445,13 +1510,13 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1445
1510
|
const pendingTools: { name: string; query: string; output: string }[] = [];
|
|
1446
1511
|
// the agent's own consumption (2026-10-11): every tool probe is a real
|
|
1447
1512
|
// model call — its usage joins the charge so credits price the true cost
|
|
1448
|
-
|
|
1513
|
+
|
|
1449
1514
|
// On an IMAGE turn the probe CARRIES THE IMAGE (2026-10-03, the owner's
|
|
1450
1515
|
// architecture): the model that reads the plate is the model that issues
|
|
1451
1516
|
// the tool call — one reader, one tool, no parallel extraction racing a
|
|
1452
1517
|
// second vision pass that drifts hour to hour. The old probe dropped the
|
|
1453
1518
|
// image-bearing message, which is why the private bridge had to exist.
|
|
1454
|
-
if (multiTurn && env.DB) {
|
|
1519
|
+
if (multiTurn && env.DB && !nameplate?.manufacturer) {
|
|
1455
1520
|
try {
|
|
1456
1521
|
const imageTurn = typeof withImage === "string";
|
|
1457
1522
|
if (imageTurn) mark("Reading the photograph to identify the instrument");
|
|
@@ -1536,13 +1601,10 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1536
1601
|
// provenance — ahead of the prose passages.
|
|
1537
1602
|
const cites = boundModel ? [modelCitation(boundModel), ...citations(usedHits)] : citations(usedHits);
|
|
1538
1603
|
|
|
1539
|
-
if (
|
|
1604
|
+
if (live) {
|
|
1540
1605
|
const stream = await generateStream(env, model, messages, routeEffort(routed.route));
|
|
1541
1606
|
if (stream) {
|
|
1542
|
-
const
|
|
1543
|
-
const sse = new ReadableStream({
|
|
1544
|
-
async start(controller) {
|
|
1545
|
-
const send = (obj: unknown) => controller.enqueue(encoder.encode(`data: ${JSON.stringify(obj)}\n\n`));
|
|
1607
|
+
const send = (obj: unknown) => live!.send(obj);
|
|
1546
1608
|
// the agent trace arrives first: every questioning round the
|
|
1547
1609
|
// assistant ran — the photograph read, the register search, any
|
|
1548
1610
|
// re-answer — so the user watched what happened, not a spinner
|
|
@@ -1570,12 +1632,12 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1570
1632
|
if (headSettled) { send({ type: "token", v: tok }); continue; }
|
|
1571
1633
|
if (full.length < 80) continue;
|
|
1572
1634
|
headSettled = true;
|
|
1573
|
-
if (toolNarration(full)) {
|
|
1574
|
-
console.log("tool-narration(stream): regenerating once");
|
|
1635
|
+
if (toolNarration(full) || intentPreamble(full)) {
|
|
1636
|
+
console.log(toolNarration(full) ? "tool-narration(stream): regenerating once" : "intent-preamble(stream): regenerating once");
|
|
1575
1637
|
send({ type: "retry" });
|
|
1576
1638
|
const regen = await generateStream(env, model, [
|
|
1577
1639
|
...messages,
|
|
1578
|
-
{ role: "system", content: TOOL_NARRATION_NOTICE },
|
|
1640
|
+
{ role: "system", content: toolNarration(full) ? TOOL_NARRATION_NOTICE : INTENT_PREAMBLE_NOTICE },
|
|
1579
1641
|
], routeEffort(routed.route));
|
|
1580
1642
|
if (regen) {
|
|
1581
1643
|
full = "";
|
|
@@ -1594,12 +1656,12 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1594
1656
|
}
|
|
1595
1657
|
if (!headSettled) {
|
|
1596
1658
|
headSettled = true;
|
|
1597
|
-
if (!toolNarration(full)) send({ type: "token", v: full });
|
|
1659
|
+
if (!toolNarration(full) && !intentPreamble(full)) send({ type: "token", v: full });
|
|
1598
1660
|
}
|
|
1599
1661
|
} catch {
|
|
1600
1662
|
// stream ended prematurely — deliver what we have
|
|
1601
1663
|
}
|
|
1602
|
-
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages), full.length, probeTokensUsed);
|
|
1664
|
+
const tq = await chargeAnswerTokens(env, bucketId, stats, messageChars(messages!), full.length, probeTokensUsed);
|
|
1603
1665
|
send({ type: "quota", quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } });
|
|
1604
1666
|
// the blind-answer guard ON THE STREAM PATH (the JSON path has
|
|
1605
1667
|
// had it since 0.2.80): an image rode the turn, the register
|
|
@@ -1655,10 +1717,10 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1655
1717
|
new Promise<null>((r) => setTimeout(() => r(null), THRESHOLDS.entailmentBudgetMs)),
|
|
1656
1718
|
]);
|
|
1657
1719
|
if (raced) {
|
|
1658
|
-
const v = entailmentVerdict(raced
|
|
1659
|
-
streamedEntailment = { support: v.support, score: Math.round(raced
|
|
1720
|
+
const v = entailmentVerdict(raced!.score, THRESHOLDS.entailmentSupportedFloor, THRESHOLDS.entailmentPartialFloor);
|
|
1721
|
+
streamedEntailment = { support: v.support, score: Math.round(raced!.score * 100) / 100, ...(raced!.ungrounded_claims?.length ? { ungrounded: raced!.ungrounded_claims.slice(0, 3) } : {}) };
|
|
1660
1722
|
streamedNote = v.note;
|
|
1661
|
-
if (v.support !== "supported") console.log("entailment(stream):", v.support, raced
|
|
1723
|
+
if (v.support !== "supported") console.log("entailment(stream):", v.support, raced!.score);
|
|
1662
1724
|
send({ type: "confidence", source_quality: sourceQuality, confidence_note: streamedNote || qualityNote(sourceQuality), ...(streamedEntailment ? { entailment: streamedEntailment } : {}) });
|
|
1663
1725
|
}
|
|
1664
1726
|
}
|
|
@@ -1675,19 +1737,17 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1675
1737
|
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 }),
|
|
1676
1738
|
);
|
|
1677
1739
|
}
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
});
|
|
1740
|
+
live!.close();
|
|
1741
|
+
} else {
|
|
1742
|
+
// the streamed lane's generation is unavailable — the line goes
|
|
1743
|
+
// out on the already-open SSE; the json lane never enters this block
|
|
1744
|
+
live!.send({ type: "token", v: "The generation model is unavailable; please retry." });
|
|
1745
|
+
live!.send({ type: "done", model: MODELS.member, query_hash: "", context_applied: NO_CONTEXT });
|
|
1746
|
+
live!.close();
|
|
1747
|
+
return null;
|
|
1748
|
+
}
|
|
1749
|
+
return null;
|
|
1689
1750
|
}
|
|
1690
|
-
}
|
|
1691
1751
|
|
|
1692
1752
|
const tGen = Date.now();
|
|
1693
1753
|
// ── speculative draft-verify (TODO.sota/02 row 2): when the ablation
|
|
@@ -1706,9 +1766,9 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1706
1766
|
return null;
|
|
1707
1767
|
});
|
|
1708
1768
|
if (spec) {
|
|
1709
|
-
answer = spec
|
|
1769
|
+
answer = spec!.answer;
|
|
1710
1770
|
stageTiming.speculative = Date.now() - tGen;
|
|
1711
|
-
console.log("speculative:", spec
|
|
1771
|
+
console.log("speculative:", spec!.nDrafts, "drafts,", spec!.chosen, "viable — verified");
|
|
1712
1772
|
}
|
|
1713
1773
|
}
|
|
1714
1774
|
if (!answer) answer = await generateOnce(env, model, messages, routeEffort(routed.route));
|
|
@@ -1733,19 +1793,25 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1733
1793
|
);
|
|
1734
1794
|
answer = await generateOnce(env, MODELS.fallback, flat, routeEffort(routed.route));
|
|
1735
1795
|
}
|
|
1796
|
+
if (answer === null) {
|
|
1797
|
+
// both generations failed — the json lane returns the error shape; the
|
|
1798
|
+
// streamed lane's equivalent lives inside the streamed block above
|
|
1799
|
+
telemetry(env, ctx, tier, "ask", model, false, 0, queryHash, q.lang, undefined, telemetryMeta());
|
|
1800
|
+
return err(502, "generation_failed", "The generation model is unavailable; please retry.");
|
|
1801
|
+
}
|
|
1736
1802
|
// the protocol scrub: a TOOL line is the agent loop's wire protocol,
|
|
1737
1803
|
// never user-facing prose — the answer model occasionally echoes the
|
|
1738
1804
|
// line it just composed (observed live: 'TOOL db.find …' opened a
|
|
1739
1805
|
// served answer). Stripped here and on the streamed path.
|
|
1740
1806
|
const scrubToolLines = (t: string) => t.replace(/^TOOL [a-z_.]+.*$\n?/gm, "").replace(/^\n+/, "");
|
|
1741
|
-
if (answer) answer = scrubToolLines(answer);
|
|
1742
|
-
if (answer) answer = canonicalRefusal(answer
|
|
1743
|
-
if (answer) answer = scrubToolLines(answer);
|
|
1807
|
+
if (answer) answer = scrubToolLines(answer!);
|
|
1808
|
+
if (answer) answer = canonicalRefusal(answer!, refusalScope(answerCites(answer!)));
|
|
1809
|
+
if (answer) answer = scrubToolLines(answer!);
|
|
1744
1810
|
// the misplaced-pin wall: a CITED answer is not a refusal — excise
|
|
1745
1811
|
// the pinned sentence if the model dropped it in as a disclaimer
|
|
1746
1812
|
// (measured on chip-scoped asks: correct cited answers carrying the
|
|
1747
1813
|
// pin mid-text, read by users and graders as total refusals)
|
|
1748
|
-
if (answer) answer = exciseMisplacedPin(answer
|
|
1814
|
+
if (answer) answer = exciseMisplacedPin(answer!, answerCites(answer!));
|
|
1749
1815
|
// the tool-narration leak (2026-10-14, the R 134-1 probe): the answer
|
|
1750
1816
|
// model sometimes NARRATES the agent loop's call in prose — 'The
|
|
1751
1817
|
// db.find tool returned, for the query "…": {…json…}' — instead of
|
|
@@ -1759,42 +1825,44 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1759
1825
|
// the passages or a typed table was retyped as markdown; the retry
|
|
1760
1826
|
// wins only if it verifies better.
|
|
1761
1827
|
let used = usedHits;
|
|
1762
|
-
if (answer && !answer
|
|
1763
|
-
const anchors = checkQuoteAnchors(answer
|
|
1828
|
+
if (answer && !answer!.includes(refusalAnswer())) {
|
|
1829
|
+
const anchors = checkQuoteAnchors(answer!, used.map((h: Hit) => h.text));
|
|
1764
1830
|
const hasTableUnit = used.some((h: Hit) => h.metadata.unit_id && h.metadata.block === "table");
|
|
1765
|
-
const retyped = tableRetyped(answer
|
|
1831
|
+
const retyped = tableRetyped(answer!, hasTableUnit);
|
|
1766
1832
|
// presenting a served table's DATA without its unit reference is the
|
|
1767
1833
|
// same contract violation as retyping it — the HARD RULE wants the
|
|
1768
1834
|
// token wherever the table's values carry the answer
|
|
1769
1835
|
const unreferenced = (() => {
|
|
1770
|
-
if (!hasTableUnit || answer
|
|
1836
|
+
if (!hasTableUnit || answer!.includes("[[u:")) return false;
|
|
1771
1837
|
const norm = (s: string) => (s.match(/\d[\d ,.]{1,8}\d/g) ?? []).map((x) => x.replace(/[ ,.]/g, ""));
|
|
1772
|
-
const nums = norm(answer);
|
|
1838
|
+
const nums = norm(answer!);
|
|
1773
1839
|
if (nums.length < 2) return false;
|
|
1774
1840
|
const tableNums = new Set(
|
|
1775
1841
|
norm(used.filter((h: Hit) => h.metadata.unit_id && h.metadata.block === "table").map((h: Hit) => h.text).join(" ")),
|
|
1776
1842
|
);
|
|
1777
1843
|
return nums.filter((n) => tableNums.has(n)).length >= 2;
|
|
1778
1844
|
})();
|
|
1779
|
-
if (anchors.violations.length > 0 || retyped || unreferenced || toolNarration(answer)) {
|
|
1780
|
-
console.log("contract check:", anchors.violations.length, "anchor violations; tableRetyped:", retyped, "; tableDataUnreferenced:", unreferenced, "; toolNarration:", toolNarration(answer), "— regenerating");
|
|
1845
|
+
if (anchors.violations.length > 0 || retyped || unreferenced || toolNarration(answer!) || intentPreamble(answer!)) {
|
|
1846
|
+
console.log("contract check:", anchors.violations.length, "anchor violations; tableRetyped:", retyped, "; tableDataUnreferenced:", unreferenced, "; toolNarration:", toolNarration(answer!), "; intentPreamble:", intentPreamble(answer!), "— regenerating");
|
|
1781
1847
|
// name the EXACT unit the token must reference — a generic note
|
|
1782
1848
|
// leaves the model guessing which id to write
|
|
1783
1849
|
const tableUnitId = unreferenced
|
|
1784
1850
|
? used.find((h: Hit) => h.metadata.unit_id && h.metadata.block === "table")?.metadata.unit_id
|
|
1785
1851
|
: undefined;
|
|
1786
|
-
const note = toolNarration(answer)
|
|
1852
|
+
const note = toolNarration(answer!)
|
|
1787
1853
|
? TOOL_NARRATION_NOTICE
|
|
1854
|
+
: intentPreamble(answer)
|
|
1855
|
+
? INTENT_PREAMBLE_NOTICE
|
|
1788
1856
|
: retyped || unreferenced
|
|
1789
1857
|
? `Correction notice: your draft reproduced a table as markdown or presented a served table's data without its reference. Rewrite the answer: describe the table in prose, cite the clause, and write the reference token [[u:${tableUnitId ?? "<unit id>"}]] exactly where the table belongs. Do not render any table as markdown.`
|
|
1790
1858
|
: ANCHOR_CORRECTION_NOTE;
|
|
1791
1859
|
generateRetries += 1;
|
|
1792
1860
|
const corrected = await generateOnce(env, model, [...messages, { role: "system", content: note }], routeEffort(routed.route));
|
|
1793
1861
|
if (corrected) {
|
|
1794
|
-
const correctedAnswer = canonicalRefusal(corrected
|
|
1862
|
+
const correctedAnswer = canonicalRefusal(corrected!, refusalScope(answerCites(corrected!)));
|
|
1795
1863
|
const retryAnchors = checkQuoteAnchors(correctedAnswer, used.map((h: Hit) => h.text));
|
|
1796
1864
|
const retryRetyped = tableRetyped(correctedAnswer, hasTableUnit);
|
|
1797
|
-
if (retryAnchors.violations.length < anchors.violations.length || (!retryRetyped && retyped) || (unreferenced && correctedAnswer.includes("[[u:")) || !toolNarration(correctedAnswer)) {
|
|
1865
|
+
if (retryAnchors.violations.length < anchors.violations.length || (!retryRetyped && retyped) || (unreferenced && correctedAnswer.includes("[[u:")) || !toolNarration(correctedAnswer) || !intentPreamble(correctedAnswer)) {
|
|
1798
1866
|
answer = correctedAnswer;
|
|
1799
1867
|
}
|
|
1800
1868
|
}
|
|
@@ -1816,27 +1884,27 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1816
1884
|
// The model critiques its own answer; if claims are ungrounded, retry
|
|
1817
1885
|
// retrieval with the missing-info hint (max one retry).
|
|
1818
1886
|
// Ref: selfrag.github.io; arXiv 2606.05658 bounded reflection
|
|
1819
|
-
if (answer && !answer
|
|
1820
|
-
const reflection = await reflect(env.AI, MODELS.grader, q.query, answer
|
|
1821
|
-
console.log("reflection:", reflection ? (reflection
|
|
1822
|
-
if (reflection && !reflection
|
|
1887
|
+
if (answer && !answer!.includes(refusalAnswer())) {
|
|
1888
|
+
const reflection = await reflect(env.AI, MODELS.grader, q.query, answer!, hits.map((h: Hit) => h.text));
|
|
1889
|
+
console.log("reflection:", reflection ? (reflection!.grounded ? "grounded" : "ungrounded") : "null");
|
|
1890
|
+
if (reflection && !reflection!.grounded && reflection!.missing_info) {
|
|
1823
1891
|
// re-retrieve targeting what was missing — the declared context's
|
|
1824
1892
|
// hard seal binds the retry exactly as the first pass
|
|
1825
1893
|
// (TODO.ai-platform/02)
|
|
1826
1894
|
const retryRetrieve = await retrieve(env, q.query, {
|
|
1827
1895
|
prev,
|
|
1828
|
-
understanding: { ...understanding, standalone_query: `${understanding?.standalone_query || q.query} ${reflection
|
|
1896
|
+
understanding: { ...understanding, standalone_query: `${understanding?.standalone_query || q.query} ${reflection!.missing_info}` } as any,
|
|
1829
1897
|
sealScope: declaredScoped ? docScope : null,
|
|
1830
1898
|
datasetScope: narrowed ? corpora : null,
|
|
1831
1899
|
standardKeys,
|
|
1832
|
-
...(routeSubset ? { stageSubset: routeSubset } : {}),
|
|
1900
|
+
...(routeSubset ? { stageSubset: routeSubset ?? undefined } : {}),
|
|
1833
1901
|
});
|
|
1834
1902
|
if (retryRetrieve.hits.length > 0) {
|
|
1835
1903
|
const { messages: retryMessages, usedHits: retryUsed } = buildMessages(q.query, retryRetrieve.hits, q.lang, keptHistory, undefined, summary, budget);
|
|
1836
1904
|
const retryAnswer = await generateOnce(env, model, retryMessages, routeEffort(routed.route));
|
|
1837
1905
|
// the answer now comes from the retry passages — citations must follow
|
|
1838
1906
|
if (retryAnswer) {
|
|
1839
|
-
answer = canonicalRefusal(retryAnswer
|
|
1907
|
+
answer = canonicalRefusal(retryAnswer!, refusalScope(answerCites(retryAnswer!)));
|
|
1840
1908
|
used = retryUsed;
|
|
1841
1909
|
}
|
|
1842
1910
|
}
|
|
@@ -1847,14 +1915,14 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1847
1915
|
telemetry(env, ctx, tier, "ask", model, false, 0, queryHash, q.lang, undefined, telemetryMeta());
|
|
1848
1916
|
return err(502, "generation_failed", "The generation model is unavailable; please retry.");
|
|
1849
1917
|
}
|
|
1850
|
-
const finalCites = boundModel ? [modelCitation(boundModel), ...citations(used)] : citations(used);
|
|
1851
|
-
const c2ns = answer
|
|
1918
|
+
const finalCites = boundModel ? [modelCitation(boundModel!), ...citations(used)] : citations(used);
|
|
1919
|
+
const c2ns = answer!.includes(refusalAnswer())
|
|
1852
1920
|
? { text: answer, blocks: [] as Awaited<ReturnType<typeof contractV2>>["blocks"], dropped: [] as string[] }
|
|
1853
|
-
: await contractV2(env.DB, answer
|
|
1921
|
+
: await contractV2(env.DB, answer!, used);
|
|
1854
1922
|
answer = c2ns.text;
|
|
1855
|
-
const finalAnchors = answer
|
|
1923
|
+
const finalAnchors = answer!.includes(refusalAnswer())
|
|
1856
1924
|
? { total: 0, violations: [] as string[] }
|
|
1857
|
-
: checkQuoteAnchors(answer
|
|
1925
|
+
: checkQuoteAnchors(answer!, used.map((h: Hit) => h.text));
|
|
1858
1926
|
if (finalAnchors.violations.length > 0) {
|
|
1859
1927
|
console.log("anchors:", finalAnchors.violations.length, "of", finalAnchors.total, "unverified — not caching");
|
|
1860
1928
|
}
|
|
@@ -1866,20 +1934,20 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1866
1934
|
// fallback) but contractV2 then dropped the reference because the unit
|
|
1867
1935
|
// wasn't in the used passages — leaving no block and no token.
|
|
1868
1936
|
let completionBlocks: Awaited<ReturnType<typeof completeTables>> = [];
|
|
1869
|
-
if (!answer
|
|
1870
|
-
completionBlocks = await completeTables(env.DB, answer
|
|
1937
|
+
if (!answer!.includes(refusalAnswer()) && !c2ns.blocks.some((b: any) => b.type === "table")) {
|
|
1938
|
+
completionBlocks = await completeTables(env.DB, answer!, used);
|
|
1871
1939
|
if (completionBlocks.length) console.log("contract completion:", completionBlocks.length, "table block(s) attached server-side");
|
|
1872
1940
|
}
|
|
1873
1941
|
|
|
1874
1942
|
// figure completion (#172) — see ./completion for the rationale
|
|
1875
|
-
completionBlocks.push(...(await completeFigures(env.DB, answer
|
|
1943
|
+
completionBlocks.push(...(await completeFigures(env.DB, answer!, [...c2ns.blocks, ...completionBlocks], used)));
|
|
1876
1944
|
|
|
1877
1945
|
// the blind-answer regeneration (2026-10-03, live on the photo case):
|
|
1878
1946
|
// the multimodal call can answer AROUND an attached image — a 200 whose
|
|
1879
1947
|
// text claims nothing was provided — while the bridge succeeded and the
|
|
1880
1948
|
// register note carries actual rows. One bounded regeneration, decided on
|
|
1881
1949
|
// that exact signature; the retry carries a sterner ground instruction.
|
|
1882
|
-
if (withImage && register?.rows?.length && answer
|
|
1950
|
+
if (withImage && register?.rows?.length && answer!.includes(refusalAnswer())) {
|
|
1883
1951
|
console.log("blind-answer: the register note carried rows but the answer refused — regenerating once");
|
|
1884
1952
|
mark("Re-answering from the register results");
|
|
1885
1953
|
const regen = await generateOnce(env, model, [
|
|
@@ -1887,8 +1955,8 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1887
1955
|
{ 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." },
|
|
1888
1956
|
messages[messages.length - 1],
|
|
1889
1957
|
], effort);
|
|
1890
|
-
if (regen && !regen
|
|
1891
|
-
answer = canonicalRefusal(regen
|
|
1958
|
+
if (regen && !regen!.includes(refusalAnswer())) {
|
|
1959
|
+
answer = canonicalRefusal(regen!, refusalScope(answerCites(regen!)));
|
|
1892
1960
|
console.log("blind-answer: regeneration grounded");
|
|
1893
1961
|
}
|
|
1894
1962
|
}
|
|
@@ -1901,25 +1969,25 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1901
1969
|
// rung note untouched; refusals and uncited answers have nothing to gate.
|
|
1902
1970
|
let entailment: { support: string; score: number; ungrounded?: string[] } | null = null;
|
|
1903
1971
|
let entailmentNote = "";
|
|
1904
|
-
if (THRESHOLDS.entailmentGate && jsonQuality && used.length && !answer
|
|
1972
|
+
if (THRESHOLDS.entailmentGate && jsonQuality && used.length && !answer!.includes(refusalAnswer())) {
|
|
1905
1973
|
const raced = await Promise.race([
|
|
1906
|
-
scoreFaithfulness(env.AI, roleModel(env, "grader"), answer
|
|
1974
|
+
scoreFaithfulness(env.AI, roleModel(env, "grader"), answer!, (used as Hit[]).map((h: Hit) => h.text)),
|
|
1907
1975
|
new Promise<null>((r) => setTimeout(() => r(null), THRESHOLDS.entailmentBudgetMs)),
|
|
1908
1976
|
]);
|
|
1909
1977
|
if (raced) {
|
|
1910
|
-
const v = entailmentVerdict(raced
|
|
1911
|
-
entailment = { support: v.support, score: Math.round(raced
|
|
1978
|
+
const v = entailmentVerdict(raced!.score, THRESHOLDS.entailmentSupportedFloor, THRESHOLDS.entailmentPartialFloor);
|
|
1979
|
+
entailment = { support: v.support, score: Math.round(raced!.score * 100) / 100, ...(raced!.ungrounded_claims?.length ? { ungrounded: raced!.ungrounded_claims.slice(0, 3) } : {}) };
|
|
1912
1980
|
entailmentNote = v.note;
|
|
1913
|
-
if (v.support !== "supported") console.log("entailment:", v.support, raced
|
|
1981
|
+
if (v.support !== "supported") console.log("entailment:", v.support, raced!.score, raced!.ungrounded_claims?.slice(0, 2));
|
|
1914
1982
|
}
|
|
1915
1983
|
}
|
|
1916
|
-
const out = { answer, citations: finalCites, ...(jsonQuality ? { source_quality: jsonQuality
|
|
1917
|
-
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer
|
|
1984
|
+
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 } };
|
|
1985
|
+
const cacheable = !contextual && !declaredCtx && !ablate && cacheableAnswer(answer!, refusalScope(answerCites(answer!))) && finalAnchors.violations.length === 0;
|
|
1918
1986
|
if (cacheable) {
|
|
1919
1987
|
const ck = exactCacheKey(env.INDEX_VERSION, gen, ns, await sha256Hex(cacheKeyMaterial(q.query, q.lang, salt)));
|
|
1920
1988
|
ctx.waitUntil(env.CACHE.put(ck, JSON.stringify(out), { expirationTtl: LIMITS.cacheTtlSec }));
|
|
1921
1989
|
}
|
|
1922
|
-
telemetry(env, ctx, tier, "ask", model, true, answer
|
|
1990
|
+
telemetry(env, ctx, tier, "ask", model, true, answer!.length, queryHash, q.lang, undefined, telemetryMeta());
|
|
1923
1991
|
// grounding transparency for integrators (and the eval battery): the
|
|
1924
1992
|
// passages the answer was actually built from — response-only, never
|
|
1925
1993
|
// stored in the answer cache
|
|
@@ -1929,25 +1997,23 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
|
|
|
1929
1997
|
text: h.text.slice(0, 1200),
|
|
1930
1998
|
...((h.metadata as any).table_selection ? { sel: (h.metadata as any).table_selection } : {}),
|
|
1931
1999
|
}));
|
|
1932
|
-
const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages), answer
|
|
2000
|
+
const tq = await chargeAnswerTokens(env, bucketId, undefined, messageChars(messages!), answer!.length, probeTokensUsed);
|
|
1933
2001
|
return json({ ...out, context: contextOut, read: readAs(), quota: { ...quota, ...tq, credits: await creditBudget(env, bucketId), credit_estimate: creditPreview } }, 200, { ...corsHeaders(req), "server-timing": serverTiming() });
|
|
1934
|
-
}
|
|
2002
|
+
}
|
|
1935
2003
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
...cors,
|
|
1945
|
-
},
|
|
1946
|
-
});
|
|
2004
|
+
// THE DISPATCH: the streamed lane's response went out at the top — the
|
|
2005
|
+
// pipeline has been feeding it live. The json lane awaits the pipeline
|
|
2006
|
+
// and returns its aggregate.
|
|
2007
|
+
if (liveResponse) {
|
|
2008
|
+
void pipeline(live!, q).catch(() => { try { live!.close(); } catch { /* the client went away */ } });
|
|
2009
|
+
return liveResponse;
|
|
2010
|
+
}
|
|
2011
|
+
return (await pipeline(null, q))!;
|
|
1947
2012
|
}
|
|
1948
2013
|
|
|
1949
2014
|
|
|
1950
2015
|
|
|
2016
|
+
|
|
1951
2017
|
/** RAGAS-style metric battery (G13): judge an (question, answer, passages)
|
|
1952
2018
|
* triple — faithfulness, answer relevancy, context precision. Driven by
|
|
1953
2019
|
* tests/eval-suite.mjs; prompts are data; refuses nothing, judges only. */
|