use serde::{Deserialize, Serialize};
use crate::chat::types::{ConversationItem, Lifecycle};
use crate::project_session::ProjectObservation;
use crate::task::TaskObservation;
use crate::wave::journal::MessageOp;
use crate::wave::playhead::{BodyProvenance, PlayheadView, StepOutcome};
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>,
reason: Option<String>,
},
TurnSteered { answers: Vec<String> },
MessagesRequeued { ids: Vec<String> },
BodyStarted { body: BodyProvenance },
BodySessionUpdated { body_id: String, session_id: String },
BodyFinished {
body_id: String,
outcome: StepOutcome,
reason: String,
},
LoopState { to: ResidentStateTo, reason: 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,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextResponse {
pub playhead: PlayheadView,
pub provider_session: Option<ProviderSessionRef>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ProviderSessionRef {
pub harness: String,
pub session_id: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum InboxFrame {
Message {
id: String,
op: MessageOp,
text: String,
from: Option<String>,
},
Task {
observation: TaskObservation,
},
Project {
observation: ProjectObservation,
},
Interrupt,
Skip,
}
#[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),
reason: None,
},
ResidentDelta::TurnSteered {
answers: vec!["msg-3".into()],
},
ResidentDelta::MessagesRequeued {
ids: vec!["msg-3".into()],
},
ResidentDelta::LoopState {
to: ResidentStateTo::Failed,
reason: "harness disconnected".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": "loop_state", "to": "failed" }),
serde_json::json!({ "kind": "messages_requeued" }),
] {
assert!(
serde_json::from_value::<ResidentDelta>(bad.clone()).is_err(),
"must reject {bad}"
);
}
assert!(serde_json::from_value::<InboxFrame>(serde_json::json!({
"kind": "message",
"text": "hi",
"from": null
}))
.is_err());
let frame: InboxFrame = serde_json::from_value(serde_json::json!({
"kind": "message",
"id": "msg-1",
"op": "message",
"text": "hi",
"from": null
}))
.expect("message frame parses");
assert!(matches!(frame, InboxFrame::Message { from: None, .. }));
}
}