Skip to content

Templates

Pre-built company templates, personality presets, and template builder.

Schema

schema

Template schema: Pydantic models for company templates.

TemplateVariable pydantic-model

Bases: BaseModel

A user-configurable variable within a template.

Variables declared here are extracted from the template YAML during the first parsing pass (before Jinja2 rendering). The variables section must use plain YAML -- no Jinja2 expressions.

Attributes:

Name Type Description
name NotBlankStr

Variable name (used in {{ name }} placeholders).

description str

Human-readable description for prompts/docs.

var_type Literal['str', 'int', 'float', 'bool']

Expected Python type name.

default str | int | float | bool | None

Default value (None means no default is provided). The required attribute determines whether the user must supply a value.

required bool

Whether the user must provide this value.

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

Validators:

  • _validate_required_has_no_default
  • _validate_default_matches_var_type

name pydantic-field

name

Variable name

description pydantic-field

description = ''

Human-readable description

var_type pydantic-field

var_type = 'str'

Expected value type

default pydantic-field

default = None

Default value

required pydantic-field

required = False

Whether required

TemplateAgentConfig pydantic-model

Bases: BaseModel

Agent definition within a template.

Uses string references and presets rather than full AgentConfig. The renderer expands these into full agent configuration dicts.

Attributes:

Name Type Description
role NotBlankStr

Built-in role name (case-insensitive match to role catalog).

name str

Agent name (may contain Jinja2 placeholders; empty triggers auto-generation).

level SeniorityLevel

Seniority level override.

model NotBlankStr | dict[str, Any]

Model tier alias ("large", "medium", "small") or a structured ModelRequirement dict with tier, priority, min_context, and capabilities fields.

personality_preset NotBlankStr | None

Named personality preset from the presets registry.

personality dict[str, Any] | None

Inline personality config dict (alternative to personality_preset).

department NotBlankStr | None

Department override (None uses the template system default during rendering).

merge_id str

Stable identity for inheritance merge. When a template has multiple agents with the same (role, department) pair, merge_id disambiguates them so child templates can target a specific agent.

remove bool

Merge directive -- when True, removes matching parent agent during inheritance.

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

Validators:

  • _validate_modelmodel
  • _validate_personality_mutual_exclusion

role pydantic-field

role

Built-in role name

name pydantic-field

name = ''

Agent name (may have Jinja2 vars)

level pydantic-field

level = MID

Seniority level

model pydantic-field

model = 'medium'

Model tier alias or structured ModelRequirement dict

personality_preset pydantic-field

personality_preset = None

Named personality preset

personality pydantic-field

personality = None

Inline personality override (alternative to preset)

department pydantic-field

department = None

Department override

merge_id pydantic-field

merge_id = ''

Stable identity for inheritance merge

remove pydantic-field

remove = False

Merge directive: remove matching parent agent

TemplateDepartmentConfig pydantic-model

Bases: BaseModel

Department definition within a template.

Provides structural information -- department names, budget allocations, the head role, reporting lines, and operational policies.

Attributes:

Name Type Description
name NotBlankStr

Department name (standard or custom).

budget_percent float

Percentage of company budget (0-100).

head_role NotBlankStr | None

Role name of the department head.

head_merge_id NotBlankStr | None

Optional merge_id of the head agent. Should be provided when multiple agents share the same role used in head_role.

reporting_lines tuple[dict[str, str], ...]

Reporting line definitions within this department.

policies dict[str, Any] | None

Department operational policies.

ceremony_policy dict[str, Any] | None

Per-department ceremony policy override (dict[str, Any] | None). None inherits the project-level policy.

remove bool

Merge directive -- when True, removes matching parent department during inheritance.

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

Validators:

  • _validate_head_merge_id_requires_head_role

name pydantic-field

name

Department name

budget_percent pydantic-field

budget_percent = 0.0

Percentage of company budget

head_role pydantic-field

head_role = None

Role name of department head

head_merge_id pydantic-field

head_merge_id = None

merge_id of the head agent for disambiguation

reporting_lines pydantic-field

reporting_lines = ()

Reporting line definitions

policies pydantic-field

policies = None

Department operational policies

ceremony_policy pydantic-field

ceremony_policy = None

Per-department ceremony policy override

remove pydantic-field

remove = False

Merge directive: remove matching parent department

TemplateMetadata pydantic-model

Bases: BaseModel

Metadata about a company template.

Attributes:

Name Type Description
name NotBlankStr

Template display name.

description str

What this template is for.

version NotBlankStr

Semantic version string.

company_type CompanyType

Which CompanyType this template creates.

min_agents int

Minimum number of agents.

max_agents int

Maximum number of agents.

tags tuple[NotBlankStr, ...]

Categorization tags.

skill_patterns tuple[SkillPattern, ...]

Skill interaction patterns this template exhibits.

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

Validators:

  • _validate_agent_range
  • _validate_unique_skill_patterns

name pydantic-field

name

Template display name

description pydantic-field

description = ''

Template description

version pydantic-field

version = '1.0.0'

Semantic version

company_type pydantic-field

company_type

Company type this template creates

min_agents pydantic-field

min_agents = 1

Minimum agents

max_agents pydantic-field

max_agents = 100

Maximum agents

tags pydantic-field

tags = ()

Categorization tags

skill_patterns pydantic-field

skill_patterns = ()

Skill interaction patterns

