teapot-coding-agent 0.37.68 → 0.37.70

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.
@@ -1850,7 +1850,26 @@ export class Agent {
1850
1850
  durationMs: Date.now() - t0,
1851
1851
  result: capped,
1852
1852
  });
1853
- this.messages.push({ role: "tool", tool_call_id: call.id, content: capped });
1853
+ /**
1854
+ * A tool may hand the model an IMAGE (read_file on a picture).
1855
+ *
1856
+ * It rides on the tool message as `content_parts`, which `sanitize()` already knows how to send
1857
+ * to a vision model. The text stays in `content` so the log, the timeline and a non-vision model
1858
+ * all still see something meaningful.
1859
+ */
1860
+ this.messages.push({
1861
+ role: "tool",
1862
+ tool_call_id: call.id,
1863
+ content: capped,
1864
+ ...(result.image
1865
+ ? {
1866
+ content_parts: [
1867
+ { type: "text", text: capped },
1868
+ { type: "image_url", image_url: { url: result.image.url } },
1869
+ ],
1870
+ }
1871
+ : {}),
1872
+ });
1854
1873
  this.consecutiveToolErrors = result.ok ? 0 : this.consecutiveToolErrors + 1;
1855
1874
  if (this.consecutiveToolErrors >= this.opts.maxConsecutiveToolErrors) {
1856
1875
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
@@ -32,6 +32,13 @@ export async function looksBinary(p) {
32
32
  await fh.close();
33
33
  }
34
34
  }
35
+ /** the `looksBinary()` kinds that are pictures a vision model can actually receive */
36
+ const IMAGE_MIME = {
37
+ "PNG image": "image/png",
38
+ "JPEG image": "image/jpeg",
39
+ "GIF image": "image/gif",
40
+ "WEBP image": "image/webp",
41
+ };
35
42
  function sniff(head) {
36
43
  const hex = (n) => head.subarray(0, n).toString("hex");
37
44
  if (hex(8).startsWith("89504e47"))
@@ -40,6 +47,9 @@ function sniff(head) {
40
47
  return "JPEG image";
41
48
  if (head.subarray(0, 4).toString("latin1") === "GIF8")
42
49
  return "GIF image";
50
+ // RIFF....WEBP — the four bytes after "RIFF" are the size, so "WEBP" sits at offset 8
51
+ if (head.subarray(0, 4).toString("latin1") === "RIFF" && head.subarray(8, 12).toString("latin1") === "WEBP")
52
+ return "WEBP image";
43
53
  if (head.subarray(0, 4).toString("latin1") === "PK\x03\x04")
44
54
  return "zip archive";
45
55
  if (hex(4) === "25504446")
@@ -979,7 +989,8 @@ export function lineIdMode(args) {
979
989
  export const TOOLS = [
980
990
  {
981
991
  name: "read_file",
982
- description: "Read a text file from the workspace. Returns numbered lines (`N| ` prefixes are display-only — never copy them into edit_file). " +
992
+ description: "Read a file from the workspace. Returns numbered lines (`N| ` prefixes are display-only — never copy them into edit_file). " +
993
+ "On an IMAGE (png/jpeg/gif/webp) the picture itself is attached to the result, so you can see it — do not decode bytes. " +
983
994
  "With `line_ids:\"hash\"` it instead returns `LINE:HASH| content` anchors (the hashline format used by other coding harnesses), " +
984
995
  "which edit_file accepts VERBATIM and re-verifies against the file — so prefer it when you intend to edit what you just read. " +
985
996
  "Pass `paths` (max 10) to read SEVERAL files in one call instead of one call per file; " +
@@ -1045,6 +1056,27 @@ export const TOOLS = [
1045
1056
  try {
1046
1057
  const probe = await looksBinary(p);
1047
1058
  if (probe.binary) {
1059
+ /**
1060
+ * An IMAGE is handed to the model as an image; anything else is still refused.
1061
+ *
1062
+ * Refusing images was never a feature — it was the fix for returning raw bytes as mangled
1063
+ * text (`d907af3`). But the operator's point is the asymmetry: a user can attach a picture
1064
+ * and the agent cannot, so the agent is asked to work on files it is not allowed to see.
1065
+ * MEASURED: `read_file` had never been able to return an image at all; the multimodal commit
1066
+ * (`257333a`) was the USER path (composer → `/prompt`), not a tool.
1067
+ */
1068
+ const mime = IMAGE_MIME[probe.kind];
1069
+ if (mime) {
1070
+ const buf = await fs.readFile(p);
1071
+ const name = path.basename(p);
1072
+ return {
1073
+ ok: true,
1074
+ result: `${str(args.path)} is a ${probe.kind} (${probe.bytes} bytes) — the image itself is ` +
1075
+ `attached to this message, so you are looking at it. Describe or reason about it ` +
1076
+ `directly; there is no need to decode bytes.`,
1077
+ image: { url: `data:${mime};base64,${buf.toString("base64")}`, name },
1078
+ };
1079
+ }
1048
1080
  return {
1049
1081
  ok: false,
1050
1082
  result: `${str(args.path)} is a ${probe.kind} (${probe.bytes} bytes), not text — read_file returns ` +
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.37.68",
3
+ "version": "0.37.70",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",