pub(super) const EVENT_IDS_HEADER: &str = "a2a-rs-event-ids";
pub(super) const EVENT_ID_KEY: &str = "a2a_rs_event_id";
#[cfg(feature = "server")]
pub(super) fn parse_last_event_id(headers: &http::HeaderMap) -> Option<u64> {
headers
.get("last-event-id")
.and_then(|value| value.to_str().ok())
.and_then(|value| value.trim().parse::<u64>().ok())
}
#[cfg(feature = "server")]
pub(super) fn wants_event_ids(headers: &http::HeaderMap) -> bool {
headers.contains_key(EVENT_IDS_HEADER)
}
#[cfg(feature = "server")]
pub(super) fn stamp_event_id(event: &mut crate::port::UpdateEvent, id: u64) {
use crate::port::UpdateEvent;
let metadata = match event {
UpdateEvent::StatusUpdate(event) => &mut event.metadata,
UpdateEvent::ArtifactUpdate(event) => &mut event.metadata,
};
metadata
.get_or_insert_with(serde_json::Map::new)
.insert(EVENT_ID_KEY.to_string(), id.to_string().into());
}
#[cfg(feature = "http-client")]
pub(super) fn take_event_id(item: &mut crate::port::StreamItem) -> Option<u64> {
use crate::port::StreamItem;
let metadata = match item {
StreamItem::StatusUpdate(event) => &mut event.metadata,
StreamItem::ArtifactUpdate(event) => &mut event.metadata,
StreamItem::Task(_) => return None,
};
let bag = metadata.as_mut()?;
let stamped = bag.remove(EVENT_ID_KEY);
if bag.is_empty() {
*metadata = None;
}
stamped?.as_str()?.parse().ok()
}
#[cfg(all(test, feature = "server", feature = "http-client"))]
mod tests {
use super::*;
use crate::domain::{TaskArtifactUpdateEvent, TaskStatusUpdateEvent};
use crate::port::{StreamItem, UpdateEvent};
fn status_update(metadata: Option<serde_json::Map<String, serde_json::Value>>) -> UpdateEvent {
UpdateEvent::StatusUpdate(TaskStatusUpdateEvent {
task_id: "task-1".to_string(),
context_id: "ctx-1".to_string(),
kind: "status-update".to_string(),
status: Default::default(),
metadata,
})
}
#[test]
fn a_stamped_id_round_trips_and_leaves_no_key_behind() {
let mut event = status_update(None);
stamp_event_id(&mut event, 42);
let UpdateEvent::StatusUpdate(event) = event else {
unreachable!()
};
let mut item = StreamItem::StatusUpdate(event);
assert_eq!(take_event_id(&mut item), Some(42));
let StreamItem::StatusUpdate(event) = &item else {
unreachable!()
};
assert_eq!(
event.metadata, None,
"an event the agent sent no metadata on must not gain a bag"
);
}
#[test]
fn stamping_keeps_the_agents_metadata() {
let mut bag = serde_json::Map::new();
bag.insert("step".to_string(), "planning".into());
let mut event = status_update(Some(bag));
stamp_event_id(&mut event, 7);
let UpdateEvent::StatusUpdate(event) = event else {
unreachable!()
};
let mut item = StreamItem::StatusUpdate(event);
assert_eq!(take_event_id(&mut item), Some(7));
let StreamItem::StatusUpdate(event) = &item else {
unreachable!()
};
assert_eq!(
event.metadata.as_ref().and_then(|bag| bag.get("step")),
Some(&serde_json::Value::from("planning"))
);
}
#[test]
fn an_id_past_double_precision_survives() {
let id = (1u64 << 53) + 1;
let mut event = status_update(None);
stamp_event_id(&mut event, id);
let UpdateEvent::StatusUpdate(event) = event else {
unreachable!()
};
assert_eq!(
take_event_id(&mut StreamItem::StatusUpdate(event)),
Some(id)
);
}
#[test]
fn an_unstamped_event_has_no_id() {
let event = TaskStatusUpdateEvent {
task_id: "task-1".to_string(),
context_id: "ctx-1".to_string(),
kind: "status-update".to_string(),
status: Default::default(),
metadata: None,
};
assert_eq!(take_event_id(&mut StreamItem::StatusUpdate(event)), None);
let artifact = TaskArtifactUpdateEvent {
task_id: "task-1".to_string(),
context_id: "ctx-1".to_string(),
kind: "artifact-update".to_string(),
artifact: Default::default(),
append: None,
last_chunk: None,
metadata: None,
};
assert_eq!(
take_event_id(&mut StreamItem::ArtifactUpdate(artifact)),
None
);
}
#[test]
fn event_ids_are_stamped_only_when_asked_for() {
let mut headers = http::HeaderMap::new();
assert!(!wants_event_ids(&headers));
headers.insert(EVENT_IDS_HEADER, http::HeaderValue::from_static("1"));
assert!(wants_event_ids(&headers));
}
#[test]
fn a_last_event_id_header_parses_or_reads_as_absent() {
let header = |value| {
let mut headers = http::HeaderMap::new();
headers.insert("last-event-id", http::HeaderValue::from_static(value));
parse_last_event_id(&headers)
};
assert_eq!(header("17"), Some(17));
assert_eq!(header(" 17 "), Some(17), "SSE allows surrounding space");
assert_eq!(header("not-a-number"), None);
assert_eq!(header(""), None);
assert_eq!(parse_last_event_id(&http::HeaderMap::new()), None);
}
}