TemplateMemoryConfig pydantic-model

Bases: BaseModel

Template-level memory configuration overrides.

Attributes:

Name Type Description
embedder EmbedderOverrideConfig | None

Optional embedder override for the template.

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

embedder pydantic-field

embedder = None

Optional embedder override

CompanyTemplate pydantic-model

Bases: BaseModel

A complete company template definition.

This is the top-level model parsed from a template YAML file during the first pass (before Jinja2 rendering). It holds metadata, variable declarations, and the structural definitions for agents and departments.

The raw YAML text is stored separately by the loader for the second pass (Jinja2 rendering).

Attributes:

Name Type Description
metadata TemplateMetadata

Template metadata.

variables tuple[TemplateVariable, ...]

Declared template variables (plain YAML, no Jinja2).

agents tuple[TemplateAgentConfig, ...]

Template agent definitions.

departments tuple[TemplateDepartmentConfig, ...]

Template department definitions.

workflow WorkflowType

Workflow name.

workflow_config dict[str, Any]

Optional Kanban/Sprint sub-configurations, validated as WorkflowConfig on the rendered RootConfig.

communication NotBlankStr

Communication pattern name.

budget_monthly float

Default monthly budget in USD (base currency).

autonomy dict[str, Any]

Autonomy configuration dict (e.g. {"level": "semi"}).

workflow_handoffs tuple[dict[str, Any], ...]

Cross-department workflow handoff definitions.

escalation_paths tuple[dict[str, Any], ...]

Cross-department escalation path definitions.

extends NotBlankStr | None

Parent template name for inheritance (None for standalone templates).

memory TemplateMemoryConfig

Memory configuration overrides (e.g. embedder settings).

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

Validators:

  • _normalize_extendsextends
  • _validate_agent_count_in_range
  • _validate_unique_variable_names
  • _validate_unique_department_names
  • _validate_unique_pack_names

metadata pydantic-field

metadata

Template metadata

variables pydantic-field

variables = ()

Declared template variables

agents pydantic-field

agents

Template agent definitions

departments pydantic-field

departments = ()

Template department definitions

workflow pydantic-field

workflow = AGILE_KANBAN

Workflow type

workflow_config pydantic-field

workflow_config

Optional Kanban/Sprint sub-configurations. Validated as WorkflowConfig on the rendered RootConfig.

communication pydantic-field

communication = 'hybrid'

Communication pattern

budget_monthly pydantic-field

budget_monthly = 50.0

Default monthly budget in USD (base currency)

autonomy pydantic-field

autonomy

Autonomy configuration

workflow_handoffs pydantic-field

workflow_handoffs = ()

Cross-department workflow handoffs

escalation_paths pydantic-field

escalation_paths = ()

Cross-department escalation paths

extends pydantic-field

extends = None

Parent template name for inheritance

uses_packs pydantic-field

uses_packs = ()

Pack names to compose into this template

memory pydantic-field

memory

Memory configuration overrides.

Loader

loader

Template loading from built-in, user directory, and file-system sources.

Implements a two-pass loading strategy:

  • Pass 1: YAML-parse the template to extract metadata and the variables section (which uses plain YAML, no Jinja2).
  • Pass 2: Performed later by the renderer -- Jinja2-renders the raw YAML text, then YAML-parses the result.

Both are returned bundled as a :class:LoadedTemplate dataclass.

TemplateInfo dataclass

TemplateInfo(
    name,
    display_name,
    description,
    source,
    tags=(),
    skill_patterns=(),
    variables=(),
    agent_count=0,
    department_count=0,
    autonomy_level=SEMI,
    workflow="agile_kanban",
)

Summary information about an available template.

Attributes:

Name Type Description
name str

Template identifier (e.g. "startup").

display_name str

Human-readable display name.

description str

Short description.

source Literal['builtin', 'user']

Where the template was found ("builtin" or "user").

tags tuple[str, ...]

Free-form categorization tags for filtering and discovery.

skill_patterns tuple[SkillPattern, ...]

Skill design pattern identifiers describing how the template's agents interact.

variables tuple[TemplateVariable, ...]

User-configurable TemplateVariable instances extracted from the template's variables section.

agent_count int

Number of agents defined in the template.

department_count int

Number of departments defined in the template.

autonomy_level AutonomyLevel

Autonomy level governing approval routing.

workflow str

Workflow type (e.g. "agile_kanban", "kanban").

LoadedTemplate dataclass

LoadedTemplate(template, raw_yaml, source_name)

Result of loading a template: structured data + raw text.

Attributes:

Name Type Description
template CompanyTemplate

Validated CompanyTemplate from Pass 1.

raw_yaml str

Raw YAML text for Pass 2 (Jinja2 rendering).

source_name str

Label for error messages.

list_templates

list_templates()

Return all available templates (user directory + built-in).

User templates override built-in ones. Sorted by name.

Returns:

Type Description
tuple[TemplateInfo, ...]

Sorted tuple of :class:TemplateInfo objects.

