use std::time::{SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
use super::ids::{AgentId, RoomId};
pub fn now_micros() -> i64 {
let dur = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default();
i64::try_from(dur.as_micros()).unwrap_or(i64::MAX)
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum RoomScope {
Remote(String),
PathPrefix(std::path::PathBuf),
Global,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Room {
pub room_id: RoomId,
pub scope: RoomScope,
pub title: String,
pub created_at: i64,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageMeta {
pub id: String,
pub room: RoomId,
pub from: AgentId,
pub ts_micros: i64,
pub subject: String,
pub tags: Vec<String>,
pub reply_to: Option<String>,
pub body_len: u32,
pub body_sha: String,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageBody(pub Vec<u8>);
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Subscription {
pub agent_id: AgentId,
pub room: RoomId,
pub created_at: i64,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum AgentKind {
Serve,
Cli,
Hook,
A2a,
#[default]
Other,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentRecord {
pub agent_id: AgentId,
pub card: AgentCard,
pub kind: AgentKind,
pub first_seen: i64,
pub last_seen: i64,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgentCard {
#[serde(default)]
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub skills: Vec<String>,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Role {
User,
Agent,
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
pub enum Part {
Text(String),
Data(serde_json::Value),
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct MessageView {
pub message_id: String,
pub context_id: String,
pub role: Role,
pub parts: Vec<Part>,
}
impl MessageView {
pub fn from_meta_and_body(meta: &MessageMeta, body: &[u8]) -> Self {
Self {
message_id: meta.id.clone(),
context_id: meta.room.as_str().to_string(),
role: Role::Agent,
parts: vec![Part::Text(String::from_utf8_lossy(body).into_owned())],
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn now_micros_is_positive_and_monotonic_ish() {
let a = now_micros();
assert!(a > 0, "now_micros should be after the epoch");
}
#[test]
fn room_scope_round_trips_through_msgpack() {
for scope in [
RoomScope::Remote("github.com/foo/bar".to_string()),
RoomScope::PathPrefix(std::path::PathBuf::from("/home/u/work")),
RoomScope::Global,
] {
let bytes = rmp_serde::to_vec_named(&scope).expect("encode");
let back: RoomScope = rmp_serde::from_slice(&bytes).expect("decode");
assert_eq!(scope, back);
}
}
#[test]
fn message_meta_round_trips_and_is_small() {
let meta = MessageMeta {
id: "m-1".to_string(),
room: RoomId::parse("room-1").expect("room"),
from: AgentId::parse("agent-1").expect("agent"),
ts_micros: 123,
subject: "hello".to_string(),
tags: vec!["t1".to_string()],
reply_to: None,
body_len: 5,
body_sha: "abc".to_string(),
};
let bytes = rmp_serde::to_vec_named(&meta).expect("encode");
let back: MessageMeta = rmp_serde::from_slice(&bytes).expect("decode");
assert_eq!(meta, back);
}
}