DirectProject 聊天事件改用 ts-rs 导出的 tagged enum 并删掉 turn id
Project CI / AI game creator shell Rust shard 1/4 (pull_request) Failing after 5m11s
Project CI / AI game creator shell Rust shard 2/4 (pull_request) Successful in 4m51s
Project CI / AI game creator shell Rust smoke (pull_request) Successful in 1m57s
Project CI / Backend tests (pull_request) Failing after 11s
Project CI / AI game creator shell Rust shard 3/4 (pull_request) Successful in 4m50s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m2s
Project CI / Repository checks (pull_request) Failing after 15s
Project CI / AI game creator shell Rust shard 4/4 (pull_request) Successful in 4m14s
Project CI / Native shell tests (pull_request) Failing after 2m13s
Project CI / AI game creator shell web tests (pull_request) Failing after 46s
Project CI / Frontend tests (pull_request) Failing after 3m32s

- 新增 direct_thread_wire.rs:DirectThreadItem / DirectThreadEvent / 订阅与历史切片全部改成 ts-rs 导出的 tagged enum,取代原大而全的可空结构体
- 删除 direct_thread_raw_item.rs,模块注册与直通引用改到 direct_thread_wire
- 条目身份只看一个 itemId:工具条目的第二个 id 在 Rust 边界归一,不再对外暴露
- 删除 DirectProject 聊天事件里的 turn id:生命周期用无载荷的 turn.started / turn.completed{status} 表示
- append 直接接收 DirectThreadEvent 并返回同一事件,队列内部自算 seq
- 请求事件改为携带 DirectThreadRequestKind,去掉字符串中转
- 思考增量走 ReasoningDelta 通道,与正文增量共用 item.delta
- at 用 #[ts(as = "f64")] 对齐 Tauri JSON 通道的 number
- 用 cargo test export_bindings 重新生成 project-workspace/generated 绑定
This commit is contained in:
2026-09-16 19:38:31 +08:00
parent 09ad0073fe
commit 2748468d12
21 changed files with 1315 additions and 1018 deletions
@@ -21,7 +21,7 @@ mod direct_project_history;
mod direct_project_turn_history;
mod direct_runtime;
mod direct_thread_manager;
mod direct_thread_raw_item;
mod direct_thread_wire;
mod direct_tool_bridge;
mod direct_tool_calls;
mod direct_tools_mcp;
@@ -56,7 +56,7 @@ pub(crate) use direct_project_history::*;
pub(crate) use direct_project_turn_history::*;
pub(crate) use direct_runtime::*;
pub(crate) use direct_thread_manager::*;
pub(crate) use direct_thread_raw_item::*;
pub(crate) use direct_thread_wire::*;
pub(crate) use direct_tool_bridge::*;
pub(crate) use direct_tool_calls::*;
pub(crate) use direct_tools_mcp::*;
@@ -564,6 +564,17 @@ enum CodexTurnEvent {
item_id: String,
delta: String,
},
/// 思考正文增量:app-server `item/reasoning/summaryTextDelta` 的明文思考文本。
///
/// `item/reasoning/summaryTextDelta`core `ReasoningContentDelta`)与
/// `item/reasoning/textDelta`core `ReasoningRawContentDelta`)都进这条通道:前者是
/// reasoning item 的 `summary`,后者是它的 `content`,两段文本都随 `item/completed`
/// 落进 `project.jsonl`、此前也已经在完成时展示给用户。plan 文本与命令输出仍然只降级为
/// 活动状态,不下发正文。
ReasoningDelta {
item_id: String,
delta: String,
},
IntermediateText(String),
Activity(&'static str),
Item {
@@ -571,7 +582,7 @@ enum CodexTurnEvent {
params: serde_json::Value,
},
Request {
event_type: &'static str,
kind: DirectThreadRequestKind,
params: serde_json::Value,
},
RawItem(serde_json::Value),
@@ -741,40 +752,14 @@ fn direct_codex_safe_activity_for_item_value(item: &serde_json::Value) -> &'stat
direct_codex_safe_activity_for_item(item_type)
}
/// Project an app-server item into the small public payload carried by the
/// DirectProject event queue. Full item contents are persisted in JSONL and
/// must not be forwarded through the runtime event stream.
/// 运行态事件载荷:与历史切片同形的脱敏原始条目;拿不到条目时给空对象。
/// 运行态事件载荷:与历史切片同形的脱敏原始条目;拿不到身份或类型就整条跳过。
///
/// 这里不生成工具卡片形状:标题、折叠摘要和可见性都是前端投影的职责。
fn direct_thread_raw_item_payload(
fn direct_thread_event_item(
root: &std::path::Path,
item: &serde_json::Value,
turn_id: Option<&str>,
completed: bool,
now_ms: u64,
) -> serde_json::Value {
direct_thread_raw_item_from_value(root, item, turn_id, completed, now_ms)
.and_then(|raw| serde_json::to_value(raw).ok())
.unwrap_or_else(|| serde_json::json!({}))
}
fn direct_thread_item_id(item: &serde_json::Value) -> Option<String> {
item.get("id")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_string)
}
/// 原始 response item 的调用 id:工具条目的 app-server `itemId` 就是这个值,
/// 所以它是两个 id 空间唯一的对齐点。
fn direct_thread_item_call_id(item: &serde_json::Value) -> Option<String> {
item.get("call_id")
.or_else(|| item.get("callId"))
.and_then(serde_json::Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
) -> Option<DirectThreadItem> {
direct_thread_item_from_value(root, item, direct_tool_call_now_ms())
}
fn direct_codex_command_is_game_verification(command: &str) -> bool {
@@ -991,19 +976,21 @@ fn direct_codex_safe_activity_for_notification(method: &str) -> Option<&'static
}
}
fn direct_codex_request_event_type(method: &str) -> Option<&'static str> {
fn direct_codex_request_event_type(method: &str) -> Option<DirectThreadRequestKind> {
match method {
"item/fileChange/requestApproval"
| "item/commandExecution/requestApproval"
| "item/permissions/requestApproval" => Some("approval.requested"),
"item/tool/requestUserInput" | "item/mcpToolCall/requestUserInput" => Some("ask.requested"),
| "item/permissions/requestApproval" => Some(DirectThreadRequestKind::ApprovalRequested),
"item/tool/requestUserInput" | "item/mcpToolCall/requestUserInput" => {
Some(DirectThreadRequestKind::AskRequested)
}
_ => None,
}
}
fn direct_codex_resolution_event_type(method: &str) -> Option<&'static str> {
fn direct_codex_resolution_event_type(method: &str) -> Option<DirectThreadRequestKind> {
match method {
"serverRequest/resolved" => Some("request.resolved"),
"serverRequest/resolved" => Some(DirectThreadRequestKind::RequestResolved),
_ => None,
}
}
@@ -1076,6 +1063,33 @@ fn direct_codex_notification_event(
intermediate_text: Option<String>,
safe_activity: Option<&'static str>,
) -> Option<CodexTurnEvent> {
// 思考正文走独立通道,交给 DirectProject 的运行态事件;它不因为
// "preparing 活动" 的降级规则被丢掉,否则界面只能等 item/completed 才看到思考。
//
// 两条通知都下发正文,不下发活动文本:
// - `item/reasoning/summaryTextDelta`core `ReasoningContentDelta`)→ reasoning item 的 `summary`
// - `item/reasoning/textDelta`core `ReasoningRawContentDelta`)→ reasoning item 的 `content`
// 正是 `project.jsonl` 里保存、并在此前 `item/completed` 已经展示给用户的同一段文本。
// 因此这里只是把"完成时才看到"提前为"边生成边看到",没有放宽可见文本的范围;
// 未识别的 plan 文本与命令输出仍然只降级为活动状态,不下发正文。
if matches!(
method,
"item/reasoning/summaryTextDelta" | "item/reasoning/textDelta"
) {
return params
.get("delta")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(|delta| CodexTurnEvent::ReasoningDelta {
item_id: params
.get("itemId")
.and_then(serde_json::Value::as_str)
.filter(|value| !value.is_empty())
.map(str::to_string)
.unwrap_or_else(|| "direct-missing-item".to_string()),
delta: delta.to_string(),
});
}
let (activity, intermediate_text) = match (&intermediate_text, safe_activity) {
(Some(_), Some(activity)) if activity == "preparing" => (Some(activity), None),
_ => (safe_activity, intermediate_text),
@@ -2943,19 +2957,7 @@ impl CodexAppServerConnection {
turn_start_guard.armed = false;
let direct_thread_id = history_root.to_string_lossy().into_owned();
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "turn.started".to_string(),
turn_id: turn_id.clone(),
item_id: None,
call_id: None,
payload: serde_json::json!({
"threadId": thread_id,
"turnId": turn_id,
}),
},
);
append_direct_thread_event(&direct_thread_id, DirectThreadEvent::turn_started());
}
let mut receiver = self.register_turn(&turn_id).await;
let mut direct_project_history = DirectProjectHistoryAccumulator::default();
@@ -3010,18 +3012,13 @@ impl CodexAppServerConnection {
direct_project_history.observe_delta(&item_id, &delta);
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "item.delta".to_string(),
turn_id: turn_id.clone(),
item_id: Some(item_id.clone()),
call_id: None,
// 事件自足:增量也要说明它是哪类条目的正文,
// 前端 reducer 不允许靠猜 itemId 的来源决定 kind。
payload: serde_json::json!({
"delta": delta.clone(),
"kind": "message",
}),
},
// 事件自足:增量自带 item 身份与正文类别(正文 / 思考),
// 前端 reducer 不允许靠猜 itemId 的来源决定 kind。
DirectThreadEvent::item_delta(
item_id.clone(),
DirectThreadDeltaKind::Message,
delta.clone(),
),
);
}
streamed_text.push_str(&delta);
@@ -3052,6 +3049,18 @@ impl CodexAppServerConnection {
});
}
}
Some(CodexTurnEvent::ReasoningDelta { item_id, delta }) => {
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::item_delta(
item_id,
DirectThreadDeltaKind::Reasoning,
delta,
),
);
}
}
Some(CodexTurnEvent::IntermediateText(text)) => {
if let Some(observer) = direct_observer.as_deref_mut() {
observer(DirectCodexTurnObservation::IntermediateText(text));
@@ -3064,13 +3073,7 @@ impl CodexAppServerConnection {
"rawResponseItem/completed 缺少 item".to_string(),
));
}
let entry_payload = direct_thread_raw_item_payload(
history_root,
&item,
Some(turn_id.as_str()),
true,
direct_tool_call_now_ms(),
);
let entry_item = direct_thread_event_item(history_root, &item);
let history_root = history_root.to_path_buf();
let history_item = item.clone();
tokio::task::spawn_blocking(move || {
@@ -3084,20 +3087,15 @@ impl CodexAppServerConnection {
})?
.map_err(platform_llm::LlmError::InvalidRequest)?;
direct_project_history.complete_item(&item);
let item_id = direct_thread_item_id(&item);
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "item.completed".to_string(),
turn_id: turn_id.clone(),
item_id,
call_id: direct_thread_item_call_id(&item),
payload: entry_payload,
},
);
if let Some(entry_item) = entry_item {
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::item_completed(entry_item),
);
}
}
}
Some(CodexTurnEvent::Request { event_type, params }) => {
Some(CodexTurnEvent::Request { kind, params }) => {
if self.inner.workspace_mode == CodexAppServerWorkspaceMode::DirectProject {
let request_id = params
.get("requestId")
@@ -3107,15 +3105,7 @@ impl CodexAppServerConnection {
.map(str::to_string);
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: event_type.to_string(),
turn_id: turn_id.clone(),
item_id: None,
call_id: None,
payload: request_id
.map(|id| serde_json::json!({ "requestId": id }))
.unwrap_or_else(|| serde_json::json!({})),
},
DirectThreadEvent::request(kind, request_id),
);
}
}
@@ -3226,23 +3216,14 @@ impl CodexAppServerConnection {
&& self.inner.workspace_mode
== CodexAppServerWorkspaceMode::DirectProject
{
let item_id = direct_thread_item_id(item);
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "item.started".to_string(),
turn_id: turn_id.clone(),
item_id,
call_id: None,
payload: direct_thread_raw_item_payload(
history_root,
item,
Some(turn_id.as_str()),
completed,
direct_tool_call_now_ms(),
),
},
);
if let Some(entry_item) =
direct_thread_event_item(history_root, item)
{
append_direct_thread_event(
&direct_thread_id,
DirectThreadEvent::item_started(entry_item),
);
}
}
}
}
@@ -3284,13 +3265,7 @@ impl CodexAppServerConnection {
{
append_direct_thread_event(
&direct_thread_id,
DirectThreadRawEventDraft {
event_type: "turn.completed".to_string(),
turn_id: turn_id.clone(),
item_id: None,
call_id: None,
payload: serde_json::json!({ "status": status }),
},
DirectThreadEvent::turn_completed(status.to_string()),
);
}
match status {
@@ -3985,8 +3960,8 @@ async fn read_game_creator_codex_app_server_stdout(
continue;
}
}
let event = if let Some(event_type) = direct_codex_resolution_event_type(method) {
CodexTurnEvent::Request { event_type, params }
let event = if let Some(kind) = direct_codex_resolution_event_type(method) {
CodexTurnEvent::Request { kind, params }
} else if let Some(activity) = safe_activity {
// Preparing notifications may carry private plan/reasoning text;
// expose only the safe activity category. Other categories may
@@ -4038,8 +4013,8 @@ async fn read_game_creator_codex_app_server_stdout(
),
method if direct_codex_request_event_type(method).is_some() => {
CodexTurnEvent::Request {
event_type: direct_codex_request_event_type(method)
.expect("request event type checked above"),
kind: direct_codex_request_event_type(method)
.expect("request kind checked above"),
params,
}
}
@@ -4550,15 +4525,10 @@ mod tests {
"arguments": { "path": "game/index.html", "token": "secret" },
"result": { "content": "large output" }
});
assert_eq!(direct_thread_item_id(&item).as_deref(), Some("item-1"));
// 运行态事件必须自足:载荷是脱敏原始条目,前端不需要再按 itemId 取快照。
let payload = direct_thread_raw_item_payload(
std::path::Path::new("."),
&item,
Some("turn-1"),
false,
1000,
);
let projected = direct_thread_event_item(std::path::Path::new("."), &item).expect("item");
assert_eq!(projected.item_id(), "item-1");
let payload = serde_json::to_value(&projected).expect("payload");
assert_eq!(
payload.get("itemType").and_then(serde_json::Value::as_str),
Some("mcpToolCall")
@@ -4567,10 +4537,6 @@ mod tests {
payload.get("itemId").and_then(serde_json::Value::as_str),
Some("item-1")
);
assert_eq!(
payload.get("turnId").and_then(serde_json::Value::as_str),
Some("turn-1")
);
// 卡片标题 / 折叠摘要 / kind 属于前端投影:载荷里不得出现这些 UI 语义。
assert!(payload.get("toolCall").is_none(), "{payload}");
assert!(payload.get("title").is_none(), "{payload}");
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -8,7 +8,7 @@
//! 为什么不复用 `project.jsonl`:那条链路的回读只投影 `role ∈ {user, assistant}` 的
//! 文本条目,而且会被注入 Codex 上下文。往里面塞新形状既装不下,又有污染模型上下文的风险。
use super::direct_thread_raw_item::sanitize_detail_text;
use super::direct_thread_wire::sanitize_detail_text;
use crate::config::{prepare_game_creator_private_path_for_read, write_game_creator_private_file};
use crate::project::{enforce_project_permission_policy, project_append_lock_for};
use serde::{Deserialize, Serialize};
@@ -5388,7 +5388,7 @@ pub(crate) async fn read_direct_project_history_slice(
.and_then(|item| item.get("id"))
.and_then(serde_json::Value::as_str)
.map(str::to_string);
let items = direct_thread_raw_items_from_history(root, &items, |item| {
let items = direct_thread_items_from_history(root, &items, |item| {
item.get("id")
.and_then(serde_json::Value::as_str)
.and_then(|id| item_timestamps.get(id).copied())
@@ -1,5 +1,4 @@
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectCodexUserRuntimeRegionPart } from './DirectCodexUserRuntimeRegionPart';
export type DirectCodexUserContentPart =
@@ -1,5 +1,9 @@
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectCodexUserMessageItem } from './DirectCodexUserMessageItem';
export type DirectCodexUserItem = DirectCodexUserMessageItem;
/**
* DirectProject 本轮 user input 的唯一结构化入口。
*/
export type DirectCodexUserItem = {
type: 'message';
} & DirectCodexUserMessageItem;
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectCodexUserItem } from './DirectCodexUserItem';
export type DirectCodexUserMessageEnvelope = { item: DirectCodexUserItem };
@@ -1,11 +1,9 @@
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectCodexUserContentPart } from './DirectCodexUserContentPart';
import type { DirectCodexUserRole } from './DirectCodexUserRole';
export type DirectCodexUserMessageItem = {
type: 'message';
role: DirectCodexUserRole;
content: DirectCodexUserContentPart[];
content: Array<DirectCodexUserContentPart>;
id: string;
};
@@ -1,3 +1,3 @@
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DirectCodexUserRole = 'user';
@@ -1,13 +1,13 @@
// This file is generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type DirectCodexUserRuntimeRegionPart = {
label: string;
runId?: string;
versionId?: string;
elementTag?: string;
elementRole?: string;
text?: string;
width?: number;
height?: number;
resourceIds: string[];
runId: string | null;
versionId: string | null;
elementTag: string | null;
elementRole: string | null;
text: string | null;
width: number | null;
height: number | null;
resourceIds: Array<string>;
};
@@ -0,0 +1,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectThreadEvent } from './DirectThreadEvent';
export type DirectThreadConsumeResult = { events: Array<DirectThreadEvent> };
@@ -0,0 +1,6 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 增量正文属于哪类条目。
*/
export type DirectThreadDeltaKind = 'message' | 'reasoning';
@@ -0,0 +1,30 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectThreadDeltaKind } from './DirectThreadDeltaKind';
import type { DirectThreadItem } from './DirectThreadItem';
import type { DirectThreadRequestKind } from './DirectThreadRequestKind';
/**
* Thread Manager 下发的运行态事件。
*
* 顺序由数组顺序给出(同一个 subscriber 的 `consume` 按队列顺序返回),因此不需要 `seq`:
* 游标是 Thread Manager 的内部事实,不下发。
*
* 事件不带回合身份:DirectProject 同一时刻只有一个回合在跑,"当前回合是否还在跑"由
* 生命周期事件在序列中的位置给出,`turn_id` 对前端没有任何额外信息。
*/
export type DirectThreadEvent =
| { type: 'turn.started' }
| { type: 'turn.completed'; status: string }
| { type: 'item.started'; item: DirectThreadItem }
| { type: 'item.completed'; item: DirectThreadItem }
| {
type: 'item.delta';
itemId: string;
kind: DirectThreadDeltaKind;
delta: string;
}
| {
type: 'request';
kind: DirectThreadRequestKind;
requestId: string | null;
};
@@ -0,0 +1,12 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 一条文件变更。
*/
export type DirectThreadFileChange = {
path: string;
/**
* `add` | `update` | `delete`
*/
kind: string;
};
@@ -0,0 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectThreadItem } from './DirectThreadItem';
export type DirectThreadHistorySlice = {
/**
* 脱敏条目,顺序即文件顺序;与运行态事件里的条目同形。
*/
items: Array<DirectThreadItem>;
hasMore: boolean;
/**
* 本次切片的原始 item id 锚点:无论切片里有没有可显示条目,分页都靠它向前。
*/
firstItemId: string | null;
};
@@ -0,0 +1,76 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectThreadFileChange } from './DirectThreadFileChange';
/**
* 聊天视图的输入条目:一条 Codex 原始条目的脱敏投影。
*
* `itemType` 就是 Codex 的原始类型,逐字透传;前端按它决定投影成消息、思考还是工具卡片。
* 未识别的类型走 [`DirectThreadItem::Other`]Rust 不替前端决定它是否可见。
*
* 条目上的 `at` 是只用于显示的毫秒时间戳:ts-rs 默认把 `u64` 映射成 `bigint`
* 而 Tauri 的 JSON 通道传过来的是 `number`,因此统一标 `#[ts(as = "f64")]` 对齐。
*/
export type DirectThreadItem =
| {
itemType: 'message';
/**
* 归一身份:全链路只有这一个 id。
*/
itemId: string;
/**
* 原始 role`user` / `assistant` / `system` / …);显示与否由前端判断。
*/
role: string;
text: string;
at: number;
}
| { itemType: 'reasoning'; itemId: string; text: string; at: number }
| {
itemType: 'function_call';
itemId: string;
name: string;
arguments: string;
at: number;
}
| {
itemType: 'function_call_output';
itemId: string;
output: string;
at: number;
}
| {
itemType: 'commandExecution';
itemId: string;
command: string;
output: string | null;
/**
* app-server 原始状态:`inProgress` / `completed` / `failed` / `declined` / …
*/
status: string | null;
exitCode: number | null;
at: number;
}
| {
itemType: 'fileChange';
itemId: string;
changes: Array<DirectThreadFileChange>;
at: number;
}
| {
itemType: 'mcpToolCall';
itemId: string;
tool: string;
arguments: string;
output: string | null;
status: string | null;
at: number;
}
| {
itemType: 'webSearch';
itemId: string;
query: string | null;
output: string | null;
at: number;
}
| { itemType: 'contextCompaction'; itemId: string; at: number }
| { itemType: 'other'; itemId: string; rawType: string; at: number };
@@ -0,0 +1,9 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
/**
* 审批 / 提问请求与解决:本轮只透传,不并入聊天状态。
*/
export type DirectThreadRequestKind =
| 'approval.requested'
| 'ask.requested'
| 'request.resolved';
@@ -0,0 +1,14 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
import type { DirectThreadEvent } from './DirectThreadEvent';
export type DirectThreadSubscriptionBootstrap = {
subscriptionId: string;
/**
* 首屏历史锚点:`project.jsonl` 里最后一条原始 item id。
*/
lastCompletedItemId: string | null;
/**
* 该 subscriber 此刻应当处理的运行态事件(游标已经在队尾)。
*/
events: Array<DirectThreadEvent>;
};