1use chrono::{DateTime, Utc};
27use serde::{Deserialize, Serialize};
28
29pub const CHANNEL_SCHEMA_VERSION: u32 = 2;
33
34#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
44#[serde(rename_all = "kebab-case")]
45pub enum ChannelState {
46 Submitted,
47 Working,
48 InputRequired,
49 Completed,
50 Failed,
51 Canceled,
52 Rejected,
53 Stale,
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
61#[serde(tag = "kind", rename_all = "kebab-case")]
62pub enum ChannelActor {
63 Human { name: String },
64 Agent { id: String },
65 System,
66}
67
68impl ChannelActor {
69 pub fn local_human() -> Self {
71 let name = std::env::var("USER")
72 .or_else(|_| std::env::var("USERNAME"))
73 .ok()
74 .filter(|s| !s.is_empty())
75 .unwrap_or_else(|| "you".to_string());
76 ChannelActor::Human { name }
77 }
78}
79
80#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
82#[serde(rename_all = "lowercase")]
83pub enum ParticipantRole {
84 Owner,
85 Router,
86 Delegate,
87 Observer,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct Participant {
92 pub actor: ChannelActor,
93 pub role: ParticipantRole,
94 pub joined_at: DateTime<Utc>,
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99pub struct Goal {
100 #[serde(default)]
101 pub statement: String,
102 #[serde(default)]
103 pub acceptance_criteria: Vec<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Channel {
109 pub v: u32,
110 pub id: String,
111 pub title: String,
112 #[serde(default)]
113 pub goal: Goal,
114 pub state: ChannelState,
115 pub owner: ChannelActor,
116 #[serde(default)]
117 pub participants: Vec<Participant>,
118 pub created_at: DateTime<Utc>,
119 pub updated_at: DateTime<Utc>,
120}
121
122#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
123#[serde(rename_all = "kebab-case")]
124pub enum EventKind {
125 Message,
126 Delegation,
127 Handoff,
128 ToolCall,
129 ToolResult,
130 StateChange,
131 Artifact,
132 HitlRequest,
133 HitlResponse,
134 Note,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize)]
139pub struct ChannelEvent {
140 pub seq: u64,
141 pub ts: DateTime<Utc>,
142 pub actor: ChannelActor,
143 pub kind: EventKind,
144 #[serde(default)]
145 pub payload: serde_json::Value,
146 #[serde(default, skip_serializing_if = "Option::is_none")]
147 pub idempotency_key: Option<String>,
148 #[serde(default, skip_serializing_if = "Option::is_none")]
153 pub sig: Option<String>,
154 #[serde(default, skip_serializing_if = "Option::is_none")]
156 pub key_version: Option<u32>,
157}
158
159#[cfg(test)]
160mod tests {
161 use super::*;
162
163 #[test]
164 fn channel_state_serializes_kebab() {
165 let j = serde_json::to_string(&ChannelState::InputRequired).unwrap();
166 assert_eq!(j, "\"input-required\"");
167 }
168
169 #[test]
170 fn event_round_trips() {
171 let ev = ChannelEvent {
172 seq: 3,
173 ts: Utc::now(),
174 actor: ChannelActor::Agent { id: "qa".into() },
175 kind: EventKind::Message,
176 payload: serde_json::json!({ "text": "hello", "task_id": "t-1" }),
177 idempotency_key: None,
178 sig: None,
179 key_version: None,
180 };
181 let line = serde_json::to_string(&ev).unwrap();
182 let back: ChannelEvent = serde_json::from_str(&line).unwrap();
183 assert_eq!(back.seq, 3);
184 assert_eq!(back.actor, ChannelActor::Agent { id: "qa".into() });
185 assert_eq!(back.payload["text"], "hello");
186 }
187
188 #[test]
189 fn system_actor_round_trips() {
190 let a = ChannelActor::System;
191 let j = serde_json::to_string(&a).unwrap();
192 assert_eq!(j, "{\"kind\":\"system\"}");
193 let back: ChannelActor = serde_json::from_str(&j).unwrap();
194 assert_eq!(back, ChannelActor::System);
195 }
196
197 #[test]
198 fn event_omits_sig_fields_when_absent_and_reads_old_rows() {
199 let ev = ChannelEvent {
201 seq: 0,
202 ts: Utc::now(),
203 actor: ChannelActor::System,
204 kind: EventKind::Note,
205 payload: serde_json::json!({}),
206 idempotency_key: None,
207 sig: None,
208 key_version: None,
209 };
210 let line = serde_json::to_string(&ev).unwrap();
211 assert!(!line.contains("sig"), "sig must be omitted when None");
212 assert!(
213 !line.contains("key_version"),
214 "key_version must be omitted when None"
215 );
216
217 let old = r#"{"seq":0,"ts":"2026-06-16T00:00:00Z","actor":{"kind":"system"},"kind":"note","payload":{}}"#;
219 let back: ChannelEvent = serde_json::from_str(old).unwrap();
220 assert_eq!(back.sig, None);
221 assert_eq!(back.key_version, None);
222 }
223}