use crate::config::AtmConfig;
use crate::error::AtmError;
use crate::schema::AgentType;
use crate::schema::{AgentMember, MessageEnvelope};
use crate::types::{AgentName, IsoTimestamp, ModelName, PaneId, TaskId, TeamName};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use super::{AckTransition, MessageKey, ReplaySource, TaskState, sealed};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
pub struct TaskStoreTaskMetadata {
#[serde(default)]
pub fields: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreTaskRecord {
pub team: TeamName,
pub task_id: TaskId,
pub state: TaskState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<AgentName>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub linked_message_keys: Vec<MessageKey>,
#[serde(default)]
pub metadata: TaskStoreTaskMetadata,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub created_at: Option<IsoTimestamp>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub updated_at: Option<IsoTimestamp>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreHealthSnapshot {
pub team: TeamName,
pub member_count: u64,
#[serde(default)]
pub stale: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub refreshed_at: Option<IsoTimestamp>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum RosterMemberKind {
Permanent,
Ephemeral,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum RosterHarness {
ClaudeCode,
CodexCli,
GeminiCli,
Opencode,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterMemberRecord {
pub team_name: TeamName,
pub agent_name: AgentName,
pub member_kind: RosterMemberKind,
pub harness: RosterHarness,
#[serde(default)]
pub agent_type: AgentType,
#[serde(default)]
pub model: ModelName,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub recipient_pane_id: Option<PaneId>,
#[serde(default)]
pub metadata_json: Map<String, Value>,
}
impl RosterMemberRecord {
pub fn from_claude_code_member(team_name: TeamName, member: AgentMember) -> Self {
let recipient_pane_id = member.tmux_pane_id;
let mut metadata_json = member.extra;
if !member.agent_id.is_empty() {
metadata_json.insert("agentId".to_string(), Value::String(member.agent_id));
}
if let Some(joined_at) = member.joined_at {
metadata_json.insert(
"joinedAt".to_string(),
Value::Number(serde_json::Number::from(joined_at)),
);
}
if !member.cwd.is_empty() {
metadata_json.insert("cwd".to_string(), Value::String(member.cwd));
}
Self {
team_name,
agent_name: member.name,
member_kind: RosterMemberKind::Permanent,
harness: RosterHarness::ClaudeCode,
agent_type: member.agent_type,
model: member.model,
recipient_pane_id,
metadata_json,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClaudeCodeRosterMember {
pub member_name: AgentName,
pub harness: RosterHarness,
pub inbox_path: Option<PathBuf>,
pub tmux_pane_id: Option<PaneId>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClaudeCodeTeamRoster {
pub team_name: TeamName,
pub members: Arc<[ClaudeCodeRosterMember]>,
}
impl ClaudeCodeTeamRoster {
pub fn from_roster_snapshot(team_name: TeamName, records: &[RosterMemberRecord]) -> Self {
let members = records
.iter()
.filter(|record| record.harness == RosterHarness::ClaudeCode)
.map(|record| ClaudeCodeRosterMember {
member_name: record.agent_name.clone(),
harness: record.harness,
inbox_path: None,
tmux_pane_id: record.recipient_pane_id.clone(),
})
.collect::<Vec<_>>()
.into();
Self { team_name, members }
}
pub fn contains_member(&self, member: &AgentName) -> bool {
self.members
.iter()
.any(|entry| entry.member_name == *member)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreCreateTaskRequest {
pub team: TeamName,
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreCreateTaskResponse {
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreLoadTaskRequest {
pub team: TeamName,
pub task_id: TaskId,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreLoadTaskResponse {
#[serde(default)]
pub record: Option<TaskStoreTaskRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreUpdateTaskRequest {
pub team: TeamName,
pub task_id: TaskId,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub owner: Option<AgentName>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<TaskState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<TaskStoreTaskMetadata>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub append_message_keys: Vec<MessageKey>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreUpdateTaskResponse {
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreAttachMessageLinkRequest {
pub team: TeamName,
pub task_id: TaskId,
pub message_key: MessageKey,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreAttachMessageLinkResponse {
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreDetachMessageLinkRequest {
pub team: TeamName,
pub task_id: TaskId,
pub message_key: MessageKey,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreDetachMessageLinkResponse {
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreRecordAckTransitionRequest {
pub team: TeamName,
pub task_id: TaskId,
pub message_key: MessageKey,
pub actor: AgentName,
pub transitioned_at: IsoTimestamp,
pub transition: AckTransition,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreRecordAckTransitionResponse {
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreQueryTaskMetadataRequest {
pub team: TeamName,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub task_id: Option<TaskId>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message_key: Option<MessageKey>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state: Option<TaskState>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreQueryTaskMetadataResponse {
pub records: Vec<TaskStoreTaskRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreRequest {
pub team: TeamName,
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct TaskStoreResponse {
pub record: TaskStoreTaskRecord,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreReplaceRosterRequest {
pub team: TeamName,
pub members: Vec<RosterMemberRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<ReplaySource>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreReplaceRosterResponse {
pub team: TeamName,
pub previous_member_count: u64,
pub current_member_count: u64,
pub replaced: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreLoadRosterRequest {
pub team: TeamName,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreLoadRosterResponse {
pub team: TeamName,
pub members: Vec<RosterMemberRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreQueryMembershipRequest {
pub team: TeamName,
pub member: AgentName,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreQueryMembershipResponse {
pub team: TeamName,
#[serde(default)]
pub member: Option<RosterMemberRecord>,
#[serde(default)]
pub is_member: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreHealthSnapshotRequest {
pub team: TeamName,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreHealthSnapshotResponse {
pub snapshot: RosterStoreHealthSnapshot,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreListTeamsRequest;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreListTeamsResponse {
pub teams: Vec<TeamName>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreRequest {
pub team: TeamName,
pub members: Vec<RosterMemberRecord>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<ReplaySource>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct RosterStoreResponse {
pub team: TeamName,
pub previous_member_count: u64,
pub current_member_count: u64,
pub replaced: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ConfigLoadRequest {
pub current_dir: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConfigLoadResponse {
pub config: Option<AtmConfig>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxSourceFileRecord {
pub path: PathBuf,
pub messages: Vec<MessageEnvelope>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxIngressImportRequest {
pub home_dir: PathBuf,
pub team: TeamName,
pub agent: AgentName,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxIngressImportResponse {
pub source_files: Vec<InboxSourceFileRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxIngressIdentityFingerprintRequest {
pub message: MessageEnvelope,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxIngressIdentityFingerprintResponse {
pub fingerprint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxIngressDiagnosticsRequest {
pub source_files: Vec<InboxSourceFileRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxIngressDiagnosticsResponse {
pub duplicate_message_ids: usize,
pub messages_without_ids: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InboxIngressRequest;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InboxIngressResponse;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxExportRecordRequest {
pub source_files: Vec<InboxSourceFileRecord>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxExportRecordResponse {
pub committed_paths: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxExportReexportMessageRequest {
pub path: PathBuf,
pub messages: Vec<MessageEnvelope>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxExportReexportMessageResponse {
pub wrote_messages: usize,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum ClaudeCompatibilityDeliveryMode {
RecoveredLogicalMessageSet,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct InboxExportAppendMessageSetRequest {
pub path: PathBuf,
pub messages: Vec<MessageEnvelope>,
pub mode: ClaudeCompatibilityDeliveryMode,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct InboxExportAppendMessageSetResponse {
pub wrote_messages: usize,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InboxExportRequest;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct InboxExportResponse;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct NonClaudeOutboundDeliveryRequest {
pub team: TeamName,
pub agent: AgentName,
pub recipient_pane_id: Option<PaneId>,
pub messages: Vec<MessageEnvelope>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct NonClaudeOutboundDeliveryResponse {
pub delivered_messages: usize,
}
pub trait TaskStore: sealed::Sealed {
fn create_task(
&self,
request: TaskStoreCreateTaskRequest,
) -> Result<TaskStoreCreateTaskResponse, AtmError>;
fn load_task(
&self,
request: TaskStoreLoadTaskRequest,
) -> Result<TaskStoreLoadTaskResponse, AtmError>;
fn update_task(
&self,
request: TaskStoreUpdateTaskRequest,
) -> Result<TaskStoreUpdateTaskResponse, AtmError>;
fn attach_message_link(
&self,
request: TaskStoreAttachMessageLinkRequest,
) -> Result<TaskStoreAttachMessageLinkResponse, AtmError>;
fn detach_message_link(
&self,
request: TaskStoreDetachMessageLinkRequest,
) -> Result<TaskStoreDetachMessageLinkResponse, AtmError>;
fn record_ack_transition(
&self,
request: TaskStoreRecordAckTransitionRequest,
) -> Result<TaskStoreRecordAckTransitionResponse, AtmError>;
fn query_task_metadata(
&self,
request: TaskStoreQueryTaskMetadataRequest,
) -> Result<TaskStoreQueryTaskMetadataResponse, AtmError>;
}
pub trait RosterStore: sealed::Sealed {
fn replace_roster(
&self,
request: RosterStoreReplaceRosterRequest,
) -> Result<RosterStoreReplaceRosterResponse, AtmError>;
fn load_roster(
&self,
request: RosterStoreLoadRosterRequest,
) -> Result<RosterStoreLoadRosterResponse, AtmError>;
fn query_membership(
&self,
request: RosterStoreQueryMembershipRequest,
) -> Result<RosterStoreQueryMembershipResponse, AtmError>;
fn list_teams(
&self,
request: RosterStoreListTeamsRequest,
) -> Result<RosterStoreListTeamsResponse, AtmError>;
fn health_snapshot(
&self,
request: RosterStoreHealthSnapshotRequest,
) -> Result<RosterStoreHealthSnapshotResponse, AtmError>;
}
pub trait ConfigIngress: sealed::Sealed {
fn load_config(&self, request: ConfigLoadRequest) -> Result<ConfigLoadResponse, AtmError>;
}
pub trait InboxIngress: sealed::Sealed {
fn import_inbox_source(
&self,
request: InboxIngressImportRequest,
) -> Result<InboxIngressImportResponse, AtmError>;
fn compute_identity_fingerprint(
&self,
request: InboxIngressIdentityFingerprintRequest,
) -> InboxIngressIdentityFingerprintResponse;
fn report_diagnostics(
&self,
request: InboxIngressDiagnosticsRequest,
) -> InboxIngressDiagnosticsResponse;
}
pub trait InboxExport: sealed::Sealed {
fn export_record(
&self,
request: InboxExportRecordRequest,
) -> Result<InboxExportRecordResponse, AtmError>;
fn reexport_message(
&self,
request: InboxExportReexportMessageRequest,
) -> Result<InboxExportReexportMessageResponse, AtmError>;
fn append_message_set(
&self,
request: InboxExportAppendMessageSetRequest,
) -> Result<InboxExportAppendMessageSetResponse, AtmError>;
}
pub trait NonClaudeOutbound: sealed::Sealed {
fn deliver_payloads(
&self,
request: NonClaudeOutboundDeliveryRequest,
) -> Result<NonClaudeOutboundDeliveryResponse, AtmError>;
}