重构:turn.completed 的 status 由自由字符串收成 typed 枚举
- 新增 TurnCompletedStatus(completed / interrupted / aborted / failed),作为 turn.rs 里 status 字段的类型 - 新增只作构造参数的 TurnOutcome(completed / interrupted / aborted):turn_completed 拿不到 Failed,"failed 但没有失败载荷"在构造层就写不出来 - turn_completed_failed 固定写 TurnCompletedStatus::Failed,载荷与状态在同一条构造路径上 - 内部状态比较与用例断言改为比较枚举取值;线上 JSON 形状、字段名与前端契约不变 - 重新生成绑定:新增 TurnCompletedStatus.ts,ThreadEvent.ts 的 status 由 string 收成字面量联合
This commit is contained in:
@@ -5699,7 +5699,7 @@ mod tests {
|
||||
failure: None,
|
||||
at: Some(_),
|
||||
user_item_id: Some(user_item_id),
|
||||
}] if status == "aborted" && user_item_id == "direct-codex:client-turn-exited:user"
|
||||
}] if *status == TurnCompletedStatus::Aborted && user_item_id == "direct-codex:client-turn-exited:user"
|
||||
));
|
||||
|
||||
let next = TurnReservation::accept_for_test(&thread_id, "client-turn-next");
|
||||
@@ -8263,12 +8263,13 @@ done
|
||||
.find_map(|event| match event {
|
||||
ThreadEvent::TurnCompleted {
|
||||
status, failure, ..
|
||||
} => Some((status.clone(), failure.clone())),
|
||||
} => Some((*status, failure.clone())),
|
||||
_ => None,
|
||||
})
|
||||
.expect("连接死亡之后逻辑回合必须有终态");
|
||||
assert_eq!(
|
||||
terminal.0, "failed",
|
||||
terminal.0,
|
||||
TurnCompletedStatus::Failed,
|
||||
"失败事实必须先落地:{:?}",
|
||||
consumed.events
|
||||
);
|
||||
|
||||
@@ -5324,7 +5324,7 @@ mod tests {
|
||||
assert!(
|
||||
matches!(
|
||||
terminal,
|
||||
ThreadEvent::TurnCompleted { status, .. } if status == "aborted"
|
||||
ThreadEvent::TurnCompleted { status, .. } if *status == TurnCompletedStatus::Aborted
|
||||
),
|
||||
"{terminal:?}"
|
||||
);
|
||||
|
||||
@@ -206,7 +206,7 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(terminal.len(), 1, "一轮只许有一条终态:{events:?}");
|
||||
let (status, failure, user_item_id) = terminal[0];
|
||||
assert_eq!(status, "failed");
|
||||
assert_eq!(*status, TurnCompletedStatus::Failed);
|
||||
let failure = failure.as_ref().expect("失败终态必须带载荷");
|
||||
assert!(
|
||||
!failure_detail(failure).trim().is_empty(),
|
||||
@@ -333,7 +333,7 @@ mod tests {
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(terminals.len(), 1, "一轮只许有一条终态:{events:?}");
|
||||
let (status, failure) = terminals[0];
|
||||
assert_eq!(status, "failed");
|
||||
assert_eq!(*status, TurnCompletedStatus::Failed);
|
||||
let failure = failure.as_ref().expect("失败终态必须带载荷");
|
||||
let detail = failure_detail(failure);
|
||||
assert!(detail.contains("写入本项目对话历史失败"), "{detail}");
|
||||
|
||||
@@ -291,7 +291,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::agent::{
|
||||
consume_thread, enqueue_pending_turn, subscribe_thread, thread_turn_is_active, Deadline,
|
||||
PendingTurn, ThreadEvent, TimedOut, TurnFailure,
|
||||
PendingTurn, ThreadEvent, TimedOut, TurnCompletedStatus, TurnFailure,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
|
||||
@@ -426,7 +426,7 @@ mod tests {
|
||||
matches!(
|
||||
events.get(terminal),
|
||||
Some(ThreadEvent::TurnCompleted { status, failure: Some(failure), .. })
|
||||
if status == "failed" && matches!(failure, TurnFailure::HostDropped)
|
||||
if *status == TurnCompletedStatus::Failed && matches!(failure, TurnFailure::HostDropped)
|
||||
),
|
||||
"{events:?}"
|
||||
);
|
||||
@@ -489,7 +489,7 @@ mod tests {
|
||||
assert!(
|
||||
events.iter().any(|event| matches!(
|
||||
event,
|
||||
ThreadEvent::TurnCompleted { status, .. } if status == "aborted"
|
||||
ThreadEvent::TurnCompleted { status, .. } if *status == TurnCompletedStatus::Aborted
|
||||
)),
|
||||
"{events:?}"
|
||||
);
|
||||
|
||||
@@ -1241,13 +1241,13 @@ mod tests {
|
||||
let mut manager = ThreadManager::with_limits(100, 100_000);
|
||||
manager.append(
|
||||
"thread-1",
|
||||
ThreadEvent::turn_completed("completed".to_string(), FIXED_AT_MS),
|
||||
ThreadEvent::turn_completed(TurnOutcome::Completed, FIXED_AT_MS),
|
||||
);
|
||||
let bootstrap = manager.subscribe("thread-1");
|
||||
assert!(matches!(
|
||||
bootstrap.events.as_slice(),
|
||||
[ThreadEvent::TurnCompleted { status, at, .. }]
|
||||
if status == "completed" && *at == Some(FIXED_AT_MS)
|
||||
if *status == TurnCompletedStatus::Completed && *at == Some(FIXED_AT_MS)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -1266,7 +1266,7 @@ mod tests {
|
||||
assert!(matches!(
|
||||
bootstrap.events.as_slice(),
|
||||
[ThreadEvent::TurnCompleted { status, failure, at, .. }]
|
||||
if status == "failed"
|
||||
if *status == TurnCompletedStatus::Failed
|
||||
&& failure.as_ref().is_some_and(|failure| matches!(
|
||||
failure,
|
||||
crate::agent::TurnFailure::HostDropped
|
||||
@@ -1350,7 +1350,7 @@ mod tests {
|
||||
);
|
||||
manager.append(
|
||||
"thread-1",
|
||||
ThreadEvent::turn_completed("completed".to_string(), 3_000)
|
||||
ThreadEvent::turn_completed(TurnOutcome::Completed, 3_000)
|
||||
.with_user_item_id(Some("direct-codex:turn-1:user")),
|
||||
);
|
||||
let second = manager.subscribe("thread-1");
|
||||
@@ -1439,7 +1439,7 @@ mod tests {
|
||||
|
||||
manager.complete_turn(
|
||||
thread_id,
|
||||
ThreadEvent::turn_completed("completed".to_string(), 5_000),
|
||||
ThreadEvent::turn_completed(TurnOutcome::Completed, 5_000),
|
||||
);
|
||||
assert!(snapshot_of(&manager, thread_id).is_none());
|
||||
}
|
||||
@@ -1733,7 +1733,7 @@ mod tests {
|
||||
assert!(
|
||||
matches!(
|
||||
events[0],
|
||||
ThreadEvent::TurnCompleted { ref status, .. } if status == "aborted"
|
||||
ThreadEvent::TurnCompleted { ref status, .. } if *status == TurnCompletedStatus::Aborted
|
||||
),
|
||||
"{events:?}"
|
||||
);
|
||||
@@ -1832,7 +1832,7 @@ mod tests {
|
||||
|
||||
complete_turn(
|
||||
&thread_id,
|
||||
ThreadEvent::turn_completed("completed".to_string(), 5_000),
|
||||
ThreadEvent::turn_completed(TurnOutcome::Completed, 5_000),
|
||||
);
|
||||
assert_eq!(
|
||||
crate::agent::direct_active_turns_event_test_count(),
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use crate::agent::{ThreadEvent, TurnError, Unclassified};
|
||||
use crate::agent::{ThreadEvent, TurnError, TurnOutcome, Unclassified};
|
||||
|
||||
use super::wire::TurnFailure;
|
||||
|
||||
@@ -40,11 +40,11 @@ impl TurnCompletion {
|
||||
pub(crate) fn event(self, completed_at: u64, user_item_id: Option<&str>) -> ThreadEvent {
|
||||
let event = match self {
|
||||
Self::Failed(failure) => ThreadEvent::turn_completed_failed(failure, completed_at),
|
||||
Self::Completed => ThreadEvent::turn_completed("completed".to_string(), completed_at),
|
||||
Self::Completed => ThreadEvent::turn_completed(TurnOutcome::Completed, completed_at),
|
||||
Self::Interrupted => {
|
||||
ThreadEvent::turn_completed("interrupted".to_string(), completed_at)
|
||||
ThreadEvent::turn_completed(TurnOutcome::Interrupted, completed_at)
|
||||
}
|
||||
Self::Aborted => ThreadEvent::turn_completed("aborted".to_string(), completed_at),
|
||||
Self::Aborted => ThreadEvent::turn_completed(TurnOutcome::Aborted, completed_at),
|
||||
};
|
||||
event.with_user_item_id(user_item_id)
|
||||
}
|
||||
@@ -142,7 +142,8 @@ impl SessionOutcome {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::agent::{
|
||||
ModelCallKind, ThreadEvent, TransportClosed, TurnError, TurnInterrupted, Unclassified,
|
||||
ModelCallKind, ThreadEvent, TransportClosed, TurnCompletedStatus, TurnError,
|
||||
TurnInterrupted, Unclassified,
|
||||
};
|
||||
use platform_llm::LlmError;
|
||||
|
||||
@@ -288,7 +289,7 @@ stderrClass=nonempty;stderrBytes=1000";
|
||||
assert!(event.failure().is_none());
|
||||
assert!(matches!(
|
||||
event,
|
||||
ThreadEvent::TurnCompleted { ref status, .. } if status == "completed"
|
||||
ThreadEvent::TurnCompleted { ref status, .. } if *status == TurnCompletedStatus::Completed
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -366,7 +366,7 @@ fn lifecycle_events_serialize_event_level_at_as_optional_number() {
|
||||
);
|
||||
|
||||
let completed =
|
||||
serde_json::to_value(ThreadEvent::turn_completed("completed".to_string(), 2_000))
|
||||
serde_json::to_value(ThreadEvent::turn_completed(TurnOutcome::Completed, 2_000))
|
||||
.expect("serialize turn.completed");
|
||||
assert_eq!(
|
||||
completed,
|
||||
@@ -437,7 +437,7 @@ fn lifecycle_events_carry_optional_opener_user_item_id() {
|
||||
started
|
||||
);
|
||||
|
||||
let completed = ThreadEvent::turn_completed("interrupted".to_string(), 2_000)
|
||||
let completed = ThreadEvent::turn_completed(TurnOutcome::Interrupted, 2_000)
|
||||
.with_user_item_id(Some("direct-codex:turn-1:user"));
|
||||
assert_eq!(completed.user_item_id(), Some("direct-codex:turn-1:user"));
|
||||
assert_eq!(
|
||||
@@ -526,7 +526,7 @@ fn turn_completed_carries_failure_payload_only_when_failed() {
|
||||
);
|
||||
|
||||
// 正常终态不带载荷,也不回写 `failure: null`。
|
||||
let completed = ThreadEvent::turn_completed("completed".to_string(), 5_000);
|
||||
let completed = ThreadEvent::turn_completed(TurnOutcome::Completed, 5_000);
|
||||
assert_eq!(completed.failure(), None);
|
||||
assert_eq!(
|
||||
serde_json::to_value(&completed).expect("serialize completed turn"),
|
||||
|
||||
@@ -31,6 +31,41 @@ pub(crate) enum QueueRemovalOutcome {
|
||||
NotFound,
|
||||
}
|
||||
|
||||
/// `turn.completed` 的终态取值。
|
||||
///
|
||||
/// 正常收场是前三档、不带载荷;`Failed` **一定**带 [`TurnFailure`] 载荷,只由
|
||||
/// [`ThreadEvent::turn_completed_failed`] 写出。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
|
||||
pub(crate) enum TurnCompletedStatus {
|
||||
Completed,
|
||||
Interrupted,
|
||||
Aborted,
|
||||
Failed,
|
||||
}
|
||||
|
||||
/// 正常收场的三档(不含失败):只作 [`ThreadEvent::turn_completed`] 的构造参数,不进线上形状。
|
||||
///
|
||||
/// 失败必须带载荷,所以这里没有 `Failed`——这就是 [`ThreadEvent::turn_completed`] 写不出
|
||||
/// "failed 但没有载荷"的原因。
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum TurnOutcome {
|
||||
Completed,
|
||||
Interrupted,
|
||||
Aborted,
|
||||
}
|
||||
|
||||
impl From<TurnOutcome> for TurnCompletedStatus {
|
||||
fn from(outcome: TurnOutcome) -> Self {
|
||||
match outcome {
|
||||
TurnOutcome::Completed => Self::Completed,
|
||||
TurnOutcome::Interrupted => Self::Interrupted,
|
||||
TurnOutcome::Aborted => Self::Aborted,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Thread Manager 下发的运行态事件。
|
||||
///
|
||||
/// 顺序由数组顺序给出(同一个 subscriber 的 `consume` 按队列顺序返回),因此不需要 `seq`:
|
||||
@@ -72,9 +107,10 @@ pub(crate) enum ThreadEvent {
|
||||
},
|
||||
#[serde(rename = "turn.completed")]
|
||||
TurnCompleted {
|
||||
/// 终态语义:`completed` / `interrupted` / `aborted` 是正常收场;`failed` 是**失败**,
|
||||
/// 此时必须带 `failure` 载荷。
|
||||
status: String,
|
||||
/// 终态语义:`completed` / `interrupted` / `aborted` 是正常收场,不带载荷;`failed` 是
|
||||
/// **失败**,一定带 `failure` 载荷。正常收场的构造参数是 [`TurnOutcome`](不含 `Failed`),
|
||||
/// [`ThreadEvent::turn_completed`] 因此写不出"failed 但没有载荷"。
|
||||
status: TurnCompletedStatus,
|
||||
/// 失败载荷:只有 `status == "failed"` 才有;失败原因只从这里下发一次。
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -155,19 +191,19 @@ impl ThreadEvent {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn turn_completed(status: String, at: u64) -> Self {
|
||||
pub(crate) fn turn_completed(status: TurnOutcome, at: u64) -> Self {
|
||||
Self::TurnCompleted {
|
||||
status,
|
||||
status: status.into(),
|
||||
failure: None,
|
||||
at: Some(at),
|
||||
user_item_id: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 失败终态:`status` 固定 `"failed"`,原因必须随事件一起带出去。
|
||||
/// 失败终态:`status` 固定 `failed`,原因必须随事件一起带出去。
|
||||
pub(crate) fn turn_completed_failed(failure: TurnFailure, at: u64) -> Self {
|
||||
Self::TurnCompleted {
|
||||
status: "failed".to_string(),
|
||||
status: TurnCompletedStatus::Failed,
|
||||
failure: Some(failure),
|
||||
at: Some(at),
|
||||
user_item_id: None,
|
||||
|
||||
+5
-3
@@ -3,6 +3,7 @@ import type { QueueRemovalReason } from './QueueRemovalReason';
|
||||
import type { ThreadDeltaKind } from './ThreadDeltaKind';
|
||||
import type { ThreadItem } from './ThreadItem';
|
||||
import type { ThreadRequestKind } from './ThreadRequestKind';
|
||||
import type { TurnCompletedStatus } from './TurnCompletedStatus';
|
||||
import type { TurnFailure } from './TurnFailure';
|
||||
import type { UserItem } from './UserItem';
|
||||
|
||||
@@ -47,10 +48,11 @@ export type ThreadEvent =
|
||||
| {
|
||||
type: 'turn.completed';
|
||||
/**
|
||||
* 终态语义:`completed` / `interrupted` / `aborted` 是正常收场;`failed` 是**失败**,
|
||||
* 此时必须带 `failure` 载荷。
|
||||
* 终态语义:`completed` / `interrupted` / `aborted` 是正常收场,不带载荷;`failed` 是
|
||||
* **失败**,一定带 `failure` 载荷。正常收场的构造参数是 [`TurnOutcome`](不含 `Failed`),
|
||||
* [`ThreadEvent::turn_completed`] 因此写不出"failed 但没有载荷"。
|
||||
*/
|
||||
status: string;
|
||||
status: TurnCompletedStatus;
|
||||
/**
|
||||
* 失败载荷:只有 `status == "failed"` 才有;失败原因只从这里下发一次。
|
||||
*/
|
||||
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* `turn.completed` 的终态取值。
|
||||
*
|
||||
* 正常收场是前三档、不带载荷;`Failed` **一定**带 [`TurnFailure`] 载荷,只由
|
||||
* [`ThreadEvent::turn_completed_failed`] 写出。
|
||||
*/
|
||||
export type TurnCompletedStatus =
|
||||
| 'completed'
|
||||
| 'interrupted'
|
||||
| 'aborted'
|
||||
| 'failed';
|
||||
Reference in New Issue
Block a user