删掉聊天回读路径里已无人调用的回读函数与前端死类型
删除 Rust 侧 read_direct_tool_calls_at 与 read_direct_turn_stream_at:前端已改走项目对话历史,这两个回读函数不再有调用方 direct_tool_calls / direct_turn_stream 的模块注释与上限常量口径改成写侧,回读命令退役后不再提「回读」 direct_tool_calls 单测改为直接读盘断言落盘内容,删掉只服务已删回读的跳过损坏行 / 合并重复行 / 回读上限三条用例 写前读取的 UTF-8 容错用例改名为 tool_call_pre_read_skips_invalid_utf8_line 并补缺文件返回空表断言 app/types.ts 删除 GameCreatorDirectTurnUpdateEvent 与 TurnStream* 五个只服务旧事件链路的类型 工具卡片方案 §3 由「回读命令」改写为「落盘契约(写侧)」,文首修订说明同步
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
//! GameAgent 对话「工具调用卡片」的采集、持久化与回读。
|
||||
//! GameAgent 对话「工具调用卡片」的采集与持久化。
|
||||
//!
|
||||
//! 契约见 `docs/technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md`:
|
||||
//! Codex app-server 的 `item/started` / `item/completed` 里带着完整的命令 / 文件变更
|
||||
@@ -9,7 +9,7 @@
|
||||
//! 文本条目,而且会被注入 Codex 上下文。往里面塞新形状既装不下,又有污染模型上下文的风险。
|
||||
|
||||
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::config::write_game_creator_private_file;
|
||||
use crate::project::{enforce_project_permission_policy, project_append_lock_for};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -22,7 +22,7 @@ use std::path::{Path, PathBuf};
|
||||
pub(crate) const DIRECT_TOOL_CALL_RECORD_TYPE: &str = "tool_call_item";
|
||||
/// 条目 schema 版本。
|
||||
pub(crate) const DIRECT_TOOL_CALL_SCHEMA_VERSION: &str = "agc-tool-call.v1";
|
||||
/// 回读上限:只保留最近这么多条(按 `updatedAt` / `startedAt` 取最新)。
|
||||
/// 落盘上限:只保留最近这么多条(按 `updatedAt` / `startedAt` 取最新)。
|
||||
pub(crate) const DIRECT_TOOL_CALL_LIMIT: usize = 200;
|
||||
/// `detail.command` / `detail.output` 的字符上限。
|
||||
const DIRECT_TOOL_CALL_DETAIL_MAX_CHARS: usize = 4000;
|
||||
@@ -397,15 +397,6 @@ fn normalize_tool_calls(calls: Vec<DirectToolCall>) -> Vec<DirectToolCall> {
|
||||
normalized
|
||||
}
|
||||
|
||||
/// 回读:文件缺失返回空数组;单行损坏跳过;按时间正序,最多最近 200 条。
|
||||
pub(crate) fn read_direct_tool_calls_at(root: &Path) -> Result<Vec<DirectToolCall>, String> {
|
||||
let path = tool_calls_path(root);
|
||||
if !prepare_game_creator_private_path_for_read(&path, false, "工具调用历史")? {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Ok(normalize_tool_calls(read_tool_call_lines(&path)))
|
||||
}
|
||||
|
||||
/// 状态的「确定性」排序:终态(`completed` / `failed`)优先于 `running`。
|
||||
fn status_certainty(status: &str) -> u8 {
|
||||
match status {
|
||||
@@ -554,10 +545,22 @@ mod tests {
|
||||
use super::{
|
||||
direct_tool_call_from_item, direct_tool_call_now_ms, direct_tool_call_status,
|
||||
direct_tool_call_status_changed, persist_direct_tool_call_at, persist_direct_tool_calls_at,
|
||||
read_direct_tool_calls_at, sanitize_detail_text, tool_calls_path, DirectToolCall,
|
||||
DirectToolCallDetail, DIRECT_TOOL_CALL_LIMIT, DIRECT_TOOL_CALL_SCHEMA_VERSION,
|
||||
read_tool_call_lines, sanitize_detail_text, tool_call_from_line, tool_calls_path,
|
||||
DirectToolCall, DirectToolCallDetail, DIRECT_TOOL_CALL_LIMIT,
|
||||
DIRECT_TOOL_CALL_SCHEMA_VERSION,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::path::Path;
|
||||
|
||||
/// 写侧用例直接读文件:回读命令退役后不再经过 `normalize_tool_calls` 的合并与裁剪,
|
||||
/// 断言因此落在「磁盘上到底写了什么」这一层。
|
||||
fn persisted_tool_calls(root: &Path) -> Vec<DirectToolCall> {
|
||||
std::fs::read_to_string(tool_calls_path(root))
|
||||
.unwrap_or_default()
|
||||
.lines()
|
||||
.filter_map(tool_call_from_line)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 一行合法的落盘信封(回读用例的夹具)。
|
||||
fn tool_call_row(id: &str, started_at: u64, updated_at: u64) -> String {
|
||||
@@ -682,7 +685,7 @@ mod tests {
|
||||
.expect("completed tool call");
|
||||
persist_direct_tool_call_at(root.path(), &completed).expect("persist completed");
|
||||
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read tool calls");
|
||||
let calls = persisted_tool_calls(root.path());
|
||||
assert_eq!(calls.len(), 1, "同一 id 只能有一行");
|
||||
assert_eq!(calls[0].status, "completed");
|
||||
assert_eq!(calls[0].started_at, 1000, "startedAt 不被 completed 覆盖");
|
||||
@@ -782,85 +785,6 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据:单行损坏只跳过该行,不整体失败;缺文件返回空数组。
|
||||
#[test]
|
||||
fn tool_call_read_skips_corrupted_lines() {
|
||||
let root = init_tool_call_project("tool-call-corrupt");
|
||||
let path = tool_calls_path(root.path());
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).expect("create dir");
|
||||
let good = serde_json::to_string(&json!({
|
||||
"type": "tool_call_item",
|
||||
"payload": {
|
||||
"schemaVersion": "agc-tool-call.v1",
|
||||
"id": "item-good",
|
||||
"turnId": "turn-1",
|
||||
"kind": "command",
|
||||
"title": "执行命令",
|
||||
"summary": "npm run build",
|
||||
"status": "completed",
|
||||
"detail": {"command": "npm run build"},
|
||||
"startedAt": 1,
|
||||
"updatedAt": 2
|
||||
}
|
||||
}))
|
||||
.expect("serialize good row");
|
||||
std::fs::write(
|
||||
&path,
|
||||
format!("{good}\n{{ not json\n{{\"type\":\"other\",\"payload\":{{}}}}\n{good}\n"),
|
||||
)
|
||||
.expect("write fixture");
|
||||
|
||||
let missing = tempfile::tempdir().expect("missing dir");
|
||||
assert!(
|
||||
read_direct_tool_calls_at(missing.path())
|
||||
.expect("missing file is empty")
|
||||
.is_empty(),
|
||||
"历史文件缺失必须返回空数组"
|
||||
);
|
||||
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read with corrupted lines");
|
||||
assert_eq!(calls.len(), 1, "坏行被跳过,同 id 归并成一条");
|
||||
assert_eq!(calls[0].id, "item-good");
|
||||
}
|
||||
|
||||
/// 判据:回读按时间正序,且超出上限时保留最新。
|
||||
#[test]
|
||||
fn tool_call_read_is_ordered_and_capped() {
|
||||
let root = init_tool_call_project("tool-call-cap");
|
||||
let total = DIRECT_TOOL_CALL_LIMIT + 5;
|
||||
let calls = (0..total)
|
||||
.map(|index| {
|
||||
direct_tool_call_from_item(
|
||||
root.path(),
|
||||
&json!({
|
||||
"id": format!("item-{index:04}"),
|
||||
"type": "commandExecution",
|
||||
"command": format!("run {index}"),
|
||||
"startedAtMs": 1000 + index as u64,
|
||||
}),
|
||||
"turn-1",
|
||||
false,
|
||||
1000 + index as u64,
|
||||
)
|
||||
.expect("tool call")
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
persist_direct_tool_calls_at(root.path(), &calls).expect("persist batch");
|
||||
|
||||
let read = read_direct_tool_calls_at(root.path()).expect("read capped");
|
||||
assert_eq!(read.len(), DIRECT_TOOL_CALL_LIMIT, "超出上限保留最新 N 条");
|
||||
assert_eq!(
|
||||
read.first().expect("first").id,
|
||||
format!("item-{:04}", total - DIRECT_TOOL_CALL_LIMIT),
|
||||
"最早被裁掉的是最旧的条目"
|
||||
);
|
||||
assert!(
|
||||
read.windows(2)
|
||||
.all(|pair| pair[0].timestamp() <= pair[1].timestamp()),
|
||||
"回读必须按时间正序"
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据:fileChange 的标题按去重后的变更数量,摘要取首个变更路径。
|
||||
#[test]
|
||||
fn tool_call_file_change_title_counts_unique_paths() {
|
||||
@@ -1059,7 +983,7 @@ mod tests {
|
||||
|
||||
persist_direct_tool_call_at(root.path(), &completed).expect("persist completed first");
|
||||
persist_direct_tool_call_at(root.path(), &running).expect("persist stale running");
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read after stale single write");
|
||||
let calls = persisted_tool_calls(root.path());
|
||||
assert_eq!(calls.len(), 1, "同一 id 只能有一行");
|
||||
assert_eq!(
|
||||
calls[0].status, "completed",
|
||||
@@ -1071,7 +995,7 @@ mod tests {
|
||||
// 回合末整批落盘那条路径同样不得回退。
|
||||
persist_direct_tool_calls_at(root.path(), std::slice::from_ref(&running))
|
||||
.expect("persist stale running batch");
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read after stale batch write");
|
||||
let calls = persisted_tool_calls(root.path());
|
||||
assert_eq!(
|
||||
calls[0].status, "completed",
|
||||
"整批落盘路径同样不得把 completed 打回 running"
|
||||
@@ -1079,28 +1003,6 @@ mod tests {
|
||||
assert_eq!(calls[0].updated_at, 2000, "整批落盘不得回退 updatedAt");
|
||||
}
|
||||
|
||||
/// 判据:读回时同 id 的重复行也按 `updatedAt` 单调合并(磁盘上留有旧快照不得回退状态)。
|
||||
#[test]
|
||||
fn tool_call_read_merges_duplicate_rows_monotonically() {
|
||||
let root = init_tool_call_project("tool-call-read-monotonic");
|
||||
let path = tool_calls_path(root.path());
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).expect("create dir");
|
||||
let completed = tool_call_row("item-1", 1000, 2000);
|
||||
let stale_running = tool_call_row("item-1", 1000, 1000)
|
||||
.replace("\"status\":\"completed\"", "\"status\":\"running\"");
|
||||
assert!(stale_running.contains("\"status\":\"running\""));
|
||||
std::fs::write(&path, format!("{completed}\n{stale_running}\n")).expect("write fixture");
|
||||
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read duplicate rows");
|
||||
assert_eq!(calls.len(), 1, "同 id 归并成一条");
|
||||
assert_eq!(
|
||||
calls[0].status, "completed",
|
||||
"磁盘上更旧的快照不得把状态打回 running"
|
||||
);
|
||||
assert_eq!(calls[0].updated_at, 2000, "归并保留更新的 updatedAt");
|
||||
assert_eq!(calls[0].started_at, 1000);
|
||||
}
|
||||
|
||||
/// 判据:项目内绝对路径落成项目相对路径,项目外绝对路径保持既有占位形状。
|
||||
#[test]
|
||||
fn tool_call_paths_become_project_relative() {
|
||||
@@ -1153,9 +1055,10 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据:单行损坏(含非法 UTF-8 字节)只跳过损坏行,后续合法记录必须继续读回。
|
||||
/// 判据:写前读取时单行损坏(含非法 UTF-8 字节)只跳过损坏行,后续合法记录必须继续读回,
|
||||
/// 否则一次截断写入会把整份工具卡片从后续重写里抹掉。
|
||||
#[test]
|
||||
fn tool_call_read_skips_invalid_utf8_line() {
|
||||
fn tool_call_pre_read_skips_invalid_utf8_line() {
|
||||
let root = init_tool_call_project("tool-call-invalid-utf8");
|
||||
let path = tool_calls_path(root.path());
|
||||
std::fs::create_dir_all(path.parent().expect("parent")).expect("create dir");
|
||||
@@ -1168,7 +1071,7 @@ mod tests {
|
||||
bytes.extend_from_slice(tool_call_row("item-b", 2000, 2000).as_bytes());
|
||||
bytes.push(b'\n');
|
||||
std::fs::write(&path, &bytes).expect("write invalid utf8 fixture");
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read with invalid utf8");
|
||||
let calls = read_tool_call_lines(&path);
|
||||
assert_eq!(
|
||||
calls.len(),
|
||||
2,
|
||||
@@ -1188,7 +1091,7 @@ mod tests {
|
||||
bytes.extend_from_slice(tool_call_row("item-c", 3000, 3000).as_bytes());
|
||||
bytes.push(b'\n');
|
||||
std::fs::write(&path, &bytes).expect("write truncated utf8 fixture");
|
||||
let calls = read_direct_tool_calls_at(root.path()).expect("read with truncated line");
|
||||
let calls = read_tool_call_lines(&path);
|
||||
assert_eq!(
|
||||
calls.len(),
|
||||
2,
|
||||
@@ -1196,10 +1099,16 @@ mod tests {
|
||||
);
|
||||
assert_eq!(calls[0].id, "item-a");
|
||||
assert_eq!(calls[1].id, "item-c");
|
||||
|
||||
let missing = tempfile::tempdir().expect("missing dir");
|
||||
assert!(
|
||||
read_tool_call_lines(&tool_calls_path(missing.path())).is_empty(),
|
||||
"历史文件缺失时写前读取必须返回空表"
|
||||
);
|
||||
}
|
||||
|
||||
/// 判据:200 条上限是「按时间保留最新 200 条」,超出时更早回合的卡片会被静默丢弃
|
||||
/// (契约内行为,不是缺陷)。本用例只钉住现状与时间正序。
|
||||
/// 判据:落到磁盘上的行同样受 200 条上限约束——「按时间保留最新 200 条」,更早回合的
|
||||
/// 卡片会被静默丢弃(契约内行为,不是缺陷)。本用例只钉住现状与时间正序。
|
||||
#[test]
|
||||
fn tool_call_cap_drops_oldest_turn_cards() {
|
||||
let root = init_tool_call_project("tool-call-cap-oldest");
|
||||
@@ -1236,7 +1145,7 @@ mod tests {
|
||||
.expect("newest tool call");
|
||||
persist_direct_tool_call_at(root.path(), &newest).expect("persist newest");
|
||||
|
||||
let read = read_direct_tool_calls_at(root.path()).expect("read capped");
|
||||
let read = persisted_tool_calls(root.path());
|
||||
assert_eq!(read.len(), DIRECT_TOOL_CALL_LIMIT, "上限仍是 200 条");
|
||||
assert_eq!(
|
||||
read.last().expect("last").id,
|
||||
@@ -1251,7 +1160,7 @@ mod tests {
|
||||
assert!(
|
||||
read.windows(2)
|
||||
.all(|pair| pair[0].timestamp() <= pair[1].timestamp()),
|
||||
"回读必须按时间正序"
|
||||
"落盘顺序必须按时间正序"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! GameAgent 对话「回合流」的采集、持久化与回读。
|
||||
//! GameAgent 对话「回合流」的采集与持久化。
|
||||
//!
|
||||
//! 顺序真相放在一处:`<projectRoot>/.agent/conversations/turn-stream.jsonl` 按**出现顺序**
|
||||
//! 记录一个回合里的文本段与工具调用。工具条目只记位置标记(`callId`),工具本身的正文
|
||||
@@ -11,7 +11,7 @@
|
||||
//! `project.jsonl` 保留原始消息;本流补充文本与工具交替的 item 顺序,不能重复展示两份正文。
|
||||
|
||||
use crate::agent::sanitize_detail_text;
|
||||
use crate::config::{prepare_game_creator_private_path_for_read, write_game_creator_private_file};
|
||||
use crate::config::write_game_creator_private_file;
|
||||
use crate::project::{enforce_project_permission_policy, project_append_lock_for};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
@@ -339,15 +339,6 @@ pub(crate) fn upsert_direct_turn_stream_item_at(
|
||||
})
|
||||
}
|
||||
|
||||
/// 回读:文件缺失返回空数组;单行损坏跳过;按 `seq` 正序,最多最后 400 条。
|
||||
pub(crate) fn read_direct_turn_stream_at(root: &Path) -> Result<Vec<DirectTurnStreamItem>, String> {
|
||||
let path = turn_stream_path(root);
|
||||
if !prepare_game_creator_private_path_for_read(&path, false, "回合流历史")? {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
Ok(normalize_stream_items(read_stream_lines(&path)))
|
||||
}
|
||||
|
||||
/// 追加一段固定身份的文本段(失败说明等):位置排在当前流末尾。
|
||||
///
|
||||
/// 幂等:同一 `(turnId, itemId)` 已经存在时只更新文本与 `updatedAt`(回合重放 / 重复收尾
|
||||
|
||||
@@ -1077,27 +1077,6 @@ export interface AgentProgressEvent {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export type GameCreatorDirectTurnUpdateStatus =
|
||||
| 'accepted'
|
||||
| 'running'
|
||||
| 'streaming'
|
||||
| 'finalizing'
|
||||
| 'completed'
|
||||
| 'failed';
|
||||
|
||||
export type GameCreatorDirectTurnActivity =
|
||||
| 'request-accepted'
|
||||
| 'preparing'
|
||||
| 'file-read'
|
||||
| 'file-write'
|
||||
| 'game-verify'
|
||||
| 'command-exec'
|
||||
| 'controlled-tool'
|
||||
| 'web-search'
|
||||
| 'context-compaction'
|
||||
| 'response-finalization'
|
||||
| 'none';
|
||||
|
||||
export type GameCreatorDirectToolCallKind =
|
||||
| 'command'
|
||||
| 'file_change'
|
||||
@@ -1125,10 +1104,9 @@ export interface GameCreatorDirectToolCallDetail {
|
||||
/**
|
||||
* 一条工具调用(Codex item 的结构化投影)。
|
||||
*
|
||||
* 契约见 `docs/technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md`:
|
||||
* 字段形状与 Rust 侧 `DirectToolCall`、独立历史文件
|
||||
* `.agent/conversations/tool-calls.jsonl` 的 payload 一致(这里少 `turnId` 的变体用于
|
||||
* 事件增量,见下面 `GameCreatorDirectTurnToolCall`)。
|
||||
* 契约见 `docs/technical/【技术方案】GameAgent对话工具调用卡片-2026-09-14.md`:字段形状与
|
||||
* Rust 侧 `DirectToolCall`、独立历史文件 `.agent/conversations/tool-calls.jsonl` 的 payload
|
||||
* 一致(DirectProject 聊天卡片用 `Omit<GameCreatorDirectToolCall, 'turnId'>` 这一变体)。
|
||||
*/
|
||||
export interface GameCreatorDirectToolCall {
|
||||
schemaVersion: string;
|
||||
@@ -1143,69 +1121,6 @@ export interface GameCreatorDirectToolCall {
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** 事件里下发的增量条目:与持久化同形,去掉 `turnId`(回合 id 在事件顶层)。 */
|
||||
export type GameCreatorDirectTurnToolCall = Omit<
|
||||
GameCreatorDirectToolCall,
|
||||
'turnId'
|
||||
>;
|
||||
|
||||
export interface GameCreatorDirectTurnUpdateEvent {
|
||||
projectPath: string;
|
||||
turnId: string;
|
||||
sequence: number;
|
||||
status: GameCreatorDirectTurnUpdateStatus;
|
||||
activity?: GameCreatorDirectTurnActivity | null;
|
||||
accumulatedText?: string | null;
|
||||
/**
|
||||
* 本回合内**发生变化**的结构化工具调用(只有变化时才带,不是每个 heartbeat 都带全量)。
|
||||
* 可选:老版本事件没有这个字段,前端拿到 `undefined` 时必须与改造前行为一致。
|
||||
*/
|
||||
toolCalls?: GameCreatorDirectTurnToolCall[] | null;
|
||||
/**
|
||||
* 本回合当前累计的思考过程(流式,整段替换);拿不到时字段缺席。
|
||||
*/
|
||||
reasoningText?: string | null;
|
||||
/**
|
||||
* 「文本段 + 工具」的**顺序真相**里本次发生变化的那几条。
|
||||
*
|
||||
* 顺序由 `seq`(条目首次出现时钉死)决定,与落盘 `turn-stream.jsonl` 完全同一份数据,
|
||||
* 前端不再自己猜切点。可选:老版本事件没有这个字段。
|
||||
*/
|
||||
streamItems?: TurnStreamItem[] | null;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/** 回合流里的一个 `text` 段;`text` 是该段当前累计全文(会随 delta 增长)。 */
|
||||
export interface TurnStreamTextItem extends TurnStreamItemBase {
|
||||
kind: 'text';
|
||||
text: string;
|
||||
}
|
||||
|
||||
/** 回合流里的一个 `tool` 位置标记;工具正文在 `tool-calls.jsonl`(按 `callId` 关联)。 */
|
||||
export interface TurnStreamToolItem extends TurnStreamItemBase {
|
||||
kind: 'tool';
|
||||
callId: string;
|
||||
}
|
||||
|
||||
interface TurnStreamItemBase {
|
||||
schemaVersion: string;
|
||||
/** 幂等身份:文本段 `text:<turnId>:<itemId>`、工具 `tool:<turnId>:<callId>`。 */
|
||||
id: string;
|
||||
turnId: string;
|
||||
/** 首次出现的写入序号:**顺序真相**,按它升序渲染。 */
|
||||
seq: number;
|
||||
/** 条目首次出现的时刻(Unix 毫秒),同 `seq` 时用它排序。 */
|
||||
at: number;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回合流条目(`read_direct_turn_stream` 的返回元素)。
|
||||
*
|
||||
* 与 Rust `DirectTurnStreamItem` 同形:`text` 段 ↔ `tool` 位置标记。
|
||||
*/
|
||||
export type TurnStreamItem = TurnStreamTextItem | TurnStreamToolItem;
|
||||
|
||||
/** `cancel_direct_codex_turn` 的返回值。 */
|
||||
export interface DirectTurnCancelView {
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
本方案的**卡片表现层**(折叠 / 展开、标题与摘要文案、耗时与时间显示、脱敏、无障碍、样式)仍然是有效契约;**数据来源层**已被 `docs/adr/【ADR】DirectProject对话历史单一事实源-2026-09-16.md` 取代,边界改为:
|
||||
|
||||
- DirectProject 聊天框的工具卡片由**运行态事件 + 项目对话历史**在前端投影生成(`features/project-workspace/directThreadItemProjection.ts`),不再读取 `tool-calls.jsonl`,`read_direct_tool_calls` 不再是聊天视图的输入。
|
||||
- DirectProject 聊天框的工具卡片由**运行态事件 + 项目对话历史**在前端投影生成(`features/project-workspace/directThreadItemProjection.ts`),不再读取 `tool-calls.jsonl`;`read_direct_tool_calls` 命令与 Rust 侧 `read_direct_tool_calls_at` 回读函数已删除,该文件现在只有 DirectRuntime 的写入。
|
||||
- 报文中不再有 `toolCalls` 增量字段与 `GameCreatorDirectTurnUpdateEvent` 这条实时链路:卡片形状由前端从脱敏原始条目生成,事件里只有 `item.started` / `item.completed` / `item.delta`(线上模型见 `agent/direct_thread_wire.rs`,由 ts-rs 导出绑定)。
|
||||
- 卡片身份只有一个 `itemId`(工具条目在 `project.jsonl` 里带的两个 id 已在 Rust 边界归一),前端卡片形状是 `Omit<GameCreatorDirectToolCall, 'turnId'>`:聊天卡片不再有回合身份。
|
||||
- 下面「### 1. 工具调用条目」「### 2. 实时事件」「### 3. 回读命令」三节描述的是 DirectRuntime 自己的账本(`tool-calls.jsonl` 的写入形状与脱敏规则仍然有效,DirectRuntime 保留),**不再是 DirectProject 聊天框的读路径**;「### 4. 前端合并与渲染」中按 `turnId` 归并、按 `turn-stream.jsonl` 的 `seq` 交替的规则已作废,改为按事件顺序 + 历史文件顺序投影。
|
||||
@@ -56,9 +56,9 @@ toolCalls?: DirectTurnToolCall[] | null;
|
||||
- 字段**可选**:老版本事件解析路径必须保持兼容(前端拿到 `undefined` 时行为与现在一致)。
|
||||
- `DirectTurnToolCall` 与上面 payload 同形(去掉 `turnId`)。
|
||||
|
||||
### 3. 回读命令
|
||||
### 3. 落盘契约(写侧)
|
||||
|
||||
新增 Tauri 命令 `read_direct_tool_calls(projectPath)`,返回按时间正序的 `DirectTurnToolCall[]`。
|
||||
DirectRuntime 写 `<projectRoot>/.agent/conversations/tool-calls.jsonl`;回读命令与 Rust 侧回读函数已随聊天读路径退役删除,下面的语义约束的是**写进文件的行**。
|
||||
|
||||
- **上限语义**:200 条是「按时间保留最新 200 条」。超出时更早回合的卡片会被**静默丢弃**(老回合卡片会消失),不做分页、不做历史回填;同一 `id` 的多条记录先按 `updatedAt` 合并,再按时间正序裁剪。
|
||||
- 历史文件缺失 → 返回空数组,不报错。
|
||||
|
||||
Reference in New Issue
Block a user