tirea_contract/runtime/
control.rs1use crate::event::interaction::{FrontendToolInvocation, Interaction};
7use crate::thread::Thread;
8use serde::{Deserialize, Serialize};
9use tirea_state::State;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
13pub struct InferenceError {
14 #[serde(rename = "type")]
16 pub error_type: String,
17 pub message: String,
19}
20
21#[derive(Debug, Clone, Default, Serialize, Deserialize, State)]
26#[tirea(path = "loop_control")]
27pub struct LoopControlState {
28 #[tirea(default = "None")]
30 pub pending_interaction: Option<Interaction>,
31 #[tirea(default = "None")]
34 pub pending_frontend_invocation: Option<FrontendToolInvocation>,
35 #[tirea(default = "None")]
37 pub inference_error: Option<InferenceError>,
38}
39
40pub trait LoopControlExt {
42 fn pending_interaction(&self) -> Option<Interaction>;
44}
45
46impl LoopControlExt for Thread {
47 fn pending_interaction(&self) -> Option<Interaction> {
48 self.rebuild_state()
49 .ok()
50 .and_then(|state| {
51 state
52 .get(LoopControlState::PATH)
53 .and_then(|lc| lc.get("pending_interaction"))
54 .cloned()
55 })
56 .and_then(|value| serde_json::from_value::<Interaction>(value).ok())
57 }
58}
59
60#[cfg(test)]
61mod tests {
62 use super::*;
63 use serde_json::Value;
64
65 #[test]
66 fn test_interaction_new() {
67 let interaction = Interaction::new("int_1", "confirm");
68 assert_eq!(interaction.id, "int_1");
69 assert_eq!(interaction.action, "confirm");
70 assert!(interaction.message.is_empty());
71 assert_eq!(interaction.parameters, Value::Null);
72 assert!(interaction.response_schema.is_none());
73 }
74
75 #[test]
76 fn test_loop_control_state_defaults() {
77 let state = LoopControlState::default();
78 assert!(state.pending_interaction.is_none());
79 assert!(state.inference_error.is_none());
80 }
81}