Problem
apply_random_mutation() returns a mutated Workflow IR with changes applied (e.g., rewritten prompt_template on an AgentNode). But consumers that rebuild from a config object (like Package.compile()) lose these mutations because compile() always starts fresh from the Package's knobs/defaults.
This means PROMPT_MUTATE has no effect in any outer loop that follows the pattern:
child_wf, rec = apply_random_mutation(parent_wf, strategy, gen)
# ... extract knob overrides from child_wf.knob_values ...
child_pipeline = build_pipeline(cfg_with_overrides) # rebuilds from config
result = evaluate(child_pipeline) # prompt mutation is lost
The mutated prompt lives in child_wf.nodes[target].prompt_template but is never seen by the evaluator.
Impact
Discovered in the chess evolution demo: after 30+ generations with 50% PROMPT_MUTATE weight and a working default_prompt_rewriter (38/38 Opus rewrites succeeded), every prompt mutation was actually evaluated with the original prompt. The rewritten prompts were silently discarded.
This affects any outer loop consumer that doesn't evaluate the Workflow IR directly — which is the natural pattern when using Package ecosystem's compile().
Proposed fix
The outer loop should carry prompt (and other node-level) mutations through the evaluation path. Options:
Workflow.apply_to(package: Package) -> Package — patches mutations from the IR back onto a Package, returning a new Package ready to compile+evaluate
WorkflowExecutor accepts Workflow directly — skip the Package round-trip entirely for evaluation
- Store node-level mutations in
knob_values — treat prompts as knobs so they survive the compile() round-trip (this is what OptKnob(kind="prompt") was designed for, but mutate_prompt bypasses it)
Option 3 is the most aligned with the existing design — PROMPT_MUTATE should write to knob_values when the target node has an associated OptKnob(kind="prompt"), so mutations survive compile().
References
🤖 Generated with Claude Code
Problem
apply_random_mutation()returns a mutatedWorkflowIR with changes applied (e.g., rewrittenprompt_templateon anAgentNode). But consumers that rebuild from a config object (likePackage.compile()) lose these mutations becausecompile()always starts fresh from the Package's knobs/defaults.This means PROMPT_MUTATE has no effect in any outer loop that follows the pattern:
The mutated prompt lives in
child_wf.nodes[target].prompt_templatebut is never seen by the evaluator.Impact
Discovered in the chess evolution demo: after 30+ generations with 50% PROMPT_MUTATE weight and a working
default_prompt_rewriter(38/38 Opus rewrites succeeded), every prompt mutation was actually evaluated with the original prompt. The rewritten prompts were silently discarded.This affects any outer loop consumer that doesn't evaluate the
WorkflowIR directly — which is the natural pattern when using Package ecosystem'scompile().Proposed fix
The outer loop should carry prompt (and other node-level) mutations through the evaluation path. Options:
Workflow.apply_to(package: Package) -> Package— patches mutations from the IR back onto a Package, returning a new Package ready to compile+evaluateWorkflowExecutoracceptsWorkflowdirectly — skip the Package round-trip entirely for evaluationknob_values— treat prompts as knobs so they survive thecompile()round-trip (this is whatOptKnob(kind="prompt")was designed for, butmutate_promptbypasses it)Option 3 is the most aligned with the existing design —
PROMPT_MUTATEshould write toknob_valueswhen the target node has an associatedOptKnob(kind="prompt"), so mutations survivecompile().References
factory/workflow/package.py🤖 Generated with Claude Code