Route extension hints into context-window metadata (#40533)

## What changed

- Add a `ContextWindow` prompt slot for thread-scoped extension contributions.
- Include fragments in that slot in the token-budget context-window message.
- Keep the MCP `notes/thread_hint` bridge when the native history-notes extension is disabled, without falling back to it after a native request failure.

GitOrigin-RevId: befcb7cc8570b846094b2b542a112b186b39a30a
This commit is contained in:
pmccrary-oai
2026-08-25 03:38:24 +00:00
committed by copyberry
parent 9c9675d3d0
commit 3a469a297d
4 changed files with 57 additions and 36 deletions
@@ -15,7 +15,7 @@ pub(crate) struct TokenBudgetContext {
first_window_id: Uuid,
previous_window_id: Option<Uuid>,
window_id: Uuid,
mcp_result: Option<String>,
thread_hint: Option<String>,
}
impl TokenBudgetContext {
@@ -24,14 +24,14 @@ impl TokenBudgetContext {
first_window_id: Uuid,
previous_window_id: Option<Uuid>,
window_id: Uuid,
mcp_result: Option<String>,
thread_hint: Option<String>,
) -> Self {
Self {
agent_path,
first_window_id,
previous_window_id,
window_id,
mcp_result,
thread_hint,
}
}
}
@@ -68,8 +68,8 @@ impl ContextualUserFragment for TokenBudgetContext {
if let Some(previous_window_id) = self.previous_window_id {
lines.push(format!("Previous context window id: {previous_window_id}"));
}
if let Some(mcp_result) = &self.mcp_result {
lines.push(mcp_result.clone());
if let Some(thread_hint) = &self.thread_hint {
lines.push(thread_hint.clone());
}
format!("\n{}\n", lines.join("\n"))
}
+49 -30
View File
@@ -65,6 +65,7 @@ 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::PromptSlot;
use codex_extension_api::TurnContextContributionInput;
use codex_features::FEATURES;
use codex_features::Feature;
@@ -3590,6 +3591,7 @@ impl Session {
let mut developer_sections = Vec::<RenderedFragment>::with_capacity(8);
let mut contextual_user_sections = Vec::<RenderedFragment>::with_capacity(2);
let mut separate_developer_sections = Vec::<RenderedFragment>::new();
let mut context_window_hints = Vec::new();
let (session_source, auto_compact_window_ids) = {
let state = self.state.lock().await;
(
@@ -3649,7 +3651,14 @@ impl Session {
)
.await
{
developer_sections.push(fragment.into());
match fragment.slot() {
PromptSlot::ContextWindow => {
context_window_hints.push(fragment.text().to_string());
}
PromptSlot::DeveloperPolicy | PromptSlot::DeveloperCapabilities => {
developer_sections.push(fragment.into());
}
}
}
}
for contributor in &context_contributors {
@@ -3671,34 +3680,44 @@ impl Session {
if turn_context.config.features.enabled(Feature::TokenBudget)
&& turn_context.model_context_window().is_some()
{
let mcp_result = self
.services
.mcp_runtime
.latest_call_tool(
"notes",
"thread_hint",
/*environment_id*/ None,
/*arguments*/ None,
Some(serde_json::json!({
"threadId": self.thread_id().to_string(),
})),
/*requested_timeout*/ None,
/*wait_for_server*/ true,
)
.await
.ok()
.and_then(|result| {
let text = result
.content
.iter()
.filter_map(|content| {
content.get("text").and_then(serde_json::Value::as_str)
})
.filter(|text| !text.is_empty())
.collect::<Vec<_>>()
.join("\n");
(!text.is_empty()).then_some(text)
});
// Keep the legacy bridge hint when native Notes is disabled. A failed
// native request must not fall back to the bridge.
if !turn_context
.config
.token_budget
.as_ref()
.is_some_and(|config| config.use_history_notes_extension)
&& let Some(mcp_result) = self
.services
.mcp_runtime
.latest_call_tool(
"notes",
"thread_hint",
/*environment_id*/ None,
/*arguments*/ None,
Some(serde_json::json!({
"threadId": self.thread_id().to_string(),
})),
/*requested_timeout*/ None,
/*wait_for_server*/ true,
)
.await
.ok()
.and_then(|result| {
let text = result
.content
.iter()
.filter_map(|content| {
content.get("text").and_then(serde_json::Value::as_str)
})
.filter(|text| !text.is_empty())
.collect::<Vec<_>>()
.join("\n");
(!text.is_empty()).then_some(text)
})
{
context_window_hints.push(mcp_result);
}
separate_developer_sections.push(
crate::context::TokenBudgetContext::new(
session_source
@@ -3707,7 +3726,7 @@ impl Session {
auto_compact_window_ids.first_window_id,
auto_compact_window_ids.previous_window_id,
auto_compact_window_ids.window_id,
mcp_result,
(!context_window_hints.is_empty()).then(|| context_window_hints.join("\n")),
)
.render_fragment(),
);
+1 -1
View File
@@ -116,7 +116,7 @@ impl Session {
window_ids.first_window_id,
window_ids.previous_window_id,
window_ids.window_id,
/*mcp_result*/ None,
/*thread_hint*/ None,
));
if let Some(guidance) = turn_context
.config
@@ -8,6 +8,8 @@ use codex_protocol::models::ContentItemKind;
pub enum PromptSlot {
DeveloperPolicy,
DeveloperCapabilities,
/// Text inside the context-window message, supplied by `contribute_thread_context`.
ContextWindow,
}
#[derive(Clone, Debug, PartialEq, Eq)]