fix: 레거시 스킬 토글이 도구를 막던 문제 — 키워드 게이트로 일원화
사용자 확인: 스킬 on/off UI는 예전 것이고 지금은 앱별 게이팅을 쓴다. 그런데 skills_state.json이 tool-scope의 meteorologist/lawyer/presenter 게이트를 통해 여전히 도구를 **막는** 유일한 소비자로 남아 있었고, 아무도 갱신하지 않는 값이라 조용한 기능 상실을 만들고 있었다. 실측된 라이브 영향: papa는 meteorologist:false → "오늘 서울 미세먼지 어때?"에 weather_kma / weather_openmeteo 만 실렸다. weather_kma엔 대기질 데이터가 아예 없으므로(모델 프로필에도 명시돼 있음) 답은 실패 아니면 창작 둘 중 하나였다. 바로 옆에 있던 weather_airkorea(에어코리아, 키 승인·정상)가 죽은 플래그에 막혀 있었음. - meteorologist/lawyer/presenter 스킬 조건 제거 → 키워드 게이트만 사용 - ToolScopeInput.isSkillEnabledForUser는 유지(앱세션 게이트용, 스킬이 실제 프롬프트 파일을 갖게 되면 다시 쓸 자리) - 검증: papa·jasmine 모두 미세먼지/발표자료/판례 도구 정상 노출 tests 463 통과. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XZX9JHFSLZRuK9rR4sCVBs
This commit is contained in:
@@ -68,7 +68,6 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
const topicText = String(input.recentUserText || input.message || '');
|
||||
const sessionId = String(input.sessionId || '');
|
||||
const userWorkspace = input.userWorkspace;
|
||||
const isSkillEnabledForUser = input.isSkillEnabledForUser;
|
||||
|
||||
// Weather comes in two tiers (2026-07-29). "내일 날씨" is an everyday question, not a
|
||||
// specialist one, but all nine weather tools sat behind the meteorologist skill — so with the
|
||||
@@ -77,9 +76,9 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
// weather_openmeteo = everywhere else, both with precipitation chance) for 446 tokens, and
|
||||
// only when a weather keyword is present. They need no skill toggle.
|
||||
const basicWeatherToolNames = new Set(['weather_kma', 'weather_openmeteo']);
|
||||
// The rest stay behind the skill: reanalysis/climate archives (ERA5, CDS, CMIP6, NASA POWER)
|
||||
// are genuinely specialist, air quality is a separate concern, and weather_search is a
|
||||
// redundant fallback whose schema costs as much as openmeteo while carrying less data.
|
||||
// Air quality is a separate concern from the forecast, and weather_search is a redundant
|
||||
// fallback whose schema costs as much as openmeteo while carrying less data — so they load
|
||||
// on the weather keyword but are not part of the everyday pair above.
|
||||
const weatherToolNames = new Set(['weather_search', 'weather_airpollution', 'weather_airkorea']);
|
||||
// 2026-09-02 (efficiency audit): these four are climate-reanalysis/projection archives, not
|
||||
// "what's the weather" tools — ERA5 and CDS are Copernicus reanalysis, CMIP6 is scenario
|
||||
@@ -146,9 +145,18 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
// coverage). Reuses prompt-gates.ts's DISASTER_PATTERN rather than keeping a second copy of
|
||||
// the same keyword list here, per this file's own header note on gates needing to agree.
|
||||
const hasDisasterKeyword = DISASTER_PATTERN.test(topicText);
|
||||
const meteorologistEnabled = isSkillEnabledForUser('meteorologist', userWorkspace);
|
||||
const lawyerEnabled = isSkillEnabledForUser('lawyer', userWorkspace);
|
||||
const presenterEnabled = isSkillEnabledForUser('presenter', userWorkspace);
|
||||
// 2026-09-02: the per-user skills_state.json toggles are legacy — the UI moved to per-app
|
||||
// gating and nobody maintains these flags any more. They were still the only load-bearing
|
||||
// consumer of that state, and stale values were silently WITHHOLDING tools: papa has
|
||||
// meteorologist:false, so "오늘 서울 미세먼지 어때?" got weather_kma/openmeteo only —
|
||||
// and weather_kma carries no air-quality data at all, so the answer could only be a
|
||||
// failure or an invention. weather_airkorea (에어코리아, key approved and working) was
|
||||
// sitting right there, gated off by a flag from a UI the user no longer uses.
|
||||
//
|
||||
// The keyword gates below are the real cost control and they are already narrow. A toggle
|
||||
// that is never updated is not a safety mechanism, it is a trap — so the skill condition is
|
||||
// dropped and the keyword alone decides. `isSkillEnabledForUser` stays on ToolScopeInput for
|
||||
// the app-session gates and for whenever skills get real prompt files again.
|
||||
const hasPptxKeyword = /슬라이드|발표|pptx|ppt\b|프레젠테이션|피피티|덱|presentation/i.test(message);
|
||||
// 2026-08-25: 논문 도구는 pptx 키워드에만 묶여 있어서, "논문 찾아줘"로는 아예 안 켜지고
|
||||
// 모델이 web_search로 때웠다(실사용 로그에서 확인). 학술 검색은 web_search보다 확실히
|
||||
@@ -244,8 +252,8 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
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 (basicWeatherToolNames.has(name) && !isWeatherAppSession && !hasWeatherKeyword) return false;
|
||||
if (weatherToolNames.has(name) && !isWeatherAppSession && !(meteorologistEnabled && hasWeatherKeyword)) return false;
|
||||
if (climateToolNames.has(name) && !isWeatherAppSession && !(meteorologistEnabled && hasClimateKeyword)) return false;
|
||||
if (weatherToolNames.has(name) && !isWeatherAppSession && !hasWeatherKeyword) return false;
|
||||
if (climateToolNames.has(name) && !isWeatherAppSession && !hasClimateKeyword) return false;
|
||||
// Windy map screenshots only make sense inside the dedicated weather app tab (the
|
||||
// model there has already generated the embed.windy.com URL to capture) — unlike the
|
||||
// other weather tools, not offered via the meteorologist skill in general chat.
|
||||
@@ -261,7 +269,7 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
if (name === 'weather_map_screenshot' && !isWeatherAppSession && !hasSatelliteKeyword) return false;
|
||||
if (name === 'nhc_active_storms' && !isWeatherAppSession) return false;
|
||||
if (ollamaWebToolNames.has(name) && !hasOllamaWebKeyword) return false;
|
||||
if (legalToolNames.has(name) && !isLawyerAppSession && !(lawyerEnabled && hasLegalKeyword)) return false;
|
||||
if (legalToolNames.has(name) && !isLawyerAppSession && !hasLegalKeyword) return false;
|
||||
if ((name === 'excel_read' || name === 'excel_write') && !isAccountantAppSession && !hasExcelKeyword) return false;
|
||||
if (coderToolNames.has(name) && !hasCodeKeyword) return false;
|
||||
if (name === 'shell' && !hasCodeKeyword) return false;
|
||||
@@ -274,7 +282,7 @@ export function buildSkillToolFilter(input: ToolScopeInput): (toolName: string)
|
||||
// ~1,600 tokens of presentation API surface (create_presentation alone is 833, the largest
|
||||
// tool schema in the codebase) on every turn including plain greetings.
|
||||
// Now matches the weather/legal shape directly above: exclude unless skill AND keyword agree.
|
||||
if (pptxToolNames.has(name) && !(presenterEnabled && hasPptxKeyword)) return false;
|
||||
if (pptxToolNames.has(name) && !hasPptxKeyword) return false;
|
||||
if (academicToolNames.has(name) && !hasPptxKeyword && !hasAcademicKeyword) return false;
|
||||
if (name === 'cms_hospital_compare' && !hasHospitalDataKeyword) return false;
|
||||
if (emailToolNames.has(name) && !hasEmailKeyword) return false;
|
||||
|
||||
@@ -111,13 +111,18 @@ describe('키워드 게이트 — 필요할 때 확실히 나타난다', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('전문 날씨 도구는 여전히 스킬을 요구한다', () => {
|
||||
// 재분석·기후 아카이브(ERA5/CMIP6/CDS/NASA POWER)와 대기질은 일상 질문이 아니다.
|
||||
const f = filterFor('내일 날씨 어때');
|
||||
for (const n of ['weather_era5', 'weather_cmip6', 'weather_cds', 'weather_nasa_power', 'weather_airkorea', 'weather_search']) {
|
||||
assert.equal(f(n), false, `${n} 이(가) 스킬 없이 노출됨`);
|
||||
// [2026-09-02] 스킬 토글은 레거시가 됐다 — UI가 앱별 게이팅으로 넘어가면서 아무도
|
||||
// skills_state.json을 갱신하지 않는데, 이 토글이 도구를 **막는** 유일한 소비자로 남아
|
||||
// 있었다. papa의 meteorologist:false 때문에 "오늘 미세먼지 어때?"가 weather_kma/openmeteo
|
||||
// 만 받았고, weather_kma엔 대기질 데이터가 아예 없어서 실패 아니면 창작밖에 안 나왔다.
|
||||
// 이제 키워드만으로 결정한다.
|
||||
test('대기질 도구는 스킬 토글과 무관하게 날씨 키워드로 열린다', () => {
|
||||
const f = filterFor('오늘 서울 미세먼지 어때?');
|
||||
for (const n of ['weather_airkorea', 'weather_airpollution', 'weather_kma', 'weather_openmeteo']) {
|
||||
assert.equal(f(n), true, `${n} 이(가) 레거시 토글 때문에 막힘`);
|
||||
}
|
||||
assert.equal(buildSkillToolFilter({ ...allSkills, message: '내일 날씨 어때' })('weather_airkorea'), true);
|
||||
// 기후 아카이브는 여전히 별도 키워드를 요구한다
|
||||
assert.equal(f('weather_era5'), false);
|
||||
});
|
||||
|
||||
// [2026-09-02 효율 감사] ERA5/CDS/CMIP6/NASA POWER는 "오늘 날씨"가 아니라 기후 재분석·시나리오
|
||||
|
||||
Reference in New Issue
Block a user