Source code in src/synthorg/templates/loader.py
def list_templates() -> tuple[TemplateInfo, ...]:
    """Return all available templates (user directory + built-in).

    User templates override built-in ones. Sorted by name.

    Returns:
        Sorted tuple of :class:`TemplateInfo` objects.
    """
    seen: dict[str, TemplateInfo] = {}

    # User templates (higher priority).
    _collect_user_templates(seen)

    # Built-in templates (lower priority).
    for name in sorted(BUILTIN_TEMPLATES):
        if name not in seen:
            try:
                loaded = _load_builtin(name)
                seen[name] = _template_info_from_loaded(
                    name,
                    loaded,
                    "builtin",
                )
            except (TemplateRenderError, TemplateValidationError, OSError) as exc:
                logger.exception(
                    TEMPLATE_BUILTIN_DEFECT,
                    template_name=name,
                    error=str(exc),
                )

    return tuple(info for _, info in sorted(seen.items()))

list_builtin_templates

list_builtin_templates()

Return names of all built-in templates.

Returns:

Type Description
tuple[str, ...]

Sorted tuple of built-in template names.

Source code in src/synthorg/templates/loader.py
def list_builtin_templates() -> tuple[str, ...]:
    """Return names of all built-in templates.

    Returns:
        Sorted tuple of built-in template names.
    """
    return tuple(sorted(BUILTIN_TEMPLATES))

load_template

load_template(name)

Load a template by name: user directory first, then builtins.

Parameters:

Name Type Description Default
name str

Template name (e.g. "startup").

required

Returns:

Type Description
LoadedTemplate

class:LoadedTemplate with validated data and raw YAML.

Raises:

Type Description
TemplateNotFoundError

If no template with name exists.

Source code in src/synthorg/templates/loader.py
def load_template(name: str) -> LoadedTemplate:
    """Load a template by name: user directory first, then builtins.

    Args:
        name: Template name (e.g. ``"startup"``).

    Returns:
        :class:`LoadedTemplate` with validated data and raw YAML.

    Raises:
        TemplateNotFoundError: If no template with *name* exists.
    """
    name_clean = name.strip().lower()
    logger.debug(TEMPLATE_LOAD_START, template_name=name_clean)

    # Sanitize to prevent path traversal (OS-independent).
    if "/" in name_clean or "\\" in name_clean or ".." in name_clean:
        msg = f"Invalid template name {name!r}: must not contain path separators"
        logger.warning(TEMPLATE_LOAD_INVALID_NAME, template_name=name)
        raise TemplateNotFoundError(
            msg,
            locations=(ConfigLocation(file_path=f"<template:{name}>"),),
        )

    # Try user directory first.
    if _USER_TEMPLATES_DIR.is_dir():
        user_path = _USER_TEMPLATES_DIR / f"{name_clean}.yaml"
        if user_path.is_file():
            result = _load_from_file(user_path)
            logger.debug(
                TEMPLATE_LOAD_SUCCESS,
                template_name=name_clean,
                source="user",
            )
            return result

    # Fall back to builtins.
    if name_clean in BUILTIN_TEMPLATES:
        result = _load_builtin(name_clean)
        logger.debug(
            TEMPLATE_LOAD_SUCCESS,
            template_name=name_clean,
            source="builtin",
        )
        return result

    available = list_builtin_templates()
    logger.error(
        TEMPLATE_LOAD_ERROR,
        template_name=name,
        available=list(available),
    )
    msg = f"Unknown template {name!r}. Available: {list(available)}"
    raise TemplateNotFoundError(
        msg,
        locations=(ConfigLocation(file_path=f"<template:{name}>"),),
    )

load_template_file

load_template_file(path)

Load a template from an explicit file path.

Parameters:

Name Type Description Default
path Path | str

Path to the template YAML file.

required

Returns:

Type Description
LoadedTemplate

class:LoadedTemplate with validated data and raw YAML.

Raises:

Type Description
TemplateNotFoundError

If the file does not exist.

TemplateValidationError

If validation fails.

Source code in src/synthorg/templates/loader.py
def load_template_file(path: Path | str) -> LoadedTemplate:
    """Load a template from an explicit file path.

    Args:
        path: Path to the template YAML file.

    Returns:
        :class:`LoadedTemplate` with validated data and raw YAML.

    Raises:
        TemplateNotFoundError: If the file does not exist.
        TemplateValidationError: If validation fails.
    """
    path = Path(path)
    if not path.is_file():
        msg = f"Template file not found: {path}"
        logger.warning(TEMPLATE_LOAD_NOT_FOUND, path=str(path))
        raise TemplateNotFoundError(
            msg,
            locations=(ConfigLocation(file_path=str(path)),),
        )
    return _load_from_file(path)

Renderer

renderer

Template rendering: Jinja2 substitution + validation to RootConfig.

Implements the second pass of the two-pass rendering pipeline:

  1. Collect user variables + defaults from the CompanyTemplate.
  2. Render the raw YAML text through a Jinja2 SandboxedEnvironment.
  3. YAML-parse the rendered text.
  4. Build a RootConfig-compatible dict and validate.

Template inheritance (extends) is resolved at the renderer level: each template's Jinja2 is rendered independently, then configs are merged via :func:~synthorg.templates.merge.merge_template_configs.

render_template

render_template(loaded, variables=None, *, locales=None, custom_presets=None)

Render a loaded template into a validated RootConfig.

Resolves template inheritance (extends) before validation.

Parameters:

Name Type Description Default
loaded LoadedTemplate

:class:LoadedTemplate from the loader.

required
variables dict[str, Any] | None

User-supplied variable values (overrides defaults).

None
locales list[str] | None

Faker locale codes for auto-name generation. Defaults to all Latin-script locales when None.

