Skip to main content

fxrs_core/
message.rs

1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3
4/// A provider-neutral conversation role.
5#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
6#[serde(rename_all = "lowercase")]
7pub enum Role {
8    System,
9    User,
10    Assistant,
11    Tool,
12}
13
14/// Integrity of the exact tool arguments received from a provider.
15#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
16#[serde(rename_all = "snake_case")]
17pub enum ToolArgumentIntegrity {
18    #[default]
19    Valid,
20    MalformedJson,
21}
22
23/// Origin of a tool result. Provider-executed calls are never dispatched again
24/// by the local runtime.
25#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
26#[serde(rename_all = "snake_case")]
27pub enum ToolExecutionProvenance {
28    #[default]
29    FxLocal,
30    Provider,
31}
32
33/// Stable tool-call representation shared by gateways, sessions, and tools.
34///
35/// `arguments_json` retains the provider bytes for diagnostics and durable
36/// replay. Callers should use [`ToolCall::arguments`] when they need a parsed
37/// value.
38#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
39pub struct ToolCall {
40    pub id: String,
41    pub name: String,
42    pub arguments_json: String,
43    #[serde(default)]
44    pub argument_integrity: ToolArgumentIntegrity,
45    #[serde(default, skip_serializing_if = "Option::is_none")]
46    pub provisional_id: Option<String>,
47    #[serde(default, skip_serializing_if = "Option::is_none")]
48    pub provider_result: Option<String>,
49    #[serde(default)]
50    pub provenance: ToolExecutionProvenance,
51}
52
53impl ToolCall {
54    pub fn arguments(&self) -> Result<Value, serde_json::Error> {
55        serde_json::from_str(&self.arguments_json)
56    }
57}
58
59#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
60#[serde(rename_all = "snake_case")]
61pub enum CachePolicy {
62    #[default]
63    Default,
64    NoCache,
65}
66
67/// Provider-neutral message. Optional fields are role-dependent but remain in
68/// one type so gateways can project them without lossy intermediate formats.
69#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
70pub struct ChatMessage {
71    pub role: Role,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub content: Option<String>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub tool_call_id: Option<String>,
76    #[serde(default, skip_serializing_if = "Option::is_none")]
77    pub tool_name: Option<String>,
78    #[serde(default, skip_serializing_if = "Vec::is_empty")]
79    pub tool_calls: Vec<ToolCall>,
80    #[serde(default)]
81    pub permission_feedback: bool,
82    #[serde(default)]
83    pub cache_policy: CachePolicy,
84}
85
86impl ChatMessage {
87    pub fn text(role: Role, content: impl Into<String>) -> Self {
88        Self {
89            role,
90            content: Some(content.into()),
91            tool_call_id: None,
92            tool_name: None,
93            tool_calls: Vec::new(),
94            permission_feedback: false,
95            cache_policy: CachePolicy::Default,
96        }
97    }
98}
99
100#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
101#[serde(rename_all = "kebab-case")]
102pub enum FinishReason {
103    Stop,
104    Length,
105    ContentFilter,
106    ToolCalls,
107    Error,
108    Other,
109}
110
111#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
112pub struct Usage {
113    #[serde(default, skip_serializing_if = "Option::is_none")]
114    pub input_tokens: Option<u64>,
115    #[serde(default, skip_serializing_if = "Option::is_none")]
116    pub output_tokens: Option<u64>,
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn tool_call_preserves_raw_json_and_parses_on_demand() {
125        let call = ToolCall {
126            id: "call_1".into(),
127            name: "read_file".into(),
128            arguments_json: r#"{"path":"README.md"}"#.into(),
129            argument_integrity: ToolArgumentIntegrity::Valid,
130            provisional_id: None,
131            provider_result: None,
132            provenance: ToolExecutionProvenance::FxLocal,
133        };
134
135        assert_eq!(call.arguments().unwrap()["path"], "README.md");
136        assert_eq!(call.arguments_json, r#"{"path":"README.md"}"#);
137    }
138}