Require content kinds for extension prompt fragments (#40177)

## What changed

- Require each `PromptFragment` to include a producer-owned `ContentItemKind`.
- Re-export `ContentItemKind` from `codex_extension_api` and assign stable kinds to skills, memories, examples, and test contributors.
- Limit prompt slots to developer policy and developer capabilities, and combine contributed fragments into a single developer update.

## Testing

- Update extension registry tests to verify fragment ordering with the new content-kind metadata.

GitOrigin-RevId: ef8624a9b4ae9dce025bca9c0954d91dd8075e85
This commit is contained in:
pakrym-oai
2026-08-23 03:12:58 +00:00
committed by copyberry
parent eff640f458
commit 546eed94ac
9 changed files with 67 additions and 83 deletions
+3
View File
@@ -128,6 +128,9 @@ impl codex_extension_api::ContextContributor for GuardianMemoryContextProbe {
{
vec![codex_extension_api::PromptFragment::developer_policy(
GUARDIAN_MEMORY_CONTEXT_PROBE,
codex_extension_api::ContentItemKind(
"guardian.memory_context_probe".to_string(),
),
)]
} else {
Vec::new()
+6 -60
View File
@@ -62,8 +62,6 @@ use codex_execpolicy::prefix_rule_migration;
use codex_extension_api::ConversationHistorySnapshot;
use codex_extension_api::ExtensionDataInit;
use codex_extension_api::LoadedUserInstructions;
use codex_extension_api::PromptFragment;
use codex_extension_api::PromptSlot;
use codex_extension_api::TurnContextContributionInput;
use codex_features::FEATURES;
use codex_features::Feature;
@@ -999,25 +997,6 @@ async fn thread_title_from_thread_store(
(!title.is_empty() && thread.preview.trim() != title).then(|| title.to_string())
}
fn push_prompt_fragment(
fragment: PromptFragment,
developer_sections: &mut Vec<String>,
contextual_user_sections: &mut Vec<String>,
separate_developer_sections: &mut Vec<String>,
) {
match fragment.slot() {
PromptSlot::DeveloperPolicy | PromptSlot::DeveloperCapabilities => {
developer_sections.push(fragment.text().to_string());
}
PromptSlot::ContextualUser => {
contextual_user_sections.push(fragment.text().to_string());
}
PromptSlot::SeparateDeveloper => {
separate_developer_sections.push(fragment.text().to_string());
}
}
}
impl Session {
pub(crate) async fn app_server_client_metadata(&self) -> AppServerClientMetadata {
let state = self.state.lock().await;
@@ -3486,8 +3465,6 @@ impl Session {
) -> Vec<ResponseItem> {
let turn_context = step_context.turn.as_ref();
let mut developer_sections = Vec::new();
let mut contextual_user_sections = Vec::new();
let mut separate_developer_sections = Vec::new();
let context_contributors = self.services.extensions.context_contributors().to_vec();
for contributor in &context_contributors {
@@ -3502,34 +3479,13 @@ impl Session {
})
.await
{
push_prompt_fragment(
fragment,
&mut developer_sections,
&mut contextual_user_sections,
&mut separate_developer_sections,
);
developer_sections.push(fragment.text().to_string());
}
}
let mut items = Vec::with_capacity(3);
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(developer_sections)
{
items.push(developer_message);
}
for section in separate_developer_sections {
if let Some(developer_message) =
crate::context_manager::updates::build_developer_update_item(vec![section])
{
items.push(developer_message);
}
}
if let Some(contextual_user_message) =
crate::context_manager::updates::build_contextual_user_message(contextual_user_sections)
{
items.push(contextual_user_message);
}
items
crate::context_manager::updates::build_developer_update_item(developer_sections)
.into_iter()
.collect()
}
pub(crate) async fn build_initial_context_with_world_state(
@@ -3598,12 +3554,7 @@ impl Session {
)
.await
{
push_prompt_fragment(
fragment,
&mut developer_sections,
&mut contextual_user_sections,
&mut separate_developer_sections,
);
developer_sections.push(fragment.text().to_string());
}
}
for contributor in &context_contributors {
@@ -3618,12 +3569,7 @@ impl Session {
})
.await
{
push_prompt_fragment(
fragment,
&mut developer_sections,
&mut contextual_user_sections,
&mut separate_developer_sections,
);
developer_sections.push(fragment.text().to_string());
}
}
// This is full-context metadata. Steady-state context diffs should not re-emit it.
+2
View File
@@ -9239,6 +9239,7 @@ impl codex_extension_api::ContextContributor for PromptExtensionTestContributor
.then(|| {
codex_extension_api::PromptFragment::developer_policy(
"prompt extension enabled",
codex_extension_api::ContentItemKind("test.prompt_extension".to_string()),
)
})
.into_iter()
@@ -9271,6 +9272,7 @@ impl codex_extension_api::ContextContributor for TurnContextExtensionTestContrib
.then(|| {
codex_extension_api::PromptFragment::developer_policy(
"turn context extension enabled",
codex_extension_api::ContentItemKind("test.turn_context".to_string()),
)
})
.into_iter()
@@ -2,6 +2,7 @@ use std::sync::Arc;
use std::sync::atomic::AtomicU64;
use std::sync::atomic::Ordering;
use codex_extension_api::ContentItemKind;
use codex_extension_api::ContextContributor;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionRegistryBuilder;
@@ -28,6 +29,7 @@ impl ContextContributor for StyleContributor {
vec![PromptFragment::developer_policy(
"Prefer short answers unless the user asks for detail.",
ContentItemKind("example.style_instructions".to_string()),
)]
})
}
@@ -48,6 +50,7 @@ impl ContextContributor for UsageContributor {
vec![PromptFragment::developer_capability(
"This extension can contribute more than one prompt fragment.",
ContentItemKind("example.usage_instructions".to_string()),
)]
})
}
@@ -1,41 +1,38 @@
// All this file should be replaced by the existing fragment implementation ofc
use codex_protocol::models::ContentItemKind;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum PromptSlot {
DeveloperPolicy,
DeveloperCapabilities,
ContextualUser,
SeparateDeveloper,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PromptFragment {
slot: PromptSlot,
text: String,
content_kind: ContentItemKind,
}
impl PromptFragment {
/// Creates a prompt fragment for the given slot.
pub fn new(slot: PromptSlot, text: impl Into<String>) -> Self {
pub fn new(slot: PromptSlot, text: impl Into<String>, content_kind: ContentItemKind) -> Self {
Self {
slot,
text: text.into(),
content_kind,
}
}
/// Creates a developer-policy prompt fragment.
pub fn developer_policy(text: impl Into<String>) -> Self {
Self::new(PromptSlot::DeveloperPolicy, text)
pub fn developer_policy(text: impl Into<String>, content_kind: ContentItemKind) -> Self {
Self::new(PromptSlot::DeveloperPolicy, text, content_kind)
}
/// Creates a developer-capabilities prompt fragment.
pub fn developer_capability(text: impl Into<String>) -> Self {
Self::new(PromptSlot::DeveloperCapabilities, text)
}
/// Creates a separate top-level developer prompt fragment.
pub fn separate_developer(text: impl Into<String>) -> Self {
Self::new(PromptSlot::SeparateDeveloper, text)
pub fn developer_capability(text: impl Into<String>, content_kind: ContentItemKind) -> Self {
Self::new(PromptSlot::DeveloperCapabilities, text, content_kind)
}
/// Returns the target prompt slot.
@@ -47,4 +44,9 @@ impl PromptFragment {
pub fn text(&self) -> &str {
&self.text
}
/// Returns the producer-owned classification of the model-visible text.
pub fn content_kind(&self) -> &ContentItemKind {
&self.content_kind
}
}
+1
View File
@@ -17,6 +17,7 @@ pub use capabilities::NoopResponseItemInjector;
pub use capabilities::ResponseItemInjectionFuture;
pub use capabilities::ResponseItemInjector;
pub use codex_context_fragments::ContextualUserFragment;
pub use codex_protocol::models::ContentItemKind;
pub use codex_protocol::models::ResponseItem;
pub use codex_protocol::security_risk::SecurityRiskScore;
pub use codex_tools::ConversationHistory;
+24 -9
View File
@@ -5,6 +5,7 @@ use std::sync::Mutex;
use codex_extension_api::ApprovalReviewContributor;
use codex_extension_api::ConfigContributor;
use codex_extension_api::ContentItemKind;
use codex_extension_api::ContextContributor;
use codex_extension_api::ContextualUserFragment;
use codex_extension_api::ExtensionData;
@@ -16,7 +17,6 @@ use codex_extension_api::ExtensionRegistryBuilder;
use codex_extension_api::ExtensionWarning;
use codex_extension_api::McpServerContributionContext;
use codex_extension_api::PromptFragment;
use codex_extension_api::PromptSlot;
use codex_extension_api::SkillInvocationContributor;
use codex_extension_api::ThreadLifecycleContributor;
use codex_extension_api::TokenUsageContributor;
@@ -194,6 +194,7 @@ impl ContextContributor for NamedContextContributor {
) -> ExtensionFuture<'a, Vec<PromptFragment>> {
Box::pin(std::future::ready(vec![PromptFragment::developer_policy(
self.0,
ContentItemKind("test.thread_context".to_string()),
)]))
}
}
@@ -205,10 +206,12 @@ impl ContextContributor for NamedTurnContextContributor {
&'a self,
_input: TurnContextContributionInput<'a>,
) -> ExtensionFuture<'a, Vec<PromptFragment>> {
Box::pin(std::future::ready(vec![PromptFragment::new(
PromptSlot::ContextualUser,
self.0,
)]))
Box::pin(std::future::ready(vec![
PromptFragment::developer_capability(
self.0,
ContentItemKind("test.turn_context".to_string()),
),
]))
}
}
@@ -289,10 +292,22 @@ async fn contributors_preserve_registration_order() {
assert_eq!(
fragments,
vec![
PromptFragment::developer_policy("first"),
PromptFragment::developer_policy("second"),
PromptFragment::new(PromptSlot::ContextualUser, "turn-first"),
PromptFragment::new(PromptSlot::ContextualUser, "turn-second"),
PromptFragment::developer_policy(
"first",
ContentItemKind("test.thread_context".to_string()),
),
PromptFragment::developer_policy(
"second",
ContentItemKind("test.thread_context".to_string()),
),
PromptFragment::developer_capability(
"turn-first",
ContentItemKind("test.turn_context".to_string()),
),
PromptFragment::developer_capability(
"turn-second",
ContentItemKind("test.turn_context".to_string()),
),
]
);
assert_eq!(
+7 -1
View File
@@ -2,6 +2,7 @@ use std::sync::Arc;
use codex_core::config::Config;
use codex_extension_api::ConfigContributor;
use codex_extension_api::ContentItemKind;
use codex_extension_api::ContextContributor;
use codex_extension_api::ExtensionData;
use codex_extension_api::ExtensionFuture;
@@ -63,7 +64,12 @@ impl ContextContributor for MemoriesExtension {
build_memory_tool_developer_instructions(&config.codex_home)
.await
.map(PromptFragment::developer_policy)
.map(|instructions| {
PromptFragment::developer_policy(
instructions,
ContentItemKind("memories.instructions".to_string()),
)
})
.into_iter()
.collect()
})
+7 -1
View File
@@ -9,6 +9,7 @@ use codex_exec_server::FileSystemSandboxContext;
use codex_exec_server::LOCAL_ENVIRONMENT_ID;
use codex_exec_server::ResolvedSelectedCapabilityRoot;
use codex_extension_api::ConfigContributor;
use codex_extension_api::ContentItemKind;
use codex_extension_api::ContextContributor;
use codex_extension_api::ContextualUserFragment;
use codex_extension_api::ExtensionData;
@@ -236,7 +237,12 @@ where
}
rendered
.fragment
.map(|fragment| PromptFragment::developer_capability(fragment.render()))
.map(|fragment| {
PromptFragment::developer_capability(
fragment.render(),
ContentItemKind("skills.catalog".to_string()),
)
})
.into_iter()
.collect()
})