mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Expose and persist turn lineage across the app server (#51415)
## 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
This commit is contained in:
@@ -862,6 +862,7 @@ fn session_event_to_analytics_notification(
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
started_at: started.started_at,
|
||||
root_turn_id: started.root_turn_id.clone(),
|
||||
..analytics_turn(&started.turn_id, TurnStatus::InProgress)
|
||||
},
|
||||
})
|
||||
@@ -920,6 +921,7 @@ fn session_event_to_analytics_notification(
|
||||
fn analytics_turn(turn_id: &str, status: TurnStatus) -> Turn {
|
||||
Turn {
|
||||
id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
status,
|
||||
|
||||
@@ -721,6 +721,7 @@ fn sample_turn_start_response() -> ClientResponsePayload {
|
||||
ClientResponsePayload::TurnStart(TurnStartResponse {
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
|
||||
@@ -265,6 +265,7 @@ pub(super) fn sample_turn_start_response(turn_id: &str) -> ClientResponsePayload
|
||||
ClientResponsePayload::TurnStart(codex_app_server_protocol::TurnStartResponse {
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![],
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
@@ -284,6 +285,7 @@ pub(super) fn sample_turn_started_notification(
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![],
|
||||
status: AppServerTurnStatus::InProgress,
|
||||
@@ -321,6 +323,7 @@ pub(super) fn sample_turn_completed_notification(
|
||||
thread_id: thread_id.to_string(),
|
||||
turn: Turn {
|
||||
id: turn_id.to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![],
|
||||
status,
|
||||
|
||||
@@ -1082,6 +1082,7 @@ mod tests {
|
||||
thread_id: "thread".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
|
||||
@@ -5736,6 +5736,13 @@
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"parentTurnId": {
|
||||
"description": "ID of the turn that caused this new turn to start.\n\nSet this when starting work on behalf of another turn, such as delegated work in a different thread. Leave unset for work started directly by the user. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -5747,6 +5754,13 @@
|
||||
],
|
||||
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this new turn.\n\nWhen setting `parentTurnId`, set this to the parent turn's `rootTurnId` when known. This keeps descendant work attributed to the original turn. If omitted, the new turn becomes its own root. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -6403,6 +6403,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
+21
@@ -25604,6 +25604,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -25921,6 +25928,13 @@
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"parentTurnId": {
|
||||
"description": "ID of the turn that caused this new turn to start.\n\nSet this when starting work on behalf of another turn, such as delegated work in a different thread. Leave unset for work started directly by the user. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -25932,6 +25946,13 @@
|
||||
],
|
||||
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this new turn.\n\nWhen setting `parentTurnId`, set this to the parent turn's `rootTurnId` when known. This keeps descendant work attributed to the original turn. If omitted, the new turn becomes its own root. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
+21
@@ -23318,6 +23318,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
@@ -23635,6 +23642,13 @@
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"parentTurnId": {
|
||||
"description": "ID of the turn that caused this new turn to start.\n\nSet this when starting work on behalf of another turn, such as delegated work in a different thread. Leave unset for work started directly by the user. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -23646,6 +23660,13 @@
|
||||
],
|
||||
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this new turn.\n\nWhen setting `parentTurnId`, set this to the parent turn's `rootTurnId` when known. This keeps descendant work attributed to the original turn. If omitted, the new turn becomes its own root. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1888,6 +1888,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2628,6 +2628,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2419,6 +2419,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
+7
@@ -2419,6 +2419,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2419,6 +2419,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2680,6 +2680,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2419,6 +2419,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2628,6 +2628,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2419,6 +2419,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -1888,6 +1888,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -2419,6 +2419,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -1888,6 +1888,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -819,6 +819,13 @@
|
||||
"outputSchema": {
|
||||
"description": "Optional JSON Schema used to constrain the final assistant message for this turn."
|
||||
},
|
||||
"parentTurnId": {
|
||||
"description": "ID of the turn that caused this new turn to start.\n\nSet this when starting work on behalf of another turn, such as delegated work in a different thread. Leave unset for work started directly by the user. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"personality": {
|
||||
"anyOf": [
|
||||
{
|
||||
@@ -830,6 +837,13 @@
|
||||
],
|
||||
"description": "@deprecated `friendly` and `pragmatic` no longer select a style. Changing this does not rewrite the thread's existing instructions."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this new turn.\n\nWhen setting `parentTurnId`, set this to the parent turn's `rootTurnId` when known. This keeps descendant work attributed to the original turn. If omitted, the new turn becomes its own root. Ignored when this request adds input to an active turn.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"sandboxPolicy": {
|
||||
"anyOf": [
|
||||
{
|
||||
|
||||
@@ -1888,6 +1888,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
@@ -1888,6 +1888,13 @@
|
||||
"default": "full",
|
||||
"description": "Describes how much of `items` has been loaded for this turn."
|
||||
},
|
||||
"rootTurnId": {
|
||||
"description": "ID of the first turn in the chain of work that led to this turn. Pass this as `rootTurnId` when starting work on behalf of this turn. May be null in older history or a `review/start` response.",
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
]
|
||||
},
|
||||
"startedAt": {
|
||||
"description": "Unix timestamp (in seconds) when the turn started.",
|
||||
"format": "int64",
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -11,6 +11,12 @@ export type Turn = {
|
||||
* Identifier for this turn. Codex-generated turn IDs are UUIDv7.
|
||||
*/
|
||||
id: string,
|
||||
/**
|
||||
* ID of the first turn in the chain of work that led to this turn.
|
||||
* Pass this as `rootTurnId` when starting work on behalf of this turn.
|
||||
* May be null in older history or a `review/start` response.
|
||||
*/
|
||||
rootTurnId: string | null,
|
||||
/**
|
||||
* Thread items currently included in this turn payload.
|
||||
*/
|
||||
|
||||
@@ -19,7 +19,22 @@ disabledPluginIds?: Array<string> | null, clientUserMessageId?: string | null, i
|
||||
* Optional source classification for the caller that starts this turn.
|
||||
* Ignored when this request steers an already-active turn.
|
||||
*/
|
||||
turnTrigger?: string | null, toolOutput?: TurnToolOutput | null, /**
|
||||
turnTrigger?: string | null, /**
|
||||
* ID of the turn that caused this new turn to start.
|
||||
*
|
||||
* Set this when starting work on behalf of another turn, such as delegated
|
||||
* work in a different thread. Leave unset for work started directly by the
|
||||
* user. Ignored when this request adds input to an active turn.
|
||||
*/
|
||||
parentTurnId?: string | null, /**
|
||||
* ID of the first turn in the chain of work that led to this new turn.
|
||||
*
|
||||
* When setting `parentTurnId`, set this to the parent turn's `rootTurnId`
|
||||
* when known. This keeps descendant work attributed to the original turn.
|
||||
* If omitted, the new turn becomes its own root. Ignored when this request
|
||||
* adds input to an active turn.
|
||||
*/
|
||||
rootTurnId?: string | null, toolOutput?: TurnToolOutput | null, /**
|
||||
* Override the working directory for this turn and subsequent turns.
|
||||
*/
|
||||
cwd?: string | null, /**
|
||||
|
||||
@@ -225,6 +225,7 @@ macro_rules! client_request_definitions {
|
||||
) => {
|
||||
/// Request from the client to the server.
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
#[serde(tag = "method", rename_all = "camelCase")]
|
||||
pub enum ClientRequest {
|
||||
$(
|
||||
|
||||
@@ -134,6 +134,7 @@ impl From<ThreadHistoryTurnMetadata> for Turn {
|
||||
fn from(value: ThreadHistoryTurnMetadata) -> Self {
|
||||
Self {
|
||||
id: value.turn_id,
|
||||
root_turn_id: value.root_turn_id,
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: value.error,
|
||||
@@ -1729,6 +1730,7 @@ impl PendingTurn {
|
||||
fn snapshot(&self, items_view: TurnItemsView) -> Turn {
|
||||
Turn {
|
||||
id: self.id.clone(),
|
||||
root_turn_id: self.root_turn_id.clone(),
|
||||
items: items_view.project_items(&self.items),
|
||||
items_view,
|
||||
error: self.error.clone(),
|
||||
@@ -1744,6 +1746,7 @@ impl From<PendingTurn> for Turn {
|
||||
fn from(value: PendingTurn) -> Self {
|
||||
Self {
|
||||
id: value.id,
|
||||
root_turn_id: value.root_turn_id,
|
||||
items: value.items,
|
||||
items_view: TurnItemsView::Full,
|
||||
error: value.error,
|
||||
@@ -1988,6 +1991,7 @@ mod tests {
|
||||
review_output: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2050,6 +2054,7 @@ mod tests {
|
||||
completed_at_ms: 0,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2213,6 +2218,7 @@ mod tests {
|
||||
started_at_ms: 0,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2269,6 +2275,7 @@ mod tests {
|
||||
completed_at_ms: 1_000,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2351,6 +2358,7 @@ mod tests {
|
||||
completed_at_ms: 1_000,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2463,6 +2471,7 @@ mod tests {
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2567,6 +2576,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2670,6 +2680,7 @@ mod tests {
|
||||
saved_path: Some(test_path_buf("/tmp/ig_123.png").abs()),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-image".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2686,6 +2697,7 @@ mod tests {
|
||||
turns[0],
|
||||
Turn {
|
||||
id: "turn-image".into(),
|
||||
root_turn_id: None,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -2797,6 +2809,7 @@ mod tests {
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some("turn-1".into()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Replaced,
|
||||
@@ -3060,6 +3073,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -3732,6 +3746,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -3799,6 +3814,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -3844,6 +3860,7 @@ mod tests {
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -3908,6 +3925,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -3953,6 +3971,7 @@ mod tests {
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4038,6 +4057,7 @@ mod tests {
|
||||
Some(expected.clone())
|
||||
);
|
||||
builder.handle_event(&EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: Some(100),
|
||||
last_agent_message: None,
|
||||
@@ -4090,8 +4110,13 @@ mod tests {
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(builder.active_turn_snapshot().unwrap().items.len(), 1);
|
||||
assert_eq!(
|
||||
builder.active_turn_snapshot().unwrap().root_turn_id,
|
||||
expected.root_turn_id
|
||||
);
|
||||
|
||||
builder.handle_event(&EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
started_at: Some(100),
|
||||
last_agent_message: None,
|
||||
@@ -4103,8 +4128,16 @@ mod tests {
|
||||
expected.status = TurnStatus::Completed;
|
||||
expected.completed_at = Some(102);
|
||||
expected.duration_ms = Some(2_000);
|
||||
assert_eq!(builder.active_turn_metadata_snapshot(), Some(expected));
|
||||
assert_eq!(
|
||||
builder.active_turn_metadata_snapshot(),
|
||||
Some(expected.clone())
|
||||
);
|
||||
assert_eq!(
|
||||
builder.active_turn_snapshot_with_items_view(TurnItemsView::NotLoaded),
|
||||
Some(Turn::from(expected.clone()))
|
||||
);
|
||||
assert_eq!(builder.active_turn_snapshot().unwrap().items.len(), 1);
|
||||
assert_eq!(builder.finish()[0].root_turn_id, expected.root_turn_id);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4270,6 +4303,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4296,6 +4330,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4312,6 +4347,7 @@ mod tests {
|
||||
questions: None,
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-b".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4371,6 +4407,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: Some(10),
|
||||
last_agent_message: None,
|
||||
@@ -4395,6 +4432,7 @@ mod tests {
|
||||
vec![
|
||||
Turn {
|
||||
id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
items_view: TurnItemsView::Full,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
@@ -4420,6 +4458,7 @@ mod tests {
|
||||
},
|
||||
Turn {
|
||||
id: "turn-b".into(),
|
||||
root_turn_id: None,
|
||||
items_view: TurnItemsView::Full,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "item-2".into(),
|
||||
@@ -4460,6 +4499,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4486,6 +4526,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some("turn-a".into()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Replaced,
|
||||
@@ -4541,6 +4582,7 @@ mod tests {
|
||||
resume_metadata: None,
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-compact".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4556,6 +4598,7 @@ mod tests {
|
||||
turns,
|
||||
vec![Turn {
|
||||
id: "turn-compact".into(),
|
||||
root_turn_id: None,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -4810,6 +4853,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4835,6 +4879,7 @@ mod tests {
|
||||
turns[0],
|
||||
Turn {
|
||||
id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: None,
|
||||
@@ -4881,6 +4926,7 @@ mod tests {
|
||||
}),
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -4935,6 +4981,7 @@ mod tests {
|
||||
..Default::default()
|
||||
}),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: Some(10),
|
||||
last_agent_message: None,
|
||||
@@ -4958,6 +5005,7 @@ mod tests {
|
||||
build_turns_from_rollout_items(&items),
|
||||
vec![Turn {
|
||||
id: "turn-a".into(),
|
||||
root_turn_id: None,
|
||||
items_view: TurnItemsView::Full,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "item-1".into(),
|
||||
@@ -5008,6 +5056,7 @@ mod tests {
|
||||
})),
|
||||
RolloutItem::ResponseItem(hook_prompt.into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -5091,6 +5140,7 @@ mod tests {
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -5152,6 +5202,7 @@ mod tests {
|
||||
.into(),
|
||||
),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -5331,6 +5382,7 @@ mod tests {
|
||||
)));
|
||||
let complete_changes = builder.handle_rollout_item_with_changes(&RolloutItem::EventMsg(
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -5421,6 +5473,7 @@ mod tests {
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-a".into(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
|
||||
@@ -35,7 +35,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(event)) => ThreadHistoryChangeSet {
|
||||
changed_turns: vec![ThreadHistoryTurnMetadata {
|
||||
turn_id: event.turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
root_turn_id: event.root_turn_id.clone(),
|
||||
status: if event.error.is_some() {
|
||||
TurnStatus::Failed
|
||||
} else {
|
||||
@@ -60,7 +60,7 @@ pub fn project_rollout_line(line: &RolloutLine) -> ThreadHistoryChangeSet {
|
||||
ThreadHistoryChangeSet {
|
||||
changed_turns: vec![ThreadHistoryTurnMetadata {
|
||||
turn_id: turn_id.clone(),
|
||||
root_turn_id: None,
|
||||
root_turn_id: event.root_turn_id.clone(),
|
||||
status: TurnStatus::Interrupted,
|
||||
error: event.error.as_ref().map(|error| TurnError {
|
||||
message: error.message.clone(),
|
||||
|
||||
@@ -37,6 +37,7 @@ fn projects_turn_lifecycle_without_prior_builder_state() {
|
||||
)));
|
||||
let completed = project(RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
TurnCompleteEvent {
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
@@ -60,7 +61,7 @@ fn projects_turn_lifecycle_without_prior_builder_state() {
|
||||
ThreadHistoryChangeSet {
|
||||
changed_turns: vec![ThreadHistoryTurnMetadata {
|
||||
turn_id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
status: TurnStatus::Completed,
|
||||
error: None,
|
||||
started_at: Some(10),
|
||||
@@ -82,6 +83,7 @@ fn projects_failed_turn_completion_as_snapshot() {
|
||||
|
||||
let changes = project(RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
error: Some(error),
|
||||
@@ -199,6 +201,7 @@ fn projects_optional_completed_item_lifecycle_timestamps() {
|
||||
fn ignores_legacy_abort_without_turn_id_and_context_only_records() {
|
||||
let aborted = project(RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
@@ -237,6 +240,7 @@ fn ignores_legacy_abort_without_turn_id_and_context_only_records() {
|
||||
fn projects_identified_turn_aborts() {
|
||||
let changes = project(RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
TurnAbortedEvent {
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
@@ -251,7 +255,7 @@ fn projects_identified_turn_aborts() {
|
||||
ThreadHistoryChangeSet {
|
||||
changed_turns: vec![ThreadHistoryTurnMetadata {
|
||||
turn_id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
root_turn_id: Some("root-turn".into()),
|
||||
status: TurnStatus::Interrupted,
|
||||
error: None,
|
||||
started_at: Some(10),
|
||||
|
||||
@@ -5060,6 +5060,8 @@ fn turn_start_params_preserve_explicit_null_service_tier() {
|
||||
client_user_message_id: None,
|
||||
input: vec![],
|
||||
turn_trigger: None,
|
||||
parent_turn_id: None,
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
|
||||
@@ -386,6 +386,10 @@ impl<'de> Deserialize<'de> for Thread {
|
||||
pub struct Turn {
|
||||
/// Identifier for this turn. Codex-generated turn IDs are UUIDv7.
|
||||
pub id: String,
|
||||
/// ID of the first turn in the chain of work that led to this turn.
|
||||
/// Pass this as `rootTurnId` when starting work on behalf of this turn.
|
||||
/// May be null in older history or a `review/start` response.
|
||||
pub root_turn_id: Option<String>,
|
||||
/// Thread items currently included in this turn payload.
|
||||
pub items: Vec<ThreadItem>,
|
||||
/// Describes how much of `items` has been loaded for this turn.
|
||||
|
||||
@@ -177,6 +177,21 @@ pub struct TurnStartParams {
|
||||
/// Ignored when this request steers an already-active turn.
|
||||
#[ts(optional = nullable)]
|
||||
pub turn_trigger: Option<String>,
|
||||
/// ID of the turn that caused this new turn to start.
|
||||
///
|
||||
/// Set this when starting work on behalf of another turn, such as delegated
|
||||
/// work in a different thread. Leave unset for work started directly by the
|
||||
/// user. Ignored when this request adds input to an active turn.
|
||||
#[ts(optional = nullable)]
|
||||
pub parent_turn_id: Option<String>,
|
||||
/// ID of the first turn in the chain of work that led to this new turn.
|
||||
///
|
||||
/// When setting `parentTurnId`, set this to the parent turn's `rootTurnId`
|
||||
/// when known. This keeps descendant work attributed to the original turn.
|
||||
/// If omitted, the new turn becomes its own root. Ignored when this request
|
||||
/// adds input to an active turn.
|
||||
#[ts(optional = nullable)]
|
||||
pub root_turn_id: Option<String>,
|
||||
#[ts(optional = nullable)]
|
||||
pub tool_output: Option<Box<TurnToolOutput>>,
|
||||
/// Optional metadata to enrich Codex's ResponsesAPI turn metadata.
|
||||
|
||||
@@ -162,6 +162,7 @@ pub(crate) async fn apply_bespoke_event_handling(
|
||||
let state = thread_state.lock().await;
|
||||
let mut turn = state.active_turn_snapshot().unwrap_or_else(|| Turn {
|
||||
id: payload.turn_id.clone(),
|
||||
root_turn_id: payload.root_turn_id.clone(),
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
@@ -1311,6 +1312,7 @@ async fn handle_turn_plan_update(
|
||||
|
||||
struct TurnCompletionMetadata {
|
||||
status: TurnStatus,
|
||||
root_turn_id: Option<String>,
|
||||
error: Option<TurnError>,
|
||||
last_agent_message: Option<ThreadItem>,
|
||||
started_at: Option<i64>,
|
||||
@@ -1332,6 +1334,7 @@ async fn emit_turn_completed_with_status(
|
||||
thread_id: conversation_id.to_string(),
|
||||
turn: Turn {
|
||||
id: event_turn_id,
|
||||
root_turn_id: turn_completion_metadata.root_turn_id,
|
||||
items,
|
||||
items_view,
|
||||
error: turn_completion_metadata.error,
|
||||
@@ -1519,6 +1522,7 @@ async fn handle_turn_complete(
|
||||
event_turn_id,
|
||||
TurnCompletionMetadata {
|
||||
status,
|
||||
root_turn_id: turn_complete_event.root_turn_id,
|
||||
error,
|
||||
last_agent_message,
|
||||
started_at: turn_summary.started_at,
|
||||
@@ -1544,6 +1548,7 @@ async fn handle_turn_interrupted(
|
||||
event_turn_id,
|
||||
TurnCompletionMetadata {
|
||||
status: TurnStatus::Interrupted,
|
||||
root_turn_id: turn_aborted_event.root_turn_id,
|
||||
error: turn_aborted_event.error.map(|error| TurnError {
|
||||
message: error.message,
|
||||
codex_error_info: error.codex_error_info.map(Into::into),
|
||||
@@ -2179,6 +2184,7 @@ mod tests {
|
||||
|
||||
fn turn_complete_event(turn_id: &str) -> TurnCompleteEvent {
|
||||
TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2191,6 +2197,7 @@ mod tests {
|
||||
|
||||
fn turn_aborted_event(turn_id: &str) -> TurnAbortedEvent {
|
||||
TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(turn_id.to_string()),
|
||||
started_at: None,
|
||||
reason: codex_protocol::protocol::TurnAbortReason::Interrupted,
|
||||
@@ -3513,7 +3520,10 @@ mod tests {
|
||||
handle_turn_interrupted(
|
||||
conversation_id,
|
||||
event_turn_id.clone(),
|
||||
turn_aborted_event(&event_turn_id),
|
||||
TurnAbortedEvent {
|
||||
root_turn_id: Some("root-a".to_string()),
|
||||
..turn_aborted_event(&event_turn_id)
|
||||
},
|
||||
&outgoing,
|
||||
&thread_state,
|
||||
)
|
||||
@@ -3523,6 +3533,7 @@ mod tests {
|
||||
match msg {
|
||||
ServerNotification::TurnCompleted(n) => {
|
||||
assert_eq!(n.turn.id, event_turn_id);
|
||||
assert_eq!(n.turn.root_turn_id.as_deref(), Some("root-a"));
|
||||
assert_eq!(n.turn.status, TurnStatus::Interrupted);
|
||||
assert_eq!(n.turn.error, None);
|
||||
assert_eq!(n.turn.completed_at, Some(TEST_TURN_COMPLETED_AT));
|
||||
|
||||
@@ -1033,6 +1033,7 @@ mod tests {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
status: TurnStatus::Completed,
|
||||
|
||||
@@ -684,6 +684,8 @@ async fn turn_start_jsonrpc_span_parents_core_turn_spans() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: None,
|
||||
parent_turn_id: None,
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
|
||||
@@ -103,6 +103,7 @@ impl ThreadRequestProcessor {
|
||||
if let Err(err) = thread
|
||||
.append_rollout_items(&[RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(saved.turn_id.clone()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
|
||||
@@ -5901,6 +5901,7 @@ fn stored_turn_to_api_turn(
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
Ok(Turn {
|
||||
id: turn.turn_id,
|
||||
root_turn_id: turn.root_turn_id,
|
||||
items,
|
||||
items_view,
|
||||
status,
|
||||
|
||||
@@ -291,6 +291,7 @@ mod thread_processor_behavior_tests {
|
||||
))];
|
||||
let active_turn = Turn {
|
||||
id: "live-turn".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "live-user-message".to_string(),
|
||||
client_id: None,
|
||||
|
||||
@@ -205,8 +205,11 @@ impl ThreadQueueRequestProcessor {
|
||||
)
|
||||
.await
|
||||
.map_err(queue_error)?;
|
||||
let turn_id = match submission {
|
||||
StartIfIdleSubmission::Started { turn_id } => turn_id,
|
||||
let (turn_id, root_turn_id) = match submission {
|
||||
StartIfIdleSubmission::Started {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
} => (turn_id, root_turn_id),
|
||||
StartIfIdleSubmission::NotSubmitted {
|
||||
reason: NotSubmittedReason::ServerDraining,
|
||||
} => return Err(crate::error_code::server_draining_error()),
|
||||
@@ -229,6 +232,7 @@ impl ThreadQueueRequestProcessor {
|
||||
Ok(ThreadQueueStartResponse {
|
||||
turn: Turn {
|
||||
id: turn_id,
|
||||
root_turn_id: Some(root_turn_id),
|
||||
items: vec![],
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
|
||||
@@ -236,6 +236,7 @@ mod tests {
|
||||
name: None,
|
||||
turns: vec![Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items,
|
||||
items_view: TurnItemsView::Full,
|
||||
status: TurnStatus::Completed,
|
||||
|
||||
@@ -654,10 +654,12 @@ impl TurnRequestProcessor {
|
||||
.with_thread_settings(thread_settings)
|
||||
.on_start(TurnStartOptions {
|
||||
turn_trigger: params.turn_trigger,
|
||||
parent_turn_id: params.parent_turn_id,
|
||||
initiating_agent_path: None,
|
||||
root_turn_id: params.root_turn_id,
|
||||
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)
|
||||
@@ -669,9 +671,15 @@ impl TurnRequestProcessor {
|
||||
self.track_error_response(&request_id, &error, /*error_type*/ None);
|
||||
error
|
||||
})?;
|
||||
let (turn_id, started) = match submission {
|
||||
TurnInputSubmission::Started { turn_id } => (turn_id, true),
|
||||
TurnInputSubmission::Steered { turn_id } => (turn_id, false),
|
||||
let (turn_id, root_turn_id, started) = match submission {
|
||||
TurnInputSubmission::Started {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
} => (turn_id, root_turn_id, true),
|
||||
TurnInputSubmission::Steered {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
} => (turn_id, root_turn_id, false),
|
||||
TurnInputSubmission::NotSubmitted { reason } => {
|
||||
let error = if reason == NotSubmittedReason::ServerDraining {
|
||||
crate::error_code::server_draining_error()
|
||||
@@ -703,6 +711,7 @@ impl TurnRequestProcessor {
|
||||
.await;
|
||||
let turn = Turn {
|
||||
id: turn_id,
|
||||
root_turn_id: Some(root_turn_id),
|
||||
items: vec![],
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
@@ -1409,6 +1418,7 @@ impl TurnRequestProcessor {
|
||||
|
||||
Turn {
|
||||
id: turn_id,
|
||||
root_turn_id: None,
|
||||
items,
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
error: None,
|
||||
|
||||
@@ -1534,6 +1534,7 @@ async fn thread_fork_creates_reference_backed_paginated_thread() -> Result<()> {
|
||||
collaboration_mode_kind: Default::default(),
|
||||
})),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
@@ -1858,7 +1859,7 @@ async fn assert_thread_fork_freezes_active_paginated_turn_as_interrupted(
|
||||
&RolloutItem::EventMsg(EventMsg::TurnStarted(TurnStartedEvent {
|
||||
turn_attribution: None,
|
||||
turn_id: "active-turn".to_string(),
|
||||
root_turn_id: None,
|
||||
root_turn_id: Some("causal-root".to_string()),
|
||||
trace_id: None,
|
||||
started_at: Some(10),
|
||||
model_context_window: None,
|
||||
@@ -1967,6 +1968,7 @@ async fn assert_thread_fork_freezes_active_paginated_turn_as_interrupted(
|
||||
append_rollout_item_to_path(
|
||||
source_path.as_path(),
|
||||
&RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "active-turn".to_string(),
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
@@ -2034,6 +2036,7 @@ async fn assert_thread_fork_freezes_active_paginated_turn_as_interrupted(
|
||||
.await?;
|
||||
assert_eq!(turns.len(), 1);
|
||||
assert_eq!(turns[0].id, "active-turn");
|
||||
assert_eq!(turns[0].root_turn_id.as_deref(), Some("causal-root"));
|
||||
assert_eq!(turns[0].status, TurnStatus::Interrupted);
|
||||
assert_eq!(turns[0].items.len(), 1);
|
||||
assert!(matches!(
|
||||
|
||||
@@ -815,6 +815,10 @@ async fn queue_start_without_id_starts_the_head_when_idle() -> Result<()> {
|
||||
.await?;
|
||||
let started: ThreadQueueStartResponse =
|
||||
timeout(READ_TIMEOUT, app.read_response(start_request_id)).await??;
|
||||
assert_eq!(
|
||||
started.turn.root_turn_id.as_deref(),
|
||||
Some(started.turn.id.as_str())
|
||||
);
|
||||
let started_item = loop {
|
||||
let started_item: ItemStartedNotification =
|
||||
timeout(READ_TIMEOUT, app.read_notification("item/started")).await??;
|
||||
|
||||
@@ -215,6 +215,7 @@ async fn thread_read_can_include_turns() -> Result<()> {
|
||||
assert_eq!(thread.turns.len(), 1);
|
||||
let turn = &thread.turns[0];
|
||||
assert_eq!(turn.status, TurnStatus::Completed);
|
||||
assert_eq!(turn.root_turn_id, None);
|
||||
assert_eq!(turn.items_view, TurnItemsView::Full);
|
||||
assert_eq!(turn.items.len(), 1, "expected user message item");
|
||||
match &turn.items[0] {
|
||||
@@ -238,9 +239,13 @@ async fn thread_read_can_include_turns() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Preserves a file-backed image across a completed turn, including a cold history read.
|
||||
/// Preserves a file-backed image and causal root across completion and a cold history read.
|
||||
#[test_case::test_case(ThreadHistoryMode::Legacy; "legacy")]
|
||||
#[test_case::test_case(ThreadHistoryMode::Paginated; "paginated")]
|
||||
#[tokio::test]
|
||||
async fn thread_read_preserves_file_id_from_completed_turn() -> Result<()> {
|
||||
async fn thread_read_preserves_file_id_and_root_from_completed_turn(
|
||||
history_mode: ThreadHistoryMode,
|
||||
) -> Result<()> {
|
||||
let server = create_mock_responses_server_repeating_assistant("Done").await;
|
||||
let codex_home = TempDir::new()?;
|
||||
MockResponsesConfig::new(&server.uri()).write(codex_home.path())?;
|
||||
@@ -252,6 +257,7 @@ async fn thread_read_preserves_file_id_from_completed_turn() -> Result<()> {
|
||||
let start_id = mcp
|
||||
.send_thread_start_request_with_auto_env(ThreadStartParams {
|
||||
model: Some("mock-model".to_string()),
|
||||
history_mode: Some(history_mode),
|
||||
..Default::default()
|
||||
})
|
||||
.await?;
|
||||
@@ -264,15 +270,18 @@ async fn thread_read_preserves_file_id_from_completed_turn() -> Result<()> {
|
||||
detail: None,
|
||||
};
|
||||
|
||||
timeout(
|
||||
let completed = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.start_turn_and_wait_for_completion(TurnStartParams {
|
||||
thread_id: thread.id.clone(),
|
||||
input: vec![image.clone()],
|
||||
root_turn_id: Some("causal-root".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
)
|
||||
.await??;
|
||||
let expected_root = "causal-root";
|
||||
assert_eq!(completed.turn.root_turn_id.as_deref(), Some(expected_root));
|
||||
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
@@ -287,6 +296,10 @@ async fn thread_read_preserves_file_id_from_completed_turn() -> Result<()> {
|
||||
panic!("expected completed turn to start with a user message");
|
||||
};
|
||||
assert_eq!(content, &vec![image.clone()]);
|
||||
assert_eq!(
|
||||
loaded_thread.turns[0].root_turn_id.as_deref(),
|
||||
Some(expected_root)
|
||||
);
|
||||
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.shutdown_gracefully()).await??;
|
||||
drop(mcp);
|
||||
@@ -297,7 +310,7 @@ async fn thread_read_preserves_file_id_from_completed_turn() -> Result<()> {
|
||||
.await?;
|
||||
let read_id = mcp
|
||||
.send_thread_read_request(ThreadReadParams {
|
||||
thread_id: thread.id,
|
||||
thread_id: thread.id.clone(),
|
||||
include_turns: true,
|
||||
})
|
||||
.await?;
|
||||
@@ -308,6 +321,24 @@ async fn thread_read_preserves_file_id_from_completed_turn() -> Result<()> {
|
||||
panic!("expected persisted turn to start with a user message");
|
||||
};
|
||||
assert_eq!(content, &vec![image]);
|
||||
assert_eq!(
|
||||
restarted_thread.turns[0].root_turn_id.as_deref(),
|
||||
Some(expected_root)
|
||||
);
|
||||
let page_id = mcp
|
||||
.send_thread_turns_list_request(ThreadTurnsListParams {
|
||||
thread_id: thread.id,
|
||||
cursor: None,
|
||||
limit: Some(1),
|
||||
sort_direction: Some(SortDirection::Asc),
|
||||
items_view: None,
|
||||
})
|
||||
.await?;
|
||||
let page: ThreadTurnsListResponse =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_response(page_id)).await??;
|
||||
assert_eq!(page.data.len(), 1);
|
||||
assert_eq!(page.data[0].id, completed.turn.id);
|
||||
assert_eq!(page.data[0].root_turn_id.as_deref(), Some(expected_root));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1787,6 +1818,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
|
||||
let expected_turn_1_full = Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
@@ -1816,6 +1848,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
};
|
||||
let expected_turn_2_full = Turn {
|
||||
id: "turn-2".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![ThreadItem::UserMessage {
|
||||
id: "user-2".to_string(),
|
||||
client_id: None,
|
||||
@@ -1979,6 +2012,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
first_page.data,
|
||||
vec![Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items: vec![
|
||||
ThreadItem::UserMessage {
|
||||
id: "user-1".to_string(),
|
||||
@@ -2016,6 +2050,7 @@ async fn paginated_history_lists_and_legacy_reads_use_projected_turns_and_items(
|
||||
second_page.data,
|
||||
vec![Turn {
|
||||
id: "turn-2".to_string(),
|
||||
root_turn_id: None,
|
||||
items: Vec::new(),
|
||||
items_view: TurnItemsView::NotLoaded,
|
||||
status: TurnStatus::Interrupted,
|
||||
@@ -2438,6 +2473,7 @@ fn paginated_turn_started(turn_id: &str) -> RolloutItem {
|
||||
|
||||
fn paginated_turn_completed(turn_id: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
|
||||
@@ -4317,6 +4317,7 @@ async fn thread_resume_token_usage_replay_can_belong_to_interrupted_turn() -> Re
|
||||
"timestamp": meta_rfc3339,
|
||||
"type": "event_msg",
|
||||
"payload": serde_json::to_value(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(interrupted_turn_id.to_string()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
|
||||
@@ -129,6 +129,7 @@ async fn timeline_pages_mix_items_and_resolve_the_opening_realtime_session() ->
|
||||
},
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
|
||||
@@ -583,11 +583,13 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> {
|
||||
})
|
||||
.await?;
|
||||
assert!(!turn.id.is_empty());
|
||||
assert_eq!(turn.root_turn_id.as_deref(), Some(turn.id.as_str()));
|
||||
|
||||
let started: TurnStartedNotification =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("turn/started")).await??;
|
||||
assert_eq!(started.thread_id, thread.id);
|
||||
assert_eq!(started.turn.id, turn.id);
|
||||
assert_eq!(started.turn.root_turn_id.as_deref(), Some(turn.id.as_str()));
|
||||
assert_eq!(started.turn.status, TurnStatus::InProgress);
|
||||
|
||||
let completed: TurnCompletedNotification = timeout(
|
||||
@@ -597,6 +599,10 @@ async fn turn_start_with_empty_input_runs_model_request() -> Result<()> {
|
||||
.await??;
|
||||
assert_eq!(completed.thread_id, thread.id);
|
||||
assert_eq!(completed.turn.id, turn.id);
|
||||
assert_eq!(
|
||||
completed.turn.root_turn_id.as_deref(),
|
||||
Some(turn.id.as_str())
|
||||
);
|
||||
assert_eq!(completed.turn.status, TurnStatus::Completed);
|
||||
assert_eq!(completed.turn.items_view, TurnItemsView::Summary);
|
||||
assert!(matches!(
|
||||
@@ -703,16 +709,17 @@ async fn turn_start_steers_active_turn_and_returns_active_turn_id() -> Result<()
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: Some("goal".to_string()),
|
||||
parent_turn_id: Some("initiating-turn".to_string()),
|
||||
root_turn_id: Some("causal-root".to_string()),
|
||||
cyber_access_program: Some(CyberAccessProgram::DaybreakBlue),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/started"),
|
||||
)
|
||||
.await??;
|
||||
let started: TurnStartedNotification =
|
||||
timeout(DEFAULT_READ_TIMEOUT, mcp.read_notification("turn/started")).await??;
|
||||
assert_eq!(active_turn.root_turn_id.as_deref(), Some("causal-root"));
|
||||
assert_eq!(started.turn.root_turn_id.as_deref(), Some("causal-root"));
|
||||
timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
server.wait_for_request_count(/*count*/ 1),
|
||||
@@ -746,21 +753,25 @@ async fn turn_start_steers_active_turn_and_returns_active_turn_id() -> Result<()
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: Some("user".to_string()),
|
||||
parent_turn_id: Some("steering-turn".to_string()),
|
||||
root_turn_id: Some("steering-root".to_string()),
|
||||
cyber_access_program: Some(CyberAccessProgram::Standard),
|
||||
..Default::default()
|
||||
},
|
||||
})
|
||||
.await?;
|
||||
assert_eq!(steered_turn.id, active_turn.id);
|
||||
assert_eq!(steered_turn.root_turn_id, active_turn.root_turn_id);
|
||||
|
||||
release_response
|
||||
.send(())
|
||||
.expect("active response gate should remain open");
|
||||
timeout(
|
||||
let completed: TurnCompletedNotification = timeout(
|
||||
DEFAULT_READ_TIMEOUT,
|
||||
mcp.read_stream_until_notification_message("turn/completed"),
|
||||
mcp.read_notification("turn/completed"),
|
||||
)
|
||||
.await??;
|
||||
assert_eq!(completed.turn.root_turn_id.as_deref(), Some("causal-root"));
|
||||
|
||||
let requests = server.requests().await;
|
||||
assert_eq!(requests.len(), 2);
|
||||
@@ -773,6 +784,8 @@ async fn turn_start_steers_active_turn_and_returns_active_turn_id() -> Result<()
|
||||
.context("expected x-codex-turn-metadata")?,
|
||||
)?;
|
||||
assert_eq!(turn_metadata["turn_trigger"].as_str(), Some("goal"));
|
||||
assert_eq!(turn_metadata["parent_turn_id"], "initiating-turn");
|
||||
assert_eq!(turn_metadata["root_turn_id"], "causal-root");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -3389,6 +3402,8 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: None,
|
||||
parent_turn_id: None,
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
@@ -3442,6 +3457,8 @@ async fn turn_start_explicit_local_environment_updates_legacy_cwd_between_turns(
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
turn_trigger: None,
|
||||
parent_turn_id: None,
|
||||
root_turn_id: None,
|
||||
tool_output: None,
|
||||
responsesapi_client_metadata: None,
|
||||
additional_context: None,
|
||||
|
||||
@@ -142,7 +142,7 @@ impl LocalAgentControl {
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(input).on_start(start_options))
|
||||
.await
|
||||
{
|
||||
Ok(TurnInputSubmission::Started { turn_id }) => Ok(turn_id),
|
||||
Ok(TurnInputSubmission::Started { turn_id, .. }) => Ok(turn_id),
|
||||
Ok(TurnInputSubmission::Steered { .. }) => {
|
||||
// MAv1 exposes an opaque `submission_id` to the model. The legacy
|
||||
// `Op::UserInput` path returned a fresh ID for every steer, while the
|
||||
|
||||
@@ -194,6 +194,7 @@ async fn mark_thread_completed(thread: &CodexThread) {
|
||||
.send_event(
|
||||
turn.as_ref(),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn.sub_id.clone(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("done".to_string()),
|
||||
@@ -214,6 +215,7 @@ async fn mark_thread_interrupted(thread: &CodexThread) {
|
||||
.send_event(
|
||||
turn.as_ref(),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(turn.sub_id.clone()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
|
||||
@@ -590,6 +590,7 @@ async fn on_event_updates_status_from_task_complete() {
|
||||
),
|
||||
] {
|
||||
let status = agent_status_from_event(&EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("done".to_string()),
|
||||
@@ -617,6 +618,7 @@ async fn on_event_updates_status_from_error() {
|
||||
#[tokio::test]
|
||||
async fn on_event_updates_status_from_turn_aborted() {
|
||||
let status = agent_status_from_event(&EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -3948,6 +3950,7 @@ async fn multi_agent_v2_completion_ignores_dead_direct_parent() {
|
||||
.send_event(
|
||||
tester_turn.as_ref(),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: tester_turn.sub_id.clone(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("done".to_string()),
|
||||
@@ -4030,6 +4033,7 @@ async fn multi_agent_v2_completion_queues_message_for_direct_parent() {
|
||||
.send_event(
|
||||
tester_turn.as_ref(),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: tester_turn.sub_id.clone(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("done".to_string()),
|
||||
|
||||
@@ -51,6 +51,7 @@ async fn forward_events_filters_private_events_before_blocked_send_is_cancelled(
|
||||
.send(Event {
|
||||
id: "full".to_string(),
|
||||
msg: EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
|
||||
@@ -425,9 +425,13 @@ impl CodexThread {
|
||||
.submit_turn_input_with_mode(request, TurnInputMode::StartIfIdle)
|
||||
.await?
|
||||
{
|
||||
TurnInputSubmission::Started { turn_id } => {
|
||||
Ok(StartIfIdleSubmission::Started { turn_id })
|
||||
}
|
||||
TurnInputSubmission::Started {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
} => Ok(StartIfIdleSubmission::Started {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
}),
|
||||
TurnInputSubmission::NotSubmitted { reason } => {
|
||||
Ok(StartIfIdleSubmission::NotSubmitted { reason })
|
||||
}
|
||||
@@ -489,9 +493,13 @@ impl CodexThread {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
TurnInputSubmission::Started { turn_id } => {
|
||||
Ok(StartIfIdleSubmission::Started { turn_id })
|
||||
}
|
||||
TurnInputSubmission::Started {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
} => Ok(StartIfIdleSubmission::Started {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
}),
|
||||
TurnInputSubmission::NotSubmitted { reason } => {
|
||||
Ok(StartIfIdleSubmission::NotSubmitted { reason })
|
||||
}
|
||||
@@ -554,7 +562,9 @@ impl CodexThread {
|
||||
.submit_turn_input_with_mode(request, TurnInputMode::Steer { expected_turn_id })
|
||||
.await?
|
||||
{
|
||||
TurnInputSubmission::Steered { turn_id } => Ok(SteerSubmission::Steered { turn_id }),
|
||||
TurnInputSubmission::Steered { turn_id, .. } => {
|
||||
Ok(SteerSubmission::Steered { turn_id })
|
||||
}
|
||||
TurnInputSubmission::NotSubmitted { reason } => {
|
||||
Ok(SteerSubmission::NotSubmitted { reason })
|
||||
}
|
||||
|
||||
@@ -107,6 +107,7 @@ async fn run_review_preserves_evidence_during_parent_compaction() {
|
||||
let text = serde_json::to_string(&content).unwrap();
|
||||
reply
|
||||
.send(Ok(TurnInputSubmission::Started {
|
||||
root_turn_id: id.clone(),
|
||||
turn_id: id.clone(),
|
||||
}))
|
||||
.unwrap();
|
||||
@@ -179,6 +180,7 @@ fn turn_complete_event(
|
||||
Event {
|
||||
id: turn_id.to_string(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: last_agent_message.map(str::to_string),
|
||||
@@ -194,6 +196,7 @@ fn turn_aborted_event(turn_id: &str) -> Event {
|
||||
Event {
|
||||
id: turn_id.to_string(),
|
||||
msg: EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(turn_id.to_string()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -728,6 +731,7 @@ async fn run_review_on_reused_session_waits_for_submitted_turn() {
|
||||
};
|
||||
reply
|
||||
.send(Ok(TurnInputSubmission::Started {
|
||||
root_turn_id: id.clone(),
|
||||
turn_id: id.clone(),
|
||||
}))
|
||||
.expect("reply to guardian submission");
|
||||
@@ -791,7 +795,10 @@ async fn run_review_removes_trunk_when_event_stream_is_broken() {
|
||||
panic!("expected turn-input submission");
|
||||
};
|
||||
reply
|
||||
.send(Ok(TurnInputSubmission::Started { turn_id: id }))
|
||||
.send(Ok(TurnInputSubmission::Started {
|
||||
root_turn_id: id.clone(),
|
||||
turn_id: id,
|
||||
}))
|
||||
.expect("reply to guardian submission");
|
||||
drop(tx_event);
|
||||
|
||||
|
||||
@@ -2971,6 +2971,7 @@ async fn guardian_reused_trunk_ignores_stale_prior_turn_completion() -> anyhow::
|
||||
.send_trunk_event_raw_for_test(Event {
|
||||
id: "stale-turn".to_string(),
|
||||
msg: EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: "stale-turn".to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: Some(
|
||||
|
||||
@@ -73,6 +73,7 @@ pub(super) async fn record(session: &Session, text: String) -> std::io::Result<(
|
||||
if recording_turn {
|
||||
events.push(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
turn_id: turn.sub_id.clone(),
|
||||
root_turn_id: Some(turn.sub_id.clone()),
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
started_at: Some(now.timestamp()),
|
||||
|
||||
@@ -140,6 +140,7 @@ fn interrupted_turn_is_not_associated_with_a_new_voice_session(
|
||||
},
|
||||
));
|
||||
let aborted = EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: aborted_turn_id.map(str::to_string),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
@@ -175,6 +176,7 @@ fn interrupted_turn_is_not_associated_with_a_new_voice_session(
|
||||
fn interrupted_turn_keeps_its_existing_voice_session_for_late_artifacts() {
|
||||
let mut state = started_state();
|
||||
state.observe(&EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
|
||||
@@ -2266,12 +2266,7 @@ impl Session {
|
||||
let event = TurnStartedEvent {
|
||||
turn_attribution: (task_kind == TaskKind::Regular).then(|| turn_context.attribution()),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
root_turn_id: Some(
|
||||
turn_context
|
||||
.turn_metadata_state
|
||||
.root_turn_id()
|
||||
.unwrap_or_else(|| turn_context.sub_id.clone()),
|
||||
),
|
||||
root_turn_id: Some(turn_context.root_turn_id()),
|
||||
trace_id: turn_context.trace_id.clone(),
|
||||
started_at: turn_context.turn_timing_state.started_at_unix_secs().await,
|
||||
model_context_window: turn_context.model_context_window(),
|
||||
|
||||
@@ -284,6 +284,7 @@ fn completed_user_turn_rollout(
|
||||
rollout_items.extend(items);
|
||||
rollout_items.push(RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
@@ -547,6 +548,7 @@ async fn record_initial_history_resumed_hydrates_previous_turn_settings_from_lif
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
@@ -638,6 +640,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
|
||||
RolloutItem::ResponseItem(turn_one_assistant.clone().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: first_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -681,6 +684,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_com
|
||||
RolloutItem::ResponseItem(turn_two_assistant.into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: rolled_back_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -768,6 +772,7 @@ async fn reconstruct_history_rollback_keeps_history_and_metadata_in_sync_for_inc
|
||||
RolloutItem::ResponseItem(turn_one_assistant.clone().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: first_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -873,6 +878,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
|
||||
RolloutItem::ResponseItem(turn_one_assistant.clone().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: first_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -907,6 +913,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
|
||||
RolloutItem::ResponseItem(turn_two_assistant.into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: second_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -935,6 +942,7 @@ async fn reconstruct_history_rollback_skips_non_user_turns_for_history_and_metad
|
||||
}),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: standalone_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1017,6 +1025,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
|
||||
RolloutItem::ResponseItem(assistant_message("turn 1 assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: first_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1042,6 +1051,7 @@ async fn reconstruct_history_rollback_counts_inter_agent_assistant_turns() {
|
||||
RolloutItem::ResponseItem(assistant_reply.into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: assistant_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1119,6 +1129,7 @@ async fn reconstruct_history_rollback_clears_history_and_metadata_when_exceeding
|
||||
RolloutItem::ResponseItem(assistant_message("only assistant").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: only_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1177,6 +1188,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: user_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1200,6 +1212,7 @@ async fn record_initial_history_resumed_rollback_skips_only_user_turns() {
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: standalone_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1262,6 +1275,7 @@ async fn record_initial_history_resumed_rollback_drops_incomplete_user_turn_comp
|
||||
RolloutItem::TurnContext(previous_context_item.clone()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: previous_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -1640,6 +1654,7 @@ async fn bounded_replay_matches_full_replay_after_empty_turn_compactions(
|
||||
});
|
||||
wake_items.push(RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
codex_protocol::protocol::TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(format!("wake-{window_number}")),
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
@@ -2095,6 +2110,7 @@ async fn reconstruct_history_legacy_compaction_without_replacement_history_clear
|
||||
RolloutItem::TurnContext(current_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: current_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2189,6 +2205,7 @@ async fn record_initial_history_resumed_turn_context_after_compaction_reestablis
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: previous_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2315,6 +2332,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: previous_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2347,6 +2365,7 @@ async fn record_initial_history_resumed_aborted_turn_without_id_clears_active_tu
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
codex_protocol::protocol::TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: None,
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2457,6 +2476,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: previous_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2489,6 +2509,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
)),
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
codex_protocol::protocol::TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(unmatched_abort_turn_id),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2500,6 +2521,7 @@ async fn record_initial_history_resumed_unmatched_abort_preserves_active_turn_fo
|
||||
RolloutItem::TurnContext(current_context_item.clone()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: current_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2600,6 +2622,7 @@ async fn record_initial_history_resumed_trailing_incomplete_turn_compaction_clea
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: previous_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -2791,6 +2814,7 @@ async fn record_initial_history_resumed_replaced_incomplete_compacted_turn_clear
|
||||
RolloutItem::TurnContext(previous_context_item),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: previous_turn_id,
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
|
||||
@@ -816,6 +816,7 @@ async fn interrupting_regular_turn_waiting_on_startup_prewarm_emits_turn_aborted
|
||||
.expect("expected turn aborted event")
|
||||
.expect("channel open");
|
||||
let EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id,
|
||||
turn_id,
|
||||
reason,
|
||||
error: _,
|
||||
@@ -826,6 +827,7 @@ async fn interrupting_regular_turn_waiting_on_startup_prewarm_emits_turn_aborted
|
||||
else {
|
||||
panic!("expected turn aborted event");
|
||||
};
|
||||
assert_eq!(root_turn_id, Some(tc.root_turn_id()));
|
||||
assert_eq!(turn_id, Some(tc.sub_id.clone()));
|
||||
assert_eq!(reason, TurnAbortReason::Interrupted);
|
||||
assert!(started_at.is_some());
|
||||
@@ -4360,6 +4362,7 @@ async fn record_initial_history_forked_hydrates_previous_turn_settings() {
|
||||
RolloutItem::TurnContext(previous_context_item.clone()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
codex_protocol::protocol::TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
@@ -6282,6 +6285,7 @@ async fn compaction_persists_resume_metadata_and_companion_records() {
|
||||
if let Some(turn_id) = last_started_turn_id {
|
||||
items.push(RolloutItem::EventMsg(EventMsg::TurnComplete(
|
||||
TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
|
||||
@@ -399,6 +399,13 @@ enum TurnContextBuildMode {
|
||||
}
|
||||
|
||||
impl TurnContext {
|
||||
/// Returns the resolved causal root for this admitted turn.
|
||||
pub(crate) fn root_turn_id(&self) -> String {
|
||||
self.turn_metadata_state
|
||||
.root_turn_id()
|
||||
.unwrap_or_else(|| self.sub_id.clone())
|
||||
}
|
||||
|
||||
/// Builds a review turn with shared session grants and fresh turn-local state.
|
||||
pub(super) fn for_review(
|
||||
&self,
|
||||
|
||||
@@ -268,8 +268,9 @@ pub(super) async fn handle(
|
||||
}
|
||||
};
|
||||
// Link this request's trace to the accepted turn, which may have an older trace.
|
||||
if let Ok(TurnInputSubmission::Started { turn_id } | TurnInputSubmission::Steered { turn_id }) =
|
||||
&result
|
||||
if let Ok(
|
||||
TurnInputSubmission::Started { turn_id, .. } | TurnInputSubmission::Steered { turn_id, .. },
|
||||
) = &result
|
||||
{
|
||||
tracing::Span::current().record("turn.id", turn_id);
|
||||
}
|
||||
@@ -306,7 +307,7 @@ pub(super) async fn handle_recovery(
|
||||
/*expected_previous_turn_id*/ None,
|
||||
)
|
||||
.await;
|
||||
if let Ok(TurnInputSubmission::Started { turn_id }) = &result {
|
||||
if let Ok(TurnInputSubmission::Started { turn_id, .. }) = &result {
|
||||
tracing::Span::current().record("turn.id", turn_id);
|
||||
}
|
||||
result
|
||||
@@ -363,9 +364,12 @@ async fn start_or_steer(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(turn_id) => {
|
||||
Ok((turn_id, root_turn_id)) => {
|
||||
settings.apply_steered(session, submission_id).await?;
|
||||
Ok(TurnInputSubmission::Steered { turn_id })
|
||||
Ok(TurnInputSubmission::Steered {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
})
|
||||
}
|
||||
Err(NotSubmittedReason::NoActiveTurn) => {
|
||||
// MAv1 sends explicit input to spawned agents as part of an existing
|
||||
@@ -413,10 +417,11 @@ async fn start_or_steer(
|
||||
.push(pending_turn_input(session, input, &turn_context.sub_id, origin).await);
|
||||
}
|
||||
session
|
||||
.spawn_task(turn_context, task_input, RegularTask::new())
|
||||
.spawn_task(Arc::clone(&turn_context), task_input, RegularTask::new())
|
||||
.await;
|
||||
Ok(TurnInputSubmission::Started {
|
||||
turn_id: submission_id,
|
||||
root_turn_id: turn_context.root_turn_id(),
|
||||
})
|
||||
}
|
||||
Err(reason) => Ok(TurnInputSubmission::NotSubmitted { reason }),
|
||||
@@ -575,10 +580,11 @@ async fn start_if_idle(
|
||||
}
|
||||
}
|
||||
session
|
||||
.start_task(turn_context, task_input, RegularTask::new())
|
||||
.start_task(Arc::clone(&turn_context), task_input, RegularTask::new())
|
||||
.await;
|
||||
Ok(TurnInputSubmission::Started {
|
||||
turn_id: submission_id,
|
||||
root_turn_id: turn_context.root_turn_id(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -626,9 +632,12 @@ async fn steer(
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(turn_id) => {
|
||||
Ok((turn_id, root_turn_id)) => {
|
||||
settings.apply_steered(session, submission_id).await?;
|
||||
Ok(TurnInputSubmission::Steered { turn_id })
|
||||
Ok(TurnInputSubmission::Steered {
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
})
|
||||
}
|
||||
Err(reason) => Ok(TurnInputSubmission::NotSubmitted { reason }),
|
||||
}
|
||||
@@ -721,7 +730,7 @@ impl Session {
|
||||
required_final_output_json_schema: Option<&Value>,
|
||||
responsesapi_client_metadata: Option<HashMap<String, String>>,
|
||||
origin: UserInputOrigin,
|
||||
) -> Result<String, NotSubmittedReason> {
|
||||
) -> Result<(String, String), NotSubmittedReason> {
|
||||
let mut active = self.active_turn.lock().await;
|
||||
let Some(active_turn) = active.as_mut() else {
|
||||
return Err(NotSubmittedReason::NoActiveTurn);
|
||||
@@ -799,7 +808,10 @@ impl Session {
|
||||
pending_input,
|
||||
)
|
||||
.await;
|
||||
Ok(active_turn_id.clone())
|
||||
Ok((
|
||||
active_turn_id.clone(),
|
||||
active_task.turn_context.root_turn_id(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@ async fn steering_does_not_wait_for_realtime_history() {
|
||||
assert_eq!(
|
||||
submission,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_context.sub_id.clone(),
|
||||
turn_id: turn_context.sub_id.clone()
|
||||
}
|
||||
);
|
||||
@@ -922,6 +923,7 @@ async fn steer_only_enforces_expected_turn_id() {
|
||||
assert_eq!(
|
||||
submission,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_context.sub_id.clone(),
|
||||
turn_id: turn_context.sub_id.clone()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -820,6 +820,7 @@ impl Session {
|
||||
self.emit_turn_abort_lifecycle(reason.clone(), turn_context.extension_data.as_ref())
|
||||
.await;
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: Some(turn_context.root_turn_id()),
|
||||
turn_id: Some(turn_context.sub_id.clone()),
|
||||
reason,
|
||||
error: None,
|
||||
@@ -836,6 +837,7 @@ impl Session {
|
||||
self.emit_turn_stop_lifecycle(turn_context.extension_data.as_ref())
|
||||
.await;
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: Some(turn_context.root_turn_id()),
|
||||
turn_id: turn_context.sub_id.clone(),
|
||||
last_agent_message,
|
||||
error,
|
||||
@@ -1003,6 +1005,7 @@ impl Session {
|
||||
self.emit_turn_abort_lifecycle(reason.clone(), task.turn_context.extension_data.as_ref())
|
||||
.await;
|
||||
let event = EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: Some(task.turn_context.root_turn_id()),
|
||||
turn_id: Some(task.turn_context.sub_id.clone()),
|
||||
reason,
|
||||
error,
|
||||
|
||||
@@ -137,7 +137,7 @@ async fn turn_completion_metrics_follow_model_switch(scenario: UsageScenario) {
|
||||
.with_runtime_reader(),
|
||||
)
|
||||
.expect("in-memory metrics client");
|
||||
let (mut session, mut turn_context, _receiver) = make_session_and_context_with_rx().await;
|
||||
let (mut session, mut turn_context, receiver) = make_session_and_context_with_rx().await;
|
||||
let session_telemetry = session
|
||||
.services
|
||||
.session_telemetry
|
||||
@@ -243,7 +243,21 @@ async fn turn_completion_metrics_follow_model_switch(scenario: UsageScenario) {
|
||||
} else {
|
||||
Ok(None)
|
||||
};
|
||||
let expected_root = turn_context.root_turn_id();
|
||||
session.on_task_finished(turn_context, task_result).await;
|
||||
let mut terminal_root = None;
|
||||
while let Ok(event) = receiver.try_recv() {
|
||||
match event.msg {
|
||||
codex_protocol::protocol::EventMsg::TurnComplete(event) => {
|
||||
terminal_root = event.root_turn_id;
|
||||
}
|
||||
codex_protocol::protocol::EventMsg::TurnAborted(event) => {
|
||||
terminal_root = event.root_turn_id;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
assert_eq!(terminal_root, Some(expected_root));
|
||||
|
||||
let snapshot = metrics.snapshot().expect("runtime metrics snapshot");
|
||||
let token_usage_metric = find_metric(&snapshot, TURN_TOKEN_USAGE_METRIC);
|
||||
|
||||
@@ -2606,6 +2606,7 @@ struct SnapshotTurnState {
|
||||
ends_mid_turn: bool,
|
||||
active_turn_id: Option<String>,
|
||||
active_turn_started_at: Option<i64>,
|
||||
active_turn_root_id: Option<String>,
|
||||
active_turn_start_index: Option<usize>,
|
||||
}
|
||||
|
||||
@@ -2626,6 +2627,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
};
|
||||
}
|
||||
@@ -2633,7 +2635,10 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
|
||||
return SnapshotTurnState {
|
||||
ends_mid_turn: true,
|
||||
active_turn_id,
|
||||
active_turn_started_at: active_turn_snapshot.and_then(|turn| turn.started_at),
|
||||
active_turn_started_at: active_turn_snapshot
|
||||
.as_ref()
|
||||
.and_then(|turn| turn.started_at),
|
||||
active_turn_root_id: active_turn_snapshot.and_then(|turn| turn.root_turn_id),
|
||||
active_turn_start_index: builder.active_turn_start_index(),
|
||||
};
|
||||
}
|
||||
@@ -2646,6 +2651,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
};
|
||||
};
|
||||
@@ -2662,6 +2668,7 @@ fn snapshot_turn_state(history: &InitialHistory) -> SnapshotTurnState {
|
||||
}),
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
}
|
||||
}
|
||||
@@ -2689,6 +2696,7 @@ fn fork_history_from_snapshot(
|
||||
append_interrupted_boundary(
|
||||
history,
|
||||
snapshot_state.active_turn_id,
|
||||
snapshot_state.active_turn_root_id,
|
||||
snapshot_state.active_turn_started_at,
|
||||
interrupted_marker,
|
||||
)
|
||||
@@ -2705,10 +2713,12 @@ fn fork_history_from_snapshot(
|
||||
fn append_interrupted_boundary(
|
||||
history: InitialHistory,
|
||||
turn_id: Option<String>,
|
||||
root_turn_id: Option<String>,
|
||||
started_at: Option<i64>,
|
||||
interrupted_marker: InterruptedTurnHistoryMarker,
|
||||
) -> InitialHistory {
|
||||
let aborted_event = RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id,
|
||||
turn_id,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
error: None,
|
||||
|
||||
@@ -718,6 +718,7 @@ fn truncates_before_requested_user_message() {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -744,6 +745,7 @@ fn truncates_before_requested_user_message() {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -769,6 +771,7 @@ fn out_of_range_truncation_drops_only_unfinished_suffix_mid_turn() {
|
||||
ends_mid_turn: true,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -818,6 +821,7 @@ fn out_of_range_truncation_drops_pre_user_active_turn_prefix() {
|
||||
ends_mid_turn: true,
|
||||
active_turn_id: Some("turn-2".to_string()),
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: Some(2),
|
||||
},
|
||||
);
|
||||
@@ -863,6 +867,7 @@ async fn ignores_session_prefix_messages_when_truncating() {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -2772,6 +2777,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() {
|
||||
append_interrupted_boundary(
|
||||
committed_history,
|
||||
/*turn_id*/ None,
|
||||
/*root_turn_id*/ None,
|
||||
/*started_at*/ None,
|
||||
InterruptedTurnHistoryMarker::ContextualUser,
|
||||
)
|
||||
@@ -2782,6 +2788,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() {
|
||||
RolloutItem::ResponseItem(user_msg("hello").into()),
|
||||
RolloutItem::ResponseItem(contextual_user_interrupted_marker().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: None,
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2797,6 +2804,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() {
|
||||
append_interrupted_boundary(
|
||||
InitialHistory::New,
|
||||
/*turn_id*/ None,
|
||||
/*root_turn_id*/ None,
|
||||
/*started_at*/ None,
|
||||
InterruptedTurnHistoryMarker::ContextualUser,
|
||||
)
|
||||
@@ -2806,6 +2814,7 @@ fn interrupted_fork_snapshot_appends_interrupt_boundary() {
|
||||
serde_json::to_value(vec![
|
||||
RolloutItem::ResponseItem(contextual_user_interrupted_marker().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: None,
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2828,6 +2837,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() {
|
||||
append_interrupted_boundary(
|
||||
committed_history,
|
||||
/*turn_id*/ None,
|
||||
/*root_turn_id*/ None,
|
||||
/*started_at*/ None,
|
||||
InterruptedTurnHistoryMarker::Disabled,
|
||||
)
|
||||
@@ -2837,6 +2847,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() {
|
||||
serde_json::to_value(vec![
|
||||
RolloutItem::ResponseItem(user_msg("hello").into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: None,
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2852,6 +2863,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() {
|
||||
append_interrupted_boundary(
|
||||
InitialHistory::New,
|
||||
/*turn_id*/ None,
|
||||
/*root_turn_id*/ None,
|
||||
/*started_at*/ None,
|
||||
InterruptedTurnHistoryMarker::Disabled,
|
||||
)
|
||||
@@ -2860,6 +2872,7 @@ fn disabled_interrupted_fork_snapshot_appends_only_interrupt_event() {
|
||||
.expect("serialize disabled interrupted empty fork history"),
|
||||
serde_json::to_value(vec![RolloutItem::EventMsg(EventMsg::TurnAborted(
|
||||
TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: None,
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2879,6 +2892,7 @@ fn interrupted_snapshot_is_not_mid_turn() {
|
||||
RolloutItem::ResponseItem(assistant_msg("partial").into()),
|
||||
RolloutItem::ResponseItem(contextual_user_interrupted_marker().into()),
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some("turn-1".to_string()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -2894,6 +2908,7 @@ fn interrupted_snapshot_is_not_mid_turn() {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -2951,6 +2966,7 @@ fn completed_legacy_event_history_is_not_mid_turn() {
|
||||
ends_mid_turn: false,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -2976,6 +2992,7 @@ fn mixed_response_and_legacy_user_event_history_is_mid_turn() {
|
||||
ends_mid_turn: true,
|
||||
active_turn_id: None,
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: None,
|
||||
},
|
||||
);
|
||||
@@ -3060,6 +3077,7 @@ async fn interrupted_fork_snapshot_does_not_synthesize_turn_id_for_legacy_histor
|
||||
.expect("serialize interrupted marker");
|
||||
let interrupted_abort_json = serde_json::to_value(RolloutItem::EventMsg(
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: expected_turn_id,
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
@@ -3155,6 +3173,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
ends_mid_turn: true,
|
||||
active_turn_id: Some("turn-explicit".to_string()),
|
||||
active_turn_started_at: None,
|
||||
active_turn_root_id: None,
|
||||
active_turn_start_index: Some(1),
|
||||
},
|
||||
);
|
||||
@@ -3184,6 +3203,7 @@ async fn interrupted_fork_snapshot_preserves_explicit_turn_id() {
|
||||
matches!(
|
||||
item,
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(turn_id),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
|
||||
@@ -55,6 +55,7 @@ fn turn_started(turn_id: &str) -> RolloutItem {
|
||||
|
||||
fn turn_completed(turn_id: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
|
||||
@@ -1502,6 +1502,7 @@ async fn multi_agent_v2_list_agents_returns_completed_status() {
|
||||
.send_event(
|
||||
child_turn.as_ref(),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: child_turn.sub_id.clone(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("done".to_string()),
|
||||
@@ -1939,6 +1940,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
|
||||
.send_event(
|
||||
first_turn.as_ref(),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: first_turn.sub_id.clone(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("first done".to_string()),
|
||||
@@ -1981,6 +1983,7 @@ async fn multi_agent_v2_followup_task_completion_notifies_parent_on_every_turn()
|
||||
.send_event(
|
||||
second_turn.as_ref(),
|
||||
EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: second_turn.sub_id.clone(),
|
||||
started_at: None,
|
||||
last_agent_message: Some("second done".to_string()),
|
||||
@@ -2144,6 +2147,7 @@ async fn multi_agent_v2_interrupted_turn_does_not_notify_parent() {
|
||||
.send_event(
|
||||
aborted_turn.as_ref(),
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(aborted_turn.sub_id.clone()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
|
||||
@@ -111,7 +111,7 @@ async fn root_turn_suspension_preserves_unfinished_turn_history() {
|
||||
}]))
|
||||
.await
|
||||
.expect("start root turn");
|
||||
let codex_core::TurnInputSubmission::Started { turn_id } = submitted else {
|
||||
let codex_core::TurnInputSubmission::Started { turn_id, .. } = submitted else {
|
||||
panic!("expected a started root turn");
|
||||
};
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnStarted(_))).await;
|
||||
@@ -191,7 +191,8 @@ async fn root_turn_suspension_preserves_unfinished_turn_history() {
|
||||
.await
|
||||
.expect("recover the unfinished turn"),
|
||||
codex_core::StartIfIdleSubmission::Started {
|
||||
turn_id: turn_id.clone(),
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id: turn_id.clone()
|
||||
},
|
||||
);
|
||||
let completed = wait_for_event(&resumed.codex, |event| {
|
||||
|
||||
@@ -4622,7 +4622,7 @@ async fn paginated_compaction_cold_resume_from_bounded_suffix() -> Result<()> {
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
let codex_core::TurnInputSubmission::Started { turn_id } = test
|
||||
let codex_core::TurnInputSubmission::Started { turn_id, .. } = test
|
||||
.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: FUNCTION_CALL_LIMIT_MSG.into(),
|
||||
|
||||
@@ -39,7 +39,7 @@ async fn recover_turn_restores_cyber_access_program_without_making_it_sticky() -
|
||||
.await;
|
||||
let mut builder = test_codex().with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing());
|
||||
let initial = builder.build_with_streaming_server(&initial_server).await?;
|
||||
let TurnInputSubmission::Started { turn_id } = initial
|
||||
let TurnInputSubmission::Started { turn_id, .. } = initial
|
||||
.codex
|
||||
.start_or_steer_turn(
|
||||
TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
@@ -105,7 +105,8 @@ async fn recover_turn_restores_cyber_access_program_without_making_it_sticky() -
|
||||
assert_eq!(
|
||||
submission,
|
||||
StartIfIdleSubmission::Started {
|
||||
turn_id: turn_id.clone(),
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id: turn_id.clone()
|
||||
}
|
||||
);
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -133,6 +134,7 @@ async fn recover_turn_restores_cyber_access_program_without_making_it_sticky() -
|
||||
.await;
|
||||
let TurnInputSubmission::Started {
|
||||
turn_id: next_turn_id,
|
||||
..
|
||||
} = test
|
||||
.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
|
||||
@@ -408,7 +408,7 @@ async fn ephemeral_system_thread_prewarm_skips_and_turn_observes_fresh_state(
|
||||
.and_then(|connection| connection.get(2))
|
||||
.context("system turn follow-up request")?
|
||||
.body_json();
|
||||
let codex_core::TurnInputSubmission::Started { turn_id } = submission else {
|
||||
let codex_core::TurnInputSubmission::Started { turn_id, .. } = submission else {
|
||||
panic!("background input should start a turn");
|
||||
};
|
||||
assert_root_turn(&turn, Some(&turn_id))?;
|
||||
|
||||
@@ -271,7 +271,7 @@ approvals_reviewer = "user"
|
||||
text_elements: Vec::new(),
|
||||
}]);
|
||||
let submitted = test.codex.start_or_steer_turn(request).await?;
|
||||
let TurnInputSubmission::Started { turn_id } = submitted else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = submitted else {
|
||||
anyhow::bail!("expected a new turn, got {submitted:?}");
|
||||
};
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ async fn idle_response_items_include_pending_mailbox_in_first_request() -> anyho
|
||||
responses::user_message_item("automatic response item"),
|
||||
)))
|
||||
.await?;
|
||||
let StartIfIdleSubmission::Started { turn_id } = submission else {
|
||||
let StartIfIdleSubmission::Started { turn_id, .. } = submission else {
|
||||
panic!("automatic input should start a turn");
|
||||
};
|
||||
wait_for_turn_complete(test.codex.as_ref()).await;
|
||||
@@ -152,7 +152,7 @@ async fn standalone_tool_output_starts_instruction_turn() -> anyhow::Result<()>
|
||||
.codex
|
||||
.start_or_steer_turn(TurnInputRequest::new(TurnInput::ResponseItem(output)))
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id } = submission else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = submission else {
|
||||
panic!("standalone output should start a turn");
|
||||
};
|
||||
wait_for_turn_complete(test.codex.as_ref()).await;
|
||||
@@ -1924,7 +1924,7 @@ async fn interrupt_if_no_pending_input_checks_turn_and_queue(
|
||||
.build_with_auto_env(&config_server)
|
||||
.await?;
|
||||
let codex = &test.codex;
|
||||
let TurnInputSubmission::Started { turn_id } = codex
|
||||
let TurnInputSubmission::Started { turn_id, .. } = codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: INITIAL_PROMPT.to_string(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
@@ -235,7 +235,7 @@ async fn steered_input_checkpoint_controls_next_request(
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id } = first else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = first else {
|
||||
panic!("first input should start a turn");
|
||||
};
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -259,7 +259,10 @@ async fn steered_input_checkpoint_controls_next_request(
|
||||
};
|
||||
assert_eq!(
|
||||
test.codex.start_or_steer_turn(input).await?,
|
||||
TurnInputSubmission::Steered { turn_id }
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id
|
||||
}
|
||||
);
|
||||
// Steering still waits for the existing inference stream to finish.
|
||||
assert!(checkpoint_requests.try_recv().is_err());
|
||||
|
||||
@@ -5751,7 +5751,7 @@ async fn inbound_handoff_request_updates_realtime_state_during_active_turn(
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id } = started else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = started else {
|
||||
panic!("expected the text input to start a turn");
|
||||
};
|
||||
|
||||
@@ -5867,7 +5867,13 @@ async fn inbound_handoff_request_updates_realtime_state_during_active_turn(
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
assert_eq!(steered, TurnInputSubmission::Steered { turn_id });
|
||||
assert_eq!(
|
||||
steered,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id
|
||||
}
|
||||
);
|
||||
let _ = second_completed_tx.send(());
|
||||
second_completion
|
||||
.await
|
||||
|
||||
@@ -265,7 +265,7 @@ async fn reasoning_effort_override_recovery_reuses_trusted_tail_update() -> anyh
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id } = submission else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = submission else {
|
||||
panic!("expected a new turn");
|
||||
};
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -314,7 +314,10 @@ async fn reasoning_effort_override_recovery_reuses_trusted_tail_update() -> anyh
|
||||
cyber_access_program: None,
|
||||
})
|
||||
.await?,
|
||||
StartIfIdleSubmission::Started { turn_id },
|
||||
StartIfIdleSubmission::Started {
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id
|
||||
},
|
||||
);
|
||||
wait_for_event(&resumed.codex, |event| {
|
||||
matches!(event, EventMsg::TurnComplete(_))
|
||||
|
||||
@@ -2555,7 +2555,7 @@ async fn active_environment_update_wakes_the_old_wait_with_the_new_selection() -
|
||||
],
|
||||
)
|
||||
.await;
|
||||
let TurnInputSubmission::Started { turn_id } = thread
|
||||
let TurnInputSubmission::Started { turn_id, .. } = thread
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "use the selected environment".to_string(),
|
||||
text_elements: Vec::new(),
|
||||
|
||||
@@ -157,7 +157,7 @@ pub(crate) async fn pending_subagent_scenario(
|
||||
.await;
|
||||
|
||||
let mut created = test.thread_manager.subscribe_thread_created();
|
||||
let TurnInputSubmission::Started { turn_id } = root
|
||||
let TurnInputSubmission::Started { turn_id, .. } = root
|
||||
.thread
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
text: "Delegate waiting for the shared workspace, and report what the worker finds."
|
||||
|
||||
@@ -80,6 +80,7 @@ fn resume_history(
|
||||
})),
|
||||
RolloutItem::TurnContext(turn_ctx),
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
|
||||
@@ -86,7 +86,7 @@ async fn initial_plugin_ids_use_turn_context_without_extra_settings_checkpoints(
|
||||
.codex
|
||||
.start_turn_if_idle(TurnInputRequest::user_input(Vec::new()))
|
||||
.await?;
|
||||
let StartIfIdleSubmission::Started { turn_id } = submission else {
|
||||
let StartIfIdleSubmission::Started { turn_id, .. } = submission else {
|
||||
panic!("expected an accepted first turn, got {submission:?}");
|
||||
};
|
||||
wait_for_event(
|
||||
@@ -220,7 +220,7 @@ async fn settings_notifications_keep_their_commit_across_postcommit_work(
|
||||
.with_thread_settings(thread_settings),
|
||||
)
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id } = result else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = result else {
|
||||
panic!("expected a new turn, got {result:?}");
|
||||
};
|
||||
Ok(turn_id)
|
||||
|
||||
@@ -86,7 +86,7 @@ impl SettingsOperation {
|
||||
.with_thread_settings(thread_settings),
|
||||
)
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id } = result else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = result else {
|
||||
anyhow::bail!("expected a new turn, got {result:?}");
|
||||
};
|
||||
turn_id
|
||||
|
||||
@@ -90,7 +90,10 @@ async fn yielded_skill_read_keeps_originating_turn_metadata() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}]);
|
||||
let submitted = test.codex.start_or_steer_turn(request).await?;
|
||||
let TurnInputSubmission::Started { turn_id: turn_a } = submitted else {
|
||||
let TurnInputSubmission::Started {
|
||||
turn_id: turn_a, ..
|
||||
} = submitted
|
||||
else {
|
||||
anyhow::bail!("expected turn A to start, got {submitted:?}");
|
||||
};
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -135,7 +138,10 @@ async fn yielded_skill_read_keeps_originating_turn_metadata() -> Result<()> {
|
||||
text_elements: Vec::new(),
|
||||
}]))
|
||||
.await?;
|
||||
let TurnInputSubmission::Started { turn_id: turn_b } = submitted else {
|
||||
let TurnInputSubmission::Started {
|
||||
turn_id: turn_b, ..
|
||||
} = submitted
|
||||
else {
|
||||
anyhow::bail!("expected turn B to start, got {submitted:?}");
|
||||
};
|
||||
wait_for_event(&test.codex, |event| {
|
||||
|
||||
@@ -87,7 +87,8 @@ async fn turn_extension_data_is_captured_for_automatic_turns() -> anyhow::Result
|
||||
})
|
||||
.await?
|
||||
.thread;
|
||||
let TurnInputSubmission::Started { turn_id } = submit_user_message(&thread, "start").await?
|
||||
let TurnInputSubmission::Started { turn_id, .. } =
|
||||
submit_user_message(&thread, "start").await?
|
||||
else {
|
||||
anyhow::bail!("first input must start a turn");
|
||||
};
|
||||
@@ -106,6 +107,7 @@ async fn turn_extension_data_is_captured_for_automatic_turns() -> anyhow::Result
|
||||
))
|
||||
.await?,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id: turn_id.clone()
|
||||
}
|
||||
);
|
||||
@@ -635,7 +637,8 @@ async fn recover_turn_if_idle_preserves_id_and_resumes_plan_mode() {
|
||||
assert_eq!(
|
||||
submission,
|
||||
StartIfIdleSubmission::Started {
|
||||
turn_id: turn_id.to_string(),
|
||||
root_turn_id: turn_id.to_string(),
|
||||
turn_id: turn_id.to_string()
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -752,6 +755,7 @@ async fn recovery_preserves_durable_turn_attribution(
|
||||
);
|
||||
let TurnInputSubmission::Started {
|
||||
turn_id: previous_turn_id,
|
||||
..
|
||||
} = submit_user_message(&test.codex, "establish the initial context baseline").await?
|
||||
else {
|
||||
panic!("expected the initial turn");
|
||||
@@ -801,14 +805,14 @@ async fn recovery_preserves_durable_turn_attribution(
|
||||
..Default::default()
|
||||
});
|
||||
let turn_id = if matches!(pause, RecoveryPause::AfterLateCompletion) {
|
||||
let TurnInputSubmission::Started { turn_id } =
|
||||
let TurnInputSubmission::Started { turn_id, .. } =
|
||||
test.codex.start_or_steer_turn(request).await?
|
||||
else {
|
||||
panic!("expected a new turn while the previous stop hook is pending");
|
||||
};
|
||||
turn_id
|
||||
} else {
|
||||
let StartIfIdleSubmission::Started { turn_id } =
|
||||
let StartIfIdleSubmission::Started { turn_id, .. } =
|
||||
test.codex.start_turn_if_idle(request).await?
|
||||
else {
|
||||
panic!("expected a new turn");
|
||||
@@ -887,6 +891,7 @@ async fn recovery_preserves_durable_turn_attribution(
|
||||
})
|
||||
.await?,
|
||||
StartIfIdleSubmission::Started {
|
||||
root_turn_id: root.to_owned(),
|
||||
turn_id: turn_id.clone()
|
||||
},
|
||||
);
|
||||
@@ -930,10 +935,15 @@ async fn recovery_preserves_durable_turn_attribution(
|
||||
);
|
||||
}
|
||||
test.codex.submit(Op::Interrupt).await?;
|
||||
wait_for_event(&test.codex, |event| {
|
||||
matches!(event, EventMsg::TurnAborted(_))
|
||||
let aborted = wait_for_event_match(&test.codex, |event| match event {
|
||||
EventMsg::TurnAborted(event) => Some(event.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
(aborted.turn_id.as_deref(), aborted.root_turn_id.as_deref()),
|
||||
(Some(turn_id.as_str()), Some(root)),
|
||||
);
|
||||
if trigger != Some("automation") {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -943,6 +953,7 @@ async fn recovery_preserves_durable_turn_attribution(
|
||||
let response = responses::mount_sse_once(&next_server, responses::sse_completed("new")).await;
|
||||
let TurnInputSubmission::Started {
|
||||
turn_id: next_turn_id,
|
||||
..
|
||||
} = test
|
||||
.codex
|
||||
.start_or_steer_turn(
|
||||
@@ -994,6 +1005,7 @@ async fn continue_turn_if_idle_starts_new_turn_with_internal_input() {
|
||||
responses::mount_sse_once(&server, responses::sse_completed("original")).await;
|
||||
let TurnInputSubmission::Started {
|
||||
turn_id: previous_turn_id,
|
||||
..
|
||||
} = test
|
||||
.codex
|
||||
.start_or_steer_turn(TurnInputRequest::user_input(vec![UserInput::Text {
|
||||
@@ -1030,7 +1042,7 @@ async fn continue_turn_if_idle_starts_new_turn_with_internal_input() {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let TurnInputSubmission::Started { turn_id } = submission else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = submission else {
|
||||
panic!("continuation did not start")
|
||||
};
|
||||
wait_for_event(&test.codex, |event| {
|
||||
@@ -1166,12 +1178,20 @@ async fn turn_input_submission_reports_started_and_steered_for_concurrent_submis
|
||||
let (started_turn_id, steered_turn_id, started_message) =
|
||||
match (&first_submission, &second_submission) {
|
||||
(
|
||||
TurnInputSubmission::Started { turn_id: started },
|
||||
TurnInputSubmission::Steered { turn_id: steered },
|
||||
TurnInputSubmission::Started {
|
||||
turn_id: started, ..
|
||||
},
|
||||
TurnInputSubmission::Steered {
|
||||
turn_id: steered, ..
|
||||
},
|
||||
) => (started, steered, "first message"),
|
||||
(
|
||||
TurnInputSubmission::Steered { turn_id: steered },
|
||||
TurnInputSubmission::Started { turn_id: started },
|
||||
TurnInputSubmission::Steered {
|
||||
turn_id: steered, ..
|
||||
},
|
||||
TurnInputSubmission::Started {
|
||||
turn_id: started, ..
|
||||
},
|
||||
) => (started, steered, "second message"),
|
||||
_ => panic!(
|
||||
"concurrent messages must start exactly one turn and steer the other: \
|
||||
@@ -1231,7 +1251,7 @@ async fn turn_input_submission_applies_thread_settings_only_after_accepted_input
|
||||
let started = submit_user_message(codex, "start turn")
|
||||
.await
|
||||
.expect("first message should start a turn");
|
||||
let TurnInputSubmission::Started { turn_id } = started else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = started else {
|
||||
panic!("first message should start a turn");
|
||||
};
|
||||
timeout(
|
||||
@@ -1256,7 +1276,13 @@ async fn turn_input_submission_applies_thread_settings_only_after_accepted_input
|
||||
)
|
||||
.await
|
||||
.expect("persistent settings should not reject a steer");
|
||||
assert_eq!(steered, TurnInputSubmission::Steered { turn_id });
|
||||
assert_eq!(
|
||||
steered,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
codex.config_snapshot().await.approval_policy,
|
||||
AskForApproval::Never
|
||||
@@ -1353,7 +1379,7 @@ async fn start_or_steer_turn_requires_matching_active_output_schema() {
|
||||
)
|
||||
.await
|
||||
.expect("first message should start a turn");
|
||||
let TurnInputSubmission::Started { turn_id } = started else {
|
||||
let TurnInputSubmission::Started { turn_id, .. } = started else {
|
||||
panic!("first message should start a turn");
|
||||
};
|
||||
timeout(
|
||||
@@ -1397,7 +1423,13 @@ async fn start_or_steer_turn_requires_matching_active_output_schema() {
|
||||
)
|
||||
.await
|
||||
.expect("matching schema should steer");
|
||||
assert_eq!(steered, TurnInputSubmission::Steered { turn_id });
|
||||
assert_eq!(
|
||||
steered,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: turn_id.clone(),
|
||||
turn_id
|
||||
}
|
||||
);
|
||||
|
||||
release_response
|
||||
.send(())
|
||||
@@ -1438,7 +1470,7 @@ async fn sampling_is_ready_for_daemon_recovery(
|
||||
builder = builder.with_exec_server_url(&remote.websocket_url);
|
||||
}
|
||||
let test = builder.build_with_streaming_server(&server).await?;
|
||||
let StartIfIdleSubmission::Started { turn_id } = test
|
||||
let StartIfIdleSubmission::Started { turn_id, .. } = test
|
||||
.codex
|
||||
.start_turn_if_idle(TurnInputRequest::user_input(input))
|
||||
.await?
|
||||
@@ -1510,7 +1542,7 @@ async fn daemon_recovery_includes_local_environment_that_finished_starting() ->
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
let StartIfIdleSubmission::Started { turn_id } = started else {
|
||||
let StartIfIdleSubmission::Started { turn_id, .. } = started else {
|
||||
anyhow::bail!("turn should start");
|
||||
};
|
||||
|
||||
|
||||
@@ -163,6 +163,7 @@ async fn steer_joins_existing_turn_without_extending_sampling(
|
||||
)
|
||||
.await?,
|
||||
TurnInputSubmission::Steered {
|
||||
root_turn_id: call.turn_id.clone(),
|
||||
turn_id: call.turn_id.clone()
|
||||
}
|
||||
);
|
||||
@@ -223,6 +224,7 @@ async fn steer_joins_existing_turn_without_extending_sampling(
|
||||
})
|
||||
.await?,
|
||||
StartIfIdleSubmission::Started {
|
||||
root_turn_id: call.turn_id.clone(),
|
||||
turn_id: call.turn_id.clone()
|
||||
}
|
||||
);
|
||||
|
||||
@@ -320,6 +320,7 @@ fn turn_completed_recovers_final_message_from_turn_items() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
@@ -370,6 +371,7 @@ fn turn_completed_overwrites_stale_final_message_from_turn_items() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
@@ -421,6 +423,7 @@ fn turn_completed_preserves_streamed_final_message_when_turn_items_are_empty() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
@@ -465,6 +468,7 @@ fn turn_failed_clears_stale_final_message() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
@@ -510,6 +514,7 @@ fn turn_interrupted_clears_stale_final_message() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Interrupted,
|
||||
|
||||
@@ -35,6 +35,7 @@ fn failed_turn_does_not_overwrite_output_last_message_file() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
|
||||
@@ -1233,6 +1233,8 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
|
||||
disabled_plugin_ids: None,
|
||||
thread_id: primary_thread_id_for_span.clone(),
|
||||
turn_trigger: Some("exec".to_string()),
|
||||
parent_turn_id: None,
|
||||
root_turn_id: None,
|
||||
client_user_message_id: None,
|
||||
input: items.into_iter().map(Into::into).collect(),
|
||||
tool_output: None,
|
||||
|
||||
@@ -385,6 +385,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
turns: vec![
|
||||
codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![AppServerThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
@@ -402,6 +403,7 @@ fn turn_items_for_thread_returns_matching_turn_items() {
|
||||
},
|
||||
codex_app_server_protocol::Turn {
|
||||
id: "turn-2".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![AppServerThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
@@ -437,6 +439,7 @@ fn should_backfill_turn_completed_items_backfills_persisted_summaries_only() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: codex_app_server_protocol::Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Summary,
|
||||
items: Vec::new(),
|
||||
status: codex_app_server_protocol::TurnStatus::Completed,
|
||||
|
||||
@@ -145,6 +145,7 @@ fn turn_started_emits_turn_started_event() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::InProgress,
|
||||
@@ -1197,6 +1198,7 @@ fn plan_update_emits_started_then_updated_then_completed() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
@@ -1257,6 +1259,7 @@ fn plan_update_after_completion_starts_new_todo_list_with_new_id() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
@@ -1342,6 +1345,7 @@ fn token_usage_update_is_emitted_on_turn_completion() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
@@ -1378,6 +1382,7 @@ fn turn_completion_recovers_final_message_from_turn_items() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
@@ -1457,6 +1462,7 @@ fn turn_completion_reconciles_started_items_from_turn_items() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::CommandExecution {
|
||||
model_context: None,
|
||||
@@ -1531,6 +1537,7 @@ fn turn_completion_overwrites_stale_final_message_from_turn_items() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::AgentMessage {
|
||||
id: "msg-1".to_string(),
|
||||
@@ -1585,6 +1592,7 @@ fn turn_completion_preserves_streamed_final_message_when_turn_items_are_empty()
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Completed,
|
||||
@@ -1636,6 +1644,7 @@ fn failed_turn_clears_stale_final_message() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
@@ -1665,6 +1674,7 @@ fn turn_completion_falls_back_to_final_plan_text() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: vec![ThreadItem::Plan {
|
||||
id: "plan-1".to_string(),
|
||||
@@ -1721,6 +1731,7 @@ fn turn_failure_prefers_structured_error_message() {
|
||||
thread_id: "thread-1".to_string(),
|
||||
turn: Turn {
|
||||
id: "turn-1".to_string(),
|
||||
root_turn_id: None,
|
||||
items_view: codex_app_server_protocol::TurnItemsView::Full,
|
||||
items: Vec::new(),
|
||||
status: TurnStatus::Failed,
|
||||
|
||||
@@ -85,7 +85,7 @@ impl AgentRunner {
|
||||
)
|
||||
.await?
|
||||
{
|
||||
StartIfIdleSubmission::Started { turn_id } => turn_id,
|
||||
StartIfIdleSubmission::Started { turn_id, .. } => turn_id,
|
||||
StartIfIdleSubmission::NotSubmitted { reason } => {
|
||||
return Err(CodexErr::InvalidRequest(format!(
|
||||
"agent prompt was not submitted: {reason:?}"
|
||||
|
||||
@@ -485,7 +485,7 @@ impl GoalRuntimeHandle {
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(StartIfIdleSubmission::Started { turn_id }) => {
|
||||
Ok(StartIfIdleSubmission::Started { turn_id, .. }) => {
|
||||
// Turn-stop evaluation takes the same permit, so even a fast response
|
||||
// cannot finish before this host-admitted continuation is identified.
|
||||
self.inner.accounting_state.mark_goal_continuation(turn_id);
|
||||
|
||||
@@ -43,7 +43,7 @@ pub async fn start_review_turn(
|
||||
match crate::run_before_review_deadline(deadline, external_cancel, runtime.submit_turn(request))
|
||||
.await?
|
||||
{
|
||||
Ok(TurnInputSubmission::Started { turn_id }) => Ok(turn_id),
|
||||
Ok(TurnInputSubmission::Started { turn_id, .. }) => Ok(turn_id),
|
||||
result => Err(GuardianReviewSessionOutcome::SessionFailed {
|
||||
error: match result {
|
||||
Ok(submission) => anyhow!("guardian review input was not started: {submission:?}"),
|
||||
|
||||
@@ -393,7 +393,7 @@ async fn starting_a_selected_item_preserves_the_remaining_queue() -> anyhow::Res
|
||||
|
||||
assert!(matches!(
|
||||
submission,
|
||||
StartIfIdleSubmission::Started { turn_id } if !turn_id.is_empty()
|
||||
StartIfIdleSubmission::Started { turn_id, .. } if !turn_id.is_empty()
|
||||
));
|
||||
assert_eq!(vec![first], service.list(thread_id).await?);
|
||||
wait_for_event_match(test.codex.as_ref(), |event| match event {
|
||||
|
||||
@@ -207,6 +207,7 @@ fn turn_complete_item(
|
||||
completed_at: Option<i64>,
|
||||
) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id,
|
||||
last_agent_message: None,
|
||||
error: None,
|
||||
|
||||
@@ -2130,6 +2130,10 @@ pub struct ContextCompactedEvent;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct TurnCompleteEvent {
|
||||
/// Resolved causal root for this turn; absent on older or synthetic events.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub root_turn_id: Option<String>,
|
||||
pub turn_id: String,
|
||||
pub last_agent_message: Option<String>,
|
||||
/// Terminal error details when the turn completed unsuccessfully.
|
||||
@@ -4225,6 +4229,10 @@ pub struct Chunk {
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)]
|
||||
pub struct TurnAbortedEvent {
|
||||
/// Resolved causal root for this turn; absent on older or synthetic events.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub root_turn_id: Option<String>,
|
||||
pub turn_id: Option<String>,
|
||||
pub reason: TurnAbortReason,
|
||||
/// Optional error describing why the turn was interrupted.
|
||||
@@ -6103,9 +6111,12 @@ mod tests {
|
||||
|
||||
match event {
|
||||
EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
turn_id, reason, ..
|
||||
turn_id,
|
||||
root_turn_id,
|
||||
reason,
|
||||
..
|
||||
}) => {
|
||||
assert_eq!(turn_id, None);
|
||||
assert_eq!((turn_id, root_turn_id), (None, None));
|
||||
assert_eq!(reason, TurnAbortReason::Interrupted);
|
||||
}
|
||||
_ => panic!("expected turn_aborted event"),
|
||||
|
||||
@@ -208,16 +208,23 @@ pub struct TurnStartOptions {
|
||||
///
|
||||
/// 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.
|
||||
/// 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 },
|
||||
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 },
|
||||
Steered {
|
||||
turn_id: String,
|
||||
root_turn_id: String,
|
||||
},
|
||||
/// Core rejected the input without applying settings or start options.
|
||||
NotSubmitted { reason: NotSubmittedReason },
|
||||
}
|
||||
@@ -226,7 +233,10 @@ pub enum TurnInputSubmission {
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum StartIfIdleSubmission {
|
||||
/// Core started a turn. Persistent thread settings and start options were applied.
|
||||
Started { turn_id: String },
|
||||
Started {
|
||||
turn_id: String,
|
||||
root_turn_id: String,
|
||||
},
|
||||
/// Core rejected the input without applying settings or start options.
|
||||
NotSubmitted { reason: NotSubmittedReason },
|
||||
}
|
||||
|
||||
@@ -61,6 +61,7 @@ fn turn_started(turn_id: &str) -> RolloutItem {
|
||||
|
||||
fn turn_complete(turn_id: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::TurnComplete(TurnCompleteEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: turn_id.to_string(),
|
||||
started_at: None,
|
||||
last_agent_message: None,
|
||||
@@ -73,6 +74,7 @@ fn turn_complete(turn_id: &str) -> RolloutItem {
|
||||
|
||||
fn turn_aborted(turn_id: &str) -> RolloutItem {
|
||||
RolloutItem::EventMsg(EventMsg::TurnAborted(TurnAbortedEvent {
|
||||
root_turn_id: None,
|
||||
turn_id: Some(turn_id.to_string()),
|
||||
started_at: None,
|
||||
reason: TurnAbortReason::Interrupted,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user