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,5 @@
Copyright (c) 2026 Marko Markovic. All rights reserved.
This software is proprietary and confidential. Unauthorized copying,
modification, distribution, or use of this software, via any medium,
is strictly prohibited.

View File

@@ -0,0 +1,41 @@
# @waggle/optimizer
Thin wrapper around [@ax-llm/ax](https://github.com/ax-llm/ax) that exposes a
handful of typed LLM-program primitives (`summarizer`, `classifier`,
`prompt_expander`). Consumed by the Waggle server's `optimizer-service.ts`
to run Ax programs with vault-resolved API keys.
## Why this package still exists
The Skills 2.0 verification doc asked whether this package should be archived
in favor of the evolution stack in `packages/agent/` (GEPA loop, evolve-schema,
iterative-optimizer, judge, compose-evolution). The answer is **no** — the two
systems solve different problems:
| `@waggle/optimizer` | `packages/agent/src/iterative-optimizer.ts` + friends |
|---|---|
| Runs a fixed Ax program (e.g. "summarize this text") once | Evolves a prompt across many trials by proposing mutations and scoring with a judge |
| One-shot execution | Closed loop: generate → judge → gate → deploy |
| API: `optimizer.summarize(text)` | API: `runEvolutionCycle()`, `iterateUntilBudget()` |
| 132 lines | 500+ lines across 10+ files |
The agent-side GEPA / EvolveSchema stack is the "learn to write better prompts
over time" system. This package is the "please execute this typed Ax program
now" utility. The server uses the latter for deterministic summarization,
classification, and prompt expansion tasks that do **not** need a feedback
loop.
Production consumer: `packages/server/src/local/services/optimizer-service.ts`
— see the `execute()` method.
## Programs
- **summarizer** — `textToSummarize → summaryText`
- **classifier** — `textToClassify → intentCategory`
- **prompt_expander** — `briefPrompt → expandedPrompt`
## When to extend
- **Need a new one-shot Ax program?** Add a signature in `src/signatures.ts`.
- **Need a closed-loop optimizer that evolves prompts?** That's evolution —
use `packages/agent/src/evolution-orchestrator.ts`, not this package.

View File

@@ -0,0 +1,18 @@
{
"name": "@waggle/optimizer",
"version": "0.1.0",
"description": "Waggle optimizer — GEPA prompt optimization with Ax signatures",
"type": "module",
"main": "src/optimizer.ts",
"exports": {
".": "./src/index.ts"
},
"scripts": {
"build": "tsc",
"test": "vitest run"
},
"dependencies": {
"@ax-llm/ax": "^19.0.12"
},
"license": "MIT"
}

View File

@@ -0,0 +1,13 @@
export { PromptOptimizer, type OptimizerConfig, type ExecutionResult } from './optimizer.js';
export {
SUMMARIZER_SIGNATURE,
CLASSIFIER_SIGNATURE,
PROMPT_EXPANDER_SIGNATURE,
createSummarizer,
createClassifier,
createPromptExpander,
getProgram,
PROGRAM_REGISTRY,
type ProgramName,
type ProgramEntry,
} from './signatures.js';

View File

@@ -0,0 +1,50 @@
import type { AxAIService } from '@ax-llm/ax';
import { type ProgramName, getProgram, PROGRAM_REGISTRY } from './signatures.js';
export interface ExecutionResult {
programName: ProgramName;
input: Record<string, string>;
output: Record<string, string>;
}
export interface OptimizerConfig {
ai: AxAIService;
}
export class PromptOptimizer {
private ai: AxAIService;
constructor(config: OptimizerConfig) {
this.ai = config.ai;
}
async execute(programName: ProgramName, input: Record<string, string>): Promise<ExecutionResult> {
const entry = getProgram(programName);
const program = entry.create();
const result = await program.forward(this.ai, input);
return {
programName,
input,
output: result as Record<string, string>,
};
}
async summarize(text: string): Promise<string> {
const result = await this.execute('summarizer', { textToSummarize: text });
return result.output.summaryText;
}
async classify(text: string): Promise<string> {
const result = await this.execute('classifier', { textToClassify: text });
return result.output.intentCategory;
}
async expandPrompt(text: string): Promise<string> {
const result = await this.execute('prompt_expander', { briefPrompt: text });
return result.output.expandedPrompt;
}
listPrograms(): ProgramName[] {
return PROGRAM_REGISTRY.map(p => p.name);
}
}

View File

@@ -0,0 +1,69 @@
import { AxGen, AxSignature } from '@ax-llm/ax';
import type { AxAIService } from '@ax-llm/ax';
// --- Signature Definitions ---
export const SUMMARIZER_SIGNATURE = new AxSignature(
'textToSummarize:string "The text content to summarize" -> summaryText:string "A concise summary of the input text"'
);
export const CLASSIFIER_SIGNATURE = new AxSignature(
'textToClassify:string "Text to classify into a category" -> intentCategory:class "question, command, observation, request, greeting"'
);
export const PROMPT_EXPANDER_SIGNATURE = new AxSignature(
'briefPrompt:string "A brief or vague user prompt" -> expandedPrompt:string "A detailed, well-structured prompt with clear instructions"'
);
// --- Program Definitions ---
export function createSummarizer(): AxGen {
const gen = new AxGen(SUMMARIZER_SIGNATURE);
gen.setInstruction(
'You are a precise summarizer. Produce a concise summary that captures the key points of the input text. ' +
'Keep the summary under 3 sentences.'
);
return gen;
}
export function createClassifier(): AxGen {
const gen = new AxGen(CLASSIFIER_SIGNATURE);
gen.setInstruction(
'Classify the user input into exactly one intent category. ' +
'question = asking for information, command = directing an action, ' +
'observation = stating a fact, request = asking for help, greeting = social pleasantry.'
);
return gen;
}
export function createPromptExpander(): AxGen {
const gen = new AxGen(PROMPT_EXPANDER_SIGNATURE);
gen.setInstruction(
'Take a brief or vague user prompt and expand it into a detailed, well-structured prompt. ' +
'Add context, specify the desired format, and clarify ambiguities. ' +
'The expanded prompt should be actionable by an AI assistant.'
);
return gen;
}
// --- Program Registry ---
export type ProgramName = 'summarizer' | 'classifier' | 'prompt_expander';
export interface ProgramEntry {
name: ProgramName;
create: () => AxGen;
signature: AxSignature;
}
export const PROGRAM_REGISTRY: ProgramEntry[] = [
{ name: 'summarizer', create: createSummarizer, signature: SUMMARIZER_SIGNATURE },
{ name: 'classifier', create: createClassifier, signature: CLASSIFIER_SIGNATURE },
{ name: 'prompt_expander', create: createPromptExpander, signature: PROMPT_EXPANDER_SIGNATURE },
];
export function getProgram(name: ProgramName): ProgramEntry {
const entry = PROGRAM_REGISTRY.find(p => p.name === name);
if (!entry) throw new Error(`Unknown program: ${name}`);
return entry;
}

View File

@@ -0,0 +1,223 @@
import { describe, it, expect, vi } from 'vitest';
import {
SUMMARIZER_SIGNATURE,
CLASSIFIER_SIGNATURE,
PROMPT_EXPANDER_SIGNATURE,
createSummarizer,
createClassifier,
createPromptExpander,
getProgram,
PROGRAM_REGISTRY,
type ProgramName,
} from '../src/signatures.js';
import { PromptOptimizer } from '../src/optimizer.js';
import type { AxAIService } from '@ax-llm/ax';
// Convert camelCase to Title Case (what Ax uses in prompts)
function toTitleCase(camel: string): string {
return camel.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase()).trim();
}
// Mock AI service that returns predictable outputs
// Must implement enough of AxAIService for AxGen.forward() to work
function createMockAI(responses: Record<string, string>): AxAIService {
// Format response as Ax expects: Title Case field names followed by values
const content = Object.entries(responses)
.map(([k, v]) => `${toTitleCase(k)}: ${v}`)
.join('\n');
return {
chat: vi.fn().mockResolvedValue({
results: [{ content, index: 0 }],
modelUsage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
}),
embed: vi.fn(),
getFeatures: vi.fn().mockReturnValue({
functions: true,
streaming: true,
hasThinkingBudget: false,
hasShowThoughts: false,
structuredOutputs: false,
media: {},
caching: { supported: false, types: [] },
thinking: false,
multiTurn: true,
}),
getOptions: vi.fn().mockReturnValue({ debug: false, verbose: false }),
setOptions: vi.fn(),
getName: vi.fn().mockReturnValue('mock'),
getId: vi.fn().mockReturnValue('mock-id'),
getModelList: vi.fn().mockReturnValue([]),
getLastUsedChatModel: vi.fn().mockReturnValue('mock-model'),
getLastUsedEmbedModel: vi.fn().mockReturnValue(undefined),
getLastUsedModelConfig: vi.fn().mockReturnValue(undefined),
getMetrics: vi.fn().mockReturnValue(undefined),
getLogger: vi.fn().mockReturnValue(undefined),
} as unknown as AxAIService;
}
describe('Prompt Optimization (Ax Integration)', () => {
describe('Signature definitions', () => {
it('summarizer signature has correct input/output fields', () => {
const inputs = SUMMARIZER_SIGNATURE.getInputFields();
const outputs = SUMMARIZER_SIGNATURE.getOutputFields();
expect(inputs).toHaveLength(1);
expect(inputs[0].name).toBe('textToSummarize');
expect(outputs).toHaveLength(1);
expect(outputs[0].name).toBe('summaryText');
});
it('classifier signature has correct input/output fields', () => {
const inputs = CLASSIFIER_SIGNATURE.getInputFields();
const outputs = CLASSIFIER_SIGNATURE.getOutputFields();
expect(inputs).toHaveLength(1);
expect(inputs[0].name).toBe('textToClassify');
expect(outputs).toHaveLength(1);
expect(outputs[0].name).toBe('intentCategory');
expect(outputs[0].type?.name).toBe('class');
expect(outputs[0].type?.options).toEqual(['question', 'command', 'observation', 'request', 'greeting']);
});
it('prompt expander signature has correct input/output fields', () => {
const inputs = PROMPT_EXPANDER_SIGNATURE.getInputFields();
const outputs = PROMPT_EXPANDER_SIGNATURE.getOutputFields();
expect(inputs).toHaveLength(1);
expect(inputs[0].name).toBe('briefPrompt');
expect(outputs).toHaveLength(1);
expect(outputs[0].name).toBe('expandedPrompt');
});
});
describe('Program creation', () => {
it('creates summarizer program with instruction', () => {
const program = createSummarizer();
expect(program).toBeDefined();
expect(program.getInstruction()).toContain('summarizer');
});
it('creates classifier program with instruction', () => {
const program = createClassifier();
expect(program).toBeDefined();
expect(program.getInstruction()).toContain('Classify');
});
it('creates prompt expander program with instruction', () => {
const program = createPromptExpander();
expect(program).toBeDefined();
expect(program.getInstruction()).toContain('expand');
});
});
describe('Program registry', () => {
it('contains all three programs', () => {
expect(PROGRAM_REGISTRY).toHaveLength(3);
const names = PROGRAM_REGISTRY.map(p => p.name);
expect(names).toContain('summarizer');
expect(names).toContain('classifier');
expect(names).toContain('prompt_expander');
});
it('getProgram returns correct entry', () => {
const entry = getProgram('summarizer');
expect(entry.name).toBe('summarizer');
expect(typeof entry.create).toBe('function');
expect(entry.signature).toBe(SUMMARIZER_SIGNATURE);
});
it('getProgram throws for unknown program', () => {
expect(() => getProgram('nonexistent' as ProgramName)).toThrow('Unknown program: nonexistent');
});
it('each registry entry creates a valid program', () => {
for (const entry of PROGRAM_REGISTRY) {
const program = entry.create();
expect(program).toBeDefined();
expect(program.getInstruction()).toBeTruthy();
}
});
});
describe('PromptOptimizer', () => {
it('lists all available programs', () => {
const mockAI = createMockAI({});
const optimizer = new PromptOptimizer({ ai: mockAI });
const programs = optimizer.listPrograms();
expect(programs).toEqual(['summarizer', 'classifier', 'prompt_expander']);
});
});
// API-dependent tests - these test actual execution through the Ax framework
// They use a mock AI service, but still exercise the full AxGen.forward() pipeline
describe('Signature execution (API-dependent)', () => {
it('summarizer produces string output type', async () => {
const mockAI = createMockAI({ summaryText: 'This is a test summary.' });
const optimizer = new PromptOptimizer({ ai: mockAI });
const result = await optimizer.execute('summarizer', {
textToSummarize: 'A long text about AI agents and their capabilities in modern software.',
});
expect(result.programName).toBe('summarizer');
expect(result.input.textToSummarize).toContain('AI agents');
expect(typeof result.output.summaryText).toBe('string');
});
it('classifier returns valid category', async () => {
const mockAI = createMockAI({ intentCategory: 'question' });
const optimizer = new PromptOptimizer({ ai: mockAI });
const result = await optimizer.execute('classifier', {
textToClassify: 'What is the weather like today?',
});
expect(result.programName).toBe('classifier');
expect(['question', 'command', 'observation', 'request', 'greeting']).toContain(
result.output.intentCategory
);
});
it('prompt expander produces expanded output', async () => {
const mockAI = createMockAI({ expandedPrompt: 'Please write a comprehensive blog post about AI, covering...' });
const optimizer = new PromptOptimizer({ ai: mockAI });
const result = await optimizer.execute('prompt_expander', {
briefPrompt: 'Write about AI',
});
expect(result.programName).toBe('prompt_expander');
expect(typeof result.output.expandedPrompt).toBe('string');
expect(result.output.expandedPrompt.length).toBeGreaterThan(0);
});
});
describe('Intent classification across categories', () => {
const categories = [
{ input: 'What time is it?', expected: 'question' },
{ input: 'Delete all files now', expected: 'command' },
{ input: 'The sky is blue', expected: 'observation' },
{ input: 'Can you help me with this?', expected: 'request' },
{ input: 'Hello, how are you?', expected: 'greeting' },
];
for (const { input, expected } of categories) {
it(`classifies "${input}" as ${expected}`, async () => {
const mockAI = createMockAI({ intentCategory: expected });
const optimizer = new PromptOptimizer({ ai: mockAI });
const category = await optimizer.classify(input);
expect(category).toBe(expected);
});
}
});
describe('Convenience methods', () => {
it('summarize() returns summary string', async () => {
const mockAI = createMockAI({ summaryText: 'Brief summary here.' });
const optimizer = new PromptOptimizer({ ai: mockAI });
const summary = await optimizer.summarize('Long text about many topics...');
expect(typeof summary).toBe('string');
expect(summary.length).toBeGreaterThan(0);
});
it('expandPrompt() returns expanded string', async () => {
const mockAI = createMockAI({ expandedPrompt: 'Detailed expanded prompt...' });
const optimizer = new PromptOptimizer({ ai: mockAI });
const expanded = await optimizer.expandPrompt('Fix bug');
expect(typeof expanded).toBe('string');
expect(expanded.length).toBeGreaterThan(0);
});
});
});

View File

@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022"],
"outDir": "dist",
"rootDir": "src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"composite": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests"]
}

View File

@@ -0,0 +1,9 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
testTimeout: 30_000,
include: ['tests/**/*.test.ts'],
},
});