None
custom_presets Mapping[str, dict[str, Any]] | None

Optional mapping of custom preset names to personality config dicts for resolving user-defined presets.

None

Returns:

Type Description
RootConfig

Validated, frozen :class:RootConfig.

Raises:

Type Description
TemplateRenderError

If rendering fails.

TemplateValidationError

If validation fails.

TemplateInheritanceError

If inheritance resolution fails.

Source code in src/synthorg/templates/renderer.py
def render_template(
    loaded: LoadedTemplate,
    variables: dict[str, Any] | None = None,
    *,
    locales: list[str] | None = None,
    custom_presets: Mapping[str, dict[str, Any]] | None = None,
) -> RootConfig:
    """Render a loaded template into a validated RootConfig.

    Resolves template inheritance (``extends``) before validation.

    Args:
        loaded: :class:`LoadedTemplate` from the loader.
        variables: User-supplied variable values (overrides defaults).
        locales: Faker locale codes for auto-name generation.
            Defaults to all Latin-script locales when ``None``.
        custom_presets: Optional mapping of custom preset names to
            personality config dicts for resolving user-defined presets.

    Returns:
        Validated, frozen :class:`RootConfig`.

    Raises:
        TemplateRenderError: If rendering fails.
        TemplateValidationError: If validation fails.
        TemplateInheritanceError: If inheritance resolution fails.
    """
    logger.info(
        TEMPLATE_RENDER_START,
        source_name=loaded.source_name,
    )
    config_dict = _render_to_dict(
        loaded,
        variables,
        locales=locales,
        custom_presets=custom_presets,
    )

    # Merge with defaults and validate.
    merged = deep_merge(default_config_dict(), config_dict)
    result = validate_as_root_config(merged, loaded.source_name)
    logger.info(
        TEMPLATE_RENDER_SUCCESS,
        source_name=loaded.source_name,
    )
    return result

Merge

merge

Template config merging for inheritance.

Provides merge_template_configs which combines a parent config dict with a child config dict, implementing the merge semantics described in the template inheritance design.

merge_template_configs

merge_template_configs(parent, child)

Merge a parent config dict with a child config dict.

Merge strategies by field:

  • company_name, company_type: child wins if present.
  • config (dict): deep-merged; child keys override parent.
  • agents (list): merged by (role, department, merge_id) key.
  • departments (list): merged by name (case-insensitive).
  • workflow, workflow_handoffs, escalation_paths: child replaces entirely if present; otherwise inherited from parent.

Parameters:

Name Type Description Default
parent dict[str, Any]

Rendered parent config dict (post-Jinja2, pre-defaults).

required
child dict[str, Any]

Rendered child config dict (post-Jinja2, pre-defaults).

required

Returns:

Type Description
dict[str, Any]

New merged config dict.

Source code in src/synthorg/templates/merge.py
def merge_template_configs(
    parent: dict[str, Any],
    child: dict[str, Any],
) -> dict[str, Any]:
    """Merge a parent config dict with a child config dict.

    Merge strategies by field:

    - ``company_name``, ``company_type``: child wins if present.
    - ``config`` (dict): deep-merged; child keys override parent.
    - ``agents`` (list): merged by ``(role, department, merge_id)`` key.
    - ``departments`` (list): merged by ``name`` (case-insensitive).
    - ``workflow``, ``workflow_handoffs``, ``escalation_paths``: child
      replaces entirely if present; otherwise inherited from parent.

    Args:
        parent: Rendered parent config dict (post-Jinja2, pre-defaults).
        child: Rendered child config dict (post-Jinja2, pre-defaults).

    Returns:
        New merged config dict.
    """
    logger.debug(TEMPLATE_INHERIT_MERGE, action="start")

    result: dict[str, Any] = {}

    # Scalars: child wins if present.
    for key in ("company_name", "company_type"):
        if key in child and child[key] is not None:
            result[key] = child[key]
        elif key in parent:
            result[key] = parent[key]

    # Config dict: deep merge.
    parent_config = parent.get("config", {})
    child_config = child.get("config", {})
    if parent_config or child_config:
        result["config"] = deep_merge(
            parent_config if isinstance(parent_config, dict) else {},
            child_config if isinstance(child_config, dict) else {},
        )

    # Agents: merge by (role, department, merge_id) key.
    parent_agents = parent.get("agents", [])
    child_agents = child.get("agents", [])
    if parent_agents or child_agents:
        result["agents"] = _merge_agents(parent_agents, child_agents)

    # Departments: merge by name.
    parent_depts = parent.get("departments", [])
    child_depts = child.get("departments", [])
    if parent_depts or child_depts:
        result["departments"] = _merge_departments(parent_depts, child_depts)

    # Replace-if-present fields (deep-copied to prevent reference sharing).
    for key in ("workflow", "workflow_handoffs", "escalation_paths"):
        if key in child and child[key] is not None:
            result[key] = copy.deepcopy(child[key])
        elif key in parent:
            result[key] = copy.deepcopy(parent[key])

    logger.debug(TEMPLATE_INHERIT_MERGE, action="done")
    return result

Presets

presets

Personality presets and auto-name generation for templates.

Provides comprehensive personality presets with Big Five dimensions and behavioral enums, plus internationally diverse auto-name generation backed by the Faker library.

get_personality_preset

