187 lines
5.3 KiB
Rust
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,
|
|
}
|