mirror of
https://github.com/openai/codex.git
synced 2026-05-16 09:12:54 +00:00
merge hook developer context messages
This commit is contained in:
@@ -13,8 +13,8 @@ impl HookAdditionalContext {
|
||||
|
||||
impl ContextualUserFragment for HookAdditionalContext {
|
||||
const ROLE: &'static str = "developer";
|
||||
const START_MARKER: &'static str = "";
|
||||
const END_MARKER: &'static str = "";
|
||||
const START_MARKER: &'static str = "<hook_context>";
|
||||
const END_MARKER: &'static str = "</hook_context>";
|
||||
|
||||
fn body(&self) -> String {
|
||||
self.text.clone()
|
||||
|
||||
@@ -902,6 +902,7 @@ fn drop_last_n_user_turns_trims_context_updates_above_rolled_back_turn() {
|
||||
user_input_text_msg("turn 1 user"),
|
||||
assistant_msg("turn 1 assistant"),
|
||||
developer_msg("Generated images are saved to /tmp as /tmp/image-1.png by default."),
|
||||
developer_msg("<hook_context>ROLLED_BACK_HOOK_CONTEXT</hook_context>"),
|
||||
developer_msg("<collaboration_mode>ROLLED_BACK_DEV_INSTRUCTIONS</collaboration_mode>"),
|
||||
user_input_text_msg(
|
||||
"<environment_context><cwd>PRETURN_CONTEXT_DIFF_CWD</cwd></environment_context>",
|
||||
|
||||
@@ -30,6 +30,7 @@ const CONTEXTUAL_DEVELOPER_PREFIXES: &[&str] = &[
|
||||
COLLABORATION_MODE_OPEN_TAG,
|
||||
REALTIME_CONVERSATION_OPEN_TAG,
|
||||
"<personality_spec>",
|
||||
"<hook_context>",
|
||||
];
|
||||
|
||||
pub(crate) fn is_contextual_user_message_content(message: &[ContentItem]) -> bool {
|
||||
|
||||
@@ -33,6 +33,7 @@ use serde_json::Value;
|
||||
|
||||
use crate::context::ContextualUserFragment;
|
||||
use crate::context::HookAdditionalContext;
|
||||
use crate::context_manager::updates::build_developer_update_item;
|
||||
use crate::event_mapping::parse_turn_item;
|
||||
use crate::session::session::Session;
|
||||
use crate::session::turn_context::TurnContext;
|
||||
@@ -452,11 +453,13 @@ pub(crate) async fn record_additional_contexts(
|
||||
}
|
||||
|
||||
fn additional_context_messages(additional_contexts: Vec<String>) -> Vec<ResponseItem> {
|
||||
additional_contexts
|
||||
let sections = additional_contexts
|
||||
.into_iter()
|
||||
.map(HookAdditionalContext::new)
|
||||
.map(ContextualUserFragment::into)
|
||||
.collect()
|
||||
.map(|context| context.render())
|
||||
.collect();
|
||||
|
||||
build_developer_update_item(sections).into_iter().collect()
|
||||
}
|
||||
|
||||
async fn emit_hook_started_events(
|
||||
@@ -616,36 +619,39 @@ mod tests {
|
||||
use codex_utils_absolute_path::test_support::test_path_buf;
|
||||
|
||||
#[test]
|
||||
fn additional_context_messages_stay_separate_and_ordered() {
|
||||
fn additional_context_messages_merge_and_preserve_order() {
|
||||
let messages = additional_context_messages(vec![
|
||||
"first tide note".to_string(),
|
||||
"second tide note".to_string(),
|
||||
]);
|
||||
|
||||
assert_eq!(messages.len(), 2);
|
||||
assert_eq!(messages.len(), 1);
|
||||
assert_eq!(
|
||||
messages
|
||||
.iter()
|
||||
.map(|message| match message {
|
||||
codex_protocol::models::ResponseItem::Message { role, content, .. } => {
|
||||
let text = content
|
||||
let texts = content
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
ContentItem::InputText { text } => text.as_str(),
|
||||
ContentItem::InputText { text } => text.clone(),
|
||||
ContentItem::InputImage { .. } | ContentItem::OutputText { .. } => {
|
||||
panic!("expected input text content, got {item:?}")
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
(role.as_str(), text)
|
||||
.collect::<Vec<_>>();
|
||||
(role.as_str(), texts)
|
||||
}
|
||||
other => panic!("expected developer message, got {other:?}"),
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![
|
||||
("developer", "first tide note".to_string()),
|
||||
("developer", "second tide note".to_string()),
|
||||
],
|
||||
vec![(
|
||||
"developer",
|
||||
vec![
|
||||
"<hook_context>first tide note</hook_context>".to_string(),
|
||||
"<hook_context>second tide note</hook_context>".to_string(),
|
||||
],
|
||||
)],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -180,6 +180,50 @@ else:
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_parallel_session_start_hooks(home: &Path, contexts: &[&str]) -> Result<()> {
|
||||
let hook_entries = contexts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, context)| {
|
||||
let script_path = home.join(format!("session_start_hook_{index}.py"));
|
||||
let context_json =
|
||||
serde_json::to_string(context).context("serialize session start context")?;
|
||||
let script = format!(
|
||||
r#"import json
|
||||
|
||||
print(json.dumps({{
|
||||
"hookSpecificOutput": {{
|
||||
"hookEventName": "SessionStart",
|
||||
"additionalContext": {context_json}
|
||||
}}
|
||||
}}))
|
||||
"#
|
||||
);
|
||||
fs::write(&script_path, script).with_context(|| {
|
||||
format!(
|
||||
"write session start hook script fixture at {}",
|
||||
script_path.display()
|
||||
)
|
||||
})?;
|
||||
Ok(serde_json::json!({
|
||||
"type": "command",
|
||||
"command": format!("python3 {}", script_path.display()),
|
||||
}))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
|
||||
let hooks = serde_json::json!({
|
||||
"hooks": {
|
||||
"SessionStart": [{
|
||||
"hooks": hook_entries,
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
fs::write(home.join("hooks.json"), hooks.to_string()).context("write hooks.json")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_user_prompt_submit_hook(
|
||||
home: &Path,
|
||||
blocked_prompt: &str,
|
||||
@@ -1064,6 +1108,56 @@ async fn session_start_hook_spills_large_additional_context() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn parallel_session_start_additional_contexts_share_one_developer_message() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let response = mount_sse_once(
|
||||
&server,
|
||||
sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_assistant_message("msg-1", "merged hook context observed"),
|
||||
ev_completed("resp-1"),
|
||||
]),
|
||||
)
|
||||
.await;
|
||||
let contexts = ["first tide note", "second tide note"];
|
||||
|
||||
let mut builder = test_codex()
|
||||
.with_pre_build_hook(move |home| {
|
||||
if let Err(error) = write_parallel_session_start_hooks(home, &contexts) {
|
||||
panic!("failed to write parallel session start hook fixtures: {error}");
|
||||
}
|
||||
})
|
||||
.with_config(trust_discovered_hooks);
|
||||
let test = builder.build(&server).await?;
|
||||
|
||||
test.submit_turn("hello").await?;
|
||||
|
||||
let request = response.single_request();
|
||||
let merged_developer_messages = request
|
||||
.input()
|
||||
.iter()
|
||||
.filter(|item| item.get("role").and_then(Value::as_str) == Some("developer"))
|
||||
.filter(|item| {
|
||||
let joined = item["content"]
|
||||
.as_array()
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.filter_map(|content| content.get("text").and_then(Value::as_str))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
contexts
|
||||
.iter()
|
||||
.all(|context| joined.contains(&format!("<hook_context>{context}</hook_context>")))
|
||||
})
|
||||
.count();
|
||||
assert_eq!(merged_developer_messages, 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stop_hook_spills_large_continuation_prompt() -> Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
Reference in New Issue
Block a user