From 8ae89610f3c5f2b140b7237c4117cf6f8d484e3a Mon Sep 17 00:00:00 2001 From: Paul Kloppers Date: Wed, 13 May 2026 23:41:44 +0200 Subject: [PATCH] fix(redstone): force image delivery + raise tool-loop ceiling + log tool calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduced: "send me the recipe for an anvil" got a TEXT reply describing the recipe but no image. Three changes to fix this class of problem: 1) Iteration cap 4 → 6. The wiki+photo flow is naturally wiki_search → wiki_page → wiki_page_images → tg_send_photo → final text — five tool calls + one text response. With only 4 iterations the model often ran out before reaching tg_send_photo or before emitting closing text, returning {text:null, ranTool:true} which the handler swallows silently. 2) Persona prompt: image delivery is now MANDATORY when the user asks for a picture / image / recipe / "what it looks like". Added an explicit fallback chain: filter='recipe' → 'craft' → 'grid' → the wiki_page thumbnail_url. The previous phrasing was suggestive only, which let the model answer in pure text. 3) Tool-call tracing. Both dispatchers now log `[redstone:or|gem] tool -> name args` before each call and `tool <- name ok|err msg` after. Without this, silent "model called tools but no image showed up" failures had no log line at all and we had to guess. Co-Authored-By: Claude Opus 4.7 --- bot/bot.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/bot/bot.ts b/bot/bot.ts index e87bb1c..0c1521a 100644 --- a/bot/bot.ts +++ b/bot/bot.ts @@ -1210,6 +1210,8 @@ function redstonePersona(user: User, chatType: string, chatId: number, srv: stri "- Minecraft server: check status (server_status), start/stop/restart services (server_up/down/restart), tail logs (server_logs), look up players in the bot's database (players_list/player_get).", "- Admin-only tools (the caller's role is shown in the context block): run RCON commands (rcon), trigger a world backup (backup), and modify player records (player_upsert/set_*/remove, seen_list). If a non-admin asks for one, politely refuse and don't call it.", "- Minecraft wiki: look things up on minecraft.wiki and surface images into the chat. Usual flow for a 'how do I craft X?' / 'what is X?' question: wiki_search to pick the exact page title → wiki_page for a one-line summary + thumbnail → wiki_page_images with filter='recipe' or 'craft' or 'grid' to find a crafting-grid image, then tg_send_photo to send the image URL straight into the chat (caption it briefly).", + "- IMAGE DELIVERY IS MANDATORY when the user asks for a picture / image / recipe / what it looks like. You MUST call tg_send_photo with a URL from wiki_page_images (or the thumbnail_url from wiki_page as fallback). Describing the recipe only in words is not enough — the user explicitly asked for the image. After tg_send_photo succeeds, you can stop; the photo IS the reply.", + "- If wiki_page_images returns no matches with filter='recipe', retry with filter='craft' or filter='grid'. If still empty, fall back to wiki_page's thumbnail_url. Always send SOMETHING visual when an image was requested.", "- When you use a tool, you usually don't need a separate text reply unless you're adding context — the tool's effect IS the response. After a server_status / players_list lookup, summarize the answer in one sentence.", "", "Stickers — drop emojis inline and the bot does the rest:", @@ -1371,7 +1373,7 @@ async function runRedstoneTurnViaOpenRouter( { role: "user", content: userText }, ]; let ranTool = false; - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 6; i++) { const data = await callOpenRouterOnce(messages); if (!data) return null; // hard failure — caller falls back to Gemini const msg = data.choices?.[0]?.message; @@ -1399,11 +1401,14 @@ async function runRedstoneTurnViaOpenRouter( continue; } try { + console.info("[redstone:or] tool ->", tc.function.name, JSON.stringify(args).slice(0, 200)); const result = await withSlowToolIndicator(chatId, tc.function.name, () => tool.handler(args, toolCtx)); response = { result }; ranTool = true; + console.info("[redstone:or] tool <-", tc.function.name, "ok"); } catch (e) { response = { error: (e as Error).message }; + console.warn("[redstone:or] tool <-", tc.function.name, "err:", (e as Error).message); } } messages.push({ role: "tool", tool_call_id: tc.id, content: JSON.stringify(response) }); @@ -1417,7 +1422,7 @@ async function runRedstoneTurnViaGemini( ): Promise { const contents: GeminiContent[] = [...history, { role: "user", parts: [{ text: userText }] }]; let ranTool = false; - for (let i = 0; i < 4; i++) { + for (let i = 0; i < 6; i++) { const data = await callGeminiOnce(contents, systemPrompt); if (!data) return { text: null, ranTool }; const parts = data.candidates?.[0]?.content?.parts ?? []; @@ -1445,11 +1450,14 @@ async function runRedstoneTurnViaGemini( response = { error: `tool '${fc.functionCall.name}' requires admin role; the calling user is '${callerRole}'. Inform the user, do not retry.` }; } else { try { + console.info("[redstone:gem] tool ->", fc.functionCall.name, JSON.stringify(fc.functionCall.args).slice(0, 200)); const result = await withSlowToolIndicator(chatId, fc.functionCall.name, () => tool.handler(fc.functionCall.args, toolCtx)); response = { result }; ranTool = true; + console.info("[redstone:gem] tool <-", fc.functionCall.name, "ok"); } catch (e) { response = { error: (e as Error).message }; + console.warn("[redstone:gem] tool <-", fc.functionCall.name, "err:", (e as Error).message); } } responseParts.push({ functionResponse: { name: fc.functionCall.name, response } });