@konneal/engine 0.2.80 → 0.2.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  handleAsk
3
- } from "./chunk-LTGOJVDF.js";
3
+ } from "./chunk-DH3WDTZX.js";
4
4
  import "./chunk-7HWJOFTQ.js";
5
5
  import "./chunk-SPKPH54J.js";
6
6
  import "./chunk-A3QHHUN5.js";
@@ -2877,12 +2877,17 @@ Answer account questions from these records ONLY: name the record when you use i
2877
2877
  const pendingTools = [];
2878
2878
  if (/certificate|certified|certification/i.test(q.query) && env.DB) {
2879
2879
  try {
2880
- const probe = await env.AI.run(model, {
2881
- messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}
2880
+ const imageTurn = typeof withImage === "string";
2881
+ const probeMessages = [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}
2882
+
2883
+ ${imageTurn ? "A photograph of the instrument is attached after this message. Transcribe the maker, model and any printed certificate number you can read, then emit the TOOL line with those tokens as the query \u2014 never invented." : ""}
2882
2884
 
2883
- ${q.query}` }],
2884
- max_tokens: 300,
2885
- reasoning_effort: "low"
2885
+ ${q.query}`.replace("\n\n\n\n", "\n\n") }];
2886
+ if (imageTurn) probeMessages.push({ role: "user", content: [{ type: "image_url", image_url: { url: withImage } }] });
2887
+ const probe = await env.AI.run(model, {
2888
+ messages: probeMessages,
2889
+ max_tokens: 600,
2890
+ reasoning_effort: imageTurn ? "high" : "low"
2886
2891
  });
2887
2892
  const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
2888
2893
  const call = parseToolCall(probeText);
@@ -2901,16 +2906,13 @@ ${q.query}` }],
2901
2906
  const wantsMarkings = /\b(mark(ing|ings)?|nameplate|label|inscription|engrav|sticker|plate)\b|certificat/i.test(q.query);
2902
2907
  const last = messages[messages.length - 1];
2903
2908
  const note = wantsMarkings ? "\n\n(The user attached a photo with this question. First transcribe every inscription you can actually read in the image \u2014 names, model references, accuracy classes, numeric values with their units, certificate or approval numbers \u2014 quoting them verbatim. Then answer the question from the numbered passages, citing the requirement each interpretation rests on. If an inscription is unreadable, say so; never invent a marking.)" : "\n\n(The user attached an image with this question; interpret it directly when answering.)";
2904
- if (Array.isArray(last.content)) {
2909
+ if (typeof last.content === "string") {
2910
+ last.content = last.content + note;
2911
+ } else {
2905
2912
  const textPart = last.content.find((p) => p.type === "text");
2906
2913
  if (textPart) textPart.text += note;
2907
- last.content = [...last.content, { type: "image_url", image_url: { url: withImage } }];
2908
- } else {
2909
- last.content = [
2910
- { type: "text", text: last.content + note },
2911
- { type: "image_url", image_url: { url: withImage } }
2912
- ];
2913
2914
  }
2915
+ messages.push({ role: "user", content: [{ type: "image_url", image_url: { url: withImage } }] });
2914
2916
  console.log("user image attached to generation");
2915
2917
  }
2916
2918
  const queryHash = await sha256Hex(q.query);
@@ -31,7 +31,7 @@ export interface HistoryTurn {
31
31
  export interface BuiltMessages {
32
32
  messages: {
33
33
  role: string;
34
- content: string;
34
+ content: string | any[];
35
35
  }[];
36
36
  usedHits: Hit[];
37
37
  }
@@ -6,7 +6,7 @@ import {
6
6
  modelGroundingBlock,
7
7
  scoreFaithfulness,
8
8
  scoreJudge
9
- } from "../../chunk-LTGOJVDF.js";
9
+ } from "../../chunk-DH3WDTZX.js";
10
10
  import {
11
11
  handleSearch
12
12
  } from "../../chunk-KO6DRI7C.js";
@@ -959,7 +959,7 @@ async function handleMcp(env, ctx, req, tier, key) {
959
959
  // stream:false forces the JSON lane (anon defaults to SSE)
960
960
  body: JSON.stringify({ ...args, stream: false })
961
961
  });
962
- const res = name === "ask" ? await (await import("../../ask-7V4ECBLN.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
962
+ const res = name === "ask" ? await (await import("../../ask-QLY2CODU.js")).handleAsk(env, ctx, inner, tier, key) : await (await import("../../search-LVUQLA3C.js")).handleSearch(env, ctx, inner, tier, key);
963
963
  return res.json().catch(() => ({ error: { message: "tool transport failed", status: res.status } }));
964
964
  });
965
965
  if (out.ok && "accepted" in out) return new Response(null, { status: 202 });
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.80",
30
+ "version": "0.2.82",
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",
@@ -85,7 +85,7 @@ function embedWarm(env: Env, text: string): Promise<number[] | null> {
85
85
  * AI 8005s that scale with payload size), and only the ONE pinned
86
86
  * figure attaches (the type-intent pin already chose the answering
87
87
  * object; a second base64 blob doubles the flake surface for nothing). */
88
- async function attachFigureImages(env: Env, messages: { role: string; content: string }[], usedHits: Hit[], query: string): Promise<void> {
88
+ async function attachFigureImages(env: Env, messages: { role: string; content: any }[], usedHits: Hit[], query: string): Promise<void> {
89
89
  // Attach only when the question WANTS the drawing (names a figure-ish
90
90
  // artifact) or the pinned figure sits in the top prose passage's own
91
91
  // clause (it IS the answering object) — a plain definition question
@@ -1222,12 +1222,20 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1222
1222
  // invocations record into pendingTools; the stream emits them as its
1223
1223
  // first events (the UI shows what the agent consulted).
1224
1224
  const pendingTools: { name: string; query: string; output: string }[] = [];
1225
+ // On an IMAGE turn the probe CARRIES THE IMAGE (2026-10-03, the owner's
1226
+ // architecture): the model that reads the plate is the model that issues
1227
+ // the tool call — one reader, one tool, no parallel extraction racing a
1228
+ // second vision pass that drifts hour to hour. The old probe dropped the
1229
+ // image-bearing message, which is why the private bridge had to exist.
1225
1230
  if (/certificate|certified|certification/i.test(q.query) && env.DB) {
1226
1231
  try {
1232
+ const imageTurn = typeof withImage === "string";
1233
+ const probeMessages: any[] = [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}\n\n${imageTurn ? "A photograph of the instrument is attached after this message. Transcribe the maker, model and any printed certificate number you can read, then emit the TOOL line with those tokens as the query — never invented." : ""}\n\n${q.query}`.replace("\n\n\n\n", "\n\n") }];
1234
+ if (imageTurn) probeMessages.push({ role: "user", content: [{ type: "image_url", image_url: { url: withImage } }] });
1227
1235
  const probe = await (env as any).AI.run(model, {
1228
- messages: [...messages.slice(0, -1), { role: "user", content: `${TOOL_DECLARATION}\n\n${q.query}` }],
1229
- max_tokens: 300,
1230
- reasoning_effort: "low",
1236
+ messages: probeMessages,
1237
+ max_tokens: 600,
1238
+ reasoning_effort: imageTurn ? "high" : "low",
1231
1239
  });
