ADR-012: Multi-Agent Component Addressing Scheme¶
Status: Accepted Date: 2026-01-20 Deciders: gepa-adk maintainers Issue: #165
Context¶
GEPA-ADK supports multi-agent evolution where different agents can have different components evolved simultaneously. We need a clear addressing scheme for specifying which components to evolve on which agents.
Current single-agent pattern:
Multi-agent requirement:
evolve_group(
agents=[generator, refiner, critic],
trainset=trainset,
components=??? # How to specify generator.instruction + critic.output_schema?
)
Problem: Underscore Separator Ambiguity¶
The current implementation uses underscore-separated names (generator_instruction), but this is ambiguous because component names themselves contain underscores:
COMPONENT_OUTPUT_SCHEMA = "output_schema" # has underscore
COMPONENT_GENERATE_CONFIG = "generate_content_config" # has underscores
This creates parsing ambiguity: - generator_output_schema → Is this generator + output_schema or generator_output + schema?
Decision¶
Use dot-separated qualified names for multi-agent component addressing.
Format¶
Examples: - generator.instruction - critic.output_schema - refiner.generate_content_config
Why Dot Separator?¶
| Separator | Pros | Cons |
|---|---|---|
| Dot (.) | Unambiguous, matches ADK branch pattern, URI-like | Looks like attribute access |
| Underscore (_) | Python-friendly | Ambiguous with component names |
| Slash (/) | REST-like | Not Python identifier |
| Colon (:) | Namespace-like | Not Python identifier |
ADK agent names are Python identifiers (validated at agent creation), which cannot contain dots. Therefore, dots are always unambiguous separators.
Type Safety¶
To maximize type safety with ty/mypy/pyright, we introduce:
QualifiedComponentName: ANewTypethat distinguishes qualified names from plain stringsComponentSpec: A dataclass for structured construction and parsing
from typing import NewType
from dataclasses import dataclass
QualifiedComponentName = NewType("QualifiedComponentName", str)
@dataclass(frozen=True, slots=True)
class ComponentSpec:
"""Structured representation of an agent.component pair."""
agent: str
component: str
@property
def qualified(self) -> QualifiedComponentName:
"""Return dot-separated qualified name."""
return QualifiedComponentName(f"{self.agent}.{self.component}")
@classmethod
def parse(cls, qualified: QualifiedComponentName | str) -> "ComponentSpec":
"""Parse qualified name into ComponentSpec."""
name = str(qualified)
agent, component = name.split(".", 1)
return cls(agent=agent, component=component)
Usage¶
# Construction
spec = ComponentSpec(agent="generator", component="instruction")
name: QualifiedComponentName = spec.qualified # "generator.instruction"
# Parsing
spec = ComponentSpec.parse("critic.output_schema")
print(spec.agent) # "critic"
print(spec.component) # "output_schema"
# In candidates
candidate.components = {
spec.qualified: "evolved instruction text...",
}
Type Checker Benefits¶
| Error Type | Caught by ty? |
|---|---|
Field typo: ComponentSpec(agnet="x") | YES |
Attribute typo: spec.componet | YES |
Wrong type: get_handler("plain string") vs QualifiedComponentName | YES |
| Invalid format in parse | Runtime (ValueError) |
Consequences¶
Positive¶
- Unambiguous parsing: No confusion with component names containing underscores
- ADK alignment: Matches ADK's branch addressing pattern (
parent.child.grandchild) - Type safety:
NewType+ dataclass enables IDE autocomplete and type checker validation - Minimal change: Same
dict[str, str]structure, just different key format - URI convention: Follows established hierarchical naming patterns
Negative¶
- Breaking change: Existing multi-agent code using underscore separator needs migration
- Verbosity:
QualifiedComponentName("...")more verbose than plain string - Learning curve: New types to understand
Neutral¶
- Single-agent unchanged: Continues using unqualified names (
instruction,output_schema) - Dict keys remain strings: Type is
NewTypeofstr, so JSON serialization unchanged
Migration¶
Internal Migration (api.py, multi_agent.py)¶
# Before
component_name = f"{agent.name}_instruction"
# After
spec = ComponentSpec(agent=agent.name, component="instruction")
component_name = spec.qualified
Backward Compatibility¶
- Single-agent evolution: No change required
- Multi-agent evolution: Internal refactor, API signature unchanged
Alternatives Considered¶
Option B: Nested Dict Structure¶
components = {
"generator": ["instruction", "output_schema"],
"critic": ["generate_content_config"],
}
candidate = {
"generator": {"instruction": "...", "output_schema": "..."},
"critic": {"generate_content_config": "..."},
}
Rejected because: - Breaking change to Candidate.components type (dict[str, str] → nested) - More complex iteration patterns - No clear benefit over flat structure
Option C: ComponentSpec Without Qualified Names¶
components = [
ComponentSpec(agent="generator", component="instruction"),
ComponentSpec(agent="critic", component="output_schema"),
]
Partially adopted: - ComponentSpec used for construction/parsing - Qualified string names used for dict keys (serialization compatibility)
References¶
Codebase¶
src/gepa_adk/api.py- evolve_group implementationsrc/gepa_adk/adapters/multi_agent.py- MultiAgentAdaptersrc/gepa_adk/domain/types.py- Type definitionssrc/gepa_adk/ports/component_handler.py- ComponentHandler protocol
ADK Patterns¶
- Agent naming validation: Python identifiers only (no dots)
- Branch addressing:
parent.child.grandchildformat - State storage: Flat namespace keyed by agent name