补齐Agent运行态状态列表
主窗口 Agent 状态列表批量读取并展示 Runtime 状态和最近任务。 单 Agent 对话与 Runtime 使用任务 ID 作为规范 Agent ID。 兼容旧 group-role Agent 别名并映射到规范任务 ID。 补充 Rust 与前端测试,更新 Runtime 文档和共享决策记录。
This commit is contained in:
@@ -193,9 +193,9 @@ pub(crate) fn read_game_creator_agent_runtime_at(
|
||||
) -> Result<AgentRuntimeResult, String> {
|
||||
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::<AgentRuntimeState>(&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<Vec<AgentRuntimeResult>, 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<AgentRuntimeResult, String> {
|
||||
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<AgentRuntimeState, String> {
|
||||
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<String, String> {
|
||||
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,
|
||||
|
||||
@@ -244,10 +244,11 @@ pub(crate) async fn chat_with_game_creator_role_agent(
|
||||
prompt: String,
|
||||
) -> Result<GameCreatorChatAgentReply, String> {
|
||||
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<GameCreatorChatAgentReply, String> {
|
||||
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<Vec<AgentRuntimeResult>, 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<LocalAgentMemoryResult, String> {
|
||||
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<LocalAgentMemoryResult, String> {
|
||||
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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -562,7 +562,7 @@ function AgentRuntimeStatusPanel({
|
||||
<strong>最近任务</strong>
|
||||
{recentTasks.map((task) => (
|
||||
<small key={`${task.runId}-${task.updatedAt}`}>
|
||||
{`${task.status} / ${task.phase} · ${task.task || task.currentAction}`}
|
||||
{formatAgentRecentRuntimeTask(task)}
|
||||
</small>
|
||||
))}
|
||||
</div>
|
||||
@@ -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<string, AgentRuntimeState | undefined> = {},
|
||||
): AgentStatusCard[] {
|
||||
const latestByTaskId = new Map<string, GameCreationAgentRunStep>();
|
||||
const latestByGroupRole = new Map<string, GameCreationAgentRunStep>();
|
||||
@@ -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<string, AgentRuntimeState | undefined>
|
||||
>({});
|
||||
const [runtimeConfigOpen, setRuntimeConfigOpen] = useState(false);
|
||||
const [llmConfigStatus, setLlmConfigStatus] =
|
||||
useState<GameCreatorLlmConfigStatus | null>(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<LocalConversationResult>(
|
||||
'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<AgentRuntimeResult[]>(
|
||||
'read_game_creator_agent_runtimes',
|
||||
{ projectPath: nextProjectPath },
|
||||
);
|
||||
const nextRuntimeById: Record<string, AgentRuntimeState | undefined> =
|
||||
{};
|
||||
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 (
|
||||
<button
|
||||
key={agent.id}
|
||||
@@ -19980,6 +20111,21 @@ export function App() {
|
||||
<strong>{agent.title}</strong>
|
||||
<span>{`${taskGroupLabels[agent.group]} / ${agent.role} · ${taskStatusLabels[agent.status]}`}</span>
|
||||
{agentLlmStatus ? <small>{agentLlmStatus}</small> : null}
|
||||
{agentRuntimeStatus ? (
|
||||
<small>{agentRuntimeStatus}</small>
|
||||
) : null}
|
||||
{agent.runtimeTask ? (
|
||||
<small>{`当前任务:${agent.runtimeTask}`}</small>
|
||||
) : null}
|
||||
{recentRuntimeTasks.map((task, index) => (
|
||||
<small
|
||||
key={`${agent.id}-runtime-task-${task.runId}-${task.updatedAt}`}
|
||||
>
|
||||
{`${
|
||||
index === 0 ? '最近任务' : '队列记录'
|
||||
}:${formatAgentRecentRuntimeTask(task)}`}
|
||||
</small>
|
||||
))}
|
||||
{agent.pass !== null || agent.phase ? (
|
||||
<small>{`pass ${agent.pass ?? '-'} · ${agent.phase ?? '-'}`}</small>
|
||||
) : null}
|
||||
|
||||
@@ -480,7 +480,41 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
updatedAt: 1,
|
||||
};
|
||||
|
||||
const cards = deriveAgentStatusCards(manifest, trace);
|
||||
const runtimeTask = {
|
||||
schemaVersion: 'game-creator-agent-runtime-task.v1',
|
||||
agentId: 'art-asset-plan',
|
||||
taskId: 'art-asset-plan',
|
||||
sessionId: 'agent-session-art-asset-plan',
|
||||
runId: 'runtime-art-asset-plan-1',
|
||||
source: 'agent-background-task',
|
||||
task: '补齐主角规范图资产列表',
|
||||
status: 'pending',
|
||||
phase: 'queued',
|
||||
currentAction: '等待当前任务完成',
|
||||
error: null,
|
||||
updatedAt: 1234,
|
||||
};
|
||||
const cards = deriveAgentStatusCards(manifest, trace, {
|
||||
'art-asset-plan': {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'art-asset-plan',
|
||||
taskId: 'art-asset-plan',
|
||||
sessionId: 'agent-session-art-asset-plan',
|
||||
runId: 'runtime-art-asset-plan-1',
|
||||
source: 'agent-background-task',
|
||||
status: 'running',
|
||||
phase: 'planning',
|
||||
currentTask: '补齐主角规范图资产列表',
|
||||
currentAction: '拆解素材规格',
|
||||
plan: ['读取项目上下文'],
|
||||
observations: ['已创建本轮 Agent Runtime run。'],
|
||||
allowedTools: ['conversation.read'],
|
||||
lastResponse: null,
|
||||
error: null,
|
||||
updatedAt: 1234,
|
||||
recentTasks: [runtimeTask],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cards.find((card) => card.id === 'design-director')).toMatchObject({
|
||||
taskId: 'design-director',
|
||||
@@ -501,12 +535,19 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
}),
|
||||
],
|
||||
});
|
||||
expect(cards.find((card) => card.id === 'art-asset')).toMatchObject({
|
||||
expect(cards.find((card) => card.id === 'art-asset-plan')).toMatchObject({
|
||||
taskId: 'art-asset-plan',
|
||||
status: 'failed',
|
||||
summary: '美术资产生成失败',
|
||||
hasRecentEvidence: true,
|
||||
runtimeStatus: 'running',
|
||||
runtimePhase: 'planning',
|
||||
runtimeAction: '拆解素材规格',
|
||||
runtimeTask: '补齐主角规范图资产列表',
|
||||
runtimeRunId: 'runtime-art-asset-plan-1',
|
||||
runtimeRecentTasks: [runtimeTask],
|
||||
});
|
||||
expect(cards.find((card) => card.id === 'code-code')).toMatchObject({
|
||||
expect(cards.find((card) => card.id === 'code-prototype')).toMatchObject({
|
||||
taskId: 'code-prototype',
|
||||
status: 'completed',
|
||||
summary: '代码已通过生成',
|
||||
@@ -3586,7 +3627,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
submitChat('/agent-conversations');
|
||||
expect(
|
||||
await screen.findByText(
|
||||
/Agent 对话读取命令:[\s\S]*美术组 \/ Asset · 规划美术资产:\/read \.agent\/conversations\/agents\/art-asset\.jsonl/,
|
||||
/Agent 对话读取命令:[\s\S]*美术组 \/ Asset · 规划美术资产:\/read \.agent\/conversations\/agents\/art-asset-plan\.jsonl/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
fireEvent.click(
|
||||
@@ -13120,6 +13161,102 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(screen.queryByText('旧 Agent 历史消息')).toBeNull();
|
||||
});
|
||||
|
||||
it('shows runtime status and recent tasks in the main agent status list', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
'未命名游戏原型',
|
||||
);
|
||||
const runtimeTask = {
|
||||
schemaVersion: 'game-creator-agent-runtime-task.v1',
|
||||
agentId: 'design-director',
|
||||
taskId: 'design-director',
|
||||
sessionId: 'agent-session-design-director',
|
||||
runId: 'runtime-design-director-1',
|
||||
source: 'agent-background-task',
|
||||
task: '排队补齐世界观拆解',
|
||||
status: 'pending',
|
||||
phase: 'queued',
|
||||
currentAction: '等待当前任务完成',
|
||||
error: null,
|
||||
updatedAt: 10,
|
||||
};
|
||||
const runtimeState = {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'design-director',
|
||||
taskId: 'design-director',
|
||||
sessionId: 'agent-session-design-director',
|
||||
runId: 'runtime-design-director-1',
|
||||
source: 'agent-background-task',
|
||||
status: 'running',
|
||||
phase: 'planning',
|
||||
currentTask: '拆解关卡节奏',
|
||||
currentAction: '整理目标和约束',
|
||||
plan: ['读取项目上下文'],
|
||||
observations: ['已创建本轮 Agent Runtime run。'],
|
||||
allowedTools: ['conversation.read'],
|
||||
lastResponse: null,
|
||||
error: null,
|
||||
updatedAt: 10,
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'append_local_permission_log') {
|
||||
return {};
|
||||
}
|
||||
if (command === 'init_local_game_project') {
|
||||
const projectPath = String(args?.projectPath ?? '');
|
||||
return {
|
||||
projectPath,
|
||||
manifestPath: `${projectPath}/.agent/manifest.json`,
|
||||
manifest,
|
||||
};
|
||||
}
|
||||
if (command === 'read_game_creator_agent_runtimes') {
|
||||
return [
|
||||
{
|
||||
state: runtimeState,
|
||||
sessionPath:
|
||||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||||
eventPath:
|
||||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||||
taskPath:
|
||||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||||
recentEvents: [],
|
||||
recentTasks: [runtimeTask],
|
||||
},
|
||||
];
|
||||
}
|
||||
if (command === 'read_local_project_file') {
|
||||
throw new Error(
|
||||
'读取文件元数据失败:/tmp/authorized-game/.agent/run.latest.json: No such file or directory (os error 2)',
|
||||
);
|
||||
}
|
||||
if (command === 'list_local_project_files') {
|
||||
return { projectPath: String(args?.projectPath ?? ''), files: [] };
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
|
||||
renderAppAt('/?main&projectPath=%2Ftmp%2Fauthorized-game');
|
||||
|
||||
await screen.findByText('想做什么游戏?');
|
||||
const agentStatusList = screen.getByLabelText('Agent 状态列表');
|
||||
await waitFor(() => {
|
||||
const designCard = within(agentStatusList).getByRole('button', {
|
||||
name: /拆解创作方向/,
|
||||
});
|
||||
expect(designCard.textContent).toContain(
|
||||
'Runtime:running / planning · 整理目标和约束 · run runtime-design-director-1',
|
||||
);
|
||||
expect(designCard.textContent).toContain('当前任务:拆解关卡节奏');
|
||||
expect(designCard.textContent).toContain(
|
||||
'最近任务:pending / queued · 排队补齐世界观拆解',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('ignores stale agent conversation reads after switching agents', async () => {
|
||||
const manifest = createGameCreationAppManifest(
|
||||
'local-project-draft',
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
- 背景:开发用单 Agent 聊天已经能真实调用各 Agent 的 LLM 路由并持久化对话,但 Agent 仍主要表现为同步问答,用户无法明确投递一个任务让某个 Agent 独立运行,也无法同时启动多个 Agent 的工作。
|
||||
- 决策:在现有 `.agent/runtime` 和 `.agent/conversations` 基础上新增单 Agent 后台任务入口。Tauri 命令 `start_game_creator_agent_runtime_task` 立即写入该 Agent 的 runtime state/event/task history,追加用户任务到 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:Agent 先输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行只读工具并记录 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复;完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。每个 Agent 的任务历史落在 `.agent/runtime/tasks/<agentId>.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,UI 在 Runtime 面板展示最近任务。不同 Agent 使用独立 `.agent/runtime/locks/<agentId>.lock`,允许并行运行;同一 Agent 已有运行任务时,新任务会先进入该 Agent 的 pending 队列,当前 drain 持锁完成后串行继续下一条 pending。该能力仍不是独立 OS 进程或跨重启离线常驻 worker。
|
||||
- 补充:规范 Agent ID 统一使用 manifest taskId,例如 `art-asset-plan` 和 `code-prototype`;历史前端曾使用的 `group-role` 别名只在 Tauri command 层兼容并映射到规范 taskId。主窗口 Agent 状态列表通过 `read_game_creator_agent_runtimes` 批量读取 `.agent/runtime/agents/<taskId>.json` 和最近任务,把每个 Agent 的 Runtime 状态、当前动作和最近 task 直接显示在状态卡片和 `/agents` 汇总里。
|
||||
- 影响范围:`apps/ai-game-creator-shell` 的 Tauri command、Agent Runtime state/event、开发窗口单 Agent 聊天、项目内 Agent 对话弹窗、`appSurface.test.ts` 和 AI 游戏创作 App 实施计划。
|
||||
- 验证方式:运行 Tauri Rust 后台 Agent 并行测试、壳前端 appSurface 测试、壳 typecheck、编码检查和 `git diff --check`。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
@@ -29,10 +29,10 @@ Agent Runtime 负责:
|
||||
## Agent 能力清单
|
||||
|
||||
- 用户能力:聊天入口、上传文件;正式用户窗口不展示任务、文件、预览、日志、能力清单或开发专用单 Agent 聊天入口。
|
||||
- 开发窗口能力:debug 构建额外打开 `developer` 窗口,走 `index.html?agent-chat`;开发者可选择 Agent、授权本地项目路径,并通过 `read_local_conversation` / `append_local_conversation_message` 读写 `.agent/conversations/agents/<agentId>.jsonl`,通过 `agentLlm.<agentId>` 调用该 Agent 的独立 LLM 路由做真实对话,用于单独调试某个 Agent 的长期对话上下文。
|
||||
- 开发窗口能力:debug 构建额外打开 `developer` 窗口,走 `index.html?agent-chat`;开发者可选择 Agent、授权本地项目路径,并通过 `read_local_conversation` / `append_local_conversation_message` 读写 `.agent/conversations/agents/<agentId>.jsonl`,通过 `agentLlm.<agentId>` 调用该 Agent 的独立 LLM 路由做真实对话,用于单独调试某个 Agent 的长期对话上下文。这里的 `<agentId>` 以 manifest taskId 为规范值,旧 `group-role` 别名只作为兼容输入映射到 taskId。
|
||||
- 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。
|
||||
- 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作;Runtime V1 会为单 Agent 对话和生成 loop 中的角色 brief 写入独立 runtime state / event,先解决“每个 Agent 正在做什么、跑到哪一步、最近一次 task/run 是什么”的可观测性。
|
||||
- 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/<agentId>.json`、`.agent/runtime/events/<agentId>.jsonl`、`.agent/runtime/tasks/<agentId>.jsonl` 和 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:先让该 Agent 输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复并追加回对话。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务,开发窗口和项目内 Agent 对话弹窗在 Runtime 面板展示最近任务。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 已有运行任务时,新任务会先写成 `pending / queued`,由当前后台 drain 在完成后串行继续执行。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程或跨重启离线常驻 worker。
|
||||
- 后台任务能力:开发窗口单 Agent 聊天和项目内 Agent 对话弹窗可把当前输入投递为单 Agent 后台任务,Tauri 命令 `start_game_creator_agent_runtime_task` 会立即写入该 Agent 的 `.agent/runtime/agents/<agentId>.json`、`.agent/runtime/events/<agentId>.jsonl`、`.agent/runtime/tasks/<agentId>.jsonl` 和 `.agent/conversations/agents/<agentId>.jsonl`,随后在 App 进程内启动 tokio task 执行最小 Agent loop:先让该 Agent 输出 `thinkingSummary / plan / actions`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把观察结果交给 Agent 生成最终回复并追加回对话。`read_game_creator_agent_runtime` 会按 `runId` 去重返回最近任务,`read_game_creator_agent_runtimes` 批量读取所有规范 taskId 的 runtime;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表展示最近任务、当前动作和运行阶段。不同 Agent 使用各自 runtime 锁,可以并行运行;同一 Agent 已有运行任务时,新任务会先写成 `pending / queued`,由当前后台 drain 在完成后串行继续执行。该能力仍属于 Runtime V1 的进程内任务,不是独立 OS 进程或跨重启离线常驻 worker。
|
||||
- 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。
|
||||
- 记忆能力:短期记忆 `memory/session.md`、长期记忆 `memory/project.md`、项目级黑板 `memory/blackboard.md` 和角色私有记忆 `memory/agents/<group>/<role>.md`;黑板用于共享重要跨 agent 记忆,角色私有记忆只给对应角色 brief 读取和追加。最近 project / agent conversation 会作为短期 prompt 上下文读取,不替代正式 memory 文件。
|
||||
- 对话能力:结构化对话记录统一落在 `.agent/conversations/` 的 append-only JSONL;普通聊天写 `.agent/conversations/project.jsonl`,进入单个 agent 后只写对应 `.agent/conversations/agents/<agentId>.jsonl`,不把原始对话混进项目黑板或角色私有记忆。
|
||||
@@ -166,7 +166,7 @@ game-project/
|
||||
- 美术/音乐资产能从画板链路回流到本地项目。
|
||||
- 短期记忆、长期记忆、项目黑板和角色私有记忆按授权本地项目路径读写;普通用户仍只通过聊天命令访问短期 / 长期 / 黑板记忆,角色私有记忆只在单 agent 对话和生成 loop 中按目标 agent 读取。
|
||||
- 结构化对话记录按授权本地项目路径追加 JSONL;普通聊天、`/history`、工作区历史和单 agent 对话都读取 `.agent/conversations/`,最近 project / agent 对话可进入生成 prompt 上下文,但 v1 不提供 fork、archive 或云端同步。
|
||||
- Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单和 `.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus 派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。
|
||||
- Agent 状态列表从 `.agent/manifest.json` 的任务 / 角色清单、`.agent/run.latest.json` / `.agent/runs/<runId>.json` 的 step、taskGraph、passPlans、lifecycleStatus,以及 `read_game_creator_agent_runtimes` 批量读取的 `.agent/runtime/agents/<taskId>.json` 和最近任务派生;v1 不新增独立状态数据库,也不承诺完整后台 runner。
|
||||
- App 启动先检查平台登录态;登录后进入同一个客户端首页,不再有面向用户的启动器 / 主窗口切换概念。首页按 `做游戏` / `做素材` / `做方案` 保存 `game` / `art` / `doc` 初始意图,发送时弹出原生目录选择,目标目录存在且非空时必须二次确认;确认后只调用 `init_local_game_project` 初始化本地项目、`upload_local_asset` 导入附件、`append_local_conversation_message` 记录首条需求和接收回执,再写入最近项目并切到项目开发占位页。本流程不调用 `generate_local_game_draft`、`generate_platform_art_asset` 或 LLM 聊天。
|
||||
- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、切换时自动读取该 Agent 历史,并把用户消息和真实 Agent 回复持久化到 `.agent/conversations/agents/<agentId>.jsonl`,普通用户窗口不得出现 `Agent 聊天` 导航或入口。
|
||||
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口管理最近项目、打开项目、新建项目和显示目录。打开项目只读取已初始化项目并切到项目开发占位页,不打开第二窗口;新建项目仍沿用非空目录确认,不自动重建无效历史路径。
|
||||
@@ -273,8 +273,8 @@ game-project/
|
||||
- loop 每次运行会写 `.agent/run.latest.json` 和 `.agent/runs/<runId>.json`,记录 `Planner` / `Orchestrator` agenda / 16 个组内角色 brief 或 carry-over / 6 个 `GroupCoordinator` 汇总 / `Generator` / 6 个专业组交接 / `Evaluator` 质量评审 / `ArtifactWriter` / `Playtest` step、每步 `toolCalls`、输入文件、输出文件、状态、轮次、maxPasses、toolCallCount、maxToolCalls、stopReason、nextStep 和错误摘要;Planner、角色 agent 和 Generator 的 `inputPaths` 必须包含对应记忆文件、`.agent/conversations/project.jsonl`、`.agent/conversations/agents/`、`.agent/manifest.json` 和 agenda 等上下文来源,其中角色 agent 必须包含自己的 `memory/agents/<group>/<role>.md` 和 `memory/blackboard.md`;conversation 输入只取最近少量 project / agent 对话摘要,不读取全量历史;每个 step 必须带 phase、taskId、group 和 role,`.agent/run.latest.json.taskGraph` 必须记录 goal、readyTaskIds、activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和当前任务状态;`.agent/run.latest.json.passPlans` 必须按轮记录 mode、summary、activeTaskIds、carriedTaskIds、dependencyWaves、repairFocus 和 repairRoutes,作为 `/trace` 与开发窗口判断编排 loop 是否真实发生的直接证据;`run.latest.json` 是当前指针,`.agent/runs/` 保留历史 run trace;开发窗口读取 latest 展示编排过程,并复用 `file.list/read` 按文件修改时间先载入最近 20 个历史 run,滚动时再按批次读取剩余历史,普通用户窗口不展示。
|
||||
- `.agent/run.latest.json` 的 `artifacts` 使用结构化记录,包含相对路径、字节数和 `fnv1a64:` checksum;除最终本地产物外,也会收集 `.agent/passes/pass-N/` 快照,便于确认返工前后的产物差异。
|
||||
- 通过 Evaluator 和 `game.static_smoke` 后,Agent loop 会把本次 runId、状态、轮次、下一步、active / carry-over 任务和最终本地产物摘要追加到 `memory/session.md` 与 `memory/project.md`,把重要跨 agent 决策 / 依赖 / 风险摘要追加到 `memory/blackboard.md`,并把各角色本轮成功产出的角色摘要追加到 `memory/agents/<group>/<role>.md`;下一次 Planner、组内角色和 Generator 会通过记忆输入自然读取上一轮稳定原型状态,而不只依赖开发窗口 trace。
|
||||
- 单 agent 对话入口读取对应 agent conversation;用户提交后先追加用户消息,再调用 `chat_with_game_creator_role_agent` / `chat_with_game_creator_role_agent_stream` 让对应 `agentLlm.<agentId>` 结合项目上下文、Agent 私有记忆和本 Agent 历史对话生成回复,随后把回复写入对应 `.agent/conversations/agents/<agentId>.jsonl`。每轮对话会同步写 `.agent/runtime/agents/<agentId>.json` 和 `.agent/runtime/events/<agentId>.jsonl`,字段包含 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、`currentTask`、`currentAction`、`plan`、`observations`、`allowedTools`、`lastResponse` 和 `error`;流式事件会把最新 `runtimeState` 回传给界面。Runtime state 写入使用临时文件替换,event JSONL 读取会跳过坏行;`currentTask`、event detail、`lastResponse` 和 `agent.db` 摘要复用敏感上下文过滤,不保存明显 API Key / Bearer / Cookie 片段。单 agent 面板可把当前输入手动追加到对应 `memory/agents/<group>/<role>.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md` 或 `memory/agents/<group>/<role>.md`。
|
||||
- 生成 loop 中的角色 brief 也写同一套 Agent Runtime state / event:active 角色用 `source=generate-draft` 和当前 `runId` 标记正在读取上下文、调用角色专属 LLM 或本地编排、生成 brief、完成或失败;carry-over 角色同样写入开始 / 完成事件,但不会伪装成重新调用 LLM。开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示当前 Agent 的 runtime 状态、最近 task/run、阶段、动作、计划和观测;这只是 V1 可观测性,不代表已经有独立后台常驻进程或可中断任意上游 LLM 请求。
|
||||
- 单 agent 对话入口读取对应 agent conversation;用户提交后先追加用户消息,再调用 `chat_with_game_creator_role_agent` / `chat_with_game_creator_role_agent_stream` 让对应 `agentLlm.<agentId>` 结合项目上下文、Agent 私有记忆和本 Agent 历史对话生成回复,随后把回复写入对应 `.agent/conversations/agents/<agentId>.jsonl`。这里的 `<agentId>` 以任务 `taskId` 为规范值,Tauri 只兼容旧 `group-role` 别名并映射到 taskId。每轮对话会同步写 `.agent/runtime/agents/<agentId>.json` 和 `.agent/runtime/events/<agentId>.jsonl`,字段包含 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、`currentTask`、`currentAction`、`plan`、`observations`、`allowedTools`、`lastResponse` 和 `error`;流式事件会把最新 `runtimeState` 回传给界面。Runtime state 写入使用临时文件替换,event JSONL 读取会跳过坏行;`currentTask`、event detail、`lastResponse` 和 `agent.db` 摘要复用敏感上下文过滤,不保存明显 API Key / Bearer / Cookie 片段。单 agent 面板可把当前输入手动追加到对应 `memory/agents/<group>/<role>.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md` 或 `memory/agents/<group>/<role>.md`。
|
||||
- 生成 loop 中的角色 brief 也写同一套 Agent Runtime state / event:active 角色用 `source=generate-draft` 和当前 `runId` 标记正在读取上下文、调用角色专属 LLM 或本地编排、生成 brief、完成或失败;carry-over 角色同样写入开始 / 完成事件,但不会伪装成重新调用 LLM。主窗口 Agent 状态列表、开发单 Agent 聊天页和项目内单 Agent 对话弹窗只读展示当前 Agent 的 runtime 状态、最近 task/run、阶段、动作、计划和观测;这只是 V1 可观测性,不代表已经有独立后台常驻进程或可中断任意上游 LLM 请求。
|
||||
- `.agent/agent.db` 当前作为最小本地项目索引文件使用 JSONL:初始化写入 `project.init`,每次 `game.generate_draft` 追加目标、标题、本地产物路径、checkpoint 和 diff 摘要,上传 / 登记 / 画板导入资产时追加 `asset.register` 或 `asset.update`;v1 不引入 SQLite 依赖。
|
||||
- `game.generate_draft`、资产登记 / 导入、记忆写入、预览状态写入、checkpoint / restore、agent 生命周期控制、画板资源回流 / 生成和 policy 写入会先按 `.agent/policy.json` 判断本次命令是否被项目策略拒绝,再拿项目级 `.agent/project.lock` 串行化;锁只保护同一本地项目,v1 不做后台锁管理。`confirmCommands` 可把索引、状态读取、资产登记、checkpoint、预览、agent 生命周期、画板资源回流 / 生成、memory 读写删除和 conversation 读写等命令转成项目策略确认,命中时用户确认后才执行;用户可用 `/policy-confirm 命令` 加入确认列表,用 `/policy-auto 命令` 移除确认项。
|
||||
- `ArtifactWriter` 写入最终产物前把当前项目文件保存到 `.agent/checkpoints/<checkpointId>/`,写入后把新增、修改、删除计数记录到 `.agent/agent.db`;聊天命令 `/checkpoint`、`/checkpoints`、`/diff checkpoint-id` 和 `/restore checkpoint-id` 允许用户手动保存、列出最近 checkpoint、对比和确认回滚到 checkpoint,回滚时会删除 checkpoint 后新增的受跟踪项目文件。`.agent/runtime/` 属于运行观测状态,不进入项目索引、checkpoint diff 或 restore 删除范围。
|
||||
|
||||
Reference in New Issue
Block a user