get_personality_preset(name, custom_presets=None)

Look up a personality preset by name.

Custom presets are checked first (higher precedence), then builtins.

Parameters:

Name Type Description Default
name str

Preset name (case-insensitive, whitespace-stripped).

required
custom_presets Mapping[str, dict[str, Any]] | None

Optional mapping of custom preset names to personality config dicts. Keys must be lowercased.

None

Returns:

Type Description
dict[str, Any]

A copy of the personality configuration dict.

Raises:

Type Description
KeyError

If the preset name is not found in either source.

Source code in src/synthorg/templates/presets.py
def get_personality_preset(
    name: str,
    custom_presets: Mapping[str, dict[str, Any]] | None = None,
) -> dict[str, Any]:
    """Look up a personality preset by name.

    Custom presets are checked first (higher precedence), then builtins.

    Args:
        name: Preset name (case-insensitive, whitespace-stripped).
        custom_presets: Optional mapping of custom preset names to
            personality config dicts.  Keys must be lowercased.

    Returns:
        A *copy* of the personality configuration dict.

    Raises:
        KeyError: If the preset name is not found in either source.
    """
    key = name.strip().lower()
    if custom_presets is not None and key in custom_presets:
        return copy.deepcopy(custom_presets[key])
    if key in PERSONALITY_PRESETS:
        return dict(PERSONALITY_PRESETS[key])
    available = sorted(PERSONALITY_PRESETS)
    if custom_presets:
        available = sorted({*available, *custom_presets})
    msg = f"Unknown personality preset {name!r}. Available: {available}"
    logger.warning(
        TEMPLATE_PERSONALITY_PRESET_UNKNOWN,
        preset_name=name,
        available=available,
    )
    raise KeyError(msg)

validate_preset_references

validate_preset_references(template, custom_presets=None)

Check all agent personality_preset references against known presets.

Returns a tuple of warning messages for unknown presets. Does not raise -- purely advisory for pre-flight validation and template import/export scenarios.

Parameters:

Name Type Description Default
template CompanyTemplate

Parsed template to validate.

required
custom_presets Mapping[str, dict[str, Any]] | None

Optional custom preset mapping. Keys must be lowercased.

None

Returns:

Type Description
tuple[str, ...]

Tuple of warning strings (empty when all presets are known).

Source code in src/synthorg/templates/presets.py
def validate_preset_references(
    template: CompanyTemplate,
    custom_presets: Mapping[str, dict[str, Any]] | None = None,
) -> tuple[str, ...]:
    """Check all agent personality_preset references against known presets.

    Returns a tuple of warning messages for unknown presets.  Does not
    raise -- purely advisory for pre-flight validation and template
    import/export scenarios.

    Args:
        template: Parsed template to validate.
        custom_presets: Optional custom preset mapping.  Keys must
            be lowercased.

    Returns:
        Tuple of warning strings (empty when all presets are known).
    """
    issues: list[str] = []
    for agent_cfg in template.agents:
        preset = agent_cfg.personality_preset
        if preset is None:
            continue
        key = preset.strip().lower()
        if custom_presets is not None and key in custom_presets:
            continue
        if key in PERSONALITY_PRESETS:
            continue
        issues.append(
            f"Agent {agent_cfg.role!r} references unknown personality preset {preset!r}"
        )
    return tuple(issues)

generate_auto_name

generate_auto_name(role, *, seed=None, locales=None)

Generate an internationally diverse agent name using Faker.

When seed is provided, a local random.Random deterministically selects a locale, then a fresh single-locale Faker instance generates the name -- the cached instance is never mutated.

The role parameter is accepted because callers (setup_agents.py, renderer.py) pass it positionally; it does not influence name generation.

Parameters:

Name Type Description Default
role str

The agent's role name. Unused since the switch from role-based name pools to Faker.

required
seed int | None

Optional random seed for deterministic naming.

None
locales list[str] | None

Faker locale codes to draw from. Defaults to all Latin-script locales when None or empty.

None

Returns:

Type Description
str

A generated full name string.

Source code in src/synthorg/templates/presets.py
def generate_auto_name(
    role: str,  # noqa: ARG001
    *,
    seed: int | None = None,
    locales: list[str] | None = None,
) -> str:
    """Generate an internationally diverse agent name using Faker.

    When *seed* is provided, a local ``random.Random`` deterministically
    selects a locale, then a **fresh** single-locale Faker instance
    generates the name -- the cached instance is never mutated.

    The *role* parameter is accepted because callers
    (``setup_agents.py``, ``renderer.py``) pass it positionally;
    it does not influence name generation.

    Args:
        role: The agent's role name.  Unused since the switch from
            role-based name pools to Faker.
        seed: Optional random seed for deterministic naming.
        locales: Faker locale codes to draw from.  Defaults to all
            Latin-script locales when ``None`` or empty.

    Returns:
        A generated full name string.
    """
    import random  # noqa: PLC0415

    from faker import Faker  # noqa: PLC0415

    from synthorg.templates.locales import ALL_LATIN_LOCALES  # noqa: PLC0415

    locale_list = locales or list(ALL_LATIN_LOCALES)
    try:
        if seed is not None:
            rng = random.Random(seed)  # noqa: S311
            chosen_locale = rng.choice(locale_list)
            # Fresh instance -- never mutate the shared cached one.
            fake = Faker([chosen_locale])
            fake.seed_instance(seed)
        else:
            fake = _get_faker(tuple(locale_list))
        return str(fake.name())
    except MemoryError, RecursionError:
        raise
    except Exception:
        from synthorg.observability.events.template import (  # noqa: PLC0415
            TEMPLATE_NAME_GEN_FAKER_ERROR,
        )

        logger.warning(
            TEMPLATE_NAME_GEN_FAKER_ERROR,
            locales=locale_list[:5],
            seed=seed,
            exc_info=True,
        )
        # Fall back to a known-safe locale.
        fallback = Faker(["en_US"])
        if seed is not None:
            fallback.seed_instance(seed)
        return str(fallback.name())

