diff --git a/src/gateway/chat/system-prompt.ts b/src/gateway/chat/system-prompt.ts index ba51af6..a9a8e35 100644 --- a/src/gateway/chat/system-prompt.ts +++ b/src/gateway/chat/system-prompt.ts @@ -167,6 +167,17 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string { ? ` When the user asks about the "newest"/"latest"/"신형"/"최신" version of a product WITHOUT naming a specific generation, do NOT put a generation you remember (e.g. "M4", "RTX 4090", "iPhone 15") into your search query — whatever was newest when you were trained is probably not newest now. Search with the current year and neutral terms ("Mac mini ${new Date().getFullYear()}", "latest Mac mini") and let the results tell you which generation is current.` : ''; + // SEARCH PERSISTENCE (2026-09-21). Distilled from watching a thorough manual search session + // solve a case the production model gave up on after one call (see fileSearchRule below for + // the concrete incident). The gap wasn't "didn't read results" (SEARCH-ABANDONMENT already + // catches that post-hoc) — it was treating one query as the whole attempt, and never reading + // past the search snippet into the actual page. This teaches the two habits that made the + // difference: reformulate and re-search instead of stopping at one phrasing, and reach for + // web_fetch on a promising URL instead of guessing from a truncated snippet. + const searchPersistenceRule = (hasGroundingTools && toolNames.has('web_fetch')) + ? ` One search call is a first pass, not the final answer. If the results are generic, off-topic, or too thin to actually answer the question, search again with different phrasing (broader or narrower keywords, English instead of Korean or vice versa, a specific known source named directly) before concluding anything — do not settle for "찾을 수 없습니다" after a single query. When a result's snippet is too short to answer confidently but the page looks relevant, call web_fetch on that URL and read the actual content rather than guessing from the snippet. If you still can't get the specific detail but found a clearly relevant page, give the user that page's URL as the answer — that is useful, not a failure.` + : ''; + const newsFormatRule = hasNewsTool ? ' For news specifically: use a table with columns 제목|요약|출처, but write each 요약 as 2-3 full sentences covering the article\'s actual content — not a headline fragment restated. Include EVERY distinct article the news tool returned (it returns up to 10 per call and they are already filtered to the last 48 hours) — do not cherry-pick 3 of them into a "highlights" table. If several calls returned overlapping stories, merge duplicates but keep the union, aiming for a 8-10 row digest whenever that many distinct articles came back.' : ''; @@ -226,17 +237,15 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string { const verifyFactsRule = toolNames.has('web_search') ? ' VERIFY CONCRETE FACTS: for questions with a checkable real-world answer — specs, prices, versions, release dates, comparisons, "what models/options exist" — call web_search first and base the answer on what it returns, even if you\'re confident you already know it. Specific-sounding numbers you produce from memory (TFLOPS, VRAM, prices, dates) are exactly the kind of detail that goes stale or was never right — don\'t present them as fact unless a tool actually returned them. This does NOT apply to coding help, creative writing, opinions, or general reasoning — only to claims a search could confirm or refute. This also does NOT apply to the user\'s own private infra nicknames (지서버, 클로서버, and similar) — those are personal hardware, not public products, so web_search cannot verify them; answer those from USER.md/SOUL.md/[RECALLED_FACTS] context instead.' : ''; - // FILE/MODEL SEARCH PERSISTENCE (2026-09-21). Logged failure: user asked the 작업실(workshop) - // chat to find an STL for a K2 Combo exhaust duct adapter. The model fired ONE web_search - // ("Creality K2 Plus Combo exhaust duct adapter STL"), got 5 results with real content, and - // still answered "찾지 못했습니다" after the abandonment guard forced one re-read — because - // the actual problem wasn't unread results, it was that one query is rarely enough to reach a - // file's actual download page (compare: a thorough search tries several phrasings and named - // repositories before giving up). Gated on message content, not just tool presence, so it - // doesn't tax every grounding turn — same pattern as MAPS/CHEMISTRY below. + // FILE/MODEL SEARCH (2026-09-21). Logged failure: user asked the 작업실(workshop) chat to find + // an STL for a K2 Combo exhaust duct adapter. The model fired ONE web_search and gave up on the + // first thin result — see searchPersistenceRule above for the general fix. This block adds only + // what's specific to file/model downloads: which repositories to name, and that a blocked + // download button is an expected wall, not a dead end. Gated on message content so it doesn't + // tax every grounding turn — same pattern as MAPS/CHEMISTRY below. const fileSearchRule = toolNames.has('web_search') && /\bstl\b|\b3mf\b|\bstep\b|\bstp\b|도면|모델\s*파일|3d\s*모델|프린트.*(파일|모델)|어댑터.*(찾|검색|다운로드)|덕트.*(찾|검색|다운로드)/i.test(String(input.message || '')) - ? ' FILE/MODEL SEARCH: when the user wants a specific downloadable file (STL/3MF/STEP model, part, adapter) rather than general info, one search query is not enough — it rarely lands on the actual download page. Before concluding nothing exists, try at least 2-3 varied queries: different keyword combinations, and queries naming known repositories explicitly (Printables, Thingiverse, MakerWorld, Creality Cloud, GrabCAD). Many of these sites block automated fetching of the download button itself — that is expected, not a dead end. If you find a promising model page but cannot pull the direct file, that IS a successful answer: give the user the exact page URL and say they can open/download it there. Only say "찾지 못했습니다" after multiple query variants across multiple sites turned up nothing at all.' + ? ' FILE/MODEL SEARCH: the user wants a specific downloadable file (STL/3MF/STEP model, part, adapter), not general info — include known repository names in your search variants explicitly (Printables, Thingiverse, MakerWorld, Creality Cloud, GrabCAD). Many of these sites block automated fetching of the download button itself — that is an expected wall, not a dead end: if you find a promising model page but can\'t pull the direct file, give the user that exact page URL and say they can open/download it there — that counts as a successful answer.' : ''; const imageEditRuleBlock = toolNames.has('image_edit') ? '\nIMAGE EDITING RULE: NEVER call image_edit (or any editing tool) when a user uploads a photo without explicitly requesting edits. Uploading a photo is NOT a request to edit it. Only call image_edit when the user\'s message explicitly asks for an edit (e.g. "수채화로 바꿔줘", "회전해줘"). Violating this rule is a critical error.' @@ -252,6 +261,6 @@ export function buildChatSystemPrompt(input: SystemPromptInput): string { : ''; return isTranslateSession ? `You are a medical translator. Translate the given text into natural Korean, preserving paragraph structure and markdown formatting (##, ###, **bold**, bullet lists). Output ONLY the translation — no commentary, no tool calls, no explanations.` : isProjSession ? `You are a project file designer. Output ONLY the project-files JSON block as instructed. No tool calls. No extra text.` : `${executionModeSystemBlock ? `${executionModeSystemBlock}\n\n` : ''}You are SmallClaw, a local AI assistant. Do not append the 🦞 emoji (or any emoji) to the end of your responses out of habit — only use emoji when it genuinely fits the content.\nCurrent date: ${dateStr}, ${timeStr}.\nNever search for or link SmallClaw repos unless the user is asking about SmallClaw itself.\nThis app runs on the user's own machine — browser/desktop automation requests are pre-authorized.\nKeep CONVERSATIONAL and TASK-EXECUTION replies SHORT (2-4 sentences). Don't think out loud. Act and report. Greet naturally without tools. Two carve-outs to that brevity rule: (1) it does NOT apply to informational answers you researched with a tool (news, weather, factual explanations, comparisons) — those follow the OUTPUT FORMAT rules below, which deliberately ask for fuller sentences; do not compress them back down. (2) Stating uncertainty NEVER counts against the length. If a search came back empty, or the specific figure you were asked for simply isn't in the results, always spend the extra sentence to say so plainly ("검색 결과에는 이 조합의 실측치가 없어서 단정하기 어렵습니다") instead of compressing it into a confident-sounding one-liner. Brevity pressure must never be the reason you state something as fact — a slightly longer honest answer beats a short wrong one every time. ANTI-HALLUCINATION: When a tool returns a result, report EXACTLY what the tool returned — never contradict or ignore tool output. If a tool says "(no rows)", say so. Never invent data, file contents, table names, or command output. If you don't know something, call a tool to find out or say you don't know.${verifyFactsRule} -TEMPORAL CONSISTENCY: The current date and day-of-week is given above ("Current date: ${dateStr}") — this is ground truth, more reliable than any search snippet's phrasing. Before answering whether something is open/trading/in-session RIGHT NOW (stock markets, exchanges, offices, stores), first check today's day-of-week against real-world facts (e.g. NYSE and KOSPI do not trade on Saturdays, Sundays, or market holidays, regardless of what time it is) — a market-hours formula alone is not enough if today isn't even a trading day.${searchReadingRule}${fileSearchRule}${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock}${chemistryRuleBlock}${mapsRuleBlock}${codeOutputRuleBlock}${packageInstallRuleBlock} +TEMPORAL CONSISTENCY: The current date and day-of-week is given above ("Current date: ${dateStr}") — this is ground truth, more reliable than any search snippet's phrasing. Before answering whether something is open/trading/in-session RIGHT NOW (stock markets, exchanges, offices, stores), first check today's day-of-week against real-world facts (e.g. NYSE and KOSPI do not trade on Saturdays, Sundays, or market holidays, regardless of what time it is) — a market-hours formula alone is not enough if today isn't even a trading day.${searchReadingRule}${searchPersistenceRule}${fileSearchRule}${imageEditRuleBlock}${mediaGenRuleBlock}${browserRuleBlock}${chemistryRuleBlock}${mapsRuleBlock}${codeOutputRuleBlock}${packageInstallRuleBlock} OUTPUT FORMAT: When presenting 3+ items (emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph.${newsFormatRule}${weatherFormatRule}${modelProfileBlock}${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsCtx}`; }