补充Agent私有记忆写入
新增单Agent对话面板私有记忆追加入口 补充Tauri本地Agent记忆写入命令和策略测试 同步AI游戏创作App技术方案和共享决策记录
This commit is contained in:
@@ -1590,6 +1590,18 @@ fn read_local_agent_memory(
|
||||
read_local_agent_memory_at(root, task_id.trim())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn write_local_agent_memory(
|
||||
project_path: String,
|
||||
task_id: String,
|
||||
content: String,
|
||||
) -> Result<LocalAgentMemoryResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "memory.write")?;
|
||||
let _lock = acquire_project_write_lock(root, "memory.write")?;
|
||||
write_local_agent_memory_at(root, task_id.trim(), &content)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn write_local_game_memory(
|
||||
project_path: String,
|
||||
@@ -7044,6 +7056,27 @@ fn read_local_agent_memory_at(
|
||||
}
|
||||
}
|
||||
|
||||
fn write_local_agent_memory_at(
|
||||
root: &Path,
|
||||
task_id: &str,
|
||||
content: &str,
|
||||
) -> Result<LocalAgentMemoryResult, String> {
|
||||
let relative_path = agent_role_memory_relative_path_for_task(task_id)?;
|
||||
let path = resolve_local_project_path(root, &relative_path)?;
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent)
|
||||
.map_err(|error| format!("创建 Agent 记忆目录失败:{}: {error}", parent.display()))?;
|
||||
}
|
||||
fs::write(&path, content)
|
||||
.map_err(|error| format!("写入 Agent 记忆失败:{}: {error}", path.display()))?;
|
||||
Ok(LocalAgentMemoryResult {
|
||||
task_id: task_id.to_string(),
|
||||
path: path.to_string_lossy().into_owned(),
|
||||
content: content.to_string(),
|
||||
exists: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn write_local_game_memory_at(
|
||||
root: &Path,
|
||||
scope: &str,
|
||||
@@ -8689,6 +8722,7 @@ fn main() {
|
||||
delete_local_project_file,
|
||||
read_local_game_memory,
|
||||
read_local_agent_memory,
|
||||
write_local_agent_memory,
|
||||
write_local_game_memory,
|
||||
delete_local_game_memory,
|
||||
read_local_conversation,
|
||||
@@ -11648,6 +11682,30 @@ mod tests {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_agent_memory_writes_private_memory_by_task_id() {
|
||||
let root = unique_project_path();
|
||||
|
||||
let written = write_local_agent_memory_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"# 策划 Director 私有记忆\n- 保留轻量像素风\n",
|
||||
)
|
||||
.expect("write agent memory");
|
||||
assert_eq!(written.task_id, "design-director");
|
||||
assert!(written.path.ends_with("memory/agents/design/director.md"));
|
||||
assert!(written.exists);
|
||||
assert_eq!(
|
||||
written.content,
|
||||
"# 策划 Director 私有记忆\n- 保留轻量像素风\n"
|
||||
);
|
||||
|
||||
let read = read_local_agent_memory_at(&root, "design-director").expect("read agent memory");
|
||||
assert_eq!(read.content, "# 策划 Director 私有记忆\n- 保留轻量像素风\n");
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_memory_reads_respect_project_policy() {
|
||||
let root = unique_project_path();
|
||||
@@ -11677,6 +11735,32 @@ mod tests {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_agent_memory_writes_respect_project_policy() {
|
||||
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!["memory.write".to_string()],
|
||||
confirm_commands: Vec::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
|
||||
let error = write_local_agent_memory(
|
||||
root.to_string_lossy().into_owned(),
|
||||
"design-director".to_string(),
|
||||
"denied memory".to_string(),
|
||||
)
|
||||
.expect_err("agent memory write denied");
|
||||
|
||||
assert!(error.contains("项目权限策略拒绝执行:memory.write"));
|
||||
assert!(!root.join("memory/agents/design/director.md").exists());
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_game_memory_rejects_unknown_scope() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -2867,6 +2867,8 @@ function isProjectPolicyConfirmableCommandId(value: string) {
|
||||
'file.list',
|
||||
'file.read',
|
||||
'memory.read',
|
||||
'memory.write',
|
||||
'memory.delete',
|
||||
'asset.register',
|
||||
'asset.list',
|
||||
'task.list',
|
||||
@@ -4111,6 +4113,14 @@ export function App() {
|
||||
]);
|
||||
}
|
||||
}
|
||||
if (pending.commandId === 'memory.write') {
|
||||
setMemoryStatus((current) =>
|
||||
current === '等待确认' ? '已取消保存项目记忆' : current,
|
||||
);
|
||||
setAgentMemoryStatus((current) =>
|
||||
current === '等待确认' ? '已取消保存 Agent 私有记忆' : current,
|
||||
);
|
||||
}
|
||||
if (
|
||||
pending.commandId === 'file.list' ||
|
||||
pending.commandId === 'file.read' ||
|
||||
@@ -4852,6 +4862,100 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSelectedAgentPrivateMemory(
|
||||
agent: AgentStatusCard,
|
||||
content: string,
|
||||
skipPolicyConfirm = false,
|
||||
) {
|
||||
if (!agent || !content || agentConversationSavingRef.current) {
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||||
if (!invoke) {
|
||||
setAgentMemoryStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
}
|
||||
if (!nextProjectPath) {
|
||||
setAgentMemoryStatus('请先初始化本地项目');
|
||||
return;
|
||||
}
|
||||
const saveVersion = agentConversationLoadVersionRef.current;
|
||||
try {
|
||||
if (!skipPolicyConfirm) {
|
||||
const policyView = await invoke<ProjectPermissionPolicyView>(
|
||||
'read_project_permission_policy',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
if (policyView.policy.deniedCommands.includes('memory.write')) {
|
||||
const message = '项目权限策略拒绝执行:memory.write';
|
||||
markProjectPolicyDenied('memory.write', message);
|
||||
setAgentMemoryStatus(message);
|
||||
setCommandLog((current) => [
|
||||
...current,
|
||||
'permission.deny memory.write',
|
||||
]);
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: message },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
if (policyView.policy.confirmCommands.includes('memory.write')) {
|
||||
requestProjectPolicyConfirmation(
|
||||
'memory.write',
|
||||
nextProjectPath,
|
||||
`写入 ${agent.title} Agent 私有记忆`,
|
||||
() => void saveSelectedAgentPrivateMemory(agent, content, true),
|
||||
);
|
||||
setAgentMemoryStatus('等待确认');
|
||||
setMessages((current) => [
|
||||
...current,
|
||||
{ role: 'assistant', text: '准备保存 Agent 私有记忆。' },
|
||||
]);
|
||||
return;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
setAgentMemoryStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
return;
|
||||
}
|
||||
agentConversationSavingRef.current = true;
|
||||
setAgentConversationSaving(true);
|
||||
setAgentConversationInput('');
|
||||
setAgentMemoryStatus('正在保存');
|
||||
try {
|
||||
const result = await invoke<LocalAgentMemoryResult>(
|
||||
'write_local_agent_memory',
|
||||
{
|
||||
projectPath: nextProjectPath,
|
||||
taskId: agent.id,
|
||||
content: appendMemoryContent(agentMemoryContent, content),
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentMemoryContent(result.content);
|
||||
setAgentMemoryStatus(`已追加私有记忆:${result.path}`);
|
||||
setAgentConversationStatus(`已写入 ${agent.title} 私有记忆。`);
|
||||
setCommandLog((current) => [...current, 'memory.agent.write']);
|
||||
} catch (error) {
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentConversationInput(content);
|
||||
setAgentMemoryStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
agentConversationSavingRef.current = false;
|
||||
setAgentConversationSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAgentConversationSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
const agent = selectedAgent;
|
||||
@@ -4862,6 +4966,15 @@ export function App() {
|
||||
void saveAgentConversationMessage(agent, content);
|
||||
}
|
||||
|
||||
function handleAgentPrivateMemorySubmit() {
|
||||
const agent = selectedAgent;
|
||||
const content = agentConversationInput.trim();
|
||||
if (!agent || !content || agentConversationSavingRef.current) {
|
||||
return;
|
||||
}
|
||||
void saveSelectedAgentPrivateMemory(agent, content);
|
||||
}
|
||||
|
||||
function showChatHelp() {
|
||||
setCommandLog((current) => [...current, 'help.show']);
|
||||
setMessages((current) => [
|
||||
@@ -5070,7 +5183,7 @@ export function App() {
|
||||
...current,
|
||||
{
|
||||
role: 'assistant',
|
||||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、file.list、file.read、memory.read、asset.register、asset.list、task.list、agent.run_status、agent.kill、agent.retry、agent.resume、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
text: '当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、agent.run_status、agent.kill、agent.retry、agent.resume、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
},
|
||||
]);
|
||||
return;
|
||||
@@ -10362,6 +10475,13 @@ export function App() {
|
||||
<button type="submit" disabled={agentConversationSaving}>
|
||||
发送
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={agentConversationSaving}
|
||||
onClick={handleAgentPrivateMemorySubmit}
|
||||
>
|
||||
记入记忆
|
||||
</button>
|
||||
</form>
|
||||
<p className="status-line">{agentConversationStatus}</p>
|
||||
</section>
|
||||
|
||||
@@ -4519,6 +4519,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
updatedAt: number;
|
||||
}> = [];
|
||||
let agentConversationReadCount = 0;
|
||||
let agentMemoryContent = '# 策划 Director 私有记忆\n- 保留轻量像素风\n';
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'append_local_permission_log') {
|
||||
@@ -4549,7 +4550,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
return {
|
||||
taskId: args?.taskId,
|
||||
path: '/tmp/authorized-game/memory/agents/design/director.md',
|
||||
content: '# 策划 Director 私有记忆\n- 保留轻量像素风\n',
|
||||
content: agentMemoryContent,
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
if (command === 'write_local_agent_memory') {
|
||||
agentMemoryContent = String(args?.content ?? '');
|
||||
return {
|
||||
taskId: args?.taskId,
|
||||
path: '/tmp/authorized-game/memory/agents/design/director.md',
|
||||
content: agentMemoryContent,
|
||||
exists: true,
|
||||
};
|
||||
}
|
||||
@@ -4733,6 +4743,24 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
agentId: null,
|
||||
},
|
||||
});
|
||||
fireEvent.change(screen.getByLabelText('Agent 对话内容'), {
|
||||
target: { value: '稳定结论:锅铲音效要跟随连击节奏' },
|
||||
});
|
||||
fireEvent.click(
|
||||
within(agentDialog).getByRole('button', { name: '记入记忆' }),
|
||||
);
|
||||
expect(
|
||||
await screen.findByText('已写入 拆解创作方向 私有记忆。'),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByLabelText('Agent 私有记忆').textContent).toContain(
|
||||
'锅铲音效要跟随连击节奏',
|
||||
);
|
||||
expect(invoke).toHaveBeenCalledWith('write_local_agent_memory', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
taskId: 'design-director',
|
||||
content:
|
||||
'# 策划 Director 私有记忆\n- 保留轻量像素风\n- 稳定结论:锅铲音效要跟随连击节奏\n',
|
||||
});
|
||||
agentMessages.push({
|
||||
schemaVersion: 'game-creator-conversation.v1',
|
||||
role: 'assistant',
|
||||
@@ -10080,7 +10108,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、file.list、file.read、memory.read、asset.register、asset.list、task.list、agent.run_status、agent.kill、agent.retry、agent.resume、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、agent.run_status、agent.kill、agent.retry、agent.resume、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText('project.policy_write')).toBeNull();
|
||||
|
||||
@@ -3858,6 +3858,7 @@
|
||||
- 2026-07-03 调整:主窗口 header 常驻项目摘要只从当前已加载的 manifest / trace 派生任务完成数、ready 数、资产来源分布和最近命令结果;未选择工作区时不显示,不为了摘要额外触发 Tauri 读取或写入,也不把任务、文件、run history 或预览开发面板搬进普通用户窗口。
|
||||
- 2026-07-03 调整:`/llm-status` 读取到的 agent 级 LLM 配置状态可回填到主窗口 Agent 状态列表和单 Agent 对话头部,显示 provider 类型、模型、流式开关和 API Key 是否已读取;密钥本体仍不能进入聊天、状态列表、manifest、trace 或本地项目文件。
|
||||
- 2026-07-03 调整:开发窗口日志面板提供 `.agent/logs/command.log`、`.agent/logs/preview.log` 和 `.agent/logs/agent.log` 的只读查看入口,复用 `file.read` 授权策略;普通用户窗口仍只通过聊天 `/read` 和摘要消息查看需要的日志,不新增日志面板。
|
||||
- 2026-07-03 调整:单 Agent 对话面板允许用户把当前输入手动追加到该 agent 的 `memory/agents/<group>/<role>.md` 私有记忆;写入复用 `memory.write` 项目策略、项目锁和 Tauri 本地目录能力,不把普通对话流水自动混入私有记忆。
|
||||
- 2026-06-25 调整:本地 HTTP 预览静态 `HEAD` 必须返回与 `GET` 相同的真实 `Content-Length`,但不返回 body;浏览器、图片、音频和视频探测不能拿到 `Content-Length: 0` 的假响应。
|
||||
- 2026-06-25 调整:普通用户通过聊天输入 `/run` 触发待确认 `game.run_local`,确认后只能复用白名单 `game.static_smoke` 自检当前 `game/index.html`,通过后启动 `127.0.0.1` 本地 HTTP 预览。独立执行 `game.static_smoke` 时如果已有 `.agent/run.latest.json`,必须追加 `Playtest / game.static_smoke` trace step,避免“运行了代码但编排 trace 不可见”。
|
||||
- 2026-06-25 调整:普通用户通过聊天输入 `/trace` 触发只读 `agent.trace_read`,读取 `.agent/run.latest.json` 并在聊天里摘要 loop 轮次、stopReason、nextStep、active / carry-over 任务、repairRoutes、agent 建议命令和最近 step。trace 面板仍只在开发窗口展示,普通用户窗口不新增面板。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user