tirea_contract/runtime/phase/
action_set.rs

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