1use std::collections::BTreeSet;
18
19use serde::{Deserialize, Deserializer, Serialize};
20use termivar_core::{EntityId, Outcome};
21use thiserror::Error;
22
23use crate::planner::{ActionSuppressionContext, ScheduledActionAuthorizationError};
24use crate::{
25 AdaptationLedger, AdaptationLimits, AdaptiveDecision, AdaptivePipeline, AdaptivePipelineError,
26 AttackPlan, AttackPlanner, ExperiencePolicy, ExperienceStore, ExperienceStoreError,
27 ExperienceWrite, KnowledgeBase, KnowledgeBaseError, KnowledgeSnapshot, KnowledgeWrite,
28 PayloadStrategyRef, PipelineDirective, PlannerError, PlanningContext,
29 ResolvedVerificationTarget, RuleApplication, RuleEngine, RuleEngineError, VerificationCase,
30 VerificationError, VerificationPipeline, VerificationReport,
31};
32
33mod command;
34mod policy;
35mod receipts;
36mod state;
37
38pub(crate) use command::{command_requiring_host_policy_context, execution_command_action_id};
39pub use command::{DecisionActionOrigin, DecisionLoopCommand, DecisionStopReason};
40#[cfg(test)]
41use policy::transition_from_adaptive;
42pub use policy::DecisionLoop;
43pub use receipts::{DecisionOutcomeReport, DecisionPlanningReport, DecisionReasoningCommitReceipt};
44pub use state::{
45 DecisionLoopState, DecisionSession, DecisionSessionSummary, DecisionSessionTransition,
46};
47
48#[derive(Debug, Error)]
50#[non_exhaustive]
51pub enum DecisionLoopError {
52 #[error("maximum decision-loop action cycles must be greater than zero")]
54 ZeroActionCycles,
55
56 #[error("cannot {operation} while decision loop is {state}")]
58 InvalidTransition {
59 operation: &'static str,
61 state: &'static str,
63 },
64
65 #[error("decision session subject {expected} does not match case subject {actual}")]
67 CaseSubjectMismatch {
68 expected: EntityId,
70 actual: EntityId,
72 },
73
74 #[error("decision session cannot await evidence with zero issued action cycles")]
76 AwaitingWithoutAction,
77
78 #[error("decision-loop action cycle counter overflowed")]
80 ActionCycleOverflow,
81
82 #[error("scheduled action {action_id} has no eligible verification target")]
85 NoEligibleScheduledVerificationTarget {
86 action_id: String,
88 },
89
90 #[error("scheduled action {action_id} has no eligible motivation hypothesis")]
93 NoEligibleScheduledMotivationHypothesis {
94 action_id: String,
96 },
97
98 #[error("adaptive execution of action {action_id} requires explicit host suppression context")]
101 AdaptiveExecutionRequiresHostPolicyContext {
102 action_id: String,
104 },
105
106 #[error("decision action {action_id} is not registered with the planner")]
108 UnregisteredDecisionAction {
109 action_id: String,
111 },
112
113 #[error("decision case for action {action_id} exceeds current claim authority")]
116 DecisionCaseAuthorityExceeded {
117 action_id: String,
119 },
120
121 #[error("adaptive action {action_id} is not eligible under current planner policy")]
123 IneligibleAdaptiveAction {
124 action_id: String,
126 },
127
128 #[error("defense enforcement suppresses action {action_id}")]
130 DefenseSuppressedAction {
131 action_id: String,
133 },
134
135 #[error(
137 "adaptive action {action_id} declares prerequisites and must be selected by normal planning"
138 )]
139 AdaptiveActionRequiresPlanning {
140 action_id: String,
142 },
143
144 #[error(transparent)]
146 Rules(#[from] RuleEngineError),
147
148 #[error(transparent)]
150 Planner(#[from] PlannerError),
151
152 #[error(transparent)]
154 Verification(#[from] VerificationError),
155
156 #[error(transparent)]
158 Adaptive(#[from] AdaptivePipelineError),
159
160 #[error(transparent)]
162 Experience(#[from] ExperienceStoreError),
163
164 #[error("planning snapshot became stale: {source}")]
166 StalePlanningSnapshot {
167 #[source]
169 source: KnowledgeBaseError,
170 },
171
172 #[error("planning failed after reasoning was committed: {source}")]
174 PlanningAfterReasoningCommit {
175 receipt: Box<DecisionReasoningCommitReceipt>,
177 #[source]
179 source: Box<DecisionLoopError>,
180 },
181}
182
183impl DecisionLoopError {
184 pub fn committed_reasoning(&self) -> Option<&DecisionReasoningCommitReceipt> {
186 match self {
187 Self::PlanningAfterReasoningCommit { receipt, .. } => Some(receipt),
188 _ => None,
189 }
190 }
191
192 pub fn into_committed_reasoning(self) -> Option<DecisionReasoningCommitReceipt> {
194 match self {
195 Self::PlanningAfterReasoningCommit { receipt, .. } => Some(*receipt),
196 _ => None,
197 }
198 }
199}
200
201#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
203pub struct DecisionLoopConfig {
204 planning: PlanningContext,
205 adaptation: AdaptationLimits,
206 experience: ExperiencePolicy,
207 max_action_cycles: u32,
208}
209
210impl DecisionLoopConfig {
211 pub fn new(
213 planning: PlanningContext,
214 adaptation: AdaptationLimits,
215 experience: ExperiencePolicy,
216 max_action_cycles: u32,
217 ) -> Result<Self, DecisionLoopError> {
218 if max_action_cycles == 0 {
219 return Err(DecisionLoopError::ZeroActionCycles);
220 }
221 Ok(Self {
222 planning,
223 adaptation,
224 experience,
225 max_action_cycles,
226 })
227 }
228
229 pub fn planning(self) -> PlanningContext {
231 self.planning
232 }
233
234 pub fn adaptation(self) -> AdaptationLimits {
236 self.adaptation
237 }
238
239 pub fn experience(self) -> ExperiencePolicy {
241 self.experience
242 }
243
244 pub fn max_action_cycles(self) -> u32 {
246 self.max_action_cycles
247 }
248}
249
250pub(crate) struct ActiveEvidenceSnapshots<'a> {
251 baseline: &'a KnowledgeSnapshot,
252 after_probe: &'a KnowledgeSnapshot,
253}
254
255impl<'a> ActiveEvidenceSnapshots<'a> {
256 pub(crate) const fn new(
257 baseline: &'a KnowledgeSnapshot,
258 after_probe: &'a KnowledgeSnapshot,
259 ) -> Self {
260 Self {
261 baseline,
262 after_probe,
263 }
264 }
265}
266
267impl<'de> Deserialize<'de> for DecisionLoopConfig {
268 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
269 where
270 D: Deserializer<'de>,
271 {
272 #[derive(Deserialize)]
273 struct WireConfig {
274 planning: PlanningContext,
275 adaptation: AdaptationLimits,
276 experience: ExperiencePolicy,
277 max_action_cycles: u32,
278 }
279
280 let wire = WireConfig::deserialize(deserializer)?;
281 Self::new(
282 wire.planning,
283 wire.adaptation,
284 wire.experience,
285 wire.max_action_cycles,
286 )
287 .map_err(serde::de::Error::custom)
288 }
289}
290
291#[cfg(test)]
292#[path = "decision_loop/decision_loop_tests.rs"]
293mod tests;