mirror of
https://github.com/openai/codex.git
synced 2026-11-08 20:40:33 +00:00
Support typed content in contextual prompt fragments (#54776)
## Why Contextual fragments and extension-owned world-state sections currently return strings, limiting them to text content. ## What changed - Return `ContentItem` from fragment bodies and rendering, preserving classification and attribution. Apply text markers only to `InputText`; pass other content through unchanged. - Allow extension prompt and world-state contributions to carry typed content, and preserve media in context-window hints. - Hash non-text world-state fragments by their serialized content and role while retaining the existing text hashing behavior. - Add shared content-to-tool-output conversion that preserves image references, image detail, and audio. ## Testing Add coverage for typed world-state hashing and preservation of image content and attribution. Expand code-mode output tests to cover input text, output text, file-backed and inline images, and audio. GitOrigin-RevId: 2ed00d466a3f169b151202f3eef71e667ea64be1
This commit is contained in:
@@ -27,6 +27,7 @@ use codex_protocol::models::LocalShellExecAction;
|
||||
use codex_protocol::models::LocalShellStatus;
|
||||
use codex_protocol::models::ReasoningItemContent;
|
||||
use codex_protocol::models::ReasoningItemReasoningSummary;
|
||||
use codex_protocol::models::input_text;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::default_input_modalities;
|
||||
@@ -37,6 +38,7 @@ use codex_protocol::protocol::InterAgentCommunication;
|
||||
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::protocol::TurnContextItem;
|
||||
use codex_protocol_test_support::ContentItemTestExt;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_output_truncation::TruncationPolicy;
|
||||
use codex_utils_output_truncation::truncate_text;
|
||||
@@ -601,7 +603,7 @@ fn world_state_transitions_persist_changed_state_and_skip_unchanged_state() {
|
||||
return (None, None);
|
||||
}
|
||||
(Some(snapshot), Some(RenderedWorldStateFragment::new(
|
||||
"developer", ("<test>", "</test>"), value,
|
||||
"developer", ("<test>", "</test>"), input_text(value.to_string()),
|
||||
)))
|
||||
}));
|
||||
state
|
||||
@@ -615,7 +617,7 @@ fn world_state_transitions_persist_changed_state_and_skip_unchanged_state() {
|
||||
let WorldStateUpdateContent::Fragment(fragment) = &fragments[0].content else {
|
||||
panic!("expected a text fragment");
|
||||
};
|
||||
assert_eq!(fragment.body(), "after");
|
||||
assert_eq!(fragment.body().expect_text(), "after");
|
||||
let patch = patch.expect("changed snapshot must be persisted");
|
||||
assert!(!patch.full);
|
||||
let mut replayed = WorldStateSnapshot::from(&full.state);
|
||||
@@ -646,7 +648,7 @@ fn world_state_reconciles_matching_legacy_history_once() {
|
||||
fragments
|
||||
.into_iter()
|
||||
.map(|update| match update.content {
|
||||
WorldStateUpdateContent::Fragment(fragment) => fragment.body(),
|
||||
WorldStateUpdateContent::Fragment(fragment) => fragment.body().expect_text(),
|
||||
WorldStateUpdateContent::Item(item) =>
|
||||
panic!("expected a text fragment, got {item:?}"),
|
||||
})
|
||||
@@ -1943,7 +1945,9 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() {
|
||||
#[test]
|
||||
fn drop_last_n_user_turns_preserves_annotations_for_surviving_developer_fragments() {
|
||||
let turn_id = "rolled-back-turn";
|
||||
let model_switch = ModelSwitchInstructions::new("switched model instructions").render();
|
||||
let model_switch = ModelSwitchInstructions::new("switched model instructions")
|
||||
.render()
|
||||
.expect_text();
|
||||
let developer_message = ResponseItem::Message {
|
||||
status: None,
|
||||
encrypted_content: None,
|
||||
|
||||
@@ -372,11 +372,8 @@ pub(crate) fn strip_images_when_unsupported(
|
||||
for content_item in content_items.iter() {
|
||||
match content_item {
|
||||
FunctionCallOutputContentItem::InputImage { .. } => {
|
||||
normalized_content_items.push(
|
||||
FunctionCallOutputContentItem::InputText {
|
||||
text: UnsupportedMedia::IMAGE.render(),
|
||||
},
|
||||
);
|
||||
normalized_content_items
|
||||
.push(UnsupportedMedia::IMAGE.render().into());
|
||||
}
|
||||
_ => normalized_content_items.push(content_item.clone()),
|
||||
}
|
||||
@@ -423,9 +420,7 @@ pub(crate) fn strip_audio_when_unsupported(
|
||||
content_item,
|
||||
FunctionCallOutputContentItem::InputAudio { .. }
|
||||
) {
|
||||
*content_item = FunctionCallOutputContentItem::InputText {
|
||||
text: UnsupportedMedia::AUDIO.render(),
|
||||
};
|
||||
*content_item = UnsupportedMedia::AUDIO.render().into();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::context::ContextualUserFragment;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::ContentItemKind;
|
||||
use codex_protocol::models::InternalChatMessageMetadataPassthrough;
|
||||
use codex_protocol::models::input_text;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -31,8 +32,8 @@ impl ContextualUserFragment for TestFragment {
|
||||
("", "")
|
||||
}
|
||||
|
||||
fn body(&self) -> String {
|
||||
self.text.to_string()
|
||||
fn body(&self) -> ContentItem {
|
||||
input_text(self.text.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user