mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +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:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user