Layer the compiled prompt onto whatever the phase already assembled.
Returning contributed alone is what this capability used to do, and it
silently discarded two things: Agent(instructions=...), which the
kernel's own stage puts on the assembly, and any earlier capability's
contribution. An adopter who passed both a prompt capability and their own
instructions got only the compiled text, with nothing in the trace to say
the rest had been dropped.
Order is base-then-compiled: the instructions handed to the Agent are
the operator's, and the compiled layers are content this capability was
configured to add underneath them.
Source code in src/symfonic/capabilities/prompting/composition.py
| def compose_assembly(context: Any, contributed: Any) -> Any:
"""Layer the compiled prompt onto whatever the phase already assembled.
Returning ``contributed`` alone is what this capability used to do, and it
silently discarded two things: ``Agent(instructions=...)``, which the
kernel's own stage puts on the assembly, and any earlier capability's
contribution. An adopter who passed both a prompt capability and their own
instructions got only the compiled text, with nothing in the trace to say
the rest had been dropped.
Order is base-then-compiled: the instructions handed to the ``Agent`` are
the operator's, and the compiled layers are content this capability was
configured to add underneath them.
"""
prior = getattr(context, "assembly", None)
existing = getattr(prior, "instructions", None)
existing_blocks = tuple(getattr(prior, "system_blocks", ()))
if not existing:
if not existing_blocks:
return contributed
return replace(
contributed,
system_blocks=(*existing_blocks, *contributed.system_blocks),
)
if existing == contributed.instructions:
# Both assemblies project the same bytes. Carrying both block lists
# would send those bytes twice on a structured-system provider.
return contributed
joined = existing
if contributed.instructions:
joined = f"{existing}\n\n{contributed.instructions}"
# An Anthropic breakpoint caches its prefix, including earlier system
# blocks. Keeping deployment instructions as their own unannotated block
# preserves their provenance and avoids assigning *them* a cache policy;
# it does not claim that an ensuing cached contribution excludes them from
# the provider's cached prefix. Earlier composed blocks already carry
# their own annotations and must remain intact.
prior_blocks = existing_blocks or ({"type": "text", "text": existing},)
blocks = (*prior_blocks, *contributed.system_blocks)
return replace(contributed, instructions=joined, system_blocks=blocks)
|