宿主:失败载荷的 kind 改成 typed 枚举
- 新增 `DirectTurnFailureKind`,成为 `DirectTurnFailure.kind` 的唯一取值表:`timeout / model-failed / transport-failed / request-rejected / environment-not-ready / turn-interrupted / host-dropped`(补齐原先两份注释都漏掉的 `turn-interrupted`) - `DirectModelCallKind::wire_kind` 与 `DirectTurnError::wire_kind` 改成返回该枚举,`DirectTurnFailure::new` / `DirectTurnTerminal::host_dropped` 同步改签名;线上取值仍是原来的 kebab-case 字符串 - 补 `failure_kind_wire_values_are_stable` 用例:7 个变体的序列化 / 反序列化取值逐条钉住,改名即改协议会先在这里失败 - 前端生成绑定重新导出:`chat/generated/DirectTurnFailureKind.ts` 新增,`DirectTurnFailure.ts` 的 `kind` 由 `string` 收窄成 union - 受影响断言(`direct_thread_manager` / `direct_turn_accept` / `direct_thread_wire` / `codex_app_server`)改成比较枚举变体
This commit is contained in:
@@ -1137,8 +1137,6 @@ pub(super) async fn wait_outcome(
|
||||
mod tests {
|
||||
use super::super::DirectTurnDeadline;
|
||||
|
||||
/// 通道断开在事件载荷里的稳定分类(`DirectTurnError::wire_kind` 的取值之一)。
|
||||
const EXPECTED_TRANSPORT_KIND: &str = "transport-failed";
|
||||
use super::*;
|
||||
|
||||
fn fixture() -> (tempfile::TempDir, Arc<ExecutionAdapter>) {
|
||||
@@ -1200,7 +1198,10 @@ mod tests {
|
||||
|
||||
// 终态判定读这份事实,界面才有理由把它当失败讲,而不是"本轮已结束"。
|
||||
let failure = adapter.turn_failure().expect("host fact must be recorded");
|
||||
assert_eq!(failure.wire_kind(), Some(EXPECTED_TRANSPORT_KIND));
|
||||
assert_eq!(
|
||||
failure.wire_kind(),
|
||||
Some(super::super::DirectTurnFailureKind::TransportFailed)
|
||||
);
|
||||
assert!(failure.to_string().contains("SIGKILL"));
|
||||
// 报告与事件载荷同一份原因:用户看到的现象和交付状态对得上。
|
||||
assert!(adapter.report().contains("SIGKILL"));
|
||||
@@ -1212,7 +1213,10 @@ mod tests {
|
||||
})
|
||||
.await;
|
||||
let failure = adapter.turn_failure().expect("first reason is kept");
|
||||
assert_eq!(failure.wire_kind(), Some(EXPECTED_TRANSPORT_KIND));
|
||||
assert_eq!(
|
||||
failure.wire_kind(),
|
||||
Some(super::super::DirectTurnFailureKind::TransportFailed)
|
||||
);
|
||||
assert!(failure.to_string().contains("SIGKILL"));
|
||||
assert!(!failure.to_string().contains("超时"));
|
||||
}
|
||||
|
||||
@@ -6122,7 +6122,10 @@ mod tests {
|
||||
));
|
||||
let payload = direct_turn_terminal_write(None, Some(&failed)).expect("真失败必须写终态");
|
||||
let error = payload.expect_err("失败终态必须带载荷");
|
||||
assert_eq!(error.wire_kind(), Some("transport-failed"));
|
||||
assert_eq!(
|
||||
error.wire_kind(),
|
||||
Some(crate::agent::DirectTurnFailureKind::TransportFailed)
|
||||
);
|
||||
|
||||
// 解析失败(structured output 非法)也走这条投影:以前解析排在终态**之后**,
|
||||
// 于是这条 Err 谁都不接——终态已经是 `completed`,兜底的 `finish_if_unfinished`
|
||||
@@ -6134,7 +6137,7 @@ mod tests {
|
||||
direct_turn_terminal_write(None, Some(&parse_failed)).expect("解析失败必须写终态");
|
||||
assert_eq!(
|
||||
payload.expect_err("解析失败必须带载荷").wire_kind(),
|
||||
Some("model-failed")
|
||||
Some(crate::agent::DirectTurnFailureKind::ModelFailed)
|
||||
);
|
||||
|
||||
let payload =
|
||||
|
||||
@@ -874,7 +874,10 @@ mod tests {
|
||||
manager.append(
|
||||
"thread-1",
|
||||
DirectThreadEvent::turn_completed_failed(
|
||||
crate::agent::DirectTurnFailure::new("host-dropped", "回合宿主任务提前结束"),
|
||||
crate::agent::DirectTurnFailure::new(
|
||||
crate::agent::DirectTurnFailureKind::HostDropped,
|
||||
"回合宿主任务提前结束",
|
||||
),
|
||||
FIXED_AT_MS,
|
||||
),
|
||||
);
|
||||
@@ -884,7 +887,8 @@ mod tests {
|
||||
bootstrap.events.as_slice(),
|
||||
[DirectThreadEvent::TurnCompleted { status, failure, at, .. }]
|
||||
if status == "failed"
|
||||
&& failure.as_ref().is_some_and(|failure| failure.kind == "host-dropped")
|
||||
&& failure.as_ref().is_some_and(|failure| failure.kind
|
||||
== crate::agent::DirectTurnFailureKind::HostDropped)
|
||||
&& *at == Some(FIXED_AT_MS)
|
||||
));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
use crate::agent::redact_secret_tokens;
|
||||
use crate::agent::sanitize_error_context;
|
||||
use crate::agent::DirectTurnFailureKind;
|
||||
use crate::redact_absolute_path_tokens;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -219,17 +220,17 @@ impl DirectThreadRequestKind {
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
|
||||
pub(crate) struct DirectTurnFailure {
|
||||
/// 稳定失败分类:`timeout` / `model-failed` / `transport-failed` / `request-rejected` /
|
||||
/// `environment-not-ready` / `host-dropped`。
|
||||
pub(crate) kind: String,
|
||||
/// 稳定失败分类;取值表就是 [`DirectTurnFailureKind`],投影只走
|
||||
/// [`DirectTurnError::wire_kind`]。
|
||||
pub(crate) kind: DirectTurnFailureKind,
|
||||
/// 脱敏 + 截断后的失败原因。
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
||||
impl DirectTurnFailure {
|
||||
pub(crate) fn new(kind: impl Into<String>, message: impl Into<String>) -> Self {
|
||||
pub(crate) fn new(kind: DirectTurnFailureKind, message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
kind: kind.into(),
|
||||
kind,
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
@@ -1416,14 +1417,17 @@ mod tests {
|
||||
#[test]
|
||||
fn turn_completed_carries_failure_payload_only_when_failed() {
|
||||
let failed = DirectThreadEvent::turn_completed_failed(
|
||||
DirectTurnFailure::new("model-failed", "上游返回 500:模型服务暂不可用"),
|
||||
DirectTurnFailure::new(
|
||||
crate::agent::DirectTurnFailureKind::ModelFailed,
|
||||
"上游返回 500:模型服务暂不可用",
|
||||
),
|
||||
4_000,
|
||||
)
|
||||
.with_user_item_id(Some("direct-codex:turn-1:user"));
|
||||
assert_eq!(
|
||||
failed.failure(),
|
||||
Some(&DirectTurnFailure::new(
|
||||
"model-failed",
|
||||
crate::agent::DirectTurnFailureKind::ModelFailed,
|
||||
"上游返回 500:模型服务暂不可用"
|
||||
))
|
||||
);
|
||||
|
||||
@@ -85,7 +85,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::agent::{
|
||||
consume_direct_thread, direct_thread_turn_is_active, subscribe_direct_thread,
|
||||
DirectThreadEvent, DirectTurnFailure,
|
||||
DirectThreadEvent, DirectTurnFailure, DirectTurnFailureKind,
|
||||
};
|
||||
|
||||
/// 订阅并把 bootstrap 拿掉:之后的 `consume` 只返回这次订阅之后产生的事件。
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
failure: Some(failure),
|
||||
..
|
||||
} => {
|
||||
assert_eq!(failure.kind, "host-dropped");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::HostDropped);
|
||||
assert_eq!(user_item_id.as_deref(), Some("u-1"));
|
||||
}
|
||||
other => panic!("expected a failed terminal, got {other:?}"),
|
||||
@@ -189,7 +189,10 @@ mod tests {
|
||||
|
||||
// 深层收口:真正跑完这一轮的代码算出来的终态。
|
||||
let deep = DirectThreadEvent::turn_completed_failed(
|
||||
DirectTurnFailure::new("timeout".to_string(), "等待模型回执超时".to_string()),
|
||||
DirectTurnFailure::new(
|
||||
DirectTurnFailureKind::Timeout,
|
||||
"等待模型回执超时".to_string(),
|
||||
),
|
||||
2_000,
|
||||
)
|
||||
.with_user_item_id(Some("u-1"));
|
||||
@@ -206,7 +209,10 @@ mod tests {
|
||||
assert_eq!(completed.len(), 1, "一轮只许有一条终态:{events:?}");
|
||||
match completed[0] {
|
||||
DirectThreadEvent::TurnCompleted { failure, .. } => {
|
||||
assert_eq!(failure.as_ref().map(|f| f.kind.as_str()), Some("timeout"));
|
||||
assert_eq!(
|
||||
failure.as_ref().map(|f| f.kind),
|
||||
Some(DirectTurnFailureKind::Timeout)
|
||||
);
|
||||
}
|
||||
other => panic!("expected a terminal, got {other:?}"),
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
use std::fmt;
|
||||
|
||||
use platform_llm::LlmError;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
|
||||
/// app-server 把"原生失败分类"写进原因文本时的结构化前缀。
|
||||
@@ -210,6 +210,31 @@ impl DirectCodexNativeKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// 失败载荷 `DirectTurnFailure.kind` 的唯一取值表。
|
||||
///
|
||||
/// 只给界面选语气,不参与流程分支(宿主与前端两侧都不得按它分流);载荷里的 `kind` 只能从这里
|
||||
/// 投影(见 [`DirectTurnError::wire_kind`]),别在别处再拼字符串。线上取值由 `kebab-case` 给出,
|
||||
/// 枚举成员名与线上取值一一对应,改名即改协议。
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, TS)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[ts(export, export_to = concat!(env!("CARGO_MANIFEST_DIR"), "/../src/view/project-development/chat/generated/"))]
|
||||
pub(crate) enum DirectTurnFailureKind {
|
||||
/// 等待模型回执撞上上限。
|
||||
Timeout,
|
||||
/// 模型 / 上游 / 交付阶段的失败(说不出更细分类的也归这里)。
|
||||
ModelFailed,
|
||||
/// 执行通道断开。
|
||||
TransportFailed,
|
||||
/// app-server 或上游明确拒绝了这次请求。
|
||||
RequestRejected,
|
||||
/// 接单之后的连接 / 配置 / 凭据 / 脚手架未就绪(不是模型的错,界面语气也不同)。
|
||||
EnvironmentNotReady,
|
||||
/// app-server 单方面把这一轮判成中断(用户没要求停止、宿主也没在收尾)。
|
||||
TurnInterrupted,
|
||||
/// 宿主任务提前结束(panic / 被取消):说不出原因的那一种兜底。
|
||||
HostDropped,
|
||||
}
|
||||
|
||||
/// 模型调用失败(app-server 一次 `turn` 的结果)的分类,跟着拒单 / 失败载荷一起给前端。
|
||||
///
|
||||
/// 每个变体对应平台层 `LlmError` 的一个分支,于是 [`DirectTurnError::wire_kind`] 的取值与改造前
|
||||
@@ -255,17 +280,17 @@ pub(crate) enum DirectModelCallKind {
|
||||
|
||||
impl DirectModelCallKind {
|
||||
/// 事件失败载荷里的稳定分类(只影响界面语气,前端不得拿它做流程分支)。
|
||||
fn wire_kind(&self) -> &'static str {
|
||||
fn wire_kind(&self) -> DirectTurnFailureKind {
|
||||
match self {
|
||||
Self::ResponseTimedOut { .. } => "timeout",
|
||||
Self::ResponseTimedOut { .. } => DirectTurnFailureKind::Timeout,
|
||||
Self::ConnectionFailed { .. } | Self::TransportBroken | Self::StreamUnavailable => {
|
||||
"transport-failed"
|
||||
DirectTurnFailureKind::TransportFailed
|
||||
}
|
||||
Self::RequestRejected { .. } => "request-rejected",
|
||||
Self::RequestRejected { .. } => DirectTurnFailureKind::RequestRejected,
|
||||
Self::UpstreamFailed { .. }
|
||||
| Self::PaidCreditsInsufficient
|
||||
| Self::EmptyResponse
|
||||
| Self::PayloadInvalid { .. } => "model-failed",
|
||||
| Self::PayloadInvalid { .. } => DirectTurnFailureKind::ModelFailed,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -475,15 +500,17 @@ impl DirectTurnError {
|
||||
}
|
||||
|
||||
/// 事件失败载荷里的稳定分类。调用级拒绝与控制流不会走到这里。
|
||||
pub(crate) fn wire_kind(&self) -> Option<&'static str> {
|
||||
pub(crate) fn wire_kind(&self) -> Option<DirectTurnFailureKind> {
|
||||
match self {
|
||||
Self::ModelCallFailed { kind, .. } => Some(kind.wire_kind()),
|
||||
Self::TransportClosed { .. } => Some("transport-failed"),
|
||||
Self::TransportClosed { .. } => Some(DirectTurnFailureKind::TransportFailed),
|
||||
// 接了单才失败的连接 / 配置 / 凭据类原因:它们不是模型的问题,界面语气也不一样。
|
||||
Self::EnvironmentNotReady { .. } => Some("environment-not-ready"),
|
||||
Self::TimedOut { .. } => Some("timeout"),
|
||||
Self::TurnInterrupted { .. } => Some("turn-interrupted"),
|
||||
Self::TurnFailed { .. } | Self::TurnFailedUnclassified { .. } => Some("model-failed"),
|
||||
Self::EnvironmentNotReady { .. } => Some(DirectTurnFailureKind::EnvironmentNotReady),
|
||||
Self::TimedOut { .. } => Some(DirectTurnFailureKind::Timeout),
|
||||
Self::TurnInterrupted { .. } => Some(DirectTurnFailureKind::TurnInterrupted),
|
||||
Self::TurnFailed { .. } | Self::TurnFailedUnclassified { .. } => {
|
||||
Some(DirectTurnFailureKind::ModelFailed)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1043,36 +1070,45 @@ mod tests {
|
||||
#[test]
|
||||
fn wire_kind_matches_the_previous_llm_error_classification() {
|
||||
let cases = [
|
||||
(LlmError::Timeout { attempts: 3 }, "timeout"),
|
||||
(
|
||||
LlmError::Timeout { attempts: 3 },
|
||||
DirectTurnFailureKind::Timeout,
|
||||
),
|
||||
(
|
||||
LlmError::InvalidConfig("missing key".into()),
|
||||
"request-rejected",
|
||||
DirectTurnFailureKind::RequestRejected,
|
||||
),
|
||||
(
|
||||
LlmError::InvalidRequest("codex-app-server-error:context-window-exceeded".into()),
|
||||
"request-rejected",
|
||||
DirectTurnFailureKind::RequestRejected,
|
||||
),
|
||||
(
|
||||
LlmError::Connectivity {
|
||||
attempts: 2,
|
||||
message: "Codex app-server 连接失败".into(),
|
||||
},
|
||||
"transport-failed",
|
||||
DirectTurnFailureKind::TransportFailed,
|
||||
),
|
||||
(
|
||||
LlmError::Transport("DirectProject 收尾历史失败".into()),
|
||||
"transport-failed",
|
||||
DirectTurnFailureKind::TransportFailed,
|
||||
),
|
||||
(
|
||||
LlmError::StreamUnavailable,
|
||||
DirectTurnFailureKind::TransportFailed,
|
||||
),
|
||||
(LlmError::StreamUnavailable, "transport-failed"),
|
||||
(
|
||||
LlmError::Upstream {
|
||||
status_code: 502,
|
||||
message: "上游 502".into(),
|
||||
},
|
||||
"model-failed",
|
||||
DirectTurnFailureKind::ModelFailed,
|
||||
),
|
||||
(LlmError::EmptyResponse, DirectTurnFailureKind::ModelFailed),
|
||||
(
|
||||
LlmError::Deserialize("bad payload".into()),
|
||||
DirectTurnFailureKind::ModelFailed,
|
||||
),
|
||||
(LlmError::EmptyResponse, "model-failed"),
|
||||
(LlmError::Deserialize("bad payload".into()), "model-failed"),
|
||||
];
|
||||
for (error, expected) in cases {
|
||||
let projected = DirectTurnError::from_model_call(&error);
|
||||
@@ -1081,6 +1117,32 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// 载荷 `kind` 的线上取值只有这一份:改枚举成员名就是改协议,必须在这一条用例上先失败。
|
||||
/// 新增变体时在这里补一行(生成绑定 `DirectTurnFailureKind.ts` 会同步出现新取值)。
|
||||
#[test]
|
||||
fn failure_kind_wire_values_are_stable() {
|
||||
let cases = [
|
||||
(DirectTurnFailureKind::Timeout, "timeout"),
|
||||
(DirectTurnFailureKind::ModelFailed, "model-failed"),
|
||||
(DirectTurnFailureKind::TransportFailed, "transport-failed"),
|
||||
(DirectTurnFailureKind::RequestRejected, "request-rejected"),
|
||||
(
|
||||
DirectTurnFailureKind::EnvironmentNotReady,
|
||||
"environment-not-ready",
|
||||
),
|
||||
(DirectTurnFailureKind::TurnInterrupted, "turn-interrupted"),
|
||||
(DirectTurnFailureKind::HostDropped, "host-dropped"),
|
||||
];
|
||||
for (kind, wire) in cases {
|
||||
assert_eq!(serde_json::to_value(kind).expect("serialize"), wire);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<DirectTurnFailureKind>(&format!("\"{wire}\""))
|
||||
.expect("deserialize"),
|
||||
kind
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 原生分类被读成 typed 值:未知分类不吞掉,落 `Other`。
|
||||
#[test]
|
||||
fn native_kind_is_read_from_the_structured_prefix_only() {
|
||||
|
||||
@@ -15,14 +15,16 @@
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use super::{redact_agent_runtime_error, DirectThreadEvent, DirectTurnError, DirectTurnFailure};
|
||||
use super::{
|
||||
redact_agent_runtime_error, DirectThreadEvent, DirectTurnError, DirectTurnFailure,
|
||||
DirectTurnFailureKind,
|
||||
};
|
||||
|
||||
/// `turn.completed.failure.message` 的字符上限:与本地错误文案同一档——够说清原因,又不至于
|
||||
/// 把整段上游报文塞进事件队列。
|
||||
const DIRECT_TURN_FAILURE_MESSAGE_MAX_CHARS: usize = 600;
|
||||
|
||||
/// 宿主任务提前结束(panic / future 被丢弃 / 终态之前的早退)时的分类与文案。
|
||||
const DIRECT_TURN_FAILURE_HOST_DROPPED_KIND: &str = "host-dropped";
|
||||
const DIRECT_TURN_FAILURE_HOST_DROPPED_MESSAGE: &str =
|
||||
"陶泥儿回合的宿主任务提前结束(崩溃或任务被取消),本轮已按失败收口,请重试。";
|
||||
|
||||
@@ -91,7 +93,9 @@ impl DirectTurnTerminal {
|
||||
Self {
|
||||
status: "failed".to_string(),
|
||||
failure: Some(DirectTurnFailure::new(
|
||||
failure.wire_kind().unwrap_or("model-failed").to_string(),
|
||||
failure
|
||||
.wire_kind()
|
||||
.unwrap_or(DirectTurnFailureKind::ModelFailed),
|
||||
redact_agent_runtime_error(
|
||||
history_root,
|
||||
&failure.to_string(),
|
||||
@@ -108,7 +112,7 @@ impl DirectTurnTerminal {
|
||||
Self {
|
||||
status: "failed".to_string(),
|
||||
failure: Some(DirectTurnFailure::new(
|
||||
DIRECT_TURN_FAILURE_HOST_DROPPED_KIND.to_string(),
|
||||
DirectTurnFailureKind::HostDropped,
|
||||
DIRECT_TURN_FAILURE_HOST_DROPPED_MESSAGE.to_string(),
|
||||
)),
|
||||
}
|
||||
@@ -146,7 +150,7 @@ mod tests {
|
||||
.failure
|
||||
.expect("transport error must fail the turn");
|
||||
assert_eq!(terminal.status, "failed");
|
||||
assert_eq!(failure.kind, "transport-failed");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::TransportFailed);
|
||||
assert!(failure.message.contains("收尾历史失败"));
|
||||
}
|
||||
|
||||
@@ -162,7 +166,7 @@ mod tests {
|
||||
let terminal = direct_turn_terminal("interrupted", Err(error), None, &history_root());
|
||||
let failure = terminal.failure.expect("native failure must fail the turn");
|
||||
assert_eq!(terminal.status, "failed");
|
||||
assert_eq!(failure.kind, "request-rejected");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::RequestRejected);
|
||||
assert_eq!(
|
||||
failure.message,
|
||||
"codex-app-server-error:context-window-exceeded"
|
||||
@@ -178,7 +182,7 @@ mod tests {
|
||||
let failure = terminal
|
||||
.failure
|
||||
.expect("unreadable ledger must fail the turn");
|
||||
assert_eq!(failure.kind, "model-failed");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::ModelFailed);
|
||||
assert_eq!(failure.message, "报告");
|
||||
}
|
||||
|
||||
@@ -198,7 +202,7 @@ stderrClass=nonempty;stderrBytes=1000";
|
||||
);
|
||||
let failure = terminal.failure.expect("host fact must fail the turn");
|
||||
assert_eq!(terminal.status, "failed");
|
||||
assert_eq!(failure.kind, "transport-failed");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::TransportFailed);
|
||||
assert!(failure.message.contains("SIGKILL"));
|
||||
assert!(!failure.message.contains("正在核对自有子进程"));
|
||||
|
||||
@@ -216,7 +220,7 @@ stderrClass=nonempty;stderrBytes=1000";
|
||||
&history_root(),
|
||||
);
|
||||
let failure = terminal.failure.expect("host fact must fail the turn");
|
||||
assert_eq!(failure.kind, "turn-interrupted");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::TurnInterrupted);
|
||||
assert!(failure.message.contains("本轮模型执行被中断"));
|
||||
}
|
||||
|
||||
@@ -230,8 +234,8 @@ stderrClass=nonempty;stderrBytes=1000";
|
||||
let failing = direct_turn_terminal("interrupted", Err(error), None, &history_root());
|
||||
let event = failing.event(2_000, Some("direct-codex:turn-1:user"));
|
||||
assert_eq!(
|
||||
event.failure().map(|failure| failure.kind.as_str()),
|
||||
Some("model-failed")
|
||||
event.failure().map(|failure| failure.kind),
|
||||
Some(DirectTurnFailureKind::ModelFailed)
|
||||
);
|
||||
assert_eq!(event.user_item_id(), Some("direct-codex:turn-1:user"));
|
||||
assert_eq!(event.at(), Some(2_000));
|
||||
@@ -251,7 +255,7 @@ stderrClass=nonempty;stderrBytes=1000";
|
||||
let terminal = DirectTurnTerminal::host_dropped();
|
||||
assert_eq!(terminal.status, "failed");
|
||||
let failure = terminal.failure.expect("host-dropped must fail the turn");
|
||||
assert_eq!(failure.kind, "host-dropped");
|
||||
assert_eq!(failure.kind, DirectTurnFailureKind::HostDropped);
|
||||
assert!(!failure.message.trim().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -1,4 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { DirectTurnFailureKind } from './DirectTurnFailureKind';
|
||||
|
||||
/**
|
||||
* 失败终态的可下发载荷(`turn.completed.status == "failed"` 时必有,其余终态没有)。
|
||||
@@ -8,10 +9,10 @@
|
||||
*/
|
||||
export type DirectTurnFailure = {
|
||||
/**
|
||||
* 稳定失败分类:`timeout` / `model-failed` / `transport-failed` / `request-rejected` /
|
||||
* `environment-not-ready` / `host-dropped`。
|
||||
* 稳定失败分类;取值表就是 [`DirectTurnFailureKind`],投影只走
|
||||
* [`DirectTurnError::wire_kind`]。
|
||||
*/
|
||||
kind: string;
|
||||
kind: DirectTurnFailureKind;
|
||||
/**
|
||||
* 脱敏 + 截断后的失败原因。
|
||||
*/
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* 失败载荷 `DirectTurnFailure.kind` 的唯一取值表。
|
||||
*
|
||||
* 只给界面选语气,不参与流程分支(宿主与前端两侧都不得按它分流);载荷里的 `kind` 只能从这里
|
||||
* 投影(见 [`DirectTurnError::wire_kind`]),别在别处再拼字符串。线上取值由 `kebab-case` 给出,
|
||||
* 枚举成员名与线上取值一一对应,改名即改协议。
|
||||
*/
|
||||
export type DirectTurnFailureKind =
|
||||
| 'timeout'
|
||||
| 'model-failed'
|
||||
| 'transport-failed'
|
||||
| 'request-rejected'
|
||||
| 'environment-not-ready'
|
||||
| 'turn-interrupted'
|
||||
| 'host-dropped';
|
||||
Reference in New Issue
Block a user