use serde::{Deserialize, Serialize};
use super::content::ContentBlock;
use super::message::{Role, Visibility};
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ContextMessageTarget {
#[default]
System,
Session,
Conversation,
SuffixSystem,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContextMessage {
pub key: String,
pub role: Role,
pub content: Vec<ContentBlock>,
pub visibility: Visibility,
pub target: ContextMessageTarget,
pub cooldown_turns: u32,
#[serde(default)]
pub persistent: bool,
#[serde(default)]
pub consume_after_emit: bool,
#[serde(default)]
pub priority: i32,
}
impl ContextMessage {
pub fn system(key: impl Into<String>, text: impl Into<String>) -> Self {
Self {
key: key.into(),
role: Role::System,
content: vec![ContentBlock::text(text)],
visibility: Visibility::Internal,
target: ContextMessageTarget::System,
cooldown_turns: 0,
persistent: false,
consume_after_emit: false,
priority: 0,
}
}
pub fn suffix_system(key: impl Into<String>, text: impl Into<String>) -> Self {
Self {
key: key.into(),
role: Role::System,
content: vec![ContentBlock::text(text)],
visibility: Visibility::Internal,
target: ContextMessageTarget::SuffixSystem,
cooldown_turns: 0,
persistent: false,
consume_after_emit: false,
priority: 0,
}
}
pub fn session(key: impl Into<String>, role: Role, text: impl Into<String>) -> Self {
Self {
key: key.into(),
role,
content: vec![ContentBlock::text(text)],
visibility: Visibility::Internal,
target: ContextMessageTarget::Session,
cooldown_turns: 0,
persistent: false,
consume_after_emit: false,
priority: 0,
}
}
pub fn conversation(key: impl Into<String>, role: Role, text: impl Into<String>) -> Self {
Self {
key: key.into(),
role,
content: vec![ContentBlock::text(text)],
visibility: Visibility::All,
target: ContextMessageTarget::Conversation,
cooldown_turns: 0,
persistent: false,
consume_after_emit: false,
priority: 0,
}
}
pub fn system_persistent(key: impl Into<String>, text: impl Into<String>) -> Self {
Self {
key: key.into(),
role: Role::System,
content: vec![ContentBlock::text(text)],
visibility: Visibility::Internal,
target: ContextMessageTarget::System,
cooldown_turns: 0,
persistent: true,
consume_after_emit: false,
priority: 0,
}
}
pub fn emit_once(
key: impl Into<String>,
text: impl Into<String>,
target: ContextMessageTarget,
) -> Self {
Self {
key: key.into(),
role: Role::System,
content: vec![ContentBlock::text(text)],
visibility: Visibility::Internal,
target,
cooldown_turns: 0,
persistent: true,
consume_after_emit: true,
priority: 0,
}
}
#[must_use]
pub fn with_cooldown(mut self, turns: u32) -> Self {
self.cooldown_turns = turns;
self
}
#[must_use]
pub fn with_persistent(mut self, persistent: bool) -> Self {
self.persistent = persistent;
self
}
#[must_use]
pub fn with_consume_after_emit(mut self, consume: bool) -> Self {
self.consume_after_emit = consume;
self
}
#[must_use]
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn system_context_message_defaults() {
let msg = ContextMessage::system("my.key", "remember this");
assert_eq!(msg.key, "my.key");
assert_eq!(msg.role, Role::System);
assert_eq!(msg.target, ContextMessageTarget::System);
assert_eq!(msg.visibility, Visibility::Internal);
assert_eq!(msg.cooldown_turns, 0);
assert!(!msg.persistent);
assert!(!msg.consume_after_emit);
assert_eq!(msg.priority, 0);
}
#[test]
fn with_cooldown_builder() {
let msg = ContextMessage::system("k", "text").with_cooldown(5);
assert_eq!(msg.cooldown_turns, 5);
}
#[test]
fn context_message_serde_roundtrip() {
let msg = ContextMessage {
key: "test.key".into(),
role: Role::User,
content: vec![ContentBlock::text("hello")],
visibility: Visibility::All,
target: ContextMessageTarget::Conversation,
cooldown_turns: 3,
persistent: true,
consume_after_emit: false,
priority: 10,
};
let json = serde_json::to_value(&msg).unwrap();
let parsed: ContextMessage = serde_json::from_value(json).unwrap();
assert_eq!(parsed, msg);
}
#[test]
fn conversation_target_visible_by_default() {
let msg = ContextMessage::conversation("conv.key", Role::User, "visible text");
assert_eq!(msg.target, ContextMessageTarget::Conversation);
assert_eq!(msg.visibility, Visibility::All);
}
#[test]
fn system_target_internal_by_default() {
let msg = ContextMessage::system("sys.key", "internal text");
assert_eq!(msg.target, ContextMessageTarget::System);
assert_eq!(msg.visibility, Visibility::Internal);
let suffix = ContextMessage::suffix_system("suffix.key", "suffix text");
assert_eq!(suffix.target, ContextMessageTarget::SuffixSystem);
assert_eq!(suffix.visibility, Visibility::Internal);
let session = ContextMessage::session("sess.key", Role::System, "session text");
assert_eq!(session.target, ContextMessageTarget::Session);
assert_eq!(session.visibility, Visibility::Internal);
}
#[test]
fn with_cooldown_builder_pattern() {
let msg = ContextMessage::conversation("k", Role::User, "text").with_cooldown(10);
assert_eq!(msg.cooldown_turns, 10);
assert_eq!(msg.key, "k");
assert_eq!(msg.role, Role::User);
assert_eq!(msg.target, ContextMessageTarget::Conversation);
assert_eq!(msg.visibility, Visibility::All);
}
#[test]
fn system_persistent_constructor() {
let msg = ContextMessage::system_persistent("sys.persist", "always here");
assert!(msg.persistent);
assert!(!msg.consume_after_emit);
assert_eq!(msg.target, ContextMessageTarget::System);
assert_eq!(msg.role, Role::System);
}
#[test]
fn emit_once_constructor() {
let msg = ContextMessage::emit_once("once.key", "one shot", ContextMessageTarget::Session);
assert!(msg.persistent);
assert!(msg.consume_after_emit);
assert_eq!(msg.target, ContextMessageTarget::Session);
}
#[test]
fn builder_chain_new_fields() {
let msg = ContextMessage::system("k", "v")
.with_persistent(true)
.with_consume_after_emit(true)
.with_priority(42);
assert!(msg.persistent);
assert!(msg.consume_after_emit);
assert_eq!(msg.priority, 42);
}
#[test]
fn context_message_target_ordering() {
assert!(ContextMessageTarget::System < ContextMessageTarget::Session);
assert!(ContextMessageTarget::Session < ContextMessageTarget::Conversation);
assert!(ContextMessageTarget::Conversation < ContextMessageTarget::SuffixSystem);
}
#[test]
fn serde_defaults_for_new_fields() {
let json = r#"{"key":"k","role":"system","content":[{"type":"text","text":"hi"}],"visibility":"internal","target":"system","cooldown_turns":0}"#;
let msg: ContextMessage = serde_json::from_str(json).unwrap();
assert!(!msg.persistent);
assert!(!msg.consume_after_emit);
assert_eq!(msg.priority, 0);
}
}