严格使用 DirectProject Response item 历史
移除 game-creator-conversation.v1 读取 fallback 历史注入前逐项投影 canonical user item 为 Codex message 无效 item 在持久化前失败关闭
This commit is contained in:
+8
-1
@@ -11,8 +11,15 @@ pub(super) fn build_direct_project_history_injection_params(
|
|||||||
history_root: &Path,
|
history_root: &Path,
|
||||||
thread_id: &str,
|
thread_id: &str,
|
||||||
) -> Result<Value, platform_llm::LlmError> {
|
) -> Result<Value, platform_llm::LlmError> {
|
||||||
let items = read_direct_project_history_items_at(history_root)
|
let canonical_items = read_direct_project_history_items_at(history_root)
|
||||||
.map_err(platform_llm::LlmError::InvalidRequest)?;
|
.map_err(platform_llm::LlmError::InvalidRequest)?;
|
||||||
|
let items = canonical_items
|
||||||
|
.iter()
|
||||||
|
.map(|item| {
|
||||||
|
direct_codex_user_item_to_response_item(history_root, item)
|
||||||
|
.map_err(platform_llm::LlmError::InvalidRequest)
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
let params = serde_json::json!({"threadId": thread_id, "items": items});
|
let params = serde_json::json!({"threadId": thread_id, "items": items});
|
||||||
let payload_bytes = serde_json::to_vec(¶ms)
|
let payload_bytes = serde_json::to_vec(¶ms)
|
||||||
.map(|bytes| bytes.len().saturating_add(1))
|
.map(|bytes| bytes.len().saturating_add(1))
|
||||||
|
|||||||
@@ -2702,7 +2702,11 @@ impl CodexAppServerConnection {
|
|||||||
}
|
}
|
||||||
if let Some(client_turn_id) = direct_client_turn_id {
|
if let Some(client_turn_id) = direct_client_turn_id {
|
||||||
let user_item = match direct_user_item {
|
let user_item = match direct_user_item {
|
||||||
Some(item) => item.clone(),
|
Some(item) => {
|
||||||
|
direct_codex_user_item_to_response_item(history_root, item)
|
||||||
|
.map_err(platform_llm::LlmError::InvalidRequest)?;
|
||||||
|
item.clone()
|
||||||
|
}
|
||||||
None => direct_project_local_message_item(
|
None => direct_project_local_message_item(
|
||||||
"user",
|
"user",
|
||||||
current_prompt,
|
current_prompt,
|
||||||
|
|||||||
@@ -9,4 +9,7 @@ pub(crate) use model::{
|
|||||||
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
|
DirectCodexUserMessageItem, DirectCodexUserRole, DirectCodexUserRuntimeRegionPart,
|
||||||
};
|
};
|
||||||
pub(crate) use validation::validate_direct_codex_user_item;
|
pub(crate) use validation::validate_direct_codex_user_item;
|
||||||
pub(crate) use wire::{direct_codex_user_item_to_prompt, direct_codex_user_item_to_wire_input};
|
pub(crate) use wire::{
|
||||||
|
direct_codex_user_item_to_prompt, direct_codex_user_item_to_response_item,
|
||||||
|
direct_codex_user_item_to_wire_input,
|
||||||
|
};
|
||||||
|
|||||||
@@ -4,6 +4,36 @@ use crate::agent::{read_manifest_for_project, sanitize_attachment_local_path};
|
|||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// 将历史中的 canonical user item 投影为 Codex `response_item` message。
|
||||||
|
/// 非 user message 的标准 Response item 原样返回;未知形状直接失败。
|
||||||
|
pub(crate) fn direct_codex_user_item_to_response_item(
|
||||||
|
root: &Path,
|
||||||
|
item: &Value,
|
||||||
|
) -> Result<Value, String> {
|
||||||
|
let is_user_message = item.get("type").and_then(Value::as_str) == Some("message")
|
||||||
|
&& item.get("role").and_then(Value::as_str) == Some("user");
|
||||||
|
if !is_user_message {
|
||||||
|
if item.get("type").and_then(Value::as_str).is_some() {
|
||||||
|
return Ok(item.clone());
|
||||||
|
}
|
||||||
|
return Err("DirectProject 历史 item 缺少 type,无法投影为 Codex item".to_string());
|
||||||
|
}
|
||||||
|
let canonical: DirectCodexUserItem = serde_json::from_value(item.clone())
|
||||||
|
.map_err(|error| format!("DirectProject user item 无法转换为 Codex item:{error}"))?;
|
||||||
|
let Value::Array(content) = direct_codex_user_item_to_wire_input(root, &canonical)? else {
|
||||||
|
return Err("DirectProject user item wire content 不是数组".to_string());
|
||||||
|
};
|
||||||
|
let mut projected = serde_json::json!({
|
||||||
|
"type": "message",
|
||||||
|
"role": "user",
|
||||||
|
"content": content,
|
||||||
|
});
|
||||||
|
if let Some(id) = item.get("id").and_then(Value::as_str) {
|
||||||
|
projected["id"] = Value::String(id.to_string());
|
||||||
|
}
|
||||||
|
Ok(projected)
|
||||||
|
}
|
||||||
|
|
||||||
/// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。
|
/// 将 canonical user item 转为 app-server `turn/start.input` 可接受的文本数组。
|
||||||
/// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。
|
/// AGC 私有 part 只在这里投影为安全摘要,canonical item 本身不被修改。
|
||||||
pub(crate) fn direct_codex_user_item_to_wire_input(
|
pub(crate) fn direct_codex_user_item_to_wire_input(
|
||||||
|
|||||||
@@ -17,9 +17,6 @@ const DIRECT_PROJECT_HISTORY_SCAN_PROBE_FINISHED: usize = 1;
|
|||||||
/// 写入侧与读取侧共用同一个信封类型:`project.jsonl` 由项目主对话与 DirectProject 共享,
|
/// 写入侧与读取侧共用同一个信封类型:`project.jsonl` 由项目主对话与 DirectProject 共享,
|
||||||
/// 这个值一旦只在写入侧改动,读取侧就会把对方的行当成坏行,整份历史立刻读不出来。
|
/// 这个值一旦只在写入侧改动,读取侧就会把对方的行当成坏行,整份历史立刻读不出来。
|
||||||
pub(crate) const DIRECT_PROJECT_HISTORY_RECORD_TYPE: &str = "response_item";
|
pub(crate) const DIRECT_PROJECT_HISTORY_RECORD_TYPE: &str = "response_item";
|
||||||
/// 格式切换到 `response_item`(#282)之前,DirectProject 主对话通过通用对话写入器
|
|
||||||
/// 落到同一份 `project.jsonl`,行形状是 `PersistedLocalConversationMessageRecord`。
|
|
||||||
const DIRECT_PROJECT_HISTORY_LEGACY_SCHEMA_VERSION: &str = "game-creator-conversation.v1";
|
|
||||||
const DIRECT_PROJECT_INTERNAL_CONTEXT_KINDS: &[&str] = &[
|
const DIRECT_PROJECT_INTERNAL_CONTEXT_KINDS: &[&str] = &[
|
||||||
"host_skills.instructions",
|
"host_skills.instructions",
|
||||||
"permissions.instructions",
|
"permissions.instructions",
|
||||||
@@ -180,79 +177,22 @@ fn direct_project_history_item_from_line(
|
|||||||
Ok(Some(item))
|
Ok(Some(item))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 只接受两种行信封:`response_item`,以及白名单化的 legacy 行。返回 `None` 表示行已
|
/// 只接受 `response_item` 行信封;其它历史格式不提供迁移或 fallback,直接失败关闭。
|
||||||
/// 被识别、但不该进入 Codex 上下文(legacy 的 `tool` 行)。
|
|
||||||
/// 其余任何形状(含换了 `schemaVersion`、带 `type` 却不是 `response_item`、role 不在
|
|
||||||
/// legacy 写入器自己的角色集合内、content 不是非空字符串)都判定为损坏并失败关闭。
|
|
||||||
fn direct_project_history_item_from_parsed_line(
|
fn direct_project_history_item_from_parsed_line(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
parsed: &Value,
|
parsed: &Value,
|
||||||
) -> Result<Option<Value>, String> {
|
) -> Result<Option<Value>, String> {
|
||||||
if parsed.get("type").and_then(Value::as_str) == Some(DIRECT_PROJECT_HISTORY_RECORD_TYPE) {
|
if parsed.get("type").and_then(Value::as_str) != Some(DIRECT_PROJECT_HISTORY_RECORD_TYPE) {
|
||||||
return parsed
|
return Err(format!(
|
||||||
|
"DirectProject 历史记录类型无效:{}",
|
||||||
|
path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
parsed
|
||||||
.get("payload")
|
.get("payload")
|
||||||
.cloned()
|
.cloned()
|
||||||
.map(Some)
|
.map(Some)
|
||||||
.ok_or_else(|| format!("DirectProject 历史记录缺少 payload:{}", path.display()));
|
.ok_or_else(|| format!("DirectProject 历史记录缺少 payload:{}", path.display()))
|
||||||
}
|
|
||||||
direct_project_legacy_row(parsed)
|
|
||||||
.map(|row| row.message_item())
|
|
||||||
.ok_or_else(|| format!("DirectProject 历史记录类型无效:{}", path.display()))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 白名单化 legacy 行的投影结果。
|
|
||||||
enum DirectProjectLegacyRow {
|
|
||||||
/// user/assistant 行:投影成 message item 进入历史。
|
|
||||||
Message(Value),
|
|
||||||
/// 已识别但不可注入的行(`tool`):与 developer/system item 同样处理,不进 Codex
|
|
||||||
/// 上下文。legacy 行不是 Responses item,`tool` 行无法还原成真正的工具 item,
|
|
||||||
/// 注入会造出假的工具消息;聊天投影本来也只展示 user/assistant。
|
|
||||||
NotChat,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DirectProjectLegacyRow {
|
|
||||||
fn message_item(self) -> Option<Value> {
|
|
||||||
match self {
|
|
||||||
DirectProjectLegacyRow::Message(item) => Some(item),
|
|
||||||
DirectProjectLegacyRow::NotChat => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 格式切换前的 legacy 行投影。存量用户项目的历史文件全是这种行,读取时投影成与
|
|
||||||
/// `direct_project_local_message_item` 同形状的 message item,`role` 与 `content`
|
|
||||||
/// 原样保留(不 trim、不改写、不合并),未知字段忽略。
|
|
||||||
///
|
|
||||||
/// 角色白名单取的是 legacy 写入器自己的角色集合,也就是
|
|
||||||
/// `project/conversation.rs` 里 `matches!(role, "user" | "assistant" | "tool")` 这一
|
|
||||||
/// 条校验,所以「legacy 写入器能写出的行」被完整覆盖,不会有第三种角色漏进来;
|
|
||||||
/// 白名单之外的角色(手改文件、未来写入器)仍按损坏失败关闭。
|
|
||||||
fn direct_project_legacy_row(parsed: &Value) -> Option<DirectProjectLegacyRow> {
|
|
||||||
let object = parsed.as_object()?;
|
|
||||||
if object.contains_key("type")
|
|
||||||
|| object.get("schemaVersion").and_then(Value::as_str)
|
|
||||||
!= Some(DIRECT_PROJECT_HISTORY_LEGACY_SCHEMA_VERSION)
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let role = object.get("role").and_then(Value::as_str)?;
|
|
||||||
if !matches!(role, "user" | "assistant" | "tool") {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let content = object.get("content").and_then(Value::as_str)?;
|
|
||||||
if content.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if role == "tool" {
|
|
||||||
return Some(DirectProjectLegacyRow::NotChat);
|
|
||||||
}
|
|
||||||
let message_id = object
|
|
||||||
.get("messageId")
|
|
||||||
.and_then(Value::as_str)
|
|
||||||
.filter(|message_id| !message_id.is_empty());
|
|
||||||
Some(DirectProjectLegacyRow::Message(
|
|
||||||
direct_project_message_item(role, content, message_id),
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 测试专用探针:在"锁外幂等回扫"这一段的两端回调。
|
/// 测试专用探针:在"锁外幂等回扫"这一段的两端回调。
|
||||||
@@ -637,9 +577,8 @@ mod tests {
|
|||||||
std::fs::write(&path, format!("{}\n", lines.join("\n"))).expect("write history fixture");
|
std::fs::write(&path, format!("{}\n", lines.join("\n"))).expect("write history fixture");
|
||||||
}
|
}
|
||||||
|
|
||||||
const LEGACY_USER_ROW: &str = r#"{"schemaVersion":"game-creator-conversation.v1","role":"user","content":"请创建菜单","agentId":null,"messageId":"direct-codex:turn-0001:user","updatedAt":1757000000}"#;
|
|
||||||
const LEGACY_ASSISTANT_ROW: &str = r#"{"schemaVersion":"game-creator-conversation.v1","role":"assistant","content":"已完成 第一行\n第二行 ","agentId":null,"updatedAt":1757000001}"#;
|
|
||||||
const RESPONSE_ITEM_ROW: &str = r#"{"type":"response_item","payload":{"type":"message","role":"user","id":"codex-item-2","content":[{"type":"input_text","text":"再加一个按钮"}]}}"#;
|
const RESPONSE_ITEM_ROW: &str = r#"{"type":"response_item","payload":{"type":"message","role":"user","id":"codex-item-2","content":[{"type":"input_text","text":"再加一个按钮"}]}}"#;
|
||||||
|
const RESPONSE_ASSISTANT_ROW: &str = r#"{"type":"response_item","payload":{"type":"message","role":"assistant","content":[{"type":"output_text","text":"已完成"}]}}"#;
|
||||||
|
|
||||||
/// 判据:争用类失败会被"有界退避重试"真的吃掉,最终把条目落一行。
|
/// 判据:争用类失败会被"有界退避重试"真的吃掉,最终把条目落一行。
|
||||||
///
|
///
|
||||||
@@ -648,7 +587,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn contention_failure_is_retried_with_bounded_backoff() {
|
fn contention_failure_is_retried_with_bounded_backoff() {
|
||||||
let root = init_history_project("contention-retry");
|
let root = init_history_project("contention-retry");
|
||||||
write_history_lines(root.path(), &[LEGACY_USER_ROW]);
|
write_history_lines(root.path(), &[RESPONSE_ITEM_ROW]);
|
||||||
let marker = root
|
let marker = root
|
||||||
.path()
|
.path()
|
||||||
.join(".agent/runtime/test-fail-next-direct-project-history-append");
|
.join(".agent/runtime/test-fail-next-direct-project-history-append");
|
||||||
@@ -680,7 +619,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn contention_failure_beyond_the_backoff_budget_fails_closed() {
|
fn contention_failure_beyond_the_backoff_budget_fails_closed() {
|
||||||
let root = init_history_project("contention-bounded");
|
let root = init_history_project("contention-bounded");
|
||||||
write_history_lines(root.path(), &[LEGACY_USER_ROW]);
|
write_history_lines(root.path(), &[RESPONSE_ITEM_ROW]);
|
||||||
let marker = root
|
let marker = root
|
||||||
.path()
|
.path()
|
||||||
.join(".agent/runtime/test-fail-next-direct-project-history-append");
|
.join(".agent/runtime/test-fail-next-direct-project-history-append");
|
||||||
@@ -723,7 +662,7 @@ mod tests {
|
|||||||
write_history_lines(
|
write_history_lines(
|
||||||
root.path(),
|
root.path(),
|
||||||
&[
|
&[
|
||||||
LEGACY_USER_ROW,
|
RESPONSE_ITEM_ROW,
|
||||||
r#"{"schemaVersion":"game-creator"#,
|
r#"{"schemaVersion":"game-creator"#,
|
||||||
RESPONSE_ITEM_ROW,
|
RESPONSE_ITEM_ROW,
|
||||||
],
|
],
|
||||||
@@ -761,7 +700,7 @@ mod tests {
|
|||||||
let root = init_history_project("scan-outside-lock");
|
let root = init_history_project("scan-outside-lock");
|
||||||
write_history_lines(
|
write_history_lines(
|
||||||
root.path(),
|
root.path(),
|
||||||
&[LEGACY_USER_ROW, RESPONSE_ITEM_ROW, LEGACY_ASSISTANT_ROW],
|
&[RESPONSE_ITEM_ROW, RESPONSE_ITEM_ROW, RESPONSE_ASSISTANT_ROW],
|
||||||
);
|
);
|
||||||
let path = history_path(root.path());
|
let path = history_path(root.path());
|
||||||
let fired = Arc::new(AtomicBool::new(false));
|
let fired = Arc::new(AtomicBool::new(false));
|
||||||
@@ -805,7 +744,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn concurrent_append_after_the_out_of_lock_scan_still_prevents_a_duplicate_row() {
|
fn concurrent_append_after_the_out_of_lock_scan_still_prevents_a_duplicate_row() {
|
||||||
let root = init_history_project("scan-stale-state");
|
let root = init_history_project("scan-stale-state");
|
||||||
write_history_lines(root.path(), &[LEGACY_USER_ROW]);
|
write_history_lines(root.path(), &[RESPONSE_ITEM_ROW]);
|
||||||
let path = history_path(root.path());
|
let path = history_path(root.path());
|
||||||
let item = json!({
|
let item = json!({
|
||||||
"type": "message",
|
"type": "message",
|
||||||
@@ -845,7 +784,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn conflicting_item_id_still_fails_closed_with_the_scan_outside_the_lock() {
|
fn conflicting_item_id_still_fails_closed_with_the_scan_outside_the_lock() {
|
||||||
let root = init_history_project("id-conflict-outside-lock");
|
let root = init_history_project("id-conflict-outside-lock");
|
||||||
write_history_lines(root.path(), &[LEGACY_USER_ROW]);
|
write_history_lines(root.path(), &[RESPONSE_ITEM_ROW]);
|
||||||
append_direct_project_history_item_at(
|
append_direct_project_history_item_at(
|
||||||
root.path(),
|
root.path(),
|
||||||
&json!({
|
&json!({
|
||||||
@@ -1033,91 +972,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn legacy_conversation_rows_project_into_responses_message_items() {
|
fn non_response_item_history_fails_closed() {
|
||||||
let root = init_history_project("legacy-projection");
|
|
||||||
write_history_lines(root.path(), &[LEGACY_USER_ROW, LEGACY_ASSISTANT_ROW]);
|
|
||||||
|
|
||||||
let items = read_direct_project_history_items_at(root.path()).expect("read legacy history");
|
|
||||||
assert_eq!(
|
|
||||||
items,
|
|
||||||
vec![
|
|
||||||
json!({
|
|
||||||
"type": "message",
|
|
||||||
"role": "user",
|
|
||||||
"id": "direct-codex:turn-0001:user",
|
|
||||||
"content": [{"type": "input_text", "text": "请创建菜单"}],
|
|
||||||
}),
|
|
||||||
json!({
|
|
||||||
"type": "message",
|
|
||||||
"role": "assistant",
|
|
||||||
"content": [{"type": "output_text", "text": "已完成 第一行\n第二行 "}],
|
|
||||||
}),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
let chat = read_direct_project_chat_history_at(root.path()).expect("read legacy chat");
|
|
||||||
assert_eq!(chat.messages.len(), 2);
|
|
||||||
assert_eq!(chat.messages[0].role, "user");
|
|
||||||
assert_eq!(chat.messages[0].content, "请创建菜单");
|
|
||||||
assert_eq!(
|
|
||||||
chat.messages[0].message_id.as_deref(),
|
|
||||||
Some("direct-codex:turn-0001:user")
|
|
||||||
);
|
|
||||||
assert_eq!(chat.messages[1].role, "assistant");
|
|
||||||
assert_eq!(chat.messages[1].content, "已完成 第一行\n第二行 ");
|
|
||||||
assert_eq!(chat.messages[1].message_id, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mixed_legacy_and_response_item_history_reads_in_file_order() {
|
|
||||||
let root = init_history_project("mixed-history");
|
|
||||||
write_history_lines(
|
|
||||||
root.path(),
|
|
||||||
&[LEGACY_USER_ROW, RESPONSE_ITEM_ROW, LEGACY_ASSISTANT_ROW],
|
|
||||||
);
|
|
||||||
|
|
||||||
let items = read_direct_project_history_items_at(root.path()).expect("read mixed history");
|
|
||||||
assert_eq!(
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.map(|item| item.get("id").and_then(Value::as_str))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![
|
|
||||||
Some("direct-codex:turn-0001:user"),
|
|
||||||
Some("codex-item-2"),
|
|
||||||
None
|
|
||||||
]
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
items[1],
|
|
||||||
json!({
|
|
||||||
"type": "message",
|
|
||||||
"role": "user",
|
|
||||||
"id": "codex-item-2",
|
|
||||||
"content": [{"type": "input_text", "text": "再加一个按钮"}],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
items[2]["content"][0]["text"],
|
|
||||||
json!("已完成 第一行\n第二行 ")
|
|
||||||
);
|
|
||||||
|
|
||||||
let chat = read_direct_project_chat_history_at(root.path()).expect("read mixed chat");
|
|
||||||
assert_eq!(
|
|
||||||
chat.messages
|
|
||||||
.iter()
|
|
||||||
.map(|message| (message.role.as_str(), message.content.as_str()))
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec![
|
|
||||||
("user", "请创建菜单"),
|
|
||||||
("user", "再加一个按钮"),
|
|
||||||
("assistant", "已完成 第一行\n第二行 "),
|
|
||||||
]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_compat_keeps_unrecognized_rows_failing_closed() {
|
|
||||||
let unrecognized_rows = [
|
let unrecognized_rows = [
|
||||||
// 带 type 却不是 response_item
|
// 带 type 却不是 response_item
|
||||||
r#"{"type":"message","role":"user","content":[{"type":"input_text","text":"x"}]}"#,
|
r#"{"type":"message","role":"user","content":[{"type":"input_text","text":"x"}]}"#,
|
||||||
@@ -1159,61 +1014,4 @@ mod tests {
|
|||||||
.expect_err("broken json must fail closed");
|
.expect_err("broken json must fail closed");
|
||||||
assert!(error.starts_with("解析 DirectProject 历史失败"), "{error}");
|
assert!(error.starts_with("解析 DirectProject 历史失败"), "{error}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn legacy_tool_row_is_recognized_but_stays_out_of_the_codex_context() {
|
|
||||||
let root = init_history_project("legacy-tool-row");
|
|
||||||
write_history_lines(
|
|
||||||
root.path(),
|
|
||||||
&[
|
|
||||||
LEGACY_USER_ROW,
|
|
||||||
r#"{"schemaVersion":"game-creator-conversation.v1","role":"tool","content":"{\"ok\":true}","agentId":null,"updatedAt":1757000002}"#,
|
|
||||||
LEGACY_ASSISTANT_ROW,
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
let items = read_direct_project_history_items_at(root.path()).expect("read history");
|
|
||||||
assert_eq!(
|
|
||||||
items
|
|
||||||
.iter()
|
|
||||||
.map(|item| item["role"].as_str().unwrap_or_default())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec!["user", "assistant"]
|
|
||||||
);
|
|
||||||
|
|
||||||
let chat = read_direct_project_chat_history_at(root.path()).expect("read chat");
|
|
||||||
assert_eq!(
|
|
||||||
chat.messages
|
|
||||||
.iter()
|
|
||||||
.map(|message| message.role.as_str())
|
|
||||||
.collect::<Vec<_>>(),
|
|
||||||
vec!["user", "assistant"]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn generic_writer_rows_are_covered_by_the_legacy_whitelist() {
|
|
||||||
let root = init_history_project("generic-writer-shape");
|
|
||||||
crate::append_local_conversation_message_at(
|
|
||||||
root.path(),
|
|
||||||
None,
|
|
||||||
crate::LocalConversationMessage {
|
|
||||||
role: "user".to_string(),
|
|
||||||
content: "通用写入器写的旧格式回合".to_string(),
|
|
||||||
agent_id: None,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.expect("append legacy row through the generic writer");
|
|
||||||
|
|
||||||
let items =
|
|
||||||
read_direct_project_history_items_at(root.path()).expect("read generic writer row");
|
|
||||||
assert_eq!(
|
|
||||||
items,
|
|
||||||
vec![json!({
|
|
||||||
"type": "message",
|
|
||||||
"role": "user",
|
|
||||||
"content": [{"type": "input_text", "text": "通用写入器写的旧格式回合"}],
|
|
||||||
})]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user