1use indexmap::IndexMap;
4use serde::{Deserialize, Deserializer, Serialize, Serializer};
5
6use crate::content::{AssistantContent, UserContent, UserContentField, UserTextField};
7use crate::json::{JsonObject, JsonValue};
8use crate::primitives::{StopReason, ThinkingLevel, TimestampMs, Usage};
9
10#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13pub struct ToolDecl {
14 pub name: String,
16 pub description: String,
18 #[serde(default)]
20 pub parameters: JsonValue,
21 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub constrained_sampling: Option<ConstrainedSampling>,
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
28#[serde(untagged)]
29pub enum ConstrainedSampling {
30 Disabled(FalseLiteral),
32 Config(ConstrainedSamplingConfig),
34}
35
36#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
38pub struct FalseLiteral;
39
40impl Serialize for FalseLiteral {
41 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
42 serializer.serialize_bool(false)
43 }
44}
45
46impl<'de> Deserialize<'de> for FalseLiteral {
47 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
48 if bool::deserialize(deserializer)? {
49 Err(serde::de::Error::custom("expected `false`"))
50 } else {
51 Ok(Self)
52 }
53 }
54}
55
56#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct ConstrainedSamplingConfig {
60 #[serde(rename = "type")]
62 pub kind: String,
63 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub strict: Option<StrictMode>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
68 pub variants: Option<IndexMap<String, String>>,
69}
70
71#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
73#[serde(rename_all = "lowercase")]
74pub enum StrictMode {
75 Prefer,
77 Require,
79 Off,
81}
82
83#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
85pub struct ToolRef {
86 pub name: String,
88}
89
90pub type Sections = IndexMap<String, Option<String>>;
92
93#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
95#[serde(rename_all = "camelCase")]
96pub struct SystemMessage {
97 #[serde(default)]
99 pub content: UserTextField,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub sections: Option<Sections>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub tools_added: Option<Vec<ToolDecl>>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub tools_removed: Option<Vec<ToolRef>>,
109 #[serde(default)]
111 pub timestamp: TimestampMs,
112}
113
114#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
116#[serde(rename_all = "camelCase")]
117pub struct UserMessage {
118 #[serde(default)]
120 pub content: UserContentField,
121 #[serde(default)]
123 pub timestamp: TimestampMs,
124}
125
126#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
128#[serde(rename_all = "camelCase")]
129pub struct AssistantMessage {
130 #[serde(default)]
132 pub content: Vec<AssistantContent>,
133 pub api: String,
135 pub provider: String,
137 pub model: String,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub response_model: Option<String>,
142 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub response_id: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub provider_thinking_level: Option<String>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
150 pub thinking_level: Option<ThinkingLevel>,
151 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub diagnostics: Option<Vec<AssistantDiagnostic>>,
154 #[serde(default)]
156 pub usage: Usage,
157 pub stop_reason: StopReason,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
161 pub deferred: Option<DeferredHandle>,
162 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub error_message: Option<String>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub raw_stop_reason: Option<String>,
168 #[serde(default, skip_serializing_if = "Option::is_none")]
170 pub end_turn: Option<bool>,
171 #[serde(default)]
173 pub timestamp: TimestampMs,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
176 pub duration_ms: Option<u64>,
177}
178
179impl AssistantMessage {
180 #[must_use]
183 pub fn empty(
184 api: impl Into<String>,
185 provider: impl Into<String>,
186 model: impl Into<String>,
187 timestamp: TimestampMs,
188 ) -> Self {
189 Self {
190 content: Vec::new(),
191 api: api.into(),
192 provider: provider.into(),
193 model: model.into(),
194 response_model: None,
195 response_id: None,
196 provider_thinking_level: None,
197 thinking_level: None,
198 diagnostics: None,
199 usage: Usage::default(),
200 stop_reason: StopReason::Pending,
201 deferred: None,
202 error_message: None,
203 raw_stop_reason: None,
204 end_turn: None,
205 timestamp,
206 duration_ms: None,
207 }
208 }
209}
210
211#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
213#[serde(rename_all = "camelCase")]
214pub struct AssistantDiagnostic {
215 #[serde(rename = "type")]
217 pub kind: String,
218 pub timestamp: TimestampMs,
220 #[serde(default, skip_serializing_if = "Option::is_none")]
222 pub error: Option<DiagnosticError>,
223 #[serde(default, skip_serializing_if = "Option::is_none")]
225 pub details: Option<JsonObject>,
226}
227
228#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
230#[serde(rename_all = "camelCase")]
231pub struct DiagnosticError {
232 #[serde(default, skip_serializing_if = "Option::is_none")]
234 pub name: Option<String>,
235 pub message: String,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
239 pub stack: Option<String>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub code: Option<JsonValue>,
243}
244
245#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
247#[serde(rename_all = "camelCase")]
248pub struct DeferredHandle {
249 pub provider: String,
251 pub model_id: String,
253 pub api: String,
255 pub id: String,
257 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub expires_at: Option<i64>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub poll_after_ms: Option<u64>,
263 #[serde(
265 default,
266 skip_serializing_if = "Option::is_none",
267 deserialize_with = "crate::json::keep_null"
268 )]
269 pub data: Option<JsonValue>,
270}
271
272#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
274#[serde(rename_all = "lowercase")]
275pub enum NestedStatus {
276 Ok,
278 Error,
280 Unfinished,
282}
283
284#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct NestedToolCallRecord {
288 pub id: String,
290 pub name: String,
292 #[serde(default, skip_serializing_if = "Option::is_none")]
294 pub arguments: Option<JsonObject>,
295 #[serde(default, skip_serializing_if = "Option::is_none")]
297 pub arguments_bytes: Option<u64>,
298 pub status: NestedStatus,
300 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub duration_ms: Option<u64>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
305 pub error: Option<String>,
306}
307
308#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
310pub struct NestedToolCalls {
311 pub calls: Vec<NestedToolCallRecord>,
313 pub complete: bool,
315}
316
317#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
319#[serde(rename_all = "camelCase")]
320pub struct ToolResultMessage {
321 pub tool_call_id: String,
323 pub tool_name: String,
325 #[serde(default)]
327 pub content: Vec<UserContent>,
328 #[serde(
330 default,
331 skip_serializing_if = "Option::is_none",
332 deserialize_with = "crate::json::keep_null"
333 )]
334 pub details: Option<JsonValue>,
335 #[serde(default, skip_serializing_if = "Option::is_none")]
337 pub usage: Option<Usage>,
338 #[serde(default, skip_serializing_if = "Option::is_none")]
340 pub nested_calls: Option<NestedToolCalls>,
341 pub is_error: bool,
343 #[serde(default)]
345 pub timestamp: TimestampMs,
346 #[serde(default, skip_serializing_if = "Option::is_none")]
348 pub duration_ms: Option<u64>,
349}
350
351#[allow(
353 clippy::large_enum_variant,
354 reason = "variants mirror the wire shapes; boxing would leak into every match across crates"
355)]
356#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
357#[serde(tag = "role")]
358pub enum Message {
359 #[serde(rename = "system")]
361 System(SystemMessage),
362 #[serde(rename = "user")]
364 User(UserMessage),
365 #[serde(rename = "assistant")]
367 Assistant(AssistantMessage),
368 #[serde(rename = "toolResult")]
370 ToolResult(ToolResultMessage),
371}
372
373impl Message {
374 #[must_use]
376 pub const fn timestamp(&self) -> TimestampMs {
377 match self {
378 Self::System(m) => m.timestamp,
379 Self::User(m) => m.timestamp,
380 Self::Assistant(m) => m.timestamp,
381 Self::ToolResult(m) => m.timestamp,
382 }
383 }
384
385 #[must_use]
387 pub const fn role(&self) -> &'static str {
388 match self {
389 Self::System(_) => "system",
390 Self::User(_) => "user",
391 Self::Assistant(_) => "assistant",
392 Self::ToolResult(_) => "toolResult",
393 }
394 }
395}
396
397#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
399#[serde(rename_all = "camelCase")]
400pub struct BashExecutionMessage {
401 pub command: String,
403 #[serde(default)]
405 pub output: String,
406 #[serde(default, skip_serializing_if = "Option::is_none")]
408 pub exit_code: Option<i32>,
409 #[serde(default)]
411 pub cancelled: bool,
412 #[serde(default)]
414 pub truncated: bool,
415 #[serde(default, skip_serializing_if = "Option::is_none")]
417 pub full_output_path: Option<String>,
418 #[serde(default)]
420 pub timestamp: TimestampMs,
421 #[serde(default, skip_serializing_if = "Option::is_none")]
423 pub exclude_from_context: Option<bool>,
424}
425
426#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
428#[serde(rename_all = "camelCase")]
429pub struct CustomMessage {
430 pub custom_type: String,
432 #[serde(default)]
434 pub content: UserContentField,
435 #[serde(default)]
437 pub display: bool,
438 #[serde(
440 default,
441 skip_serializing_if = "Option::is_none",
442 deserialize_with = "crate::json::keep_null"
443 )]
444 pub details: Option<JsonValue>,
445 #[serde(default)]
447 pub timestamp: TimestampMs,
448}
449
450#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
452#[serde(rename_all = "camelCase")]
453pub struct BranchSummaryMessage {
454 pub summary: String,
456 #[serde(default)]
458 pub from_id: Option<String>,
459 #[serde(default)]
461 pub timestamp: TimestampMs,
462}
463
464#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
466#[serde(rename_all = "camelCase")]
467pub struct CompactionSummaryMessage {
468 pub summary: String,
470 #[serde(default)]
472 pub tokens_before: u64,
473 #[serde(default)]
475 pub timestamp: TimestampMs,
476}
477
478#[allow(
480 clippy::large_enum_variant,
481 reason = "variants mirror the wire shapes; boxing would leak into every match across crates"
482)]
483#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
484#[serde(tag = "role")]
485pub enum AgentMessage {
486 #[serde(rename = "system")]
488 System(SystemMessage),
489 #[serde(rename = "user")]
491 User(UserMessage),
492 #[serde(rename = "assistant")]
494 Assistant(AssistantMessage),
495 #[serde(rename = "toolResult")]
497 ToolResult(ToolResultMessage),
498 #[serde(rename = "bashExecution")]
500 BashExecution(BashExecutionMessage),
501 #[serde(rename = "custom")]
503 Custom(CustomMessage),
504 #[serde(rename = "branchSummary")]
506 BranchSummary(BranchSummaryMessage),
507 #[serde(rename = "compactionSummary")]
509 CompactionSummary(CompactionSummaryMessage),
510}
511
512impl AgentMessage {
513 #[must_use]
515 pub fn as_llm(&self) -> Option<Message> {
516 match self {
517 Self::System(m) => Some(Message::System(m.clone())),
518 Self::User(m) => Some(Message::User(m.clone())),
519 Self::Assistant(m) => Some(Message::Assistant(m.clone())),
520 Self::ToolResult(m) => Some(Message::ToolResult(m.clone())),
521 _ => None,
522 }
523 }
524
525 #[must_use]
527 pub const fn role(&self) -> &'static str {
528 match self {
529 Self::System(_) => "system",
530 Self::User(_) => "user",
531 Self::Assistant(_) => "assistant",
532 Self::ToolResult(_) => "toolResult",
533 Self::BashExecution(_) => "bashExecution",
534 Self::Custom(_) => "custom",
535 Self::BranchSummary(_) => "branchSummary",
536 Self::CompactionSummary(_) => "compactionSummary",
537 }
538 }
539
540 #[must_use]
542 pub const fn timestamp(&self) -> TimestampMs {
543 match self {
544 Self::System(m) => m.timestamp,
545 Self::User(m) => m.timestamp,
546 Self::Assistant(m) => m.timestamp,
547 Self::ToolResult(m) => m.timestamp,
548 Self::BashExecution(m) => m.timestamp,
549 Self::Custom(m) => m.timestamp,
550 Self::BranchSummary(m) => m.timestamp,
551 Self::CompactionSummary(m) => m.timestamp,
552 }
553 }
554
555 #[must_use]
557 pub const fn as_system(&self) -> Option<&SystemMessage> {
558 match self {
559 Self::System(m) => Some(m),
560 _ => None,
561 }
562 }
563}
564
565impl From<Message> for AgentMessage {
566 fn from(message: Message) -> Self {
567 match message {
568 Message::System(m) => Self::System(m),
569 Message::User(m) => Self::User(m),
570 Message::Assistant(m) => Self::Assistant(m),
571 Message::ToolResult(m) => Self::ToolResult(m),
572 }
573 }
574}
575
576#[cfg(test)]
577mod tests {
578 use super::*;
579 use serde_json::json;
580
581 fn round_trip(raw: &JsonValue) -> Option<JsonValue> {
582 let message: AgentMessage = serde_json::from_value(raw.clone()).ok()?;
583 serde_json::to_value(message).ok()
584 }
585
586 #[test]
587 fn doc_examples_round_trip() {
588 for raw in [
589 json!({"role":"system","content":"","sections":{"preamble":"You are…","tools":"<tools>","cwd":"/p"},
590 "toolsAdded":[{"name":"read","description":"...","parameters":{}}],"timestamp":1_733_234_400_000_i64}),
591 json!({"role":"system","content":"","sections":{"skills":"<skills>","gone":null},"toolsRemoved":[{"name":"write"}],"timestamp":1}),
592 json!({"role":"user","content":"Hello","timestamp":1}),
593 json!({"role":"assistant","content":[{"type":"text","text":"Hi!"}],"api":"anthropic-messages","provider":"anthropic",
594 "model":"claude-sonnet-4-5","usage":{"input":1,"output":2,"cacheRead":0,"cacheWrite":0,"totalTokens":3,
595 "cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0}},"stopReason":"stop","timestamp":2}),
596 json!({"role":"toolResult","toolCallId":"call_123","toolName":"bash","content":[{"type":"text","text":"output"}],"isError":false,"timestamp":3}),
597 json!({"role":"bashExecution","command":"ls","output":"","exitCode":0,"cancelled":false,"truncated":false,"timestamp":4}),
598 json!({"role":"custom","customType":"x","content":"c","display":true,"details":{"a":1},"timestamp":5}),
599 json!({"role":"branchSummary","summary":"s","fromId":null,"timestamp":6}),
600 json!({"role":"compactionSummary","summary":"s","tokensBefore":10,"timestamp":7}),
601 ] {
602 assert_eq!(round_trip(&raw), Some(raw));
603 }
604 }
605
606 #[test]
607 fn constrained_sampling_false_and_config() {
608 let decl: ToolDecl = serde_json::from_value(
609 json!({"name":"a","description":"b","parameters":{},"constrainedSampling":false}),
610 )
611 .unwrap_or_else(|_| ToolDecl {
612 name: String::new(),
613 description: String::new(),
614 parameters: JsonValue::Null,
615 constrained_sampling: None,
616 });
617 assert_eq!(
618 decl.constrained_sampling,
619 Some(ConstrainedSampling::Disabled(FalseLiteral))
620 );
621 let raw = json!({"type":"json_schema","strict":"prefer"});
622 let config: Option<ConstrainedSampling> = serde_json::from_value(raw.clone()).ok();
623 assert!(matches!(config, Some(ConstrainedSampling::Config(_))));
624 assert_eq!(serde_json::to_value(config).ok(), Some(raw));
625 assert!(serde_json::from_value::<FalseLiteral>(json!(true)).is_err());
626 }
627}