release: v2.0.1 - Advanced Knowledge Mix & Architectural Intelligence

This commit is contained in:
g1nation
2026-05-13 22:05:39 +09:00
parent c32b17377b
commit e85e11aac6
23 changed files with 1758 additions and 78 deletions
+22 -14
View File
@@ -98,23 +98,31 @@ export class RetrievalOrchestrator {
fusionLog.push(`Expanded tokens: [${expandedTokens.slice(0, 15).join(', ')}]`);
// ── ① Brain File Search (TF-IDF enhanced, optionally hybrid with embeddings) ──
// `brainFileLimit === 0` is meaningful (Knowledge Mix "model knowledge only"
// mode), so use `??` rather than `||`. When the caller explicitly passes 0,
// we skip retrieval entirely instead of falling back to the default of 8.
const scopeFolders = options.scopeFolders ?? [];
const brainChunks = this.searchBrainFiles(
query,
expandedTokens,
options.brain,
options.brainFileLimit || 8,
options.includeRawConversations || false,
scopeFolders,
options.queryEmbedding,
options.embeddingModel,
options.embeddingBlendAlpha
);
const brainFileLimit = options.brainFileLimit ?? 8;
const brainChunks = brainFileLimit > 0
? this.searchBrainFiles(
query,
expandedTokens,
options.brain,
brainFileLimit,
options.includeRawConversations || false,
scopeFolders,
options.queryEmbedding,
options.embeddingModel,
options.embeddingBlendAlpha
)
: [];
allChunks.push(...brainChunks);
fusionLog.push(
scopeFolders.length > 0
? `Brain search (scoped to ${scopeFolders.length} folder(s)): ${brainChunks.length} chunks`
: `Brain search: ${brainChunks.length} chunks found`
brainFileLimit === 0
? 'Brain search: skipped (Knowledge Mix weight = 0)'
: scopeFolders.length > 0
? `Brain search (scoped to ${scopeFolders.length} folder(s)): ${brainChunks.length} chunks`
: `Brain search: ${brainChunks.length} chunks found`
);
// ── ② Memory Layers ──
+161
View File
@@ -0,0 +1,161 @@
/**
* Knowledge Mix — controls how much the assistant leans on Second Brain
* evidence vs. the model's own general knowledge for a given query.
*
* The single integer "secondBrainWeight" (0100) drives three things:
*
* 1. RAG chunk budget — how many brain files we feed the model.
* 2. Retrieval ratio — what fraction of the context budget RAG can claim.
* 3. Prompt policy — natural-language instruction injected into the
* system prompt telling the model how to balance
* its own knowledge against the evidence shown.
*
* Per-agent overrides (AgentKnowledgeEntry.secondBrainWeight) win over the
* global config (g1nation.knowledgeMix.secondBrainWeight). Both fall back to
* the default `DEFAULT_WEIGHT` (balanced) when nothing is set.
*
* Keeping this module isolated and pure makes it trivial to unit-test the
* mapping curve and to extend it later (e.g. add a "creative" axis) without
* touching retrieval or prompt assembly.
*/
import { getConfig } from '../config';
import { getOrCreateAgentEntry } from '../skills/agentKnowledgeMap';
export const DEFAULT_WEIGHT = 50;
/** Where the resolved weight came from — surfaced in `_lastRetrievalInfo` for UX. */
export type KnowledgeMixSource = 'agent' | 'global' | 'default';
export interface ResolvedKnowledgeMix {
/** Integer in [0, 100]. */
weight: number;
source: KnowledgeMixSource;
/** Agent name when `source === 'agent'`, else undefined. */
agent?: string;
}
/**
* Resolve the effective weight for the active turn.
*
* Precedence: per-agent → global config → default. Out-of-range values from
* either source are clamped, never silently zeroed (a typo in JSON should not
* disable retrieval entirely).
*/
export function resolveKnowledgeMix(agentFileOrName?: string): ResolvedKnowledgeMix {
if (agentFileOrName && agentFileOrName !== 'none') {
try {
const entry = getOrCreateAgentEntry(agentFileOrName);
if (typeof entry.secondBrainWeight === 'number' && Number.isFinite(entry.secondBrainWeight)) {
return {
weight: _clamp(entry.secondBrainWeight),
source: 'agent',
agent: entry.name,
};
}
} catch {
// Map missing or unreadable — fall through to global.
}
}
try {
const cfg = getConfig();
if (typeof cfg.knowledgeMixSecondBrainWeight === 'number') {
return { weight: _clamp(cfg.knowledgeMixSecondBrainWeight), source: 'global' };
}
} catch {
// getConfig should never throw in practice, but keep this safe in tests.
}
return { weight: DEFAULT_WEIGHT, source: 'default' };
}
function _clamp(n: number): number {
if (!Number.isFinite(n)) return DEFAULT_WEIGHT;
return Math.max(0, Math.min(100, Math.round(n)));
}
/**
* Map a weight to the maximum number of brain files (long-term memory) the
* retriever is allowed to consider for this turn.
*
* Curve was chosen so that:
* - 0 fully disables brain-file retrieval (model-only mode).
* - 50 maps to roughly the existing default (`memoryLongTermFiles`), so
* behaviour without any per-agent setting matches the status quo.
* - 100 pushes the cap toward `selectWithinBudget`'s hard ceiling (12).
*
* The configured `memoryLongTermFiles` is treated as the "balanced" baseline:
* it's scaled up at high weights and damped at low weights.
*/
export function mapWeightToBrainFileLimit(weight: number, configuredLimit: number): number {
const w = _clamp(weight);
if (w === 0) return 0;
const baseline = Math.max(1, configuredLimit || 6);
// Linear interpolation:
// w=0 → 0
// w=25 → baseline * 0.5
// w=50 → baseline
// w=75 → baseline * 1.5
// w=100 → baseline * 2 (capped at 12 elsewhere)
const scaled = Math.round((w / 50) * baseline);
// Honour the orchestrator's hard cap (12) so we never blow the budget.
return Math.max(0, Math.min(12, scaled));
}
/**
* Map a weight to the retrieval ratio (fraction of the context-budget that
* RAG can claim). Lower weights mean RAG gets a smaller slice and leaves more
* room for conversation history / system prompt.
*/
export function mapWeightToRetrievalRatio(weight: number): number {
const w = _clamp(weight);
// 0 → 0.05 (still room for tiny lessons block), 50 → 0.40 (status quo), 100 → 0.60.
return Math.max(0.05, Math.min(0.6, 0.05 + (w / 100) * 0.55));
}
/**
* Build the natural-language policy block injected into the system prompt.
* Returns `''` when the weight is exactly the default (50) — at the midpoint
* there's nothing useful to tell the model, and quieter prompts behave better.
*/
export function buildKnowledgeMixPolicy(mix: ResolvedKnowledgeMix): string {
const w = _clamp(mix.weight);
if (w === DEFAULT_WEIGHT) return '';
const header = `[KNOWLEDGE MIX POLICY]\nSecond Brain reliance: ${w}% (model knowledge: ${100 - w}%).`;
let body: string;
if (w === 0) {
body = [
'Second Brain retrieval is disabled for this turn.',
'Answer from your general knowledge and the conversation history alone.',
'Do not invent file citations.',
].join('\n');
} else if (w < 25) {
body = [
'Rely primarily on your own general knowledge.',
'Treat any Second Brain notes shown below as light reference material only.',
'Brainstorming, broad explanations and creative synthesis are encouraged.',
].join('\n');
} else if (w < 50) {
body = [
'Lean on your general knowledge; use Second Brain notes as supporting context.',
'Cite Brain files only when they materially shape the answer.',
].join('\n');
} else if (w < 75) {
body = [
'Prefer Second Brain evidence when it is present.',
'Use your general knowledge to connect, explain, and fill harmless background.',
'Do not override explicit Second Brain evidence with model assumptions.',
].join('\n');
} else if (w < 100) {
body = [
'Treat Second Brain notes as the primary evidence for this answer.',
'Cite Brain files for any non-trivial claim and quote relevant lines when needed.',
'If the notes do not cover a point, say so explicitly instead of guessing.',
].join('\n');
} else {
body = [
'Second Brain notes are the only authoritative source for this answer.',
'Cite Brain files for every substantive claim.',
'If a point is not in the notes, reply that it is outside the recorded knowledge — do not fall back to general knowledge.',
].join('\n');
}
return `${header}\n${body}`;
}