Skip to main content

lash_sansio/
plugin.rs

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