支持Agent工具确认后继续
新增一次性工具确认票据并在Runtime权限gate中按run消费。 提供确认待确认后台任务的Tauri命令和开发面板按钮。 确认后把原waiting run标记为confirmed并投递新run继续执行。 补充确认续跑测试并同步Agent Runtime文档。
This commit is contained in:
@@ -375,6 +375,19 @@ fn start_game_creator_agent_background_task_with_run_id_at(
|
||||
agent_id: &str,
|
||||
task: &str,
|
||||
run_id: &str,
|
||||
) -> Result<(AgentRuntimeResult, String), String> {
|
||||
start_game_creator_agent_background_task_with_confirmed_tool_at(
|
||||
root, agent_id, task, run_id, None, "",
|
||||
)
|
||||
}
|
||||
|
||||
fn start_game_creator_agent_background_task_with_confirmed_tool_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
task: &str,
|
||||
run_id: &str,
|
||||
confirmed_command_id: Option<&str>,
|
||||
confirmation_note: &str,
|
||||
) -> Result<(AgentRuntimeResult, String), String> {
|
||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||
validate_project_root(root)?;
|
||||
@@ -408,6 +421,15 @@ fn start_game_creator_agent_background_task_with_run_id_at(
|
||||
"task": pending_task.task,
|
||||
}),
|
||||
)?;
|
||||
if let Some(command_id) = confirmed_command_id {
|
||||
write_game_creator_agent_runtime_tool_confirmation(
|
||||
root,
|
||||
&agent_id,
|
||||
&run_id,
|
||||
command_id,
|
||||
confirmation_note,
|
||||
)?;
|
||||
}
|
||||
let Some(runtime_lock) = try_acquire_game_creator_agent_runtime_task_lock(root, &agent_id)?
|
||||
else {
|
||||
let result = read_game_creator_agent_runtime_at(root, &agent_id)?;
|
||||
@@ -592,6 +614,102 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
pub(crate) fn confirm_game_creator_agent_runtime_task_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
next_run_id: &str,
|
||||
note: &str,
|
||||
) -> Result<AgentRuntimeResult, String> {
|
||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||
validate_project_root(root)?;
|
||||
if run_id.trim().is_empty() {
|
||||
return Err("Agent Runtime runId 不能为空".to_string());
|
||||
}
|
||||
let target_run_id = normalize_game_creator_agent_runtime_run_id(&agent_id, run_id);
|
||||
let task =
|
||||
read_latest_game_creator_agent_runtime_task_by_run_id(root, &agent_id, &target_run_id)?
|
||||
.ok_or_else(|| format!("未找到 Agent Runtime 任务:{target_run_id}"))?;
|
||||
if task.status != "waiting-for-confirmation" {
|
||||
return Err(format!(
|
||||
"Agent Runtime 任务不在待确认状态,不能确认继续:{target_run_id}"
|
||||
));
|
||||
}
|
||||
let runtime = read_game_creator_agent_runtime_at(root, &agent_id)?.state;
|
||||
if runtime.run_id != target_run_id || runtime.status != "waiting-for-confirmation" {
|
||||
return Err(format!(
|
||||
"Agent Runtime 当前状态不是该待确认 run:{target_run_id}"
|
||||
));
|
||||
}
|
||||
let tool_call = runtime
|
||||
.recent_tool_calls
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|call| call.status == "waiting-for-confirmation")
|
||||
.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 note = sanitize_agent_runtime_text(note, 240);
|
||||
let confirmed_task = if note.trim().is_empty() {
|
||||
format!(
|
||||
"继续已确认的后台任务:{}\n\n开发者已确认工具动作:{}。",
|
||||
task.task, tool_call.tool
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"继续已确认的后台任务:{}\n\n开发者已确认工具动作:{}。确认说明:{}",
|
||||
task.task, tool_call.tool, note
|
||||
)
|
||||
};
|
||||
let requested_run_id = if next_run_id.trim().is_empty() {
|
||||
format!("{target_run_id}-confirm-{}", unix_timestamp())
|
||||
} else {
|
||||
next_run_id.trim().to_string()
|
||||
};
|
||||
let (result, confirmed_run_id) =
|
||||
start_game_creator_agent_background_task_with_confirmed_tool_at(
|
||||
root,
|
||||
&agent_id,
|
||||
&confirmed_task,
|
||||
&requested_run_id,
|
||||
Some(command_id),
|
||||
¬e,
|
||||
)?;
|
||||
append_game_creator_agent_runtime_task_record(
|
||||
root,
|
||||
&AgentRuntimeTaskRecord {
|
||||
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
|
||||
agent_id: task.agent_id.clone(),
|
||||
task_id: task.task_id.clone(),
|
||||
session_id: task.session_id.clone(),
|
||||
run_id: task.run_id.clone(),
|
||||
source: task.source.clone(),
|
||||
task: task.task.clone(),
|
||||
status: "completed".to_string(),
|
||||
phase: "confirmed".to_string(),
|
||||
current_action: format!("已确认 {},继续 run {}", tool_call.tool, confirmed_run_id),
|
||||
error: None,
|
||||
updated_at: unix_timestamp(),
|
||||
},
|
||||
)?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.tool_confirmation.approved",
|
||||
"agentId": task.agent_id,
|
||||
"taskId": task.task_id,
|
||||
"sessionId": task.session_id,
|
||||
"runId": task.run_id,
|
||||
"confirmedRunId": confirmed_run_id,
|
||||
"tool": tool_call.tool,
|
||||
"commandId": command_id,
|
||||
"note": note,
|
||||
}),
|
||||
)?;
|
||||
emit_game_creator_agent_runtime_update(root, &agent_id);
|
||||
read_game_creator_agent_runtime_at(root, &agent_id).or(Ok(result))
|
||||
}
|
||||
|
||||
fn game_creator_agent_background_task_default_plan() -> Vec<String> {
|
||||
vec![
|
||||
"记录开发者投递的后台任务".to_string(),
|
||||
@@ -845,9 +963,14 @@ async fn run_game_creator_agent_background_task(
|
||||
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
let observation =
|
||||
execute_game_creator_agent_runtime_tool_action(&root, &agent_id, &task, action)
|
||||
.await;
|
||||
let observation = execute_game_creator_agent_runtime_tool_action(
|
||||
&root,
|
||||
&agent_id,
|
||||
runtime.run_id.as_str(),
|
||||
&task,
|
||||
action,
|
||||
)
|
||||
.await;
|
||||
if stop_game_creator_agent_runtime_if_cancel_requested(&root, &mut runtime) {
|
||||
return AgentBackgroundTaskOutcome::Finished;
|
||||
}
|
||||
@@ -1445,6 +1568,7 @@ fn parse_game_creator_agent_tool_plan_response(
|
||||
pub(crate) async fn execute_game_creator_agent_runtime_tool_action(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
task: &str,
|
||||
action: &AgentRuntimeToolAction,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
@@ -1452,7 +1576,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_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, command_id)
|
||||
game_creator_agent_runtime_tool_policy_block(root, agent_id, run_id, command_id)
|
||||
{
|
||||
let (status, summary) = match blocked {
|
||||
AgentRuntimeToolPolicyBlock::Denied(summary) => ("blocked", summary),
|
||||
@@ -1533,6 +1657,101 @@ fn game_creator_agent_runtime_tool_command_id(tool: &str) -> Option<&'static str
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_confirmation_path_component(value: &str, fallback: &str) -> String {
|
||||
let normalized = value
|
||||
.trim()
|
||||
.chars()
|
||||
.map(|character| {
|
||||
if character.is_ascii_alphanumeric()
|
||||
|| character == '-'
|
||||
|| character == '_'
|
||||
|| character == '.'
|
||||
{
|
||||
character
|
||||
} else {
|
||||
'-'
|
||||
}
|
||||
})
|
||||
.collect::<String>();
|
||||
let normalized = normalized.trim_matches('-');
|
||||
if normalized.is_empty() {
|
||||
fallback.to_string()
|
||||
} else {
|
||||
truncate_agent_runtime_text(normalized, 160)
|
||||
}
|
||||
}
|
||||
|
||||
fn game_creator_agent_runtime_tool_confirmation_path(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &str,
|
||||
) -> PathBuf {
|
||||
root.join(".agent/runtime/confirmations")
|
||||
.join(agent_runtime_confirmation_path_component(agent_id, "agent"))
|
||||
.join(agent_runtime_confirmation_path_component(run_id, "run"))
|
||||
.join(format!(
|
||||
"{}.json",
|
||||
agent_runtime_confirmation_path_component(command_id, "command")
|
||||
))
|
||||
}
|
||||
|
||||
fn write_game_creator_agent_runtime_tool_confirmation(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &str,
|
||||
note: &str,
|
||||
) -> Result<(), String> {
|
||||
let path =
|
||||
game_creator_agent_runtime_tool_confirmation_path(root, agent_id, run_id, command_id);
|
||||
if let Some(parent) = path.parent() {
|
||||
fs::create_dir_all(parent).map_err(|error| {
|
||||
format!(
|
||||
"创建 Agent Runtime 工具确认目录失败:{}: {error}",
|
||||
parent.display()
|
||||
)
|
||||
})?;
|
||||
}
|
||||
let payload = serde_json::json!({
|
||||
"schemaVersion": AGENT_RUNTIME_SCHEMA_VERSION,
|
||||
"agentId": agent_id,
|
||||
"runId": run_id,
|
||||
"commandId": command_id,
|
||||
"note": sanitize_agent_runtime_text(note, 240),
|
||||
"updatedAt": unix_timestamp(),
|
||||
});
|
||||
let content = serde_json::to_string_pretty(&payload)
|
||||
.map_err(|error| format!("序列化 Agent Runtime 工具确认失败:{error}"))?;
|
||||
fs::write(&path, content).map_err(|error| {
|
||||
format!(
|
||||
"写入 Agent Runtime 工具确认失败:{}: {error}",
|
||||
path.display()
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn consume_game_creator_agent_runtime_tool_confirmation(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &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!(
|
||||
"消费 Agent Runtime 工具确认失败:{}: {error}",
|
||||
path.display()
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn agent_runtime_executable_tools() -> Vec<&'static str> {
|
||||
vec![
|
||||
"memory.read",
|
||||
@@ -1637,6 +1856,7 @@ fn agent_runtime_effective_tool_policy_at(
|
||||
fn game_creator_agent_runtime_tool_policy_block(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
command_id: &str,
|
||||
) -> Option<AgentRuntimeToolPolicyBlock> {
|
||||
let view = match read_project_permission_policy_at(root) {
|
||||
@@ -1679,6 +1899,13 @@ fn game_creator_agent_runtime_tool_policy_block(
|
||||
.iter()
|
||||
.any(|command| command == command_id)
|
||||
{
|
||||
match consume_game_creator_agent_runtime_tool_confirmation(
|
||||
root, &agent_id, run_id, command_id,
|
||||
) {
|
||||
Ok(true) => return None,
|
||||
Ok(false) => {}
|
||||
Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)),
|
||||
}
|
||||
return Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(format!(
|
||||
"项目权限策略要求用户确认:{command_id}"
|
||||
)));
|
||||
@@ -1695,6 +1922,13 @@ 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,
|
||||
) {
|
||||
Ok(true) => return None,
|
||||
Ok(false) => {}
|
||||
Err(error) => return Some(AgentRuntimeToolPolicyBlock::Denied(error)),
|
||||
}
|
||||
return Some(AgentRuntimeToolPolicyBlock::RequiresConfirmation(format!(
|
||||
"Agent 权限策略要求用户确认:{agent_id} / {command_id}"
|
||||
)));
|
||||
|
||||
@@ -426,6 +426,28 @@ pub(crate) fn retry_game_creator_agent_runtime_task(
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn confirm_game_creator_agent_runtime_task(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
run_id: String,
|
||||
next_run_id: String,
|
||||
note: String,
|
||||
) -> Result<AgentRuntimeResult, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
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")?;
|
||||
confirm_game_creator_agent_runtime_task_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
run_id.trim(),
|
||||
next_run_id.trim(),
|
||||
note.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_game_creator_agent_runtime(
|
||||
project_path: String,
|
||||
|
||||
@@ -1202,6 +1202,7 @@ fn main() {
|
||||
start_game_creator_agent_runtime_task,
|
||||
cancel_game_creator_agent_runtime_task,
|
||||
retry_game_creator_agent_runtime_task,
|
||||
confirm_game_creator_agent_runtime_task,
|
||||
read_game_creator_agent_runtime,
|
||||
read_game_creator_agent_runtimes,
|
||||
resume_game_creator_agent_runtime_tasks,
|
||||
|
||||
@@ -1640,6 +1640,7 @@ async fn background_agent_runtime_tool_action_respects_agent_policy() {
|
||||
let observation = execute_game_creator_agent_runtime_tool_action(
|
||||
&root,
|
||||
"design-director",
|
||||
"direct-policy-test-run",
|
||||
"读取设计笔记",
|
||||
&AgentRuntimeToolAction {
|
||||
tool: "file.read".to_string(),
|
||||
@@ -4360,6 +4361,117 @@ async fn background_agent_runtime_tool_action_respects_confirm_policy() {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_confirm_and_continue_waiting_tool_action() {
|
||||
let root = unique_project_path();
|
||||
init_local_game_project_at(&root, "project-1", "月光厨房").expect("project init");
|
||||
fs::write(root.join("game/notes.txt"), "核心循环笔记").expect("write notes");
|
||||
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 read_plan = serde_json::json!({
|
||||
"thinkingSummary": "需要读项目笔记",
|
||||
"plan": ["读取项目笔记", "回复开发者"],
|
||||
"actions": [
|
||||
{
|
||||
"tool": "file.read",
|
||||
"reason": "确认项目笔记",
|
||||
"input": { "path": "game/notes.txt" }
|
||||
}
|
||||
],
|
||||
"response": ""
|
||||
})
|
||||
.to_string();
|
||||
let final_plan = serde_json::json!({
|
||||
"thinkingSummary": "已经拿到项目笔记",
|
||||
"plan": [],
|
||||
"actions": [],
|
||||
"response": "确认后已读取笔记:核心循环笔记。"
|
||||
})
|
||||
.to_string();
|
||||
let base_url = spawn_mock_llm_server_responses_with_capture(
|
||||
vec![read_plan.clone(), read_plan, final_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-confirm-run",
|
||||
)
|
||||
.expect("start background task");
|
||||
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.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 confirmed = confirm_game_creator_agent_runtime_task_at(
|
||||
&root,
|
||||
"design-director",
|
||||
"design-confirm-run",
|
||||
"design-confirm-run-approved",
|
||||
"允许读取项目笔记",
|
||||
)
|
||||
.expect("confirm waiting task");
|
||||
assert_eq!(confirmed.state.run_id, "design-confirm-run-approved");
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("confirmed run plan request");
|
||||
receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("confirmed run final plan request");
|
||||
|
||||
let runtime = wait_for_agent_runtime_idle(&root, "design-director");
|
||||
assert_eq!(runtime.status, "idle");
|
||||
assert_eq!(
|
||||
runtime.last_response.as_deref(),
|
||||
Some("确认后已读取笔记:核心循环笔记。")
|
||||
);
|
||||
assert_eq!(runtime.task_queue.waiting_for_confirmation, 0);
|
||||
assert!(runtime
|
||||
.observations
|
||||
.iter()
|
||||
.any(|item| item.contains("file.read:ok")));
|
||||
let runtime_result =
|
||||
read_game_creator_agent_runtime_at(&root, "design-director").expect("runtime result");
|
||||
assert!(runtime_result.recent_tasks.iter().any(|task| {
|
||||
task.run_id == "design-confirm-run"
|
||||
&& task.status == "completed"
|
||||
&& task.phase == "confirmed"
|
||||
}));
|
||||
assert!(runtime_result.recent_tasks.iter().any(|task| {
|
||||
task.run_id == "design-confirm-run-approved" && task.status == "completed"
|
||||
}));
|
||||
let agent_db = fs::read_to_string(root.join(".agent/agent.db")).expect("agent db");
|
||||
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\""));
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn background_agent_runtime_can_list_project_files() {
|
||||
let root = unique_project_path();
|
||||
|
||||
@@ -772,18 +772,24 @@ function agentRuntimeCanRetry(status: string) {
|
||||
return ['cancelled', 'failed', 'completed', 'idle'].includes(status);
|
||||
}
|
||||
|
||||
function agentRuntimeCanConfirm(status: string) {
|
||||
return status === 'waiting-for-confirmation';
|
||||
}
|
||||
|
||||
function AgentRuntimeStatusPanel({
|
||||
runtime,
|
||||
error,
|
||||
controlBusy = false,
|
||||
onCancelRuntimeTask,
|
||||
onRetryRuntimeTask,
|
||||
onConfirmRuntimeTask,
|
||||
}: {
|
||||
runtime: AgentRuntimeState | null;
|
||||
error?: string | null;
|
||||
controlBusy?: boolean;
|
||||
onCancelRuntimeTask?: (runId: string) => void;
|
||||
onRetryRuntimeTask?: (runId: string) => void;
|
||||
onConfirmRuntimeTask?: (runId: string) => void;
|
||||
}) {
|
||||
if (!runtime && error) {
|
||||
return (
|
||||
@@ -818,14 +824,27 @@ function AgentRuntimeStatusPanel({
|
||||
Boolean(runtime.runId) &&
|
||||
agentRuntimeCanRetry(runtime.status) &&
|
||||
Boolean(onRetryRuntimeTask);
|
||||
const canConfirm =
|
||||
Boolean(runtime.runId) &&
|
||||
agentRuntimeCanConfirm(runtime.status) &&
|
||||
Boolean(onConfirmRuntimeTask);
|
||||
return (
|
||||
<section className="agent-runtime-status" aria-label="Agent Runtime 状态">
|
||||
<header>
|
||||
<strong>{`${runtime.status} / ${runtime.phase}`}</strong>
|
||||
<small>{runtime.sessionId}</small>
|
||||
</header>
|
||||
{onCancelRuntimeTask || onRetryRuntimeTask ? (
|
||||
{onCancelRuntimeTask || onRetryRuntimeTask || onConfirmRuntimeTask ? (
|
||||
<div className="agent-runtime-actions" aria-label="Agent Runtime 操作">
|
||||
<button
|
||||
type="button"
|
||||
disabled={controlBusy || !canConfirm}
|
||||
onClick={() =>
|
||||
runtime.runId && onConfirmRuntimeTask?.(runtime.runId)
|
||||
}
|
||||
>
|
||||
确认继续
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
disabled={controlBusy || !canCancel}
|
||||
@@ -13861,6 +13880,79 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function confirmSelectedAgentRuntimeTask(
|
||||
agent: AgentStatusCard,
|
||||
runId: string,
|
||||
) {
|
||||
if (!agent || !runId || agentConversationBackgroundBusyRef.current) {
|
||||
return;
|
||||
}
|
||||
const invoke = resolveTauriInvoke();
|
||||
const nextProjectPath = resolveChatProjectPath(localProject);
|
||||
if (!invoke) {
|
||||
setAgentConversationStatus('需要在 Tauri App 内运行');
|
||||
return;
|
||||
}
|
||||
if (!nextProjectPath) {
|
||||
setAgentConversationStatus('请先初始化本地项目');
|
||||
return;
|
||||
}
|
||||
const llmWarning = formatAgentLlmConfigWarning(llmConfigStatus, agent);
|
||||
if (llmWarning) {
|
||||
setAgentConversationStatus(llmWarning);
|
||||
return;
|
||||
}
|
||||
const saveVersion = agentConversationLoadVersionRef.current;
|
||||
agentConversationBackgroundBusyRef.current = true;
|
||||
setAgentConversationBackgroundBusy(true);
|
||||
setAgentConversationStatus('正在确认并继续 Agent 后台任务');
|
||||
try {
|
||||
const runtime = await invoke<AgentRuntimeResult>(
|
||||
'confirm_game_creator_agent_runtime_task',
|
||||
{
|
||||
projectPath: nextProjectPath,
|
||||
agentId: agent.id,
|
||||
runId,
|
||||
nextRunId: createAgentChatRunId('agent-background-confirm'),
|
||||
note: '开发者已确认待执行工具动作',
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
const nextRuntime = agentRuntimeStateFromResult(runtime);
|
||||
setAgentConversationRuntime(nextRuntime);
|
||||
rememberAgentRuntimeState(nextRuntime);
|
||||
setAgentConversationRuntimeError('');
|
||||
const conversation = await invoke<LocalConversationResult>(
|
||||
'read_local_conversation',
|
||||
{
|
||||
projectPath: nextProjectPath,
|
||||
agentId: agent.id,
|
||||
},
|
||||
);
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentConversationMessages(conversation.messages);
|
||||
setAgentConversationStatus(agentRuntimeStartStatus(runtime));
|
||||
setCommandLog((current) => [
|
||||
...current,
|
||||
'agent.runtime.tool_confirmation.approved',
|
||||
]);
|
||||
} catch (error) {
|
||||
if (agentConversationLoadVersionRef.current !== saveVersion) {
|
||||
return;
|
||||
}
|
||||
setAgentConversationStatus(
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
} finally {
|
||||
agentConversationBackgroundBusyRef.current = false;
|
||||
setAgentConversationBackgroundBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveSelectedAgentPrivateMemory(
|
||||
agent: AgentStatusCard,
|
||||
content: string,
|
||||
@@ -21263,6 +21355,11 @@ export function App() {
|
||||
? void retrySelectedAgentRuntimeTask(selectedAgent, runId)
|
||||
: undefined
|
||||
}
|
||||
onConfirmRuntimeTask={(runId) =>
|
||||
selectedAgent
|
||||
? void confirmSelectedAgentRuntimeTask(selectedAgent, runId)
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div
|
||||
className="agent-conversation-list"
|
||||
|
||||
@@ -4066,6 +4066,7 @@
|
||||
- 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 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。
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user