1pub mod message;
2pub mod prompt;
3
4pub use message::{
5 BaseRenderCache, Message, MessageRole, MessageSequence, Part, PartAttachment, PartKind,
6 PruneState, RenderedPrompt, append_rendered_prompt, messages_are_prompt_resume_safe,
7 render_prompt, render_transcript_prompt, shared_parts,
8};
9pub use prompt::{
10 MAIN_AGENT_INTRO, PromptBuiltin, PromptLayer, PromptSlot, PromptSlotLayer, PromptTemplate,
11 PromptTemplateEntry, PromptTemplateSection, ResolvedPromptLayer, default_prompt_template,
12 resolve_prompt_layers,
13};
14
15use std::sync::Arc;
16
17use crate::MessageOrigin;
18use crate::ToolDefinition;
19use crate::llm::types::LlmToolSpec;
20use crate::plugin::{CheckpointKind, PluginMessage, PluginRuntimeEvent};
21
22#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
24pub struct ProtocolEvent {
25 pub plugin_id: String,
26 pub payload: serde_json::Value,
27}
28
29impl ProtocolEvent {
30 pub fn typed<T>(plugin_id: impl Into<String>, event: T) -> Result<Self, serde_json::Error>
31 where
32 T: serde::Serialize,
33 {
34 Ok(Self {
35 plugin_id: plugin_id.into(),
36 payload: serde_json::to_value(event)?,
37 })
38 }
39
40 pub fn decode<T>(&self, expected_plugin_id: &str) -> Result<Option<T>, serde_json::Error>
41 where
42 T: for<'de> serde::Deserialize<'de>,
43 {
44 if self.plugin_id != expected_plugin_id {
45 return Ok(None);
46 }
47 serde_json::from_value(self.payload.clone()).map(Some)
48 }
49}
50
51#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
53#[serde(tag = "kind", rename_all = "snake_case")]
54#[allow(clippy::large_enum_variant)]
56pub enum SessionAppendNode {
57 Message {
58 message: PluginMessage,
59 #[serde(default, skip_serializing_if = "Option::is_none")]
60 caused_by: Option<crate::CausalRef>,
61 },
62 ProtocolEvent {
63 event: ProtocolEvent,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 caused_by: Option<crate::CausalRef>,
66 },
67 Plugin {
68 plugin_type: String,
69 #[serde(default)]
70 body: serde_json::Value,
71 #[serde(default, skip_serializing_if = "Option::is_none")]
72 caused_by: Option<crate::CausalRef>,
73 },
74}
75
76impl SessionAppendNode {
77 pub fn message(message: PluginMessage) -> Self {
78 Self::Message {
79 message,
80 caused_by: None,
81 }
82 }
83
84 pub fn plugin(plugin_type: impl Into<String>, body: serde_json::Value) -> Self {
85 Self::Plugin {
86 plugin_type: plugin_type.into(),
87 body,
88 caused_by: None,
89 }
90 }
91
92 pub fn protocol_event(event: ProtocolEvent) -> Self {
93 Self::ProtocolEvent {
94 event,
95 caused_by: None,
96 }
97 }
98
99 pub fn with_caused_by(mut self, caused_by: crate::CausalRef) -> Self {
100 match &mut self {
101 Self::Message {
102 caused_by: cause, ..
103 }
104 | Self::ProtocolEvent {
105 caused_by: cause, ..
106 }
107 | Self::Plugin {
108 caused_by: cause, ..
109 } => *cause = Some(caused_by),
110 }
111 self
112 }
113}
114
115#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
119#[allow(clippy::large_enum_variant)]
121pub enum SessionHistoryRecord<PE = ()> {
122 Conversation(ConversationRecord),
123 Protocol(PE),
124}
125
126#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
127pub struct ConversationRecord {
128 pub id: String,
129 pub role: MessageRole,
130 pub parts: Arc<Vec<Part>>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub origin: Option<MessageOrigin>,
133}
134
135#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
136pub struct AcceptedInjectedTurnInput {
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub id: Option<String>,
139 pub message: PluginMessage,
140}
141
142impl ConversationRecord {
143 pub fn from_message(message: Message) -> Self {
144 Self {
145 id: message.id,
146 role: message.role,
147 parts: message.parts,
148 origin: message.origin,
149 }
150 }
151
152 pub fn to_message(&self) -> Message {
153 Message {
154 id: self.id.clone(),
155 role: self.role,
156 parts: Arc::clone(&self.parts),
157 origin: self.origin.clone(),
158 }
159 }
160}
161
162#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
164pub struct TokenUsage {
165 pub input_tokens: i64,
166 pub output_tokens: i64,
167 pub cache_read_input_tokens: i64,
168 pub cache_write_input_tokens: i64,
169 pub reasoning_output_tokens: i64,
170}
171
172impl TokenUsage {
173 pub fn total(&self) -> i64 {
174 self.input_tokens
175 + self.output_tokens
176 + self.cache_read_input_tokens
177 + self.cache_write_input_tokens
178 }
179
180 pub fn input_total(&self) -> i64 {
181 self.input_tokens + self.cache_read_input_tokens + self.cache_write_input_tokens
182 }
183
184 pub fn add(&mut self, other: &TokenUsage) {
185 self.input_tokens += other.input_tokens;
186 self.output_tokens += other.output_tokens;
187 self.cache_read_input_tokens += other.cache_read_input_tokens;
188 self.cache_write_input_tokens += other.cache_write_input_tokens;
189 self.reasoning_output_tokens += other.reasoning_output_tokens;
190 }
191}
192
193#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
207pub struct ErrorEnvelope {
208 pub kind: String,
209 #[serde(default, skip_serializing_if = "Option::is_none")]
210 pub code: Option<String>,
211 #[serde(default, skip_serializing_if = "Option::is_none")]
212 pub terminal_reason: Option<crate::llm::types::LlmTerminalReason>,
213 pub user_message: String,
214 #[serde(default, skip_serializing_if = "Option::is_none")]
215 pub raw: Option<String>,
216 #[serde(default, skip_serializing_if = "Option::is_none")]
220 pub retryable: Option<bool>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub provider_failure_kind: Option<crate::llm::types::ProviderFailureKind>,
226}
227
228#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
229#[serde(tag = "type")]
230#[allow(clippy::large_enum_variant)]
232pub enum SessionStreamEvent {
233 #[serde(rename = "text_delta")]
234 TextDelta { content: String },
235 #[serde(rename = "reasoning_delta")]
240 ReasoningDelta { content: String },
241 #[serde(rename = "tool_call")]
242 ToolCall {
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 call_id: Option<String>,
245 name: String,
246 args: serde_json::Value,
247 output: crate::ToolCallOutput,
248 duration_ms: u64,
249 },
250 #[serde(rename = "tool_call_start")]
251 ToolCallStart {
252 #[serde(default, skip_serializing_if = "Option::is_none")]
253 call_id: Option<String>,
254 name: String,
255 args: serde_json::Value,
256 },
257 #[serde(rename = "message")]
258 Message { text: String, kind: String },
259 #[serde(rename = "llm_request")]
260 LlmRequest {
261 protocol_iteration: usize,
262 message_count: usize,
263 tool_list: String,
264 },
265 #[serde(rename = "llm_response")]
266 LlmResponse {
267 protocol_iteration: usize,
268 content: String,
269 duration_ms: u64,
270 },
271 #[serde(rename = "token_usage")]
272 TokenUsage {
273 protocol_iteration: usize,
274 usage: TokenUsage,
275 cumulative: TokenUsage,
276 },
277 #[serde(rename = "child_token_usage")]
278 ChildTokenUsage {
279 session_id: String,
280 source: String,
281 model: String,
282 protocol_iteration: usize,
283 usage: TokenUsage,
284 cumulative: TokenUsage,
285 },
286 #[serde(rename = "retry_status")]
287 RetryStatus {
288 wait_seconds: u64,
289 attempt: usize,
290 max_attempts: usize,
291 reason: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
293 envelope: Option<ErrorEnvelope>,
294 },
295 #[serde(rename = "injected_turn_input_accepted")]
296 InjectedTurnInputAccepted {
297 inputs: Vec<AcceptedInjectedTurnInput>,
298 checkpoint: CheckpointKind,
299 },
300 #[serde(rename = "injected_messages_committed")]
301 InjectedMessagesCommitted {
302 messages: Vec<PluginMessage>,
303 checkpoint: CheckpointKind,
304 },
305 #[serde(rename = "plugin_event")]
306 PluginEvent {
307 plugin_id: String,
308 event: PluginRuntimeEvent,
309 },
310 #[serde(rename = "turn_outcome")]
313 TurnOutcome { outcome: TurnOutcome },
314 #[serde(rename = "done")]
315 Done,
316 #[serde(rename = "error")]
317 Error {
318 message: String,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 envelope: Option<ErrorEnvelope>,
321 },
322}
323
324#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
325#[serde(rename_all = "snake_case")]
326pub enum TurnOutcome {
327 Finished(TurnFinish),
328 AgentFrameSwitch {
329 frame_id: String,
330 task: String,
331 #[serde(default, skip_serializing_if = "Vec::is_empty")]
332 initial_nodes: Vec<SessionAppendNode>,
333 },
334 Stopped(TurnStop),
335}
336
337#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
338#[serde(rename_all = "snake_case")]
339pub enum TurnFinish {
340 AssistantMessage {
341 text: String,
342 },
343 FinalValue {
344 value: serde_json::Value,
345 },
346 ToolValue {
347 tool_name: String,
348 value: serde_json::Value,
349 },
350}
351
352#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
353#[serde(rename_all = "snake_case")]
354pub enum TurnStop {
355 Cancelled,
356 Incomplete,
357 InvalidInput,
358 MaxTurns,
359 ToolFailure,
360 ProviderError,
361 PluginAbort,
362 RuntimeError,
363 SubmittedError {
364 value: serde_json::Value,
365 },
366 ToolError {
367 tool_name: String,
368 value: serde_json::Value,
369 },
370}
371
372#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
373pub struct TurnTerminationPolicyState {
374 turn_limit_final_scheduled: bool,
375}
376
377impl Default for TurnTerminationPolicyState {
378 fn default() -> Self {
379 Self::new()
380 }
381}
382
383impl TurnTerminationPolicyState {
384 pub fn new() -> Self {
385 Self {
386 turn_limit_final_scheduled: false,
387 }
388 }
389
390 pub fn should_force_exit_after_grace_turn(&self) -> bool {
391 self.turn_limit_final_scheduled
392 }
393
394 pub fn turn_limit_final_to_schedule(
395 &self,
396 protocol_iteration: usize,
397 protocol_run_offset: usize,
398 max_turns: Option<usize>,
399 ) -> Option<usize> {
400 if self.turn_limit_final_scheduled {
401 return None;
402 }
403 let max = max_turns?;
404 if protocol_iteration < protocol_run_offset + max {
405 return None;
406 }
407 Some(max)
408 }
409
410 pub fn mark_turn_limit_final_scheduled(&mut self) {
411 self.turn_limit_final_scheduled = true;
412 }
413}
414
415pub fn make_error_envelope(
416 kind: &str,
417 code: Option<&str>,
418 terminal_reason: Option<crate::llm::types::LlmTerminalReason>,
419 user_message: impl Into<String>,
420 raw: Option<String>,
421) -> ErrorEnvelope {
422 let user_message = user_message.into();
423 ErrorEnvelope {
424 kind: kind.to_string(),
425 code: code.map(str::to_string),
426 terminal_reason,
427 user_message,
428 raw: raw.map(|s| truncate_raw_error(s.trim())),
429 retryable: None,
430 provider_failure_kind: None,
431 }
432}
433
434pub fn make_error_event(
435 kind: &str,
436 code: Option<&str>,
437 user_message: impl Into<String>,
438 raw: Option<String>,
439) -> SessionStreamEvent {
440 let user_message = user_message.into();
441 SessionStreamEvent::Error {
442 message: user_message.clone(),
443 envelope: Some(make_error_envelope(kind, code, None, user_message, raw)),
444 }
445}
446
447pub fn truncate_raw_error(s: &str) -> String {
448 const MAX_RAW: usize = 4000;
449 let raw_len = s.chars().count();
450 if raw_len <= MAX_RAW {
451 return s.to_string();
452 }
453 let keep = MAX_RAW / 2;
454 let head = s.chars().take(keep).collect::<String>();
455 let tail = s
456 .chars()
457 .rev()
458 .take(keep)
459 .collect::<Vec<_>>()
460 .into_iter()
461 .rev()
462 .collect::<String>();
463 let omitted = raw_len.saturating_sub(keep * 2);
464 format!("{head}\n\n... ({omitted} chars omitted) ...\n\n{tail}")
465}
466
467pub fn reassign_part_ids(message_id: &str, parts: &mut [Part]) {
468 for (idx, part) in parts.iter_mut().enumerate() {
469 part.id = format!("{message_id}.p{idx}");
470 }
471}
472
473pub fn model_tool_specs_iter<'a>(
474 tools: impl IntoIterator<Item = &'a ToolDefinition>,
475) -> Vec<LlmToolSpec> {
476 tools
477 .into_iter()
478 .map(|tool| {
479 let model_tool = tool.model_tool();
480 LlmToolSpec {
481 name: model_tool.name,
482 description: model_tool.description,
483 input_schema: model_tool.input_schema,
484 output_schema: model_tool.output_schema,
485 }
486 })
487 .collect()
488}
489
490pub fn model_tool_specs(tools: &[ToolDefinition]) -> Vec<LlmToolSpec> {
491 model_tool_specs_iter(tools.iter())
492}
493
494#[cfg(test)]
495mod tests {
496 use super::{ErrorEnvelope, SessionStreamEvent, TurnOutcome};
497 use crate::llm::types::{LlmTerminalReason, ProviderFailureKind};
498
499 #[test]
508 fn error_envelope_decodes_legacy_snapshot_without_retryability_fields() {
509 let legacy = r#"{
510 "kind":"llm_provider",
511 "code":"429",
512 "terminal_reason":"provider_error",
513 "user_message":"LLM error: rate limited",
514 "raw":"{\"error\":\"rate_limited\"}"
515 }"#;
516 let envelope: ErrorEnvelope = serde_json::from_str(legacy).expect("legacy envelope");
517 assert_eq!(envelope.kind, "llm_provider");
518 assert_eq!(envelope.retryable, None);
519 assert_eq!(envelope.provider_failure_kind, None);
520
521 let legacy_event = r#"{
524 "type":"error",
525 "message":"LLM error: rate limited",
526 "envelope":{"kind":"llm_provider","user_message":"LLM error: rate limited"}
527 }"#;
528 let event: SessionStreamEvent = serde_json::from_str(legacy_event).expect("legacy event");
529 match event {
530 SessionStreamEvent::Error { envelope, .. } => {
531 let envelope = envelope.expect("envelope");
532 assert_eq!(envelope.retryable, None);
533 assert_eq!(envelope.provider_failure_kind, None);
534 }
535 other => panic!("expected error event, got {other:?}"),
536 }
537 }
538
539 #[test]
540 fn error_envelope_roundtrips_retryability_fields() {
541 let envelope = ErrorEnvelope {
542 kind: "llm_provider".to_string(),
543 code: Some("429".to_string()),
544 terminal_reason: Some(LlmTerminalReason::ProviderError),
545 user_message: "LLM error: rate limited".to_string(),
546 raw: None,
547 retryable: Some(true),
548 provider_failure_kind: Some(ProviderFailureKind::Quota),
549 };
550 let json = serde_json::to_value(&envelope).expect("serialize envelope");
551 assert_eq!(json["retryable"], serde_json::json!(true));
552 assert_eq!(json["provider_failure_kind"], serde_json::json!("quota"));
553 let decoded: ErrorEnvelope = serde_json::from_value(json).expect("decode envelope");
554 assert_eq!(decoded.retryable, Some(true));
555 assert_eq!(
556 decoded.provider_failure_kind,
557 Some(ProviderFailureKind::Quota)
558 );
559 }
560
561 #[test]
562 fn error_envelope_omits_unset_retryability_fields_on_the_wire() {
563 let envelope = ErrorEnvelope {
564 kind: "plugin".to_string(),
565 code: Some("plugin_abort".to_string()),
566 terminal_reason: None,
567 user_message: "stopped".to_string(),
568 raw: None,
569 retryable: None,
570 provider_failure_kind: None,
571 };
572 let json = serde_json::to_value(&envelope).expect("serialize envelope");
573 let object = json.as_object().expect("object");
574 assert!(!object.contains_key("retryable"));
575 assert!(!object.contains_key("provider_failure_kind"));
576 }
577
578 #[test]
579 fn provider_failure_kind_decodes_unknown_future_codes() {
580 let decoded: ProviderFailureKind =
583 serde_json::from_value(serde_json::json!("some_future_kind")).expect("future kind");
584 assert_eq!(decoded, ProviderFailureKind::Unknown);
585 for kind in [
586 ProviderFailureKind::Transport,
587 ProviderFailureKind::Timeout,
588 ProviderFailureKind::Http,
589 ProviderFailureKind::Stream,
590 ProviderFailureKind::Auth,
591 ProviderFailureKind::Validation,
592 ProviderFailureKind::Quota,
593 ProviderFailureKind::Unsupported,
594 ProviderFailureKind::Unknown,
595 ] {
596 let json = serde_json::to_value(kind).expect("serialize kind");
597 assert_eq!(json, serde_json::json!(kind.code()));
598 let round: ProviderFailureKind = serde_json::from_value(json).expect("decode kind");
599 assert_eq!(round, kind);
600 }
601 }
602
603 #[test]
604 fn agent_frame_switch_decodes_legacy_event_without_initial_nodes() {
605 let legacy = r#"{
606 "type":"turn_outcome",
607 "outcome":{
608 "agent_frame_switch":{
609 "frame_id":"frame-2",
610 "task":"continue"
611 }
612 }
613 }"#;
614 let event: SessionStreamEvent =
615 serde_json::from_str(legacy).expect("legacy frame switch event");
616 match event {
617 SessionStreamEvent::TurnOutcome {
618 outcome:
619 TurnOutcome::AgentFrameSwitch {
620 frame_id,
621 task,
622 initial_nodes,
623 },
624 } => {
625 assert_eq!(frame_id, "frame-2");
626 assert_eq!(task, "continue");
627 assert!(initial_nodes.is_empty());
628 }
629 other => panic!("expected agent-frame switch event, got {other:?}"),
630 }
631 }
632}