1use serde::{Deserialize, Serialize};
5
6pub const MAX_TEXT_BYTES: usize = 2_000;
9pub const MAX_TARGET_BYTES: usize = 512;
11pub const MAX_LABEL_BYTES: usize = 128;
13pub const MAX_ROOT_BYTES: usize = 1_024;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
18#[serde(rename_all = "snake_case")]
19pub enum Kind {
20 UserMessage,
22 AgentMessage,
24 ToolCall,
26 FileRead,
28 FileWrite,
30 Shell,
32 Permission,
34 TurnStart,
36 TurnEnd,
38 Error,
40}
41
42impl Kind {
43 pub const ALL: [Kind; 10] = [
44 Kind::UserMessage,
45 Kind::AgentMessage,
46 Kind::ToolCall,
47 Kind::FileRead,
48 Kind::FileWrite,
49 Kind::Shell,
50 Kind::Permission,
51 Kind::TurnStart,
52 Kind::TurnEnd,
53 Kind::Error,
54 ];
55
56 pub fn as_str(self) -> &'static str {
57 match self {
58 Kind::UserMessage => "user_message",
59 Kind::AgentMessage => "agent_message",
60 Kind::ToolCall => "tool_call",
61 Kind::FileRead => "file_read",
62 Kind::FileWrite => "file_write",
63 Kind::Shell => "shell",
64 Kind::Permission => "permission",
65 Kind::TurnStart => "turn_start",
66 Kind::TurnEnd => "turn_end",
67 Kind::Error => "error",
68 }
69 }
70
71 pub fn parse(name: &str) -> Option<Kind> {
73 Kind::ALL.into_iter().find(|kind| kind.as_str() == name)
74 }
75}
76
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
79#[serde(rename_all = "snake_case")]
80pub enum Source {
81 Hook,
83 Mcp,
85 Acp,
87}
88
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
91#[serde(rename_all = "snake_case")]
92pub enum Outcome {
93 Ok,
94 Error,
95 Denied,
96 Cancelled,
97 Asked,
99 Allowed,
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105pub struct Draft {
106 pub session_id: String,
107 pub agent: String,
108 pub project_root: String,
109 pub kind: Kind,
110 pub source: Source,
111 pub tool: Option<String>,
112 pub target: Option<String>,
113 pub outcome: Option<Outcome>,
114 pub text: Option<String>,
115}
116
117impl Draft {
118 pub fn new(
119 session_id: impl Into<String>,
120 agent: impl Into<String>,
121 project_root: impl Into<String>,
122 kind: Kind,
123 source: Source,
124 ) -> Self {
125 Self {
126 session_id: session_id.into(),
127 agent: agent.into(),
128 project_root: project_root.into(),
129 kind,
130 source,
131 tool: None,
132 target: None,
133 outcome: None,
134 text: None,
135 }
136 }
137
138 pub fn tool(mut self, tool: impl Into<String>) -> Self {
139 self.tool = Some(tool.into());
140 self
141 }
142
143 pub fn target(mut self, target: impl Into<String>) -> Self {
144 self.target = Some(target.into());
145 self
146 }
147
148 pub fn outcome(mut self, outcome: Outcome) -> Self {
149 self.outcome = Some(outcome);
150 self
151 }
152
153 pub fn text(mut self, text: impl Into<String>) -> Self {
154 self.text = Some(text.into());
155 self
156 }
157}
158
159#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct Event {
163 pub seq: u64,
165 pub at: String,
167 pub session_id: String,
168 pub agent: String,
169 pub project_root: String,
170 pub kind: Kind,
171 pub source: Source,
172 pub tool: Option<String>,
173 pub target: Option<String>,
174 pub outcome: Option<Outcome>,
175 pub text: Option<String>,
176 pub truncated: bool,
178}
179
180pub(crate) fn bound(text: &str, max: usize) -> (String, bool) {
182 if text.len() <= max {
183 return (text.to_owned(), false);
184 }
185 let mut end = max;
186 while end > 0 && !text.is_char_boundary(end) {
187 end -= 1;
188 }
189 (format!("{}…", &text[..end]), true)
190}
191
192pub(crate) fn present(value: Option<String>) -> Option<String> {
194 value.filter(|text| !text.trim().is_empty())
195}