Skip to main content

Agent Runtime

The Agent Runtime coordinates the lifecycle of goals, agents, and cognitive resources across the WASP platform.

AgentRuntime (src/agent_manager/runtime.py)

AgentRuntime manages individual agent instances within the AgentOrchestrator:

  • Each agent has a goal queue (stored in Redis)
  • The runtime ticks active agents every 15 seconds (AgentTickJob)
  • Sorts goals by priority before each tick
  • Enforces CPU backpressure threshold (default: 85%)

Agent Lifecycle

create_agent()


Agent(status=IDLE, goal_queue=[])

▼ (goal assigned)
Agent(status=RUNNING, current_goal=Goal(...))

├── goal completes → Agent(status=IDLE)
├── goal fails → Agent(status=ERROR) → retry
└── agent paused → Agent(status=PAUSED)

CPU Backpressure

Before processing each goal tick, the runtime checks CPU usage:

cpu_pct = await asyncio.to_thread(psutil.cpu_percent, interval=0.1)
if cpu_pct > settings.agents_cpu_threshold: # default: 85%
return # Skip this tick

Using asyncio.to_thread() prevents the blocking psutil.cpu_percent() call from stalling the event loop.

GoalOrchestrator (src/goal_orchestrator/orchestrator.py)

The central coordinator for autonomous goal execution:

Key Methods

MethodDescription
create_goal(objective, chat_id, priority, source)Create and persist a new goal
tick()Advance all active goals (called every 15s by GoalTickJob)
_tick_one(goal)Advance a single goal by up to 3 steps
invoke(goal_id)Execute a specific goal on-demand
get_goal(goal_id)Retrieve goal state
list_goals(status)List goals by status
delete_goal(goal_id)Remove goal from Redis

Concurrent Execution

The orchestrator processes up to GOAL_MAX_CONCURRENT goals (default: 3) per tick using asyncio.gather():

active_goals = sorted(
[g for g in all_goals if g.status == GoalStatus.ACTIVE],
key=lambda g: g.priority,
reverse=True, # Higher priority first
)[:self.max_concurrent]

await asyncio.gather(*[self._tick_one(g) for g in active_goals])

Steps Per Tick

Each tick, a goal can advance up to 3 consecutive steps:

async def _tick_one(self, goal: Goal) -> None:
for step in range(3): # Up to 3 steps per tick
result = await self._execute_next_task(goal)
if result.done or result.waiting:
break

This allows faster goal completion without waiting 15 seconds between each step.

CognitiveBudget

Each goal tracks its resource usage:

@dataclass
class CognitiveBudget:
tokens_planning: int = 0 # Tokens used for planning
tokens_execution: int = 0 # Tokens used in execution
replans: int = 0 # Number of replans
memory_bytes: int = 0 # Working memory used
start_time: float = field(default_factory=time.time)

Budget is checked before each step:

if budget.tokens_execution >= settings.goal_budget_max_tokens_execution:
goal.status = GoalStatus.FAILED
goal.fail_reason = "budget_exceeded"

Service Registry

All major services are registered in a central service registry (src/runtime/registry.py):

registry.register(SERVICE_MEMORY, memory)
registry.register(SERVICE_MODELS, model_manager)
registry.register(SERVICE_SKILLS, skill_registry)
registry.register(SERVICE_EXECUTOR, skill_executor)
registry.register(SERVICE_SCHEDULER, scheduler)
registry.register(SERVICE_BUS, bus)
registry.register(SERVICE_HEALTH, health_monitor)
registry.register(SERVICE_INTROSPECTOR, introspector)
registry.register(SERVICE_BROKER, broker_client)
registry.register(SERVICE_METRICS, metrics_collector)
registry.register(SERVICE_ECONOMICS, economics_tracker)

This allows any component to access services without circular imports.

State Persistence

Goal state is persisted in Redis:

goals  →  Hash: {goal_id: json_blob}
agents → Hash: {agent_id: json_blob}

All goal state is serialized with Pydantic and stored as JSON. Goals survive container restarts.

Goal Meta-Reflection

GoalMetaReflectionJob (runs every 5 minutes):

  • Analyzes goal health across the last hour
  • Detects replan storms (>6 replans in 10 minutes)
  • Logs meta-state for monitoring
  • Sends Telegram alert if storm detected