Skip to content

Role models

FrameworkConfig.role_models is a per-role ModelConfig override map. Adopters who want to route different call sites in the framework to different model SKUs (e.g. a cheap Haiku for summarisation, a high-reasoning Sonnet for the brain, a tiny model for the critic) set entries on this map.

Shape

from symfonic.agent.config import FrameworkConfig
from symfonic.core.config import AgentConfig, ModelConfig

fw = FrameworkConfig(
    agent=AgentConfig(
        model=ModelConfig(model_name="claude-sonnet-4-5"),  # brain default
    ),
    role_models={
        "summary": ModelConfig(model_name="claude-haiku-4-5"),
        "critic":  ModelConfig(model_name="claude-haiku-4-5"),
    },
)

Unset roles fall back to AgentConfig.model.

Role taxonomy

Role Site Wired in
action react node brain LLM (engine.py:1144-1158) v7.9.0
summary ContextWindowSummariser (core/nodes/context_window.py) v7.24.2
critic MetacognitiveCriticMiddleware (agent/middleware/metacognitive.py) v7.24.2
reflection self-reflection (engine.py:7233) v7.14.1
router CapabilityRouter (memory/layers/procedural/router.py) v7.9.1+ (backlog)
consolidation Phase 12 promotion (core/learning/consolidation.py) v7.9.1+ (backlog)

v7.24.2 update

role_models['summary'] and role_models['critic'] were documented at agent/config.py:1050-1053 since v7.9.0 but the consuming call sites never read them. Adopters who set the override silently got the framework default Sonnet on both call sites.

v7.24.2 closes both gaps:

  • summary -- create_context_window_node grows a role_models kwarg; core/presets.py threads it from AgentConfig.role_models; engine.py propagates the full FrameworkConfig.role_models map onto AgentConfig.role_models (alongside the existing 'action' propagation). The summariser uses role_models['summary'] as-is when set, otherwise builds the pre-v7.24.2 default from CompactionConfig.compaction_model.
  • critic -- MetacognitiveCriticMiddleware consults self._config.role_models.get('critic') before falling back to the bare ModelConfig() default. No threading needed -- the middleware already held a FrameworkConfig reference via self._config.

The default path (no override set) is byte-identical pre-vs-post v7.24.2: adopters who never touched role_models see no change.

Resolver helper

The canonical resolver is SymfonicAgent.resolve_model_config(role) (engine.py:2578). It returns the override when set, or AgentConfig.model otherwise. Call sites that don't have a SymfonicAgent handle (e.g. middleware constructed before the agent is fully assembled) can replicate the same shape by reading FrameworkConfig.role_models.get(role) directly.

Adopter pattern: cost vs quality

fw = FrameworkConfig(
    agent=AgentConfig(
        model=ModelConfig(model_name="claude-sonnet-4-5"),  # brain
    ),
    role_models={
        # Summarisation is bulk-text -- cheap Haiku wins on cost.
        "summary": ModelConfig(model_name="claude-haiku-4-5"),
        # Critic is short-form -- cheap Haiku also wins on cost, but
        # adopters who saw the critic miss subtle CorePreference
        # violations should pin Sonnet here.
        "critic":  ModelConfig(model_name="claude-haiku-4-5"),
        # Reflection sees the same prompt the brain saw -- usually
        # the same SKU.  Override only if you want a "second opinion"
        # from a different model class.
        "reflection": ModelConfig(model_name="claude-sonnet-4-5"),
    },
)

The framework owns the orchestration; the adopter owns the model assignment. This is the v7.8.3-era architectural principle that role_models operationalises.