mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
## Why Clients starting delegated work need to attribute descendant turns to the original turn and retain that attribution through completion and history reads. ## What changed - Accept optional `parentTurnId` and `rootTurnId` in `turn/start`, and expose `rootTurnId` on returned turns and lifecycle notifications. - Default new turns to their own root when none is supplied. Preserve the active turn's root when steering, ignoring incoming lineage fields. - Carry root IDs through completion, interruption, recovery, and fork boundaries, and persist them for paginated history reads. Leave unknown roots null in older history. - Update protocol schemas and generated TypeScript and Python bindings. ## Testing Extend coverage for default and explicit roots, steering without changing lineage, interrupted and recovered turns, history reads after restart in both history modes, nested fork pagination, and compatibility with older database writers. GitOrigin-RevId: f1a570a12bd11742929fd2e8b017e5d35aa07dcb
289 lines
10 KiB
Rust
289 lines
10 KiB
Rust
//! Turn-input request and result types shared by Core's submission APIs.
|
|
|
|
use crate::AgentPath;
|
|
use crate::models::ResponseItem;
|
|
use crate::protocol::AdditionalContextEntry;
|
|
use crate::protocol::InterAgentCommunication;
|
|
use crate::protocol::NonSteerableTurnKind;
|
|
use crate::protocol::ThreadSettingsOverrides;
|
|
use crate::protocol::W3cTraceContext;
|
|
use crate::user_input::UserInput;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
use serde_json::Value;
|
|
use std::collections::BTreeMap;
|
|
use std::collections::HashMap;
|
|
use ts_rs::TS;
|
|
|
|
/// Result of stopping an unfinished root turn so another worker can recover it.
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum SuspendTurnOutcome {
|
|
Suspended {
|
|
turn_id: String,
|
|
},
|
|
NotActive,
|
|
/// A currently loaded descendant would remain running after root handoff.
|
|
HasLiveDescendants,
|
|
UnsupportedTask,
|
|
}
|
|
|
|
/// Input consumed by a regular turn.
|
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
|
pub enum TurnInput {
|
|
UserInput {
|
|
content: Vec<UserInput>,
|
|
client_id: Option<String>,
|
|
},
|
|
ResponseItem(ResponseItem),
|
|
InterAgentCommunication(InterAgentCommunication),
|
|
}
|
|
|
|
/// One turn input and the context that follows it through submission.
|
|
///
|
|
/// Callers choose start-or-steer, idle-start, or steer-only behavior through
|
|
/// the corresponding `CodexThread` method.
|
|
#[derive(Clone, Debug)]
|
|
pub struct TurnInputRequest {
|
|
pub input: TurnInput,
|
|
pub thread_settings: ThreadSettingsOverrides,
|
|
pub start: TurnStartOptions,
|
|
pub additional_context: BTreeMap<String, AdditionalContextEntry>,
|
|
pub responsesapi_client_metadata: Option<HashMap<String, String>>,
|
|
pub trace: Option<W3cTraceContext>,
|
|
}
|
|
|
|
/// Attribution retained across recovery, sleep, and model-context compaction.
|
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, JsonSchema, TS)]
|
|
pub struct TurnAttribution {
|
|
pub turn_id: String,
|
|
pub turn_trigger: Option<String>,
|
|
pub parent_turn_id: Option<String>,
|
|
pub initiating_agent_path: Option<AgentPath>,
|
|
pub root_turn_id: Option<String>,
|
|
}
|
|
|
|
impl TurnAttribution {
|
|
/// Restores provenance without changing the thread's execution settings.
|
|
pub fn start_options(&self) -> TurnStartOptions {
|
|
TurnStartOptions {
|
|
turn_trigger: self.turn_trigger.clone(),
|
|
parent_turn_id: self.parent_turn_id.clone(),
|
|
initiating_agent_path: self.initiating_agent_path.clone(),
|
|
root_turn_id: self.root_turn_id.clone(),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Request to resume sampling for an interrupted regular turn.
|
|
///
|
|
/// Sampling restarts under `turn_id`, which must be the ID already recorded
|
|
/// for that turn.
|
|
#[derive(Clone, Debug)]
|
|
pub struct RecoverTurnRequest {
|
|
pub turn_id: String,
|
|
pub thread_settings: ThreadSettingsOverrides,
|
|
pub trace: Option<W3cTraceContext>,
|
|
/// Program recorded in the interrupted turn's persisted context.
|
|
pub cyber_access_program: Option<CyberAccessProgram>,
|
|
}
|
|
|
|
impl TurnInputRequest {
|
|
/// Creates turn input that can be passed to one of the submission methods.
|
|
pub fn new(input: TurnInput) -> Self {
|
|
Self {
|
|
input,
|
|
thread_settings: ThreadSettingsOverrides::default(),
|
|
start: TurnStartOptions::default(),
|
|
additional_context: BTreeMap::new(),
|
|
responsesapi_client_metadata: None,
|
|
trace: None,
|
|
}
|
|
}
|
|
|
|
/// Creates ordinary user input without a client-provided message id.
|
|
pub fn user_input(content: Vec<UserInput>) -> Self {
|
|
Self::new(TurnInput::UserInput {
|
|
content,
|
|
client_id: None,
|
|
})
|
|
}
|
|
|
|
/// Persistent thread settings applied when Core accepts this input.
|
|
///
|
|
/// Settings are applied for both `Started` and `Steered`. A steered input
|
|
/// cannot change its already-active turn context, so those settings apply
|
|
/// to subsequent turns. `NotSubmitted` leaves them unapplied.
|
|
pub fn with_thread_settings(mut self, thread_settings: ThreadSettingsOverrides) -> Self {
|
|
self.thread_settings = thread_settings;
|
|
self
|
|
}
|
|
|
|
/// Options for new turns, also consulted for input provenance and active-turn
|
|
/// compatibility when steering.
|
|
pub fn on_start(mut self, start: TurnStartOptions) -> Self {
|
|
self.start = start;
|
|
self
|
|
}
|
|
|
|
/// Context merged whether this request starts or steers.
|
|
pub fn with_additional_context(
|
|
mut self,
|
|
additional_context: BTreeMap<String, AdditionalContextEntry>,
|
|
) -> Self {
|
|
self.additional_context = additional_context;
|
|
self
|
|
}
|
|
|
|
/// Responses metadata attached whether this request starts or steers.
|
|
pub fn with_responses_metadata(
|
|
mut self,
|
|
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
|
) -> Self {
|
|
self.responsesapi_client_metadata = responsesapi_client_metadata;
|
|
self
|
|
}
|
|
|
|
/// Trace context used when this request crosses the session loop.
|
|
pub fn with_trace(mut self, trace: Option<W3cTraceContext>) -> Self {
|
|
self.trace = trace;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// How Core should route submitted turn input.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum TurnInputMode {
|
|
/// Start a regular turn when idle, otherwise steer the active regular turn.
|
|
StartOrSteer,
|
|
/// Start only when the thread is idle.
|
|
StartIfIdle,
|
|
/// Start an internal continuation when idle.
|
|
/// Reject if another task has started since the expected previous turn.
|
|
ContinueIfIdle { expected_previous_turn_id: String },
|
|
/// Steer only if this exact turn is active.
|
|
Steer { expected_turn_id: String },
|
|
}
|
|
|
|
/// Requested cyber treatment for an OpenAI model turn.
|
|
/// Authorization and model-tier restrictions remain server-owned.
|
|
#[derive(Debug, Serialize, Deserialize, Clone, Copy, PartialEq, Eq, JsonSchema, TS)]
|
|
#[serde(rename_all = "snake_case")]
|
|
#[ts(rename_all = "snake_case")]
|
|
pub enum CyberAccessProgram {
|
|
Standard,
|
|
DaybreakBlue,
|
|
DaybreakRed,
|
|
}
|
|
|
|
/// Options for the new-turn branch of a submission.
|
|
///
|
|
/// Core only records these options when input starts a turn. When input steers,
|
|
/// `final_output_json_schema` is a compatibility requirement: Core only
|
|
/// accepts the steer if the active turn already uses the same schema. For
|
|
/// child input, Core also compares root lineage to detect ambiguity.
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct TurnStartOptions {
|
|
/// Source classification for this request. Recorded on new turns and captured
|
|
/// as input provenance when steering, without changing the active turn's trigger.
|
|
pub turn_trigger: Option<String>,
|
|
/// Structured-output schema for a new turn. When steering, Core rejects
|
|
/// the input if the active turn uses a different schema.
|
|
pub final_output_json_schema: Option<Value>,
|
|
/// Service tier for a new turn, without changing the thread's preference.
|
|
pub service_tier: Option<String>,
|
|
/// Parent turn lineage recorded if this request starts a new turn.
|
|
pub parent_turn_id: Option<String>,
|
|
/// Agent owning the parent turn, retained when recovering delegated work.
|
|
pub initiating_agent_path: Option<AgentPath>,
|
|
/// Causal root turn lineage recorded if this request starts a new turn.
|
|
pub root_turn_id: Option<String>,
|
|
/// Explicit cyber treatment for this turn. Omission preserves the backend's
|
|
/// automatic behavior.
|
|
pub cyber_access_program: Option<CyberAccessProgram>,
|
|
}
|
|
|
|
/// What Core did with input submitted through `start_or_steer_turn`.
|
|
///
|
|
/// Started and Steered only mean Core accepted the input for turn processing. They
|
|
/// do not wait for user-prompt hooks, updating the in-memory model context,
|
|
/// rollout persistence, or sampling. The returned root is the accepted turn's
|
|
/// resolved causal root, including when steering preserves an existing turn.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum TurnInputSubmission {
|
|
/// Core started a turn. Persistent thread settings and start options were applied.
|
|
Started {
|
|
turn_id: String,
|
|
root_turn_id: String,
|
|
},
|
|
/// Core steered an active turn. Persistent thread settings were applied for
|
|
/// subsequent turns. No new turn was created, so lineage metadata was not
|
|
/// recorded. If the request included `final_output_json_schema`, the active
|
|
/// turn already used the same schema.
|
|
Steered {
|
|
turn_id: String,
|
|
root_turn_id: String,
|
|
},
|
|
/// Core rejected the input without applying settings or start options.
|
|
NotSubmitted { reason: NotSubmittedReason },
|
|
}
|
|
|
|
/// What Core did with input submitted only for an idle turn start.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum StartIfIdleSubmission {
|
|
/// Core started a turn. Persistent thread settings and start options were applied.
|
|
Started {
|
|
turn_id: String,
|
|
root_turn_id: String,
|
|
},
|
|
/// Core rejected the input without applying settings or start options.
|
|
NotSubmitted { reason: NotSubmittedReason },
|
|
}
|
|
|
|
/// What Core did with input submitted only for steering.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum SteerSubmission {
|
|
/// Core steered an active turn. Persistent thread settings were applied for
|
|
/// subsequent turns.
|
|
Steered { turn_id: String },
|
|
/// Core rejected the input without applying settings.
|
|
NotSubmitted { reason: NotSubmittedReason },
|
|
}
|
|
|
|
/// Why Core did not accept submitted turn input for turn processing.
|
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
|
pub enum NotSubmittedReason {
|
|
/// New work superseded the expected previous turn of an internal continuation.
|
|
Superseded,
|
|
|
|
/// The host is draining and no longer permits new regular turns.
|
|
ServerDraining,
|
|
|
|
/// `start_turn_if_idle` found an active turn.
|
|
NotIdle,
|
|
|
|
/// `start_turn_if_idle` yielded to higher-priority trigger-turn mailbox input.
|
|
PendingTriggerTurn,
|
|
|
|
/// `start_turn_if_idle` received automatic non-user input for a turn that
|
|
/// would run in Plan mode.
|
|
PlanMode,
|
|
|
|
/// `steer_turn` found no active turn.
|
|
NoActiveTurn,
|
|
|
|
/// `steer_turn` found a different active turn.
|
|
ExpectedTurnMismatch { expected: String, actual: String },
|
|
|
|
/// `start_or_steer_turn` or `steer_turn` found an active turn that does not accept steering.
|
|
ActiveTurnNotSteerable { turn_kind: NonSteerableTurnKind },
|
|
|
|
/// `start_or_steer_turn` or `steer_turn` required a final output schema
|
|
/// that differs from the active turn's schema.
|
|
ActiveTurnOutputSchemaMismatch,
|
|
|
|
/// `start_or_steer_turn` or `steer_turn` reached a steering path with empty user input.
|
|
EmptyInput,
|
|
}
|