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}