v4.1.0: 메인챗 툴/시스템프롬프트 오버헤드 66% 감소 + 실시간 컨텍스트 게이지
- 2개월 tool_audit.log 실사용 확인 후 미사용 에이전트 툴(start_task 등)과 포트 9222 연결실패로 고장난 브라우저 툴을 메인챗에서 제외 - 날씨/법률/논문 툴을 스킬+키워드 조건부로 전환, dental/mind 전용 MCP DB 24개를 해당 앱 세션(dental_/mn_)에만 스코핑, weather/lawyer 앱 세션은 스킬·키워드와 무관하게 항상 포함되도록 별도 처리 - 위 변경으로 메인챗 고정 오버헤드 24,439 → 8,169 토큰(-66%) 실측 확인 - tool_overhead SSE 이벤트를 모델 호출 전에 전송해 세션 게이지가 중단된 턴에서도 툴 스키마+시스템프롬프트 크기를 반영하도록 수정 (app.js) - 부수 수정: heartbeat 세션을 lastMainSessionId 폴백에서 완전 격리(다른 세션 기록 오염 방지), webSearch 빈 결과를 실패로 오판하던 버그 수정 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "smallclaw",
|
"name": "smallclaw",
|
||||||
"version": "4.0.0",
|
"version": "4.1.0",
|
||||||
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
"description": "Local AI agent framework powered by Ollama - OpenClaw alternative",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+85
-16
@@ -1041,7 +1041,12 @@ const heartbeatRunner = new HeartbeatRunner({
|
|||||||
configPath: path.join(CONFIG_DIR_PATH, 'heartbeat', 'config.json'),
|
configPath: path.join(CONFIG_DIR_PATH, 'heartbeat', 'config.json'),
|
||||||
handleChat: (message, sessionId, sendSSE, pinnedMessages, abortSignal, callerContext, modelOverride, executionMode, username) =>
|
handleChat: (message, sessionId, sendSSE, pinnedMessages, abortSignal, callerContext, modelOverride, executionMode, username) =>
|
||||||
handleChat(message, sessionId, sendSSE, pinnedMessages, abortSignal, callerContext, modelOverride, executionMode, username),
|
handleChat(message, sessionId, sendSSE, pinnedMessages, abortSignal, callerContext, modelOverride, executionMode, username),
|
||||||
getMainSessionId: () => lastMainSessionId || 'default',
|
// Dedicated, isolated session — must NEVER fall back to lastMainSessionId.
|
||||||
|
// That global variable reflects whichever app/user last sent a chat message
|
||||||
|
// server-wide, so heartbeat's autonomous tool calls (which can touch any
|
||||||
|
// registered MCP tool, including sensitive per-app databases) would otherwise
|
||||||
|
// get appended into a random unrelated app's conversation history.
|
||||||
|
getMainSessionId: () => 'heartbeat_internal',
|
||||||
getIsModelBusy: () => checkIsModelBusy(),
|
getIsModelBusy: () => checkIsModelBusy(),
|
||||||
broadcast: broadcastWS,
|
broadcast: broadcastWS,
|
||||||
deliverTelegram: (text: string) => telegramChannel.sendToAllowed(text),
|
deliverTelegram: (text: string) => telegramChannel.sendToAllowed(text),
|
||||||
@@ -2893,7 +2898,9 @@ async function webSearch(query: string): Promise<string> {
|
|||||||
try {
|
try {
|
||||||
const { executeWebSearch } = await import('../tools/web.js');
|
const { executeWebSearch } = await import('../tools/web.js');
|
||||||
const res = await executeWebSearch({ query, max_results: 5 });
|
const res = await executeWebSearch({ query, max_results: 5 });
|
||||||
if (res.success && res.stdout) return res.stdout;
|
// res.success with empty stdout means the provider genuinely found 0 results —
|
||||||
|
// that's a valid answer, not a failure. Only exceptions should fall through to DDG.
|
||||||
|
if (res.success) return res.stdout || `"${query}"에 대한 검색 결과가 없습니다.`;
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.warn(`[v2] webSearch ${provider} failed, falling back to DDG:`, err.message);
|
console.warn(`[v2] webSearch ${provider} failed, falling back to DDG:`, err.message);
|
||||||
}
|
}
|
||||||
@@ -5064,11 +5071,11 @@ async function handleCodeChat(
|
|||||||
const history = (historyOverride || []).map(m => ({ role: (m.role === 'user' ? 'user' : 'assistant') as 'user' | 'assistant', content: String(m.content) }));
|
const history = (historyOverride || []).map(m => ({ role: (m.role === 'user' ? 'user' : 'assistant') as 'user' | 'assistant', content: String(m.content) }));
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'Asia/Seoul' });
|
||||||
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Seoul' });
|
||||||
|
|
||||||
// Tools available in the editor: everything except browser/messaging/timer/etc.
|
// Tools available in the editor: everything except browser/messaging/timer/etc.
|
||||||
const codeAiBlockedTools = new Set(['start_task', 'task_control', 'write_note', 'browser_open', 'browser_snapshot', 'browser_click', 'browser_fill', 'browser_press_key', 'browser_wait', 'browser_close', 'image_search', 'image_url', 'image_edit', 'image_info', 'image_read', 'weather_search', 'start_timer', 'send_message', 'send_telegram', 'kakao_send_message', 'create_presentation', 'edit_presentation', 'spawn_subagent', 'schedule_job', 'parse_schedule_pattern']);
|
const codeAiBlockedTools = new Set(['start_task', 'task_control', 'write_note', 'browser_open', 'browser_snapshot', 'browser_click', 'browser_fill', 'browser_press_key', 'browser_wait', 'browser_close', 'image_search', 'image_url', 'image_edit', 'image_info', 'image_read', 'weather_search', 'weather_map_screenshot', 'start_timer', 'send_message', 'send_telegram', 'kakao_send_message', 'create_presentation', 'edit_presentation', 'spawn_subagent', 'schedule_job', 'parse_schedule_pattern']);
|
||||||
// useTools:false → plan/ask phase; pass empty tools so the model generates text only
|
// useTools:false → plan/ask phase; pass empty tools so the model generates text only
|
||||||
const tools = useTools ? buildTools().filter((t: any) => !codeAiBlockedTools.has(String(t?.function?.name || ''))) : [];
|
const tools = useTools ? buildTools().filter((t: any) => !codeAiBlockedTools.has(String(t?.function?.name || ''))) : [];
|
||||||
|
|
||||||
@@ -5269,19 +5276,61 @@ async function handleChat(
|
|||||||
// coder_delete_lines, coder_list_files, shell, python_eval, and web_search to directly
|
// coder_delete_lines, coder_list_files, shell, python_eval, and web_search to directly
|
||||||
// inspect and fix code. Blocked tools are ones that don't make sense in
|
// inspect and fix code. Blocked tools are ones that don't make sense in
|
||||||
// an editor context (browser automation, messaging, timers, presentations, etc.)
|
// an editor context (browser automation, messaging, timers, presentations, etc.)
|
||||||
const codeAiBlockedTools = new Set(['start_task', 'task_control', 'write_note', 'browser_open', 'browser_snapshot', 'browser_click', 'browser_fill', 'browser_press_key', 'browser_wait', 'browser_close', 'image_search', 'image_url', 'image_edit', 'image_info', 'image_read', 'weather_search', 'start_timer', 'send_message', 'send_telegram', 'kakao_send_message', 'create_presentation', 'edit_presentation', 'spawn_subagent', 'schedule_job', 'parse_schedule_pattern']);
|
const codeAiBlockedTools = new Set(['start_task', 'task_control', 'write_note', 'browser_open', 'browser_snapshot', 'browser_click', 'browser_fill', 'browser_press_key', 'browser_wait', 'browser_close', 'image_search', 'image_url', 'image_edit', 'image_info', 'image_read', 'weather_search', 'weather_map_screenshot', 'start_timer', 'send_message', 'send_telegram', 'kakao_send_message', 'create_presentation', 'edit_presentation', 'spawn_subagent', 'schedule_job', 'parse_schedule_pattern']);
|
||||||
const weatherToolNames = new Set(['weather_search', 'weather_airpollution', 'weather_openmeteo', 'weather_kma', 'weather_airkorea', 'weather_nasa_power', 'weather_era5', 'weather_cds', 'weather_cmip6']);
|
const weatherToolNames = new Set(['weather_search', 'weather_airpollution', 'weather_openmeteo', 'weather_kma', 'weather_airkorea', 'weather_nasa_power', 'weather_era5', 'weather_cds', 'weather_cmip6']);
|
||||||
const legalToolNames = new Set(['korean_law_search', 'korean_law_fetch', 'us_case_search']);
|
const legalToolNames = new Set(['korean_law_search', 'korean_law_fetch', 'us_case_search']);
|
||||||
const pptxToolNames = new Set(['create_presentation', 'edit_presentation']);
|
const pptxToolNames = new Set(['create_presentation', 'edit_presentation']);
|
||||||
|
// In practice these are only ever used while building an academic presentation (gather
|
||||||
|
// papers → create_presentation), not in standalone chat — gate them the same way as the
|
||||||
|
// pptx tools themselves rather than always loading 5 more schemas.
|
||||||
|
const academicToolNames = new Set(['pubmed_search', 'pubmed_fetch', 'pubmed_fulltext', 'openalex_search', 'semantic_search']);
|
||||||
const meteorologistEnabled = isSkillEnabledForUser('meteorologist', userWorkspace);
|
const meteorologistEnabled = isSkillEnabledForUser('meteorologist', userWorkspace);
|
||||||
const lawyerEnabled = isSkillEnabledForUser('lawyer', userWorkspace);
|
const lawyerEnabled = isSkillEnabledForUser('lawyer', userWorkspace);
|
||||||
const presenterEnabled = isSkillEnabledForUser('presenter', userWorkspace);
|
const presenterEnabled = isSkillEnabledForUser('presenter', userWorkspace);
|
||||||
const hasPptxKeyword = /슬라이드|발표|pptx|ppt\b|프레젠테이션|피피티|덱\b|presentation/i.test(message);
|
const hasPptxKeyword = /슬라이드|발표|pptx|ppt\b|프레젠테이션|피피티|덱\b|presentation/i.test(message);
|
||||||
|
// Weather/legal skills being ON means the user does that kind of work sometimes, not that
|
||||||
|
// every single turn is about it — without a keyword gate the full 9 weather / 3 legal tools
|
||||||
|
// rode along on unrelated turns too. Mirrors the pptx pattern (skill + keyword) below.
|
||||||
|
const hasWeatherKeyword = /날씨|기온|강수|미세먼지|대기질|태풍|기후|일기예보|장마|폭염|한파|자외선|황사|weather|forecast/i.test(message);
|
||||||
|
const hasLegalKeyword = /법률|판례|소송|변호사|법원|법조문|조항|계약서|형법|민법|법령|legal|lawsuit|court|statute/i.test(message);
|
||||||
|
// Sub-agent/task-orchestration tools that tool_audit.log shows near-zero real usage for
|
||||||
|
// (2026-05-04~2026-07-12: start_task/request_secondary_assist/subagent_spawn/
|
||||||
|
// delegate_to_specialist/spawn_subagent = 0 calls; task_control/parse_schedule_pattern/
|
||||||
|
// spawn_agent = a handful, all stale). Kept registered for future agent-composition work,
|
||||||
|
// just not sent to the model in normal chat. schedule_job is excluded from this set — it's
|
||||||
|
// self-contained (list/create/update/pause/resume/delete/run_now) and still actively used.
|
||||||
|
const dormantAgentTools = new Set(['start_task', 'task_control', 'parse_schedule_pattern', 'request_secondary_assist', 'subagent_spawn', 'delegate_to_specialist', 'spawn_subagent', 'spawn_agent']);
|
||||||
|
// Browser automation — parked. tool_audit.log shows every browser_open call from 07-09
|
||||||
|
// onward failing with "Chrome launched but did not respond on port 9222". Repeated debug
|
||||||
|
// runs showed the live `tools` array alternates between two mutually-exclusive sets for the
|
||||||
|
// SAME "ping" request against the SAME running process — open/snapshot/click/fill/press_key/
|
||||||
|
// wait/close one time, scroll/get_images the next — apparently some live Chrome-reachability
|
||||||
|
// probe swapping which schema set gets exposed. Excluding all 9 names covers both variants.
|
||||||
|
// Re-enable by removing this set once browser automation is fixed / needed (e.g. home shopping).
|
||||||
|
const dormantBrowserTools = new Set(['browser_open', 'browser_snapshot', 'browser_click', 'browser_fill', 'browser_press_key', 'browser_wait', 'browser_close', 'browser_scroll', 'browser_get_images']);
|
||||||
|
// MCP database servers built for a specific dedicated app tab (dental-agent.html →
|
||||||
|
// 'dental_' sessions, mind-app.html → 'mn_' sessions) — 62% of the tool-schema overhead
|
||||||
|
// (12,124 of 19,531 tokens measured) came from these 3 servers × 8 sub-tools each, riding
|
||||||
|
// along on every main-chat turn for every user even though they're single-purpose lookup
|
||||||
|
// tables for one app. Scope them to the app session that actually uses them.
|
||||||
|
const isDentalAppSession = /^dental_/.test(String(sessionId || ''));
|
||||||
|
const isMindAppSession = /^mn_/.test(String(sessionId || ''));
|
||||||
|
// Weather/lawyer have their own dedicated app tabs (weather-app.html → 'wt_' sessions,
|
||||||
|
// lawyer-app.html → 'lw_' sessions). Inside those apps the skill+keyword gate below is
|
||||||
|
// wrong — a "wt_" session IS the weather context even if a given message has no weather
|
||||||
|
// keyword (e.g. a follow-up "내일은?"), so always include the tools there.
|
||||||
|
const isWeatherAppSession = /^wt_/.test(String(sessionId || ''));
|
||||||
|
const isLawyerAppSession = /^lw_/.test(String(sessionId || ''));
|
||||||
const skillToolFilter = (t: any) => {
|
const skillToolFilter = (t: any) => {
|
||||||
const name = String(t?.function?.name || '');
|
const name = String(t?.function?.name || '');
|
||||||
if (weatherToolNames.has(name) && !meteorologistEnabled) return false;
|
if (dormantAgentTools.has(name)) return false;
|
||||||
if (legalToolNames.has(name) && !lawyerEnabled) return false;
|
if (dormantBrowserTools.has(name)) return false;
|
||||||
|
if (name.startsWith('mcp__dental-dict-sqlite__') && !isDentalAppSession) return false;
|
||||||
|
if ((name.startsWith('mcp__psychotherapy-cases-sqlite__') || name.startsWith('mcp__psychiatry-cases-sqlite__')) && !isMindAppSession) return false;
|
||||||
|
if (weatherToolNames.has(name) && !isWeatherAppSession && !(meteorologistEnabled && hasWeatherKeyword)) return false;
|
||||||
|
if (legalToolNames.has(name) && !isLawyerAppSession && !(lawyerEnabled && hasLegalKeyword)) return false;
|
||||||
if (pptxToolNames.has(name) && presenterEnabled && !hasPptxKeyword) return false;
|
if (pptxToolNames.has(name) && presenterEnabled && !hasPptxKeyword) return false;
|
||||||
|
if (academicToolNames.has(name) && !hasPptxKeyword) return false;
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
const tools = isBootStartupTurn
|
const tools = isBootStartupTurn
|
||||||
@@ -5620,8 +5669,9 @@ async function handleChat(
|
|||||||
const toolMessageContent = (multiAgentActive && (isBrowserTool || isDesktopTool))
|
const toolMessageContent = (multiAgentActive && (isBrowserTool || isDesktopTool))
|
||||||
? (isBrowserTool ? buildBrowserAck(toolName, toolResult) : buildDesktopAck(toolName, toolResult))
|
? (isBrowserTool ? buildBrowserAck(toolName, toolResult) : buildDesktopAck(toolName, toolResult))
|
||||||
: toolResult.result;
|
: toolResult.result;
|
||||||
const _imagingTools3 = new Set(['image_edit', 'image_info', 'image_read', 'python_eval']);
|
const _imagingTools3 = new Set(['image_edit', 'image_info', 'image_read', 'python_eval', 'weather_map_screenshot']);
|
||||||
const _supportsVision3 = /kimi|openai|claude|gemini|gpt(?!-oss)/i.test(String(_activeModelName || primaryProvider || ''));
|
const _supportsVision3 = /kimi|openai|claude|gemini|gpt(?!-oss)/i.test(String(_activeModelName || primaryProvider || ''));
|
||||||
|
if (_imagingTools3.has(toolName)) console.log(`[v2] IMG-VISION tool=${toolName} embedVision=${_supportsVision3} model=${_activeModelName || primaryProvider}`);
|
||||||
const _resolvedToolContent3 = _imagingTools3.has(toolName) && typeof toolMessageContent === 'string'
|
const _resolvedToolContent3 = _imagingTools3.has(toolName) && typeof toolMessageContent === 'string'
|
||||||
? resolveToolImageContent(toolMessageContent, workspacePath, _supportsVision3)
|
? resolveToolImageContent(toolMessageContent, workspacePath, _supportsVision3)
|
||||||
: toolMessageContent;
|
: toolMessageContent;
|
||||||
@@ -5677,8 +5727,8 @@ async function handleChat(
|
|||||||
: '';
|
: '';
|
||||||
|
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
|
const dateStr = now.toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', timeZone: 'Asia/Seoul' });
|
||||||
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
const timeStr = now.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', timeZone: 'Asia/Seoul' });
|
||||||
const executionModeSystemBlock = (() => {
|
const executionModeSystemBlock = (() => {
|
||||||
if (executionMode === 'background_task') {
|
if (executionMode === 'background_task') {
|
||||||
return [
|
return [
|
||||||
@@ -5703,18 +5753,35 @@ async function handleChat(
|
|||||||
return '';
|
return '';
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// browser_* tools are excluded from the default tool list (dormantBrowserTools, parked
|
||||||
|
// until a dedicated use case). The BROWSER RULE paragraph below only makes sense when the
|
||||||
|
// model actually has browser tools to be tempted to misuse — skip it otherwise so it isn't
|
||||||
|
// dead weight on every single main-chat turn.
|
||||||
|
const hasBrowserTools = tools.some((t: any) => String(t?.function?.name || '').startsWith('browser_'));
|
||||||
|
const browserRuleBlock = hasBrowserTools
|
||||||
|
? '\nBROWSER RULE: NEVER call browser_open, browser_snapshot, browser_click, browser_fill, browser_scroll, or any browser_* tool unless the user EXPLICITLY asks to use the browser (e.g. "브라우저로 열어줘", "Chrome으로 봐줘", "사이트 직접 들어가봐"). "열어줘" or "보여줘" alone is NOT a browser request — answer from knowledge, use web_fetch/web_search, or output the relevant URL/embed. Pasting a URL or asking a question is also NOT a browser request.'
|
||||||
|
: '';
|
||||||
const messages: any[] = [
|
const messages: any[] = [
|
||||||
{
|
{
|
||||||
role: 'system',
|
role: 'system',
|
||||||
content: 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.\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 responses SHORT (1-2 sentences). Don't think out loud. Act and report. Greet naturally without tools.
|
content: 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.\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 responses SHORT (1-2 sentences). Don't think out loud. Act and report. Greet naturally without tools.
|
||||||
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.
|
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.
|
||||||
IMAGE 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.
|
IMAGE 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.${browserRuleBlock}
|
||||||
BROWSER RULE: NEVER call browser_open, browser_snapshot, browser_click, browser_fill, browser_scroll, or any browser_* tool unless the user EXPLICITLY asks to use the browser (e.g. "브라우저로 열어줘", "Chrome으로 봐줘", "사이트 직접 들어가봐"). "열어줘" or "보여줘" alone is NOT a browser request — answer from knowledge, use web_fetch/web_search, or output the relevant URL/embed. Pasting a URL or asking a question is also NOT a browser request.
|
|
||||||
CODE OUTPUT: When writing code in a fenced code block, start with a filename comment on line 1: \`# filename: snake_game.py\` (Python), \`// filename: app.js\` (JS/C), \`<!-- filename: index.html -->\` (HTML). Never repeat code already written in this conversation. For modifications to existing files, use coder_overwrite_lines or coder_insert_lines (not coder_write_file — it only works for NEW files). All code changes are presented as diffs for the user to review before being applied. Write code directly — do not ask for permission.
|
CODE OUTPUT: When writing code in a fenced code block, start with a filename comment on line 1: \`# filename: snake_game.py\` (Python), \`// filename: app.js\` (JS/C), \`<!-- filename: index.html -->\` (HTML). Never repeat code already written in this conversation. For modifications to existing files, use coder_overwrite_lines or coder_insert_lines (not coder_write_file — it only works for NEW files). All code changes are presented as diffs for the user to review before being applied. Write code directly — do not ask for permission.
|
||||||
PACKAGE INSTALL: NEVER run pip install, npm install, apt-get, or any package installation command. If a package is missing, just write the code and mention the package name in a comment — let the user decide whether to install it. Do NOT attempt to install packages yourself.
|
PACKAGE INSTALL: NEVER run pip install, npm install, apt-get, or any package installation command. If a package is missing, just write the code and mention the package name in a comment — let the user decide whether to install it. Do NOT attempt to install packages yourself.
|
||||||
OUTPUT FORMAT: When presenting 3+ items (news articles, emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph. Example: news → table with columns 제목|요약|출처.${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsManager.buildPromptContextForUser(username ? getUserWorkspace(username) : null, 16000, message)}`,
|
OUTPUT FORMAT: When presenting 3+ items (news articles, emails, search results, lists), always use a markdown table or structured bullet list with clear headers. Never dump them as a long paragraph. Example: news → table with columns 제목|요약|출처.${callerContext ? '\n\n' + callerContext : ''}${browserStateCtx}${personalityCtx}${skillsManager.buildPromptContextForUser(username ? getUserWorkspace(username) : null, 16000, message)}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
// Emit the fixed-overhead size (system prompt + tool schemas) as soon as both are known —
|
||||||
|
// before the model call — so the client has a real per-session baseline even if the turn
|
||||||
|
// gets aborted before a 'usage' event (with the real prompt_eval_count) ever arrives.
|
||||||
|
// Without this, the UI's char/4 fallback silently ignores this baseline entirely, even
|
||||||
|
// though it dwarfs the actual conversation text in most sessions.
|
||||||
|
try {
|
||||||
|
const systemPromptTokens = Math.ceil(String(messages[0]?.content || '').length / 3.5);
|
||||||
|
const toolSchemaTokens = Math.ceil(JSON.stringify(tools).length / 3.5);
|
||||||
|
sendSSE('tool_overhead', { tokens: systemPromptTokens + toolSchemaTokens });
|
||||||
|
} catch {}
|
||||||
|
|
||||||
if (pinnedMessages && pinnedMessages.length > 0) {
|
if (pinnedMessages && pinnedMessages.length > 0) {
|
||||||
messages.push({ role: 'user', content: '[PINNED CONTEXT - Important messages from earlier in our conversation:]' });
|
messages.push({ role: 'user', content: '[PINNED CONTEXT - Important messages from earlier in our conversation:]' });
|
||||||
@@ -6766,8 +6833,9 @@ RULES:
|
|||||||
const toolMessageContent = (multiAgentActive && (isBrowserTool || isDesktopTool))
|
const toolMessageContent = (multiAgentActive && (isBrowserTool || isDesktopTool))
|
||||||
? (isBrowserTool ? buildBrowserAck(toolName, toolResult) : buildDesktopAck(toolName, toolResult))
|
? (isBrowserTool ? buildBrowserAck(toolName, toolResult) : buildDesktopAck(toolName, toolResult))
|
||||||
: toolResult.result;
|
: toolResult.result;
|
||||||
const _imagingTools2 = new Set(['image_edit', 'image_info', 'image_read', 'python_eval']);
|
const _imagingTools2 = new Set(['image_edit', 'image_info', 'image_read', 'python_eval', 'weather_map_screenshot']);
|
||||||
const _supportsVision2 = /kimi|openai|claude|gemini|gpt(?!-oss)/i.test(String(_activeModelName || primaryProvider || ''));
|
const _supportsVision2 = /kimi|openai|claude|gemini|gpt(?!-oss)/i.test(String(_activeModelName || primaryProvider || ''));
|
||||||
|
if (_imagingTools2.has(toolName)) console.log(`[v2] IMG-VISION tool=${toolName} embedVision=${_supportsVision2} model=${_activeModelName || primaryProvider}`);
|
||||||
const _resolvedContent2 = _imagingTools2.has(toolName) && typeof toolMessageContent === 'string'
|
const _resolvedContent2 = _imagingTools2.has(toolName) && typeof toolMessageContent === 'string'
|
||||||
? resolveToolImageContent(toolMessageContent, workspacePath, _supportsVision2)
|
? resolveToolImageContent(toolMessageContent, workspacePath, _supportsVision2)
|
||||||
: toolMessageContent;
|
: toolMessageContent;
|
||||||
@@ -8246,8 +8314,9 @@ RULES:
|
|||||||
const toolMessageContent = (multiAgentActive && (isBrowserTool || isDesktopTool))
|
const toolMessageContent = (multiAgentActive && (isBrowserTool || isDesktopTool))
|
||||||
? (isBrowserTool ? buildBrowserAck(toolName, toolResult) : buildDesktopAck(toolName, toolResult))
|
? (isBrowserTool ? buildBrowserAck(toolName, toolResult) : buildDesktopAck(toolName, toolResult))
|
||||||
: toolResult.result;
|
: toolResult.result;
|
||||||
const _imagingTools = new Set(['image_edit', 'image_info', 'image_read', 'python_eval']);
|
const _imagingTools = new Set(['image_edit', 'image_info', 'image_read', 'python_eval', 'weather_map_screenshot']);
|
||||||
const _supportsVision = /kimi|openai|claude|gemini|gpt(?!-oss)/i.test(String(_activeModelName || primaryProvider || ''));
|
const _supportsVision = /kimi|openai|claude|gemini|gpt(?!-oss)/i.test(String(_activeModelName || primaryProvider || ''));
|
||||||
|
if (_imagingTools.has(toolName)) console.log(`[v2] IMG-VISION tool=${toolName} embedVision=${_supportsVision} model=${_activeModelName || primaryProvider}`);
|
||||||
const _resolvedToolContent = _imagingTools.has(toolName) && typeof toolMessageContent === 'string'
|
const _resolvedToolContent = _imagingTools.has(toolName) && typeof toolMessageContent === 'string'
|
||||||
? resolveToolImageContent(toolMessageContent, workspacePath, _supportsVision)
|
? resolveToolImageContent(toolMessageContent, workspacePath, _supportsVision)
|
||||||
: toolMessageContent;
|
: toolMessageContent;
|
||||||
@@ -12874,7 +12943,7 @@ app.get('/api/agent/session/:id', (req, res) => {
|
|||||||
// Per-app session ids (see appSessionsPath above) live in the same sessions/ dir
|
// Per-app session ids (see appSessionsPath above) live in the same sessions/ dir
|
||||||
// as main-chat sessions but belong to their own app UI — keep them out of the
|
// as main-chat sessions but belong to their own app UI — keep them out of the
|
||||||
// main chat sidebar.
|
// main chat sidebar.
|
||||||
const APP_SESSION_PREFIXES = ['iv_', 'dt_', 'lw_', 'mn_', 'ac_', 'wt_', 'music_'];
|
const APP_SESSION_PREFIXES = ['iv_', 'dt_', 'lw_', 'mn_', 'ac_', 'wt_', 'music_', 'dental_'];
|
||||||
|
|
||||||
// GET /api/chat/sessions — list current user's sessions from disk
|
// GET /api/chat/sessions — list current user's sessions from disk
|
||||||
app.get('/api/chat/sessions', async (req, res) => {
|
app.get('/api/chat/sessions', async (req, res) => {
|
||||||
|
|||||||
+14
-1
@@ -1893,6 +1893,19 @@ async function sendChat(queuedMessage = null) {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sent before the model call, so it survives a turn that gets aborted before
|
||||||
|
// a real 'usage' event (with prompt_eval_count) arrives — lets the fallback
|
||||||
|
// estimate in renderSessionsList include tool-schema overhead instead of
|
||||||
|
// silently ignoring it.
|
||||||
|
case 'tool_overhead': {
|
||||||
|
const overhead = Number(event.tokens || 0);
|
||||||
|
if (overhead > 0) {
|
||||||
|
const idx = chatSessions.findIndex(s => s.id === activeChatSessionId);
|
||||||
|
if (idx !== -1) { chatSessions[idx].toolOverheadTokens = overhead; saveChatSessions(); }
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case 'done':
|
case 'done':
|
||||||
finalReply = event.reply || '';
|
finalReply = event.reply || '';
|
||||||
if (finalReply) partialContent = finalReply;
|
if (finalReply) partialContent = finalReply;
|
||||||
@@ -4452,7 +4465,7 @@ function renderSessionsList() {
|
|||||||
<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
|
<div style="display:flex;align-items:center;gap:6px;margin-top:4px">
|
||||||
${s.automated ? '<span class="session-auto-badge">Auto</span>' : ''}
|
${s.automated ? '<span class="session-auto-badge">Auto</span>' : ''}
|
||||||
<span class="badge badge-queued">${(s.history || []).length} msgs</span>
|
<span class="badge badge-queued">${(s.history || []).length} msgs</span>
|
||||||
${(() => { const fmtTok = n => n>=1e6?`${(n/1e6).toFixed(1)}M`:n>=1000?`${Math.round(n/1000)}k`:`${n}`; const ct = Number(s.contextTokens||0) || Math.round((s.history||[]).reduce((a,m)=>a+(m.content||'').length,0)/4); const mx = _appCtxMax; if (!ct) return ''; if (mx > 0) { const pct = Math.min(100, Math.round(ct/mx*100)); const col = pct>=80?'var(--err)':pct>=50?'var(--warn)':'var(--muted)'; return `<span style="font-size:10px;color:${col}">${fmtTok(ct)}/${fmtTok(mx)}</span>`; } return `<span style="font-size:10px;color:var(--muted)">${fmtTok(ct)} tok</span>`; })()}
|
${(() => { const fmtTok = n => n>=1e6?`${(n/1e6).toFixed(1)}M`:n>=1000?`${Math.round(n/1000)}k`:`${n}`; const real = Number(s.contextTokens||0); const ct = real || (Number(s.toolOverheadTokens||0) + Math.round((s.history||[]).reduce((a,m)=>a+(m.content||'').length,0)/4)); const mx = _appCtxMax; if (!ct) return ''; if (mx > 0) { const pct = Math.min(100, Math.round(ct/mx*100)); const col = pct>=80?'var(--err)':pct>=50?'var(--warn)':'var(--muted)'; return `<span style="font-size:10px;color:${col}">${real?'':'~'}${fmtTok(ct)}/${fmtTok(mx)}</span>`; } return `<span style="font-size:10px;color:var(--muted)">${real?'':'~'}${fmtTok(ct)} tok</span>`; })()}
|
||||||
<span style="color:var(--muted);font-size:10px">${timeAgo(s.updatedAt || s.createdAt)}</span>
|
<span style="color:var(--muted);font-size:10px">${timeAgo(s.updatedAt || s.createdAt)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user