From e38ad19efa1a374238e0d9cad52563425de63fa7 Mon Sep 17 00:00:00 2001 From: AIGameCreator App Date: Fri, 10 Jul 2026 00:07:46 +0800 Subject: [PATCH] =?UTF-8?q?=E8=A1=A5=E9=BD=90Agent=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=80=81=E7=8A=B6=E6=80=81=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主窗口 Agent 状态列表批量读取并展示 Runtime 状态和最近任务。 单 Agent 对话与 Runtime 使用任务 ID 作为规范 Agent ID。 兼容旧 group-role Agent 别名并映射到规范任务 ID。 补充 Rust 与前端测试,更新 Runtime 文档和共享决策记录。 --- .../src-tauri/src/agent.rs | 97 ++++++++--- .../src-tauri/src/commands.rs | 20 ++- .../src-tauri/src/main.rs | 1 + .../src-tauri/src/tests.rs | 89 ++++++++++ apps/ai-game-creator-shell/src/App.tsx | 164 +++++++++++++++++- .../tests/appSurface.test.ts | 145 +++++++++++++++- .../shared-memory/decision-log.md | 1 + ...案】AI游戏创作智能体App实施计划-2026-06-24.md | 10 +- 8 files changed, 485 insertions(+), 42 deletions(-) diff --git a/apps/ai-game-creator-shell/src-tauri/src/agent.rs b/apps/ai-game-creator-shell/src-tauri/src/agent.rs index 5ea8d1300..9be94b00e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/agent.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/agent.rs @@ -193,9 +193,9 @@ pub(crate) fn read_game_creator_agent_runtime_at( ) -> Result { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; validate_project_root(root)?; - let session_path = game_creator_agent_runtime_session_path(root, agent_id); - let event_path = game_creator_agent_runtime_event_path(root, agent_id); - let task_path = game_creator_agent_runtime_task_path(root, agent_id); + let session_path = game_creator_agent_runtime_session_path(root, &agent_id); + let event_path = game_creator_agent_runtime_event_path(root, &agent_id); + let task_path = game_creator_agent_runtime_task_path(root, &agent_id); let mut state = match fs::read_to_string(&session_path) { Ok(content) => serde_json::from_str::(&content).map_err(|error| { format!( @@ -204,7 +204,7 @@ pub(crate) fn read_game_creator_agent_runtime_at( ) })?, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - default_game_creator_agent_runtime_state(agent_id, "") + default_game_creator_agent_runtime_state(&agent_id, "") } Err(error) => { return Err(format!( @@ -213,7 +213,7 @@ pub(crate) fn read_game_creator_agent_runtime_at( )); } }; - normalize_game_creator_agent_runtime_state(&mut state, agent_id); + normalize_game_creator_agent_runtime_state(&mut state, &agent_id); let recent_events = read_recent_game_creator_agent_runtime_events(&event_path)?; let recent_tasks = read_recent_game_creator_agent_runtime_tasks(&task_path)?; Ok(AgentRuntimeResult { @@ -226,13 +226,40 @@ pub(crate) fn read_game_creator_agent_runtime_at( }) } +pub(crate) fn read_game_creator_agent_runtimes_at( + root: &Path, +) -> Result, String> { + validate_project_root(root)?; + let manifest = read_manifest_for_project(root)?; + let mut agent_ids = std::collections::BTreeSet::new(); + for task in manifest.tasks { + if let Ok(agent_id) = normalize_game_creator_runtime_agent_id(&task.id) { + agent_ids.insert(agent_id); + } + let alias = + game_creator_agent_role_alias_id(game_creation_agent_group_id(&task.group), &task.role); + if let Ok(agent_id) = normalize_game_creator_runtime_agent_id(&alias) { + agent_ids.insert(agent_id); + } + } + for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { + for role in group.roles { + agent_ids.insert(role.task_id.to_string()); + } + } + agent_ids + .into_iter() + .map(|agent_id| read_game_creator_agent_runtime_at(root, &agent_id)) + .collect() +} + pub(crate) fn start_game_creator_agent_background_task_at( root: &Path, agent_id: &str, task: &str, run_id: &str, ) -> Result { - let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?.to_string(); + let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; validate_project_root(root)?; let task = task.trim(); if task.is_empty() { @@ -1010,13 +1037,13 @@ pub(crate) fn start_game_creator_agent_runtime_task_at( ) -> Result { let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; validate_project_root(root)?; - let run_id = normalize_game_creator_agent_runtime_run_id(agent_id, run_id); + let run_id = normalize_game_creator_agent_runtime_run_id(&agent_id, run_id); let task = task.trim(); if task.is_empty() { return Err("Agent Runtime 任务不能为空".to_string()); } let runtime_task = sanitize_agent_runtime_text(task, 180); - let mut state = default_game_creator_agent_runtime_state(agent_id, &run_id); + let mut state = default_game_creator_agent_runtime_state(&agent_id, &run_id); state.source = source.trim().to_string(); state.status = "running".to_string(); state.phase = "planning".to_string(); @@ -1144,14 +1171,36 @@ pub(crate) fn fail_game_creator_agent_runtime_turn_at( Ok(state) } -fn normalize_game_creator_runtime_agent_id(agent_id: &str) -> Result<&str, String> { +pub(crate) fn normalize_game_creator_runtime_agent_id(agent_id: &str) -> Result { let agent_id = agent_id.trim(); if agent_id.is_empty() { return Err("Agent ID 不能为空".to_string()); } - game_creator_agent_role_definition(agent_id) - .ok_or_else(|| format!("未知 Agent:{agent_id}"))?; - Ok(agent_id) + if let Some((_group, role)) = game_creator_agent_role_definition(agent_id) { + return Ok(role.task_id.to_string()); + } + for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS { + for role in group.roles { + if game_creator_agent_role_alias_id(group.id, role.role) == agent_id { + return Ok(role.task_id.to_string()); + } + } + } + Err(format!("未知 Agent:{agent_id}")) +} + +fn game_creator_agent_role_alias_id(group: &str, role: &str) -> String { + format!("{group}-{role}") + .to_lowercase() + .chars() + .map(|character| { + if character.is_ascii_alphanumeric() || character == '_' || character == '-' { + character + } else { + '-' + } + }) + .collect() } fn normalize_game_creator_agent_runtime_run_id(agent_id: &str, run_id: &str) -> String { @@ -1770,20 +1819,17 @@ fn build_game_creator_role_agent_context( root: &Path, agent_id: &str, ) -> Result<(GameCreatorLlmConfig, String, String), String> { - let agent_id = agent_id.trim(); - if agent_id.is_empty() { - return Err("Agent ID 不能为空".to_string()); - } + let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?; validate_project_root(root)?; - let (group_definition, role_definition) = game_creator_agent_role_definition(agent_id) + let (group_definition, role_definition) = game_creator_agent_role_definition(&agent_id) .ok_or_else(|| format!("未知 Agent:{agent_id}"))?; let short_memory = read_optional_text(&root.join("memory/session.md"))?; let long_memory = read_optional_text(&root.join("memory/project.md"))?; let project_blackboard = read_optional_text(&root.join(PROJECT_BLACKBOARD_MEMORY_PATH))?; - let agent_memory = read_local_agent_memory_at(root, agent_id)?.content; + let agent_memory = read_local_agent_memory_at(root, &agent_id)?.content; let asset_context = render_local_asset_prompt_context(root)?; - let conversation_context = render_local_conversation_prompt_context(root, Some(agent_id))?; + let conversation_context = render_local_conversation_prompt_context(root, Some(&agent_id))?; let identity = format!( "你当前是 {} / {},taskId={},角色代号={}。请只以这个专业 Agent 的身份回应。", group_definition.label, role_definition.role, role_definition.task_id, role_definition.id @@ -1810,7 +1856,7 @@ fn build_game_creator_role_agent_context( .join("\n\n"); let app_config = load_game_creator_app_config()?; - let llm = resolve_game_creator_llm_config_for_agent(&app_config, agent_id); + let llm = resolve_game_creator_llm_config_for_agent(&app_config, &agent_id); Ok((llm, format!("agentLlm.{agent_id}"), context)) } @@ -4204,6 +4250,17 @@ pub(crate) fn game_creation_agent_group_from_id( } } +fn game_creation_agent_group_id(group: &GameCreationAppAgentGroup) -> &'static str { + match group { + GameCreationAppAgentGroup::Design => "design", + GameCreationAppAgentGroup::Balance => "balance", + GameCreationAppAgentGroup::Art => "art", + GameCreationAppAgentGroup::Audio => "audio", + GameCreationAppAgentGroup::Code => "code", + GameCreationAppAgentGroup::Publishing => "publishing", + } +} + pub(crate) fn append_static_smoke_step( root: &Path, prompt: &str, diff --git a/apps/ai-game-creator-shell/src-tauri/src/commands.rs b/apps/ai-game-creator-shell/src-tauri/src/commands.rs index a08318348..abebf86a2 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/commands.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/commands.rs @@ -244,10 +244,11 @@ pub(crate) async fn chat_with_game_creator_role_agent( prompt: String, ) -> Result { let root = Path::new(project_path.trim()); + let agent_id = normalize_game_creator_runtime_agent_id(agent_id.trim())?; enforce_project_permission_policy(root, "conversation.read")?; enforce_project_permission_policy(root, "conversation.write")?; let _lock = acquire_project_write_lock(root, "conversation.write")?; - chat_with_game_creator_role_agent_runtime_at(root, agent_id.trim(), prompt.trim(), "") + chat_with_game_creator_role_agent_runtime_at(root, &agent_id, prompt.trim(), "") .await .map(|(reply, _runtime)| reply) } @@ -261,7 +262,7 @@ pub(crate) async fn chat_with_game_creator_role_agent_stream( run_id: String, ) -> Result { let project_path = project_path.trim().to_string(); - let agent_id = agent_id.trim().to_string(); + let agent_id = normalize_game_creator_runtime_agent_id(agent_id.trim())?; let run_id = run_id.trim().to_string(); let root = Path::new(project_path.as_str()); enforce_project_permission_policy(root, "conversation.read")?; @@ -402,6 +403,15 @@ pub(crate) fn read_game_creator_agent_runtime( read_game_creator_agent_runtime_at(root, agent_id.trim()) } +#[tauri::command] +pub(crate) fn read_game_creator_agent_runtimes( + project_path: String, +) -> Result, String> { + let root = Path::new(project_path.trim()); + enforce_project_permission_policy(root, "conversation.read")?; + read_game_creator_agent_runtimes_at(root) +} + #[tauri::command] pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus { check_game_creator_llm_config_from_config() @@ -672,8 +682,9 @@ pub(crate) fn read_local_agent_memory( task_id: String, ) -> Result { let root = Path::new(project_path.trim()); + let task_id = normalize_game_creator_runtime_agent_id(task_id.trim())?; enforce_project_permission_policy(root, "memory.read")?; - read_local_agent_memory_at(root, task_id.trim()) + read_local_agent_memory_at(root, &task_id) } #[tauri::command] @@ -683,9 +694,10 @@ pub(crate) fn write_local_agent_memory( content: String, ) -> Result { let root = Path::new(project_path.trim()); + let task_id = normalize_game_creator_runtime_agent_id(task_id.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) + write_local_agent_memory_at(root, &task_id, &content) } #[tauri::command] diff --git a/apps/ai-game-creator-shell/src-tauri/src/main.rs b/apps/ai-game-creator-shell/src-tauri/src/main.rs index c81d5c74f..b574fa48e 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/main.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/main.rs @@ -1057,6 +1057,7 @@ fn main() { chat_with_game_creator_role_agent_stream, start_game_creator_agent_runtime_task, read_game_creator_agent_runtime, + read_game_creator_agent_runtimes, check_game_creator_llm_config, read_game_creator_app_config, write_game_creator_app_config, diff --git a/apps/ai-game-creator-shell/src-tauri/src/tests.rs b/apps/ai-game-creator-shell/src-tauri/src/tests.rs index 765351a35..cbcf3de09 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/tests.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/tests.rs @@ -1087,6 +1087,95 @@ async fn chat_with_game_creator_agent_uses_project_context_and_chat_llm_route() fs::remove_dir_all(root).ok(); } +#[tokio::test] +async fn role_agent_legacy_alias_maps_to_canonical_task_runtime_and_route() { + let root = unique_project_path(); + init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init"); + write_local_agent_memory_at(&root, "art-asset-plan", "私有记忆:先列角色规范图素材") + .expect("write asset memory"); + append_local_conversation_message_at( + &root, + Some("art-asset-plan"), + LocalConversationMessage { + role: "user".to_string(), + content: "上一轮:美术资产要从主角规范图开始".to_string(), + agent_id: None, + }, + ) + .expect("append asset conversation"); + let runtime = start_game_creator_agent_runtime_task_at( + &root, + "art-asset", + "排队规划美术资产", + "alias-runtime-run", + "agent-background-task", + "测试旧别名兼容", + vec!["确认旧别名会落到规范 taskId".to_string()], + ) + .expect("start alias runtime task"); + + assert_eq!(runtime.agent_id, "art-asset-plan"); + assert_eq!(runtime.task_id, "art-asset-plan"); + assert_eq!(runtime.session_id, "agent-session-art-asset-plan"); + let alias_read = + read_game_creator_agent_runtime_at(&root, "art-asset").expect("read alias runtime"); + assert_eq!(alias_read.state.agent_id, "art-asset-plan"); + assert!(alias_read + .session_path + .ends_with(".agent/runtime/agents/art-asset-plan.json")); + let runtimes = read_game_creator_agent_runtimes_at(&root).expect("read all runtimes"); + assert!(runtimes + .iter() + .any(|result| result.state.agent_id == "design-director")); + assert!(runtimes + .iter() + .any(|result| result.state.agent_id == "art-asset-plan")); + + let (sender, receiver) = mpsc::channel(); + let base_url = spawn_mock_llm_server_responses_with_capture( + vec!["会先按规范图列出资产清单。".to_string()], + Some(sender), + ); + let _config_guard = write_test_local_config(format!( + r#"{{ + "llm": {{ + "apiKey": "global-key", + "baseUrl": "https://global.example.test/v1", + "model": "global-model", + "apiKind": "openai_responses" + }}, + "agentLlm": {{ + "art-asset-plan": {{ + "apiKey": "asset-key", + "baseUrl": {base_url:?}, + "model": "asset-chat-model", + "apiKind": "openai_responses" + }} + }} +}}"# + )); + + let reply = chat_with_game_creator_role_agent_at(&root, "art-asset", "请继续推进美术资产") + .await + .expect("role alias chat reply"); + + assert_eq!(reply.reply_text, "会先按规范图列出资产清单。"); + let request = receiver + .recv_timeout(Duration::from_secs(1)) + .expect("captured alias role chat request"); + assert!(request.contains("POST /responses HTTP/1.1")); + assert!(request.contains("asset-chat-model")); + assert!(request.contains("美术组 / Asset")); + assert!(request.contains("taskId=art-asset-plan")); + assert!(request.contains("私有记忆:先列角色规范图素材")); + assert!(request.contains("上一轮:美术资产要从主角规范图开始")); + assert!(request.contains("请继续推进美术资产")); + assert!(!request.contains("global-model")); + assert!(!request.contains("global-key")); + + fs::remove_dir_all(root).ok(); +} + #[tokio::test] async fn chat_with_game_creator_role_agent_uses_agent_context_and_route() { let root = unique_project_path(); diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 34474ebcb..0cec6d8a5 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -562,7 +562,7 @@ function AgentRuntimeStatusPanel({ 最近任务 {recentTasks.map((task) => ( - {`${task.status} / ${task.phase} · ${task.task || task.currentAction}`} + {formatAgentRecentRuntimeTask(task)} ))} @@ -685,6 +685,12 @@ interface AgentStatusCard { inputPaths: string[]; outputPaths: string[]; toolCalls: GameCreationAgentToolCallTrace[]; + runtimeStatus: string | null; + runtimePhase: string | null; + runtimeAction: string | null; + runtimeTask: string | null; + runtimeRunId: string | null; + runtimeRecentTasks: AgentRuntimeTaskRecord[]; } type AgentTaskGraphState = 'active' | 'carried' | 'ready'; @@ -9885,9 +9891,7 @@ function summarizeLimitedLocalCommands( } function agentConversationId(task: GameCreationAppTaskState) { - return `${task.group}-${task.role}` - .toLowerCase() - .replace(/[^a-z0-9_-]/g, '-'); + return task.id; } function taskStatusFromTraceStep( @@ -9957,6 +9961,7 @@ function taskRowsForAgentStatus( export function deriveAgentStatusCards( nextManifest: GameCreationAppManifest, trace: GameCreationAgentRunTrace | null, + runtimeByAgentId: Record = {}, ): AgentStatusCard[] { const latestByTaskId = new Map(); const latestByGroupRole = new Map(); @@ -9973,8 +9978,10 @@ export function deriveAgentStatusCards( const latestStep = latestByTaskId.get(task.id) ?? latestByGroupRole.get(agentGroupRoleKey(task.group, task.role) ?? ''); + const cardId = agentConversationId(task); + const runtime = runtimeByAgentId[cardId] ?? runtimeByAgentId[task.id] ?? null; return { - id: agentConversationId(task), + id: cardId, taskId: task.id, title: task.title, group: task.group, @@ -9989,6 +9996,12 @@ export function deriveAgentStatusCards( inputPaths: latestStep?.inputPaths ?? [], outputPaths: latestStep?.outputPaths ?? [], toolCalls: latestStep?.toolCalls ?? [], + runtimeStatus: runtime?.status ?? null, + runtimePhase: runtime?.phase ?? null, + runtimeAction: runtime?.currentAction ?? null, + runtimeTask: runtime?.currentTask ?? null, + runtimeRunId: runtime?.runId ?? null, + runtimeRecentTasks: runtime?.recentTasks ?? [], }; }); } @@ -10012,10 +10025,16 @@ function sameAgentStatusCard(left: AgentStatusCard, right: AgentStatusCard) { left.pass === right.pass && left.phase === right.phase && left.lifecycleStatus === right.lifecycleStatus && + left.runtimeStatus === right.runtimeStatus && + left.runtimePhase === right.runtimePhase && + left.runtimeAction === right.runtimeAction && + left.runtimeTask === right.runtimeTask && + left.runtimeRunId === right.runtimeRunId && left.hasRecentEvidence === right.hasRecentEvidence && left.taskGraphState === right.taskGraphState && sameStringArray(left.inputPaths, right.inputPaths) && sameStringArray(left.outputPaths, right.outputPaths) && + sameAgentRuntimeTasks(left.runtimeRecentTasks, right.runtimeRecentTasks) && left.toolCalls.length === right.toolCalls.length && left.toolCalls.every((toolCall, index) => { const other = right.toolCalls[index]; @@ -10031,6 +10050,27 @@ function sameAgentStatusCard(left: AgentStatusCard, right: AgentStatusCard) { ); } +function sameAgentRuntimeTasks( + left: AgentRuntimeTaskRecord[], + right: AgentRuntimeTaskRecord[], +) { + return ( + left.length === right.length && + left.every((task, index) => { + const other = right[index]; + return ( + other && + task.runId === other.runId && + task.status === other.status && + task.phase === other.phase && + task.task === other.task && + task.currentAction === other.currentAction && + task.updatedAt === other.updatedAt + ); + }) + ); +} + function hasProjectFile(files: LocalProjectFileEntry[], path: string) { return files.some((file) => file.kind === 'file' && file.path === path); } @@ -10762,6 +10802,25 @@ function formatAgentCardLlmStatus( ].join(' · '); } +function formatAgentCardRuntimeStatus(agent: AgentStatusCard) { + if (!agent.runtimeStatus) { + return null; + } + return [ + `Runtime:${agent.runtimeStatus} / ${agent.runtimePhase ?? '-'}`, + agent.runtimeAction, + agent.runtimeRunId ? `run ${agent.runtimeRunId}` : null, + ] + .filter(Boolean) + .join(' · '); +} + +function formatAgentRecentRuntimeTask(task: AgentRuntimeTaskRecord) { + return `${task.status} / ${task.phase} · ${ + task.task || task.currentAction || task.runId + }`; +} + function formatAgentDialogLlmStatus( status: GameCreatorLlmConfigStatus | null, agent: AgentStatusCard, @@ -10806,8 +10865,18 @@ function summarizeAgentStatusCardsForChat( ? `编排 ${agentTaskGraphStateLabels[agent.taskGraphState]}` : null, formatAgentCardLlmStatus(status, agent), + formatAgentCardRuntimeStatus(agent), ].filter(Boolean); - return `- ${parts.join(' · ')}\n ${agent.summary}`; + const latestRuntimeTask = agent.runtimeRecentTasks.at(-1); + return [ + `- ${parts.join(' · ')}`, + ` ${agent.summary}`, + latestRuntimeTask + ? ` 最近任务:${formatAgentRecentRuntimeTask(latestRuntimeTask)}` + : null, + ] + .filter(Boolean) + .join('\n'); }) .join('\n')}`; } @@ -11420,6 +11489,9 @@ export function App() { const [agentRunHistoryLoadingMore, setAgentRunHistoryLoadingMore] = useState(false); const [agentRunStatus, setAgentRunStatus] = useState('未运行'); + const [agentRuntimeById, setAgentRuntimeById] = useState< + Record + >({}); const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false); const [llmConfigStatus, setLlmConfigStatus] = useState(null); @@ -12370,6 +12442,7 @@ export function App() { setProjectCheckpoints([]); setAgentRunHistory([]); setAgentRunHistoryFiles([]); + setAgentRuntimeById({}); setMessages(conversationMessages); setConversationVisibleCount(CONVERSATION_INITIAL_VISIBLE_COUNT); savedConversationProjectPathRef.current = openedProject.projectPath; @@ -12483,7 +12556,9 @@ export function App() { }, ); if (agentConversationLoadVersionRef.current === loadVersion) { - setAgentConversationRuntime(agentRuntimeStateFromResult(runtime)); + const nextRuntime = agentRuntimeStateFromResult(runtime); + setAgentConversationRuntime(nextRuntime); + rememberAgentRuntimeState(nextRuntime); setAgentConversationRuntimeError(''); } } catch (error) { @@ -12729,6 +12804,7 @@ export function App() { } if (payload.runtimeState) { setAgentConversationRuntime(payload.runtimeState); + rememberAgentRuntimeState(payload.runtimeState); setAgentConversationRuntimeError(''); } if (payload.status === 'started') { @@ -12800,7 +12876,9 @@ export function App() { }, ); if (agentConversationLoadVersionRef.current === saveVersion) { - setAgentConversationRuntime(agentRuntimeStateFromResult(runtime)); + const nextRuntime = agentRuntimeStateFromResult(runtime); + setAgentConversationRuntime(nextRuntime); + rememberAgentRuntimeState(nextRuntime); setAgentConversationRuntimeError(''); } } catch (error) { @@ -12949,7 +13027,9 @@ export function App() { if (agentConversationLoadVersionRef.current !== saveVersion) { return; } - setAgentConversationRuntime(agentRuntimeStateFromResult(runtime)); + const nextRuntime = agentRuntimeStateFromResult(runtime); + setAgentConversationRuntime(nextRuntime); + rememberAgentRuntimeState(nextRuntime); setAgentConversationRuntimeError(''); const conversation = await invoke( 'read_local_conversation', @@ -19050,11 +19130,56 @@ export function App() { } } + function rememberAgentRuntimeState(runtime: AgentRuntimeState | null) { + if (!runtime) { + return; + } + setAgentRuntimeById((current) => { + const previous = current[runtime.agentId] ?? current[runtime.taskId]; + const mergedRuntime = { + ...runtime, + recentTasks: runtime.recentTasks ?? previous?.recentTasks ?? [], + }; + return { + ...current, + [mergedRuntime.agentId]: mergedRuntime, + [mergedRuntime.taskId]: mergedRuntime, + }; + }); + } + + async function refreshAgentRuntimes( + nextProjectPath = resolveChatProjectPath(localProject) ?? '', + ) { + const invoke = resolveTauriInvoke(); + if (!invoke || !nextProjectPath) { + setAgentRuntimeById({}); + return; + } + try { + const runtimes = await invoke( + 'read_game_creator_agent_runtimes', + { projectPath: nextProjectPath }, + ); + const nextRuntimeById: Record = + {}; + for (const runtimeResult of runtimes) { + const runtime = agentRuntimeStateFromResult(runtimeResult); + nextRuntimeById[runtime.agentId] = runtime; + nextRuntimeById[runtime.taskId] = runtime; + } + setAgentRuntimeById(nextRuntimeById); + } catch { + setAgentRuntimeById({}); + } + } + async function refreshAgentRunTrace( nextProjectPath = resolveChatProjectPath(localProject) ?? '', ) { if (!nextProjectPath) { setAgentRunStatus('请先初始化本地项目'); + await refreshAgentRuntimes(nextProjectPath); await refreshAgentRunHistory(nextProjectPath); return null; } @@ -19062,6 +19187,7 @@ export function App() { '.agent/run.latest.json', nextProjectPath, ); + await refreshAgentRuntimes(nextProjectPath); await refreshAgentRunHistory(nextProjectPath); return summary; } @@ -19103,6 +19229,7 @@ export function App() { const agentStatusCards = deriveAgentStatusCards( manifest, agentRunTrace ?? agentRunHistory[0]?.trace ?? null, + agentRuntimeById, ); const mainProjectSummary = localProject ? summarizeMainProjectHeader(manifest, agentStatusCards) @@ -19970,6 +20097,10 @@ export function App() { llmConfigStatus, agent, ); + const agentRuntimeStatus = formatAgentCardRuntimeStatus(agent); + const recentRuntimeTasks = agent.runtimeRecentTasks + .slice(-2) + .reverse(); return (