release: v2.0.8 - UX Persistence & Per-Agent Knowledge Mix (2026-05-14)

This commit is contained in:
g1nation
2026-05-14 00:56:20 +09:00
parent 8104caf8d9
commit 147536fb13
19 changed files with 423 additions and 55 deletions
@@ -1,5 +1,5 @@
{
"result": "Final report with inconsistencies. This should be long enough to pass validation.",
"createdAt": 1778686589725,
"createdAt": 1778687752353,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "[CONFLICT WARNING] 성능이 200% 증가했습니다. vs 그러나 동시에 50% 감소했습니다. 최적화와 성능 저하가 동시에 발견됨.",
"createdAt": 1778686589724,
"createdAt": 1778687752352,
"modelVersion": "unknown"
}
@@ -1,5 +1,5 @@
{
"result": "Detailed Execution Plan: 1. Research 2. Analyze 3. Write report with high quality.",
"createdAt": 1778686589722,
"createdAt": 1778687752351,
"modelVersion": "unknown"
}
@@ -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"
}
@@ -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": {
+10
View File
@@ -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
- **텔레그램 결과물 추적 강화:** 텔레그램 보고서에 에이전트가 생성한 파일 경로(`*결과물:*`)와 세션 폴더 위치를 명시적으로 포함하여, 생성된 자산을 즉시 확인할 수 있도록 개선했습니다.
+15
View File
@@ -0,0 +1,15 @@
<!--
Astra activity-bar icon.
VS Code activity-bar conventions:
• Monochrome — colour comes from `fill="currentColor"` so the theme
engine handles active/inactive + light/dark variants.
• 24×24 viewBox (the canonical activity-bar size).
• Single path is preferred (cheap to render, no gradients).
Shape: the brand "✦" four-pointed sparkle, centred. Geometric so it
stays legible at 16px.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 1.5L13.7 9.3 21.5 11 13.7 12.7 12 20.5 10.3 12.7 2.5 11 10.3 9.3z"/>
</svg>

After

Width:  |  Height:  |  Size: 621 B

+31 -20
View File
@@ -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. */
+11 -10
View File
@@ -15,6 +15,15 @@
<button class="icon-btn" id="newChatBtn" data-tooltip="New Chat">New</button>
<button class="icon-btn toggle-chip active" id="brainTraceBtn" data-tooltip="Second Brain Trace Mode">Trace</button>
<button class="icon-btn toggle-chip" id="internetBtn" data-tooltip="Internet Access">Web</button>
<!--
Corp toggle — replaces the old pill-shaped chip that lived
in the records-line. Same rounded-rectangle style as the
other header buttons. Clicking toggles 1인 기업 모드 on/off;
the adjacent ▾ opens the manage overlay (agents · models ·
prompt edits · Knowledge Mix sliders).
-->
<button class="icon-btn toggle-chip" id="companyChip" data-tooltip="1인 기업 모드 토글">Corp</button>
<button class="icon-btn" id="companyManageBtn" data-tooltip="회사 관리 (에이전트·모델·prompt·Knowledge Mix)"></button>
<div class="hdr-dropdown" data-dd>
<button class="icon-btn" id="toolsMenuBtn" data-dd-trigger data-tooltip="Tools">Tools ▾</button>
<div class="hdr-menu" id="toolsMenu" data-dd-menu>
@@ -106,16 +115,8 @@
<span id="chronicleAutoStatus" title="Project records are saved automatically after meaningful project turns.">Auto Records</span>
<span class="rl-latest" id="recordsLatest"></span>
</div>
<!--
Company-mode chip. Click toggles enabled; the ▾ opens the manage
overlay. The chip stays visible at all times so the user can flip
into 1인 기업 mode from anywhere in the chat surface.
-->
<button class="company-chip" id="companyChip" data-active="false" title="1인 기업 모드 토글">
<span class="company-chip-icon">🏢</span>
<span class="company-chip-label" id="companyChipLabel">Company OFF</span>
</button>
<button class="icon-btn company-manage-btn" id="companyManageBtn" data-tooltip="회사 관리 (에이전트·모델 설정)"></button>
<!-- (Removed) Corp chip moved to the header toolbar above —
see #companyChip / #companyManageBtn alongside New/Trace/Web. -->
<div class="hdr-dropdown" data-dd>
<button class="icon-btn" id="recordsMenuBtn" data-dd-trigger data-tooltip="Chronicle records">Records ▾</button>
<div class="hdr-menu hdr-menu-wide" id="recordsMenu" data-dd-menu>
+99 -6
View File
@@ -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);
+24 -1
View File
@@ -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": {
+18
View File
@@ -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<never> = {
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 {
+60
View File
@@ -30,6 +30,7 @@ function _defaultState(): CompanyState {
activeAgentIds: DEFAULT_ACTIVE_AGENTS.slice(),
modelOverrides: {},
promptOverrides: {},
knowledgeMixOverrides: {},
};
}
@@ -75,11 +76,25 @@ function _normalize(raw: Partial<CompanyState> | 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<string, number> = {};
if (raw.knowledgeMixOverrides && typeof raw.knowledgeMixOverrides === 'object') {
for (const [agentId, v] of Object.entries(raw.knowledgeMixOverrides as Record<string, unknown>)) {
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<CompanyState> {
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
+62 -2
View File
@@ -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 (0100) — 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 `<create_file>` 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);
+2
View File
@@ -21,7 +21,9 @@ export {
setActiveAgents,
setAgentModelOverride,
setAgentPromptOverride,
setAgentKnowledgeMix,
resolveAgentPrompt,
resolveCompanyKnowledgeMix,
activeAgentIds,
isAgentActive,
modelForAgent,
+33
View File
@@ -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) {
+12
View File
@@ -76,6 +76,18 @@ export interface CompanyState {
* so changes apply on the very next turn — no restart required.
*/
promptOverrides: Record<string, AgentPromptOverride>;
/**
* Optional per-agent Knowledge Mix override — same semantics as the
* global `g1nation.knowledgeMix.secondBrainWeight` setting (0100, 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<string, number>;
}
/** Output of the CEO planner LLM call after JSON parsing. */
+15
View File
@@ -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
+15
View File
@@ -1397,9 +1397,13 @@ export class SidebarChatProvider implements vscode.WebviewViewProvider, BridgeIn
async _sendCompanyAgents(): Promise<void> {
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 `<create_file>` actually
// hit disk. Without this, agents would *claim* to create