串行化Agent本地JSONL追加

新增按目标文件路径串行的本地追加写入helper。

让agent.db、对话、Runtime事件任务和Agent运行日志统一写完整JSON行。

补充并发追加测试覆盖JSONL逐行可解析。

同步Agent Runtime技术方案和决策记录。
This commit is contained in:
AIGameCreator App
2026-07-10 06:16:17 +08:00
parent 1a764af20c
commit b30ca890bb
5 changed files with 126 additions and 40 deletions
@@ -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