Skip to main content

tirea_contract/runtime/phase/
action_set.rs

1use crate::runtime::action::Action;
2use crate::runtime::inference::InferenceRequestTransform;
3use crate::runtime::run::TerminationReason;
4use crate::runtime::state::AnyStateAction;
5use crate::runtime::tool_call::gate::{SuspendTicket, ToolCallAction};
6use crate::runtime::tool_call::ToolResult;
7use std::sync::Arc;
8
9/// A typed collection of actions for a specific phase.
10///
11/// `ActionSet<A>` is the return type of all [`AgentBehavior`](super::super::behavior::AgentBehavior)
12/// hooks. It is the unit of composition: plugins can define named functions
13/// that return `ActionSet<A>` combining multiple core actions, and callers
14/// compose them with [`ActionSet::and`].
15///
16/// [`From<A> for ActionSet<A>`] allows a single action to be returned anywhere
17/// an `ActionSet<A>` is expected, and [`From<AnyStateAction> for A`] is
18/// implemented for every phase action enum so state changes can be expressed
19/// without explicit wrapping.
20#[derive(Default)]
21pub struct ActionSet<A>(Vec<A>);
22
23impl<A> ActionSet<A> {
24    /// Empty set — default value, returned when a plugin does nothing.
25    pub fn empty() -> Self {
26        Self(Vec::new())
27    }
28
29    /// Single-action set.
30    pub fn single(a: impl Into<A>) -> Self {
31        Self(vec![a.into()])
32    }
33
34    /// Combine with another action set or anything that converts into one.
35    #[must_use]
36    pub fn and(mut self, other: impl Into<ActionSet<A>>) -> Self {
37        self.0.extend(other.into().0);
38        self
39    }
40
41    pub fn is_empty(&self) -> bool {
42        self.0.is_empty()
43    }
44
45    pub fn len(&self) -> usize {
46        self.0.len()
47    }
48
49    /// Borrow the inner slice.
50    pub fn as_slice(&self) -> &[A] {
51        &self.0
52    }
53
54    /// Consume into the inner `Vec`.
55    pub fn into_vec(self) -> Vec<A> {
56        self.0
57    }
58}
59
60impl<A> IntoIterator for ActionSet<A> {
61    type Item = A;
62    type IntoIter = std::vec::IntoIter<A>;
63    fn into_iter(self) -> Self::IntoIter {
64        self.0.into_iter()
65    }
66}
67
68impl<A> From<Vec<A>> for ActionSet<A> {
69    fn from(v: Vec<A>) -> Self {
70        Self(v)
71    }
72}
73
74impl<A> Extend<A> for ActionSet<A> {
75    fn extend<T: IntoIterator<Item = A>>(&mut self, iter: T) {
76        self.0.extend(iter);
77    }
78}
79
80// =========================================================================
81// Phase-specific action enums
82// =========================================================================
83
84/// Actions valid in lifecycle phases: RunStart, StepStart, StepEnd, RunEnd.
85///
86/// Only state changes are valid here; there is no inference or tool context.
87pub enum LifecycleAction {
88    State(AnyStateAction),
89}
90
91impl From<AnyStateAction> for LifecycleAction {
92    fn from(sa: AnyStateAction) -> Self {
93        Self::State(sa)
94    }
95}
96
97impl From<LifecycleAction> for ActionSet<LifecycleAction> {
98    fn from(a: LifecycleAction) -> Self {
99        ActionSet::single(a)
100    }
101}
102
103impl From<AnyStateAction> for ActionSet<LifecycleAction> {
104    fn from(sa: AnyStateAction) -> Self {
105        ActionSet::single(LifecycleAction::State(sa))
106    }
107}
108
109// -------------------------------------------------------------------------
110
111/// Actions valid in `BeforeInference`.
112pub enum BeforeInferenceAction {
113    /// Append a system-prompt context block.
114    AddSystemContext(String),
115    /// Append a session message.
116    AddSessionContext(String),
117    /// Remove one tool by id.
118    ExcludeTool(String),
119    /// Keep only the listed tool ids.
120    IncludeOnlyTools(Vec<String>),
121    /// Register a request transform applied after messages are assembled.
122    AddRequestTransform(Arc<dyn InferenceRequestTransform>),
123    /// Request run termination before inference fires.
124    Terminate(TerminationReason),
125    /// Emit a persistent state change.
126    State(AnyStateAction),
127}
128
129impl From<AnyStateAction> for BeforeInferenceAction {
130    fn from(sa: AnyStateAction) -> Self {
131        Self::State(sa)
132    }
133}
134
135impl From<BeforeInferenceAction> for ActionSet<BeforeInferenceAction> {
136    fn from(a: BeforeInferenceAction) -> Self {
137        ActionSet::single(a)
138    }
139}
140
141impl From<AnyStateAction> for ActionSet<BeforeInferenceAction> {
142    fn from(sa: AnyStateAction) -> Self {
143        ActionSet::single(BeforeInferenceAction::State(sa))
144    }
145}
146
147// -------------------------------------------------------------------------
148
149/// Actions valid in `AfterInference`.
150pub enum AfterInferenceAction {
151    /// Request run termination after seeing the LLM response.
152    Terminate(TerminationReason),
153    /// Emit a persistent state change.
154    State(AnyStateAction),
155}
156
157impl From<AnyStateAction> for AfterInferenceAction {
158    fn from(sa: AnyStateAction) -> Self {
159        Self::State(sa)
160    }
161}
162
163impl From<AfterInferenceAction> for ActionSet<AfterInferenceAction> {
164    fn from(a: AfterInferenceAction) -> Self {
165        ActionSet::single(a)
166    }
167}
168
169impl From<AnyStateAction> for ActionSet<AfterInferenceAction> {
170    fn from(sa: AnyStateAction) -> Self {
171        ActionSet::single(AfterInferenceAction::State(sa))
172    }
173}
174
175// -------------------------------------------------------------------------
176
177/// Actions valid in `BeforeToolExecute`.
178pub enum BeforeToolExecuteAction {
179    /// Block tool execution with a denial reason.
180    Block(String),
181    /// Suspend tool execution pending external confirmation.
182    Suspend(SuspendTicket),
183    /// Short-circuit tool execution with a pre-built result.
184    SetToolResult(ToolResult),
185    /// Emit a persistent state change.
186    State(AnyStateAction),
187}
188
189impl BeforeToolExecuteAction {
190    /// Convenience: forward a [`ToolCallAction`] as a `BeforeToolExecuteAction`.
191    pub fn from_decision(decision: ToolCallAction) -> Self {
192        match decision {
193            ToolCallAction::Block { reason } => Self::Block(reason),
194            ToolCallAction::Suspend(ticket) => Self::Suspend(*ticket),
195            ToolCallAction::Proceed => {
196                unreachable!("Proceed is not emitted as a BeforeToolExecuteAction")
197            }
198        }
199    }
200}
201
202impl From<AnyStateAction> for BeforeToolExecuteAction {
203    fn from(sa: AnyStateAction) -> Self {
204        Self::State(sa)
205    }
206}
207
208impl From<BeforeToolExecuteAction> for ActionSet<BeforeToolExecuteAction> {
209    fn from(a: BeforeToolExecuteAction) -> Self {
210        ActionSet::single(a)
211    }
212}
213
214impl From<AnyStateAction> for ActionSet<BeforeToolExecuteAction> {
215    fn from(sa: AnyStateAction) -> Self {
216        ActionSet::single(BeforeToolExecuteAction::State(sa))
217    }
218}
219
220// -------------------------------------------------------------------------
221
222/// Actions valid in `AfterToolExecute`.
223pub enum AfterToolExecuteAction {
224    /// Append a system-role reminder after the tool result.
225    AddSystemReminder(String),
226    /// Append a user-role message after the tool result.
227    AddUserMessage(String),
228    /// Emit a persistent state change.
229    State(AnyStateAction),
230}
231
232impl Action for AfterToolExecuteAction {
233    fn label(&self) -> &'static str {
234        match self {
235            Self::AddSystemReminder(_) => "add_system_reminder",
236            Self::AddUserMessage(_) => "add_user_message",
237            Self::State(_) => "state_action",
238        }
239    }
240
241    fn validate(&self, phase: super::types::Phase) -> Result<(), String> {
242        if phase == super::types::Phase::AfterToolExecute {
243            Ok(())
244        } else {
245            Err(format!(
246                "AfterToolExecuteAction '{}' is only valid in AfterToolExecute, got {phase}",
247                self.label()
248            ))
249        }
250    }
251
252    fn apply(self: Box<Self>, step: &mut super::step::StepContext<'_>) {
253        match *self {
254            Self::AddSystemReminder(text) => step.messaging.reminders.push(text),
255            Self::AddUserMessage(text) => step.messaging.user_messages.push(text),
256            Self::State(action) => step.emit_state_action(action),
257        }
258    }
259
260    fn is_state_action(&self) -> bool {
261        matches!(self, Self::State(_))
262    }
263
264    fn into_state_action(self: Box<Self>) -> Option<AnyStateAction> {
265        match *self {
266            Self::State(action) => Some(action),
267            _ => None,
268        }
269    }
270}
271
272impl From<AnyStateAction> for AfterToolExecuteAction {
273    fn from(sa: AnyStateAction) -> Self {
274        Self::State(sa)
275    }
276}
277
278impl From<AfterToolExecuteAction> for ActionSet<AfterToolExecuteAction> {
279    fn from(a: AfterToolExecuteAction) -> Self {
280        ActionSet::single(a)
281    }
282}
283
284impl From<AnyStateAction> for ActionSet<AfterToolExecuteAction> {
285    fn from(sa: AnyStateAction) -> Self {
286        ActionSet::single(AfterToolExecuteAction::State(sa))
287    }
288}