1use super::process::ProcessWakeDelivery;
2use crate::{PluginMessage, TurnCause, TurnInput};
3
4#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
5#[serde(tag = "kind", rename_all = "snake_case")]
6pub enum SessionCommand {
7 RefreshToolCatalog { reason: String },
12 ResetSession { reason: String },
13}
14
15impl SessionCommand {
16 pub fn kind(&self) -> &'static str {
17 match self {
18 Self::RefreshToolCatalog { .. } => "refresh_tool_catalog",
19 Self::ResetSession { .. } => "reset_session",
20 }
21 }
22
23 pub fn source_key(&self, idempotency_key: impl AsRef<str>) -> String {
24 format!("command:{}:{}", self.kind(), idempotency_key.as_ref())
25 }
26}
27
28#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
29pub struct SessionCommandReceipt {
30 pub session_id: String,
31 pub batch_id: String,
32 pub source_key: String,
33}
34
35#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
36#[serde(rename_all = "snake_case")]
37pub enum DeliveryPolicy {
38 EarliestSafeBoundary,
39 AfterCurrentTurnCommit,
40}
41
42impl DeliveryPolicy {
43 pub fn as_str(self) -> &'static str {
44 match self {
45 Self::EarliestSafeBoundary => "earliest_safe_boundary",
46 Self::AfterCurrentTurnCommit => "after_current_turn_commit",
47 }
48 }
49
50 pub fn from_wire_str(value: &str) -> Option<Self> {
51 match value {
52 "earliest_safe_boundary" => Some(Self::EarliestSafeBoundary),
53 "after_current_turn_commit" => Some(Self::AfterCurrentTurnCommit),
54 _ => None,
55 }
56 }
57}
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum SlotPolicy {
62 Join,
63 Exclusive,
64}
65
66impl SlotPolicy {
67 pub fn as_str(self) -> &'static str {
68 match self {
69 Self::Join => "join",
70 Self::Exclusive => "exclusive",
71 }
72 }
73
74 pub fn from_wire_str(value: &str) -> Option<Self> {
75 match value {
76 "join" => Some(Self::Join),
77 "exclusive" => Some(Self::Exclusive),
78 _ => None,
79 }
80 }
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
84#[serde(rename_all = "snake_case")]
85pub enum MergeKey {
86 Never,
87 PayloadDefault,
88 Group(String),
89}
90
91#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
92#[serde(tag = "type", rename_all = "snake_case")]
93pub enum QueuedWorkPayload {
94 ProcessWake {
95 wake: Box<ProcessWakeDelivery>,
96 },
97 AgentFrameTask {
98 frame_id: crate::AgentFrameId,
99 task: String,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
101 protocol_turn_options: Option<crate::ProtocolTurnOptions>,
102 },
103 SessionCommand {
104 command: Box<SessionCommand>,
105 },
106}
107
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
109#[serde(rename_all = "snake_case")]
110pub enum QueuedWorkClass {
111 SessionCommand,
112 TurnWork,
113}
114
115impl QueuedWorkPayload {
116 pub fn process_wake(wake: ProcessWakeDelivery) -> Self {
117 Self::ProcessWake {
118 wake: Box::new(wake),
119 }
120 }
121
122 pub fn session_command(command: SessionCommand) -> Self {
123 Self::SessionCommand {
124 command: Box::new(command),
125 }
126 }
127
128 pub fn agent_frame_task(
129 frame_id: impl Into<crate::AgentFrameId>,
130 task: impl Into<String>,
131 protocol_turn_options: Option<crate::ProtocolTurnOptions>,
132 ) -> Self {
133 Self::AgentFrameTask {
134 frame_id: frame_id.into(),
135 task: task.into(),
136 protocol_turn_options,
137 }
138 }
139
140 pub fn work_class(&self) -> QueuedWorkClass {
141 match self {
142 Self::SessionCommand { .. } => QueuedWorkClass::SessionCommand,
143 Self::ProcessWake { .. } | Self::AgentFrameTask { .. } => QueuedWorkClass::TurnWork,
144 }
145 }
146}
147
148#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
149pub struct QueuedWorkItem {
150 pub item_id: String,
151 pub payload: QueuedWorkPayload,
152}
153
154#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
155pub struct QueuedWorkBatch {
156 pub batch_id: String,
157 pub session_id: String,
158 pub enqueue_seq: u64,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub source_key: Option<String>,
161 pub delivery_policy: DeliveryPolicy,
162 pub slot_policy: SlotPolicy,
163 pub merge_key: MergeKey,
164 pub available_at_ms: u64,
165 pub enqueued_at_ms: u64,
166 pub items: Vec<QueuedWorkItem>,
167}
168
169impl QueuedWorkBatch {
170 pub fn work_class(&self) -> Option<QueuedWorkClass> {
171 work_class_for_payloads(self.items.iter().map(|item| &item.payload))
172 }
173
174 pub fn is_session_command_work(&self) -> bool {
175 self.work_class() == Some(QueuedWorkClass::SessionCommand)
176 }
177
178 pub fn is_turn_work(&self) -> bool {
179 self.work_class() == Some(QueuedWorkClass::TurnWork)
180 }
181}
182
183#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
184pub struct QueuedWorkBatchDraft {
185 pub session_id: String,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
187 pub source_key: Option<String>,
188 pub delivery_policy: DeliveryPolicy,
189 pub slot_policy: SlotPolicy,
190 pub merge_key: MergeKey,
191 pub available_at_ms: u64,
192 pub payloads: Vec<QueuedWorkPayload>,
193}
194
195impl QueuedWorkBatchDraft {
196 pub fn new(
197 session_id: impl Into<String>,
198 delivery_policy: DeliveryPolicy,
199 slot_policy: SlotPolicy,
200 payloads: impl Into<Vec<QueuedWorkPayload>>,
201 ) -> Self {
202 Self {
203 session_id: session_id.into(),
204 source_key: None,
205 delivery_policy,
206 slot_policy,
207 merge_key: MergeKey::Never,
208 available_at_ms: 0,
209 payloads: payloads.into(),
210 }
211 }
212
213 pub fn with_source_key(mut self, source_key: impl Into<String>) -> Self {
214 self.source_key = Some(source_key.into());
215 self
216 }
217
218 pub fn with_available_at_ms(mut self, available_at_ms: u64) -> Self {
219 self.available_at_ms = available_at_ms;
220 self
221 }
222
223 pub fn with_merge_key(mut self, merge_key: MergeKey) -> Self {
224 self.merge_key = merge_key;
225 self
226 }
227
228 pub fn work_class(&self) -> Option<QueuedWorkClass> {
229 work_class_for_payloads(self.payloads.iter())
230 }
231}
232
233fn work_class_for_payloads<'a>(
234 payloads: impl IntoIterator<Item = &'a QueuedWorkPayload>,
235) -> Option<QueuedWorkClass> {
236 let mut payloads = payloads.into_iter();
237 let first = payloads.next()?.work_class();
238 payloads
239 .all(|payload| payload.work_class() == first)
240 .then_some(first)
241}
242
243#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
244#[serde(rename_all = "snake_case")]
245pub enum QueuedWorkClaimBoundary {
246 ActiveTurnCheckpoint,
247 Idle,
248}
249
250#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
251pub struct QueuedWorkCompletion {
252 pub session_id: String,
253 pub claim_id: String,
254 pub lease_token: String,
255 pub batch_ids: Vec<String>,
256}
257
258#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
259pub struct QueuedWorkClaim {
260 pub session_id: String,
261 pub claim_id: String,
262 pub owner: crate::LeaseOwnerIdentity,
263 pub lease_token: String,
264 pub fencing_token: u64,
265 pub session_lease_generation: u64,
269 pub batches: Vec<QueuedWorkBatch>,
270}
271
272impl QueuedWorkClaim {
273 pub fn completion(&self) -> QueuedWorkCompletion {
274 QueuedWorkCompletion {
275 session_id: self.session_id.clone(),
276 claim_id: self.claim_id.clone(),
277 lease_token: self.lease_token.clone(),
278 batch_ids: self
279 .batches
280 .iter()
281 .map(|batch| batch.batch_id.clone())
282 .collect(),
283 }
284 }
285
286 pub fn is_empty(&self) -> bool {
287 self.batches.iter().all(|batch| batch.items.is_empty())
288 }
289
290 pub fn materialize_for_checkpoint(&self) -> QueuedCheckpointWork {
291 let messages = Vec::new();
292 let transient_messages = Vec::new();
293 let mut turn_causes = Vec::new();
294 for batch in &self.batches {
295 for item in &batch.items {
296 match &item.payload {
297 QueuedWorkPayload::ProcessWake { wake } => {
298 turn_causes.push(crate::process_wake_turn_cause(wake));
299 }
300 QueuedWorkPayload::AgentFrameTask { .. } => {}
301 QueuedWorkPayload::SessionCommand { .. } => {}
302 }
303 }
304 }
305 QueuedCheckpointWork {
306 messages,
307 transient_messages,
308 turn_causes,
309 }
310 }
311
312 pub async fn materialize_for_checkpoint_with_attachments(
313 &self,
314 _attachment_store: &crate::SessionAttachmentStore,
315 ) -> Result<QueuedCheckpointWork, String> {
316 let messages = Vec::new();
317 let transient_messages = Vec::new();
318 let mut turn_causes = Vec::new();
319 for batch in &self.batches {
320 for item in &batch.items {
321 match &item.payload {
322 QueuedWorkPayload::ProcessWake { wake } => {
323 turn_causes.push(crate::process_wake_turn_cause(wake));
324 }
325 QueuedWorkPayload::AgentFrameTask { .. } => {}
326 QueuedWorkPayload::SessionCommand { .. } => {}
327 }
328 }
329 }
330 Ok(QueuedCheckpointWork {
331 messages,
332 transient_messages,
333 turn_causes,
334 })
335 }
336
337 pub fn exclusive_session_command(&self) -> Option<(&QueuedWorkBatch, &SessionCommand)> {
338 if self.batches.len() != 1 {
339 return None;
340 }
341 let batch = self.batches.first()?;
342 if batch.slot_policy != SlotPolicy::Exclusive || batch.items.len() != 1 {
343 return None;
344 }
345 let item = batch.items.first()?;
346 match &item.payload {
347 QueuedWorkPayload::SessionCommand { command } => Some((batch, command.as_ref())),
348 _ => None,
349 }
350 }
351
352 pub fn materialize_for_turn(&self) -> QueuedTurnWork {
353 let checkpoint = self.materialize_for_checkpoint();
354 let mut input = TurnInput::empty();
355 for batch in &self.batches {
356 for item in &batch.items {
357 if let QueuedWorkPayload::AgentFrameTask {
358 task,
359 protocol_turn_options,
360 ..
361 } = &item.payload
362 {
363 input = TurnInput::text(task.clone());
364 input.protocol_turn_options = protocol_turn_options.clone();
365 }
366 }
367 }
368 QueuedTurnWork {
369 input,
370 messages: checkpoint.messages,
371 turn_causes: checkpoint.turn_causes,
372 }
373 }
374}
375
376#[derive(Clone, Debug, Default)]
377pub struct QueuedCheckpointWork {
378 pub messages: Vec<PluginMessage>,
379 pub transient_messages: Vec<PluginMessage>,
380 pub turn_causes: Vec<TurnCause>,
381}
382
383#[derive(Clone, Debug)]
384pub struct QueuedTurnWork {
385 pub input: TurnInput,
386 pub messages: Vec<PluginMessage>,
387 pub turn_causes: Vec<TurnCause>,
388}
389
390pub fn process_wake_batch_draft(wake: ProcessWakeDelivery) -> QueuedWorkBatchDraft {
391 let source_key = format!("process:{}:event:{}:wake", wake.process_id, wake.sequence);
392 QueuedWorkBatchDraft::new(
393 wake.target_session_id.clone(),
394 DeliveryPolicy::EarliestSafeBoundary,
395 SlotPolicy::Exclusive,
396 vec![QueuedWorkPayload::process_wake(wake)],
397 )
398 .with_source_key(source_key)
399}