diff --git a/codex-rs/context-fragments/src/additional_context.rs b/codex-rs/context-fragments/src/additional_context.rs index d1c5147d2f..71b286b560 100644 --- a/codex-rs/context-fragments/src/additional_context.rs +++ b/codex-rs/context-fragments/src/additional_context.rs @@ -1,3 +1,4 @@ +use codex_protocol::models::ContentItemKind; use codex_utils_string::truncate_middle_with_token_budget; use crate::ContextualUserFragment; @@ -23,6 +24,10 @@ impl ContextualUserFragment for AdditionalContextUserFragment { "user" } + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("generic.user_additional_context".to_string()) + } + fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() } @@ -69,6 +74,10 @@ impl ContextualUserFragment for AdditionalContextDeveloperFragment { "developer" } + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("generic.developer_additional_context".to_string()) + } + fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() } diff --git a/codex-rs/context-fragments/src/annotated_content.rs b/codex-rs/context-fragments/src/annotated_content.rs new file mode 100644 index 0000000000..f4fe6358b8 --- /dev/null +++ b/codex-rs/context-fragments/src/annotated_content.rs @@ -0,0 +1,36 @@ +use codex_protocol::models::ContentItem; +use codex_protocol::models::ContentItemKind; + +/// Model-visible content paired with its harness-owned classification. +#[derive(Clone, Debug, PartialEq)] +pub struct AnnotatedContent { + content: ContentItem, + kind: ContentItemKind, +} + +impl AnnotatedContent { + /// Creates content and its classification together. + pub fn new(content: ContentItem, kind: ContentItemKind) -> Self { + Self { content, kind } + } + + /// Creates model-visible input text and its classification together. + pub fn input_text(text: impl Into, kind: ContentItemKind) -> Self { + Self::new(ContentItem::InputText { text: text.into() }, kind) + } + + /// Returns the model-visible content. + pub fn content(&self) -> &ContentItem { + &self.content + } + + /// Returns the classification associated with the content. + pub fn kind(&self) -> &ContentItemKind { + &self.kind + } + + /// Separates the content from its classification at an API boundary. + pub fn into_parts(self) -> (ContentItem, ContentItemKind) { + (self.content, self.kind) + } +} diff --git a/codex-rs/context-fragments/src/fragment.rs b/codex-rs/context-fragments/src/fragment.rs index f7fb6821f6..2ee321ef3a 100644 --- a/codex-rs/context-fragments/src/fragment.rs +++ b/codex-rs/context-fragments/src/fragment.rs @@ -1,7 +1,33 @@ +use crate::AnnotatedContent; use codex_protocol::models::ContentItem; +use codex_protocol::models::ContentItemKind; use codex_protocol::models::ResponseInputItem; use codex_protocol::models::ResponseItem; +/// A rendered contextual fragment and the role that owns its annotated content. +#[derive(Clone, Debug, PartialEq)] +pub struct RenderedFragment { + role: &'static str, + content: AnnotatedContent, +} + +impl RenderedFragment { + /// Returns the response role associated with this fragment. + pub fn role(&self) -> &'static str { + self.role + } + + /// Returns this fragment's model-visible content and classification. + pub fn annotated_content(&self) -> &AnnotatedContent { + &self.content + } + + /// Separates the role and annotated content at an API boundary. + pub fn into_parts(self) -> (&'static str, AnnotatedContent) { + (self.role, self.content) + } +} + /// Context payload that is injected as a message fragment. /// /// Implementations own the response role and provide the exact fragment body. @@ -14,6 +40,9 @@ use codex_protocol::models::ResponseItem; pub trait ContextualUserFragment { fn role(&self) -> &'static str; + /// Returns a stable `.` classification, using `generic` for shared fragments. + fn content_kind(&self) -> ContentItemKind; + /// Whether this fragment must be recorded as its own response item. fn requires_separate_message(&self) -> bool { false @@ -45,6 +74,14 @@ pub trait ContextualUserFragment { format!("{start_marker}{body}{end_marker}") } + /// Renders the role, model-visible content, and classification together. + fn render_fragment(&self) -> RenderedFragment { + RenderedFragment { + role: self.role(), + content: AnnotatedContent::input_text(self.render(), self.content_kind()), + } + } + fn into(self) -> ResponseItem where Self: Sized, diff --git a/codex-rs/context-fragments/src/lib.rs b/codex-rs/context-fragments/src/lib.rs index 0c189fbfb5..224968bf13 100644 --- a/codex-rs/context-fragments/src/lib.rs +++ b/codex-rs/context-fragments/src/lib.rs @@ -1,6 +1,9 @@ mod additional_context; +mod annotated_content; mod fragment; pub use additional_context::AdditionalContextDeveloperFragment; pub use additional_context::AdditionalContextUserFragment; +pub use annotated_content::AnnotatedContent; pub use fragment::ContextualUserFragment; +pub use fragment::RenderedFragment; diff --git a/codex-rs/core/src/context/approved_command_prefix_saved.rs b/codex-rs/core/src/context/approved_command_prefix_saved.rs index 176df83035..c4b7324631 100644 --- a/codex-rs/core/src/context/approved_command_prefix_saved.rs +++ b/codex-rs/core/src/context/approved_command_prefix_saved.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; pub(crate) const APPROVED_COMMAND_PREFIX_SAVED_MESSAGE_PREFIX: &str = "Approved command prefix saved:"; @@ -17,6 +18,10 @@ impl ApprovedCommandPrefixSaved { } impl ContextualUserFragment for ApprovedCommandPrefixSaved { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("permissions.approved_command_prefix_saved".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/apps_instructions.rs b/codex-rs/core/src/context/apps_instructions.rs index 71d836d4ea..be0cee5598 100644 --- a/codex-rs/core/src/context/apps_instructions.rs +++ b/codex-rs/core/src/context/apps_instructions.rs @@ -3,11 +3,16 @@ use codex_protocol::protocol::APPS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct AppsInstructions; impl ContextualUserFragment for AppsInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("apps.instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/available_plugins_instructions.rs b/codex-rs/core/src/context/available_plugins_instructions.rs index a31d61d7fd..e9f31bc12d 100644 --- a/codex-rs/core/src/context/available_plugins_instructions.rs +++ b/codex-rs/core/src/context/available_plugins_instructions.rs @@ -2,11 +2,16 @@ use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct AvailablePluginsInstructions; impl ContextualUserFragment for AvailablePluginsInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("plugins.usage_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/current_time_reminder.rs b/codex-rs/core/src/context/current_time_reminder.rs index 7b6b61da5c..e70bd0c9d7 100644 --- a/codex-rs/core/src/context/current_time_reminder.rs +++ b/codex-rs/core/src/context/current_time_reminder.rs @@ -2,6 +2,7 @@ use chrono::DateTime; use chrono::Utc; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; pub(crate) struct CurrentTimeReminder { current_time: DateTime, @@ -20,6 +21,10 @@ impl CurrentTimeReminder { } impl ContextualUserFragment for CurrentTimeReminder { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("current_time.reminder".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/environments_instructions.rs b/codex-rs/core/src/context/environments_instructions.rs index 7a4cfe676e..b613a62a01 100644 --- a/codex-rs/core/src/context/environments_instructions.rs +++ b/codex-rs/core/src/context/environments_instructions.rs @@ -2,10 +2,15 @@ use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::ENVIRONMENTS_INSTRUCTIONS_OPEN_TAG; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; pub(crate) struct EnvironmentsInstructions; impl ContextualUserFragment for EnvironmentsInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("environments.instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/guardian_followup_review_reminder.rs b/codex-rs/core/src/context/guardian_followup_review_reminder.rs index cb3569c67a..4cbb86f4a9 100644 --- a/codex-rs/core/src/context/guardian_followup_review_reminder.rs +++ b/codex-rs/core/src/context/guardian_followup_review_reminder.rs @@ -1,9 +1,14 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct GuardianFollowupReviewReminder; impl ContextualUserFragment for GuardianFollowupReviewReminder { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("guardian.followup_review_reminder".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/guardian_node_repl_policy.rs b/codex-rs/core/src/context/guardian_node_repl_policy.rs index 0c43f6ed9b..0353cf0826 100644 --- a/codex-rs/core/src/context/guardian_node_repl_policy.rs +++ b/codex-rs/core/src/context/guardian_node_repl_policy.rs @@ -1,9 +1,14 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) struct GuardianNodeReplPolicy; impl ContextualUserFragment for GuardianNodeReplPolicy { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("guardian.node_repl_policy".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/guardian_review_evidence.rs b/codex-rs/core/src/context/guardian_review_evidence.rs index a216b3e7b5..7acaadefec 100644 --- a/codex-rs/core/src/context/guardian_review_evidence.rs +++ b/codex-rs/core/src/context/guardian_review_evidence.rs @@ -8,6 +8,7 @@ use serde_json::json; use super::ContextualUserFragment; use crate::codex_thread::GuardianAuthorizationVersion; use crate::guardian::guardian_truncate_text; +use codex_protocol::models::ContentItemKind; const MAX_RETAINED_REVIEWS: usize = 8; // Including markers, each rendered fragment stays below 1,000 approximate tokens. @@ -108,6 +109,10 @@ pub struct GuardianReviewEvidenceFragment { } impl ContextualUserFragment for GuardianReviewEvidenceFragment { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("guardian.review_evidence".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/hook_additional_context.rs b/codex-rs/core/src/context/hook_additional_context.rs index bd23468440..9fe2bcc299 100644 --- a/codex-rs/core/src/context/hook_additional_context.rs +++ b/codex-rs/core/src/context/hook_additional_context.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct HookAdditionalContext { @@ -12,6 +13,10 @@ impl HookAdditionalContext { } impl ContextualUserFragment for HookAdditionalContext { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("hooks.additional_context".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/image_resize_notice.rs b/codex-rs/core/src/context/image_resize_notice.rs index e9248aa1b9..53f98d4a04 100644 --- a/codex-rs/core/src/context/image_resize_notice.rs +++ b/codex-rs/core/src/context/image_resize_notice.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ImageResizeNoticeSource { @@ -32,6 +33,10 @@ impl ImageResizeNotice { } impl ContextualUserFragment for ImageResizeNotice { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("images.resize_notice".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/inter_agent_completion_message.rs b/codex-rs/core/src/context/inter_agent_completion_message.rs index b31e27e1ad..ec04c4f1b5 100644 --- a/codex-rs/core/src/context/inter_agent_completion_message.rs +++ b/codex-rs/core/src/context/inter_agent_completion_message.rs @@ -1,6 +1,7 @@ use codex_protocol::AgentPath; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct InterAgentCompletionMessage { @@ -20,6 +21,10 @@ impl InterAgentCompletionMessage { } impl ContextualUserFragment for InterAgentCompletionMessage { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("multi_agent.inter_agent_completion_message".to_string()) + } + fn role(&self) -> &'static str { "assistant" } diff --git a/codex-rs/core/src/context/inter_agent_message.rs b/codex-rs/core/src/context/inter_agent_message.rs index 6ce06527f4..006a02cbea 100644 --- a/codex-rs/core/src/context/inter_agent_message.rs +++ b/codex-rs/core/src/context/inter_agent_message.rs @@ -1,6 +1,7 @@ use codex_protocol::AgentPath; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum InterAgentMessageType { @@ -42,6 +43,10 @@ impl InterAgentMessage { } impl ContextualUserFragment for InterAgentMessage { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("multi_agent.inter_agent_message".to_string()) + } + fn role(&self) -> &'static str { "assistant" } diff --git a/codex-rs/core/src/context/internal_model_context.rs b/codex-rs/core/src/context/internal_model_context.rs index cbba3ecd75..e8b4ee0907 100644 --- a/codex-rs/core/src/context/internal_model_context.rs +++ b/codex-rs/core/src/context/internal_model_context.rs @@ -1,6 +1,7 @@ //! Hidden user-context fragment for extension-owned model steering. use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; use std::error::Error; use std::fmt; @@ -76,6 +77,10 @@ impl InternalModelContextFragment { } impl ContextualUserFragment for InternalModelContextFragment { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("generic.internal_model_context".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/legacy_apply_patch_exec_command_warning.rs b/codex-rs/core/src/context/legacy_apply_patch_exec_command_warning.rs index c764a8838a..c6b5db24c6 100644 --- a/codex-rs/core/src/context/legacy_apply_patch_exec_command_warning.rs +++ b/codex-rs/core/src/context/legacy_apply_patch_exec_command_warning.rs @@ -1,10 +1,15 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; // This warning is not produced anymore but fragment definition is used to filter messaged from old sessions #[derive(Debug, Clone, PartialEq)] pub(crate) struct LegacyApplyPatchExecCommandWarning; impl ContextualUserFragment for LegacyApplyPatchExecCommandWarning { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("apply_patch.legacy_exec_command_warning".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/legacy_model_mismatch_warning.rs b/codex-rs/core/src/context/legacy_model_mismatch_warning.rs index d713993c90..7f69bb02e7 100644 --- a/codex-rs/core/src/context/legacy_model_mismatch_warning.rs +++ b/codex-rs/core/src/context/legacy_model_mismatch_warning.rs @@ -1,10 +1,15 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; // This warning is not produced anymore but fragment definition is used to filter messaged from old sessions #[derive(Debug, Clone, PartialEq)] pub(crate) struct LegacyModelMismatchWarning; impl ContextualUserFragment for LegacyModelMismatchWarning { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("model_switch.legacy_mismatch_warning".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/legacy_unified_exec_process_limit_warning.rs b/codex-rs/core/src/context/legacy_unified_exec_process_limit_warning.rs index 59fe03a2d8..1d11e11dec 100644 --- a/codex-rs/core/src/context/legacy_unified_exec_process_limit_warning.rs +++ b/codex-rs/core/src/context/legacy_unified_exec_process_limit_warning.rs @@ -1,10 +1,15 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; // This warning is not produced anymore but fragment definition is used to filter messaged from old sessions #[derive(Debug, Clone, PartialEq)] pub(crate) struct LegacyUnifiedExecProcessLimitWarning; impl ContextualUserFragment for LegacyUnifiedExecProcessLimitWarning { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("unified_exec.legacy_process_limit_warning".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/model_switch_instructions.rs b/codex-rs/core/src/context/model_switch_instructions.rs index 3b86943c66..3c121e5314 100644 --- a/codex-rs/core/src/context/model_switch_instructions.rs +++ b/codex-rs/core/src/context/model_switch_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct ModelSwitchInstructions { @@ -14,6 +15,10 @@ impl ModelSwitchInstructions { } impl ContextualUserFragment for ModelSwitchInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("model_switch.instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/multi_agent_mode_instructions.rs b/codex-rs/core/src/context/multi_agent_mode_instructions.rs index 552124c604..a03f125e3e 100644 --- a/codex-rs/core/src/context/multi_agent_mode_instructions.rs +++ b/codex-rs/core/src/context/multi_agent_mode_instructions.rs @@ -1,5 +1,6 @@ use super::ContextualUserFragment; use codex_protocol::config_types::MultiAgentMode; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::MULTI_AGENT_MODE_CLOSE_TAG; use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG; @@ -25,6 +26,10 @@ impl MultiAgentModeInstructions { } impl ContextualUserFragment for MultiAgentModeInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("multi_agent.mode_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/multi_agent_role_instructions.rs b/codex-rs/core/src/context/multi_agent_role_instructions.rs index 66c96afd59..6979b6cbd9 100644 --- a/codex-rs/core/src/context/multi_agent_role_instructions.rs +++ b/codex-rs/core/src/context/multi_agent_role_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct MultiAgentRoleInstructions { @@ -23,6 +24,10 @@ impl MultiAgentRoleInstructions { } impl ContextualUserFragment for MultiAgentRoleInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("multi_agent.role_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/multi_agent_usage_hint.rs b/codex-rs/core/src/context/multi_agent_usage_hint.rs index e205d1a217..7739b51af1 100644 --- a/codex-rs/core/src/context/multi_agent_usage_hint.rs +++ b/codex-rs/core/src/context/multi_agent_usage_hint.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; /// Configured multi-agent instructions emitted as a standalone developer message. #[derive(Clone, Debug, PartialEq, Eq)] @@ -15,6 +16,10 @@ impl MultiAgentUsageHint { } impl ContextualUserFragment for MultiAgentUsageHint { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("multi_agent.usage_hint".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/network_rule_saved.rs b/codex-rs/core/src/context/network_rule_saved.rs index 4826027007..5826c41096 100644 --- a/codex-rs/core/src/context/network_rule_saved.rs +++ b/codex-rs/core/src/context/network_rule_saved.rs @@ -1,6 +1,7 @@ use super::ContextualUserFragment; use codex_protocol::approvals::NetworkPolicyAmendment; use codex_protocol::approvals::NetworkPolicyRuleAction; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct NetworkRuleSaved { @@ -18,6 +19,10 @@ impl NetworkRuleSaved { } impl ContextualUserFragment for NetworkRuleSaved { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("network_proxy.rule_saved".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/node_repl_review_evidence.rs b/codex-rs/core/src/context/node_repl_review_evidence.rs index e68b17dc5b..3732196491 100644 --- a/codex-rs/core/src/context/node_repl_review_evidence.rs +++ b/codex-rs/core/src/context/node_repl_review_evidence.rs @@ -6,6 +6,7 @@ use std::sync::PoisonError; use codex_features::Feature; use codex_protocol::models::ContentItem; +use codex_protocol::models::ContentItemKind; use codex_protocol::user_input::UserInput; use codex_protocol::user_input::UserInput::Image; use codex_protocol::user_input::UserInput::Text; @@ -358,6 +359,10 @@ impl NodeReplReviewEvidenceFragment { } impl ContextualUserFragment for NodeReplReviewEvidenceFragment { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("guardian.node_repl_review_evidence".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/personality_spec_instructions.rs b/codex-rs/core/src/context/personality_spec_instructions.rs index 8ab92bfcdd..856a4e0b54 100644 --- a/codex-rs/core/src/context/personality_spec_instructions.rs +++ b/codex-rs/core/src/context/personality_spec_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct PersonalitySpecInstructions { @@ -12,6 +13,10 @@ impl PersonalitySpecInstructions { } impl ContextualUserFragment for PersonalitySpecInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("personality.spec_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/plugin_instructions.rs b/codex-rs/core/src/context/plugin_instructions.rs index be2ac8ec09..dab4f60abd 100644 --- a/codex-rs/core/src/context/plugin_instructions.rs +++ b/codex-rs/core/src/context/plugin_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct PluginInstructions { @@ -12,6 +13,10 @@ impl PluginInstructions { } impl ContextualUserFragment for PluginInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("plugins.instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/realtime_delegation.rs b/codex-rs/core/src/context/realtime_delegation.rs index ba3a92ce87..eba3ee70f4 100644 --- a/codex-rs/core/src/context/realtime_delegation.rs +++ b/codex-rs/core/src/context/realtime_delegation.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; const MAX_REALTIME_DELEGATION_FIELD_BYTES: usize = 4 * 1024; const TRUNCATION_MARKER: &str = "…"; @@ -31,6 +32,10 @@ impl<'a> RealtimeDelegation<'a> { } impl ContextualUserFragment for RealtimeDelegation<'_> { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("realtime_conversation.delegation".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/realtime_end_instructions.rs b/codex-rs/core/src/context/realtime_end_instructions.rs index 872e4dd90c..b658b2a39e 100644 --- a/codex-rs/core/src/context/realtime_end_instructions.rs +++ b/codex-rs/core/src/context/realtime_end_instructions.rs @@ -1,5 +1,6 @@ use super::ContextualUserFragment; use codex_prompts::END_INSTRUCTIONS; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::REALTIME_CONVERSATION_CLOSE_TAG; use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG; @@ -21,6 +22,10 @@ impl RealtimeEndInstructions { } impl ContextualUserFragment for RealtimeEndInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("realtime_conversation.end_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/realtime_start_instructions.rs b/codex-rs/core/src/context/realtime_start_instructions.rs index 074f1a9789..615ee95093 100644 --- a/codex-rs/core/src/context/realtime_start_instructions.rs +++ b/codex-rs/core/src/context/realtime_start_instructions.rs @@ -1,5 +1,6 @@ use super::ContextualUserFragment; use codex_prompts::START_INSTRUCTIONS; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::REALTIME_CONVERSATION_CLOSE_TAG; use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG; @@ -7,6 +8,10 @@ use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG; pub(crate) struct RealtimeStartInstructions; impl ContextualUserFragment for RealtimeStartInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("realtime_conversation.start_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/realtime_start_with_instructions.rs b/codex-rs/core/src/context/realtime_start_with_instructions.rs index a61130969d..bc41164511 100644 --- a/codex-rs/core/src/context/realtime_start_with_instructions.rs +++ b/codex-rs/core/src/context/realtime_start_with_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::REALTIME_CONVERSATION_CLOSE_TAG; use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG; @@ -16,6 +17,10 @@ impl RealtimeStartWithInstructions { } impl ContextualUserFragment for RealtimeStartWithInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("realtime_conversation.custom_start_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/recommended_plugins_instructions.rs b/codex-rs/core/src/context/recommended_plugins_instructions.rs index b8f4b998c2..829c56fb61 100644 --- a/codex-rs/core/src/context/recommended_plugins_instructions.rs +++ b/codex-rs/core/src/context/recommended_plugins_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; use codex_tools::DiscoverableTool; const RECOMMENDED_PLUGINS_INTRO: &str = @@ -26,6 +27,10 @@ impl RecommendedPluginsInstructions { } impl ContextualUserFragment for RecommendedPluginsInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("plugins.recommendations".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/rollout_budget.rs b/codex-rs/core/src/context/rollout_budget.rs index 33ed724b8e..9b717da2b7 100644 --- a/codex-rs/core/src/context/rollout_budget.rs +++ b/codex-rs/core/src/context/rollout_budget.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct RolloutBudgetContext { @@ -6,6 +7,10 @@ pub(crate) struct RolloutBudgetContext { } impl ContextualUserFragment for RolloutBudgetContext { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("rollout_budget.remaining_tokens".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/subagent_notification.rs b/codex-rs/core/src/context/subagent_notification.rs index 6d92b976b9..b243cea5ad 100644 --- a/codex-rs/core/src/context/subagent_notification.rs +++ b/codex-rs/core/src/context/subagent_notification.rs @@ -1,3 +1,4 @@ +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::AgentStatus; use super::ContextualUserFragment; @@ -18,6 +19,10 @@ impl SubagentNotification { } impl ContextualUserFragment for SubagentNotification { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("multi_agent.subagent_notification".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/token_budget_context.rs b/codex-rs/core/src/context/token_budget_context.rs index b74db69cf3..c52993b3a9 100644 --- a/codex-rs/core/src/context/token_budget_context.rs +++ b/codex-rs/core/src/context/token_budget_context.rs @@ -2,6 +2,7 @@ use super::ContextualUserFragment; use super::world_state::PreviousSectionState; use super::world_state::WorldStateSection; use codex_protocol::AgentPath; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::CONTEXT_WINDOW_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_CLOSE_TAG; use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG; @@ -36,6 +37,10 @@ impl TokenBudgetContext { } impl ContextualUserFragment for TokenBudgetContext { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("token_budget.context_window".to_string()) + } + fn role(&self) -> &'static str { "developer" } @@ -101,6 +106,10 @@ impl ContextWindowGuidance { } impl ContextualUserFragment for ContextWindowGuidance { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("token_budget.context_window_guidance".to_string()) + } + fn role(&self) -> &'static str { "developer" } @@ -139,6 +148,10 @@ impl TokenBudgetRemainingContext { } impl ContextualUserFragment for TokenBudgetRemainingContext { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("token_budget.remaining_tokens".to_string()) + } + fn role(&self) -> &'static str { "developer" } @@ -175,6 +188,10 @@ impl TokenBudgetReminder { } impl ContextualUserFragment for TokenBudgetReminder { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("token_budget.reminder".to_string()) + } + fn role(&self) -> &'static str { "developer" } @@ -206,6 +223,10 @@ impl AutoCompactFallbackPrompt { } impl ContextualUserFragment for AutoCompactFallbackPrompt { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("compaction.auto_fallback_prompt".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/turn_aborted.rs b/codex-rs/core/src/context/turn_aborted.rs index c2ef156b34..9d350c1354 100644 --- a/codex-rs/core/src/context/turn_aborted.rs +++ b/codex-rs/core/src/context/turn_aborted.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct TurnAborted { @@ -17,6 +18,10 @@ impl TurnAborted { } impl ContextualUserFragment for TurnAborted { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("generic.turn_aborted".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/user_instructions.rs b/codex-rs/core/src/context/user_instructions.rs index 5c4e211834..f75cb24d57 100644 --- a/codex-rs/core/src/context/user_instructions.rs +++ b/codex-rs/core/src/context/user_instructions.rs @@ -1,4 +1,5 @@ use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct UserInstructions { @@ -7,6 +8,10 @@ pub(crate) struct UserInstructions { } impl ContextualUserFragment for UserInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("agents_md.instructions".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/user_shell_command.rs b/codex-rs/core/src/context/user_shell_command.rs index 377342e502..cdc96f6412 100644 --- a/codex-rs/core/src/context/user_shell_command.rs +++ b/codex-rs/core/src/context/user_shell_command.rs @@ -1,6 +1,7 @@ use std::time::Duration; use super::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; #[derive(Debug, Clone, PartialEq)] pub(crate) struct UserShellCommand { @@ -27,6 +28,10 @@ impl UserShellCommand { } impl ContextualUserFragment for UserShellCommand { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("shell.user_command".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/world_state/collaboration_mode.rs b/codex-rs/core/src/context/world_state/collaboration_mode.rs index b0c1611505..9963ad7aa9 100644 --- a/codex-rs/core/src/context/world_state/collaboration_mode.rs +++ b/codex-rs/core/src/context/world_state/collaboration_mode.rs @@ -4,6 +4,7 @@ use super::WorldStateSection; use crate::context::ContextualUserFragment; use codex_protocol::config_types::CollaborationMode; use codex_protocol::config_types::ModeKind; +use codex_protocol::models::ContentItemKind; use codex_protocol::openai_models::CollaborationModeMessages; use codex_protocol::protocol::COLLABORATION_MODE_CLOSE_TAG; use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG; @@ -125,6 +126,10 @@ struct CollaborationModeInstructions { } impl ContextualUserFragment for CollaborationModeInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("collaboration_mode.instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/world_state/environment.rs b/codex-rs/core/src/context/world_state/environment.rs index e7dffc1b31..7a1edd68c6 100644 --- a/codex-rs/core/src/context/world_state/environment.rs +++ b/codex-rs/core/src/context/world_state/environment.rs @@ -6,6 +6,7 @@ use crate::context::environment_context::NetworkContext; use crate::context::environment_context::push_xml_escaped_text; use crate::environment_selection::TurnEnvironmentSnapshot; use crate::session::turn_context::TurnContext; +use codex_protocol::models::ContentItemKind; use codex_utils_path_uri::PathUri; use serde::Deserialize; use serde::Serialize; @@ -154,6 +155,10 @@ impl WorldStateSection for EnvironmentsState { } impl ContextualUserFragment for EnvironmentsState { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("environments.environment_context".to_string()) + } + fn role(&self) -> &'static str { "user" } @@ -188,6 +193,10 @@ enum EnvironmentUpdate { } impl ContextualUserFragment for RenderedEnvironments { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("environments.environment_context".to_string()) + } + fn role(&self) -> &'static str { "user" } diff --git a/codex-rs/core/src/context/world_state/managed_developer_instructions.rs b/codex-rs/core/src/context/world_state/managed_developer_instructions.rs index 797ec28f05..665596290e 100644 --- a/codex-rs/core/src/context/world_state/managed_developer_instructions.rs +++ b/codex-rs/core/src/context/world_state/managed_developer_instructions.rs @@ -3,6 +3,7 @@ use super::WorldStateHash; use super::WorldStateSection; use crate::context::ContextualUserFragment; use codex_config::Sourced; +use codex_protocol::models::ContentItemKind; use codex_utils_string::approx_bytes_for_tokens; use codex_utils_string::approx_tokens_from_byte_count; use serde::Deserialize; @@ -21,6 +22,10 @@ pub(crate) struct ManagedDeveloperInstructions { } impl ContextualUserFragment for ManagedDeveloperInstructions { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("managed_config.developer_instructions".to_string()) + } + fn role(&self) -> &'static str { "developer" } diff --git a/codex-rs/core/src/context/world_state/mod.rs b/codex-rs/core/src/context/world_state/mod.rs index 9cfafeaa37..2ebd0ee450 100644 --- a/codex-rs/core/src/context/world_state/mod.rs +++ b/codex-rs/core/src/context/world_state/mod.rs @@ -22,6 +22,7 @@ use codex_extension_api::PreviousWorldStateSection; use codex_extension_api::RenderedWorldStateFragment; use codex_extension_api::WorldStateSectionContribution; use codex_protocol::models::ContentItem; +use codex_protocol::models::ContentItemKind; use codex_protocol::models::ResponseItem; use indexmap::IndexMap; use serde::Deserialize; @@ -167,25 +168,35 @@ impl ErasedWorldStateSection for ExtensionWorldStateSection { PreviousSectionState::Unknown => PreviousWorldStateSection::Unknown, PreviousSectionState::Known(previous) => PreviousWorldStateSection::Known(previous), }; - self.0 - .render_diff(previous) - .map(|fragment| Box::new(WorldStateContextFragment(fragment)) as _) + self.0.render_diff(previous).map(|fragment| { + Box::new(WorldStateContextFragment { + fragment, + content_kind: ContentItemKind(format!("{}.instructions", self.0.id())), + }) as _ + }) } } -struct WorldStateContextFragment(RenderedWorldStateFragment); +struct WorldStateContextFragment { + fragment: RenderedWorldStateFragment, + content_kind: ContentItemKind, +} impl ContextualUserFragment for WorldStateContextFragment { + fn content_kind(&self) -> ContentItemKind { + self.content_kind.clone() + } + fn role(&self) -> &'static str { - self.0.role() + self.fragment.role() } fn markers(&self) -> (&'static str, &'static str) { - self.0.markers() + self.fragment.markers() } fn body(&self) -> String { - self.0.body().to_string() + self.fragment.body().to_string() } fn type_markers() -> (&'static str, &'static str) { diff --git a/codex-rs/core/src/context/world_state/tools.rs b/codex-rs/core/src/context/world_state/tools.rs index e385fb6d83..f43c3420fd 100644 --- a/codex-rs/core/src/context/world_state/tools.rs +++ b/codex-rs/core/src/context/world_state/tools.rs @@ -4,6 +4,7 @@ use super::WorldStateSection; use crate::context::ContextualUserFragment; use crate::context::environment_context::push_xml_escaped_text; use codex_extension_api::RenderedWorldStateFragment; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::TOOLS_CLOSE_TAG; use codex_protocol::protocol::TOOLS_OPEN_TAG; use std::collections::BTreeMap; @@ -97,9 +98,14 @@ impl WorldStateSection for ToolsState { ) } }; - Some(Box::new(WorldStateContextFragment( - RenderedWorldStateFragment::new("developer", (TOOLS_OPEN_TAG, TOOLS_CLOSE_TAG), body), - ))) + Some(Box::new(WorldStateContextFragment { + fragment: RenderedWorldStateFragment::new( + "developer", + (TOOLS_OPEN_TAG, TOOLS_CLOSE_TAG), + body, + ), + content_kind: ContentItemKind("tools.deferred_namespaces".to_string()), + })) } } diff --git a/codex-rs/core/src/context/world_state/world_state_tests.rs b/codex-rs/core/src/context/world_state/world_state_tests.rs index 7f6f629791..0fa309bdbd 100644 --- a/codex-rs/core/src/context/world_state/world_state_tests.rs +++ b/codex-rs/core/src/context/world_state/world_state_tests.rs @@ -1,4 +1,6 @@ use super::*; +use codex_context_fragments::AnnotatedContent; +use codex_protocol::models::ContentItemKind; use pretty_assertions::assert_eq; use serde::Deserialize; use serde::Serialize; @@ -36,6 +38,10 @@ impl WorldStateSection for TestSection { struct TestFragment(String); impl ContextualUserFragment for TestFragment { + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("generic.test".to_string()) + } + fn role(&self) -> &'static str { "user" } @@ -157,6 +163,38 @@ fn extension_owned_section_uses_its_snapshot_and_renderer() { ); } +#[test] +fn extension_owned_section_uses_its_stable_id_as_content_kind_feature() { + let mut world_state = WorldState::default(); + world_state.add_extension_section(WorldStateSectionContribution::new( + "extension_test", + json!({"value": "after"}), + |_| { + Some(RenderedWorldStateFragment::new( + "developer", + ("", ""), + "after", + )) + }, + )); + + let rendered = world_state.render_diff(&WorldStateSnapshot::default()); + + assert_eq!( + rendered + .into_iter() + .map(|fragment| fragment.render_fragment().into_parts()) + .collect::>(), + vec![( + "developer", + AnnotatedContent::input_text( + "after", + ContentItemKind("extension_test.instructions".to_string()), + ), + )] + ); +} + #[test] fn missing_retained_fragment_is_rendered_again() { let mut world_state = WorldState::default(); diff --git a/codex-rs/ext/skills/src/extension.rs b/codex-rs/ext/skills/src/extension.rs index 9041951b72..f492fdc84e 100644 --- a/codex-rs/ext/skills/src/extension.rs +++ b/codex-rs/ext/skills/src/extension.rs @@ -9,7 +9,6 @@ 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; @@ -238,10 +237,7 @@ where rendered .fragment .map(|fragment| { - PromptFragment::developer_capability( - fragment.render(), - ContentItemKind("skills.catalog".to_string()), - ) + PromptFragment::developer_capability(fragment.render(), fragment.content_kind()) }) .into_iter() .collect() diff --git a/codex-rs/ext/skills/src/fragments.rs b/codex-rs/ext/skills/src/fragments.rs index daf74bc04d..0e2e3018db 100644 --- a/codex-rs/ext/skills/src/fragments.rs +++ b/codex-rs/ext/skills/src/fragments.rs @@ -1,4 +1,5 @@ use codex_extension_api::ContextualUserFragment; +use codex_protocol::models::ContentItemKind; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_CLOSE_TAG; use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG; @@ -40,6 +41,10 @@ impl ContextualUserFragment for AvailableSkillsInstructions { "developer" } + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("skills.catalog".to_string()) + } + fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() } @@ -73,6 +78,10 @@ impl ContextualUserFragment for SkillInstructions { "user" } + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("skills.selected_skill_instructions".to_string()) + } + fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() } diff --git a/codex-rs/prompts/src/permissions_instructions.rs b/codex-rs/prompts/src/permissions_instructions.rs index 82f1ffc400..f3dc736a4b 100644 --- a/codex-rs/prompts/src/permissions_instructions.rs +++ b/codex-rs/prompts/src/permissions_instructions.rs @@ -2,6 +2,7 @@ use codex_context_fragments::ContextualUserFragment; use codex_execpolicy::Policy; use codex_protocol::config_types::ApprovalsReviewer; use codex_protocol::config_types::SandboxMode; +use codex_protocol::models::ContentItemKind; use codex_protocol::models::PermissionProfile; use codex_protocol::models::format_allow_prefixes; use codex_protocol::openai_models::ApprovalMessages; @@ -177,6 +178,10 @@ impl ContextualUserFragment for PermissionsInstructions { "developer" } + fn content_kind(&self) -> ContentItemKind { + ContentItemKind("generic.permissions_instructions".to_string()) + } + fn markers(&self) -> (&'static str, &'static str) { Self::type_markers() }