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