运行态事件与历史切片共用聊天条目投影
- 新增 direct_chat_entry 深模块,把原始 response item 与 app-server item 投影成同一聊天条目 - item.started/item.completed 载荷改为完整条目,前端不再需要按 itemId 取快照 - 原始 function_call 与 function_call_output 按 callId 并成一张卡片,文本与明细统一脱敏截断 - 历史切片新增 entries 与 first_item_id,分页锚点不再依赖是否出现可显示条目
This commit is contained in:
@@ -14,6 +14,7 @@ mod codex_cli;
|
||||
mod codex_provider_proxy;
|
||||
mod design_runtime;
|
||||
mod design_tools;
|
||||
mod direct_chat_entry;
|
||||
mod direct_codex_attachments;
|
||||
mod direct_codex_audit;
|
||||
mod direct_codex_user_item;
|
||||
@@ -48,6 +49,7 @@ pub(crate) use codex_cli::{
|
||||
};
|
||||
pub(crate) use codex_provider_proxy::*;
|
||||
pub(crate) use design_runtime::*;
|
||||
pub(crate) use direct_chat_entry::*;
|
||||
pub(crate) use direct_codex_attachments::*;
|
||||
pub(crate) use direct_codex_audit::*;
|
||||
pub(crate) use direct_codex_user_item::*;
|
||||
|
||||
@@ -744,13 +744,17 @@ fn direct_codex_safe_activity_for_item_value(item: &serde_json::Value) -> &'stat
|
||||
/// 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_item_started_payload(item: &serde_json::Value) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"itemType": item
|
||||
.get("type")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or("unknown"),
|
||||
})
|
||||
/// 运行态事件载荷:与历史切片同形的聊天条目;拿不到条目时给空对象。
|
||||
fn direct_chat_entry_payload(
|
||||
root: &std::path::Path,
|
||||
item: &serde_json::Value,
|
||||
turn_id: Option<&str>,
|
||||
completed: bool,
|
||||
now_ms: u64,
|
||||
) -> serde_json::Value {
|
||||
direct_chat_entry_from_item(root, item, turn_id, completed, now_ms)
|
||||
.and_then(|entry| serde_json::to_value(entry).ok())
|
||||
.unwrap_or_else(|| serde_json::json!({}))
|
||||
}
|
||||
|
||||
fn direct_thread_item_id(item: &serde_json::Value) -> Option<String> {
|
||||
@@ -3053,6 +3057,13 @@ impl CodexAppServerConnection {
|
||||
"rawResponseItem/completed 缺少 item".to_string(),
|
||||
));
|
||||
}
|
||||
let entry_payload = direct_chat_entry_payload(
|
||||
history_root,
|
||||
&item,
|
||||
Some(turn_id.as_str()),
|
||||
true,
|
||||
direct_tool_call_now_ms(),
|
||||
);
|
||||
let history_root = history_root.to_path_buf();
|
||||
let history_item = item.clone();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
@@ -3074,7 +3085,7 @@ impl CodexAppServerConnection {
|
||||
turn_id: turn_id.clone(),
|
||||
item_id,
|
||||
call_id: direct_thread_item_call_id(&item),
|
||||
payload: serde_json::json!({}),
|
||||
payload: entry_payload,
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -3216,7 +3227,13 @@ impl CodexAppServerConnection {
|
||||
turn_id: turn_id.clone(),
|
||||
item_id,
|
||||
call_id: None,
|
||||
payload: direct_thread_item_started_payload(item),
|
||||
payload: direct_chat_entry_payload(
|
||||
history_root,
|
||||
item,
|
||||
Some(turn_id.as_str()),
|
||||
completed,
|
||||
direct_tool_call_now_ms(),
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -4527,10 +4544,27 @@ mod tests {
|
||||
"result": { "content": "large output" }
|
||||
});
|
||||
assert_eq!(direct_thread_item_id(&item).as_deref(), Some("item-1"));
|
||||
assert_eq!(
|
||||
direct_thread_item_started_payload(&item),
|
||||
serde_json::json!({ "itemType": "mcpToolCall" })
|
||||
// 运行态事件必须自足:载荷是投影后的聊天条目,前端不需要再按 itemId 取快照。
|
||||
let payload = direct_chat_entry_payload(
|
||||
std::path::Path::new("."),
|
||||
&item,
|
||||
Some("turn-1"),
|
||||
false,
|
||||
1000,
|
||||
);
|
||||
assert_eq!(
|
||||
payload.get("kind").and_then(serde_json::Value::as_str),
|
||||
Some("tool")
|
||||
);
|
||||
assert_eq!(
|
||||
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")
|
||||
);
|
||||
assert!(payload.get("toolCall").is_some(), "{payload}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,6 +10,8 @@ use std::collections::{HashMap, HashSet};
|
||||
use std::sync::{Mutex, OnceLock};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::agent::DirectChatEntry;
|
||||
|
||||
const DEFAULT_MAX_EVENTS: usize = 8_192;
|
||||
const DEFAULT_MAX_BYTES: usize = 8 * 1024 * 1024;
|
||||
|
||||
@@ -71,6 +73,10 @@ pub(crate) struct DirectThreadHistorySlice {
|
||||
pub(crate) items: Vec<Value>,
|
||||
pub(crate) has_more: bool,
|
||||
pub(crate) item_timestamps: std::collections::BTreeMap<String, u64>,
|
||||
/// 与运行态事件同形的聊天条目(顺序即文件顺序)。
|
||||
pub(crate) entries: Vec<DirectChatEntry>,
|
||||
/// 本次切片的原始条目锚点:无论切片里有没有可显示条目,分页都要靠它继续向前。
|
||||
pub(crate) first_item_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
|
||||
@@ -5383,10 +5383,18 @@ pub(crate) async fn read_direct_project_history_slice(
|
||||
before_item_id.as_deref(),
|
||||
limit.unwrap_or(20),
|
||||
)?;
|
||||
let entries = direct_chat_entries_from_history(root, &items);
|
||||
let first_item_id = items
|
||||
.first()
|
||||
.and_then(|item| item.get("id"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::to_string);
|
||||
Ok(DirectThreadHistorySlice {
|
||||
items,
|
||||
has_more,
|
||||
item_timestamps,
|
||||
entries,
|
||||
first_item_id,
|
||||
})
|
||||
})
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user