Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/goal.rs
T
kdletters 1363b9374c
Project CI / Repository checks (push) Successful in 1m14s
Project CI / Frontend tests (push) Successful in 3m16s
Project CI / Backend tests (push) Successful in 4m26s
Project CI / Native shell tests (push) Successful in 16m2s
引入游戏智能体动态目标验收图 (#161)
## 变更

- 冻结根 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>
2026-08-12 10:34:20 +08:00

1275 lines
47 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 已有未结束 GoalgoalId={} 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 已有未结束 GoalgoalId={} 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(&current)
|| 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,
&current.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(&current) {
if let Some(journal) = read_game_creator_agent_runtime_finalization_journal(
root,
&agent_id,
&current.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 存在 GoalgoalId={} 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)
}