补齐单Agent持久目标模式
新增Agent会话级Goal的创建编辑暂停恢复清理与CLI/Tauri入口 升级Runtime context v4、pending action v5和finalization v3的Goal绑定 修复恢复半提交、缺失Goal投影及暂停过渡态的失败关闭 补齐开发窗口目标模式、故障注入测试和项目文档
This commit is contained in:
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -435,6 +435,134 @@ pub(crate) fn start_game_creator_agent_runtime_task(
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn read_game_creator_agent_goal(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
) -> Result<Option<AgentGoalRecord>, String> {
|
||||
let root = Path::new(project_path.trim());
|
||||
enforce_project_permission_policy(root, "conversation.read")?;
|
||||
enforce_project_permission_policy(root, "agent.run_status")?;
|
||||
read_game_creator_agent_goal_at(root, agent_id.trim(), session_id.trim())
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn start_game_creator_agent_goal(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: Option<String>,
|
||||
outcome: String,
|
||||
constraints: Vec<String>,
|
||||
verification: Vec<String>,
|
||||
run_id: String,
|
||||
) -> Result<AgentGoalMutationResult, 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")?;
|
||||
start_game_creator_agent_goal_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.as_deref(),
|
||||
outcome.trim(),
|
||||
constraints,
|
||||
verification,
|
||||
run_id.trim(),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn edit_game_creator_agent_goal(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
goal_id: String,
|
||||
expected_revision: u64,
|
||||
outcome: String,
|
||||
constraints: Vec<String>,
|
||||
verification: Vec<String>,
|
||||
) -> Result<AgentGoalMutationResult, 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")?;
|
||||
edit_game_creator_agent_goal_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.trim(),
|
||||
goal_id.trim(),
|
||||
expected_revision,
|
||||
outcome.trim(),
|
||||
constraints,
|
||||
verification,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn pause_game_creator_agent_goal(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
goal_id: String,
|
||||
expected_revision: u64,
|
||||
) -> Result<AgentGoalMutationResult, 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")?;
|
||||
pause_game_creator_agent_goal_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.trim(),
|
||||
goal_id.trim(),
|
||||
expected_revision,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn resume_game_creator_agent_goal(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
goal_id: String,
|
||||
expected_revision: u64,
|
||||
) -> Result<AgentGoalMutationResult, 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_permission_policy(root, "agent.resume")?;
|
||||
resume_game_creator_agent_goal_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.trim(),
|
||||
goal_id.trim(),
|
||||
expected_revision,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn clear_game_creator_agent_goal(
|
||||
project_path: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
goal_id: String,
|
||||
expected_revision: u64,
|
||||
) -> Result<AgentGoalMutationResult, 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")?;
|
||||
clear_game_creator_agent_goal_at(
|
||||
root,
|
||||
agent_id.trim(),
|
||||
session_id.trim(),
|
||||
goal_id.trim(),
|
||||
expected_revision,
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub(crate) fn steer_game_creator_agent_runtime_task(
|
||||
project_path: String,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -54,6 +54,7 @@ mod config;
|
||||
mod debug;
|
||||
mod delegation;
|
||||
mod git_inspect;
|
||||
mod goal;
|
||||
mod image_inspect;
|
||||
mod isolated_agent;
|
||||
mod patchset;
|
||||
@@ -77,6 +78,7 @@ use commands::*;
|
||||
use config::*;
|
||||
use delegation::*;
|
||||
use git_inspect::*;
|
||||
use goal::*;
|
||||
use image_inspect::*;
|
||||
use isolated_agent::*;
|
||||
use patchset::*;
|
||||
@@ -190,6 +192,18 @@ struct AgentRuntimeState {
|
||||
#[serde(default)]
|
||||
current_goal: String,
|
||||
#[serde(default)]
|
||||
goal_id: Option<String>,
|
||||
#[serde(default)]
|
||||
goal_revision: u64,
|
||||
#[serde(default)]
|
||||
goal_status: Option<String>,
|
||||
#[serde(default)]
|
||||
goal_outcome: Option<String>,
|
||||
#[serde(default)]
|
||||
goal_constraints: Vec<String>,
|
||||
#[serde(default)]
|
||||
goal_verification: Vec<String>,
|
||||
#[serde(default)]
|
||||
current_action: String,
|
||||
#[serde(default)]
|
||||
waiting_on: String,
|
||||
@@ -346,6 +360,8 @@ struct AgentRuntimeTaskQueueSummary {
|
||||
#[serde(default)]
|
||||
waiting_for_confirmation: u32,
|
||||
#[serde(default)]
|
||||
paused: u32,
|
||||
#[serde(default)]
|
||||
cancelled: u32,
|
||||
#[serde(default)]
|
||||
completed: u32,
|
||||
@@ -364,6 +380,7 @@ impl Default for AgentRuntimeTaskQueueSummary {
|
||||
pending: 0,
|
||||
running: 0,
|
||||
waiting_for_confirmation: 0,
|
||||
paused: 0,
|
||||
cancelled: 0,
|
||||
completed: 0,
|
||||
failed: 0,
|
||||
@@ -426,6 +443,12 @@ struct AgentRuntimeTaskRecord {
|
||||
#[serde(default)]
|
||||
delegation_id: Option<String>,
|
||||
#[serde(default)]
|
||||
goal_id: Option<String>,
|
||||
#[serde(default)]
|
||||
goal_revision: u64,
|
||||
#[serde(default)]
|
||||
goal_status: Option<String>,
|
||||
#[serde(default)]
|
||||
task: String,
|
||||
#[serde(default)]
|
||||
status: String,
|
||||
@@ -441,6 +464,46 @@ struct AgentRuntimeTaskRecord {
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
struct AgentGoalRecord {
|
||||
schema_version: String,
|
||||
project_id: String,
|
||||
goal_id: String,
|
||||
agent_id: String,
|
||||
session_id: String,
|
||||
run_id: String,
|
||||
revision: u64,
|
||||
status: String,
|
||||
outcome: String,
|
||||
constraints: Vec<String>,
|
||||
verification: Vec<String>,
|
||||
#[serde(default)]
|
||||
completion_evidence: Vec<String>,
|
||||
#[serde(default)]
|
||||
response_fingerprint: Option<String>,
|
||||
created_at: u64,
|
||||
#[serde(default)]
|
||||
pause_requested_at: Option<u64>,
|
||||
#[serde(default)]
|
||||
paused_at: Option<u64>,
|
||||
#[serde(default)]
|
||||
completed_at: Option<u64>,
|
||||
#[serde(default)]
|
||||
cleared_at: Option<u64>,
|
||||
#[serde(default)]
|
||||
error: Option<String>,
|
||||
updated_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentGoalMutationResult {
|
||||
goal: AgentGoalRecord,
|
||||
runtime: AgentRuntimeResult,
|
||||
provider_interrupted: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct AgentRuntimeResult {
|
||||
@@ -1446,6 +1509,12 @@ fn main() {
|
||||
chat_with_game_creator_role_agent,
|
||||
chat_with_game_creator_role_agent_stream,
|
||||
start_game_creator_agent_runtime_task,
|
||||
read_game_creator_agent_goal,
|
||||
start_game_creator_agent_goal,
|
||||
edit_game_creator_agent_goal,
|
||||
pause_game_creator_agent_goal,
|
||||
resume_game_creator_agent_goal,
|
||||
clear_game_creator_agent_goal,
|
||||
steer_game_creator_agent_runtime_task,
|
||||
cancel_game_creator_agent_runtime_task,
|
||||
retry_game_creator_agent_runtime_task,
|
||||
|
||||
@@ -2556,7 +2556,7 @@ fn agent_runtime_task_is_terminal_for_session_mutation(record: &AgentRuntimeTask
|
||||
&& record.phase != "needs-reconciliation"
|
||||
}
|
||||
|
||||
fn ensure_agent_session_has_no_live_tasks(
|
||||
pub(crate) fn ensure_agent_session_has_no_live_tasks(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
session_id: &str,
|
||||
|
||||
@@ -12,7 +12,7 @@ use std::sync::{Arc, Mutex, MutexGuard, OnceLock};
|
||||
use std::thread;
|
||||
use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
|
||||
|
||||
pub(crate) const EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION: u32 = 2;
|
||||
pub(crate) const EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION: u32 = 3;
|
||||
|
||||
const EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME: &str = "agent-runner.endpoint.json";
|
||||
const EXTERNAL_AGENT_RUNNER_LOCK_FILE_NAME: &str = "agent-runner.lock";
|
||||
@@ -2288,7 +2288,12 @@ fn dispatch_external_agent_runner_runtime_request(
|
||||
|
||||
if matches!(
|
||||
request.method.as_str(),
|
||||
"runtime.wake_pending" | "runtime.resume" | "runtime.continue_action" | "runtime.steer"
|
||||
"runtime.wake_pending"
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
) && state.draining.load(Ordering::Acquire)
|
||||
{
|
||||
return ExternalAgentRunnerResponse::failure(
|
||||
@@ -2300,7 +2305,12 @@ fn dispatch_external_agent_runner_runtime_request(
|
||||
|
||||
let token = state.endpoint_snapshot().token;
|
||||
let response = match request.method.as_str() {
|
||||
"runtime.wake_pending" | "runtime.resume" | "runtime.continue_action" | "runtime.steer" => {
|
||||
"runtime.wake_pending"
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel" => {
|
||||
let root = match external_agent_runner_request_root(request) {
|
||||
Ok(root) => root,
|
||||
Err(error) => {
|
||||
@@ -2356,6 +2366,78 @@ fn dispatch_external_agent_runner_runtime_request(
|
||||
"providerInterrupted": provider_interrupted,
|
||||
}))
|
||||
})(),
|
||||
"runtime.pause" => (|| {
|
||||
let agent = external_agent_runner_request_agent(request)?;
|
||||
let run_id = external_agent_runner_request_run_id(request)?;
|
||||
let runtime = crate::read_game_creator_agent_runtime_at(&root, &agent)?;
|
||||
if runtime.state.run_id != run_id {
|
||||
return Err("runtime.pause 与当前 Agent runId 不匹配".to_string());
|
||||
}
|
||||
let goal = crate::read_game_creator_agent_goal_at(
|
||||
&root,
|
||||
&agent,
|
||||
&runtime.state.session_id,
|
||||
)?
|
||||
.ok_or_else(|| "runtime.pause 未找到当前 Session Goal".to_string())?;
|
||||
if goal.run_id != run_id
|
||||
|| goal.status != crate::AGENT_GOAL_STATUS_PAUSE_REQUESTED
|
||||
{
|
||||
return Err(
|
||||
"runtime.pause 缺少精确的 durable pause request".to_string()
|
||||
);
|
||||
}
|
||||
let provider_interrupted =
|
||||
crate::interrupt_game_creator_agent_runtime_provider_request_at(
|
||||
&root, &agent, &run_id,
|
||||
)?;
|
||||
let runtime =
|
||||
crate::pause_game_creator_agent_runtime_for_goal_at(&root, &goal)?;
|
||||
Ok(json!({
|
||||
"accepted": true,
|
||||
"providerInterrupted": provider_interrupted,
|
||||
"status": runtime.state.status,
|
||||
"phase": runtime.state.phase,
|
||||
}))
|
||||
})(),
|
||||
"runtime.cancel" => (|| {
|
||||
let agent = external_agent_runner_request_agent(request)?;
|
||||
let run_id = external_agent_runner_request_run_id(request)?;
|
||||
let runtime = crate::read_game_creator_agent_runtime_at(&root, &agent)?;
|
||||
if runtime.state.run_id != run_id {
|
||||
return Err("runtime.cancel 与当前 Agent runId 不匹配".to_string());
|
||||
}
|
||||
let goal = crate::read_game_creator_agent_goal_at(
|
||||
&root,
|
||||
&agent,
|
||||
&runtime.state.session_id,
|
||||
)?
|
||||
.ok_or_else(|| "runtime.cancel 未找到当前 Session Goal".to_string())?;
|
||||
if goal.run_id != run_id || goal.status != crate::AGENT_GOAL_STATUS_CLEARING
|
||||
{
|
||||
return Err(
|
||||
"runtime.cancel 缺少精确的 durable Goal clear request".to_string()
|
||||
);
|
||||
}
|
||||
crate::write_game_creator_agent_runtime_cancel_request(
|
||||
&root,
|
||||
&agent,
|
||||
&run_id,
|
||||
"开发者清理持久 Goal",
|
||||
)?;
|
||||
let provider_interrupted =
|
||||
crate::interrupt_game_creator_agent_runtime_provider_request_at(
|
||||
&root, &agent, &run_id,
|
||||
)?;
|
||||
let runtime = crate::cancel_game_creator_agent_runtime_task_at(
|
||||
&root, &agent, &run_id,
|
||||
)?;
|
||||
Ok(json!({
|
||||
"accepted": true,
|
||||
"providerInterrupted": provider_interrupted,
|
||||
"status": runtime.state.status,
|
||||
"phase": runtime.state.phase,
|
||||
}))
|
||||
})(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
match result {
|
||||
@@ -2503,6 +2585,8 @@ fn handle_external_agent_runner_request(
|
||||
| "runtime.resume"
|
||||
| "runtime.continue_action"
|
||||
| "runtime.steer"
|
||||
| "runtime.pause"
|
||||
| "runtime.cancel"
|
||||
| "runner.shutdown_if_idle"
|
||||
| "shutdown_if_idle" => dispatch_external_agent_runner_runtime_request(&request, state),
|
||||
_ => ExternalAgentRunnerResponse::failure(
|
||||
@@ -2514,10 +2598,10 @@ fn handle_external_agent_runner_request(
|
||||
}
|
||||
|
||||
fn external_agent_runner_runtime_state_is_idle(status: &str, phase: &str) -> bool {
|
||||
if matches!(phase, "completed" | "cancelled" | "failed") {
|
||||
if matches!(phase, "completed" | "cancelled" | "failed" | "paused") {
|
||||
return true;
|
||||
}
|
||||
matches!(status, "idle" | "failed" | "cancelled")
|
||||
matches!(status, "idle" | "failed" | "cancelled" | "paused")
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize)]
|
||||
@@ -3400,6 +3484,56 @@ pub(crate) fn steer_external_agent_runner(
|
||||
parse_external_agent_runner_steer_result(&result)
|
||||
}
|
||||
|
||||
pub(crate) fn pause_external_agent_runner(
|
||||
root: &Path,
|
||||
agent: &str,
|
||||
run_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
if [agent, run_id]
|
||||
.into_iter()
|
||||
.any(|value| value.trim().is_empty())
|
||||
{
|
||||
return Err("暂停 Agent Goal 必须同时提供 agent/runId".to_string());
|
||||
}
|
||||
let result = send_external_agent_runner_runtime_request(
|
||||
root,
|
||||
"runtime.pause",
|
||||
Some(agent.trim()),
|
||||
Some(run_id.trim()),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
result
|
||||
.get("providerInterrupted")
|
||||
.and_then(Value::as_bool)
|
||||
.ok_or_else(|| "Agent Runner runtime.pause 响应缺少 providerInterrupted".to_string())
|
||||
}
|
||||
|
||||
pub(crate) fn cancel_external_agent_runner_goal(
|
||||
root: &Path,
|
||||
agent: &str,
|
||||
run_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
if [agent, run_id]
|
||||
.into_iter()
|
||||
.any(|value| value.trim().is_empty())
|
||||
{
|
||||
return Err("清理 Agent Goal 必须同时提供 agent/runId".to_string());
|
||||
}
|
||||
let result = send_external_agent_runner_runtime_request(
|
||||
root,
|
||||
"runtime.cancel",
|
||||
Some(agent.trim()),
|
||||
Some(run_id.trim()),
|
||||
None,
|
||||
None,
|
||||
)?;
|
||||
result
|
||||
.get("providerInterrupted")
|
||||
.and_then(Value::as_bool)
|
||||
.ok_or_else(|| "Agent Runner runtime.cancel 响应缺少 providerInterrupted".to_string())
|
||||
}
|
||||
|
||||
fn parse_external_agent_runner_steer_result(result: &Value) -> Result<bool, String> {
|
||||
result
|
||||
.get("providerInterrupted")
|
||||
@@ -3873,6 +4007,153 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run() {
|
||||
let pause_directory = unique_test_directory();
|
||||
let pause_root = pause_directory.0.join("pause-project");
|
||||
crate::init_local_game_project_at(
|
||||
&pause_root,
|
||||
"project-goal-pause-rpc",
|
||||
"Runner Goal pause 测试",
|
||||
)
|
||||
.expect("initialize Goal pause project");
|
||||
let mut pause_runtime = crate::start_game_creator_agent_runtime_task_for_session_at(
|
||||
&pause_root,
|
||||
"code-prototype",
|
||||
None,
|
||||
"暂停同一 Goal run",
|
||||
"run-goal-pause-rpc",
|
||||
"agent-background-task",
|
||||
"等待暂停",
|
||||
vec!["保持同一 run".to_string()],
|
||||
)
|
||||
.expect("start Goal pause runtime");
|
||||
let pause_goal = crate::seed_game_creator_agent_goal_for_runtime_test_at(
|
||||
&pause_root,
|
||||
&mut pause_runtime,
|
||||
"暂停后继续同一 run",
|
||||
crate::AGENT_GOAL_STATUS_PAUSE_REQUESTED,
|
||||
)
|
||||
.expect("seed pause-requested Goal");
|
||||
let pause_token = "goal-pause-rpc-token-goal-pause-rpc-token";
|
||||
let pause_state = ExternalAgentRunnerServerState::new(
|
||||
pause_directory
|
||||
.0
|
||||
.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
|
||||
test_endpoint(pause_token, "goal-pause-rpc-boot", 30313),
|
||||
);
|
||||
let pause_response = dispatch_external_agent_runner_runtime_request(
|
||||
&ExternalAgentRunnerRequest {
|
||||
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
request_id: "goal-pause-rpc-request".to_string(),
|
||||
token: pause_token.to_string(),
|
||||
method: "runtime.pause".to_string(),
|
||||
params: ExternalAgentRunnerRequestParams {
|
||||
root: Some(pause_root.to_string_lossy().into_owned()),
|
||||
agent: Some("code-prototype".to_string()),
|
||||
run_id: Some("run-goal-pause-rpc".to_string()),
|
||||
..ExternalAgentRunnerRequestParams::default()
|
||||
},
|
||||
},
|
||||
&pause_state,
|
||||
);
|
||||
assert!(
|
||||
pause_response.ok,
|
||||
"runtime.pause failed: {:?}",
|
||||
pause_response.error
|
||||
);
|
||||
assert_eq!(
|
||||
pause_response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|value| value["providerInterrupted"].as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
let paused = crate::read_game_creator_agent_runtime_at(&pause_root, "code-prototype")
|
||||
.expect("read paused Goal runtime")
|
||||
.state;
|
||||
assert_eq!(paused.run_id, pause_goal.run_id);
|
||||
assert_eq!(paused.status, "paused");
|
||||
assert_eq!(
|
||||
paused.goal_status.as_deref(),
|
||||
Some(crate::AGENT_GOAL_STATUS_PAUSED)
|
||||
);
|
||||
|
||||
let cancel_directory = unique_test_directory();
|
||||
let cancel_root = cancel_directory.0.join("cancel-project");
|
||||
crate::init_local_game_project_at(
|
||||
&cancel_root,
|
||||
"project-goal-cancel-rpc",
|
||||
"Runner Goal cancel 测试",
|
||||
)
|
||||
.expect("initialize Goal cancel project");
|
||||
let mut cancel_runtime = crate::start_game_creator_agent_runtime_task_for_session_at(
|
||||
&cancel_root,
|
||||
"code-prototype",
|
||||
None,
|
||||
"清理同一 Goal run",
|
||||
"run-goal-cancel-rpc",
|
||||
"agent-background-task",
|
||||
"等待清理",
|
||||
vec!["清理同一 run".to_string()],
|
||||
)
|
||||
.expect("start Goal cancel runtime");
|
||||
let cancel_goal = crate::seed_game_creator_agent_goal_for_runtime_test_at(
|
||||
&cancel_root,
|
||||
&mut cancel_runtime,
|
||||
"清理当前 Goal",
|
||||
crate::AGENT_GOAL_STATUS_CLEARING,
|
||||
)
|
||||
.expect("seed clearing Goal");
|
||||
let cancel_token = "goal-cancel-rpc-token-goal-cancel-rpc-token";
|
||||
let cancel_state = ExternalAgentRunnerServerState::new(
|
||||
cancel_directory
|
||||
.0
|
||||
.join(EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
|
||||
test_endpoint(cancel_token, "goal-cancel-rpc-boot", 30314),
|
||||
);
|
||||
let cancel_response = dispatch_external_agent_runner_runtime_request(
|
||||
&ExternalAgentRunnerRequest {
|
||||
protocol_version: EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
|
||||
request_id: "goal-cancel-rpc-request".to_string(),
|
||||
token: cancel_token.to_string(),
|
||||
method: "runtime.cancel".to_string(),
|
||||
params: ExternalAgentRunnerRequestParams {
|
||||
root: Some(cancel_root.to_string_lossy().into_owned()),
|
||||
agent: Some("code-prototype".to_string()),
|
||||
run_id: Some("run-goal-cancel-rpc".to_string()),
|
||||
..ExternalAgentRunnerRequestParams::default()
|
||||
},
|
||||
},
|
||||
&cancel_state,
|
||||
);
|
||||
assert!(
|
||||
cancel_response.ok,
|
||||
"runtime.cancel failed: {:?}",
|
||||
cancel_response.error
|
||||
);
|
||||
assert_eq!(
|
||||
cancel_response
|
||||
.result
|
||||
.as_ref()
|
||||
.and_then(|value| value["providerInterrupted"].as_bool()),
|
||||
Some(false)
|
||||
);
|
||||
let cancelled = crate::read_game_creator_agent_runtime_at(&cancel_root, "code-prototype")
|
||||
.expect("read cancelled Goal runtime")
|
||||
.state;
|
||||
assert_eq!(cancelled.run_id, cancel_goal.run_id);
|
||||
assert_eq!(cancelled.status, "cancelled");
|
||||
let cleared = crate::read_game_creator_agent_goal_at(
|
||||
&cancel_root,
|
||||
"code-prototype",
|
||||
&cancel_goal.session_id,
|
||||
)
|
||||
.expect("read cleared Goal")
|
||||
.expect("cleared Goal exists");
|
||||
assert_eq!(cleared.status, crate::AGENT_GOAL_STATUS_CLEARED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn draining_rejects_runtime_steer() {
|
||||
let directory = unique_test_directory();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1364,7 +1364,7 @@ textarea {
|
||||
|
||||
.launcher-agent-chat-mode {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, auto);
|
||||
grid-template-columns: repeat(3, auto);
|
||||
gap: 2px;
|
||||
padding: 2px;
|
||||
border: 1px solid #d8dde5;
|
||||
@@ -1387,6 +1387,21 @@ textarea {
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-composer-summary {
|
||||
min-width: 0;
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid #d8dde5;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
color: #44536a;
|
||||
font-size: 12px;
|
||||
line-height: 34px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.launcher-agent-runtime-stack {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -1396,6 +1411,69 @@ textarea {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-status {
|
||||
display: grid;
|
||||
gap: 7px;
|
||||
margin: 12px 14px 0;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-status header {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-status header > div:first-child,
|
||||
.launcher-agent-goal-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-status strong {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-status p,
|
||||
.launcher-agent-goal-status small {
|
||||
margin: 0;
|
||||
color: #647084;
|
||||
font-size: 12px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-actions {
|
||||
display: flex;
|
||||
flex: 0 0 auto;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-actions button {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
height: 28px;
|
||||
padding: 0 9px;
|
||||
border: 1px solid #d8dde5;
|
||||
border-radius: 7px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-actions button:disabled {
|
||||
background: #eef2f7;
|
||||
color: #9aa3b2;
|
||||
}
|
||||
|
||||
.agent-llm-warning {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1868,6 +1946,15 @@ textarea {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-status header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-actions {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.launcher-agent-session-bar {
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
}
|
||||
@@ -1912,6 +1999,50 @@ textarea {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-dialog {
|
||||
width: min(560px, 100%);
|
||||
max-height: min(760px, calc(100vh - 48px));
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-fields {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-fields label {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
color: #44536a;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-fields textarea {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
padding: 9px 10px;
|
||||
border: 1px solid #d8dde5;
|
||||
border-radius: 8px;
|
||||
background: #fff;
|
||||
color: #111827;
|
||||
font: inherit;
|
||||
line-height: 1.5;
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.launcher-agent-goal-fields textarea:focus-visible {
|
||||
border-color: #64748b;
|
||||
outline: 2px solid rgb(100 116 139 / 18%);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.launcher-dialog .launcher-agent-goal-dialog-error {
|
||||
margin-top: 10px;
|
||||
color: #b42318;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.launcher-dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4577,6 +4577,16 @@
|
||||
- 终审补充:finalization v2 读取边界必须再次要求所有结构化步骤 `completed` 且 active index 为空;仅重算合法 `planSnapshotFingerprint / finalizationId` 的未完成快照也失败关闭。开发 CLI 的 Runtime JSON 只输出状态和安全身份,递归移除 `sessionPath / eventPath / taskPath`,不把项目绝对存储位置写入命令 transcript;Tauri/App 内部结果结构保持不变。
|
||||
- 验收现状:确定性回归已通过,真实 `gpt-5.5 llm-runtime` 连续三轮均在首个 Provider planning POST 返回前因同一 TLS record-layer failure 失败,未产生 plan/tool/kill/steer 证据;第三轮绝对路径、密钥和诱饵泄漏为 0。V1.17 继续保持未 PASS,Provider 恢复后必须完整重跑。
|
||||
|
||||
## 2026-07-15 AI 游戏创作 Agent Runtime V1.18 单 Agent 持久 Goal mode
|
||||
|
||||
- 决策:Goal 规范记录使用 `game-creator-agent-goal.v1`,current 路径固定为 `.agent/runtime/goals/current/<agentHash>/<sessionHash>.json`,终态 history 路径固定为 `.agent/runtime/goals/history/<agentHash>/<goalHash>.json`;hash 取对应稳定身份 SHA-256 十六进制前 32 位。Goal 绑定 Agent/Session/run 和单调内容 revision,Runtime state 与 task 只保存身份、revision、状态投影,不复制 Goal 正文或建立第二份生命周期事实源。
|
||||
- 恢复快照:context bundle 升级为 `game-creator-runtime-context-bundle.v4`,绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`。Provider planning/final 中断或返回到 pause 安全边界时,先以恢复后的 `active` Goal 语义持久化 continuation,再把当前 Runtime/Goal 收束为 paused;不能只写暂停状态而丢失恢复轮次。
|
||||
- 动作门禁:pending action 升级为 `game-creator-pending-action.v5`,在 project revision、verification gate、repository context fingerprint 和 steer cursor 之外绑定 `goalId / goalRevision / goalSnapshotFingerprint`;旧 v1-v4 全部失败关闭。Goal edit 提交新 revision 后,旧自动动作和旧待确认动作统一转成 `blocked` observation,在原 run 重规划,禁止执行旧副作用、从当前 Goal 猜回绑定或创建 retry run。
|
||||
- 暂停恢复:Runner 重启先处理 cancel / Goal control,再进入 process reconciliation、finalization、pending action 和 runnable task;`pause-requested` 必须先收束成 `paused`,`paused` 直接保持休眠。resume 的有效迁移只接受 `paused -> active`,先清理同一 run 遗留 cancel tombstone,再唤醒原 Agent/Session/run,不创建新 run;若 sidecar 已 `active` 但 Runtime 投影或 Runner 唤醒未提交,重复 resume 继续补齐同一 run,不能假成功。当前 Agent/Session/run 的 Goal sidecar 损坏或冲突时,即使 Runtime 缺少 legacy `goalId` 投影也失败关闭到 reconciliation。
|
||||
- Finalization:journal 升级为 `game-creator-runtime-finalization.v3` 并绑定 Goal revision/快照。assistant 按稳定 messageId 落盘后,先可靠写入 Runtime completed task/state,再提交 Goal completed,并补写携带 Goal 终态的 task/state projection;全部可靠后 journal 才进入 `runtime-completed` 并删除。assistant 尚未落盘且 Goal revision 漂移时丢弃旧 prepared journal 并 same-run 重规划,assistant 已落盘后只补投影,不再请求 Provider。
|
||||
- 展示边界:开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层完成,并展示状态、revision、完成标准和暂停/恢复/清理;纯聊天 CLI 提供对应 `/goal` 命令。正式用户 Project Supervisor 页面不暴露 Goal 管理控件。
|
||||
- 验收现状:确定性回归与 UI 覆盖不能替代真实 Provider 长链路。截至 2026-07-15 尚未记录 V1.18 真实 Provider PASS;恢复后必须用一次性项目完成 Goal edit、pause、Runner 强杀、重启保持 paused、显式同 run resume、唯一 assistant 和零旧动作重放的交叉取证。
|
||||
|
||||
## 2026-07-15 Project Supervisor 纯聊天短入口
|
||||
|
||||
- 决策:无 GUI 开发聊天省略 `parentAgentId` 时固定进入 `project-supervisor`;新增 `npm run agc:chat -- --config-dir <AppData> [--init] <project>` 作为总控入口。原 `agc:swarm` 和显式 `<parentAgentId>` 继续保留给专业父 Agent 调试,不改变既有调用兼容性。
|
||||
|
||||
@@ -3205,10 +3205,20 @@
|
||||
|
||||
- 症状:同一 Provider planning 返回多个 actions;前一个验证动作修改了 `AGENTS.md` 或其它启动上下文来源,后一个写动作仍执行成功,下一轮只看到普通 `ok` observation,没有 `repositoryContextDrift=true`。
|
||||
- 原因:context bundle v3 在 action 激活和 observation 落盘后都会同步完整计划,同时重新扫描 repository startup context。若 drift gate 从最新 bundle 读取 fingerprint,前一个动作造成的漂移会被同步成新基线,后续动作不再与 Provider planning 真正看到的旧规范比较。
|
||||
- 处理:Provider request builder 必须把实际渲染的 repository fingerprint 和工具计划一起返回,并写入 `game-creator-pending-action.v4` 的 `plannedRepositoryContextFingerprint`;同批自动动作、待确认动作和恢复动作都只复核该持久快照。旧 v1-v3 缺少身份,失败关闭,不能从最新 bundle 猜回。
|
||||
- 验证:`runtime_v11_closure_repository_context_drift_replans_before_auto_mutations` 必须覆盖 file.write / file.patch / file.delete / project.patchset / project.restore 五种动作,证明前置验证导致规范漂移后旧动作零执行、同 run 收到稳定 drift observation;`legacy_context_and_pending_records_fail_closed` 覆盖 v3 拒绝。
|
||||
- 处理:Provider request builder 必须把实际渲染的 repository fingerprint 和工具计划一起返回;现行 `game-creator-pending-action.v5` 除 `plannedRepositoryContextFingerprint` 外,还绑定 steer cursor 与 `goalId / goalRevision / goalSnapshotFingerprint`。同批自动动作、待确认动作和恢复动作只复核 planning 时持久化的快照;旧 v1-v4 缺少现行完整身份,失败关闭,不能从最新 bundle 或当前 Goal 猜回。
|
||||
- 验证:`runtime_v11_closure_repository_context_drift_replans_before_auto_mutations` 必须覆盖 file.write / file.patch / file.delete / project.patchset / project.restore 五种动作,证明前置验证导致规范漂移后旧动作零执行、同 run 收到稳定 drift observation;`legacy_context_and_pending_records_fail_closed` 覆盖 v4 拒绝。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/agent.rs`、`main.rs`、`tests.rs`、`apps/ai-game-creator-shell/src/App.tsx`、`tests/appSurface.test.ts`。
|
||||
|
||||
## 持久 Goal 不能只靠 Runtime state 推断,也不能让旧动作跨 revision 执行
|
||||
|
||||
- 现象:Goal 编辑后,旧的自动动作或待确认动作仍按旧目标执行;暂停后重启 Runner 又先恢复 finalization/pending action 并继续调用 Provider;或 assistant 已经可见,但 Goal 先标成 completed、Runtime task/state 仍停在非终态。另一类恢复问题是 pause 已中断 Provider,却没有保存准确 continuation,或 resume 先把 sidecar 写成 `active`、后续 Runtime 恢复失败,重试却假成功并永久停在 paused。
|
||||
- 原因:把每 Agent 的 latest Runtime state 当成 Goal 正文事实源,没有独立 Agent/Session Goal sidecar;pending action 未绑定 Goal ID、revision 和快照;恢复扫描把 pause control 放在 finalization/pending action 之后;或 finalization 把 Goal completed 当成 Runtime completed 之前的提交点。只在内存里中断 Provider,也无法保证进程退出后仍有可恢复上下文;只看 sidecar 的 `active` 也不能证明 Runtime 投影和 Runner 唤醒已经提交。旧/半写 Runtime 缺少 `goalId` 时吞掉 sidecar 损坏错误,还会把有 Goal 的 run 错当成无 Goal run。
|
||||
- 处理:Goal 正文只认 `.agent/runtime/goals/current/<agentHash>/<sessionHash>.json`,终态历史写入 `.agent/runtime/goals/history/<agentHash>/<goalHash>.json`;Runtime state/task 仅作投影。context bundle v4 固定绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`;Provider 中断边界先保存按 `active` 恢复语义构造的 continuation,再把同一 run 收束为 paused。
|
||||
- 动作与恢复:pending action v5 同时绑定 Goal ID、revision 和 snapshot fingerprint,旧 v1-v4 失败关闭。Goal edit 后,旧自动/确认动作写成稳定 `blocked` observation 并在同一 run 重规划;不能执行旧副作用,也不能转成 retry run。重启先处理 cancel / Goal control,`pause-requested` 收束为 `paused` 后直接休眠;resume 只做 `paused -> active`,先删除同一 run 的旧 cancel tombstone,再唤醒原 run。若首次 resume 在 sidecar 提交后失败,重试必须继续补 Runtime/Runner;sidecar 损坏或身份冲突时,无论 Runtime 是否已有 `goalId` 都进入 reconciliation。
|
||||
- 完成顺序:`game-creator-runtime-finalization.v3` 绑定 Goal 快照。assistant 落盘后,先写 Runtime completed task/state,再写 Goal completed 并补齐携带 Goal 终态的 Runtime projection;全部成功后才删除 finalization journal。prepared 且 assistant 未落盘时发现 Goal revision 已更新,必须丢弃旧回复并 same-run 重规划。
|
||||
- 验证:确定性测试至少覆盖旧自动/确认动作转 blocked、paused 重启零 Provider、同 run resume、旧 cancel tombstone 清理、Provider 中断边界 context、assistant 后 Runtime/Goal completed 顺序和 v1-v4 pending 拒绝。真实 Provider 还必须经历 Goal edit、pause、Runner 强杀和显式 resume,并证明 run/session 不变、旧动作零重放、唯一 assistant;未完成该链路时不得写 V1.18 PASS。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/goal.rs`、`agent.rs`、`runner.rs`、`tests.rs`、`apps/ai-game-creator-shell/src/App.tsx`、`tests/appSurface.test.ts`。
|
||||
|
||||
## iOS 退款问询的 result_code 不是 debug 状态
|
||||
|
||||
- 现象:为了先观察真实 iOS 退款通知,回调返回 `ErrCode=0 + IosRefundQueryResponse.result_code=1`,并把 evidence 写成“调试阶段不执行自动退款决策”,看起来像安全 ACK,实际已经向微信建议拒绝退款。
|
||||
|
||||
@@ -757,10 +757,48 @@ OpenAI Chat / Responses 的 strict function schema 顶层固定为 `thinkingSumm
|
||||
|
||||
截至 2026-07-15,确定性门禁已通过:Tauri 单线程全量 690 项中 686 passed / 4 ignored,结构化计划、finalization 恢复和前端竞态定向回归全部通过。真实 `gpt-5.5` `llm-runtime` 连续三轮均在首个 Provider planning POST 返回前因 TLS record-layer failure 进入 failed,尚未产生 function plan、工具动作、Runner kill 或 steer,因此仍未记录 V1.17 真实 Provider PASS。首轮验收器同时发现 CLI `runtimeJson` 暴露 `sessionPath / eventPath / taskPath`;CLI 输出视图移除这三个绝对存储路径后,第三轮项目绝对路径 transcript/report 泄漏计数均为 0。外部请求失败不能替代完整真实门禁,后续 Provider 恢复后必须重跑本节命令。
|
||||
|
||||
## V1.18 单 Agent 持久 Goal mode
|
||||
|
||||
V1.18 对标 Codex CLI `/goal` 的长任务语义:目标文本既是首轮任务,也是后续完成判断的上层标准。Goal 不是 `currentGoal` 的展示别名,也不建立第二套 Runner;它绑定一个 Agent active Session 和同一 run,复用现有持久计划、steer、工具策略、确认、verification gate、context bundle、finalization 与 External Runner。
|
||||
|
||||
### Goal 身份与持久化
|
||||
|
||||
- 当前 Session 同时最多有一个非终态 Goal。规范记录为 `game-creator-agent-goal.v1`,current 保存在 `.agent/runtime/goals/current/<agentHash>/<sessionHash>.json`,旧终态 Goal 在新建前归档到 `.agent/runtime/goals/history/<agentHash>/<goalHash>.json`;三个 hash 均取对应稳定身份 SHA-256 十六进制的前 32 位,路径不直接暴露 Agent、Session 或 Goal 原始标识。记录固定绑定 `projectId / goalId / agentId / sessionId / runId / revision`,并保存 outcome、最多 8 条 constraints、最多 8 条 verification、状态、时间和最终有界系统证据。
|
||||
- 生命周期单向为 `active -> pause-requested -> paused -> active`,以及 `active|pause-requested|paused -> clearing -> cleared`、`active -> completed`;身份或持久文件损坏进入 `needs-reconciliation`。暂停/恢复不增加 revision;只有 outcome、constraints 或 verification 真实变化时 revision 单调增加。相同 expectedRevision 与相同正文幂等,旧 revision 或不同正文冲突失败关闭。
|
||||
- `AgentRuntimeState` 保存 Goal 身份、revision 与状态;task journal 继续用既有 Agent/Session/run 身份与规范 Goal sidecar 交叉校验,不复制 Goal 正文或建立第二份生命周期投影。状态读取再从 Goal sidecar 补齐 outcome/constraints/verification。普通历史 Runtime 没有 Goal 字段时按非 Goal run 兼容,不能凭 `currentTask` 自动迁移成 Goal。
|
||||
|
||||
### 启动、编辑与运行隔离
|
||||
|
||||
- `/goal <文本>` 或 Tauri start command 创建 Goal 并用同一 outcome 启动首个 background run;未显式提供 verification 时,outcome 自身作为完成标准。Goal 活跃或暂停期间,当前 Session 禁止另起不相关 run;后续普通输入默认继续走同 run steer,独立任务应使用另一 Session。
|
||||
- 编辑 Goal 先在项目写锁内提交 revision,再把规范化的新目标作为同 run steer 持久化。Provider 正在 planning 时允许中断;确认中或工具执行中只排队,旧动作在下一安全边界前必须校验 Goal revision,不能在目标已变更后继续执行。旧自动动作或待确认动作若绑定旧 Goal 快照,统一转成 `blocked` observation 并在同一 run 重规划,不执行旧副作用,也不创建 retry run。编辑失败不得回退已提交 revision,Runtime 会以 sidecar 为事实源重规划并拒绝旧 finalization。
|
||||
- Goal 内容进入每轮 planning/final reply 的显式“持久目标”上下文。模型仍通过 V1.17 `planUpdate` 维护可观察步骤;Goal revision 不推进 project revision、不改变权限或 verification gate,也不能放宽 sandbox/approval。
|
||||
|
||||
### 暂停、恢复与清理
|
||||
|
||||
- pause 写 durable request,并通过 typed Runner `runtime.pause` 中断当前 planning/final Provider;已进入工具的动作允许返回后再停。Provider 中断或返回边界先把可恢复 continuation 写入 v4 context bundle,其中恢复快照按 `active` Goal 语义保存,随后 Runtime 才在 LLM/工具/observation/finalization 安全边界把同一 run 收束为 `paused`。Runner 重启时先处理 cancel / Goal control,再进入 process reconciliation、finalization 和 pending action;`pause-requested` 会先收束成 `paused`,`paused` 直接保持休眠,不生成 assistant、不创建新 run、不调用 Provider。
|
||||
- 暂停父 Agent 不撤销已经 durable 投递的专业 child;child 可以把结果写成 ready,但父 run 在显式 resume 前不能认领或继续 Provider。Runner-owned process session 在暂停提交前终止,恢复后由 Agent 根据 observation 重规划,禁止按 PID 重连或重放未知 start。
|
||||
- resume 的有效状态迁移只接受 `paused -> active`,先清理同一 run 遗留的 cancel tombstone,再把原 Agent/Session/run 重新投影为 pending 并唤醒 External Runner;不创建 retry run。若进程在 Goal sidecar 已写成 `active`、Runtime 投影或 Runner 唤醒尚未完成时失败,重复 resume 必须识别同一 run 的半提交并继续补齐,不能把 `active` 单独当成恢复成功或直接返回。pending confirmation 仍回到确认态,普通 planning 从 v4 context bundle 继续。clear 对活跃 Goal 复用取消 tombstone,待安全取消后把 Goal 写为 cleared;paused/completed Goal 可直接清理。clear 不删除历史 conversation、task、event 或 Goal history。
|
||||
|
||||
### 恢复与完成门禁
|
||||
|
||||
- context bundle 升级为 `game-creator-runtime-context-bundle.v4`,绑定 `goalId / goalRevision / goalStatus / goalSnapshotFingerprint`;v3 在现有身份、计划和 verification 校验通过后从 Runtime/Goal sidecar 补齐,v2 继续先按 V1.17 迁移计划再补 Goal。v4 任一 Goal 身份、revision 或快照不一致都失败关闭。当前 Agent/Session/run 的 Goal sidecar 无法读取、损坏或身份冲突时,即使 legacy/半写 Runtime 尚无 `goalId` 投影也必须进入 `needs-reconciliation`,不得退化成无 Goal run 或绕过完成门禁。
|
||||
- pending action 升级为 `game-creator-pending-action.v5`,在既有 project revision、verification gate、repository context fingerprint 和 steer cursor 外,固定绑定 `goalId / goalRevision / goalSnapshotFingerprint`。旧 v1-v4 一律失败关闭,不能从当前 Goal 猜回缺失绑定;Goal edit 后,无论动作原为自动还是待确认,都把旧记录收束成稳定 `blocked` observation 并在同一 run 重规划。
|
||||
- finalization journal 升级为 `game-creator-runtime-finalization.v3`,把 Goal 快照指纹纳入 finalizationId。prepared 回复必须绑定当前 active Goal、同一 run/revision、全部完成的结构化计划、清空的 process/join/delegate 屏障和现有 verification gate。Goal 编辑、暂停、清理或 revision 漂移会让未提交 assistant 的旧 finalization 失效并回到同 run;assistant 已提交后只允许按 journal 原快照补齐 Runtime 与 Goal completed,不能重新请求 Provider。
|
||||
- assistant 持久化后,finalization 先写 Runtime completed task/state,再把规范 Goal 写为 completed,并补写携带 completed Goal 状态的 task/state projection;两层投影均可靠后,journal 才推进 `runtime-completed` 并删除。完成证据由系统从最终结构化计划、verification gate、run/session 身份和 response fingerprint 生成,不保存模型 thinking 或原始私有 observation。Goal 未完成、paused、clearing、sidecar 缺失或 revision 不匹配时,response 不能绕过完成门禁。
|
||||
|
||||
### 控制面与验收
|
||||
|
||||
- 纯聊天入口支持 `/goal <文本>`、`/goal status`、`/goal pause`、`/goal resume`、`/goal edit <文本>`、`/goal clear`;开发 Agent UI 使用 `执行 / 聊天 / 目标` 三段模式,Goal 创建/编辑通过独立弹层提交,并在状态行显示 outcome、revision、状态与完成标准,提供暂停/恢复/清理。普通用户 Supervisor 首页不暴露开发 Goal 管理控件。
|
||||
- 确定性验收覆盖:单 Session 单 Goal、跨 Agent/Session 隔离、expectedRevision 幂等/冲突、活跃 Goal 阻止新 run、Provider in-flight pause、工具返回后 pause、paused 重启不自启、同 run resume、pending confirmation 恢复、编辑触发 steer 与旧动作失效、clear/cancel 竞态、v4/v3/v2 恢复、v3 finalization、assistant 后崩溃补齐 completed,以及 Goal 元数据零 project revision/policy 变化。
|
||||
- 真实 Provider 使用一次性项目证明:Goal 自行建立并多次更新计划,运行中编辑一次,暂停并强杀 Runner,重启后保持 paused,显式恢复同 run,最终全部步骤和 verification 收束后只写一个 assistant;task/event/context/finalization/goal/conversation 交叉证明无新主 run、无动作重放、无 paused Provider 调用,并扫描密钥、Goal 正文和项目绝对路径的公共泄漏。
|
||||
- 截至 2026-07-15,V1.18 真实 Provider 门禁尚未通过,不能把 Rust/Runner/UI 确定性回归或短鉴权请求外推为 V1.18 PASS;Provider 链路恢复后仍需完整执行上一条一次性项目验收。
|
||||
|
||||
## 验收命令
|
||||
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml agent_runtime_context_bundle_migrates_v2_and_rejects_v3_plan_mismatch -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml agent_goal_ -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml goal_context_bundle_v4_migrates_v3_and_v2_then_rejects_plan_mismatch -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run -- --nocapture`
|
||||
- `npm run ai-game-creator-shell:typecheck`
|
||||
- `npm run test -- apps/ai-game-creator-shell/tests`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml`
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user