From 147536fb13600a40c2cb615fe4ba0a8ab57120f0 Mon Sep 17 00:00:00 2001 From: g1nation Date: Thu, 14 May 2026 00:56:20 +0900 Subject: [PATCH] release: v2.0.8 - UX Persistence & Per-Agent Knowledge Mix (2026-05-14) --- ...d46d2ca2057b05c488be1dcf439166ac5a9a1.json | 2 +- ...9f4f39d2bc368f77456c37b5eef9a94a66b5c.json | 2 +- ...5c7a44d7661af673b24e3f49551a7a2e50280.json | 2 +- ...adc543795e4b427b64540a49c9ab27c7fe213.json | 4 +- ...son => stress_conflict_1778687752337.json} | 22 ++-- PATCHNOTES.md | 10 ++ assets/icon-activitybar.svg | 15 +++ media/sidebar.css | 51 +++++---- media/sidebar.html | 21 ++-- media/sidebar.js | 105 +++++++++++++++++- package.json | 25 ++++- src/extension.ts | 18 +++ src/features/company/companyConfig.ts | 60 ++++++++++ src/features/company/dispatcher.ts | 64 ++++++++++- src/features/company/index.ts | 2 + src/features/company/promptBuilder.ts | 33 ++++++ src/features/company/types.ts | 12 ++ src/sidebar/chatHandlers.ts | 15 +++ src/sidebarProvider.ts | 15 +++ 19 files changed, 423 insertions(+), 55 deletions(-) rename .astra/tests/stress/.astra/missions/{stress_conflict_1778686589692.json => stress_conflict_1778687752337.json} (81%) create mode 100644 assets/icon-activitybar.svg diff --git a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json index 5786b09..717af1d 100644 --- a/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json +++ b/.astra/tests/stress/.astra/cache/259a37934ead3910a8722b82054d46d2ca2057b05c488be1dcf439166ac5a9a1.json @@ -1,5 +1,5 @@ { "result": "Final report with inconsistencies. This should be long enough to pass validation.", - "createdAt": 1778686589725, + "createdAt": 1778687752353, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json index 0e3ae76..dfd6dde 100644 --- a/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json +++ b/.astra/tests/stress/.astra/cache/65775be352df43297b63c7af59c9f4f39d2bc368f77456c37b5eef9a94a66b5c.json @@ -1,5 +1,5 @@ { "result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", - "createdAt": 1778686589724, + "createdAt": 1778687752352, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json index 76eb41a..b82653e 100644 --- a/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json +++ b/.astra/tests/stress/.astra/cache/6894d26c5b0a55d25d756a473225c7a44d7661af673b24e3f49551a7a2e50280.json @@ -1,5 +1,5 @@ { "result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", - "createdAt": 1778686589722, + "createdAt": 1778687752351, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json index 31fba0f..5fb06c8 100644 --- a/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json +++ b/.astra/tests/stress/.astra/cache/88cb61499f88ed38165b64bd3e8adc543795e4b427b64540a49c9ab27c7fe213.json @@ -1,5 +1,5 @@ { - "result": "---\nid: stress_conflict_1778686589692\ndate: 2026-05-13T15:36:29.726Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (20ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (11ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (2ms)\n", - "createdAt": 1778686589726, + "result": "---\nid: stress_conflict_1778687752337\ndate: 2026-05-13T15:55:52.353Z\ntype: knowledge_artifact\nstandard: P-Reinforce v3.0\ntags: [automated, connect_ai, brain_sync]\n---\n\n## 📌 Brief Summary\nFinal report with inconsistencies. This should be long enough to pass validation.\n\nFinal report with inconsistencies. This should be long enough to pass validation.\n\n---\n## 💡 Astra의 선제적 제안 (Proactive Next Actions)\nFinal report with inconsistencies. This should be long enough to pass validation.\n---\n## 🛡️ Reliability & Audit Summary\n> [!NOTE]\n> 이 문서는 ConnectAI의 **Intelligent Resilience** 엔진에 의해 검증 및 정제되었습니다.\n\n| Metric | Value | Status |\n| :--- | :--- | :--- |\n| **Conflict Risk** | `60/100` | ⚠️ Medium |\n| **Fallbacks Used** | `0` | ✅ None |\n| **Auto Retries** | `0` | ✅ Stable |\n| **Deduplication** | `0` | Standard |\n| **Processing Time** | `0.0s` | ✅ Fast |\n\n### 🔍 Decision Audit Trail\n- **[PLANNER]** 전략 수립 중... (13ms)\n- **[RESEARCHER]** 핵심 정보 수집 및 분석 중... (2ms)\n- **[WRITER]** 최종 리포트 작성 및 편집 중... (1ms)\n", + "createdAt": 1778687752353, "modelVersion": "unknown" } \ No newline at end of file diff --git a/.astra/tests/stress/.astra/missions/stress_conflict_1778686589692.json b/.astra/tests/stress/.astra/missions/stress_conflict_1778687752337.json similarity index 81% rename from .astra/tests/stress/.astra/missions/stress_conflict_1778686589692.json rename to .astra/tests/stress/.astra/missions/stress_conflict_1778687752337.json index 5902755..ba380df 100644 --- a/.astra/tests/stress/.astra/missions/stress_conflict_1778686589692.json +++ b/.astra/tests/stress/.astra/missions/stress_conflict_1778687752337.json @@ -1,8 +1,8 @@ { - "missionId": "stress_conflict_1778686589692", + "missionId": "stress_conflict_1778687752337", "status": "completed", - "startTime": "2026-05-13T15:36:29.692Z", - "totalElapsedMs": 34, + "startTime": "2026-05-13T15:55:52.337Z", + "totalElapsedMs": 16, "results": { "planner": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.", "researcher": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.", @@ -16,30 +16,30 @@ { "from": "idle", "to": "planner", - "durationMs": 20, + "durationMs": 13, "message": "전략 수립 중...", - "ts": "2026-05-13T15:36:29.712Z" + "ts": "2026-05-13T15:55:52.350Z" }, { "from": "planner", "to": "researcher", - "durationMs": 11, + "durationMs": 2, "message": "핵심 정보 수집 및 분석 중...", - "ts": "2026-05-13T15:36:29.723Z" + "ts": "2026-05-13T15:55:52.352Z" }, { "from": "researcher", "to": "writer", - "durationMs": 2, + "durationMs": 1, "message": "최종 리포트 작성 및 편집 중...", - "ts": "2026-05-13T15:36:29.725Z" + "ts": "2026-05-13T15:55:52.353Z" }, { "from": "writer", "to": "completed", - "durationMs": 1, + "durationMs": 0, "message": "미션 완료", - "ts": "2026-05-13T15:36:29.726Z" + "ts": "2026-05-13T15:55:52.353Z" } ], "resilienceMetrics": { diff --git a/PATCHNOTES.md b/PATCHNOTES.md index e2feb8d..c3e7b87 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,5 +1,15 @@ # Astra Patch Notes +## v2.0.8 (2026-05-14) +### 🚀 UX Persistence & Per-Agent Knowledge Mix +- **Astra Launcher 도입:** 실수로 채팅 탭을 닫았을 때 사이드바에서 즉시 다시 열 수 있는 전용 런처 뷰를 추가하여 접근성을 높였습니다. +- **에이전트별 지식 믹스(Knowledge Mix) 오버라이드:** 비즈니스 에이전트마다 '세컨드 브레인' 지식 활용 비중을 개별적으로 설정할 수 있는 기능을 도입했습니다. 이제 각 전문가 에이전트의 특성에 맞춰 지식 검색 깊이를 조절할 수 있습니다. +- **사이드바 UI 및 인터랙션 정교화:** 사이드바(`sidebar.js`, `sidebar.css`)의 디자인을 개선하고, 비즈니스 워크플로우에서의 상태 시각화를 최적화했습니다. +- **시스템 안정성 강화:** 익스텐션 코어와 사이드바 간의 상태 동기화 로직을 보완하여 장시간 사용 시의 신뢰성을 확보했습니다. +- **신규 패키징:** `astra-2.0.8.vsix` 패키지를 통해 사용자 편의성이 극대화된 새로운 버전을 배포합니다. + +--- + ## v2.0.7 (2026-05-14) ### 📢 Enhanced Telegram Reporting & File Visibility - **텔레그램 결과물 추적 강화:** 텔레그램 보고서에 에이전트가 생성한 파일 경로(`*결과물:*`)와 세션 폴더 위치를 명시적으로 포함하여, 생성된 자산을 즉시 확인할 수 있도록 개선했습니다. diff --git a/assets/icon-activitybar.svg b/assets/icon-activitybar.svg new file mode 100644 index 0000000..32fabe0 --- /dev/null +++ b/assets/icon-activitybar.svg @@ -0,0 +1,15 @@ + + + + diff --git a/media/sidebar.css b/media/sidebar.css index 4853eb3..6655e81 100644 --- a/media/sidebar.css +++ b/media/sidebar.css @@ -321,26 +321,10 @@ .input-footer { display: flex; align-items: center; justify-content: space-between; } .footer-left { display: flex; align-items: center; gap: 8px; } - /* Company chip — sits in the records-line beside the Records ▾ menu. */ - .company-chip { - display: inline-flex; align-items: center; gap: 5px; - background: var(--surface); - border: 1px solid var(--border); - border-radius: 999px; - padding: 3px 10px; - color: var(--text-dim); - font-size: 11px; font-weight: 600; - cursor: pointer; - transition: all 0.15s ease; - } - .company-chip:hover { border-color: var(--border-bright); color: var(--text-primary); } - .company-chip[data-active="true"] { - background: var(--accent-glow); - border-color: var(--accent); - color: var(--accent); - } - .company-chip-icon { font-size: 12px; } - .company-manage-btn { padding: 2px 6px; font-size: 11px; margin-left: 2px; } + /* (Removed) Pill-shaped `.company-chip` / `.company-manage-btn` — + the Corp toggle now lives in the header toolbar reusing the + existing `.icon-btn.toggle-chip` rounded-rectangle look. See + #companyChip in sidebar.html. */ .company-name-input { flex: 1; background: var(--input-bg); border: 1px solid var(--border); border-radius: 6px; padding: 6px 10px; color: var(--text-primary); font-size: 12px; @@ -407,6 +391,33 @@ background: var(--accent-glow); } + /* Per-agent Knowledge Mix slider row — sits between the controls + row and the (collapsed) prompt editor. Indent matches the + prompt-editor indent (38px) so the emoji stays as the visual + "ruler" for everything that follows. */ + .company-agent-mix-row { + display: flex; align-items: center; gap: 8px; + margin: 6px 0 0 38px; + font-size: 10px; color: var(--text-dim); + } + .company-agent-mix-label { flex-shrink: 0; } + .company-agent-mix-slider { + flex: 1; min-width: 0; + accent-color: var(--accent); + cursor: pointer; + } + .company-agent-mix-slider:disabled { opacity: 0.5; cursor: not-allowed; } + .company-agent-mix-hint { + flex-shrink: 0; font-size: 9.5px; color: var(--text-dim); + min-width: 165px; text-align: right; + } + .company-agent-mix-cbwrap { + display: inline-flex; align-items: center; gap: 3px; + font-size: 9.5px; cursor: pointer; color: var(--text-dim); + flex-shrink: 0; + } + .company-agent-mix-cbwrap input[type="checkbox"] { accent-color: var(--accent); } + /* Expandable prompt editor under each agent card. Toggled via the Edit button. Three textareas (tagline / specialty / persona) + Reset / Save / Cancel — empty save clears that field's override. */ diff --git a/media/sidebar.html b/media/sidebar.html index d9bc85c..8d4e191 100644 --- a/media/sidebar.html +++ b/media/sidebar.html @@ -15,6 +15,15 @@ + + +
@@ -106,16 +115,8 @@ Auto Records
- - - +
diff --git a/media/sidebar.js b/media/sidebar.js index 2be9fda..cace912 100644 --- a/media/sidebar.js +++ b/media/sidebar.js @@ -1432,7 +1432,6 @@ // model overrides. State round-trips through `companyStatus` / // `companyAgents` messages so the webview and extension stay in sync. const _companyChip = document.getElementById('companyChip'); - const _companyChipLabel = document.getElementById('companyChipLabel'); const _companyManageBtn = document.getElementById('companyManageBtn'); const _companyOverlay = document.getElementById('companyOverlay'); const _closeCompanyBtns = [ @@ -1444,17 +1443,29 @@ const _companyAgentList = document.getElementById('companyAgentList'); const _companyStatusEl = document.getElementById('companyStatus'); + /** + * Chip lives in the main header toolbar now, so it uses the same + * `icon-btn.active` styling as `brainTraceBtn` / `internetBtn`. + * Detail (company name + agent count) goes in the tooltip — the + * label stays a constant "Corp" so the toolbar tone-and-manner + * isn't broken by a wildly varying-width chip. + */ const renderCompanyChip = (active, summary) => { - if (!_companyChip || !_companyChipLabel) return; - _companyChip.setAttribute('data-active', active ? 'true' : 'false'); - _companyChipLabel.textContent = active ? (summary || 'Company ON') : 'Company OFF'; + if (!_companyChip) return; + _companyChip.classList.toggle('active', !!active); + _companyChip.setAttribute( + 'data-tooltip', + active + ? `1인 기업 ON · ${summary || ''}`.trim() + : '1인 기업 모드 OFF — 클릭해서 켜기', + ); }; if (_companyChip) { _companyChip.onclick = () => { - const isActive = _companyChip.getAttribute('data-active') === 'true'; + const isActive = _companyChip.classList.contains('active'); // Optimistic flip — backend echoes the canonical state back. - renderCompanyChip(!isActive, _companyChipLabel?.textContent || ''); + renderCompanyChip(!isActive, ''); vscode.postMessage({ type: 'setCompanyEnabled', value: !isActive }); }; } @@ -1612,6 +1623,15 @@ row.appendChild(controls); li.appendChild(row); + // ── Row 1.5: per-agent Knowledge Mix slider ── + // CEO doesn't dispatch agents itself, it only synthesises, + // so the brain mix for CEO turns is governed by the + // *specialist* it dispatched — exposing the slider for CEO + // would just be a confusing dead control. + if (a.id !== 'ceo') { + li.appendChild(_buildAgentKnowledgeMixSlider(a, payload.globalKnowledgeMixWeight)); + } + // ── Row 2 (collapsed by default): prompt editor ── li.appendChild(_buildAgentPromptEditor(a)); _companyAgentList.appendChild(li); @@ -1619,6 +1639,79 @@ if (_companyStatusEl) _companyStatusEl.textContent = ''; } + /** + * Inline Knowledge Mix slider for one agent. Empty (override = null) + * means "use the global slider value" — shown as a hint label so the + * user knows what they'll fall back to. Slider commits on `change` + * (not `input`) so dragging doesn't spam writes. + */ + function _buildAgentKnowledgeMixSlider(a, globalWeight) { + const row = document.createElement('div'); + row.className = 'company-agent-mix-row'; + const usingOverride = a.knowledgeMixOverride !== null && a.knowledgeMixOverride !== undefined; + const effective = a.effectiveKnowledgeMixWeight; + const label = document.createElement('span'); + label.className = 'company-agent-mix-label'; + label.textContent = '🎚 Knowledge Mix'; + const slider = document.createElement('input'); + slider.type = 'range'; + slider.min = '0'; slider.max = '100'; slider.step = '5'; + slider.value = String(effective); + slider.disabled = !usingOverride; + slider.className = 'company-agent-mix-slider'; + const hint = document.createElement('span'); + hint.className = 'company-agent-mix-hint'; + const renderHint = () => { + const w = parseInt(slider.value, 10) || 50; + const tag = usingOverride + ? `override · Model ${100 - w}% / Brain ${w}%` + : `global · Model ${100 - effective}% / Brain ${effective}%`; + hint.textContent = tag; + }; + const cb = document.createElement('input'); + cb.type = 'checkbox'; cb.checked = !usingOverride; + cb.className = 'company-agent-mix-cb'; + cb.title = '글로벌 슬라이더 값 사용'; + cb.onchange = () => { + if (cb.checked) { + // Reset to global. + slider.disabled = true; + slider.value = String(globalWeight ?? 50); + vscode.postMessage({ + type: 'setCompanyAgentKnowledgeMix', + agentId: a.id, value: null, + }); + } else { + // Take ownership at the current displayed value. + slider.disabled = false; + const w = parseInt(slider.value, 10) || 50; + vscode.postMessage({ + type: 'setCompanyAgentKnowledgeMix', + agentId: a.id, value: w, + }); + } + }; + slider.addEventListener('input', renderHint); + slider.addEventListener('change', () => { + if (cb.checked) return; // safety: shouldn't fire when disabled + const w = parseInt(slider.value, 10) || 50; + vscode.postMessage({ + type: 'setCompanyAgentKnowledgeMix', + agentId: a.id, value: w, + }); + }); + renderHint(); + const cbWrap = document.createElement('label'); + cbWrap.className = 'company-agent-mix-cbwrap'; + cbWrap.appendChild(cb); + cbWrap.appendChild(document.createTextNode(' use global')); + row.appendChild(label); + row.appendChild(slider); + row.appendChild(hint); + row.appendChild(cbWrap); + return row; + } + /** * Build the per-agent prompt editor. Hidden until the user clicks * the Edit button. Three fields (tagline / specialty / persona); diff --git a/package.json b/package.json index ab4299f..1f08537 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.0.7", + "version": "2.0.8", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", @@ -148,6 +148,29 @@ } ] }, + "viewsContainers": { + "activitybar": [ + { + "id": "astra-activity", + "title": "Astra", + "icon": "assets/icon-activitybar.svg" + } + ] + }, + "views": { + "astra-activity": [ + { + "id": "astra-launcher", + "name": "Astra Launcher" + } + ] + }, + "viewsWelcome": [ + { + "view": "astra-launcher", + "contents": "✦ **Astra** — 로컬 AI 인텔리전스 레이어\n\nChat 탭을 닫았을 때 여기서 다시 열 수 있습니다.\n\n[$(comment-discussion) Open Chat](command:g1nation.openChat)\n[$(add) New Chat](command:g1nation.newChat)\n[$(gear) Settings](command:g1nation.settings.focus)\n\n---\n\n**1인 기업 모드**\n\n[$(organization) Manage Agents](command:g1nation.company.manage)\n[$(folder-opened) Open Sessions Folder](command:g1nation.company.openSessions)\n\n---\n\n**Project Architecture**\n\n[$(file-text) Open Architecture Doc](command:g1nation.architecture.open)\n[$(refresh) Refresh Architecture](command:g1nation.architecture.refresh)\n\n---\n\n**Lessons / Knowledge**\n\n[$(lightbulb) Manage Lessons](command:g1nation.lesson.manage)\n[$(edit) Edit Agent ↔ Knowledge Map](command:g1nation.skills.editKnowledgeMap)" + } + ], "configuration": { "title": "Astra", "properties": { diff --git a/src/extension.ts b/src/extension.ts index c67fbb6..f4a7db5 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -156,6 +156,24 @@ export async function activate(context: vscode.ExtensionContext) { }) ); + // ── Activity Bar launcher view ──────────────────────────────────────── + // Adds a sparkle (✦) icon to VS Code's left activity bar. Clicking it + // opens a small sidebar with action buttons (Open Chat / New Chat / + // Settings / Company / Architecture). Solves the user-reported pain + // point: when the Astra Chat editor tab is accidentally closed, there + // was no one-click way to reopen it short of restarting the extension. + // + // The view itself has no items — VS Code renders the `viewsWelcome` + // content from package.json instead, which is just a list of command + // links. Cheap, theme-aware, no webview to maintain. + const astraLauncherProvider: vscode.TreeDataProvider = { + getTreeItem: () => new vscode.TreeItem(''), + getChildren: () => [], + }; + context.subscriptions.push( + vscode.window.registerTreeDataProvider('astra-launcher', astraLauncherProvider), + ); + // 4. Initialize Bridge Server (Port 4825) const bridge = new BridgeServer(provider); try { diff --git a/src/features/company/companyConfig.ts b/src/features/company/companyConfig.ts index a9212eb..857a934 100644 --- a/src/features/company/companyConfig.ts +++ b/src/features/company/companyConfig.ts @@ -30,6 +30,7 @@ function _defaultState(): CompanyState { activeAgentIds: DEFAULT_ACTIVE_AGENTS.slice(), modelOverrides: {}, promptOverrides: {}, + knowledgeMixOverrides: {}, }; } @@ -75,11 +76,25 @@ function _normalize(raw: Partial | undefined): CompanyState { } } } + // Knowledge-mix overrides — validate that values are integers in [0,100] + // and the agent id is a known one. Anything else is dropped silently so + // a hand-edited globalState doesn't put garbage into the resolver. + const knowledgeMixOverrides: Record = {}; + if (raw.knowledgeMixOverrides && typeof raw.knowledgeMixOverrides === 'object') { + for (const [agentId, v] of Object.entries(raw.knowledgeMixOverrides as Record)) { + if (!getCompanyAgent(agentId)) continue; + if (typeof v === 'number' && Number.isFinite(v)) { + const w = Math.max(0, Math.min(100, Math.round(v))); + knowledgeMixOverrides[agentId] = w; + } + } + } return { enabled, companyName, activeAgentIds: withoutCeo, modelOverrides: overrides, promptOverrides, + knowledgeMixOverrides, }; } @@ -199,6 +214,28 @@ export async function setAgentPromptOverride( return next; } +/** + * Set / clear a per-agent Knowledge Mix override. Pass `null` to revert + * the agent back to the global default. Anything outside `[0, 100]` is + * clamped — sliders that overshoot don't corrupt persistence. + */ +export async function setAgentKnowledgeMix( + context: vscode.ExtensionContext, + agentId: string, + weight: number | null, +): Promise { + const cur = readCompanyState(context); + const overrides = { ...cur.knowledgeMixOverrides }; + if (weight === null || weight === undefined || !Number.isFinite(weight)) { + delete overrides[agentId]; + } else { + overrides[agentId] = Math.max(0, Math.min(100, Math.round(weight))); + } + const next: CompanyState = { ...cur, knowledgeMixOverrides: overrides }; + await writeCompanyState(context, next); + return next; +} + // ── Derived helpers (no I/O) ──────────────────────────────────────────────── /** @@ -242,6 +279,29 @@ export function summarizeForChip(state: CompanyState): string { return `${state.companyName} · ${count} agents`; } +/** + * Resolve the effective Knowledge Mix weight for a specific company agent. + * Precedence: per-agent override → global `g1nation.knowledgeMix.secondBrainWeight` + * → default 50. Returns weight + the source label, mirroring + * `resolveKnowledgeMix` in `src/retrieval/knowledgeMix.ts` so downstream + * code can read both the *value* and *where it came from* (handy for UI + * hints and the scope footer). + */ +export function resolveCompanyKnowledgeMix( + state: CompanyState, + agentId: string, + globalDefault: number, +): { weight: number; source: 'agent' | 'global' | 'default' } { + const override = state.knowledgeMixOverrides?.[agentId]; + if (typeof override === 'number' && Number.isFinite(override)) { + return { weight: Math.max(0, Math.min(100, Math.round(override))), source: 'agent' }; + } + if (typeof globalDefault === 'number' && Number.isFinite(globalDefault)) { + return { weight: Math.max(0, Math.min(100, Math.round(globalDefault))), source: 'global' }; + } + return { weight: 50, source: 'default' }; +} + /** * Resolve the *effective* prompt fields for an agent — merge the static * default from `agents.ts` with any user-saved override. Returns plain diff --git a/src/features/company/dispatcher.ts b/src/features/company/dispatcher.ts index 6e923e4..86c8ec1 100644 --- a/src/features/company/dispatcher.ts +++ b/src/features/company/dispatcher.ts @@ -32,9 +32,15 @@ */ import * as vscode from 'vscode'; import { IAIService } from '../../core/services'; -import { logError, logInfo } from '../../utils'; +import { getActiveBrainProfile, logError, logInfo } from '../../utils'; +import { retrieveScoped, buildContextBlock } from '../../skills/scopedBrainRetriever'; +import { resolveScopeForAgent } from '../../skills/agentKnowledgeMap'; +import { + mapWeightToBrainFileLimit, + buildKnowledgeMixPolicy, +} from '../../retrieval/knowledgeMix'; import { getCompanyAgent } from './agents'; -import { modelForAgent, readCompanyState } from './companyConfig'; +import { modelForAgent, readCompanyState, resolveCompanyKnowledgeMix } from './companyConfig'; import { runCeoPlanner } from './ceoPlanner'; import { runCeoReporter } from './ceoReporter'; import { buildSpecialistPrompt } from './promptBuilder'; @@ -86,6 +92,18 @@ export interface DispatcherDeps { ai: IAIService; /** Default model to fall back to when an agent has no override. */ defaultModel: string; + /** + * Global Knowledge Mix weight (0–100) — fallback when an agent has no + * per-agent override. Mirrors `g1nation.knowledgeMix.secondBrainWeight`. + */ + globalKnowledgeMixWeight: number; + /** + * Baseline number of brain files to retrieve at weight=50 (balanced). + * The actual count is `mapWeightToBrainFileLimit(weight, baseline)`. + * Pass the same value the chat path uses (`config.memoryLongTermFiles`) + * so company-mode behaviour stays in sync. + */ + brainFileBaseline?: number; /** * Apply ConnectAI's action-tag executor to the specialist's raw response. * Without this hook, agent outputs containing `` etc. would @@ -266,10 +284,52 @@ async function _dispatchOne( }; }); + // ── Second Brain RAG for this specialist ──────────────────────────────── + // The non-company chat path uses `AgentExecutor.buildMemoryContext` to + // pull RAG chunks before every LLM call. The dispatcher used to skip + // that entirely, leaving company agents *blind* to the user's stored + // knowledge — which made the Knowledge Mix slider effectively a no-op + // for company turns. We now run a lightweight scoped retrieval here so + // every dispatch sees the same brain the user expects, weighted by the + // agent's own Knowledge Mix. + const { weight: knowledgeWeight, source: knowledgeMixSource } = + resolveCompanyKnowledgeMix(state, agentId, deps.globalKnowledgeMixWeight); + const brainFileLimit = mapWeightToBrainFileLimit(knowledgeWeight, deps.brainFileBaseline ?? 6); + let brainContext = ''; + if (brainFileLimit > 0) { + try { + const brain = getActiveBrainProfile(); + const brainRoot = brain?.localBrainPath || ''; + if (brainRoot) { + // Reuse the agent ↔ knowledge map: if the same agent name + // appears there (free-form .md path or canonical id), we + // honour its `knowledgeFolders` scope. Otherwise we search + // the whole brain so a missing mapping doesn't starve the + // dispatcher. + const scope = resolveScopeForAgent(agentId, brainRoot); + const retrieval = retrieveScoped(task, brainRoot, scope.folders, { + maxResults: brainFileLimit, + }); + brainContext = buildContextBlock(retrieval); + } + } catch (e: any) { + logError('company.dispatcher: RAG retrieval failed; continuing without brain context.', { + agentId, error: e?.message ?? String(e), + }); + } + } + const policyBlock = buildKnowledgeMixPolicy({ + weight: knowledgeWeight, + source: knowledgeMixSource, + agent: def.name, + }); + const system = buildSpecialistPrompt({ agentId, state, agentMemory: memory, sharedDecisions: decisions, peerOutputs, + brainContext, // injected as `[SECOND BRAIN CONTEXT]` block + knowledgeMixPolicy: policyBlock, // injected as `[KNOWLEDGE MIX POLICY]` block }); const model = modelForAgent(state, agentId, deps.defaultModel); diff --git a/src/features/company/index.ts b/src/features/company/index.ts index 7c40f2b..38c15cf 100644 --- a/src/features/company/index.ts +++ b/src/features/company/index.ts @@ -21,7 +21,9 @@ export { setActiveAgents, setAgentModelOverride, setAgentPromptOverride, + setAgentKnowledgeMix, resolveAgentPrompt, + resolveCompanyKnowledgeMix, activeAgentIds, isAgentActive, modelForAgent, diff --git a/src/features/company/promptBuilder.ts b/src/features/company/promptBuilder.ts index f29441f..b9f6b33 100644 --- a/src/features/company/promptBuilder.ts +++ b/src/features/company/promptBuilder.ts @@ -34,6 +34,19 @@ export interface SpecialistPromptInputs { * again so we don't double-pay tokens for one transformation. */ peerOutputs?: Array<{ agentId: string; agentName: string; emoji: string; content: string }>; + /** + * Pre-rendered Second Brain context block (from `buildContextBlock` in + * scopedBrainRetriever). Empty when retrieval found nothing or + * Knowledge Mix weight is 0. Inserted as evidence the specialist can + * cite — `[제2뇌 컨텍스트 ...]` header already included by the builder. + */ + brainContext?: string; + /** + * Pre-rendered Knowledge Mix policy block (from `buildKnowledgeMixPolicy`). + * Empty when the weight is the neutral 50 (no policy needed). + * Tells the specialist how heavily to rely on the brain context. + */ + knowledgeMixPolicy?: string; } /** @@ -124,6 +137,26 @@ export function buildSpecialistPrompt(inputs: SpecialistPromptInputs): string { } } + // ── Second Brain context (RAG) ── + // Pre-rendered by the dispatcher via `retrieveScoped + buildContextBlock`. + // The block already carries its own `[제2뇌 컨텍스트 ...]` header so we + // just sandwich it. Empty when retrieval yielded nothing. + const brain = (inputs.brainContext ?? '').trim(); + if (brain) { + parts.push(''); + parts.push(brain); + } + + // ── Knowledge Mix policy ── + // Tells the agent how to *use* the Second Brain context above (or + // ignore it when the weight is low). Skipped at the neutral weight 50 + // so we don't add noise when there's nothing distinctive to say. + const policy = (inputs.knowledgeMixPolicy ?? '').trim(); + if (policy) { + parts.push(''); + parts.push(policy); + } + // ── Long-term memory ── const memory = (inputs.agentMemory ?? '').trim(); if (memory) { diff --git a/src/features/company/types.ts b/src/features/company/types.ts index 182d127..4019cda 100644 --- a/src/features/company/types.ts +++ b/src/features/company/types.ts @@ -76,6 +76,18 @@ export interface CompanyState { * so changes apply on the very next turn — no restart required. */ promptOverrides: Record; + /** + * Optional per-agent Knowledge Mix override — same semantics as the + * global `g1nation.knowledgeMix.secondBrainWeight` setting (0–100, where + * higher means "lean on Second Brain evidence harder"). Missing key → + * fall back to the global. Stored as integer. + * + * Why per-agent matters here: a Developer dispatch usually wants high + * model knowledge (general coding patterns) and minimal brain noise, + * while a Researcher / Writer benefits from heavy brain retrieval + * because their job is to cite recorded knowledge. + */ + knowledgeMixOverrides: Record; } /** Output of the CEO planner LLM call after JSON parsing. */ diff --git a/src/sidebar/chatHandlers.ts b/src/sidebar/chatHandlers.ts index 62310e4..bd0baa7 100644 --- a/src/sidebar/chatHandlers.ts +++ b/src/sidebar/chatHandlers.ts @@ -203,6 +203,21 @@ export async function handleChatMessage(provider: SidebarChatProvider, data: any } return true; } + case 'setCompanyAgentKnowledgeMix': { + // Per-agent Knowledge Mix override. `null`/missing value falls + // back to the global slider. The dispatcher reads this on the + // *next* turn — no restart required. + const { setAgentKnowledgeMix } = await import('../features/company'); + const agentId = typeof data.agentId === 'string' ? data.agentId : ''; + if (!agentId) return true; + const raw = data.value; + const weight = (raw === null || raw === undefined || !Number.isFinite(Number(raw))) + ? null + : Math.max(0, Math.min(100, Math.round(Number(raw)))); + await setAgentKnowledgeMix(provider._context, agentId, weight); + await provider._sendCompanyAgents(); + return true; + } case 'setCompanyAgentPrompt': { // Patch one agent's persona / specialty / tagline. Each field is // optional in the payload; passing an *empty string* explicitly diff --git a/src/sidebarProvider.ts b/src/sidebarProvider.ts index 4f7e30b..223d752 100644 --- a/src/sidebarProvider.ts +++ b/src/sidebarProvider.ts @@ -1397,9 +1397,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn async _sendCompanyAgents(): Promise { if (!this._view) return; const state = readCompanyState(this._context); + const cfg = getConfig(); + const globalWeight = cfg.knowledgeMixSecondBrainWeight ?? 50; const agents = COMPANY_AGENT_ORDER.map((id) => { const def = COMPANY_AGENTS[id]; const override = state.promptOverrides[id] || {}; + const kmOverride = state.knowledgeMixOverrides[id]; + const hasKmOverride = typeof kmOverride === 'number'; return { id, name: def.name, @@ -1421,12 +1425,17 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn personaOverridden: !!override.persona, specialtyOverridden: !!override.specialty, taglineOverridden: !!override.tagline, + // Knowledge Mix — null when using global default, number otherwise. + knowledgeMixOverride: hasKmOverride ? kmOverride : null, + // What the dispatcher *will actually use* this turn (for hint UI). + effectiveKnowledgeMixWeight: hasKmOverride ? kmOverride : globalWeight, }; }); this._view.webview.postMessage({ type: 'companyAgents', value: { companyName: state.companyName, + globalKnowledgeMixWeight: globalWeight, agents, }, }); @@ -1449,6 +1458,12 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn context: this._context, ai, defaultModel: cfg.defaultModel || 'gemma4:e2b', + // Knowledge Mix wiring so company specialists *also* see the + // user's Second Brain — same global default + per-agent + // override semantics the chat path uses. Without this the + // Knowledge Mix slider had no effect on company turns. + globalKnowledgeMixWeight: cfg.knowledgeMixSecondBrainWeight ?? 50, + brainFileBaseline: cfg.memoryLongTermFiles ?? 6, // Hand the dispatcher a thunk into ConnectAI's action-tag // executor so specialist outputs like `` actually // hit disk. Without this, agents would *claim* to create