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:
rka-oai
2026-08-26 23:58:10 +00:00
committed by copyberry
parent 2926014075
commit e56e4922eb
52 changed files with 3327 additions and 42 deletions
@@ -604,6 +604,7 @@ impl ThreadHistoryBuilder {
let should_upsert = match item {
codex_protocol::items::TurnItem::Plan(plan) => !plan.text.is_empty(),
codex_protocol::items::TurnItem::HookPrompt(_)
| codex_protocol::items::TurnItem::FunctionCallOutput(_)
| codex_protocol::items::TurnItem::CommandExecution(_)
| codex_protocol::items::TurnItem::DynamicToolCall(_)
| codex_protocol::items::TurnItem::CollabAgentToolCall(_)
@@ -34,6 +34,7 @@ use codex_protocol::items::McpToolCallStatus as CoreMcpToolCallStatus;
use codex_protocol::items::TurnItem as CoreTurnItem;
use codex_protocol::memory_citation::MemoryCitation as CoreMemoryCitation;
use codex_protocol::memory_citation::MemoryCitationEntry as CoreMemoryCitationEntry;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::MessagePhase;
use codex_protocol::models::ResponseItem;
use codex_protocol::openai_models::ReasoningEffort;
@@ -257,6 +258,14 @@ pub enum ThreadItem {
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
FunctionCallOutput {
id: String,
name: String,
namespace: Option<String>,
output: FunctionCallOutputBody,
},
#[serde(rename_all = "camelCase")]
#[ts(rename_all = "camelCase")]
/// EXPERIMENTAL - proposed plan item content. The completed plan item is
/// authoritative and may not match the concatenation of `PlanDelta` text.
Plan {
@@ -429,6 +438,7 @@ impl ThreadItem {
ThreadItem::UserMessage { id, .. }
| ThreadItem::HookPrompt { id, .. }
| ThreadItem::AgentMessage { id, .. }
| ThreadItem::FunctionCallOutput { id, .. }
| ThreadItem::Plan { id, .. }
| ThreadItem::Reasoning { id, .. }
| ThreadItem::CommandExecution { id, .. }
@@ -873,6 +883,12 @@ impl From<CoreTurnItem> for ThreadItem {
delivery: agent.delivery,
}
}
CoreTurnItem::FunctionCallOutput(output) => ThreadItem::FunctionCallOutput {
id: output.id,
name: output.name,
namespace: output.namespace,
output: output.output,
},
CoreTurnItem::Plan(plan) => ThreadItem::Plan {
id: plan.id,
text: plan.text,
@@ -4724,6 +4724,7 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
client_user_message_id: None,
input: vec![],
turn_trigger: None,
tool_output: None,
responsesapi_client_metadata: None,
additional_context: None,
environments: None,
@@ -9,6 +9,7 @@ use codex_protocol::config_types::CollaborationMode;
use codex_protocol::config_types::MultiAgentMode;
use codex_protocol::config_types::Personality;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::models::FunctionCallOutputBody;
use codex_protocol::models::ImageDetail;
use codex_protocol::openai_models::ReasoningEffort;
use codex_protocol::plan_tool::PlanItemArg as CorePlanItemArg;
@@ -134,6 +135,15 @@ impl From<CyberAccessProgram> for CoreCyberAccessProgram {
}
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct TurnToolOutput {
pub name: String,
pub namespace: Option<String>,
pub output: FunctionCallOutputBody,
}
#[derive(
Serialize, Deserialize, Debug, Default, Clone, PartialEq, JsonSchema, TS, ExperimentalApi,
)]
@@ -148,6 +158,8 @@ pub struct TurnStartParams {
/// Ignored when this request steers an already-active turn.
#[ts(optional = nullable)]
pub turn_trigger: Option<String>,
#[ts(optional = nullable)]
pub tool_output: Option<Box<TurnToolOutput>>,
/// Optional metadata to enrich Codex's ResponsesAPI turn metadata.
///
/// Entries are flattened into the JSON string sent as