Model Requirements

model_requirements

Structured model requirements and personality-based model affinity.

Provides :class:ModelRequirement for expressing what kind of LLM an agent needs (tier, priority, context window, capabilities) and a preset-keyed affinity mapping that supplies soft defaults when the template does not specify full requirements.

ModelTier module-attribute

ModelTier = Literal['large', 'medium', 'small']

Model capability tier: large (most capable), medium, small (cheapest).

ModelRequirement pydantic-model

Bases: BaseModel

Structured model requirement for a template agent.

Describes what an agent needs from an LLM without referencing a specific provider or model. Used by the matching engine to select the best available model.

Attributes:

Name Type Description
tier ModelTier

Cost/capability tier (large = most capable, small = cheapest).

priority ModelPriority

Optimization axis when multiple models match a tier.

min_context int

Minimum context window in tokens (0 = no minimum).

capabilities tuple[str, ...]

Future-use capability tags (e.g. "reasoning").

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

tier pydantic-field

tier = 'medium'

Cost/capability tier

priority pydantic-field

priority = 'balanced'

Optimization axis for model selection

min_context pydantic-field

min_context = 0

Minimum context window in tokens

capabilities pydantic-field

capabilities = ()

Future-use capability tags

parse_model_requirement

parse_model_requirement(raw)

Parse a model requirement from a string tier or dict.

Backward-compatible: accepts the legacy "medium" string format used by existing template YAML files as well as the new dict format.

Parameters:

Name Type Description Default
raw str | dict[str, Any]

Either a tier string ("large", "medium", "small") or a dict with ModelRequirement fields.

required

Returns:

Type Description
ModelRequirement

Parsed ModelRequirement.

Raises:

Type Description
ValueError

If raw is a string not in the valid tier set.

ValidationError

If raw is a dict with invalid fields.

Source code in src/synthorg/templates/model_requirements.py
def parse_model_requirement(raw: str | dict[str, Any]) -> ModelRequirement:
    """Parse a model requirement from a string tier or dict.

    Backward-compatible: accepts the legacy ``"medium"`` string format
    used by existing template YAML files as well as the new dict format.

    Args:
        raw: Either a tier string (``"large"``, ``"medium"``, ``"small"``)
            or a dict with ``ModelRequirement`` fields.

    Returns:
        Parsed ``ModelRequirement``.

    Raises:
        ValueError: If *raw* is a string not in the valid tier set.
        ValidationError: If *raw* is a dict with invalid fields.
    """
    if isinstance(raw, str):
        key = raw.strip().lower()
        if key not in _VALID_TIERS:
            msg = f"Invalid model tier {raw!r}. Valid tiers: {sorted(_VALID_TIERS)}"
            logger.warning(
                TEMPLATE_MODEL_REQUIREMENT_INVALID,
                raw_tier=raw,
                valid_tiers=sorted(_VALID_TIERS),
            )
            raise ValueError(msg)
        result = ModelRequirement(tier=key)  # type: ignore[arg-type]
    else:
        try:
            result = ModelRequirement(**raw)
        except ValidationError:
            logger.warning(
                TEMPLATE_MODEL_REQUIREMENT_INVALID,
                raw_requirement=raw,
                reason="dict_validation_failed",
                exc_info=True,
            )
            raise

    logger.debug(
        TEMPLATE_MODEL_REQUIREMENT_PARSED,
        tier=result.tier,
        priority=result.priority,
    )
    return result

resolve_model_requirement

resolve_model_requirement(tier_str, preset_name=None)

Merge a template tier alias with personality-preset affinity.

The template's tier always wins. Affinity provides priority and min_context defaults based on the personality preset.

Parameters:

Name Type Description Default
tier_str str

Tier alias from the template agent config.

required
preset_name str | None

Optional personality preset name for affinity lookup.

None

Returns:

Type Description
ModelRequirement

Resolved ModelRequirement.

Source code in src/synthorg/templates/model_requirements.py
def resolve_model_requirement(
    tier_str: str,
    preset_name: str | None = None,
) -> ModelRequirement:
    """Merge a template tier alias with personality-preset affinity.

    The template's tier always wins.  Affinity provides ``priority``
    and ``min_context`` defaults based on the personality preset.

    Args:
        tier_str: Tier alias from the template agent config.
        preset_name: Optional personality preset name for affinity lookup.

    Returns:
        Resolved ``ModelRequirement``.
    """
    affinity: dict[str, Any] = dict(
        MODEL_AFFINITY.get((preset_name or "").strip().lower(), {}),
    )

    merged: dict[str, Any] = {"tier": tier_str.strip().lower()}
    # Affinity values fill in priority and min_context when available.
    if "priority" in affinity:
        merged["priority"] = affinity["priority"]
    if "min_context" in affinity:
        merged["min_context"] = affinity["min_context"]

    result = parse_model_requirement(merged)
    logger.debug(
        TEMPLATE_MODEL_REQUIREMENT_RESOLVED,
        tier=result.tier,
        priority=result.priority,
        min_context=result.min_context,
        preset=preset_name,
    )
    return result

