Skip to content

symfonic.capabilities.prompting.request

request

The compiler's only accepted input, and the one legal way to derive a child.

:class:PromptCompileRequest is a normalized value rather than adopter configuration. Re-parsing a config file inside the compiler is how a second, subtly different interpretation of context_strategy gets born; the facade normalizes once and hands the result here.

:func:derive_child_request is the delegation half of the same rule. A child narrows: it may drop contributions and lower its budget, and every widening attempt is refused here rather than at the point of use, because a child that could add a contribution or raise a ceiling its parent did not have would make the parent's compile a suggestion.

PromptCompileRequest dataclass

PromptCompileRequest(instructions: str | None = None, contributions: tuple[PromptContribution, ...] = (), strategy: ContextStrategy = ContextStrategy.STRATIFIED, budget: PromptBudget = PromptBudget(), policy: RenderPolicy = DEFAULT_POLICY, scope_path: str = '', turn: int = 0, last_known_good: InMemoryLastKnownGood | None = None, estimator: object | None = None, principal_grants: frozenset[str] = frozenset(), operator_overrides: Mapping[str, str] | None = None)

Everything one prompt compile is allowed to depend on.

derive_child_request

derive_child_request(parent: PromptCompileRequest, *, contributions: Sequence[PromptContribution] | None = None, budget: PromptBudget | None = None, scope_path: str | None = None, instructions: str | None = None, principal_grants: frozenset[str] | None = None, operator_overrides: Mapping[str, str] | None = None) -> PromptCompileRequest

Derive a delegated child's request by narrowing parent.

Omitting contributions inherits the parent's inheritable set — the safe default, since a child silently losing its boundaries is the failure that matters. Naming them narrows: every named id must already be inheritable on the parent, so a child cannot promote a contribution its parent marked non-inheritable by re-declaring it under the same name.

The two S01 access axes inherit, and the rule is stated rather than implied. Delegation here is the same principal on the same turn, so a child that dropped principal_grants would silently withhold from the subagent a contribution the parent was entitled to, and one that dropped operator_overrides would serve the source's own text where the operator had replaced it — a divergence between what the parent and the child read that no diagnostic would explain. Inheriting is therefore the default, and both axes narrow the same way everything else here does:

  • principal_grants must be a subset of the parent's. A child cannot hand itself a verb the turn's principal was not granted.
  • operator_overrides must be a sub-mapping of the parent's — the same text under a subset of the same ids. A child may drop an override; it may not introduce one, nor change the text of one, because either would make the operator's decision for the parent turn something the child rewrites.

Delegation is a block-visibility boundary elsewhere in this repo (tests/agent/subagents/test_child_block_lockdown.py), and that boundary is drawn by inherit on the contribution, which is enforced above. These two axes are about the principal and the operator, not about which blocks a child may see.

Source code in src/symfonic/capabilities/prompting/request.py
def derive_child_request(
    parent: PromptCompileRequest,
    *,
    contributions: Sequence[PromptContribution] | None = None,
    budget: PromptBudget | None = None,
    scope_path: str | None = None,
    instructions: str | None = None,
    principal_grants: frozenset[str] | None = None,
    operator_overrides: Mapping[str, str] | None = None,
) -> PromptCompileRequest:
    """Derive a delegated child's request by narrowing ``parent``.

    Omitting ``contributions`` inherits the parent's inheritable set — the safe
    default, since a child silently losing its boundaries is the failure that
    matters. Naming them narrows: every named id must already be inheritable on
    the parent, so a child cannot promote a contribution its parent marked
    non-inheritable by re-declaring it under the same name.

    **The two S01 access axes inherit, and the rule is stated rather than
    implied.** Delegation here is the same principal on the same turn, so a
    child that dropped ``principal_grants`` would silently withhold from the
    subagent a contribution the parent was entitled to, and one that dropped
    ``operator_overrides`` would serve the source's own text where the operator
    had replaced it — a divergence between what the parent and the child read
    that no diagnostic would explain. Inheriting is therefore the default, and
    both axes narrow the same way everything else here does:

    * ``principal_grants`` must be a subset of the parent's. A child cannot
      hand itself a verb the turn's principal was not granted.
    * ``operator_overrides`` must be a sub-mapping of the parent's — the same
      text under a subset of the same ids. A child may drop an override; it may
      not introduce one, nor change the text of one, because either would make
      the operator's decision for the parent turn something the child rewrites.

    Delegation is a block-*visibility* boundary elsewhere in this repo
    (``tests/agent/subagents/test_child_block_lockdown.py``), and that boundary
    is drawn by ``inherit`` on the contribution, which is enforced above. These
    two axes are about the principal and the operator, not about which blocks a
    child may see.
    """
    available = {c.contribution_id: c for c in inheritable(parent.contributions)}
    if contributions is None:
        selected: tuple[PromptContribution, ...] = tuple(available.values())
    else:
        unknown = [c.contribution_id for c in contributions if c.contribution_id not in available]
        if unknown:
            raise InheritanceError(
                f"child request declares {unknown!r}, which the parent did not contribute "
                "(or marked non-inheritable). A child narrows its parent's standing context; "
                "it never widens it."
            )
        selected = tuple(contributions)

    child_budget = parent.budget if budget is None else budget
    if not child_budget.narrows(parent.budget):
        raise InheritanceError(
            f"child budget {child_budget.max_total_tokens} does not narrow the parent's "
            f"{parent.budget.max_total_tokens}; a delegated run may not spend more than the "
            "run that delegated to it."
        )

    return replace(
        parent,
        contributions=selected,
        budget=child_budget,
        scope_path=parent.scope_path if scope_path is None else scope_path,
        instructions=parent.instructions if instructions is None else instructions,
        principal_grants=_child_grants(parent, principal_grants),
        operator_overrides=_child_overrides(parent, operator_overrides),
    )