mirror of
https://github.com/openai/codex.git
synced 2026-10-14 16:38:43 +00:00
Move plugin recommendations into developer context (#47349)
## What changed Emit `<recommended_plugins>` with the `developer` role and append it to the shared developer message after other context. Retain recognition of legacy user-role recommendations in existing rollouts. ## Testing Add a startup context snapshot and assertions that recommendations share the developer message with permissions instructions. Update core and app-server tests to check developer-role recommendations, including across successive model requests. GitOrigin-RevId: 0326ff347e86ec38b948f93e39e90e6a88a8730c
This commit is contained in:
@@ -13,6 +13,7 @@ use codex_app_server_protocol::TurnStartParams;
|
||||
use codex_app_server_protocol::UserInput;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::responses;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use std::time::Duration;
|
||||
@@ -170,9 +171,20 @@ async fn recommended_plugins_after_external_login(
|
||||
.any(|text| text.contains("suggest a plugin"))
|
||||
})
|
||||
.expect("turn request");
|
||||
let contextual_user_message = request.message_input_texts("user").join("\n");
|
||||
assert!(contextual_user_message.contains("<recommended_plugins>"));
|
||||
assert!(contextual_user_message.contains("- GitHub (github@openai-curated-remote)"));
|
||||
let recommendations = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<recommended_plugins>"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
recommendations,
|
||||
vec![concat!(
|
||||
"<recommended_plugins>\n",
|
||||
"Here is a list of plugins that are available but not installed.\n\n",
|
||||
"- GitHub (github@openai-curated-remote)\n",
|
||||
"</recommended_plugins>",
|
||||
)]
|
||||
);
|
||||
let body = request.body_json();
|
||||
let tool_names = body
|
||||
.get("tools")
|
||||
|
||||
@@ -27,6 +27,7 @@ const CONTEXTUAL_USER_FRAGMENT_MATCHERS: &[fn(&str) -> bool] = &[
|
||||
TurnAborted::matches_text,
|
||||
SubagentNotification::matches_text,
|
||||
InternalModelContextFragment::matches_text,
|
||||
// compatibility for user-role recommendation messages in existing rollouts
|
||||
RecommendedPluginsInstructions::matches_text,
|
||||
LegacyUnifiedExecProcessLimitWarning::matches_text,
|
||||
LegacyApplyPatchExecCommandWarning::matches_text,
|
||||
|
||||
@@ -32,7 +32,7 @@ impl ContextualUserFragment for RecommendedPluginsInstructions {
|
||||
}
|
||||
|
||||
fn role(&self) -> &'static str {
|
||||
"user"
|
||||
"developer"
|
||||
}
|
||||
|
||||
fn markers(&self) -> (&'static str, &'static str) {
|
||||
|
||||
@@ -4226,12 +4226,6 @@ impl Session {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(recommended_plugins) = recommended_plugin_candidates
|
||||
.as_deref()
|
||||
.and_then(RecommendedPluginsInstructions::from_plugins)
|
||||
{
|
||||
contextual_user_sections.push(recommended_plugins.render_fragment());
|
||||
}
|
||||
let context_contributors = self.services.extensions.context_contributors().to_vec();
|
||||
for contributor in &context_contributors {
|
||||
for fragment in contributor
|
||||
@@ -4360,6 +4354,13 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(recommended_plugins) = recommended_plugin_candidates
|
||||
.as_deref()
|
||||
.and_then(RecommendedPluginsInstructions::from_plugins)
|
||||
{
|
||||
developer_sections.push(recommended_plugins.render_fragment());
|
||||
}
|
||||
|
||||
let mut items = Vec::with_capacity(4);
|
||||
if let Some(developer_message) =
|
||||
crate::context_manager::updates::build_rendered_message(developer_sections)
|
||||
|
||||
@@ -9986,7 +9986,10 @@ async fn build_initial_context_reuses_in_flight_recommendation_prewarm() {
|
||||
|
||||
let (_, initial_context) = tokio::join!(prewarm, initial_context);
|
||||
assert_eq!(
|
||||
user_input_texts(&initial_context),
|
||||
developer_input_texts(&initial_context)
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<recommended_plugins>"))
|
||||
.collect::<Vec<_>>(),
|
||||
vec![concat!(
|
||||
"<recommended_plugins>\n",
|
||||
"Here is a list of plugins that are available but not installed.\n\n",
|
||||
|
||||
@@ -34,6 +34,8 @@ use codex_protocol::protocol::ThreadSettingsOverrides;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use core_test_support::apps_test_server::AppsTestServer;
|
||||
use core_test_support::context_snapshot;
|
||||
use core_test_support::context_snapshot::ContextSnapshotOptions;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
@@ -457,7 +459,7 @@ async fn mount_remote_calendar_installed_plugins(server: &wiremock::MockServer)
|
||||
#[test_case(Feature::RecommendedPlugins, Some(Feature::RemotePlugin); "remote plugins disabled")]
|
||||
#[test_case(Feature::RecommendedPlugins, Some(Feature::RecommendedPlugins); "recommendations disabled")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn startup_recommendations(
|
||||
async fn startup_recommendations_use_developer_message(
|
||||
feature: Feature,
|
||||
disabled_feature: Option<Feature>,
|
||||
) -> Result<()> {
|
||||
@@ -546,16 +548,53 @@ async fn startup_recommendations(
|
||||
|
||||
test.submit_turn("suggest a plugin").await?;
|
||||
|
||||
if feature == Feature::RecommendedPlugins && disabled_feature.is_none() {
|
||||
insta::assert_snapshot!(
|
||||
"startup_recommendations_developer_context",
|
||||
context_snapshot::format_request_history_snapshot(
|
||||
"Startup recommendations share the developer message with other context.",
|
||||
&model_response.requests(),
|
||||
&ContextSnapshotOptions::default().rewrite_known_segments(),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
let request = model_response.single_request();
|
||||
let user_context = request.message_input_texts("user").join("\n");
|
||||
let developer_context = request.message_input_texts("developer").join("\n");
|
||||
assert_eq!(
|
||||
user_context.contains("<recommended_plugins>"),
|
||||
developer_context.contains("<recommended_plugins>"),
|
||||
expect_recommendations,
|
||||
);
|
||||
assert_eq!(
|
||||
user_context.contains("- GitHub (github@openai-curated-remote)"),
|
||||
developer_context.contains("- GitHub (github@openai-curated-remote)"),
|
||||
expect_recommendations,
|
||||
);
|
||||
if expect_recommendations {
|
||||
let developer_messages = request.message_input_text_groups("developer");
|
||||
let recommendation_message = developer_messages
|
||||
.iter()
|
||||
.find(|message| {
|
||||
message
|
||||
.iter()
|
||||
.any(|text| text.starts_with("<recommended_plugins>"))
|
||||
})
|
||||
.expect("developer message with recommendations");
|
||||
assert!(
|
||||
recommendation_message
|
||||
.iter()
|
||||
.any(|text| text.starts_with("<permissions instructions>")),
|
||||
"recommendations should share the developer message with other context"
|
||||
);
|
||||
assert_eq!(
|
||||
recommendation_message.last().map(String::as_str),
|
||||
Some(concat!(
|
||||
"<recommended_plugins>\n",
|
||||
"Here is a list of plugins that are available but not installed.\n\n",
|
||||
"- GitHub (github@openai-curated-remote)\n",
|
||||
"</recommended_plugins>",
|
||||
))
|
||||
);
|
||||
}
|
||||
let tools = tool_names(&request.body_json());
|
||||
assert_eq!(
|
||||
tools
|
||||
@@ -622,7 +661,7 @@ async fn mcp_discovery_overlaps_endpoint_plugin_recommendations() -> Result<()>
|
||||
let request = response.single_request();
|
||||
assert!(
|
||||
request
|
||||
.message_input_texts("user")
|
||||
.message_input_texts("developer")
|
||||
.join("\n")
|
||||
.contains("github@openai-curated-remote"),
|
||||
"the completed request should preserve endpoint recommendations"
|
||||
@@ -733,7 +772,7 @@ async fn unavailable_recommendations_preserve_legacy_workflow(
|
||||
let request = &requests[0];
|
||||
assert!(
|
||||
!request
|
||||
.message_input_texts("user")
|
||||
.message_input_texts("developer")
|
||||
.join("\n")
|
||||
.contains("<recommended_plugins>")
|
||||
);
|
||||
@@ -1028,10 +1067,23 @@ async fn endpoint_mode_injects_candidates_hides_list_and_rejects_invented_ids()
|
||||
|
||||
let requests = mock.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
let contextual_user_message = requests[0].message_input_texts("user").join("\n");
|
||||
assert!(contextual_user_message.contains("<recommended_plugins>"));
|
||||
assert!(contextual_user_message.contains("github@openai-curated-remote"));
|
||||
assert!(contextual_user_message.contains("google-calendar@openai-curated-remote"));
|
||||
for request in &requests {
|
||||
let recommendations = request
|
||||
.message_input_texts("developer")
|
||||
.into_iter()
|
||||
.filter(|text| text.starts_with("<recommended_plugins>"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
recommendations,
|
||||
vec![concat!(
|
||||
"<recommended_plugins>\n",
|
||||
"Here is a list of plugins that are available but not installed.\n\n",
|
||||
"- GitHub (github@openai-curated-remote)\n",
|
||||
"- Google Calendar (google-calendar@openai-curated-remote)\n",
|
||||
"</recommended_plugins>",
|
||||
)]
|
||||
);
|
||||
}
|
||||
let body = requests[0].body_json();
|
||||
let tools = tool_names(&body);
|
||||
assert!(
|
||||
@@ -1435,7 +1487,7 @@ async fn endpoint_mode_with_no_eligible_candidates_exposes_no_suggestion_tools()
|
||||
let request = mock.single_request();
|
||||
assert!(
|
||||
!request
|
||||
.message_input_texts("user")
|
||||
.message_input_texts("developer")
|
||||
.join("\n")
|
||||
.contains("<recommended_plugins>")
|
||||
);
|
||||
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
---
|
||||
source: core/tests/suite/request_plugin_install.rs
|
||||
expression: "context_snapshot::format_request_history_snapshot(\"Startup recommendations share the developer message with other context.\",\n&model_response.requests(),\n&ContextSnapshotOptions::default().rewrite_known_segments(),)"
|
||||
---
|
||||
Scenario: Startup recommendations share the developer message with other context.
|
||||
|
||||
## Window 1
|
||||
-- request 1 (turn) --
|
||||
00:message/developer[3]:
|
||||
[01] <PERMISSIONS_INSTRUCTIONS>
|
||||
[02] <APPS_INSTRUCTIONS>
|
||||
[03] <recommended_plugins>
|
||||
Here is a list of plugins that are available but not installed.
|
||||
|
||||
- GitHub (github@openai-curated-remote)
|
||||
</recommended_plugins>
|
||||
01:message/user:
|
||||
<ENVIRONMENT_CONTEXT>
|
||||
02:message/user:
|
||||
suggest a plugin
|
||||
Reference in New Issue
Block a user