Skip to content

Commit 38fdc27

Browse files
charley-oaicodex
andcommitted
Move subagent context to developer envelope
Co-authored-by: Codex <noreply@openai.com>
1 parent 6da91e2 commit 38fdc27

12 files changed

+85
-71
lines changed

codex-rs/core/src/agent/control.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,7 @@ impl AgentControl {
430430
return;
431431
};
432432
parent_thread
433-
.inject_user_message_without_turn(format_subagent_notification_message(
433+
.inject_developer_message_without_turn(format_subagent_notification_message(
434434
&child_thread_id.to_string(),
435435
&status,
436436
))
@@ -560,7 +560,7 @@ mod tests {
560560
let ResponseItem::Message { role, content, .. } = item else {
561561
return false;
562562
};
563-
if role != "user" {
563+
if role != "developer" {
564564
return false;
565565
}
566566
content.iter().any(|content_item| match content_item {

codex-rs/core/src/codex.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3133,7 +3133,7 @@ impl Session {
31333133
}
31343134
if let Some(subagent_roster) = crate::session_prefix::SubagentRosterContext::new(subagents)
31353135
{
3136-
contextual_user_envelope.push_fragment(subagent_roster);
3136+
developer_envelope.push(subagent_roster);
31373137
}
31383138

31393139
let mut items = Vec::with_capacity(2);

codex-rs/core/src/codex_thread.rs

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,10 +101,9 @@ impl CodexThread {
101101
self.codex.session.total_token_usage().await
102102
}
103103

104-
/// Records a user-role session-prefix message without creating a new user turn boundary.
105-
pub(crate) async fn inject_user_message_without_turn(&self, message: String) {
104+
async fn inject_message_without_turn(&self, role: &str, message: String) {
106105
let pending_item = ResponseInputItem::Message {
107-
role: "user".to_string(),
106+
role: role.to_string(),
108107
content: vec![ContentItem::InputText { text: message }],
109108
};
110109
let pending_items = vec![pending_item];
@@ -128,6 +127,17 @@ impl CodexThread {
128127
.await;
129128
}
130129

130+
/// Records a user-role session-prefix message without creating a new user turn boundary.
131+
#[cfg(test)]
132+
pub(crate) async fn inject_user_message_without_turn(&self, message: String) {
133+
self.inject_message_without_turn("user", message).await;
134+
}
135+
136+
/// Records a developer-role session-prefix message without creating a new user turn boundary.
137+
pub(crate) async fn inject_developer_message_without_turn(&self, message: String) {
138+
self.inject_message_without_turn("developer", message).await;
139+
}
140+
131141
pub fn rollout_path(&self) -> Option<PathBuf> {
132142
self.rollout_path.clone()
133143
}

codex-rs/core/src/context_manager/history_tests.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,18 @@ fn user_input_text_msg(text: &str) -> ResponseItem {
7070
}
7171
}
7272

73+
fn developer_input_text_msg(text: &str) -> ResponseItem {
74+
ResponseItem::Message {
75+
id: None,
76+
role: "developer".to_string(),
77+
content: vec![ContentItem::InputText {
78+
text: text.to_string(),
79+
}],
80+
end_turn: None,
81+
phase: None,
82+
}
83+
}
84+
7385
fn custom_tool_call_output(call_id: &str, output: &str) -> ResponseItem {
7486
ResponseItem::CustomToolCallOutput {
7587
call_id: call_id.to_string(),
@@ -616,7 +628,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
616628
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
617629
),
618630
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
619-
user_input_text_msg(
631+
developer_input_text_msg(
620632
"<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>",
621633
),
622634
user_input_text_msg("turn 1 user"),
@@ -638,7 +650,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
638650
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
639651
),
640652
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
641-
user_input_text_msg(
653+
developer_input_text_msg(
642654
"<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>",
643655
),
644656
user_input_text_msg("turn 1 user"),
@@ -659,7 +671,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
659671
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
660672
),
661673
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
662-
user_input_text_msg(
674+
developer_input_text_msg(
663675
"<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>",
664676
),
665677
];
@@ -673,7 +685,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
673685
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
674686
),
675687
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
676-
user_input_text_msg(
688+
developer_input_text_msg(
677689
"<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>",
678690
),
679691
user_input_text_msg("turn 1 user"),
@@ -693,7 +705,7 @@ fn drop_last_n_user_turns_ignores_session_prefix_user_messages() {
693705
"<skill>\n<name>demo</name>\n<path>skills/demo/SKILL.md</path>\nbody\n</skill>",
694706
),
695707
user_input_text_msg("<user_shell_command>echo 42</user_shell_command>"),
696-
user_input_text_msg(
708+
developer_input_text_msg(
697709
"<subagent_notification>{\"agent_id\":\"a\",\"status\":\"completed\"}</subagent_notification>",
698710
),
699711
user_input_text_msg("turn 1 user"),

codex-rs/core/src/contextual_user_message.rs

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -185,22 +185,13 @@ pub(crate) const USER_SHELL_COMMAND_FRAGMENT: ModelVisibleFragmentSpec =
185185
);
186186
pub(crate) const TURN_ABORTED_FRAGMENT: ModelVisibleFragmentSpec =
187187
ModelVisibleFragmentSpec::contextual_user(TURN_ABORTED_OPEN_TAG, TURN_ABORTED_CLOSE_TAG);
188-
pub(crate) const SUBAGENTS_FRAGMENT: ModelVisibleFragmentSpec =
189-
ModelVisibleFragmentSpec::contextual_user(SUBAGENTS_OPEN_TAG, SUBAGENTS_CLOSE_TAG);
190-
pub(crate) const SUBAGENT_NOTIFICATION_FRAGMENT: ModelVisibleFragmentSpec =
191-
ModelVisibleFragmentSpec::contextual_user(
192-
SUBAGENT_NOTIFICATION_OPEN_TAG,
193-
SUBAGENT_NOTIFICATION_CLOSE_TAG,
194-
);
195188

196189
const CONTEXTUAL_USER_FRAGMENTS: &[ModelVisibleFragmentSpec] = &[
197190
AGENTS_MD_FRAGMENT,
198191
ENVIRONMENT_CONTEXT_FRAGMENT,
199192
SKILL_FRAGMENT,
200193
USER_SHELL_COMMAND_FRAGMENT,
201194
TURN_ABORTED_FRAGMENT,
202-
SUBAGENTS_FRAGMENT,
203-
SUBAGENT_NOTIFICATION_FRAGMENT,
204195
];
205196

206197
pub(crate) fn is_contextual_user_fragment(content_item: &ContentItem) -> bool {
@@ -241,21 +232,6 @@ mod tests {
241232
}));
242233
}
243234

244-
#[test]
245-
fn detects_subagent_notification_fragment_case_insensitively() {
246-
assert!(
247-
SUBAGENT_NOTIFICATION_FRAGMENT
248-
.matches_text("<SUBAGENT_NOTIFICATION>{}</subagent_notification>")
249-
);
250-
}
251-
252-
#[test]
253-
fn detects_subagents_fragment() {
254-
assert!(is_contextual_user_fragment(&ContentItem::InputText {
255-
text: "<subagents>\n - agent-1: atlas\n</subagents>".to_string(),
256-
}));
257-
}
258-
259235
#[test]
260236
fn ignores_regular_user_text() {
261237
assert!(!is_contextual_user_fragment(&ContentItem::InputText {

codex-rs/core/src/session_prefix.rs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
use codex_protocol::protocol::AgentStatus;
22

3-
/// Helpers for model-visible session state markers that are stored in user-role
4-
/// messages but are not user intent.
3+
/// Helpers for model-visible subagent session state rendered in the developer
4+
/// envelope.
5+
use crate::contextual_user_message::DEVELOPER_FRAGMENT;
56
use crate::contextual_user_message::ModelVisibleFragment;
6-
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_FRAGMENT;
7-
use crate::contextual_user_message::SUBAGENTS_FRAGMENT;
7+
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_CLOSE_TAG;
8+
use crate::contextual_user_message::SUBAGENT_NOTIFICATION_OPEN_TAG;
9+
use crate::contextual_user_message::SUBAGENTS_CLOSE_TAG;
10+
use crate::contextual_user_message::SUBAGENTS_OPEN_TAG;
811

912
pub(crate) struct SubagentRosterContext {
1013
subagents: String,
@@ -22,7 +25,7 @@ impl SubagentRosterContext {
2225

2326
impl ModelVisibleFragment for SubagentRosterContext {
2427
fn spec(&self) -> crate::contextual_user_message::ModelVisibleFragmentSpec {
25-
SUBAGENTS_FRAGMENT
28+
DEVELOPER_FRAGMENT
2629
}
2730

2831
fn render_text(&self) -> String {
@@ -32,7 +35,7 @@ impl ModelVisibleFragment for SubagentRosterContext {
3235
.map(|line| format!(" {line}"))
3336
.collect::<Vec<_>>()
3437
.join("\n");
35-
SUBAGENTS_FRAGMENT.wrap_body(lines)
38+
format!("{SUBAGENTS_OPEN_TAG}\n{lines}\n{SUBAGENTS_CLOSE_TAG}")
3639
}
3740
}
3841

@@ -43,7 +46,7 @@ struct SubagentNotification<'a> {
4346

4447
impl ModelVisibleFragment for SubagentNotification<'_> {
4548
fn spec(&self) -> crate::contextual_user_message::ModelVisibleFragmentSpec {
46-
SUBAGENT_NOTIFICATION_FRAGMENT
49+
DEVELOPER_FRAGMENT
4750
}
4851

4952
fn render_text(&self) -> String {
@@ -52,7 +55,9 @@ impl ModelVisibleFragment for SubagentNotification<'_> {
5255
"status": self.status,
5356
})
5457
.to_string();
55-
SUBAGENT_NOTIFICATION_FRAGMENT.wrap_body(payload_json)
58+
format!(
59+
"{SUBAGENT_NOTIFICATION_OPEN_TAG}\n{payload_json}\n{SUBAGENT_NOTIFICATION_CLOSE_TAG}"
60+
)
5661
}
5762
}
5863

codex-rs/core/tests/common/context_snapshot.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ mod tests {
345345
fn redacted_text_mode_normalizes_subagents_fragment() {
346346
let items = vec![json!({
347347
"type": "message",
348-
"role": "user",
348+
"role": "developer",
349349
"content": [{
350350
"type": "input_text",
351351
"text": "<subagents>\n - agent-1: atlas\n - agent-2\n</subagents>"
@@ -357,7 +357,7 @@ mod tests {
357357
&ContextSnapshotOptions::default().render_mode(ContextSnapshotRenderMode::RedactedText),
358358
);
359359

360-
assert_eq!(rendered, "00:message/user:<SUBAGENTS:count=2>");
360+
assert_eq!(rendered, "00:message/developer:<SUBAGENTS:count=2>");
361361
}
362362

363363
#[test]

codex-rs/core/tests/suite/model_visible_layout.rs

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -54,26 +54,40 @@ fn agents_message_count(request: &ResponsesRequest) -> usize {
5454
}
5555

5656
fn format_subagents_fragment_snapshot(subagents: &[&str]) -> String {
57-
let mut content = vec![json!({
58-
"type": "input_text",
59-
"text": "<environment_context>\n <cwd>/tmp/example</cwd>\n <shell>bash</shell>\n</environment_context>",
60-
})];
61-
if !subagents.is_empty() {
57+
let items = if subagents.is_empty() {
58+
vec![json!({
59+
"type": "message",
60+
"role": "user",
61+
"content": [{
62+
"type": "input_text",
63+
"text": "<environment_context>\n <cwd>/tmp/example</cwd>\n <shell>bash</shell>\n</environment_context>",
64+
}],
65+
})]
66+
} else {
6267
let lines = subagents
6368
.iter()
6469
.map(|line| format!(" {line}"))
6570
.collect::<Vec<_>>()
6671
.join("\n");
67-
content.push(json!({
68-
"type": "input_text",
69-
"text": format!("<subagents>\n{lines}\n</subagents>"),
70-
}));
71-
}
72-
let items = vec![json!({
73-
"type": "message",
74-
"role": "user",
75-
"content": content,
76-
})];
72+
vec![
73+
json!({
74+
"type": "message",
75+
"role": "developer",
76+
"content": [{
77+
"type": "input_text",
78+
"text": format!("<subagents>\n{lines}\n</subagents>"),
79+
}],
80+
}),
81+
json!({
82+
"type": "message",
83+
"role": "user",
84+
"content": [{
85+
"type": "input_text",
86+
"text": "<environment_context>\n <cwd>/tmp/example</cwd>\n <shell>bash</shell>\n</environment_context>",
87+
}],
88+
}),
89+
]
90+
};
7791
context_snapshot::format_response_items_snapshot(items.as_slice(), &context_snapshot_options())
7892
}
7993

Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
---
22
source: core/tests/suite/model_visible_layout.rs
3-
assertion_line: 481
3+
assertion_line: 495
44
expression: "format_subagents_fragment_snapshot(&[\"- agent-1: Atlas\"])"
55
---
6-
00:message/user[2]:
7-
[01] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
8-
[02] <SUBAGENTS:count=1>
6+
00:message/developer:<SUBAGENTS:count=1>
7+
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
---
22
source: core/tests/suite/model_visible_layout.rs
3-
assertion_line: 491
3+
assertion_line: 505
44
expression: "format_subagents_fragment_snapshot(&[\"- agent-1: Atlas\",\n\"- agent-2: Juniper\"])"
55
---
6-
00:message/user[2]:
7-
[01] <ENVIRONMENT_CONTEXT:cwd=<CWD>>
8-
[02] <SUBAGENTS:count=2>
6+
00:message/developer:<SUBAGENTS:count=2>
7+
01:message/user:<ENVIRONMENT_CONTEXT:cwd=<CWD>>

0 commit comments

Comments
 (0)