Skip to content

symfonic.agent.backend.structured_plan

structured_plan

Whether this plan may promise a structured answer, checked before it runs.

Split from :mod:symfonic.agent.backend.plan at the 300-line budget. Both halves answer one question -- can the configured model actually produce the type the caller asked for -- and the plan factory's remaining methods answer a different one, which is what makes this a seam rather than a line count.

require_structured_output

require_structured_output(chat_model: Any, output_type: type) -> None

Prove the model can bind the schema, before a single token is spent.

hasattr(model, "with_structured_output") is not proof: BaseChatModel declares the method and raises NotImplementedError from it, so a presence check passes for every model and the failure lands mid-run โ€” exactly the RES-9 failure mode. Actually binding the schema is the real probe, and it is not a provider call: it builds a runnable locally and touches no network.

Source code in src/symfonic/agent/backend/structured_plan.py
def require_structured_output(chat_model: Any, output_type: type) -> None:
    """Prove the model can bind the schema, before a single token is spent.

    ``hasattr(model, "with_structured_output")`` is not proof: ``BaseChatModel``
    declares the method and raises ``NotImplementedError`` from it, so a
    presence check passes for every model and the failure lands mid-run โ€”
    exactly the RES-9 failure mode. Actually binding the schema is the real
    probe, and it is not a provider call: it builds a runnable locally and
    touches no network.
    """
    from symfonic.agent.structured import supports_structured_output

    message = (
        "The configured model provider cannot produce structured output; "
        "drop output_type or use a tool-calling / JSON-mode provider."
    )
    if not supports_structured_output(chat_model):
        raise ConfigurationError(message)
    try:
        chat_model.with_structured_output(output_type)
    except NotImplementedError as exc:
        raise ConfigurationError(message) from exc

validate_output_type

validate_output_type(output_type: type[BaseModel] | None) -> None

Refuse anything that is not a Pydantic model, at construction.

Source code in src/symfonic/agent/backend/structured_plan.py
def validate_output_type(output_type: type[BaseModel] | None) -> None:
    """Refuse anything that is not a Pydantic model, at construction."""
    if output_type is None:
        return
    if not isinstance(output_type, type) or not issubclass(output_type, BaseModel):
        raise ConfigurationError(
            f"output_type must be a Pydantic BaseModel subclass, got {output_type!r}."
        )