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