串行化Agent本地JSONL追加
新增按目标文件路径串行的本地追加写入helper。 让agent.db、对话、Runtime事件任务和Agent运行日志统一写完整JSON行。 补充并发追加测试覆盖JSONL逐行可解析。 同步Agent Runtime技术方案和决策记录。
This commit is contained in:
@@ -3733,15 +3733,9 @@ fn append_game_creator_agent_runtime_event(
|
||||
detail: detail.map(|value| sanitize_agent_runtime_text(value, 500)),
|
||||
updated_at: unix_timestamp(),
|
||||
};
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|error| format!("打开 Agent Runtime 事件失败:{}: {error}", path.display()))?;
|
||||
serde_json::to_writer(&mut file, &event)
|
||||
let line = serde_json::to_string(&event)
|
||||
.map_err(|error| format!("序列化 Agent Runtime 事件失败:{error}"))?;
|
||||
file.write_all(b"\n")
|
||||
.map_err(|error| format!("写入 Agent Runtime 事件失败:{}: {error}", path.display()))?;
|
||||
append_jsonl_line(&path, &line, "Agent Runtime 事件")?;
|
||||
emit_game_creator_agent_runtime_update(root, &state.agent_id);
|
||||
Ok(())
|
||||
}
|
||||
@@ -3884,15 +3878,9 @@ fn append_game_creator_agent_runtime_task_record(
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|error| format!("打开 Agent Runtime 任务失败:{}: {error}", path.display()))?;
|
||||
serde_json::to_writer(&mut file, &record)
|
||||
let line = serde_json::to_string(&record)
|
||||
.map_err(|error| format!("序列化 Agent Runtime 任务失败:{error}"))?;
|
||||
file.write_all(b"\n")
|
||||
.map_err(|error| format!("写入 Agent Runtime 任务失败:{}: {error}", path.display()))
|
||||
append_jsonl_line(&path, &line, "Agent Runtime 任务")
|
||||
}
|
||||
|
||||
fn refresh_game_creator_agent_runtime_task_queue(
|
||||
@@ -7550,15 +7538,9 @@ pub(crate) fn append_agent_run_jsonl(
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("创建 Agent 事件目录失败:{}: {error}", parent.display()))?;
|
||||
}
|
||||
let mut line =
|
||||
let line =
|
||||
serde_json::to_string(value).map_err(|error| format!("序列化 Agent 事件失败:{error}"))?;
|
||||
line.push('\n');
|
||||
fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.and_then(|mut file| file.write_all(line.as_bytes()))
|
||||
.map_err(|error| format!("写入 Agent 事件失败:{}: {error}", path.display()))
|
||||
append_jsonl_line(&path, &line, "Agent 事件")
|
||||
}
|
||||
|
||||
pub(crate) fn write_agent_run_context_bundle(
|
||||
|
||||
@@ -85,15 +85,44 @@ pub(crate) fn append_agent_db_record(
|
||||
"updatedAt".to_string(),
|
||||
serde_json::Value::Number(serde_json::Number::from(unix_timestamp())),
|
||||
);
|
||||
let line = serde_json::to_string(&record)
|
||||
.map_err(|error| format!("序列化 Agent 本地索引失败:{error}"))?;
|
||||
append_jsonl_line(&path, &line, "Agent 本地索引")
|
||||
}
|
||||
|
||||
static PROJECT_APPEND_LOCKS: OnceLock<Mutex<BTreeMap<PathBuf, Arc<Mutex<()>>>>> = OnceLock::new();
|
||||
|
||||
fn project_append_locks() -> &'static Mutex<BTreeMap<PathBuf, Arc<Mutex<()>>>> {
|
||||
PROJECT_APPEND_LOCKS.get_or_init(|| Mutex::new(BTreeMap::new()))
|
||||
}
|
||||
|
||||
fn project_append_lock_for(path: &Path) -> Result<Arc<Mutex<()>>, String> {
|
||||
let mut locks = project_append_locks()
|
||||
.lock()
|
||||
.map_err(|_| "获取本地追加写锁失败:锁已损坏".to_string())?;
|
||||
Ok(locks
|
||||
.entry(path.to_path_buf())
|
||||
.or_insert_with(|| Arc::new(Mutex::new(())))
|
||||
.clone())
|
||||
}
|
||||
|
||||
pub(crate) fn append_jsonl_line(path: &Path, line: &str, error_label: &str) -> Result<(), String> {
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("创建{error_label}目录失败:{}: {error}", parent.display()))?;
|
||||
}
|
||||
let append_lock = project_append_lock_for(path)?;
|
||||
let _append_guard = append_lock
|
||||
.lock()
|
||||
.map_err(|_| format!("获取{error_label}追加写锁失败:锁已损坏"))?;
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|error| format!("打开 Agent 本地索引失败:{}: {error}", path.display()))?;
|
||||
let line = serde_json::to_string(&record)
|
||||
.map_err(|error| format!("序列化 Agent 本地索引失败:{error}"))?;
|
||||
file.write_all(format!("{line}\n").as_bytes())
|
||||
.map_err(|error| format!("写入 Agent 本地索引失败:{}: {error}", path.display()))
|
||||
.open(path)
|
||||
.map_err(|error| format!("打开{error_label}失败:{}: {error}", path.display()))?;
|
||||
file.write_all(line.as_bytes())
|
||||
.and_then(|_| file.write_all(b"\n"))
|
||||
.map_err(|error| format!("写入{error_label}失败:{}: {error}", path.display()))
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -310,15 +339,9 @@ pub(crate) fn append_local_conversation_message_at(
|
||||
agent_id: normalized_agent_id.clone(),
|
||||
updated_at: unix_timestamp(),
|
||||
};
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.map_err(|error| format!("打开对话记录失败:{}: {error}", path.display()))?;
|
||||
serde_json::to_writer(&mut file, &record)
|
||||
.map_err(|error| format!("序列化对话记录失败:{error}"))?;
|
||||
file.write_all(b"\n")
|
||||
.map_err(|error| format!("写入对话记录失败:{}: {error}", path.display()))?;
|
||||
let line =
|
||||
serde_json::to_string(&record).map_err(|error| format!("序列化对话记录失败:{error}"))?;
|
||||
append_jsonl_line(&path, &line, "对话记录")?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
|
||||
@@ -125,6 +125,85 @@ fn use_test_runtime_config_dir(path: PathBuf) -> TestRuntimeConfigDirGuard {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_jsonl_appends_keep_records_line_delimited() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "并行追加测试").expect("project init");
|
||||
let conversation_path = root.join(".agent/conversations/agents/design-director.jsonl");
|
||||
let worker_count = 8;
|
||||
let records_per_worker = 20;
|
||||
let gate = Arc::new((StdMutex::new(false), Condvar::new()));
|
||||
let mut handles = Vec::new();
|
||||
|
||||
for worker_index in 0..worker_count {
|
||||
let root = root.clone();
|
||||
let conversation_path = conversation_path.clone();
|
||||
let gate = Arc::clone(&gate);
|
||||
handles.push(std::thread::spawn(move || {
|
||||
let (lock, cvar) = &*gate;
|
||||
let mut started = lock.lock().expect("start gate lock");
|
||||
while !*started {
|
||||
started = cvar.wait(started).expect("wait start gate");
|
||||
}
|
||||
drop(started);
|
||||
|
||||
for record_index in 0..records_per_worker {
|
||||
let payload = "x".repeat(4096);
|
||||
let line = serde_json::json!({
|
||||
"schemaVersion": LOCAL_CONVERSATION_SCHEMA_VERSION,
|
||||
"role": "assistant",
|
||||
"content": format!("worker-{worker_index}-record-{record_index}-{payload}"),
|
||||
"agentId": "design-director",
|
||||
"updatedAt": unix_timestamp(),
|
||||
})
|
||||
.to_string();
|
||||
append_jsonl_line(&conversation_path, &line, "测试对话记录")
|
||||
.expect("append conversation line");
|
||||
append_agent_db_record(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "test.parallel.append",
|
||||
"workerIndex": worker_index,
|
||||
"recordIndex": record_index,
|
||||
}),
|
||||
)
|
||||
.expect("append agent db record");
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
{
|
||||
let (lock, cvar) = &*gate;
|
||||
let mut started = lock.lock().expect("start gate lock");
|
||||
*started = true;
|
||||
cvar.notify_all();
|
||||
}
|
||||
for handle in handles {
|
||||
handle.join().expect("append worker");
|
||||
}
|
||||
|
||||
let expected_records = worker_count * records_per_worker;
|
||||
let conversation = fs::read_to_string(&conversation_path).expect("conversation file");
|
||||
let conversation_records = conversation.lines().collect::<Vec<_>>();
|
||||
assert_eq!(conversation_records.len(), expected_records);
|
||||
for line in conversation_records {
|
||||
serde_json::from_str::<Value>(line).expect("conversation line should be valid json");
|
||||
}
|
||||
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
let mut parallel_records = 0;
|
||||
for line in agent_db.lines() {
|
||||
let value =
|
||||
serde_json::from_str::<Value>(line).expect("agent db line should be valid json");
|
||||
if value.get("recordType").and_then(Value::as_str) == Some("test.parallel.append") {
|
||||
parallel_records += 1;
|
||||
}
|
||||
}
|
||||
assert_eq!(parallel_records, expected_records);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_runtime_lock_status_keeps_fresh_same_process_locks_busy() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -4065,6 +4065,7 @@
|
||||
- 2026-07-10 调整:Agent Runtime 后台任务支持按 Agent / runId 取消和重试。取消通过 `.agent/runtime/cancel/<agentId>/<runId>.json` 写入本地取消请求,并向任务 JSONL、事件流和 `agent.db` 追加 `cancelled` 审计;pending 任务被取消后不会被 drain 消费,running 任务会在当前 LLM 或工具调用返回后的检查点停止,不再继续执行工具或保存最终 assistant 回复。重试只能基于已有非 running / pending / waiting-for-confirmation 任务创建新的 run,并继续走 `agent.resume` 自动权限和同一 Agent 队列锁。
|
||||
- 2026-07-10 调整:Agent Runtime 后台任务的 `runId` 是同一 Agent 任务历史的身份,不允许复用覆盖。`start_game_creator_agent_runtime_task`、`agent.delegate` 和 retry 进入后台队列前会读取该 Agent 全量 task JSONL 历史;若调用方传入的规范化 runId 已存在,Runtime 自动追加 `-dup-<timestamp>-<attempt>` 生成实际 runId。任务队列、delegate observation 和 `agent.db` 审计都必须使用实际 runId,避免 `latest_game_creator_agent_runtime_tasks` 按 runId 去重时折叠掉不同任务。
|
||||
- 2026-07-10 调整:Agent Runtime 的 `memory.write scope=agent` 只能写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆、不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须走 `blackboard.write`,给单个 Agent 留上下文必须走 `agent.message`。
|
||||
- 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db`、`.agent/conversations/**/*.jsonl`、`.agent/runtime/events/*.jsonl`、`.agent/runtime/tasks/*.jsonl`、`.agent/activity.jsonl` 和 `.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`。`currentGoal` 固定表达本轮任务目标,`waitingOn` 表达当前等待 LLM、工具观察、开发者输入或失败处理;后台任务生命周期、`agent.run_status` observation、下一轮 planning prompt、开发单 Agent 对话页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都必须展示同一份目标 / 等待状态。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`。后台 Agent loop 每轮规划前刷新当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,字段只做运行观测,不改变 loop 上限或权限 gate。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user