Skip to content

symfonic.migration.framework_config

framework_config

Compose a public Agent from the supported subset of a FrameworkConfig.

CORE-235. The contract -- what is translated, inherited, obsolete or refused -- is :mod:symfonic.migration.framework_config_contract. This module only applies it. The result is a plain :class:symfonic.Agent, so it is what an AgentHost composer may return; SymfonicAgent and AgentBuilder are untouched.

agent_from_config

agent_from_config(config: FrameworkConfig, model_provider: Any, *, instructions: str | None = None, tools: Sequence[Any] = (), capabilities: Sequence[Any] = ()) -> Agent

Return an Agent honouring config's translated settings.

The caller must explicitly set auto_consolidate=False on the legacy config: native QUICK requires separately proved exact-case AgentHost composition. An omitted stock True refuses before Agent construction.

instructions, tools and capabilities are the caller's, passed to Agent unchanged: the config never contributes a system prompt, a tool or a memory port. Memory, sessions and history therefore work exactly as they do on any Agent -- through the capabilities composed here and the session_id/history arguments of each call.

agent.model and agent.recursion_limit at their stock values are not requests: the Agent keeps its provider-declared model and native round budget. agent.model is translated field by field: only the fields away from ModelConfig() are applied, over the provider's declared default.

Raises:

Type Description
TypeError

config is not a FrameworkConfig.

UnsupportedFrameworkSettingError

an active setting is outside the contract, or synthesize_on_empty_final is set beside a TerminalSynthesisCapability the caller composed. Raised before the Agent exists, so nothing reaches the provider, memory or a tool.

Source code in src/symfonic/migration/framework_config.py
def agent_from_config(
    config: FrameworkConfig,
    model_provider: Any,
    *,
    instructions: str | None = None,
    tools: Sequence[Any] = (),
    capabilities: Sequence[Any] = (),
) -> Agent:
    """Return an ``Agent`` honouring ``config``'s translated settings.

    The caller must explicitly set ``auto_consolidate=False`` on the legacy
    config: native QUICK requires separately proved exact-case AgentHost
    composition. An omitted stock ``True`` refuses before Agent construction.

    ``instructions``, ``tools`` and ``capabilities`` are the caller's, passed
    to ``Agent`` unchanged: the config never contributes a system prompt, a
    tool or a memory port. Memory, sessions and history therefore work exactly
    as they do on any ``Agent`` -- through the capabilities composed here and
    the ``session_id``/``history`` arguments of each call.

    ``agent.model`` and ``agent.recursion_limit`` at their stock values are not
    requests: the ``Agent`` keeps its provider-declared model and native round
    budget. ``agent.model`` is translated field by field: only the fields away
    from ``ModelConfig()`` are applied, over the provider's declared default.

    Raises:
        TypeError: ``config`` is not a ``FrameworkConfig``.
        UnsupportedFrameworkSettingError: an active setting is outside the
            contract, or ``synthesize_on_empty_final`` is set beside a
            ``TerminalSynthesisCapability`` the caller composed. Raised before
            the ``Agent`` exists, so nothing reaches the provider, memory or a
            tool.
    """
    if not isinstance(config, FrameworkConfig):
        raise TypeError(f"agent_from_config needs a FrameworkConfig, not {type(config).__name__}")
    if config.auto_consolidate is not False or "auto_consolidate" not in config.model_fields_set:
        # A caller-owned Sequence may do work while being inspected. Refuse
        # legacy consolidation before touching it.
        refused = unsupported_settings(config, composed=False)
        raise UnsupportedFrameworkSettingError(refused, composed=False)
    composed = tuple(capabilities)
    refused = unsupported_settings(config, composed=bool(composed))
    if config.synthesize_on_empty_final and any(
        isinstance(cap, TerminalSynthesisCapability) for cap in composed
    ):
        # The Agent would refuse the pair too, but unversioned: a host could
        # not tell it from a failed turn. Refused here it maps like any other.
        refused = (*refused, ("synthesize_on_empty_final", _DUPLICATE_SYNTHESIS))
    if refused:
        raise UnsupportedFrameworkSettingError(refused, composed=bool(composed))
    if config.synthesize_on_empty_final:
        composed = (*composed, TerminalSynthesisCapability())
    stock = FrameworkConfig().agent
    model, limit = config.agent.model, config.agent.recursion_limit
    return Agent(
        model_provider,
        instructions=instructions,
        model=_translated_model(model, model_provider),
        tools=tools,
        capabilities=composed,
        max_model_rounds=(
            None if equivalent(limit, stock.recursion_limit) else rounds_for_recursion_limit(limit)
        ),
    )