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#[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}