use serde::{Deserialize, Serialize};
use crate::chat::types::{ConversationItem, Lifecycle};
use crate::wave::journal::{Attribution, MessageOp};
pub const RESIDENT_TOKEN_HEADER: &str = "x-lf-resident-token";
pub const RESIDENT_TOKEN_ENV: &str = "LF_WAVE_RESIDENT_TOKEN";
pub const RESIDENT_TOKEN_FILE: &str = ".wave-resident-token";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResidentDelta {
TurnOpened { answers: Vec<String> },
TurnText { text: String },
TurnItem { item: ConversationItem },
TurnUsage {
input_tokens: Option<u64>,
output_tokens: Option<u64>,
cache_read_tokens: Option<u64>,
},
TurnFinished {
status: Lifecycle,
cost_usd: Option<f64>,
},
TurnSteered { answers: Vec<String> },
MessagesRequeued { ids: Vec<String> },
MindState { to: ResidentStateTo, reason: String },
ThreadStarted { vendor: String, thread_id: String },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ResidentStateTo {
Interrupting,
Failed,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PostDeltasRequest {
pub deltas: Vec<ResidentDelta>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct PostDeltasResponse {
pub accepted: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttachRequest {
pub pid: u32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct AttachResponse {
pub wave: String,
pub thread_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextResponse {
pub thread_id: Option<String>,
pub in_flight: Vec<InFlightWorker>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InFlightWorker {
pub run_id: String,
pub flow: String,
pub task: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct InboxFrame {
pub id: Option<String>,
pub op: MessageOp,
pub text: String,
pub from: Option<Attribution>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn resident_delta_round_trips_every_variant() {
let deltas = vec![
ResidentDelta::TurnOpened {
answers: vec!["msg-1".into(), "msg-2".into()],
},
ResidentDelta::TurnText {
text: "thinking".into(),
},
ResidentDelta::TurnItem {
item: ConversationItem::Tool {
id: "t-1".into(),
name: "Bash".into(),
status: Lifecycle::Completed,
input: None,
output: Some("ok".into()),
},
},
ResidentDelta::TurnUsage {
input_tokens: Some(10),
output_tokens: Some(4),
cache_read_tokens: None,
},
ResidentDelta::TurnFinished {
status: Lifecycle::Completed,
cost_usd: Some(0.02),
},
ResidentDelta::TurnSteered {
answers: vec!["msg-3".into()],
},
ResidentDelta::MessagesRequeued {
ids: vec!["msg-3".into()],
},
ResidentDelta::MindState {
to: ResidentStateTo::Failed,
reason: "harness disconnected".into(),
},
ResidentDelta::ThreadStarted {
vendor: "codex".into(),
thread_id: "thread-abc".into(),
},
];
for delta in deltas {
let value = serde_json::to_value(&delta).expect("serialize");
let decoded: ResidentDelta = serde_json::from_value(value).expect("deserialize");
assert_eq!(decoded, delta);
}
}
#[test]
fn absent_required_fields_are_parse_errors() {
for bad in [
serde_json::json!({ "kind": "turn_opened" }),
serde_json::json!({ "kind": "turn_finished", "cost_usd": null }),
serde_json::json!({ "kind": "turn_text" }),
serde_json::json!({ "kind": "mind_state", "to": "failed" }),
serde_json::json!({ "kind": "messages_requeued" }),
serde_json::json!({ "kind": "thread_started", "vendor": "codex" }),
] {
assert!(
serde_json::from_value::<ResidentDelta>(bad.clone()).is_err(),
"must reject {bad}"
);
}
assert!(serde_json::from_value::<InboxFrame>(
serde_json::json!({ "id": null, "text": "hi", "from": null })
)
.is_err());
let frame: InboxFrame = serde_json::from_value(
serde_json::json!({ "op": "message", "text": "hi", "id": null, "from": null }),
)
.expect("explicit nulls parse");
assert_eq!(frame.id, None);
assert_eq!(frame.from, None);
}
}