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