Type-state pattern¶
Agent encodes its "lifecycle state" into a type parameter, so calls made out of order are reported by the static checker rather than only at runtime.
The three states¶
from xun import Agent
T = Agent.T # namespace, equivalent to xun.agent_state.T
| State | Meaning | Typical source |
|---|---|---|
T.Uninit |
constructed, not initialized | Agent() (it is also the default parameter of the bare type) |
T.Init |
initialized, ready to execute | agent.initialize(), entering a with block, setup_agent() |
T.Final |
finalized, no longer usable | agent.finalize(), exiting a with block |
T.Alive |
union of Uninit | Init |
the constraint domain of system() / instruct() |
T.Any |
any state | use it when acting purely as a container or a parameter |
StateT is covariant: Agent[T.Init] may appear where Agent[T.Any] is required, but not the other way round.
Rebinding is required¶
A state transition rewrites the internal _lifecycle in place and casts the same object back, so the return value is the very same object as the argument — only its type differs. Rebind the variable to keep the types precise:
agent = Agent()
agent = agent.initialize() # Agent[Agent.T.Init]
...
agent = agent.finalize() # Agent[Agent.T.Final]
A with statement does this for you:
with Agent() as agent: # agent: Agent[Agent.T.Init]
result = agent.instruct("Hi").execute()
Method availability¶
| API | Uninit |
Init |
Final |
|---|---|---|---|
initialize() |
allowed | rejected statically (returns itself idempotently at runtime) | RuntimeError at runtime |
execute() / execute_command() |
not allowed | allowed | not allowed |
system() / instruct() |
allowed | allowed | not allowed |
info() / warning() / error() / get_choice() / get_confirm() |
not allowed | allowed | not allowed |
display_event() |
allowed | allowed | allowed |
finalize() |
allowed | allowed | allowed |
cancel() / check_cancel() / is_running |
allowed | allowed | allowed |
Agent.inherit() / is_initialized() / is_finalized() |
allowed (the argument is Agent[T.Any]) |
allowed | allowed |
Typical static errors:
agent_u = Agent()
# agent_u.execute() # error: Cannot access attribute `execute` for class `Agent[Agent.T.Uninit]`
agent_f = Agent().initialize().finalize()
# agent_f.system("hi") # error: Cannot access attribute `system` for class `Agent[Agent.T.Final]`
In a notebook you can inspect what is inferred with reveal_type():
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]
Runtime versus static checking¶
The constraints exist only in the type layer: nothing enforces them at runtime, so an out-of-order call does not crash immediately. The framework therefore adds a runtime check at the most critical spot — execute() verifies Agent.is_initialized(self) on entry and, when the agent is not initialized, raises:
RuntimeError: Agent 'xxx' is not initialized. Call agent.initialize() or use 'with agent:'.
The checks are expressed as TypeGuard helper functions, so types can be narrowed inside a function signature:
from xun import Agent
def run(agent: Agent[Agent.T.Any]) -> str | None:
if Agent.is_initialized(agent): # narrowed to Agent[Agent.T.Init]
return agent.instruct("ping").execute().unwrap()
return None
Implementation notes¶
- Type layer: the base class
_AgentState(v=0, i.e.T.Any) and the three subclasses_Uninit/_Init/_Final(v=1/2/3), told apart by their distinct class attributev, exposed through theTnamespace. - Runtime layer: the instance field
_lifecycleholds one of those instances; comparison only looks at_lifecycle.v. - The conversion function
_cast_self(state)rewrites_lifecycleandcastsselfback — the single place in the whole design where state is written. Agent.Tmust be a plain class attribute rather than atype T = Talias, otherwiseAgent.T.Initcannot be resolved in some IDEs.- Display-related helpers are defined on
AgentDisplayMixin[StateT], whoseselfannotations refer to the mixin rather than toAgent; this preserves "only theInitstate may output" without breaking the subtyping relationship.