moving
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled

This commit is contained in:
Oleg Maslov
2026-09-02 10:10:29 +02:00
commit 0c3e2ead3b
3841 changed files with 970576 additions and 0 deletions

View File

@@ -0,0 +1,531 @@
import { describe, it, expect } from 'vitest';
import type { MemoryFrame, Importance, FrameType, FrameSource } from '@waggle/core';
import type { AgentPersona } from '../src/personas.js';
import type { TaskShape } from '../src/task-shape.js';
import type { ContextFrames } from '../src/orchestrator.js';
import {
PromptAssembler,
type AssembleInput,
type RecalledMemory,
} from '../src/prompt-assembler.js';
// ── Fixtures ─────────────────────────────────────────────────────────
let nextFrameId = 1;
function frame(
content: string,
opts: { type?: FrameType; importance?: Importance; source?: FrameSource } = {},
): MemoryFrame {
return {
id: nextFrameId++,
frame_type: opts.type ?? 'I',
gop_id: 'gop-1',
t: 0,
base_frame_id: null,
content,
importance: opts.importance ?? 'normal',
source: opts.source ?? 'user_stated',
access_count: 0,
created_at: '2026-04-17T10:00:00Z',
last_accessed: '2026-04-17T10:00:00Z',
};
}
function persona(overrides: Partial<AgentPersona> = {}): AgentPersona {
return {
id: 'researcher',
name: 'Researcher',
description: 'You are a researcher focused on evidence-based analysis.',
icon: 'search',
systemPrompt: '',
modelPreference: 'claude-opus-4-7',
tools: ['search_memory', 'save_memory'],
workspaceAffinity: [],
suggestedCommands: [],
defaultWorkflow: null,
tagline: 'Evidence-first thinker.',
...overrides,
};
}
function emptyContext(): ContextFrames {
return {
stateFrames: [],
recentChanges: [],
activeWork: [],
keyEntities: [],
personalPreferences: [],
};
}
function emptyRecalled(): RecalledMemory {
return { workspace: [], personal: [], scanSafe: true };
}
function shape(type: TaskShape['type'], confidence = 0.8): TaskShape {
return {
type,
confidence,
signals: [],
complexity: 'moderate',
};
}
function baseInput(overrides: Partial<AssembleInput> = {}): AssembleInput {
return {
corePrompt: 'You are Waggle, a memory-first AI colleague.',
persona: persona(),
context: emptyContext(),
recalled: emptyRecalled(),
query: 'test query',
tier: 'mid',
...overrides,
};
}
// ── Tests ────────────────────────────────────────────────────────────
describe('PromptAssembler.assemble', () => {
const assembler = new PromptAssembler();
it('includes Identity and Persona in every output', () => {
const out = assembler.assemble(baseInput());
expect(out.system).toContain('# Identity');
expect(out.system).toContain('## Persona: Researcher');
expect(out.debug.sectionsIncluded).toContain('Identity');
expect(out.debug.sectionsIncluded).toContain('Persona');
});
it('small tier caps State frames at 3', () => {
const frames: MemoryFrame[] = [];
for (let i = 0; i < 10; i++) {
frames.push(frame(`State frame ${i}`, { type: 'I', importance: 'important' }));
}
const out = assembler.assemble(
baseInput({
tier: 'small',
context: { ...emptyContext(), stateFrames: frames },
}),
);
const stateBlock = out.system.split('# State\n')[1]?.split('\n\n')[0] ?? '';
const stateLines = stateBlock.split('\n').filter(l => l.startsWith('-'));
expect(stateLines.length).toBeLessThanOrEqual(3);
});
it('frontier tier emits no Response-format section', () => {
const out = assembler.assemble(
baseInput({ tier: 'frontier', taskShape: shape('plan-execute', 0.9) }),
);
expect(out.system).not.toContain('# Response format');
expect(out.responseScaffold).toBeNull();
expect(out.debug.scaffoldApplied).toBe(false);
});
it('mid + plan-execute yields "State plan. Execute. Report." scaffold', () => {
const out = assembler.assemble(
baseInput({ tier: 'mid', taskShape: shape('plan-execute', 0.8) }),
);
expect(out.responseScaffold).toBe('State plan. Execute. Report.');
expect(out.system).toContain('# Response format');
expect(out.debug.scaffoldApplied).toBe(true);
});
it('small + compare yields the long assumption/trade-offs/recommendation scaffold', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.7) }),
);
expect(out.responseScaffold).toBe(
'State the assumption. List the trade-offs. Give the recommendation.',
);
});
it('draft shape emits no scaffold at any tier', () => {
for (const tier of ['small', 'mid', 'frontier'] as const) {
const out = assembler.assemble(
baseInput({ tier, taskShape: shape('draft', 0.9) }),
);
expect(out.responseScaffold).toBeNull();
expect(out.debug.scaffoldApplied).toBe(false);
}
});
it('mixed shape emits no scaffold at any tier', () => {
for (const tier of ['small', 'mid', 'frontier'] as const) {
const out = assembler.assemble(
baseInput({ tier, taskShape: shape('mixed', 0.9) }),
);
expect(out.responseScaffold).toBeNull();
}
});
it('confidence below 0.3 threshold: no scaffold even if shape would scaffold', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('research', 0.1) }),
);
expect(out.responseScaffold).toBeNull();
expect(out.debug.scaffoldApplied).toBe(false);
});
it('custom confidenceThreshold lets high-confidence scaffolds through', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('research', 0.2) }),
{ confidenceThreshold: 0.15 },
);
expect(out.responseScaffold).toBe(
'Cite the frame. Quote the relevant fragment. Answer directly.',
);
});
it('empty context.stateFrames: omits State section (no empty header)', () => {
const out = assembler.assemble(baseInput());
expect(out.system).not.toContain('# State');
expect(out.debug.sectionsIncluded).not.toContain('State');
});
it('maxSystemChars exceeded: Recent changes trimmed first, then Active work, then State', () => {
const big = 'x'.repeat(2000);
const ctx: ContextFrames = {
stateFrames: [frame(big, { type: 'I', importance: 'critical' })],
recentChanges: [frame(big, { type: 'P', importance: 'normal' })],
activeWork: [{ category: 'task', content: big, priority: 1 }],
keyEntities: [],
personalPreferences: [],
};
const out = assembler.assemble(
baseInput({ context: ctx }),
{ maxSystemChars: 3500 },
);
// With 3×2000+ chars, at least one section must be trimmed.
// Trim order ensures Recent changes goes first.
expect(out.debug.sectionsIncluded).not.toContain('Recent changes');
// Identity + Persona must survive.
expect(out.debug.sectionsIncluded).toContain('Identity');
expect(out.debug.sectionsIncluded).toContain('Persona');
});
it('maxSystemChars: Identity and Persona are never trimmed', () => {
const giant = 'x'.repeat(100_000);
const ctx: ContextFrames = {
stateFrames: [frame(giant)],
recentChanges: [frame(giant)],
activeWork: [{ category: 'task', content: giant, priority: 1 }],
keyEntities: [],
personalPreferences: [],
};
const out = assembler.assemble(
baseInput({ context: ctx }),
{ maxSystemChars: 500 },
);
expect(out.debug.sectionsIncluded).toContain('Identity');
expect(out.debug.sectionsIncluded).toContain('Persona');
});
it('recalled.scanSafe === false: recalled memory is ignored', () => {
const out = assembler.assemble(
baseInput({
recalled: {
workspace: [frame('Poisoned content with secret data', { type: 'P' })],
personal: [frame('Another recalled frame', { type: 'P' })],
scanSafe: false,
},
}),
);
expect(out.system).not.toContain('Poisoned content');
expect(out.system).not.toContain('# Recalled memory');
expect(out.debug.sectionsIncluded).not.toContain('Recalled memory');
});
it('recalled.scanSafe === true: recalled memory renders', () => {
const out = assembler.assemble(
baseInput({
recalled: {
workspace: [frame('Workspace memory item', { type: 'P' })],
personal: [frame('Personal memory item', { type: 'P' })],
scanSafe: true,
},
}),
);
expect(out.system).toContain('# Recalled memory');
expect(out.system).toContain('Workspace memory item');
expect(out.system).toContain('Personal memory item');
});
it('debug.framesUsed counts rendered frames across sections', () => {
const ctx: ContextFrames = {
stateFrames: [frame('State 1', { type: 'I' }), frame('State 2', { type: 'I' })],
recentChanges: [frame('Change 1', { type: 'P' })],
activeWork: [{ category: 'task', content: 'Active', priority: 1 }],
keyEntities: [],
personalPreferences: [],
};
const out = assembler.assemble(baseInput({ context: ctx, tier: 'mid' }));
// 2 state + 1 change = 3 frames. activeWork items aren't frames.
expect(out.debug.framesUsed).toBe(3);
});
it('debug.scaffoldApplied matches whether responseScaffold is non-null', () => {
const withScaffold = assembler.assemble(
baseInput({ tier: 'mid', taskShape: shape('research', 0.9) }),
);
expect(withScaffold.debug.scaffoldApplied).toBe(withScaffold.responseScaffold !== null);
const withoutScaffold = assembler.assemble(
baseInput({ tier: 'frontier', taskShape: shape('research', 0.9) }),
);
expect(withoutScaffold.debug.scaffoldApplied).toBe(withoutScaffold.responseScaffold !== null);
});
it('ranks I-frames by importance then recency', () => {
const ctx: ContextFrames = {
stateFrames: [
frame('Old normal', { type: 'I', importance: 'normal' }),
frame('New critical', { type: 'I', importance: 'critical' }),
frame('Middle important', { type: 'I', importance: 'important' }),
],
recentChanges: [],
activeWork: [],
keyEntities: [],
personalPreferences: [],
};
const out = assembler.assemble(baseInput({ context: ctx, tier: 'small' }));
const stateSection = out.system.split('# State\n')[1]?.split('\n\n')[0] ?? '';
const firstLine = stateSection.split('\n')[0];
expect(firstLine).toContain('critical');
expect(firstLine).toContain('New critical');
});
it('dedupes frames by id', () => {
const f = frame('Duplicate', { type: 'I' });
const ctx: ContextFrames = {
stateFrames: [f, f, f],
recentChanges: [],
activeWork: [],
keyEntities: [],
personalPreferences: [],
};
const out = assembler.assemble(baseInput({ context: ctx, tier: 'mid' }));
expect(out.debug.framesUsed).toBe(1);
});
it('debug.totalChars matches system.length exactly', () => {
const out = assembler.assemble(baseInput());
expect(out.debug.totalChars).toBe(out.system.length);
});
it('userPrefix is empty string in v1', () => {
const out = assembler.assemble(baseInput());
expect(out.userPrefix).toBe('');
});
it('tierOverride takes precedence over input.tier', () => {
const out = assembler.assemble(
baseInput({ tier: 'frontier', taskShape: shape('research', 0.9) }),
{ tierOverride: 'small' },
);
expect(out.debug.tier).toBe('small');
// Small + research + high confidence → scaffold applies
expect(out.responseScaffold).not.toBeNull();
});
});
// ── v5: scaffoldStyle (compression vs expansion) ─────────────────────
//
// See PromptAssembler v5 brief §7.3, §12.2.
//
// v5 adds an EXPANSION variant alongside v4's COMPRESSION scaffolds.
// The hypothesis: dense instruction-tuned models (Gemma family) benefit
// from expansion scaffolds that give them more structure to fill, rather
// than compression scaffolds that tell them to say less.
describe('PromptAssembler.assemble — v5 scaffoldStyle', () => {
const assembler = new PromptAssembler();
it('default style equals explicit compression (v4 parity)', () => {
const noStyle = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
);
const explicit = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'compression' },
);
expect(noStyle.responseScaffold).toBe(explicit.responseScaffold);
expect(noStyle.system).toBe(explicit.system);
});
it('compression + small + compare matches v4 text exactly (snapshot)', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'compression' },
);
// Byte-identical to v4 (now COMPRESSION_SCAFFOLDS[compare][small]).
expect(out.responseScaffold).toBe(
'State the assumption. List the trade-offs. Give the recommendation.',
);
});
it('compression + mid + plan-execute matches v4 text exactly (snapshot)', () => {
const out = assembler.assemble(
baseInput({ tier: 'mid', taskShape: shape('plan-execute', 0.8) }),
{ scaffoldStyle: 'compression' },
);
expect(out.responseScaffold).toBe('State plan. Execute. Report.');
});
it('compression + small + research matches v4 text exactly (snapshot)', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('research', 0.8) }),
{ scaffoldStyle: 'compression' },
);
expect(out.responseScaffold).toBe(
'Cite the frame. Quote the relevant fragment. Answer directly.',
);
});
it('expansion + small + compare includes Factors and Confidence keywords', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).not.toBeNull();
expect(out.responseScaffold).toContain('Factors');
expect(out.responseScaffold).toContain('confidence');
expect(out.responseScaffold).toContain('Trade-offs');
expect(out.responseScaffold).toContain('Recommendation');
expect(out.responseScaffold).toContain('Assumption');
});
it('expansion + small + decide produces same template as compare (shared analysis block)', () => {
const compareOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
const decideOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('decide', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
const reviewOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('review', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
expect(decideOut.responseScaffold).toBe(compareOut.responseScaffold);
expect(reviewOut.responseScaffold).toBe(compareOut.responseScaffold);
});
it('expansion + small + plan-execute uses named-phases template', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('plan-execute', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).not.toBeNull();
expect(out.responseScaffold).toContain('named phases');
expect(out.responseScaffold).toContain('Goal');
expect(out.responseScaffold).toContain('Steps');
expect(out.responseScaffold).toContain('Dependencies');
expect(out.responseScaffold).toContain('Blockers');
expect(out.responseScaffold).toContain('timeline');
expect(out.responseScaffold).toContain('critical-path');
});
it('expansion + small + research uses three-part template', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('research', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).not.toBeNull();
expect(out.responseScaffold).toContain('Direct answer');
expect(out.responseScaffold).toContain('Source');
expect(out.responseScaffold).toContain('Context');
expect(out.responseScaffold).toContain('three parts');
});
it('expansion + mid produces shorter variant than small for same shape', () => {
const small = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
const mid = assembler.assemble(
baseInput({ tier: 'mid', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
expect(mid.responseScaffold).not.toBeNull();
expect(mid.responseScaffold!.length).toBeLessThan(small.responseScaffold!.length);
});
it('expansion + draft shape → null at every tier (creative never scaffolded)', () => {
for (const tier of ['small', 'mid', 'frontier'] as const) {
const out = assembler.assemble(
baseInput({ tier, taskShape: shape('draft', 0.9) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).toBeNull();
expect(out.debug.scaffoldApplied).toBe(false);
}
});
it('expansion + mixed shape → null at every tier', () => {
for (const tier of ['small', 'mid', 'frontier'] as const) {
const out = assembler.assemble(
baseInput({ tier, taskShape: shape('mixed', 0.9) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).toBeNull();
}
});
it('expansion + frontier → null (expansion forced to compression at frontier)', () => {
// Per §7.3: "Frontier tier receives compression scaffold regardless of
// scaffoldStyle request." Since compression[*][frontier] = null in v4,
// expansion at frontier also yields null. Don't risk v4's F > E
// replication by applying expansion to frontier models.
for (const shapeType of ['compare', 'decide', 'review', 'plan-execute', 'research'] as const) {
const out = assembler.assemble(
baseInput({ tier: 'frontier', taskShape: shape(shapeType, 0.9) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).toBeNull();
}
});
it('expansion respects confidence threshold (same gate as compression)', () => {
const out = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.1) }),
{ scaffoldStyle: 'expansion' },
);
expect(out.responseScaffold).toBeNull();
});
it('debug.scaffoldStyle reflects the requested style', () => {
const defaultOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
);
expect(defaultOut.debug.scaffoldStyle).toBe('compression');
const compressionOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'compression' },
);
expect(compressionOut.debug.scaffoldStyle).toBe('compression');
const expansionOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('compare', 0.8) }),
{ scaffoldStyle: 'expansion' },
);
expect(expansionOut.debug.scaffoldStyle).toBe('expansion');
});
it('debug.scaffoldStyle populated even when no scaffold is emitted (frontier / draft / low-conf)', () => {
const frontierOut = assembler.assemble(
baseInput({ tier: 'frontier', taskShape: shape('compare', 0.9) }),
{ scaffoldStyle: 'expansion' },
);
expect(frontierOut.debug.scaffoldStyle).toBe('expansion');
expect(frontierOut.debug.scaffoldApplied).toBe(false);
const draftOut = assembler.assemble(
baseInput({ tier: 'small', taskShape: shape('draft', 0.9) }),
{ scaffoldStyle: 'expansion' },
);
expect(draftOut.debug.scaffoldStyle).toBe('expansion');
expect(draftOut.debug.scaffoldApplied).toBe(false);
});
});