Skip to content

Lifecycle and Type States

Agent takes its lifecycle state as a type parameter (Type-State Pattern): the state lives in the generic parameter, and transition methods return the next state. Mistakes such as "execute before initializing" or "change the prompt after finalizing" are therefore reported by static checking instead of blowing up at runtime.

States

The state classes are defined in agent_lifecycle.py and exposed as Agent.T:

Annotation Meaning Available operations
Agent[T.Uninit] Constructed but not initialized (the default state of a bare Agent) initialize(), system(), instruct(), attaching parts
Agent[T.Init] Initialized execute(), execute_command(), info() / get_choice() and the other display methods
Agent[T.Final] Finalized, no longer usable Only the idempotent no-op finalize()
Agent[T.Alive] Uninit \| Init, still usable Builder methods system() / instruct()
Agent[T.Any] Any state, for function signatures that accept an agent —
stateDiagram-v2
    [*] --> Uninit: Agent(...)
    Uninit --> Init: initialize()
    Init --> Final: finalize()
    Uninit --> Final: finalize()
    Final --> Final: finalize() (no-op)
    note right of Final: initialize() raises RuntimeError

You must rebind

A transition method returns the very same object, but with a different type, so you must reassign it:

agent = Agent()
agent = agent.initialize()      # Agent[T.Init]
agent = agent.finalize()        # Agent[T.Final]

Without rebinding, the static type stays at the old state and the checking is lost. system() and instruct() are transparent with respect to state (the generic constraint is on Agent[T.Alive]); they return self, so you can chain them freely, but attaching them to a Final agent is an error.

from typing import reveal_type, TYPE_CHECKING

agent_u = Agent()
if TYPE_CHECKING: reveal_type(agent_u)      # Agent[Agent.T.Uninit]

agent_i = agent_u.initialize()
if TYPE_CHECKING: reveal_type(agent_i)      # Agent[Agent.T.Init]

agent_f = agent_i.finalize()
if TYPE_CHECKING: reveal_type(agent_f)      # Agent[Agent.T.Final]

# The static checker rejects both of these:
# agent_u.execute()   # Cannot access attribute "execute" for class "Agent[Agent.T.Uninit]"
# agent_f.system("hi")  # Cannot access attribute "system" for class "Agent[Agent.T.Final]"

with statement

__enter__ calls initialize() and __exit__ calls finalize(), so forgetting to clean up never skips the unbind:

with Agent() as agent:
    result = agent.instruct("Hi~").execute()
    print(result.unwrap())

There is one more safety net: a weakref.finalize is registered at construction, so an agent that was initialized and then discarded is finalized when it is collected, and the display layer never keeps believing it has a live agent attached.

The runtime is guarded too

Type annotations only apply to static checking; equivalent checks exist at runtime:

Location Behavior
execute() / execute_command() Raise RuntimeError when the agent is not initialized, telling you to call initialize() or use with
initialize() Returns itself immediately when already initialized (idempotent); raises RuntimeError when finalized
finalize() A no-op on agents that are finalized or were never initialized (only the state transitions; no hooks, no unbind)
Agent.is_initialized(agent) / Agent.is_finalized(agent) TypeGuard static methods, for narrowing the type at runtime

The reason is_initialized is a static method rather than a property is that Pylance only accepts custom TypeGuards with at least one explicit parameter.

The four classes of mistakes this design blocks

  1. Executing before initialization: extensions not loaded, display layer not bound, workspace not prepared, model not probed.
  2. Using the agent after finalization: the display layer is unbound, so events and replies are silently lost.
  3. Initializing twice or forgetting to clean up: both transition methods are idempotent, and with plus weakref.finalize provide the safety net.
  4. A stale variable type: transition methods return the new state, forcing you to rebind so the type at every later call site keeps matching reality.

State when deriving

Agent.inherit(parent, ...) returns Agent[T.Uninit] — a derived child agent must call initialize() itself, which is what replays the extensions, binds the display layer, and installs the compaction strategy. See Sub-agent Derivation for the details of derivation.