补齐单Agent持久目标模式

新增Agent会话级Goal的创建编辑暂停恢复清理与CLI/Tauri入口
升级Runtime context v4、pending action v5和finalization v3的Goal绑定
修复恢复半提交、缺失Goal投影及暂停过渡态的失败关闭
补齐开发窗口目标模式、故障注入测试和项目文档
This commit is contained in:
AIGameCreator App
2026-07-15 06:46:48 +08:00
parent 5f317e79f8
commit b0be078d6c
16 changed files with 6561 additions and 109 deletions
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
+132 -1
View File
@@ -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 和单调内容 revisionRuntime 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。
- Finalizationjournal 升级为 `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 调试,不改变既有调用兼容性。
+12 -2
View File
@@ -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 sidecarpending 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/Runnersidecar 损坏或身份冲突时,无论 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。编辑失败不得回退已提交 revisionRuntime 会以 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 投递的专业 childchild 可以把结果写成 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 写为 clearedpaused/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 失效并回到同 runassistant 已提交后只允许按 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 收束后只写一个 assistanttask/event/context/finalization/goal/conversation 交叉证明无新主 run、无动作重放、无 paused Provider 调用,并扫描密钥、Goal 正文和项目绝对路径的公共泄漏。
- 截至 2026-07-15V1.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