diff --git a/PATCHNOTES.md b/PATCHNOTES.md index ae1e4c3..9cb9cfd 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,23 @@ # Astra Patch Notes +## v2.2.312 (2026-07-20) +### ๐Ÿ“‹ ๋ณด๊ณ  ํ’ˆ์งˆ โ€” "## 4.๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๋ณด๊ณ ์„œ" ๋ฒ„๊ทธ + ์ผ๋ฐ˜๋ก  ์ดํ‰ ์ฐจ๋‹จ +์‹ค์‚ฌ๋ก€: "connectai ํ”„๋กœ์ ํŠธ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ๋ณด๊ณ ํ•ด์ค˜" โ†’ ๋‹ต๋ณ€์ด "## 4. ์ข…ํ•ฉ ๋ถ„์„ ๊ฒฐ๊ณผ"๋ถ€ํ„ฐ ์‹œ์ž‘(1~3 ์ฆ๋ฐœ), ๋‚ด์šฉ์€ ํ”„๋กœ์ ํŠธ๋ช…์„ ๋ฐ”๊ฟ”๋„ ์„ฑ๋ฆฝํ•˜๋Š” 7์ค„ ์ผ๋ฐ˜๋ก . +- **์ค‘๊ฐ„ ๋ผ์šด๋“œ ๋ณธ๋ฌธ ํ‘œ์‹œ ๋ฒ„๊ทธ ์ˆ˜์ • (๊ทผ๋ณธ ์›์ธ)**: ์•ก์…˜ ๋ฃจํ”„์—์„œ ์•ก์…˜๊ณผ *ํ•จ๊ป˜* ๋ณธ๋ฌธ ์„น์…˜์„ ์“ด ๋ผ์šด๋“œ๋Š” ํ™”๋ฉด์— ํ•œ ๋ฒˆ๋„ ํ‘œ์‹œ๋˜์ง€ ์•Š๊ณ (๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ์€ depth 0 ์ „์šฉ) ์ตœ์ข… ๋ผ์šด๋“œ๋งŒ append ๋๋‹ค. ๋ชจ๋ธ์€ ํžˆ์Šคํ† ๋ฆฌ์—์„œ ์ž๊ธฐ ์ด์ „ ์„น์…˜(1~3)์„ ๋ณด๊ณ  "## 4."๋ถ€ํ„ฐ ์ด์–ด ์จ์„œ ์‚ฌ์šฉ์ž์—๊ฒ 4๋ฒˆ๋ถ€ํ„ฐ ๋„์ฐฉ. ์ด์ œ ์ค‘๊ฐ„ ๋ผ์šด๋“œ์˜ ๊ฐ€์‹œ ๋ณธ๋ฌธ์„ ์•ก์…˜ ํƒœ๊ทธ๋งŒ ๊ฑท์–ด๋‚ด๊ณ (์‹ ๊ทœ stripForDisplay.ts) ๋ฒ„๋ธ”์— ์ˆœ์„œ๋Œ€๋กœ ํ‘œ์‹œํ•œ๋‹ค. +- **๋ถ„์„ ๋ณด๊ณ  ์—…๋ฌด ์œ ํ˜• ์‹ ์„ค (requirementGraph)**: "๋ถ„์„ํ•˜๊ณ  ๋ณด๊ณ ํ•ด์ค˜/ํ˜„ํ™ฉ ๋ณด๊ณ /๊ฒ€ํ†  ๋ณด๊ณ "๊ฐ€ ์–ด๋–ค ์œ ํ˜•์—๋„ ์•ˆ ๊ฑธ๋ฆฌ๋˜ ๊ฐญ์„ ๋ง‰์Œ. ํ•„์ˆ˜ ์š”์†Œ: ๋ณด๊ณ  ๊ฐœ์š”(์ฒซ ์ค„์— ๋Œ€์ƒโ€”๊ทผ๊ฑฐโ€”๊ฒฐ๋ก  ์ž๊ธฐ์„ ์–ธ), **ํŒŒ์ผ ๊ทผ๊ฑฐ**(์ฃผ์žฅ๋งˆ๋‹ค ์‹ค์ œ ์ฝ์€ ํŒŒ์ผ ์ธ์šฉ, ์ผ๋ฐ˜๋ก  ๊ธˆ์ง€), ๊ตฌ์กฐยทํ˜„ํ™ฉ, ํ•ต์‹ฌ ๋ฐœ๊ฒฌ, ๋‹ค์Œ ๋‹จ๊ณ„. ์œ ํ˜• ๊ฐ์ง€ ์‹œ "์ตœ๋Œ€ 3์„น์…˜" ์ผ๋ฐ˜ ๊ทœ์น™๋ณด๋‹ค ํ•„์ˆ˜ ์š”์†Œ ์ปค๋ฒ„ ์šฐ์„  ๋ช…์‹œ(๋ณด๊ณ ์„œ๊ฐ€ ์–•์•„์ง€๋Š” ์ƒ์ถฉ ์ œ๊ฑฐ). +- **๊ทผ๊ฑฐ ์—†๋Š” ๋ถ„์„ ๊ฐ์ง€ (Hollow ์˜ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ)**: ํŒŒ์ผ์„ ์ฝ๊ณ ๋„(read/investigate โ‰ฅ1) ๋‹ต๋ณ€์— ํŒŒ์ผ ์ธ์šฉ์ด 2๊ฐœ ๋ฏธ๋งŒ์ธ ์žฅ๋ฌธ ๋ถ„์„์ด๋ฉด "๊ทผ๊ฑฐ ์ธ์šฉ ์—†์Œ" footer ๋กœ ๊ฒฝ๊ณ  โ€” ๊ทธ๋Ÿด๋“ฏํ•œ ์ดํ‰์ด ๊ฒ€์ฆ๋œ ๋ถ„์„์ฒ˜๋Ÿผ ํ†ต๊ณผํ•˜๋Š” ๊ฒƒ์„ ์ฐจ๋‹จ. +๊ฒ€์ฆ: tsc ๋ฌด์˜ค๋ฅ˜ + jest 954 ํ†ต๊ณผ(์‹ ๊ทœ 13: analysis-report 3 + ungrounded 5 + stripForDisplay 5) + esbuild ์ •์ƒ. + +## v2.2.311 (2026-07-20) +### โšก ์‘๋‹ต ์ง€์—ฐ ๊ทผ๋ณธ ๊ฐœ์„  โ€” KV ์บ์‹œ ์นœํ™” ํ”„๋กฌํ”„ํŠธ + ์ œ2๋‡Œ ์ƒ์ฃผ ์บ์‹œ +์‹ค์ธก(usage.jsonl): ์ถœ๋ ฅ 22ํ† ํฐ ๋‹ต๋ณ€์— 94.7์ดˆ โ€” ์ง€์—ฐ์˜ 90%+ ๊ฐ€ ์ƒ์„ฑ์ด ์•„๋‹ˆ๋ผ **๋งค ํ„ด 13k+ ํ† ํฐ ์ „์ฒด ์žฌํ”„๋ฆฌํ•„**์ด์—ˆ๋‹ค. ์›์ธ์€ (a) ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ์ƒ๋‹จ์˜ ๋ถ„ ๋‹จ์œ„ ์‹œ๊ฐ์ด llama.cpp prompt cache ๋ฅผ ์ฒซ ํ† ํฐ๋ถ€ํ„ฐ ๋ฌดํšจํ™”, (b) ํ„ด๋งˆ๋‹ค ๋ฐ”๋€Œ๋Š” RAG/๋™์  ๋ธ”๋ก์ด ์ „๋ถ€ message[0] ์— ์žˆ์–ด ํžˆ์Šคํ† ๋ฆฌ ์บ์‹œ๊นŒ์ง€ ์—ฐ์‡„ ๋ฌดํšจํ™”, (c) 32K ์ฐฝ์—์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—๋งŒ 8k ํ† ํฐ ์˜ˆ์‚ฐ, (d) continuation ๋งˆ๋‹ค ๋นˆ ์ฟผ๋ฆฌ๋กœ ์žฌ๊ฒ€์ƒ‰(3~8์ดˆ), (e) ๋งค ์งˆ์˜ 6,500 ํŒŒ์ผ ๋””๋ ‰ํ„ฐ๋ฆฌ ์›Œํฌ + statSync. +- **KV ์บ์‹œ ๋ถ„๋ฆฌ (`kvCachePromptSplit`, ๊ธฐ๋ณธ ON)**: message[0] ์„ ๋ถˆ๋ณ€ ์ •์  ๋ณธ๋ฌธ(ํŽ˜๋ฅด์†Œ๋‚˜ยท๊ทœ์น™ยทํ™˜๊ฒฝ)์œผ๋กœ ๊ณ ์ •ํ•˜๊ณ , ๋‚ ์งœ/RAG/[CONTEXT]/๋™์  ๋ธ”๋ก ์ „๋ถ€๋ฅผ *๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ ์ง์ „*์˜ internal system ๋ฉ”์‹œ์ง€๋กœ ์ด๋™. ํ”„๋ฆฌํ”ฝ์Šค(์ •์  ํ”„๋กฌํ”„ํŠธ+๊ณผ๊ฑฐ ํžˆ์Šคํ† ๋ฆฌ)๊ฐ€ ์บ์‹œ์—์„œ ์žฌ์‚ฌ์šฉ๋˜์–ด ์žฌํ”„๋ฆฌํ•„์ด "์ง์ „ ๊ตํ™˜ + ๋™์  ์ปจํ…์ŠคํŠธ"๋กœ ์ค„์–ด๋“ ๋‹ค. [CONTEXT] truncation ๋„ tail ์— ์ ์šฉ. ์ปค์Šคํ…€ systemPrompt ํ˜ธ์ถœ์ž(๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ๋“ฑ)๋Š” ์ข…์ „ ๊ฒฝ๋กœ ์œ ์ง€. +- **๊ฒ€์ƒ‰ ํ† ํฐ ์˜ˆ์‚ฐ ํ˜„์‹คํ™” (`retrievalTokenBudget`, 0=์ž๋™)**: ์ž๋™๊ฐ’์„ ์ฐฝ์˜ 25%(8k~80k) โ†’ 12%(2.5k~6k ํด๋žจํ”„)๋กœ. ๋งค ํ„ด ์žฌํ”„๋ฆฌํ•„๋˜๋Š” tail ์„ ์ง์ ‘ ์–‡๊ฒŒ ๋งŒ๋“ ๋‹ค. +- **continuation ์€ depth-0 memoryCtx ์žฌ์‚ฌ์šฉ**: ์•ก์…˜ ๋ผ์šด๋“œ๋งˆ๋‹ค์˜ ์žฌ๊ฒ€์ƒ‰ ์ œ๊ฑฐ โ€” ๋ผ์šด๋“œ๋‹น 3~8์ดˆ ์ ˆ๊ฐ + ๋นˆ ์ฟผ๋ฆฌ ์žฌ๊ฒ€์ƒ‰์œผ๋กœ ์ฒญํฌ๊ฐ€ ๊ฐˆ๋ฆฌ๋˜ ๋ฌธ์ œ ์ œ๊ฑฐ + ํ„ด ๋‚ด ํ”„๋กฌํ”„ํŠธ ์•ˆ์ •ํ™”. +- **์ œ2๋‡Œ ์ƒ์ฃผ ์บ์‹œ (์‹ ๊ทœ brainWatch.ts)**: ๋ธŒ๋ ˆ์ธ ๋ฃจํŠธ์— ์žฌ๊ท€ fs.watch ๋ฅผ ๊ฑธ๊ณ  ์„ธ๋Œ€ ์นด์šดํ„ฐ๋กœ ๋ฌดํšจํ™” โ€” ๋ณ€๊ฒฝ ์—†์œผ๋ฉด ๋””๋ ‰ํ„ฐ๋ฆฌ ์›Œํฌ์™€ ํŒŒ์ผ๋ณ„ statSync(์ˆ˜์ฒœ ํšŒ)๋ฅผ ํ†ต์งธ๋กœ ์ƒ๋žตํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ์˜ ํŒŒ์ผ ๋ชฉ๋กยทํ† ํฐ ์ธ๋ฑ์Šค๋ฅผ ์‹ ๋ขฐํ•œ๋‹ค. "์ œ2๋‡Œ๋ฅผ ํ•ญ์ƒ ๋ฉ”๋ชจ๋ฆฌ์—" ์•„์ด๋””์–ด์˜ ์•ˆ์ „ํ•œ ๊ตฌํ˜„: ์›๋ณธ ๋Œ€์‹  ๊ฒ€์ƒ‰ ํŒŒ์ƒ๋ฌผ์„ ์ƒ์ฃผ์‹œํ‚ค๊ณ  ์›Œ์ฒ˜๊ฐ€ ์ •ํ•ฉ์„ฑ์„ ๋งก๋Š”๋‹ค. ์ˆ˜์ • ์งํ›„ 3์ดˆ ์ฐฝ์€ ์‹ ๋ขฐ ๋ช…๋ถ€์—์„œ ์ œ์™ธ(์ด๋ฒคํŠธ ์ง€์—ฐ ๋ ˆ์ด์Šค ๊ฐ€๋“œ), ์›Œ์ฒ˜ ๋ถˆ๊ฐ€ ํ™˜๊ฒฝ์€ ์ž๋™์œผ๋กœ ์ข…์ „(๋งค๋ฒˆ stat) ํด๋ฐฑ. ํ™œ์„ฑํ™” ์‹œ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ๋ฐ์œผ๋กœ ์ฒซ ์งˆ์˜์˜ ์ธ๋ฑ์Šค ํŒŒ์‹ฑ ๋น„์šฉ๋„ ์„ ๋ถˆ ์ฒ˜๋ฆฌ, ์œ ํœด ํ•ด์ œ 30๋ถ„โ†’2์‹œ๊ฐ„. +- ๊ธฐ๋Œ€ ํšจ๊ณผ: ๋™์ผ ์„ธ์…˜ ๋ฐ˜๋ณต ํ„ด์˜ ํ”„๋ฆฌํ•„์ด 13k โ†’ ์ˆ˜๋ฐฑ~2k ํ† ํฐ ์ˆ˜์ค€. LM Studio ์ชฝ GPU ์˜คํ”„๋กœ๋“œ/Flash Attention ์„ค์ •๊ณผ ๋ณ‘ํ–‰ ๊ถŒ์žฅ. +๊ฒ€์ฆ: tsc ๋ฌด์˜ค๋ฅ˜ + jest 941 ํ†ต๊ณผ(์‹ ๊ทœ 15: split 6 + brainCache 4 + promptSplit 5) + esbuild ์ •์ƒ. + ## v2.2.307 (2026-07-11) ### ๐Ÿ—๏ธ /benchmark ๋ ˆ์ด์•„์›ƒ ๊ณจ๊ฒฉ ๋ถ„์„ + ํ•ฉ์„ฑ ์•ˆ์ •ํ™” koritips ์‹ค์ธก ํ”ผ๋“œ๋ฐฑ ๋ฐ˜์˜: โ‘  ํ•ฉ์„ฑ์ด ํŒŒํŠธ 2 ์—์„œ ํƒ€์ž„์•„์›ƒ("This operation was aborted")์œผ๋กœ ์ฃฝ์œผ๋ฉด์„œ 105์ดˆ ๊ฑธ๋ฆฐ ํŒŒํŠธ 1๊นŒ์ง€ ํ๊ธฐ โ‘ก ๋‚จ์€ ์›์‹œ ์š”์•ฝ์ด ์ƒ‰ยทํฐํŠธ๋ฟ์ด๋ผ "๋ฐฐ๋„ˆ๊ฐ€ ๋กค๋ง์ธ์ง€, ๊ฐ ํŽ˜์ด์ง€๊ฐ€ ์–ด๋–ค ๋ธ”๋ก ์ˆœ์„œ์ธ์ง€" ์•Œ ๊ธธ์ด ์—†์—ˆ์Œ. diff --git a/package.json b/package.json index 6c469bd..dd6f01d 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.310", + "version": "2.2.312", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -1294,6 +1294,16 @@ "type": "string", "default": "", "markdownDescription": "์กฐ์‚ฌํ˜• ์š”์ฒญ('~ํด๋” ์กฐ์‚ฌํ•ด์ค˜') ์ „์šฉ ๋ชจ๋ธ ์˜ค๋ฒ„๋ผ์ด๋“œ. ์˜ˆ: 'claude-code:sonnet'. ์†Œํ˜• ๋กœ์ปฌ ๋ชจ๋ธ์ด ํŒŒ์ผ๋ช…๋งŒ ๋ณด๊ณ  ๋‚ด์šฉ์„ ์ƒ์ƒํ•˜๋Š” ํ—›์กฐ์‚ฌ ๋ฐฉ์ง€์ฑ…์˜ ํ•˜๋‚˜ โ€” ๋น„์šฐ๋ฉด ํ˜„์žฌ ์„ ํƒ ๋ชจ๋ธ ์‚ฌ์šฉ." + }, + "g1nation.kvCachePromptSplit": { + "type": "boolean", + "default": true, + "markdownDescription": "**KV ์บ์‹œ ์นœํ™” ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ** โ€” ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ๋ฅผ '๋ถˆ๋ณ€ ๋ณธ๋ฌธ(message[0])'๊ณผ 'ํ„ด๋ณ„ ๋™์  ์ปจํ…์ŠคํŠธ(๋งˆ์ง€๋ง‰ ์งˆ๋ฌธ ์ง์ „ ์ฃผ์ž…)'๋กœ ๋ถ„๋ฆฌํ•ด LM Studio/llama.cpp ์˜ prompt cache ํ”„๋ฆฌํ”ฝ์Šค ์žฌ์‚ฌ์šฉ์„ ๊ทน๋Œ€ํ™”. ๋งค ํ„ด ์ „์ฒด ์žฌํ”„๋ฆฌํ•„(1๋งŒ+ ํ† ํฐ, ์ˆ˜์‹ญ ์ดˆ)์„ ์ง์ „ ๊ตํ™˜ ๋ถ„๋Ÿ‰์œผ๋กœ ์ค„์ธ๋‹ค. ํŠน์ • ๋ชจ๋ธ ํ…œํ”Œ๋ฆฟ์—์„œ ์‘๋‹ต ํ’ˆ์งˆ ๋ฌธ์ œ๊ฐ€ ๋ณด์ด๋ฉด ๋„์„ธ์š”." + }, + "g1nation.retrievalTokenBudget": { + "type": "number", + "default": 0, + "markdownDescription": "**RAG ๊ฒ€์ƒ‰ ํ† ํฐ ์˜ˆ์‚ฐ ์ƒํ•œ** โ€” ๋‘๋‡Œ/๋ฉ”๋ชจ๋ฆฌ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ํ”„๋กฌํ”„ํŠธ์—์„œ ์ฐจ์ง€ํ•  ์ตœ๋Œ€ ํ† ํฐ. `0 = ์ž๋™`(์ปจํ…์ŠคํŠธ ์ฐฝ์˜ 12%, 2,500~6,000 ํด๋žจํ”„). ๊ฐ’์ด ํด์ˆ˜๋ก ๊ทผ๊ฑฐ๋Š” ํ’๋ถ€ํ•ด์ง€์ง€๋งŒ ๋กœ์ปฌ ๋ชจ๋ธ ํ”„๋ฆฌํ•„์ด ๋А๋ ค์ง„๋‹ค." } } } diff --git a/src/agent.ts b/src/agent.ts index 6e2925f..bb9e354 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -5,6 +5,8 @@ import * as fs from 'fs'; import { findBrainFiles, getSystemPrompt, + getStaticSystemPrompt, + getDateTimeContextBlock, shouldAutoPushBrain, buildApiUrl, getActiveBrainProfile, @@ -322,6 +324,14 @@ export class AgentExecutor { actionStats: { reads: number; lists: number; investigates: number }; /** [v2.2.309] ์กฐ์‚ฌ ํ„ด ๋ชจ๋ธ ์˜ค๋ฒ„๋ผ์ด๋“œ โ€” depth 0 ์—์„œ ๊ฒฐ์ •, continuation ์—๋„ ์œ ์ง€. */ investigationModelOverride: string | null; + /** + * [v2.2.311] depth 0 ์—์„œ ๋นŒ๋“œํ•œ memoryCtx ๋ฌธ์ž์—ด ์บ์‹œ โ€” continuation depth ๋Š” + * ์žฌ๊ฒ€์ƒ‰ํ•˜์ง€ ์•Š๊ณ  ์ด๊ฑธ ์žฌ์‚ฌ์šฉํ•œ๋‹ค. ์ข…์ „์—” depth ๋งˆ๋‹ค buildMemoryContext ๋ฅผ ๋‹ค์‹œ + * ๋Œ๋ ธ๋Š”๋ฐ, (a) ๊ฒ€์ƒ‰ 3~8์ดˆ๊ฐ€ ๋ผ์šด๋“œ๋งˆ๋‹ค ์ถ”๊ฐ€๋˜๊ณ  (b) continuation ์˜ prompt ๋Š” + * null ์ด๋ผ *๋นˆ ์ฟผ๋ฆฌ๋กœ* ์žฌ๊ฒ€์ƒ‰ํ•ด ์—‰๋šฑํ•œ ์ฒญํฌ๋กœ ๊ฐˆ์•„๋ผ์šฐ๊ณ  (c) ํ”„๋กฌํ”„ํŠธ๊ฐ€ ํ”๋“ค๋ ค + * KV ์บ์‹œ๋„ ๊นจ์กŒ๋‹ค. actionStats ์™€ ๊ฐ™์€ ์ด์œ ๋กœ depth 0 ์ง„์ž…๋ถ€์—์„œ๋งŒ ์ดˆ๊ธฐํ™”. + */ + memoryCtxCache: string | null; } = { retrieval: null, lessons: [], @@ -331,6 +341,7 @@ export class AgentExecutor { confidenceSignals: null, actionStats: { reads: 0, lists: 0, investigates: 0 }, investigationModelOverride: null, + memoryCtxCache: null, }; /** Per-turn state ์ผ๊ด„ ์ •๋ฆฌ. turn ์‹œ์ž‘/abort/load session ์‹œ ํ˜ธ์ถœ. */ @@ -560,6 +571,7 @@ export class AgentExecutor { // [v2.2.309] turn ์ „์ฒด(๋ชจ๋“  depth) ๋ˆ„์  ์ƒํƒœ โ€” depth 0 ์—์„œ๋งŒ ์ดˆ๊ธฐํ™”. this._turnCtx.actionStats = { reads: 0, lists: 0, investigates: 0 }; this._turnCtx.investigationModelOverride = null; + this._turnCtx.memoryCtxCache = null; } // 1. Prepare Context @@ -755,22 +767,29 @@ export class AgentExecutor { ? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}` : ''; const retrievalStartMs = Date.now(); - const memoryCtx = isCasualConversation - ? '' - : await (async () => { - this.resetTurnContext(); - return buildMemoryContextFn({ - currentPrompt: prompt || '', - activeBrain, - agentSkillFile: options.agentSkillFile, - chatHistory: this.chatHistory, - memoryManager: this.memoryManager, - retrievalOrchestrator: this.retrievalOrchestrator, - context: this.context, - currentTaskId: this.currentTaskId, - turnCtx: this._turnCtx, - }); - })(); + // [v2.2.311] continuation depth ๋Š” depth 0 ์˜ memoryCtx ๋ฅผ ์žฌ์‚ฌ์šฉ โ€” ์žฌ๊ฒ€์ƒ‰ + // 3~8์ดˆ ์ œ๊ฑฐ + ๋นˆ ์ฟผ๋ฆฌ ์žฌ๊ฒ€์ƒ‰์œผ๋กœ ์ฒญํฌ๊ฐ€ ๊ฐˆ๋ฆฌ๋Š” ๋ฌธ์ œ ์ œ๊ฑฐ + ํ”„๋กฌํ”„ํŠธ ์•ˆ์ •ํ™” + // (dynamicBlocks ๋“ฑ turnCtx ํŒŒ์ƒ๋ฌผ๋„ depth 0 ๊ฒƒ์ด ๊ทธ๋Œ€๋กœ ์œ ์ง€๋œ๋‹ค). + let memoryCtx: string; + if (isCasualConversation) { + memoryCtx = ''; + } else if (loopDepth > 0 && this._turnCtx.memoryCtxCache !== null) { + memoryCtx = this._turnCtx.memoryCtxCache; + } else { + this.resetTurnContext(); + memoryCtx = await buildMemoryContextFn({ + currentPrompt: prompt || '', + activeBrain, + agentSkillFile: options.agentSkillFile, + chatHistory: this.chatHistory, + memoryManager: this.memoryManager, + retrievalOrchestrator: this.retrievalOrchestrator, + context: this.context, + currentTaskId: this.currentTaskId, + turnCtx: this._turnCtx, + }); + this._turnCtx.memoryCtxCache = memoryCtx; + } if (loopDepth === 0 && !isCasualConversation && this._turnCtx.retrieval) { recordTelemetry({ kind: 'retrieval', @@ -806,9 +825,19 @@ export class AgentExecutor { ? buildPriorTurnConclusionContext(this.chatHistory) : ''; // System prompt build (agent vs astra mode) โ†’ src/agent/handlePrompt/{buildAgentModeSystemPrompt,buildAstraModeSystemPrompt}.ts - const fullSystemPrompt: string = isAgentMode + // + // [KV ์บ์‹œ ๋ถ„๋ฆฌ v2.2.311] ๊ธฐ๋ณธ ๊ฒฝ๋กœ(ํ˜ธ์ถœ์ž๊ฐ€ systemPrompt ๋ฅผ ๋„˜๊ธฐ์ง€ ์•Š์€ ๊ฒฝ์šฐ)๋Š” + // message[0] ์„ *์ •์  ๋ณธ๋ฌธ๋งŒ* ์œผ๋กœ ๊ณ ์ •ํ•˜๊ณ , ๋‚ ์งœ/RAG/[CONTEXT]/๋™์  ๋ธ”๋ก ์ „๋ถ€๋ฅผ + // dynamicContextTail ๋กœ ๋ถ„๋ฆฌํ•ด computeBudgetedRequest ๊ฐ€ ๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ ์ง์ „์— + // ์‚ฝ์ž…ํ•œ๋‹ค. llama.cpp prompt cache ๊ฐ€ ์ •์  ํ”„๋กฌํ”„ํŠธ+๊ณผ๊ฑฐ ํžˆ์Šคํ† ๋ฆฌ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ฒŒ ๋˜์–ด + // ๋งค ํ„ด ์ „์ฒด ์žฌํ”„๋ฆฌํ•„(์‹ค์ธก 13k ํ† ํฐ โ‰ˆ 90์ดˆ)์ด "์ง์ „ ๊ตํ™˜ + tail" ๋กœ ์ค„์–ด๋“ ๋‹ค. + // ์ปค์Šคํ…€ systemPrompt ํ˜ธ์ถœ์ž(๋ฉ€ํ‹ฐ์—์ด์ „ํŠธ ๋“ฑ)๋Š” ์ข…์ „ ๋‹จ์ผ-์‹œ์Šคํ…œ ๊ฒฝ๋กœ ์œ ์ง€. + const kvSplitEnabled = getConfig().kvCachePromptSplit !== false + && options.systemPrompt === undefined; + const builderBasePrompt = kvSplitEnabled ? '' : systemPrompt; + const builtSystemPrompt: string = isAgentMode ? buildAgentModeSystemPrompt({ - systemPrompt, + systemPrompt: builderBasePrompt, agentSkillContext: options.agentSkillContext || '', modeBridgeCtx, priorConclusionCtx, @@ -824,7 +853,7 @@ export class AgentExecutor { }) : buildAstraModeSystemPrompt({ prompt, - systemPrompt, + systemPrompt: builderBasePrompt, modeBridgeCtx, priorConclusionCtx, designerCtx, @@ -839,6 +868,12 @@ export class AgentExecutor { knowledgeMix: this._turnCtx.knowledgeMix, dynamicBlocks: this._turnCtx.dynamicBlocks, }); + // Split ๋ชจ๋“œ: head = ์ •์  ํ”„๋กฌํ”„ํŠธ(๋ถˆ๋ณ€), tail = ๋‚ ์งœ + ๋นŒ๋” ์‚ฐ์ถœ(๋™์  ์ „๋ถ€). + // Legacy ๋ชจ๋“œ: ์ข…์ „ ๊ทธ๋Œ€๋กœ head ์— ์ „๋ถ€. + const fullSystemPrompt: string = kvSplitEnabled ? getStaticSystemPrompt() : builtSystemPrompt; + const dynamicContextTail: string | undefined = kvSplitEnabled + ? `${getDateTimeContextBlock()}${builtSystemPrompt}` + : undefined; // Context budget computation โ†’ src/agent/handlePrompt/computeBudgetedRequest.ts const imageCount = (reqMessages as any[]) .reduce((n, m) => n + (Array.isArray(m?.images) ? m.images.length : 0), 0); @@ -874,7 +909,7 @@ export class AgentExecutor { const lastUserIdx = reqMessages.map((m) => m.role).lastIndexOf('user'); const lastUser = lastUserIdx >= 0 ? reqMessages[lastUserIdx] : undefined; const content = typeof lastUser?.content === 'string' ? lastUser.content : ''; - const sysTokens = estimateTokens(fullSystemPrompt) + 4; + const sysTokens = estimateTokens(fullSystemPrompt) + (dynamicContextTail ? estimateTokens(dynamicContextTail) : 0) + 4; const mrCfg = { enabled: true, triggerRatio: config.mapReduceTriggerRatio, @@ -934,6 +969,7 @@ export class AgentExecutor { const _budget = computeBudgetedRequest({ fullSystemPrompt, + dynamicContextTail, reqMessages, actualModel, config, @@ -1462,6 +1498,22 @@ export class AgentExecutor { await this.context.workspaceState.update('lastActionStr', currentActionStr); logInfo('Autonomous loop continuing after actions.', { loopDepth: loopDepth + 1, actions: report }); + // [v2.2.312] ์ค‘๊ฐ„ ๋ผ์šด๋“œ ๋ณธ๋ฌธ ํ‘œ์‹œ โ€” ์•ก์…˜๊ณผ *ํ•จ๊ป˜* ์ž‘์„ฑ๋œ ์„น์…˜์ด ํ™”๋ฉด์—์„œ + // ์ฆ๋ฐœํ•˜๋˜ ๋ฒ„๊ทธ ์ˆ˜์ •. ์ข…์ „์—” ์•ก์…˜์ด ์žˆ๋Š” ๋ผ์šด๋“œ๋Š” ์—ฌ๊ธฐ์„œ return ํ•˜๋ฉฐ ๋ณธ๋ฌธ์„ + // ํ•œ ๋ฒˆ๋„ webview ์— ๋ณด๋‚ด์ง€ ์•Š์•˜๊ณ (ํ‘œ์‹œ๋Š” ๋ผ์ด๋ธŒ ์ŠคํŠธ๋ฆฌ๋ฐ๋ฟ โ€” depth 0 ์ „์šฉ), + // ์ตœ์ข… ๋ผ์šด๋“œ๋งŒ streamChunk ๋กœ ๋ถ™์—ˆ๋‹ค. ๊ทธ ๊ฒฐ๊ณผ ๋ชจ๋ธ์ด ํžˆ์Šคํ† ๋ฆฌ์—์„œ ์ž๊ธฐ ์ด์ „ + // ์„น์…˜(1~3)์„ ๋ณด๊ณ  "## 4."๋ถ€ํ„ฐ ์ด์–ด ์จ์„œ, ์‚ฌ์šฉ์ž์—๊ฒŒ๋Š” 4๋ฒˆ๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” + // ๋ณด๊ณ ์„œ๊ฐ€ ๋„์ฐฉํ–ˆ๋‹ค (์‹ค์‚ฌ๋ก€). ๋ผ์ด๋ธŒ๋กœ ์ด๋ฏธ ํ‘œ์‹œ๋œ depth 0 ๋Š” ์ค‘๋ณต ๋ฐฉ์ง€๋กœ ์ œ์™ธ. + if (loopDepth > 0 || !postLiveDeltas) { + try { + const { stripActionTagsForDisplay } = await import('./agent/actions/stripForDisplay'); + const roundVisible = stripActionTagsForDisplay(finalAssistantContent); + if (roundVisible) { + this.webview.postMessage({ type: 'streamChunk', value: `${loopDepth > 0 ? '\n\n' : ''}${roundVisible}` }); + } + } catch { /* ํ‘œ์‹œ ์‹คํŒจ๊ฐ€ ๋ฃจํ”„๋ฅผ ๋ง‰์ง€ ์•Š์Œ */ } + } + // Explicitly tell the AI to look at the results and continue const continuationPrompt = `The requested local action has been executed.\nAction report:\n${report.join('\n')}\nUse the action result messages already in the conversation to answer the user's original request directly, in the user's language. Do not say you are waiting for the next instruction.`; @@ -1554,6 +1606,14 @@ export class AgentExecutor { if (hollowInv.hollow) { this.webview.postMessage({ type: 'streamChunk', value: formatHollowInvestigationFooter(hollowInv.fileMentions) }); logInfo('Hollow Investigation ๊ฐ์ง€ (continuation).', { files: hollowInv.fileMentions, stats: this._turnCtx.actionStats }); + } else { + // [v2.2.312] ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ โ€” ํŒŒ์ผ์„ ์ฝ๊ณ ๋„ ๊ทผ๊ฑฐ ์ธ์šฉ ์—†๋Š” ์ผ๋ฐ˜๋ก  ๋ถ„์„ ๊ฒฝ๊ณ . + const { detectUngroundedAnalysis, formatUngroundedAnalysisFooter } = await import('./intelligence/investigationPipeline'); + const ug = detectUngroundedAnalysis(finalAssistantContent, this._turnCtx.actionStats); + if (ug.ungrounded) { + this.webview.postMessage({ type: 'streamChunk', value: formatUngroundedAnalysisFooter(ug.readCount) }); + logInfo('Ungrounded Analysis ๊ฐ์ง€ (continuation).', { readCount: ug.readCount, fileRefs: ug.fileRefs }); + } } } catch { /* ๊ฐ์ง€ ์‹คํŒจ๊ฐ€ ๋‹ต๋ณ€์„ ๋ง‰์ง€ ์•Š์Œ */ } } diff --git a/src/agent/actions/stripForDisplay.ts b/src/agent/actions/stripForDisplay.ts new file mode 100644 index 0000000..d5e23e0 --- /dev/null +++ b/src/agent/actions/stripForDisplay.ts @@ -0,0 +1,31 @@ +/** + * [v2.2.312] ํ™”๋ฉด ํ‘œ์‹œ์šฉ ์•ก์…˜ ํƒœ๊ทธ ์ œ๊ฑฐ. + * + * ์•ก์…˜ ๋ฃจํ”„์˜ ์ค‘๊ฐ„ ๋ผ์šด๋“œ(์•ก์…˜๊ณผ ํ•จ๊ป˜ ๋ณธ๋ฌธ ์„น์…˜์„ ์“ด ๋ผ์šด๋“œ)๋ฅผ ์‚ฌ์šฉ์ž ๋ฒ„๋ธ”์— + * ํ‘œ์‹œํ•  ๋•Œ, ๊ฐ™์€ ์‹คํ–‰ ํƒœ๊ทธ ์›๋ฌธ์ด ๊ทธ๋Œ€๋กœ ๋…ธ์ถœ๋˜์ง€ ์•Š๋„๋ก ๊ฑท์–ด๋‚ธ๋‹ค. + * ์‹คํ–‰ ์ž์ฒด๋Š” ์ด ํ•จ์ˆ˜์™€ ๋ฌด๊ด€ โ€” executeActions ๋Š” ์›๋ฌธ(cleanedVisible)์„ ๊ทธ๋Œ€๋กœ + * ๋ฐ›๋Š”๋‹ค. ์—ฌ๊ธฐ๋Š” *ํ‘œ์‹œ ์ „์šฉ* ๊ฒฝ๋กœ๋‹ค. + */ + +const ACTION_TAG_NAMES = [ + 'read_file', 'list_files', 'investigate_files', + 'create_file', 'edit_file', 'delete_file', + 'run_command', 'run_code', + 'read_brain', 'list_brain', + 'read_sheet', 'write_sheet', 'append_sheet', + 'fetch_url', 'calculate', + 'create_calendar_event', 'add_task', 'update_task', 'complete_task', + // edit_file ๋‚ด๋ถ€ ์„œ๋ธŒ ํƒœ๊ทธ โ€” ๋ธ”๋ก ์ œ๊ฑฐ๊ฐ€ ์‹คํŒจํ•œ ์ž”์žฌ๊นŒ์ง€ ์ •๋ฆฌ. + 'search', 'replace', +]; + +export function stripActionTagsForDisplay(text: string): string { + let t = text || ''; + for (const name of ACTION_TAG_NAMES) { + // ์ž๊ธฐ๋‹ซํž˜ โ†’ ๋ธ”๋ก ... โ†’ ์ž”์žฌ ์—ฌ๋Š”/๋‹ซ๋Š” ํƒœ๊ทธ ์ˆœ. + t = t.replace(new RegExp(`<${name}\\b[^>]*/>`, 'gi'), ''); + t = t.replace(new RegExp(`<${name}\\b[^>]*>[\\s\\S]*?`, 'gi'), ''); + t = t.replace(new RegExp(`]*>`, 'gi'), ''); + } + return t.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim(); +} diff --git a/src/agent/handlePrompt/computeBudgetedRequest.ts b/src/agent/handlePrompt/computeBudgetedRequest.ts index 641a276..03401ee 100644 --- a/src/agent/handlePrompt/computeBudgetedRequest.ts +++ b/src/agent/handlePrompt/computeBudgetedRequest.ts @@ -13,6 +13,14 @@ import { buildDroppedHistorySummary } from '../../lib/contextBuilders/droppedHis export interface ComputeBudgetedRequestInput { fullSystemPrompt: string; + /** + * [KV ์บ์‹œ ๋ถ„๋ฆฌ v2.2.311] ํ„ด๋งˆ๋‹ค ๋ณ€ํ•˜๋Š” ์ปจํ…์ŠคํŠธ(RAG/[CONTEXT]/๋‚ ์งœ/๋™์  ๋ธ”๋ก)๋ฅผ + * message[0] ์ด ์•„๋‹ˆ๋ผ *๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ ์ง์ „*์˜ internal system ๋ฉ”์‹œ์ง€๋กœ ์‹ฃ๋Š”๋‹ค. + * message[0](fullSystemPrompt)์€ ํ„ด ์‚ฌ์ด์— ๋ถˆ๋ณ€ โ†’ llama.cpp prompt cache ๊ฐ€ + * "์ •์  ํ”„๋กฌํ”„ํŠธ + ๊ณผ๊ฑฐ ํžˆ์Šคํ† ๋ฆฌ" ํ”„๋ฆฌํ”ฝ์Šค๋ฅผ ์žฌ์‚ฌ์šฉํ•˜๊ณ , ๋งค ํ„ด ํ”„๋ฆฌํ•„์€ + * "์ง์ „ ๊ตํ™˜ + ์ด ๋ธ”๋ก"๋งŒ์œผ๋กœ ์ค„์–ด๋“ ๋‹ค. undefined ๋ฉด ์ข…์ „ ๋‹จ์ผ-์‹œ์Šคํ…œ ๋™์ž‘. + */ + dynamicContextTail?: string; /** Caller is expected to have run `capChatHistory` on this already. */ reqMessages: ChatMessage[]; actualModel: string; @@ -126,12 +134,27 @@ export function computeBudgetedRequest(input: ComputeBudgetedRequestInput): Comp 1024, Math.floor((ctxLimits.contextLength - ctxLimits.safetyMargin - preferredOutputReserve - imageTokenReserve) * 0.65) ); - const { prompt: budgetedSystemPrompt, truncated: systemTruncated } = - truncateSystemPromptContext(fullSystemPrompt, systemCapTokens); + // Split ๋ชจ๋“œ๋ฉด [CONTEXT] ๋Š” dynamicContextTail ์ชฝ์— ์žˆ์œผ๋ฏ€๋กœ truncation ๋„ tail ์— ์ ์šฉ. + // (์ •์  head ์—๋Š” [CONTEXT] ๋งˆ์ปค๊ฐ€ ์—†์–ด truncate ๊ฐ€ no-op โ€” head ๋Š” ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค.) + const splitMode = typeof input.dynamicContextTail === 'string' && input.dynamicContextTail.trim().length > 0; + let budgetedSystemPrompt = fullSystemPrompt; + let budgetedTail = splitMode ? input.dynamicContextTail! : ''; + let systemTruncated = false; + if (splitMode) { + const headTokens = estimateTokens(fullSystemPrompt); + const tailCap = Math.max(512, systemCapTokens - headTokens); + const t = truncateSystemPromptContext(budgetedTail, tailCap); + budgetedTail = t.prompt; + systemTruncated = t.truncated; + } else { + const t = truncateSystemPromptContext(fullSystemPrompt, systemCapTokens); + budgetedSystemPrompt = t.prompt; + systemTruncated = t.truncated; + } if (systemTruncated) { logInfo('System prompt context truncated to fit the context window.', { model: actualModel, systemCapTokens }); } - const systemTokens = estimateTokens(budgetedSystemPrompt) + 4; + const systemTokens = estimateTokens(budgetedSystemPrompt) + (splitMode ? estimateTokens(budgetedTail) + 4 : 0) + 4; // (2) ๋Œ€ํ™” ๊ธฐ๋ก ์••์ถ•. const historyBudget = Math.max( @@ -156,9 +179,27 @@ export function computeBudgetedRequest(input: ComputeBudgetedRequestInput): Comp } } + // Split ๋ชจ๋“œ: ๋™์  ์ปจํ…์ŠคํŠธ๋ฅผ ๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ *์ง์ „*์— ์‚ฝ์ž…. ์ด ์œ„์น˜๋ผ์•ผ + // (a) ์ด์ „ ์š”์ฒญ๊ณผ์˜ ๊ณตํ†ต ํ”„๋ฆฌํ”ฝ์Šค(์ •์  ์‹œ์Šคํ…œ + ๊ณผ๊ฑฐ ํžˆ์Šคํ† ๋ฆฌ)๊ฐ€ ์ตœ๋Œ€๋กœ ๋ณด์กด๋˜๊ณ  + // (b) continuation(์•ก์…˜ ๊ฒฐ๊ณผ๊ฐ€ ๋’ค์— ๋ถ™๋Š” ๋ผ์šด๋“œ)์—์„œ๋„ ๊ฐ™์€ ์ž๋ฆฌ๋ผ ํ„ด ๋‚ด ์บ์‹œ๊ฐ€ ์œ ์ง€๋œ๋‹ค. + // user ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์œผ๋ฉด(์ด๋ก ์ƒ) ํžˆ์Šคํ† ๋ฆฌ ๋์— ๋ถ™์ธ๋‹ค โ€” ์ƒ์„ฑ ์ง์ „์ด๋ฏ€๋กœ ์—ฌ์ „ํžˆ ์œ ํšจ. + let historyWithTail = budgetedHistory; + if (splitMode) { + const tailMsg: ChatMessage = { role: 'system', content: budgetedTail, internal: true }; + const lastUserIdx = (() => { + for (let i = budgetedHistory.length - 1; i >= 0; i--) { + if (budgetedHistory[i].role === 'user') return i; + } + return -1; + })(); + historyWithTail = lastUserIdx >= 0 + ? [...budgetedHistory.slice(0, lastUserIdx), tailMsg, ...budgetedHistory.slice(lastUserIdx)] + : [...budgetedHistory, tailMsg]; + } + const messagesForRequest: ChatMessage[] = [ { role: 'system', content: budgetedSystemPrompt, internal: true }, - ...budgetedHistory + ...historyWithTail ]; // (3) ๋™์  ์ถœ๋ ฅ ์ƒํ•œ. diff --git a/src/config.ts b/src/config.ts index 2ef55f3..b7761e2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -233,6 +233,19 @@ export interface IAgentConfig { * ํ• ๋ฃจ์‹œ๋„ค์ด์…˜ ๋Œ€์ฑ…. ๋นˆ ๋ฌธ์ž์—ด์ด๋ฉด ๋น„ํ™œ์„ฑ (ํ˜„์žฌ ์„ ํƒ ๋ชจ๋ธ ๊ทธ๋Œ€๋กœ). */ investigationModel: string; + /** + * [v2.2.311] KV ์บ์‹œ ์นœํ™” ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ โ€” message[0] ์„ ์ •์  ๋ณธ๋ฌธ์œผ๋กœ ๊ณ ์ •ํ•˜๊ณ  + * ๋‚ ์งœ/RAG/[CONTEXT]/๋™์  ๋ธ”๋ก์„ ๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ ์ง์ „์˜ system ๋ฉ”์‹œ์ง€๋กœ ์˜ฎ๊ธด๋‹ค. + * llama.cpp prompt cache ํ”„๋ฆฌํ”ฝ์Šค ์žฌ์‚ฌ์šฉ์œผ๋กœ ํ„ด๋‹น ์žฌํ”„๋ฆฌํ•„์„ ๋Œ€ํญ ์ถ•์†Œ. ๊ธฐ๋ณธ true. + * ํŠน์ • ๋ชจ๋ธ ํ…œํ”Œ๋ฆฟ์—์„œ ๋ฌธ์ œ ์‹œ false ๋กœ ์ข…์ „ ๋‹จ์ผ-์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋ณต๊ท€. + */ + kvCachePromptSplit: boolean; + /** + * [v2.2.311] RAG/๋ฉ”๋ชจ๋ฆฌ ๊ฒ€์ƒ‰ ํ† ํฐ ์˜ˆ์‚ฐ ์ƒํ•œ. 0 = ์ž๋™(์ปจํ…์ŠคํŠธ ์ฐฝ์˜ 12%, + * 2,500~6,000 ํด๋žจํ”„). ์ข…์ „ ์ž๋™๊ฐ’(์ฐฝ์˜ 25%, ์ตœ๋Œ€ 80k)์€ 32k ์ฐฝ์—์„œ 8k ํ† ํฐ์„ + * ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์— ์จ์„œ ๋กœ์ปฌ ํ”„๋ฆฌํ•„๋งŒ ์ˆ˜์‹ญ ์ดˆ๋ฅผ ํƒœ์› ๋‹ค โ€” ์ƒํ•œ์„ ๋‚ฎ์ถฐ ๋‹ต๋ณ€ ์ง€์—ฐ์„ ์ค„์ธ๋‹ค. + */ + retrievalTokenBudget: number; /** Self-check ์ „์šฉ ๋ชจ๋ธ ID. ๋น„๋ฉด defaultModel. ๋น ๋ฅธ ์ž‘์€ ๋ชจ๋ธ ๊ถŒ์žฅ. */ selfCheckModel: string; /** Self-check LLM ํ˜ธ์ถœ ํƒ€์ž„์•„์›ƒ (์ดˆ). ๊ธฐ๋ณธ 6. */ @@ -583,6 +596,8 @@ export function getConfig(): IAgentConfig { intentClarificationStrictness: (cfg.get('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium', citationTraceEnabled: cfg.get('citationTraceEnabled', true), investigationModel: (cfg.get('investigationModel', '') || '').trim(), + kvCachePromptSplit: cfg.get('kvCachePromptSplit', true), + retrievalTokenBudget: Math.max(0, cfg.get('retrievalTokenBudget', 0) || 0), selfCheckEnabled: cfg.get('selfCheckEnabled', false), selfCheckModel: cfg.get('selfCheckModel', '') || '', selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get('selfCheckTimeoutSec', 6))), diff --git a/src/extension.ts b/src/extension.ts index 1117d38..e823dd3 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -86,6 +86,17 @@ export async function activate(context: vscode.ExtensionContext) { // ๋‘๋‡Œ์— ๊ธฐ๋ก. ์ž๊ธฐ ํ‰๊ฐ€๊ฐ€ ๊ตฌ์‹ ์Šค๋ƒ…์ƒท ๋Œ€์‹  ํ•ญ์ƒ ํ˜„ํ–‰ ์†Œ์Šค๋ฅผ ๊ทผ๊ฑฐ๋กœ ํ•˜๊ฒŒ ํ•œ๋‹ค. void ensureFeatureInventory(context); + // [v2.2.311] ์ œ2๋‡Œ ์บ์‹œ ์›Œ๋ฐ โ€” ํ™œ์„ฑํ™” ์งํ›„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ๋กœ ํŒŒ์ผ ๋ชฉ๋ก + ํ† ํฐ ์ธ๋ฑ์Šค + // (์ˆ˜์‹ญ MB JSON)๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์˜ฌ๋ ค, ์„ธ์…˜ ์ฒซ ์งˆ์˜๊ฐ€ ๊ทธ ๋น„์šฉ(์ˆ˜ ์ดˆ)์„ ๋’ค์ง‘์–ด์“ฐ์ง€ + // ์•Š๊ฒŒ ํ•œ๋‹ค. ์›Œ์ฒ˜๊ฐ€ ๊ฐ™์ด ๊ฑธ๋ ค ์ดํ›„ ํ„ด์€ ๋ณ€๊ฒฝ ์—†์œผ๋ฉด ์›Œํฌ/stat ์„ ํ†ต์งธ๋กœ ์ƒ๋žต. + try { + const { getActiveBrainProfile } = require('./utils') as typeof import('./utils'); + const { warmBrainCache } = require('./retrieval/brainWatch') as typeof import('./retrieval/brainWatch'); + const { getBrainTokenIndex } = require('./retrieval/brainIndex') as typeof import('./retrieval/brainIndex'); + const brainPath = getActiveBrainProfile()?.localBrainPath; + if (brainPath) warmBrainCache(brainPath, (files) => { getBrainTokenIndex(brainPath, files); }); + } catch { /* ์›Œ๋ฐ ์‹คํŒจ๋Š” ๋น„์น˜๋ช… โ€” ์ฒซ ์งˆ์˜๊ฐ€ ์ข…์ „ ๊ฒฝ๋กœ๋กœ ์ฒ˜๋ฆฌ */ } + // Initialize Astra Path Resolver (.astra โ†’ ConnectAI/.astra/) initAstraPathResolver(context); diff --git a/src/intelligence/investigationPipeline.ts b/src/intelligence/investigationPipeline.ts index f851d02..885c009 100644 --- a/src/intelligence/investigationPipeline.ts +++ b/src/intelligence/investigationPipeline.ts @@ -226,3 +226,31 @@ export function formatHollowInvestigationFooter(fileMentions: string[]): string '๋‚ด์šฉ ๊ฒ€์ฆ์ด ํ•„์š”ํ•˜๋ฉด ๋‹ค์‹œ ์š”์ฒญํ•ด ์ฃผ์„ธ์š” โ€” ๋‹ค์Œ ํ„ด์—์„œ ``๋กœ ๊ฐ ํŒŒ์ผ์„ ์‹ค์ œ๋กœ ์ฝ๊ณ  ๋‹ตํ•ฉ๋‹ˆ๋‹ค.', ].join('\n'); } + +/** + * [v2.2.312] ํ—›์กฐ์‚ฌ์˜ ๋ฐ˜๋Œ€ ๋ฐฉํ–ฅ ์‹คํŒจ โ€” ํŒŒ์ผ์„ *์ฝ๊ณ ๋„* ๋‹ต๋ณ€์— ํŒŒ์ผ ๊ทผ๊ฑฐ๋ฅผ ํ•˜๋‚˜๋„ + * ์ธ์šฉํ•˜์ง€ ์•Š์€ "์ผ๋ฐ˜๋ก  ๋ถ„์„" ๊ฐ์ง€. ์‹ค์‚ฌ๋ก€: ํ”„๋กœ์ ํŠธ ๋ถ„์„ ๋ณด๊ณ ๊ฐ€ 7์ค„์งœ๋ฆฌ + * ํ”„๋กœ์ ํŠธ๋ช…๋งŒ ๋ฐ”๊ฟ”๋„ ์„ฑ๋ฆฝํ•˜๋Š” ์ดํ‰("~๊ฐ€ ์ธ์ƒ์ ์ž…๋‹ˆ๋‹ค")์œผ๋กœ ๋๋‚จ. + * ์กฐ๊ฑด: read/investigate ๋ฅผ 1ํšŒ ์ด์ƒ ํ–ˆ๊ณ , ๋‹ต๋ณ€์ด ์‹ค์งˆ ๋ถ„๋Ÿ‰(300์ž+)์ธ๋ฐ + * ํŒŒ์ผ ์ฐธ์กฐ๊ฐ€ 2๊ฐœ ๋ฏธ๋งŒ โ†’ ungrounded. (์งง์€ ํ™•์ธํ˜• ์‘๋‹ต์€ ์˜คํƒ ๋ฐฉ์ง€๋กœ ์ œ์™ธ.) + */ +export function detectUngroundedAnalysis( + answer: string, + stats: TurnActionStats | null | undefined, +): { ungrounded: boolean; readCount: number; fileRefs: number } { + const readCount = (stats?.reads ?? 0) + (stats?.investigates ?? 0); + if (!stats || readCount === 0) return { ungrounded: false, readCount, fileRefs: 0 }; + const body = (answer || '').trim(); + if (body.length < 300) return { ungrounded: false, readCount, fileRefs: 0 }; + const refs = new Set(Array.from(body.matchAll(FILE_MENTION_RE), m => m[0])); + return { ungrounded: refs.size < 2, readCount, fileRefs: refs.size }; +} + +export function formatUngroundedAnalysisFooter(readCount: number): string { + return [ + '', + '---', + `โš ๏ธ **๊ทผ๊ฑฐ ์ธ์šฉ ์—†์Œ** โ€” ํŒŒ์ผ์„ ${readCount}ํšŒ ์ฝ๊ณ ๋„ ๋‹ต๋ณ€์— ์‹ค์ œ ํŒŒ์ผยท์ฝ”๋“œ ๊ทผ๊ฑฐ๊ฐ€ ์ธ์šฉ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์œ„ ๋‚ด์šฉ์€ ์ผ๋ฐ˜๋ก ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.`, + '๊ตฌ์ฒด ๊ทผ๊ฑฐ๊ฐ€ ํ•„์š”ํ•˜๋ฉด "์‹ค์ œ ์ฝ์€ ํŒŒ์ผ๋ช…์„ ์ธ์šฉํ•ด์„œ ๋‹ค์‹œ ์ •๋ฆฌํ•ด์ค˜"๋ผ๊ณ  ์š”์ฒญํ•ด ์ฃผ์„ธ์š”.', + ].join('\n'); +} diff --git a/src/intelligence/requirementGraph.ts b/src/intelligence/requirementGraph.ts index f270e04..bf08f2e 100644 --- a/src/intelligence/requirementGraph.ts +++ b/src/intelligence/requirementGraph.ts @@ -154,6 +154,42 @@ export const DEFAULT_TASK_REQUIREMENTS: TaskRequirement[] = [ }, ], }, + { + // [v2.2.312] ์‹ค์‚ฌ๋ก€ ๋Œ€์‘ โ€” "E:\...ํ”„๋กœ์ ํŠธ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ๋˜์–ด ์žˆ๋Š”์ง€ ๋ณด๊ณ ํ•ด์ค˜"๊ฐ€ + // ์–ด๋–ค ์œ ํ˜•์—๋„ ์•ˆ ๊ฑธ๋ ค 7์ค„์งœ๋ฆฌ ์ผ๋ฐ˜๋ก  ์ดํ‰์œผ๋กœ ๋๋‚ฌ๋‹ค. '๋ณด๊ณ ํ•ด์ค˜/ํ˜„ํ™ฉ ๋ณด๊ณ ' ๋ฅ˜๋ฅผ + // ์žก์•„ ํŒŒ์ผ ๊ทผ๊ฑฐ ์ธ์šฉ์„ ํ•„์ˆ˜ ์š”์†Œ๋กœ ๊ฐ•์ œํ•œ๋‹ค. + id: 'analysis-report', + label: '๋ถ„์„ ๋ณด๊ณ ', + detectKeywords: ['๋ถ„์„[^\\n]{0,20}๋ณด๊ณ ', '๋ณด๊ณ ํ•ด\\s*์ค˜?', 'ํ˜„ํ™ฉ\\s*(๋ณด๊ณ |์ •๋ฆฌ)', '์–ด๋–ป๊ฒŒ\\s*๋˜์–ด\\s*์žˆ๋Š”์ง€', '๊ฒ€ํ† [^\\n]{0,10}๋ณด๊ณ '], + coverageCheck: true, + elements: [ + { + id: 'overview', label: '๋ณด๊ณ  ๊ฐœ์š”', + hint: '์ฒซ ์ค„์— "๋ณด๊ณ  ๊ฐœ์š”: (๋ถ„์„ ๋Œ€์ƒ) โ€” (ํ™•์ธํ•œ ๊ทผ๊ฑฐ: ์‹ค์ œ ์ฝ์€ ํŒŒ์ผ ์ˆ˜) โ€” (ํ•œ ์ค„ ๊ฒฐ๋ก )" ํ˜•์‹์œผ๋กœ ์‹œ์ž‘. ์š”์ฒญ์ž๊ฐ€ ์ฒซ ์ค„๋งŒ ์ฝ์–ด๋„ ๋ฌด์—‡์„ ์–ด๋–ค ๊ทผ๊ฑฐ๋กœ ํŒ๋‹จํ–ˆ๋Š”์ง€ ์•Œ ์ˆ˜ ์žˆ๊ฒŒ.', + detectPatterns: ['๋ณด๊ณ  ?๊ฐœ์š”', '๋ถ„์„ ?๋Œ€์ƒ', '๊ฐœ์š”'], + }, + { + id: 'file-evidence', label: 'ํŒŒ์ผ ๊ทผ๊ฑฐ', + hint: '๋ชจ๋“  ์ฃผ์š” ์ฃผ์žฅ์— ์ด๋ฒˆ ๋Œ€ํ™”์—์„œ ์‹ค์ œ๋กœ ์ฝ์€ ํŒŒ์ผ๋ช…ยท๊ฒฝ๋กœ๋ฅผ ์ธ์šฉ. ํ”„๋กœ์ ํŠธ ์ด๋ฆ„์„ ๋ฐ”๊ฟ”๋„ ์„ฑ๋ฆฝํ•˜๋Š” ์ผ๋ฐ˜๋ก ("~๊ฐ€ ์ธ์ƒ์ ์ž…๋‹ˆ๋‹ค", "~ํ•  ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค")์€ ๊ธˆ์ง€ โ€” ๊ทผ๊ฑฐ ์—†๋Š” ๋ฌธ์žฅ์€ ์“ฐ์ง€ ๋ง ๊ฒƒ.', + detectPatterns: ['\\.(ts|tsx|js|mjs|cjs|py|md|json|html|css|java|cs)\\b', 'src[\\\\/]', 'package\\.json'], + }, + { + id: 'structure', label: '๊ตฌ์กฐยทํ˜„ํ™ฉ', + hint: 'ํ”„๋กœ์ ํŠธ/๋Œ€์ƒ์˜ ์‹ค์ œ ๊ตฌ์„ฑ โ€” ํ•ต์‹ฌ ๋ชจ๋“ˆ, ์ง„ํ–‰ ์ƒํƒœ, ํ™•์ธ๋œ ์‚ฌ์‹ค. STATUS.md ๋“ฑ ํ˜„์žฌ ์ƒํƒœ ๊ธฐ๋ก์ด ์žˆ์œผ๋ฉด ๊ทธ๊ฒƒ์„ ๊ธฐ์ค€์œผ๋กœ.', + detectPatterns: ['๊ตฌ์กฐ', '๊ตฌ์„ฑ', '๋ชจ๋“ˆ', '์•„ํ‚คํ…์ฒ˜', 'ํ˜„ํ™ฉ', '์ƒํƒœ'], + }, + { + id: 'findings', label: 'ํ•ต์‹ฌ ๋ฐœ๊ฒฌยท์ด์Šˆ', + hint: '๋ถ„์„์—์„œ ๋“œ๋Ÿฌ๋‚œ ๊ฐ•์ ยท๋ฌธ์ œยท์ด์Šˆ. ๊ฐ ํ•ญ๋ชฉ์— ๊ทผ๊ฑฐ ํŒŒ์ผ ์—ฐ๊ฒฐ.', + detectPatterns: ['๋ฐœ๊ฒฌ', '๊ฐ•์ ', '๋ฌธ์ œ', '์ด์Šˆ', '๋ฆฌ์Šคํฌ', '๊ณผ์ œ'], + }, + { + id: 'next-steps', label: '๋‹ค์Œ ๋‹จ๊ณ„', + hint: '์ด ๋ณด๊ณ ๋ฅผ ๋ฐ›์€ ์‚ฌ๋žŒ์ด ์ทจํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ์ฒด์  ๋‹ค์Œ ํ–‰๋™.', + detectPatterns: ['๋‹ค์Œ ?๋‹จ๊ณ„', '๊ถŒ์žฅ', '์ œ์•ˆ', 'ํ›„์†'], + }, + ], + }, { id: 'work-research', label: '์—…๋ฌด์กฐ์‚ฌ', @@ -233,6 +269,9 @@ export function buildRequirementGraphBlock( } lines.push(''); lines.push('์ œ์ถœ ์ „ ์œ„ ์ฒดํฌ๋ฆฌ์ŠคํŠธ๋ฅผ ์Šค์Šค๋กœ ์ ๊ฒ€ํ•˜๊ณ , ๋ˆ„๋ฝ ์š”์†Œ๊ฐ€ ์žˆ์œผ๋ฉด ๋ณด์™„ ํ›„ ๋‹ต๋ณ€ํ•  ๊ฒƒ.'); + // [v2.2.312] ์—…๋ฌด ์œ ํ˜•์ด ๊ฐ์ง€๋œ ๋‹ต๋ณ€์€ ์ผ๋ฐ˜ "์ตœ๋Œ€ 3์„น์…˜" ๊ทœ์น™๋ณด๋‹ค ํ•„์ˆ˜ ์š”์†Œ ์ปค๋ฒ„๊ฐ€ + // ์šฐ์„  โ€” ๋ณด๊ณ ์„œ๊ฐ€ 3์„น์…˜ ์ œํ•œ ๋•Œ๋ฌธ์— ์–•์•„์ง€๋Š” ์ƒ์ถฉ ์ œ๊ฑฐ. + lines.push('์ด ์—…๋ฌด ์œ ํ˜•์ด ๊ฐ์ง€๋œ ๋‹ต๋ณ€์—์„œ๋Š” "์ตœ๋Œ€ 3์„น์…˜" ์ผ๋ฐ˜ ๊ทœ์น™๋ณด๋‹ค ํ•„์ˆ˜ ์š”์†Œ ์ปค๋ฒ„๊ฐ€ ์šฐ์„ ํ•œ๋‹ค โ€” ์š”์†Œ๋ณ„๋กœ ์„น์…˜์„ ๋‚˜๋ˆ ๋„ ๋œ๋‹ค.'); lines.push('[/TASK REQUIREMENTS]'); return lines.join('\n'); } diff --git a/src/lib/contextBuilders/memoryContext.ts b/src/lib/contextBuilders/memoryContext.ts index bcf5cd3..7c1c43f 100644 --- a/src/lib/contextBuilders/memoryContext.ts +++ b/src/lib/contextBuilders/memoryContext.ts @@ -166,11 +166,13 @@ export async function buildMemoryContext(deps: MemoryContextDeps): Promise 0) effectiveScopeFolders = domainScope.folders; } - // Context ์œˆ๋„์šฐ ๋น„๋ก€ retrieval ์˜ˆ์‚ฐ. 32K โ†’ 8K, 230K โ†’ 57K, 80K cap (scoring ์†๋„). - const scaledTotalBudget = Math.min( - 80000, - Math.max(8000, Math.floor(config.contextLength * 0.25)), - ); + // Context ์œˆ๋„์šฐ ๋น„๋ก€ retrieval ์˜ˆ์‚ฐ. + // [v2.2.311] ์ข…์ „ ์ž๋™๊ฐ’(์ฐฝ์˜ 25%, 8k~80k)์€ 32K ์ฐฝ์—์„œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ์—๋งŒ 8k ํ† ํฐ์„ ํƒœ์›Œ + // ๋กœ์ปฌ ํ”„๋ฆฌํ•„ ์ˆ˜์‹ญ ์ดˆ์˜ ์ฃผ๋ฒ”์ด์—ˆ๋‹ค. ์ž๋™๊ฐ’์„ ์ฐฝ์˜ 12%(2.5k~6k ํด๋žจํ”„)๋กœ ๋‚ฎ์ถ”๊ณ , + // ๋” ํ’๋ถ€ํ•œ ๊ทผ๊ฑฐ๊ฐ€ ํ•„์š”ํ•˜๋ฉด g1nation.retrievalTokenBudget ์œผ๋กœ ๋ช…์‹œ ์ƒํ–ฅ. + const scaledTotalBudget = config.retrievalTokenBudget > 0 + ? config.retrievalTokenBudget + : Math.min(6000, Math.max(2500, Math.floor(config.contextLength * 0.12))); // medium-term layer ์šฉ ์˜› ์„ธ์…˜ ํ›„๋ณด. sidebar ๊ฐ€ ์ง์ ‘ ์“ฐ๋Š” key ๋ฅผ read-through. const rawSessions = deps.context.globalState.get('chat_sessions', []) || []; diff --git a/src/retrieval/brainIndex.ts b/src/retrieval/brainIndex.ts index 0522281..0e7dfb6 100644 --- a/src/retrieval/brainIndex.ts +++ b/src/retrieval/brainIndex.ts @@ -17,6 +17,7 @@ import { tokenize, countConflictIndicators } from './scoring'; import { detectLessonKind } from './lessonHelpers'; import { splitIntoSections } from './chunker'; import { logInfo } from '../utils'; +import { ensureBrainWatcher, getBrainGeneration } from './brainWatch'; // v5 adds optional per-file `chunks` (section-level index, Phase 1-๊ฐ€) alongside the // v4 per-file `embedding`. Older indexes are auto-rebuilt on first load โ€” no migration @@ -109,6 +110,16 @@ interface BrainState { diskPath: string | null; // null if we can't determine a writable path writeTimer?: ReturnType; evictTimer?: ReturnType; + /** + * [v2.2.311] statSync ์ƒ๋žต ์ตœ์ ํ™”์˜ ์žฅ๋ถ€. statValidatedGen ์„ธ๋Œ€์—์„œ ์‹ค์ œ๋กœ + * stat(๋˜๋Š” ์žฌ์ƒ‰์ธ)์„ ๊ฑฐ์นœ ํŒŒ์ผ๋งŒ statValidated ์— ๋“ค์–ด๊ฐ„๋‹ค โ€” ์„ธ๋Œ€๊ฐ€ ๊ทธ๋Œ€๋กœ๋ฉด + * ๊ทธ ํŒŒ์ผ๋“ค์€ ์žฌ-stat ์—†์ด ์บ์‹œ๋ฅผ ์‹ ๋ขฐํ•œ๋‹ค. ์„ธ๋Œ€๊ฐ€ ๋ฐ”๋€Œ๋ฉด(ํŒŒ์ผ์‹œ์Šคํ…œ ๋ณ€๊ฒฝ) + * ์ง‘ํ•ฉ์„ ๋น„์›Œ ์ „ ํŒŒ์ผ์ด ๋‹ค์‹œ ํ•œ ๋ฒˆ stat ์„ ๊ฑฐ์นœ๋‹ค. ํŒŒ์ผ *๋‹จ์œ„* ๋กœ ๊ธฐ๋กํ•˜๋Š” + * ์ด์œ : ์Šค์ฝ”ํ”„ ๊ฒ€์ƒ‰(๋ถ€๋ถ„ ๋ชฉ๋ก) ๋’ค ์ „์ฒด ๊ฒ€์ƒ‰์ด ์™€๋„, ๊ฒ€์ฆ ์•ˆ ๋œ ํŒŒ์ผ์ด + * ์‹ ๋ขฐ ๊ฒฝ๋กœ๋กœ ์ƒˆ์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ. + */ + statValidatedGen?: number; + statValidated?: Set; } const _states = new Map(); @@ -117,7 +128,16 @@ const _states = new Map(); // ํ™•์žฅ ํ˜ธ์ŠคํŠธ ๋ฉ”๋ชจ๋ฆฌ์— ์„ธ์…˜ ๋‚ด๋‚ด ์ƒ์ฃผํ–ˆ๋‹ค. ์ธ๋ฑ์Šค๋Š” mtime ํ‚ค ๋””์Šคํฌ ์บ์‹œ์—์„œ ์–ธ์ œ๋“  ์‹ธ๊ฒŒ // ์žฌ๋กœ๋“œ๋˜๋ฏ€๋กœ, ๋งˆ์ง€๋ง‰ ์‚ฌ์šฉ ํ›„ IDLE_EVICT_MS ๊ฐ€ ์ง€๋‚˜๋ฉด dirty ๋ฅผ flush ํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ์—์„œ ๋‚ด๋ฆฐ๋‹ค. // ์ƒ์‹œ ์ธํ„ฐ๋ฒŒ ์—†์ด ์ ‘๊ทผํ•  ๋•Œ๋งˆ๋‹ค ์žฌ์˜ˆ์•ฝ๋˜๋Š” one-shot unref ํƒ€์ด๋จธ๋ผ ๊ทธ ์ž์ฒด์˜ ๋ฐœ์—ด ๋น„์šฉ์€ ์—†๋‹ค. -const IDLE_EVICT_MS = 30 * 60_000; +// [v2.2.311] 30๋ถ„ โ†’ 2์‹œ๊ฐ„: ์ ์‹ฌ/ํšŒ์˜ ๋’ค ์ฒซ ์งˆ์˜๊ฐ€ ๋Œ€ํ˜• ์ธ๋ฑ์Šค(์ˆ˜์‹ญ MB JSON) ์žฌํŒŒ์‹ฑ์„ +// ๋’ค์ง‘์–ด์“ฐ๋Š” ๋นˆ๋„๋ฅผ ์ค„์ธ๋‹ค. ๋ฉ”๋ชจ๋ฆฌ ์ ˆ์•ฝ๋ณด๋‹ค ์‘๋‹ต ์ง€์—ฐ์ด ๋” ์•„ํ”ˆ ๋กœ์ปฌ-ํผ์ŠคํŠธ ์›Œํฌ๋กœ๋“œ. +const IDLE_EVICT_MS = 120 * 60_000; + +/** + * [v2.2.311] stat ์ƒ๋žต ์‹ ๋ขฐ ๋ช…๋ถ€์— ์˜ฌ๋ฆฌ์ง€ ์•Š๋Š” "๊ฐ“ ์ˆ˜์ •๋œ ํŒŒ์ผ" ์ฐฝ. fs.watch ์ด๋ฒคํŠธ๋Š” + * ๋น„๋™๊ธฐ(+๋””๋ฐ”์šด์Šค)๋ผ, ์ˆ˜์ • ์งํ›„ ๊ฐ™์€ ํ‹ฑ์— ๋“ค์–ด์˜จ ์งˆ์˜๋Š” ์„ธ๋Œ€ bump ๋ฅผ ์•„์ง ๋ชป ๋ณธ๋‹ค โ€” + * ๊ทธ ์ฐฝ ์•ˆ์˜ ํŒŒ์ผ์€ ๋ช…๋ถ€์—์„œ ์ œ์™ธํ•ด ๋‹ค์Œ ํ˜ธ์ถœ์—์„œ๋„ stat ์„ ๊ฑฐ์น˜๊ฒŒ ํ•œ๋‹ค. + */ +const RECENT_MTIME_GUARD_MS = 3000; function touchEviction(st: BrainState, brainPath: string): void { if (st.evictTimer) clearTimeout(st.evictTimer); @@ -206,8 +226,35 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB const seen = new Set(); let reindexed = 0; + // [v2.2.311] ์›Œ์ฒ˜ ์„ธ๋Œ€๊ฐ€ ๋งˆ์ง€๋ง‰ stat ๊ฒ€์ฆ ์ดํ›„ ๊ทธ๋Œ€๋กœ๋ฉด โ€” ํŒŒ์ผ์‹œ์Šคํ…œ ๋ณ€๊ฒฝ ์—†์Œ โ€” + // ๊ทธ ์„ธ๋Œ€์—์„œ ์ด๋ฏธ stat ์„ ๊ฑฐ์นœ ํŒŒ์ผ์˜ statSync ๋ฅผ ์ƒ๋žตํ•˜๊ณ  ์บ์‹œ ํ•ญ๋ชฉ์„ ์‹ ๋ขฐ. + // ์›Œ์ฒ˜๊ฐ€ ์—†์œผ๋ฉด(gen === -1) ์ข…์ „๋Œ€๋กœ ๋งค๋ฒˆ stat โ€” ์ •ํ™•์„ฑ ์ €ํ•˜ ์—†์Œ. + ensureBrainWatcher(brainPath); + const watchGen = getBrainGeneration(brainPath); + if (watchGen >= 0 && st.statValidatedGen !== watchGen) { + st.statValidatedGen = watchGen; + st.statValidated = new Set(); + } + const validated = watchGen >= 0 ? (st.statValidated ?? (st.statValidated = new Set())) : null; + for (const file of files) { seen.add(file); + const cachedFast = validated && validated.has(file) ? st.index.entries[file] : undefined; + if (cachedFast + && Array.isArray(cachedFast.tokens) + && Array.isArray(cachedFast.titleTokens)) { + out.push({ + filePath: file, + relativePath: cachedFast.relativePath, + title: cachedFast.title, + tokens: cachedFast.tokens, + titleTokens: cachedFast.titleTokens, + conflictCount: cachedFast.conflictCount || 0, + mtimeMs: cachedFast.mtimeMs, + kind: cachedFast.kind || '', + }); + continue; + } let stat: fs.Stats; try { stat = fs.statSync(file); @@ -220,6 +267,7 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB && cached.size === stat.size && Array.isArray(cached.tokens) && Array.isArray(cached.titleTokens)) { + if (Date.now() - stat.mtimeMs > RECENT_MTIME_GUARD_MS) validated?.add(file); out.push({ filePath: file, relativePath: cached.relativePath, @@ -255,6 +303,7 @@ export function getBrainTokenIndex(brainPath: string, files: string[]): IndexedB st.index.entries[file] = entry; st.dirty = true; reindexed++; + if (Date.now() - entry.mtimeMs > RECENT_MTIME_GUARD_MS) validated?.add(file); out.push({ filePath: file, relativePath, diff --git a/src/retrieval/brainWatch.ts b/src/retrieval/brainWatch.ts new file mode 100644 index 0000000..02d0614 --- /dev/null +++ b/src/retrieval/brainWatch.ts @@ -0,0 +1,140 @@ +/** + * ============================================================ + * Brain Watch โ€” ์ œ2๋‡Œ ํŒŒ์ผ์‹œ์Šคํ…œ ์›Œ์ฒ˜ + ์ƒ์ฃผ ํŒŒ์ผ ๋ชฉ๋ก ์บ์‹œ (v2.2.311) + * + * ๋ฌธ์ œ: ๊ฒ€์ƒ‰์€ ์ด๋ฏธ mtime ํ‚ค ํ† ํฐ ์ธ๋ฑ์Šค(brain-index.json)๋กœ ํŒŒ์ผ *๋‚ด์šฉ* ์žฌํŒŒ์‹ฑ์„ + * ํ”ผํ•˜์ง€๋งŒ, ๋งค ์งˆ์˜๋งˆ๋‹ค (a) ์ „์ฒด ๋””๋ ‰ํ„ฐ๋ฆฌ ์žฌ๊ท€ ์›Œํฌ(์ˆ˜์ฒœ ํŒŒ์ผ)์™€ (b) ํŒŒ์ผ๋ณ„ + * fs.statSync ์ˆ˜์ฒœ ํšŒ๋ฅผ ๋‹ค์‹œ ์ˆ˜ํ–‰ํ–ˆ๋‹ค โ€” ์ธ๋ฑ์Šค๊ฐ€ ์‹ ์„ ํ•œ์ง€ ํ™•์ธํ•  ๋ฐฉ๋ฒ•์ด "์ „๋ถ€ + * ๋‹ค์‹œ stat" ๋ฟ์ด์—ˆ๊ธฐ ๋•Œ๋ฌธ. ๋ธŒ๋ ˆ์ธ 6,500 ํŒŒ์ผ ๊ธฐ์ค€ ํ„ด๋‹น 0.5~1.5์ดˆ. + * + * ํ•ด๊ฒฐ: ๋ธŒ๋ ˆ์ธ ๋ฃจํŠธ์— ์žฌ๊ท€ fs.watch ๋ฅผ ํ•˜๋‚˜ ๊ฑธ๊ณ  ๋ณ€๊ฒฝ ์‹œ ์„ธ๋Œ€(generation) ์นด์šดํ„ฐ๋งŒ + * ์˜ฌ๋ฆฐ๋‹ค. ์„ธ๋Œ€๊ฐ€ ๊ทธ๋Œ€๋กœ๋ฉด โ€” ํŒŒ์ผ ๋ชฉ๋ก๋„, mtime ๋„ ์•ˆ ๋ณ€ํ•œ ๊ฒƒ โ€” ์›Œํฌ/stat ์„ ํ†ต์งธ๋กœ + * ์ƒ๋žตํ•˜๊ณ  ๋ฉ”๋ชจ๋ฆฌ์˜ ๋ชฉ๋กยท์ธ๋ฑ์Šค๋ฅผ ๊ทธ๋Œ€๋กœ ์‹ ๋ขฐํ•œ๋‹ค. ์ด๊ฒƒ์ด "์ œ2๋‡Œ๋ฅผ ํ•ญ์ƒ ๋ฉ”๋ชจ๋ฆฌ์— + * ์˜ฌ๋ ค๋†“๊ธฐ"์˜ ์•ˆ์ „ํ•œ ํ˜•ํƒœ๋‹ค: ์›๋ณธ ํŒŒ์ผ์„ ํ†ต์งธ๋กœ ์ƒ์ฃผ์‹œํ‚ค๋Š” ๋Œ€์‹ (์ˆ˜๋ฐฑ MB), ๊ฒ€์ƒ‰์— + * ์‹ค์ œ๋กœ ์“ฐ๋Š” ํŒŒ์ƒ๋ฌผ(ํ† ํฐ ์ธ๋ฑ์Šค + ํŒŒ์ผ ๋ชฉ๋ก)์„ ์ƒ์ฃผ์‹œํ‚ค๊ณ  ์›Œ์ฒ˜๊ฐ€ ๋ฌดํšจํ™”๋ฅผ ๋งก๋Š”๋‹ค. + * + * ์›Œ์ฒ˜๋ฅผ ๋ชป ์—ฌ๋Š” ํ™˜๊ฒฝ(๊ถŒํ•œ/๋„คํŠธ์›Œํฌ ๋“œ๋ผ์ด๋ธŒ ๋“ฑ)์—์„œ๋Š” generation ์ด -1 ๋กœ ๊ณ ์ •๋˜๊ณ  + * ๋ชจ๋“  ํ˜ธ์ถœ์ž๋Š” ์ข…์ „ ๋™์ž‘(๋งค๋ฒˆ stat)์œผ๋กœ ํด๋ฐฑํ•œ๋‹ค โ€” ์ •ํ™•์„ฑ์€ ์›Œ์ฒ˜ ๊ฐ€์šฉ์„ฑ๊ณผ ๋ฌด๊ด€. + * ============================================================ + */ + +import * as fs from 'fs'; +import * as path from 'path'; +import { findBrainFiles, invalidateBrainFilesCache, logInfo } from '../utils'; + +interface WatchState { + watcher: fs.FSWatcher | null; + /** ํŒŒ์ผ์‹œ์Šคํ…œ ๋ณ€๊ฒฝ๋งˆ๋‹ค ์ฆ๊ฐ€. ์›Œ์ฒ˜ ์—†์œผ๋ฉด ์ƒํƒœ ์ž์ฒด๊ฐ€ failed=true. */ + generation: number; + failed: boolean; + /** generation ๊ธฐ์ค€ ํŒŒ์ผ ๋ชฉ๋ก ์บ์‹œ. */ + listGeneration: number; + list: string[] | null; + /** ์ด๋ฒคํŠธ ํญ์ฃผ(๋Œ€๋Ÿ‰ sync) ์‹œ bump ๋ฅผ ๋ฌถ๋Š” ๋””๋ฐ”์šด์Šค ํƒ€์ด๋จธ. */ + bumpTimer: ReturnType | null; +} + +const _watches = new Map(); + +/** ์ธ๋ฑ์Šค ์บ์‹œ ๋“ฑ ์šฐ๋ฆฌ ์ž์‹ ์˜ ์‚ฐ์ถœ๋ฌผ ๊ฒฝ๋กœ โ€” ์ด ์•„๋ž˜ ๋ณ€๊ฒฝ์€ ๋ฌดํšจํ™” ์‚ฌ์œ ๊ฐ€ ์•„๋‹ˆ๋‹ค. */ +function isSelfNoise(relPath: string): boolean { + // .astra (brain-index.json, eval, growth โ€ฆ) ์™€ .git ์€ ๊ฒ€์ƒ‰ ๋Œ€์ƒ๋„ ์•„๋‹ˆ๊ณ  + // ํ„ด๋งˆ๋‹ค ์šฐ๋ฆฌ๊ฐ€ ์ง์ ‘ ์“ฐ๋Š” ๊ณณ์ด๋ผ, ์—ฌ๊ธฐ์„œ ์„ธ๋Œ€๋ฅผ ์˜ฌ๋ฆฌ๋ฉด ์บ์‹œ๊ฐ€ ์˜์›ํžˆ ๋ชป ์‚ฐ๋‹ค. + return /(^|[\\/])(\.astra|\.git)([\\/]|$)/.test(relPath); +} + +function bump(st: WatchState, brainPath: string): void { + if (st.bumpTimer) return; // ์ด๋ฏธ ์˜ˆ์•ฝ๋จ โ€” ๋ชฐ์•„์„œ 1ํšŒ + st.bumpTimer = setTimeout(() => { + st.bumpTimer = null; + st.generation++; + st.list = null; + invalidateBrainFilesCache(brainPath); + }, 300); + if (typeof (st.bumpTimer as any).unref === 'function') (st.bumpTimer as any).unref(); +} + +/** + * ๋ธŒ๋ ˆ์ธ ๋ฃจํŠธ์— ์›Œ์ฒ˜๋ฅผ ๋ณด์žฅํ•œ๋‹ค (idempotent). ์‹คํŒจํ•ด๋„ throw ํ•˜์ง€ ์•Š๋Š”๋‹ค โ€” + * ์ดํ›„ getBrainGeneration() ์ด -1 ์„ ๋ฐ˜ํ™˜ํ•ด ํ˜ธ์ถœ์ž๋“ค์ด stat ๊ฒฝ๋กœ๋กœ ํด๋ฐฑ. + */ +export function ensureBrainWatcher(brainPath: string): void { + if (!brainPath) return; + if (_watches.has(brainPath)) return; + const st: WatchState = { watcher: null, generation: 0, failed: false, listGeneration: -1, list: null, bumpTimer: null }; + _watches.set(brainPath, st); + try { + // recursive fs.watch ๋Š” Windows/macOS ์—์„œ ๋„ค์ดํ‹ฐ๋ธŒ ์ง€์›. (Linux ๋Š” Node 20+.) + st.watcher = fs.watch(brainPath, { recursive: true }, (_event, filename) => { + const rel = filename ? String(filename) : ''; + if (rel && isSelfNoise(rel)) return; + bump(st, brainPath); + }); + st.watcher.on('error', () => { + st.failed = true; + try { st.watcher?.close(); } catch { /* already closed */ } + st.watcher = null; + logInfo('Brain watcher failed โ€” falling back to per-query stat.', { brainPath }); + }); + logInfo('Brain watcher started โ€” file list & stat checks now cached until change.', { brainPath }); + } catch (e: any) { + st.failed = true; + st.watcher = null; + logInfo('Brain watcher unavailable โ€” falling back to per-query stat.', { brainPath, error: e?.message ?? String(e) }); + } +} + +/** + * ํ˜„์žฌ ์„ธ๋Œ€ ๋ฒˆํ˜ธ. ์›Œ์ฒ˜๊ฐ€ ์—†๊ฑฐ๋‚˜ ์ฃฝ์—ˆ์œผ๋ฉด -1 โ€” ํ˜ธ์ถœ์ž๋Š” ์บ์‹œ ์‹ ๋ขฐ ์ตœ์ ํ™”๋ฅผ ๊บผ์•ผ ํ•œ๋‹ค. + */ +export function getBrainGeneration(brainPath: string): number { + const st = _watches.get(brainPath); + if (!st || st.failed || !st.watcher) return -1; + return st.generation; +} + +/** + * ์„ธ๋Œ€ ๊ธฐ๋ฐ˜ ํŒŒ์ผ ๋ชฉ๋ก โ€” ์„ธ๋Œ€๊ฐ€ ๊ทธ๋Œ€๋กœ๋ฉด ๋””๋ ‰ํ„ฐ๋ฆฌ ์›Œํฌ ์—†์ด ๋ฉ”๋ชจ๋ฆฌ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜. + * ์›Œ์ฒ˜๊ฐ€ ์—†์œผ๋ฉด findBrainFiles(5์ดˆ TTL)๋กœ ํด๋ฐฑํ•ด ์ข…์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘. + */ +export function listBrainFilesCached(brainPath: string): string[] { + if (!brainPath) return []; + ensureBrainWatcher(brainPath); + const st = _watches.get(brainPath)!; + if (st.failed || !st.watcher) return findBrainFiles(brainPath); + if (st.list !== null && st.listGeneration === st.generation) return st.list.slice(); + const files = findBrainFiles(brainPath); + st.list = files.slice(); + st.listGeneration = st.generation; + return files; +} + +/** + * ํ™œ์„ฑํ™” ์งํ›„ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์›Œ๋ฐ โ€” ์ฒซ ์งˆ์˜๊ฐ€ 78MB ์ธ๋ฑ์Šค ํŒŒ์‹ฑ + ์ „์ฒด ์›Œํฌ๋ฅผ + * ๋’ค์ง‘์–ด์“ฐ์ง€ ์•Š๋„๋ก ๋ฏธ๋ฆฌ ์˜ฌ๋ ค ๋‘”๋‹ค. ์‹คํŒจ๋Š” ์กฐ์šฉํžˆ ๋ฌด์‹œ (๋‹ค์Œ ์งˆ์˜๊ฐ€ ์ •์ƒ ๊ฒฝ๋กœ). + */ +export function warmBrainCache(brainPath: string, warmTokenIndex: (files: string[]) => void): void { + if (!brainPath) return; + setTimeout(() => { + try { + const t0 = Date.now(); + const files = listBrainFilesCached(brainPath); + warmTokenIndex(files); + logInfo('Brain cache warmed.', { brainPath, files: files.length, ms: Date.now() - t0 }); + } catch (e: any) { + logInfo('Brain cache warm failed (non-fatal).', { brainPath, error: e?.message ?? String(e) }); + } + }, 2500); +} + +/** ํ…Œ์ŠคํŠธ/๋ธŒ๋ ˆ์ธ ์ „ํ™˜์šฉ โ€” ์›Œ์ฒ˜ ํ•ด์ œ. */ +export function disposeBrainWatcher(brainPath?: string): void { + const targets = brainPath ? [brainPath] : Array.from(_watches.keys()); + for (const key of targets) { + const st = _watches.get(key); + if (!st) continue; + if (st.bumpTimer) { clearTimeout(st.bumpTimer); st.bumpTimer = null; } + try { st.watcher?.close(); } catch { /* noop */ } + _watches.delete(key); + } +} diff --git a/src/retrieval/index.ts b/src/retrieval/index.ts index 597fb2e..39a9543 100644 --- a/src/retrieval/index.ts +++ b/src/retrieval/index.ts @@ -15,7 +15,8 @@ import * as fs from 'fs'; import * as path from 'path'; import { BrainProfile } from '../config'; -import { findBrainFiles, summarizeText } from '../utils'; +import { summarizeText } from '../utils'; +import { listBrainFilesCached } from './brainWatch'; import { isInside } from '../lib/paths'; import { MemoryManager } from '../memory'; import { RetrievalChunk, RetrievalResult, ContextBudgetConfig } from './types'; @@ -329,7 +330,8 @@ export class RetrievalOrchestrator { try { const scoped = (file: string) => scopeFolders.length === 0 || scopeFolders.some((folder) => isInside(folder, file)); - const allFiles = findBrainFiles(brain.localBrainPath) + // [v2.2.311] ์›Œ์ฒ˜ ์„ธ๋Œ€ ๊ธฐ๋ฐ˜ ์ƒ์ฃผ ๋ชฉ๋ก โ€” ๋ณ€๊ฒฝ ์—†์œผ๋ฉด ๋””๋ ‰ํ„ฐ๋ฆฌ ์žฌ๊ท€ ์›Œํฌ ์ƒ๋žต. + const allFiles = listBrainFilesCached(brain.localBrainPath) .filter(scoped) .filter((file) => { const rel = path.relative(brain.localBrainPath, file); diff --git a/src/utils.ts b/src/utils.ts index e9f5d97..04a05da 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -494,14 +494,19 @@ function getEnvironmentBlock(): string { - Canonical example for this OS: ${example}`; } -export function getSystemPrompt(): string { - const now = new Date(); - const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' }); - const isoDate = now.toISOString().split('T')[0]; +/** + * [KV ์บ์‹œ ๋ถ„๋ฆฌ v2.2.311] ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ์˜ *ํ„ด ์‚ฌ์ด์— ๋ณ€ํ•˜์ง€ ์•Š๋Š”* ๋ถ€๋ถ„๋งŒ ๋ฐ˜ํ™˜. + * ์—ฌ๊ธฐ์— ๋ถ„ ๋‹จ์œ„ ์‹œ๊ฐ ๊ฐ™์€ ๊ฐ€๋ณ€ ์š”์†Œ๊ฐ€ ์„ž์ด๋ฉด LM Studio(llama.cpp)์˜ prompt cache ๊ฐ€ + * ์š”์ฒญ ์ฒซ ํ† ํฐ๋ถ€ํ„ฐ ๋ฌดํšจํ™”๋˜์–ด ๋งค ํ„ด 1๋งŒ+ ํ† ํฐ ์ „์ฒด๋ฅผ ๋‹ค์‹œ ํ”„๋ฆฌํ•„ํ•œ๋‹ค โ€” ์‹ค์ธก 94์ดˆ/ํ„ด์˜ + * ์ฃผ๋ฒ”. ๋‚ ์งœ/์‹œ๊ฐ์€ `getDateTimeContextBlock()` ์œผ๋กœ ๋ถ„๋ฆฌํ•ด ์š”์ฒญ ํ›„๋ฏธ(๋™์  ์ปจํ…์ŠคํŠธ + * ๋ฉ”์‹œ์ง€)์— ์‹ฃ๋Š”๋‹ค. + */ +export function getStaticSystemPrompt(): string { const envBlock = getEnvironmentBlock(); - const base = `${BASE_SYSTEM_PROMPT}\n\n[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").\n\n[์ถœ๋ ฅ ์œ„์ƒ ๊ทœ์น™ โ€” ๋ฐ˜๋“œ์‹œ ์ค€์ˆ˜]\n- ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜๊ณ , ํ•œ ๋‹จ์–ด ์•ˆ์— ํ•œ๊ธ€๊ณผ ์˜๋ฌธ ์•ŒํŒŒ๋ฒณ์„ ์„ž์ง€ ๋งˆ์‹œ์˜ค ("๊ฒฐently", "์ธorp" ๊ฐ™์€ ๊นจ์ง„ ํ•ฉ์„ฑ ํ‘œ๊ธฐ ์ ˆ๋Œ€ ๊ธˆ์ง€).\n- ์™ธ๋ž˜์–ดยท๊ธฐ์ˆ  ์šฉ์–ด๋Š” ์™„์ „ํ•œ ํ•œ๊ธ€ ํ‘œ๊ธฐ ๋˜๋Š” ์™„์ „ํ•œ ์˜๋ฌธ ๋‹จ์–ด ์ค‘ ํ•˜๋‚˜๋กœ ์ผ๊ด€๋˜๊ฒŒ ์“ฐ์‹œ์˜ค.\n- ๋‚ด๋ถ€ ๊ฒ€์ฆยท์ฒดํฌ ๋กœ๊ทธ(Consistency/Completeness/Accuracy ๋“ฑ) ๋ธ”๋ก์„ ์‚ฌ์šฉ์ž ์ถœ๋ ฅ์— ํฌํ•จํ•˜์ง€ ๋งˆ์‹œ์˜ค.${envBlock}`; + const base = `${BASE_SYSTEM_PROMPT}\n\n[์ถœ๋ ฅ ์œ„์ƒ ๊ทœ์น™ โ€” ๋ฐ˜๋“œ์‹œ ์ค€์ˆ˜]\n- ์ž์—ฐ์Šค๋Ÿฌ์šด ํ•œ๊ตญ์–ด๋กœ ์ž‘์„ฑํ•˜๊ณ , ํ•œ ๋‹จ์–ด ์•ˆ์— ํ•œ๊ธ€๊ณผ ์˜๋ฌธ ์•ŒํŒŒ๋ฒณ์„ ์„ž์ง€ ๋งˆ์‹œ์˜ค ("๊ฒฐently", "์ธorp" ๊ฐ™์€ ๊นจ์ง„ ํ•ฉ์„ฑ ํ‘œ๊ธฐ ์ ˆ๋Œ€ ๊ธˆ์ง€).\n- ์™ธ๋ž˜์–ดยท๊ธฐ์ˆ  ์šฉ์–ด๋Š” ์™„์ „ํ•œ ํ•œ๊ธ€ ํ‘œ๊ธฐ ๋˜๋Š” ์™„์ „ํ•œ ์˜๋ฌธ ๋‹จ์–ด ์ค‘ ํ•˜๋‚˜๋กœ ์ผ๊ด€๋˜๊ฒŒ ์“ฐ์‹œ์˜ค.\n- ๋‚ด๋ถ€ ๊ฒ€์ฆยท์ฒดํฌ ๋กœ๊ทธ(Consistency/Completeness/Accuracy ๋“ฑ) ๋ธ”๋ก์„ ์‚ฌ์šฉ์ž ์ถœ๋ ฅ์— ํฌํ•จํ•˜์ง€ ๋งˆ์‹œ์˜ค.${envBlock}`; // Self-Reflector Phase A โ€” ์‚ฌ์šฉ์ž ์„ค์ •์ด ์ผœ์ ธ ์žˆ์œผ๋ฉด ๋‹ต๋ณ€ ๋์— ์ž๊ธฐ๊ฒ€์ฆ // ๋ธ”๋ก์„ ๊ฐ•์ œํ•˜๋Š” ๋ฃฐ์„ prepend. require๋กœ ๋™์  ๋กœ๋“œํ•ด ์ˆœํ™˜ import ํšŒํ”ผ. + // (์„ค์ •์€ ์„ธ์…˜ ์ค‘ ๋ฐ”๋€” ์ˆ˜ ์žˆ์ง€๋งŒ ๋“œ๋ฌผ๋‹ค โ€” ๋ฐ”๋€Œ๋ฉด ๊ทธ ํ„ด๋งŒ ์บ์‹œ ๋ฏธ์Šค, ๋ฌธ์ œ ์—†์Œ.) try { const { getConfig } = require('./config') as typeof import('./config'); const { appendSelfReflectorRule } = require('./features/selfReflector/selfReflectorPrompt') as typeof import('./features/selfReflector/selfReflectorPrompt'); @@ -513,4 +518,21 @@ export function getSystemPrompt(): string { } } +/** [CURRENT DATE/TIME] ๋ธ”๋ก โ€” ๊ฐ€๋ณ€์ด๋ฏ€๋กœ ์ •์  ํ”„๋กฌํ”„ํŠธ๊ฐ€ ์•„๋‹ˆ๋ผ ๋™์  ์ปจํ…์ŠคํŠธ์— ์ฃผ์ž…. */ +export function getDateTimeContextBlock(): string { + const now = new Date(); + const dateTimeStr = now.toLocaleString('ko-KR', { timeZone: 'Asia/Seoul', year: 'numeric', month: '2-digit', day: '2-digit', weekday: 'long', hour: '2-digit', minute: '2-digit' }); + const isoDate = now.toISOString().split('T')[0]; + return `[CURRENT DATE/TIME]\nToday: ${isoDate} (${dateTimeStr})\nUse this date as the absolute reference for any date-related calculations (e.g., "this week", "today", "yesterday").`; +} + +export function getSystemPrompt(): string { + // ๊ธฐ์กด ํ˜ธ์ถœ๋ถ€ ํ˜ธํ™˜ โ€” ์ •์  ๋ณธ๋ฌธ + ๋‚ ์งœ ๋ธ”๋ก์„ ํ•ฉ์นœ ์ข…์ „ ํ˜•ํƒœ ๊ทธ๋Œ€๋กœ. + const staticPart = getStaticSystemPrompt(); + const dateBlock = getDateTimeContextBlock(); + // BASE ์งํ›„ยท์œ„์ƒ ๊ทœ์น™ ์•ž์ด๋˜ ์ข…์ „ ์œ„์น˜์™€ ๋‹ฌ๋ฆฌ ๋์— ๋ถ™์ง€๋งŒ, ๋‹จ๋… ์‚ฌ์šฉ์ฒ˜(๋ธŒ๋ฆฌํ•‘ยทํ‰๊ฐ€ + // ๋“ฑ 1ํšŒ์„ฑ ํ˜ธ์ถœ)์—๋Š” ์œ„์น˜๊ฐ€ ์˜๋ฏธ ์—†๋‹ค. ์ฑ„ํŒ… ๋ณธ๋ฅ˜๋Š” split ๊ฒฝ๋กœ๋ฅผ ์“ด๋‹ค. + return `${staticPart}\n\n${dateBlock}`; +} + export const SYSTEM_PROMPT = BASE_SYSTEM_PROMPT; diff --git a/tests/brainCache.test.ts b/tests/brainCache.test.ts new file mode 100644 index 0000000..65e217a --- /dev/null +++ b/tests/brainCache.test.ts @@ -0,0 +1,98 @@ +/** + * [v2.2.311] ์ œ2๋‡Œ ์ƒ์ฃผ ์บ์‹œ ๊ณ„์ธต โ€” ์›Œ์ฒ˜ ์„ธ๋Œ€ ๊ธฐ๋ฐ˜ stat ์ƒ๋žต + ํŒŒ์ผ ๋ชฉ๋ก ์บ์‹œ. + * + * ํ•ต์‹ฌ ๊ณ„์•ฝ: + * 1. ์›Œ์ฒ˜ ์„ธ๋Œ€๊ฐ€ ๊ทธ๋Œ€๋กœ๋ฉด getBrainTokenIndex ๋Š” ํŒŒ์ผ๋ณ„ statSync ๋ฅผ ์ƒ๋žตํ•œ๋‹ค. + * 2. ์„ธ๋Œ€๊ฐ€ ๋ฐ”๋€Œ๋ฉด(ํŒŒ์ผ์‹œ์Šคํ…œ ๋ณ€๊ฒฝ) ๋‹ค์‹œ stat ์„ ๊ฑฐ์ณ ๋ณ€๊ฒฝ๋ถ„์„ ์žฌ์ƒ‰์ธํ•œ๋‹ค. + * 3. ๋ถ€๋ถ„(์Šค์ฝ”ํ”„) ๋ชฉ๋ก์œผ๋กœ ๊ฒ€์ฆ๋œ ์ƒํƒœ์—์„œ ์ „์ฒด ๋ชฉ๋ก ์งˆ์˜๊ฐ€ ์™€๋„, ๊ฒ€์ฆ ์•ˆ ๋œ + * ํŒŒ์ผ์€ ์‹ ๋ขฐ ๊ฒฝ๋กœ๋กœ ์ƒˆ์ง€ ์•Š๋Š”๋‹ค (ํŒŒ์ผ ๋‹จ์œ„ ๊ฒ€์ฆ ์žฅ๋ถ€). + * 4. ์›Œ์ฒ˜ ๋ถˆ๊ฐ€ ํ™˜๊ฒฝ(gen === -1)์—์„œ๋Š” ์ข…์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ๋งค๋ฒˆ stat. + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; + +// brainWatch ๋ฅผ ๊ฒฐ์ •์ ์œผ๋กœ ์ œ์–ด โ€” ์‹ค์ œ fs.watch ์ด๋ฒคํŠธ ํƒ€์ด๋ฐ์— ์˜์กดํ•˜์ง€ ์•Š๋Š”๋‹ค. +let mockGen = 0; +jest.mock('../src/retrieval/brainWatch', () => ({ + ensureBrainWatcher: jest.fn(), + getBrainGeneration: jest.fn(() => mockGen), + listBrainFilesCached: jest.fn(), + warmBrainCache: jest.fn(), + disposeBrainWatcher: jest.fn(), +})); + +import { getBrainTokenIndex } from '../src/retrieval/brainIndex'; + +function makeBrain(files: Record): { root: string; paths: string[] } { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-brain-')); + const paths: string[] = []; + for (const [name, content] of Object.entries(files)) { + const p = path.join(root, name); + fs.writeFileSync(p, content, 'utf8'); + // RECENT_MTIME_GUARD(3์ดˆ): ๊ฐ“ ์ˆ˜์ •๋œ ํŒŒ์ผ์€ ์‹ ๋ขฐ ๋ช…๋ถ€์— ์˜ค๋ฅด์ง€ ์•Š์œผ๋ฏ€๋กœ, + // "๋ณ€๊ฒฝ ์—†์Œ" ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ…Œ์ŠคํŠธํ•˜๋ ค๋ฉด mtime ์„ ๊ณผ๊ฑฐ๋กœ ๋˜๋Œ๋ฆฐ๋‹ค. + const past = new Date(Date.now() - 60_000); + fs.utimesSync(p, past, past); + paths.push(p); + } + return { root, paths }; +} + +function countStatsFor(paths: string[], spy: jest.SpyInstance): number { + return spy.mock.calls.filter((c) => paths.includes(String(c[0]))).length; +} + +describe('getBrainTokenIndex โ€” ์›Œ์ฒ˜ ์„ธ๋Œ€ ๊ธฐ๋ฐ˜ stat ์ƒ๋žต', () => { + afterEach(() => jest.restoreAllMocks()); + + test('๊ฐ™์€ ์„ธ๋Œ€์˜ 2๋ฒˆ์งธ ํ˜ธ์ถœ์€ statSync 0ํšŒ (์บ์‹œ ์‹ ๋ขฐ)', () => { + mockGen = 0; + const { root, paths } = makeBrain({ 'a.md': '์•ŒํŒŒ ๋‚ด์šฉ', 'b.md': '๋ฒ ํƒ€ ๋‚ด์šฉ' }); + expect(getBrainTokenIndex(root, paths)).toHaveLength(2); + + const spy = jest.spyOn(fs, 'statSync'); + const r2 = getBrainTokenIndex(root, paths); + expect(r2).toHaveLength(2); + expect(countStatsFor(paths, spy)).toBe(0); + expect(r2.map((d) => d.title).sort()).toEqual(['a', 'b']); + }); + + test('์„ธ๋Œ€๊ฐ€ ๋ฐ”๋€Œ๋ฉด ์žฌ-stat ํ›„ ๋ณ€๊ฒฝ ๋‚ด์šฉ์„ ์žฌ์ƒ‰์ธํ•œ๋‹ค', () => { + mockGen = 0; + const { root, paths } = makeBrain({ 'a.md': '์›๋ณธ ํ† ํฐ๋“ค' }); + getBrainTokenIndex(root, paths); + + // ํŒŒ์ผ ๋ณ€๊ฒฝ + ์„ธ๋Œ€ bump (mtime ์ฐจ์ด๋ฅผ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด utimesSync ๋กœ ๊ฐ•์ œ) + fs.writeFileSync(paths[0], '์™„์ „ํžˆ ์ƒˆ๋กœ์šด ๋‚ด์šฉ์ž…๋‹ˆ๋‹ค', 'utf8'); + const future = new Date(Date.now() + 5000); + fs.utimesSync(paths[0], future, future); + mockGen = 1; + + const spy = jest.spyOn(fs, 'statSync'); + const r = getBrainTokenIndex(root, paths); + expect(countStatsFor(paths, spy)).toBeGreaterThan(0); + expect(r[0].tokens).toEqual(expect.arrayContaining(['์ƒˆ๋กœ์šด'])); + }); + + test('์Šค์ฝ”ํ”„ ๊ฒ€์ฆ ํ›„ ์ „์ฒด ์งˆ์˜ โ€” ๋ฏธ๊ฒ€์ฆ ํŒŒ์ผ์€ ์‹ ๋ขฐ ๊ฒฝ๋กœ๋กœ ์ƒˆ์ง€ ์•Š๋Š”๋‹ค', () => { + mockGen = 0; + const { root, paths } = makeBrain({ 'in-scope.md': '์Šค์ฝ”ํ”„ ์•ˆ', 'out-scope.md': '์Šค์ฝ”ํ”„ ๋ฐ–' }); + const [inScope, outScope] = paths; + getBrainTokenIndex(root, [inScope]); // ๋ถ€๋ถ„ ๋ชฉ๋ก๋งŒ ๊ฒ€์ฆ๋จ + + const spy = jest.spyOn(fs, 'statSync'); + getBrainTokenIndex(root, paths); // ์ „์ฒด ์งˆ์˜ + expect(countStatsFor([inScope], spy)).toBe(0); // ๊ฒ€์ฆ๋œ ํŒŒ์ผ์€ ์ƒ๋žต + expect(countStatsFor([outScope], spy)).toBeGreaterThan(0); // ๋ฏธ๊ฒ€์ฆ ํŒŒ์ผ์€ stat + }); + + test('์›Œ์ฒ˜ ๋ถˆ๊ฐ€(gen=-1) ํ™˜๊ฒฝ์€ ๋งค๋ฒˆ stat (์ข…์ „ ๋™์ž‘)', () => { + mockGen = -1; + const { root, paths } = makeBrain({ 'a.md': '๋‚ด์šฉ' }); + getBrainTokenIndex(root, paths); + const spy = jest.spyOn(fs, 'statSync'); + getBrainTokenIndex(root, paths); + expect(countStatsFor(paths, spy)).toBeGreaterThan(0); + }); +}); diff --git a/tests/computeBudgetedRequest.test.ts b/tests/computeBudgetedRequest.test.ts index 75bb816..82c2972 100644 --- a/tests/computeBudgetedRequest.test.ts +++ b/tests/computeBudgetedRequest.test.ts @@ -56,3 +56,92 @@ describe('computeBudgetedRequest โ€” real-window alignment', () => { expect(run({ actualContextLength: NaN }).effectiveContextLength).toBe(32768); }); }); + +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +// [v2.2.311] KV ์บ์‹œ ๋ถ„๋ฆฌ โ€” dynamicContextTail ์‚ฝ์ž… ๊ณ„์•ฝ. +// message[0] ์€ ๋ถˆ๋ณ€ ์ •์  ํ”„๋กฌํ”„ํŠธ๋กœ ๋‚จ๊ณ , ๋™์  ์ปจํ…์ŠคํŠธ๋Š” ๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ +// ์ง์ „์˜ internal system ๋ฉ”์‹œ์ง€๋กœ ๋“ค์–ด๊ฐ€์•ผ ํ”„๋กฌํ”„ํŠธ ํ”„๋ฆฌํ”ฝ์Šค ์บ์‹œ๊ฐ€ ์‚ฐ๋‹ค. +// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +describe('computeBudgetedRequest โ€” dynamicContextTail (KV cache split)', () => { + const HEAD = 'STATIC HEAD PROMPT'; + const TAIL = '[CURRENT DATE/TIME]\nToday: 2026-07-20\n\n[CONTEXT]\nRAG chunk A\n[/CONTEXT]'; + + // ์ฃผ์˜: default parameter ๋Š” ๋ช…์‹œ์  undefined ์—๋„ ์ ์šฉ๋˜๋ฏ€๋กœ ์“ฐ์ง€ ์•Š๋Š”๋‹ค โ€” + // "tail ์—†์Œ" ์ผ€์ด์Šค๋ฅผ ์ •ํ™•ํžˆ ํ‘œํ˜„ํ•˜๊ธฐ ์œ„ํ•ด ํ•ญ์ƒ ๋ช…์‹œ์ ์œผ๋กœ ๋„˜๊ธด๋‹ค. + function runSplit(reqMessages: ChatMessage[], tail: string | undefined) { + return computeBudgetedRequest({ + fullSystemPrompt: HEAD, + dynamicContextTail: tail, + reqMessages, + actualModel: 'some-13b-model', + config: baseConfig, + imageCount: 0, + }); + } + + test('tail ์€ ๋งˆ์ง€๋ง‰ user ๋ฉ”์‹œ์ง€ ์ง์ „์— internal system ์œผ๋กœ ์‚ฝ์ž…๋œ๋‹ค', () => { + const msgs: ChatMessage[] = [ + { role: 'user', content: '์ฒซ ์งˆ๋ฌธ' }, + { role: 'assistant', content: '์ฒซ ๋‹ต' }, + { role: 'user', content: '๋‘๋ฒˆ์งธ ์งˆ๋ฌธ' }, + ]; + const r = runSplit(msgs, TAIL); + expect(r.messagesForRequest[0]).toMatchObject({ role: 'system', content: HEAD }); + const idx = r.messagesForRequest.findIndex((m) => m.content === TAIL); + expect(idx).toBeGreaterThan(0); + expect(r.messagesForRequest[idx].role).toBe('system'); + expect(r.messagesForRequest[idx + 1]).toMatchObject({ role: 'user', content: '๋‘๋ฒˆ์งธ ์งˆ๋ฌธ' }); + }); + + test('continuation(๋งˆ์ง€๋ง‰ user ๋’ค ์•ก์…˜ ๊ฒฐ๊ณผ system)์ด ์žˆ์–ด๋„ tail ์€ user ์•ž', () => { + const msgs: ChatMessage[] = [ + { role: 'user', content: '์กฐ์‚ฌํ•ด์ค˜' }, + { role: 'system', content: '[Result of read_file] ...', internal: true }, + ]; + const r = runSplit(msgs, TAIL); + const tailIdx = r.messagesForRequest.findIndex((m) => m.content === TAIL); + const userIdx = r.messagesForRequest.findIndex((m) => m.role === 'user'); + const actionIdx = r.messagesForRequest.findIndex((m) => String(m.content).startsWith('[Result of')); + expect(tailIdx).toBeLessThan(userIdx); + expect(actionIdx).toBeGreaterThan(userIdx); + }); + + test('user ๋ฉ”์‹œ์ง€๊ฐ€ ์—†์œผ๋ฉด tail ์„ ํžˆ์Šคํ† ๋ฆฌ ๋์— ๋ถ™์ธ๋‹ค', () => { + const msgs: ChatMessage[] = [{ role: 'system', content: 'internal only', internal: true }]; + const r = runSplit(msgs, TAIL); + const last = r.messagesForRequest[r.messagesForRequest.length - 1]; + expect(last.content).toBe(TAIL); + }); + + test('tail ๋ฏธ์ง€์ •์ด๋ฉด ์ข…์ „ ๋‹จ์ผ-์‹œ์Šคํ…œ ๋™์ž‘ ๊ทธ๋Œ€๋กœ', () => { + const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }]; + const r = runSplit(msgs, undefined); + expect(r.messagesForRequest).toHaveLength(2); + expect(r.messagesForRequest[0].content).toBe(HEAD); + }); + + test('์˜ˆ์‚ฐ ์ดˆ๊ณผ ์‹œ [CONTEXT] truncation ์€ tail ์— ์ ์šฉ๋˜๊ณ  head ๋Š” ๋ณด์กด', () => { + const hugeTail = `[CONTEXT]\n${'๊ธด ๋ฐฐ๊ฒฝ ์ง€์‹ ์ฒญํฌ. '.repeat(20000)}\n[/CONTEXT]`; + const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }]; + const r = computeBudgetedRequest({ + fullSystemPrompt: HEAD, + dynamicContextTail: hugeTail, + reqMessages: msgs, + actualModel: 'some-13b-model', + config: { ...baseConfig, contextLength: 8192 }, + imageCount: 0, + }); + expect(r.systemTruncated).toBe(true); + expect(r.messagesForRequest[0].content).toBe(HEAD); + const tailMsg = r.messagesForRequest.find((m) => String(m.content).includes('[CONTEXT]')); + expect(tailMsg).toBeDefined(); + expect(String(tailMsg!.content).length).toBeLessThan(hugeTail.length); + }); + + test('systemTokens ๋Š” head+tail ํ•ฉ์‚ฐ์œผ๋กœ ๋ณด๊ณ ๋œ๋‹ค', () => { + const msgs: ChatMessage[] = [{ role: 'user', content: 'hello' }]; + const withTail = runSplit(msgs, TAIL); + const withoutTail = runSplit(msgs, undefined); + expect(withTail.systemTokens).toBeGreaterThan(withoutTail.systemTokens); + }); +}); diff --git a/tests/investigationPipeline.test.ts b/tests/investigationPipeline.test.ts index 7e8c570..f82ad65 100644 --- a/tests/investigationPipeline.test.ts +++ b/tests/investigationPipeline.test.ts @@ -165,3 +165,29 @@ describe('detectHollowInvestigation โ€” ํ—›์กฐ์‚ฌ ๊ฐ์ง€', () => { expect(footer).toContain('investigate_files'); }); }); + +// [v2.2.312] ๊ทผ๊ฑฐ ์—†๋Š” ๋ถ„์„ ๊ฐ์ง€ โ€” ํŒŒ์ผ์„ ์ฝ๊ณ ๋„ ์ธ์šฉ์ด ์—†๋Š” ์ผ๋ฐ˜๋ก  ๋ณด๊ณ . +import { detectUngroundedAnalysis, formatUngroundedAnalysisFooter } from '../src/intelligence/investigationPipeline'; + +describe('detectUngroundedAnalysis โ€” ์ผ๋ฐ˜๋ก  ๋ถ„์„ ๊ฐ์ง€', () => { + const generic = '์ด ํ”„๋กœ์ ํŠธ๋Š” ๋กœ์ปฌ LLM๊ณผ RAG ๊ธฐ์ˆ ์˜ ๊ฒฐํ•ฉ ์‹œ๋„๊ฐ€ ๋งค์šฐ ์ธ์ƒ์ ์ž…๋‹ˆ๋‹ค. '.repeat(10); + + test('read ๋ฅผ ํ•˜๊ณ ๋„ ํŒŒ์ผ ์ธ์šฉ ์—†๋Š” ์žฅ๋ฌธ = ungrounded', () => { + const r = detectUngroundedAnalysis(generic, { reads: 3, lists: 1, investigates: 0 }); + expect(r.ungrounded).toBe(true); + expect(r.readCount).toBe(3); + }); + test('ํŒŒ์ผ ๊ทผ๊ฑฐ๋ฅผ 2๊ฐœ ์ด์ƒ ์ธ์šฉํ•˜๋ฉด ์ •์ƒ', () => { + const grounded = `${generic} ๊ทผ๊ฑฐ: agent.ts ์˜ handlePrompt ์™€ memoryContext.ts ์˜ buildMemoryContext ๋ฅผ ํ™•์ธํ•จ.`; + expect(detectUngroundedAnalysis(grounded, { reads: 3, lists: 1, investigates: 0 }).ungrounded).toBe(false); + }); + test('read ๊ฐ€ ์—†์œผ๋ฉด ํŒ์ • ๋Œ€์ƒ ์•„๋‹˜ (hollow ๊ฐ์ง€์˜ ์˜์—ญ)', () => { + expect(detectUngroundedAnalysis(generic, { reads: 0, lists: 1, investigates: 0 }).ungrounded).toBe(false); + }); + test('์งง์€ ํ™•์ธํ˜• ์‘๋‹ต์€ ์˜คํƒ ๋ฐฉ์ง€๋กœ ์ œ์™ธ', () => { + expect(detectUngroundedAnalysis('๋„ค, ์„œ๋ฒ„ ์ •์ƒ์ž…๋‹ˆ๋‹ค.', { reads: 1, lists: 0, investigates: 0 }).ungrounded).toBe(false); + }); + test('footer ์— ์ฝ์€ ํšŸ์ˆ˜ ํฌํ•จ', () => { + expect(formatUngroundedAnalysisFooter(3)).toContain('3ํšŒ'); + }); +}); diff --git a/tests/promptSplit.test.ts b/tests/promptSplit.test.ts new file mode 100644 index 0000000..919a6fb --- /dev/null +++ b/tests/promptSplit.test.ts @@ -0,0 +1,60 @@ +/** + * [v2.2.311] KV ์บ์‹œ ์นœํ™” ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ โ€” ์ •์ /๋™์  ๊ฒฝ๊ณ„ ๊ณ„์•ฝ. + * + * ์ •์  ํ”„๋กฌํ”„ํŠธ(getStaticSystemPrompt)์— ๋ถ„ ๋‹จ์œ„ ์‹œ๊ฐ ๊ฐ™์€ ๊ฐ€๋ณ€ ์š”์†Œ๊ฐ€ ์„ž์ด๋Š” ์ˆœ๊ฐ„ + * llama.cpp prompt cache ๋Š” ์š”์ฒญ ์ฒซ ํ† ํฐ๋ถ€ํ„ฐ ๋ฌดํšจํ™”๋œ๋‹ค (์‹ค์ธก: 13k ํ† ํฐ ์ „์ฒด ์žฌํ”„๋ฆฌํ•„ + * โ‰ˆ 90์ดˆ/ํ„ด). ์ด ํ…Œ์ŠคํŠธ๋Š” ๊ทธ ํšŒ๊ท€๋ฅผ ๋ง‰๋Š” ๊ฐ€๋“œ๋ ˆ์ผ์ด๋‹ค. + */ +import { getStaticSystemPrompt, getDateTimeContextBlock, getSystemPrompt } from '../src/utils'; +import { buildAstraModeSystemPrompt } from '../src/agent/handlePrompt/buildAstraModeSystemPrompt'; + +describe('์ •์ /๋™์  ์‹œ์Šคํ…œ ํ”„๋กฌํ”„ํŠธ ๋ถ„๋ฆฌ', () => { + test('์ •์  ํ”„๋กฌํ”„ํŠธ์—๋Š” ๋‚ ์งœ/์‹œ๊ฐ์ด ์—†๋‹ค (์บ์‹œ ๋ฌดํšจํ™” ๊ฐ€๋“œ)', () => { + const s = getStaticSystemPrompt(); + expect(s).not.toContain('[CURRENT DATE/TIME]'); + // ์˜ค๋Š˜ ๋‚ ์งœ ๋ฌธ์ž์—ด์ด ์–ด๋–ค ํ˜•ํƒœ๋กœ๋„ ๋“ค์–ด์žˆ์œผ๋ฉด ์•ˆ ๋œ๋‹ค. + const iso = new Date().toISOString().split('T')[0]; + expect(s).not.toContain(iso); + }); + + test('์ •์  ํ”„๋กฌํ”„ํŠธ๋Š” ์—ฐ์† ํ˜ธ์ถœ์—์„œ ๋™์ผํ•˜๋‹ค (ํ”„๋ฆฌํ”ฝ์Šค ๋ถˆ๋ณ€)', () => { + expect(getStaticSystemPrompt()).toBe(getStaticSystemPrompt()); + }); + + test('๋‚ ์งœ ๋ธ”๋ก์€ ์˜ค๋Š˜ ๋‚ ์งœ๋ฅผ ํฌํ•จํ•œ๋‹ค', () => { + const block = getDateTimeContextBlock(); + expect(block).toContain('[CURRENT DATE/TIME]'); + expect(block).toContain(new Date().toISOString().split('T')[0]); + }); + + test('๋ ˆ๊ฑฐ์‹œ getSystemPrompt ๋Š” ์ •์  ๋ณธ๋ฌธ + ๋‚ ์งœ ๋ธ”๋ก์„ ๋ชจ๋‘ ํฌํ•จ (ํ˜ธํ™˜)', () => { + const s = getSystemPrompt(); + expect(s).toContain('[CURRENT DATE/TIME]'); + expect(s).toContain('[์ถœ๋ ฅ ์œ„์ƒ ๊ทœ์น™ โ€” ๋ฐ˜๋“œ์‹œ ์ค€์ˆ˜]'); + }); + + test('๋นŒ๋”์— systemPrompt="" ๋ฅผ ๋„˜๊ฒจ๋„ [CONTEXT] ๊ณจ๊ฒฉ์€ ์œ ์ง€๋œ๋‹ค (tail ์ƒ์„ฑ ๊ฒฝ๋กœ)', () => { + const tail = buildAstraModeSystemPrompt({ + prompt: '์งˆ๋ฌธ', + systemPrompt: '', + modeBridgeCtx: '', + priorConclusionCtx: '', + designerCtx: '', + projectArchitectureCtx: '', + secondBrainTraceCtx: '', + memoryCtx: 'RAG ๋ณธ๋ฌธ', + knowledgeContextForPrompt: '', + contextBlock: '์—ด๋ฆฐ ํŒŒ์ผ ๋‚ด์šฉ', + negativeCtx: '', + isCasualConversation: false, + localPathContext: '', + knowledgeMix: null, + dynamicBlocks: new Map([['behavior-constraints', '[GROUNDING] ๊ทผ๊ฑฐ ๊ฐ•๋„: ๋ณดํ†ต']]), + }); + expect(tail).toContain('[CONTEXT]'); + expect(tail).toContain('RAG ๋ณธ๋ฌธ'); + expect(tail).toContain('์—ด๋ฆฐ ํŒŒ์ผ ๋‚ด์šฉ'); + expect(tail).toContain('[GROUNDING]'); + expect(tail.startsWith('You are Astra')).toBe(false); // ์ •์  ๋ณธ๋ฌธ์ด ์„ž์ด๋ฉด ์•ˆ ๋จ + }); +}); diff --git a/tests/requirementGraph.test.ts b/tests/requirementGraph.test.ts index a7d218a..65f994e 100644 --- a/tests/requirementGraph.test.ts +++ b/tests/requirementGraph.test.ts @@ -124,3 +124,23 @@ describe('DEFAULT_TASK_REQUIREMENTS ๋ฌด๊ฒฐ์„ฑ', () => { } }); }); + +// [v2.2.312] ๋ถ„์„ ๋ณด๊ณ  ์œ ํ˜• โ€” "ํ”„๋กœ์ ํŠธ ๋ถ„์„ํ•˜๊ณ  ๋ณด๊ณ ํ•ด์ค˜" ์‹ค์‚ฌ๋ก€๊ฐ€ ์œ ํ˜• ๋ฏธ๊ฐ์ง€๋กœ +// ์ผ๋ฐ˜๋ก  ์ดํ‰์ด ๋˜๋˜ ๊ฐญ์„ ๋ง‰๋Š”๋‹ค. +describe('detectTaskType โ€” analysis-report (v2.2.312)', () => { + test('์‹ค์‚ฌ๋ก€ ๋ฌธ์žฅ์„ ์ •ํ™•ํžˆ ๊ฐ์ง€ํ•œ๋‹ค', () => { + expect(detectTaskType('E:\Wiki\connectai ํ”„๋กœ์ ํŠธ๋ฅผ ๋ถ„์„ํ•˜๊ณ  ์–ด๋–ป๊ฒŒ ๋˜์–ด ์žˆ๋Š”์ง€ ๋ณด๊ณ ํ•ด์ค˜')?.id).toBe('analysis-report'); + expect(detectTaskType('์ง„ํ–‰ ํ˜„ํ™ฉ ๋ณด๊ณ  ๋ถ€ํƒํ•ด')?.id).toBe('analysis-report'); + expect(detectTaskType('์ด ์ฝ”๋“œ ๊ฒ€ํ† ํ•ด์„œ ๋ณด๊ณ ํ•ด์ค˜')?.id).toBe('analysis-report'); + }); + test('๋” ๊ตฌ์ฒด์ ์ธ ์œ ํ˜•์ด ์šฐ์„ ํ•œ๋‹ค (ํšŒ์˜๋กยท์‹œ์žฅ์กฐ์‚ฌ)', () => { + expect(detectTaskType('ํšŒ์˜๋ก์œผ๋กœ ์ •๋ฆฌํ•ด์„œ ๋ณด๊ณ ํ•ด์ค˜')?.id).toBe('meeting-minutes'); + expect(detectTaskType('์‹œ์žฅ์กฐ์‚ฌ ๊ฒฐ๊ณผ ๋ณด๊ณ ํ•ด์ค˜')?.id).toBe('market-research'); + }); + test('๋ธ”๋ก์— ํŒŒ์ผ ๊ทผ๊ฑฐ ์š”์†Œ์™€ ์„น์…˜ ๊ทœ์น™ ์˜ค๋ฒ„๋ผ์ด๋“œ๊ฐ€ ํฌํ•จ๋œ๋‹ค', () => { + const block = buildRequirementGraphBlock('ํ”„๋กœ์ ํŠธ ๋ถ„์„ํ•˜๊ณ  ๋ณด๊ณ ํ•ด์ค˜'); + expect(block).toContain('ํŒŒ์ผ ๊ทผ๊ฑฐ'); + expect(block).toContain('๋ณด๊ณ  ๊ฐœ์š”'); + expect(block).toContain('์ตœ๋Œ€ 3์„น์…˜'); + }); +}); diff --git a/tests/stripForDisplay.test.ts b/tests/stripForDisplay.test.ts new file mode 100644 index 0000000..f8a4f92 --- /dev/null +++ b/tests/stripForDisplay.test.ts @@ -0,0 +1,35 @@ +/** + * [v2.2.312] ์ค‘๊ฐ„ ๋ผ์šด๋“œ ํ‘œ์‹œ์šฉ ์•ก์…˜ ํƒœ๊ทธ ์ œ๊ฑฐ โ€” "## 4.๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๋ณด๊ณ ์„œ" ๋ฒ„๊ทธ์˜ + * ํ‘œ์‹œ ๊ฒฝ๋กœ ํ—ฌํผ. ํƒœ๊ทธ๋Š” ๊ฑท์–ด๋‚ด๋˜ ๋ณธ๋ฌธ ์„น์…˜์€ ๋ณด์กดํ•ด์•ผ ํ•œ๋‹ค. + */ +import { stripActionTagsForDisplay } from '../src/agent/actions/stripForDisplay'; + +describe('stripActionTagsForDisplay', () => { + test('์ž๊ธฐ๋‹ซํž˜ ํƒœ๊ทธ ์ œ๊ฑฐ + ๋ณธ๋ฌธ ๋ณด์กด', () => { + const input = '## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š”\n๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•œ๋‹ค.\n\n'; + const out = stripActionTagsForDisplay(input); + expect(out).toContain('## 1. ํ”„๋กœ์ ํŠธ ๊ฐœ์š”'); + expect(out).not.toContain(' { + const input = '๋ถ„์„ ์‹œ์ž‘.\ncd x; npm test\n๋ณธ๋ฌธ\n๋.'; + const out = stripActionTagsForDisplay(input); + expect(out).toContain('๋ถ„์„ ์‹œ์ž‘.'); + expect(out).toContain('๋.'); + expect(out).not.toContain('npm test'); + expect(out).not.toContain(' { + const out = stripActionTagsForDisplay('์•ž ๋’ค'); + expect(out).not.toContain(' { + expect(stripActionTagsForDisplay('## 2. ๊ตฌ์กฐ\n- src/agent.ts')).toBe('## 2. ๊ตฌ์กฐ\n- src/agent.ts'); + }); + test('๋นˆ ์ž…๋ ฅ ์•ˆ์ „', () => { + expect(stripActionTagsForDisplay('')).toBe(''); + }); +});