diff --git a/clients/go/ahp/reducers.go b/clients/go/ahp/reducers.go index e7ffacb3..a498d4c5 100644 --- a/clients/go/ahp/reducers.go +++ b/clients/go/ahp/reducers.go @@ -181,7 +181,7 @@ func touchChatModified(state *ahptypes.ChatState) { // ─── Active-turn helpers ─────────────────────────────────────────────── -func endTurn(state *ahptypes.ChatState, turnID string, turnState ahptypes.TurnState, terminalStatus *ahptypes.SessionStatus, errInfo *ahptypes.ErrorInfo) ReduceOutcome { +func endTurn(state *ahptypes.ChatState, turnID string, duration int64, turnState ahptypes.TurnState, terminalStatus *ahptypes.SessionStatus, errInfo *ahptypes.ErrorInfo) ReduceOutcome { if state.ActiveTurn == nil || state.ActiveTurn.Id != turnID { return ReduceOutcomeNoOp } @@ -220,8 +220,16 @@ func endTurn(state *ahptypes.ChatState, turnID string, turnState ahptypes.TurnSt }}) } + // Defensive clamp: duration is producer-supplied and opaque to this + // reducer, but a negative value would be nonsensical to display. + if duration < 0 { + duration = 0 + } + startedAt := active.StartedAt turn := ahptypes.Turn{ Id: active.Id, + StartedAt: &startedAt, + Duration: &duration, Message: active.Message, ResponseParts: parts, Usage: active.Usage, @@ -231,7 +239,7 @@ func endTurn(state *ahptypes.ChatState, turnID string, turnState ahptypes.TurnSt state.Turns = append(state.Turns, turn) state.InputRequests = nil - touchChatModified(state) + state.ModifiedAt = nowISOString() state.Status = summaryStatus(state, terminalStatus) return ReduceOutcomeApplied } @@ -464,13 +472,13 @@ func ApplyActionToChat(state *ahptypes.ChatState, action ahptypes.StateAction) R state.ActiveTurn.ResponseParts = append(state.ActiveTurn.ResponseParts, a.Part) return ReduceOutcomeApplied case *ahptypes.ChatTurnCompleteAction: - return endTurn(state, a.TurnId, ahptypes.TurnStateComplete, nil, nil) + return endTurn(state, a.TurnId, a.Duration, ahptypes.TurnStateComplete, nil, nil) case *ahptypes.ChatTurnCancelledAction: - return endTurn(state, a.TurnId, ahptypes.TurnStateCancelled, nil, nil) + return endTurn(state, a.TurnId, a.Duration, ahptypes.TurnStateCancelled, nil, nil) case *ahptypes.ChatErrorAction: errCopy := a.Error errStatus := ahptypes.SessionStatusError - return endTurn(state, a.TurnId, ahptypes.TurnStateError, &errStatus, &errCopy) + return endTurn(state, a.TurnId, a.Duration, ahptypes.TurnStateError, &errStatus, &errCopy) case *ahptypes.ChatActivityChangedAction: state.Activity = a.Activity return ReduceOutcomeApplied @@ -924,11 +932,12 @@ func ApplyActionToSession(state *ahptypes.SessionState, action ahptypes.StateAct func applyTurnStarted(state *ahptypes.ChatState, a *ahptypes.ChatTurnStartedAction) ReduceOutcome { state.ActiveTurn = &ahptypes.ActiveTurn{ Id: a.TurnId, + StartedAt: a.StartedAt, Message: a.Message, ResponseParts: []ahptypes.ResponsePart{}, } state.Status = summaryStatus(state, nil) - touchChatModified(state) + state.ModifiedAt = nowISOString() state.Status = withStatusFlag(state.Status, ahptypes.SessionStatusIsRead, false) if a.QueuedMessageId != nil { diff --git a/clients/go/ahptypes/actions.generated.go b/clients/go/ahptypes/actions.generated.go index f5f61b18..495c9c3c 100644 --- a/clients/go/ahptypes/actions.generated.go +++ b/clients/go/ahptypes/actions.generated.go @@ -212,6 +212,8 @@ type ChatTurnStartedAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` + // ISO 8601 timestamp when this turn started. + StartedAt string `json:"startedAt"` // The new message Message Message `json:"message"` // If this turn was auto-started from a queued message, the ID of that message @@ -460,6 +462,11 @@ type ChatTurnCompleteAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` + // Elapsed turn duration in milliseconds, measured by the producer's own + // clock. Clients MUST NOT derive this by subtracting timestamps — cross- + // client clocks may differ — and MUST treat it as opaque, producer-supplied + // data. + Duration int64 `json:"duration"` // Additional provider-specific metadata for this action. // // Clients MAY look for well-known keys here to provide enhanced UI, and @@ -475,6 +482,11 @@ type ChatTurnCancelledAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` + // Elapsed turn duration in milliseconds, measured by the producer's own + // clock. Clients MUST NOT derive this by subtracting timestamps — cross- + // client clocks may differ — and MUST treat it as opaque, producer-supplied + // data. + Duration int64 `json:"duration"` // Additional provider-specific metadata for this action. // // Clients MAY look for well-known keys here to provide enhanced UI, and @@ -490,6 +502,11 @@ type ChatErrorAction struct { Type ActionType `json:"type"` // Turn identifier TurnId string `json:"turnId"` + // Elapsed turn duration in milliseconds, measured by the producer's own + // clock. Clients MUST NOT derive this by subtracting timestamps — cross- + // client clocks may differ — and MUST treat it as opaque, producer-supplied + // data. + Duration int64 `json:"duration"` // Error details Error ErrorInfo `json:"error"` // Additional provider-specific metadata for this action. diff --git a/clients/go/ahptypes/state.generated.go b/clients/go/ahptypes/state.generated.go index 09d345d4..6e57fc01 100644 --- a/clients/go/ahptypes/state.generated.go +++ b/clients/go/ahptypes/state.generated.go @@ -1148,6 +1148,10 @@ type SessionConfigState struct { type Turn struct { // Turn identifier Id string `json:"id"` + // ISO 8601 timestamp when this turn started. + StartedAt *string `json:"startedAt,omitempty"` + // Turn duration in milliseconds. + Duration *int64 `json:"duration,omitempty"` // The message that initiated the turn Message Message `json:"message"` // All response content in stream order: text, tool calls, reasoning, and content refs. @@ -1167,6 +1171,8 @@ type Turn struct { type ActiveTurn struct { // Turn identifier Id string `json:"id"` + // ISO 8601 timestamp when this turn started. + StartedAt string `json:"startedAt"` // The message that initiated the turn Message Message `json:"message"` // All response content in stream order: text, tool calls, reasoning, and content refs. diff --git a/clients/go/examples/reducers_demo/main.go b/clients/go/examples/reducers_demo/main.go index 7e10b7f0..139d8c63 100644 --- a/clients/go/examples/reducers_demo/main.go +++ b/clients/go/examples/reducers_demo/main.go @@ -20,9 +20,10 @@ func main() { actions := []ahptypes.StateAction{ {Value: &ahptypes.ChatTurnStartedAction{ - Type: ahptypes.ActionTypeChatTurnStarted, - TurnId: "t1", - Message: ahptypes.Message{Text: "Hello!", Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}}, + Type: ahptypes.ActionTypeChatTurnStarted, + TurnId: "t1", + StartedAt: "2026-07-09T20:00:00.000Z", + Message: ahptypes.Message{Text: "Hello!", Origin: ahptypes.MessageOrigin{Kind: ahptypes.MessageKindUser}}, }}, {Value: &ahptypes.ChatResponsePartAction{ Type: ahptypes.ActionTypeChatResponsePart, @@ -40,8 +41,9 @@ func main() { Content: "there!", }}, {Value: &ahptypes.ChatTurnCompleteAction{ - Type: ahptypes.ActionTypeChatTurnComplete, - TurnId: "t1", + Type: ahptypes.ActionTypeChatTurnComplete, + TurnId: "t1", + Duration: 1000, }}, } diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt index 5edb26e1..dbe1f83c 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/Reducers.kt @@ -332,6 +332,7 @@ private fun updateResponsePart( private fun endTurn( state: ChatState, turnId: String, + duration: Long, turnState: TurnState, terminalStatus: SessionStatus? = null, error: ErrorInfo? = null, @@ -386,8 +387,12 @@ private fun endTurn( ) } + // Defensive clamp: `duration` is producer-supplied and opaque to this + // reducer, but a negative value would be nonsensical to display. val turn = Turn( id = active.id, + startedAt = active.startedAt, + duration = maxOf(0L, duration), message = active.message, responseParts = finalizedParts, usage = active.usage, @@ -747,6 +752,7 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when val withTurn = state.copy( activeTurn = ActiveTurn( id = a.turnId, + startedAt = a.startedAt, message = a.message, responseParts = emptyList(), usage = null, @@ -796,13 +802,13 @@ public fun chatReducer(state: ChatState, action: StateAction): ChatState = when } is StateActionChatTurnComplete -> - endTurn(state, action.value.turnId, TurnState.COMPLETE) + endTurn(state, action.value.turnId, action.value.duration, TurnState.COMPLETE) is StateActionChatTurnCancelled -> - endTurn(state, action.value.turnId, TurnState.CANCELLED) + endTurn(state, action.value.turnId, action.value.duration, TurnState.CANCELLED) is StateActionChatError -> - endTurn(state, action.value.turnId, TurnState.ERROR, SessionStatus.ERROR, action.value.error) + endTurn(state, action.value.turnId, action.value.duration, TurnState.ERROR, SessionStatus.ERROR, action.value.error) is StateActionChatActivityChanged -> state.copy(activity = action.value.activity) diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt index cd859497..46101893 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/Actions.generated.kt @@ -290,6 +290,10 @@ data class ChatTurnStartedAction( * Turn identifier */ val turnId: String, + /** + * ISO 8601 timestamp when this turn started. + */ + val startedAt: String, /** * The new message */ @@ -609,6 +613,13 @@ data class ChatTurnCompleteAction( * Turn identifier */ val turnId: String, + /** + * Elapsed turn duration in milliseconds, measured by the producer's own + * clock. Clients MUST NOT derive this by subtracting timestamps — cross- + * client clocks may differ — and MUST treat it as opaque, producer-supplied + * data. + */ + val duration: Long, /** * Additional provider-specific metadata for this action. * @@ -629,6 +640,13 @@ data class ChatTurnCancelledAction( * Turn identifier */ val turnId: String, + /** + * Elapsed turn duration in milliseconds, measured by the producer's own + * clock. Clients MUST NOT derive this by subtracting timestamps — cross- + * client clocks may differ — and MUST treat it as opaque, producer-supplied + * data. + */ + val duration: Long, /** * Additional provider-specific metadata for this action. * @@ -649,6 +667,13 @@ data class ChatErrorAction( * Turn identifier */ val turnId: String, + /** + * Elapsed turn duration in milliseconds, measured by the producer's own + * clock. Clients MUST NOT derive this by subtracting timestamps — cross- + * client clocks may differ — and MUST treat it as opaque, producer-supplied + * data. + */ + val duration: Long, /** * Error details */ diff --git a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt index babb1ec2..ab8982a9 100644 --- a/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt +++ b/clients/kotlin/src/main/kotlin/com/microsoft/agenthostprotocol/generated/State.generated.kt @@ -1576,6 +1576,14 @@ data class Turn( * Turn identifier */ val id: String, + /** + * ISO 8601 timestamp when this turn started. + */ + val startedAt: String? = null, + /** + * Turn duration in milliseconds. + */ + val duration: Long? = null, /** * The message that initiated the turn */ @@ -1607,6 +1615,10 @@ data class ActiveTurn( * Turn identifier */ val id: String, + /** + * ISO 8601 timestamp when this turn started. + */ + val startedAt: String, /** * The message that initiated the turn */ diff --git a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt index ba36c542..633193b6 100644 --- a/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt +++ b/clients/kotlin/src/test/kotlin/com/microsoft/agenthostprotocol/ReducersTest.kt @@ -209,23 +209,20 @@ class ReducersTest { } @Test - fun `currentTimestampProvider override flows through to reducer outputs`() { - currentTimestampProvider = { 12345L } - - // The chat reducer stamps `modifiedAt` from the injected timestamp - // provider. (The session reducer no longer stamps a timestamp — the - // host owns the root-channel summary's `modifiedAt`.) + fun `turn start stores producer timestamp while modifiedAt uses local clock`() { val chatResult = chatReducer( newChat(), StateActionChatTurnStarted( ChatTurnStartedAction( type = ActionType.CHAT_TURN_STARTED, turnId = "turn-1", + startedAt = "1970-01-01T00:00:12.345Z", message = userMessage("hello"), ), ), ) - assertEquals("1970-01-01T00:00:12.345Z", chatResult.modifiedAt) + assertEquals("1970-01-01T00:00:09.999Z", chatResult.modifiedAt) + assertEquals("1970-01-01T00:00:12.345Z", chatResult.activeTurn?.startedAt) } @Test diff --git a/clients/rust/crates/ahp-types/src/actions.rs b/clients/rust/crates/ahp-types/src/actions.rs index e871381c..cd5b3e25 100644 --- a/clients/rust/crates/ahp-types/src/actions.rs +++ b/clients/rust/crates/ahp-types/src/actions.rs @@ -321,6 +321,8 @@ pub struct SessionDefaultChatChangedAction { pub struct ChatTurnStartedAction { /// Turn identifier pub turn_id: String, + /// ISO 8601 timestamp when this turn started. + pub started_at: String, /// The new message pub message: Message, /// If this turn was auto-started from a queued message, the ID of that message @@ -613,6 +615,11 @@ pub struct ChatToolCallContentChangedAction { pub struct ChatTurnCompleteAction { /// Turn identifier pub turn_id: String, + /// Elapsed turn duration in milliseconds, measured by the producer's own + /// clock. Clients MUST NOT derive this by subtracting timestamps — cross- + /// client clocks may differ — and MUST treat it as opaque, producer-supplied + /// data. + pub duration: i64, /// Additional provider-specific metadata for this action. /// /// Clients MAY look for well-known keys here to provide enhanced UI, and @@ -630,6 +637,11 @@ pub struct ChatTurnCompleteAction { pub struct ChatTurnCancelledAction { /// Turn identifier pub turn_id: String, + /// Elapsed turn duration in milliseconds, measured by the producer's own + /// clock. Clients MUST NOT derive this by subtracting timestamps — cross- + /// client clocks may differ — and MUST treat it as opaque, producer-supplied + /// data. + pub duration: i64, /// Additional provider-specific metadata for this action. /// /// Clients MAY look for well-known keys here to provide enhanced UI, and @@ -647,6 +659,11 @@ pub struct ChatTurnCancelledAction { pub struct ChatErrorAction { /// Turn identifier pub turn_id: String, + /// Elapsed turn duration in milliseconds, measured by the producer's own + /// clock. Clients MUST NOT derive this by subtracting timestamps — cross- + /// client clocks may differ — and MUST treat it as opaque, producer-supplied + /// data. + pub duration: i64, /// Error details pub error: ErrorInfo, /// Additional provider-specific metadata for this action. diff --git a/clients/rust/crates/ahp-types/src/state.rs b/clients/rust/crates/ahp-types/src/state.rs index 6775f54e..6da14c19 100644 --- a/clients/rust/crates/ahp-types/src/state.rs +++ b/clients/rust/crates/ahp-types/src/state.rs @@ -1474,6 +1474,12 @@ pub struct SessionConfigState { pub struct Turn { /// Turn identifier pub id: String, + /// ISO 8601 timestamp when this turn started. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub started_at: Option, + /// Turn duration in milliseconds. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub duration: Option, /// The message that initiated the turn pub message: Message, /// All response content in stream order: text, tool calls, reasoning, and content refs. @@ -1497,6 +1503,8 @@ pub struct Turn { pub struct ActiveTurn { /// Turn identifier pub id: String, + /// ISO 8601 timestamp when this turn started. + pub started_at: String, /// The message that initiated the turn pub message: Message, /// All response content in stream order: text, tool calls, reasoning, and content refs. diff --git a/clients/rust/crates/ahp/src/reducers.rs b/clients/rust/crates/ahp/src/reducers.rs index 4f854ba9..fdf36a9a 100644 --- a/clients/rust/crates/ahp/src/reducers.rs +++ b/clients/rust/crates/ahp/src/reducers.rs @@ -278,6 +278,7 @@ fn touch_chat_modified(state: &mut ChatState) { fn end_turn( state: &mut ChatState, turn_id: &str, + duration: i64, turn_state: TurnState, terminal_status: Option, error: Option, @@ -344,8 +345,12 @@ fn end_turn( }) .collect(); + // Defensive clamp: `duration` is producer-supplied and opaque to this + // reducer, but a negative value would be nonsensical to display. let turn = Turn { id: active.id, + started_at: Some(active.started_at), + duration: Some(duration.max(0)), message: active.message, response_parts, usage: active.usage, @@ -355,7 +360,7 @@ fn end_turn( state.turns.push(turn); state.input_requests = None; - touch_chat_modified(state); + state.modified_at = now_iso(); state.status = summary_status(state, terminal_status); ReduceOutcome::Applied } @@ -874,15 +879,26 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu active.response_parts.push(a.part.clone()); ReduceOutcome::Applied } - StateAction::ChatTurnComplete(a) => { - end_turn(state, &a.turn_id, TurnState::Complete, None, None) - } - StateAction::ChatTurnCancelled(a) => { - end_turn(state, &a.turn_id, TurnState::Cancelled, None, None) - } + StateAction::ChatTurnComplete(a) => end_turn( + state, + &a.turn_id, + a.duration, + TurnState::Complete, + None, + None, + ), + StateAction::ChatTurnCancelled(a) => end_turn( + state, + &a.turn_id, + a.duration, + TurnState::Cancelled, + None, + None, + ), StateAction::ChatError(a) => end_turn( state, &a.turn_id, + a.duration, TurnState::Error, Some(SessionStatus::Error), Some(a.error.clone()), @@ -1097,6 +1113,7 @@ pub fn apply_action_to_chat(state: &mut ChatState, action: &StateAction) -> Redu fn apply_turn_started(state: &mut ChatState, a: &ChatTurnStartedAction) -> ReduceOutcome { state.active_turn = Some(ActiveTurn { id: a.turn_id.clone(), + started_at: a.started_at.clone(), message: a.message.clone(), response_parts: Vec::new(), usage: None, @@ -1767,6 +1784,7 @@ mod tests { let mut s = empty_chat("copilot:/s1/chat/1"); let action = StateAction::ChatTurnStarted(ChatTurnStartedAction { turn_id: "t1".into(), + started_at: "2026-07-09T20:00:00.000Z".into(), message: user_message("hi"), queued_message_id: None, meta: None, @@ -1784,6 +1802,7 @@ mod tests { let mut s = empty_chat("copilot:/s1/chat/1"); s.active_turn = Some(ActiveTurn { id: "t1".into(), + started_at: "2026-07-09T20:00:00.000Z".into(), message: user_message("hi"), response_parts: vec![ResponsePart::Markdown(MarkdownResponsePart { id: "p1".into(), @@ -1809,6 +1828,7 @@ mod tests { let mut s = empty_chat("copilot:/s1/chat/1"); s.active_turn = Some(ActiveTurn { id: "t1".into(), + started_at: "2026-07-09T20:00:00.000Z".into(), message: user_message("hi"), response_parts: Vec::new(), usage: None, @@ -1816,6 +1836,7 @@ mod tests { s.status = SessionStatus::InProgress.bits(); let a = StateAction::ChatTurnComplete(ahp_types::actions::ChatTurnCompleteAction { turn_id: "t1".into(), + duration: 1000, meta: None, }); assert_eq!(apply_action_to_chat(&mut s, &a), ReduceOutcome::Applied); @@ -1885,6 +1906,7 @@ mod tests { // A chat-scoped action is out of scope for the session reducer. let turn = StateAction::ChatTurnComplete(ahp_types::actions::ChatTurnCompleteAction { turn_id: "t1".into(), + duration: 1000, meta: None, }); assert_eq!( diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift index edb1dcdc..be9c894b 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/Actions.generated.swift @@ -246,6 +246,8 @@ public struct ChatTurnStartedAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String + /// ISO 8601 timestamp when this turn started. + public var startedAt: String /// The new message public var message: Message /// If this turn was auto-started from a queued message, the ID of that message @@ -262,6 +264,7 @@ public struct ChatTurnStartedAction: Codable, Sendable { enum CodingKeys: String, CodingKey { case type case turnId + case startedAt case message case queuedMessageId case meta = "_meta" @@ -270,12 +273,14 @@ public struct ChatTurnStartedAction: Codable, Sendable { public init( type: ActionType, turnId: String, + startedAt: String, message: Message, queuedMessageId: String? = nil, meta: [String: AnyCodable]? = nil ) { self.type = type self.turnId = turnId + self.startedAt = startedAt self.message = message self.queuedMessageId = queuedMessageId self.meta = meta @@ -711,6 +716,11 @@ public struct ChatTurnCompleteAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String + /// Elapsed turn duration in milliseconds, measured by the producer's own + /// clock. Clients MUST NOT derive this by subtracting timestamps — cross- + /// client clocks may differ — and MUST treat it as opaque, producer-supplied + /// data. + public var duration: Int /// Additional provider-specific metadata for this action. /// /// Clients MAY look for well-known keys here to provide enhanced UI, and @@ -723,16 +733,19 @@ public struct ChatTurnCompleteAction: Codable, Sendable { enum CodingKeys: String, CodingKey { case type case turnId + case duration case meta = "_meta" } public init( type: ActionType, turnId: String, + duration: Int, meta: [String: AnyCodable]? = nil ) { self.type = type self.turnId = turnId + self.duration = duration self.meta = meta } } @@ -741,6 +754,11 @@ public struct ChatTurnCancelledAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String + /// Elapsed turn duration in milliseconds, measured by the producer's own + /// clock. Clients MUST NOT derive this by subtracting timestamps — cross- + /// client clocks may differ — and MUST treat it as opaque, producer-supplied + /// data. + public var duration: Int /// Additional provider-specific metadata for this action. /// /// Clients MAY look for well-known keys here to provide enhanced UI, and @@ -753,16 +771,19 @@ public struct ChatTurnCancelledAction: Codable, Sendable { enum CodingKeys: String, CodingKey { case type case turnId + case duration case meta = "_meta" } public init( type: ActionType, turnId: String, + duration: Int, meta: [String: AnyCodable]? = nil ) { self.type = type self.turnId = turnId + self.duration = duration self.meta = meta } } @@ -771,6 +792,11 @@ public struct ChatErrorAction: Codable, Sendable { public var type: ActionType /// Turn identifier public var turnId: String + /// Elapsed turn duration in milliseconds, measured by the producer's own + /// clock. Clients MUST NOT derive this by subtracting timestamps — cross- + /// client clocks may differ — and MUST treat it as opaque, producer-supplied + /// data. + public var duration: Int /// Error details public var error: ErrorInfo /// Additional provider-specific metadata for this action. @@ -785,6 +811,7 @@ public struct ChatErrorAction: Codable, Sendable { enum CodingKeys: String, CodingKey { case type case turnId + case duration case error case meta = "_meta" } @@ -792,11 +819,13 @@ public struct ChatErrorAction: Codable, Sendable { public init( type: ActionType, turnId: String, + duration: Int, error: ErrorInfo, meta: [String: AnyCodable]? = nil ) { self.type = type self.turnId = turnId + self.duration = duration self.error = error self.meta = meta } diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift index 9a6990fc..12c63924 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Generated/State.generated.swift @@ -1412,6 +1412,10 @@ public struct SessionConfigState: Codable, Sendable { public struct Turn: Codable, Sendable { /// Turn identifier public var id: String + /// ISO 8601 timestamp when this turn started. + public var startedAt: String? + /// Turn duration in milliseconds. + public var duration: Int? /// The message that initiated the turn public var message: Message /// All response content in stream order: text, tool calls, reasoning, and content refs. @@ -1428,6 +1432,8 @@ public struct Turn: Codable, Sendable { public init( id: String, + startedAt: String? = nil, + duration: Int? = nil, message: Message, responseParts: [ResponsePart], usage: UsageInfo? = nil, @@ -1435,6 +1441,8 @@ public struct Turn: Codable, Sendable { error: ErrorInfo? = nil ) { self.id = id + self.startedAt = startedAt + self.duration = duration self.message = message self.responseParts = responseParts self.usage = usage @@ -1446,6 +1454,8 @@ public struct Turn: Codable, Sendable { public struct ActiveTurn: Codable, Sendable { /// Turn identifier public var id: String + /// ISO 8601 timestamp when this turn started. + public var startedAt: String /// The message that initiated the turn public var message: Message /// All response content in stream order: text, tool calls, reasoning, and content refs. @@ -1457,11 +1467,13 @@ public struct ActiveTurn: Codable, Sendable { public init( id: String, + startedAt: String, message: Message, responseParts: [ResponsePart], usage: UsageInfo? = nil ) { self.id = id + self.startedAt = startedAt self.message = message self.responseParts = responseParts self.usage = usage diff --git a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift index f3c955ee..bc7a4028 100644 --- a/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift +++ b/clients/swift/AgentHostProtocol/Sources/AgentHostProtocol/Reducers.swift @@ -105,6 +105,7 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { next.modifiedAt = currentTimestamp() next.activeTurn = ActiveTurn( id: a.turnId, + startedAt: a.startedAt, message: a.message, responseParts: [], usage: nil @@ -138,13 +139,13 @@ public func chatReducer(state: ChatState, action: StateAction) -> ChatState { return next case .chatTurnComplete(let a): - return endTurn(state: state, turnId: a.turnId, turnState: .complete) + return endTurn(state: state, turnId: a.turnId, duration: a.duration, turnState: .complete) case .chatTurnCancelled(let a): - return endTurn(state: state, turnId: a.turnId, turnState: .cancelled) + return endTurn(state: state, turnId: a.turnId, duration: a.duration, turnState: .cancelled) case .chatError(let a): - return endTurn(state: state, turnId: a.turnId, turnState: .error, terminalStatus: .error, error: a.error) + return endTurn(state: state, turnId: a.turnId, duration: a.duration, turnState: .error, terminalStatus: .error, error: a.error) case .chatActivityChanged(let a): var next = state @@ -887,6 +888,7 @@ private func upsertInputRequest(state: ChatState, request: ChatInputRequest) -> private func endTurn( state: ChatState, turnId: String, + duration: Int, turnState: TurnState, terminalStatus: SessionStatus? = nil, error: ErrorInfo? = nil @@ -940,8 +942,12 @@ private func endTurn( } } + // Defensive clamp: `duration` is producer-supplied and opaque to this + // reducer, but a negative value would be nonsensical to display. let turn = Turn( id: activeTurn.id, + startedAt: activeTurn.startedAt, + duration: max(0, duration), message: activeTurn.message, responseParts: responseParts, usage: activeTurn.usage, diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift index 5de270a4..09bae9ea 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/NativeReducerTests.swift @@ -50,6 +50,7 @@ final class NativeReducerTests: XCTestCase { turns: [], activeTurn: ActiveTurn( id: T, + startedAt: "2026-07-09T20:00:00.000Z", message: Message(text: "Hello", origin: MessageOrigin(kind: .user)), responseParts: [], usage: nil diff --git a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift index 88924b3e..b234db36 100644 --- a/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift +++ b/clients/swift/AgentHostProtocol/Tests/AgentHostProtocolTests/ReducersTests.swift @@ -46,6 +46,7 @@ final class ReducersTests: XCTestCase { turns: [], activeTurn: ActiveTurn( id: T, + startedAt: "2026-07-09T20:00:00.000Z", message: Message(text: "Hello", origin: MessageOrigin(kind: .user)), responseParts: [], usage: nil @@ -88,7 +89,8 @@ final class ReducersTests: XCTestCase { func testClientDispatchableReturnsTrue() { let action: StateAction = .chatTurnStarted(ChatTurnStartedAction( - type: .chatTurnStarted, turnId: T, message: Message(text: "Hello", origin: MessageOrigin(kind: .user)) + type: .chatTurnStarted, turnId: T, startedAt: "2026-07-09T20:00:00.000Z", + message: Message(text: "Hello", origin: MessageOrigin(kind: .user)) )) XCTAssertTrue(isClientDispatchable(action)) } @@ -101,6 +103,10 @@ final class ReducersTests: XCTestCase { // MARK: - Timestamp Behavior func testChatTurnStartedUpdatesModifiedAt() { + let originalProvider = currentTimestampProvider + currentTimestampProvider = { 9_999 } + defer { currentTimestampProvider = originalProvider } + let state = ChatState( resource: C, title: "Test Chat", @@ -111,10 +117,12 @@ final class ReducersTests: XCTestCase { let next = chatReducer( state: state, action: .chatTurnStarted(ChatTurnStartedAction( - type: .chatTurnStarted, turnId: T, message: Message(text: "Hello", origin: MessageOrigin(kind: .user)) + type: .chatTurnStarted, turnId: T, startedAt: "2026-07-09T20:00:00.000Z", + message: Message(text: "Hello", origin: MessageOrigin(kind: .user)) )) ) - XCTAssertGreaterThan(next.modifiedAt, state.modifiedAt) + XCTAssertEqual(next.modifiedAt, "1970-01-01T00:00:09.999Z") + XCTAssertEqual(next.activeTurn?.startedAt, "2026-07-09T20:00:00.000Z") } func testTitleChangedUpdatesTitle() { diff --git a/docs/.changes/20260709-turn-timing.json b/docs/.changes/20260709-turn-timing.json new file mode 100644 index 00000000..e8e37dea --- /dev/null +++ b/docs/.changes/20260709-turn-timing.json @@ -0,0 +1,4 @@ +{ + "type": "added", + "message": "`chat/turnStarted` carries a `startedAt` timestamp, `chat/turnComplete`/`chat/turnCancelled`/`chat/error` carry an elapsed `duration` (milliseconds, producer's own clock), and completed turns expose their start time and duration." +} diff --git a/schema/actions.schema.json b/schema/actions.schema.json index 038c7b4c..26162e44 100644 --- a/schema/actions.schema.json +++ b/schema/actions.schema.json @@ -649,6 +649,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The new message" @@ -666,6 +670,7 @@ "required": [ "type", "turnId", + "startedAt", "message" ] }, @@ -1089,6 +1094,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1097,7 +1106,8 @@ }, "required": [ "type", - "turnId" + "turnId", + "duration" ] }, "ChatTurnCancelledAction": { @@ -1111,6 +1121,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -1119,7 +1133,8 @@ }, "required": [ "type", - "turnId" + "turnId", + "duration" ] }, "ChatErrorAction": { @@ -1133,6 +1148,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "error": { "$ref": "#/$defs/ErrorInfo", "description": "Error details" @@ -1146,6 +1165,7 @@ "required": [ "type", "turnId", + "duration", "error" ] }, @@ -4779,6 +4799,14 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, + "duration": { + "type": "number", + "description": "Turn duration in milliseconds." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -4819,6 +4847,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -4837,6 +4869,7 @@ }, "required": [ "id", + "startedAt", "message", "responseParts", "usage" diff --git a/schema/commands.schema.json b/schema/commands.schema.json index c1b658c8..838c6cf5 100644 --- a/schema/commands.schema.json +++ b/schema/commands.schema.json @@ -4067,6 +4067,14 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, + "duration": { + "type": "number", + "description": "Turn duration in milliseconds." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -4107,6 +4115,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -4125,6 +4137,7 @@ }, "required": [ "id", + "startedAt", "message", "responseParts", "usage" @@ -6482,6 +6495,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The new message" @@ -6499,6 +6516,7 @@ "required": [ "type", "turnId", + "startedAt", "message" ] }, @@ -6922,6 +6940,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -6930,7 +6952,8 @@ }, "required": [ "type", - "turnId" + "turnId", + "duration" ] }, "ChatTurnCancelledAction": { @@ -6944,6 +6967,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -6952,7 +6979,8 @@ }, "required": [ "type", - "turnId" + "turnId", + "duration" ] }, "ChatErrorAction": { @@ -6966,6 +6994,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "error": { "$ref": "#/$defs/ErrorInfo", "description": "Error details" @@ -6979,6 +7011,7 @@ "required": [ "type", "turnId", + "duration", "error" ] }, diff --git a/schema/errors.schema.json b/schema/errors.schema.json index f64961ff..7491372f 100644 --- a/schema/errors.schema.json +++ b/schema/errors.schema.json @@ -2858,6 +2858,14 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, + "duration": { + "type": "number", + "description": "Turn duration in milliseconds." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -2898,6 +2906,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -2916,6 +2928,7 @@ }, "required": [ "id", + "startedAt", "message", "responseParts", "usage" @@ -7393,6 +7406,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The new message" @@ -7410,6 +7427,7 @@ "required": [ "type", "turnId", + "startedAt", "message" ] }, @@ -7745,6 +7763,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -7753,7 +7775,8 @@ }, "required": [ "type", - "turnId" + "turnId", + "duration" ] }, "ChatTurnCancelledAction": { @@ -7767,6 +7790,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "_meta": { "type": "object", "additionalProperties": {}, @@ -7775,7 +7802,8 @@ }, "required": [ "type", - "turnId" + "turnId", + "duration" ] }, "ChatErrorAction": { @@ -7789,6 +7817,10 @@ "type": "string", "description": "Turn identifier" }, + "duration": { + "type": "number", + "description": "Elapsed turn duration in milliseconds, measured by the producer's own\nclock. Clients MUST NOT derive this by subtracting timestamps — cross-\nclient clocks may differ — and MUST treat it as opaque, producer-supplied\ndata." + }, "error": { "$ref": "#/$defs/ErrorInfo", "description": "Error details" @@ -7802,6 +7834,7 @@ "required": [ "type", "turnId", + "duration", "error" ] }, diff --git a/schema/notifications.schema.json b/schema/notifications.schema.json index 3c534515..6a1f0318 100644 --- a/schema/notifications.schema.json +++ b/schema/notifications.schema.json @@ -3018,6 +3018,14 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, + "duration": { + "type": "number", + "description": "Turn duration in milliseconds." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -3058,6 +3066,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -3076,6 +3088,7 @@ }, "required": [ "id", + "startedAt", "message", "responseParts", "usage" diff --git a/schema/state.schema.json b/schema/state.schema.json index 7a107af7..d9f1ac3c 100644 --- a/schema/state.schema.json +++ b/schema/state.schema.json @@ -2769,6 +2769,14 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, + "duration": { + "type": "number", + "description": "Turn duration in milliseconds." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -2809,6 +2817,10 @@ "type": "string", "description": "Turn identifier" }, + "startedAt": { + "type": "string", + "description": "ISO 8601 timestamp when this turn started." + }, "message": { "$ref": "#/$defs/Message", "description": "The message that initiated the turn" @@ -2827,6 +2839,7 @@ }, "required": [ "id", + "startedAt", "message", "responseParts", "usage" diff --git a/types/channels-chat/actions.ts b/types/channels-chat/actions.ts index 0fd6b8d4..c1f5e663 100644 --- a/types/channels-chat/actions.ts +++ b/types/channels-chat/actions.ts @@ -65,6 +65,8 @@ export interface ChatTurnStartedAction { type: ActionType.ChatTurnStarted; /** Turn identifier */ turnId: string; + /** ISO 8601 timestamp when this turn started. */ + startedAt: string; /** The new message */ message: Message; /** If this turn was auto-started from a queued message, the ID of that message */ @@ -343,6 +345,13 @@ export interface ChatTurnCompleteAction { type: ActionType.ChatTurnComplete; /** Turn identifier */ turnId: string; + /** + * Elapsed turn duration in milliseconds, measured by the producer's own + * clock. Clients MUST NOT derive this by subtracting timestamps — cross- + * client clocks may differ — and MUST treat it as opaque, producer-supplied + * data. + */ + duration: number; /** * Additional provider-specific metadata for this action. * @@ -366,6 +375,13 @@ export interface ChatTurnCancelledAction { type: ActionType.ChatTurnCancelled; /** Turn identifier */ turnId: string; + /** + * Elapsed turn duration in milliseconds, measured by the producer's own + * clock. Clients MUST NOT derive this by subtracting timestamps — cross- + * client clocks may differ — and MUST treat it as opaque, producer-supplied + * data. + */ + duration: number; /** * Additional provider-specific metadata for this action. * @@ -388,6 +404,13 @@ export interface ChatErrorAction { type: ActionType.ChatError; /** Turn identifier */ turnId: string; + /** + * Elapsed turn duration in milliseconds, measured by the producer's own + * clock. Clients MUST NOT derive this by subtracting timestamps — cross- + * client clocks may differ — and MUST treat it as opaque, producer-supplied + * data. + */ + duration: number; /** Error details */ error: ErrorInfo; /** diff --git a/types/channels-chat/reducer.ts b/types/channels-chat/reducer.ts index 23764292..1da23f9a 100644 --- a/types/channels-chat/reducer.ts +++ b/types/channels-chat/reducer.ts @@ -117,6 +117,7 @@ function endTurn( state: ChatState, turnId: string, turnState: TurnState, + duration: number, terminalStatus?: SessionStatus.Error, error?: { errorType: string; message: string; stack?: string }, ): ChatState { @@ -148,6 +149,10 @@ function endTurn( const turn: Turn = { id: active.id, + startedAt: active.startedAt, + // Defensive clamp: the duration is producer-supplied and opaque to this + // reducer, but a negative value would be nonsensical to display. + duration: Math.max(0, duration), message: active.message, responseParts, usage: active.usage, @@ -276,6 +281,7 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st ...state, activeTurn: { id: action.turnId, + startedAt: action.startedAt, message: action.message, responseParts: [], usage: undefined, @@ -322,13 +328,13 @@ export function chatReducer(state: ChatState, action: ChatAction, log?: (msg: st }; case ActionType.ChatTurnComplete: - return endTurn(state, action.turnId, TurnState.Complete); + return endTurn(state, action.turnId, TurnState.Complete, action.duration); case ActionType.ChatTurnCancelled: - return endTurn(state, action.turnId, TurnState.Cancelled); + return endTurn(state, action.turnId, TurnState.Cancelled, action.duration); case ActionType.ChatError: - return endTurn(state, action.turnId, TurnState.Error, SessionStatus.Error, action.error); + return endTurn(state, action.turnId, TurnState.Error, action.duration, SessionStatus.Error, action.error); case ActionType.ChatActivityChanged: return { ...state, activity: action.activity }; diff --git a/types/channels-chat/state.ts b/types/channels-chat/state.ts index 00e6f689..9dff723f 100644 --- a/types/channels-chat/state.ts +++ b/types/channels-chat/state.ts @@ -504,6 +504,10 @@ export const enum MessageAttachmentKind { export interface Turn { /** Turn identifier */ id: string; + /** ISO 8601 timestamp when this turn started. */ + startedAt?: string; + /** Turn duration in milliseconds. */ + duration?: number; /** The message that initiated the turn */ message: Message; /** @@ -529,6 +533,8 @@ export interface Turn { export interface ActiveTurn { /** Turn identifier */ id: string; + /** ISO 8601 timestamp when this turn started. */ + startedAt: string; /** The message that initiated the turn */ message: Message; /** diff --git a/types/reducers.test.ts b/types/reducers.test.ts index f3a8c7de..0d04b739 100644 --- a/types/reducers.test.ts +++ b/types/reducers.test.ts @@ -211,7 +211,7 @@ describe('IS_CLIENT_DISPATCHABLE', () => { describe('isClientDispatchable', () => { it('returns true for client-dispatchable actions', () => { - const action = { type: ActionType.ChatTurnStarted, turnId: 't', message: { text: 'Hello', origin: { kind: MessageKind.User } } } as const; + const action = { type: ActionType.ChatTurnStarted, turnId: 't', startedAt: '2026-07-10T00:00:00.000Z', message: { text: 'Hello', origin: { kind: MessageKind.User } } } as const; assert.equal(isClientDispatchable(action), true); }); diff --git a/types/test-cases/reducers/005-session-turnstarted.json b/types/test-cases/reducers/005-session-turnstarted.json index 3c3576ad..9b8300dd 100644 --- a/types/test-cases/reducers/005-session-turnstarted.json +++ b/types/test-cases/reducers/005-session-turnstarted.json @@ -12,6 +12,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -24,6 +25,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json b/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json index 8636d1b2..a63e36ad 100644 --- a/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json +++ b/types/test-cases/reducers/006-turnstarted-with-queuedmessageid-removes-from-queuedmessages.json @@ -32,6 +32,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "First", "origin": { @@ -45,6 +46,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "First", "origin": { diff --git a/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json b/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json index 15b4ecf4..8916d48d 100644 --- a/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json +++ b/types/test-cases/reducers/007-turnstarted-with-queuedmessageid-removes-last-queued-message.json @@ -23,6 +23,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Only", "origin": { @@ -36,6 +37,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Only", "origin": { diff --git a/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json b/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json index c955affc..23d591ef 100644 --- a/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json +++ b/types/test-cases/reducers/008-turnstarted-with-queuedmessageid-removes-matching-steering-message.json @@ -21,6 +21,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Steer", "origin": { @@ -34,6 +35,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Steer", "origin": { diff --git a/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json b/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json index 4b1418e5..3775d8a4 100644 --- a/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json +++ b/types/test-cases/reducers/009-turnstarted-without-queuedmessageid-does-not-touch-pending-messages.json @@ -32,6 +32,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -44,6 +45,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/010-session-delta-appends-content.json b/types/test-cases/reducers/010-session-delta-appends-content.json index f4cefe7e..69d38431 100644 --- a/types/test-cases/reducers/010-session-delta-appends-content.json +++ b/types/test-cases/reducers/010-session-delta-appends-content.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -46,6 +47,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json b/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json index c7fef529..8575b05e 100644 --- a/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/011-session-delta-with-wrong-turnid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -37,6 +38,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json b/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json index 5ea45c46..0feba633 100644 --- a/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json +++ b/types/test-cases/reducers/013-session-responsepart-adds-to-responseparts.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -34,6 +35,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json b/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json index 2e4f14b9..b2769329 100644 --- a/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json +++ b/types/test-cases/reducers/014-session-turncomplete-finalizes-turn.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -37,13 +38,16 @@ }, { "type": "chat/turnComplete", - "turnId": "turn-1" + "turnId": "turn-1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json b/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json index 61677b59..618aefb3 100644 --- a/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json +++ b/types/test-cases/reducers/015-session-turncancelled-finalizes-turn.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -22,13 +23,16 @@ "actions": [ { "type": "chat/turnCancelled", - "turnId": "turn-1" + "turnId": "turn-1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json b/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json index 40c41bbd..6a690e3f 100644 --- a/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json +++ b/types/test-cases/reducers/016-session-error-finalizes-turn-with-error.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -23,6 +24,7 @@ { "type": "chat/error", "turnId": "turn-1", + "duration": 8999, "error": { "errorType": "runtime", "message": "Something broke" @@ -33,6 +35,8 @@ "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json b/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json index 51b4a653..b56073aa 100644 --- a/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json +++ b/types/test-cases/reducers/017-turncomplete-force-cancels-in-progress-tool-calls.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -29,13 +30,16 @@ }, { "type": "chat/turnComplete", - "turnId": "turn-1" + "turnId": "turn-1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json b/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json index 42df2388..c95e01e2 100644 --- a/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/018-turncomplete-with-wrong-turnid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -22,13 +23,15 @@ "actions": [ { "type": "chat/turnComplete", - "turnId": "wrong-turn" + "turnId": "wrong-turn", + "duration": 8999 } ], "expected": { "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json index ca778718..09391579 100644 --- a/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json +++ b/types/test-cases/reducers/019-tool-call-full-lifecycle-start-delta-ready-confirmed-complete.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -63,6 +64,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json b/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json index b6b84c22..c8577e42 100644 --- a/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json +++ b/types/test-cases/reducers/020-tool-call-ready-with-auto-confirm-transitions-to-running.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -39,6 +40,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json b/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json index 99550ee7..69bdd0cd 100644 --- a/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json +++ b/types/test-cases/reducers/021-tool-call-denied-transitions-to-cancelled.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -45,6 +46,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json b/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json index 9ed72edb..d5b5c1f2 100644 --- a/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json +++ b/types/test-cases/reducers/022-tool-call-result-confirmation-pending-approved.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -55,6 +56,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json b/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json index 40782570..563b8dd0 100644 --- a/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json +++ b/types/test-cases/reducers/023-tool-call-result-denied-cancelled-with-result-denied-reason.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -55,6 +56,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json b/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json index fc5c96d7..e5b9f540 100644 --- a/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json +++ b/types/test-cases/reducers/024-tool-call-complete-from-pending-confirmation-defaults-confirmed.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -47,6 +48,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json b/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json index b4c0d1d9..543212f6 100644 --- a/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json +++ b/types/test-cases/reducers/025-tool-call-actions-for-unknown-toolcallid-are-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -31,6 +32,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json index 86458d85..94f7f058 100644 --- a/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json +++ b/types/test-cases/reducers/026-toolcallready-transitions-running-tool-back-to-pending-confirmation.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -49,6 +50,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json b/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json index 334292b2..880caf71 100644 --- a/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json +++ b/types/test-cases/reducers/027-toolcallready-re-confirmation-approved-transitions-back-to-running.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -52,6 +53,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json b/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json index cdc46c27..414ae35d 100644 --- a/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json +++ b/types/test-cases/reducers/028-toolcallready-re-confirmation-denied-transitions-to-cancelled.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -52,6 +53,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json b/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json index 9c72391a..d7e4acd6 100644 --- a/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json +++ b/types/test-cases/reducers/029-toolcallready-ignores-non-streaming-non-running-tool-calls.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -47,6 +48,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json b/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json index c9cd4ec4..634f8b2f 100644 --- a/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json +++ b/types/test-cases/reducers/031-session-usage-updates-usage-on-active-turn.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -33,6 +34,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json b/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json index 40338a9c..a9052ee6 100644 --- a/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json +++ b/types/test-cases/reducers/032-session-reasoning-appends-reasoning-content.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -46,6 +47,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/067-session-truncated-drops-active-turn.json b/types/test-cases/reducers/067-session-truncated-drops-active-turn.json index d5dde94f..63c00236 100644 --- a/types/test-cases/reducers/067-session-truncated-drops-active-turn.json +++ b/types/test-cases/reducers/067-session-truncated-drops-active-turn.json @@ -30,6 +30,7 @@ ], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json b/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json index e9435f3c..2912d048 100644 --- a/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json +++ b/types/test-cases/reducers/068-session-truncated-drops-active-turn-even-when-clearing-all.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json index 8225df98..95a3cbef 100644 --- a/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json +++ b/types/test-cases/reducers/070-full-turn-flow-with-tool-calls-and-re-confirmation.json @@ -12,6 +12,7 @@ { "type": "chat/turnStarted", "turnId": "t1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Fix the bug", "origin": { @@ -137,13 +138,16 @@ }, { "type": "chat/turnComplete", - "turnId": "t1" + "turnId": "t1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "t1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Fix the bug", "origin": { diff --git a/types/test-cases/reducers/075-turnstarted-clears-isread.json b/types/test-cases/reducers/075-turnstarted-clears-isread.json index 80005421..fd06d1d9 100644 --- a/types/test-cases/reducers/075-turnstarted-clears-isread.json +++ b/types/test-cases/reducers/075-turnstarted-clears-isread.json @@ -12,6 +12,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -24,6 +25,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json b/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json index fdd86bb8..2cb78504 100644 --- a/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json +++ b/types/test-cases/reducers/084-toolcall-contentchanged-updates-running.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -51,6 +52,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json b/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json index 84740852..18582ffe 100644 --- a/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json +++ b/types/test-cases/reducers/085-toolcall-contentchanged-noop-non-running.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -53,6 +54,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json b/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json index 171fafa2..90507115 100644 --- a/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json +++ b/types/test-cases/reducers/086-toolcall-contentchanged-replaces-existing.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -62,6 +63,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json b/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json index 08773d64..b640dc88 100644 --- a/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/090-toolcalldelta-wrong-turnid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -42,6 +43,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json b/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json index db7e3c87..457e5f84 100644 --- a/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/091-responsepart-wrong-turnid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -34,6 +35,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json b/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json index 64d7f700..e6f35b90 100644 --- a/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/092-toolcallstart-wrong-turnid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -32,6 +33,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json b/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json index cb26096f..fa12e805 100644 --- a/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json +++ b/types/test-cases/reducers/093-usage-wrong-turnid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -33,6 +34,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json b/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json index 7c564a93..964e516b 100644 --- a/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json +++ b/types/test-cases/reducers/094-delta-nonexistent-partid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -37,6 +38,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json b/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json index cdbaab61..93a0910e 100644 --- a/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/095-toolcalldelta-wrong-status-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -45,6 +46,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json b/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json index a4472603..f616ee6b 100644 --- a/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/096-toolcallconfirmed-wrong-status-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -46,6 +47,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json b/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json index b647b7ba..f4bc585f 100644 --- a/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/097-toolcallcomplete-wrong-status-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -50,6 +51,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json b/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json index a85b5135..44f6c44d 100644 --- a/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json +++ b/types/test-cases/reducers/098-toolcallresultconfirmed-wrong-status-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -45,6 +46,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json index cb64c47b..937e2c6f 100644 --- a/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json +++ b/types/test-cases/reducers/099-endturn-force-cancels-running-tool-call.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -36,13 +37,16 @@ "actions": [ { "type": "chat/turnComplete", - "turnId": "turn-1" + "turnId": "turn-1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json b/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json index b96319e0..2eee6a8d 100644 --- a/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json +++ b/types/test-cases/reducers/100-delta-targeting-toolcall-partid-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -45,6 +46,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json b/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json index a0b381c7..f8c0e1f5 100644 --- a/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json +++ b/types/test-cases/reducers/101-toolcalldelta-without-invocationmessage.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -43,6 +44,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json b/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json index f2121787..a07517e0 100644 --- a/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json +++ b/types/test-cases/reducers/102-reasoning-targeting-non-reasoning-is-no-op.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -37,6 +38,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/103-delta-skips-parts-without-id.json b/types/test-cases/reducers/103-delta-skips-parts-without-id.json index c205564e..4e83b722 100644 --- a/types/test-cases/reducers/103-delta-skips-parts-without-id.json +++ b/types/test-cases/reducers/103-delta-skips-parts-without-id.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -40,6 +41,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json b/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json index db553adc..88fdff59 100644 --- a/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json +++ b/types/test-cases/reducers/105-session-input-full-draft-and-complete-flow.json @@ -12,6 +12,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -82,6 +83,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json b/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json index bd66b38c..102076bb 100644 --- a/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json +++ b/types/test-cases/reducers/106-session-input-requested-with-drafts-status.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -51,6 +52,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json b/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json index b7c5236c..64de36e7 100644 --- a/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json +++ b/types/test-cases/reducers/107-session-input-turn-end-cleans-turn-scoped-only.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -40,13 +41,16 @@ "actions": [ { "type": "chat/turnComplete", - "turnId": "turn-1" + "turnId": "turn-1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json b/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json index 64523607..548373c6 100644 --- a/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json +++ b/types/test-cases/reducers/108-session-input-upsert-and-clear-answer.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -64,6 +65,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json b/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json index 8987abf0..676d4c21 100644 --- a/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json +++ b/types/test-cases/reducers/110-session-input-completion-and-truncation-filtering.json @@ -30,6 +30,7 @@ ], "activeTurn": { "id": "active", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Active", "origin": { diff --git a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json index 5c32932a..fbf03a73 100644 --- a/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/111-toolcall-pending-confirmation-sets-input-needed-status.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -39,6 +40,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json b/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json index 1d112bdd..0b8153ce 100644 --- a/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json +++ b/types/test-cases/reducers/112-toolcall-pending-result-confirmation-sets-input-needed-status.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -49,6 +50,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json index 3a58c403..0822e2fc 100644 --- a/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json +++ b/types/test-cases/reducers/127-toolcallready-with-confirmation-options.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -64,6 +65,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json b/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json index 09c87fde..17caaa65 100644 --- a/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json +++ b/types/test-cases/reducers/128-toolcallconfirmed-approved-with-selectedoption.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -66,6 +67,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json b/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json index 3e2254fc..0409698e 100644 --- a/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json +++ b/types/test-cases/reducers/129-toolcallconfirmed-denied-with-selectedoption.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -67,6 +68,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json b/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json index 8909bac5..31cfac14 100644 --- a/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json +++ b/types/test-cases/reducers/130-selectedoption-carries-through-to-completed.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -69,6 +70,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json b/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json index 6d17774a..3a823536 100644 --- a/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json +++ b/types/test-cases/reducers/131-selectedoption-carries-through-result-confirmation.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -70,6 +71,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json b/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json index 805add3b..871fff31 100644 --- a/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json +++ b/types/test-cases/reducers/132-selectedoption-carries-through-result-denied.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -70,6 +71,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json b/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json index d24fd7de..4fd274f3 100644 --- a/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json +++ b/types/test-cases/reducers/158-toolcallconfirmed-approved-with-editedtoolinput-overrides-original.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -56,6 +57,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json b/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json index df65d14c..4d04fe63 100644 --- a/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json +++ b/types/test-cases/reducers/161-chat-turn-lifecycle-on-chat.json @@ -15,6 +15,7 @@ { "type": "chat/turnStarted", "turnId": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -39,13 +40,16 @@ }, { "type": "chat/turnComplete", - "turnId": "turn-1" + "turnId": "turn-1", + "duration": 8999 } ], "expected": { "turns": [ { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", + "duration": 8999, "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json b/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json index a897e5e7..a01886f2 100644 --- a/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json +++ b/types/test-cases/reducers/163-toolcallstart-carries-mcp-contributor-through-lifecycle.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -53,6 +54,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/220-toolcall-actions-update-meta.json b/types/test-cases/reducers/220-toolcall-actions-update-meta.json index e3b7e797..2eebdd14 100644 --- a/types/test-cases/reducers/220-toolcall-actions-update-meta.json +++ b/types/test-cases/reducers/220-toolcall-actions-update-meta.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { @@ -216,6 +217,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hello", "origin": { diff --git a/types/test-cases/reducers/235-session-input-completion-records-declined-request-in-turn.json b/types/test-cases/reducers/235-session-input-completion-records-declined-request-in-turn.json index 968e563c..3fd17ebc 100644 --- a/types/test-cases/reducers/235-session-input-completion-records-declined-request-in-turn.json +++ b/types/test-cases/reducers/235-session-input-completion-records-declined-request-in-turn.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Review", "origin": { @@ -63,6 +64,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Review", "origin": { diff --git a/types/test-cases/reducers/237-session-input-completion-no-op-unknown-id-with-open-requests.json b/types/test-cases/reducers/237-session-input-completion-no-op-unknown-id-with-open-requests.json index 231852ff..77d568c8 100644 --- a/types/test-cases/reducers/237-session-input-completion-no-op-unknown-id-with-open-requests.json +++ b/types/test-cases/reducers/237-session-input-completion-no-op-unknown-id-with-open-requests.json @@ -5,6 +5,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hi", "origin": { @@ -36,6 +37,7 @@ "turns": [], "activeTurn": { "id": "turn-1", + "startedAt": "1970-01-01T00:00:01.000Z", "message": { "text": "Hi", "origin": { diff --git a/types/test-cases/reducers/240-turn-duration-is-clamped-to-zero.json b/types/test-cases/reducers/240-turn-duration-is-clamped-to-zero.json new file mode 100644 index 00000000..ea33eebe --- /dev/null +++ b/types/test-cases/reducers/240-turn-duration-is-clamped-to-zero.json @@ -0,0 +1,54 @@ +{ + "description": "chat/turnComplete clamps a negative turn duration to zero", + "reducer": "chat", + "initial": { + "turns": [], + "activeTurn": { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:10.000Z", + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [], + "usage": null + }, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 8, + "modifiedAt": "1970-01-01T00:00:10.000Z" + }, + "actions": [ + { + "type": "chat/turnComplete", + "turnId": "turn-1", + "duration": -1000 + } + ], + "expected": { + "turns": [ + { + "id": "turn-1", + "startedAt": "1970-01-01T00:00:10.000Z", + "duration": 0, + "message": { + "text": "Hello", + "origin": { + "kind": "user" + } + }, + "responseParts": [], + "usage": null, + "state": "complete", + "error": null + } + ], + "activeTurn": null, + "resource": "copilot:/test-session", + "title": "Test Session", + "status": 1, + "modifiedAt": "1970-01-01T00:00:09.999Z" + } +}