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