绑定Agent工具确认到精确动作
Runtime 为工具输入生成 SHA-256 指纹和安全摘要 确认票据仅放行同一 Agent、run、command 和输入 开发窗口接通待确认动作的确认继续入口 补充动作变更阻断和界面确认回归测试 同步 Runtime V1 技术方案和项目记忆
This commit is contained in:
+1
@@ -1314,6 +1314,7 @@ dependencies = [
|
||||
"reqwest 0.12.28",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"shared-contracts",
|
||||
"tauri",
|
||||
"tauri-build",
|
||||
|
||||
@@ -10,6 +10,7 @@ tauri-build = { version = "2.6.2", features = [] }
|
||||
[dependencies]
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
platform-llm = { path = "../../../server-rs/crates/platform-llm" }
|
||||
platform-agent = { path = "../../../server-rs/crates/platform-agent" }
|
||||
reqwest = { version = "0.12", default-features = false, features = ["json", "native-tls"] }
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use super::*;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
static GAME_CREATOR_AGENT_RUNTIME_UPDATE_APP_HANDLE: OnceLock<tauri::AppHandle> = OnceLock::new();
|
||||
|
||||
@@ -377,7 +378,7 @@ fn start_game_creator_agent_background_task_with_run_id_at(
|
||||
run_id: &str,
|
||||
) -> Result<(AgentRuntimeResult, String), String> {
|
||||
start_game_creator_agent_background_task_with_confirmed_tool_at(
|
||||
root, agent_id, task, run_id, None, "",
|
||||
root, agent_id, task, run_id, None, None, "",
|
||||
)
|
||||
}
|
||||
|
||||
@@ -387,6 +388,7 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at(
|
||||
task: &str,
|
||||
run_id: &str,
|
||||
confirmed_command_id: Option<&str>,
|
||||
confirmed_action_fingerprint: Option<&str>,
|
||||
confirmation_note: &str,
|
||||
) -> Result<(AgentRuntimeResult, String), String> {
|
||||
start_game_creator_agent_background_task_with_source_at(
|
||||
@@ -396,6 +398,7 @@ fn start_game_creator_agent_background_task_with_confirmed_tool_at(
|
||||
run_id,
|
||||
"agent-background-task",
|
||||
confirmed_command_id,
|
||||
confirmed_action_fingerprint,
|
||||
confirmation_note,
|
||||
)
|
||||
}
|
||||
@@ -407,6 +410,7 @@ fn start_game_creator_agent_background_task_with_source_at(
|
||||
run_id: &str,
|
||||
source: &str,
|
||||
confirmed_command_id: Option<&str>,
|
||||
confirmed_action_fingerprint: Option<&str>,
|
||||
confirmation_note: &str,
|
||||
) -> Result<(AgentRuntimeResult, String), String> {
|
||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||
@@ -447,11 +451,15 @@ fn start_game_creator_agent_background_task_with_source_at(
|
||||
}),
|
||||
)?;
|
||||
if let Some(command_id) = confirmed_command_id {
|
||||
let action_fingerprint = confirmed_action_fingerprint
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| "Agent Runtime 工具确认缺少动作指纹".to_string())?;
|
||||
write_game_creator_agent_runtime_tool_confirmation(
|
||||
root,
|
||||
&agent_id,
|
||||
&run_id,
|
||||
command_id,
|
||||
action_fingerprint,
|
||||
confirmation_note,
|
||||
)?;
|
||||
}
|
||||
@@ -522,6 +530,7 @@ pub(crate) fn schedule_game_creator_agent_ready_tasks_at(
|
||||
&run_id,
|
||||
"agent-ready-task-scheduler",
|
||||
None,
|
||||
None,
|
||||
"",
|
||||
) {
|
||||
Ok((result, actual_run_id)) => {
|
||||
@@ -776,6 +785,12 @@ pub(crate) fn confirm_game_creator_agent_runtime_task_at(
|
||||
.ok_or_else(|| "未找到待确认工具动作".to_string())?;
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(&tool_call.tool)
|
||||
.ok_or_else(|| format!("待确认工具不在白名单中:{}", tool_call.tool))?;
|
||||
let action_fingerprint = tool_call
|
||||
.action_fingerprint
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| "待确认工具动作缺少精确指纹,请重新提交该任务".to_string())?;
|
||||
let input_summary = tool_call.input_summary.clone();
|
||||
let note = sanitize_agent_runtime_text(note, 240);
|
||||
let confirmed_task = if note.trim().is_empty() {
|
||||
format!(
|
||||
@@ -800,6 +815,7 @@ pub(crate) fn confirm_game_creator_agent_runtime_task_at(
|
||||
&confirmed_task,
|
||||
&requested_run_id,
|
||||
Some(command_id),
|
||||
Some(action_fingerprint),
|
||||
¬e,
|
||||
)?;
|
||||
append_game_creator_agent_runtime_task_record(
|
||||
@@ -830,6 +846,8 @@ pub(crate) fn confirm_game_creator_agent_runtime_task_at(
|
||||
"confirmedRunId": confirmed_run_id,
|
||||
"tool": tool_call.tool,
|
||||
"commandId": command_id,
|
||||
"actionFingerprint": action_fingerprint,
|
||||
"inputSummary": input_summary,
|
||||
"note": note,
|
||||
}),
|
||||
)?;
|
||||
@@ -1103,7 +1121,7 @@ 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);
|
||||
append_agent_runtime_tool_call_record(&root, &mut runtime, action, &observation);
|
||||
if observation.is_waiting_for_confirmation() {
|
||||
runtime.status = "waiting-for-confirmation".to_string();
|
||||
runtime.phase = "waiting-for-confirmation".to_string();
|
||||
@@ -1167,6 +1185,8 @@ async fn run_game_creator_agent_background_task(
|
||||
"taskId": runtime.task_id,
|
||||
"runId": runtime.run_id,
|
||||
"tool": observation.tool,
|
||||
"actionFingerprint": agent_runtime_tool_action_fingerprint(action),
|
||||
"inputSummary": agent_runtime_tool_action_input_summary(&root, action),
|
||||
"summary": observation.summary,
|
||||
}),
|
||||
);
|
||||
@@ -1382,6 +1402,7 @@ enum AgentRuntimeToolPolicyBlock {
|
||||
}
|
||||
|
||||
fn append_agent_runtime_tool_call_record(
|
||||
root: &Path,
|
||||
runtime: &mut AgentRuntimeState,
|
||||
action: &AgentRuntimeToolAction,
|
||||
observation: &AgentRuntimeToolObservation,
|
||||
@@ -1389,6 +1410,8 @@ fn append_agent_runtime_tool_call_record(
|
||||
runtime.recent_tool_calls.push(AgentRuntimeToolCallRecord {
|
||||
tool: observation.tool.clone(),
|
||||
status: observation.status.clone(),
|
||||
action_fingerprint: Some(agent_runtime_tool_action_fingerprint(action)),
|
||||
input_summary: agent_runtime_tool_action_input_summary(root, action),
|
||||
reason: action
|
||||
.reason
|
||||
.as_deref()
|
||||
@@ -1411,6 +1434,112 @@ fn append_agent_runtime_tool_call_record(
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_tool_action_fingerprint(action: &AgentRuntimeToolAction) -> String {
|
||||
let payload = serde_json::json!({
|
||||
"tool": action.tool.trim(),
|
||||
"input": &action.input,
|
||||
});
|
||||
let encoded = serde_json::to_vec(&payload).unwrap_or_default();
|
||||
format!("{:x}", Sha256::digest(encoded))
|
||||
}
|
||||
|
||||
fn agent_runtime_tool_action_input_summary(
|
||||
root: &Path,
|
||||
action: &AgentRuntimeToolAction,
|
||||
) -> Option<String> {
|
||||
let input = &action.input;
|
||||
let tool = action.tool.trim();
|
||||
let text = |keys: &[&str]| agent_runtime_tool_input_text(input, keys);
|
||||
let relative_path = |keys: &[&str]| {
|
||||
let value = text(keys);
|
||||
if Path::new(&value).is_absolute() {
|
||||
"[absolute path rejected]".to_string()
|
||||
} else {
|
||||
value
|
||||
}
|
||||
};
|
||||
let chars = |keys: &[&str]| {
|
||||
keys.iter()
|
||||
.find_map(|key| input.get(*key).and_then(|value| value.as_str()))
|
||||
.map(|value| value.chars().count())
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let list_len = |keys: &[&str]| {
|
||||
keys.iter()
|
||||
.find_map(|key| input.get(*key).and_then(|value| value.as_array()))
|
||||
.map(Vec::len)
|
||||
.unwrap_or(0)
|
||||
};
|
||||
let summary = match tool {
|
||||
"memory.read" => format!("scope={}", text(&["scope"])),
|
||||
"memory.write" => format!(
|
||||
"scope={} · mode={} · title={} · contentChars={}",
|
||||
text(&["scope"]),
|
||||
text(&["mode"]),
|
||||
text(&["title"]),
|
||||
chars(&["content"])
|
||||
),
|
||||
"project.restore" | "project.diff" => format!(
|
||||
"checkpointId={}",
|
||||
text(&["checkpointId", "checkpoint_id", "id"])
|
||||
),
|
||||
"file.list" | "file.read" => format!("path={}", relative_path(&["path"])),
|
||||
"file.write" => format!(
|
||||
"path={} · contentChars={}",
|
||||
relative_path(&["path"]),
|
||||
chars(&["content"])
|
||||
),
|
||||
"task.create" => format!(
|
||||
"taskId={} · title={} · group={} · role={} · dependencies={} · artifacts={} · criteria={}",
|
||||
text(&["taskId", "task_id", "id"]),
|
||||
text(&["title"]),
|
||||
text(&["group"]),
|
||||
text(&["role"]),
|
||||
list_len(&["dependencies"]),
|
||||
list_len(&["artifacts"]),
|
||||
list_len(&["acceptanceCriteria", "acceptance_criteria"])
|
||||
),
|
||||
"task.update" => format!(
|
||||
"taskId={} · status={}",
|
||||
text(&["taskId", "task_id", "id"]),
|
||||
text(&["status"])
|
||||
),
|
||||
"command.run_limited" => format!(
|
||||
"commandId={}",
|
||||
text(&["commandId", "command_id", "id"])
|
||||
),
|
||||
"canvas.asset_generate" => format!("promptChars={}", chars(&["prompt"])),
|
||||
"blackboard.write" => format!(
|
||||
"title={} · contentChars={}",
|
||||
text(&["title"]),
|
||||
chars(&["content"])
|
||||
),
|
||||
"agent.message" => format!(
|
||||
"agentId={} · contentChars={}",
|
||||
text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]),
|
||||
chars(&["content"])
|
||||
),
|
||||
"agent.delegate" => format!(
|
||||
"agentId={} · runId={} · taskChars={}",
|
||||
text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"]),
|
||||
text(&["runId", "run_id"]),
|
||||
chars(&["task"])
|
||||
),
|
||||
"agent.schedule_ready" => format!(
|
||||
"limit={}",
|
||||
input.get("limit").and_then(|value| value.as_u64()).unwrap_or(1)
|
||||
),
|
||||
"agent.run_status" => format!(
|
||||
"scope={} · agentId={}",
|
||||
text(&["scope"]),
|
||||
text(&["agentId", "agent_id", "targetAgentId", "target_agent_id"])
|
||||
),
|
||||
_ => String::new(),
|
||||
};
|
||||
let summary = redact_agent_runtime_project_paths(root, &summary, 320);
|
||||
(!summary.trim().is_empty()).then_some(summary)
|
||||
}
|
||||
|
||||
fn update_agent_runtime_plan_steps(runtime: &mut AgentRuntimeState, plan: Vec<String>) {
|
||||
runtime.plan = plan
|
||||
.into_iter()
|
||||
@@ -1700,11 +1829,16 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action(
|
||||
action: &AgentRuntimeToolAction,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let tool = action.tool.trim();
|
||||
let action_fingerprint = agent_runtime_tool_action_fingerprint(action);
|
||||
let command_id = game_creator_agent_runtime_tool_command_id(tool);
|
||||
if let Some(command_id) = command_id {
|
||||
if let Some(blocked) =
|
||||
game_creator_agent_runtime_tool_policy_block(root, agent_id, run_id, command_id)
|
||||
{
|
||||
if let Some(blocked) = game_creator_agent_runtime_tool_policy_block(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
command_id,
|
||||
&action_fingerprint,
|
||||
) {
|
||||
let (status, summary) = match blocked {
|
||||
AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary),
|
||||
AgentRuntimeToolPolicyBlock::RequiresConfirmation(summary) => {
|
||||
@@ -1836,6 +1970,7 @@ fn write_game_creator_agent_runtime_tool_confirmation(
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &str,
|
||||
action_fingerprint: &str,
|
||||
note: &str,
|
||||
) -> Result<(), String> {
|
||||
let path =
|
||||
@@ -1853,6 +1988,7 @@ fn write_game_creator_agent_runtime_tool_confirmation(
|
||||
"agentId": agent_id,
|
||||
"runId": run_id,
|
||||
"commandId": command_id,
|
||||
"actionFingerprint": action_fingerprint,
|
||||
"note": sanitize_agent_runtime_text(note, 240),
|
||||
"updatedAt": unix_timestamp(),
|
||||
});
|
||||
@@ -1871,20 +2007,40 @@ fn consume_game_creator_agent_runtime_tool_confirmation(
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &str,
|
||||
action_fingerprint: &str,
|
||||
) -> Result<bool, String> {
|
||||
if run_id.trim().is_empty() {
|
||||
return Ok(false);
|
||||
}
|
||||
let path =
|
||||
game_creator_agent_runtime_tool_confirmation_path(root, agent_id, run_id, command_id);
|
||||
match fs::remove_file(&path) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
|
||||
Err(error) => Err(format!(
|
||||
let content = match fs::read_to_string(&path) {
|
||||
Ok(content) => content,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
|
||||
Err(error) => {
|
||||
return Err(format!(
|
||||
"读取 Agent Runtime 工具确认失败:{}: {error}",
|
||||
path.display()
|
||||
))
|
||||
}
|
||||
};
|
||||
let payload = serde_json::from_str::<serde_json::Value>(&content).map_err(|error| {
|
||||
format!(
|
||||
"解析 Agent Runtime 工具确认失败:{}: {error}",
|
||||
path.display()
|
||||
)
|
||||
})?;
|
||||
let confirmed_fingerprint = payload
|
||||
.get("actionFingerprint")
|
||||
.and_then(|value| value.as_str())
|
||||
.unwrap_or_default();
|
||||
fs::remove_file(&path).map_err(|error| {
|
||||
format!(
|
||||
"消费 Agent Runtime 工具确认失败:{}: {error}",
|
||||
path.display()
|
||||
)),
|
||||
}
|
||||
)
|
||||
})?;
|
||||
Ok(!action_fingerprint.trim().is_empty() && confirmed_fingerprint == action_fingerprint)
|
||||
}
|
||||
|
||||
fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
@@ -1997,6 +2153,7 @@ fn game_creator_agent_runtime_tool_policy_block(
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &str,
|
||||
action_fingerprint: &str,
|
||||
) -> Option<AgentRuntimeToolPolicyBlock> {
|
||||
let view = match read_project_permission_policy_at(root) {
|
||||
Ok(view) => view,
|
||||
@@ -2039,7 +2196,11 @@ fn game_creator_agent_runtime_tool_policy_block(
|
||||
.any(|command| command == command_id)
|
||||
{
|
||||
match consume_game_creator_agent_runtime_tool_confirmation(
|
||||
root, &agent_id, run_id, command_id,
|
||||
root,
|
||||
&agent_id,
|
||||
run_id,
|
||||
command_id,
|
||||
action_fingerprint,
|
||||
) {
|
||||
Ok(true) => return None,
|
||||
Ok(false) => {}
|
||||
@@ -2062,7 +2223,11 @@ fn game_creator_agent_runtime_tool_policy_block(
|
||||
.unwrap_or(false)
|
||||
{
|
||||
match consume_game_creator_agent_runtime_tool_confirmation(
|
||||
root, &agent_id, run_id, command_id,
|
||||
root,
|
||||
&agent_id,
|
||||
run_id,
|
||||
command_id,
|
||||
action_fingerprint,
|
||||
) {
|
||||
Ok(true) => return None,
|
||||
Ok(false) => {}
|
||||
@@ -3517,12 +3682,23 @@ fn format_agent_runtime_task_observation(task: &AgentRuntimeTaskRecord) -> Strin
|
||||
}
|
||||
|
||||
fn format_agent_runtime_tool_call_observation(call: &AgentRuntimeToolCallRecord) -> String {
|
||||
format!(
|
||||
let mut output = format!(
|
||||
"{} / {} / {}",
|
||||
call.tool,
|
||||
call.status,
|
||||
sanitize_agent_runtime_text(&call.summary, 120)
|
||||
)
|
||||
);
|
||||
if let Some(input_summary) = call
|
||||
.input_summary
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
output.push_str(&format!(
|
||||
" / target={}",
|
||||
sanitize_agent_runtime_text(input_summary, 160)
|
||||
));
|
||||
}
|
||||
output
|
||||
}
|
||||
|
||||
fn agent_runtime_tool_input_text(input: &serde_json::Value, keys: &[&str]) -> String {
|
||||
@@ -4993,6 +5169,16 @@ fn render_agent_runtime_prompt_context(root: &Path, agent_id: &str) -> Result<St
|
||||
redact_agent_runtime_project_paths(root, reason, 160)
|
||||
));
|
||||
}
|
||||
if let Some(input_summary) = call
|
||||
.input_summary
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
{
|
||||
line.push_str(&format!(
|
||||
";目标:{}",
|
||||
redact_agent_runtime_project_paths(root, input_summary, 220)
|
||||
));
|
||||
}
|
||||
if let Some(detail) = call
|
||||
.detail
|
||||
.as_deref()
|
||||
|
||||
@@ -438,7 +438,8 @@ pub(crate) fn confirm_game_creator_agent_runtime_task(
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
enforce_project_permission_policy(root, "conversation.write")?;
|
||||
enforce_project_permission_policy(root, "agent.run_status")?;
|
||||
enforce_project_auto_permission_policy(root, "agent.resume")?;
|
||||
// 该命令只由开发者显式点击“确认继续”触发。
|
||||
enforce_project_permission_policy(root, "agent.resume")?;
|
||||
confirm_game_creator_agent_runtime_task_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
|
||||
@@ -226,6 +226,10 @@ struct AgentRuntimeToolCallRecord {
|
||||
#[serde(default)]
|
||||
status: String,
|
||||
#[serde(default)]
|
||||
action_fingerprint: Option<String>,
|
||||
#[serde(default)]
|
||||
input_summary: Option<String>,
|
||||
#[serde(default)]
|
||||
reason: Option<String>,
|
||||
#[serde(default)]
|
||||
summary: String,
|
||||
|
||||
@@ -4897,7 +4897,7 @@ async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action()
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["file.read".to_string()],
|
||||
confirm_commands: vec!["file.read".to_string(), "agent.resume".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
@@ -4953,13 +4953,31 @@ async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action()
|
||||
.expect("first plan llm request");
|
||||
let waiting_runtime = wait_for_agent_runtime_confirmation(&root, "design-director");
|
||||
assert_eq!(waiting_runtime.status, "waiting-for-confirmation");
|
||||
let pending_tool_call = waiting_runtime
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|call| call.status == "waiting-for-confirmation")
|
||||
.expect("pending tool call");
|
||||
assert_eq!(
|
||||
pending_tool_call.input_summary.as_deref(),
|
||||
Some("path=game/notes.txt")
|
||||
);
|
||||
assert_eq!(
|
||||
pending_tool_call
|
||||
.action_fingerprint
|
||||
.as_deref()
|
||||
.expect("action fingerprint")
|
||||
.len(),
|
||||
64
|
||||
);
|
||||
|
||||
let confirmed = confirm_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"design-confirm-run",
|
||||
"design-confirm-run-approved",
|
||||
"允许读取项目笔记",
|
||||
let confirmed = confirm_game_creator_agent_runtime_task(
|
||||
root.to_string_lossy().into_owned(),
|
||||
"design-director".to_string(),
|
||||
"design-confirm-run".to_string(),
|
||||
"design-confirm-run-approved".to_string(),
|
||||
"允许读取项目笔记".to_string(),
|
||||
)
|
||||
.expect("confirm waiting task");
|
||||
assert_eq!(confirmed.state.run_id, "design-confirm-run-approved");
|
||||
@@ -4995,6 +5013,126 @@ async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action()
|
||||
assert!(agent_db.contains("\"recordType\":\"agent.runtime.tool_confirmation.approved\""));
|
||||
assert!(agent_db.contains("\"confirmedRunId\":\"design-confirm-run-approved\""));
|
||||
assert!(agent_db.contains("\"tool\":\"file.read\""));
|
||||
assert!(agent_db.contains("\"actionFingerprint\":"));
|
||||
assert!(agent_db.contains("\"inputSummary\":\"path=game/notes.txt\""));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_confirmation_is_bound_to_exact_tool_input() {
|
||||
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 approved notes");
|
||||
fs::write(root.join("game/other.txt"), "未批准读取的笔记").expect("write changed notes");
|
||||
write_project_permission_policy_at(
|
||||
&root,
|
||||
ProjectPermissionPolicy {
|
||||
denied_commands: Vec::new(),
|
||||
confirm_commands: vec!["file.read".to_string()],
|
||||
agent_policies: BTreeMap::new(),
|
||||
},
|
||||
)
|
||||
.expect("write policy");
|
||||
let (sender, receiver) = mpsc::channel();
|
||||
let approved_plan = serde_json::json!({
|
||||
"thinkingSummary": "需要读已批准的项目笔记",
|
||||
"plan": ["读取已批准笔记"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "file.read",
|
||||
"reason": "读取已批准目标",
|
||||
"input": { "path": "game/notes.txt" }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let changed_plan = serde_json::json!({
|
||||
"thinkingSummary": "改为读取另一个文件",
|
||||
"plan": ["读取未批准目标"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "file.read",
|
||||
"reason": "尝试复用旧确认读取不同目标",
|
||||
"input": { "path": "game/other.txt" }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![approved_plan, changed_plan],
|
||||
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-exact-confirm-run",
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("first plan llm request");
|
||||
let first_waiting = wait_for_agent_runtime_confirmation(&root, "design-director");
|
||||
assert_eq!(first_waiting.run_id, "design-exact-confirm-run");
|
||||
|
||||
confirm_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"design-exact-confirm-run",
|
||||
"design-exact-confirm-approved",
|
||||
"只允许读取 game/notes.txt",
|
||||
)
|
||||
.expect("confirm exact action");
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("changed plan llm request");
|
||||
assert!(receiver.recv_timeout(Duration::from_millis(200)).is_err());
|
||||
|
||||
let changed_waiting = wait_for_agent_runtime_confirmation(&root, "design-director");
|
||||
assert_eq!(changed_waiting.run_id, "design-exact-confirm-approved");
|
||||
assert_eq!(changed_waiting.status, "waiting-for-confirmation");
|
||||
assert!(!changed_waiting
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("file.read:ok")));
|
||||
let changed_tool_call = changed_waiting
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|call| call.status == "waiting-for-confirmation")
|
||||
.expect("changed pending tool call");
|
||||
assert_eq!(
|
||||
changed_tool_call.input_summary.as_deref(),
|
||||
Some("path=game/other.txt")
|
||||
);
|
||||
assert_ne!(
|
||||
changed_tool_call.action_fingerprint,
|
||||
first_waiting
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|call| call.status == "waiting-for-confirmation")
|
||||
.and_then(|call| call.action_fingerprint.clone())
|
||||
);
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
assert!(!agent_db.contains("未批准读取的笔记"));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
@@ -270,6 +270,8 @@ interface AgentRuntimeToolPolicySnapshot {
|
||||
interface AgentRuntimeToolCallRecord {
|
||||
tool: string;
|
||||
status: string;
|
||||
actionFingerprint?: string | null;
|
||||
inputSummary?: string | null;
|
||||
reason: string | null;
|
||||
summary: string;
|
||||
detail: string | null;
|
||||
@@ -926,6 +928,7 @@ function AgentRuntimeStatusPanel({
|
||||
key={`${runtime.sessionId}-tool-${toolCall.updatedAt}-${index}`}
|
||||
>
|
||||
{`${toolCall.tool} · ${toolCall.status} · ${toolCall.summary}`}
|
||||
{toolCall.inputSummary ? ` · 目标:${toolCall.inputSummary}` : ''}
|
||||
{toolCall.reason ? ` · ${toolCall.reason}` : ''}
|
||||
</small>
|
||||
))}
|
||||
@@ -3934,6 +3937,66 @@ export function WorkspaceLauncher({
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAgentChatConfirmRuntimeTask(runId: string) {
|
||||
const projectPathForChat = validateAgentChatProjectPath();
|
||||
const agent = selectedLauncherAgentChatAgent();
|
||||
if (!projectPathForChat || !agent || !runId || agentChatBackgroundBusy) {
|
||||
return;
|
||||
}
|
||||
const llmWarning = getCurrentAgentChatLlmWarning(agent);
|
||||
if (llmWarning) {
|
||||
setAgentChatStatus(llmWarning);
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
if (!invoke) {
|
||||
setAgentChatStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
}
|
||||
const saveVersion = agentChatLoadVersionRef.current + 1;
|
||||
agentChatLoadVersionRef.current = saveVersion;
|
||||
setAgentChatBackgroundBusy(true);
|
||||
setAgentChatStatus('正在确认并继续 Agent 后台任务');
|
||||
try {
|
||||
const runtime = await invoke<AgentRuntimeResult>(
|
||||
'confirm_game_creator_agent_runtime_task',
|
||||
{
|
||||
projectPath: projectPathForChat,
|
||||
agentId: agent.id,
|
||||
runId,
|
||||
nextRunId: createAgentChatRunId('launcher-agent-confirm'),
|
||||
note: '开发者已确认待执行工具动作',
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatRuntime(agentRuntimeStateFromResult(runtime));
|
||||
setAgentChatRuntimeError('');
|
||||
const conversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
{
|
||||
projectPath: projectPathForChat,
|
||||
agentId: agent.id,
|
||||
},
|
||||
);
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatMessages(conversation.messages);
|
||||
setAgentChatStatus(agentRuntimeStartStatus(runtime));
|
||||
} catch (error) {
|
||||
if (agentChatLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentChatStatus(error instanceof Error ? error.message : String(error));
|
||||
} finally {
|
||||
if (agentChatLoadVersionRef.current === saveVersion) {
|
||||
setAgentChatBackgroundBusy(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const projectRows = recentWorkspaces.map((workspace) => {
|
||||
const directoryStatus = recentWorkspaceStatuses[workspace];
|
||||
const isPendingStatus = directoryStatus === undefined;
|
||||
@@ -4608,6 +4671,9 @@ export function WorkspaceLauncher({
|
||||
onRetryRuntimeTask={(runId) =>
|
||||
void handleAgentChatRetryRuntimeTask(runId)
|
||||
}
|
||||
onConfirmRuntimeTask={(runId) =>
|
||||
void handleAgentChatConfirmRuntimeTask(runId)
|
||||
}
|
||||
/>
|
||||
</section>
|
||||
<div className="launcher-agent-chat-messages" aria-label="Agent 聊天记录">
|
||||
|
||||
@@ -1436,6 +1436,8 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
{
|
||||
tool: 'file.read',
|
||||
status: 'ok',
|
||||
actionFingerprint: 'a'.repeat(64),
|
||||
inputSummary: 'path=game/notes.txt',
|
||||
reason: '读取笔记',
|
||||
summary: '已读取 game/notes.txt',
|
||||
detail: 'game/notes.txt: 连续上下文笔记',
|
||||
@@ -1690,7 +1692,9 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
expect(screen.getByText('file.read:ok · 已读取 game/notes.txt')).not.toBeNull();
|
||||
expect(screen.getByText('最近动作')).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText('file.read · ok · 已读取 game/notes.txt · 读取笔记'),
|
||||
screen.getByText(
|
||||
'file.read · ok · 已读取 game/notes.txt · 目标:path=game/notes.txt · 读取笔记',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.getByText('最近事件')).not.toBeNull();
|
||||
expect(
|
||||
@@ -1780,6 +1784,188 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
).not.toBeNull();
|
||||
});
|
||||
|
||||
it('confirms the exact pending tool action from the developer agent window', async () => {
|
||||
const waitingRuntimeState = {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'design-director',
|
||||
taskId: 'design-director',
|
||||
sessionId: 'agent-session-design-director',
|
||||
runId: 'launcher-agent-waiting',
|
||||
source: 'agent-background-task',
|
||||
status: 'waiting-for-confirmation',
|
||||
phase: 'waiting-for-confirmation',
|
||||
currentTask: '读取角色规范笔记',
|
||||
currentGoal: '确认角色规范依据',
|
||||
currentAction: '等待确认工具 file.read',
|
||||
waitingOn: '开发者确认 Agent 工具动作',
|
||||
nextStep: '确认或调整策略后继续 Agent 工具动作:file.read',
|
||||
loopIteration: 1,
|
||||
maxLoopIterations: 3,
|
||||
toolActionBudget: 3,
|
||||
plan: ['读取项目笔记'],
|
||||
observations: ['file.read:waiting-for-confirmation'],
|
||||
recentToolCalls: [
|
||||
{
|
||||
tool: 'file.read',
|
||||
status: 'waiting-for-confirmation',
|
||||
actionFingerprint: 'b'.repeat(64),
|
||||
inputSummary: 'path=game/notes.txt',
|
||||
reason: '读取角色规范依据',
|
||||
summary: '项目权限策略要求用户确认:file.read',
|
||||
detail: null,
|
||||
updatedAt: 5000,
|
||||
},
|
||||
],
|
||||
taskQueue: {
|
||||
total: 1,
|
||||
pending: 0,
|
||||
running: 0,
|
||||
waitingForConfirmation: 1,
|
||||
cancelled: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
latestRunId: 'launcher-agent-waiting',
|
||||
updatedAt: 5000,
|
||||
},
|
||||
allowedTools: ['file.read'],
|
||||
lastResponse: null,
|
||||
error: null,
|
||||
updatedAt: 5000,
|
||||
};
|
||||
const waitingTask = {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
agentId: 'design-director',
|
||||
taskId: 'design-director',
|
||||
sessionId: 'agent-session-design-director',
|
||||
runId: 'launcher-agent-waiting',
|
||||
source: 'agent-background-task',
|
||||
task: '读取角色规范笔记',
|
||||
status: 'waiting-for-confirmation',
|
||||
phase: 'waiting-for-confirmation',
|
||||
currentAction: '等待确认工具 file.read',
|
||||
error: null,
|
||||
updatedAt: 5000,
|
||||
};
|
||||
const runtimeResult = {
|
||||
state: waitingRuntimeState,
|
||||
sessionPath:
|
||||
'/tmp/authorized-game/.agent/runtime/agents/design-director.json',
|
||||
eventPath:
|
||||
'/tmp/authorized-game/.agent/runtime/events/design-director.jsonl',
|
||||
taskPath:
|
||||
'/tmp/authorized-game/.agent/runtime/tasks/design-director.jsonl',
|
||||
taskQueue: waitingRuntimeState.taskQueue,
|
||||
recentEvents: [],
|
||||
recentTasks: [waitingTask],
|
||||
};
|
||||
const invoke = vi.fn(
|
||||
async (command: string, args?: Record<string, unknown>) => {
|
||||
if (command === 'check_game_creator_llm_config') {
|
||||
return {
|
||||
configured: true,
|
||||
apiKeyPresent: true,
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-5.5',
|
||||
apiKind: 'openai_chat',
|
||||
stream: true,
|
||||
error: null,
|
||||
agents: [
|
||||
{
|
||||
agentId: 'design-director',
|
||||
label: '拆解创作方向',
|
||||
configured: true,
|
||||
apiKeyPresent: true,
|
||||
baseUrl: 'https://llm.example.test/v1',
|
||||
model: 'gpt-5.5',
|
||||
apiKind: 'openai_chat',
|
||||
stream: true,
|
||||
error: null,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
if (command === 'read_local_conversation') {
|
||||
return {
|
||||
path: '/tmp/authorized-game/.agent/conversations/agents/design-director.jsonl',
|
||||
agentId: args?.agentId,
|
||||
messages: [],
|
||||
};
|
||||
}
|
||||
if (command === 'read_game_creator_agent_runtime') {
|
||||
return runtimeResult;
|
||||
}
|
||||
if (command === 'confirm_game_creator_agent_runtime_task') {
|
||||
const runId = String(args?.nextRunId ?? 'launcher-agent-confirmed');
|
||||
const taskQueue = {
|
||||
...waitingRuntimeState.taskQueue,
|
||||
running: 1,
|
||||
waitingForConfirmation: 0,
|
||||
latestRunId: runId,
|
||||
};
|
||||
return {
|
||||
...runtimeResult,
|
||||
state: {
|
||||
...waitingRuntimeState,
|
||||
runId,
|
||||
status: 'running',
|
||||
phase: 'planning',
|
||||
currentAction: '生成 Agent 工具计划(第 1 轮)',
|
||||
waitingOn: 'Agent 输出计划或回复',
|
||||
taskQueue,
|
||||
},
|
||||
taskQueue,
|
||||
recentTasks: [
|
||||
{
|
||||
...waitingTask,
|
||||
runId,
|
||||
status: 'running',
|
||||
phase: 'planning',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
throw new Error(`unexpected invoke ${command}`);
|
||||
},
|
||||
);
|
||||
window.__TAURI__ = { core: { invoke } };
|
||||
renderLauncherAgentChatAt('/?agent-chat');
|
||||
|
||||
fireEvent.change(screen.getByLabelText('Agent 聊天项目目录'), {
|
||||
target: { value: '/tmp/authorized-game' },
|
||||
});
|
||||
fireEvent.click(screen.getByRole('button', { name: '读取历史' }));
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'waiting-for-confirmation / waiting-for-confirmation',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'file.read · waiting-for-confirmation · 项目权限策略要求用户确认:file.read · 目标:path=game/notes.txt · 读取角色规范依据',
|
||||
),
|
||||
).not.toBeNull();
|
||||
const runtimeActions = screen.getByLabelText('Agent Runtime 操作');
|
||||
const confirmButton = within(runtimeActions).getByRole('button', {
|
||||
name: '确认继续',
|
||||
}) as HTMLButtonElement;
|
||||
expect(confirmButton.disabled).toBe(false);
|
||||
fireEvent.click(confirmButton);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(invoke).toHaveBeenCalledWith(
|
||||
'confirm_game_creator_agent_runtime_task',
|
||||
expect.objectContaining({
|
||||
projectPath: '/tmp/authorized-game',
|
||||
agentId: 'design-director',
|
||||
runId: 'launcher-agent-waiting',
|
||||
note: '开发者已确认待执行工具动作',
|
||||
}),
|
||||
);
|
||||
});
|
||||
expect(await screen.findByText('running / planning')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('shows queued developer agent background tasks when the agent is already running', async () => {
|
||||
const runningRuntimeState = {
|
||||
schemaVersion: 'game-creator-agent-runtime.v1',
|
||||
@@ -18566,7 +18752,7 @@ describe('AI 游戏创作 App 界面边界', () => {
|
||||
|
||||
expect(
|
||||
await screen.findByText(
|
||||
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
'当前仅支持确认 project.index、project.status、project.checkpoint、project.diff、project.restore、project.export_package、project.export_list、file.list、file.read、memory.read、memory.write、memory.delete、asset.register、asset.list、task.list、task.create、task.update、agent.run_status、agent.kill、agent.retry、agent.resume、agent.delegate、agent.schedule_ready、agent.audit、agent.trace_read、preview.status、preview.start、preview.open、preview.stop、canvas.project_sync、canvas.asset_import、canvas.asset_generate、canvas.export_import、conversation.read 和 conversation.write。',
|
||||
),
|
||||
).not.toBeNull();
|
||||
expect(screen.queryByText('project.policy_write')).toBeNull();
|
||||
|
||||
@@ -4070,10 +4070,10 @@
|
||||
- 2026-07-10 调整:Agent Runtime 后台任务的 `runId` 是同一 Agent 任务历史的身份,不允许复用覆盖。`start_game_creator_agent_runtime_task`、`agent.delegate` 和 retry 进入后台队列前会读取该 Agent 全量 task JSONL 历史;若调用方传入的规范化 runId 已存在,Runtime 自动追加 `-dup-<timestamp>-<attempt>` 生成实际 runId。任务队列、delegate observation 和 `agent.db` 审计都必须使用实际 runId,避免 `latest_game_creator_agent_runtime_tasks` 按 runId 去重时折叠掉不同任务。
|
||||
- 2026-07-10 调整:Agent Runtime 的 `memory.write scope=agent` 只能写当前 Agent 自己的私有记忆。若 action 指定其他 `agentId / targetAgentId`,Runtime 返回 `blocked` observation,不写目标 Agent 私有记忆、不写 `agent.runtime.memory.write` 审计;跨 Agent 共享稳定结论必须走 `blackboard.write`,给单个 Agent 留上下文必须走 `agent.message`。
|
||||
- 2026-07-10 调整:Agent Runtime 和本地对话使用 append-only JSONL 作为事实源时,进程内必须按目标文件路径串行追加整行。`.agent/agent.db`、`.agent/conversations/**/*.jsonl`、`.agent/runtime/events/*.jsonl`、`.agent/runtime/tasks/*.jsonl`、`.agent/activity.jsonl` 和 `.agent/output.jsonl` 统一走共享追加 helper,避免多个后台 Agent 并行完成时 JSON record 与换行交错。
|
||||
- 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations/<agentId>/<runId>/<commandId>.json` 票据并重新入队;工具权限 gate 在 deny 之后、confirm 阶段消费该票据,只放行对应 Agent/run/command 一次。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`。
|
||||
- 2026-07-10 调整:Agent Runtime 待确认工具动作支持确认后继续。开发者确认 `waiting-for-confirmation` run 时,Runtime 为新 run 写入一次性 `.agent/runtime/confirmations/<agentId>/<runId>/<commandId>.json` 票据并重新入队;票据同时保存工具名与输入 JSON 的 SHA-256 `actionFingerprint`,权限 gate 在 deny 之后、confirm 阶段只放行对应 Agent/run/command/fingerprint 一次,模型若把同一工具改成其他路径、checkpoint 或目标 Agent,旧票据立即失效并重新进入待确认。`recentToolCalls` 和确认审计只额外保存安全 `inputSummary`,例如相对路径、checkpoint id、目标 Agent 或内容字符数,不保存待写正文、消息正文、素材 prompt 或 API Key。开发窗口必须把 `onConfirmRuntimeTask` 接入真实“确认继续”按钮;该显式确认命令允许 `agent.resume` 处于 confirm,只继续服从 deny,自动重启恢复仍要求 `agent.resume` 为 auto。原 waiting run 追加 `completed/confirmed` 任务记录,避免队列长期显示等待确认;审计记录写 `agent.runtime.tool_confirmation.approved`。
|
||||
- 2026-07-10 调整:Agent Runtime 工具箱新增 `task.create`,用于让 Agent 把目标拆成新的 manifest 任务,而不只能更新 seed task。该工具默认 `confirm` 权限,写入前要求 taskId 唯一、依赖指向已有任务、列表长度受限,并写 `agent.runtime.task.create` 审计;策略要求确认或拒绝时不修改 `.agent/manifest.json`。
|
||||
- 2026-07-10 调整:Agent Runtime 新增 `agent.schedule_ready` 调度入口,默认 `confirm` 权限。命令会扫描 `.agent/manifest.json` 中依赖已完成且仍为 `pending` 的 ready task,先把任务标成 `running`,再用 taskId 作为 Agent id 投递到既有后台队列,source 记为 `agent-ready-task-scheduler`,并写 `agent.runtime.ready_task.scheduled` 审计;后续执行仍走原 per-agent 锁、任务 JSONL、LLM loop、工具策略和事件流,不新增独立 worker。默认确认策略下该命令不会静默调度。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、reason、summary、detail 和 updatedAt。状态面板展示最近动作时使用该字段,不解析 observation 文本;写入前继续过滤敏感上下文,不保存原始密钥或任意未过滤输入。
|
||||
- 2026-07-10 调整:Agent Runtime state 新增 `recentToolCalls`,后台 loop 每次执行白名单工具后记录最近 20 条结构化动作,包含 tool、status、actionFingerprint、inputSummary、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 字符串。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user