Files
codex/codex-rs/core/src/realtime_context_tests.rs
T
Owen Lin 040dafa32d feat(core): add metadata field to ResponseItem (#28355)
## Description

This PR adds an optional `metadata` field to `ResponseItem` for
Responses API calls. Only mechanical plumbing, no actual values
populated and sent yet. Turns out just adding a new field to
`ResponseItem` has quite a large blast radius already.

This change is backwards compatible because `metadata` is optional and
omitted when absent, so existing response items and rollout history
without it still deserialize and requests that do not set it keep the
same wire shape. For provider compatibility, we strip out `metadata`
before non-OpenAI Responses requests so Azure and AWS Bedrock never see
this field.

My followup PR here will actually make use of it to start storing and
passing along `turn_id`: https://github.com/openai/codex/pull/28360

## What changed

- Added `ResponseItemMetadata` with optional `turn_id`, plus optional
`metadata` on Responses API item variants and inter-agent communication.
- Preserved item metadata through response-item rewrites such as
truncation, missing tool-output synthesis, compaction history
rebuilding, visible-history conversion, rollout/resume, and generated
app-server schemas/types.
- Strip item metadata from non-OpenAI Responses requests while
preserving it for OpenAI-shaped requests.
- Updated the mechanical fixture/test construction churn required by the
new optional field.
2026-06-15 15:05:28 -07:00

333 lines
11 KiB
Rust

use super::CURRENT_THREAD_SECTION_TOKEN_BUDGET;
use super::NOTES_SECTION_TOKEN_BUDGET;
use super::RECENT_WORK_SECTION_TOKEN_BUDGET;
use super::STARTUP_CONTEXT_HEADER;
use super::WORKSPACE_SECTION_TOKEN_BUDGET;
use super::build_current_thread_section;
use super::build_recent_work_section;
use super::build_workspace_section_with_user_root;
use super::format_section;
use super::format_startup_context_blob;
use chrono::TimeZone;
use chrono::Utc;
use codex_git_utils::GitSha;
use codex_protocol::ThreadId;
use codex_protocol::models::ContentItem;
use codex_protocol::models::PermissionProfile;
use codex_protocol::models::ResponseItem;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::GitInfo;
use codex_protocol::protocol::SessionSource;
use codex_thread_store::StoredThread;
use core_test_support::PathBufExt;
use core_test_support::PathExt;
use pretty_assertions::assert_eq;
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
fn stored_thread(cwd: &str, title: &str, first_user_message: &str) -> StoredThread {
StoredThread {
thread_id: ThreadId::new(),
extra_config: None,
rollout_path: Some(PathBuf::from("/tmp/rollout.jsonl")),
forked_from_id: None,
parent_thread_id: None,
preview: first_user_message.to_string(),
name: (!title.is_empty()).then(|| title.to_string()),
model_provider: "test-provider".to_string(),
model: Some("gpt-5.2".to_string()),
reasoning_effort: None,
created_at: Utc
.timestamp_opt(1_709_251_100, 0)
.single()
.expect("valid timestamp"),
updated_at: Utc
.timestamp_opt(1_709_251_200, 0)
.single()
.expect("valid timestamp"),
archived_at: None,
cwd: PathBuf::from(cwd),
cli_version: "test".to_string(),
source: SessionSource::Cli,
thread_source: None,
agent_nickname: None,
agent_role: None,
agent_path: None,
git_info: Some(GitInfo {
commit_hash: Some(GitSha::new("abcdef")),
branch: Some("main".to_string()),
repository_url: None,
}),
approval_mode: AskForApproval::Never,
permission_profile: PermissionProfile::read_only(),
token_usage: None,
first_user_message: Some(first_user_message.to_string()),
history: None,
}
}
fn message(role: &str, content: ContentItem) -> ResponseItem {
ResponseItem::Message {
id: None,
role: role.to_string(),
content: vec![content],
phase: None,
metadata: None,
}
}
fn user_message(text: impl Into<String>) -> ResponseItem {
message("user", ContentItem::InputText { text: text.into() })
}
fn assistant_message(text: impl Into<String>) -> ResponseItem {
message("assistant", ContentItem::OutputText { text: text.into() })
}
fn long_turn_text(index: usize) -> String {
format!(
"turn-{index}-start {} turn-{index}-middle {} turn-{index}-end",
"head filler ".repeat(160),
"tail filler ".repeat(240),
)
}
#[test]
fn current_thread_section_includes_short_turns_newest_first_until_budget() {
let items = vec![
user_message("user turn 1"),
assistant_message("assistant turn 1"),
user_message("user turn 2"),
assistant_message("assistant turn 2"),
user_message("user turn 3"),
assistant_message("assistant turn 3"),
user_message("user turn 4"),
assistant_message("assistant turn 4"),
];
assert_eq!(
build_current_thread_section(&items),
Some(
r#"Most recent user/assistant turns from this exact thread. Use them for continuity when responding.
### Latest turn
User:
user turn 4
Assistant:
assistant turn 4
### Previous turn 1
User:
user turn 3
Assistant:
assistant turn 3
### Previous turn 2
User:
user turn 2
Assistant:
assistant turn 2
### Previous turn 3
User:
user turn 1
Assistant:
assistant turn 1"#
.to_string()
)
);
}
#[test]
fn current_thread_turn_truncation_preserves_start_and_end() {
let items = vec![user_message(long_turn_text(/*index*/ 0))];
let section = build_current_thread_section(&items).expect("current thread section");
assert_eq!(
(
section.contains("turn-0-start"),
section.contains("turn-0-middle"),
section.contains("turn-0-end"),
section.contains("tokens truncated"),
),
(true, false, true, true),
);
}
#[test]
fn current_thread_section_keeps_latest_turns_when_history_exceeds_budget() {
let mut items = Vec::new();
for index in 1..=8 {
items.push(user_message(long_turn_text(index)));
items.push(assistant_message(format!("assistant turn {index}")));
}
let section = build_current_thread_section(&items).expect("current thread section");
assert_eq!(
(
section.contains("turn-8-start"),
section.contains("turn-8-end"),
section.contains("### Previous turn 2"),
section.contains("turn-1-start"),
section.contains("turn-1-end"),
),
(true, true, true, false, false),
);
}
#[test]
fn startup_context_blob_is_wrapped_in_tags_without_final_truncation() {
let body = "Startup context from Codex.\n## Current Thread\nhello";
let wrapped = format_startup_context_blob(body);
assert_eq!(
wrapped,
"<startup_context>\nStartup context from Codex.\n## Current Thread\nhello\n</startup_context>"
);
}
#[test]
fn fixed_section_budgets_apply_per_section_without_total_blob_truncation() {
let body = [
STARTUP_CONTEXT_HEADER.to_string(),
format_section(
"Current Thread",
Some("current thread ".repeat(2_000)),
CURRENT_THREAD_SECTION_TOKEN_BUDGET,
)
.expect("current thread section"),
format_section(
"Recent Work",
Some("recent work ".repeat(3_000)),
RECENT_WORK_SECTION_TOKEN_BUDGET,
)
.expect("recent work section"),
format_section(
"Machine / Workspace Map",
Some("workspace map ".repeat(2_500)),
WORKSPACE_SECTION_TOKEN_BUDGET,
)
.expect("workspace section"),
format_section(
"Notes",
Some("notes ".repeat(500)),
NOTES_SECTION_TOKEN_BUDGET,
)
.expect("notes section"),
]
.join("\n\n");
let wrapped = format_startup_context_blob(&body);
assert!(wrapped.starts_with("<startup_context>\n"));
assert!(wrapped.ends_with("\n</startup_context>"));
assert!(wrapped.contains("tokens truncated"));
assert!(wrapped.contains("## Current Thread"));
assert!(wrapped.contains("## Recent Work"));
assert!(wrapped.contains("## Machine / Workspace Map"));
assert!(wrapped.contains("## Notes"));
}
#[tokio::test]
async fn workspace_section_requires_meaningful_structure() {
let cwd = TempDir::new().expect("tempdir");
assert_eq!(
build_workspace_section_with_user_root(&cwd.path().abs(), /*user_root*/ None).await,
None
);
}
#[tokio::test]
async fn workspace_section_includes_tree_when_entries_exist() {
let cwd = TempDir::new().expect("tempdir");
fs::create_dir(cwd.path().join("docs")).expect("create docs dir");
fs::write(cwd.path().join("README.md"), "hello").expect("write readme");
let section =
build_workspace_section_with_user_root(&cwd.path().abs(), /*user_root*/ None)
.await
.expect("workspace section");
assert!(section.contains("Working directory tree:"));
assert!(section.contains("- docs/"));
assert!(section.contains("- README.md"));
}
#[tokio::test]
async fn workspace_section_includes_user_root_tree_when_distinct() {
let root = TempDir::new().expect("tempdir");
let cwd = root.path().join("cwd");
let git_root = root.path().join("git");
let user_root = root.path().join("home");
fs::create_dir_all(cwd.join("docs")).expect("create cwd docs dir");
fs::write(cwd.join("README.md"), "hello").expect("write cwd readme");
fs::create_dir_all(git_root.join(".git")).expect("create git dir");
fs::write(git_root.join("Cargo.toml"), "[workspace]").expect("write git root marker");
fs::create_dir_all(user_root.join("code")).expect("create user root child");
fs::write(user_root.join(".zshrc"), "export TEST=1").expect("write home file");
let section = build_workspace_section_with_user_root(&cwd.abs(), Some(user_root))
.await
.expect("workspace section");
assert!(section.contains("User root tree:"));
assert!(section.contains("- code/"));
assert!(!section.contains("- .zshrc"));
}
#[tokio::test]
async fn recent_work_section_groups_threads_by_cwd() {
let root = TempDir::new().expect("tempdir");
let repo = root.path().join("repo");
let workspace_a = repo.join("workspace-a");
let workspace_b = repo.join("workspace-b");
let outside = root.path().join("outside");
fs::create_dir(&repo).expect("create repo dir");
Command::new("git")
.env("GIT_CONFIG_GLOBAL", "/dev/null")
.env("GIT_CONFIG_NOSYSTEM", "1")
.args(["init"])
.current_dir(&repo)
.output()
.expect("git init");
fs::create_dir_all(&workspace_a).expect("create workspace a");
fs::create_dir_all(&workspace_b).expect("create workspace b");
fs::create_dir_all(&outside).expect("create outside dir");
let recent_threads = vec![
stored_thread(
workspace_a.to_string_lossy().as_ref(),
"Investigate realtime startup context",
"Log the startup context before sending it",
),
stored_thread(
workspace_b.to_string_lossy().as_ref(),
"Trim websocket startup payload",
"Remove memories from the realtime startup context",
),
stored_thread(outside.to_string_lossy().as_ref(), "", "Inspect flaky test"),
];
let current_cwd = workspace_a;
let repo = repo.abs();
let section = build_recent_work_section(&current_cwd.abs(), &recent_threads)
.await
.expect("recent work section");
assert!(section.contains(&format!("### Git repo: {}", repo.display())));
assert!(section.contains("Recent sessions: 2"));
assert!(section.contains("User asks:"));
assert!(section.contains(&format!(
"- {}: Log the startup context before sending it",
current_cwd.display()
)));
assert!(section.contains(&format!("### Directory: {}", outside.display())));
assert!(section.contains(&format!("- {}: Inspect flaky test", outside.display())));
}