补齐Agent Runtime多轮观察循环
后台 Agent 任务改为最多三轮 plan/action/observation/replan 循环。 Runtime state 新增 nextStep,并在状态面板和主 Agent 卡片展示下一步。 补充多轮重规划测试、前端状态展示断言,并同步实施计划和决策记录。
This commit is contained in:
@@ -402,98 +402,22 @@ async fn run_game_creator_agent_background_task(
|
||||
task: String,
|
||||
state: AgentRuntimeState,
|
||||
) {
|
||||
let mut runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
state,
|
||||
"planning",
|
||||
"生成 Agent 工具计划",
|
||||
"后台任务已开始执行,正在让 Agent 规划下一步动作。",
|
||||
) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
let fallback = default_game_creator_agent_runtime_state(&agent_id, "");
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let plan = match request_game_creator_agent_background_tool_plan_at(&root, &agent_id, &task)
|
||||
.await
|
||||
{
|
||||
Ok(plan) => plan,
|
||||
Err(error) => {
|
||||
let failed_runtime = fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: format!("后台任务失败:{error}"),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"source": runtime.source,
|
||||
"error": runtime.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !plan.thinking_summary.trim().is_empty() {
|
||||
runtime.observations.push(format!(
|
||||
"思考摘要:{}",
|
||||
sanitize_agent_runtime_text(&plan.thinking_summary, 240)
|
||||
));
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"thinking_summary",
|
||||
runtime.status.as_str(),
|
||||
runtime.phase.as_str(),
|
||||
"Agent 已形成任务理解摘要。",
|
||||
Some(&plan.thinking_summary),
|
||||
);
|
||||
}
|
||||
if !plan.plan.is_empty() {
|
||||
runtime.plan = plan.plan.clone();
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"plan",
|
||||
runtime.status.as_str(),
|
||||
runtime.phase.as_str(),
|
||||
"Agent 已生成行动计划。",
|
||||
Some(&runtime.plan.join(" / ")),
|
||||
);
|
||||
}
|
||||
|
||||
let mut runtime = state;
|
||||
let mut plan = AgentRuntimeToolPlan::default();
|
||||
let mut observations = Vec::new();
|
||||
for action in plan
|
||||
.actions
|
||||
.iter()
|
||||
.take(AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT)
|
||||
{
|
||||
let mut final_reply = None;
|
||||
|
||||
for loop_index in 0..AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT {
|
||||
runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
runtime,
|
||||
"action",
|
||||
&format!("调用工具 {}", action.tool),
|
||||
action.reason.as_deref().unwrap_or("Agent 请求工具动作。"),
|
||||
"planning",
|
||||
&format!("生成 Agent 工具计划(第 {} 轮)", loop_index + 1),
|
||||
if loop_index == 0 {
|
||||
"后台任务已开始执行,正在让 Agent 规划下一步动作。"
|
||||
} else {
|
||||
"Agent 已收到工具观察,正在修正计划并决定是否继续行动。"
|
||||
},
|
||||
) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
@@ -502,62 +426,164 @@ async fn run_game_creator_agent_background_task(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
|
||||
plan = match request_game_creator_agent_background_tool_plan_at(
|
||||
&root,
|
||||
&runtime,
|
||||
"action",
|
||||
runtime.status.as_str(),
|
||||
runtime.phase.as_str(),
|
||||
runtime.current_action.as_str(),
|
||||
action.reason.as_deref(),
|
||||
);
|
||||
let observation = execute_game_creator_agent_runtime_tool_action(&root, &agent_id, action);
|
||||
let observation_summary = observation.summary();
|
||||
runtime.observations.push(observation_summary.clone());
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"observation",
|
||||
runtime.status.as_str(),
|
||||
"observation",
|
||||
observation_summary.as_str(),
|
||||
observation.detail.as_deref(),
|
||||
);
|
||||
let _ = append_agent_db_record(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.tool_observation",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"runId": runtime.run_id,
|
||||
"tool": observation.tool,
|
||||
"status": observation.status,
|
||||
"summary": observation.summary,
|
||||
}),
|
||||
);
|
||||
observations.push(observation);
|
||||
&agent_id,
|
||||
&task,
|
||||
&observations,
|
||||
loop_index + 1,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(plan) => plan,
|
||||
Err(error) => {
|
||||
let failed_runtime =
|
||||
fail_game_creator_agent_runtime_turn_at(&root, runtime, &error);
|
||||
let _ = append_local_conversation_message_at(
|
||||
&root,
|
||||
Some(&agent_id),
|
||||
LocalConversationMessage {
|
||||
role: "assistant".to_string(),
|
||||
content: format!("后台任务失败:{error}"),
|
||||
agent_id: None,
|
||||
},
|
||||
);
|
||||
if let Ok(runtime) = failed_runtime {
|
||||
let _ = append_agent_db_record(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.background_task.failed",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"source": runtime.source,
|
||||
"error": runtime.error,
|
||||
}),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
if !plan.thinking_summary.trim().is_empty() {
|
||||
runtime.observations.push(format!(
|
||||
"第 {} 轮思考摘要:{}",
|
||||
loop_index + 1,
|
||||
sanitize_agent_runtime_text(&plan.thinking_summary, 240)
|
||||
));
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"thinking_summary",
|
||||
runtime.status.as_str(),
|
||||
runtime.phase.as_str(),
|
||||
&format!("Agent 已形成第 {} 轮任务理解摘要。", loop_index + 1),
|
||||
Some(&plan.thinking_summary),
|
||||
);
|
||||
}
|
||||
if !plan.plan.is_empty() {
|
||||
runtime.plan = plan.plan.clone();
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"plan",
|
||||
runtime.status.as_str(),
|
||||
runtime.phase.as_str(),
|
||||
&format!("Agent 已生成第 {} 轮行动计划。", loop_index + 1),
|
||||
Some(&runtime.plan.join(" / ")),
|
||||
);
|
||||
}
|
||||
|
||||
if plan.actions.is_empty() {
|
||||
if !plan.response.trim().is_empty() {
|
||||
final_reply = Some(plan.response.clone());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for action in plan
|
||||
.actions
|
||||
.iter()
|
||||
.take(AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT)
|
||||
{
|
||||
runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
runtime,
|
||||
"action",
|
||||
&format!("调用工具 {}", action.tool),
|
||||
action.reason.as_deref().unwrap_or("Agent 请求工具动作。"),
|
||||
) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
let fallback = default_game_creator_agent_runtime_state(&agent_id, "");
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"action",
|
||||
runtime.status.as_str(),
|
||||
runtime.phase.as_str(),
|
||||
runtime.current_action.as_str(),
|
||||
action.reason.as_deref(),
|
||||
);
|
||||
let observation =
|
||||
execute_game_creator_agent_runtime_tool_action(&root, &agent_id, action);
|
||||
let observation_summary = observation.summary();
|
||||
runtime.observations.push(observation_summary.clone());
|
||||
runtime.next_step = "把工具观察交给 Agent 修正计划".to_string();
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
&root,
|
||||
&runtime,
|
||||
"observation",
|
||||
runtime.status.as_str(),
|
||||
"observation",
|
||||
observation_summary.as_str(),
|
||||
observation.detail.as_deref(),
|
||||
);
|
||||
let _ = append_agent_db_record(
|
||||
&root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.tool_observation",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"runId": runtime.run_id,
|
||||
"tool": observation.tool,
|
||||
"status": observation.status,
|
||||
"summary": observation.summary,
|
||||
}),
|
||||
);
|
||||
observations.push(observation);
|
||||
}
|
||||
}
|
||||
|
||||
runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
runtime,
|
||||
"response",
|
||||
"根据观察生成最终回复",
|
||||
"Agent 已完成工具观察,正在整理最终回复。",
|
||||
) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
let fallback = default_game_creator_agent_runtime_state(&agent_id, "");
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let final_reply = if observations.is_empty() && !plan.response.trim().is_empty() {
|
||||
plan.response.clone()
|
||||
let final_reply = if let Some(reply) = final_reply {
|
||||
reply
|
||||
} else {
|
||||
runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
runtime,
|
||||
"response",
|
||||
"根据观察生成最终回复",
|
||||
"Agent 已完成工具观察,正在整理最终回复。",
|
||||
) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
let fallback = default_game_creator_agent_runtime_state(&agent_id, "");
|
||||
let _ = fail_game_creator_agent_runtime_turn_at(&root, fallback, &error);
|
||||
return;
|
||||
}
|
||||
};
|
||||
match request_game_creator_agent_background_final_reply_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
@@ -664,6 +690,7 @@ async fn run_game_creator_agent_background_task(
|
||||
);
|
||||
}
|
||||
|
||||
const AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT: usize = 3;
|
||||
const AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT: usize = 3;
|
||||
const AGENT_RUNTIME_TOOL_OBSERVATION_MAX_CHARS: usize = 900;
|
||||
pub(crate) const AGENT_RUNTIME_LOCK_STALE_AFTER_SECONDS: u64 = 300;
|
||||
@@ -711,9 +738,16 @@ async fn request_game_creator_agent_background_tool_plan_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
task: &str,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
loop_index: usize,
|
||||
) -> Result<AgentRuntimeToolPlan, String> {
|
||||
let (llm, config_path, request) =
|
||||
build_game_creator_agent_background_tool_plan_request(root, agent_id, task)?;
|
||||
let (llm, config_path, request) = build_game_creator_agent_background_tool_plan_request(
|
||||
root,
|
||||
agent_id,
|
||||
task,
|
||||
observations,
|
||||
loop_index,
|
||||
)?;
|
||||
let client = build_game_creator_llm_client_from_llm_config(&llm, &config_path)?;
|
||||
let response = request_game_creator_llm_text(&client, &llm, request)
|
||||
.await
|
||||
@@ -750,10 +784,18 @@ fn build_game_creator_agent_background_tool_plan_request(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
task: &str,
|
||||
observations: &[AgentRuntimeToolObservation],
|
||||
loop_index: usize,
|
||||
) -> Result<(GameCreatorLlmConfig, String, LlmRunRequest), String> {
|
||||
let (llm, config_path, context) = build_game_creator_role_agent_context(root, agent_id)?;
|
||||
let observations_json = if observations.is_empty() {
|
||||
"[]".to_string()
|
||||
} else {
|
||||
serde_json::to_string_pretty(observations)
|
||||
.map_err(|error| format!("序列化 Agent 工具观察失败:{error}"))?
|
||||
};
|
||||
let prompt = format!(
|
||||
"项目上下文如下。请先制定短计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\nJSON schema:{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|conversation.read|asset.list|project.index|file.read|blackboard.write|agent.message\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};file.read 使用 {{\"path\":\"项目内相对路径\"}};blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};其他工具 input 可为空。"
|
||||
"项目上下文如下。你正在执行后台 Agent loop 第 {loop_index} 轮。请基于目标、已有工具观察和当前上下文修正计划,再决定是否调用最多 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个白名单工具。只输出 JSON 对象,不要 markdown。\n\n{context}\n\n后台任务:\n{task}\n\n已有工具观察:\n{observations_json}\n\nJSON schema:{{\"thinkingSummary\":\"一句话理解\",\"plan\":[\"步骤\"],\"actions\":[{{\"tool\":\"memory.read|conversation.read|asset.list|project.index|file.read|blackboard.write|agent.message\",\"reason\":\"为什么需要\",\"input\":{{}}}}],\"response\":\"如果无需继续调用工具,可直接给最终回复\"}}\n\n工具输入约定:memory.read 使用 {{\"scope\":\"session|project|blackboard|agent\"}};file.read 使用 {{\"path\":\"项目内相对路径\"}};blackboard.write 使用 {{\"title\":\"标题\",\"content\":\"要共享给所有 Agent 的稳定结论\"}};agent.message 使用 {{\"agentId\":\"目标 taskId\",\"content\":\"给目标 Agent 的定向消息\"}};如果已有观察足够,请返回空 actions 并填写 response。其他工具 input 可为空。"
|
||||
);
|
||||
let request = LlmRunRequest::new(vec![
|
||||
LlmMessage::system(game_creator_agent_runtime_tool_plan_system_prompt()),
|
||||
@@ -1204,6 +1246,7 @@ pub(crate) fn start_game_creator_agent_runtime_task_at(
|
||||
state.phase = "planning".to_string();
|
||||
state.current_task = runtime_task.clone();
|
||||
state.current_action = current_action.trim().to_string();
|
||||
state.next_step = "等待 Agent 输出计划或回复".to_string();
|
||||
state.plan = plan;
|
||||
state.observations = vec!["已创建本轮 Agent Runtime run。".to_string()];
|
||||
state.updated_at = unix_timestamp();
|
||||
@@ -1244,6 +1287,7 @@ pub(crate) fn advance_game_creator_agent_runtime_turn_at(
|
||||
) -> Result<AgentRuntimeState, String> {
|
||||
state.phase = phase.trim().to_string();
|
||||
state.current_action = action.trim().to_string();
|
||||
state.next_step = agent_runtime_next_step_for_phase(&state.phase).to_string();
|
||||
if !observation.trim().is_empty() {
|
||||
state.observations.push(observation.trim().to_string());
|
||||
}
|
||||
@@ -1270,6 +1314,7 @@ pub(crate) fn finish_game_creator_agent_runtime_turn_at(
|
||||
state.status = "idle".to_string();
|
||||
state.phase = "completed".to_string();
|
||||
state.current_action = "等待下一轮输入".to_string();
|
||||
state.next_step = "等待下一轮输入".to_string();
|
||||
state.last_response = Some(sanitize_agent_runtime_text(response, 500));
|
||||
state.error = None;
|
||||
state
|
||||
@@ -1310,6 +1355,7 @@ pub(crate) fn fail_game_creator_agent_runtime_turn_at(
|
||||
state.status = "failed".to_string();
|
||||
state.phase = "failed".to_string();
|
||||
state.current_action = "等待开发者处理失败".to_string();
|
||||
state.next_step = "等待开发者处理失败".to_string();
|
||||
state.error = Some(sanitize_agent_runtime_text(error, 500));
|
||||
state.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_state(root, &state)?;
|
||||
@@ -1379,6 +1425,7 @@ fn default_game_creator_agent_runtime_state(agent_id: &str, run_id: &str) -> Age
|
||||
phase: "idle".to_string(),
|
||||
current_task: String::new(),
|
||||
current_action: "等待输入".to_string(),
|
||||
next_step: "等待输入".to_string(),
|
||||
plan: vec![
|
||||
"读取项目上下文".to_string(),
|
||||
"按角色职责推理".to_string(),
|
||||
@@ -1426,6 +1473,9 @@ fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, age
|
||||
if state.current_action.trim().is_empty() {
|
||||
state.current_action = "等待输入".to_string();
|
||||
}
|
||||
if state.next_step.trim().is_empty() {
|
||||
state.next_step = agent_runtime_next_step_for_phase(&state.phase).to_string();
|
||||
}
|
||||
if state.plan.is_empty() {
|
||||
state.plan = vec![
|
||||
"读取项目上下文".to_string(),
|
||||
@@ -1450,6 +1500,17 @@ fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, age
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_next_step_for_phase(phase: &str) -> &'static str {
|
||||
match phase.trim() {
|
||||
"planning" => "等待 Agent 输出计划或回复",
|
||||
"action" => "等待工具观察结果",
|
||||
"response" => "等待 Agent 整理最终回复",
|
||||
"completed" | "idle" => "等待下一轮输入",
|
||||
"failed" => "等待开发者处理失败",
|
||||
_ => "继续推进当前任务",
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_game_creator_agent_runtime_event(event: &mut AgentRuntimeEvent) {
|
||||
if event.schema_version.trim().is_empty() {
|
||||
event.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string();
|
||||
|
||||
@@ -156,6 +156,8 @@ struct AgentRuntimeState {
|
||||
#[serde(default)]
|
||||
current_action: String,
|
||||
#[serde(default)]
|
||||
next_step: String,
|
||||
#[serde(default)]
|
||||
plan: Vec<String>,
|
||||
#[serde(default)]
|
||||
observations: Vec<String>,
|
||||
|
||||
@@ -1495,6 +1495,7 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
|
||||
.expect("start background task");
|
||||
assert_eq!(started.state.status, "running");
|
||||
assert_eq!(started.state.current_action, "后台任务已投递");
|
||||
assert_eq!(started.state.next_step, "等待 Agent 输出计划或回复");
|
||||
|
||||
let plan_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
@@ -1523,6 +1524,7 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
|
||||
|
||||
assert_eq!(runtime.status, "idle");
|
||||
assert_eq!(runtime.phase, "completed");
|
||||
assert_eq!(runtime.next_step, "等待下一轮输入");
|
||||
assert_eq!(
|
||||
runtime.plan,
|
||||
vec![
|
||||
@@ -1576,6 +1578,142 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_replan_after_observation() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
fs::write(
|
||||
root.join("game/notes.txt"),
|
||||
"项目笔记:已有月光厨房核心循环,但缺少美术约束。",
|
||||
)
|
||||
.expect("write notes");
|
||||
fs::write(
|
||||
root.join(PROJECT_BLACKBOARD_MEMORY_PATH),
|
||||
"黑板:角色必须有月光围裙和暗影厨具对手。",
|
||||
)
|
||||
.expect("write blackboard");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let first_plan_json = serde_json::json!({
|
||||
"thinkingSummary": "先确认项目笔记是否已有核心循环",
|
||||
"plan": ["读取项目笔记", "根据笔记决定是否继续查约束"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "file.read",
|
||||
"reason": "需要先看到项目笔记里的核心循环",
|
||||
"input": { "path": "game/notes.txt" }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let second_plan_json = serde_json::json!({
|
||||
"thinkingSummary": "笔记确认核心循环存在,需要继续读取黑板约束",
|
||||
"plan": ["读取项目黑板", "结合笔记和黑板回复"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "memory.read",
|
||||
"reason": "第一轮观察显示缺少美术约束,需要查项目黑板",
|
||||
"input": { "scope": "blackboard" }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![
|
||||
first_plan_json,
|
||||
second_plan_json,
|
||||
"我先读了项目笔记,再根据观察补读了项目黑板:核心循环已有,后续美术要围绕月光围裙和暗影厨具推进。"
|
||||
.to_string(),
|
||||
],
|
||||
Some(sender),
|
||||
);
|
||||
let _config_guard = write_test_local_config(format!(
|
||||
r#"{{
|
||||
"agentLlm": {{
|
||||
"design-director": {{
|
||||
"apiKey": "design-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "design-runtime-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"后台连续分析项目笔记和黑板",
|
||||
"design-replan-run",
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
let first_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("first plan llm request");
|
||||
assert!(first_request.contains("第 1 轮"));
|
||||
assert!(first_request.contains("已有工具观察"));
|
||||
assert!(first_request.contains("[]"));
|
||||
let second_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("second plan llm request");
|
||||
assert!(second_request.contains("第 2 轮"));
|
||||
assert!(second_request.contains("file.read"));
|
||||
assert!(second_request.contains("项目笔记:已有月光厨房核心循环"));
|
||||
let third_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("third plan llm request");
|
||||
assert!(third_request.contains("第 3 轮"));
|
||||
assert!(third_request.contains("memory.read"));
|
||||
assert!(third_request.contains("月光围裙和暗影厨具对手"));
|
||||
|
||||
let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read runtime")
|
||||
.state;
|
||||
for _ in 0..50 {
|
||||
if runtime.status == "idle" {
|
||||
break;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read runtime")
|
||||
.state;
|
||||
}
|
||||
|
||||
assert_eq!(runtime.status, "idle");
|
||||
assert_eq!(runtime.phase, "completed");
|
||||
assert_eq!(
|
||||
runtime.plan,
|
||||
vec!["读取项目黑板".to_string(), "结合笔记和黑板回复".to_string()]
|
||||
);
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("第 1 轮思考摘要:先确认项目笔记")));
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("第 2 轮思考摘要:笔记确认核心循环存在")));
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("file.read:ok")));
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("memory.read:ok")));
|
||||
assert_eq!(
|
||||
runtime.last_response.as_deref(),
|
||||
Some("我先读了项目笔记,再根据观察补读了项目黑板:核心循环已有,后续美术要围绕月光围裙和暗影厨具推进。")
|
||||
);
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(agent_db.contains("\"tool\":\"file.read\""));
|
||||
assert!(agent_db.contains("\"tool\":\"memory.read\""));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_write_blackboard_and_message_other_agent() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -238,6 +238,7 @@ interface AgentRuntimeState {
|
||||
phase: string;
|
||||
currentTask: string;
|
||||
currentAction: string;
|
||||
nextStep?: string;
|
||||
plan: string[];
|
||||
observations: string[];
|
||||
allowedTools: string[];
|
||||
@@ -497,10 +498,30 @@ function agentRuntimeStateFromResult(
|
||||
): AgentRuntimeState {
|
||||
return {
|
||||
...result.state,
|
||||
nextStep:
|
||||
result.state.nextStep ?? agentRuntimeNextStepFromPhase(result.state.phase),
|
||||
recentTasks: result.recentTasks ?? result.state.recentTasks ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function agentRuntimeNextStepFromPhase(phase: string) {
|
||||
switch (phase) {
|
||||
case 'planning':
|
||||
return '等待 Agent 输出计划或回复';
|
||||
case 'action':
|
||||
return '等待工具观察结果';
|
||||
case 'response':
|
||||
return '等待 Agent 整理最终回复';
|
||||
case 'completed':
|
||||
case 'idle':
|
||||
return '等待下一轮输入';
|
||||
case 'failed':
|
||||
return '等待开发者处理失败';
|
||||
default:
|
||||
return '继续推进当前任务';
|
||||
}
|
||||
}
|
||||
|
||||
function agentRuntimeStartStatus(result: AgentRuntimeResult) {
|
||||
const pendingTask = (result.recentTasks ?? []).find(
|
||||
(task) => task.status === 'pending',
|
||||
@@ -533,6 +554,7 @@ function AgentRuntimeStatusPanel({
|
||||
const planItems = runtime.plan.slice(0, 3);
|
||||
const observations = runtime.observations.slice(-2);
|
||||
const recentTasks = (runtime.recentTasks ?? []).slice(-3).reverse();
|
||||
const nextStep = runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase);
|
||||
return (
|
||||
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
|
||||
<header>
|
||||
@@ -543,6 +565,7 @@ function AgentRuntimeStatusPanel({
|
||||
{runtime.runId ? <small>{`run: ${runtime.runId}`}</small> : null}
|
||||
{runtime.currentTask ? <p>{runtime.currentTask}</p> : null}
|
||||
<small>{runtime.currentAction}</small>
|
||||
{nextStep ? <small>{`下一步:${nextStep}`}</small> : null}
|
||||
{planItems.length > 0 ? (
|
||||
<ol>
|
||||
{planItems.map((item, index) => (
|
||||
@@ -688,6 +711,7 @@ interface AgentStatusCard {
|
||||
runtimeStatus: string | null;
|
||||
runtimePhase: string | null;
|
||||
runtimeAction: string | null;
|
||||
runtimeNextStep: string | null;
|
||||
runtimeTask: string | null;
|
||||
runtimeRunId: string | null;
|
||||
runtimeRecentTasks: AgentRuntimeTaskRecord[];
|
||||
@@ -9999,6 +10023,9 @@ export function deriveAgentStatusCards(
|
||||
runtimeStatus: runtime?.status ?? null,
|
||||
runtimePhase: runtime?.phase ?? null,
|
||||
runtimeAction: runtime?.currentAction ?? null,
|
||||
runtimeNextStep: runtime
|
||||
? runtime.nextStep ?? agentRuntimeNextStepFromPhase(runtime.phase)
|
||||
: null,
|
||||
runtimeTask: runtime?.currentTask ?? null,
|
||||
runtimeRunId: runtime?.runId ?? null,
|
||||
runtimeRecentTasks: runtime?.recentTasks ?? [],
|
||||
@@ -10028,6 +10055,7 @@ function sameAgentStatusCard(left: AgentStatusCard, right: AgentStatusCard) {
|
||||
left.runtimeStatus === right.runtimeStatus &&
|
||||
left.runtimePhase === right.runtimePhase &&
|
||||
left.runtimeAction === right.runtimeAction &&
|
||||
left.runtimeNextStep === right.runtimeNextStep &&
|
||||
left.runtimeTask === right.runtimeTask &&
|
||||
left.runtimeRunId === right.runtimeRunId &&
|
||||
left.hasRecentEvidence === right.hasRecentEvidence &&
|
||||
@@ -10809,6 +10837,7 @@ function formatAgentCardRuntimeStatus(agent: AgentStatusCard) {
|
||||
return [
|
||||
`Runtime:${agent.runtimeStatus} / ${agent.runtimePhase ?? '-'}`,
|
||||
agent.runtimeAction,
|
||||
agent.runtimeNextStep ? `下一步 ${agent.runtimeNextStep}` : null,
|
||||
agent.runtimeRunId ? `run ${agent.runtimeRunId}` : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
|
||||
@@ -13248,7 +13248,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
name: /拆解创作方向/,
|
||||
});
|
||||
expect(designCard.textContent).toContain(
|
||||
'Runtime:running / planning · 整理目标和约束 · run runtime-design-director-1',
|
||||
'Runtime:running / planning · 整理目标和约束 · 下一步 等待 Agent 输出计划或回复 · run runtime-design-director-1',
|
||||
);
|
||||
expect(designCard.textContent).toContain('当前任务:拆解关卡节奏');
|
||||
expect(designCard.textContent).toContain(
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
## 2026-07-09 AI 游戏创作 App Runtime V1 增加单 Agent 后台任务
|
||||
|
||||
- 背景:开发用单 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` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受 `memory.write` / `conversation.write` 策略保护的协作写动作 `blackboard.write` 和 `agent.message`;策略要求确认或拒绝时不执行写入,只把策略结果作为 observation 回给 Agent。每个 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/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 / response`,Runtime 按白名单和项目权限策略执行工具并记录 `action / observation` 事件,再把已有 observation 放回下一轮 prompt,让 Agent 修正计划、继续行动或用空 actions + response 收束;当前后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复。完成或失败后把 assistant 回复或错误追加回对话,并写入 `.agent/agent.db` 审计记录。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受 `memory.write` / `conversation.write` 策略保护的协作写动作 `blackboard.write` 和 `agent.message`;策略要求确认或拒绝时不执行写入,只把策略结果作为 observation 回给 Agent。每个 Agent 的任务历史落在 `.agent/runtime/tasks/<agentId>.jsonl`,读 runtime 时按 `runId` 去重返回最近任务,任务视角状态使用 `pending / running / completed / failed`,Runtime state 增加 `nextStep`,UI 在 Runtime 面板和主 Agent 状态卡展示当前任务、动作、下一步与最近任务。不同 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`。
|
||||
|
||||
@@ -32,7 +32,7 @@ Agent Runtime 负责:
|
||||
- 开发窗口能力: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 生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受 `memory.write` / `conversation.write` 策略保护的协作写动作 `blackboard.write` 和 `agent.message`;`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/<agentId>.jsonl` 写入 tool 留言,策略要求确认或拒绝时不执行写入,只把策略结果作为 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。
|
||||
- 后台任务能力:开发窗口单 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 / response`,Runtime 按白名单和项目权限策略执行工具动作,写入 `action / observation` 事件,再把 observation 放入下一轮 prompt 让 Agent 修正计划、继续行动或用空 actions + response 收束;后台任务最多执行 3 轮 loop,仍未收束时再按最后计划和全部观察生成最终回复并追加回对话。工具箱包含只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受 `memory.write` / `conversation.write` 策略保护的协作写动作 `blackboard.write` 和 `agent.message`;`blackboard.write` 追加 `memory/blackboard.md`,`agent.message` 给目标 `.agent/conversations/agents/<agentId>.jsonl` 写入 tool 留言,策略要求确认或拒绝时不执行写入,只把策略结果作为 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`,不把原始对话混进项目黑板或角色私有记忆。
|
||||
@@ -252,7 +252,7 @@ game-project/
|
||||
- Tauri Rust 入口保持薄壳:`src-tauri/src/main.rs` 只保留共享类型 / 常量、模块声明、CLI preflight、`tauri::Builder`、运行时配置初始化和 `invoke_handler` 清单;命令行入口放在 `cli.rs`,Tauri command 包装放在 `commands.rs`,运行时配置与 LLM 配置检查放在 `config.rs`,Agent loop 与生成编排放在 `agent.rs`,上传 / 画板 / 平台美术生成接入放在 `assets.rs`,本地项目文件、记忆、对话、权限、checkpoint、manifest 和通用路径工具放在 `project.rs`,本地 HTTP 预览与 preview 命令放在 `preview.rs`,旧窗口兼容命令放在 `windows.rs`,Rust 单测放在 `tests.rs`。后续继续拆分时保持 Tauri command 名、JSON 字段、`.agent/*` 路径和错误语义不变。
|
||||
- 本地项目初始化会创建 `game/`、`assets/`、`memory/`、`memory/agents/`、`exports/`、`.agent/logs/`,写入 `.agent/manifest.json`,生成 append-only JSONL 本地项目索引 `.agent/agent.db`,并生成默认 `game/index.html`。
|
||||
- v1 conversation 记录使用 append-only JSONL,每行带 `schemaVersion`、`role`、`content`、`agentId` 和 `updatedAt`,作为聊天历史和单 agent 对话历史的事实源;目录在首次写入时创建。
|
||||
- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/<agentId>.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/<agentId>.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/<agentId>.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.tool_observation` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受策略保护的协作写工具 `blackboard.write` 和 `agent.message`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。
|
||||
- 开发窗口和项目内 Agent 对话弹窗的“后台运行”只启动或排队单 Agent 后台任务,不阻塞等待回复;用户可刷新同一 Agent 对话或 runtime 状态查看进度和结果。后台任务会向 `.agent/runtime/tasks/<agentId>.jsonl` 追加任务视角记录,任务状态使用 `pending / running / completed / failed`,读取时按 `runId` 去重返回最近任务;runtime state 自身仍可在完成后显示 `idle / completed`,二者语义分开。同一 Agent 的 pending 任务由持有 `.agent/runtime/locks/<agentId>.lock` 的后台 drain 串行执行,避免同一 Agent 并发抢上下文;不同 Agent 仍可并行。后台任务的核心 loop 最多 3 轮:每轮把已有 observation 带回 LLM 让 Agent 重新规划,只有 actions 为空且 response 非空时提前收束,否则继续执行白名单工具,跑满后再进入最终回复整理。后台任务完成后会把 assistant 回复追加到对应 `.agent/conversations/agents/<agentId>.jsonl`,并向 `.agent/agent.db` 写入 `agent.runtime.background_task.queued` / `agent.runtime.background_task` / `agent.runtime.tool_observation` / `agent.runtime.blackboard.write` / `agent.runtime.agent.message` / `agent.runtime.background_task.completed` / `agent.runtime.background_task.failed` 审计记录。当前工具箱开放只读工具 `memory.read`、`conversation.read`、`asset.list`、`project.index`、`file.read`,以及受策略保护的协作写工具 `blackboard.write` 和 `agent.message`;若项目策略要求确认或拒绝,对应工具不会执行,Runtime 会把策略结果作为 observation 回给 Agent。`.agent/agent.db` 追加写入按整行 JSONL 写入,减少多个 Agent 同时完成时的行交错风险。
|
||||
- 普通用户可在聊天框输入 `/project /绝对路径` 生成待确认的 `project.create` 命令,用于授权并初始化本地项目目录;相对路径不会生成待确认命令;开发窗口仍可直接编辑项目路径。
|
||||
- 单窗口首页和项目组页可选择、打开、新建或显示当前输入的项目绝对路径;最近项目行也可显示目录,非法或相对路径不会调用系统文件管理器。
|
||||
- 普通用户侧的生成、上传、运行、自检、预览状态 / 启动 / 打开 / 停止、记忆写入和画板资产导入都必须先完成 `/project` 初始化;未初始化时只提示设置本地项目,不落到默认临时目录。
|
||||
@@ -274,7 +274,7 @@ game-project/
|
||||
- `.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`。这里的 `<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 请求。
|
||||
- 生成 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