///|
/// A mutable conversation: an ordered list of messages plus an optional
/// system prompt, with helpers for appending turns, estimating token usage,
/// and trimming history to fit a budget.
///
/// This is a convenience layer over the raw `Array[Message]`; it does not
/// itself call the API — turn it into a request with `to_request`.
pub struct Conversation {
mut system : String?
messages : Array[Message]
}
///|
/// Create an empty conversation, optionally with a system prompt.
pub fn Conversation::new(system? : String) -> Conversation {
{ system, messages: [] }
}
///|
/// Set (or replace) the system prompt.
pub fn Conversation::set_system(self : Conversation, prompt : String) -> Unit {
self.system = Some(prompt)
}
///|
/// Append a user message.
pub fn Conversation::user(self : Conversation, text : String) -> Unit {
self.messages.push(Message::user(text))
}
///|
/// Append an assistant message.
pub fn Conversation::assistant(self : Conversation, text : String) -> Unit {
self.messages.push(Message::assistant(text))
}
///|
/// Append the assistant message from a `ChatResponse` (the first choice), so
/// the reply becomes part of the ongoing history. Returns the appended text.
pub fn Conversation::add_response(
self : Conversation,
response : ChatResponse,
) -> String {
let text = response.text()
self.messages.push(Message::assistant(text))
text
}
///|
/// Append a tool-result message.
pub fn Conversation::tool_result(
self : Conversation,
tool_call_id : String,
content : String,
) -> Unit {
self.messages.push(Message::tool_result(tool_call_id, content))
}
///|
/// Append an arbitrary message.
pub fn Conversation::push(self : Conversation, message : Message) -> Unit {
self.messages.push(message)
}
///|
/// The number of messages currently in the history (excluding the system
/// prompt).
pub fn Conversation::length(self : Conversation) -> Int {
self.messages.length()
}
///|
/// Snapshot the full message list including the system prompt (if any) as the
/// leading `system` message.
pub fn Conversation::to_messages(self : Conversation) -> Array[Message] {
let out = []
if self.system is Some(s) {
out.push(Message::system(s))
}
for m in self.messages {
out.push(m)
}
out
}
///|
/// Build a `ChatRequest` for `model` from the conversation's current state.
pub fn Conversation::to_request(
self : Conversation,
model : String,
) -> ChatRequest {
ChatRequest::new(model, self.to_messages())
}
///|
/// A rough token-count estimate for a piece of text.
///
/// Uses the widely-cited heuristic of ~4 characters per token. This is an
/// approximation for budgeting only, not an exact tokenizer.
pub fn estimate_tokens(text : String) -> Int {
let chars = text.length()
// Round up, and count at least 1 token for any non-empty text.
if chars == 0 {
0
} else {
(chars + 3) / 4
}
}
///|
/// Estimate the token count of a single message, including a small overhead
/// per message for role/formatting (mirrors OpenAI's ~4 tokens/message).
pub fn estimate_message_tokens(message : Message) -> Int {
let text = match message.content {
Str(s) => s
Parts(parts) => {
let buf = StringBuilder::new()
for p in parts {
if p is Text(t) {
buf.write_string(t)
}
}
buf.to_string()
}
}
estimate_tokens(text) + 4
}
///|
/// Estimate the total token count of the conversation (system + all messages).
pub fn Conversation::estimate_tokens(self : Conversation) -> Int {
let mut total = 0
if self.system is Some(s) {
total = total + estimate_tokens(s) + 4
}
for m in self.messages {
total = total + estimate_message_tokens(m)
}
total
}
///|
/// Trim the oldest messages until the estimated token count fits within
/// `max_tokens`, preserving the system prompt and the most recent messages.
///
/// Returns the number of messages dropped.
pub fn Conversation::trim_to_budget(
self : Conversation,
max_tokens : Int,
) -> Int {
let mut dropped = 0
// Drop from the front (oldest) while over budget and messages remain.
while self.estimate_tokens() > max_tokens && self.messages.length() > 1 {
self.messages.remove(0) |> ignore
dropped = dropped + 1
}
dropped
}
///|
/// Remove all messages (keeping the system prompt).
pub fn Conversation::clear(self : Conversation) -> Unit {
self.messages.clear()
}