Skip to content

Commit 20f3bcc

Browse files
committed
feat(multi-agent): allow portable plaintext messages
1 parent 4f38521 commit 20f3bcc

17 files changed

Lines changed: 428 additions & 46 deletions

‎codex-rs/core/config.schema.json‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1768,6 +1768,10 @@
17681768
"enabled": {
17691769
"type": "boolean"
17701770
},
1771+
"encrypt_inter_agent_messages": {
1772+
"description": "Encrypt inter-agent message fields using the OpenAI Responses extension. Disable this for providers that only implement the portable Responses schema. When disabled without an explicit tool namespace, the client uses `agents`.",
1773+
"type": "boolean"
1774+
},
17711775
"expose_spawn_agent_model_overrides": {
17721776
"description": "Exposes `model` and `reasoning_effort` on the multi-agent v2 spawn tool and adds corresponding guidance to root and subagent usage hints.",
17731777
"type": "boolean"

‎codex-rs/core/src/client.rs‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ use codex_protocol::config_types::ReasoningSummary as ReasoningSummaryConfig;
8080
use codex_protocol::config_types::Verbosity as VerbosityConfig;
8181
use codex_protocol::models::ContentItem;
8282
use codex_protocol::models::ResponseItem;
83+
use codex_protocol::models::plaintext_agent_message_content;
8384
use codex_protocol::openai_models::ModelInfo;
8485
use codex_protocol::openai_models::ReasoningEffort as ReasoningEffortConfig;
8586
use codex_protocol::protocol::InternalSessionSource;
@@ -835,6 +836,23 @@ impl ModelClient {
835836
input
836837
.iter_mut()
837838
.for_each(ResponseItem::clear_internal_chat_message_metadata_passthrough);
839+
input = input
840+
.into_iter()
841+
.map(|item| {
842+
if let ResponseItem::AgentMessage { content, .. } = &item
843+
&& let Some(text) = plaintext_agent_message_content(content)
844+
{
845+
return ResponseItem::Message {
846+
id: None,
847+
role: "user".to_string(),
848+
content: vec![ContentItem::InputText { text }],
849+
phase: None,
850+
internal_chat_message_metadata_passthrough: None,
851+
};
852+
}
853+
item
854+
})
855+
.collect();
838856
}
839857
let tools = create_tools_json_for_responses_api(&prompt.tools)?;
840858
let (instructions, tools) = if model_info.use_responses_lite {

‎codex-rs/core/src/config/config_tests.rs‎

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10603,6 +10603,7 @@ tool_namespace = "agents"
1060310603
hide_spawn_agent_metadata = true
1060410604
expose_spawn_agent_model_overrides = false
1060510605
non_code_mode_only = true
10606+
encrypt_inter_agent_messages = false
1060610607
1060710608
[agents]
1060810609
max_concurrent_threads_per_session = 9
@@ -10650,6 +10651,7 @@ max_concurrent_threads_per_session = 9
1065010651
assert!(config.multi_agent_v2.hide_spawn_agent_metadata);
1065110652
assert!(!config.multi_agent_v2.expose_spawn_agent_model_overrides);
1065210653
assert!(config.multi_agent_v2.non_code_mode_only);
10654+
assert!(!config.multi_agent_v2.encrypt_inter_agent_messages);
1065310655

1065410656
Ok(())
1065510657
}
@@ -10674,6 +10676,7 @@ enabled = true
1067410676
config.multi_agent_v2,
1067510677
resolve_multi_agent_v2_config(&ConfigToml::default())
1067610678
);
10679+
assert!(config.multi_agent_v2.encrypt_inter_agent_messages);
1067710680
assert_eq!(
1067810681
(
1067910682
config.agent_max_threads,
@@ -10685,6 +10688,32 @@ enabled = true
1068510688
Ok(())
1068610689
}
1068710690

10691+
#[tokio::test]
10692+
async fn multi_agent_v2_plaintext_defaults_to_non_reserved_tool_namespace() -> std::io::Result<()> {
10693+
let codex_home = TempDir::new()?;
10694+
std::fs::write(
10695+
codex_home.path().join(CONFIG_TOML_FILE),
10696+
r#"[features.multi_agent_v2]
10697+
enabled = true
10698+
encrypt_inter_agent_messages = false
10699+
"#,
10700+
)?;
10701+
10702+
let config = ConfigBuilder::without_managed_config_for_tests()
10703+
.codex_home(codex_home.path().to_path_buf())
10704+
.fallback_cwd(Some(codex_home.path().to_path_buf()))
10705+
.build()
10706+
.await?;
10707+
10708+
assert_eq!(
10709+
config.multi_agent_v2.tool_namespace.as_deref(),
10710+
Some("agents")
10711+
);
10712+
assert!(!config.multi_agent_v2.encrypt_inter_agent_messages);
10713+
10714+
Ok(())
10715+
}
10716+
1068810717
#[test]
1068910718
fn multi_agent_v2_default_usage_hints_use_configured_thread_cap() {
1069010719
let config_toml = toml::from_str(

‎codex-rs/core/src/config/mod.rs‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,7 @@ You may also see them addressed as to=/root/..., which indicates your identity i
250250
"#;
251251
const DEFAULT_MULTI_AGENT_V2_MODEL_OVERRIDE_USAGE_HINT_TEXT: &str = "Full-history forks (`fork_turns` omitted or `\"all\"`) inherit the parent model and reasoning effort and do not accept overrides. Only set `model` or `reasoning_effort` when explicitly requested by the user, applicable `AGENTS.md` instructions, or skill instructions; when doing so, set `fork_turns` to `\"none\"` or a positive integer string.";
252252
const DEFAULT_MULTI_AGENT_V2_TOOL_NAMESPACE: &str = "collaboration";
253+
const DEFAULT_MULTI_AGENT_V2_PLAINTEXT_TOOL_NAMESPACE: &str = "agents";
253254
const DEFAULT_MULTI_AGENT_V2_SHARED_USAGE_HINT_TEXT: &str = r#"Note that collaboration tools cannot be called from inside `functions.exec`. Call `spawn_agent`, `send_message`, `followup_task`, `wait_agent`, `interrupt_agent`, and `list_agents` only as direct tool calls using the recipient shown in their tool definitions, such as `to=functions.collaboration.spawn_agent`, since they are intentionally absent from the `functions.exec` `tools.*` namespace. Available tools in `functions.exec` are explicitly described with a `tools` namespace in the developer message.
254255
255256
All agents share the same directory. In detail:
@@ -1178,6 +1179,7 @@ pub struct MultiAgentV2Config {
11781179
pub hide_spawn_agent_metadata: bool,
11791180
pub expose_spawn_agent_model_overrides: bool,
11801181
pub non_code_mode_only: bool,
1182+
pub encrypt_inter_agent_messages: bool,
11811183
}
11821184

11831185
impl MultiAgentV2Config {
@@ -1201,6 +1203,7 @@ impl MultiAgentV2Config {
12011203
hide_spawn_agent_metadata: true,
12021204
expose_spawn_agent_model_overrides: true,
12031205
non_code_mode_only: true,
1206+
encrypt_inter_agent_messages: true,
12041207
}
12051208
}
12061209
}
@@ -2592,13 +2595,20 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
25922595
.and_then(|config| config.multi_agent_mode_hint_text.as_ref())
25932596
.cloned()
25942597
.or(default.multi_agent_mode_hint_text);
2598+
let non_code_mode_only = base
2599+
.and_then(|config| config.non_code_mode_only)
2600+
.unwrap_or(default.non_code_mode_only);
2601+
let encrypt_inter_agent_messages = base
2602+
.and_then(|config| config.encrypt_inter_agent_messages)
2603+
.unwrap_or(default.encrypt_inter_agent_messages);
25952604
let tool_namespace = base
25962605
.and_then(|config| config.tool_namespace.as_ref())
25972606
.cloned()
2607+
.or_else(|| {
2608+
(!encrypt_inter_agent_messages)
2609+
.then(|| DEFAULT_MULTI_AGENT_V2_PLAINTEXT_TOOL_NAMESPACE.to_string())
2610+
})
25982611
.or(default.tool_namespace);
2599-
let non_code_mode_only = base
2600-
.and_then(|config| config.non_code_mode_only)
2601-
.unwrap_or(default.non_code_mode_only);
26022612

26032613
MultiAgentV2Config {
26042614
max_concurrent_threads_per_session,
@@ -2613,6 +2623,7 @@ fn resolve_multi_agent_v2_config(config_toml: &ConfigToml) -> MultiAgentV2Config
26132623
hide_spawn_agent_metadata,
26142624
expose_spawn_agent_model_overrides,
26152625
non_code_mode_only,
2626+
encrypt_inter_agent_messages,
26162627
}
26172628
}
26182629

‎codex-rs/core/src/tools/handlers/multi_agents_spec.rs‎

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,23 @@ pub struct SpawnAgentToolOptions {
3131
pub expose_spawn_agent_model_overrides: bool,
3232
pub multi_agent_version: MultiAgentVersion,
3333
pub usage_hint_text: Option<String>,
34+
pub message_format: InterAgentMessageFormat,
35+
}
36+
37+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
38+
pub enum InterAgentMessageFormat {
39+
Encrypted,
40+
Plaintext,
41+
}
42+
43+
impl InterAgentMessageFormat {
44+
pub fn from_encryption_enabled(enabled: bool) -> Self {
45+
if enabled {
46+
Self::Encrypted
47+
} else {
48+
Self::Plaintext
49+
}
50+
}
3451
}
3552

3653
impl Default for SpawnAgentToolOptions {
@@ -43,6 +60,7 @@ impl Default for SpawnAgentToolOptions {
4360
expose_spawn_agent_model_overrides: false,
4461
multi_agent_version: MultiAgentVersion::Disabled,
4562
usage_hint_text: None,
63+
message_format: InterAgentMessageFormat::Encrypted,
4664
}
4765
}
4866
}
@@ -106,7 +124,8 @@ pub fn create_spawn_agent_tool_v2(options: SpawnAgentToolOptions) -> ToolSpec {
106124
let inherited_model_guidance = (options.expose_spawn_agent_model_overrides
107125
&& !options.hide_agent_type_model_reasoning)
108126
.then_some(SPAWN_AGENT_INHERITED_MODEL_GUIDANCE);
109-
let mut properties = spawn_agent_common_properties_v2(&options.agent_type_description);
127+
let mut properties =
128+
spawn_agent_common_properties_v2(&options.agent_type_description, options.message_format);
110129
if !options.expose_agent_type {
111130
properties.remove("agent_type");
112131
}
@@ -183,7 +202,7 @@ pub fn create_send_input_tool_v1() -> ToolSpec {
183202
})
184203
}
185204

