teapot-coding-agent 0.19.9 → 0.20.1

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/README.md CHANGED
@@ -238,7 +238,7 @@ GET /api/agents/:id/file?path= fetch text file
238
238
  PUT /api/agents/:id/file?path= save { content, baseContent? } (409 on conflict)
239
239
  GET /api/agents/:id/raw?path= raw media bytes
240
240
  GET /api/agents/:id/tree file tree
241
- POST /api/agents/:id/prompt send { text, start? } → { promptId }
241
+ POST /api/agents/:id/prompt send { text, start?, images?: [{url}] } → { promptId }
242
242
  POST /api/agents/:id/prompt/cancel withdraw { promptId } (409 once delivered)
243
243
  POST /api/agents/:id/goal { text?, status?, verify? }
244
244
  POST /api/agents/:id/start|stop|fork|load
@@ -744,7 +744,7 @@ export class Agent {
744
744
  * the running loop. The very first prompt on a fresh boot also triggers the
745
745
  * lazy session restore (before the mailbox is filled, so no duplicates).
746
746
  */
747
- enqueuePrompt(text, source = "user") {
747
+ enqueuePrompt(text, source = "user", images) {
748
748
  // a prompt during a tool park (wait_children) takes over instantly:
749
749
  // unpark fixes the DISPLAY, aborting the park signal actually ends the
750
750
  // wait — the run chain resumes with this prompt drained at the boundary
@@ -770,10 +770,11 @@ export class Agent {
770
770
  const promptId = source === "user" ? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}` : "";
771
771
  void this.ensureReady()
772
772
  .then(() => {
773
- this.pendingPrompts.push({ source, text, ...(promptId ? { id: promptId } : {}) });
773
+ this.pendingPrompts.push({ source, text, images, ...(promptId ? { id: promptId } : {}) });
774
774
  return this.log.append("prompt", this.currentSession, this.currentBranch, {
775
775
  source,
776
776
  text,
777
+ ...(images?.length ? { images: images.map((i) => ({ url: i.url, name: i.name })) } : {}),
777
778
  ...(promptId ? { promptId } : {}),
778
779
  });
779
780
  })
@@ -808,7 +809,21 @@ export class Agent {
808
809
  */
809
810
  drainPendingPrompts() {
810
811
  for (const p of this.pendingPrompts.splice(0)) {
811
- this.messages.push({ role: "user", content: p.text });
812
+ // multimodal: images ride as OpenAI content parts (text first, then
813
+ // images); plain prompts keep the string form for cache friendliness
814
+ if (p.images?.length) {
815
+ this.messages.push({
816
+ role: "user",
817
+ content: p.text,
818
+ content_parts: [
819
+ { type: "text", text: p.text },
820
+ ...p.images.map((i) => ({ type: "image_url", image_url: { url: i.url } })),
821
+ ],
822
+ });
823
+ }
824
+ else {
825
+ this.messages.push({ role: "user", content: p.text });
826
+ }
812
827
  if (p.source === "user") {
813
828
  const id = p.id ?? `u${Date.now().toString(36)}${Math.random().toString(36).slice(2, 6)}`;
814
829
  this.deliveredPrompts.push(id);
@@ -2109,19 +2124,48 @@ function rebuildMessagesFrom(list) {
2109
2124
  // synthesizer below must not duplicate them
2110
2125
  const loggedResults = new Set(list.filter((e) => e.type === "tool_result").map((e) => String(e.data.callId ?? "")));
2111
2126
  const bufferedUsers = [];
2127
+ // image attachments paired with buffered prompts (multimodal replay)
2128
+ const bufferedParts = [];
2112
2129
  const flushUsers = () => {
2113
2130
  if (openCalls.size === 0) {
2114
- for (const text of bufferedUsers.splice(0))
2115
- msgs.push({ role: "user", content: text });
2131
+ const texts = bufferedUsers.splice(0);
2132
+ const parts = bufferedParts.splice(0);
2133
+ texts.forEach((text, i) => {
2134
+ const imgs = parts[i] ?? [];
2135
+ msgs.push(imgs.length
2136
+ ? {
2137
+ role: "user",
2138
+ content: text,
2139
+ content_parts: [
2140
+ { type: "text", text },
2141
+ ...imgs.map((im) => ({ type: "image_url", image_url: { url: im.url } })),
2142
+ ],
2143
+ }
2144
+ : { role: "user", content: text });
2145
+ });
2116
2146
  }
2117
2147
  };
2118
2148
  for (const e of list) {
2119
2149
  const d = e.data;
2120
2150
  if (e.type === "prompt" && typeof d.text === "string") {
2151
+ // restore image attachments so replays stay byte-identical with the
2152
+ // original request (prefix caches stay warm across restarts)
2153
+ const text = d.text;
2154
+ const imgs = Array.isArray(d.images) ? d.images : [];
2155
+ const mk = () => imgs.length
2156
+ ? {
2157
+ role: "user",
2158
+ content: text,
2159
+ content_parts: [
2160
+ { type: "text", text },
2161
+ ...imgs.map((i) => ({ type: "image_url", image_url: { url: i.url } })),
2162
+ ],
2163
+ }
2164
+ : { role: "user", content: text };
2121
2165
  if (openCalls.size > 0)
2122
- bufferedUsers.push(d.text);
2166
+ bufferedUsers.push(text), bufferedParts.push(imgs);
2123
2167
  else
2124
- msgs.push({ role: "user", content: d.text });
2168
+ msgs.push(mk());
2125
2169
  }
2126
2170
  else if (e.type === "message") {
2127
2171
  // final summaries are operator-facing only — the live loop never puts
@@ -2229,7 +2273,22 @@ function rebuildMessagesFrom(list) {
2229
2273
  }
2230
2274
  }
2231
2275
  // prompts that were still waiting on a hole-filled tail land here
2232
- for (const text of bufferedUsers.splice(0))
2233
- msgs.push({ role: "user", content: text });
2276
+ {
2277
+ const texts = bufferedUsers.splice(0);
2278
+ const parts = bufferedParts.splice(0);
2279
+ texts.forEach((text, i) => {
2280
+ const imgs = parts[i] ?? [];
2281
+ msgs.push(imgs.length
2282
+ ? {
2283
+ role: "user",
2284
+ content: text,
2285
+ content_parts: [
2286
+ { type: "text", text },
2287
+ ...imgs.map((im) => ({ type: "image_url", image_url: { url: im.url } })),
2288
+ ],
2289
+ }
2290
+ : { role: "user", content: text });
2291
+ });
2292
+ }
2234
2293
  return msgs;
2235
2294
  }
package/dist/agent/llm.js CHANGED
@@ -80,13 +80,21 @@ function looksMangled(name) {
80
80
  }
81
81
  function sanitize(messages) {
82
82
  return messages.map((m) => {
83
+ // multimodal user message → OpenAI content-parts array (text + images);
84
+ // providers without vision reject it, but those models were never a fit
85
+ if (m.content_parts?.length) {
86
+ const parts = m.content_parts.map((p) => p.type === "image_url"
87
+ ? { type: "image_url", image_url: { url: p.image_url.url } }
88
+ : { type: "text", text: p.text ?? "" });
89
+ return { role: m.role, content: parts };
90
+ }
83
91
  if ((m.role === "user" || m.role === "assistant") && !m.content) {
84
92
  return { ...m, content: m.tool_calls?.length ? "(tool call)" : "(no content)" };
85
93
  }
86
94
  if (m.role === "tool" && typeof m.content !== "string") {
87
95
  return { ...m, content: String(m.content ?? "(no output)") };
88
96
  }
89
- return m;
97
+ return { ...m };
90
98
  });
91
99
  }
92
100
  export async function chat(cfg, messages, tools, signal, onDelta) {
@@ -38,6 +38,12 @@ export async function fetchModelList(baseUrl, apiKey) {
38
38
  completion: Number(m.pricing.completion ?? 0),
39
39
  }
40
40
  : undefined,
41
+ modalities: Array.isArray(m.architecture?.input_modalities) && m.architecture.input_modalities.length
42
+ ? {
43
+ input: m.architecture.input_modalities,
44
+ output: m.architecture.output_modalities ?? ["text"],
45
+ }
46
+ : undefined,
41
47
  }))
42
48
  .filter((m) => m.id);
43
49
  cache.set(root, { at: Date.now(), list });
@@ -38,6 +38,12 @@ async function listModels(baseUrl, apiKey) {
38
38
  completion: Number(m.pricing.completion ?? 0),
39
39
  }
40
40
  : undefined,
41
+ modalities: Array.isArray(m.architecture?.input_modalities) && m.architecture.input_modalities.length
42
+ ? {
43
+ input: m.architecture.input_modalities,
44
+ output: m.architecture.output_modalities ?? ["text"],
45
+ }
46
+ : undefined,
41
47
  }))
42
48
  .filter((m) => m.id)
43
49
  .sort((a, b) => a.id.localeCompare(b.id));
@@ -607,12 +613,21 @@ export function buildApp(master) {
607
613
  if (!a)
608
614
  return c.json({ error: "not found" }, 404);
609
615
  const body = await c.req.json();
610
- if (!body.text?.trim())
611
- return c.json({ error: "text required" }, 400);
616
+ if (!body.text?.trim() && !body.images?.length)
617
+ return c.json({ error: "text or images required" }, 400);
618
+ // validate images: data URLs or http(s), size-capped (a 20MB base64 blob
619
+ // in the log would hurt every future replay of this session)
620
+ const images = (body.images ?? []).filter((im) => {
621
+ const u = String(im?.url ?? "");
622
+ if (u.startsWith("data:image/"))
623
+ return u.length < 12_000_000; // ~9MB binary
624
+ return /^https?:\/\//.test(u);
625
+ });
626
+ const text = body.text ?? "";
612
627
  // returns immediately: the prompt is logged + broadcast now, delivered to
613
628
  // the model at the next turn boundary (never blocks on a running agent).
614
629
  // promptId ties the UI's pending echo to the later prompt-delivered note.
615
- const promptId = a.enqueuePrompt(body.text, "user");
630
+ const promptId = a.enqueuePrompt(text || "(see attached image)", "user", images.length ? images : undefined);
616
631
  if (body.start !== false && a.status !== "running")
617
632
  a.start("prompt");
618
633
  return c.json({ ok: true, promptId, queued: a.snapshot().pendingPrompts });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.19.9",
3
+ "version": "0.20.1",
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",