补齐Agent Runtime连续上下文

后台 Agent 规划上下文加入本 Agent 最近状态、任务和工具动作。

新后台 run 继承同 Agent 最近回复和工具动作,避免多轮任务断档。

补充同 Agent 连续性与跨 Agent 隔离测试,同步 Runtime V1 文档。
This commit is contained in:
AIGameCreator App
2026-07-10 02:05:01 +08:00
parent eee974fb9e
commit 48cdf33c2c
4 changed files with 412 additions and 0 deletions
@@ -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();