Architecture
This page documents the internal architecture of PyAgentic, showing how agents are constructed from declaration through instantiation to runtime execution.
Overview
PyAgentic's architecture is built around three distinct phases:
- Declaration Phase - User writes agent class code, metaclass processes it
- Instantiation Phase - Agent class is instantiated with dynamic state and initialization
- Runtime Phase - Agent executes, calling tools and linked agents in an agentic loop
Each phase builds upon the previous one, transforming user-friendly declarations into sophisticated runtime behavior.
Declaration Phase
The declaration phase occurs when you define an agent class. The AgentMetaclass intercepts the class definition and generates all the internal structures needed for the agent to function.
Key Components
User Declarations:
- BaseAgent - Your agent class inherits from this
- System Message - The agent's core instructions
- State Fields - State[T] annotations with optional spec.State() configuration
- Linked Agents - Link[AgentClass] or direct type annotations with optional spec.AgentLink() configuration
- Tools - Methods decorated with @tool
Metaclass Processing:
1. Extract Attributes - Pulls state, tools, and linked agents from the class
2. C3 Linearize - Resolves inheritance from parent classes and mixins
3. Validate Definitions - Ensures all definitions are valid
4. Generate Definitions - Creates internal definition objects:
- _StateDefinition - Pairs State[T] type with StateInfo descriptor (from spec.State())
- _ToolDefinition - Schema and metadata for each tool
- _LinkedAgentDefinition - Pairs Link[T] type with AgentInfo descriptor (from spec.AgentLink())
5. Build Init - Dynamically generates __init__ signature and function
6. Build Response Model - Creates Pydantic response model from tool definitions
Generated Class Structure:
- __tool_defs__ - Registry of all tool definitions
- __state_defs__ - Registry of all state definitions
- __linked_agents__ - Registry of all linked agent types
- __response_model__ - Pydantic model for agent responses
- __init__() - Dynamically generated constructor
Supporting Utilities
The spec object provides configuration helpers using a descriptor pattern:
- spec.State() - Returns StateInfo descriptor for state fields (default, default_factory, access control, policies)
- spec.Param() - Returns ParamInfo descriptor for tool parameters (description, default, values)
- spec.AgentLink() - Returns AgentInfo descriptor for linked agents (default, default_factory, condition)
The ref object creates lazy references to state for use in tool parameters:
- ref.field.subfield creates a RefNode that resolves at runtime
- Used to constrain parameters to valid state values
Instantiation Phase
The instantiation phase occurs when you create an instance of your agent class (e.g., agent = MyAgent(...)). The dynamically generated __init__ method creates the agent's runtime state and configuration.
Initialization Flow
- Make State Model
- Creates a dynamic Pydantic model from
__state_defs__ - Each state field becomes a validated model field
-
Computed fields are included automatically
-
Compile State Values
- Processes initialization arguments
- Applies default values from
spec.State() -
Type-checks all values
-
Create State Instance
- Instantiates the dynamic state model
- Stores as
agent.state -
Includes system message and templates
-
Set Linked Agents
- Processes
AgentInfofromspec.AgentLink()for each linked agent - Applies
defaultor callsdefault_factoryif agent not provided - Attaches agent instances to the parent
- Creates tool definitions from linked agents
-
Validates linked agent types
-
Set Attributes
- Attaches any additional instance attributes
-
Binds tools as instance methods
-
Post Initialization (
__post_init__) - Check LLM Provider - Validates model string or provider instance
- Setup Tracer - Initializes observability tracer (defaults to BasicTracer)
Instance Attributes
After initialization, the agent instance has:
- state - The AgentState instance with all state fields
- Linked agents - References to other agent instances
- provider - The configured LLM provider
- tracer - The observability tracer
- model, api_key - Provider configuration
- max_call_depth - Maximum depth for the agentic loop
Runtime Phase
The runtime phase occurs when you call agent.run(input) or agent(input). The agent enters an agentic loop where it can call tools and linked agents multiple times before producing a final response.
Execution Flow
- Add User Message
- Input is added to
agent.state._messages -
State is now primed for inference
-
Get Tool Definitions
- Collects all
@toolmethods from__tool_defs__ - Generates tool definitions for linked agents via
agent.get_tool_definition() -
Creates list of available tools for the LLM
-
Process LLM Inference
- Builds prompt with system message and user input
- Sends to provider with tool schemas
-
Returns LLM response (text and/or tool calls)
-
Tool Call Routing
- If no tool calls → Build final response
- If tool calls → Route to appropriate processor:
Process Tool Call:
- Looks up tool in __tool_defs__
- Compiles arguments (resolves refs, validates types)
- Executes tool method
- Returns ToolResponse with result
Process Agent Call:
- Looks up linked agent
- Calls linked_agent.run()
- Returns AgentResponse from linked agent
- Increment Depth
- Increases loop counter
- Checks against
max_call_depth - If under limit → Loop back to inference
-
If at limit → Build final response
-
Build Response
- Combines final LLM output with all tool/agent responses
- Creates
AgentResponseinstance using__response_model__ - Returns to caller
Response Object
The AgentResponse contains:
- final_output - The LLM's final text response
- tool_responses - List of ToolResponse objects (one per tool call)
- agent_responses - List of nested AgentResponse objects (one per linked agent call)
- provider_info - Metadata about the LLM provider and usage
Each ToolResponse contains:
- output - The string result from the tool
- call_depth - Which loop iteration this was called in
- raw_kwargs - Original arguments from the LLM
- Compiled parameters specific to that tool
Key Design Patterns
Metaclass-Based Construction
Using a metaclass allows PyAgentic to inspect and transform agent classes at definition time, generating optimal runtime structures before any instances are created. This enables: - Compile-time validation of agent definitions - Pre-generated response models for type safety - Efficient tool schema generation - Inheritance and mixin support via C3 linearization
Dynamic State Management
State is defined declaratively at the class level but instantiated dynamically per agent instance. This provides: - Type-safe state access via Pydantic - Computed fields that update automatically - Access control (read/write/hidden) - Serialization support
Reference Resolution
The ref system creates lazy references at declaration time that resolve at runtime:
1. Declaration: ref.field.subfield creates RefNode(['field', 'subfield'])
2. Storage: RefNode stored in tool parameter definition
3. Runtime: When generating tool schema, RefNode.resolve(agent_reference) walks the path to get current value
This keeps tool parameters synchronized with live state values.
Tool as Universal Interface
Both custom @tool methods and linked agents use the same _ToolDefinition interface. This allows:
- Uniform handling by the LLM
- Consistent parameter validation
- Seamless composition of agents
Source Diagrams
These architecture diagrams were created using D2. The source .d2 files are available in docs/diagrams/source/:
docs/diagrams/source/declaration.d2- Declaration phase diagramdocs/diagrams/source/instantiation.d2- Instantiation phase diagramdocs/diagrams/source/runtime.d2- Runtime phase diagram
The diagrams are automatically compiled to SVG when building or deploying the documentation. To manually regenerate:
# Compile all diagrams (uses elk layout engine)
uv run task compile-diagrams
# Or compile individually with elk layout
d2 --layout elk docs/diagrams/source/declaration.d2 docs/diagrams/declaration.svg
d2 --layout elk docs/diagrams/source/instantiation.d2 docs/diagrams/instantiation.svg
d2 --layout elk docs/diagrams/source/runtime.d2 docs/diagrams/runtime.svg
Note: The .d2 source files specify the tala layout engine, but the build process overrides this with elk since tala requires a separate installation. If you have tala installed locally, you can compile without the --layout elk flag for potentially better layouts.
Next Steps
- Learn about the public API you should use
- Read the user guide for practical examples
- Explore state management for persistent agents
- See tools for extending agent capabilities