1363b9374c
## 变更 - 冻结根 Supervisor 的 Goal Contract - 引入动态 Acceptance Graph 并传播到专业 Agent - 使用当前 revision 的真实动作回执作为完成证据 - 安全处理 steer 后旧根任务树替换 ## 验证 - `npm run check:native-shells` - `npm run agc:typecheck` - `npm run check:encoding` - `cargo fmt --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml -- --check` - `git diff --check` Reviewed-on: http://192.168.35.82/git/GenarrativeAI/Genarrative/pulls/161 Co-authored-by: kdletters <kdletters@qq.com> Co-committed-by: kdletters <kdletters@qq.com>
1275 lines
47 KiB
Rust
1275 lines
47 KiB
Rust
use super::*;
|
||
use sha2::{Digest, Sha256};
|
||
|
||
pub(crate) const AGENT_GOAL_SCHEMA_VERSION: &str = "game-creator-agent-goal.v1";
|
||
pub(crate) const AGENT_GOAL_STATUS_ACTIVE: &str = "active";
|
||
pub(crate) const AGENT_GOAL_STATUS_PAUSE_REQUESTED: &str = "pause-requested";
|
||
pub(crate) const AGENT_GOAL_STATUS_PAUSED: &str = "paused";
|
||
pub(crate) const AGENT_GOAL_STATUS_CLEARING: &str = "clearing";
|
||
pub(crate) const AGENT_GOAL_STATUS_CLEARED: &str = "cleared";
|
||
pub(crate) const AGENT_GOAL_STATUS_COMPLETED: &str = "completed";
|
||
pub(crate) const AGENT_GOAL_STATUS_NEEDS_RECONCILIATION: &str = "needs-reconciliation";
|
||
|
||
const AGENT_GOAL_SIDECAR_MAX_BYTES: usize = 64 * 1024;
|
||
const AGENT_GOAL_OUTCOME_MAX_CHARS: usize = 4_000;
|
||
const AGENT_GOAL_ITEM_MAX_CHARS: usize = 1_000;
|
||
const AGENT_GOAL_ITEM_LIMIT: usize = 8;
|
||
|
||
fn agent_goal_now_nanos() -> u128 {
|
||
SystemTime::now()
|
||
.duration_since(UNIX_EPOCH)
|
||
.unwrap_or_default()
|
||
.as_nanos()
|
||
}
|
||
|
||
fn agent_goal_path_key(value: &str) -> String {
|
||
let digest = format!("{:x}", Sha256::digest(value.as_bytes()));
|
||
digest.chars().take(32).collect()
|
||
}
|
||
|
||
fn agent_goal_current_relative_path(agent_id: &str, session_id: &str) -> String {
|
||
format!(
|
||
".agent/runtime/goals/current/{}/{}.json",
|
||
agent_goal_path_key(agent_id),
|
||
agent_goal_path_key(session_id)
|
||
)
|
||
}
|
||
|
||
fn agent_goal_history_relative_path(agent_id: &str, goal_id: &str) -> String {
|
||
format!(
|
||
".agent/runtime/goals/history/{}/{}.json",
|
||
agent_goal_path_key(agent_id),
|
||
agent_goal_path_key(goal_id)
|
||
)
|
||
}
|
||
|
||
fn normalize_agent_goal_text(value: &str, max_chars: usize, label: &str) -> Result<String, String> {
|
||
let value = value.trim();
|
||
if value.is_empty() {
|
||
return Err(format!("{label} 不能为空"));
|
||
}
|
||
if value.chars().count() > max_chars {
|
||
return Err(format!("{label} 超过 {max_chars} 字符上限"));
|
||
}
|
||
if value
|
||
.chars()
|
||
.any(|character| character.is_control() && !matches!(character, '\n' | '\r' | '\t'))
|
||
{
|
||
return Err(format!("{label} 不能包含控制字符"));
|
||
}
|
||
Ok(value.to_string())
|
||
}
|
||
|
||
fn normalize_agent_goal_items(values: Vec<String>, label: &str) -> Result<Vec<String>, String> {
|
||
if values.len() > AGENT_GOAL_ITEM_LIMIT {
|
||
return Err(format!("{label} 最多 {AGENT_GOAL_ITEM_LIMIT} 条"));
|
||
}
|
||
let mut normalized = Vec::with_capacity(values.len());
|
||
for (index, value) in values.into_iter().enumerate() {
|
||
let value = normalize_agent_goal_text(
|
||
&value,
|
||
AGENT_GOAL_ITEM_MAX_CHARS,
|
||
&format!("{label} #{}", index + 1),
|
||
)?;
|
||
if normalized.iter().any(|existing| existing == &value) {
|
||
return Err(format!("{label} 不能包含重复项"));
|
||
}
|
||
normalized.push(value);
|
||
}
|
||
Ok(normalized)
|
||
}
|
||
|
||
pub(crate) fn agent_goal_status_is_valid(status: &str) -> bool {
|
||
matches!(
|
||
status,
|
||
AGENT_GOAL_STATUS_ACTIVE
|
||
| AGENT_GOAL_STATUS_PAUSE_REQUESTED
|
||
| AGENT_GOAL_STATUS_PAUSED
|
||
| AGENT_GOAL_STATUS_CLEARING
|
||
| AGENT_GOAL_STATUS_CLEARED
|
||
| AGENT_GOAL_STATUS_COMPLETED
|
||
| AGENT_GOAL_STATUS_NEEDS_RECONCILIATION
|
||
)
|
||
}
|
||
|
||
fn agent_goal_is_terminal(goal: &AgentGoalRecord) -> bool {
|
||
matches!(
|
||
goal.status.as_str(),
|
||
AGENT_GOAL_STATUS_CLEARED | AGENT_GOAL_STATUS_COMPLETED
|
||
)
|
||
}
|
||
|
||
pub(crate) fn agent_goal_snapshot_fingerprint(goal: &AgentGoalRecord) -> String {
|
||
let payload = serde_json::json!({
|
||
"projectId": goal.project_id,
|
||
"goalId": goal.goal_id,
|
||
"agentId": goal.agent_id,
|
||
"sessionId": goal.session_id,
|
||
"runId": goal.run_id,
|
||
"revision": goal.revision,
|
||
"outcome": goal.outcome,
|
||
"constraints": goal.constraints,
|
||
"verification": goal.verification,
|
||
});
|
||
format!(
|
||
"{:x}",
|
||
Sha256::digest(serde_json::to_vec(&payload).unwrap_or_default())
|
||
)
|
||
}
|
||
|
||
pub(crate) fn agent_goal_snapshot_fingerprint_for_state_at(
|
||
root: &Path,
|
||
state: &AgentRuntimeState,
|
||
) -> Result<String, String> {
|
||
let Some(goal_id) = state.goal_id.as_deref() else {
|
||
return Ok(String::new());
|
||
};
|
||
let goal = read_game_creator_agent_goal_at(root, &state.agent_id, &state.session_id)?
|
||
.ok_or_else(|| "Agent Runtime 已绑定 Goal,但规范 sidecar 缺失".to_string())?;
|
||
if goal.goal_id != goal_id
|
||
|| goal.run_id != state.run_id
|
||
|| goal.revision != state.goal_revision
|
||
{
|
||
return Err("Agent Runtime Goal 快照身份或 revision 不匹配".to_string());
|
||
}
|
||
Ok(agent_goal_snapshot_fingerprint(&goal))
|
||
}
|
||
|
||
fn validate_agent_goal_record(root: &Path, goal: &AgentGoalRecord) -> Result<(), String> {
|
||
if goal.schema_version != AGENT_GOAL_SCHEMA_VERSION
|
||
|| goal.project_id != game_creator_agent_runtime_context_project_id(root)?
|
||
|| goal.goal_id.trim().is_empty()
|
||
|| goal.agent_id.trim().is_empty()
|
||
|| goal.session_id.trim().is_empty()
|
||
|| goal.run_id.trim().is_empty()
|
||
|| goal.revision == 0
|
||
|| !agent_goal_status_is_valid(&goal.status)
|
||
|| goal.created_at == 0
|
||
|| goal.updated_at == 0
|
||
{
|
||
return Err("Agent Goal 身份或状态无效".to_string());
|
||
}
|
||
normalize_agent_goal_text(&goal.outcome, AGENT_GOAL_OUTCOME_MAX_CHARS, "Goal outcome")?;
|
||
normalize_agent_goal_items(goal.constraints.clone(), "Goal constraints")?;
|
||
let verification = normalize_agent_goal_items(goal.verification.clone(), "Goal verification")?;
|
||
if verification.is_empty() {
|
||
return Err("Goal verification 不能为空".to_string());
|
||
}
|
||
if goal.completion_evidence.len() > AGENT_GOAL_ITEM_LIMIT
|
||
|| goal
|
||
.completion_evidence
|
||
.iter()
|
||
.any(|item| item.trim().is_empty() || item.chars().count() > AGENT_GOAL_ITEM_MAX_CHARS)
|
||
{
|
||
return Err("Agent Goal 完成证据无效".to_string());
|
||
}
|
||
if goal
|
||
.response_fingerprint
|
||
.as_deref()
|
||
.is_some_and(|fingerprint| {
|
||
fingerprint.len() != 64 || !fingerprint.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||
})
|
||
{
|
||
return Err("Agent Goal 回复指纹无效".to_string());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_COMPLETED
|
||
&& (goal.completed_at.is_none()
|
||
|| goal.response_fingerprint.is_none()
|
||
|| goal.completion_evidence.is_empty())
|
||
{
|
||
return Err("已完成 Agent Goal 缺少系统完成证据".to_string());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_CLEARED && goal.cleared_at.is_none() {
|
||
return Err("已清理 Agent Goal 缺少清理时间".to_string());
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn write_agent_goal_record(root: &Path, goal: &AgentGoalRecord) -> Result<(), String> {
|
||
validate_agent_goal_record(root, goal)?;
|
||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||
root,
|
||
&agent_goal_current_relative_path(&goal.agent_id, &goal.session_id),
|
||
"Agent Goal",
|
||
goal,
|
||
AGENT_GOAL_SIDECAR_MAX_BYTES,
|
||
)
|
||
}
|
||
|
||
fn archive_agent_goal_record(root: &Path, goal: &AgentGoalRecord) -> Result<(), String> {
|
||
validate_agent_goal_record(root, goal)?;
|
||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||
root,
|
||
&agent_goal_history_relative_path(&goal.agent_id, &goal.goal_id),
|
||
"Agent Goal history",
|
||
goal,
|
||
AGENT_GOAL_SIDECAR_MAX_BYTES,
|
||
)
|
||
}
|
||
|
||
pub(crate) fn read_game_creator_agent_goal_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
validate_project_root(root)?;
|
||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||
let session_id = session_id.trim();
|
||
if session_id.is_empty() {
|
||
return Err("Agent Goal sessionId 不能为空".to_string());
|
||
}
|
||
let goal = read_agent_runtime_json_sidecar_with_max_bytes::<AgentGoalRecord>(
|
||
root,
|
||
&agent_goal_current_relative_path(&agent_id, session_id),
|
||
"Agent Goal",
|
||
AGENT_GOAL_SIDECAR_MAX_BYTES,
|
||
)?;
|
||
if let Some(goal) = &goal {
|
||
validate_agent_goal_record(root, goal)?;
|
||
if goal.agent_id != agent_id || goal.session_id != session_id {
|
||
return Err("Agent Goal 与请求的 Agent/Session 身份不匹配".to_string());
|
||
}
|
||
}
|
||
Ok(goal)
|
||
}
|
||
|
||
pub(crate) fn hydrate_game_creator_agent_goal_state_at(
|
||
root: &Path,
|
||
state: &mut AgentRuntimeState,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
if state.agent_id.trim().is_empty() || state.session_id.trim().is_empty() {
|
||
return Ok(None);
|
||
}
|
||
let goal = read_game_creator_agent_goal_at(root, &state.agent_id, &state.session_id)?;
|
||
let Some(goal) = goal else {
|
||
if state.goal_id.is_some() {
|
||
return Err("Agent Runtime 已绑定 Goal,但规范 Goal sidecar 缺失".to_string());
|
||
}
|
||
return Ok(None);
|
||
};
|
||
if goal.run_id != state.run_id {
|
||
if state.goal_id.as_deref() == Some(goal.goal_id.as_str()) {
|
||
return Err("Agent Runtime Goal runId 与规范 sidecar 不匹配".to_string());
|
||
}
|
||
return Ok(None);
|
||
}
|
||
if state
|
||
.goal_id
|
||
.as_deref()
|
||
.is_some_and(|goal_id| goal_id != goal.goal_id)
|
||
|| (state.goal_revision != 0 && state.goal_revision > goal.revision)
|
||
{
|
||
return Err("Agent Runtime Goal 身份或 revision 与规范 sidecar 冲突".to_string());
|
||
}
|
||
state.goal_id = Some(goal.goal_id.clone());
|
||
state.goal_revision = goal.revision;
|
||
state.goal_status = Some(goal.status.clone());
|
||
state.goal_outcome = Some(goal.outcome.clone());
|
||
state.goal_constraints = goal.constraints.clone();
|
||
state.goal_verification = goal.verification.clone();
|
||
state.current_goal = goal.outcome.clone();
|
||
Ok(Some(goal))
|
||
}
|
||
|
||
pub(crate) fn game_creator_agent_goal_task_binding_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
run_id: &str,
|
||
) -> Result<(Option<String>, u64, Option<String>), String> {
|
||
let Some(goal) = read_game_creator_agent_goal_at(root, agent_id, session_id)? else {
|
||
return Ok((None, 0, None));
|
||
};
|
||
if goal.run_id != run_id {
|
||
return Ok((None, 0, None));
|
||
}
|
||
Ok((Some(goal.goal_id), goal.revision, Some(goal.status)))
|
||
}
|
||
|
||
pub(crate) fn ensure_game_creator_agent_goal_allows_run_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
run_id: &str,
|
||
) -> Result<(), String> {
|
||
let Some(goal) = read_game_creator_agent_goal_at(root, agent_id, session_id)? else {
|
||
return Ok(());
|
||
};
|
||
if !agent_goal_is_terminal(&goal) && goal.run_id != run_id {
|
||
return Err(format!(
|
||
"当前 Session 已有未结束 Goal:goalId={} runId={} status={}",
|
||
goal.goal_id, goal.run_id, goal.status
|
||
));
|
||
}
|
||
Ok(())
|
||
}
|
||
|
||
fn new_agent_goal_id(project_id: &str, agent_id: &str, session_id: &str, run_id: &str) -> String {
|
||
let payload = format!(
|
||
"{project_id}\n{agent_id}\n{session_id}\n{run_id}\n{}",
|
||
agent_goal_now_nanos()
|
||
);
|
||
let fingerprint = format!("{:x}", Sha256::digest(payload.as_bytes()));
|
||
format!("goal-{}", fingerprint.chars().take(32).collect::<String>())
|
||
}
|
||
|
||
pub(crate) fn start_game_creator_agent_goal_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: Option<&str>,
|
||
outcome: &str,
|
||
constraints: Vec<String>,
|
||
verification: Vec<String>,
|
||
requested_run_id: &str,
|
||
) -> Result<AgentGoalMutationResult, String> {
|
||
validate_project_root(root)?;
|
||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||
let outcome = normalize_agent_goal_text(outcome, AGENT_GOAL_OUTCOME_MAX_CHARS, "Goal outcome")?;
|
||
let constraints = normalize_agent_goal_items(constraints, "Goal constraints")?;
|
||
let mut verification = normalize_agent_goal_items(verification, "Goal verification")?;
|
||
if verification.is_empty() {
|
||
verification.push(outcome.clone());
|
||
}
|
||
let run_id = if requested_run_id.trim().is_empty() {
|
||
format!("goal-{agent_id}-{}", agent_goal_now_nanos())
|
||
} else {
|
||
normalize_game_creator_agent_runtime_run_id(&agent_id, requested_run_id)
|
||
};
|
||
let project_id = game_creator_agent_runtime_context_project_id(root)?;
|
||
let (runtime, queued_run_id, goal) = with_agent_conversation_session_lane_at(
|
||
root,
|
||
&agent_id,
|
||
"Agent Goal 与 Session Runtime 入队",
|
||
|| {
|
||
let session_id =
|
||
resolve_agent_conversation_session_id_at(root, &agent_id, session_id, true)?;
|
||
ensure_agent_session_has_no_live_tasks(root, &agent_id, &session_id)?;
|
||
let now = unix_timestamp();
|
||
let goal = AgentGoalRecord {
|
||
schema_version: AGENT_GOAL_SCHEMA_VERSION.to_string(),
|
||
project_id: project_id.clone(),
|
||
goal_id: new_agent_goal_id(&project_id, &agent_id, &session_id, &run_id),
|
||
agent_id: agent_id.clone(),
|
||
session_id: session_id.clone(),
|
||
run_id: run_id.clone(),
|
||
revision: 1,
|
||
status: AGENT_GOAL_STATUS_ACTIVE.to_string(),
|
||
outcome: outcome.clone(),
|
||
constraints: constraints.clone(),
|
||
verification: verification.clone(),
|
||
completion_evidence: Vec::new(),
|
||
response_fingerprint: None,
|
||
created_at: now,
|
||
pause_requested_at: None,
|
||
paused_at: None,
|
||
completed_at: None,
|
||
cleared_at: None,
|
||
error: None,
|
||
updated_at: now,
|
||
};
|
||
{
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.start",
|
||
)?;
|
||
if let Some(existing) =
|
||
read_game_creator_agent_goal_at(root, &agent_id, &session_id)?
|
||
{
|
||
if !agent_goal_is_terminal(&existing) {
|
||
return Err(format!(
|
||
"当前 Session 已有未结束 Goal:goalId={} status={}",
|
||
existing.goal_id, existing.status
|
||
));
|
||
}
|
||
archive_agent_goal_record(root, &existing)?;
|
||
}
|
||
write_agent_goal_record(root, &goal)?;
|
||
}
|
||
match start_game_creator_agent_goal_task_in_session_lane_at(
|
||
root,
|
||
&agent_id,
|
||
&session_id,
|
||
&outcome,
|
||
&run_id,
|
||
) {
|
||
Ok((runtime, queued_run_id)) if queued_run_id == run_id => {
|
||
Ok((runtime, queued_run_id, goal))
|
||
}
|
||
Ok((_runtime, queued_run_id)) => {
|
||
let error = format!(
|
||
"Goal Runtime runId 漂移:expected={run_id}, actual={queued_run_id}"
|
||
);
|
||
mark_game_creator_agent_goal_needs_reconciliation_at(root, &goal, &error)?;
|
||
Err(error)
|
||
}
|
||
Err(error) => {
|
||
mark_game_creator_agent_goal_paused_after_failure_at(root, &goal, &error)?;
|
||
Err(error)
|
||
}
|
||
}
|
||
},
|
||
)?;
|
||
notify_external_agent_runner_after_background_task_enqueue(
|
||
root,
|
||
&agent_id,
|
||
&goal.session_id,
|
||
&queued_run_id,
|
||
)?;
|
||
let _ = append_agent_db_record(
|
||
root,
|
||
serde_json::json!({
|
||
"recordType": "agent.runtime.goal.started",
|
||
"agentId": goal.agent_id,
|
||
"sessionId": goal.session_id,
|
||
"runId": goal.run_id,
|
||
"goalId": goal.goal_id,
|
||
"goalRevision": goal.revision,
|
||
"outcomeSha256": format!("{:x}", Sha256::digest(goal.outcome.as_bytes())),
|
||
"constraintCount": goal.constraints.len(),
|
||
"verificationCount": goal.verification.len(),
|
||
}),
|
||
);
|
||
Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted: false,
|
||
})
|
||
}
|
||
|
||
fn update_agent_goal_record_at_locked<F>(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
expected_goal_id: &str,
|
||
expected_revision: u64,
|
||
update: F,
|
||
) -> Result<AgentGoalRecord, String>
|
||
where
|
||
F: FnOnce(&mut AgentGoalRecord) -> Result<(), String>,
|
||
{
|
||
let mut goal = read_game_creator_agent_goal_at(root, agent_id, session_id)?
|
||
.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
|
||
if goal.goal_id != expected_goal_id || goal.revision != expected_revision {
|
||
return Err(format!(
|
||
"Agent Goal 身份或 revision 已变化:goalId={} revision={}",
|
||
goal.goal_id, goal.revision
|
||
));
|
||
}
|
||
update(&mut goal)?;
|
||
goal.updated_at = unix_timestamp();
|
||
write_agent_goal_record(root, &goal)?;
|
||
Ok(goal)
|
||
}
|
||
|
||
pub(crate) fn edit_game_creator_agent_goal_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
goal_id: &str,
|
||
expected_revision: u64,
|
||
outcome: &str,
|
||
constraints: Vec<String>,
|
||
verification: Vec<String>,
|
||
) -> Result<AgentGoalMutationResult, String> {
|
||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||
let outcome = normalize_agent_goal_text(outcome, AGENT_GOAL_OUTCOME_MAX_CHARS, "Goal outcome")?;
|
||
let constraints = normalize_agent_goal_items(constraints, "Goal constraints")?;
|
||
let mut verification = normalize_agent_goal_items(verification, "Goal verification")?;
|
||
if verification.is_empty() {
|
||
verification.push(outcome.clone());
|
||
}
|
||
let (goal, changed, prepared_finalization) = {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.edit",
|
||
)?;
|
||
let current = read_game_creator_agent_goal_at(root, &agent_id, session_id)?
|
||
.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
|
||
if current.goal_id != goal_id || current.revision != expected_revision {
|
||
return Err(format!(
|
||
"Agent Goal 身份或 revision 已变化:goalId={} revision={}",
|
||
current.goal_id, current.revision
|
||
));
|
||
}
|
||
if agent_goal_is_terminal(¤t)
|
||
|| current.status == AGENT_GOAL_STATUS_CLEARING
|
||
|| current.status == AGENT_GOAL_STATUS_NEEDS_RECONCILIATION
|
||
{
|
||
return Err(format!("当前 Goal 状态不能编辑:{}", current.status));
|
||
}
|
||
if current.outcome == outcome
|
||
&& current.constraints == constraints
|
||
&& current.verification == verification
|
||
{
|
||
(current, false, false)
|
||
} else {
|
||
let prepared_finalization = if let Some(journal) =
|
||
read_game_creator_agent_runtime_finalization_journal(
|
||
root,
|
||
&agent_id,
|
||
¤t.run_id,
|
||
)? {
|
||
if game_creator_agent_runtime_finalization_assistant_exists(root, &journal)? {
|
||
return Err("当前 Goal 的 assistant 最终回复已经持久化,不能再编辑".to_string());
|
||
}
|
||
if journal.status != AGENT_RUNTIME_FINALIZATION_STATUS_PREPARED {
|
||
return Err("当前 Goal finalization 状态需要人工核对,不能编辑".to_string());
|
||
}
|
||
true
|
||
} else {
|
||
false
|
||
};
|
||
let updated = update_agent_goal_record_at_locked(
|
||
root,
|
||
&agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
|goal| {
|
||
if agent_goal_is_terminal(goal)
|
||
|| goal.status == AGENT_GOAL_STATUS_CLEARING
|
||
|| goal.status == AGENT_GOAL_STATUS_NEEDS_RECONCILIATION
|
||
{
|
||
return Err(format!("当前 Goal 状态不能编辑:{}", goal.status));
|
||
}
|
||
if goal.outcome == outcome
|
||
&& goal.constraints == constraints
|
||
&& goal.verification == verification
|
||
{
|
||
return Ok(());
|
||
}
|
||
goal.outcome = outcome.clone();
|
||
goal.constraints = constraints.clone();
|
||
goal.verification = verification.clone();
|
||
goal.revision = goal
|
||
.revision
|
||
.checked_add(1)
|
||
.ok_or_else(|| "Agent Goal revision 已达上限".to_string())?;
|
||
goal.completion_evidence.clear();
|
||
goal.response_fingerprint = None;
|
||
goal.completed_at = None;
|
||
goal.error = None;
|
||
Ok(())
|
||
},
|
||
)?;
|
||
(updated, true, prepared_finalization)
|
||
}
|
||
};
|
||
if !changed {
|
||
let runtime =
|
||
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(session_id))?;
|
||
return Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted: false,
|
||
});
|
||
}
|
||
let instruction = render_agent_goal_edit_instruction(&goal);
|
||
let mut provider_interrupted = false;
|
||
let runtime_before =
|
||
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(session_id))?;
|
||
if prepared_finalization {
|
||
if external_agent_runner_enabled() && !external_agent_runner_is_server_process() {
|
||
wake_external_agent_runner_pending_for_run(
|
||
root,
|
||
&agent_id,
|
||
&goal.run_id,
|
||
runtime_before.state.loop_iteration,
|
||
)?;
|
||
} else {
|
||
let _ = resume_game_creator_agent_background_tasks_at(root)?;
|
||
}
|
||
} else if goal.status == AGENT_GOAL_STATUS_ACTIVE
|
||
&& runtime_before.state.run_id == goal.run_id
|
||
&& matches!(
|
||
runtime_before.state.status.as_str(),
|
||
"running" | "waiting-for-confirmation" | "waiting-for-user-input"
|
||
)
|
||
{
|
||
if runtime_before.state.status == "waiting-for-user-input" {
|
||
if external_agent_runner_enabled() && !external_agent_runner_is_server_process() {
|
||
wake_external_agent_runner_pending_for_run(
|
||
root,
|
||
&agent_id,
|
||
&goal.run_id,
|
||
runtime_before.state.loop_iteration,
|
||
)?;
|
||
} else {
|
||
let _ = resume_game_creator_agent_background_tasks_at(root)?;
|
||
}
|
||
} else {
|
||
let steer_id = format!("goal-edit-{}-{}", goal.goal_id, goal.revision);
|
||
let steer = steer_game_creator_agent_runtime_task_at(
|
||
root,
|
||
&agent_id,
|
||
session_id,
|
||
&goal.run_id,
|
||
&steer_id,
|
||
&instruction,
|
||
"goal-edit",
|
||
)?;
|
||
provider_interrupted = steer.provider_interrupted;
|
||
if external_agent_runner_enabled()
|
||
&& !external_agent_runner_is_server_process()
|
||
&& !provider_interrupted
|
||
{
|
||
provider_interrupted =
|
||
steer_external_agent_runner(root, &agent_id, &goal.run_id, &steer_id)?;
|
||
}
|
||
if runtime_before.state.status == "waiting-for-confirmation" {
|
||
if external_agent_runner_enabled() && !external_agent_runner_is_server_process() {
|
||
wake_external_agent_runner_pending_for_run(
|
||
root,
|
||
&agent_id,
|
||
&goal.run_id,
|
||
runtime_before.state.loop_iteration,
|
||
)?;
|
||
} else {
|
||
let _ = resume_game_creator_agent_background_tasks_at(root)?;
|
||
}
|
||
}
|
||
}
|
||
} else if runtime_before.state.run_id == goal.run_id {
|
||
refresh_game_creator_agent_goal_runtime_projection_at(root, &goal)?;
|
||
}
|
||
let runtime =
|
||
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(session_id))?;
|
||
Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted,
|
||
})
|
||
}
|
||
|
||
pub(crate) fn pause_game_creator_agent_goal_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
goal_id: &str,
|
||
expected_revision: u64,
|
||
) -> Result<AgentGoalMutationResult, String> {
|
||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||
let (goal, already_paused) = {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.pause",
|
||
)?;
|
||
let mut already_paused = false;
|
||
let goal = update_agent_goal_record_at_locked(
|
||
root,
|
||
&agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
|goal| {
|
||
if goal.status == AGENT_GOAL_STATUS_PAUSED {
|
||
already_paused = true;
|
||
return Ok(());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_PAUSE_REQUESTED {
|
||
return Ok(());
|
||
}
|
||
if goal.status != AGENT_GOAL_STATUS_ACTIVE {
|
||
return Err(format!("当前 Goal 状态不能暂停:{}", goal.status));
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_PAUSE_REQUESTED.to_string();
|
||
goal.pause_requested_at = Some(unix_timestamp());
|
||
goal.error = None;
|
||
Ok(())
|
||
},
|
||
)?;
|
||
(goal, already_paused)
|
||
};
|
||
if already_paused {
|
||
let runtime =
|
||
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(session_id))?;
|
||
return Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted: false,
|
||
});
|
||
}
|
||
let provider_interrupted =
|
||
if external_agent_runner_enabled() && !external_agent_runner_is_server_process() {
|
||
pause_external_agent_runner(root, &agent_id, &goal.run_id)?
|
||
} else {
|
||
interrupt_game_creator_agent_runtime_provider_request_at(root, &agent_id, &goal.run_id)?
|
||
};
|
||
let runtime = pause_game_creator_agent_runtime_for_goal_at(root, &goal)?;
|
||
let goal = read_game_creator_agent_goal_at(root, &agent_id, session_id)?
|
||
.ok_or_else(|| "暂停后 Agent Goal sidecar 缺失".to_string())?;
|
||
Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted,
|
||
})
|
||
}
|
||
|
||
pub(crate) fn resume_game_creator_agent_goal_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
goal_id: &str,
|
||
expected_revision: u64,
|
||
) -> Result<AgentGoalMutationResult, String> {
|
||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||
let goal = {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.resume",
|
||
)?;
|
||
update_agent_goal_record_at_locked(
|
||
root,
|
||
&agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
|goal| {
|
||
if goal.status == AGENT_GOAL_STATUS_ACTIVE {
|
||
return Ok(());
|
||
}
|
||
if goal.status != AGENT_GOAL_STATUS_PAUSED {
|
||
return Err(format!("当前 Goal 状态不能恢复:{}", goal.status));
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_ACTIVE.to_string();
|
||
goal.pause_requested_at = None;
|
||
goal.paused_at = None;
|
||
goal.error = None;
|
||
Ok(())
|
||
},
|
||
)?
|
||
};
|
||
remove_game_creator_agent_runtime_cancel_request(root, &agent_id, &goal.run_id);
|
||
let runtime = resume_game_creator_agent_runtime_for_goal_at(root, &goal)?;
|
||
if external_agent_runner_enabled() && !external_agent_runner_is_server_process() {
|
||
wake_external_agent_runner_pending_for_run(
|
||
root,
|
||
&agent_id,
|
||
&goal.run_id,
|
||
runtime.state.loop_iteration,
|
||
)?;
|
||
} else {
|
||
let _ = resume_game_creator_agent_background_tasks_at(root)?;
|
||
}
|
||
let runtime =
|
||
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(session_id))?;
|
||
Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted: false,
|
||
})
|
||
}
|
||
|
||
pub(crate) fn clear_game_creator_agent_goal_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
goal_id: &str,
|
||
expected_revision: u64,
|
||
) -> Result<AgentGoalMutationResult, String> {
|
||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||
let goal = {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.clear",
|
||
)?;
|
||
let current = read_game_creator_agent_goal_at(root, &agent_id, session_id)?
|
||
.ok_or_else(|| "当前 Session 没有 Agent Goal".to_string())?;
|
||
if current.goal_id != goal_id || current.revision != expected_revision {
|
||
return Err(format!(
|
||
"Agent Goal 身份或 revision 已变化:goalId={} revision={}",
|
||
current.goal_id, current.revision
|
||
));
|
||
}
|
||
if !agent_goal_is_terminal(¤t) {
|
||
if let Some(journal) = read_game_creator_agent_runtime_finalization_journal(
|
||
root,
|
||
&agent_id,
|
||
¤t.run_id,
|
||
)? {
|
||
if game_creator_agent_runtime_finalization_assistant_exists(root, &journal)? {
|
||
return Err("当前 Goal 的 assistant 最终回复已经持久化,不能清理".to_string());
|
||
}
|
||
}
|
||
}
|
||
update_agent_goal_record_at_locked(
|
||
root,
|
||
&agent_id,
|
||
session_id,
|
||
goal_id,
|
||
expected_revision,
|
||
|goal| {
|
||
if goal.status == AGENT_GOAL_STATUS_CLEARED {
|
||
return Ok(());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_COMPLETED {
|
||
goal.status = AGENT_GOAL_STATUS_CLEARED.to_string();
|
||
goal.cleared_at = Some(unix_timestamp());
|
||
return Ok(());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_NEEDS_RECONCILIATION {
|
||
return Err("需要先核对 Goal/Runtime 身份,不能直接清理".to_string());
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_CLEARING.to_string();
|
||
goal.error = None;
|
||
Ok(())
|
||
},
|
||
)?
|
||
};
|
||
let mut provider_interrupted = false;
|
||
let runtime = if goal.status == AGENT_GOAL_STATUS_CLEARED {
|
||
refresh_game_creator_agent_goal_runtime_projection_at(root, &goal)?
|
||
} else if external_agent_runner_enabled() && !external_agent_runner_is_server_process() {
|
||
provider_interrupted = cancel_external_agent_runner_goal(root, &agent_id, &goal.run_id)?;
|
||
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(session_id))?
|
||
} else {
|
||
write_game_creator_agent_runtime_cancel_request(
|
||
root,
|
||
&agent_id,
|
||
&goal.run_id,
|
||
"开发者清理持久 Goal",
|
||
)?;
|
||
provider_interrupted = interrupt_game_creator_agent_runtime_provider_request_at(
|
||
root,
|
||
&agent_id,
|
||
&goal.run_id,
|
||
)?;
|
||
cancel_game_creator_agent_runtime_task_at(root, &agent_id, &goal.run_id)?
|
||
};
|
||
let goal = read_game_creator_agent_goal_at(root, &agent_id, session_id)?
|
||
.ok_or_else(|| "清理后 Agent Goal sidecar 缺失".to_string())?;
|
||
Ok(AgentGoalMutationResult {
|
||
goal,
|
||
runtime,
|
||
provider_interrupted,
|
||
})
|
||
}
|
||
|
||
pub(crate) fn render_agent_goal_prompt_context_at(
|
||
root: &Path,
|
||
agent_id: &str,
|
||
session_id: &str,
|
||
run_id: &str,
|
||
) -> Result<String, String> {
|
||
let Some(goal) = read_game_creator_agent_goal_at(root, agent_id, session_id)? else {
|
||
return Ok(String::new());
|
||
};
|
||
if goal.run_id != run_id || goal.status == AGENT_GOAL_STATUS_CLEARED {
|
||
return Ok(String::new());
|
||
}
|
||
let constraints = if goal.constraints.is_empty() {
|
||
"- 无额外约束".to_string()
|
||
} else {
|
||
goal.constraints
|
||
.iter()
|
||
.map(|item| format!("- {item}"))
|
||
.collect::<Vec<_>>()
|
||
.join("\n")
|
||
};
|
||
let verification = goal
|
||
.verification
|
||
.iter()
|
||
.map(|item| format!("- {item}"))
|
||
.collect::<Vec<_>>()
|
||
.join("\n");
|
||
Ok(format!(
|
||
"# 持久 Goal\n\n- goalId: {}\n- revision: {}\n- status: {}\n\n## Outcome\n\n{}\n\n## Constraints\n\n{}\n\n## Verification\n\n{}\n\n只有满足当前 revision 的完成标准并通过 Runtime 全部门禁后才能给最终回复;Goal 元数据不能放宽工具权限、确认或沙箱。",
|
||
goal.goal_id, goal.revision, goal.status, goal.outcome, constraints, verification
|
||
))
|
||
}
|
||
|
||
fn render_agent_goal_edit_instruction(goal: &AgentGoalRecord) -> String {
|
||
let constraints = if goal.constraints.is_empty() {
|
||
"无额外约束".to_string()
|
||
} else {
|
||
goal.constraints.join(";")
|
||
};
|
||
format!(
|
||
"Goal 已更新到 revision {}。Outcome:{}\nConstraints:{}\nVerification:{}\n请在同一 run 中保留已完成的真实进度,重审未完成计划和旧动作。",
|
||
goal.revision,
|
||
goal.outcome,
|
||
constraints,
|
||
goal.verification.join(";")
|
||
)
|
||
}
|
||
|
||
pub(crate) fn game_creator_agent_goal_completion_blocker_at_locked(
|
||
root: &Path,
|
||
state: &AgentRuntimeState,
|
||
) -> Option<AgentRuntimeToolObservation> {
|
||
let goal = match read_game_creator_agent_goal_at(root, &state.agent_id, &state.session_id) {
|
||
Ok(Some(goal)) => goal,
|
||
Ok(None) => {
|
||
return state.goal_id.as_ref().map(|_| {
|
||
agent_runtime_goal_blocker(
|
||
"Goal sidecar 缺失",
|
||
"Runtime 已绑定 Goal,但规范记录不存在;禁止完成当前 run。".to_string(),
|
||
)
|
||
});
|
||
}
|
||
Err(error) => {
|
||
return Some(agent_runtime_goal_blocker("Goal sidecar 无法读取", error));
|
||
}
|
||
};
|
||
let Some(goal_id) = state.goal_id.as_deref() else {
|
||
return (goal.run_id == state.run_id).then(|| {
|
||
agent_runtime_goal_blocker(
|
||
"Runtime 缺少 Goal 绑定",
|
||
format!(
|
||
"当前 Agent/Session/run 存在 Goal:goalId={} revision={};禁止按无 Goal 任务完成。",
|
||
goal.goal_id, goal.revision
|
||
),
|
||
)
|
||
});
|
||
};
|
||
if goal.goal_id != goal_id
|
||
|| goal.run_id != state.run_id
|
||
|| goal.revision != state.goal_revision
|
||
{
|
||
return Some(agent_runtime_goal_blocker(
|
||
"Goal 身份或 revision 已变化",
|
||
format!(
|
||
"stateGoalId={} stateRevision={} currentGoalId={} currentRevision={};旧回复必须丢弃并重规划。",
|
||
goal_id, state.goal_revision, goal.goal_id, goal.revision
|
||
),
|
||
));
|
||
}
|
||
if goal.status != AGENT_GOAL_STATUS_ACTIVE {
|
||
return Some(agent_runtime_goal_blocker(
|
||
"Goal 当前不能完成",
|
||
format!(
|
||
"status={};暂停、清理或待核对状态不能写最终回复。",
|
||
goal.status
|
||
),
|
||
));
|
||
}
|
||
None
|
||
}
|
||
|
||
fn agent_runtime_goal_blocker(summary: &str, detail: String) -> AgentRuntimeToolObservation {
|
||
AgentRuntimeToolObservation {
|
||
tool: "runtime.goal".to_string(),
|
||
status: "blocked".to_string(),
|
||
summary: summary.to_string(),
|
||
detail: Some(detail),
|
||
}
|
||
}
|
||
|
||
pub(crate) fn mark_game_creator_agent_goal_paused_for_runtime_at_locked(
|
||
root: &Path,
|
||
state: &AgentRuntimeState,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
let Some(goal_id) = state.goal_id.as_deref() else {
|
||
return Ok(None);
|
||
};
|
||
let mut goal = read_game_creator_agent_goal_at(root, &state.agent_id, &state.session_id)?
|
||
.ok_or_else(|| "暂停 Runtime 时 Agent Goal sidecar 缺失".to_string())?;
|
||
if goal.goal_id != goal_id
|
||
|| goal.run_id != state.run_id
|
||
|| goal.revision != state.goal_revision
|
||
{
|
||
return Err("暂停 Runtime 时 Agent Goal 身份或 revision 冲突".to_string());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_PAUSED {
|
||
return Ok(Some(goal));
|
||
}
|
||
if goal.status != AGENT_GOAL_STATUS_PAUSE_REQUESTED {
|
||
return Err(format!("暂停 Runtime 时 Goal 状态无效:{}", goal.status));
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_PAUSED.to_string();
|
||
goal.paused_at = Some(unix_timestamp());
|
||
goal.updated_at = unix_timestamp();
|
||
write_agent_goal_record(root, &goal)?;
|
||
Ok(Some(goal))
|
||
}
|
||
|
||
pub(crate) fn mark_game_creator_agent_goal_paused_for_runtime_at(
|
||
root: &Path,
|
||
state: &AgentRuntimeState,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.paused",
|
||
)?;
|
||
mark_game_creator_agent_goal_paused_for_runtime_at_locked(root, state)
|
||
}
|
||
|
||
pub(crate) fn mark_game_creator_agent_goal_cleared_for_runtime_at_locked(
|
||
root: &Path,
|
||
state: &AgentRuntimeState,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
let Some(goal_id) = state.goal_id.as_deref() else {
|
||
return Ok(None);
|
||
};
|
||
let mut goal = read_game_creator_agent_goal_at(root, &state.agent_id, &state.session_id)?
|
||
.ok_or_else(|| "清理 Runtime 时 Agent Goal sidecar 缺失".to_string())?;
|
||
if goal.goal_id != goal_id
|
||
|| goal.run_id != state.run_id
|
||
|| goal.revision != state.goal_revision
|
||
{
|
||
return Err("清理 Runtime 时 Agent Goal 身份或 revision 冲突".to_string());
|
||
}
|
||
if goal.status == AGENT_GOAL_STATUS_CLEARED {
|
||
return Ok(Some(goal));
|
||
}
|
||
if matches!(
|
||
goal.status.as_str(),
|
||
AGENT_GOAL_STATUS_ACTIVE | AGENT_GOAL_STATUS_PAUSE_REQUESTED
|
||
) {
|
||
goal.status = AGENT_GOAL_STATUS_PAUSED.to_string();
|
||
goal.pause_requested_at = None;
|
||
goal.paused_at = Some(unix_timestamp());
|
||
goal.error = Some("所属 Runtime 已取消;可显式恢复同一 Goal。".to_string());
|
||
goal.updated_at = unix_timestamp();
|
||
write_agent_goal_record(root, &goal)?;
|
||
return Ok(Some(goal));
|
||
}
|
||
if goal.status != AGENT_GOAL_STATUS_CLEARING {
|
||
return Ok(Some(goal));
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_CLEARED.to_string();
|
||
goal.cleared_at = Some(unix_timestamp());
|
||
goal.updated_at = unix_timestamp();
|
||
write_agent_goal_record(root, &goal)?;
|
||
Ok(Some(goal))
|
||
}
|
||
|
||
pub(crate) fn mark_game_creator_agent_goal_cleared_for_runtime_at(
|
||
root: &Path,
|
||
state: &AgentRuntimeState,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.cleared",
|
||
)?;
|
||
mark_game_creator_agent_goal_cleared_for_runtime_at_locked(root, state)
|
||
}
|
||
|
||
pub(crate) fn complete_game_creator_agent_goal_for_runtime_at_locked(
|
||
root: &Path,
|
||
state: &mut AgentRuntimeState,
|
||
response: &str,
|
||
) -> Result<Option<AgentGoalRecord>, String> {
|
||
let Some(goal_id) = state.goal_id.as_deref() else {
|
||
return Ok(None);
|
||
};
|
||
let mut goal = read_game_creator_agent_goal_at(root, &state.agent_id, &state.session_id)?
|
||
.ok_or_else(|| "完成 Runtime 时 Agent Goal sidecar 缺失".to_string())?;
|
||
if goal.goal_id != goal_id
|
||
|| goal.run_id != state.run_id
|
||
|| goal.revision != state.goal_revision
|
||
{
|
||
return Err("完成 Runtime 时 Agent Goal 身份或 revision 冲突".to_string());
|
||
}
|
||
let response_fingerprint = format!("{:x}", Sha256::digest(response.trim().as_bytes()));
|
||
if goal.status == AGENT_GOAL_STATUS_COMPLETED {
|
||
if goal.response_fingerprint.as_deref() != Some(response_fingerprint.as_str()) {
|
||
return Err("已完成 Agent Goal 的回复指纹冲突".to_string());
|
||
}
|
||
state.goal_status = Some(goal.status.clone());
|
||
return Ok(Some(goal));
|
||
}
|
||
if goal.status != AGENT_GOAL_STATUS_ACTIVE {
|
||
return Err(format!("当前 Agent Goal 状态不能完成:{}", goal.status));
|
||
}
|
||
let gate =
|
||
read_game_creator_agent_runtime_verification_gate(root, &state.agent_id, &state.run_id)?;
|
||
let completed_steps = state
|
||
.plan_steps
|
||
.iter()
|
||
.filter(|step| step.status == AGENT_RUNTIME_PLAN_STATUS_COMPLETED)
|
||
.count();
|
||
let mut completion_evidence = vec![
|
||
format!("goalRevision={}", goal.revision),
|
||
format!(
|
||
"planRevision={} completedSteps={completed_steps}",
|
||
state.plan_revision
|
||
),
|
||
format!(
|
||
"verificationRequired={} verifiedRevision={}",
|
||
gate.requires_verification,
|
||
gate.verified_revision
|
||
.map(|revision| revision.to_string())
|
||
.unwrap_or_else(|| "none".to_string())
|
||
),
|
||
format!("runId={} sessionId={}", state.run_id, state.session_id),
|
||
];
|
||
if state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
|
||
if let Some(blocker) = goal_contract_acceptance_completion_blocker_at_locked(root, state) {
|
||
return Err(format!(
|
||
"Agent Goal 的动态验收图尚未完成:{}",
|
||
blocker.summary()
|
||
));
|
||
}
|
||
if let Some(contract) =
|
||
read_game_creator_agent_runtime_goal_contract_at(root, &state.agent_id, &state.run_id)?
|
||
{
|
||
let acceptance = read_game_creator_agent_runtime_acceptance_graph_at(
|
||
root,
|
||
&state.agent_id,
|
||
&state.run_id,
|
||
)?
|
||
.ok_or_else(|| "Agent Goal 完成时缺少 Acceptance Graph 冻结快照".to_string())?;
|
||
completion_evidence.push(format!(
|
||
"goalContractFingerprint={}",
|
||
contract.contract_fingerprint
|
||
));
|
||
completion_evidence.push(format!(
|
||
"acceptanceRevision={} acceptanceStateFingerprint={}",
|
||
acceptance.revision, acceptance.state_fingerprint
|
||
));
|
||
}
|
||
}
|
||
goal.completion_evidence = completion_evidence;
|
||
goal.response_fingerprint = Some(response_fingerprint);
|
||
goal.status = AGENT_GOAL_STATUS_COMPLETED.to_string();
|
||
goal.completed_at = Some(unix_timestamp());
|
||
goal.updated_at = unix_timestamp();
|
||
goal.error = None;
|
||
write_agent_goal_record(root, &goal)?;
|
||
state.goal_status = Some(goal.status.clone());
|
||
Ok(Some(goal))
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub(crate) fn seed_game_creator_agent_goal_for_runtime_test_at(
|
||
root: &Path,
|
||
state: &mut AgentRuntimeState,
|
||
outcome: &str,
|
||
status: &str,
|
||
) -> Result<AgentGoalRecord, String> {
|
||
if !agent_goal_status_is_valid(status) {
|
||
return Err(format!("测试 Agent Goal 状态无效:{status}"));
|
||
}
|
||
let project_id = game_creator_agent_runtime_context_project_id(root)?;
|
||
let now = unix_timestamp();
|
||
let goal = AgentGoalRecord {
|
||
schema_version: AGENT_GOAL_SCHEMA_VERSION.to_string(),
|
||
project_id: project_id.clone(),
|
||
goal_id: new_agent_goal_id(
|
||
&project_id,
|
||
&state.agent_id,
|
||
&state.session_id,
|
||
&state.run_id,
|
||
),
|
||
agent_id: state.agent_id.clone(),
|
||
session_id: state.session_id.clone(),
|
||
run_id: state.run_id.clone(),
|
||
revision: 1,
|
||
status: status.to_string(),
|
||
outcome: normalize_agent_goal_text(outcome, AGENT_GOAL_OUTCOME_MAX_CHARS, "Goal outcome")?,
|
||
constraints: Vec::new(),
|
||
verification: vec![outcome.to_string()],
|
||
completion_evidence: Vec::new(),
|
||
response_fingerprint: None,
|
||
created_at: now,
|
||
pause_requested_at: (status == AGENT_GOAL_STATUS_PAUSE_REQUESTED).then_some(now),
|
||
paused_at: (status == AGENT_GOAL_STATUS_PAUSED).then_some(now),
|
||
completed_at: None,
|
||
cleared_at: None,
|
||
error: None,
|
||
updated_at: now,
|
||
};
|
||
{
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.test_seed",
|
||
)?;
|
||
write_agent_goal_record(root, &goal)?;
|
||
}
|
||
hydrate_game_creator_agent_goal_state_at(root, state)?;
|
||
state.updated_at = unix_timestamp();
|
||
append_game_creator_agent_runtime_task(root, state)?;
|
||
write_game_creator_agent_runtime_state(root, state)?;
|
||
Ok(goal)
|
||
}
|
||
|
||
#[cfg(test)]
|
||
pub(crate) fn revise_game_creator_agent_goal_for_runtime_test_at(
|
||
root: &Path,
|
||
state: &mut AgentRuntimeState,
|
||
outcome: &str,
|
||
) -> Result<AgentGoalRecord, String> {
|
||
let outcome = normalize_agent_goal_text(outcome, AGENT_GOAL_OUTCOME_MAX_CHARS, "Goal outcome")?;
|
||
let goal = {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.test_revise",
|
||
)?;
|
||
let goal_id = state
|
||
.goal_id
|
||
.as_deref()
|
||
.ok_or_else(|| "测试 Runtime 未绑定 Agent Goal".to_string())?;
|
||
update_agent_goal_record_at_locked(
|
||
root,
|
||
&state.agent_id,
|
||
&state.session_id,
|
||
goal_id,
|
||
state.goal_revision,
|
||
|goal| {
|
||
goal.outcome = outcome.clone();
|
||
goal.verification = vec![outcome.clone()];
|
||
goal.revision = goal
|
||
.revision
|
||
.checked_add(1)
|
||
.ok_or_else(|| "Agent Goal revision 已达上限".to_string())?;
|
||
Ok(())
|
||
},
|
||
)?
|
||
};
|
||
hydrate_game_creator_agent_goal_state_at(root, state)?;
|
||
state.updated_at = unix_timestamp();
|
||
append_game_creator_agent_runtime_task(root, state)?;
|
||
write_game_creator_agent_runtime_state(root, state)?;
|
||
Ok(goal)
|
||
}
|
||
|
||
fn mark_game_creator_agent_goal_paused_after_failure_at(
|
||
root: &Path,
|
||
original: &AgentGoalRecord,
|
||
error: &str,
|
||
) -> Result<(), String> {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.start_failed",
|
||
)?;
|
||
let mut goal = read_game_creator_agent_goal_at(root, &original.agent_id, &original.session_id)?
|
||
.ok_or_else(|| "启动失败后 Agent Goal sidecar 缺失".to_string())?;
|
||
if goal.goal_id != original.goal_id || goal.revision != original.revision {
|
||
return Err("启动失败后 Agent Goal 身份已变化".to_string());
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_PAUSED.to_string();
|
||
goal.paused_at = Some(unix_timestamp());
|
||
goal.error = Some(normalize_agent_goal_text(
|
||
error,
|
||
AGENT_GOAL_ITEM_MAX_CHARS,
|
||
"Goal error",
|
||
)?);
|
||
goal.updated_at = unix_timestamp();
|
||
write_agent_goal_record(root, &goal)
|
||
}
|
||
|
||
fn mark_game_creator_agent_goal_needs_reconciliation_at(
|
||
root: &Path,
|
||
original: &AgentGoalRecord,
|
||
error: &str,
|
||
) -> Result<(), String> {
|
||
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||
root,
|
||
"runtime.goal.reconciliation",
|
||
)?;
|
||
let mut goal = read_game_creator_agent_goal_at(root, &original.agent_id, &original.session_id)?
|
||
.ok_or_else(|| "Goal reconciliation sidecar 缺失".to_string())?;
|
||
if goal.goal_id != original.goal_id || goal.revision != original.revision {
|
||
return Err("Goal reconciliation 身份已变化".to_string());
|
||
}
|
||
goal.status = AGENT_GOAL_STATUS_NEEDS_RECONCILIATION.to_string();
|
||
goal.error = Some(normalize_agent_goal_text(
|
||
error,
|
||
AGENT_GOAL_ITEM_MAX_CHARS,
|
||
"Goal error",
|
||
)?);
|
||
goal.updated_at = unix_timestamp();
|
||
write_agent_goal_record(root, &goal)
|
||
}
|