GEPA Optimization Codegen Rules (@ax-llm/ax)
Use this skill to generate GEPA optimization code. Prefer the top-level optimize(...) helper for normal code, and use direct AxGEPA / AxBootstrapFewShot only when the user needs low-level optimizer control.
Use These Defaults
- Use
optimize(program, train, metric, { studentAI, teacherAI, ... }) for normal generator and flow tuning.
- Prefer
ai(), ax(), and flow() for new code.
- Use a strong
teacherAI and a cheaper studentAI.
- Pass
validationExamples when you have a holdout set.
- Set
maxMetricCalls to bound optimizer cost; optimize(...) defaults it to 100.
- Use scalar metrics for one objective and object metrics for Pareto optimization.
- Apply results with
program.applyOptimization(result.optimizedProgram!).
- For tree-wide runs, expect
optimizedProgram.componentMap.
- Persist artifacts with
axSerializeOptimizedProgram(...) and restore them with axDeserializeOptimizedProgram(...) so the same flow works in browsers and Node.
optimize(...) runs AxBootstrapFewShot -> AxGEPA for small starter sets by default, preserving the demos in result.optimizedProgram.demos.
Critical Rules
optimize(...) and AxGEPA.compile() work for a single generator and for tree-aware roots such as flows or agents with registered optimizable descendants.
- There is no separate flow-only GEPA optimizer. Use
AxGEPA for flows too.
- The metric may return either
number or Record<string, number>.
- Keep metrics deterministic and cheap by default.
- Avoid extra LLM calls inside the metric unless the user explicitly wants judge-based evaluation.
- If the user needs LLM-as-judge scoring for a non-agent GEPA run, prefer a plain typed
AxGen evaluator instead of writing a custom judge abstraction.
maxMetricCalls must be large enough to cover the initial validation pass over validationExamples.
- GEPA optimizes generic string components exposed by
getOptimizableComponents(). If a tree exposes no components, optimization will fail.
- Use held-out validation examples for selection. Do not reuse the training set as
validationExamples.
result.optimizedProgram is the easy-to-apply best candidate. result.paretoFront is the full trade-off set for multi-objective runs.
- Direct
AxGEPA still has its own bootstrap option, but top-level optimize(...) composes the existing AxBootstrapFewShot optimizer before GEPA instead.
Metric Selection
Choose the evaluation path deliberately:
- Prefer a deterministic metric when correctness can be read directly from
prediction and example.
- Prefer a deterministic metric when cost, latency, recursion depth, or tool count matters.
- Use a plain typed
AxGen evaluator only when the task is genuinely qualitative and hard to score exactly.
- For
agent.optimize(...), prefer the built-in judge path instead of manually wrapping a judge metric. Normal agent users usually do not need to set target or metric at all.
Rule of thumb:
optimize(...) on AxGen or flow: use a metric first, optionally a plain typed AxGen evaluator if needed.
agent.optimize(...): use custom metric for crisp scoring, otherwise let the built-in judge handle scoring. Add judgeAI plus judgeOptions only when you want a stronger or separate judge model.
Canonical Scalar Pattern
import { ai, ax, optimize, AxAIOpenAIModel } from '@ax-llm/ax';
const student = ai({
name: 'openai',
apiKey: process.env.OPENAI_APIKEY!,
config: { model: AxAIOpenAIModel.GPT54Mini },
});
const teacher = ai({
name: 'openai',
apiKey: process.env.OPENAI_APIKEY!,
config: { model: AxAIOpenAIModel.GPT54 },
});
const classifier = ax(
'emailText:string -> priority:class "high, normal, low", rationale:string'
);
const train = [
{ emailText: 'URGENT: Server down!', priority: 'high' },
{ emailText: 'Weekly newsletter', priority: 'low' },
];
const validation = [
{ emailText: 'Invoice overdue', priority: 'high' },
{ emailText: 'Lunch plans?', priority: 'low' },
];
const metric = ({ prediction, example }: { prediction: any; example: any }) =>
prediction?.priority === example?.priority ? 1 : 0;
const result = await optimize(classifier, train, metric, {
studentAI: student,
teacherAI: teacher,
numTrials: 12,
minibatch: true,
minibatchSize: 4,
earlyStoppingTrials: 4,
sampleCount: 1,
validationExamples: validation,
maxMetricCalls: 120,
});
classifier.applyOptimization(result.optimizedProgram!);
console.log(result.bestScore);
Canonical Pareto Pattern
import { ai, flow, optimize, AxAIOpenAIModel } from '@ax-llm/ax';
const student = ai({
name: 'openai',
apiKey: process.env.OPENAI_APIKEY!,
config: { model: AxAIOpenAIModel.GPT54Mini },
});
const teacher = ai({
name: 'openai',
apiKey: process.env.OPENAI_APIKEY!,
config: { model: AxAIOpenAIModel.GPT54 },
});
const wf = flow<{ emailText: string }>()
.n('classifier', 'emailText:string -> priority:class "high, normal, low"')
.n(
'rationale',
'emailText:string, priority:string -> rationale:string "One concise sentence"'
)
.e('classifier', (state) => ({ emailText: state.emailText }))
.e('rationale', (state) => ({
emailText: state.emailText,
priority: state.classifierResult.priority,
}))
.r((state) => ({
priority: state.classifierResult.priority,
rationale: state.rationaleResult.rationale,
}));
const train = [
{ emailText: 'URGENT: Server down!', priority: 'high' },
{ emailText: 'Weekly newsletter', priority: 'low' },
];
const validation = [
{ emailText: 'Invoice overdue', priority: 'high' },
{ emailText: 'Lunch plans?', priority: 'low' },
];
const metric = ({ prediction, example }: { prediction: any; example: any }) => {
const accuracy = prediction?.priority === example?.priority ? 1 : 0;
const rationale = typeof prediction?.rationale === 'string'
? prediction.rationale
: '';
const brevity = rationale.length <= 40 ? 1 : rationale.length <= 80 ? 0.5 : 0.1;
return { accuracy, brevity };
};
const result = await optimize(wf, train, metric, {
studentAI: student,
teacherAI: teacher,
numTrials: 16,
minibatch: true,
minibatchSize: 6,
earlyStoppingTrials: 5,
sampleCount: 1,
validationExamples: validation,
maxMetricCalls: 240,
});
for (const point of result.paretoFront) {
console.log(point.scores, point.configuration);
}
wf.applyOptimization(result.optimizedProgram!);
console.log(result.optimizedProgram?.componentMap);
Metric Patterns
// Scalar objective
const scalarMetric = ({ prediction, example }) =>
prediction.answer === example.answer ? 1 : 0;
// Multi-objective
const multiMetric = ({ prediction, example }) => ({
accuracy: prediction.answer === example.answer ? 1 : 0,
brevity:
typeof prediction?.reasoning === 'string' &&
prediction.reasoning.length < 120
? 1
: 0.2,
});
- Return plain numbers or plain object literals.
- Keep objective names stable across calls.
- Prefer normalized scores such as
0..1 so trade-offs are easy to reason about.
Result Handling
const { optimizedProgram, paretoFront } = result;
program.applyOptimization(optimizedProgram!);
// Save for later
const saved = JSON.stringify(optimizedProgram);
// Load later and re-apply
const loaded = JSON.parse(saved);
program.applyOptimization(loaded);
- Single-target runs usually populate both
optimizedProgram.instruction and optimizedProgram.componentMap.
- Tree-wide runs rely on
componentMap, keyed by full component key.
- Pareto points expose candidate configs under
point.configuration.componentMap.
Useful Options
const optimizer = new AxGEPA({
studentAI,
teacherAI,
numTrials: 20,
minibatch: true,
minibatchSize: 5,
minibatchFullEvalSteps: 5,
earlyStoppingTrials: 5,
minImprovementThreshold: 0,
sampleCount: 1,
seed: 42,
verbose: true,
});
numTrials: number of reflection/evolution rounds.
minibatch: reduce per-round evaluation cost.
minibatchSize: examples per minibatch.
earlyStoppingTrials: stop after repeated non-improvement.
minImprovementThreshold: reject tiny gains below this threshold.
seed: stabilize sampling during demos and tests.
Budgeting and Validation
- Always create distinct
train and validationExamples arrays.
- Size
maxMetricCalls for at least one full validation pass plus several rounds.
- If the user wants a strict budget, say so explicitly and set
maxMetricCalls.
- For expensive trees, start with
auto: 'light' or fewer numTrials, then scale up.
- GEPA selects among exposed components using measured accept/reject history, not LLM-generated numeric scores. The LLM proposes component text; metrics decide whether to keep it.
- Function/tool trace reflection is keyed by stable component IDs where available, so function renames do not break saved candidate maps.
Troubleshooting
- Error about
maxMetricCalls being too small: increase it until the initial validation pass fits.
- Empty or poor Pareto front: verify the metric returns numbers for every example.
- No tree optimization effect: ensure child programs are registered under the root and expose optimizable components.
- Saved optimization applies only partly: use
program.applyOptimization(...), not just setInstruction(...), so componentMap reaches the full tree.
- Agent target seems too broad: when using
agent.optimize(...), set target: 'actor', 'responder', 'all', or explicit program IDs. The wrapper filters GEPA components to the selected target.
Good Example Targets
/Users/vr/src/ax/src/examples/optimize.ts
/Users/vr/src/ax/src/examples/gepa.ts
/Users/vr/src/ax/src/examples/gepa-flow.ts
/Users/vr/src/ax/src/examples/gepa-train-inference.ts
/Users/vr/src/ax/src/examples/gepa-quality-vs-speed-optimization.ts
/Users/vr/src/ax/src/examples/axagent-gepa-optimization.ts
1---2name: ax-gepa3description: This skill helps an LLM generate correct AxGEPA optimization code using @ax-llm/ax. Use when the user asks about AxGEPA, GEPA, Pareto optimization, multi-objective prompt tuning, reflective prompt evolution, validationExamples, maxMetricCalls, or optimizing a generator, flow, or agent tree.4---56# GEPA Optimization Codegen Rules (@ax-llm/ax)78Use this skill to generate GEPA optimization code. Prefer the top-level `optimize(...)` helper for normal code, and use direct `AxGEPA` / `AxBootstrapFewShot` only when the user needs low-level optimizer control.910## Use These Defaults1112- Use `optimize(program, train, metric, { studentAI, teacherAI, ... })` for normal generator and flow tuning.13- Prefer `ai()`, `ax()`, and `flow()` for new code.14- Use a strong `teacherAI` and a cheaper `studentAI`.15- Pass `validationExamples` when you have a holdout set.16- Set `maxMetricCalls` to bound optimizer cost; `optimize(...)` defaults it to `100`.17- Use scalar metrics for one objective and object metrics for Pareto optimization.18- Apply results with `program.applyOptimization(result.optimizedProgram!)`.19- For tree-wide runs, expect `optimizedProgram.componentMap`.20- Persist artifacts with `axSerializeOptimizedProgram(...)` and restore them with `axDeserializeOptimizedProgram(...)` so the same flow works in browsers and Node.21- `optimize(...)` runs `AxBootstrapFewShot -> AxGEPA` for small starter sets by default, preserving the demos in `result.optimizedProgram.demos`.2223## Critical Rules2425- `optimize(...)` and `AxGEPA.compile()` work for a single generator and for tree-aware roots such as flows or agents with registered optimizable descendants.26- There is no separate flow-only GEPA optimizer. Use `AxGEPA` for flows too.27- The metric may return either `number` or `Record<string, number>`.28- Keep metrics deterministic and cheap by default.29- Avoid extra LLM calls inside the metric unless the user explicitly wants judge-based evaluation.30- If the user needs LLM-as-judge scoring for a non-agent GEPA run, prefer a plain typed `AxGen` evaluator instead of writing a custom judge abstraction.31- `maxMetricCalls` must be large enough to cover the initial validation pass over `validationExamples`.32- GEPA optimizes generic string components exposed by `getOptimizableComponents()`. If a tree exposes no components, optimization will fail.33- Use held-out validation examples for selection. Do not reuse the training set as `validationExamples`.34- `result.optimizedProgram` is the easy-to-apply best candidate. `result.paretoFront` is the full trade-off set for multi-objective runs.35- Direct `AxGEPA` still has its own `bootstrap` option, but top-level `optimize(...)` composes the existing `AxBootstrapFewShot` optimizer before GEPA instead.3637## Metric Selection3839Choose the evaluation path deliberately:4041- Prefer a deterministic metric when correctness can be read directly from `prediction` and `example`.42- Prefer a deterministic metric when cost, latency, recursion depth, or tool count matters.43- Use a plain typed `AxGen` evaluator only when the task is genuinely qualitative and hard to score exactly.44- For `agent.optimize(...)`, prefer the built-in judge path instead of manually wrapping a judge metric. Normal agent users usually do not need to set `target` or `metric` at all.4546Rule of thumb:4748- `optimize(...)` on `AxGen` or flow: use a metric first, optionally a plain typed `AxGen` evaluator if needed.49- `agent.optimize(...)`: use custom `metric` for crisp scoring, otherwise let the built-in judge handle scoring. Add `judgeAI` plus `judgeOptions` only when you want a stronger or separate judge model.5051## Canonical Scalar Pattern5253```typescript54import { ai, ax, optimize, AxAIOpenAIModel } from '@ax-llm/ax';5556const student = ai({57 name: 'openai',58 apiKey: process.env.OPENAI_APIKEY!,59 config: { model: AxAIOpenAIModel.GPT54Mini },60});6162const teacher = ai({63 name: 'openai',64 apiKey: process.env.OPENAI_APIKEY!,65 config: { model: AxAIOpenAIModel.GPT54 },66});6768const classifier = ax(69 'emailText:string -> priority:class "high, normal, low", rationale:string'70);7172const train = [73 { emailText: 'URGENT: Server down!', priority: 'high' },74 { emailText: 'Weekly newsletter', priority: 'low' },75];7677const validation = [78 { emailText: 'Invoice overdue', priority: 'high' },79 { emailText: 'Lunch plans?', priority: 'low' },80];8182const metric = ({ prediction, example }: { prediction: any; example: any }) =>83 prediction?.priority === example?.priority ? 1 : 0;8485const result = await optimize(classifier, train, metric, {86 studentAI: student,87 teacherAI: teacher,88 numTrials: 12,89 minibatch: true,90 minibatchSize: 4,91 earlyStoppingTrials: 4,92 sampleCount: 1,93 validationExamples: validation,94 maxMetricCalls: 120,95});9697classifier.applyOptimization(result.optimizedProgram!);98console.log(result.bestScore);99```100101## Canonical Pareto Pattern102103```typescript104import { ai, flow, optimize, AxAIOpenAIModel } from '@ax-llm/ax';105106const student = ai({107 name: 'openai',108 apiKey: process.env.OPENAI_APIKEY!,109 config: { model: AxAIOpenAIModel.GPT54Mini },110});111112const teacher = ai({113 name: 'openai',114 apiKey: process.env.OPENAI_APIKEY!,115 config: { model: AxAIOpenAIModel.GPT54 },116});117118const wf = flow<{ emailText: string }>()119 .n('classifier', 'emailText:string -> priority:class "high, normal, low"')120 .n(121 'rationale',122 'emailText:string, priority:string -> rationale:string "One concise sentence"'123 )124 .e('classifier', (state) => ({ emailText: state.emailText }))125 .e('rationale', (state) => ({126 emailText: state.emailText,127 priority: state.classifierResult.priority,128 }))129 .r((state) => ({130 priority: state.classifierResult.priority,131 rationale: state.rationaleResult.rationale,132 }));133134const train = [135 { emailText: 'URGENT: Server down!', priority: 'high' },136 { emailText: 'Weekly newsletter', priority: 'low' },137];138139const validation = [140 { emailText: 'Invoice overdue', priority: 'high' },141 { emailText: 'Lunch plans?', priority: 'low' },142];143144const metric = ({ prediction, example }: { prediction: any; example: any }) => {145 const accuracy = prediction?.priority === example?.priority ? 1 : 0;146 const rationale = typeof prediction?.rationale === 'string'147 ? prediction.rationale148 : '';149 const brevity = rationale.length <= 40 ? 1 : rationale.length <= 80 ? 0.5 : 0.1;150 return { accuracy, brevity };151};152153const result = await optimize(wf, train, metric, {154 studentAI: student,155 teacherAI: teacher,156 numTrials: 16,157 minibatch: true,158 minibatchSize: 6,159 earlyStoppingTrials: 5,160 sampleCount: 1,161 validationExamples: validation,162 maxMetricCalls: 240,163});164165for (const point of result.paretoFront) {166 console.log(point.scores, point.configuration);167}168169wf.applyOptimization(result.optimizedProgram!);170console.log(result.optimizedProgram?.componentMap);171```172173## Metric Patterns174175```typescript176// Scalar objective177const scalarMetric = ({ prediction, example }) =>178 prediction.answer === example.answer ? 1 : 0;179180// Multi-objective181const multiMetric = ({ prediction, example }) => ({182 accuracy: prediction.answer === example.answer ? 1 : 0,183 brevity:184 typeof prediction?.reasoning === 'string' &&185 prediction.reasoning.length < 120186 ? 1187 : 0.2,188});189```190191- Return plain numbers or plain object literals.192- Keep objective names stable across calls.193- Prefer normalized scores such as `0..1` so trade-offs are easy to reason about.194195## Result Handling196197```typescript198const { optimizedProgram, paretoFront } = result;199200program.applyOptimization(optimizedProgram!);201202// Save for later203const saved = JSON.stringify(optimizedProgram);204205// Load later and re-apply206const loaded = JSON.parse(saved);207program.applyOptimization(loaded);208```209210- Single-target runs usually populate both `optimizedProgram.instruction` and `optimizedProgram.componentMap`.211- Tree-wide runs rely on `componentMap`, keyed by full component key.212- Pareto points expose candidate configs under `point.configuration.componentMap`.213214## Useful Options215216```typescript217const optimizer = new AxGEPA({218 studentAI,219 teacherAI,220 numTrials: 20,221 minibatch: true,222 minibatchSize: 5,223 minibatchFullEvalSteps: 5,224 earlyStoppingTrials: 5,225 minImprovementThreshold: 0,226 sampleCount: 1,227 seed: 42,228 verbose: true,229});230```231232- `numTrials`: number of reflection/evolution rounds.233- `minibatch`: reduce per-round evaluation cost.234- `minibatchSize`: examples per minibatch.235- `earlyStoppingTrials`: stop after repeated non-improvement.236- `minImprovementThreshold`: reject tiny gains below this threshold.237- `seed`: stabilize sampling during demos and tests.238239## Budgeting and Validation240241- Always create distinct `train` and `validationExamples` arrays.242- Size `maxMetricCalls` for at least one full validation pass plus several rounds.243- If the user wants a strict budget, say so explicitly and set `maxMetricCalls`.244- For expensive trees, start with `auto: 'light'` or fewer `numTrials`, then scale up.245- GEPA selects among exposed components using measured accept/reject history, not LLM-generated numeric scores. The LLM proposes component text; metrics decide whether to keep it.246- Function/tool trace reflection is keyed by stable component IDs where available, so function renames do not break saved candidate maps.247248## Troubleshooting249250- Error about `maxMetricCalls` being too small: increase it until the initial validation pass fits.251- Empty or poor Pareto front: verify the metric returns numbers for every example.252- No tree optimization effect: ensure child programs are registered under the root and expose optimizable components.253- Saved optimization applies only partly: use `program.applyOptimization(...)`, not just `setInstruction(...)`, so `componentMap` reaches the full tree.254- Agent target seems too broad: when using `agent.optimize(...)`, set `target: 'actor'`, `'responder'`, `'all'`, or explicit program IDs. The wrapper filters GEPA components to the selected target.255256## Good Example Targets257258- `/Users/vr/src/ax/src/examples/optimize.ts`259- `/Users/vr/src/ax/src/examples/gepa.ts`260- `/Users/vr/src/ax/src/examples/gepa-flow.ts`261- `/Users/vr/src/ax/src/examples/gepa-train-inference.ts`262- `/Users/vr/src/ax/src/examples/gepa-quality-vs-speed-optimization.ts`263- `/Users/vr/src/ax/src/examples/axagent-gepa-optimization.ts`