mirror of
https://github.com/openai/codex.git
synced 2026-10-29 19:08:43 +00:00
## Why Generic Apps guidance is emitted only while building static initial context. If the Apps MCP is unavailable then and recovers later in the same turn, its tools can become usable without the model receiving the guidance for using them. ## What - move generic Apps guidance into a persisted `apps_instructions` World State section - derive availability from the request's MCP runtime while preserving the existing feature, auth, orchestrator-MCP, and config gates - recognize legacy and retained Apps fragments so resume and compaction do not duplicate guidance - register Apps guidance as rollback-trimmable context - remove the old static injection path and its now-unused connector helper - keep tool construction on its existing independent `list_all_tools()` read rather than adding a request-wide cache; a reconnect between the two reads can differ for one request and reconciles on the next request Apps and plugin guidance now render after the remaining static host-skills block, with Apps before Plugins. ## Testing - `just fmt` - `just test -p codex-core apps_instructions` - `just test -p codex-core apps_guidance_appears_after_background_recovery_within_a_turn` - `just test -p codex-core drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn`
231 lines
8.1 KiB
Rust
231 lines
8.1 KiB
Rust
use codex_protocol::items::AgentMessageContent;
|
|
use codex_protocol::items::AgentMessageItem;
|
|
use codex_protocol::items::ReasoningItem;
|
|
use codex_protocol::items::TurnItem;
|
|
use codex_protocol::items::UserMessageItem;
|
|
use codex_protocol::items::WebSearchItem;
|
|
use codex_protocol::models::ContentItem;
|
|
use codex_protocol::models::MessagePhase;
|
|
use codex_protocol::models::ReasoningItemContent;
|
|
use codex_protocol::models::ReasoningItemReasoningSummary;
|
|
use codex_protocol::models::ResponseItem;
|
|
use codex_protocol::models::WebSearchAction;
|
|
use codex_protocol::models::is_image_close_tag_text;
|
|
use codex_protocol::models::is_image_open_tag_text;
|
|
use codex_protocol::models::is_local_image_close_tag_text;
|
|
use codex_protocol::models::is_local_image_open_tag_text;
|
|
use codex_protocol::protocol::APPS_INSTRUCTIONS_OPEN_TAG;
|
|
use codex_protocol::protocol::COLLABORATION_MODE_OPEN_TAG;
|
|
use codex_protocol::protocol::CONTEXT_WINDOW_GUIDANCE_OPEN_TAG;
|
|
use codex_protocol::protocol::CONTEXT_WINDOW_OPEN_TAG;
|
|
use codex_protocol::protocol::MULTI_AGENT_MODE_OPEN_TAG;
|
|
use codex_protocol::protocol::PLUGINS_INSTRUCTIONS_OPEN_TAG;
|
|
use codex_protocol::protocol::REALTIME_CONVERSATION_OPEN_TAG;
|
|
use codex_protocol::protocol::SKILLS_INSTRUCTIONS_OPEN_TAG;
|
|
use codex_protocol::user_input::UserInput;
|
|
use tracing::warn;
|
|
use uuid::Uuid;
|
|
|
|
use crate::context::is_contextual_user_fragment;
|
|
use crate::context::parse_visible_hook_prompt_message;
|
|
use crate::web_search::web_search_action_detail;
|
|
|
|
const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
|
|
"<permissions instructions>",
|
|
"<model_switch>",
|
|
APPS_INSTRUCTIONS_OPEN_TAG,
|
|
COLLABORATION_MODE_OPEN_TAG,
|
|
MULTI_AGENT_MODE_OPEN_TAG,
|
|
PLUGINS_INSTRUCTIONS_OPEN_TAG,
|
|
REALTIME_CONVERSATION_OPEN_TAG,
|
|
SKILLS_INSTRUCTIONS_OPEN_TAG,
|
|
"<personality_spec>",
|
|
// Keep recognizing token-budget wrappers persisted by older versions.
|
|
"<token_budget>",
|
|
CONTEXT_WINDOW_OPEN_TAG,
|
|
CONTEXT_WINDOW_GUIDANCE_OPEN_TAG,
|
|
"<rollout_budget>",
|
|
];
|
|
|
|
pub(crate) fn is_contextual_user_message_content(message: &[ContentItem]) -> bool {
|
|
message.iter().any(is_contextual_user_fragment)
|
|
}
|
|
|
|
/// Returns true when a developer message contains any rollback-trimmable contextual fragment.
|
|
///
|
|
/// `build_initial_context` can bundle these fragments together with persistent developer text in a
|
|
/// single developer message, so callers that care about invalidating a stored reference baseline
|
|
/// should pair this with `has_non_contextual_dev_message_content`.
|
|
pub(crate) fn is_contextual_dev_message_content(message: &[ContentItem]) -> bool {
|
|
message.iter().any(is_contextual_dev_fragment)
|
|
}
|
|
|
|
/// Returns true when a developer message contains any fragment that is not part of the
|
|
/// rollback-trimmable contextual prefix set.
|
|
pub(crate) fn has_non_contextual_dev_message_content(message: &[ContentItem]) -> bool {
|
|
message
|
|
.iter()
|
|
.any(|content_item| !is_contextual_dev_fragment(content_item))
|
|
}
|
|
|
|
fn is_contextual_dev_fragment(content_item: &ContentItem) -> bool {
|
|
let ContentItem::InputText { text } = content_item else {
|
|
return false;
|
|
};
|
|
|
|
let trimmed = text.trim_start();
|
|
CONTEXTUAL_DEVELOPER_PREFIXES.iter().any(|prefix| {
|
|
trimmed
|
|
.get(..prefix.len())
|
|
.is_some_and(|candidate| candidate.eq_ignore_ascii_case(prefix))
|
|
})
|
|
}
|
|
|
|
fn parse_user_message(message: &[ContentItem]) -> Option<UserMessageItem> {
|
|
if is_contextual_user_message_content(message) {
|
|
return None;
|
|
}
|
|
|
|
let mut content: Vec<UserInput> = Vec::new();
|
|
|
|
for (idx, content_item) in message.iter().enumerate() {
|
|
match content_item {
|
|
ContentItem::InputText { text } => {
|
|
if (is_local_image_open_tag_text(text) || is_image_open_tag_text(text))
|
|
&& (matches!(message.get(idx + 1), Some(ContentItem::InputImage { .. })))
|
|
|| (idx > 0
|
|
&& (is_local_image_close_tag_text(text) || is_image_close_tag_text(text))
|
|
&& matches!(message.get(idx - 1), Some(ContentItem::InputImage { .. })))
|
|
{
|
|
continue;
|
|
}
|
|
content.push(UserInput::Text {
|
|
text: text.clone(),
|
|
// Model input content does not carry UI element ranges.
|
|
text_elements: Vec::new(),
|
|
});
|
|
}
|
|
ContentItem::InputImage { image_url, detail } => {
|
|
content.push(UserInput::Image {
|
|
image_url: image_url.clone(),
|
|
detail: *detail,
|
|
});
|
|
}
|
|
ContentItem::OutputText { text } => {
|
|
warn!("Output text in user message: {}", text);
|
|
}
|
|
}
|
|
}
|
|
|
|
Some(UserMessageItem::new(&content))
|
|
}
|
|
|
|
fn parse_agent_message(
|
|
id: Option<&String>,
|
|
message: &[ContentItem],
|
|
phase: Option<MessagePhase>,
|
|
) -> AgentMessageItem {
|
|
let mut content: Vec<AgentMessageContent> = Vec::new();
|
|
for content_item in message.iter() {
|
|
match content_item {
|
|
ContentItem::InputText { text } | ContentItem::OutputText { text } => {
|
|
content.push(AgentMessageContent::Text { text: text.clone() });
|
|
}
|
|
_ => {
|
|
warn!(
|
|
"Unexpected content item in agent message: {:?}",
|
|
content_item
|
|
);
|
|
}
|
|
}
|
|
}
|
|
let id = id.cloned().unwrap_or_else(|| Uuid::new_v4().to_string());
|
|
AgentMessageItem {
|
|
id,
|
|
content,
|
|
phase,
|
|
memory_citation: None,
|
|
}
|
|
}
|
|
|
|
pub fn parse_turn_item(item: &ResponseItem) -> Option<TurnItem> {
|
|
match item {
|
|
ResponseItem::Message {
|
|
role,
|
|
content,
|
|
id,
|
|
phase,
|
|
..
|
|
} => match role.as_str() {
|
|
"user" => parse_visible_hook_prompt_message(id.as_ref(), content)
|
|
.map(TurnItem::HookPrompt)
|
|
.or_else(|| parse_user_message(content).map(TurnItem::UserMessage)),
|
|
"assistant" => Some(TurnItem::AgentMessage(parse_agent_message(
|
|
id.as_ref(),
|
|
content,
|
|
phase.clone(),
|
|
))),
|
|
"system" => None,
|
|
_ => None,
|
|
},
|
|
ResponseItem::Reasoning {
|
|
id,
|
|
summary,
|
|
content,
|
|
..
|
|
} => {
|
|
let summary_text = summary
|
|
.iter()
|
|
.map(|entry| match entry {
|
|
ReasoningItemReasoningSummary::SummaryText { text } => text.clone(),
|
|
})
|
|
.collect();
|
|
let raw_content = content
|
|
.clone()
|
|
.unwrap_or_default()
|
|
.into_iter()
|
|
.map(|entry| match entry {
|
|
ReasoningItemContent::ReasoningText { text }
|
|
| ReasoningItemContent::Text { text } => text,
|
|
})
|
|
.collect();
|
|
Some(TurnItem::Reasoning(ReasoningItem {
|
|
id: id.clone().unwrap_or_default(),
|
|
summary_text,
|
|
raw_content,
|
|
}))
|
|
}
|
|
ResponseItem::WebSearchCall { id, action, .. } => {
|
|
let (action, query) = match action {
|
|
Some(action) => (action.clone(), web_search_action_detail(action)),
|
|
None => (WebSearchAction::Other, String::new()),
|
|
};
|
|
Some(TurnItem::WebSearch(WebSearchItem {
|
|
id: id.clone().unwrap_or_default(),
|
|
query,
|
|
action,
|
|
}))
|
|
}
|
|
ResponseItem::ImageGenerationCall {
|
|
id,
|
|
status,
|
|
revised_prompt,
|
|
result,
|
|
..
|
|
} => Some(TurnItem::ImageGeneration(
|
|
codex_protocol::items::ImageGenerationItem {
|
|
id: id.clone()?,
|
|
status: status.clone(),
|
|
revised_prompt: revised_prompt.clone(),
|
|
result: result.clone(),
|
|
saved_path: None,
|
|
},
|
|
)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "event_mapping_tests.rs"]
|
|
mod tests;
|