收紧Agent私有记忆边界

禁止Agent通过memory.write写入其他Agent私有记忆。

跨Agent共享改由blackboard.write或agent.message承接。

补充后台Runtime测试覆盖跨Agent私有记忆阻断。

同步Agent Runtime技术方案和决策记录。
This commit is contained in:
AIGameCreator App
2026-07-10 06:09:15 +08:00
parent 0aa34cad79
commit 1a764af20c
4 changed files with 125 additions and 15 deletions
@@ -1377,7 +1377,7 @@ fn build_game_creator_agent_background_tool_plan_request(
let tool_policy_json = serde_json::to_string_pretty(&tool_policy)
.map_err(|error| format!("序列化 Agent 工具策略失败:{error}"))?;
let prompt = format!(
"当前工具策略:\n{tool_policy_json}\n\n项目上下文如下。你正在执行后台 Agent loop 第 {loop_index} 轮。请基于目标、已有工具观察和当前上下文修正计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\n已有工具观察:\n{observations_json}\n\nJSON schema{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|memory.write|conversation.read|asset.list|project.index|project.diff|file.list|file.read|file.write|task.list|task.update|command.run_limited|preview.start|canvas.asset_generate|blackboard.write|agent.message|agent.delegate|agent.run_status\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}}memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}}project.diff 使用 {{\"checkpointId\":\"checkpoint id\"}},用于读取当前项目相对路径 diff 摘要;file.list 使用 {{\"path\":\"可选项目内相对目录或文件\"}},path 为空时列出项目摘要;file.read 使用 {{\"path\":\"项目内相对路径\"}}file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"文件内容\"}}task.list input 可为空,用于读取 manifest 任务图、状态和 readyTaskIdstask.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}}command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}},只支持本地静态自检;preview.start input 可为空,用于启动当前项目的 127.0.0.1 本地 HTTP 预览;canvas.asset_generate 使用 {{\"prompt\":\"要生成的美术素材描述\"}},通过配置的 External Editor API 生成首版素材并登记到 assetsblackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}}agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}}agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"runId\":\"可选 run id\"}},用于把任务投递到另一个 Agent 的独立队列;agent.run_status 使用 {{\"agentId\":\"可选目标 taskId\",\"scope\":\"self|all\"}},用于读取自己或其他 Agent 的 Runtime 状态摘要;如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。"
"当前工具策略:\n{tool_policy_json}\n\n项目上下文如下。你正在执行后台 Agent loop 第 {loop_index} 轮。请基于目标、已有工具观察和当前上下文修正计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\n已有工具观察:\n{observations_json}\n\nJSON schema{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|memory.write|conversation.read|asset.list|project.index|project.diff|file.list|file.read|file.write|task.list|task.update|command.run_limited|preview.start|canvas.asset_generate|blackboard.write|agent.message|agent.delegate|agent.run_status\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}}memory.write 使用 {{\"scope\":\"agent|project|session|blackboard\",\"title\":\"标题\",\"content\":\"要沉淀的稳定结论\",\"mode\":\"append|overwrite\"}},其中 agent scope 只能写当前 Agent 自己的私有记忆,跨 Agent 共享请用 blackboard.write 或 agent.messageproject.diff 使用 {{\"checkpointId\":\"checkpoint id\"}},用于读取当前项目相对路径 diff 摘要;file.list 使用 {{\"path\":\"可选项目内相对目录或文件\"}},path 为空时列出项目摘要;file.read 使用 {{\"path\":\"项目内相对路径\"}}file.write 使用 {{\"path\":\"项目内相对路径\",\"content\":\"文件内容\"}}task.list input 可为空,用于读取 manifest 任务图、状态和 readyTaskIdstask.update 使用 {{\"taskId\":\"manifest taskId\",\"status\":\"pending|running|waiting-for-confirmation|completed|failed\"}}command.run_limited 使用 {{\"commandId\":\"game.static_smoke\"}},只支持本地静态自检;preview.start input 可为空,用于启动当前项目的 127.0.0.1 本地 HTTP 预览;canvas.asset_generate 使用 {{\"prompt\":\"要生成的美术素材描述\"}},通过配置的 External Editor API 生成首版素材并登记到 assetsblackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}}agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}}agent.delegate 使用 {{\"agentId\":\"目标 taskId\",\"task\":\"要委派的后台任务\",\"runId\":\"可选 run id\"}},用于把任务投递到另一个 Agent 的独立队列;agent.run_status 使用 {{\"agentId\":\"可选目标 taskId\",\"scope\":\"self|all\"}},用于读取自己或其他 Agent 的 Runtime 状态摘要;如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。"
);
let request = LlmRunRequest::new(vec![
LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()),
@@ -1741,22 +1741,11 @@ fn observe_agent_runtime_memory_write(
detail: None,
};
}
let _lock = match acquire_project_write_lock(root, "memory.write") {
Ok(lock) => lock,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "memory.write".to_string(),
status: "failed".to_string(),
summary: sanitize_agent_runtime_text(&error, 240),
detail: None,
};
}
};
let title = agent_runtime_tool_input_text(input, &["title", "topic"]);
let entry = agent_runtime_memory_write_entry(agent_id, &title, &content);
let overwrite = agent_runtime_tool_input_text(input, &["mode", "writeMode"])
.eq_ignore_ascii_case("overwrite");
let result = if scope == "agent" {
let target_agent_id = if scope == "agent" {
let target_agent_id = agent_runtime_tool_input_text(input, &["agentId", "targetAgentId"]);
let target_agent_id = if target_agent_id.trim().is_empty() {
agent_id.to_string()
@@ -1773,6 +1762,36 @@ fn observe_agent_runtime_memory_write(
}
}
};
if target_agent_id != agent_id {
return AgentRuntimeToolObservation {
tool: "memory.write".to_string(),
status: "blocked".to_string(),
summary: format!(
"Agent 私有记忆只能由本人写入:{agent_id} 不能写入 {target_agent_id}"
),
detail: Some(
"跨 Agent 共享稳定结论请使用 blackboard.write;给单个 Agent 留上下文请使用 agent.message。"
.to_string(),
),
};
}
Some(target_agent_id)
} else {
None
};
let _lock = match acquire_project_write_lock(root, "memory.write") {
Ok(lock) => lock,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "memory.write".to_string(),
status: "failed".to_string(),
summary: sanitize_agent_runtime_text(&error, 240),
detail: None,
};
}
};
let result = if scope == "agent" {
let target_agent_id = target_agent_id.unwrap_or_else(|| agent_id.to_string());
read_local_agent_memory_at(root, &target_agent_id)
.and_then(|existing| {
let next_content =
@@ -4478,7 +4497,7 @@ pub(crate) fn game_creator_role_agent_chat_system_prompt() -> &'static str {
}
pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt() -> &'static str {
"你是 Genarrative AI 游戏创作多智能体 Runtime 中的专业 Agent。你必须在白名单工具内规划行动:先给一句 thinkingSummary,再给短计划,再决定是否请求工具。只能请求 memory.read、memory.write、conversation.read、asset.list、project.index、project.diff、file.list、file.read、file.write、task.list、task.update、command.run_limited、preview.start、canvas.asset_generate、blackboard.write、agent.message、agent.delegate、agent.run_status。不要假装工具已执行;工具结果会由 Runtime 作为 observation 返回。只输出 JSON 对象,不要 markdown,不要泄露密钥。"
"你是 Genarrative AI 游戏创作多智能体 Runtime 中的专业 Agent。你必须在白名单工具内规划行动:先给一句 thinkingSummary,再给短计划,再决定是否请求工具。只能请求 memory.read、memory.write、conversation.read、asset.list、project.index、project.diff、file.list、file.read、file.write、task.list、task.update、command.run_limited、preview.start、canvas.asset_generate、blackboard.write、agent.message、agent.delegate、agent.run_status。Agent 私有记忆只能由本人写入,跨 Agent 共享稳定结论用 blackboard.write,给单个 Agent 留上下文用 agent.message。不要假装工具已执行;工具结果会由 Runtime 作为 observation 返回。只输出 JSON 对象,不要 markdown,不要泄露密钥。"
}
pub(crate) fn game_creator_agent_role_definition(
@@ -3019,6 +3019,95 @@ async fn background_agent_runtime_can_write_memory_and_project_files() {
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_blocks_cross_agent_private_memory_write() {
let root = unique_project_path();
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
write_project_permission_policy_at(
&root,
ProjectPermissionPolicy {
denied_commands: Vec::new(),
confirm_commands: Vec::new(),
agent_policies: BTreeMap::new(),
},
)
.expect("allow runtime writes");
let (sender, receiver) = mpsc::channel();
let plan_json = serde_json::json!({
"thinkingSummary": "想把策划结论交给美术 Agent",
"plan": ["尝试写美术 Agent 私有记忆"],
"actions": [
{
"tool": "memory.write",
"reason": "错误地尝试直接改另一个 Agent 的私有记忆",
"input": {
"scope": "agent",
"agentId": "art-asset-plan",
"title": "角色素材方向",
"content": "主角需要月光围裙和银色锅铲。"
}
}
],
"response": ""
})
.to_string();
let base_url = spawn_mock_llm_server_responses_with_capture(
vec![
plan_json,
"我会改用黑板或定向消息共享给美术 Agent。".to_string(),
],
Some(sender),
);
let _config_guard = write_test_local_config(format!(
r#"{{
"agentLlm": {{
"design-director": {{
"apiKey": "design-key",
"baseUrl": {base_url:?},
"model": "design-runtime-model",
"apiKind": "openai_responses"
}}
}}
}}"#
));
start_game_creator_agent_background_task_at(
&root,
"design-director",
"后台把角色素材方向交给美术",
"design-cross-memory-run",
)
.expect("start background task");
let plan_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("plan llm request");
assert!(plan_request.contains("Agent 私有记忆只能由本人写入"));
let second_request = receiver
.recv_timeout(Duration::from_secs(2))
.expect("second llm request");
assert!(second_request.contains(
"memory.writeblocked · Agent 私有记忆只能由本人写入:design-director 不能写入 art-asset-plan"
));
assert!(second_request.contains("blackboard.write"));
assert!(second_request.contains("agent.message"));
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
assert!(runtime.observations.iter().any(|item| item.contains(
"memory.writeblocked · Agent 私有记忆只能由本人写入:design-director 不能写入 art-asset-plan"
)));
let art_memory = read_local_agent_memory_at(&root, "art-asset-plan").expect("art memory");
assert!(!art_memory.content.contains("月光围裙"));
let design_memory =
read_local_agent_memory_at(&root, "design-director").expect("design memory");
assert!(!design_memory.content.contains("月光围裙"));
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
assert!(!agent_db.contains("\"recordType\":\"agent.runtime.memory.write\""));
assert!(!agent_db.contains("\"targetAgentId\":\"art-asset-plan\""));
fs::remove_dir_all(root).ok();
}
#[tokio::test]
async fn background_agent_runtime_write_tools_respect_project_policy() {
let root = unique_project_path();
@@ -4064,6 +4064,7 @@
- 2026-07-10 调整:后台 Agent 工具命中确认策略时不再当作 `blocked` observation 继续收尾,而是把当前 Runtime 写成 `status/phase = waiting-for-confirmation``waitingOn` 固定为等待开发者确认工具动作,`recentToolCalls`、事件流、任务记录和 `taskQueue.waitingForConfirmation` 都保留该事实;同一 Agent 的后台 drain 暂停,不继续消费后续 pending 任务。命中拒绝策略仍使用 `blocked` observation 交回 Agent 修正计划。
- 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 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