Route response item IDs through a shared generator (#59312)

## What changed

Introduce `ItemIdGenerator` and pass the runtime-owned generator through sessions, model clients, history, hooks, tool outputs, and memory requests. Keep UUIDv7 generation as the default and replace direct calls to `ResponseItemId::new` with the shared callable.

Preserve existing item IDs and backfill missing or empty IDs through the same generator, including forked history before reconstruction and persistence.

## Testing

Extend coverage for UUIDv7 IDs on user messages, agent messages, and additional tools, and assert that forked model context matches the backfilled items persisted to history.

GitOrigin-RevId: 0dd9ccf0f46f0f1005a6acfc740133b03ce82259
This commit is contained in:
Ruslan Nigmatullin
2026-11-04 04:02:00 +00:00
committed by copyberry
parent f2b94ef8b5
commit d84c872464
68 changed files with 634 additions and 336 deletions
@@ -0,0 +1,12 @@
//! Response item ID generation shared by sessions and their consumers.
use crate::ResponseItemId;
use std::sync::Arc;
/// Generates response item IDs from a type prefix for an owning session. Clones share the same callable.
pub type ItemIdGenerator = Arc<dyn Fn(&str) -> ResponseItemId + Send + Sync>;
#[cfg(test)]
pub(super) fn uuid_v7_item_id_generator_for_test() -> ItemIdGenerator {
Arc::new(|prefix| ResponseItemId::with_suffix(prefix, uuid::Uuid::now_v7()))
}
+9 -4
View File
@@ -1,5 +1,5 @@
use crate::AgentPath;
use crate::ResponseItemId;
use crate::ItemIdGenerator;
use crate::ThreadId;
use crate::dynamic_tools::DynamicToolCallOutputContentItem;
use crate::mcp::CallToolResult;
@@ -727,7 +727,10 @@ impl HookPromptFragment {
}
}
pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<ResponseItem> {
pub fn build_hook_prompt_message(
fragments: &[HookPromptFragment],
item_id_generator: &ItemIdGenerator,
) -> Option<ResponseItem> {
let mut content = Vec::new();
let mut metadata = Vec::new();
for fragment in fragments {
@@ -744,7 +747,7 @@ pub fn build_hook_prompt_message(fragments: &[HookPromptFragment]) -> Option<Res
Some(ResponseItem::Message {
status: None,
encrypted_content: None,
id: Some(ResponseItemId::new("msg")),
id: Some(item_id_generator("msg")),
role: "user".to_string(),
content,
phase: None,
@@ -920,7 +923,9 @@ mod tests {
HookPromptFragment::from_single_hook("Retry with care & joy.", "hook-run-1"),
HookPromptFragment::from_single_hook("Then summarize cleanly.", "hook-run-2"),
];
let message = build_hook_prompt_message(&original).expect("hook prompt");
let message =
build_hook_prompt_message(&original, &crate::uuid_v7_item_id_generator_for_test())
.expect("hook prompt");
let ResponseItem::Message { id, content, .. } = message else {
panic!("expected hook prompt message");
+4
View File
@@ -1,6 +1,7 @@
pub mod account;
mod agent_path;
pub mod auth;
mod item_id_generator;
mod response_item_id;
mod response_usage;
mod sanitized_git_url;
@@ -8,6 +9,9 @@ mod session_id;
mod thread_id;
mod tool_name;
pub use agent_path::AgentPath;
pub use item_id_generator::ItemIdGenerator;
#[cfg(test)]
use item_id_generator::uuid_v7_item_id_generator_for_test;
pub use permission_profile_intersection::PermissionIntersectionError;
pub use permission_profile_intersection::intersect_effective_permission_profiles;
pub use response_item_id::ResponseItemId;
@@ -1,5 +1,4 @@
use super::ConfigurationReasoning;
use crate::ResponseItemId;
use crate::models::ResponseItem;
use crate::openai_models::ReasoningEffort;
use pretty_assertions::assert_eq;
@@ -21,7 +20,9 @@ fn configuration_update_round_trips_without_message_fields() {
}
);
item.set_id(Some(ResponseItemId::new("configuration-id")));
item.set_id(Some(crate::uuid_v7_item_id_generator_for_test()(
"configuration-id",
)));
item.set_turn_id_if_missing("turn-id");
item.set_create_time_if_missing(serde_json::Number::from(123));
assert_eq!(
@@ -17,10 +17,6 @@ use ts_rs::TS;
pub struct ResponseItemId(String);
impl ResponseItemId {
pub fn new(prefix: &str) -> Self {
Self::with_suffix(prefix, uuid::Uuid::now_v7())
}
pub fn with_suffix(prefix: &str, suffix: impl fmt::Display) -> Self {
Self(format!("{prefix}_{suffix}"))
}
@@ -5,7 +5,7 @@ use super::ResponseItemId;
#[test]
fn creates_prefixed_uuid_v7_ids() {
let id = ResponseItemId::new("msg");
let id = crate::uuid_v7_item_id_generator_for_test()("msg");
let uuid = id.strip_prefix("msg_").expect("message prefix");
assert_eq!(
Uuid::parse_str(uuid).expect("UUID suffix").get_version(),