Skip to main content

lash_sansio/
plugin.rs

1use std::sync::Arc;
2
3use crate::{MessageOrigin, MessageRole, Part};
4use serde::{Deserialize, Serialize};
5
6#[derive(Clone, Debug, Serialize, Deserialize)]
7pub struct PluginMessage {
8    pub role: MessageRole,
9    pub content: String,
10    #[serde(default, skip_serializing_if = "Option::is_none")]
11    pub origin: Option<MessageOrigin>,
12    #[serde(default, skip_serializing_if = "Vec::is_empty")]
13    pub parts: Vec<Part>,
14    #[serde(default, skip_serializing_if = "Vec::is_empty")]
15    pub images: Vec<Vec<u8>>,
16}
17
18impl PluginMessage {
19    pub fn text(role: MessageRole, content: impl Into<String>) -> Self {
20        Self {
21            role,
22            content: content.into(),
23            origin: None,
24            parts: Vec::new(),
25            images: Vec::new(),
26        }
27    }
28
29    pub fn with_origin(mut self, origin: MessageOrigin) -> Self {
30        self.origin = Some(origin);
31        self
32    }
33
34    pub fn first_text(&self) -> Option<&str> {
35        if !self.content.is_empty() {
36            return Some(self.content.as_str());
37        }
38        self.parts.iter().find_map(|part| {
39            matches!(part.kind, crate::PartKind::Text | crate::PartKind::Prose)
40                .then_some(part.content.as_str())
41        })
42    }
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
46pub struct PromptContributionGate {
47    #[serde(default, skip_serializing_if = "Vec::is_empty")]
48    pub tools: Vec<String>,
49    #[serde(default)]
50    pub minimum_availability: crate::ToolAvailability,
51}
52
53impl PromptContributionGate {
54    pub fn is_empty(&self) -> bool {
55        self.tools.is_empty()
56    }
57}
58
59#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
60pub struct PromptContribution {
61    pub slot: crate::PromptSlot,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub title: Option<Arc<str>>,
64    #[serde(default)]
65    pub priority: i32,
66    #[serde(default, skip_serializing_if = "PromptContributionGate::is_empty")]
67    pub gate: PromptContributionGate,
68    pub content: Arc<str>,
69}
70
71impl PromptContribution {
72    pub fn new(
73        slot: crate::PromptSlot,
74        title: impl Into<Arc<str>>,
75        content: impl Into<Arc<str>>,
76    ) -> Self {
77        let title: Arc<str> = title.into();
78        let title = (!title.trim().is_empty()).then_some(title);
79        Self {
80            slot,
81            title,
82            priority: 0,
83            gate: PromptContributionGate {
84                tools: Vec::new(),
85                minimum_availability: crate::ToolAvailability::default(),
86            },
87            content: content.into(),
88        }
89    }
90
91    pub fn with_priority(mut self, priority: i32) -> Self {
92        self.priority = priority;
93        self
94    }
95
96    pub fn requires_tool(
97        mut self,
98        tool_name: impl Into<String>,
99        minimum_availability: crate::ToolAvailability,
100    ) -> Self {
101        self.gate = PromptContributionGate {
102            tools: vec![tool_name.into()],
103            minimum_availability,
104        };
105        self
106    }
107
108    pub fn requires_any_tool(
109        mut self,
110        tool_names: impl IntoIterator<Item = impl Into<String>>,
111        minimum_availability: crate::ToolAvailability,
112    ) -> Self {
113        self.gate = PromptContributionGate {
114            tools: tool_names.into_iter().map(Into::into).collect(),
115            minimum_availability,
116        };
117        self
118    }
119
120    pub fn intro(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
121        Self::new(crate::PromptSlot::Intro, title, content)
122    }
123
124    pub fn execution(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
125        Self::new(crate::PromptSlot::Execution, title, content)
126    }
127
128    pub fn guidance(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
129        Self::new(crate::PromptSlot::Guidance, title, content)
130    }
131
132    pub fn project_instructions(content: impl Into<Arc<str>>) -> Self {
133        Self::new(
134            crate::PromptSlot::ProjectInstructions,
135            "Project Instructions",
136            content,
137        )
138    }
139
140    pub fn runtime_context(content: impl Into<Arc<str>>) -> Self {
141        Self::new(
142            crate::PromptSlot::RuntimeContext,
143            "Runtime Context",
144            content,
145        )
146    }
147
148    pub fn environment(title: impl Into<Arc<str>>, content: impl Into<Arc<str>>) -> Self {
149        Self::new(crate::PromptSlot::Environment, title, content)
150    }
151}
152
153#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
154#[serde(tag = "kind", rename_all = "snake_case")]
155pub enum PluginRuntimeEvent {
156    Status {
157        key: String,
158        label: String,
159        #[serde(default, skip_serializing_if = "Option::is_none")]
160        detail: Option<String>,
161    },
162    Custom {
163        name: String,
164        payload: serde_json::Value,
165    },
166}
167
168#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub enum CheckpointKind {
171    AfterWork,
172    BeforeCompletion,
173}