release: v2.0.8 - UX Persistence & Per-Agent Knowledge Mix (2026-05-14)
This commit is contained in:
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user