11 KiB
GEPA Scope Audit — 2026-04-30
Status: HALT-AND-PM. Three findings, two of them launch-blocking. Decisions needed from Marko before I patch.
Summary table
| Mechanism | Source | Production scope | Status | Action |
|---|---|---|---|---|
| GEPA input optimizer (vague-prompt expansion via @ax-llm/ax) | chat.ts:709 getOptimizerService(server) |
First user message only since 2026-04-16 (commit 6e0cd8b) |
Intentional — documented rationale (mid-conversation "yes"/"LGTM" replies were getting expanded into phantom instructions). | No change unless Marko wants to revert. |
Evolved Behavioral-Spec overrides (buildActiveBehavioralSpec + on-disk overrides) |
chat.ts:207 reads server.activeBehavioralSpec per buildSystemPrompt call |
Every turn (cache invalidates on history-length change; live re-decoration on 'behavioral-spec:reloaded' event) |
✅ Correct. | None. |
Evolved Persona overrides (deployPersonaOverride writing to ~/.waggle/personas/<id>.json) |
chat.ts:264 + :924 use getPersona(activePersonaId) |
Built-ins only. Evolved personas (custom IDs) never resolved. | ❌ Bug. | Switch to listPersonas-based lookup. |
PromptAssembler v5 evolved shapes (Faza 1 variants claude::gen1-v1, qwen-thinking::gen1-v1) |
orchestrator.buildAssembledPrompt(...) defined but never called outside eval harnesses |
Never applied in production. Feature flag exists; consumer code missing. | ❌ Missing wiring — most likely the "regression" Marko was sensing. | Wire agent-loop.ts to check isEnabled('PROMPT_ASSEMBLER') and use the assembled path when on. |
1. GEPA input optimizer — first-call-only, intentional
Trace path: packages/server/src/local/routes/chat.ts:709 → if (!hasCustomRunner && isFirstUserMessage) → getOptimizerService(server) → optimizer.optimize(...).
Commit that narrowed it: 6e0cd8b fix(agent): GEPA mid-conversation expansion + entity extractor person bias (2026-04-16).
Diff:
- if (!hasCustomRunner) {
+ if (!hasCustomRunner && isFirstUserMessage) {
Rationale (from the commit message + code comment):
RC-1: GEPA was running on EVERY user message, expanding mid-conversation replies like "yes thats the story" into phantom instructions the user never intended. Now only runs on the first message in a session.
This is the input-side prompt expansion (cheap-Haiku classifier that rewrites vague user messages into more concrete ones), NOT the output-side evolved system prompt application. The two are commonly conflated under the GEPA umbrella but they're distinct subsystems.
My read: the rationale is valid. Mid-conversation "yes" without context absolutely would get misclassified as a vague standalone request. Reverting would re-introduce the phantom-expansion bug.
Decision needed: confirm we keep first-message-only. ✅ default recommendation.
2. Evolved Behavioral-Spec overrides — every turn, correct
Trace path:
- Boot:
packages/server/src/local/index.ts:343-352buildsactiveBehavioralSpec = buildActiveBehavioralSpec(loadBehavioralSpecOverrides(dataDir)), decoratesserver.activeBehavioralSpec, listens for'behavioral-spec:reloaded'and re-derives + re-decorates on each fire. - Per turn:
chat.ts:207const activeSpec = server.activeBehavioralSpec ?? BEHAVIORAL_SPEC; prompt += '\n' + activeSpec.rules;— reads the live decorator value. - Cache invalidation:
chat.ts:122-125cache key includeshistoryLength. Each turn the history grows by 1-2 entries → cache miss →buildSystemPromptre-runs → readsserver.activeBehavioralSpecafresh.
Per project memory (S1): "Hot-reload on 'behavioral-spec:reloaded' event. End-to-end test proves accept → live spec update path works." ✅ Confirmed by code reading.
No action needed. This is the path that satisfies the "+12.5pp continuous uplift" claim for the behavioral-spec subset of evolution outputs. If Faza 1 produced behavioral-spec deltas, those land correctly.
3. Evolved Persona overrides — bug, never applied
Trace path:
- Deploy:
packages/agent/src/evolution-deploy.ts:67-90deployPersonaOverride(dataDir, {personaId, systemPrompt})writes~/.waggle/personas/<id>.jsonatomically. - Loader:
packages/agent/src/custom-personas.ts:12-30loadCustomPersonas(dataDir)reads all.jsonfiles in that dir on every call. - Catalog:
packages/agent/src/personas.ts:67-70listPersonas()returns[...PERSONAS, ...customPersonas]. - Consumer (chat — bug here):
packages/server/src/local/routes/chat.ts:264const persona = getPersona(activePersonaId);—getPersonais built-in-only:Andexport function getPersona(id: string): AgentPersona | null { return PERSONAS.find(p => p.id === id) ?? null; // built-in only }personas.ts:54even comments this explicitly:/** Get a persona by ID (built-in only — use listPersonas() for full catalog) */.
Symptom: for any evolved persona deployed under a non-built-in ID (e.g. claude::gen1-v1), getPersona returns null → composePersonaPrompt(prompt, null, ...) → DOCX hint + tone instruction added but the evolved system prompt is dropped on the floor. Same on chat.ts:924.
Even for SHADOW IDs (deploy with id matching a built-in like coder to override): find(p => p.id === 'coder') would still return the built-in entry from PERSONAS because getPersona doesn't read custom at all.
Confidence: HIGH that this is a bug. The deploy comment says "loader picks it up on next listPersonas() call" — listPersonas works, but the chat consumer uses getPersona.
Proposed fix (single-file, 2 lines):
// chat.ts top of file
- import { getPersona, composePersonaPrompt, BEHAVIORAL_SPEC } from '@waggle/agent';
+ import { listPersonas, composePersonaPrompt, BEHAVIORAL_SPEC } from '@waggle/agent';
+
+ const findPersona = (id: string) => listPersonas().find(p => p.id === id) ?? null;
// chat.ts:264 + chat.ts:924
- const persona = getPersona(activePersonaId);
+ const persona = findPersona(activePersonaId);
listPersonas re-reads custom from disk on every call (no cache, see custom-personas.ts:12-30), so updates are picked up immediately. The find ordering does prefer built-in for shadow-IDs — which is the safer default (don't accidentally let a malformed evolved persona hijack coder). If shadowing is desired, the deploy step needs to use a derived ID.
Decision needed: ratify the fix as proposed.
4. PromptAssembler v5 evolved shapes — missing production wiring
Most likely THE bug Marko was sensing.
Trace path:
- Definition:
packages/agent/src/orchestrator.ts:516async buildAssembledPrompt(query, persona, opts)— produces a tier-adaptive, typed, scaffolded prompt via the v5 sixth layer. - Doc-comment at line 512:
/** Consumers: agent-loop.ts when isEnabled('PROMPT_ASSEMBLER'). */ - Reality:
agent-loop.tshas zero references toPROMPT_ASSEMBLER,isEnabled,FEATURE_FLAGS, orbuildAssembledPrompt. Verified via:grep -n "PROMPT_ASSEMBLER\|isEnabled\|buildAssembledPrompt\|FEATURE_FLAGS" packages/agent/src/agent-loop.ts → no results - The only callers of
buildAssembledPromptoutside the orchestrator's own definition are:tests/eval/prompt-assembler-eval.ts:335tests/eval/prompt-assembler-v5-eval.ts:688tests/prompt-assembler-feature-flag.test.ts:65, 81
All four are eval harnesses or unit tests. Production runtime never invokes the assembled path.
Implication for the launch story:
- The +12.5pp uplift numbers from Faza 1 evals are real — those evals call
buildAssembledPromptcorrectly. - But in production (chat + spawn),
WAGGLE_PROMPT_ASSEMBLER=1does nothing. The flag is a no-op. The runtime only usesorch.buildSystemPrompt()(the v4 / pre-PA path). - "Continuous +12.5pp uplift" claim does not match implementation today.
This is a missing-wiring, not a deliberate narrowing. Marko needs to ratify whether to (a) ship the wiring now, or (b) hold the launch claim until a different rollout strategy lands.
Proposed fix (sketch, agent-loop integration):
In packages/agent/src/agent-loop.ts, around the system-prompt usage, gate on the flag:
import { isEnabled } from './feature-flags.js';
// ...
const systemPrompt = isEnabled('PROMPT_ASSEMBLER') && config.orchestrator
? (await config.orchestrator.buildAssembledPrompt(/* query */, /* persona */, /* opts */)).prompt
: config.systemPrompt;
But that requires:
- Threading
orchestrator+query(latest user message) intoAgentLoopConfig— currentlysystemPromptis pre-built by the caller. - Threading
personaso the assembler can layer it. - Threading
taskShapeif Faza 1's evolved shapes are task-typed (e.g.,Plan,Recall,Summarize). - Doing this both for chat (
chat.ts:1042agentConfig construction) AND for spawn (fleet.tsrunAgentLoop call I just shipped — Phase B).
This is a substantial wiring task, not a 2-line patch. ~30-90 min of careful edits across 3-4 files plus tests.
Decision needed: ratify whether to wire it now (and accept the scope), or ship the launch with the v4 path and revisit. If we ship with v4: launch story copy needs to change from "+12.5pp continuous uplift" to something honest about the eval-vs-production gap, OR the flag default needs to flip to ON with the wiring.
My recommendation
- Finding #1 (input optimizer first-call-only): No change. Documented rationale is sound.
- Finding #2 (behavioral-spec): No change. Already correct (every turn, hot-reloadable).
- Finding #3 (persona override consumer): Patch. 2-line fix in chat.ts. Low risk. Tests:
vitest run packages/agent/tests/evolution-deploy.test.ts(existing)- Manual verify: deploy a custom persona via the personas API, set workspace.personaId to the custom id, send a chat → response should reflect the evolved system prompt.
- Finding #4 (PromptAssembler wiring): HALT-AND-PM. This is a launch story decision. Three options:
- (a) Wire it now: 30-90 min, scope creep relative to today's session, but shippable.
- (b) Adjust the launch claim: ship with v4 path, copy honestly says "evolved variants demonstrated +12.5pp in offline eval; production rollout staged".
- (c) Move the May 14 routine forward to also include this wiring task — bundles the structural fix with contract tests.
Awaiting Marko's call on #3 and #4. No code changes shipped yet. Investigation only.
Files touched by this audit: none. This is a research-only output.
Audit refs:
packages/server/src/local/routes/chat.ts:122-272, 660-738, 920-925, 1042-1056, 1503packages/server/src/local/index.ts:340-352packages/agent/src/personas.ts:54-70packages/agent/src/custom-personas.ts:12-30packages/agent/src/evolution-deploy.ts:60-90packages/agent/src/orchestrator.ts:466-506, 512-516packages/agent/src/agent-loop.ts(full file — verified zero PROMPT_ASSEMBLER references)packages/agent/src/feature-flags.ts:34- Commit
6e0cd8b(2026-04-16) GEPA optimizer first-message narrowing