Add an agent
This guide shows you how to add a strategist agent — a subsystem that negotiates a budget through GORNA, selects one lane, and dispatches it every frame.
Prerequisites: you have completed Extending the engine and read Agents.
Use an agent only when the subsystem needs GORNA negotiation. Work that just runs deterministically each tick belongs in a direct service (
AssetService,EcsMaintenance, aDataSystem), not an agent.
Step 1 — Implement Agent and Default — and nothing else
An agent struct implements exactly two traits: Agent and
Default. No start/stop, no builders, no accessors — those would violate the
agent contract. Construction is always Default::default(). The agent owns one
LaneKind and stores only its own GORNA/strategy state; every shared service is
looked up from the Runtime each frame.
#![allow(unused)]
fn main() {
use std::any::Any;
use std::time::Duration;
use khora_core::agent::{Agent, AgentImportance, ExecutionPhase, ExecutionTiming};
use khora_core::context::EngineContext;
use khora_core::control::gorna::{
AgentId, AgentStatus, NegotiationRequest, NegotiationResponse, ResourceBudget,
StrategyId, StrategyOption,
};
use khora_core::lane::{Lane, LaneContext};
#[derive(Default)]
pub struct FoliageAgent {
lane: Option<Box<dyn Lane>>,
current_strategy: StrategyId,
}
}
Step 2 — Pick an AgentId
AgentId is a fixed enum — there is no Custom variant. A new subsystem reuses
the slot closest to its work. Foliage rendering, for example, reuses
AgentId::Renderer. The variants are: Renderer, ShadowRenderer, Overlay,
Physics, Ecs, Ui, Audio, Asset.
#![allow(unused)]
fn main() {
impl Agent for FoliageAgent {
fn id(&self) -> AgentId {
AgentId::Renderer
}
}
Step 3 — Offer strategies in negotiate, pick a lane in apply_budget
negotiate returns the strategies this agent can run with their estimated cost; the
DCC issues one back. apply_budget records the chosen strategy and selects the
matching lane — this is how the subsystem scales itself under load.
#![allow(unused)]
fn main() {
fn negotiate(&mut self, _request: NegotiationRequest) -> NegotiationResponse {
NegotiationResponse {
strategies: vec![
StrategyOption {
id: StrategyId::HighPerformance,
estimated_time: Duration::from_micros(800),
estimated_vram: 4 * 1024 * 1024,
},
StrategyOption {
id: StrategyId::LowPower,
estimated_time: Duration::from_micros(200),
estimated_vram: 1024 * 1024,
},
],
timing_adjustment: None,
}
}
fn apply_budget(&mut self, budget: ResourceBudget) {
self.current_strategy = budget.strategy_id;
// Pick the lane matching the issued strategy.
// self.lane = Some(Box::new(...));
}
}
Step 4 — Dispatch the lane in execute
Read inputs from context.bus, build a LaneContext, dispatch the lane, and write
outputs to context.deck. The agent does only lane selection and dispatch — all
real work lives in the lane (see Add a lane).
#![allow(unused)]
fn main() {
fn execute(&mut self, context: &mut EngineContext<'_>) {
let Some(lane) = self.lane.as_ref() else { return };
let mut ctx = LaneContext::new();
// … insert Views read from context.bus into ctx …
if let Err(e) = lane.execute(&mut ctx) {
log::error!("foliage lane failed: {e}");
}
let _ = context;
}
fn report_status(&self) -> AgentStatus {
AgentStatus {
agent_id: self.id(),
health_score: 1.0,
current_strategy: self.current_strategy,
is_stalled: false,
message: "foliage ok".to_owned(),
}
}
}
Step 5 — Declare execution timing
execution_timing tells the scheduler when the agent runs. The real phases are
INIT, OBSERVE, TRANSFORM, MUTATE, OUTPUT, FINALIZE. OUTPUT is the
render/present phase; TRANSFORM is per-frame logic. Mark work the scheduler may
drop under budget pressure as AgentImportance::Optional — only Optional is
negotiable; Critical and Important always run.
#![allow(unused)]
fn main() {
fn execution_timing(&self) -> ExecutionTiming {
ExecutionTiming {
allowed_phases: vec![ExecutionPhase::OUTPUT],
default_phase: ExecutionPhase::OUTPUT,
priority: 0.7,
importance: AgentImportance::Optional,
fixed_timestep: None,
dependencies: Vec::new(),
}
}
fn as_any(&self) -> &dyn Any { self }
fn as_any_mut(&mut self) -> &mut dyn Any { self }
}
}
Step 6 — Register the agent via AgentProvider
Game and host code register agents through the AgentProvider::register_agents hook.
Wrap the agent in Arc<Mutex<…>> and hand it to the DCC with a priority. Use
register_agent_for_mode to restrict it to specific engine modes.
#![allow(unused)]
fn main() {
impl AgentProvider for MyGame {
fn register_agents(&self, dcc: &DccService, _runtime: &mut Runtime) {
dcc.register_agent(Arc::new(Mutex::new(FoliageAgent::default())), 0.7);
}
}
}
Step 7 — Verify it works
cargo test --workspace
cargo run -p sandbox
Confirm the agent’s report_status message and its lane’s effect appear in the running
frame. For render agents, check there are no wgpu/Vulkan validation errors.
Related
- Agents — the full agent contract and lifecycle.
- GORNA — how budgets are negotiated and issued.
- Control GORNA adaptation — pin or bound this agent’s strategy from host code.