Model Matcher

model_matcher

Tier-to-model matching engine.

Given a :class:~synthorg.templates.model_requirements.ModelRequirement and a set of available provider models, selects the best-fit model by classifying models into cost-based tiers and ranking within each tier according to the requirement's priority axis.

ModelMatch pydantic-model

Bases: BaseModel

Result of matching a single agent to a provider model.

Attributes:

Name Type Description
agent_index int

Index of the agent in the template agent list.

provider_name NotBlankStr

Name of the matched provider.

model_id NotBlankStr

Matched model identifier.

tier ModelTier

Original tier requirement from the template.

score float

Match quality score (higher is better, 0-1 range).

Config:

  • frozen: True
  • extra: forbid
  • allow_inf_nan: False

Fields:

match_model

match_model(requirement, available)

Select the best model for a requirement from available models.

Models are classified into cost-based tiers (thirds by input cost), then ranked within the matching tier according to the requirement's priority axis.

Parameters:

Name Type Description Default
requirement ModelRequirement

Structured model requirement.

required
available tuple[ProviderModelConfig, ...]

Tuple of available models from a single provider.

required

Returns:

Type Description
tuple[ProviderModelConfig | None, float]

Tuple of (best matching model or None, score 0-1).

Source code in src/synthorg/templates/model_matcher.py
def match_model(
    requirement: ModelRequirement,
    available: tuple[ProviderModelConfig, ...],
) -> tuple[ProviderModelConfig | None, float]:
    """Select the best model for a requirement from available models.

    Models are classified into cost-based tiers (thirds by input cost),
    then ranked within the matching tier according to the requirement's
    priority axis.

    Args:
        requirement: Structured model requirement.
        available: Tuple of available models from a single provider.

    Returns:
        Tuple of (best matching model or None, score 0-1).
    """
    if not available:
        return None, 0.0

    # Filter by minimum context window.
    candidates = [m for m in available if m.max_context >= requirement.min_context]
    if not candidates:
        return None, 0.0

    # Classify into tiers by cost.
    tier_models = _classify_tiers(candidates)
    tier_candidates = tier_models.get(requirement.tier, [])

    # Fall back to next-best tier if exact tier is empty.
    if not tier_candidates:
        for fallback in _TIER_FALLBACK[requirement.tier]:
            tier_candidates = tier_models.get(fallback, [])
            if tier_candidates:
                break

    if not tier_candidates:
        return None, 0.0

    # Rank within tier by priority axis.
    best = _rank_by_priority(tier_candidates, requirement.priority)
    score = _compute_score(best, requirement, tier_candidates)
    return best, score

match_all_agents

match_all_agents(agents, providers)

Batch-match template agents to provider models.

For each agent, resolves its model requirement and finds the best model across all configured providers.

Note

The agents list is shallow-copied from the caller. Each dict is shared, so nested mutable values (e.g. personality) are not copied. This function only reads agent dicts.

Parameters:

Name Type Description Default
agents list[dict[str, Any]]

List of expanded agent config dicts. Model requirement resolution uses three paths (checked in order):

  • model_requirement (ModelRequirement): used directly.
  • model_requirement (dict): deserialized to ModelRequirement via parse_model_requirement.
  • tier (str) + optional personality_preset (str): resolved via resolve_model_requirement with personality-based affinity defaults.
required
providers dict[str, Any]

Provider name -> provider config mapping. Each provider config must have a models attribute returning a tuple of ProviderModelConfig.

required

Returns:

Type Description
list[ModelMatch]

List of ModelMatch results. Agents may be omitted from

list[ModelMatch]

the result when no models exist across any provider or when

list[ModelMatch]

requirement resolution fails. Agents with a viable provider

list[ModelMatch]

but no tier match get a ModelMatch with score 0 and the

list[ModelMatch]

first available provider/model as a fallback.

