khora_core/agent/mod.rs
1// Copyright 2025 eraflo
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Traits for autonomous engine subsystems (Agents).
16
17pub mod completion;
18pub mod dependency;
19pub mod execution_phase;
20pub mod mode;
21pub mod timing;
22
23use crate::control::gorna::AgentId;
24use crate::control::gorna::{AgentStatus, NegotiationRequest, NegotiationResponse, ResourceBudget};
25use crate::EngineContext;
26use std::any::Any;
27
28pub use completion::{AgentCompletionMap, AgentDone, CompletionOutcome};
29pub use dependency::{AgentDependency, DependencyCondition, DependencyKind};
30pub use execution_phase::ExecutionPhase;
31pub use mode::EngineMode;
32pub use timing::{AgentImportance, ExecutionTiming};
33
34/// The foundational interface for an Intelligent Subsystem Agent (ISA).
35///
36/// Each major subsystem (Rendering, Physics, etc.) implements this trait to
37/// participate in the engine's dynamic resource negotiation (GORNA).
38///
39/// # Lifecycle
40///
41/// 1. `on_initialize(ctx)` — called **once** after registration. The agent
42/// caches required services and initializes its lanes.
43/// 2. `execute(ctx)` — called **every frame**. The agent selects the appropriate
44/// lanes based on the current GORNA budget and dispatches their `Lane::execute()`.
45/// 3. `negotiate(request)` / `apply_budget(budget)` — called by the GORNA
46/// arbitrator on the DCC background thread when the system re-evaluates strategy.
47/// 4. `report_status()` — polled by GORNA for health monitoring.
48///
49/// An agent must contain **no business logic** beyond lane selection, budget
50/// negotiation, and lane dispatch. All real work belongs in [`Lane`] implementations.
51///
52/// # Examples
53///
54/// A skeleton agent. Real agents cache their lanes in `on_initialize` and select
55/// one per frame in `execute` based on the budget applied via `apply_budget`.
56/// (Marked `ignore` because a compiling impl needs the GORNA request/response
57/// types and an [`EngineContext`] from the running engine.)
58///
59/// ```ignore
60/// use khora_core::agent::{Agent, ExecutionTiming};
61/// use khora_core::control::gorna::{
62/// AgentId, AgentStatus, NegotiationRequest, NegotiationResponse, ResourceBudget,
63/// };
64/// use khora_core::EngineContext;
65/// use std::any::Any;
66///
67/// #[derive(Default)]
68/// struct MyAgent;
69///
70/// impl Agent for MyAgent {
71/// fn id(&self) -> AgentId { AgentId::Renderer }
72///
73/// fn negotiate(&mut self, request: NegotiationRequest) -> NegotiationResponse {
74/// // Propose a strategy that fits `request`'s constraints.
75/// NegotiationResponse::default()
76/// }
77///
78/// fn apply_budget(&mut self, _budget: ResourceBudget) {
79/// // Adjust quality / LOD to stay within the allocated budget.
80/// }
81///
82/// fn report_status(&self) -> AgentStatus { AgentStatus::default() }
83///
84/// fn execute(&mut self, _ctx: &mut EngineContext<'_>) {
85/// // Pick a lane for this frame and dispatch `Lane::execute`.
86/// }
87///
88/// fn as_any(&self) -> &dyn Any { self }
89/// fn as_any_mut(&mut self) -> &mut dyn Any { self }
90/// }
91/// ```
92pub trait Agent: Send + Sync {
93 /// Returns the unique identifier for this agent.
94 fn id(&self) -> AgentId;
95
96 /// Negotiates with the DCC to determine the best execution strategy
97 /// given the current global resource constraints and priorities.
98 fn negotiate(&mut self, request: NegotiationRequest) -> NegotiationResponse;
99
100 /// Applies a resource budget issued by the DCC.
101 /// The agent must adjust its internal logic (e.g., LOD, quality settings)
102 /// to stay within the allocated limits.
103 fn apply_budget(&mut self, budget: ResourceBudget);
104
105 /// Reports the current status and health of the agent.
106 fn report_status(&self) -> AgentStatus;
107
108 /// Called **once** after the agent is registered with the DCC.
109 ///
110 /// The agent should cache services from `context.services`, initialize
111 /// its lane registry, and prepare any persistent state.
112 /// Default implementation is a no-op.
113 fn on_initialize(&mut self, _context: &mut EngineContext<'_>) {}
114
115 /// Called **every frame** by the engine loop.
116 ///
117 /// The agent selects the appropriate lanes based on the current GORNA
118 /// strategy, builds a [`LaneContext`](crate::lane::LaneContext), and dispatches
119 /// [`Lane::execute()`](crate::lane::Lane::execute) for each lane that should run.
120 fn execute(&mut self, context: &mut EngineContext<'_>);
121
122 /// Declares WHEN and HOW this agent should execute within the frame pipeline.
123 ///
124 /// The Scheduler uses this information to filter agents by phase, order them
125 /// by priority, and skip optional agents under budget pressure.
126 ///
127 /// Default implementation returns a timing that allows execution in all phases
128 /// with `Important` priority.
129 fn execution_timing(&self) -> ExecutionTiming {
130 ExecutionTiming::default()
131 }
132
133 /// Declares this agent's data-access footprint during [`execute`](Self::execute),
134 /// so the scheduler can decide whether it is safe to run concurrently with
135 /// other agents in the same phase.
136 ///
137 /// Defaults to [`AgentAccess::Exclusive`] — the safe fallback: the agent is
138 /// assumed to need exclusive `&mut World`, so it runs serially. An agent
139 /// overrides this to [`AgentAccess::Isolated`] **only** if `execute` provably
140 /// touches no `World` and no shared mutable engine resource (it reads the
141 /// `LaneBus` and writes solely its own `OutputDeck`).
142 fn access(&self) -> AgentAccess {
143 AgentAccess::Exclusive
144 }
145
146 /// Declares the [`OutputDeck`](crate::lane::OutputDeck) slot types this agent
147 /// writes during [`execute`](Self::execute), by [`TypeId`](std::any::TypeId).
148 ///
149 /// The scheduler uses this at wave-formation time: two concurrency-eligible
150 /// agents grouped into the same wave write into private deck shards that are
151 /// folded back together afterwards, so they **must** write disjoint slot
152 /// types. Declaring the written slots lets the scheduler catch a collision
153 /// when the wave is built — naming the offending agents — instead of only
154 /// discovering it defensively during the shard merge.
155 ///
156 /// Defaults to empty: an agent that writes no deck slot (or only runs
157 /// `Exclusive`, i.e. never in a concurrent wave) need not override it.
158 fn deck_writes(&self) -> Vec<std::any::TypeId> {
159 Vec::new()
160 }
161
162 /// Allows downcasting to concrete agent types.
163 fn as_any(&self) -> &dyn Any;
164
165 /// Allows mutable downcasting to concrete agent types.
166 fn as_any_mut(&mut self) -> &mut dyn Any;
167}
168
169/// An agent's data-access footprint during [`Agent::execute`], used by the
170/// scheduler's parallel executor to decide which agents may run concurrently.
171///
172/// The default ([`Exclusive`](Self::Exclusive)) is the safe fallback: the agent
173/// may touch the ECS `World` mutably (or a shared mutable resource), so it must
174/// run serially. Only an agent that provably confines itself to reading the
175/// `LaneBus` and writing its own `OutputDeck` should declare
176/// [`Isolated`](Self::Isolated).
177#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
178pub enum AgentAccess {
179 /// Needs exclusive `&mut World` (or mutates a shared resource); runs
180 /// serially. The safe default.
181 #[default]
182 Exclusive,
183 /// Touches no `World` and writes only its own `OutputDeck` — no shared
184 /// mutable engine resources. Eligible for concurrent execution: the
185 /// scheduler runs it with [`WorldAccess::None`](crate::WorldAccess::None)
186 /// and a private deck shard, folded back into the shared deck after the
187 /// concurrent wave. Any number may run in the same wave.
188 Isolated,
189 /// Reads the `World` immutably (never mutates it) and may write shared
190 /// engine resources. The scheduler runs it with a shared
191 /// [`WorldAccess::Shared`](crate::WorldAccess::Shared) reference so many
192 /// world-readers execute concurrently. Because it may write shared
193 /// resources whose ordering matters, **at most one `SharedWorld` agent runs
194 /// per wave** (alongside any number of `Isolated` agents).
195 SharedWorld,
196}