release: v2.0.8 - UX Persistence & Per-Agent Knowledge Mix (2026-05-14)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 `<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);
|
||||
|
||||
|
||||
@@ -21,7 +21,9 @@ export {
|
||||
setActiveAgents,
|
||||
setAgentModelOverride,
|
||||
setAgentPromptOverride,
|
||||
setAgentKnowledgeMix,
|
||||
resolveAgentPrompt,
|
||||
resolveCompanyKnowledgeMix,
|
||||
activeAgentIds,
|
||||
isAgentActive,
|
||||
modelForAgent,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 (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<string, number>;
|
||||
}
|
||||
|
||||
/** Output of the CEO planner LLM call after JSON parsing. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user