Agent System¶
The agent system defines how autonomous computational entities are created, managed, and coordinated. Every agent follows the same core lifecycle but can be specialized through capabilities and action policies.
Guiding Principle¶
Agent control flow and all decisions should be driven by a reasoning LLM given sufficient context, not hardcoded logic. But the actions available to the LLM planner are as important as the context it reasons over. The framework provides a rich ecosystem of capabilities that expose different actions and cognitive processes to the LLM, enabling emergent behavior from the combinatorial explosion of possible action interleavings.
The framework provides structure (lifecycle, capabilities, blackboard access), but the LLM decides what to do next. Meta-choices available to the LLM include delegating to another agent, meta-reasoning about its own strategy, initiating multi-agent deliberation, and building new tools.
Agent Base Class¶
polymathera.colony.agents.base.Agent is the base class for all agents. It provides:
- Lifecycle management: State transitions through
AgentState - Blackboard access: Read/write shared state via
EnhancedBlackboard - VCM access: Read virtual context pages
- Inference submission: Submit requests to vLLM replicas
- Capability management: Add, remove, and query
AgentCapabilityinstances - Hook registry:
HookRegistryfor AOP interception - Session tracking: Every
run()call creates anAgentRuntracked in the session
Agent Lifecycle States¶
Defined in polymathera.colony.agents.models.AgentState:
INITIALIZED --> RUNNING --> IDLE --> STOPPED
| ^
v |
WAITING ----------------
|
v
SUSPENDED --> RUNNING (resumed)
|
v
FAILED
| State | Description |
|---|---|
INITIALIZED |
Agent created, not yet started |
RUNNING |
Actively executing an action policy iteration |
WAITING |
Blocked on input, tool result, or child agent |
LOADED |
VCM-bound agent with pages resident in cache |
UNLOADED |
VCM-bound agent with pages evicted from cache |
IDLE |
Between tasks, not actively executing |
SUSPENDED |
Resources freed, state serialized, can be resumed |
STOPPED |
Gracefully terminated |
FAILED |
Terminated due to error |
Agent Types¶
VCM-Bound Agents¶
Loaded and unloaded together with their assigned VCM pages. When their pages are evicted from cache, these agents are suspended. When pages are reloaded, agents resume from their serialized state.
Unbound Agents¶
Dynamically select which VCM pages to load. They are not tied to specific pages and can request page loads as needed during execution.
Service Agents¶
Always-running agents that provide services to other agents (e.g., a MemoryManagementAgent that consolidates memories across the system).
Supervisor Agents¶
Independent of page state. They monitor and coordinate other agents, make delegation decisions, and handle escalation.
AgentCapability¶
polymathera.colony.agents.base.AgentCapability is the extension point for agent functionality. Each capability encapsulates a specific aspect or protocol:
class AgentCapability(ABC):
"""Base class for capabilities.
Operates in four modes:
1. Local: runs within the owning agent
2. Remote: parent communicates with child via shared scope
3. Shared scope: multiple agents share a namespace
4. Detached: standalone without agent context
"""
def __init__(
self,
agent: Agent | None = None,
scope_id: str | None = None,
*,
blackboard: EnhancedBlackboard | None = None,
capability_key: str | None = None,
): ...
The four modes in practice:
# 1. Local mode — capability runs within its owning agent
memory = MemoryCapability(agent=self) # scope_id defaults to agent.agent_id
# 2. Remote mode — parent monitors a child agent's progress
child_cap = ResultCapability(agent=parent, scope_id=child_agent_id)
await child_cap.stream_events_to_queue(self.get_event_queue())
result = await asyncio.wait_for(child_cap.get_result_future(), timeout=30.0)
# 3. Shared scope — all game participants see each other's events
game_cap = NegotiationGameProtocol(agent=self, scope_id=game_id)
# 4. Detached mode — external system interacts with agents via blackboard
external_cap = MyCapability(agent=None, scope_id=target_agent_id)
Capabilities provide:
- Action executors: Methods decorated with
@action_executorthat theActionPolicycan invoke (conscious cognitive processes) - Hookables: Methods marked
@hookablethat other components can intercept - Hooks (method call interceptors): Hooks the capability registers on other components (via
hook_handler()) - Event streams: Capabilities can publish events to their scoped blackboard, which other agents can subscribe to (e.g., for game protocols or parent-child communication)
- Event handlers: Capabilities can subscribe to events from other capabilities or agents, enabling reactive behavior and emergent coordination patterns
- Services or background processes: Subconscious cognitive processes (consolidation, rehearsal)
class PageGraphCapability(AgentCapability):
"""Example capability with action executors and hooks."""
@action_executor()
async def traverse(
self, start_pages: list[str], strategy: str = "bfs", max_depth: int = 5
) -> dict[str, Any]:
"""Auto-discovered by ActionPolicy — the LLM planner can invoke this."""
graph = await self._get_page_graph()
...
@action_executor(exclude_from_planning=True)
async def update_edge(self, source: str, target: str, weight: float) -> None:
"""exclude_from_planning=True: only invoked programmatically, not by the LLM."""
...
Capabilities as AOP Aspects
Each AgentCapability is an "aspect" in the aspect-oriented programming sense. The ActionPolicy plays the role of the "aspect weaver," deciding which capabilities to activate and in what order. Emergent behavior arises from the combinatorial explosion of possible capability interleavings -- the framework does not model all paths explicitly.
Merge with docs/design-insights/capabilities-as-aspects.md and reference here.
LLM-Decidable Actions
The LLM should be able to reason about when/how to use each AgentCapability action.
Add subconscious processes to planning prompt
Subconscious processes (e.g., memory consolidation) are not directly invoked by the LLM, but they affect the agent's state and capabilities. The LLM should be aware of these processes and their effects when making decisions. Add explanation of how to include subconscious processes in the planning prompt and how they interact with conscious actions.
Scope-Based Communication¶
Capabilities use scope_id for flexible communication patterns:
agent.agent_id(default): Agent-local scopechild_agent_id: Parent-to-child communicationgame_idortask_id: Shared scope for group coordination
The publish() method writes records to the capability's scoped blackboard. If the scope is VCM-mapped, writes are automatically discoverable by other agents via the VCM.
# publish() replaces manual blackboard key construction:
# key = f"{self.scope_id}:analysis:result:{result.result_id}"
# await bb.write(key=key, value=result.model_dump(), created_by=...)
# With:
await self.publish(result, tags={"analysis"})
# Key is resolved automatically via BlackboardPublishable protocol on the record.
ActionPolicy¶
polymathera.colony.agents.base.ActionPolicy is the abstract base for decision-making. It receives execution state and produces iteration results:
class ActionPolicy(ABC):
async def execute_iteration(
self, state: ActionPolicyExecutionState
) -> ActionPolicyIterationResult:
...
The policy manages which capabilities are active via use_agent_capabilities() and disable_agent_capabilities(). See Action Policies for the full planning architecture.
The agent's main execution loop calls run_step() repeatedly. Each step invokes the action policy, which gathers context from capabilities, asks the LLM what to do next, and dispatches the chosen action:
@tracing(publish_key=lambda self: self.agent_id)
class Agent:
@hookable
async def run_step(self) -> None:
"""Execute one iteration of the agent's reasoning loop.
@hookable: capabilities can register BEFORE/AFTER/AROUND hooks
to prepare context, post-process results, or wrap execution.
Uses repeated run_step() instead of a single long-running run()
to facilitate suspension and state management across replicas.
"""
if self.state not in (AgentState.RUNNING, AgentState.IDLE):
return
result = await self.action_policy.execute_iteration(self._build_state())
await self._apply_iteration_result(result)
def add_capability(self, capability: AgentCapability, *,
include_actions: list[str] | None = None,
exclude_actions: list[str] | None = None) -> None:
"""Add a capability. Must still call action_policy.use_agent_capabilities()
to include it in planning."""
def get_capability(self, name: str) -> AgentCapability | None:
"""Retrieve a capability by name."""
AgentHandle — Parent-Child Communication¶
AgentHandle is the interface for sending work to an agent and receiving results. It works in both owned mode (parent agent communicating with a child) and detached mode (external code like CLI communicating with an agent).
run() — Request/Result¶
handle = AgentHandle(child_agent_id="agent-xyz", owner=parent_agent)
# namespace scopes the blackboard partition (baked into scope_id)
# protocol defines the key format (defaults to AgentRunProtocol)
run = await handle.run(
{"query": "analyze this code"},
protocol=AgentRunProtocol, # default — defines key format
namespace="compliance", # scopes the blackboard partition
timeout=60,
)
print(run.output_data)
The namespace parameter is required — it identifies which capability on the child agent should handle the request. This prevents interference when an agent has multiple capabilities using the same protocol. The namespace is appended to the scope_id, giving each capability its own blackboard partition (e.g., ...colony:C:namespace:compliance).
The protocol parameter (default AgentRunProtocol) defines the key format for request/result exchange within that partition:
- Parent writes to scope
...colony:C:namespace:compliancewith keyrequest:run:{request_id} - Child's
ComplianceCapabilityhas the same scope (set in its__init__via the same namespace) - Child detects requests via
@event_handler(pattern=AgentRunProtocol.request_pattern()) - Child writes result with key
result:run:{request_id}on the same scope - Parent listens for
result:run:{request_id}on the same scope
The blackboard scope parameter (default BlackboardScope.AGENT) determines the scope level. AGENT-scoped protocols write to the child's agent-level blackboard. COLONY-scoped capabilities pass scope=BlackboardScope.COLONY to write to the shared colony blackboard.
See Blackboard Protocols for the full protocol design.
run_streamed() — Streaming Events¶
async for event in handle.run_streamed(
{"query": "analyze"},
protocol=AgentRunProtocol,
namespace="compliance",
timeout=300,
):
print(f"{event.event_type}: {event.data}")
if event.event_type == "completed":
break
Uses AgentRunProtocol.event_pattern(request_id) to subscribe to incremental events on the namespace-scoped blackboard. The child emits events via AgentRunProtocol.event_key(request_id, event_name).
Custom Protocols¶
For specialized communication patterns, pass a different protocol:
from polymathera.colony.agents.blackboard.protocol import WorkAssignmentProtocol
from polymathera.colony.agents.scopes import BlackboardScope
# Use a colony-scoped protocol for coordinator→worker communication
run = await handle.run(
work_unit,
protocol=WorkAssignmentProtocol,
scope=BlackboardScope.COLONY,
namespace="pool",
)
Suspension and Resumption¶
When an agent cannot proceed — blocked on children, out of resources, or by explicit request — it is suspended. Suspension persists the agent's full state (plan progress, working set, page access patterns) to Redis and frees its resources. The agent is deleted from the replica.
Resumption is handled by AgentSystemDeployment._resource_monitor_loop(), which evaluates structured ResumptionConditions:
| Condition | When met | Example |
|---|---|---|
CHILDREN_COMPLETED |
All blocking child agents reach STOPPED/FAILED | Agent waiting for workers to finish |
RESOURCE_AVAILABLE |
Resources freed (checked by attempting spawn) | Agent evicted for capacity |
PAGES_AVAILABLE |
Required VCM pages loaded on some replica | Cache-aware resumption (future) |
IMMEDIATE |
Always — resume ASAP | Explicit suspension lifted |
CUSTOM |
Never (requires external trigger) | Application-specific logic |
Action executors signal blocking with structured metadata:
return ActionResult(
success=False,
blocked=True,
blocked_reason="Waiting for worker agents",
blocking_agent_ids=["agent-abc", "agent-def"], # structured, not text
)
The system constructs a CHILDREN_COMPLETED condition from blocking_agent_ids and stores it in the suspension state. The monitor loop checks AgentSystemState for child completion before attempting resume.
ResourceExhausted Handling¶
When a replica has insufficient resources for a new agent, the system follows an ordered strategy:
- Hard page affinity: Only place on replicas with required pages
- Soft affinity: Try replicas with preferred pages, fall back to others
- Retry later: Queue the agent for later placement
- Suspend existing agents: Free resources by suspending lower-priority agents
The ResourceExhausted exception (in polymathera.colony.agents.base) triggers this cascade.
Agent Actions Taxonomy¶
Actions are typed via polymathera.colony.agents.models.ActionType:
| Category | Actions |
|---|---|
| Planning | PLAN_CREATE, PLAN_REVISE, PLAN_BACKTRACK |
| Reasoning | ANALYZE, HYPOTHESIS_TEST, DECISION_MAKE |
| Tools | TOOL_DISCOVER, TOOL_USE, TOOL_FIX, TOOL_CREATE |
| Context | CONTEXT_FETCH, CONTEXT_COMPACT, CONTEXT_SUMMARIZE |
| Communication | MESSAGE_SEND, MESSAGE_RECEIVE, NEGOTIATE |
| Memory | MEMORY_SEARCH, MEMORY_READ, MEMORY_WRITE |
| Orchestration | AGENT_SPAWN, AGENT_TERMINATE, AGENT_MONITOR |
| Output | OUTPUT_WRITE, OUTPUT_FORMAT, REPORT_GENERATE |
Long-running actions should be idempotent, pausable, resumable, checkpointable, and cancellable. Complex actions are best implemented as separate agents managed by the AgentSystemDeployment.
Blueprints¶
This section is incomplete and needs expansion
Add explanation of how blueprints work for serializable agent specifications, validation, and instantiation with dependency injection.
Agent configuration uses the blueprint pattern for serializable, deployable specifications:
AgentBlueprint: Full agent specification (capabilities, policy, resources)AgentCapabilityBlueprint: Capability class + constructor kwargsActionPolicyBlueprint: Policy class + constructor kwargs (created viaActionPolicy.bind())
Blueprints are validated for serializability at creation time and instantiated at deployment time with injected dependencies (agent reference, blackboard, etc.).