Source code in src/synthorg/templates/model_matcher.py
def match_all_agents(
    agents: list[dict[str, Any]],
    providers: dict[str, Any],
) -> list[ModelMatch]:
    """Batch-match template agents to provider models.

    For each agent, resolves its model requirement and finds the best
    model across all configured providers.

    Note:
        The *agents* list is shallow-copied from the caller. Each dict
        is shared, so nested mutable values (e.g. ``personality``) are
        **not** copied. This function only reads agent dicts.

    Args:
        agents: List of expanded agent config dicts.  Model requirement
            resolution uses three paths (checked in order):

            - ``model_requirement`` (``ModelRequirement``): used directly.
            - ``model_requirement`` (dict): deserialized to
              ``ModelRequirement`` via ``parse_model_requirement``.
            - ``tier`` (str) + optional ``personality_preset`` (str):
              resolved via ``resolve_model_requirement`` with
              personality-based affinity defaults.
        providers: Provider name -> provider config mapping.  Each
            provider config must have a ``models`` attribute returning
            a tuple of ``ProviderModelConfig``.

    Returns:
        List of ``ModelMatch`` results.  Agents may be omitted from
        the result when no models exist across any provider or when
        requirement resolution fails.  Agents with a viable provider
        but no tier match get a ``ModelMatch`` with score 0 and the
        first available provider/model as a fallback.
    """
    from synthorg.templates.model_requirements import (  # noqa: PLC0415
        ModelRequirement,
        parse_model_requirement,
        resolve_model_requirement,
    )

    results: list[ModelMatch] = []

    # Flatten all models across providers for fallback.
    all_models: list[tuple[str, ProviderModelConfig]] = [
        (pname, m) for pname, pcfg in providers.items() for m in pcfg.models
    ]

    for idx, agent in enumerate(agents):
        resolved = _resolve_agent_requirement(
            agent,
            idx,
            ModelRequirement,
            parse_model_requirement,
            resolve_model_requirement,
        )
        if resolved is None:
            continue
        req, tier = resolved

        best_provider: str | None = None
        best_model: ProviderModelConfig | None = None
        best_score = 0.0

        # Try each provider.
        for pname, pcfg in providers.items():
            model, score = match_model(req, pcfg.models)
            if model is not None and score > best_score:
                best_provider = pname
                best_model = model
                best_score = score

        if best_provider is not None and best_model is not None:
            logger.debug(
                TEMPLATE_MODEL_MATCH_SUCCESS,
                agent_index=idx,
                provider=best_provider,
                model=best_model.id,
                score=best_score,
            )
            results.append(
                ModelMatch(
                    agent_index=idx,
                    provider_name=best_provider,
                    model_id=best_model.id,
                    tier=tier,
                    score=best_score,
                ),
            )
        elif all_models:
            # Fallback: assign first available model with score 0.
            fb_provider, fb_model = all_models[0]
            logger.warning(
                TEMPLATE_MODEL_MATCH_FAILED,
                agent_index=idx,
                tier=tier,
                fallback_provider=fb_provider,
                fallback_model=fb_model.id,
            )
            results.append(
                ModelMatch(
                    agent_index=idx,
                    provider_name=fb_provider,
                    model_id=fb_model.id,
                    tier=tier,
                    score=0.0,
                ),
            )
        else:
            logger.warning(
                TEMPLATE_MODEL_MATCH_FAILED,
                agent_index=idx,
                tier=tier,
                reason="no_models_available",
            )

    return results

Errors

errors

Custom exception hierarchy for template errors.

TemplateError

TemplateError(message, locations=())

Bases: ConfigError

Base exception for template errors.

Source code in src/synthorg/config/errors.py
def __init__(
    self,
    message: str,
    locations: tuple[ConfigLocation, ...] = (),
) -> None:
    self.message = message
    self.locations = locations
    super().__init__(message)

TemplateNotFoundError

TemplateNotFoundError(message, locations=())

Bases: TemplateError

Raised when a template cannot be found.

Source code in src/synthorg/config/errors.py
def __init__(
    self,
    message: str,
    locations: tuple[ConfigLocation, ...] = (),
) -> None:
    self.message = message
    self.locations = locations
    super().__init__(message)

TemplateRenderError

TemplateRenderError(message, locations=())

Bases: TemplateError

Raised when template rendering fails.

Covers Jinja2 evaluation errors, missing required variables, YAML parse errors during template processing, and invalid numeric values in rendered output.

Source code in src/synthorg/config/errors.py
def __init__(
    self,
    message: str,
    locations: tuple[ConfigLocation, ...] = (),
) -> None:
    self.message = message
    self.locations = locations
    super().__init__(message)

TemplateInheritanceError

TemplateInheritanceError(message, locations=())

Bases: TemplateRenderError

Raised when template inheritance fails.

Covers circular inheritance chains, excessive depth, and merge conflicts.

Source code in src/synthorg/config/errors.py
def __init__(
    self,
    message: str,
    locations: tuple[ConfigLocation, ...] = (),
) -> None:
    self.message = message
    self.locations = locations
    super().__init__(message)

TemplateValidationError

TemplateValidationError(message, locations=(), field_errors=())

Bases: TemplateError

Raised when a rendered template fails validation.

Attributes:

Name Type Description
field_errors

Per-field error messages as (key_path, message) pairs.

Source code in src/synthorg/templates/errors.py
def __init__(
    self,
    message: str,
    locations: tuple[ConfigLocation, ...] = (),
    field_errors: tuple[tuple[str, str], ...] = (),
) -> None:
    super().__init__(message, locations)
    self.field_errors = field_errors

__str__

__str__()

Format validation error with per-field details.

Source code in src/synthorg/templates/errors.py
def __str__(self) -> str:
    """Format validation error with per-field details."""
    if not self.field_errors:
        return super().__str__()
    parts = [f"{self.message} ({len(self.field_errors)} errors):"]
    loc_by_key: dict[str, ConfigLocation] = {
        loc.key_path: loc for loc in self.locations if loc.key_path
    }
    for key_path, msg in self.field_errors:
        parts.append(f"  {key_path}: {msg}")
        loc = loc_by_key.get(key_path)
        if loc and loc.file_path:
            if loc.line is not None and loc.column is not None:
                line_info = f" at line {loc.line}, column {loc.column}"
            elif loc.line is not None:
                line_info = f" at line {loc.line}"
            else:
                line_info = ""
            parts.append(f"    in {loc.file_path}{line_info}")
    return "\n".join(parts)