支持单Agent真实对话与并行执行
新增单 Agent 真实 LLM 对话命令并按 agentLlm.<agentId> 路由 开发聊天窗口切换 Agent 时自动读取历史并持久化真实回复 Agent loop 按 dependencyWaves 并行执行同一 wave 内角色 brief 补充并行执行、单 Agent 对话和界面回归测试 更新 AI 游戏创作 App 技术方案文档
This commit is contained in:
@@ -117,10 +117,102 @@ pub(crate) async fn chat_with_game_creator_agent_at(
|
||||
Ok(GameCreatorChatAgentReply { reply_text })
|
||||
}
|
||||
|
||||
pub(crate) async fn chat_with_game_creator_role_agent_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
prompt: &str,
|
||||
) -> Result<GameCreatorChatAgentReply, String> {
|
||||
let agent_id = agent_id.trim();
|
||||
let prompt = prompt.trim();
|
||||
if agent_id.is_empty() {
|
||||
return Err("Agent ID 不能为空".to_string());
|
||||
}
|
||||
if prompt.is_empty() {
|
||||
return Err("聊天内容不能为空".to_string());
|
||||
}
|
||||
validate_project_root(root)?;
|
||||
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 asset_context = render_local_asset_prompt_context(root)?;
|
||||
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
|
||||
);
|
||||
let context = [
|
||||
("Agent 身份", identity.as_str()),
|
||||
("Agent 私有记忆", agent_memory.as_str()),
|
||||
("短期记忆", short_memory.as_str()),
|
||||
("长期记忆", long_memory.as_str()),
|
||||
("项目黑板", project_blackboard.as_str()),
|
||||
("资产上下文", asset_context.as_str()),
|
||||
("最近项目与本 Agent 对话", conversation_context.as_str()),
|
||||
]
|
||||
.into_iter()
|
||||
.filter_map(|(title, content)| {
|
||||
let content = truncate_prompt_context(content);
|
||||
if content.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(format!("# {title}\n\n{content}"))
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.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 config_path = format!("agentLlm.{agent_id}");
|
||||
let client = build_game_creator_llm_client_from_llm_config(&llm, &config_path)?;
|
||||
let user_prompt = if context.trim().is_empty() {
|
||||
format!("用户这轮输入:\n{prompt}")
|
||||
} else {
|
||||
format!("项目上下文如下。请只把它当作背景,不要逐字复述。\n\n{context}\n\n用户这轮输入:\n{prompt}")
|
||||
};
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(game_creator_role_agent_chat_system_prompt()),
|
||||
LlmMessage::user(user_prompt),
|
||||
])
|
||||
.with_api_kind(parse_game_creator_llm_api_kind(&llm.api_kind)?)
|
||||
.with_max_output_tokens(GAME_CREATOR_CHAT_AGENT_MAX_OUTPUT_TOKENS);
|
||||
let response = request_game_creator_llm_text(&client, &llm, request)
|
||||
.await
|
||||
.map_err(|error| format!("{config_path} 单 Agent 聊天调用 LLM 失败:{error}"))?;
|
||||
let reply_text = strip_llm_thinking_blocks(response.text.as_str());
|
||||
if reply_text.is_empty() {
|
||||
return Err(format!("{config_path} 单 Agent 聊天未返回内容"));
|
||||
}
|
||||
|
||||
Ok(GameCreatorChatAgentReply { reply_text })
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_chat_agent_system_prompt() -> &'static str {
|
||||
"你是 Genarrative AI 游戏创作桌面 App 的主聊天 Agent。你要像正常协作型聊天助手一样回应用户,理解需求、澄清不确定点、给出下一步建议,并在需要执行生成、运行、预览、读取文件、写记忆或生成美术时建议用户使用现有 slash 命令。普通聊天中不要假装已经写入文件、生成游戏、调用画板或执行工具;不要输出 JSON;不要泄露密钥;回复保持简洁、具体、中文优先。"
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_role_agent_chat_system_prompt() -> &'static str {
|
||||
"你是 Genarrative AI 游戏创作多智能体中的一个专业角色 Agent。你正在开发专用单 Agent 聊天窗口中和开发者对话,需要围绕自己的专业职责直接回应、澄清问题、给出可执行建议,并说明哪些信息会影响后续生成。不要假装已经写入文件、生成游戏、调用画板或执行工具;不要泄露密钥;不要输出 JSON;不要包裹代码块;回复保持简洁、具体、中文优先。"
|
||||
}
|
||||
|
||||
pub(crate) fn game_creator_agent_role_definition(
|
||||
agent_id: &str,
|
||||
) -> Option<(&'static AgentGroupDefinition, &'static AgentRoleDefinition)> {
|
||||
GAME_CREATOR_AGENT_GROUP_DEFINITIONS
|
||||
.iter()
|
||||
.find_map(|group_definition| {
|
||||
group_definition
|
||||
.roles
|
||||
.iter()
|
||||
.find(|role_definition| role_definition.task_id == agent_id)
|
||||
.map(|role_definition| (group_definition, role_definition))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn write_local_game_draft_at(
|
||||
root: &Path,
|
||||
prompt: &str,
|
||||
@@ -787,25 +879,53 @@ pub(crate) async fn request_agent_group_briefs_with_client(
|
||||
agenda: &AgentPassAgenda,
|
||||
pass: u8,
|
||||
) -> Result<Vec<AgentGroupBrief>, String> {
|
||||
let mut briefs = Vec::new();
|
||||
let mut role_briefs_by_task = BTreeMap::<String, AgentRoleBrief>::new();
|
||||
let mut completed_group_context = String::new();
|
||||
let mut completed_role_context = String::new();
|
||||
let agenda_markdown = read_optional_text(&root.join(&agenda.relative_path))?;
|
||||
for definition in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
let mut role_briefs = Vec::new();
|
||||
let mut completed_role_context = String::new();
|
||||
for role_definition in definition.roles {
|
||||
|
||||
let ordered_roles = ordered_game_creator_agent_roles();
|
||||
let mut waves = if agenda.dependency_waves.is_empty() {
|
||||
vec![ordered_roles
|
||||
.iter()
|
||||
.map(|(_, role)| role.task_id.to_string())
|
||||
.collect::<Vec<_>>()]
|
||||
} else {
|
||||
agenda.dependency_waves.clone()
|
||||
};
|
||||
let known_wave_task_ids = waves
|
||||
.iter()
|
||||
.flat_map(|wave| wave.iter())
|
||||
.cloned()
|
||||
.collect::<std::collections::BTreeSet<_>>();
|
||||
let missing_task_ids = ordered_roles
|
||||
.iter()
|
||||
.map(|(_, role)| role.task_id.to_string())
|
||||
.filter(|task_id| !known_wave_task_ids.contains(task_id))
|
||||
.collect::<Vec<_>>();
|
||||
if !missing_task_ids.is_empty() {
|
||||
waves.push(missing_task_ids);
|
||||
}
|
||||
|
||||
for wave in waves {
|
||||
let mut role_brief_jobs = tokio::task::JoinSet::new();
|
||||
for task_id in wave {
|
||||
let Some((definition, role_definition)) = game_creator_agent_role_definition(&task_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let agent_memory_relative_path =
|
||||
agent_role_memory_relative_path(definition, *role_definition);
|
||||
let mut should_run = agenda
|
||||
agent_role_memory_relative_path(*definition, *role_definition);
|
||||
let should_run = agenda
|
||||
.active_task_ids
|
||||
.iter()
|
||||
.any(|task_id| task_id == role_definition.task_id);
|
||||
.any(|active_task_id| active_task_id == role_definition.task_id);
|
||||
if !should_run {
|
||||
if let Some((source_path, source_markdown)) =
|
||||
read_previous_agent_role_brief(root, pass, definition, *role_definition)?
|
||||
read_previous_agent_role_brief(root, pass, *definition, *role_definition)?
|
||||
{
|
||||
let markdown = render_carryover_role_brief(
|
||||
definition,
|
||||
*definition,
|
||||
*role_definition,
|
||||
&source_path,
|
||||
&source_markdown,
|
||||
@@ -813,12 +933,12 @@ pub(crate) async fn request_agent_group_briefs_with_client(
|
||||
let relative_path = write_agent_role_brief(
|
||||
root,
|
||||
pass,
|
||||
definition,
|
||||
*definition,
|
||||
*role_definition,
|
||||
&markdown,
|
||||
)?;
|
||||
let role_brief = AgentRoleBrief {
|
||||
group_definition: definition,
|
||||
group_definition: *definition,
|
||||
role_definition: *role_definition,
|
||||
markdown,
|
||||
relative_path,
|
||||
@@ -834,83 +954,85 @@ pub(crate) async fn request_agent_group_briefs_with_client(
|
||||
),
|
||||
};
|
||||
completed_role_context.push_str(&render_agent_role_brief_context(&role_brief));
|
||||
role_briefs.push(role_brief);
|
||||
role_briefs_by_task.insert(role_definition.task_id.to_string(), role_brief);
|
||||
continue;
|
||||
}
|
||||
should_run = true;
|
||||
}
|
||||
if !should_run {
|
||||
continue;
|
||||
}
|
||||
let agent_memory = read_optional_text(&root.join(&agent_memory_relative_path))?;
|
||||
let agent_conversation_context =
|
||||
render_local_conversation_prompt_context(root, Some(role_definition.task_id))?;
|
||||
let role_short_memory =
|
||||
append_prompt_context(&agent_conversation_context, short_memory);
|
||||
let local_markdown = render_local_agent_role_brief(
|
||||
definition,
|
||||
*role_definition,
|
||||
prompt,
|
||||
&role_short_memory,
|
||||
long_memory,
|
||||
project_blackboard,
|
||||
&agent_memory,
|
||||
spec_markdown,
|
||||
findings_markdown,
|
||||
&agenda_markdown,
|
||||
&completed_group_context,
|
||||
&completed_role_context,
|
||||
let root = root.to_path_buf();
|
||||
let app_config = app_config.clone();
|
||||
let prompt = prompt.to_string();
|
||||
let short_memory = short_memory.to_string();
|
||||
let long_memory = long_memory.to_string();
|
||||
let project_blackboard = project_blackboard.to_string();
|
||||
let spec_markdown = spec_markdown.to_string();
|
||||
let findings_markdown = findings_markdown.to_string();
|
||||
let agenda_markdown = agenda_markdown.clone();
|
||||
let completed_group_context = completed_group_context.clone();
|
||||
let completed_role_context = completed_role_context.clone();
|
||||
role_brief_jobs.spawn(async move {
|
||||
build_agent_role_brief_draft(
|
||||
root,
|
||||
app_config,
|
||||
*definition,
|
||||
*role_definition,
|
||||
prompt,
|
||||
short_memory,
|
||||
long_memory,
|
||||
project_blackboard,
|
||||
spec_markdown,
|
||||
findings_markdown,
|
||||
agenda_markdown,
|
||||
completed_group_context,
|
||||
completed_role_context,
|
||||
pass,
|
||||
)
|
||||
.await
|
||||
});
|
||||
}
|
||||
let mut role_brief_drafts = Vec::new();
|
||||
while let Some(result) = role_brief_jobs.join_next().await {
|
||||
let draft = result.map_err(|error| format!("角色 Agent 并行任务失败:{error}"))??;
|
||||
role_brief_drafts.push(draft);
|
||||
}
|
||||
role_brief_drafts.sort_by_key(|draft| {
|
||||
ordered_roles
|
||||
.iter()
|
||||
.position(|(_, role)| role.task_id == draft.role_definition.task_id)
|
||||
.unwrap_or(usize::MAX)
|
||||
});
|
||||
for draft in role_brief_drafts {
|
||||
let relative_path = write_agent_role_brief(
|
||||
root,
|
||||
pass,
|
||||
);
|
||||
let (markdown, tool_id, summary) =
|
||||
if has_game_creator_agent_llm_override(app_config, role_definition.task_id) {
|
||||
let markdown = request_agent_role_brief_with_config(
|
||||
app_config,
|
||||
role_definition.task_id,
|
||||
&local_markdown,
|
||||
)
|
||||
.await?;
|
||||
(
|
||||
markdown,
|
||||
format!("llm.chat.{}", role_definition.task_id),
|
||||
format!(
|
||||
"{} / {} 使用 agentLlm.{} 生成 brief",
|
||||
definition.label, role_definition.role, role_definition.task_id
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
local_markdown,
|
||||
role_definition.tool_id.to_string(),
|
||||
format!(
|
||||
"本地编排生成 {} / {} brief",
|
||||
definition.label, role_definition.role
|
||||
),
|
||||
)
|
||||
};
|
||||
let relative_path =
|
||||
write_agent_role_brief(root, pass, definition, *role_definition, &markdown)?;
|
||||
draft.group_definition,
|
||||
draft.role_definition,
|
||||
&draft.markdown,
|
||||
)?;
|
||||
let role_brief = AgentRoleBrief {
|
||||
group_definition: definition,
|
||||
role_definition: *role_definition,
|
||||
markdown,
|
||||
group_definition: draft.group_definition,
|
||||
role_definition: draft.role_definition,
|
||||
markdown: draft.markdown,
|
||||
relative_path,
|
||||
memory_relative_path: agent_memory_relative_path,
|
||||
status: "completed".to_string(),
|
||||
tool_id,
|
||||
summary,
|
||||
memory_relative_path: draft.memory_relative_path,
|
||||
status: draft.status,
|
||||
tool_id: draft.tool_id,
|
||||
summary: draft.summary,
|
||||
};
|
||||
completed_role_context.push_str(&render_agent_role_brief_context(&role_brief));
|
||||
role_briefs.push(role_brief);
|
||||
role_briefs_by_task.insert(role_brief.role_definition.task_id.to_string(), role_brief);
|
||||
}
|
||||
completed_group_context = render_completed_agent_group_context(&role_briefs_by_task);
|
||||
}
|
||||
|
||||
let mut briefs = Vec::new();
|
||||
for definition in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
let role_briefs = definition
|
||||
.roles
|
||||
.iter()
|
||||
.filter_map(|role_definition| role_briefs_by_task.get(role_definition.task_id).cloned())
|
||||
.collect::<Vec<_>>();
|
||||
let markdown = render_agent_group_brief_markdown(&role_briefs);
|
||||
let relative_path = write_agent_group_brief(root, pass, definition, &markdown)?;
|
||||
completed_group_context.push_str(&format!(
|
||||
"## {} / {}\n\n{}\n\n",
|
||||
definition.label,
|
||||
definition.role,
|
||||
markdown.trim()
|
||||
));
|
||||
briefs.push(AgentGroupBrief {
|
||||
definition,
|
||||
markdown,
|
||||
@@ -921,6 +1043,128 @@ pub(crate) async fn request_agent_group_briefs_with_client(
|
||||
Ok(briefs)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct AgentRoleBriefDraft {
|
||||
group_definition: AgentGroupDefinition,
|
||||
role_definition: AgentRoleDefinition,
|
||||
markdown: String,
|
||||
memory_relative_path: String,
|
||||
status: String,
|
||||
tool_id: String,
|
||||
summary: String,
|
||||
}
|
||||
|
||||
fn ordered_game_creator_agent_roles(
|
||||
) -> Vec<(&'static AgentGroupDefinition, &'static AgentRoleDefinition)> {
|
||||
GAME_CREATOR_AGENT_GROUP_DEFINITIONS
|
||||
.iter()
|
||||
.flat_map(|group_definition| {
|
||||
group_definition
|
||||
.roles
|
||||
.iter()
|
||||
.map(move |role_definition| (group_definition, role_definition))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_completed_agent_group_context(
|
||||
role_briefs_by_task: &BTreeMap<String, AgentRoleBrief>,
|
||||
) -> String {
|
||||
let mut output = String::new();
|
||||
for group_definition in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
|
||||
let role_briefs = group_definition
|
||||
.roles
|
||||
.iter()
|
||||
.filter_map(|role_definition| role_briefs_by_task.get(role_definition.task_id).cloned())
|
||||
.collect::<Vec<_>>();
|
||||
if role_briefs.is_empty() {
|
||||
continue;
|
||||
}
|
||||
output.push_str(&format!(
|
||||
"## {} / {}\n\n{}\n\n",
|
||||
group_definition.label,
|
||||
group_definition.role,
|
||||
render_agent_group_brief_markdown(&role_briefs).trim()
|
||||
));
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
async fn build_agent_role_brief_draft(
|
||||
root: PathBuf,
|
||||
app_config: GameCreatorAppConfig,
|
||||
definition: AgentGroupDefinition,
|
||||
role_definition: AgentRoleDefinition,
|
||||
prompt: String,
|
||||
short_memory: String,
|
||||
long_memory: String,
|
||||
project_blackboard: String,
|
||||
spec_markdown: String,
|
||||
findings_markdown: String,
|
||||
agenda_markdown: String,
|
||||
completed_group_context: String,
|
||||
completed_role_context: String,
|
||||
pass: u8,
|
||||
) -> Result<AgentRoleBriefDraft, String> {
|
||||
let agent_memory_relative_path = agent_role_memory_relative_path(definition, role_definition);
|
||||
let agent_memory = read_optional_text(&root.join(&agent_memory_relative_path))?;
|
||||
let agent_conversation_context =
|
||||
render_local_conversation_prompt_context(&root, Some(role_definition.task_id))?;
|
||||
let role_short_memory = append_prompt_context(&agent_conversation_context, &short_memory);
|
||||
let local_markdown = render_local_agent_role_brief(
|
||||
definition,
|
||||
role_definition,
|
||||
&prompt,
|
||||
&role_short_memory,
|
||||
&long_memory,
|
||||
&project_blackboard,
|
||||
&agent_memory,
|
||||
&spec_markdown,
|
||||
&findings_markdown,
|
||||
&agenda_markdown,
|
||||
&completed_group_context,
|
||||
&completed_role_context,
|
||||
pass,
|
||||
);
|
||||
let (markdown, tool_id, summary) =
|
||||
if has_game_creator_agent_llm_override(&app_config, role_definition.task_id) {
|
||||
let markdown = request_agent_role_brief_with_config(
|
||||
&app_config,
|
||||
role_definition.task_id,
|
||||
&local_markdown,
|
||||
)
|
||||
.await?;
|
||||
(
|
||||
markdown,
|
||||
format!("llm.chat.{}", role_definition.task_id),
|
||||
format!(
|
||||
"{} / {} 使用 agentLlm.{} 生成 brief",
|
||||
definition.label, role_definition.role, role_definition.task_id
|
||||
),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
local_markdown,
|
||||
role_definition.tool_id.to_string(),
|
||||
format!(
|
||||
"本地编排生成 {} / {} brief",
|
||||
definition.label, role_definition.role
|
||||
),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(AgentRoleBriefDraft {
|
||||
group_definition: definition,
|
||||
role_definition,
|
||||
markdown,
|
||||
memory_relative_path: agent_memory_relative_path,
|
||||
status: "completed".to_string(),
|
||||
tool_id,
|
||||
summary,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn has_game_creator_agent_llm_override(
|
||||
config: &GameCreatorAppConfig,
|
||||
agent_id: &str,
|
||||
|
||||
@@ -237,6 +237,17 @@ pub(crate) async fn chat_with_game_creator_agent(
|
||||
chat_with_game_creator_agent_at(root, prompt.trim()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) async fn chat_with_game_creator_role_agent(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
prompt: String,
|
||||
) -> Result<GameCreatorChatAgentReply, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
chat_with_game_creator_role_agent_at(root, agent_id.trim(), prompt.trim()).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn check_game_creator_llm_config() -> GameCreatorLlmConfigStatus {
|
||||
check_game_creator_llm_config_from_config()
|
||||
|
||||
@@ -927,6 +927,7 @@ fn main() {
|
||||
control_agent_run,
|
||||
generate_local_game_draft,
|
||||
chat_with_game_creator_agent,
|
||||
chat_with_game_creator_role_agent,
|
||||
check_game_creator_llm_config,
|
||||
read_game_creator_app_config,
|
||||
write_game_creator_app_config,
|
||||
|
||||
@@ -2,7 +2,7 @@ use super::*;
|
||||
use serde_json::Value;
|
||||
use std::io::{Read, Write};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::sync::{Mutex as StdMutex, MutexGuard as StdMutexGuard};
|
||||
use std::sync::{Arc, Condvar, Mutex as StdMutex, MutexGuard as StdMutexGuard};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use zip::write::SimpleFileOptions;
|
||||
|
||||
@@ -589,6 +589,63 @@ fn spawn_mock_llm_server_responses_with_capture(
|
||||
base_url
|
||||
}
|
||||
|
||||
fn spawn_barrier_mock_llm_server(
|
||||
response_content: String,
|
||||
barrier: Arc<(StdMutex<usize>, Condvar)>,
|
||||
expected_requests: usize,
|
||||
request_sender: mpsc::Sender<String>,
|
||||
) -> String {
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("mock llm bind");
|
||||
let base_url = format!("http://{}", listener.local_addr().expect("mock llm addr"));
|
||||
std::thread::spawn(move || {
|
||||
let (mut stream, _) = listener.accept().expect("mock llm accept");
|
||||
let mut request_buffer = [0_u8; 8192];
|
||||
let read_len = stream.read(&mut request_buffer).unwrap_or(0);
|
||||
let _ =
|
||||
request_sender.send(String::from_utf8_lossy(&request_buffer[..read_len]).into_owned());
|
||||
let (lock, cvar) = &*barrier;
|
||||
let mut count = lock.lock().expect("barrier lock");
|
||||
*count += 1;
|
||||
cvar.notify_all();
|
||||
while *count < expected_requests {
|
||||
let wait_result = cvar
|
||||
.wait_timeout(count, Duration::from_secs(2))
|
||||
.expect("barrier wait");
|
||||
count = wait_result.0;
|
||||
if wait_result.1.timed_out() && *count < expected_requests {
|
||||
let body = r#"{"error":"parallel barrier timeout"}"#;
|
||||
let response = format!(
|
||||
"HTTP/1.1 500 Internal Server Error\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("mock llm timeout response");
|
||||
return;
|
||||
}
|
||||
}
|
||||
drop(count);
|
||||
let body = serde_json::json!({
|
||||
"id": "resp_game_creator_mock",
|
||||
"model": "mock-game-model",
|
||||
"output_text": response_content,
|
||||
"status": "completed",
|
||||
"usage": { "input_tokens": 11, "output_tokens": 22, "total_tokens": 33 }
|
||||
})
|
||||
.to_string();
|
||||
let response = format!(
|
||||
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",
|
||||
body.len(),
|
||||
body
|
||||
);
|
||||
stream
|
||||
.write_all(response.as_bytes())
|
||||
.expect("mock llm response");
|
||||
});
|
||||
base_url
|
||||
}
|
||||
|
||||
fn spawn_mock_external_canvas_api_server() -> String {
|
||||
let listener = TcpListener::bind(("127.0.0.1", 0)).expect("mock canvas api bind");
|
||||
let base_url = format!(
|
||||
@@ -848,10 +905,16 @@ async fn chat_with_game_creator_agent_uses_project_context_and_chat_llm_route()
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
fs::write(root.join("memory/session.md"), "短期记忆:用户偏好轻快节奏")
|
||||
.expect("write session memory");
|
||||
fs::write(root.join("memory/project.md"), "长期记忆:项目核心是月光厨房")
|
||||
.expect("write project memory");
|
||||
fs::write(root.join(PROJECT_BLACKBOARD_MEMORY_PATH), "黑板:角色要先有规范图")
|
||||
.expect("write blackboard memory");
|
||||
fs::write(
|
||||
root.join("memory/project.md"),
|
||||
"长期记忆:项目核心是月光厨房",
|
||||
)
|
||||
.expect("write project memory");
|
||||
fs::write(
|
||||
root.join(PROJECT_BLACKBOARD_MEMORY_PATH),
|
||||
"黑板:角色要先有规范图",
|
||||
)
|
||||
.expect("write blackboard memory");
|
||||
upload_local_asset_at(&root, "hero.png", "image/png", b"fake-png").expect("asset upload");
|
||||
append_local_conversation_message_at(
|
||||
&root,
|
||||
@@ -911,6 +974,81 @@ async fn chat_with_game_creator_agent_uses_project_context_and_chat_llm_route()
|
||||
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();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
fs::write(root.join("memory/session.md"), "短期记忆:用户偏好轻快节奏")
|
||||
.expect("write session memory");
|
||||
fs::write(
|
||||
root.join("memory/project.md"),
|
||||
"长期记忆:项目核心是月光厨房",
|
||||
)
|
||||
.expect("write project memory");
|
||||
fs::write(
|
||||
root.join(PROJECT_BLACKBOARD_MEMORY_PATH),
|
||||
"黑板:角色要先有规范图",
|
||||
)
|
||||
.expect("write blackboard memory");
|
||||
write_local_agent_memory_at(&root, "art-director", "私有记忆:偏好厚涂剪影")
|
||||
.expect("write art memory");
|
||||
append_local_conversation_message_at(
|
||||
&root,
|
||||
Some("art-director"),
|
||||
LocalConversationMessage {
|
||||
role: "user".to_string(),
|
||||
content: "上一轮:美术要强化开罗风轮廓".to_string(),
|
||||
agent_id: None,
|
||||
},
|
||||
)
|
||||
.expect("append agent conversation");
|
||||
|
||||
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-director": {{
|
||||
"apiKey": "art-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "art-chat-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
let reply =
|
||||
chat_with_game_creator_role_agent_at(&root, "art-director", "我要生成一个开罗风格的 dota")
|
||||
.await
|
||||
.expect("role chat reply");
|
||||
|
||||
assert_eq!(reply.reply_text, "可以,我会先收敛角色规范图的视觉方向。");
|
||||
let request = receiver
|
||||
.recv_timeout(Duration::from_secs(1))
|
||||
.expect("captured role chat llm request");
|
||||
assert!(request.contains("POST /responses HTTP/1.1"));
|
||||
assert!(request.contains("art-chat-model"));
|
||||
assert!(request.contains("美术组 / Director"));
|
||||
assert!(request.contains("taskId=art-director"));
|
||||
assert!(request.contains("私有记忆:偏好厚涂剪影"));
|
||||
assert!(request.contains("上一轮:美术要强化开罗风轮廓"));
|
||||
assert!(request.contains("我要生成一个开罗风格的 dota"));
|
||||
assert!(!request.contains("global-model"));
|
||||
assert!(!request.contains("global-key"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_loop_uses_per_agent_llm_overrides() {
|
||||
let root = unique_project_path();
|
||||
@@ -998,6 +1136,107 @@ async fn agent_loop_uses_per_agent_llm_overrides() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn agent_role_briefs_run_same_wave_llm_agents_in_parallel() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "local-project-draft", "未命名游戏原型")
|
||||
.expect("init project");
|
||||
let agenda_path = root.join(".agent/passes/pass-1/agenda.md");
|
||||
fs::create_dir_all(agenda_path.parent().expect("agenda parent")).expect("agenda dir");
|
||||
fs::write(&agenda_path, "# Test Agenda\n").expect("write agenda");
|
||||
|
||||
let barrier = Arc::new((StdMutex::new(0_usize), Condvar::new()));
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let design_director_base_url = spawn_barrier_mock_llm_server(
|
||||
"Director 并行 brief".to_string(),
|
||||
Arc::clone(&barrier),
|
||||
2,
|
||||
sender.clone(),
|
||||
);
|
||||
let design_foundation_base_url = spawn_barrier_mock_llm_server(
|
||||
"Gameplay 并行 brief".to_string(),
|
||||
Arc::clone(&barrier),
|
||||
2,
|
||||
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": {{
|
||||
"design-director": {{
|
||||
"apiKey": "design-director-key",
|
||||
"baseUrl": {design_director_base_url:?},
|
||||
"model": "design-director-model",
|
||||
"apiKind": "openai_responses"
|
||||
}},
|
||||
"design-foundation": {{
|
||||
"apiKey": "design-foundation-key",
|
||||
"baseUrl": {design_foundation_base_url:?},
|
||||
"model": "design-foundation-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
let app_config = load_game_creator_app_config().expect("load app config");
|
||||
let agenda = AgentPassAgenda {
|
||||
relative_path: ".agent/passes/pass-1/agenda.md".to_string(),
|
||||
task_graph_relative_path: ".agent/passes/pass-1/task-graph.json".to_string(),
|
||||
active_task_ids: vec![
|
||||
"design-director".to_string(),
|
||||
"design-foundation".to_string(),
|
||||
],
|
||||
carried_task_ids: vec![],
|
||||
dependency_waves: vec![vec![
|
||||
"design-director".to_string(),
|
||||
"design-foundation".to_string(),
|
||||
]],
|
||||
repair_focus: vec![],
|
||||
repair_routes: vec![],
|
||||
summary: "并行测试".to_string(),
|
||||
};
|
||||
|
||||
let briefs = request_agent_group_briefs_with_client(
|
||||
&root,
|
||||
&app_config,
|
||||
"做一个开罗风格动作游戏",
|
||||
"短期记忆",
|
||||
"长期记忆",
|
||||
"项目黑板",
|
||||
"Planner 规格",
|
||||
"Evaluator 反馈",
|
||||
&agenda,
|
||||
1,
|
||||
)
|
||||
.await
|
||||
.expect("role briefs");
|
||||
|
||||
let captured_requests = receiver
|
||||
.try_iter()
|
||||
.filter(|request| request.contains("POST /responses HTTP/1.1"))
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(captured_requests.len(), 2);
|
||||
assert!(captured_requests
|
||||
.iter()
|
||||
.any(|request| request.contains("design-director-model")));
|
||||
assert!(captured_requests
|
||||
.iter()
|
||||
.any(|request| request.contains("design-foundation-model")));
|
||||
let design_group = briefs
|
||||
.iter()
|
||||
.find(|brief| brief.definition.id == "design")
|
||||
.expect("design group");
|
||||
assert!(design_group.markdown.contains("Director 并行 brief"));
|
||||
assert!(design_group.markdown.contains("Gameplay 并行 brief"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn llm_config_check_reports_status_without_leaking_key() {
|
||||
let missing = check_game_creator_llm_config_values(
|
||||
|
||||
@@ -2774,8 +2774,8 @@ export function WorkspaceLauncher({
|
||||
setRecentWorkspaceStatuses({});
|
||||
}
|
||||
|
||||
function validateAgentChatProjectPath() {
|
||||
const trimmedProjectPath = agentChatProjectPath.trim();
|
||||
function validateAgentChatProjectPath(projectPath = agentChatProjectPath) {
|
||||
const trimmedProjectPath = projectPath.trim();
|
||||
if (!trimmedProjectPath || !isAbsoluteProjectPath(trimmedProjectPath)) {
|
||||
setAgentChatStatus('请提供项目绝对路径');
|
||||
return null;
|
||||
@@ -2787,10 +2787,12 @@ export function WorkspaceLauncher({
|
||||
return trimmedProjectPath;
|
||||
}
|
||||
|
||||
function selectedLauncherAgentChatAgent(): LauncherAgentChatAgent | null {
|
||||
function selectedLauncherAgentChatAgent(
|
||||
agentId = agentChatSelectedAgentId,
|
||||
): LauncherAgentChatAgent | null {
|
||||
return (
|
||||
launcherAgentChatAgents.find(
|
||||
(agent) => agent.id === agentChatSelectedAgentId,
|
||||
(agent) => agent.id === agentId,
|
||||
) ??
|
||||
launcherAgentChatAgents[0] ??
|
||||
null
|
||||
@@ -2812,16 +2814,20 @@ export function WorkspaceLauncher({
|
||||
setAgentChatStatus('已取消');
|
||||
return;
|
||||
}
|
||||
setAgentChatProjectPath(selectedPath);
|
||||
setAgentChatStatus('已选择项目目录');
|
||||
setAgentChatProjectPath(selectedPath);
|
||||
void loadAgentChatConversation(agentChatSelectedAgentId, selectedPath);
|
||||
} catch (error) {
|
||||
setAgentChatStatus(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAgentChatConversation() {
|
||||
const projectPathForChat = validateAgentChatProjectPath();
|
||||
const agent = selectedLauncherAgentChatAgent();
|
||||
async function loadAgentChatConversation(
|
||||
agentId = agentChatSelectedAgentId,
|
||||
projectPath = agentChatProjectPath,
|
||||
) {
|
||||
const projectPathForChat = validateAgentChatProjectPath(projectPath);
|
||||
const agent = selectedLauncherAgentChatAgent(agentId);
|
||||
if (!projectPathForChat || !agent) {
|
||||
return;
|
||||
}
|
||||
@@ -2896,6 +2902,18 @@ export function WorkspaceLauncher({
|
||||
return;
|
||||
}
|
||||
setAgentChatMessages(savedUserResult.messages);
|
||||
setAgentChatStatus('Agent 正在思考');
|
||||
const reply = await invoke<GameCreatorChatAgentReply>(
|
||||
'chat_with_game_creator_role_agent',
|
||||
{
|
||||
projectPath: projectPathForChat,
|
||||
agentId: agent.id,
|
||||
prompt: content,
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
const assistantResult = await invoke<LocalConversationResult>(
|
||||
'append_local_conversation_message',
|
||||
{
|
||||
@@ -2903,7 +2921,7 @@ export function WorkspaceLauncher({
|
||||
agentId: agent.id,
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content: localAgentConversationReceipt(agent),
|
||||
content: reply.replyText,
|
||||
agentId: null,
|
||||
},
|
||||
},
|
||||
@@ -2922,7 +2940,7 @@ export function WorkspaceLauncher({
|
||||
if (savedUserResult) {
|
||||
setAgentChatMessages(savedUserResult.messages);
|
||||
setAgentChatStatus(
|
||||
`已保存用户消息;Agent 回执失败:${
|
||||
`已保存用户消息;Agent 回复失败:${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
@@ -3548,8 +3566,7 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
onClick={() => {
|
||||
setAgentChatSelectedAgentId(agent.id);
|
||||
setAgentChatMessages([]);
|
||||
setAgentChatStatus('已切换 Agent,请读取历史');
|
||||
void loadAgentChatConversation(agent.id);
|
||||
}}
|
||||
>
|
||||
<strong>{agent.title}</strong>
|
||||
|
||||
@@ -925,6 +925,11 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
})),
|
||||
};
|
||||
}
|
||||
if (command === 'chat_with_game_creator_role_agent') {
|
||||
return {
|
||||
replyText: `Agent 回复:${String(args?.prompt ?? '')}`,
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
@@ -954,7 +959,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。',
|
||||
'Agent 回复:请单独评估这个角色设定流程',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('append_local_conversation_message', {
|
||||
@@ -971,11 +976,59 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
agentId: 'design-director',
|
||||
message: {
|
||||
role: 'assistant',
|
||||
content:
|
||||
'已记录给 拆解创作方向。下一次生成会把这条对话作为该 agent 的上下文读取。',
|
||||
content: 'Agent 回复:请单独评估这个角色设定流程',
|
||||
agentId: null,
|
||||
},
|
||||
});
|
||||
expect(invoke).toHaveBeenCalledWith('chat_with_game_creator_role_agent', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: 'design-director',
|
||||
prompt: '请单独评估这个角色设定流程',
|
||||
});
|
||||
});
|
||||
|
||||
it('loads selected developer agent history immediately after switching agents', async () => {
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'read_local_conversation') {
|
||||
if (args?.agentId === 'art-director') {
|
||||
return {
|
||||
path: '/tmp/authorized-game/.agent/conversations/agents/art-director.jsonl',
|
||||
agentId: 'art-director',
|
||||
messages: [
|
||||
{
|
||||
schemaVersion: '1',
|
||||
role: 'assistant',
|
||||
content: '美术 Agent 历史已加载',
|
||||
agentId: null,
|
||||
updatedAt: 1000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
return {
|
||||
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
|
||||
agentId: args?.agentId,
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderLauncherAgentChatAt('/?agent-chat');
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
|
||||
target: { value: '/tmp/authorized-game' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: /确定视觉方向/ }));
|
||||
|
||||
expect(await screen.findByText('美术 Agent 历史已加载')).not.toBeNull();
|
||||
expect(screen.queryByText('暂无对话')).toBeNull();
|
||||
expect(invoke).toHaveBeenCalledWith('read_local_conversation', {
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: 'art-director',
|
||||
});
|
||||
});
|
||||
|
||||
it('refreshes recent project status before entering the project placeholder', async () => {
|
||||
|
||||
@@ -29,7 +29,7 @@ Agent Runtime 负责:
|
||||
## Agent 能力清单
|
||||
|
||||
- 用户能力:聊天入口、上传文件;正式用户窗口不展示任务、文件、预览、日志、能力清单或开发专用单 Agent 聊天入口。
|
||||
- 开发窗口能力:debug 构建额外打开 `developer` 窗口,走 `index.html?agent-chat`;开发者可选择 Agent、授权本地项目路径,并通过 `read_local_conversation` / `append_local_conversation_message` 读写 `.agent/conversations/agents/<agentId>.jsonl`,用于单独调试某个 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 的长期对话上下文。
|
||||
- 命令能力:内置命令调用、权限 gate、执行日志;v1 只允许白名单受限命令,不执行任意 shell。
|
||||
- 编排能力:任务拆分、任务图依赖、专业组调度、多智能体协作。
|
||||
- 任务图能力:每轮 Orchestrator agenda、ready / active task 选择、Evaluator 结构化返工路由、返工轮 carry-over。
|
||||
@@ -160,7 +160,7 @@ game-project/
|
||||
- 结构化对话记录按授权本地项目路径追加 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。
|
||||
- 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/conversations/agents/<agentId>.jsonl`,普通用户窗口不得出现 `Agent 聊天` 导航或入口。
|
||||
- debug 构建启动后在用户 `client` 窗口之外额外打开 `developer` 窗口;该窗口用于开发者单独选择 Agent、切换时自动读取该 Agent 历史,并把用户消息和真实 Agent 回复持久化到 `.agent/conversations/agents/<agentId>.jsonl`,普通用户窗口不得出现 `Agent 聊天` 导航或入口。
|
||||
- 首页最近项目只展示最近 3 个有效项目;项目组页在同一窗口管理最近项目、打开项目、新建项目和显示目录。打开项目只读取已初始化项目并切到项目开发占位页,不打开第二窗口;新建项目仍沿用非空目录确认,不自动重建无效历史路径。
|
||||
- 项目开发占位页保留左侧栏和顶部栏,展示项目名、路径、创建模式、首条需求、附件导入结果、最近 run 状态和后续“项目开发画布”占位;本轮不落地真正画板 + Agent 双栏。
|
||||
|
||||
@@ -259,12 +259,12 @@ game-project/
|
||||
- 主窗口“配置”面板和聊天 `/config` 命令读写 Tauri 应用配置目录中的 `game-creator.config.json`,覆盖 LLM API Key、base URL、模型、API 类型、流式请求、超时、重试和画板 External API 配置;主聊天 Agent、Planner、Orchestrator、Generator、Evaluator 和 16 个角色 agent 都可在 `agentLlm` 中单独覆盖 API Key、base URL、模型、API 类型和流式请求,空项继承全局 LLM 配置;主聊天 Agent 使用 `agentLlm.chat`,默认生成链路仍只让 Planner / Generator 调 LLM,配置了 `agentLlm.<taskId>` 的角色 agent 会改用自己的 provider 生成 brief,未配置的角色 agent 继续使用本地 brief;生成游戏、主聊天或平台美术时如果返回 LLM / editorApi 缺配置错误,主窗口自动打开同一个运行时配置弹窗;API Key 输入框使用密码字段并关闭自动填充,数值项在 UI 层夹住下限,Rust 写配置时也拒绝过低超时,保存时只写运行时配置文件,不写仓库模板、本地项目、trace 或 manifest。
|
||||
- 聊天输入 `/llm-status` 会触发只读 `llm.config_check`,确认全局 LLM 以及各 agent resolved 后的 base_url、model、API 类型和 API Key 是否已从客户端配置读取;状态消息不会显示或保存 API Key;当前生成链路只要求 Planner / Generator 就绪。`/llm-routes` 复用同一检查结果,但输出按 agent 展开的路由清单和缺口摘要,用于确认哪些 agent 解析后走全局路由、哪些 agent 走单独 provider。
|
||||
- `game.generate_draft` 的 LLM JSON 必须包含 `handoffs` 数组,覆盖 `design`、`balance`、`art`、`audio`、`code`、`publishing` 6 个专业组;每组必须给出 role、summary、outputs 和 next,缺组或交接内容不完整会判定为模型输出无效并进入返工。
|
||||
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;每个角色 brief 必须读取自己的私有记忆 `memory/agents/<group>/<role>.md` 和项目黑板 `memory/blackboard.md`,写入 `.agent/passes/pass-N/groups/<group>/*.md`,再汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。
|
||||
- `game.generate_draft` 的真实生成路径使用最小 Planner / Orchestrator / 组内角色 agent / Generator / Evaluator loop:Planner 写 `.agent/spec.md`;每轮 Orchestrator 先写 `.agent/passes/pass-N/agenda.md` 和 `.agent/passes/pass-N/task-graph.json`,首轮全量调度 16 个角色任务,返工轮按 `.agent/findings.md` 生成结构化 `repairRoutes`,重跑命中问题的角色任务及其下游依赖任务,其余角色 brief 从上一轮 carry-over;`task-graph.json` 记录 activeTaskIds、carriedTaskIds、repairFocus、repairRoutes 和按依赖排序的 dependencyWaves;角色 brief 执行层按 dependencyWaves 调度,wave 之间串行、同一 wave 内多个角色 Agent 以 async task 并行运行;每个角色 brief 必须读取自己的私有记忆 `memory/agents/<group>/<role>.md`、本 Agent 对话和项目黑板 `memory/blackboard.md`,写入 `.agent/passes/pass-N/groups/<group>/*.md`,再按原组顺序汇总为 `.agent/passes/pass-N/groups/*.md`;Generator 必须读取用户需求、记忆、`.agent/spec.md`、本轮 `agenda.md`、`task-graph.json`、`.agent/findings.md` 和 6 组汇总 brief 后返回结构化 JSON;每轮会把 Generator 草案拆成 6 组交接快照,写入 `.agent/passes/pass-N/`;Evaluator 做质量评审并写 `.agent/findings.md`,通过后才进入 `game.static_smoke` 静态自检和预览试玩。
|
||||
- loop 最多执行 3 轮;Evaluator 发现 HTML 非自包含、缺少 `canvas`、缺少 `requestAnimationFrame`、缺少输入监听或用户输入未转义时,把问题写入 `.agent/findings.md` 并让下一轮 Generator 修复。3 轮仍失败则 `game.generate_draft` 失败,不写最终游戏产物。
|
||||
- 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;用户提交后先追加用户消息,再追加一条本地 agent 回执,二者都写入对应 `.agent/conversations/agents/<agentId>.jsonl`。单 agent 面板可把当前输入手动追加到对应 `memory/agents/<group>/<role>.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md` 或 `memory/agents/<group>/<role>.md`;v1 不把本地回执伪装成实时 LLM 回复。
|
||||
- 单 agent 对话入口读取对应 agent conversation;用户提交后先追加用户消息,再调用 `chat_with_game_creator_role_agent` 让对应 `agentLlm.<agentId>` 结合项目上下文、Agent 私有记忆和本 Agent 历史对话生成回复,随后把回复写入对应 `.agent/conversations/agents/<agentId>.jsonl`。单 agent 面板可把当前输入手动追加到对应 `memory/agents/<group>/<role>.md`,写入前复用 `memory.write` 项目策略和本地项目锁;最近对话可作为本次生成 prompt 上下文读取,但只有经过显式总结、用户显式手动沉淀或生成 loop 成功沉淀的稳定结论,才追加到 `memory/blackboard.md` 或 `memory/agents/<group>/<role>.md`。
|
||||
- `.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 后新增的受跟踪项目文件。
|
||||
|
||||
Reference in New Issue
Block a user