Skip to main content

ai_crew_sync/
model.rs

1//! Wire types returned by the MCP tools.
2//!
3//! Timestamps are RFC 3339 strings rather than typed datetimes: the consumer is
4//! a language model, and a plain string is both unambiguous and free of extra
5//! schema dependencies.
6
7use schemars::JsonSchema;
8use serde::Serialize;
9
10/// `serde_json::Value` fields would produce a boolean `true` schema, which
11/// some MCP clients' validators reject; an empty object schema means the same
12/// ("anything") and passes everywhere.
13pub fn any_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
14    schemars::json_schema!({})
15}
16
17pub fn ts(dt: chrono::DateTime<chrono::Utc>) -> String {
18    dt.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)
19}
20
21pub fn ts_opt(dt: Option<chrono::DateTime<chrono::Utc>>) -> Option<String> {
22    dt.map(ts)
23}
24
25#[derive(Debug, Serialize, JsonSchema)]
26pub struct WhoAmI {
27    /// Your agent handle. Other agents address you by this name.
28    pub agent: String,
29    pub agent_id: String,
30    pub team: String,
31    pub team_id: String,
32    /// Which of your concurrent working contexts this connection is, taken
33    /// from the `X-Crew-Session` header — usually the repository you are in.
34    /// `null` means the shared session: you sent no header, and your presence,
35    /// task claims and locks are not separated from your other sessions.
36    pub session: Option<String>,
37    /// Channel this session posts to when `post_message` is called with
38    /// neither `channel` nor `to` — the one named after your session, if the
39    /// team has one. `null` means there is none, so you must always say where
40    /// a message goes.
41    pub default_channel: Option<String>,
42    /// Number of unread direct messages waiting for you.
43    pub unread_direct_messages: i64,
44    /// Tasks currently claimed by you and not yet completed.
45    pub open_claimed_tasks: i64,
46}
47
48// ------------------------------------------------------------------ agents --
49
50#[derive(Debug, Serialize, JsonSchema)]
51pub struct AgentInfo {
52    pub name: String,
53    pub display_name: Option<String>,
54    /// Which working context the fields below describe — usually a repository
55    /// name. Absent for the shared session, used by clients that send no
56    /// `X-Crew-Session` header, so a roster of teammates who use no sessions
57    /// serialises exactly as it did before sessions existed.
58    ///
59    /// Which row the summary describes, in order: a **live** session before a
60    /// dead one, a **named** session before the shared one, then the most
61    /// recently updated. Live comes first deliberately — a named session that
62    /// died days ago should not outrank a shared row that is active now — so
63    /// the shared row can win while every named session is offline. Read
64    /// `sessions` when you need all of them; this is one of several.
65    #[serde(skip_serializing_if = "Option::is_none", default)]
66    pub session: Option<String>,
67    /// One of `active`, `idle`, `offline`. `offline` means the presence lease
68    /// expired, i.e. the agent has not sent a heartbeat recently.
69    pub status: String,
70    pub repo: Option<String>,
71    pub branch: Option<String>,
72    /// Free-text description of what this agent is currently doing.
73    pub activity: Option<String>,
74    pub last_seen: Option<String>,
75    /// True when *any* of this agent's sessions has a live presence lease.
76    pub online: bool,
77    /// Every working context this agent has open, most recently active first.
78    /// Absent when there is only one — the fields above already describe it.
79    /// A teammate with several entries here is working in several repositories
80    /// at once, and each one claims tasks and holds locks independently.
81    #[serde(skip_serializing_if = "Vec::is_empty", default)]
82    pub sessions: Vec<AgentSession>,
83}
84
85/// One working context of an agent: what that session is doing right now.
86#[derive(Debug, Serialize, JsonSchema)]
87pub struct AgentSession {
88    /// Absent for the shared session.
89    #[serde(skip_serializing_if = "Option::is_none", default)]
90    pub session: Option<String>,
91    pub status: String,
92    pub repo: Option<String>,
93    pub branch: Option<String>,
94    pub activity: Option<String>,
95    pub last_seen: Option<String>,
96    pub online: bool,
97}
98
99#[derive(Debug, Serialize, JsonSchema)]
100pub struct AgentList {
101    pub agents: Vec<AgentInfo>,
102    /// Agents with at least one live session — people, not sessions.
103    pub online_count: usize,
104}
105
106// -------------------------------------------------------------- messaging --
107
108#[derive(Debug, Serialize, JsonSchema)]
109pub struct ChannelInfo {
110    pub name: String,
111    pub topic: Option<String>,
112    pub message_count: i64,
113    pub created_at: String,
114}
115
116#[derive(Debug, Serialize, JsonSchema)]
117pub struct ChannelList {
118    pub channels: Vec<ChannelInfo>,
119}
120
121#[derive(Debug, Serialize, JsonSchema)]
122pub struct MessageInfo {
123    pub id: i64,
124    pub from: String,
125    /// Which of the sender's working contexts wrote this; `null` is their
126    /// shared session. Reply to `from/from_session` to reach the window that
127    /// is waiting, rather than whichever one notices first.
128    pub from_session: Option<String>,
129    /// True when this was posted as an announcement: something the sender
130    /// judged worth interrupting the whole team for, so it reaches every
131    /// session regardless of which channel they are focused on.
132    pub announce: bool,
133    /// Channel name for channel messages; `null` for direct messages.
134    pub channel: Option<String>,
135    /// Recipient handle for direct messages; `null` for channel messages.
136    pub to: Option<String>,
137    /// Set when this direct message was addressed to one working context of
138    /// the recipient rather than to the person. `null` means every session of
139    /// theirs sees it.
140    pub to_session: Option<String>,
141    pub body: String,
142    pub reply_to: Option<i64>,
143    #[schemars(schema_with = "any_json_schema")]
144    pub metadata: serde_json::Value,
145    /// Files attached to this message; fetch content with get_attachment.
146    pub attachments: Vec<AttachmentMeta>,
147    pub created_at: String,
148}
149
150#[derive(Debug, Serialize, serde::Deserialize, JsonSchema)]
151pub struct AttachmentMeta {
152    /// Pass this id to get_attachment to download the content.
153    pub id: i64,
154    pub filename: String,
155    pub content_type: String,
156    pub size_bytes: i64,
157}
158
159#[derive(Debug, Serialize, JsonSchema)]
160pub struct AttachmentContent {
161    pub id: i64,
162    pub filename: String,
163    pub content_type: String,
164    pub size_bytes: i64,
165    pub uploaded_by: String,
166    pub created_at: String,
167    /// The file content, base64-encoded.
168    pub data_base64: String,
169}
170
171#[derive(Debug, Serialize, JsonSchema)]
172pub struct PostMessageResult {
173    pub message: MessageInfo,
174    /// Handles that can now see this message.
175    pub delivered_to: Vec<String>,
176}
177
178#[derive(Debug, Serialize, JsonSchema)]
179pub struct MessageList {
180    pub messages: Vec<MessageInfo>,
181    /// The scope that was actually read, after normalisation.
182    pub scope: String,
183    /// Read cursor position after this call. Messages at or below this id will
184    /// not be returned again when `only_new` is true.
185    pub cursor: i64,
186    /// True when the result hit `limit` and older/newer messages remain.
187    pub truncated: bool,
188}
189
190// ------------------------------------------------------------------ tasks --
191
192#[derive(Debug, Serialize, JsonSchema)]
193pub struct TaskInfo {
194    pub key: String,
195    pub title: String,
196    pub description: Option<String>,
197    /// One of `open`, `claimed`, `done`, `cancelled`.
198    pub status: String,
199    /// Keys of tasks this one depends on.
200    pub depends_on: Vec<String>,
201    /// True while any dependency is not yet done/cancelled. Blocked tasks
202    /// cannot be claimed.
203    pub blocked: bool,
204    pub claimed_by: Option<String>,
205    /// Which of `claimed_by`'s working contexts holds the claim; `null` is
206    /// their shared session. A claim belongs to a session, not to a person —
207    /// your own other session cannot renew, release or steal this one.
208    pub claimed_session: Option<String>,
209    pub claimed_at: Option<String>,
210    /// When the current claim expires. After this instant another agent may
211    /// steal the task, so renew the lease if you are still working on it.
212    pub lease_expires_at: Option<String>,
213    /// Seconds left on the claim, so you can decide whether waiting is
214    /// reasonable without doing the arithmetic. Zero means it has lapsed.
215    pub lease_seconds_remaining: Option<i64>,
216    /// True when the claim has already lapsed.
217    pub lease_expired: bool,
218    pub result: Option<String>,
219    #[schemars(schema_with = "any_json_schema")]
220    pub metadata: serde_json::Value,
221    /// Files attached to this task; fetch content with get_attachment.
222    pub attachments: Vec<AttachmentMeta>,
223    pub created_by: Option<String>,
224    pub created_at: String,
225    pub updated_at: String,
226}
227
228#[derive(Debug, Serialize, JsonSchema)]
229pub struct TaskList {
230    pub tasks: Vec<TaskInfo>,
231    pub open: i64,
232    pub claimed: i64,
233}
234
235#[derive(Debug, Serialize, JsonSchema)]
236pub struct ClaimResult {
237    pub claimed: bool,
238    pub task: Option<TaskInfo>,
239    /// Present when `claimed` is false: why the claim did not succeed.
240    pub reason: Option<String>,
241}
242
243#[derive(Debug, Serialize, JsonSchema)]
244pub struct TaskEventInfo {
245    pub event: String,
246    pub agent: Option<String>,
247    pub detail: Option<String>,
248    pub created_at: String,
249}
250
251#[derive(Debug, Serialize, JsonSchema)]
252pub struct TaskDetail {
253    pub task: TaskInfo,
254    pub history: Vec<TaskEventInfo>,
255}
256
257// ------------------------------------------------------------------ notes --
258
259#[derive(Debug, Serialize, JsonSchema)]
260pub struct NoteInfo {
261    pub scope: String,
262    pub key: String,
263    pub value: String,
264    pub tags: Vec<String>,
265    pub updated_by: Option<String>,
266    pub updated_at: String,
267}
268
269#[derive(Debug, Serialize, JsonSchema)]
270pub struct NoteList {
271    pub notes: Vec<NoteInfo>,
272}
273
274#[derive(Debug, Serialize, JsonSchema)]
275pub struct NoteRef {
276    pub scope: String,
277    pub key: String,
278    pub found: bool,
279    pub note: Option<NoteInfo>,
280}
281
282#[derive(Debug, Serialize, JsonSchema)]
283pub struct Ack {
284    pub ok: bool,
285    pub detail: String,
286}
287
288// ------------------------------------------------------------------ locks --
289
290#[derive(Debug, Serialize, JsonSchema)]
291pub struct LockInfo {
292    pub name: String,
293    pub holder: String,
294    /// Which of the holder's working contexts took it; `null` is their shared
295    /// session. A lock belongs to a session — your own other session cannot
296    /// release it or take it over while it is live.
297    pub holder_session: Option<String>,
298    pub purpose: Option<String>,
299    pub acquired_at: String,
300    /// When the lock lapses on its own if not renewed.
301    pub expires_at: String,
302}
303
304#[derive(Debug, Serialize, JsonSchema)]
305pub struct LockList {
306    pub locks: Vec<LockInfo>,
307}
308
309#[derive(Debug, Serialize, JsonSchema)]
310pub struct LockResult {
311    pub acquired: bool,
312    pub lock: Option<LockInfo>,
313    /// Present when `acquired` is false: who holds it and until when.
314    pub reason: Option<String>,
315}
316
317// ----------------------------------------------------------------- events --
318
319#[derive(Debug, Serialize, JsonSchema)]
320pub struct WaitEvent {
321    /// One of `message`, `task`, `lock`, `note`.
322    pub kind: String,
323    /// Human-readable one-liner of what happened.
324    pub summary: String,
325}
326
327#[derive(Debug, Serialize, JsonSchema)]
328pub struct WaitResult {
329    /// True when something happened; false when the timeout elapsed quietly.
330    pub woke: bool,
331    pub timed_out: bool,
332    pub events: Vec<WaitEvent>,
333    /// Unread direct messages after the wait — if > 0, call read_messages.
334    pub unread_direct_messages: i64,
335    /// What to do next, e.g. which tool to call to fetch the details.
336    pub suggestion: String,
337}
338
339#[derive(Debug, Serialize, JsonSchema)]
340pub struct AskResult {
341    /// True when the teammate answered before the timeout.
342    pub answered: bool,
343    /// The agent the question was addressed to.
344    pub to: String,
345    /// Id of the question message. On timeout, pass it back as
346    /// `resume_message_id` to keep waiting without re-sending the question.
347    pub question_message_id: i64,
348    /// The answer: their reply to the question, or failing that their first
349    /// direct message to you after it.
350    pub answer: Option<MessageInfo>,
351    /// What to do next.
352    pub suggestion: String,
353}
354
355// ----------------------------------------------------------------- digest --
356
357#[derive(Debug, Serialize, JsonSchema)]
358pub struct DigestMessage {
359    pub from: String,
360    pub body: String,
361    pub at: String,
362}
363
364#[derive(Debug, Serialize, JsonSchema)]
365pub struct DigestChannel {
366    pub name: String,
367    pub message_count: i64,
368    pub last_messages: Vec<DigestMessage>,
369}
370
371#[derive(Debug, Serialize, JsonSchema)]
372pub struct DigestTask {
373    pub key: String,
374    pub title: String,
375    pub status: String,
376    pub claimed_by: Option<String>,
377    pub result: Option<String>,
378    pub updated_at: String,
379}
380
381#[derive(Debug, Serialize, JsonSchema)]
382pub struct DigestNote {
383    pub scope: String,
384    pub key: String,
385    pub updated_by: Option<String>,
386    pub updated_at: String,
387}
388
389#[derive(Debug, Serialize, JsonSchema)]
390pub struct DigestAgent {
391    pub name: String,
392    pub activity: Option<String>,
393    pub last_seen: Option<String>,
394    pub online: bool,
395}
396
397#[derive(Debug, Serialize, JsonSchema)]
398pub struct DigestResult {
399    /// Window covered, in hours.
400    pub hours: i64,
401    pub channels: Vec<DigestChannel>,
402    /// Tasks whose state changed inside the window, newest first.
403    pub tasks_moved: Vec<DigestTask>,
404    pub open_tasks: i64,
405    pub claimed_tasks: i64,
406    pub notes_updated: Vec<DigestNote>,
407    pub agents_seen: Vec<DigestAgent>,
408    pub active_locks: Vec<LockInfo>,
409}