mirror of
https://github.com/openai/codex.git
synced 2026-10-22 17:58:46 +00:00
Support standalone tool outputs in turn/start (#41002)
## What changed - Add an optional `toolOutput` to `turn/start` for starting or steering a turn with a named function-call output instead of user input. - Emit standalone outputs as `functionCallOutput` thread items, persist them in durable history, and include them in resumed threads. - Validate that tool outputs have a name, are not combined with nonempty `input`, satisfy the text-size limit, and use supported image URLs. ## Testing - Cover standalone tool-output notifications, model input, and resumed history in both legacy and paginated history modes. - Cover the text-size limit for structured tool output. GitOrigin-RevId: a1eeec7b9b7de67b1a1c5e829a54552b85ec118b
This commit is contained in:
@@ -664,6 +664,7 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
cwd: None,
|
||||
|
||||
@@ -5,7 +5,9 @@ use codex_agent_extension::AgentRun;
|
||||
use codex_agent_extension::AgentRunner;
|
||||
use codex_protocol::error::CodexErrorDetails;
|
||||
use codex_protocol::models::ContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputBody;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::models::FunctionCallOutputPayload;
|
||||
use codex_protocol::protocol::AdditionalContextEntry as CoreAdditionalContextEntry;
|
||||
use codex_protocol::protocol::AdditionalContextKind as CoreAdditionalContextKind;
|
||||
use codex_protocol::protocol::TurnSettingsUpdate;
|
||||
@@ -509,7 +511,38 @@ impl TurnRequestProcessor {
|
||||
})?;
|
||||
self.ensure_direct_input_allowed(&request_id, thread.as_ref())
|
||||
.await?;
|
||||
if let Err(error) = Self::validate_v2_input_limit(¶ms.input) {
|
||||
if let Some(tool_output) = ¶ms.tool_output {
|
||||
if !params.input.is_empty() {
|
||||
return Err(invalid_request(
|
||||
"`toolOutput` cannot be combined with nonempty `input`",
|
||||
));
|
||||
}
|
||||
if tool_output.name.is_empty() {
|
||||
return Err(invalid_request("`toolOutput.name` must not be empty"));
|
||||
}
|
||||
}
|
||||
let actual_chars = params
|
||||
.input
|
||||
.iter()
|
||||
.map(V2UserInput::text_char_count)
|
||||
.sum::<usize>()
|
||||
+ params
|
||||
.tool_output
|
||||
.as_ref()
|
||||
.map_or(0, |output| match &output.output {
|
||||
FunctionCallOutputBody::Text(text) => text.chars().count(),
|
||||
FunctionCallOutputBody::ContentItems(items) => items
|
||||
.iter()
|
||||
.map(|item| match item {
|
||||
FunctionCallOutputContentItem::InputText { text } => {
|
||||
text.chars().count()
|
||||
}
|
||||
_ => 0,
|
||||
})
|
||||
.sum(),
|
||||
});
|
||||
if actual_chars > MAX_USER_INPUT_TEXT_CHARS {
|
||||
let error = Self::input_too_large_error(actual_chars);
|
||||
self.track_error_response(
|
||||
&request_id,
|
||||
&error,
|
||||
@@ -532,15 +565,32 @@ impl TurnRequestProcessor {
|
||||
let environment_selections =
|
||||
resolve_turn_environment_selections(self.thread_manager.as_ref(), params.environments)?;
|
||||
|
||||
// Map v2 input items to core input items.
|
||||
let mapped_items: Vec<CoreInputItem> = params
|
||||
.input
|
||||
.into_iter()
|
||||
.map(V2UserInput::into_core)
|
||||
.collect();
|
||||
let client_user_message_id = params.client_user_message_id;
|
||||
let additional_context = map_additional_context(params.additional_context);
|
||||
let turn_has_input = !mapped_items.is_empty();
|
||||
let turn_has_input = !params.input.is_empty();
|
||||
let input = if let Some(tool_output) = params.tool_output {
|
||||
let item = ResponseItem::FunctionCallOutput {
|
||||
id: None,
|
||||
call_id: None,
|
||||
name: Some(tool_output.name),
|
||||
namespace: tool_output.namespace,
|
||||
output: FunctionCallOutputPayload {
|
||||
body: tool_output.output,
|
||||
success: None,
|
||||
},
|
||||
internal_chat_message_metadata_passthrough: None,
|
||||
};
|
||||
validate_response_item_image_urls(std::slice::from_ref(&item))?;
|
||||
TurnInput::ResponseItem(item)
|
||||
} else {
|
||||
TurnInput::UserInput {
|
||||
content: params
|
||||
.input
|
||||
.into_iter()
|
||||
.map(V2UserInput::into_core)
|
||||
.collect(),
|
||||
client_id: params.client_user_message_id,
|
||||
}
|
||||
};
|
||||
let cwd = resolve_request_cwd(params.cwd)?;
|
||||
let environments = self
|
||||
.build_environment_override(
|
||||
@@ -569,26 +619,25 @@ impl TurnRequestProcessor {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
self.seal_realtime_transcript_before_user_input(thread_id, &mapped_items)
|
||||
.await?;
|
||||
if let TurnInput::UserInput { content, .. } = &input {
|
||||
self.seal_realtime_transcript_before_user_input(thread_id, content)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let submission = thread
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::new(TurnInput::UserInput {
|
||||
content: mapped_items,
|
||||
client_id: client_user_message_id,
|
||||
})
|
||||
.with_thread_settings(thread_settings)
|
||||
.on_start(TurnStartOptions {
|
||||
turn_trigger: params.turn_trigger,
|
||||
final_output_json_schema: params.output_schema,
|
||||
service_tier: params.service_tier_for_turn,
|
||||
cyber_access_program: params.cyber_access_program.map(Into::into),
|
||||
..Default::default()
|
||||
})
|
||||
.with_additional_context(additional_context)
|
||||
.with_responses_metadata(params.responsesapi_client_metadata)
|
||||
.with_trace(self.request_trace_context(&request_id).await),
|
||||
TurnInputRequest::new(input)
|
||||
.with_thread_settings(thread_settings)
|
||||
.on_start(TurnStartOptions {
|
||||
turn_trigger: params.turn_trigger,
|
||||
final_output_json_schema: params.output_schema,
|
||||
service_tier: params.service_tier_for_turn,
|
||||
cyber_access_program: params.cyber_access_program.map(Into::into),
|
||||
..Default::default()
|
||||
})
|
||||
.with_additional_context(additional_context)
|
||||
.with_responses_metadata(params.responsesapi_client_metadata)
|
||||
.with_trace(self.request_trace_context(&request_id).await),
|
||||
)
|
||||
.await
|
||||
.map_err(|err| {
|
||||
|
||||
Reference in New Issue
Block a user