diff --git a/factory/outer_loop/mutations.py b/factory/outer_loop/mutations.py index 9090be31..519fcea0 100644 --- a/factory/outer_loop/mutations.py +++ b/factory/outer_loop/mutations.py @@ -599,9 +599,15 @@ def mutate_prompt( try: updated = node.model_copy(update={"prompt_template": new_prompt}) wf.nodes[node_id] = updated # type: ignore[assignment] - except Exception: + except Exception as exc: + log.warning("prompt_mutate_validation_failed", node=node_id, error=str(exc)) return None + # Persist in knob_values so the mutation survives Package.compile() round-trips + prompt_knob = f"_prompt_{node_id}" + wf.knob_values[prompt_knob] = new_prompt + wf.knob_expandable[prompt_knob] = f"Prompt for {node_id}" + record = MutationRecord( operator=MutationType.PROMPT_MUTATE, target_node=node_id, diff --git a/factory/workflow/package.py b/factory/workflow/package.py index 63f54823..41827e89 100644 --- a/factory/workflow/package.py +++ b/factory/workflow/package.py @@ -130,14 +130,41 @@ def configure(self, **knob_values: str | float) -> Package: return self.model_copy(update={"knobs": new_knobs}) def compile(self) -> Workflow: - """Lower this package to a flat, mutable Workflow IR.""" + """Lower this package to a flat, mutable Workflow IR. + + Prompt knobs (``_prompt_`` in knob_values) are applied + back to node prompt_templates so that PROMPT_MUTATE mutations + survive compile() round-trips. + """ wf = self.graph.model_copy(deep=True) + # Preserve _prompt_* entries from previous mutations before overwriting + saved_prompts = { + k: v for k, v in wf.knob_values.items() + if k.startswith("_prompt_") + } + saved_expandable = { + k: v for k, v in wf.knob_expandable.items() + if k.startswith("_prompt_") + } if self.knobs: wf.knob_values = {k.name: k.default for k in self.knobs} wf.knob_bounds = {k.name: list(k.bounds) for k in self.knobs if k.bounds} wf.knob_expandable = { k.name: k.expansion_hint for k in self.knobs if k.expandable } + wf.knob_values.update(saved_prompts) + wf.knob_expandable.update(saved_expandable) + for key, val in list(wf.knob_values.items()): + if key.startswith("_prompt_") and isinstance(val, str): + node_id = key[len("_prompt_"):] + node = wf.nodes.get(node_id) + if node and hasattr(node, "prompt_template"): + try: + wf.nodes[node_id] = node.model_copy( + update={"prompt_template": val} + ) + except Exception: + pass return wf