补齐Agent Runtime连续上下文
后台 Agent 规划上下文加入本 Agent 最近状态、任务和工具动作。 新后台 run 继承同 Agent 最近回复和工具动作,避免多轮任务断档。 补充同 Agent 连续性与跨 Agent 隔离测试,同步 Runtime V1 文档。
This commit is contained in:
@@ -1894,6 +1894,9 @@ pub(crate) fn start_game_creator_agent_runtime_task_at(
|
||||
return Err("Agent Runtime 任务不能为空".to_string());
|
||||
}
|
||||
let runtime_task = sanitize_agent_runtime_text(task, 180);
|
||||
let previous_state = read_game_creator_agent_runtime_at(root, &agent_id)
|
||||
.ok()
|
||||
.map(|result| result.state);
|
||||
let mut state = default_game_creator_agent_runtime_state(&agent_id, &run_id);
|
||||
state.source = source.trim().to_string();
|
||||
state.status = "running".to_string();
|
||||
@@ -1903,6 +1906,10 @@ pub(crate) fn start_game_creator_agent_runtime_task_at(
|
||||
state.next_step = "等待 Agent 输出计划或回复".to_string();
|
||||
state.plan = 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;
|
||||
state.last_response = previous_state.last_response;
|
||||
}
|
||||
refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?;
|
||||
state.updated_at = unix_timestamp();
|
||||
write_game_creator_agent_runtime_state(root, &state)?;
|
||||
@@ -2703,6 +2710,244 @@ fn redact_agent_runtime_project_paths(root: &Path, value: &str, max_chars: usize
|
||||
sanitize_agent_runtime_text(&redacted, max_chars)
|
||||
}
|
||||
|
||||
fn render_agent_runtime_tool_names(values: &[String], limit: usize) -> String {
|
||||
if values.is_empty() {
|
||||
return "无".to_string();
|
||||
}
|
||||
let mut names = values
|
||||
.iter()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.take(limit)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
if values.len() > limit {
|
||||
names.push(format!("等{}项", values.len()));
|
||||
}
|
||||
names.join(", ")
|
||||
}
|
||||
|
||||
fn render_agent_runtime_prompt_context(root: &Path, agent_id: &str) -> Result<String, String> {
|
||||
let runtime = match read_game_creator_agent_runtime_at(root, agent_id) {
|
||||
Ok(runtime) => runtime,
|
||||
Err(error) => {
|
||||
return Ok(format!(
|
||||
"Agent Runtime 连续上下文读取失败:{}",
|
||||
redact_agent_runtime_project_paths(root, &error, 360)
|
||||
));
|
||||
}
|
||||
};
|
||||
let state = &runtime.state;
|
||||
let has_meaningful_state = !state.current_task.trim().is_empty()
|
||||
|| state
|
||||
.last_response
|
||||
.as_deref()
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
|| state
|
||||
.error
|
||||
.as_deref()
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
|| !state.observations.is_empty()
|
||||
|| !state.recent_tool_calls.is_empty()
|
||||
|| !runtime.recent_events.is_empty()
|
||||
|| !runtime.recent_tasks.is_empty();
|
||||
if !has_meaningful_state {
|
||||
return Ok(String::new());
|
||||
}
|
||||
|
||||
let mut lines = Vec::new();
|
||||
lines.push(format!(
|
||||
"当前状态:status={}, phase={}, source={}, runId={}",
|
||||
redact_agent_runtime_project_paths(root, &state.status, 80),
|
||||
redact_agent_runtime_project_paths(root, &state.phase, 80),
|
||||
redact_agent_runtime_project_paths(root, &state.source, 120),
|
||||
redact_agent_runtime_project_paths(root, &state.run_id, 160)
|
||||
));
|
||||
if !state.current_task.trim().is_empty() {
|
||||
lines.push(format!(
|
||||
"当前任务:{}",
|
||||
redact_agent_runtime_project_paths(root, &state.current_task, 220)
|
||||
));
|
||||
}
|
||||
if !state.current_action.trim().is_empty() {
|
||||
lines.push(format!(
|
||||
"当前动作:{}",
|
||||
redact_agent_runtime_project_paths(root, &state.current_action, 160)
|
||||
));
|
||||
}
|
||||
if !state.next_step.trim().is_empty() {
|
||||
lines.push(format!(
|
||||
"下一步:{}",
|
||||
redact_agent_runtime_project_paths(root, &state.next_step, 160)
|
||||
));
|
||||
}
|
||||
if let Some(last_response) = state
|
||||
.last_response
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
lines.push(format!(
|
||||
"最近回复:{}",
|
||||
redact_agent_runtime_project_paths(root, last_response, 360)
|
||||
));
|
||||
}
|
||||
if let Some(error) = state
|
||||
.error
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
lines.push(format!(
|
||||
"最近错误:{}",
|
||||
redact_agent_runtime_project_paths(root, error, 360)
|
||||
));
|
||||
}
|
||||
if !state.plan.is_empty() {
|
||||
lines.push("当前/最近计划:".to_string());
|
||||
for item in state
|
||||
.plan
|
||||
.iter()
|
||||
.filter(|item| !item.trim().is_empty())
|
||||
.take(5)
|
||||
{
|
||||
lines.push(format!(
|
||||
"- {}",
|
||||
redact_agent_runtime_project_paths(root, item, 180)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let recent_observations = state
|
||||
.observations
|
||||
.iter()
|
||||
.filter(|item| !item.trim().is_empty())
|
||||
.rev()
|
||||
.take(3)
|
||||
.collect::<Vec<_>>();
|
||||
if !recent_observations.is_empty() {
|
||||
lines.push("最近观察:".to_string());
|
||||
for item in recent_observations.iter().rev() {
|
||||
lines.push(format!(
|
||||
"- {}",
|
||||
redact_agent_runtime_project_paths(root, item, 260)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let recent_tool_calls = state
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.rev()
|
||||
.take(3)
|
||||
.collect::<Vec<_>>();
|
||||
if !recent_tool_calls.is_empty() {
|
||||
lines.push("最近工具动作:".to_string());
|
||||
for call in recent_tool_calls.iter().rev() {
|
||||
let mut line = format!(
|
||||
"- {} [{}]:{}",
|
||||
redact_agent_runtime_project_paths(root, &call.tool, 80),
|
||||
redact_agent_runtime_project_paths(root, &call.status, 80),
|
||||
redact_agent_runtime_project_paths(root, &call.summary, 260)
|
||||
);
|
||||
if let Some(reason) = call
|
||||
.reason
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
line.push_str(&format!(
|
||||
";原因:{}",
|
||||
redact_agent_runtime_project_paths(root, reason, 160)
|
||||
));
|
||||
}
|
||||
if let Some(detail) = call
|
||||
.detail
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
line.push_str(&format!(
|
||||
";观察:{}",
|
||||
redact_agent_runtime_project_paths(root, detail, 260)
|
||||
));
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
let recent_events = runtime
|
||||
.recent_events
|
||||
.iter()
|
||||
.filter(|event| !event.summary.trim().is_empty())
|
||||
.rev()
|
||||
.take(4)
|
||||
.collect::<Vec<_>>();
|
||||
if !recent_events.is_empty() {
|
||||
lines.push("最近事件:".to_string());
|
||||
for event in recent_events.iter().rev() {
|
||||
let mut line = format!(
|
||||
"- {} [{} / {}]:{}",
|
||||
redact_agent_runtime_project_paths(root, &event.event_type, 120),
|
||||
redact_agent_runtime_project_paths(root, &event.status, 80),
|
||||
redact_agent_runtime_project_paths(root, &event.phase, 80),
|
||||
redact_agent_runtime_project_paths(root, &event.summary, 260)
|
||||
);
|
||||
if let Some(detail) = event
|
||||
.detail
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
line.push_str(&format!(
|
||||
";{}",
|
||||
redact_agent_runtime_project_paths(root, detail, 260)
|
||||
));
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
let recent_tasks = runtime
|
||||
.recent_tasks
|
||||
.iter()
|
||||
.rev()
|
||||
.take(3)
|
||||
.collect::<Vec<_>>();
|
||||
if !recent_tasks.is_empty() {
|
||||
lines.push("最近任务:".to_string());
|
||||
for task in recent_tasks.iter().rev() {
|
||||
let mut line = format!(
|
||||
"- {} [{} / {}]:{}",
|
||||
redact_agent_runtime_project_paths(root, &task.run_id, 160),
|
||||
redact_agent_runtime_project_paths(root, &task.status, 80),
|
||||
redact_agent_runtime_project_paths(root, &task.phase, 80),
|
||||
redact_agent_runtime_project_paths(root, &task.task, 220)
|
||||
);
|
||||
if !task.current_action.trim().is_empty() {
|
||||
line.push_str(&format!(
|
||||
";动作:{}",
|
||||
redact_agent_runtime_project_paths(root, &task.current_action, 180)
|
||||
));
|
||||
}
|
||||
if let Some(error) = task
|
||||
.error
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
line.push_str(&format!(
|
||||
";错误:{}",
|
||||
redact_agent_runtime_project_paths(root, error, 220)
|
||||
));
|
||||
}
|
||||
lines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(format!(
|
||||
"工具策略:auto={};confirm={};denied={}",
|
||||
render_agent_runtime_tool_names(&state.tool_policy.auto_tools, 8),
|
||||
render_agent_runtime_tool_names(&state.tool_policy.confirm_tools, 8),
|
||||
render_agent_runtime_tool_names(&state.tool_policy.denied_tools, 8)
|
||||
));
|
||||
|
||||
Ok(lines.join("\n"))
|
||||
}
|
||||
|
||||
fn unix_timestamp_nanos() -> u128 {
|
||||
SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
@@ -2747,6 +2992,7 @@ fn build_game_creator_role_agent_context(
|
||||
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 runtime_context = render_agent_runtime_prompt_context(root, &agent_id)?;
|
||||
let asset_context = render_local_asset_prompt_context(root)?;
|
||||
let conversation_context = render_local_conversation_prompt_context(root, Some(&agent_id))?;
|
||||
let identity = format!(
|
||||
@@ -2756,6 +3002,7 @@ fn build_game_creator_role_agent_context(
|
||||
let context = [
|
||||
("Agent 身份", identity.as_str()),
|
||||
("Agent 私有记忆", agent_memory.as_str()),
|
||||
("Agent Runtime 连续上下文", runtime_context.as_str()),
|
||||
("短期记忆", short_memory.as_str()),
|
||||
("长期记忆", long_memory.as_str()),
|
||||
("项目黑板", project_blackboard.as_str()),
|
||||
|
||||
@@ -50,6 +50,22 @@ fn unique_project_path() -> PathBuf {
|
||||
))
|
||||
}
|
||||
|
||||
fn wait_for_agent_runtime_idle(root: &Path, agent_id: &str) -> AgentRuntimeState {
|
||||
let mut runtime = read_game_creator_agent_runtime_at(root, agent_id)
|
||||
.expect("read runtime while waiting")
|
||||
.state;
|
||||
for _ in 0..50 {
|
||||
if runtime.status == "idle" {
|
||||
return runtime;
|
||||
}
|
||||
std::thread::sleep(Duration::from_millis(20));
|
||||
runtime = read_game_creator_agent_runtime_at(root, agent_id)
|
||||
.expect("read runtime while waiting")
|
||||
.state;
|
||||
}
|
||||
runtime
|
||||
}
|
||||
|
||||
fn test_local_config_path() -> PathBuf {
|
||||
Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.parent()
|
||||
@@ -1814,6 +1830,151 @@ async fn background_agent_runtime_can_replan_after_observation() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_plan_request_includes_same_agent_continuity_context() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
fs::write(
|
||||
root.join("game/continuity-notes.txt"),
|
||||
"连续上下文笔记:第一轮已经确认月光厨房核心循环。",
|
||||
)
|
||||
.expect("write continuity notes");
|
||||
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let first_plan_json = serde_json::json!({
|
||||
"thinkingSummary": "首轮需要读取项目笔记",
|
||||
"plan": ["读取项目笔记", "把观察留给下一轮"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "file.read",
|
||||
"reason": "为连续上下文留下工具证据",
|
||||
"input": { "path": "game/continuity-notes.txt" }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let art_plan_json = serde_json::json!({
|
||||
"thinkingSummary": "美术任务不应继承策划 Agent 的 runtime",
|
||||
"plan": ["独立处理美术任务"],
|
||||
"actions": [],
|
||||
"response": "美术无关任务完成。"
|
||||
})
|
||||
.to_string();
|
||||
let second_design_plan_json = serde_json::json!({
|
||||
"thinkingSummary": "第二轮应参考本 Agent 上一轮 runtime 上下文",
|
||||
"plan": ["复用上一轮工具观察", "继续设计建议"],
|
||||
"actions": [],
|
||||
"response": "第二轮设计任务完成。"
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![
|
||||
first_plan_json,
|
||||
"首轮完成:已经读取连续上下文笔记。".to_string(),
|
||||
art_plan_json,
|
||||
second_design_plan_json,
|
||||
],
|
||||
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"
|
||||
}},
|
||||
"art-director": {{
|
||||
"apiKey": "art-key",
|
||||
"baseUrl": {base_url:?},
|
||||
"model": "art-runtime-model",
|
||||
"apiKind": "openai_responses"
|
||||
}}
|
||||
}}
|
||||
}}"#
|
||||
));
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"首轮检查连续上下文",
|
||||
"design-continuity-first",
|
||||
)
|
||||
.expect("start first design task");
|
||||
let first_plan_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("first design plan request");
|
||||
assert!(first_plan_request.contains("首轮检查连续上下文"));
|
||||
let first_replan_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("first design replan request");
|
||||
assert!(first_replan_request.contains("连续上下文笔记:第一轮已经确认月光厨房核心循环"));
|
||||
let first_runtime = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(first_runtime.run_id, "design-continuity-first");
|
||||
assert_eq!(
|
||||
first_runtime.last_response.as_deref(),
|
||||
Some("首轮完成:已经读取连续上下文笔记。")
|
||||
);
|
||||
assert!(first_runtime
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.any(|call| call.tool == "file.read" && call.status == "ok"));
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"art-director",
|
||||
"美术无关任务",
|
||||
"art-continuity-check",
|
||||
)
|
||||
.expect("start art task");
|
||||
let art_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("art plan request");
|
||||
assert!(art_request.contains("美术无关任务"));
|
||||
assert!(!art_request.contains("design-continuity-first"));
|
||||
assert!(!art_request.contains("首轮完成:已经读取连续上下文笔记"));
|
||||
assert!(!art_request.contains("file.read [ok]"));
|
||||
let art_runtime = wait_for_agent_runtime_idle(&root, "art-director");
|
||||
assert_eq!(art_runtime.run_id, "art-continuity-check");
|
||||
|
||||
start_game_creator_agent_background_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"第二轮继续策划",
|
||||
"design-continuity-second",
|
||||
)
|
||||
.expect("start second design task");
|
||||
let second_design_request = receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("second design plan request");
|
||||
assert!(second_design_request.contains("# Agent Runtime 连续上下文"));
|
||||
assert!(second_design_request.contains("当前状态:status=running"));
|
||||
assert!(second_design_request.contains("runId=design-continuity-second"));
|
||||
assert!(second_design_request.contains("最近回复:首轮完成:已经读取连续上下文笔记。"));
|
||||
assert!(second_design_request.contains("最近工具动作"));
|
||||
assert!(second_design_request.contains("file.read [ok]"));
|
||||
assert!(second_design_request.contains("为连续上下文留下工具证据"));
|
||||
assert!(second_design_request.contains("连续上下文笔记:第一轮已经确认月光厨房核心循环"));
|
||||
assert!(second_design_request.contains("最近任务"));
|
||||
assert!(second_design_request.contains("design-continuity-first [completed / completed]"));
|
||||
assert!(second_design_request.contains("首轮检查连续上下文"));
|
||||
assert!(!second_design_request.contains(root.to_string_lossy().as_ref()));
|
||||
let second_runtime = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(second_runtime.run_id, "design-continuity-second");
|
||||
assert_eq!(
|
||||
second_runtime.last_response.as_deref(),
|
||||
Some("第二轮设计任务完成。")
|
||||
);
|
||||
assert!(second_runtime
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.any(|call| call.tool == "file.read" && call.status == "ok"));
|
||||
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user