前端失败说明改由事件驱动:turn.completed.failure 落成本轮说明条目,命令返回只留横幅
- 新增 conversation/directTurnFailure.ts:失败说明条目的展示身份(本轮开口身份 + :failure)与可见文案(复用 projectRuntimeVisibleError)两条口径集中一处 - directThreadChat 的 turn.completed 分支读 failure 载荷:非空原因先落成本轮最后一条说明条目,再走同一个收口函数;失败不再是第二套生命周期 - useDirectProjectChatController 的失败分支不再写聊天气泡:聊天文案唯一来源是事件,命令返回只保留运行错误横幅(含详情 long detail)与诊断留痕 - 数据流、时序与投影注释同步:标注失败说明来自事件、本地通道只剩终止说明与壳层 announce
This commit is contained in:
+8
-9
@@ -103,7 +103,7 @@ export type DirectProjectChatControllerProps = {
|
||||
* 传输层(三条通道,前端各拉各的)
|
||||
* A 运行态:notify → invoke consume_direct_project_thread → events[](实时)
|
||||
* B 历史: invoke read_direct_project_history_slice → items[](分页,文件尾反向扫描)
|
||||
* C 本地: 前端自己造(乐观用户气泡、忙态、失败 / 终止说明)
|
||||
* C 本地: 前端自己造(乐观用户气泡、忙态、终止说明)
|
||||
* ▼
|
||||
* 前端
|
||||
* useDirectThreadChatSubscription reducer:A + B 进同一份 state(turnRunning / history / live)
|
||||
@@ -120,6 +120,8 @@ export type DirectProjectChatControllerProps = {
|
||||
* 经历史切片读取(首屏按 `lastCompletedItemId` 锚定)。
|
||||
* - 运行态事件(subscribe / consume / notify):进程内;`turn.started` / `turn.completed` 是原生回合
|
||||
* 活跃与否的**唯一**判据;可回收事件被回收后靠 `lifecycle_anchor` 保住最新一条生命周期事件。
|
||||
* 失败也走这条流:`turn.completed.failure` 自己带脱敏后的原因,reducer 把它落成本轮说明条目;
|
||||
* 命令返回那条通道只提供横幅与诊断,不再写聊天文案。
|
||||
* - 本地发送:只存在于本次会话,`projectPath` 变化即清空;乐观气泡与原生条目同身份
|
||||
* (`direct-codex:{clientTurnId}:user`),所以两边按**身份**合并,不按时间戳猜。
|
||||
*
|
||||
@@ -130,7 +132,8 @@ export type DirectProjectChatControllerProps = {
|
||||
* 3. notify → consume → `turn.started`:reducer 的 `turnRunning=true`、`turnStartedAt`、`turnUserItemId`。
|
||||
* 4. `item.completed`(本轮用户条目回显):同身份条目已在历史里就合并进去,否则进 `live`;本地气泡此时被去重。
|
||||
* 5. `item.delta` / `item.started` / `item.completed`:正文追加、工具卡片 upsert(先到定形、后到只补空)。
|
||||
* 6. `turn.completed`:`live` 并入 `history` 后清空,`turnEndedAt` 冻结,边界按身份盖到本轮开口条目上。
|
||||
* 6. `turn.completed`:`live` 并入 `history` 后清空,`turnEndedAt` 冻结,边界按身份盖到本轮开口条目上;
|
||||
* 带 `failure` 载荷时,说明条目已经在上一步由 reducer 落进 `live`,随本轮一起并入历史。
|
||||
* 7. 命令收尾(`finally`):刷新清单 → `endTurnCommand()` 清掉忙态与在途身份 → 出队下一轮。
|
||||
* **顺序是契约**:出队会同步开始下一轮并设上它自己的忙态,所以清忙态必须早于出队;
|
||||
* 权限被拒那种「本轮从未发出但要继续出队」的情况,也只标记 `queueAdvance`、由这里统一收口。
|
||||
@@ -577,14 +580,10 @@ export function useDirectProjectChatController({
|
||||
true,
|
||||
);
|
||||
if (projectPathRef.current !== nextProjectPath) return;
|
||||
// 聊天里的失败说明不再由这里写:宿主已经把脱敏后的原因放进了
|
||||
// `turn.completed.failure`,reducer 会把它落成本轮最后一条条目(唯一来源)。这里只保留
|
||||
// 运行错误横幅(含 `详情:` 那份长 detail)与诊断留痕,两条通道不再各写一份文案。
|
||||
onRuntimeError(visibleMessage);
|
||||
appendLocalMessage({
|
||||
role: 'assistant',
|
||||
text: visibleMessage,
|
||||
runtimeOwned: true,
|
||||
messageId: `direct-codex:${input.clientTurnId}:failure`,
|
||||
updatedAt: Date.now(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-2
@@ -5,6 +5,10 @@
|
||||
* 运行态独有条目。这里不做可见性判断(那是投影的事):DirectProject 同一时刻只有一个回合在跑,
|
||||
* `turn.started` / `turn.completed` 只切换"是否还在跑"这一个布尔;回合身份只用原生生命周期
|
||||
* 事件自带的 canonical user identity(`userItemId`)做展示边界关联,不新建回合注册表。
|
||||
*
|
||||
* 失败(`turn.completed.status === "failed"`)不是第二套生命周期:终态还是同一个事件,只是带了
|
||||
* `failure` 载荷。这里把载荷落成本轮最后一条说明条目再走同一个收口函数——失败文案的唯一来源
|
||||
* 就是事件流,命令返回只服务运行错误横幅与诊断。
|
||||
*/
|
||||
|
||||
import type { GameCreatorDirectToolCall } from '../../../../app/types';
|
||||
@@ -14,6 +18,10 @@ import type { DirectThreadHistorySlice } from '../generated/DirectThreadHistoryS
|
||||
import type { DirectThreadItem } from '../generated/DirectThreadItem';
|
||||
import type { DirectThreadSubscriptionBootstrap } from '../generated/DirectThreadSubscriptionBootstrap';
|
||||
import { projectDirectThreadItem } from './directThreadItemProjection';
|
||||
import {
|
||||
directTurnFailureItemId,
|
||||
directTurnFailureNoticeText,
|
||||
} from './directTurnFailure';
|
||||
|
||||
export type DirectChatEntryKind = 'message' | 'reasoning' | 'tool';
|
||||
|
||||
@@ -326,7 +334,20 @@ export function reduceDirectThreadEvent(
|
||||
if (!state.turnRunning && state.live.length === 0) {
|
||||
return state;
|
||||
}
|
||||
return finishDirectThreadTurn(state, eventAt);
|
||||
// 失败终态带 `failure` 载荷:先把它落成本轮最后一条说明条目,再和正常终态走同一个收口
|
||||
// 函数。载荷在、原因非空才算一条说明;空原因不补一条空气泡(终态照样收口)。
|
||||
const failure = event.failure;
|
||||
const withNotice =
|
||||
failure && failure.message.trim()
|
||||
? upsertLiveEntry(state, {
|
||||
itemId: directTurnFailureItemId(eventUserItemId, eventAt),
|
||||
kind: 'message',
|
||||
role: 'assistant',
|
||||
text: directTurnFailureNoticeText(failure.message),
|
||||
at: eventAt,
|
||||
})
|
||||
: state;
|
||||
return finishDirectThreadTurn(withNotice, eventAt);
|
||||
}
|
||||
case 'item.delta':
|
||||
return appendLiveText(state, event);
|
||||
@@ -369,7 +390,8 @@ export function reduceDirectThreadEvent(
|
||||
/**
|
||||
* 回合收口:把运行态条目并入历史、清空运行态,并固定本轮终态时间。
|
||||
*
|
||||
* `endedAt` 只接受明确的终态时间(`turn.completed.at`,或宿主终止收口时观测到的时刻):
|
||||
* `endedAt` 只接受明确的终态时间:`turn.completed.at`(正常与失败同源),或宿主终止收口时
|
||||
* 观测到的时刻。
|
||||
* 缺失就是缺失,宁可不显示总耗时,也不用最后一条工具 / 正文的时间顶替。
|
||||
* 已经冻结的终态时间不会被后来的调用抬高;开始时间只记原生值,用户实际发送时间的优先级
|
||||
* 由投影层决定(条目上的 `at` 才是气泡时间)。
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 失败终态的展示口径:`turn.completed.failure` 载荷怎么变成聊天里那条说明。
|
||||
*
|
||||
* 只放两条规则,别在这里做事件归并(那是 `directThreadChat.ts` 的事):
|
||||
* 1. 说明条目的展示身份怎么派生;
|
||||
* 2. 事件里的原始原因怎么变成用户可见文案。
|
||||
*
|
||||
* 为什么值得单独一个文件:这两条是**跨侧约定**——身份要和前端自己造的说明(终止 / announce)
|
||||
* 区分开又保持可预期,文案映射要和运行错误横幅用同一份规则。把它们散在 reducer 里,读代码的人
|
||||
* 只能靠猜"这条失败说明是从哪冒出来的"。
|
||||
*/
|
||||
|
||||
import { projectRuntimeVisibleError } from '../../../../features/agent-runtime';
|
||||
|
||||
/** 没有本轮开口条目身份时的兜底展示身份前缀(正常路径不会用到)。 */
|
||||
const DIRECT_TURN_FAILURE_FALLBACK_ITEM_ID = 'direct-thread-turn-failure';
|
||||
|
||||
/**
|
||||
* 失败说明条目的**展示身份**:本轮开口用户条目的 canonical identity + `:failure` 后缀。
|
||||
*
|
||||
* 它是派生身份,不是原生条目身份——宿主只在 `turn.completed.failure` 里给原因,不额外造条目。
|
||||
* 用本轮身份派生可以保证"同一轮只有一条说明、跨轮不会合并",也不会和原生 itemId 撞车。
|
||||
* 身份不可证明(本轮没有落盘用户条目)时退化成与事件时间绑定的固定形状:它随事件固定、
|
||||
* 重放不变,又不会让两轮失败互相覆盖。
|
||||
*/
|
||||
export function directTurnFailureItemId(
|
||||
userItemId: string | null | undefined,
|
||||
at: number,
|
||||
): string {
|
||||
const identity = typeof userItemId === 'string' ? userItemId.trim() : '';
|
||||
if (identity) return `${identity}:failure`;
|
||||
return at > 0
|
||||
? `${DIRECT_TURN_FAILURE_FALLBACK_ITEM_ID}:${at}`
|
||||
: DIRECT_TURN_FAILURE_FALLBACK_ITEM_ID;
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败原因的可见文案:与运行错误横幅共用同一份映射(`projectRuntimeVisibleError`)。
|
||||
*
|
||||
* 事件里的 `message` 是宿主已脱敏 + 截断的原始原因,这里只做"给人看"的那一步,不再另开文案
|
||||
* 规则,也不在这里判断"这算不算失败"(那由事件载荷的有没有决定)。
|
||||
*/
|
||||
export function directTurnFailureNoticeText(message: string): string {
|
||||
const raw = typeof message === 'string' ? message.trim() : '';
|
||||
return projectRuntimeVisibleError(raw, '陶泥儿智能创作', true);
|
||||
}
|
||||
+4
-1
@@ -239,7 +239,10 @@ function newTurn(key: string): DirectChatTurnEntries {
|
||||
* 条目 + 运行期本地消息 → 回合列表。
|
||||
*
|
||||
* 每个用户条目开一个新回合;本地用户气泡(乐观发送)也算开新回合;本地 assistant 消息
|
||||
* (失败 / 终止说明)挂到当前回合末尾。同身份的本地消息不重复渲染:条目赢。
|
||||
* (终止说明、壳层 `announce`)挂到当前回合末尾。同身份的本地消息不重复渲染:条目赢。
|
||||
*
|
||||
* 失败说明不在这条本地通道里:它是宿主 `turn.completed.failure` 载荷落成的普通条目,来源与
|
||||
* 顺序都归 reducer。
|
||||
*/
|
||||
export function buildDirectChatTurns({
|
||||
entries,
|
||||
|
||||
Reference in New Issue
Block a user