186-
pub fn create_send_message_tool() -> ToolSpec {
205+
pub fn create_send_message_tool(message_format: InterAgentMessageFormat) -> ToolSpec {
187206
let properties = BTreeMap::from([
188207
(
189208
"target".to_string(),
@@ -193,10 +212,10 @@ pub fn create_send_message_tool() -> ToolSpec {
193212
),
194213
(
195214
"message".to_string(),
196-
JsonSchema::string(Some(
197-
"Message text to queue on the target agent.".to_string(),
198-
))
199-
.with_encrypted(),
215+
inter_agent_message_schema(
216+
"Message text to queue on the target agent.",
217+
message_format,
218+
),
200219
),
201220
]);
202221

@@ -215,7 +234,7 @@ pub fn create_send_message_tool() -> ToolSpec {
215234
})
216235
}
217236

218-
pub fn create_followup_task_tool() -> ToolSpec {
237+
pub fn create_followup_task_tool(message_format: InterAgentMessageFormat) -> ToolSpec {
219238
let properties = BTreeMap::from([
220239
(
221240
"target".to_string(),
@@ -226,10 +245,7 @@ pub fn create_followup_task_tool() -> ToolSpec {
226245
),
227246
(
228247
"message".to_string(),
229-
JsonSchema::string(Some(
230-
"Message text to send to the target agent.".to_string(),
231-
))
232-
.with_encrypted(),
248+
inter_agent_message_schema("Message text to send to the target agent.", message_format),
233249
),
234250
]);
235251

@@ -628,14 +644,28 @@ fn spawn_agent_common_properties_v1(agent_type_description: &str) -> BTreeMap<St
628644
])
629645
}
630646

631-
fn spawn_agent_common_properties_v2(agent_type_description: &str) -> BTreeMap<String, JsonSchema> {
647+
fn inter_agent_message_schema(
648+
description: &str,
649+
message_format: InterAgentMessageFormat,
650+
) -> JsonSchema {
651+
let schema = JsonSchema::string(Some(description.to_string()));
652+
match message_format {
653+
InterAgentMessageFormat::Encrypted => schema.with_encrypted(),
654+
InterAgentMessageFormat::Plaintext => schema,
655+
}
656+
}
657+
658+
fn spawn_agent_common_properties_v2(
659+
agent_type_description: &str,
660+
message_format: InterAgentMessageFormat,
661+
) -> BTreeMap<String, JsonSchema> {
632662
BTreeMap::from([
633663
(
634664
"message".to_string(),
635-
JsonSchema::string(Some(
636-
"Initial plain-text task for the new agent.".to_string(),
637-
))
638-
.with_encrypted(),
665+
inter_agent_message_schema(
666+
"Initial plain-text task for the new agent.",
667+
message_format,
668+
),
639669
),
640670
(
641671
"agent_type".to_string(),

‎codex-rs/core/src/tools/handlers/multi_agents_spec_tests.rs‎

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ fn spawn_agent_tool_v2_requires_task_name_and_lists_visible_models() {
5353
expose_spawn_agent_model_overrides: true,
5454
multi_agent_version: MultiAgentVersion::V2,
5555
usage_hint_text: None,
56+
message_format: InterAgentMessageFormat::Encrypted,
5657
});
5758

5859
let ToolSpec::Function(ResponsesApiTool {
@@ -134,6 +135,7 @@ fn spawn_agent_tool_v1_keeps_legacy_fork_context_field() {
134135
expose_spawn_agent_model_overrides: true,
135136
multi_agent_version: MultiAgentVersion::V1,
136137
usage_hint_text: None,
138+
message_format: InterAgentMessageFormat::Encrypted,
137139
});
138140

139141
let ToolSpec::Namespace(namespace) = tool else {
@@ -199,6 +201,7 @@ fn spawn_agent_tool_caps_visible_model_summaries() {
199201
expose_spawn_agent_model_overrides: true,
200202
multi_agent_version: MultiAgentVersion::V2,
201203
usage_hint_text: None,
204+
message_format: InterAgentMessageFormat::Encrypted,
202205
});
203206

204207
let ToolSpec::Function(ResponsesApiTool { description, .. }) = tool else {
@@ -245,6 +248,7 @@ fn spawn_agent_tool_keeps_model_controls_when_spawn_metadata_is_hidden() {
245248
expose_spawn_agent_model_overrides: true,
246249
multi_agent_version: MultiAgentVersion::V2,
247250
usage_hint_text: None,
251+
message_format: InterAgentMessageFormat::Encrypted,
248252
});
249253

250254
let ToolSpec::Function(ResponsesApiTool {
@@ -278,6 +282,7 @@ fn spawn_agent_tool_hides_model_controls_without_override_exposure() {
278282
expose_spawn_agent_model_overrides: false,
279283
multi_agent_version: MultiAgentVersion::V2,
280284
usage_hint_text: None,
285+
message_format: InterAgentMessageFormat::Encrypted,
281286
});
282287

283288
let ToolSpec::Function(ResponsesApiTool {
@@ -306,7 +311,7 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
306311
parameters,
307312
output_schema,
308313
..
309-
}) = create_send_message_tool()
314+
}) = create_send_message_tool(InterAgentMessageFormat::Encrypted)
310315
else {
311316
panic!("send_message should be a function tool");
312317
};
@@ -341,6 +346,21 @@ fn send_message_tool_requires_message_and_has_no_output_schema() {
341346
assert_eq!(output_schema, None);
342347
}
343348

349+
#[test]
350+
fn send_message_tool_can_expose_portable_plaintext_schema() {
351+
let ToolSpec::Function(ResponsesApiTool { parameters, .. }) =
352+
create_send_message_tool(InterAgentMessageFormat::Plaintext)
353+
else {
354+
panic!("send_message should be a function tool");
355+
};
356+
let message = parameters
357+
.properties
358+
.as_ref()
359+
.and_then(|properties| properties.get("message"))
360+
.expect("send_message should define message");
361+
assert_eq!(message.encrypted, None);
362+
}
363+
344364
#[test]
345365
fn followup_task_tool_requires_message_and_has_no_output_schema() {
346366
let ToolSpec::Function(ResponsesApiTool {
@@ -349,7 +369,7 @@ fn followup_task_tool_requires_message_and_has_no_output_schema() {
349369
parameters,
350370
output_schema,
351371
..
352-
}) = create_followup_task_tool()
372+
}) = create_followup_task_tool(InterAgentMessageFormat::Encrypted)
353373
else {
354374
panic!("followup_task should be a function tool");
355375
};

0 commit comments

Comments
 (0)