Files
ai-app/event-model/src/lib.rs
T

187 lines
5.3 KiB
Rust

//! The event contract shared by session drivers, transcripts, and clients.
use serde::{Deserialize, Serialize};
pub type ImageRef = String;
pub type AttachmentRef = String;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuestionOption {
pub label: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preview: Option<String>,
}
impl QuestionOption {
pub fn plain(label: impl Into<String>) -> Self {
Self {
label: label.into(),
description: None,
preview: None,
}
}
}
/// Everything a session can append to its transcript.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
// `rename_all` does not rename fields inside enum variants.
#[serde(
tag = "type",
rename_all = "camelCase",
rename_all_fields = "camelCase"
)]
pub enum Event {
UserMessage {
/// The queued message this resolves, if any.
#[serde(default, skip_serializing_if = "Option::is_none")]
id: Option<String>,
text: String,
#[serde(default, alias = "images", skip_serializing_if = "Vec::is_empty")]
attachments: Vec<AttachmentRef>,
},
MessageQueued {
id: String,
text: String,
#[serde(default, alias = "images", skip_serializing_if = "Vec::is_empty")]
attachments: Vec<AttachmentRef>,
},
MessageDropped {
id: String,
},
/// Driver-internal acknowledgement; the manager records `UserMessage`.
MessageTaken {
id: Option<String>,
text: String,
#[serde(default, alias = "images", skip_serializing_if = "Vec::is_empty")]
attachments: Vec<AttachmentRef>,
},
AssistantText {
delta: String,
},
ToolStart {
id: String,
tool: String,
input: serde_json::Value,
},
ToolUpdate {
id: String,
output: String,
},
ToolEnd {
id: String,
output: String,
#[serde(default)]
is_error: bool,
},
Image {
#[serde(rename = "ref")]
image: ImageRef,
/// The tool call whose result carried the image.
#[serde(default, skip_serializing_if = "Option::is_none")]
about: Option<String>,
},
Question {
id: String,
prompt: String,
header: Option<String>,
options: Vec<QuestionOption>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
multi_select: bool,
/// The related tool call, for permission questions.
#[serde(default, skip_serializing_if = "Option::is_none")]
about: Option<String>,
},
PeerMessage {
from: String,
text: String,
/// Seq of the `Running` event that opened the turn it belongs above.
#[serde(default, skip_serializing_if = "Option::is_none")]
turn_start: Option<u64>,
},
Answered {
id: String,
answers: Vec<String>,
},
Status {
state: SessionStatus,
},
/// Settings confirmed by the session, not merely requested.
Settings {
#[serde(default, skip_serializing_if = "Option::is_none")]
model: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
permission_mode: Option<String>,
},
UsageDelta {
tokens: u64,
/// Context held when the turn ended; this is not cumulative usage.
#[serde(default, skip_serializing_if = "Option::is_none")]
context: Option<u64>,
},
Compacted {
#[serde(default, skip_serializing_if = "Option::is_none")]
pre_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
post_tokens: Option<u64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
trigger: Option<String>,
},
CommandQueued {
id: String,
text: String,
},
CommandSent {
id: String,
text: String,
},
/// Clears model context without truncating transcript history.
Cleared,
LimitReached {
/// Epoch seconds; absent when the dialect did not report a reset.
#[serde(default, skip_serializing_if = "Option::is_none")]
resets_at: Option<f64>,
},
Error {
message: String,
},
}
/// Input context is prompt plus both cache figures, never output tokens.
pub fn context_tokens(input: u64, cache_creation: u64, cache_read: u64) -> u64 {
input + cache_creation + cache_read
}
pub fn context_after(current: Option<u64>, event: &Event) -> Option<u64> {
match event {
// Missing usage preserves the last measurement; clear does not.
Event::UsageDelta { context, .. } => context.or(current),
Event::Compacted { post_tokens, .. } => *post_tokens,
Event::Cleared => None,
_ => current,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub enum SessionStatus {
Idle,
Running,
AwaitingInput,
Compacting,
Exited,
/// A process is recorded but liveness could not be determined.
Unknown,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeqEvent {
pub seq: u64,
pub ts: f64,
#[serde(flatten)]
pub event: Event,
}