use serde_json::{Map, Value};
use super::event_store::Current;
use super::{KnlError, KnlResult};
pub const VIEW_TAIL: &str = "tail";
pub const OPT_N: &str = "n";
pub const DEFAULT_TAIL_N: usize = 20;
pub fn tail_of(events: &[Current], n: usize) -> Value {
let start = events.len().saturating_sub(n);
Value::Array(
events[start..]
.iter()
.map(|event| Value::Object((**event).clone()))
.collect(),
)
}
pub fn tail_count(opts: Option<&Map<String, Value>>) -> KnlResult<usize> {
let Some(value) = opts.and_then(|o| o.get(OPT_N)) else {
return Ok(DEFAULT_TAIL_N);
};
if value.is_null() {
return Ok(DEFAULT_TAIL_N);
}
let n = value
.as_f64()
.filter(|n| n.is_finite() && *n >= 0.0 && n.fract() == 0.0)
.ok_or_else(|| {
KnlError::Validation(format!(
"n must be a non-negative whole number, got {}",
super::event::json_type_name(value)
))
})?;
Ok(n as usize)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::knl::event::{data_field, kind_of, FIELD_DATA};
use crate::knl::History;
use serde_json::json;
fn obj(value: Value) -> Map<String, Value> {
match value {
Value::Object(map) => map,
other => panic!("test fixture must be an object, got {other}"),
}
}
fn since(h: &History, from: u64) -> Vec<Current> {
h.since(from)
.into_iter()
.map(Current::assume_current)
.collect()
}
fn append(history: &mut History, value: Value) {
let event = value.clone();
history
.append(obj(value))
.unwrap_or_else(|e| panic!("append {event}: {e}"));
}
fn mixed_history() -> History {
let mut h = History::new();
append(
&mut h,
json!({
"kind": "session_opened",
"data": { "scope_id": "scope-1", "owner": "anon" }
}),
);
append(
&mut h,
json!({ "kind": "msg_user", "data": { "content": "hi" } }),
);
append(
&mut h,
json!({
"kind": "llm_response",
"meta": { "beat": "b1" },
"data": {
"content": [{ "type": "text", "text": "ok" }],
"usage": { "input_tokens": 10, "output_tokens": 3 }
}
}),
);
append(
&mut h,
json!({
"kind": "tool_call", "meta": { "beat": "b1" },
"data": { "call_id": "c1", "name": "sh", "args": { "cmd": "ls" } }
}),
);
append(
&mut h,
json!({
"kind": "tool_result", "meta": { "beat": "b1" },
"data": { "call_id": "c1", "ok": false, "result": "boom" }
}),
);
append(
&mut h,
json!({ "kind": "note", "data": { "text": "no part of a request" } }),
);
h
}
#[test]
fn the_conversation_is_read_from_the_events_in_seq_order() {
let events = since(&mixed_history(), 0);
let kinds: Vec<&str> = events.iter().map(Current::kind).collect();
assert_eq!(
kinds,
[
"session_opened",
"msg_user",
"llm_response",
"tool_call",
"tool_result",
"note"
]
);
assert_eq!(events[1][FIELD_DATA], json!({ "content": "hi" }));
assert_eq!(
data_field(&events[2], "content"),
Some(&json!([{ "type": "text", "text": "ok" }]))
);
assert_eq!(
events[4][FIELD_DATA],
json!({ "call_id": "c1", "ok": false, "result": "boom" })
);
assert!(events[1].get("seq").is_some(), "{}", events[1]);
assert_eq!(events[3]["meta"]["beat"], json!("b1"), "{}", events[3]);
}
#[test]
fn events_read_back_in_order() {
let mut h = History::new();
append(
&mut h,
json!({
"kind": "llm_response", "meta": { "beat": "b1" },
"data": {
"content": [{ "type": "text", "text": "said last time" }],
"usage": { "input_tokens": 9_000 }
}
}),
);
append(
&mut h,
json!({ "kind": "msg_user", "data": { "content": "and now?" } }),
);
let events = since(&h, 0);
assert_eq!(events.len(), 2, "{events:?}");
assert_eq!(events[0].kind(), "llm_response");
assert_eq!(
data_field(&events[0], "content"),
Some(&json!([{ "type": "text", "text": "said last time" }]))
);
assert_eq!(events[1].kind(), "msg_user");
}
#[test]
fn the_token_account_is_read_from_the_data_the_kernel_stores_verbatim() {
let events = since(&mixed_history(), 0);
let responses: Vec<&Current> = events
.iter()
.filter(|e| e.kind() == "llm_response")
.collect();
assert_eq!(responses.len(), 1, "{events:?}");
assert_eq!(
data_field(responses[0], "usage"),
Some(&json!({ "input_tokens": 10, "output_tokens": 3 })),
"the counts are stored as the provider reported them"
);
}
#[test]
fn tail_returns_the_last_n_events_verbatim() {
let h = mixed_history();
let events = since(&h, 0);
let tail = tail_of(&events, 2);
let tail = tail.as_array().expect("array");
assert_eq!(tail.len(), 2);
assert_eq!(kind_of(&tail[0]), "tool_result");
assert_eq!(kind_of(&tail[1]), "note");
assert!(tail[1].get("seq").is_some(), "tail keeps the envelope");
assert_eq!(tail_of(&events, 99).as_array().map(Vec::len), Some(h.len()));
assert_eq!(tail_of(&events, 0).as_array().map(Vec::len), Some(0));
}
#[test]
fn tail_count_defaults_and_validates() {
assert_eq!(tail_count(None), Ok(DEFAULT_TAIL_N));
assert_eq!(tail_count(Some(&obj(json!({})))), Ok(DEFAULT_TAIL_N));
assert_eq!(tail_count(Some(&obj(json!({ "n": 5 })))), Ok(5));
assert_eq!(tail_count(Some(&obj(json!({ "n": 5.0 })))), Ok(5));
assert_eq!(tail_count(Some(&obj(json!({ "n": 0 })))), Ok(0));
let err = tail_count(Some(&obj(json!({ "n": -1 })))).expect_err("negative n");
assert!(err.reason().contains("non-negative"), "{err}");
let err = tail_count(Some(&obj(json!({ "n": "many" })))).expect_err("string n");
assert!(err.reason().contains("got string"), "{err}");
let err = tail_count(Some(&obj(json!({ "n": 1.5 })))).expect_err("fractional n");
assert!(err.reason().contains("whole number"), "{err}");
}
}