mecha10_behavior_runtime/behavior.rs
1//! Core BehaviorNode trait and types
2//!
3//! This module defines the fundamental `BehaviorNode` trait that all behaviors must implement.
4
5use crate::NodeStatus;
6use async_trait::async_trait;
7use mecha10_core::Context;
8use serde::{Deserialize, Serialize};
9use std::fmt::Debug;
10
11/// A point-in-time snapshot of a node's (and its subtree's) last-tick status, for live
12/// behavior-tree visualization. Captured on demand via `BehaviorNode::status_snapshot` - it does
13/// not itself tick anything, it just reads back what the most recent `tick()` call(s) already
14/// recorded.
15///
16/// `status: None` means this node has never been ticked yet (e.g. right after construction, or
17/// a child a `SelectOrNode`/`SequenceNode` hasn't reached yet this cycle). Intended to eventually
18/// cross a process boundary (serialized as JSON) via `/behavior/status`, hence `Serialize`/
19/// `Deserialize`.
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
21pub struct TreeStatusSnapshot {
22 pub name: String,
23 pub status: Option<NodeStatus>,
24 pub active_capability: Option<String>,
25 pub children: Vec<TreeStatusSnapshot>,
26}
27
28#[async_trait]
29pub trait BehaviorNode: Send + Sync + Debug {
30 /// Execute one tick of the behavior. Called repeatedly by the execution engine; should
31 /// perform one unit of work, return quickly, and report progress via `NodeStatus`.
32 async fn tick(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus>;
33
34 /// Get the name of this behavior node.
35 ///
36 /// Used for debugging, logging, and monitoring.
37 fn name(&self) -> &str {
38 std::any::type_name::<Self>()
39 }
40
41 /// Return mutable references to this node's child behaviors, in the order
42 /// they should receive lifecycle events. Leaf nodes should not override this;
43 /// composite nodes override it to expose their children.
44 fn children_mut(&mut self) -> Vec<&mut dyn BehaviorNode> {
45 Vec::new()
46 }
47
48 /// The `capability` this node implies should be published on `/behavior/status` while it's
49 /// actively being ticked; `None` for nodes with no such gate (the default). Composite nodes
50 /// override this to forward whichever child is currently active.
51 fn active_capability(&self) -> Option<&str> {
52 None
53 }
54
55 /// Snapshot of what this node (and, for composite nodes, its subtree) reported on its last
56 /// `tick()`, for live behavior-tree visualization. The default implementation reports
57 /// `status: None` (as if never ticked) and no children - correct for a brand-new node type
58 /// that hasn't been wired up for status tracking yet, but every node type should override
59 /// this once it stores a `last_status` field: leaf nodes return their own `last_status`,
60 /// composite nodes additionally recurse into `children_mut`'s non-`mut` counterpart (there's
61 /// no shared non-mut child accessor, so each composite overrides this to walk its own child
62 /// collection directly - same pattern as `children_mut`).
63 fn status_snapshot(&self) -> TreeStatusSnapshot {
64 TreeStatusSnapshot {
65 name: self.name().to_string(),
66 status: None,
67 active_capability: self.active_capability().map(String::from),
68 children: Vec::new(),
69 }
70 }
71
72 /// Reset this node's own local state, ignoring children.
73 ///
74 /// Called by the default `reset()` before it propagates to children.
75 fn reset_self(&mut self) {}
76
77 /// Reset the behavior to its initial state.
78 ///
79 /// Called when the behavior needs to be restarted (e.g., in a loop or after a failure).
80 async fn reset(&mut self) -> anyhow::Result<()> {
81 self.reset_self();
82 for child in self.children_mut() {
83 child.reset().await?;
84 }
85 Ok(())
86 }
87
88 /// Called when the behavior is first initialized.
89 ///
90 /// Use this for one-time setup like loading models, establishing connections, etc.
91 async fn on_init(&mut self, ctx: &Context) -> anyhow::Result<()> {
92 for child in self.children_mut() {
93 child.on_init(ctx).await?;
94 }
95 Ok(())
96 }
97
98 /// Called when the behavior is being terminated.
99 ///
100 /// Use this for cleanup like closing connections, saving state, etc.
101 async fn on_terminate(&mut self, ctx: &Context) -> anyhow::Result<()> {
102 for child in self.children_mut() {
103 child.on_terminate(ctx).await?;
104 }
105 Ok(())
106 }
107}
108
109pub type BoxedBehavior = Box<dyn BehaviorNode>;
110
111/// Maps an iterator of boxed child behaviors to `BehaviorNode::children_mut`'s trait-object
112/// return type.
113pub fn boxed_children_mut<'a>(children: impl Iterator<Item = &'a mut BoxedBehavior>) -> Vec<&'a mut dyn BehaviorNode> {
114 children.map(|c| c.as_mut() as &mut dyn BehaviorNode).collect()
115}
116
117#[macro_export]
118macro_rules! children_mut_via_wrapped_behavior {
119 ($field:ident, $wrapper:ident) => {
120 fn children_mut(&mut self) -> Vec<&mut dyn $crate::BehaviorNode> {
121 $crate::boxed_children_mut(self.$field.iter_mut().map(|$wrapper| &mut $wrapper.behavior))
122 }
123 };
124}
125
126#[macro_export]
127macro_rules! reset_child_statuses_field {
128 ($field:ident) => {
129 fn reset_self(&mut self) {
130 self.$field
131 .iter_mut()
132 .for_each(|s| *s = $crate::NodeStatus::Running);
133 }
134 };
135}
136
137#[macro_export]
138macro_rules! reset_index_field {
139 ($field:ident) => {
140 fn reset_self(&mut self) {
141 self.$field = 0;
142 }
143 };
144}
145
146#[async_trait]
147pub trait BehaviorNodeExt: BehaviorNode {
148 /// Run this behavior until it completes (Success or Failure).
149 ///
150 /// Convenience method for testing and simple execution.
151 async fn run_until_complete(&mut self, ctx: &Context) -> anyhow::Result<NodeStatus> {
152 loop {
153 match self.tick(ctx).await? {
154 NodeStatus::Success => return Ok(NodeStatus::Success),
155 NodeStatus::Failure => return Ok(NodeStatus::Failure),
156 NodeStatus::Running => continue,
157 }
158 }
159 }
160
161 /// Run this behavior with a maximum number of ticks.
162 ///
163 /// Returns `Err` if the behavior doesn't complete within `max_ticks`.
164 async fn run_with_limit(&mut self, ctx: &Context, max_ticks: usize) -> anyhow::Result<NodeStatus> {
165 for _tick_count in 0..max_ticks {
166 match self.tick(ctx).await? {
167 NodeStatus::Success => return Ok(NodeStatus::Success),
168 NodeStatus::Failure => return Ok(NodeStatus::Failure),
169 NodeStatus::Running => continue,
170 }
171 }
172 anyhow::bail!("Behavior exceeded maximum ticks ({})", max_ticks)
173 }
174}
175
176impl<T: BehaviorNode + ?Sized> BehaviorNodeExt for T {}