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
+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