1232
1240
  const probeText = typeof probe?.response === "string" ? probe.response : probe?.choices?.[0]?.message?.content ?? "";
1233
1241
  const call = parseToolCall(probeText);
@@ -1255,16 +1263,20 @@ let convEntities: Array<{ entity: string; kind: string }> = [];
1255
1263
  const note = wantsMarkings
1256
1264
  ? "\n\n(The user attached a photo with this question. First transcribe every inscription you can actually read in the image — names, model references, accuracy classes, numeric values with their units, certificate or approval numbers — quoting them verbatim. Then answer the question from the numbered passages, citing the requirement each interpretation rests on. If an inscription is unreadable, say so; never invent a marking.)"
1257
1265
  : "\n\n(The user attached an image with this question; interpret it directly when answering.)";
1258
- if (Array.isArray(last.content)) {
1266
+
1267
+ // the image rides its OWN SHORT TRAILING MESSAGE — never inside the
1268
+ // passage-bearing user message. Long text + image parts in ONE message
1269
+ // is the documented instability shape (2026-09-09); it was reintroduced
1270
+ // here, and the live "blindness" on photo answers (2026-10-03) was this
1271
+ // violation, not the model: the probe and extraction calls, which keep
1272
+ // the image on its own message, read the same plate every time.
1273
+ if (typeof last.content === "string") {
1274
+ last.content = last.content + note;
1275
+ } else {
1259
1276
  const textPart = last.content.find((p: any) => p.type === "text");
1260
1277
  if (textPart) textPart.text += note;
1261
- last.content = [...last.content, { type: "image_url", image_url: { url: withImage } }] as unknown as string;
1262
- } else {
1263
- last.content = [
1264
- { type: "text", text: last.content + note },
1265
- { type: "image_url", image_url: { url: withImage } },
1266
- ] as unknown as string;
1267
1278
  }
1279
+ messages.push({ role: "user", content: [{ type: "image_url", image_url: { url: withImage } }] });
1268
1280
  console.log("user image attached to generation");
1269
1281
  }
1270
1282
  const queryHash = await sha256Hex(q.query);
@@ -180,7 +180,10 @@ function clipToTokens(s: string, maxTok: number): string {
180
180
  }
181
181
 
182
182
  export interface BuiltMessages {
183
- messages: { role: string; content: string }[];
183
+ // content is string OR an OpenAI-style parts array — the image parts
184
+ // ride their OWN trailing user message (never inside a passage-bearing
185
+ // one), so the type admits both shapes
186
+ messages: { role: string; content: string | any[] }[];
184
187
  usedHits: Hit[]; // passages actually included (citations must match these)
185
188
  }
186
189