@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
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
handleMemories,
|
|
5
5
|
scoreFaithfulness,
|
|
6
6
|
scoreJudge
|
|
7
|
-
} from "../../chunk-
|
|
7
|
+
} from "../../chunk-TXF5VMNM.js";
|
|
8
8
|
import "../../chunk-ECOYO7EX.js";
|
|
9
9
|
import {
|
|
10
10
|
TOOLS_REGISTRY,
|
|
@@ -411,7 +411,11 @@ ${c.text.slice(0, 1500)}` }
|
|
|
411
411
|
context = typeof raw === "string" ? raw.trim().replace(/^["\']|[\"']$/g, "").slice(0, 400) : "";
|
|
412
412
|
if (!context) {
|
|
413
413
|
console.log("enrich raw keys:", Object.keys(res ?? {}), "sample:", JSON.stringify(res).slice(0, 300));
|
|
414
|
-
return { id: c.id, ok: false, error: "empty enrichment" };
|
|
414
|
+
if (contextOnly || abMode) return { id: c.id, ok: false, error: "empty enrichment" };
|
|
415
|
+
const rawText = typeof c.metadata.chunk_text === "string" && c.metadata.chunk_text ? c.metadata.chunk_text : c.text;
|
|
416
|
+
const rawVector = await embed(portModelRunner(env), MODELS.embed, rawText.slice(0, 6e3));
|
|
417
|
+
await env.VECTORIZE.upsert([{ id: c.id, values: rawVector, metadata: { ...c.metadata, chunk_text: rawText } }]);
|
|
418
|
+
return { id: c.id, ok: true, raw: true, cached: false, context: "" };
|
|
415
419
|
}
|
|
416
420
|
if (res?.usage) {
|
|
417
421
|
usage.prompt_tokens += Number(res.usage.prompt_tokens ?? 0);
|
|
@@ -1107,7 +1111,7 @@ async function callToolOnce(env, ctx, _req, tier, key, name, args, t0) {
|
|
|
1107
1111
|
const r = await runTool({ ...env, DB: env.DB }, { name, args }, "mcp");
|
|
1108
1112
|
payload = r ? { tool: r.name, query: r.query, result: r.output } : { error: { message: `tool ${name} returned nothing for the given arguments` } };
|
|
1109
1113
|
} else {
|
|
1110
|
-
const res = name === "ask" ? await (await import("../../ask-
|
|
1114
|
+
const res = name === "ask" ? await (await import("../../ask-MRVXR7CY.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-GAU5SEZI.js")).handleSearch(env, ctx, inner, tier, key);
|
|
1111
1115
|
payload = await res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
|
|
1112
1116
|
}
|
|
1113
1117
|
telemetry(env, ctx, tier, `mcp:${name}`, null, !payload?.error, JSON.stringify(payload).length, await sha256Hex(`${name}:${JSON.stringify(args ?? {})}`), void 0, void 0, { durationMs: Date.now() - t0 });
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# The agent-mode stream-first architecture (the designed next build)
|
|
2
|
+
|
|
3
|
+
## The demand (owner directive, 2026-10-15): SOTA agent bots 2026 —
|
|
4
|
+
users see EVERY call as it runs. The current shape: the ask handler
|
|
5
|
+
computes the ENTIRE pipeline (understanding → retrieval → register →
|
|
6
|
+
nameplate vision → the agent loop) BEFORE the SSE response opens —
|
|
7
|
+
the user waits minutes in silence, then receives the buffered steps +
|
|
8
|
+
the answer tokens.
|
|
9
|
+
|
|
10
|
+
## The target shape
|
|
11
|
+
1. The SSE opens at the top of the streamed branch (right after the
|
|
12
|
+
cache check): the controller is captured; `live.send(ev)` works from
|
|
13
|
+
that moment.
|
|
14
|
+
2. The pipeline runs ONCE, inside the streamed branch, emitting live:
|
|
15
|
+
- mark() sends `{type:"agent", step}` immediately (no agentSteps
|
|
16
|
+
buffer + flush).
|
|
17
|
+
- The vision/question-understanding/certificate-lookup steps each
|
|
18
|
+
send their start (and result) events — the owner's step order.
|
|
19
|
+
- The short-circuits (register count/latest, verdict) send their
|
|
20
|
+
events (citations/tokens/blocks/done) through the same channel.
|
|
21
|
+
- The generation streams tokens as today (the 0.2.148 head-buffer +
|
|
22
|
+
narration restart ride along).
|
|
23
|
+
3. The JSON lane becomes a CLIENT of the same stream: run the stream,
|
|
24
|
+
aggregate {answer, citations, blocks, quota, context} from the
|
|
25
|
+
events, return json. ONE pipeline body, two delivery modes — no
|
|
26
|
+
duplication. (The done event gains the contextOut fields — additive.)
|
|
27
|
+
|
|
28
|
+
## The mechanical plan
|
|
29
|
+
- The block (understanding → the generation, ~610-1905 in ask.ts)
|
|
30
|
+
moves into the streamed branch inside start().
|
|
31
|
+
- The exits convert: the 3 err()s → the graceful token + done; the
|
|
32
|
+
json returns inside the streamed branch → the aggregator return;
|
|
33
|
+
sseResponse(...) → live.send per event + close.
|
|
34
|
+
- The json lane below the branch → the aggregator (~40 lines).
|
|
35
|
+
- The event-shape additions are additive (context in done; the error
|
|
36
|
+
token convention).
|
|
37
|
+
|
|
38
|
+
## The safe path
|
|
39
|
+
1. The engine PR: merge safely (no deploy).
|
|
40
|
+
2. Publish → the ai scratch worktree pins 0.2.15x → `npm run dev:public`
|
|
41
|
+
(the LOCAL worker, remote bindings) → the probes: the streamed
|
|
42
|
+
Chomsky refusal, the photo flow's step order, a JSON MPE ask, the
|
|
43
|
+
long question.
|
|
44
|
+
3. Only after the probes: the ai pin bump PR → auto-deploy → the
|
|
45
|
+
production probes.
|
|
46
|
+
|
|
47
|
+
## WHAT LANDED (0.2.151, probes green)
|
|
48
|
+
|
|
49
|
+
One pipeline body, TWO DELIVERIES — but the delivery is chosen by the
|
|
50
|
+
CHANNEL PARAMETER, not the request's stream preference:
|
|
51
|
+
`pipeline(channel, qq)` where channel is the live SSE writer or null.
|
|
52
|
+
The dispatch: `if (liveResponse) { void pipeline(live, q); return
|
|
53
|
+
liveResponse; } return await pipeline(null, q);`.
|
|
54
|
+
|
|
55
|
+
Laws the build adds to the serving doctrine:
|
|
56
|
+
|
|
57
|
+
1. **THE CHANNEL DECIDES THE LANE.** Inside the pipeline, every lane
|
|
58
|
+
branch keys off `live` (the channel) — never off `wantsStream` (the
|
|
59
|
+
request preference). The two are coupled by the hoist/dispatch, but
|
|
60
|
+
the channel is the authority: a preference-guarded branch is reachable
|
|
61
|
+
with a null channel (the first probe build 500'd every json ask in
|
|
62
|
+
exactly that else).
|
|
63
|
+
2. **THE HOIST PRECEDES EVERY STEP.** The SSE opens before the vision
|
|
64
|
+
step, the understanding, the register bridge — each mark() streams at
|
|
65
|
+
the moment it runs (the owner's step order), never replayed from a
|
|
66
|
+
buffer. The agentSteps buffer survives only for the json lane's
|
|
67
|
+
single response.
|
|
68
|
+
3. **ONE READER, ONE LOOKUP (the photo flow).** The nameplate bridge
|
|
69
|
+
(vision → extract → certificates.search) is the reader on a cert-photo
|
|
70
|
+
turn; the agent loop stands down there (`!nameplate?.manufacturer`).
|
|
71
|
+
The loop's image-bearing probe remains the reader on every OTHER
|
|
72
|
+
image turn. The user watches: Reading the nameplate photograph →
|
|
73
|
+
Nameplate read: … → certificates.search — … → the answer.
|
|
74
|
+
4. **THE CFA BUDGET.** A ~1400-line closure drops TypeScript's flow
|
|
75
|
+
narrowing mid-function (values see `string | null` inside their own
|
|
76
|
+
guards). The 24 carried sites are guard-backed `!` assertions — the
|
|
77
|
+
guards are the runtime proof; a reduced repro for the narrowing loss
|
|
78
|
+
is a follow-up.
|
|
79
|
+
|
|
80
|
+
Probes (local --remote dev against production bindings): JSON MPE ask
|
|
81
|
+
(7-10 citations, table block), streamed Chomsky refusal (pinned
|
|
82
|
+
sentence, no follow_up chips), streamed knowledge (read → citations →
|
|
83
|
+
tokens → quota → done), text cert question (the loop streams), the
|
|
84
|
+
photo flow in the owner's order.
|
package/package.json
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"playwright": "^1.62.1",
|
|
28
28
|
"yaml": "^2.9.1"
|
|
29
29
|
},
|
|
30
|
-
"version": "0.2.
|
|
30
|
+
"version": "0.2.155",
|
|
31
31
|
"description": "The Konneal engine: the publisher-agnostic build pipeline and API plane for standards intelligence (retrieval, answer contract, verdicts, evaluation).",
|
|
32
32
|
"license": "BSD-3-Clause",
|
|
33
33
|
"type": "module",
|
|
@@ -78,7 +78,20 @@ export async function handleEnrich(env: Env, ctx: ExecutionContext, req: Request
|
|
|
78
78
|
context = typeof raw === "string" ? raw.trim().replace(/^["\']|[\"']$/g, "").slice(0, 400) : "";
|
|
79
79
|
if (!context) {
|
|
80
80
|
console.log("enrich raw keys:", Object.keys(res ?? {}), "sample:", JSON.stringify(res).slice(0, 300));
|
|
81
|
-
|
|
81
|
+
// The deterministic-empty class (observed 2026-10-20 on B 18's
|
|
82
|
+
// thin terms sections and two R 134-1 clauses — empty under
|
|
83
|
+
// BOTH the default and the alternate enrichment model): the
|
|
84
|
+
// chunk's own text is real corpus content, so the production
|
|
85
|
+
// flow serves it RAW rather than leaving a hole in the index
|
|
86
|
+
// — embed the original text without the context band, upsert
|
|
87
|
+
// without the ctx marker; a later forced re-enrich overwrites
|
|
88
|
+
// the vector in place. mode:"context" and "ab" keep failing:
|
|
89
|
+
// their builders want the signal, not a side-effecting write.
|
|
90
|
+
if (contextOnly || abMode) return { id: c.id, ok: false, error: "empty enrichment" };
|
|
91
|
+
const rawText = typeof c.metadata.chunk_text === "string" && c.metadata.chunk_text ? c.metadata.chunk_text : c.text;
|
|
92
|
+
const rawVector = await embed(portModelRunner(env), MODELS.embed, rawText.slice(0, 6000));
|
|
93
|
+
await env.VECTORIZE.upsert([{ id: c.id, values: rawVector, metadata: { ...c.metadata, chunk_text: rawText } }]);
|
|
94
|
+
return { id: c.id, ok: true, raw: true, cached: false, context: "" };
|
|
82
95
|
}
|
|
83
96
|
if (res?.usage) {
|
|
84
97
|
usage.prompt_tokens += Number(res.usage.prompt_tokens ?? 0);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// The answer-contract's text guards, dependency-free so plain node can
|
|
2
|
+
// load them for unit tests (ask.ts imports .md prompts that only the
|
|
3
|
+
// bundler resolves — the guards were module-local there and rode e2e
|
|
4
|
+
// alone). Both lanes share them: the JSON lane's contract retry and the
|
|
5
|
+
// streamed lane's 80-char head ruling.
|
|
6
|
+
|
|
7
|
+
// The tool-narration leak (2026-10-14, the R 134-1 probe): the answer
|
|
8
|
+
// model sometimes NARRATES the agent loop's call in prose — 'The
|
|
9
|
+
// db.find tool returned, for the query "…": {…json…}' — instead of
|
|
10
|
+
// answering. The TOOL-line scrub cannot catch prose. The JSON lane
|
|
11
|
+
// regenerates through the contract retry; the streamed lane buffers
|
|
12
|
+
// the head and sends the retry sentinel (the blind-answer guard's
|
|
13
|
+
// client contract) before streaming the fresh answer. One bounded
|
|
14
|
+
// restart per lane.
|
|
15
|
+
export function toolNarration(t: string): boolean {
|
|
16
|
+
return /^(?:the\s+)?[a-z][a-z_.]*\.[a-z_]+ tool returned\b/i.test(t.slice(0, 200)) ||
|
|
17
|
+
/tool returned,? for the query\b/i.test(t.slice(0, 300));
|
|
18
|
+
}
|
|
19
|
+
export const TOOL_NARRATION_NOTICE =
|
|
20
|
+
"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.";
|
|
21
|
+
|
|
22
|
+
// The intent preamble (2026-10-19, the ~200-word technical-file
|
|
23
|
+
// question): the draft is a PLAN, not an answer — "I'll look up the R
|
|
24
|
+
// 76-1 MPE clause and classification details in the corpus." — shipped
|
|
25
|
+
// while ten citations rode the same response. Narrow by construction:
|
|
26
|
+
// it must OPEN with a first-person intent verb AND carry no substance —
|
|
27
|
+
// short, no clause citation, no normative numbers (a real answer's
|
|
28
|
+
// opening names values or clauses).
|
|
29
|
+
export function intentPreamble(t: string): boolean {
|
|
30
|
+
const s = (t ?? "").trim();
|
|
31
|
+
if (s.length > 240) return false;
|
|
32
|
+
if (!/^(?:i'll|i will|let me|let's|i'm going to|i am going to)\b/i.test(s)) return false;
|
|
33
|
+
return !/\[\[u:|\[\^?\d+\]|\b\d+\.\d+\b/.test(s);
|
|
34
|
+
}
|
|
35
|
+
export const INTENT_PREAMBLE_NOTICE =
|
|
36
|
+
"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.";
|