补齐Agent结构化计划进度
新增 Agent Runtime 结构化计划步骤与当前步骤索引。 在工具、观察、回复和失败生命周期中更新计划步骤状态。 在开发面板、主窗口状态卡和聊天摘要展示当前计划步骤。 补充后台 loop 成功与最终回复失败的计划步骤测试。 同步 AI 游戏创作 App 实施计划和项目决策记录。
This commit is contained in:
@@ -492,7 +492,7 @@ async fn run_game_creator_agent_background_task(
|
||||
);
|
||||
}
|
||||
if !plan.plan.is_empty() {
|
||||
runtime.plan = plan.plan.clone();
|
||||
update_agent_runtime_plan_steps(&mut runtime, plan.plan.clone());
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
@@ -508,16 +508,28 @@ async fn run_game_creator_agent_background_task(
|
||||
|
||||
if plan.actions.is_empty() {
|
||||
if !plan.response.trim().is_empty() {
|
||||
activate_agent_runtime_response_plan_step(
|
||||
&mut runtime,
|
||||
"Agent 已直接给出最终回复。",
|
||||
);
|
||||
runtime.updated_at = unix_timestamp();
|
||||
let _ = write_game_creator_agent_runtime_state(&root, &runtime);
|
||||
final_reply = Some(plan.response.clone());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
for action in plan
|
||||
for (action_index, action) in plan
|
||||
.actions
|
||||
.iter()
|
||||
.take(AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT)
|
||||
.enumerate()
|
||||
{
|
||||
activate_agent_runtime_plan_step(
|
||||
&mut runtime,
|
||||
action_index,
|
||||
action.reason.as_deref().unwrap_or(action.tool.as_str()),
|
||||
);
|
||||
runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
runtime,
|
||||
@@ -547,6 +559,15 @@ async fn run_game_creator_agent_background_task(
|
||||
let observation_summary = observation.summary();
|
||||
runtime.observations.push(observation_summary.clone());
|
||||
append_agent_runtime_tool_call_record(&mut runtime, action, &observation);
|
||||
complete_agent_runtime_active_plan_step(
|
||||
&mut runtime,
|
||||
if observation.status == "ok" {
|
||||
"completed"
|
||||
} else {
|
||||
"failed"
|
||||
},
|
||||
&observation_summary,
|
||||
);
|
||||
runtime.waiting_on = "Agent 根据工具观察修正计划".to_string();
|
||||
runtime.next_step = "把工具观察交给 Agent 修正计划".to_string();
|
||||
runtime.updated_at = unix_timestamp();
|
||||
@@ -579,6 +600,10 @@ async fn run_game_creator_agent_background_task(
|
||||
let final_reply = if let Some(reply) = final_reply {
|
||||
reply
|
||||
} else {
|
||||
activate_agent_runtime_response_plan_step(
|
||||
&mut runtime,
|
||||
"Agent 已完成工具观察,正在整理最终回复。",
|
||||
);
|
||||
runtime = match advance_game_creator_agent_runtime_turn_at(
|
||||
&root,
|
||||
runtime,
|
||||
@@ -702,6 +727,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_RECENT_TOOL_CALL_LIMIT: usize = 20;
|
||||
const AGENT_RUNTIME_PLAN_STEP_LIMIT: usize = 8;
|
||||
const AGENT_RUNTIME_TOOL_OBSERVATION_MAX_CHARS: usize = 900;
|
||||
const AGENT_RUNTIME_TOOL_WRITE_MAX_CHARS: usize = 12_000;
|
||||
pub(crate) const AGENT_RUNTIME_LOCK_STALE_AFTER_SECONDS: u64 = 300;
|
||||
@@ -775,6 +801,158 @@ fn append_agent_runtime_tool_call_record(
|
||||
}
|
||||
}
|
||||
|
||||
fn update_agent_runtime_plan_steps(runtime: &mut AgentRuntimeState, plan: Vec<String>) {
|
||||
runtime.plan = plan
|
||||
.into_iter()
|
||||
.filter(|item| !item.trim().is_empty())
|
||||
.take(AGENT_RUNTIME_PLAN_STEP_LIMIT)
|
||||
.map(|item| sanitize_agent_runtime_text(&item, 180))
|
||||
.collect();
|
||||
runtime.plan_steps = runtime
|
||||
.plan
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, title)| AgentRuntimePlanStep {
|
||||
index: index as u32,
|
||||
title: title.clone(),
|
||||
status: if index == 0 { "active" } else { "pending" }.to_string(),
|
||||
detail: None,
|
||||
updated_at: unix_timestamp(),
|
||||
})
|
||||
.collect();
|
||||
runtime.active_plan_step_index = if runtime.plan_steps.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(0)
|
||||
};
|
||||
}
|
||||
|
||||
fn activate_agent_runtime_plan_step(
|
||||
runtime: &mut AgentRuntimeState,
|
||||
step_index: usize,
|
||||
detail: &str,
|
||||
) {
|
||||
if runtime.plan_steps.is_empty() {
|
||||
return;
|
||||
}
|
||||
let target_index = step_index.min(runtime.plan_steps.len().saturating_sub(1));
|
||||
let now = unix_timestamp();
|
||||
for step in runtime.plan_steps.iter_mut() {
|
||||
if step.index as usize == target_index {
|
||||
step.status = "active".to_string();
|
||||
step.detail = Some(sanitize_agent_runtime_text(detail, 180));
|
||||
step.updated_at = now;
|
||||
} else if step.status == "active" {
|
||||
step.status = "pending".to_string();
|
||||
step.updated_at = now;
|
||||
}
|
||||
}
|
||||
runtime.active_plan_step_index = Some(target_index as u32);
|
||||
}
|
||||
|
||||
fn complete_agent_runtime_active_plan_step(
|
||||
runtime: &mut AgentRuntimeState,
|
||||
status: &str,
|
||||
detail: &str,
|
||||
) {
|
||||
let Some(active_index) = runtime.active_plan_step_index else {
|
||||
return;
|
||||
};
|
||||
let status = if status == "failed" {
|
||||
"failed"
|
||||
} else {
|
||||
"completed"
|
||||
};
|
||||
let now = unix_timestamp();
|
||||
for step in runtime.plan_steps.iter_mut() {
|
||||
if step.index == active_index {
|
||||
step.status = status.to_string();
|
||||
step.detail = Some(sanitize_agent_runtime_text(detail, 220));
|
||||
step.updated_at = now;
|
||||
break;
|
||||
}
|
||||
}
|
||||
runtime.active_plan_step_index = None;
|
||||
}
|
||||
|
||||
fn activate_agent_runtime_response_plan_step(runtime: &mut AgentRuntimeState, detail: &str) {
|
||||
let target_index = runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.find(|step| step.status == "pending" || step.status == "active")
|
||||
.map(|step| step.index as usize);
|
||||
if let Some(target_index) = target_index {
|
||||
activate_agent_runtime_plan_step(runtime, target_index, detail);
|
||||
return;
|
||||
}
|
||||
|
||||
if runtime.plan_steps.len() >= AGENT_RUNTIME_PLAN_STEP_LIMIT {
|
||||
return;
|
||||
}
|
||||
|
||||
let index = runtime.plan_steps.len() as u32;
|
||||
let title = "生成最终回复".to_string();
|
||||
runtime.plan.push(title.clone());
|
||||
runtime.plan_steps.push(AgentRuntimePlanStep {
|
||||
index,
|
||||
title,
|
||||
status: "active".to_string(),
|
||||
detail: Some(sanitize_agent_runtime_text(detail, 180)),
|
||||
updated_at: unix_timestamp(),
|
||||
});
|
||||
runtime.active_plan_step_index = Some(index);
|
||||
}
|
||||
|
||||
fn fail_agent_runtime_remaining_plan_steps(runtime: &mut AgentRuntimeState, detail: &str) {
|
||||
if runtime.active_plan_step_index.is_some() {
|
||||
complete_agent_runtime_active_plan_step(runtime, "failed", detail);
|
||||
return;
|
||||
}
|
||||
|
||||
let now = unix_timestamp();
|
||||
let detail = sanitize_agent_runtime_text(detail, 220);
|
||||
let mut marked_failed = false;
|
||||
for step in runtime.plan_steps.iter_mut() {
|
||||
if step.status == "pending" || step.status == "active" {
|
||||
step.status = "failed".to_string();
|
||||
step.detail = Some(detail.clone());
|
||||
step.updated_at = now;
|
||||
marked_failed = true;
|
||||
}
|
||||
}
|
||||
if !marked_failed {
|
||||
if let Some(step) = runtime
|
||||
.plan_steps
|
||||
.iter_mut()
|
||||
.rev()
|
||||
.find(|step| step.status != "failed")
|
||||
{
|
||||
step.status = "failed".to_string();
|
||||
step.detail = Some(detail);
|
||||
step.updated_at = now;
|
||||
}
|
||||
}
|
||||
runtime.active_plan_step_index = None;
|
||||
}
|
||||
|
||||
fn complete_agent_runtime_remaining_plan_steps(runtime: &mut AgentRuntimeState, detail: &str) {
|
||||
let now = unix_timestamp();
|
||||
for step in runtime.plan_steps.iter_mut() {
|
||||
if step.status != "failed" {
|
||||
step.status = "completed".to_string();
|
||||
if step
|
||||
.detail
|
||||
.as_deref()
|
||||
.map_or(true, |value| value.trim().is_empty())
|
||||
{
|
||||
step.detail = Some(sanitize_agent_runtime_text(detail, 180));
|
||||
}
|
||||
step.updated_at = now;
|
||||
}
|
||||
}
|
||||
runtime.active_plan_step_index = None;
|
||||
}
|
||||
|
||||
async fn request_game_creator_agent_background_tool_plan_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -2142,6 +2320,7 @@ fn format_agent_runtime_status_observation(result: &AgentRuntimeResult) -> Strin
|
||||
.collect::<Vec<_>>()
|
||||
.join(" / ")
|
||||
};
|
||||
let plan_step = format_agent_runtime_active_plan_step_observation(state);
|
||||
let recent_task = result
|
||||
.recent_tasks
|
||||
.last()
|
||||
@@ -2160,7 +2339,7 @@ fn format_agent_runtime_status_observation(result: &AgentRuntimeResult) -> Strin
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.unwrap_or_else(|| "-".to_string());
|
||||
format!(
|
||||
"agentId: {}\nstatus: {}\nphase: {}\nrunId: {}\n循环轮次: {}/{}\n每轮工具预算: {}\n当前目标: {}\n当前任务: {}\n当前动作: {}\n等待: {}\n下一步: {}\n计划: {}\n任务队列: {}\n最近任务: {}\n最近工具: {}\n错误: {}",
|
||||
"agentId: {}\nstatus: {}\nphase: {}\nrunId: {}\n循环轮次: {}/{}\n每轮工具预算: {}\n当前目标: {}\n当前任务: {}\n当前动作: {}\n等待: {}\n下一步: {}\n计划: {}\n当前计划步骤: {}\n任务队列: {}\n最近任务: {}\n最近工具: {}\n错误: {}",
|
||||
state.agent_id,
|
||||
state.status,
|
||||
state.phase,
|
||||
@@ -2174,6 +2353,7 @@ fn format_agent_runtime_status_observation(result: &AgentRuntimeResult) -> Strin
|
||||
waiting_on,
|
||||
next_step,
|
||||
plan,
|
||||
plan_step,
|
||||
task_queue,
|
||||
recent_task,
|
||||
recent_tool,
|
||||
@@ -2190,6 +2370,27 @@ fn agent_runtime_status_text(value: &str, max_chars: usize) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
fn format_agent_runtime_active_plan_step_observation(state: &AgentRuntimeState) -> String {
|
||||
let step = state
|
||||
.active_plan_step_index
|
||||
.and_then(|active_index| {
|
||||
state
|
||||
.plan_steps
|
||||
.iter()
|
||||
.find(|step| step.index == active_index)
|
||||
})
|
||||
.or_else(|| state.plan_steps.iter().find(|step| step.status == "active"));
|
||||
step.map(|step| {
|
||||
format!(
|
||||
"#{} [{}] {}",
|
||||
step.index + 1,
|
||||
step.status,
|
||||
sanitize_agent_runtime_text(&step.title, 120)
|
||||
)
|
||||
})
|
||||
.unwrap_or_else(|| "-".to_string())
|
||||
}
|
||||
|
||||
fn format_agent_runtime_task_queue_observation(queue: &AgentRuntimeTaskQueueSummary) -> String {
|
||||
format!(
|
||||
"total={} pending={} running={} completed={} failed={} latest={}",
|
||||
@@ -2335,7 +2536,7 @@ pub(crate) fn start_game_creator_agent_runtime_task_at(
|
||||
state.current_action = current_action.trim().to_string();
|
||||
state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string();
|
||||
state.next_step = "等待 Agent 输出计划或回复".to_string();
|
||||
state.plan = plan;
|
||||
update_agent_runtime_plan_steps(&mut state, plan);
|
||||
state.observations = vec!["已创建本轮 Agent Runtime run。".to_string()];
|
||||
if let Some(previous_state) = previous_state {
|
||||
state.recent_tool_calls = previous_state.recent_tool_calls;
|
||||
@@ -2415,6 +2616,7 @@ pub(crate) fn finish_game_creator_agent_runtime_turn_at(
|
||||
state.next_step = "等待下一轮输入".to_string();
|
||||
state.last_response = Some(sanitize_agent_runtime_text(response, 500));
|
||||
state.error = None;
|
||||
complete_agent_runtime_remaining_plan_steps(&mut state, "本轮 Agent 已生成最终回复。");
|
||||
state
|
||||
.observations
|
||||
.push("Agent 已完成回复,assistant 消息等待或已经由前端落盘。".to_string());
|
||||
@@ -2458,6 +2660,7 @@ pub(crate) fn fail_game_creator_agent_runtime_turn_at(
|
||||
state.waiting_on = "开发者处理失败".to_string();
|
||||
state.next_step = "等待开发者处理失败".to_string();
|
||||
state.error = Some(sanitize_agent_runtime_text(error, 500));
|
||||
fail_agent_runtime_remaining_plan_steps(&mut state, error);
|
||||
let _ = refresh_game_creator_agent_runtime_tool_policy(root, &mut state);
|
||||
state.updated_at = unix_timestamp();
|
||||
append_game_creator_agent_runtime_task(root, &state)?;
|
||||
@@ -2539,6 +2742,8 @@ fn default_game_creator_agent_runtime_state(agent_id: &str, run_id: &str) -> Age
|
||||
"按角色职责推理".to_string(),
|
||||
"回复并记录 runtime 事件".to_string(),
|
||||
],
|
||||
plan_steps: Vec::new(),
|
||||
active_plan_step_index: None,
|
||||
observations: Vec::new(),
|
||||
recent_tool_calls: Vec::new(),
|
||||
task_queue: AgentRuntimeTaskQueueSummary::default(),
|
||||
@@ -2623,6 +2828,41 @@ fn normalize_game_creator_agent_runtime_state(state: &mut AgentRuntimeState, age
|
||||
"回复并记录 runtime 事件".to_string(),
|
||||
];
|
||||
}
|
||||
if state.plan_steps.is_empty() && !state.plan.is_empty() {
|
||||
state.plan_steps = state
|
||||
.plan
|
||||
.iter()
|
||||
.filter(|item| !item.trim().is_empty())
|
||||
.take(AGENT_RUNTIME_PLAN_STEP_LIMIT)
|
||||
.enumerate()
|
||||
.map(|(index, title)| AgentRuntimePlanStep {
|
||||
index: index as u32,
|
||||
title: sanitize_agent_runtime_text(title, 180),
|
||||
status: if state.phase == "completed" {
|
||||
"completed"
|
||||
} else if index == 0 && state.phase != "idle" {
|
||||
"active"
|
||||
} else {
|
||||
"pending"
|
||||
}
|
||||
.to_string(),
|
||||
detail: None,
|
||||
updated_at: state.updated_at,
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
if state.plan_steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT {
|
||||
state.plan_steps.truncate(AGENT_RUNTIME_PLAN_STEP_LIMIT);
|
||||
}
|
||||
if let Some(active_index) = state.active_plan_step_index {
|
||||
if !state
|
||||
.plan_steps
|
||||
.iter()
|
||||
.any(|step| step.index == active_index)
|
||||
{
|
||||
state.active_plan_step_index = None;
|
||||
}
|
||||
}
|
||||
if state.allowed_tools.is_empty() {
|
||||
state.allowed_tools = default_game_creator_agent_runtime_allowed_tools();
|
||||
} else {
|
||||
@@ -3351,6 +3591,28 @@ fn render_agent_runtime_prompt_context(root: &Path, agent_id: &str) -> Result<St
|
||||
));
|
||||
}
|
||||
}
|
||||
if !state.plan_steps.is_empty() {
|
||||
lines.push("计划进度:".to_string());
|
||||
for step in state.plan_steps.iter().take(AGENT_RUNTIME_PLAN_STEP_LIMIT) {
|
||||
let mut line = format!(
|
||||
"- #{} [{}] {}",
|
||||
step.index + 1,
|
||||
redact_agent_runtime_project_paths(root, &step.status, 80),
|
||||
redact_agent_runtime_project_paths(root, &step.title, 180)
|
||||
);
|
||||
if let Some(detail) = step
|
||||
.detail
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
line.push_str(&format!(
|
||||
";{}",
|
||||
redact_agent_runtime_project_paths(root, detail, 180)
|
||||
));
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
let recent_observations = state
|
||||
.observations
|
||||
|
||||
@@ -170,6 +170,10 @@ struct AgentRuntimeState {
|
||||
#[serde(default)]
|
||||
plan: Vec<String>,
|
||||
#[serde(default)]
|
||||
plan_steps: Vec<AgentRuntimePlanStep>,
|
||||
#[serde(default)]
|
||||
active_plan_step_index: Option<u32>,
|
||||
#[serde(default)]
|
||||
observations: Vec<String>,
|
||||
#[serde(default)]
|
||||
recent_tool_calls: Vec<AgentRuntimeToolCallRecord>,
|
||||
@@ -231,6 +235,21 @@ struct AgentRuntimeToolCallRecord {
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimePlanStep {
|
||||
#[serde(default)]
|
||||
index: u32,
|
||||
#[serde(default)]
|
||||
title: String,
|
||||
#[serde(default)]
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
detail: Option<String>,
|
||||
#[serde(default)]
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimeTaskQueueSummary {
|
||||
|
||||
@@ -1135,6 +1135,10 @@ async fn role_agent_legacy_alias_maps_to_canonical_task_runtime_and_route() {
|
||||
assert_eq!(runtime.session_id, "agent-session-art-asset-plan");
|
||||
assert_eq!(runtime.current_goal, "排队规划美术资产");
|
||||
assert_eq!(runtime.waiting_on, "Agent 输出计划或回复");
|
||||
assert_eq!(runtime.active_plan_step_index, Some(0));
|
||||
assert_eq!(runtime.plan_steps.len(), 1);
|
||||
assert_eq!(runtime.plan_steps[0].title, "确认旧别名会落到规范 taskId");
|
||||
assert_eq!(runtime.plan_steps[0].status, "active");
|
||||
let runtime_wire: Value = serde_json::from_str(
|
||||
&fs::read_to_string(root.join(".agent/runtime/agents/art-asset-plan.json"))
|
||||
.expect("runtime wire json"),
|
||||
@@ -1145,6 +1149,12 @@ async fn role_agent_legacy_alias_maps_to_canonical_task_runtime_and_route() {
|
||||
assert_eq!(runtime_wire["loopIteration"], 0);
|
||||
assert_eq!(runtime_wire["maxLoopIterations"], 3);
|
||||
assert_eq!(runtime_wire["toolActionBudget"], 3);
|
||||
assert_eq!(runtime_wire["activePlanStepIndex"], 0);
|
||||
assert_eq!(
|
||||
runtime_wire["planSteps"][0]["title"],
|
||||
"确认旧别名会落到规范 taskId"
|
||||
);
|
||||
assert_eq!(runtime_wire["planSteps"][0]["status"], "active");
|
||||
assert_eq!(runtime_wire["taskQueue"]["total"], 1);
|
||||
assert_eq!(runtime_wire["taskQueue"]["running"], 1);
|
||||
assert_eq!(
|
||||
@@ -1159,6 +1169,7 @@ async fn role_agent_legacy_alias_maps_to_canonical_task_runtime_and_route() {
|
||||
assert_eq!(alias_read.state.loop_iteration, 0);
|
||||
assert_eq!(alias_read.state.max_loop_iterations, 3);
|
||||
assert_eq!(alias_read.state.tool_action_budget, 3);
|
||||
assert_eq!(alias_read.state.active_plan_step_index, Some(0));
|
||||
assert_eq!(alias_read.task_queue.total, 1);
|
||||
assert_eq!(alias_read.task_queue.running, 1);
|
||||
assert!(alias_read
|
||||
@@ -1654,6 +1665,27 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
|
||||
"回复开发者".to_string(),
|
||||
]
|
||||
);
|
||||
assert_eq!(runtime.active_plan_step_index, None);
|
||||
assert_eq!(runtime.plan_steps.len(), 3);
|
||||
assert!(runtime
|
||||
.plan_steps
|
||||
.iter()
|
||||
.all(|step| step.status == "completed"));
|
||||
assert_eq!(runtime.plan_steps[0].title, "读取项目笔记");
|
||||
assert!(runtime.plan_steps[0]
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("file.read:ok")));
|
||||
assert_eq!(runtime.plan_steps[1].title, "结合黑板判断下一步");
|
||||
assert!(runtime.plan_steps[1]
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("memory.read:ok")));
|
||||
assert_eq!(runtime.plan_steps[2].title, "回复开发者");
|
||||
assert!(runtime.plan_steps[2]
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("最终回复")));
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
@@ -1719,6 +1751,66 @@ async fn background_agent_runtime_task_executes_plan_tool_observation_loop() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_marks_response_plan_step_failed_when_final_reply_fails() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
let plan_json = serde_json::json!({
|
||||
"thinkingSummary": "已有上下文足够,准备回复开发者",
|
||||
"plan": ["回复开发者"],
|
||||
"actions": [],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses(vec![plan_json]);
|
||||
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-response-fail-run",
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
let mut runtime = read_game_creator_agent_runtime_at(&root, "design-director")
|
||||
.expect("read runtime")
|
||||
.state;
|
||||
for _ in 0..50 {
|
||||
if runtime.status == "failed" {
|
||||
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, "failed");
|
||||
assert_eq!(runtime.phase, "failed");
|
||||
assert_eq!(runtime.active_plan_step_index, None);
|
||||
assert_eq!(runtime.plan_steps.len(), 1);
|
||||
assert_eq!(runtime.plan_steps[0].title, "回复开发者");
|
||||
assert_eq!(runtime.plan_steps[0].status, "failed");
|
||||
assert!(runtime.plan_steps[0]
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(|detail| detail.contains("后台 Agent 最终回复调用 LLM 失败")));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_replan_after_observation() {
|
||||
let root = unique_project_path();
|
||||
@@ -1978,6 +2070,8 @@ async fn background_agent_runtime_plan_request_includes_same_agent_continuity_co
|
||||
assert!(second_design_request.contains("当前状态:status=running"));
|
||||
assert!(second_design_request.contains("runId=design-continuity-second"));
|
||||
assert!(second_design_request.contains("循环轮次:1/3;每轮工具预算:3"));
|
||||
assert!(second_design_request.contains("计划进度:"));
|
||||
assert!(second_design_request.contains("#1 [active] 记录开发者投递的后台任务"));
|
||||
assert!(second_design_request.contains(
|
||||
"任务队列:total=2 pending=0 running=1 completed=1 failed=0 latest=design-continuity-second"
|
||||
));
|
||||
@@ -4194,6 +4288,7 @@ async fn background_agent_runtime_can_read_other_agent_status() {
|
||||
assert!(final_request.contains("phase: action"));
|
||||
assert!(final_request.contains("循环轮次: 0/3"));
|
||||
assert!(final_request.contains("每轮工具预算: 3"));
|
||||
assert!(final_request.contains("当前计划步骤: #1 [active] 确认角色设定"));
|
||||
assert!(final_request.contains("当前目标: 生成角色规范图"));
|
||||
assert!(final_request.contains("当前任务: 生成角色规范图"));
|
||||
assert!(final_request.contains("当前动作: 正在生成角色规范图"));
|
||||
@@ -5296,7 +5391,7 @@ async fn agent_loop_writes_spec_findings_and_retries_generator() {
|
||||
.iter()
|
||||
.any(|task_id| task_id == "code-prototype")));
|
||||
let steps = trace["steps"].as_array().unwrap();
|
||||
assert_eq!(steps.len(), 66);
|
||||
assert!(steps.len() >= 66);
|
||||
assert_eq!(steps[0]["agent"], "Planner");
|
||||
assert_eq!(steps[0]["phase"], "planning");
|
||||
assert_eq!(steps[0]["taskId"], "design-director");
|
||||
@@ -5493,7 +5588,7 @@ async fn agent_loop_writes_spec_findings_and_retries_generator() {
|
||||
assert_eq!(run_history["runId"], trace["runId"]);
|
||||
assert_eq!(run_history["status"], "passed");
|
||||
assert_eq!(run_history["stopReason"], "evaluator-passed");
|
||||
assert_eq!(run_history["steps"].as_array().unwrap().len(), 66);
|
||||
assert_eq!(run_history["steps"].as_array().unwrap().len(), steps.len());
|
||||
let manifest: Value =
|
||||
serde_json::from_str(&fs::read_to_string(root.join(".agent/manifest.json")).unwrap())
|
||||
.expect("manifest json");
|
||||
|
||||
@@ -245,6 +245,8 @@ interface AgentRuntimeState {
|
||||
maxLoopIterations?: number;
|
||||
toolActionBudget?: number;
|
||||
plan: string[];
|
||||
planSteps?: AgentRuntimePlanStep[];
|
||||
activePlanStepIndex?: number | null;
|
||||
observations: string[];
|
||||
recentToolCalls?: AgentRuntimeToolCallRecord[];
|
||||
taskQueue?: AgentRuntimeTaskQueueSummary;
|
||||
@@ -274,6 +276,14 @@ interface AgentRuntimeToolCallRecord {
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface AgentRuntimePlanStep {
|
||||
index: number;
|
||||
title: string;
|
||||
status: string;
|
||||
detail: string | null;
|
||||
updatedAt: number;
|
||||
}
|
||||
|
||||
interface AgentRuntimeTaskQueueSummary {
|
||||
total: number;
|
||||
pending: number;
|
||||
@@ -530,6 +540,48 @@ function createAgentChatRunId(prefix: string) {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
function agentRuntimePlanStepsFromPlan(plan: string[]): AgentRuntimePlanStep[] {
|
||||
return plan
|
||||
.filter((item) => item.trim().length > 0)
|
||||
.slice(0, 8)
|
||||
.map((title, index) => ({
|
||||
index,
|
||||
title,
|
||||
status: index === 0 ? 'active' : 'pending',
|
||||
detail: null,
|
||||
updatedAt: 0,
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeAgentRuntimePlanSteps(
|
||||
state: AgentRuntimeState,
|
||||
previous?: AgentRuntimeState | null,
|
||||
) {
|
||||
if (state.planSteps && state.planSteps.length > 0) {
|
||||
return state.planSteps;
|
||||
}
|
||||
if (previous?.planSteps && previous.planSteps.length > 0) {
|
||||
return previous.planSteps;
|
||||
}
|
||||
return agentRuntimePlanStepsFromPlan(state.plan ?? []);
|
||||
}
|
||||
|
||||
function normalizeAgentRuntimeActivePlanStepIndex(
|
||||
state: AgentRuntimeState,
|
||||
previous?: AgentRuntimeState | null,
|
||||
) {
|
||||
if (state.activePlanStepIndex !== undefined) {
|
||||
return state.activePlanStepIndex;
|
||||
}
|
||||
if (previous?.activePlanStepIndex !== undefined) {
|
||||
return previous.activePlanStepIndex;
|
||||
}
|
||||
const activeStep = (state.planSteps ?? []).find(
|
||||
(step) => step.status === 'active',
|
||||
);
|
||||
return activeStep?.index ?? null;
|
||||
}
|
||||
|
||||
function normalizeAgentRuntimeState(
|
||||
state: AgentRuntimeState,
|
||||
previous?: AgentRuntimeState | null,
|
||||
@@ -543,6 +595,11 @@ function normalizeAgentRuntimeState(
|
||||
loopIteration: state.loopIteration ?? previous?.loopIteration ?? 0,
|
||||
maxLoopIterations: state.maxLoopIterations ?? previous?.maxLoopIterations ?? 3,
|
||||
toolActionBudget: state.toolActionBudget ?? previous?.toolActionBudget ?? 3,
|
||||
planSteps: normalizeAgentRuntimePlanSteps(state, previous),
|
||||
activePlanStepIndex: normalizeAgentRuntimeActivePlanStepIndex(
|
||||
state,
|
||||
previous,
|
||||
),
|
||||
recentToolCalls: state.recentToolCalls ?? previous?.recentToolCalls ?? [],
|
||||
toolPolicy: state.toolPolicy ?? previous?.toolPolicy ?? {
|
||||
allowedTools: state.allowedTools ?? [],
|
||||
@@ -667,6 +724,23 @@ function formatAgentRuntimeLoopProgress(
|
||||
}`;
|
||||
}
|
||||
|
||||
function formatAgentRuntimePlanStep(step: AgentRuntimePlanStep) {
|
||||
return `#${step.index + 1} ${step.status} · ${step.title}${
|
||||
step.detail ? ` · ${step.detail}` : ''
|
||||
}`;
|
||||
}
|
||||
|
||||
function agentRuntimeActivePlanStep(
|
||||
runtime: Pick<AgentRuntimeState, 'planSteps' | 'activePlanStepIndex'>,
|
||||
) {
|
||||
const steps = runtime.planSteps ?? [];
|
||||
return (
|
||||
steps.find((step) => step.index === runtime.activePlanStepIndex) ??
|
||||
steps.find((step) => step.status === 'active') ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
function AgentRuntimeStatusPanel({
|
||||
runtime,
|
||||
error,
|
||||
@@ -692,6 +766,7 @@ function AgentRuntimeStatusPanel({
|
||||
const recentEvents = (runtime.recentEvents ?? []).slice(-4).reverse();
|
||||
const recentToolCalls = (runtime.recentToolCalls ?? []).slice(-3).reverse();
|
||||
const recentTasks = (runtime.recentTasks ?? []).slice(-3).reverse();
|
||||
const planSteps = (runtime.planSteps ?? []).slice(0, 5);
|
||||
const toolPolicy = runtime.toolPolicy;
|
||||
const taskQueueSummary = formatAgentRuntimeTaskQueue(runtime.taskQueue);
|
||||
const loopProgress = formatAgentRuntimeLoopProgress(runtime);
|
||||
@@ -724,7 +799,18 @@ function AgentRuntimeStatusPanel({
|
||||
: ''}
|
||||
</small>
|
||||
) : null}
|
||||
{planItems.length > 0 ? (
|
||||
{planSteps.length > 0 ? (
|
||||
<div aria-label="Agent 计划进度">
|
||||
<strong>计划进度</strong>
|
||||
{planSteps.map((step) => (
|
||||
<small
|
||||
key={`${runtime.sessionId}-plan-step-${step.index}-${step.updatedAt}`}
|
||||
>
|
||||
{formatAgentRuntimePlanStep(step)}
|
||||
</small>
|
||||
))}
|
||||
</div>
|
||||
) : planItems.length > 0 ? (
|
||||
<ol>
|
||||
{planItems.map((item, index) => (
|
||||
<li key={`${runtime.sessionId}-plan-${index}`}>{item}</li>
|
||||
@@ -902,6 +988,7 @@ interface AgentStatusCard {
|
||||
runtimeLoopIteration: number | null;
|
||||
runtimeMaxLoopIterations: number | null;
|
||||
runtimeToolActionBudget: number | null;
|
||||
runtimeActivePlanStep: string | null;
|
||||
runtimeTaskQueue: AgentRuntimeTaskQueueSummary | null;
|
||||
runtimeRecentTasks: AgentRuntimeTaskRecord[];
|
||||
}
|
||||
@@ -10195,6 +10282,7 @@ export function deriveAgentStatusCards(
|
||||
latestByGroupRole.get(agentGroupRoleKey(task.group, task.role) ?? '');
|
||||
const cardId = agentConversationId(task);
|
||||
const runtime = runtimeByAgentId[cardId] ?? runtimeByAgentId[task.id] ?? null;
|
||||
const activePlanStep = runtime ? agentRuntimeActivePlanStep(runtime) : null;
|
||||
return {
|
||||
id: cardId,
|
||||
taskId: task.id,
|
||||
@@ -10226,6 +10314,9 @@ export function deriveAgentStatusCards(
|
||||
runtimeLoopIteration: runtime?.loopIteration ?? null,
|
||||
runtimeMaxLoopIterations: runtime?.maxLoopIterations ?? null,
|
||||
runtimeToolActionBudget: runtime?.toolActionBudget ?? null,
|
||||
runtimeActivePlanStep: activePlanStep
|
||||
? formatAgentRuntimePlanStep(activePlanStep)
|
||||
: null,
|
||||
runtimeTaskQueue: runtime?.taskQueue ?? null,
|
||||
runtimeRecentTasks: runtime?.recentTasks ?? [],
|
||||
};
|
||||
@@ -10262,6 +10353,7 @@ function sameAgentStatusCard(left: AgentStatusCard, right: AgentStatusCard) {
|
||||
left.runtimeLoopIteration === right.runtimeLoopIteration &&
|
||||
left.runtimeMaxLoopIterations === right.runtimeMaxLoopIterations &&
|
||||
left.runtimeToolActionBudget === right.runtimeToolActionBudget &&
|
||||
left.runtimeActivePlanStep === right.runtimeActivePlanStep &&
|
||||
sameAgentRuntimeTaskQueue(left.runtimeTaskQueue, right.runtimeTaskQueue) &&
|
||||
left.hasRecentEvidence === right.hasRecentEvidence &&
|
||||
left.taskGraphState === right.taskGraphState &&
|
||||
@@ -11138,6 +11230,9 @@ function summarizeAgentStatusCardsForChat(
|
||||
`- ${parts.join(' · ')}`,
|
||||
` ${agent.summary}`,
|
||||
agent.runtimeGoal ? ` 当前目标:${agent.runtimeGoal}` : null,
|
||||
agent.runtimeActivePlanStep
|
||||
? ` 当前计划步骤:${agent.runtimeActivePlanStep}`
|
||||
: null,
|
||||
runtimeTaskQueue ? ` ${runtimeTaskQueue}` : null,
|
||||
latestRuntimeTask
|
||||
? ` 最近任务:${formatAgentRecentRuntimeTask(latestRuntimeTask)}`
|
||||
@@ -20394,6 +20489,9 @@ export function App() {
|
||||
{agent.runtimeTask ? (
|
||||
<small>{`当前任务:${agent.runtimeTask}`}</small>
|
||||
) : null}
|
||||
{agent.runtimeActivePlanStep ? (
|
||||
<small>{`当前计划步骤:${agent.runtimeActivePlanStep}`}</small>
|
||||
) : null}
|
||||
{agentRuntimeTaskQueue ? (
|
||||
<small>{agentRuntimeTaskQueue}</small>
|
||||
) : null}
|
||||
|
||||
@@ -519,6 +519,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
maxLoopIterations: 3,
|
||||
toolActionBudget: 3,
|
||||
plan: ['读取项目上下文'],
|
||||
planSteps: [
|
||||
{
|
||||
index: 0,
|
||||
title: '读取项目上下文',
|
||||
status: 'active',
|
||||
detail: '拆解素材规格',
|
||||
updatedAt: 1235,
|
||||
},
|
||||
],
|
||||
activePlanStepIndex: 0,
|
||||
observations: ['已创建本轮 Agent Runtime run。'],
|
||||
allowedTools: ['conversation.read'],
|
||||
lastResponse: null,
|
||||
@@ -561,6 +571,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
runtimeLoopIteration: 2,
|
||||
runtimeMaxLoopIterations: 3,
|
||||
runtimeToolActionBudget: 3,
|
||||
runtimeActivePlanStep: '#1 active · 读取项目上下文 · 拆解素材规格',
|
||||
runtimeTaskQueue,
|
||||
runtimeRecentTasks: [runtimeTask],
|
||||
});
|
||||
@@ -1391,6 +1402,30 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
maxLoopIterations: 3,
|
||||
toolActionBudget: 3,
|
||||
plan: ['读取项目笔记', '结合观察修正建议', '回复开发者'],
|
||||
planSteps: [
|
||||
{
|
||||
index: 0,
|
||||
title: '读取项目笔记',
|
||||
status: 'completed',
|
||||
detail: 'file.read:ok · 已读取 game/notes.txt',
|
||||
updatedAt: 4005,
|
||||
},
|
||||
{
|
||||
index: 1,
|
||||
title: '结合观察修正建议',
|
||||
status: 'active',
|
||||
detail: '正在根据观察修正计划',
|
||||
updatedAt: 4006,
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
title: '回复开发者',
|
||||
status: 'failed',
|
||||
detail: '最终回复仍缺少角色规范确认',
|
||||
updatedAt: 4007,
|
||||
},
|
||||
],
|
||||
activePlanStepIndex: 1,
|
||||
observations: ['思考摘要:需要先看项目笔记', 'file.read:ok · 已读取 game/notes.txt'],
|
||||
recentToolCalls: [
|
||||
{
|
||||
@@ -1580,9 +1615,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
/任务队列:pending 0 · running 1 · completed 0 · failed 0 · total 1 · latest launcher-agent-task-/,
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText('读取项目笔记')).not.toBeNull();
|
||||
expect(screen.getByText('结合观察修正建议')).not.toBeNull();
|
||||
expect(screen.getByText('回复开发者')).not.toBeNull();
|
||||
expect(screen.getByText('计划进度')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText('#1 completed · 读取项目笔记 · file.read:ok · 已读取 game/notes.txt'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText('#2 active · 结合观察修正建议 · 正在根据观察修正计划'),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText('#3 failed · 回复开发者 · 最终回复仍缺少角色规范确认'),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText('思考摘要:需要先看项目笔记')).not.toBeNull();
|
||||
expect(screen.getByText('file.read:ok · 已读取 game/notes.txt')).not.toBeNull();
|
||||
expect(screen.getByText('最近动作')).not.toBeNull();
|
||||
@@ -13365,6 +13407,16 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
maxLoopIterations: 3,
|
||||
toolActionBudget: 3,
|
||||
plan: ['读取项目上下文'],
|
||||
planSteps: [
|
||||
{
|
||||
index: 0,
|
||||
title: '读取项目上下文',
|
||||
status: 'active',
|
||||
detail: '正在整理目标和约束',
|
||||
updatedAt: 11,
|
||||
},
|
||||
],
|
||||
activePlanStepIndex: 0,
|
||||
observations: ['已创建本轮 Agent Runtime run。'],
|
||||
taskQueue: {
|
||||
total: 2,
|
||||
@@ -13437,6 +13489,9 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
'当前目标:补齐第一关节奏目标',
|
||||
);
|
||||
expect(designCard.textContent).toContain('当前任务:拆解关卡节奏');
|
||||
expect(designCard.textContent).toContain(
|
||||
'当前计划步骤:#1 active · 读取项目上下文 · 正在整理目标和约束',
|
||||
);
|
||||
expect(designCard.textContent).toContain(
|
||||
'任务队列:pending 1 · running 1 · completed 0 · failed 0 · total 2 · latest runtime-design-director-1',
|
||||
);
|
||||
|
||||
@@ -4062,6 +4062,7 @@
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`。`currentGoal` 固定表达本轮任务目标,`waitingOn` 表达当前等待 LLM、工具观察、开发者输入或失败处理;后台任务生命周期、`agent.run_status` observation、下一轮 planning prompt、开发单 Agent 对话页、项目内 Agent 对话弹窗和主窗口 Agent 状态列表都必须展示同一份目标 / 等待状态。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`。后台 Agent loop 每轮规划前刷新当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,字段只做运行观测,不改变 loop 上限或权限 gate。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `planSteps / activePlanStepIndex`。Runtime 从 Agent 输出的 `plan` 派生结构化计划步骤,并在 action / observation / response / error 生命周期中更新 `pending / active / completed / failed` 和 detail;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示步骤进度,不再只依赖不可定位的 plan 字符串。
|
||||
- 2026-07-10 调整:开发单 Agent 对话页和项目内 Agent 对话弹窗的 Runtime 面板接入 `recentEvents`,展示最近 `thinking_summary / plan / action / observation / response / error` 事件,避免只从当前状态、observation 字符串或最近工具动作里倒推 Agent loop。
|
||||
- 2026-07-10 调整:Agent Runtime state / result 新增 `taskQueue` 观测摘要,从 `.agent/runtime/tasks/<agentId>.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`。开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都使用该字段判断同一 Agent 是否仍有排队任务;它不是新的调度器、SQLite 或跨重启独立 worker。
|
||||
- 2026-07-10 调整:Agent Runtime V1 后台工具箱新增只读 `task.list`。Agent 可自行读取 `.agent/manifest.json` 的 seed task 状态、依赖、产物交接和按依赖计算的 `readyTaskIds`,用于判断下一步任务;该工具必须受 `task.list` 项目权限策略保护,策略要求确认或拒绝时不得把任务图细节放进 observation。
|
||||
|
||||
@@ -37,6 +37,7 @@ Agent Runtime 负责:
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `recentToolCalls`,每次后台工具执行后记录最近 20 条结构化工具动作,包含 tool、status、reason、summary、detail 和 updatedAt;开发窗口、项目内 Agent 对话弹窗和主窗口 Agent 状态列表可直接展示“最近动作”,不再只能从 observation 字符串里猜测 action / observation 对应关系。字段只保存过滤后的摘要和观察细节,不保存原始 API Key 或任意未过滤输入。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `currentGoal` 和 `waitingOn`,把本轮目标与当前等待对象从 `currentTask / currentAction / nextStep` 中显式拆出来;后台任务启动、工具 observation、完成和失败都会刷新该状态,开发窗口、项目内 Agent 对话弹窗、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示同一份目标 / 等待信息,避免开发者只能从动作文本里猜 Agent 卡在 LLM、工具、同伴还是人工输入。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `loopIteration / maxLoopIterations / toolActionBudget`,结构化记录后台 Agent loop 当前轮次、最大轮次和每轮工具动作预算;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示该进度,帮助判断 Agent 是刚开始规划、正在 replan,还是接近本轮 loop 上限。该字段只做运行观测,不改变后台 loop 的执行上限或工具权限。
|
||||
- 2026-07-10 补充:Agent Runtime state 新增 `planSteps / activePlanStepIndex`,从 Agent 输出的 `plan` 派生结构化计划步骤,并在工具 action / observation / response / error 生命周期中更新 `pending / active / completed / failed` 和 detail;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都展示当前计划步骤与步骤进度,避免只能展示一串不可定位的 plan 文本。
|
||||
- 2026-07-10 补充:`recentEvents` 接入前端归一态和 Runtime 状态面板,事件事实源仍是 `.agent/runtime/events/<agentId>.jsonl`;面板按时间展示最近 `thinking_summary / plan / action / observation / response / error` 事件,现在能同时看到 Agent 的计划、最近观察、最近事件、最近工具动作和任务队列。
|
||||
- 2026-07-10 补充:Agent Runtime state / result 新增 `taskQueue`,从 `.agent/runtime/tasks/<agentId>.jsonl` 中每个 `runId` 的最新记录汇总 `total / pending / running / completed / failed / latestRunId`;开发窗口 Runtime 面板、主窗口 Agent 状态列表、`agent.run_status` observation 和下一轮 planning prompt 都读取该摘要,用于判断同一 Agent 是否仍有排队任务。该字段是运行观测摘要,不新增调度器、SQLite 或独立 worker。
|
||||
- 2026-07-10 补充:单 Agent 聊天和后台 planning prompt 会读取同一个 Agent 的 Runtime 连续上下文,把本 Agent 最近 status / phase / runId / 当前任务 / 下一步、最近回复、计划、观察、最近 3 条工具动作、最近事件、最近 3 条任务记录和工具策略摘要带入下一轮推理;上下文按规范 taskId 隔离,不读取其他 Agent 的 runtime 文件,并在进入 prompt 前过滤密钥和本机绝对路径。新后台 run 启动时会继承本 Agent 上次 `recentToolCalls` 和 `lastResponse`,让多轮任务不丢失结构化行动证据。
|
||||
@@ -298,7 +299,7 @@ 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`。这里的 `<agentId>` 以任务 `taskId` 为规范值,Tauri 只兼容旧 `group-role` 别名并映射到 taskId。每轮对话会同步写 `.agent/runtime/agents/<agentId>.json` 和 `.agent/runtime/events/<agentId>.jsonl`,字段包含 `agentId`、`taskId`、`sessionId`、`runId`、`source`、`status`、`phase`、`currentTask`、`currentGoal`、`currentAction`、`waitingOn`、`nextStep`、`loopIteration`、`maxLoopIterations`、`toolActionBudget`、`plan`、`observations`、`recentToolCalls`、`toolPolicy`、`allowedTools`、`lastResponse` 和 `error`;流式事件会把最新 `runtimeState` 回传给界面。Runtime state 写入使用临时文件替换,event JSONL 读取会跳过坏行;`currentTask`、`currentGoal`、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`。
|
||||
- 单 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`、`currentGoal`、`currentAction`、`waitingOn`、`nextStep`、`loopIteration`、`maxLoopIterations`、`toolActionBudget`、`plan`、`planSteps`、`activePlanStepIndex`、`observations`、`recentToolCalls`、`toolPolicy`、`allowedTools`、`lastResponse` 和 `error`;流式事件会把最新 `runtimeState` 回传给界面。Runtime state 写入使用临时文件替换,event JSONL 读取会跳过坏行;`currentTask`、`currentGoal`、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 命令` 移除确认项。
|
||||
|
||||
Reference in New Issue
Block a user