mirror of
https://github.com/openai/codex.git
synced 2026-09-16 12:13:30 +00:00
## Why Reused call IDs, ambiguous exec/wait associations, and missing history can make tool-call completeness claims unreliable. Truncating one cell's recorded calls should also preserve completeness for unrelated cells. ## What changed - Track observed call and runtime cell IDs with bounded memory, including call IDs from initial history, and withhold completeness when evidence is missing or ambiguous. - Validate exec/wait input-output associations and keep invalidated completeness from returning on retries. Withhold wait completeness after resume or fork, where runtime cell IDs can be reused. - Keep truncation markers and omission counts local to each output, clearing completeness across the affected cell while preserving unrelated cells within the metadata budget. - Refresh recording enablement from runtime configuration without changing execution features, and preserve the live recorder during legacy configuration reloads. ## Testing Add regression coverage for reused IDs, compaction, resumed waits, mismatched wrappers, runtime recording changes, and metadata budgeting that preserves unrelated complete calls. GitOrigin-RevId: 0bc85964bee079879620ec0664eea530a13b61fb
597 lines
22 KiB
Rust
597 lines
22 KiB
Rust
use anyhow::Result;
|
|
use pretty_assertions::assert_eq;
|
|
|
|
use super::super::FunctionCallOutputPayload;
|
|
use super::super::ResponseInputItem;
|
|
use super::*;
|
|
|
|
fn passthrough_metadata(turn_id: &str) -> InternalChatMessageMetadataPassthrough {
|
|
InternalChatMessageMetadataPassthrough {
|
|
turn_id: Some(turn_id.to_string()),
|
|
..Default::default()
|
|
}
|
|
}
|
|
|
|
fn output(call_id: &str) -> ResponseItem {
|
|
ResponseItem::from(ResponseInputItem::FunctionCallOutput {
|
|
call_id: call_id.to_string(),
|
|
output: FunctionCallOutputPayload::from_text(String::new()),
|
|
})
|
|
}
|
|
|
|
fn first_executed_tool_call(item: &mut ResponseItem) -> Option<&mut ExecutedToolCall> {
|
|
item.internal_chat_message_metadata_passthrough_mut()
|
|
.and_then(Option::as_mut)
|
|
.and_then(|metadata| metadata.executed_tool_calls.as_mut())
|
|
.and_then(|calls| calls.first_mut())
|
|
}
|
|
|
|
#[test]
|
|
fn executed_tool_call_prompt_budget_includes_metadata_fields() -> Result<()> {
|
|
let metadata_bytes = |items: &[ResponseItem]| -> Result<usize> {
|
|
items.iter().try_fold(0_usize, |bytes, item| {
|
|
let with_metadata = serde_json::to_vec(item)?.len();
|
|
let mut without_metadata = item.clone();
|
|
without_metadata.clear_executed_tool_calls();
|
|
let without_metadata = serde_json::to_vec(&without_metadata)?.len();
|
|
Ok(bytes + with_metadata.saturating_sub(without_metadata))
|
|
})
|
|
};
|
|
|
|
for with_turn_id in [true, false] {
|
|
let mut items = (0..1_000)
|
|
.map(|index| {
|
|
let mut item = output(&index.to_string());
|
|
*item
|
|
.internal_chat_message_metadata_passthrough_mut()
|
|
.unwrap() = with_turn_id.then(|| passthrough_metadata("turn-1"));
|
|
item.append_executed_tool_calls(vec![ExecutedToolCall::new(
|
|
String::new(),
|
|
serde_json::Value::Null,
|
|
)]);
|
|
item.set_tool_call_cell_id("cell-\"\\");
|
|
item.mark_tool_calls_complete();
|
|
item
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
bound_executed_tool_calls_for_prompt(&mut items);
|
|
assert!(metadata_bytes(&items)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
for metadata in items
|
|
.iter()
|
|
.filter_map(ResponseItem::executed_tool_call_metadata)
|
|
{
|
|
assert_eq!(metadata.tool_calls_complete, None);
|
|
}
|
|
|
|
let calls = items
|
|
.iter()
|
|
.filter_map(ResponseItem::executed_tool_call_metadata)
|
|
.filter_map(|metadata| metadata.executed_tool_calls.as_ref())
|
|
.flatten()
|
|
.collect::<Vec<_>>();
|
|
// Shares smaller than a local marker omit that output; surviving calls stay unchanged.
|
|
assert!(!calls.is_empty() && calls.len() < 1_000);
|
|
assert!(calls.iter().all(|call| {
|
|
**call == ExecutedToolCall::new(String::new(), serde_json::Value::Null)
|
|
}));
|
|
|
|
let bounded_items = items.clone();
|
|
bound_executed_tool_calls_for_prompt(&mut items);
|
|
assert_eq!(items, bounded_items);
|
|
|
|
let oversized_names =
|
|
["\0", "é"].map(|name| name.repeat(MAX_EXECUTED_TOOL_CALL_METADATA_BYTES));
|
|
let mut oversized_items = oversized_names
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(index, name)| {
|
|
let mut item = output(&index.to_string());
|
|
*item
|
|
.internal_chat_message_metadata_passthrough_mut()
|
|
.unwrap() = with_turn_id.then(|| passthrough_metadata("turn-1"));
|
|
item.append_executed_tool_calls(vec![ExecutedToolCall::new(
|
|
name.clone(),
|
|
serde_json::Value::Null,
|
|
)]);
|
|
item.set_tool_call_cell_id("cell-\"\\");
|
|
item
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
bound_executed_tool_calls_for_prompt(&mut oversized_items);
|
|
assert!(metadata_bytes(&oversized_items)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
let calls = oversized_items
|
|
.iter()
|
|
.filter_map(ResponseItem::executed_tool_call_metadata)
|
|
.filter_map(|metadata| metadata.executed_tool_calls.as_ref())
|
|
.flatten()
|
|
.collect::<Vec<_>>();
|
|
assert_eq!(calls.len(), 2);
|
|
for ((item, call), oversized_name) in
|
|
oversized_items.iter().zip(calls).zip(&oversized_names)
|
|
{
|
|
assert_eq!(
|
|
item.executed_tool_call_metadata()
|
|
.unwrap()
|
|
.cell_id
|
|
.as_deref(),
|
|
Some("cell-\"\\"),
|
|
);
|
|
assert!(call.name.len() < oversized_name.len());
|
|
assert!(oversized_name.starts_with(&call.name));
|
|
let truncation = call.truncation().expect("trusted omission marker");
|
|
assert_eq!(truncation.omitted_calls, None);
|
|
assert_eq!(truncation.original_name_bytes, Some(oversized_name.len()));
|
|
assert_eq!(
|
|
serde_json::to_value(&call.arguments)?["_codex_executed_tool_call_truncated"]["original_name_bytes"],
|
|
serde_json::json!(oversized_name.len()),
|
|
);
|
|
}
|
|
|
|
let bounded_items = oversized_items.clone();
|
|
bound_executed_tool_calls_for_prompt(&mut oversized_items);
|
|
assert_eq!(oversized_items, bounded_items);
|
|
}
|
|
|
|
let arguments = serde_json::json!({ "payload": "x".repeat(7 * 1024) });
|
|
let argument_bytes = serde_json::to_vec(&arguments)?.len();
|
|
assert!(argument_bytes < MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES);
|
|
assert!(6 * argument_bytes > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
let mut items = ["A", "wait-A", "B"].map(|call_id| {
|
|
let mut item = output(call_id);
|
|
item.mark_tool_calls_complete();
|
|
item
|
|
});
|
|
for item in &mut items[..2] {
|
|
item.set_tool_call_cell_id("cell-A");
|
|
}
|
|
let calls = vec![ExecutedToolCall::new("test_tool".to_string(), arguments); 6];
|
|
items[0].append_executed_tool_calls(calls.clone());
|
|
items[2].append_executed_tool_calls(vec![ExecutedToolCall::new(
|
|
"test_tool".to_string(),
|
|
serde_json::json!({}),
|
|
)]);
|
|
assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
for bound in [
|
|
bound_executed_tool_calls_for_prompt,
|
|
bound_executed_tool_calls_for_prompt_prioritizing_recent,
|
|
] {
|
|
let mut bounded = items.clone();
|
|
bound(&mut bounded);
|
|
assert!(metadata_bytes(&bounded)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
assert_eq!(bounded[2], items[2]);
|
|
let metadata = bounded[0].executed_tool_call_metadata().unwrap();
|
|
assert_eq!(metadata.tool_calls_complete, None);
|
|
let recorded = metadata.executed_tool_calls.as_ref().unwrap();
|
|
assert_eq!(recorded.len(), 1);
|
|
assert_eq!(recorded[0].name, "test_tool");
|
|
let truncation = recorded[0].truncation().unwrap();
|
|
assert_eq!(truncation.original_bytes, argument_bytes);
|
|
assert_eq!(truncation.omitted_calls, Some(5));
|
|
assert_eq!(
|
|
bounded[1]
|
|
.executed_tool_call_metadata()
|
|
.unwrap()
|
|
.tool_calls_complete,
|
|
None,
|
|
);
|
|
bounded[0].append_executed_tool_calls(calls.clone());
|
|
assert!(metadata_bytes(&bounded)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
bound(&mut bounded);
|
|
assert!(metadata_bytes(&bounded)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
assert_eq!(bounded[2], items[2]);
|
|
assert_eq!(
|
|
first_executed_tool_call(&mut bounded[0])
|
|
.unwrap()
|
|
.truncation()
|
|
.unwrap()
|
|
.omitted_calls,
|
|
Some(11),
|
|
);
|
|
}
|
|
|
|
let mut items = ["exec", "wait"].map(|call_id| {
|
|
let mut item = output(call_id);
|
|
item.set_tool_call_cell_id("cell-\"\\");
|
|
item
|
|
});
|
|
items[0].append_executed_tool_calls(vec![
|
|
ExecutedToolCall::new(
|
|
"test_tool".to_string(),
|
|
serde_json::json!({ "payload": "x".repeat(MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES - 256) }),
|
|
);
|
|
4
|
|
]);
|
|
items[1].mark_tool_calls_complete();
|
|
let expected = items.clone();
|
|
assert!(
|
|
first_executed_tool_call(&mut items[0])
|
|
.expect("recorded call should exist")
|
|
.set_tool_result_sources(ToolResultSources::new(
|
|
(0..MAX_TOOL_RESULT_SOURCES)
|
|
.map(|index| ToolResultSource {
|
|
r#type: "document".to_string(),
|
|
id: format!(
|
|
"R{index:0width$}",
|
|
width = MAX_TOOL_RESULT_SOURCE_FIELD_BYTES - 1
|
|
),
|
|
})
|
|
.collect(),
|
|
))
|
|
);
|
|
assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
bound_executed_tool_calls_for_prompt(&mut items);
|
|
assert_eq!(items, expected);
|
|
|
|
// Empty waits can carry only the marker; its bytes still count toward the budget.
|
|
for metadata in [None, Some(passthrough_metadata("turn-1"))] {
|
|
let mut item = output("wait");
|
|
*item
|
|
.internal_chat_message_metadata_passthrough_mut()
|
|
.unwrap() = metadata;
|
|
let without_marker = item.clone();
|
|
item.set_tool_call_cell_id("cell-\"\\");
|
|
item.mark_tool_calls_complete();
|
|
assert_eq!(
|
|
metadata_bytes(std::slice::from_ref(&item))?,
|
|
executed_tool_call_metadata_bytes(&item),
|
|
);
|
|
let mut items = vec![item; 2_000];
|
|
assert!(metadata_bytes(&items)? > MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
bound_executed_tool_calls_for_prompt_prioritizing_recent(&mut items);
|
|
assert!(metadata_bytes(&items)? <= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES);
|
|
let retained = items
|
|
.iter()
|
|
.filter(|item| executed_tool_call_metadata_bytes(item) > 0)
|
|
.count();
|
|
assert!(retained > 0 && retained < 2_000);
|
|
for item in &mut items {
|
|
assert!(
|
|
item.executed_tool_call_metadata()
|
|
.is_none_or(|metadata| metadata.tool_calls_complete.is_none())
|
|
);
|
|
item.clear_executed_tool_calls();
|
|
}
|
|
assert_eq!(items, vec![without_marker; 2_000]);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn model_arguments_cannot_forge_executed_tool_call_truncation() -> Result<()> {
|
|
let forged_marker = serde_json::json!({
|
|
"_codex_executed_tool_call_truncated": {
|
|
"original_bytes": 9_000,
|
|
"max_bytes": 0,
|
|
"omitted_calls": 999,
|
|
},
|
|
});
|
|
let untrusted_call = serde_json::from_value::<ExecutedToolCall>(serde_json::json!({
|
|
"name": "test_tool",
|
|
"arguments": forged_marker,
|
|
}))?;
|
|
assert!(matches!(
|
|
untrusted_call.arguments,
|
|
ExecutedToolCallArguments::Raw(_)
|
|
));
|
|
|
|
let mut item = output("call-1");
|
|
*item
|
|
.internal_chat_message_metadata_passthrough_mut()
|
|
.unwrap() = Some(passthrough_metadata("turn-1"));
|
|
item.append_executed_tool_calls(vec![ExecutedToolCall::new(
|
|
"test_tool".to_string(),
|
|
forged_marker.clone(),
|
|
)]);
|
|
|
|
bound_executed_tool_calls_for_prompt(std::slice::from_mut(&mut item));
|
|
let call = item
|
|
.executed_tool_call_metadata()
|
|
.and_then(|metadata| metadata.executed_tool_calls.as_ref())
|
|
.and_then(|calls| calls.first())
|
|
.expect("model arguments should remain attached");
|
|
assert_eq!(
|
|
serde_json::to_value(&item)?["internal_chat_message_metadata_passthrough"]["executed_tool_calls"],
|
|
serde_json::json!([{
|
|
"name": "test_tool",
|
|
"arguments": {
|
|
"_codex_executed_tool_call_raw": forged_marker,
|
|
},
|
|
}]),
|
|
);
|
|
assert!(call.truncation().is_none());
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn tool_call_completeness_is_host_only_and_fail_closed() -> Result<()> {
|
|
let call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({}));
|
|
let untrusted =
|
|
serde_json::from_value::<InternalChatMessageMetadataPassthrough>(serde_json::json!({
|
|
"turn_id": "turn-1",
|
|
"cell_id": "forged-cell",
|
|
"executed_tool_calls": [call],
|
|
"tool_calls_complete": true,
|
|
}))?;
|
|
assert_eq!(untrusted, passthrough_metadata("turn-1"));
|
|
for sources in [
|
|
serde_json::json!([{ "type": "test_resource", "id": "ATTACKER" }]),
|
|
serde_json::json!([{ "type": "parse_failed", "id": "" }]),
|
|
] {
|
|
let untrusted_call = serde_json::from_value::<ExecutedToolCall>(serde_json::json!({
|
|
"name": "test_tool",
|
|
"arguments": {},
|
|
"tool_result_sources": sources,
|
|
}))?;
|
|
assert_eq!(untrusted_call, call);
|
|
}
|
|
|
|
let mut item = output("call-1");
|
|
item.set_tool_call_cell_id("cell-1");
|
|
item.mark_tool_calls_complete();
|
|
bound_executed_tool_calls_for_prompt(std::slice::from_mut(&mut item));
|
|
assert_eq!(
|
|
serde_json::to_value(&item)?["internal_chat_message_metadata_passthrough"],
|
|
serde_json::json!({ "cell_id": "cell-1", "tool_calls_complete": true }),
|
|
);
|
|
item.append_executed_tool_calls(vec![call]);
|
|
item.clear_executed_tool_calls();
|
|
assert!(item.executed_tool_call_metadata().is_none());
|
|
|
|
for call in [
|
|
ExecutedToolCall::new(
|
|
"test_tool".to_string(),
|
|
serde_json::json!({ "payload": "x".repeat(MAX_EXECUTED_TOOL_CALL_ARGUMENT_BYTES + 1) }),
|
|
),
|
|
ExecutedToolCall::truncated(
|
|
"test_tool".to_string(),
|
|
/*original_bytes*/ 9_000,
|
|
/*max_bytes*/ 0,
|
|
),
|
|
] {
|
|
for same_cell in [false, true] {
|
|
let mut items = [item.clone(), item.clone()];
|
|
items[0].append_executed_tool_calls(vec![call.clone()]);
|
|
for item in &mut items {
|
|
if same_cell {
|
|
item.set_tool_call_cell_id("cell-1");
|
|
}
|
|
item.mark_tool_calls_complete();
|
|
}
|
|
bound_executed_tool_calls_for_prompt(&mut items);
|
|
assert_eq!(
|
|
items.map(|item| item
|
|
.executed_tool_call_metadata()
|
|
.unwrap()
|
|
.tool_calls_complete),
|
|
[None, (!same_cell).then_some(true)],
|
|
);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn tool_result_source_snapshots_replace_atomically() -> Result<()> {
|
|
let source = |kind: &str, id: &str| ToolResultSource {
|
|
r#type: kind.to_string(),
|
|
id: id.to_string(),
|
|
};
|
|
let mut call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({}));
|
|
let mut sources = (0..MAX_TOOL_RESULT_SOURCES - 1)
|
|
.map(|index| source("test_resource", &format!("R{index}")))
|
|
.collect::<Vec<_>>();
|
|
sources.push(source("other_resource", "R0"));
|
|
sources.push(sources[0].clone());
|
|
let capture = ToolResultSources::new(sources.clone());
|
|
sources.truncate(MAX_TOOL_RESULT_SOURCES);
|
|
assert_eq!(capture, ToolResultSources(Some(sources.clone())));
|
|
assert!(call.set_tool_result_sources(capture));
|
|
sources.push(source("test_resource", "OVERFLOW"));
|
|
let capture = ToolResultSources::new(sources);
|
|
assert_eq!(capture, ToolResultSources(None));
|
|
assert!(!call.set_tool_result_sources(capture));
|
|
assert!(
|
|
serde_json::to_value(&call)?
|
|
.get("tool_result_sources")
|
|
.is_none()
|
|
);
|
|
|
|
// Measure UTF-8 bytes, and clear old evidence instead of keeping a partial replacement.
|
|
let field = format!(
|
|
"é{}",
|
|
"x".repeat(MAX_TOOL_RESULT_SOURCE_FIELD_BYTES - "é".len())
|
|
);
|
|
let bounded = source(&field, &field);
|
|
let oversized = format!("{field}x");
|
|
for invalid in [
|
|
source(&oversized, "R1"),
|
|
source("test_resource", &oversized),
|
|
] {
|
|
assert!(call.set_tool_result_sources(ToolResultSources::new(vec![bounded.clone()])));
|
|
assert_eq!(call.tool_result_sources, Some(vec![bounded.clone()]));
|
|
let capture = ToolResultSources::new(vec![source("test_resource", "R1"), invalid]);
|
|
assert_eq!(capture, ToolResultSources(None));
|
|
assert!(!call.set_tool_result_sources(capture));
|
|
assert_eq!(call.tool_result_sources, None);
|
|
}
|
|
|
|
assert!(call.set_tool_result_sources(ToolResultSources::new(vec![bounded])));
|
|
assert!(call.set_tool_result_sources(ToolResultSources::parse_failed()));
|
|
assert_eq!(
|
|
serde_json::to_value(&call)?,
|
|
serde_json::json!({
|
|
"name": "test_tool",
|
|
"arguments": {},
|
|
"tool_result_sources": [{ "type": "parse_failed", "id": "" }],
|
|
})
|
|
);
|
|
assert!(call.set_tool_result_sources(ToolResultSources::new(Vec::new())));
|
|
assert_eq!(
|
|
serde_json::to_value(&call)?["tool_result_sources"],
|
|
serde_json::json!([])
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn tool_result_metadata_is_host_only_bounded_and_redacted() -> Result<()> {
|
|
let mut call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({}));
|
|
let without_metadata = call.clone();
|
|
for metadata in [
|
|
serde_json::json!({
|
|
"arbitrary-key": { "secret": "not-for-debug", "ids": [1, "é", null] },
|
|
"other": false,
|
|
}),
|
|
serde_json::json!({}),
|
|
serde_json::json!(null),
|
|
] {
|
|
let capture = ToolResultMetadata::new(&metadata);
|
|
assert_eq!(format!("{capture:?}"), "ToolResultMetadata([redacted])");
|
|
assert!(capture.is_some());
|
|
call.set_tool_result_metadata(capture);
|
|
let wire = serde_json::to_value(&call)?;
|
|
assert_eq!(
|
|
wire,
|
|
serde_json::json!({
|
|
"name": "test_tool",
|
|
"arguments": {},
|
|
"tool_result_metadata": metadata,
|
|
})
|
|
);
|
|
assert!(!format!("{call:?}").contains("not-for-debug"));
|
|
assert_eq!(
|
|
serde_json::from_value::<ExecutedToolCall>(wire)?,
|
|
without_metadata
|
|
);
|
|
}
|
|
|
|
let exact_limit = serde_json::json!({
|
|
"value": "x".repeat(MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - r#"{"value":""}"#.len()),
|
|
});
|
|
let capture = ToolResultMetadata::new(&exact_limit);
|
|
assert!(capture.is_some());
|
|
call.set_tool_result_metadata(capture);
|
|
assert_eq!(
|
|
serde_json::to_value(&call)?["tool_result_metadata"],
|
|
exact_limit
|
|
);
|
|
// JSON escaping counts toward the bound, not just the in-memory string length.
|
|
let oversized =
|
|
serde_json::json!({ "value": "\0".repeat(MAX_EXECUTED_TOOL_CALL_METADATA_BYTES / 2) });
|
|
let capture = ToolResultMetadata::new(&oversized);
|
|
assert!(capture.is_some());
|
|
call.set_tool_result_metadata(capture);
|
|
let wire = serde_json::to_value(&call)?;
|
|
assert_eq!(
|
|
wire,
|
|
serde_json::json!({
|
|
"name": "test_tool",
|
|
"arguments": {},
|
|
"tool_result_metadata": "omitted_due_to_size_limit",
|
|
})
|
|
);
|
|
assert_eq!(
|
|
serde_json::from_value::<ExecutedToolCall>(wire)?,
|
|
without_metadata
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn raw_result_metadata_is_shed_before_sources_calls_or_completion() -> Result<()> {
|
|
let mut call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({}));
|
|
call.set_tool_result_sources(ToolResultSources::new(vec![ToolResultSource {
|
|
r#type: "test_resource".to_string(),
|
|
id: "R1".to_string(),
|
|
}]));
|
|
let mut items = ["first", "second", "without-metadata"].map(|call_id| {
|
|
let mut item = ResponseItem::from(ResponseInputItem::FunctionCallOutput {
|
|
call_id: call_id.to_string(),
|
|
output: FunctionCallOutputPayload::from_text("unchanged output".to_string()),
|
|
});
|
|
item.append_executed_tool_calls(vec![call.clone()]);
|
|
item.set_tool_call_cell_id(call_id);
|
|
item.mark_tool_calls_complete();
|
|
item
|
|
});
|
|
let mut expected = items.clone();
|
|
let metadata = ToolResultMetadata::new(&serde_json::json!({ "large": "x".repeat(20_000) }));
|
|
for item in &mut items[..2] {
|
|
first_executed_tool_call(item)
|
|
.unwrap()
|
|
.set_tool_result_metadata(metadata.clone());
|
|
}
|
|
for item in &mut expected[..2] {
|
|
first_executed_tool_call(item)
|
|
.unwrap()
|
|
.set_tool_result_metadata(ToolResultMetadata::new(&serde_json::json!(
|
|
"omitted_due_to_size_limit"
|
|
)));
|
|
}
|
|
assert!(
|
|
items
|
|
.iter()
|
|
.map(executed_tool_call_metadata_bytes)
|
|
.sum::<usize>()
|
|
> MAX_EXECUTED_TOOL_CALL_METADATA_BYTES
|
|
);
|
|
bound_executed_tool_calls_for_prompt(&mut items);
|
|
assert_eq!(items, expected);
|
|
assert!(
|
|
items
|
|
.iter()
|
|
.map(executed_tool_call_metadata_bytes)
|
|
.sum::<usize>()
|
|
<= MAX_EXECUTED_TOOL_CALL_METADATA_BYTES
|
|
);
|
|
bound_executed_tool_calls_for_prompt(&mut items);
|
|
assert_eq!(items, expected);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn result_metadata_markers_do_not_displace_existing_evidence() {
|
|
let mut item = ResponseItem::from(ResponseInputItem::FunctionCallOutput {
|
|
call_id: "exec".to_string(),
|
|
output: FunctionCallOutputPayload::from_text("unchanged output".to_string()),
|
|
});
|
|
let mut call = ExecutedToolCall::new("test_tool".to_string(), serde_json::json!({}));
|
|
call.set_tool_result_sources(ToolResultSources::new(vec![ToolResultSource {
|
|
r#type: "test_resource".to_string(),
|
|
id: "R1".to_string(),
|
|
}]));
|
|
item.append_executed_tool_calls(vec![call; 4]);
|
|
item.set_tool_call_cell_id("exec");
|
|
item.mark_tool_calls_complete();
|
|
let remaining_bytes =
|
|
MAX_EXECUTED_TOOL_CALL_METADATA_BYTES - executed_tool_call_metadata_bytes(&item);
|
|
let calls = item
|
|
.ensure_tool_call_metadata()
|
|
.unwrap()
|
|
.executed_tool_calls
|
|
.as_mut()
|
|
.unwrap();
|
|
for (index, call) in calls.iter_mut().enumerate() {
|
|
// Fill the budget exactly, with every argument still below its individual limit.
|
|
call.arguments = ExecutedToolCallArguments::Raw(serde_json::json!(
|
|
"x".repeat(remaining_bytes / 4 + usize::from(index < remaining_bytes % 4))
|
|
));
|
|
}
|
|
assert_eq!(
|
|
executed_tool_call_metadata_bytes(&item),
|
|
MAX_EXECUTED_TOOL_CALL_METADATA_BYTES
|
|
);
|
|
let expected = item.clone();
|
|
first_executed_tool_call(&mut item)
|
|
.unwrap()
|
|
.set_tool_result_metadata(ToolResultMetadata::new(
|
|
&serde_json::json!({ "too-large": "x".repeat(32 * 1024) }),
|
|
));
|
|
bound_executed_tool_calls_for_prompt(std::slice::from_mut(&mut item));
|
|
assert_eq!(item, expected);
|
|
}
|