From a8d258f7f0495dcbae5401af3b6ac87c577980ee Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sat, 23 Aug 2025 23:36:10 -0700 Subject: [PATCH] not optional --- codex-rs/core/src/codex.rs | 29 ++++++------------- .../core/src/subagents/defaults/hello.json | 4 +-- codex-rs/core/src/subagents/definition.rs | 13 ++++----- codex-rs/core/src/subagents/runner.rs | 20 +++++-------- 4 files changed, 25 insertions(+), 41 deletions(-) diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index c90bac0a71..84f78e6150 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2153,11 +2153,10 @@ async fn handle_function_call( match result { Ok(message) => { - // If this subagent declares an output schema, validate against it and - // return the JSON body unmodified. Otherwise fallback to envelope for text. - if let Some(def) = turn_context.subagents_registry.get(&agent_name) - && let Some(schema) = def.output_schema() - { + // Validate against the subagent's output schema and return + // the JSON body unmodified. + if let Some(def) = turn_context.subagents_registry.get(&agent_name) { + let schema = def.output_schema(); match serde_json::from_str::(&message) { Ok(val) => { if let Err(err) = @@ -2195,24 +2194,14 @@ async fn handle_function_call( }, } } else { - // No schema: best-effort envelope - let json_payload = match serde_json::from_str::(&message) - { - Ok(val) => serde_json::json!({ - "subagent": agent_name, - "output": val, - }), - Err(_) => serde_json::json!({ - "subagent": agent_name, - "output": { "type": "text", "message": message }, - }), - }; - let content = serde_json::to_string(&json_payload).unwrap_or(message); ResponseInputItem::FunctionCallOutput { call_id, output: FunctionCallOutputPayload { - content, - success: Some(true), + content: format!( + "subagent not found when validating output: {}", + agent_name + ), + success: Some(false), }, } } diff --git a/codex-rs/core/src/subagents/defaults/hello.json b/codex-rs/core/src/subagents/defaults/hello.json index 2e831b137d..4a39c33780 100644 --- a/codex-rs/core/src/subagents/defaults/hello.json +++ b/codex-rs/core/src/subagents/defaults/hello.json @@ -1,8 +1,8 @@ { "name": "hello", "description": "Built-in test subagent that replies with a greeting", - "instructions": "Reply with exactly this text and nothing else: Hello from subagent", + "instructions": "Reply with exactly this text and nothing else, as a JSON string: Hello from subagent", + "output_schema": { "type": "string" }, "tools": [], "reasoning_effort": "minimal" } - diff --git a/codex-rs/core/src/subagents/definition.rs b/codex-rs/core/src/subagents/definition.rs index 74f46d9fb9..5b9d326da9 100644 --- a/codex-rs/core/src/subagents/definition.rs +++ b/codex-rs/core/src/subagents/definition.rs @@ -20,11 +20,10 @@ pub struct SubagentDefinition { pub instructions: String, //TODO: add allowed tools. we inherit the parent agent's tools for now. - /// Optional structured output schema. When set, the subagent must return a - /// single JSON value that validates against this schema. The schema will be - /// embedded into the subagent's instructions so the model can adhere to it. - #[serde(default)] - output_schema: Option, + /// Structured output schema. The subagent must return a single JSON value + /// that validates against this schema. The schema will be embedded into the + /// subagent's instructions so the model can adhere to it. + output_schema: JsonSchema, /// Optional model override for this subagent. When not provided, inherits /// the parent session's configured model. @@ -59,7 +58,7 @@ impl SubagentDefinition { }) } - pub(crate) fn output_schema(&self) -> Option<&JsonSchema> { - self.output_schema.as_ref() + pub(crate) fn output_schema(&self) -> &JsonSchema { + &self.output_schema } } diff --git a/codex-rs/core/src/subagents/runner.rs b/codex-rs/core/src/subagents/runner.rs index 3ab0e00129..4a27641bf9 100644 --- a/codex-rs/core/src/subagents/runner.rs +++ b/codex-rs/core/src/subagents/runner.rs @@ -19,24 +19,20 @@ pub struct RunSubagentArgs { /// /// For user- and project-scoped subagents, we append their instructions to the /// parent session's base instructions. For embedded defaults, we use only the -/// subagent's instructions. If an output schema is present, we augment the -/// subagent instructions with strict JSON output requirements. +/// subagent's instructions. We always augment the subagent instructions with +/// strict JSON output requirements based on its schema. fn compose_base_instructions_for_subagent( def: &SubagentDefinition, parent_base_instructions: Option<&str>, ) -> String { // Start with the subagent's own instructions, optionally augmented with // structured output requirements. - let child_instructions = if let Some(schema) = def.output_schema() { - let schema_json = serde_json::to_string_pretty(schema).unwrap_or_else(|_| "{}".to_string()); - format!( - "{instructions}\n\nOutput format requirements:\n- Reply with a single JSON value that strictly matches the following JSON Schema.\n- Do not include any commentary, markdown, or extra text.\n- Do not include trailing explanations.\n\nSchema:\n{schema_json}\n", - instructions = def.instructions, - schema_json = schema_json - ) - } else { - def.instructions.clone() - }; + let schema_json = + serde_json::to_string_pretty(def.output_schema()).unwrap_or_else(|_| "{}".to_string()); + let child_instructions = format!( + "{}\n\nOutput format requirements:\n- Reply with a single JSON value that strictly matches the following JSON Schema.\n- Do not include any commentary, markdown, or extra text.\n- Do not include trailing explanations.\n\nSchema:\n{}\n", + def.instructions, schema_json + ); match def.source { SubagentSource::User | SubagentSource::Project => match parent_base_instructions {