Files
Genarrative/apps/ai-game-creator-shell/src-tauri/src/agent/runtime_state.rs
T
kdletters b12a81e9c2
Project CI / Repository checks (push) Successful in 2m47s
Project CI / Frontend tests (push) Successful in 3m15s
Project CI / Backend tests (push) Successful in 7m12s
Project CI / Native shell tests (push) Failing after 13m9s
修复 AGC 自主运行时测试目录权限与任务目录加固
恢复 relaxed autonomous lane 的美术工具与写入门禁旁路

统一 Runtime 任务与事件目录的安全创建和校验

测试临时目录在 Windows 下初始化当前用户 owner
2026-09-12 20:31:43 +08:00

4320 lines
158 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::*;
static AGENT_RUNTIME_EVENT_ID_SEQUENCE: std::sync::atomic::AtomicU64 =
std::sync::atomic::AtomicU64::new(1);
pub(crate) const AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX: &str = "runtime-public-status-";
/// task journal 只在**读**的时候校验 phase 白名单,写侧不校验。所以一个没登记的
/// phase 落盘之后,整份 journal 从那一行起再也读不出来:`agent.run_status` 对该
/// Agent 永久失败,父 run 只能瞎转到 needs-reconciliation。实测就是这么炸的。
/// 让写方和白名单引用同一个常量,两边不可能再漂移。
fn game_creator_agent_runtime_public_status_message_id(
agent_id: &str,
session_id: &str,
run_id: &str,
status: &str,
) -> String {
let correlation_id =
game_creator_agent_runtime_message_correlation_id(agent_id, session_id, run_id);
game_creator_agent_runtime_public_status_message_id_for_correlation(&correlation_id, status)
}
fn game_creator_agent_runtime_public_status_message_id_for_correlation(
correlation_id: &str,
status: &str,
) -> String {
let status_fingerprint = format!("{:x}", Sha256::digest(status.as_bytes()));
format!(
"{AGENT_RUNTIME_PUBLIC_STATUS_MESSAGE_ID_PREFIX}{correlation_id}-{}",
status_fingerprint.chars().take(16).collect::<String>()
)
}
pub(crate) fn game_creator_agent_runtime_message_correlation_id(
agent_id: &str,
session_id: &str,
run_id: &str,
) -> String {
let identity = format!("{agent_id}\n{session_id}\n{run_id}");
let fingerprint = format!("{:x}", Sha256::digest(identity.as_bytes()));
fingerprint.chars().take(32).collect::<String>()
}
pub(crate) fn game_creator_agent_runtime_accepted_public_message(agent_id: &str) -> String {
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
"任务已接收,项目总控 Agent 正在启动处理。".to_string()
} else {
"任务已接收,专业 Agent 正在启动处理。".to_string()
}
}
pub(crate) fn append_game_creator_agent_runtime_public_status_message_at(
root: &Path,
agent_id: &str,
session_id: &str,
run_id: &str,
status: &str,
content: &str,
) -> Result<(), String> {
let message_id =
game_creator_agent_runtime_public_status_message_id(agent_id, session_id, run_id, status);
append_local_conversation_message_for_session_idempotent_at(
root,
None,
None,
LocalConversationMessage {
role: "assistant".to_string(),
content: content.to_string(),
agent_id: None,
},
&message_id,
)
.map(|_| ())
}
pub(crate) fn append_game_creator_agent_runtime_public_status_message_for_correlation_at(
root: &Path,
correlation_id: &str,
status: &str,
content: &str,
) -> Result<(), String> {
let message_id =
game_creator_agent_runtime_public_status_message_id_for_correlation(correlation_id, status);
append_local_conversation_message_for_session_idempotent_at(
root,
None,
None,
LocalConversationMessage {
role: "assistant".to_string(),
content: content.to_string(),
agent_id: None,
},
&message_id,
)
.map(|_| ())
}
pub(crate) fn append_game_creator_agent_runtime_terminal_public_message_at(
root: &Path,
state: &AgentRuntimeState,
error: &str,
) -> Result<(), String> {
let content = game_creator_agent_runtime_failure_conversation_message(&state.agent_id, error);
let status = if state.phase == "budget-exhausted" {
"budget-exhausted"
} else if state.phase == "needs-reconciliation" {
"needs-reconciliation"
} else {
"failed"
};
if state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& (state.parent_agent_id.is_some() || state.parent_run_id.is_some())
{
// Receipt and isolated-join continuations are internal Supervisor
// work. Their backend-owned Runtime event may remain visible, but a
// second Session message would duplicate that same terminal outcome
// in the formal project chat.
return Ok(());
}
if state.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& state.parent_agent_id.is_none()
&& state.parent_run_id.is_none()
{
return append_game_creator_agent_runtime_public_status_message_at(
root,
&state.agent_id,
&state.session_id,
&state.run_id,
status,
&content,
);
}
let message_id = game_creator_agent_runtime_public_status_message_id(
&state.agent_id,
&state.session_id,
&state.run_id,
status,
);
append_local_conversation_message_for_session_idempotent_at(
root,
Some(&state.agent_id),
Some(&state.session_id),
LocalConversationMessage {
role: "assistant".to_string(),
content,
agent_id: None,
},
&message_id,
)
.map(|_| ())
}
fn new_game_creator_agent_runtime_event_id(
state: &AgentRuntimeState,
event_type: &str,
phase: &str,
action_id: Option<&str>,
) -> String {
if let Some(action_id) = action_id {
return format!(
"runtime-event-action-{}-{}-{}-{}",
state.run_id, event_type, phase, action_id
);
}
let sequence =
AGENT_RUNTIME_EVENT_ID_SEQUENCE.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(
"runtime-event-{}-{}-{}-{}",
std::process::id(),
unix_millis(),
sequence,
event_type
)
}
fn game_creator_agent_runtime_event_type_is_public(event_type: &str) -> bool {
matches!(
event_type,
"thinking_summary"
| "plan"
| "plan_update"
| "action"
| "observation"
| "turn.started"
| "turn.progress"
| "turn.completed"
| "turn.failed"
| "turn.budget_exhausted"
| "turn.cancelled"
| "response"
| "response.stale"
| "goal.paused"
| "goal.resumed"
| "agent.delegate.result"
| "agent.delegate.result_failed"
) || event_type.starts_with("tool_confirmation.")
|| event_type.starts_with("user_input.")
}
fn game_creator_agent_runtime_public_event_text(
root: &Path,
event_type: &str,
summary: &str,
) -> Option<String> {
let event_type = event_type.trim();
if !game_creator_agent_runtime_event_type_is_public(event_type) {
return None;
}
let summary = redact_agent_runtime_error(root, summary.trim(), 240);
if summary.is_empty() {
return None;
}
let lower = summary.to_ascii_lowercase();
if [
"runtime.",
"agent.runtime.",
"provider.",
"provider_request.",
"parallel_read_batch.",
"provider_action_batch.",
"finalization.",
"context.",
"process_session.",
"steer.",
"autonomous_manifest.parent_wake",
"agent.delegate.parent_wake",
"command.exec",
"command.exec:",
"command.output_read",
"command.output_read:",
"agent.action_history",
"agent.action_history:",
]
.iter()
.any(|prefix| lower.starts_with(prefix))
{
return None;
}
if [
"sha256",
"fingerprint",
"authorization",
"bearer",
"api key",
"api_key",
"password",
"secret",
"cookie",
"token=",
"private process output",
"<absolute-path",
"<redacted-url",
"[redacted",
]
.iter()
.any(|marker| lower.contains(marker))
{
return None;
}
Some(summary)
}
pub(crate) fn start_game_creator_agent_runtime_turn_at(
root: &Path,
agent_id: &str,
prompt: &str,
run_id: &str,
) -> Result<AgentRuntimeState, String> {
start_game_creator_agent_runtime_turn_for_session_at(root, agent_id, None, prompt, run_id)
}
pub(crate) fn start_game_creator_agent_runtime_turn_for_session_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
prompt: &str,
run_id: &str,
) -> Result<AgentRuntimeState, String> {
start_game_creator_agent_runtime_task_for_session_at(
root,
agent_id,
session_id,
prompt,
run_id,
"agent-chat",
"读取项目上下文",
vec![
"读取项目记忆、黑板、本 Agent 私有记忆和最近对话".to_string(),
"按当前 Agent 职责独立推理本轮建议".to_string(),
"流式回复开发者并把本轮 runtime 状态落盘".to_string(),
],
)
}
pub(crate) fn start_game_creator_agent_runtime_task_at(
root: &Path,
agent_id: &str,
task: &str,
run_id: &str,
source: &str,
current_action: &str,
plan: Vec<String>,
) -> Result<AgentRuntimeState, String> {
start_game_creator_agent_runtime_task_for_session_at(
root,
agent_id,
None,
task,
run_id,
source,
current_action,
plan,
)
}
pub(crate) fn start_game_creator_agent_runtime_task_for_session_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
task: &str,
run_id: &str,
source: &str,
current_action: &str,
plan: Vec<String>,
) -> Result<AgentRuntimeState, String> {
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
validate_project_root(root)?;
with_agent_conversation_session_lane_at(root, &agent_id, "Agent Session Runtime 启动", || {
start_game_creator_agent_runtime_task_for_session_in_session_lane_at(
root,
&agent_id,
session_id,
task,
run_id,
source,
current_action,
plan,
)
})
}
pub(super) fn start_game_creator_agent_runtime_task_for_session_in_session_lane_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
task: &str,
run_id: &str,
source: &str,
current_action: &str,
plan: Vec<String>,
) -> Result<AgentRuntimeState, String> {
let isolated_instance = agent_id
.starts_with("child-")
.then(|| resolve_isolated_agent_instance_at(root, &agent_id))
.transpose()?;
let session_id = resolve_agent_conversation_session_id_at(root, &agent_id, session_id, true)?;
let run_id = normalize_game_creator_agent_runtime_run_id(&agent_id, run_id);
if isolated_instance
.as_ref()
.is_some_and(|instance| instance.session_id != session_id || instance.run_id != run_id)
{
return Err("动态隔离子 Agent Session/Run 与实例契约不一致".to_string());
}
let task = task.trim();
if task.is_empty() {
return Err("Agent Runtime 任务不能为空".to_string());
}
let runtime_task = sanitize_agent_runtime_text(task, AGENT_RUNTIME_TASK_MAX_CHARS);
if source.trim() == AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE {
ensure_game_creator_agent_delegate_receipt_conversation_at(
root,
&agent_id,
&session_id,
task,
)?;
}
let queued_task_record = read_latest_game_creator_agent_runtime_task_by_run_id(
root, &agent_id, &run_id,
)?
.filter(|record| {
record.session_id == session_id
&& record.source == source.trim()
&& matches!(
record.status.as_str(),
"pending" | "running" | "waiting-for-confirmation" | "waiting-for-user-input"
)
});
let task_link = queued_task_record
.as_ref()
.map(|record| AgentRuntimeTaskLink {
parent_agent_id: record.parent_agent_id.clone(),
parent_run_id: record.parent_run_id.clone(),
delegation_id: record.delegation_id.clone(),
})
.unwrap_or_default();
let previous_state =
read_game_creator_agent_runtime_for_session_at(root, &agent_id, Some(&session_id))
.ok()
.map(|result| result.state);
let mut state = default_game_creator_agent_runtime_state(&agent_id, &run_id);
// A queued autonomous task may be owned by an Agent whose runtime
// identity differs from the manifest task it is executing. Preserve the
// durable task_id when hydrating the state; falling back to agent_id keeps
// legacy/ordinary runs unchanged.
state.task_id = queued_task_record
.as_ref()
.map(|record| record.task_id.trim())
.filter(|task_id| !task_id.is_empty())
.unwrap_or(agent_id.as_ref())
.to_string();
state.started_at = queued_task_record
.as_ref()
.map(|record| record.updated_at)
.filter(|updated_at| *updated_at > 0)
.unwrap_or_else(unix_timestamp);
state.session_id = session_id;
state.source = source.trim().to_string();
let (run_profile, run_profile_binding_fingerprint) = agent_runtime_run_profile_identity_at(
root,
&agent_id,
&run_id,
queued_task_record
.as_ref()
.map(|record| record.run_profile.as_str()),
queued_task_record
.as_ref()
.map(|record| record.run_profile_binding_fingerprint.as_str()),
)?;
state.run_profile = run_profile;
state.run_profile_binding_fingerprint = run_profile_binding_fingerprint;
state.parent_agent_id = task_link.parent_agent_id;
state.parent_run_id = task_link.parent_run_id;
state.delegation_id = task_link.delegation_id;
state.status = "running".to_string();
state.phase = "planning".to_string();
state.current_task = runtime_task.clone();
state.current_goal = runtime_task.clone();
state.current_action = current_action.trim().to_string();
state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string();
state.next_step = "等待 Agent 输出计划或回复".to_string();
update_agent_runtime_plan_steps(&mut state, plan);
state.observations = vec!["已创建本轮 Agent Runtime run。".to_string()];
if let Some(previous_state) = previous_state {
let same_runtime_run = previous_state.agent_id == state.agent_id
&& previous_state.task_id == state.task_id
&& previous_state.session_id == state.session_id
&& previous_state.run_id == state.run_id
&& previous_state.source == state.source
&& previous_state.current_task == state.current_task;
if same_runtime_run {
if previous_state.started_at > 0 {
state.started_at = previous_state.started_at;
}
state.loop_iteration = previous_state.loop_iteration;
state.max_loop_iterations = previous_state.max_loop_iterations;
state.tool_action_budget = previous_state.tool_action_budget;
validate_agent_runtime_structured_plan_snapshot(
previous_state.plan_revision,
&previous_state.plan_explanation,
&previous_state.plan,
&previous_state.plan_steps,
previous_state.active_plan_step_index,
)?;
state.plan_revision = previous_state.plan_revision;
state.plan_explanation = previous_state.plan_explanation;
state.plan = previous_state.plan;
state.plan_steps = previous_state.plan_steps;
state.active_plan_step_index = previous_state.active_plan_step_index;
}
state.recent_tool_calls = previous_state.recent_tool_calls;
state.context_usage = previous_state.context_usage;
state.last_response = previous_state.last_response;
}
hydrate_game_creator_agent_goal_state_at(root, &mut state)?;
refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?;
state.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
let runtime_task_sha256 = format!("{:x}", Sha256::digest(runtime_task.as_bytes()));
let runtime_task_chars = runtime_task.chars().count();
let private_runtime_task = agent_runtime_task_requires_private_audit(
state.goal_id.as_deref(),
state.parent_agent_id.as_deref(),
);
let public_runtime_task = (!private_runtime_task).then(|| runtime_task.clone());
let runtime_event_detail = if private_runtime_task {
format!(
"goalRevision={} · taskChars={runtime_task_chars} · taskSha256={runtime_task_sha256} · goalBound={} · delegated={}",
state.goal_revision,
state.goal_id.is_some(),
state.parent_agent_id.is_some(),
)
} else {
runtime_task.clone()
};
append_game_creator_agent_runtime_event(
root,
&state,
"turn.started",
"running",
"planning",
"Agent Runtime 开始处理本轮输入。",
Some(&runtime_event_detail),
)?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.turn",
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"source": state.source,
"status": state.status,
"phase": state.phase,
"task": public_runtime_task,
"taskSha256": runtime_task_sha256,
"taskChars": runtime_task_chars,
"goalBound": state.goal_id.is_some(),
"delegated": state.parent_agent_id.is_some(),
}),
)?;
Ok(state)
}
pub(crate) fn advance_game_creator_agent_runtime_turn_at(
root: &Path,
mut state: AgentRuntimeState,
phase: &str,
action: &str,
observation: &str,
) -> Result<AgentRuntimeState, String> {
state.phase = phase.trim().to_string();
state.current_action = action.trim().to_string();
state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string();
state.next_step = agent_runtime_next_step_for_phase(&state.phase).to_string();
if !observation.trim().is_empty() {
state.observations.push(observation.trim().to_string());
}
refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?;
state.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
append_game_creator_agent_runtime_event(
root,
&state,
"turn.progress",
state.status.as_str(),
state.phase.as_str(),
state.current_action.as_str(),
Some(observation),
)?;
Ok(state)
}
pub(super) fn prepare_game_creator_agent_runtime_completed_state(
root: &Path,
mut state: AgentRuntimeState,
response: &str,
) -> Result<AgentRuntimeState, String> {
state.pending_tool_action = None;
state.status = "idle".to_string();
state.phase = "completed".to_string();
state.current_action = "等待下一轮输入".to_string();
state.waiting_on = "开发者下一轮输入".to_string();
state.next_step = "等待下一轮输入".to_string();
state.last_response = Some(sanitize_agent_runtime_text(
response,
static_delegate_result_detail_max_chars(response, 500),
));
state.error = None;
complete_agent_runtime_remaining_plan_steps(&mut state, "本轮 Agent 已生成最终回复。");
state
.observations
.push("Agent 已完成回复,assistant 消息等待或已经由前端落盘。".to_string());
refresh_game_creator_agent_runtime_tool_policy(root, &mut state)?;
state.updated_at = unix_timestamp();
Ok(state)
}
pub(crate) fn redact_agent_runtime_private_process_output_from_response(
response: &str,
observations: &[AgentRuntimeToolObservation],
) -> String {
if agent_runtime_observations_contain_private_process_output(observations) {
"持久进程交互已完成,私有进程输出已省略。".to_string()
} else {
response.trim().to_string()
}
}
pub(super) fn agent_runtime_observations_contain_private_process_output(
observations: &[AgentRuntimeToolObservation],
) -> bool {
observations.iter().any(|observation| {
observation.tool == "command.poll"
&& observation.status == "ok"
&& observation
.detail
.as_deref()
.and_then(|detail| serde_json::from_str::<serde_json::Value>(detail).ok())
.and_then(|detail| {
detail
.get("output")
.and_then(serde_json::Value::as_str)
.map(|output| !output.is_empty())
})
.unwrap_or(false)
})
}
pub(crate) fn finish_game_creator_agent_runtime_turn_at(
root: &Path,
state: AgentRuntimeState,
response: &str,
) -> Result<AgentRuntimeState, String> {
let mut state = prepare_game_creator_agent_runtime_completed_state(root, state, response)?;
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
append_game_creator_agent_runtime_event(
root,
&state,
"turn.completed",
"idle",
"completed",
"Agent Runtime 完成本轮处理。",
state.last_response.as_deref(),
)?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.completed",
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"source": state.source,
"responsePreview": state.last_response,
}),
)?;
remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?;
publish_game_creator_agent_delegate_result_for_state(
root,
&state,
state.last_response.as_deref(),
);
Ok(state)
}
pub(super) fn game_creator_agent_runtime_event_exists(
root: &Path,
agent_id: &str,
run_id: &str,
event_type: &str,
) -> Result<bool, String> {
let path = game_creator_agent_runtime_event_path(root, agent_id);
let file = match File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => {
return Err(format!(
"读取 Agent Runtime 事件失败:{}: {error}",
path.display()
))
}
};
for line in BufReader::new(file).lines() {
let line = line
.map_err(|error| format!("读取 Agent Runtime 事件失败:{}: {error}", path.display()))?;
if line.trim().is_empty() {
continue;
}
let event = serde_json::from_str::<AgentRuntimeEvent>(&line)
.map_err(|error| format!("解析 Agent Runtime 事件失败:{}: {error}", path.display()))?;
if event.run_id == run_id && event.event_type == event_type {
return Ok(true);
}
}
Ok(false)
}
pub(crate) fn agent_db_record_exists_for_finalization(
root: &Path,
expected_record: &serde_json::Value,
) -> Result<bool, String> {
let record_type = expected_record
.get("recordType")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| "finalization 审计缺少 recordType".to_string())?;
let finalization_id = expected_record
.get("finalizationId")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| "finalization 审计缺少 finalizationId".to_string())?;
let message_id = expected_record
.get("messageId")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| "finalization 审计缺少 messageId".to_string())?;
let path = root.join(".agent/agent.db");
let file = match File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => {
return Err(format!(
"读取 Agent 本地索引失败:{}: {error}",
path.display()
));
}
};
let mut exact_matches = 0usize;
for line in BufReader::new(file).lines() {
let line =
line.map_err(|error| format!("读取 Agent 本地索引失败:{}: {error}", path.display()))?;
if line.trim().is_empty() {
continue;
}
let record = serde_json::from_str::<serde_json::Value>(&line)
.map_err(|error| format!("解析 Agent 本地索引失败:{}: {error}", path.display()))?;
if record.get("recordType").and_then(serde_json::Value::as_str) != Some(record_type)
|| record
.get("finalizationId")
.and_then(serde_json::Value::as_str)
!= Some(finalization_id)
|| record.get("messageId").and_then(serde_json::Value::as_str) != Some(message_id)
{
continue;
}
if !agent_db_stored_record_matches_expected_payload(&record, expected_record) {
return Err(format!(
"Agent finalization 审计内容冲突:recordType={record_type} finalizationId={finalization_id}"
));
}
exact_matches = exact_matches.saturating_add(1);
if exact_matches > 1 {
return Err(format!(
"Agent finalization 审计重复:recordType={record_type} finalizationId={finalization_id}"
));
}
}
Ok(exact_matches == 1)
}
pub(super) fn agent_db_record_exists_for_action(
root: &Path,
record_type: &str,
agent_id: &str,
run_id: &str,
action_id: &str,
) -> Result<bool, String> {
let path = root.join(".agent/agent.db");
let file = match File::open(&path) {
Ok(file) => file,
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(false),
Err(error) => {
return Err(format!(
"读取 Agent 本地索引失败:{}: {error}",
path.display()
));
}
};
for line in BufReader::new(file).lines() {
let line =
line.map_err(|error| format!("读取 Agent 本地索引失败:{}: {error}", path.display()))?;
if line.trim().is_empty() {
continue;
}
let record = serde_json::from_str::<serde_json::Value>(&line)
.map_err(|error| format!("解析 Agent 本地索引失败:{}: {error}", path.display()))?;
if record.get("recordType").and_then(serde_json::Value::as_str) == Some(record_type)
&& record.get("agentId").and_then(serde_json::Value::as_str) == Some(agent_id)
&& record.get("runId").and_then(serde_json::Value::as_str) == Some(run_id)
&& record.get("actionId").and_then(serde_json::Value::as_str) == Some(action_id)
{
return Ok(true);
}
}
Ok(false)
}
pub(super) fn finish_game_creator_agent_background_runtime_turn_idempotently_at(
root: &Path,
state: AgentRuntimeState,
journal: &AgentRuntimeFinalizationJournal,
response: &str,
) -> Result<AgentRuntimeState, String> {
let mut completed = prepare_game_creator_agent_runtime_completed_state(root, state, response)?;
let expected_terminal_detail = completed.last_response.clone().unwrap_or_default();
let public_response_detail = format!(
"responseSha256={} responseChars={}",
journal.response_fingerprint,
journal.response.chars().count()
);
match read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&completed.agent_id,
&completed.run_id,
)? {
Some(task) if task.status == "completed" => {
if task.terminal_detail.as_deref() != Some(expected_terminal_detail.as_str()) {
return Err("Agent Runtime completed task 与 finalization 回复不匹配".to_string());
}
}
Some(task)
if matches!(
task.status.as_str(),
"failed" | "cancelled" | "budget-exhausted"
) =>
{
return Err(format!(
"Agent Runtime finalization 与既有终态冲突:{}",
task.status
));
}
_ => append_game_creator_agent_runtime_task(root, &completed)?,
}
refresh_game_creator_agent_runtime_task_queue(root, &mut completed)?;
write_game_creator_agent_runtime_state(root, &completed)?;
append_game_creator_agent_runtime_finalization_lifecycle_stage(
root,
journal,
"runtime-completed",
unix_timestamp(),
)?;
complete_game_creator_agent_goal_for_runtime_at_locked(root, &mut completed, response)?;
let goal_projection_matches = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&completed.agent_id,
&completed.run_id,
)?
.is_some_and(|task| {
task.status == "completed"
&& task.goal_id == completed.goal_id
&& task.goal_revision == completed.goal_revision
&& task.goal_status == completed.goal_status
});
if !goal_projection_matches {
append_game_creator_agent_runtime_task(root, &completed)?;
}
refresh_game_creator_agent_runtime_task_queue(root, &mut completed)?;
write_game_creator_agent_runtime_state(root, &completed)?;
append_game_creator_agent_runtime_finalization_lifecycle_stage(
root,
journal,
"goal-completed",
unix_timestamp(),
)?;
if !game_creator_agent_runtime_event_exists(
root,
&completed.agent_id,
&completed.run_id,
"turn.completed",
)? {
append_game_creator_agent_runtime_event(
root,
&completed,
"turn.completed",
"idle",
"completed",
"Agent Runtime 完成本轮处理。",
Some(&public_response_detail),
)?;
}
let completed_audit = serde_json::json!({
"recordType": "agent.runtime.completed",
"agentId": completed.agent_id,
"taskId": completed.task_id,
"sessionId": completed.session_id,
"runId": completed.run_id,
"source": completed.source,
"finalizationId": journal.finalization_id,
"messageId": journal.message_id,
"responseFingerprint": journal.response_fingerprint,
"responseChars": journal.response.chars().count(),
});
if !agent_db_record_exists_for_finalization(root, &completed_audit)? {
append_agent_db_record(root, completed_audit)?;
}
remove_game_creator_agent_runtime_pending_tool_action(
root,
&completed.agent_id,
&completed.run_id,
)?;
remove_game_creator_agent_runtime_provider_action_batch(
root,
&completed.agent_id,
&completed.run_id,
)?;
remove_game_creator_agent_runtime_confirmations(root, &completed.agent_id, &completed.run_id)?;
provider_retry::remove_at(root, &completed.agent_id, &completed.run_id)?;
publish_game_creator_agent_delegate_result_for_state_at_locked(
root,
&completed,
completed.last_response.as_deref(),
);
if !game_creator_agent_runtime_event_exists(
root,
&completed.agent_id,
&completed.run_id,
"response",
)? {
append_game_creator_agent_runtime_event(
root,
&completed,
"response",
completed.status.as_str(),
completed.phase.as_str(),
"Agent 已生成最终回复。",
Some(&public_response_detail),
)?;
}
let background_completed_audit = serde_json::json!({
"recordType": "agent.runtime.background_task.completed",
"agentId": completed.agent_id,
"taskId": completed.task_id,
"sessionId": completed.session_id,
"runId": completed.run_id,
"source": completed.source,
"finalizationId": journal.finalization_id,
"messageId": journal.message_id,
"responseFingerprint": journal.response_fingerprint,
"responseChars": journal.response.chars().count(),
});
if !agent_db_record_exists_for_finalization(root, &background_completed_audit)? {
append_agent_db_record(root, background_completed_audit)?;
}
Ok(completed)
}
pub(crate) fn prepare_game_creator_agent_background_stale_continuation_at(
root: &Path,
state: &mut AgentRuntimeState,
task: &str,
plan: &mut AgentRuntimeToolPlan,
observations: &mut Vec<AgentRuntimeToolObservation>,
context_tracker: &mut AgentRuntimeContextWindowTracker,
blocker: AgentRuntimeToolObservation,
) -> Result<AgentRuntimeContinuationContext, String> {
let blocker_summary = blocker.summary();
let blocker_detail = blocker.detail.clone();
let runtime_blocker_summary = blocker_detail
.as_deref()
.filter(|detail| !detail.trim().is_empty())
.map(|detail| format!("{blocker_summary}{detail}"))
.unwrap_or_else(|| blocker_summary.clone());
retry_agent_runtime_active_plan_step(
state,
"最终回复生成期间项目 revision 已变化,旧回复已丢弃并等待重新规划。",
);
state.status = "running".to_string();
state.phase = "observation".to_string();
state.current_action = "丢弃过期最终回复并回到 planning".to_string();
state.waiting_on = "Agent 重新规划并验证当前项目 revision".to_string();
state.next_step = "根据 runtime.verification blocker 重新规划、验证并生成当前回复".to_string();
state.error = None;
state.observations.push(runtime_blocker_summary);
state.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, state)?;
refresh_game_creator_agent_runtime_task_queue(root, state)?;
write_game_creator_agent_runtime_state(root, state)?;
append_game_creator_agent_runtime_event(
root,
state,
"response.stale",
state.status.as_str(),
state.phase.as_str(),
"最终回复生成期间项目 revision 已变化,已丢弃旧回复并在同一 run 重新规划。",
blocker_detail.as_deref(),
)?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.response_stale",
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"source": state.source,
"summary": blocker_summary,
"detail": blocker_detail,
}),
)?;
plan.actions.clear();
plan.response.clear();
context_tracker.record(&blocker);
observations.push(blocker);
*observations =
sanitize_game_creator_agent_runtime_context_observations_for_storage(root, observations);
let next_loop_index = usize::try_from(state.loop_iteration).unwrap_or(usize::MAX);
persist_game_creator_agent_runtime_context(
root,
state,
task,
plan,
observations,
next_loop_index,
context_tracker,
)?;
let mut continuation = AgentRuntimeContinuationContext {
plan: plan.clone(),
observations: observations.clone(),
next_loop_index,
context_stalled: false,
..AgentRuntimeContinuationContext::default()
};
context_tracker.apply_to_continuation(&mut continuation);
Ok(continuation)
}
pub(crate) fn game_creator_agent_runtime_finalization_assistant_exists(
root: &Path,
journal: &AgentRuntimeFinalizationJournal,
) -> Result<bool, String> {
let Some(message) = read_local_conversation_message_by_id_for_session_at(
root,
Some(&journal.agent_id),
Some(&journal.session_id),
&journal.message_id,
)?
else {
return Ok(false);
};
if message.role != "assistant"
|| message.content != journal.response
|| message.agent_id.as_deref() != Some(journal.agent_id.as_str())
{
return Err("Agent Runtime finalization messageId 与既有会话消息冲突".to_string());
}
Ok(true)
}
pub(super) fn advance_game_creator_agent_runtime_finalization_at<F>(
root: &Path,
state: AgentRuntimeState,
journal: &mut AgentRuntimeFinalizationJournal,
checkpoint: &mut F,
) -> Result<AgentRuntimeState, String>
where
F: FnMut(AgentRuntimeFinalizationCheckpoint) -> Result<(), String>,
{
let assistant_exists = game_creator_agent_runtime_finalization_assistant_exists(root, journal)?;
append_game_creator_agent_runtime_finalization_lifecycle_stage(
root,
journal,
"prepared",
journal.prepared_at,
)?;
match journal.status.as_str() {
AGENT_RUNTIME_FINALIZATION_STATUS_PREPARED => {
append_local_conversation_message_for_session_idempotent_with_finalization_at(
root,
Some(&journal.agent_id),
Some(&journal.session_id),
LocalConversationMessage {
role: "assistant".to_string(),
content: journal.response.clone(),
agent_id: None,
},
&journal.message_id,
&journal.finalization_id,
)?;
checkpoint(AgentRuntimeFinalizationCheckpoint::AssistantAppended)?;
let now = unix_timestamp();
journal.status = AGENT_RUNTIME_FINALIZATION_STATUS_ASSISTANT_PERSISTED.to_string();
journal.assistant_persisted_at = Some(now);
journal.updated_at = now;
write_game_creator_agent_runtime_finalization_journal(root, journal)?;
}
AGENT_RUNTIME_FINALIZATION_STATUS_ASSISTANT_PERSISTED
| AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED => {
if !assistant_exists {
return Err(
"Agent Runtime finalization 已记录 assistant,但会话消息不存在".to_string(),
);
}
}
_ => return Err("Agent Runtime finalization 状态无效".to_string()),
}
append_game_creator_agent_runtime_finalization_lifecycle_stage(
root,
journal,
"assistant-persisted",
journal.assistant_persisted_at.unwrap_or(journal.updated_at),
)?;
let completed = finish_game_creator_agent_background_runtime_turn_idempotently_at(
root,
state,
journal,
&journal.response,
)?;
if journal.status != AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED {
checkpoint(AgentRuntimeFinalizationCheckpoint::RuntimeCompleted)?;
let now = unix_timestamp();
journal.status = AGENT_RUNTIME_FINALIZATION_STATUS_RUNTIME_COMPLETED.to_string();
journal.runtime_completed_at = Some(now);
journal.updated_at = now;
write_game_creator_agent_runtime_finalization_journal(root, journal)?;
}
append_game_creator_agent_runtime_finalization_lifecycle_stage(
root,
journal,
"goal-completed",
journal.runtime_completed_at.unwrap_or(journal.updated_at),
)?;
mark_game_creator_agent_runtime_response_stream_committed_at(root, &completed, journal)?;
checkpoint(AgentRuntimeFinalizationCheckpoint::ResponseStreamCommitted)?;
remove_game_creator_agent_runtime_finalization_recovery_sidecars(
root,
&journal.agent_id,
&journal.run_id,
)?;
Ok(completed)
}
pub(super) fn record_game_creator_agent_runtime_finalization_pending(
root: &Path,
state: &AgentRuntimeState,
error: &str,
) {
let error = redact_agent_runtime_error(root, error, 500);
let mut visible_state = read_game_creator_agent_runtime_at(root, &state.agent_id)
.map(|result| result.state)
.unwrap_or_else(|_| state.clone());
let same_live_run = visible_state.run_id == state.run_id
&& !matches!(
visible_state.phase.as_str(),
"completed" | "cancelled" | "failed"
);
if same_live_run {
visible_state.status = "running".to_string();
visible_state.phase = "finalizing".to_string();
visible_state.current_action = "正在恢复最终回复持久化".to_string();
visible_state.waiting_on = "Agent Runtime finalization 恢复".to_string();
visible_state.next_step = "修复持久化错误后自动完成当前 run".to_string();
visible_state.error = Some(error.clone());
visible_state.updated_at = unix_timestamp();
let _ = append_game_creator_agent_runtime_task(root, &visible_state);
let _ = refresh_game_creator_agent_runtime_task_queue(root, &mut visible_state);
let _ = write_game_creator_agent_runtime_state(root, &visible_state);
}
let _ = append_game_creator_agent_runtime_event(
root,
&visible_state,
"finalization.pending",
visible_state.status.as_str(),
visible_state.phase.as_str(),
"最终回复已进入可恢复持久化,等待恢复完成。",
Some(&error),
);
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.finalization_pending",
"agentId": visible_state.agent_id.clone(),
"taskId": visible_state.task_id.clone(),
"sessionId": visible_state.session_id.clone(),
"runId": visible_state.run_id.clone(),
"source": visible_state.source.clone(),
"error": error,
}),
);
emit_game_creator_agent_runtime_update(root, &visible_state.agent_id);
}
pub(crate) fn finish_game_creator_agent_background_runtime_turn_with_checkpoint_at<F>(
root: &Path,
mut state: AgentRuntimeState,
response: &str,
response_revision: u64,
observations: &[AgentRuntimeToolObservation],
mut checkpoint: F,
) -> Result<AgentBackgroundFinalizationOutcome, String>
where
F: FnMut(AgentRuntimeFinalizationCheckpoint) -> Result<(), String>,
{
let _lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.background.complete",
)?;
if game_creator_agent_runtime_cancel_requested(root, &state) {
mark_game_creator_agent_runtime_cancelled_at_locked(
root,
&mut state,
"Agent 后台任务已按开发者请求取消",
Some("取消请求在最终回复持久化前生效。"),
)?;
return Ok(AgentBackgroundFinalizationOutcome::Cancelled(state));
}
let steer_snapshot =
read_game_creator_agent_runtime_steer_ledger(root, &state.agent_id, &state.run_id)?;
if steer_snapshot.entries.values().any(|entry| {
entry.identity.sequence > state.applied_steer_cursor || entry.status != "applied"
}) {
return Ok(AgentBackgroundFinalizationOutcome::Stale(
AgentRuntimeToolObservation {
tool: "runtime.steer".to_string(),
status: "stale".to_string(),
summary: "最终回复生成期间收到运行中追加指令,旧回复已作废".to_string(),
detail: Some(format!(
"responseSteerCursor={} · latestSteerSequence={}",
state.applied_steer_cursor,
steer_snapshot
.entries
.values()
.map(|entry| entry.identity.sequence)
.max()
.unwrap_or(0)
)),
},
));
}
let relaxed_autonomous = autonomous_relaxed_profile(&state);
let blocker = if relaxed_autonomous || response_is_static_delegate_user_input_envelope(response)
{
// Relaxed autonomous runs and clarification envelopes do not require
// manifest, project-revision, verification or platform-artifact reads
// before settling; cancellation/steer handling above still applies.
None
} else {
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
if let Some(blocker) = structured_plan_completion_blocker(&state) {
Some(blocker)
} else if let Some(blocker) =
game_creator_agent_goal_completion_blocker_at_locked(root, &state)
{
Some(blocker)
} else if let Some(blocker) =
goal_contract_acceptance_completion_blocker_at_locked(root, &state)
{
Some(blocker)
} else if let Some(blocker) = agent_runtime_non_verification_completion_blocker_at_locked(
root,
&state.agent_id,
&state.run_id,
) {
Some(blocker)
} else if let Some(blocker) =
autonomous_game_build_completion_blocker_at_locked(root, &state)
{
Some(blocker)
} else if current_revision.revision != response_revision {
Some(agent_runtime_verification_blocker(
"最终回复基于的项目 revision 已过期,不能把任务标记为完成",
format!(
"responseRevision={response_revision}, currentRevision={};请根据最新项目状态重新规划后再生成最终回复。",
current_revision.revision
),
))
} else {
evaluate_project_verification_completion_at_locked(
root,
&state.agent_id,
&state.run_id,
observations,
)?
}
};
if let Some(blocker) = blocker {
let detail = blocker.detail.as_deref().unwrap_or_default();
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.completion_blocked",
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"source": state.source,
"summary": blocker.summary,
"detail": detail,
}),
);
return Ok(AgentBackgroundFinalizationOutcome::Stale(blocker));
}
let response =
redact_agent_runtime_private_process_output_from_response(response, observations);
let mut journal = build_game_creator_agent_runtime_finalization_journal(
root,
&state,
&response,
response_revision,
)
.map_err(|error| redact_agent_runtime_error(root, &error, 500))?;
write_game_creator_agent_runtime_finalization_journal(root, &journal)
.map_err(|error| redact_agent_runtime_error(root, &error, 500))?;
if let Err(error) = append_game_creator_agent_runtime_finalization_lifecycle_stage(
root,
&journal,
"prepared",
journal.prepared_at,
) {
let error = redact_agent_runtime_error(root, &error, 500);
record_game_creator_agent_runtime_finalization_pending(root, &state, &error);
return Ok(AgentBackgroundFinalizationOutcome::Pending(error));
}
if let Err(error) = checkpoint(AgentRuntimeFinalizationCheckpoint::Prepared) {
let error = redact_agent_runtime_error(root, &error, 500);
record_game_creator_agent_runtime_finalization_pending(root, &state, &error);
return Ok(AgentBackgroundFinalizationOutcome::Pending(error));
}
match advance_game_creator_agent_runtime_finalization_at(
root,
state.clone(),
&mut journal,
&mut checkpoint,
) {
Ok(completed) => {
if let Err(error) =
close_game_creator_agent_runtime_steer_ledger_at_locked(root, &completed)
{
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.steer.close_failed",
"agentId": completed.agent_id,
"taskId": completed.task_id,
"sessionId": completed.session_id,
"runId": completed.run_id,
"source": completed.source,
"appliedSteerCursor": completed.applied_steer_cursor,
"error": sanitize_agent_runtime_text(&error, 240),
}),
);
}
Ok(AgentBackgroundFinalizationOutcome::Completed(completed))
}
Err(error) => {
let error = redact_agent_runtime_error(root, &error, 500);
record_game_creator_agent_runtime_finalization_pending(root, &state, &error);
Ok(AgentBackgroundFinalizationOutcome::Pending(error))
}
}
}
pub(crate) fn finish_game_creator_agent_background_runtime_turn_at(
root: &Path,
state: AgentRuntimeState,
response: &str,
response_revision: u64,
observations: &[AgentRuntimeToolObservation],
) -> Result<AgentBackgroundFinalizationOutcome, String> {
finish_game_creator_agent_background_runtime_turn_with_checkpoint_at(
root,
state,
response,
response_revision,
observations,
|_| Ok(()),
)
}
fn game_creator_agent_runtime_failure_metadata(error: &str) -> (String, usize) {
(
format!("{:x}", Sha256::digest(error.as_bytes())),
error.chars().count(),
)
}
fn game_creator_agent_runtime_public_failure_detail(agent_id: &str, error: &str) -> String {
game_creator_agent_runtime_failure_conversation_message(agent_id, error)
}
pub(crate) fn append_game_creator_agent_background_task_failed_audit(
root: &Path,
state: &AgentRuntimeState,
failure_kind: &'static str,
) -> Result<(), String> {
let error = state
.error
.as_deref()
.ok_or_else(|| "后台任务失败审计缺少私有错误诊断".to_string())?;
let (error_sha256, error_chars) = game_creator_agent_runtime_failure_metadata(error);
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.failed",
"agentId": state.agent_id,
"taskId": state.task_id,
"sessionId": state.session_id,
"runId": state.run_id,
"source": state.source,
"failureKind": failure_kind,
"errorSha256": error_sha256,
"errorChars": error_chars,
}),
)
}
pub(crate) fn fail_game_creator_agent_runtime_turn_at(
root: &Path,
mut state: AgentRuntimeState,
error: &str,
) -> Result<AgentRuntimeState, String> {
state.pending_tool_action = None;
state.status = "failed".to_string();
state.phase = "failed".to_string();
state.current_action = "等待开发者处理失败".to_string();
state.waiting_on = "开发者处理失败".to_string();
state.next_step = "等待开发者处理失败".to_string();
let public_error = redact_agent_runtime_error(root, error, 500);
state.error = Some(public_error.clone());
let _ = crate::error_report::report_agent_runtime_error(&state.agent_id, &public_error);
// The public terminal message is deliberately committed before the
// remaining Runtime projections. Even if a task/event/state write is the
// failing subsystem, the user still receives one stable failure outcome.
let public_status_result =
append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error);
write_non_terminal_isolated_child_cancel_tombstones_for_parent_at(
root,
&state.agent_id,
&state.run_id,
"父 Agent 任务失败,取消动态隔离子任务",
)?;
fail_agent_runtime_remaining_plan_steps(&mut state, &public_error);
let _ = refresh_game_creator_agent_runtime_tool_policy(root, &mut state);
state.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
append_game_creator_agent_runtime_event(
root,
&state,
"error",
"failed",
"failed",
"Agent Runtime 本轮处理失败。",
state.error.as_deref(),
)?;
append_game_creator_agent_runtime_event(
root,
&state,
"turn.failed",
"failed",
"failed",
"Agent Runtime 本轮处理失败。",
state.error.as_deref(),
)?;
remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?;
publish_game_creator_agent_delegate_result_for_state(root, &state, state.error.as_deref());
if public_status_result.is_err() {
append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error)?;
}
Ok(state)
}
pub(crate) fn fail_game_creator_agent_runtime_budget_at(
root: &Path,
mut state: AgentRuntimeState,
error: &str,
) -> Result<AgentRuntimeState, String> {
state.pending_tool_action = None;
state.status = "failed".to_string();
state.phase = "budget-exhausted".to_string();
state.current_action = "Agent loop 预算耗尽".to_string();
state.waiting_on = "开发者调整目标或重试".to_string();
state.next_step = "调整任务范围后重试".to_string();
let public_error = redact_agent_runtime_error(root, error, 500);
state.error = Some(public_error.clone());
let _ = crate::error_report::report_agent_runtime_error(&state.agent_id, &public_error);
let public_status_result =
append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error);
write_non_terminal_isolated_child_cancel_tombstones_for_parent_at(
root,
&state.agent_id,
&state.run_id,
"父 Agent 任务预算耗尽,取消动态隔离子任务",
)?;
fail_agent_runtime_remaining_plan_steps(&mut state, &public_error);
let _ = refresh_game_creator_agent_runtime_tool_policy(root, &mut state);
state.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &state)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut state)?;
write_game_creator_agent_runtime_state(root, &state)?;
append_game_creator_agent_runtime_event(
root,
&state,
"error",
"failed",
"budget-exhausted",
"Agent Runtime 达到 loop 预算但任务仍未收束。",
state.error.as_deref(),
)?;
append_game_creator_agent_runtime_event(
root,
&state,
"turn.budget_exhausted",
"failed",
"budget-exhausted",
"Agent Runtime 达到 loop 预算但任务仍未收束。",
state.error.as_deref(),
)?;
remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?;
publish_game_creator_agent_delegate_result_for_state(root, &state, state.error.as_deref());
if public_status_result.is_err() {
append_game_creator_agent_runtime_terminal_public_message_at(root, &state, &public_error)?;
}
Ok(state)
}
pub(crate) fn normalize_game_creator_runtime_agent_id(agent_id: &str) -> Result<String, String> {
let agent_id = agent_id.trim();
if agent_id.is_empty() {
return Err("Agent ID 不能为空".to_string());
}
if game_creator_runtime_agent_catalog()?
.get(agent_id)
.is_some()
{
return Ok(agent_id.to_string());
}
for group in GAME_CREATOR_AGENT_GROUP_DEFINITIONS {
for role in group.roles {
if game_creator_agent_role_alias_id(group.id, role.role) == agent_id {
return Ok(role.task_id.to_string());
}
}
}
if agent_id.starts_with("child-")
&& agent_id.len() <= 96
&& normalize_conversation_agent_id(agent_id).is_ok()
{
return Ok(agent_id.to_string());
}
Err(format!("未知 Agent{agent_id}"))
}
pub(crate) fn game_creator_runtime_template_agent_id_at(
root: &Path,
agent_id: &str,
) -> Result<String, String> {
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
if agent_id.starts_with("child-") {
return resolve_isolated_agent_instance_at(root, &agent_id)
.map(|instance| instance.template_agent_id);
}
Ok(agent_id)
}
pub(super) fn game_creator_agent_role_alias_id(group: &str, role: &str) -> String {
format!("{group}-{role}")
.to_lowercase()
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() || character == '_' || character == '-' {
character
} else {
'-'
}
})
.collect()
}
pub(crate) fn normalize_game_creator_agent_runtime_run_id(agent_id: &str, run_id: &str) -> String {
let run_id = run_id.trim();
if run_id.is_empty() {
format!("agent-runtime-{agent_id}-{}", unix_timestamp())
} else {
truncate_agent_runtime_text(run_id, 160)
}
}
pub(super) fn unique_game_creator_agent_runtime_run_id(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<String, String> {
let base_run_id = normalize_game_creator_agent_runtime_run_id(agent_id, run_id);
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records = read_all_game_creator_agent_runtime_tasks(&path)?;
let used_run_ids = records
.into_iter()
.map(|record| record.run_id)
.collect::<std::collections::BTreeSet<_>>();
if !used_run_ids.contains(&base_run_id) {
return Ok(base_run_id);
}
let prefix = truncate_agent_runtime_text(&base_run_id, 118);
for attempt in 0..100 {
let candidate = format!("{prefix}-dup-{}-{}", unix_timestamp_nanos(), attempt);
if !used_run_ids.contains(&candidate) {
return Ok(candidate);
}
}
Err(format!("无法生成唯一 Agent Runtime runId{base_run_id}"))
}
pub(crate) fn default_game_creator_agent_runtime_state(
agent_id: &str,
run_id: &str,
) -> AgentRuntimeState {
AgentRuntimeState {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: agent_id.to_string(),
task_id: agent_id.to_string(),
session_id: format!("agent-session-{agent_id}"),
run_id: run_id.to_string(),
source: "agent-chat".to_string(),
run_profile: default_agent_runtime_run_profile(),
run_profile_binding_fingerprint: String::new(),
parent_agent_id: None,
parent_run_id: None,
delegation_id: None,
status: "idle".to_string(),
phase: "idle".to_string(),
current_task: String::new(),
current_goal: String::new(),
goal_id: None,
goal_revision: 0,
goal_status: None,
goal_outcome: None,
goal_constraints: Vec::new(),
goal_verification: Vec::new(),
current_action: "等待输入".to_string(),
waiting_on: "开发者输入".to_string(),
next_step: "等待输入".to_string(),
loop_iteration: 0,
plan_update_idle_rounds: 0,
stale_finalization_rounds: 0,
max_loop_iterations: AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT as u32,
tool_action_budget: AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT as u32,
plan_revision: 0,
plan_explanation: String::new(),
plan: vec![
"读取项目上下文".to_string(),
"按角色职责推理".to_string(),
"回复并记录 runtime 事件".to_string(),
],
plan_steps: Vec::new(),
active_plan_step_index: None,
observations: Vec::new(),
recent_tool_calls: Vec::new(),
pending_tool_action: None,
task_queue: AgentRuntimeTaskQueueSummary::default(),
allowed_tools: default_game_creator_agent_runtime_allowed_tools_for_agent(agent_id),
tool_policy: AgentRuntimeToolPolicySnapshot::default(),
applied_steer_cursor: 0,
applied_steer_refs: Vec::new(),
queued_steer_count: 0,
context_usage: AgentRuntimeContextUsage::default(),
last_response: None,
error: None,
started_at: 0,
updated_at: unix_timestamp(),
}
}
pub(crate) fn agent_runtime_state_from_task_record(
record: &AgentRuntimeTaskRecord,
) -> AgentRuntimeState {
let mut state = default_game_creator_agent_runtime_state(&record.agent_id, &record.run_id);
state.task_id = record.task_id.clone();
state.session_id = record.session_id.clone();
state.source = record.source.clone();
state.run_profile = record.run_profile.clone();
state.run_profile_binding_fingerprint = record.run_profile_binding_fingerprint.clone();
state.parent_agent_id = record.parent_agent_id.clone();
state.parent_run_id = record.parent_run_id.clone();
state.delegation_id = record.delegation_id.clone();
state.goal_id = record.goal_id.clone();
state.goal_revision = record.goal_revision;
state.goal_status = record.goal_status.clone();
state.status = record.status.clone();
state.phase = record.phase.clone();
state.current_task = record.task.clone();
state.current_goal = record.task.clone();
state.current_action = record.current_action.clone();
state.waiting_on = agent_runtime_waiting_on_for_phase(&record.phase).to_string();
state.next_step = agent_runtime_next_step_for_phase(&record.phase).to_string();
state.error = record.error.clone();
state.updated_at = record.updated_at;
state
}
pub(crate) fn default_game_creator_agent_runtime_allowed_tools() -> Vec<String> {
agent_runtime_executable_tools()
.into_iter()
.map(str::to_string)
.collect()
}
/// Return the durable Runtime tool surface for an Agent identity.
///
pub(crate) fn default_game_creator_agent_runtime_allowed_tools_for_agent(
_agent_id: &str,
) -> Vec<String> {
default_game_creator_agent_runtime_allowed_tools()
}
#[cfg(test)]
mod runtime_error_redaction_tests {
use super::*;
#[test]
fn runtime_error_redaction_keeps_http_diagnostics_while_redacting_values() {
let value = concat!(
"陶泥儿请求失败:HTTP 401code=invalid-tokenfield=authorization",
"message=token=token-value-123authorization: Bearer bearer-value-456",
"payload={\"token\":\"json-token-value-789\",\"api_key\":\"api-value-012\"}"
);
let redacted = redact_agent_runtime_error(Path::new("."), value, 1_000);
assert!(redacted.contains("HTTP 401"), "{redacted}");
assert!(redacted.contains("code=invalid-token"), "{redacted}");
assert!(redacted.contains("field=authorization"), "{redacted}");
assert!(redacted.contains("message="), "{redacted}");
for secret in [
"token-value-123",
"bearer-value-456",
"json-token-value-789",
"api-value-012",
] {
assert!(!redacted.contains(secret), "{secret} leaked in {redacted}");
}
assert!(
!redacted.contains("[redacted sensitive context]"),
"{redacted}"
);
}
#[test]
fn runtime_error_redaction_hides_private_key_and_config_names_without_losing_status() {
let value = concat!(
"平台返回 HTTP 403reason=forbiddendetail=读取 .env.production 失败\n",
"-----BEGIN PRIVATE KEY-----\n",
"PRIVATE-KEY-VALUE-123\n",
"-----END PRIVATE KEY-----\n",
"message=拒绝访问"
);
let redacted = redact_agent_runtime_error(Path::new("."), value, 1_000);
assert!(redacted.contains("HTTP 403"), "{redacted}");
assert!(redacted.contains("reason=forbidden"), "{redacted}");
assert!(redacted.contains("message=拒绝访问"), "{redacted}");
assert!(!redacted.contains(".env.production"), "{redacted}");
assert!(!redacted.contains("PRIVATE-KEY-VALUE-123"), "{redacted}");
}
#[test]
fn runtime_error_redaction_handles_escaped_json_wrapped_bearer_and_camel_case_keys() {
let value = concat!(
r#"HTTP 401code=invalid-tokenpayload={\"token\":\"json-token-value-789\",\"clientsecret\":\"client-secret-value-123\"}"#,
r#"escaped={\"token\":\"raw\"tail-secret-value\"}"#,
r#"authorization: Bearer \"quoted-bearer-value-456\""#,
"authorization: Bearer <angle-bearer-value-567>",
"privatekey=private-key-value-890secretkey=secret-key-value-901",
"authtoken=auth-token-value-012bearer=bare-bearer-value-345",
"authorization => arrow-authorization-value-678"
);
let redacted = redact_agent_runtime_error(Path::new("."), value, 2_000);
assert!(redacted.contains("HTTP 401"), "{redacted}");
assert!(redacted.contains("code=invalid-token"), "{redacted}");
for secret in [
"json-token-value-789",
"client-secret-value-123",
"tail-secret-value",
"quoted-bearer-value-456",
"angle-bearer-value-567",
"private-key-value-890",
"secret-key-value-901",
"auth-token-value-012",
"bare-bearer-value-345",
"arrow-authorization-value-678",
] {
assert!(!redacted.contains(secret), "{secret} leaked in {redacted}");
}
assert!(redacted.contains("[redacted-secret]"), "{redacted}");
}
}
pub(super) fn normalize_game_creator_agent_runtime_state(
state: &mut AgentRuntimeState,
agent_id: &str,
) {
if state.schema_version.trim().is_empty() {
state.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string();
}
if state.agent_id.trim().is_empty() {
state.agent_id = agent_id.to_string();
}
if state.task_id.trim().is_empty() {
state.task_id = agent_id.to_string();
}
if state.session_id.trim().is_empty() {
state.session_id = format!("agent-session-{agent_id}");
}
if state.source.trim().is_empty() {
state.source = "agent-chat".to_string();
}
if state.status.trim().is_empty() {
state.status = "idle".to_string();
}
if state.phase.trim().is_empty() {
state.phase = "idle".to_string();
}
if state.current_goal.trim().is_empty() && !state.current_task.trim().is_empty() {
state.current_goal = state.current_task.clone();
}
if state.current_action.trim().is_empty() {
state.current_action = "等待输入".to_string();
}
if state.waiting_on.trim().is_empty() {
state.waiting_on = agent_runtime_waiting_on_for_phase(&state.phase).to_string();
}
if state.next_step.trim().is_empty() {
state.next_step = agent_runtime_next_step_for_phase(&state.phase).to_string();
}
if state.max_loop_iterations == 0 {
state.max_loop_iterations = AGENT_RUNTIME_BACKGROUND_LOOP_LIMIT as u32;
}
if state.tool_action_budget == 0 {
state.tool_action_budget = AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT as u32;
}
state.plan_explanation = if agent_runtime_has_structured_plan(state) {
sanitize_agent_runtime_text(&state.plan_explanation, 240)
} else {
String::new()
};
if state.plan.is_empty() && !agent_runtime_has_structured_plan(state) {
state.plan = vec![
"读取项目上下文".to_string(),
"按角色职责推理".to_string(),
"回复并记录 runtime 事件".to_string(),
];
}
if state.plan_steps.is_empty()
&& !state.plan.is_empty()
&& !agent_runtime_has_structured_plan(state)
{
state.plan_steps = state
.plan
.iter()
.filter(|item| !item.trim().is_empty())
.take(AGENT_RUNTIME_PLAN_STEP_LIMIT)
.enumerate()
.map(|(index, title)| AgentRuntimePlanStep {
index: index as u32,
title: sanitize_agent_runtime_text(title, 180),
status: if state.phase == "completed" {
"completed"
} else if index == 0 && state.phase != "idle" {
"active"
} else {
"pending"
}
.to_string(),
detail: None,
updated_at: state.updated_at,
})
.collect();
}
if !agent_runtime_has_structured_plan(state)
&& state.plan_steps.len() > AGENT_RUNTIME_PLAN_STEP_LIMIT
{
state.plan_steps.truncate(AGENT_RUNTIME_PLAN_STEP_LIMIT);
}
if !agent_runtime_has_structured_plan(state) {
if let Some(active_index) = state.active_plan_step_index {
if !state
.plan_steps
.iter()
.any(|step| step.index == active_index)
{
state.active_plan_step_index = None;
}
}
} else if let Err(error) = validate_agent_runtime_structured_plan_snapshot(
state.plan_revision,
&state.plan_explanation,
&state.plan,
&state.plan_steps,
state.active_plan_step_index,
) {
state.status = "needs-reconciliation".to_string();
state.phase = "needs-reconciliation".to_string();
state.current_action = "结构化计划状态损坏".to_string();
state.waiting_on = "人工核对当前 run 的计划快照".to_string();
state.next_step = "修复计划快照后恢复当前 run".to_string();
state.error = Some(sanitize_agent_runtime_text(&error, 500));
}
if state.allowed_tools.is_empty() {
state.allowed_tools = default_game_creator_agent_runtime_allowed_tools();
} else {
for tool in default_game_creator_agent_runtime_allowed_tools() {
if !state.allowed_tools.iter().any(|existing| existing == &tool) {
state.allowed_tools.push(tool);
}
}
}
if state.updated_at == 0 {
state.updated_at = unix_timestamp();
}
if state.tool_policy.allowed_tools.is_empty() {
state.tool_policy.allowed_tools = agent_runtime_executable_tools()
.into_iter()
.map(str::to_string)
.collect();
}
if state.recent_tool_calls.len() > AGENT_RUNTIME_RECENT_TOOL_CALL_LIMIT {
let keep_from = state
.recent_tool_calls
.len()
.saturating_sub(AGENT_RUNTIME_RECENT_TOOL_CALL_LIMIT);
state.recent_tool_calls = state.recent_tool_calls.split_off(keep_from);
}
}
pub(super) fn agent_runtime_next_step_for_phase(phase: &str) -> &'static str {
match phase.trim() {
"planning" => "等待 Agent 输出计划或回复",
"action" => "等待工具观察结果",
"waiting-for-confirmation" => "等待开发者确认工具动作",
"waiting-for-user-input" => "等待用户提交全部澄清回答",
"waiting-for-provider-retry" => "等待持久退避到期后恢复同一 Provider 请求",
"response" => "等待 Agent 整理最终回复",
"completed" | "idle" => "等待下一轮输入",
"cancelled" => "可重试该后台任务或提交新任务",
"paused" | "pausing" => "等待开发者恢复持久 Goal",
"failed" => "等待开发者处理失败",
_ => "继续推进当前任务",
}
}
pub(super) fn agent_runtime_waiting_on_for_phase(phase: &str) -> &'static str {
match phase.trim() {
"planning" => "Agent 输出计划或回复",
"llm" => "Agent LLM 回复",
"action" => "工具观察结果",
"waiting-for-confirmation" => "开发者确认 Agent 工具动作",
"waiting-for-user-input" => "用户回答 Agent 的澄清问题",
"waiting-for-provider-retry" => "Provider 瞬态重试退避到期",
"response" => "Agent 整理最终回复",
"completed" | "idle" => "开发者下一轮输入",
"cancelled" => "开发者下一轮输入",
"paused" | "pausing" => "开发者恢复持久 Goal",
"failed" => "开发者处理失败",
_ => "当前任务推进",
}
}
pub(super) fn normalize_game_creator_agent_runtime_event(event: &mut AgentRuntimeEvent) {
if event.schema_version.trim().is_empty() {
event.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string();
}
if event.agent_id.trim().is_empty() {
event.agent_id = event.task_id.clone();
}
if event.task_id.trim().is_empty() {
event.task_id = event.agent_id.clone();
}
if event.session_id.trim().is_empty() {
event.session_id = format!("agent-session-{}", event.agent_id);
}
if event.source.trim().is_empty() {
event.source = "agent-chat".to_string();
}
if event.event_type.trim().is_empty() {
event.event_type = "turn.event".to_string();
}
if event.status.trim().is_empty() {
event.status = "idle".to_string();
}
if event.phase.trim().is_empty() {
event.phase = "idle".to_string();
}
if event.updated_at == 0 {
event.updated_at = unix_timestamp();
}
}
pub(super) fn normalize_game_creator_agent_runtime_task(record: &mut AgentRuntimeTaskRecord) {
if record.schema_version.trim().is_empty() {
record.schema_version = AGENT_RUNTIME_SCHEMA_VERSION.to_string();
}
if record.agent_id.trim().is_empty() {
record.agent_id = record.task_id.clone();
}
if record.task_id.trim().is_empty() {
record.task_id = record.agent_id.clone();
}
if record.session_id.trim().is_empty() {
record.session_id = format!("agent-session-{}", record.agent_id);
}
if record.run_id.trim().is_empty() {
record.run_id = format!("agent-runtime-{}-{}", record.agent_id, record.updated_at);
}
if record.source.trim().is_empty() {
record.source = "agent-chat".to_string();
}
if record.run_profile.trim().is_empty() {
record.run_profile = default_agent_runtime_run_profile();
}
if record.status.trim().is_empty() {
record.status = "idle".to_string();
}
if record.phase == "completed" && record.status == "idle" {
record.status = "completed".to_string();
}
if record.phase == "failed" && record.status != "failed" {
record.status = "failed".to_string();
}
if record.phase.trim().is_empty() {
record.phase = "idle".to_string();
}
if record.current_action.trim().is_empty() {
record.current_action = "等待输入".to_string();
}
if record.updated_at == 0 {
record.updated_at = unix_timestamp();
}
}
pub(super) fn validate_game_creator_agent_runtime_task_goal_binding(
record: &AgentRuntimeTaskRecord,
) -> Result<(), String> {
match record.goal_id.as_deref() {
None if record.goal_revision == 0 && record.goal_status.is_none() => Ok(()),
Some(goal_id)
if !goal_id.trim().is_empty()
&& record.goal_revision > 0
&& record
.goal_status
.as_deref()
.is_some_and(agent_goal_status_is_valid) =>
{
Ok(())
}
_ => Err(format!(
"Agent Runtime task Goal 绑定无效:runId={}",
record.run_id
)),
}
}
pub(super) fn game_creator_agent_runtime_session_path(root: &Path, agent_id: &str) -> PathBuf {
root.join(".agent")
.join("runtime")
.join("agents")
.join(format!("{agent_id}.json"))
}
pub(crate) fn game_creator_agent_runtime_event_path(root: &Path, agent_id: &str) -> PathBuf {
root.join(".agent")
.join("runtime")
.join("events")
.join(format!("{agent_id}.jsonl"))
}
pub(crate) fn game_creator_agent_runtime_task_path(root: &Path, agent_id: &str) -> PathBuf {
root.join(".agent")
.join("runtime")
.join("tasks")
.join(format!("{agent_id}.jsonl"))
}
pub(super) fn game_creator_agent_runtime_cancel_path(
root: &Path,
agent_id: &str,
run_id: &str,
) -> PathBuf {
root.join(".agent")
.join("runtime")
.join("cancel")
.join(agent_id)
.join(format!("{run_id}.json"))
}
pub(crate) fn write_game_creator_agent_runtime_cancel_request(
root: &Path,
agent_id: &str,
run_id: &str,
reason: &str,
) -> Result<(), String> {
let path = game_creator_agent_runtime_cancel_path(root, agent_id, run_id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent Runtime 取消目录失败:{}: {error}",
parent.display()
)
})?;
}
let payload = serde_json::json!({
"agentId": agent_id,
"runId": run_id,
"reason": sanitize_agent_runtime_text(reason, 180),
"updatedAt": unix_timestamp(),
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("序列化 Agent Runtime 取消请求失败:{error}"))?;
fs::write(&path, format!("{content}\n")).map_err(|error| {
format!(
"写入 Agent Runtime 取消请求失败:{}: {error}",
path.display()
)
})
}
pub(super) fn write_non_terminal_isolated_child_cancel_tombstones_for_parent_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
reason: &str,
) -> Result<(), String> {
for child in list_non_terminal_isolated_children_for_parent_cancel_at(
root,
parent_agent_id,
parent_run_id,
)? {
write_game_creator_agent_runtime_cancel_request(
root,
&child.instance_id,
&child.run_id,
reason,
)?;
}
Ok(())
}
pub(crate) fn remove_game_creator_agent_runtime_cancel_request(
root: &Path,
agent_id: &str,
run_id: &str,
) {
let _ = fs::remove_file(game_creator_agent_runtime_cancel_path(
root, agent_id, run_id,
));
}
pub(crate) fn game_creator_agent_runtime_cancel_requested(
root: &Path,
state: &AgentRuntimeState,
) -> bool {
game_creator_agent_runtime_cancel_requested_for(root, &state.agent_id, &state.run_id)
}
pub(super) fn game_creator_agent_runtime_cancel_requested_for(
root: &Path,
agent_id: &str,
run_id: &str,
) -> bool {
if run_id.trim().is_empty() {
return false;
}
game_creator_agent_runtime_cancel_path(root, agent_id, run_id).exists()
}
#[derive(Debug)]
pub(crate) struct AgentRuntimeTaskLock {
pub(super) file: Option<File>,
}
#[cfg(unix)]
static AGENT_RUNTIME_LOCK_OPEN_GUARD: OnceLock<Mutex<()>> = OnceLock::new();
impl Drop for AgentRuntimeTaskLock {
fn drop(&mut self) {
self.file.take();
}
}
pub(crate) fn try_acquire_game_creator_agent_runtime_task_lock(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskLock>, String> {
let relative_path = format!(".agent/runtime/locks/{agent_id}.lock");
let path = root.join(&relative_path);
let Some(mut file) = try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?
else {
return Ok(None);
};
let token = format!("{}-{}", std::process::id(), unix_timestamp_nanos());
let payload = serde_json::json!({
"agentId": agent_id,
"pid": std::process::id(),
"token": token.clone(),
"createdAt": unix_timestamp(),
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("生成 Agent Runtime 锁失败:{error}"))?;
file.set_len(0)
.and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ()))
.and_then(|_| file.write_all(content.as_bytes()))
.and_then(|_| file.sync_data())
.map_err(|error| format!("写入 Agent Runtime 锁失败:{}: {error}", path.display()))?;
Ok(Some(AgentRuntimeTaskLock { file: Some(file) }))
}
pub(super) fn try_acquire_game_creator_agent_delegation_lock(
root: &Path,
delegation_id: &str,
purpose: &str,
) -> Result<Option<AgentRuntimeTaskLock>, String> {
let lock_id = agent_runtime_confirmation_path_component(delegation_id, "delegation");
let purpose = agent_runtime_confirmation_path_component(purpose, "lock");
let relative_path = format!(".agent/runtime/locks/delegations/{purpose}/{lock_id}.lock");
let path = root.join(&relative_path);
let Some(mut file) = try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?
else {
return Ok(None);
};
let payload = serde_json::json!({
"delegationId": delegation_id,
"pid": std::process::id(),
"createdAt": unix_timestamp(),
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("生成 Agent 委派回执锁失败:{error}"))?;
file.set_len(0)
.and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ()))
.and_then(|_| file.write_all(content.as_bytes()))
.and_then(|_| file.sync_data())
.map_err(|error| format!("写入 Agent 委派回执锁失败:{}: {error}", path.display()))?;
Ok(Some(AgentRuntimeTaskLock { file: Some(file) }))
}
pub(crate) fn try_acquire_game_creator_agent_delegation_lock_with_wait(
root: &Path,
delegation_id: &str,
purpose: &str,
) -> Result<Option<AgentRuntimeTaskLock>, String> {
for attempt in 0..100 {
if let Some(lock) =
try_acquire_game_creator_agent_delegation_lock(root, delegation_id, purpose)?
{
return Ok(Some(lock));
}
if attempt < 99 {
std::thread::sleep(Duration::from_millis(10));
}
}
Ok(None)
}
pub(super) fn acquire_game_creator_agent_runtime_task_journal_lock(
root: &Path,
agent_id: &str,
) -> Result<AgentRuntimeTaskLock, String> {
for attempt in 0..100 {
if let Some(lock) =
try_acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?
{
return Ok(lock);
}
if attempt < 99 {
std::thread::sleep(Duration::from_millis(10));
}
}
Err(format!(
"Agent Runtime 任务账本正被其他进程写入:{agent_id}"
))
}
pub(super) fn try_acquire_game_creator_agent_runtime_task_journal_lock(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskLock>, String> {
let agent_id = agent_runtime_confirmation_path_component(agent_id, "agent");
let relative_path = format!(".agent/runtime/locks/task-journals/{agent_id}.lock");
let path = root.join(&relative_path);
let Some(mut file) = try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?
else {
return Ok(None);
};
let payload = serde_json::json!({
"agentId": agent_id,
"pid": std::process::id(),
"createdAt": unix_timestamp(),
});
let content = serde_json::to_string_pretty(&payload)
.map_err(|error| format!("生成 Agent Runtime 任务账本锁失败:{error}"))?;
file.set_len(0)
.and_then(|_| file.seek(SeekFrom::Start(0)).map(|_| ()))
.and_then(|_| file.write_all(content.as_bytes()))
.and_then(|_| file.sync_data())
.map_err(|error| {
format!(
"写入 Agent Runtime 任务账本锁失败:{}: {error}",
path.display()
)
})?;
Ok(Some(AgentRuntimeTaskLock { file: Some(file) }))
}
pub(super) fn acquire_game_creator_agent_runtime_task_lock_with_wait(
root: &Path,
agent_id: &str,
) -> Result<AgentRuntimeTaskLock, String> {
try_acquire_game_creator_agent_runtime_task_lock_with_wait(root, agent_id)?
.ok_or_else(|| format!("Agent Runtime 正在执行该 Agent 的其他任务:{agent_id}"))
}
pub(super) fn try_acquire_game_creator_agent_runtime_task_lock_with_wait(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskLock>, String> {
// Runtime state transitions can persist several audit projections while holding
// the lane lock. Keep the wait bounded, but allow a slow CI/disk-backed
// transition to finish before reporting a false busy error.
const MAX_ATTEMPTS: usize = 100;
for attempt in 0..MAX_ATTEMPTS {
if let Some(runtime_lock) =
try_acquire_game_creator_agent_runtime_task_lock(root, agent_id)?
{
return Ok(Some(runtime_lock));
}
if attempt + 1 < MAX_ATTEMPTS {
std::thread::sleep(Duration::from_millis(10));
}
}
Ok(None)
}
#[cfg(unix)]
pub(crate) fn try_open_game_creator_agent_runtime_task_lock_file(
root: &Path,
relative_path: &str,
) -> Result<Option<File>, String> {
use std::ffi::CString;
use std::os::fd::{AsRawFd, FromRawFd};
use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
// macOS 上两个线程首次并发创建同一套 mkdirat/openat 锁目录时,loser
// 可能在最终 O_CREAT 前短暂观察到 ENOENT。进程内只串行化安全打开阶段;
// 返回后的 flock 仍负责真实的跨线程、跨进程互斥。
let _open_guard = AGENT_RUNTIME_LOCK_OPEN_GUARD
.get_or_init(|| Mutex::new(()))
.lock()
.map_err(|_| "Agent Runtime 锁安全打开门禁已损坏".to_string())?;
validate_project_root(root)?;
let relative_path = normalize_relative_path(relative_path)?;
let path = root.join(&relative_path);
let mut components = relative_path.split('/').collect::<Vec<_>>();
let file_name = components
.pop()
.ok_or_else(|| "Agent Runtime 锁路径缺少文件名".to_string())?;
let mut directory = fs::OpenOptions::new()
.read(true)
.custom_flags(libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW)
.open(root)
.map_err(|error| format!("安全打开项目目录失败:{}: {error}", root.display()))?;
for component in components {
let component =
CString::new(component).map_err(|_| "Agent Runtime 锁目录包含 NUL".to_string())?;
// SAFETY: `directory` is a live directory fd and `component` is NUL terminated.
let created = unsafe { libc::mkdirat(directory.as_raw_fd(), component.as_ptr(), 0o700) };
if created != 0 {
let error = std::io::Error::last_os_error();
if error.kind() != std::io::ErrorKind::AlreadyExists {
return Err(format!(
"创建 Agent Runtime 锁目录失败:{}: {error}",
path.display()
));
}
}
// SAFETY: `directory` and `component` remain valid for the duration of openat.
let fd = unsafe {
libc::openat(
directory.as_raw_fd(),
component.as_ptr(),
libc::O_RDONLY | libc::O_CLOEXEC | libc::O_DIRECTORY | libc::O_NOFOLLOW,
)
};
if fd < 0 {
return Err(format!(
"安全打开 Agent Runtime 锁目录失败:{}: {}",
path.display(),
std::io::Error::last_os_error()
));
}
// SAFETY: openat returned a new owned fd.
directory = unsafe { File::from_raw_fd(fd) };
}
let file_name =
CString::new(file_name).map_err(|_| "Agent Runtime 锁文件名包含 NUL".to_string())?;
// SAFETY: `directory` is a live directory fd and `file_name` is NUL terminated.
let fd = unsafe {
libc::openat(
directory.as_raw_fd(),
file_name.as_ptr(),
libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
0o600,
)
};
if fd < 0 {
return Err(format!(
"安全打开 Agent Runtime 锁失败:{}: {}",
path.display(),
std::io::Error::last_os_error()
));
}
// SAFETY: openat returned a new owned fd.
let file = unsafe { File::from_raw_fd(fd) };
let metadata = file.metadata().map_err(|error| {
format!(
"读取 Agent Runtime 锁元数据失败:{}: {error}",
path.display()
)
})?;
if !metadata.is_file() || metadata.nlink() != 1 {
return Err(format!(
"Agent Runtime 锁必须是无硬链接的普通文件:{}",
path.display()
));
}
// SAFETY: flock only observes the valid fd owned by `file`; `file` remains alive on success.
let result = unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX | libc::LOCK_NB) };
if result == 0 {
return Ok(Some(file));
}
let error = std::io::Error::last_os_error();
if error.kind() == std::io::ErrorKind::WouldBlock {
Ok(None)
} else {
Err(format!(
"获取 Agent Runtime 系统文件锁失败:{}: {error}",
path.display()
))
}
}
#[cfg(windows)]
pub(crate) fn try_open_game_creator_agent_runtime_task_lock_file(
root: &Path,
relative_path: &str,
) -> Result<Option<File>, String> {
use std::os::windows::fs::{MetadataExt, OpenOptionsExt};
const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
const FILE_SHARE_READ_WRITE: u32 = 0x0000_0003;
const ERROR_SHARING_VIOLATION: i32 = 32;
const ERROR_LOCK_VIOLATION: i32 = 33;
validate_project_root(root)?;
let relative_path = normalize_relative_path(relative_path)?;
let path = root.join(&relative_path);
let mut components = relative_path.split('/').collect::<Vec<_>>();
components
.pop()
.ok_or_else(|| "Agent Runtime 锁路径缺少文件名".to_string())?;
let mut guarded_directories = Vec::with_capacity(components.len() + 1);
let mut current = root.to_path_buf();
for component in std::iter::once(None).chain(components.into_iter().map(Some)) {
if let Some(component) = component {
current.push(component);
if !current.exists() {
if let Err(error) = fs::create_dir(&current) {
// 另一并发锁请求可能在 exists 与 create_dir 之间创建同一目录;
// 下方元数据检查仍是权威校验,并会拒绝普通文件或 reparse point。
if error.kind() != std::io::ErrorKind::AlreadyExists
&& error.raw_os_error() != Some(183)
{
return Err(format!(
"创建 Agent Runtime 锁目录失败:{}: {error}",
current.display()
));
}
}
}
}
let metadata = fs::symlink_metadata(&current).map_err(|error| {
format!(
"读取 Agent Runtime 锁目录元数据失败:{}: {error}",
current.display()
)
})?;
if !metadata.is_dir() || metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!(
"Agent Runtime 锁目录必须是普通目录且不能是 reparse point{}",
current.display()
));
}
let handle = fs::OpenOptions::new()
.read(true)
.share_mode(FILE_SHARE_READ_WRITE)
.custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
.open(&current)
.map_err(|error| {
format!(
"安全打开 Agent Runtime 锁目录失败:{}: {error}",
current.display()
)
})?;
let opened = handle.metadata().map_err(|error| {
format!(
"复核 Agent Runtime 锁目录失败:{}: {error}",
current.display()
)
})?;
if !opened.is_dir() || opened.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
return Err(format!(
"Agent Runtime 锁目录打开后身份无效:{}",
current.display()
));
}
guarded_directories.push(handle);
}
if let Ok(metadata) = fs::symlink_metadata(&path) {
if metadata.file_type().is_symlink()
|| !metadata.is_file()
|| metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0
{
return Err(format!(
"Agent Runtime 锁必须是普通文件且不能是重解析点:{}",
path.display()
));
}
}
match fs::OpenOptions::new()
.create(true)
.read(true)
.write(true)
.share_mode(0)
.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
.open(&path)
{
Ok(file) => {
let metadata = file.metadata().map_err(|error| {
format!(
"读取 Agent Runtime 锁元数据失败:{}: {error}",
path.display()
)
})?;
if !metadata.is_file() || metadata.file_type().is_symlink() {
return Err(format!(
"Agent Runtime 锁必须是无硬链接的普通文件且不能是重解析点:{}",
path.display()
));
}
validate_windows_regular_file_handle(&file, "Agent Runtime 锁")?;
Ok(Some(file))
}
Err(error)
if matches!(
error.kind(),
std::io::ErrorKind::PermissionDenied | std::io::ErrorKind::WouldBlock
) || matches!(
error.raw_os_error(),
Some(ERROR_SHARING_VIOLATION | ERROR_LOCK_VIOLATION)
) =>
{
Ok(None)
}
Err(error) => Err(format!(
"获取 Agent Runtime 系统文件锁失败:{}: {error}",
path.display()
)),
}
}
#[cfg(not(any(unix, windows)))]
pub(crate) fn try_open_game_creator_agent_runtime_task_lock_file(
root: &Path,
relative_path: &str,
) -> Result<Option<File>, String> {
Err(format!(
"当前平台不支持 Agent Runtime 系统文件锁:{}",
root.join(relative_path).display()
))
}
pub(crate) fn game_creator_agent_runtime_task_lock_is_available(
root: &Path,
agent_id: &str,
) -> Result<bool, String> {
let relative_path = format!(".agent/runtime/locks/{agent_id}.lock");
Ok(try_open_game_creator_agent_runtime_task_lock_file(root, &relative_path)?.is_some())
}
#[derive(Debug)]
pub(crate) struct AgentRuntimeTaskLockStatus {
pub(crate) is_stale: bool,
pub(crate) belongs_to_previous_process: bool,
}
pub(crate) fn read_game_creator_agent_runtime_lock_status(
path: &Path,
) -> AgentRuntimeTaskLockStatus {
let Ok(content) = fs::read_to_string(path) else {
return AgentRuntimeTaskLockStatus {
is_stale: true,
belongs_to_previous_process: true,
};
};
let Ok(value) = serde_json::from_str::<serde_json::Value>(&content) else {
return AgentRuntimeTaskLockStatus {
is_stale: true,
belongs_to_previous_process: true,
};
};
let pid = value.get("pid").and_then(serde_json::Value::as_u64);
let created_at = value
.get("createdAt")
.and_then(serde_json::Value::as_u64)
.unwrap_or(0);
let is_stale = created_at == 0
|| unix_timestamp().saturating_sub(created_at) > AGENT_RUNTIME_LOCK_STALE_AFTER_SECONDS;
let belongs_to_previous_process = pid
.map(|pid| pid != u64::from(std::process::id()))
.unwrap_or(true);
AgentRuntimeTaskLockStatus {
is_stale,
belongs_to_previous_process,
}
}
pub(crate) fn write_game_creator_agent_runtime_state(
root: &Path,
state: &AgentRuntimeState,
) -> Result<(), String> {
if !state.run_id.trim().is_empty() {
let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at(
root,
&state.agent_id,
&state.run_id,
Some(&state.run_profile),
Some(&state.run_profile_binding_fingerprint),
)?;
if run_profile != state.run_profile
|| binding_fingerprint != state.run_profile_binding_fingerprint
{
return Err("Agent Runtime 状态 Run Profile 绑定不匹配".to_string());
}
}
let path = game_creator_agent_runtime_session_path(root, &state.agent_id);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).map_err(|error| {
format!(
"创建 Agent Runtime 状态目录失败:{}: {error}",
parent.display()
)
})?;
}
let content = serde_json::to_string_pretty(state)
.map_err(|error| format!("序列化 Agent Runtime 状态失败:{error}"))?;
let temp_path = path.with_file_name(format!(
".{}.tmp.{}.{}",
path.file_name()
.and_then(|value| value.to_str())
.unwrap_or("runtime.json"),
std::process::id(),
unix_timestamp_nanos()
));
fs::write(&temp_path, format!("{content}\n")).map_err(|error| {
format!(
"写入 Agent Runtime 临时状态失败:{}: {error}",
temp_path.display()
)
})?;
fs::rename(&temp_path, &path).map_err(|error| {
let _ = fs::remove_file(&temp_path);
format!(
"替换 Agent Runtime 状态失败:{} -> {}: {error}",
temp_path.display(),
path.display()
)
})
}
pub(super) fn append_game_creator_agent_runtime_event(
root: &Path,
state: &AgentRuntimeState,
event_type: &str,
status: &str,
phase: &str,
summary: &str,
detail: Option<&str>,
) -> Result<(), String> {
append_game_creator_agent_runtime_event_with_action(
root, state, event_type, status, phase, summary, detail, None,
)
}
pub(crate) fn append_game_creator_agent_runtime_action_event(
root: &Path,
state: &AgentRuntimeState,
event_type: &str,
status: &str,
phase: &str,
summary: &str,
detail: Option<&str>,
action_id: &str,
) -> Result<(), String> {
append_game_creator_agent_runtime_event_with_action(
root,
state,
event_type,
status,
phase,
summary,
detail,
Some(action_id),
)
}
pub(super) fn append_game_creator_agent_runtime_event_with_action(
root: &Path,
state: &AgentRuntimeState,
event_type: &str,
status: &str,
phase: &str,
summary: &str,
detail: Option<&str>,
action_id: Option<&str>,
) -> Result<(), String> {
let path = game_creator_agent_runtime_event_path(root, &state.agent_id);
if let Some(parent) = path.parent() {
ensure_game_creator_private_directory_tree(parent, "Agent Runtime 事件目录")?;
prepare_game_creator_private_path_for_read(parent, true, "Agent Runtime 事件目录")?;
}
let failure_detail = matches!(
event_type,
"error" | "turn.failed" | "turn.budget_exhausted"
)
.then(|| {
detail.map(|value| game_creator_agent_runtime_public_failure_detail(&state.agent_id, value))
})
.flatten();
let public_text = if matches!(event_type, "turn.failed" | "turn.budget_exhausted") {
failure_detail
.clone()
.or_else(|| game_creator_agent_runtime_public_event_text(root, event_type, summary))
} else {
game_creator_agent_runtime_public_event_text(root, event_type, summary)
};
let event = AgentRuntimeEvent {
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: state.agent_id.clone(),
task_id: state.task_id.clone(),
session_id: state.session_id.clone(),
run_id: state.run_id.clone(),
source: state.source.clone(),
event_type: event_type.to_string(),
event_id: new_game_creator_agent_runtime_event_id(state, event_type, phase, action_id),
action_id: action_id.map(ToString::to_string),
status: status.to_string(),
phase: phase.to_string(),
summary: summary.to_string(),
public_text,
detail: detail
.filter(|_| {
!(event_type == "observation"
&& (summary.starts_with("command.exec")
|| summary.starts_with("command.output_read")))
})
.map(|value| {
if matches!(
event_type,
"error" | "turn.failed" | "turn.budget_exhausted"
) {
return failure_detail.clone().unwrap_or_else(|| {
game_creator_agent_runtime_public_failure_detail(&state.agent_id, value)
});
}
let max_chars = if event_type == "observation"
&& summary.starts_with("agent.action_history")
{
AGENT_RUNTIME_FILE_CONTEXT_MAX_CHARS
} else {
500
};
sanitize_agent_runtime_text(value, max_chars)
}),
updated_at: unix_timestamp(),
};
if let Some(action_id) = action_id {
if let Some(existing) = read_recent_game_creator_agent_runtime_events(&path)?
.into_iter()
.rev()
.find(|candidate| {
candidate.run_id == event.run_id
&& candidate.event_type == event.event_type
&& candidate.action_id.as_deref() == Some(action_id)
&& candidate.phase == event.phase
})
{
let legacy_detail_is_stricter_projection_compatible = event.event_type == "observation"
&& event.detail.is_none()
&& existing.detail.is_some();
if existing.agent_id != event.agent_id
|| existing.task_id != event.task_id
|| existing.session_id != event.session_id
|| existing.run_id != event.run_id
|| existing.source != event.source
|| existing.status != event.status
|| existing.phase != event.phase
|| existing.summary != event.summary
|| (existing.detail != event.detail
&& !legacy_detail_is_stricter_projection_compatible)
{
return Err(format!(
"Agent Runtime action event 幂等身份冲突:actionId={action_id}"
));
}
return Ok(());
}
}
let line = serde_json::to_string(&event)
.map_err(|error| format!("序列化 Agent Runtime 事件失败:{error}"))?;
append_jsonl_line(&path, &line, "Agent Runtime 事件")?;
emit_game_creator_agent_runtime_update(root, &state.agent_id);
Ok(())
}
pub(crate) fn append_game_creator_agent_runtime_task(
root: &Path,
state: &AgentRuntimeState,
) -> Result<(), String> {
append_game_creator_agent_runtime_task_record(root, &agent_runtime_task_record(state))
}
pub(crate) fn append_game_creator_agent_runtime_task_projection_once(
root: &Path,
state: &AgentRuntimeState,
action_id: &str,
) -> Result<(), String> {
let record = agent_runtime_task_record(state);
let mut projection = serde_json::to_value(&record)
.map_err(|error| format!("序列化 Agent Runtime action task 失败:{error}"))?;
projection
.as_object_mut()
.ok_or_else(|| "Agent Runtime action task 必须是 JSON object".to_string())?
.insert(
"actionId".to_string(),
serde_json::Value::String(action_id.to_string()),
);
let _journal_lock =
acquire_game_creator_agent_runtime_task_journal_lock(root, &record.agent_id)?;
let path = game_creator_agent_runtime_task_path(root, &record.agent_id);
match File::open(&path) {
Ok(file) => {
for line in BufReader::new(file).lines() {
let line = line.map_err(|error| {
format!(
"读取 Agent Runtime action task 失败:{}: {error}",
path.display()
)
})?;
let existing = match serde_json::from_str::<serde_json::Value>(line.trim()) {
Ok(existing) => existing,
Err(_) if line.trim().is_empty() => continue,
Err(error) => {
return Err(format!(
"解析 Agent Runtime action task 失败:{}: {error}",
path.display()
));
}
};
if existing.get("runId") != projection.get("runId")
|| existing.get("actionId") != projection.get("actionId")
|| existing.get("phase") != projection.get("phase")
{
continue;
}
for field in [
"agentId",
"taskId",
"sessionId",
"runId",
"actionId",
"source",
"status",
"phase",
"currentAction",
] {
if existing.get(field) != projection.get(field) {
return Err(format!(
"Agent Runtime action task 幂等身份冲突:actionId={action_id} field={field}"
));
}
}
return Ok(());
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"读取 Agent Runtime action task 失败:{}: {error}",
path.display()
));
}
}
let line = serde_json::to_string(&projection)
.map_err(|error| format!("序列化 Agent Runtime action task 失败:{error}"))?;
append_jsonl_line(&path, &line, "Agent Runtime 任务")
}
pub(super) fn agent_runtime_task_record(state: &AgentRuntimeState) -> AgentRuntimeTaskRecord {
AgentRuntimeTaskRecord {
goal_id: state.goal_id.clone(),
goal_revision: state.goal_revision,
goal_status: state.goal_status.clone(),
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: state.agent_id.clone(),
task_id: state.task_id.clone(),
session_id: state.session_id.clone(),
run_id: state.run_id.clone(),
source: state.source.clone(),
run_profile: state.run_profile.clone(),
run_profile_binding_fingerprint: state.run_profile_binding_fingerprint.clone(),
parent_agent_id: state.parent_agent_id.clone(),
parent_run_id: state.parent_run_id.clone(),
delegation_id: state.delegation_id.clone(),
task: state.current_task.clone(),
status: game_creator_agent_runtime_task_status(state),
phase: state.phase.clone(),
current_action: state.current_action.clone(),
terminal_detail: agent_runtime_terminal_detail(state),
error: state.error.clone(),
updated_at: state.updated_at,
}
}
pub(super) fn append_game_creator_agent_runtime_cancelled_task_record(
root: &Path,
record: &AgentRuntimeTaskRecord,
current_action: &str,
) -> Result<AgentRuntimeTaskRecord, String> {
let cancelled = AgentRuntimeTaskRecord {
goal_id: record.goal_id.clone(),
goal_revision: record.goal_revision,
goal_status: record.goal_status.clone(),
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: record.agent_id.clone(),
task_id: record.task_id.clone(),
session_id: record.session_id.clone(),
run_id: record.run_id.clone(),
source: record.source.clone(),
run_profile: record.run_profile.clone(),
run_profile_binding_fingerprint: record.run_profile_binding_fingerprint.clone(),
parent_agent_id: record.parent_agent_id.clone(),
parent_run_id: record.parent_run_id.clone(),
delegation_id: record.delegation_id.clone(),
task: record.task.clone(),
status: "cancelled".to_string(),
phase: "cancelled".to_string(),
current_action: current_action.to_string(),
terminal_detail: Some(sanitize_agent_runtime_text(current_action, 500)),
error: None,
updated_at: unix_timestamp(),
};
append_game_creator_agent_runtime_task_record(root, &cancelled)?;
Ok(cancelled)
}
pub(crate) fn mark_game_creator_agent_runtime_cancelled_at(
root: &Path,
state: &mut AgentRuntimeState,
summary: &str,
detail: Option<&str>,
) -> Result<(), String> {
let _project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.cancel.finalize",
)?;
mark_game_creator_agent_runtime_cancelled_at_locked(root, state, summary, detail)
}
pub(super) fn mark_game_creator_agent_runtime_cancelled_at_locked(
root: &Path,
state: &mut AgentRuntimeState,
summary: &str,
detail: Option<&str>,
) -> Result<(), String> {
terminate_process_sessions_for_run_at(root, &state.agent_id, &state.run_id)
.map_err(|error| format!("取消 Agent Runtime 前收束进程会话失败:{error}"))?;
write_non_terminal_isolated_child_cancel_tombstones_for_parent_at(
root,
&state.agent_id,
&state.run_id,
"父 Agent 任务已取消,取消动态隔离子任务",
)?;
state.pending_tool_action = None;
state.status = "cancelled".to_string();
state.phase = "cancelled".to_string();
state.current_action = summary.to_string();
state.waiting_on = "开发者下一轮输入".to_string();
state.next_step = "可重试该后台任务或提交新任务".to_string();
state.error = None;
state.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, state)?;
refresh_game_creator_agent_runtime_task_queue(root, state)?;
write_game_creator_agent_runtime_state(root, state)?;
let task_sha256 = format!("{:x}", Sha256::digest(state.current_task.as_bytes()));
let task_chars = state.current_task.chars().count();
let goal_bound = state.goal_id.is_some();
let delegated = state.parent_agent_id.is_some();
let private_task = agent_runtime_task_requires_private_audit(
state.goal_id.as_deref(),
state.parent_agent_id.as_deref(),
);
let public_task = (!private_task).then(|| state.current_task.clone());
let public_detail = if private_task {
Some(format!(
"taskChars={task_chars} · taskSha256={task_sha256} · goalBound={goal_bound} · delegated={delegated}"
))
} else {
detail.map(ToString::to_string)
};
append_game_creator_agent_runtime_event(
root,
state,
"turn.cancelled",
"cancelled",
"cancelled",
summary,
public_detail.as_deref(),
)?;
append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.cancelled",
"agentId": state.agent_id.clone(),
"taskId": state.task_id.clone(),
"sessionId": state.session_id.clone(),
"runId": state.run_id.clone(),
"source": state.source.clone(),
"task": public_task,
"taskSha256": task_sha256,
"taskChars": task_chars,
"goalBound": goal_bound,
"delegated": delegated,
"summary": summary,
}),
)?;
remove_game_creator_agent_runtime_pending_tool_action(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_action_batch(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_confirmations(root, &state.agent_id, &state.run_id)?;
remove_game_creator_agent_runtime_provider_recovery_at(root, &state.agent_id, &state.run_id)?;
if let Some(goal) = mark_game_creator_agent_goal_cleared_for_runtime_at_locked(root, state)? {
state.goal_status = Some(goal.status);
state.updated_at = unix_timestamp();
write_game_creator_agent_runtime_state(root, state)?;
}
publish_game_creator_agent_delegate_result_for_state_at_locked(
root,
state,
detail.or(Some(summary)),
);
Ok(())
}
pub(super) fn stop_game_creator_agent_runtime_if_cancel_requested(
root: &Path,
state: &mut AgentRuntimeState,
) -> bool {
if game_creator_agent_runtime_cancel_requested(root, state) {
let cancellation = mark_game_creator_agent_runtime_cancelled_at(
root,
state,
"Agent 后台任务已按开发者请求取消",
Some("取消请求会在当前 LLM 或工具调用返回后生效。"),
);
if let Err(error) = cancellation {
let error = format!("Agent 后台任务收到取消请求,但取消状态落盘失败:{error}");
state.status = "running".to_string();
state.phase = "needs-reconciliation".to_string();
state.current_action = "取消前的进程会话或持久状态需要人工核对".to_string();
state.waiting_on = "开发者核对进程会话和取消状态".to_string();
state.next_step = "刷新状态,核对 process session 后重新取消该任务".to_string();
state.error = Some(sanitize_agent_runtime_text(&error, 500));
state.updated_at = unix_timestamp();
let _ = append_game_creator_agent_runtime_task(root, state);
let _ = refresh_game_creator_agent_runtime_task_queue(root, state);
let _ = write_game_creator_agent_runtime_state(root, state);
let _ = append_game_creator_agent_runtime_event(
root,
state,
"turn.cancel_reconciliation",
"running",
"needs-reconciliation",
"取消请求未能收束进程会话,当前 run 保持待核对。",
Some(&error),
);
}
return true;
}
let pause_requested = hydrate_game_creator_agent_goal_state_at(root, state)
.ok()
.flatten()
.is_some_and(|goal| goal.status == AGENT_GOAL_STATUS_PAUSE_REQUESTED);
if !pause_requested {
return false;
}
if let Err(error) = mark_game_creator_agent_runtime_paused_at(root, state) {
let error = format!("Agent 后台任务收到暂停请求,但暂停状态落盘失败:{error}");
state.status = "running".to_string();
state.phase = "needs-reconciliation".to_string();
state.current_action = "暂停前的进程会话或持久状态需要人工核对".to_string();
state.waiting_on = "开发者核对进程会话和 Goal 状态".to_string();
state.next_step = "修复 Goal/Runtime 一致性后重新暂停".to_string();
state.error = Some(sanitize_agent_runtime_text(&error, 500));
state.updated_at = unix_timestamp();
let _ = append_game_creator_agent_runtime_task(root, state);
let _ = refresh_game_creator_agent_runtime_task_queue(root, state);
let _ = write_game_creator_agent_runtime_state(root, state);
let _ = append_game_creator_agent_runtime_event(
root,
state,
"goal.pause_reconciliation",
"running",
"needs-reconciliation",
"暂停请求未能收束进程会话或 Goal 状态,当前 run 保持待核对。",
Some(&error),
);
}
true
}
pub(crate) fn append_unique_game_creator_agent_runtime_pending_task(
root: &Path,
agent_id: &str,
session_id: &str,
task: &str,
requested_run_id: &str,
source: &str,
requested_run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
) -> Result<AgentRuntimeTaskRecord, String> {
append_unique_game_creator_agent_runtime_task_with_initial_state(
root,
agent_id,
session_id,
task,
requested_run_id,
source,
requested_run_profile,
task_link,
"pending",
"queued",
"等待当前后台任务完成",
)
}
pub(crate) fn append_unique_game_creator_agent_runtime_public_status_preparing_task(
root: &Path,
agent_id: &str,
session_id: &str,
task: &str,
requested_run_id: &str,
source: &str,
requested_run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
) -> Result<AgentRuntimeTaskRecord, String> {
append_unique_game_creator_agent_runtime_task_with_initial_state(
root,
agent_id,
session_id,
task,
requested_run_id,
source,
requested_run_profile,
task_link,
"preparing",
"public-status-pending",
"等待持久化启动确认",
)
}
fn append_unique_game_creator_agent_runtime_task_with_initial_state(
root: &Path,
agent_id: &str,
session_id: &str,
task: &str,
requested_run_id: &str,
source: &str,
requested_run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
initial_status: &str,
initial_phase: &str,
initial_current_action: &str,
) -> Result<AgentRuntimeTaskRecord, String> {
let autonomous_root_project_lock = (agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& task_link.is_none()
&& requested_run_profile
.is_some_and(|profile| profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD))
.then(|| {
acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.autonomous-root.create",
)
})
.transpose()?;
let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?;
let run_id = unique_game_creator_agent_runtime_run_id(root, agent_id, requested_run_id)?;
let run_profile_binding = bind_game_creator_agent_runtime_run_profile_at(
root,
agent_id,
&run_id,
source,
requested_run_profile,
task_link,
)?;
let (goal_id, goal_revision, goal_status) =
game_creator_agent_goal_task_binding_at(root, agent_id, session_id, &run_id)?;
let task_link = task_link.cloned().unwrap_or_default();
let task_max_chars = if source.trim() == AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE {
AGENT_RUNTIME_DELEGATE_RECEIPT_TASK_MAX_CHARS
} else {
AGENT_RUNTIME_TASK_MAX_CHARS
};
let record = AgentRuntimeTaskRecord {
goal_id,
goal_revision,
goal_status,
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: agent_id.to_string(),
task_id: agent_id.to_string(),
session_id: session_id.to_string(),
run_id,
source: source.trim().to_string(),
run_profile: run_profile_binding.profile,
run_profile_binding_fingerprint: run_profile_binding.binding_fingerprint,
parent_agent_id: task_link.parent_agent_id,
parent_run_id: task_link.parent_run_id,
delegation_id: task_link.delegation_id,
task: sanitize_agent_runtime_text(task, task_max_chars),
status: initial_status.to_string(),
phase: initial_phase.to_string(),
current_action: initial_current_action.to_string(),
terminal_detail: None,
error: None,
updated_at: unix_timestamp(),
};
append_game_creator_agent_runtime_task_record_unlocked(root, &record)?;
drop(_journal_lock);
drop(autonomous_root_project_lock);
// The relaxed autonomous lane does not create or validate completion
// contracts. Contracts are delivery metadata, not a prerequisite for
// starting or executing a task; keeping this best-effort hook for the
// strict/legacy profiles preserves their existing recovery behavior.
if !autonomous_relaxed_run_profile(&record.run_profile) {
if let Err(error) = ensure_autonomous_completion_contract_for_task_at(root, &record) {
let public_error = redact_agent_runtime_project_paths(root, &error, 500);
let failed = AgentRuntimeTaskRecord {
status: "failed".to_string(),
phase: "completion-contract-failed".to_string(),
current_action: "自主构建完成合同未能建立,任务未执行".to_string(),
terminal_detail: Some(public_error.clone()),
error: Some(public_error),
updated_at: unix_timestamp(),
..record.clone()
};
append_game_creator_agent_runtime_task_record_unlocked(root, &failed)?;
return Err(format!("自主构建完成合同建立失败,任务未执行:{error}"));
}
}
Ok(record)
}
pub(crate) fn append_or_read_exact_game_creator_agent_runtime_pending_task(
root: &Path,
agent_id: &str,
session_id: &str,
task: &str,
requested_run_id: &str,
source: &str,
requested_run_profile: Option<&str>,
task_link: &AgentRuntimeTaskLink,
) -> Result<(AgentRuntimeTaskRecord, bool), String> {
let parent_agent_id = task_link
.parent_agent_id
.as_deref()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "静态委派缺少 parentAgentId".to_string())?;
let parent_run_id = task_link
.parent_run_id
.as_deref()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "静态委派缺少 parentRunId".to_string())?;
let delegation_id = task_link
.delegation_id
.as_deref()
.filter(|value| !value.trim().is_empty())
.ok_or_else(|| "静态委派缺少 delegationId".to_string())?;
if parent_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || source != "agent-delegate" {
return Err("精确 runId 入队只允许项目总控静态委派".to_string());
}
let run_id = normalize_game_creator_agent_runtime_run_id(agent_id, requested_run_id);
let task = sanitize_agent_runtime_text(task, AGENT_RUNTIME_TASK_MAX_CHARS);
let run_profile_binding = bind_game_creator_agent_runtime_run_profile_at(
root,
agent_id,
&run_id,
source,
requested_run_profile,
Some(task_link),
)?;
let _journal_lock = acquire_game_creator_agent_runtime_task_journal_lock(root, agent_id)?;
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records = read_all_game_creator_agent_runtime_tasks(&path)?;
if let Some(existing) = records
.iter()
.rev()
.find(|record| record.delegation_id.as_deref() == Some(delegation_id))
{
if existing.agent_id != agent_id
|| existing.session_id != session_id
|| existing.run_id != run_id
|| existing.source != source
|| existing.run_profile != run_profile_binding.profile
|| existing.run_profile_binding_fingerprint != run_profile_binding.binding_fingerprint
|| existing.parent_agent_id.as_deref() != Some(parent_agent_id)
|| existing.parent_run_id.as_deref() != Some(parent_run_id)
|| existing.task != task
{
return Err(format!("静态委派任务身份冲突:{delegation_id}"));
}
return Ok((existing.clone(), false));
}
if records.iter().any(|record| record.run_id == run_id) {
return Err(format!("静态委派预留 runId 已被其他任务占用:{run_id}"));
}
let record = AgentRuntimeTaskRecord {
goal_id: None,
goal_revision: 0,
goal_status: None,
schema_version: AGENT_RUNTIME_SCHEMA_VERSION.to_string(),
agent_id: agent_id.to_string(),
task_id: agent_id.to_string(),
session_id: session_id.to_string(),
run_id,
source: source.to_string(),
run_profile: run_profile_binding.profile,
run_profile_binding_fingerprint: run_profile_binding.binding_fingerprint,
parent_agent_id: Some(parent_agent_id.to_string()),
parent_run_id: Some(parent_run_id.to_string()),
delegation_id: Some(delegation_id.to_string()),
task,
status: "pending".to_string(),
phase: "queued".to_string(),
current_action: "等待当前后台任务完成".to_string(),
terminal_detail: None,
error: None,
updated_at: unix_timestamp(),
};
append_game_creator_agent_runtime_task_record_unlocked(root, &record)?;
Ok((record, true))
}
pub(crate) fn append_game_creator_agent_runtime_task_record(
root: &Path,
record: &AgentRuntimeTaskRecord,
) -> Result<(), String> {
{
let _journal_lock =
acquire_game_creator_agent_runtime_task_journal_lock(root, &record.agent_id)?;
append_game_creator_agent_runtime_task_record_unlocked(root, record)?;
}
if record.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& game_creator_agent_runtime_terminal_status(record).is_some()
{
suppress_static_delegate_deliveries_for_parent_terminal_at(
root,
&record.agent_id,
&record.run_id,
)?;
}
Ok(())
}
pub(super) fn append_game_creator_agent_runtime_task_record_unlocked(
root: &Path,
record: &AgentRuntimeTaskRecord,
) -> Result<(), String> {
validate_game_creator_agent_runtime_task_goal_binding(record)?;
let (run_profile, binding_fingerprint) = agent_runtime_run_profile_identity_at(
root,
&record.agent_id,
&record.run_id,
Some(&record.run_profile),
Some(&record.run_profile_binding_fingerprint),
)?;
if run_profile != record.run_profile
|| binding_fingerprint != record.run_profile_binding_fingerprint
{
return Err("Agent Runtime 任务 Run Profile 绑定不匹配".to_string());
}
let path = game_creator_agent_runtime_task_path(root, &record.agent_id);
if let Some(parent) = path.parent() {
ensure_game_creator_private_directory_tree(parent, "Agent Runtime 任务目录")?;
prepare_game_creator_private_path_for_read(parent, true, "Agent Runtime 任务目录")?;
}
let line = serde_json::to_string(&record)
.map_err(|error| format!("序列化 Agent Runtime 任务失败:{error}"))?;
append_jsonl_line(&path, &line, "Agent Runtime 任务")
}
pub(crate) fn refresh_game_creator_agent_runtime_task_queue(
root: &Path,
state: &mut AgentRuntimeState,
) -> Result<(), String> {
let task_path = game_creator_agent_runtime_task_path(root, &state.agent_id);
state.task_queue = read_game_creator_agent_runtime_task_snapshot(&task_path)?.task_queue;
Ok(())
}
pub(super) fn game_creator_agent_runtime_task_status(state: &AgentRuntimeState) -> String {
if state.phase == "finalizing" {
"running".to_string()
} else if state.error.is_some() || state.status == "failed" || state.phase == "failed" {
"failed".to_string()
} else if state.status == "cancelled" || state.phase == "cancelled" {
"cancelled".to_string()
} else if state.phase == "completed" {
"completed".to_string()
} else if state.status == "running" {
"running".to_string()
} else {
state.status.clone()
}
}
pub(super) fn agent_runtime_terminal_detail(state: &AgentRuntimeState) -> Option<String> {
let detail = match state.phase.as_str() {
"completed" => state.last_response.as_deref(),
"failed" | "budget-exhausted" => state.error.as_deref(),
"cancelled" => Some(state.current_action.as_str()),
_ => None,
}?;
// 必须与 last_response 用同一条上限规则:两者相等是 finalization 幂等校验的
// 不变式(见 finish_game_creator_agent_background_runtime_turn_idempotently_at)。
let detail =
sanitize_agent_runtime_text(detail, static_delegate_result_detail_max_chars(detail, 500));
(!detail.trim().is_empty()).then_some(detail)
}
pub(crate) fn read_recent_game_creator_agent_runtime_events(
path: &Path,
) -> Result<Vec<AgentRuntimeEvent>, String> {
read_recent_game_creator_agent_runtime_events_for_session(path, None)
}
pub(super) fn read_recent_game_creator_agent_runtime_events_for_session(
path: &Path,
session_id: Option<&str>,
) -> Result<Vec<AgentRuntimeEvent>, String> {
let mut events = Vec::new();
match File::open(path) {
Ok(file) => {
for line in BufReader::new(file).lines() {
let line = line.map_err(|error| {
format!("读取 Agent Runtime 事件失败:{}: {error}", path.display())
})?;
let line = line.trim();
if line.is_empty() {
continue;
}
match serde_json::from_str::<AgentRuntimeEvent>(line) {
Ok(mut event) => {
normalize_game_creator_agent_runtime_event(&mut event);
if session_id.is_some_and(|session_id| event.session_id != session_id) {
continue;
}
events.push(event);
}
Err(_) => continue,
}
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"读取 Agent Runtime 事件失败:{}: {error}",
path.display()
));
}
}
if events.len() > AGENT_RUNTIME_RECENT_EVENT_LIMIT {
Ok(events[events.len() - AGENT_RUNTIME_RECENT_EVENT_LIMIT..].to_vec())
} else {
Ok(events)
}
}
pub(super) struct AgentRuntimeTaskSnapshot {
pub(super) task_queue: AgentRuntimeTaskQueueSummary,
pub(super) recent_tasks: Vec<AgentRuntimeTaskRecord>,
pub(super) run_started_at: Option<u64>,
}
pub(super) fn read_game_creator_agent_runtime_task_snapshot(
path: &Path,
) -> Result<AgentRuntimeTaskSnapshot, String> {
read_game_creator_agent_runtime_task_snapshot_for_session(path, None, None)
}
pub(super) fn read_game_creator_agent_runtime_task_snapshot_for_session(
path: &Path,
session_id: Option<&str>,
run_id: Option<&str>,
) -> Result<AgentRuntimeTaskSnapshot, String> {
let records = read_all_game_creator_agent_runtime_tasks(path)?;
let run_started_at = run_id.and_then(|run_id| {
records
.iter()
.filter(|record| {
record.run_id == run_id
&& session_id.map_or(true, |session_id| record.session_id == session_id)
})
.map(|record| record.updated_at)
.filter(|updated_at| *updated_at > 0)
.min()
});
let latest = latest_game_creator_agent_runtime_tasks(records)
.into_iter()
.filter(|record| session_id.map_or(true, |session_id| record.session_id == session_id))
.collect::<Vec<_>>();
let task_queue = summarize_game_creator_agent_runtime_task_queue(&latest);
let mut recent = latest;
if recent.len() > AGENT_RUNTIME_RECENT_TASK_LIMIT {
recent = recent[recent.len() - AGENT_RUNTIME_RECENT_TASK_LIMIT..].to_vec();
}
Ok(AgentRuntimeTaskSnapshot {
task_queue,
recent_tasks: recent,
run_started_at,
})
}
pub(super) fn read_next_pending_game_creator_agent_runtime_task(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?);
if records.iter().any(|record| record.status == "running") {
return Ok(None);
}
for record in records {
if record.status == "pending" {
return Ok(Some(record));
}
if let Some(queued) =
promote_accepted_game_creator_agent_runtime_preparing_task_at(root, &record)?
{
return Ok(Some(queued));
}
}
Ok(None)
}
fn promote_accepted_game_creator_agent_runtime_preparing_task_at(
root: &Path,
record: &AgentRuntimeTaskRecord,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
if record.status != "preparing" || record.phase != "public-status-pending" {
return Ok(None);
}
if !game_creator_agent_runtime_preparing_task_has_accepted_status_at(root, record)? {
if !game_creator_agent_runtime_task_has_user_message_at(root, record)? {
return Ok(None);
}
if let Err(error) =
ensure_game_creator_agent_runtime_accepted_public_status_at(root, record)
{
fail_game_creator_agent_runtime_public_start_status_at(root, record, &error)?;
return Err(format!("恢复 Agent Runtime 启动确认失败:{error}"));
}
}
let queued = AgentRuntimeTaskRecord {
status: "pending".to_string(),
phase: "queued".to_string(),
current_action: "等待当前后台任务完成".to_string(),
updated_at: unix_timestamp(),
..record.clone()
};
append_game_creator_agent_runtime_task_record(root, &queued)?;
Ok(Some(queued))
}
fn game_creator_agent_runtime_preparing_task_has_accepted_status_at(
root: &Path,
record: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
if record.status != "preparing" || record.phase != "public-status-pending" {
return Ok(false);
}
let message_id = game_creator_agent_runtime_public_status_message_id(
&record.agent_id,
&record.session_id,
&record.run_id,
"accepted",
);
let Some(message) =
read_local_conversation_message_by_id_for_session_at(root, None, None, &message_id)?
else {
return Ok(false);
};
if message.role != "assistant"
|| message.content != game_creator_agent_runtime_accepted_public_message(&record.agent_id)
{
return Err(format!(
"Agent Runtime 启动确认恢复身份冲突:runId={}",
record.run_id
));
}
Ok(true)
}
pub(crate) fn game_creator_agent_runtime_task_has_user_message_at(
root: &Path,
record: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
let message_id = agent_runtime_background_task_message_id(
&record.agent_id,
&record.session_id,
&record.run_id,
&record.source,
);
let Some(message) = read_local_conversation_message_by_id_for_session_without_touch_at(
root,
Some(&record.agent_id),
Some(&record.session_id),
&message_id,
)?
else {
return Ok(false);
};
if message.role != "user" || message.content != record.task {
return Err(format!(
"Agent Runtime 启动用户消息身份冲突:runId={}",
record.run_id
));
}
Ok(true)
}
pub(crate) fn ensure_game_creator_agent_runtime_accepted_public_status_at(
root: &Path,
record: &AgentRuntimeTaskRecord,
) -> Result<(), String> {
if game_creator_agent_runtime_preparing_task_has_accepted_status_at(root, record)? {
return Ok(());
}
let append_result = append_game_creator_agent_runtime_public_status_message_at(
root,
&record.agent_id,
&record.session_id,
&record.run_id,
"accepted",
&game_creator_agent_runtime_accepted_public_message(&record.agent_id),
);
match append_result {
Ok(()) => Ok(()),
Err(_error)
if game_creator_agent_runtime_preparing_task_has_accepted_status_at(root, record)? =>
{
// The conversation append is authoritative. A later audit write
// may fail after the message is already durable; do not strand a
// visible accepted task solely because its auxiliary audit needs
// repair.
Ok(())
}
Err(error) => Err(error),
}
}
pub(crate) fn fail_game_creator_agent_runtime_public_start_status_at(
root: &Path,
record: &AgentRuntimeTaskRecord,
error: &str,
) -> Result<(), String> {
let error = redact_agent_runtime_project_paths(root, error, 500);
let _ = crate::error_report::report_agent_runtime_error(&record.agent_id, &error);
let failed_task = AgentRuntimeTaskRecord {
status: "failed".to_string(),
phase: "public-status-write-failed".to_string(),
current_action: "启动确认未落盘,后台任务未执行".to_string(),
terminal_detail: Some(error.clone()),
error: Some(error.clone()),
updated_at: unix_timestamp(),
..record.clone()
};
let mut failed_state = agent_runtime_state_from_task_record(&failed_task);
failed_state.status = failed_task.status.clone();
failed_state.phase = failed_task.phase.clone();
let public_status_result =
append_game_creator_agent_runtime_terminal_public_message_at(root, &failed_state, &error);
append_game_creator_agent_runtime_task_record(root, &failed_task)?;
publish_game_creator_agent_delegate_result(root, &failed_task, Some(&error));
public_status_result
}
pub(super) fn suppress_game_creator_agent_delegate_receipt_for_terminal_parent(
root: &Path,
receipt_task: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
if receipt_task.source == AGENT_RUNTIME_ISOLATED_JOIN_SOURCE {
let Some(parent_run_id) = receipt_task
.parent_run_id
.as_deref()
.filter(|value| !value.trim().is_empty())
else {
append_game_creator_agent_runtime_queued_cancellation(
root,
&receipt_task.agent_id,
receipt_task,
"动态隔离 join 缺少父 run 关联,已阻止自动续跑",
)?;
return Ok(true);
};
let parent_task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&receipt_task.agent_id,
parent_run_id,
)?;
if parent_task
.as_ref()
.is_some_and(game_creator_agent_runtime_parent_blocks_delegate_receipt)
{
append_game_creator_agent_runtime_queued_cancellation(
root,
&receipt_task.agent_id,
receipt_task,
"父任务已取消或失败,动态隔离 join 不再自动续跑",
)?;
return Ok(true);
}
if parent_task.is_none() {
append_game_creator_agent_runtime_queued_cancellation(
root,
&receipt_task.agent_id,
receipt_task,
"动态隔离 join 找不到父 run,已阻止自动续跑",
)?;
return Ok(true);
}
return Ok(false);
}
if receipt_task.source != AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE {
return Ok(false);
}
let Some(parent_run_id) = receipt_task
.parent_run_id
.as_deref()
.filter(|value| !value.trim().is_empty())
else {
close_game_creator_agent_delegate_receipt_before_start(
root,
receipt_task,
"failed",
"parent-link-missing",
"委派回执缺少父 run 关联,已阻止自动续跑",
"missing-parent-run-id",
None,
)?;
return Ok(true);
};
let Some(parent_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&receipt_task.agent_id,
parent_run_id,
)?
else {
close_game_creator_agent_delegate_receipt_before_start(
root,
receipt_task,
"failed",
"parent-link-missing",
"委派回执找不到父 run,已阻止自动续跑",
"parent-run-not-found",
Some(parent_run_id),
)?;
return Ok(true);
};
if !game_creator_agent_runtime_parent_blocks_delegate_receipt(&parent_task) {
return Ok(false);
}
close_game_creator_agent_delegate_receipt_before_start(
root,
receipt_task,
"cancelled",
"parent-terminal",
"父任务已取消或失败,排队回执不再自动续跑",
"parent-became-terminal-before-receipt-start",
Some(parent_run_id),
)?;
Ok(true)
}
#[allow(clippy::too_many_arguments)]
pub(super) fn close_game_creator_agent_delegate_receipt_before_start(
root: &Path,
receipt_task: &AgentRuntimeTaskRecord,
status: &str,
phase: &str,
current_action: &str,
reason: &str,
parent_run_id: Option<&str>,
) -> Result<(), String> {
let closed = AgentRuntimeTaskRecord {
status: status.to_string(),
phase: phase.to_string(),
current_action: current_action.to_string(),
terminal_detail: Some(sanitize_agent_runtime_text(&receipt_task.task, 500)),
error: (status == "failed").then(|| current_action.to_string()),
updated_at: unix_timestamp(),
..receipt_task.clone()
};
append_game_creator_agent_runtime_task_record(root, &closed)?;
remove_game_creator_agent_runtime_cancel_request(
root,
&receipt_task.agent_id,
&receipt_task.run_id,
);
let event_state = agent_runtime_state_from_task_record(&closed);
let _ = append_game_creator_agent_runtime_event(
root,
&event_state,
"agent.delegate.result_suppressed",
status,
phase,
current_action,
parent_run_id,
);
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.agent.delegate.result_suppressed",
"agentId": receipt_task.agent_id,
"sessionId": receipt_task.session_id,
"runId": receipt_task.run_id,
"parentRunId": parent_run_id,
"delegationId": receipt_task.delegation_id,
"status": status,
"phase": phase,
"reason": reason,
}),
);
emit_game_creator_agent_runtime_update(root, &receipt_task.agent_id);
Ok(())
}
pub(crate) fn read_next_runnable_game_creator_agent_runtime_task(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
loop {
let Some(task) = read_next_pending_game_creator_agent_runtime_task(root, agent_id)? else {
return Ok(None);
};
if suppress_game_creator_agent_delegate_receipt_for_terminal_parent(root, &task)? {
continue;
}
return Ok(Some(task));
}
}
pub(super) fn game_creator_agent_runtime_has_reconciliation_barrier(
root: &Path,
agent_id: &str,
) -> Result<bool, String> {
let runtime = read_game_creator_agent_runtime_at(root, agent_id)?;
if runtime.state.phase == "needs-reconciliation" {
return Ok(true);
}
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?);
Ok(records
.into_iter()
.any(|record| record.phase == "needs-reconciliation"))
}
pub(crate) fn read_latest_game_creator_agent_runtime_task_by_run_id(
root: &Path,
agent_id: &str,
run_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
let path = game_creator_agent_runtime_task_path(root, agent_id);
let run_id = run_id.trim();
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?);
Ok(records.into_iter().find(|record| record.run_id == run_id))
}
pub(crate) fn read_latest_game_creator_agent_runtime_task_by_delegation_id(
root: &Path,
agent_id: &str,
delegation_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?);
Ok(records.into_iter().find(|record| {
record.delegation_id.as_deref() == Some(delegation_id)
&& record.source != AGENT_RUNTIME_DELEGATE_RECEIPT_SOURCE
}))
}
pub(super) fn read_recoverable_game_creator_agent_runtime_task(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
let path = game_creator_agent_runtime_task_path(root, agent_id);
let records =
latest_game_creator_agent_runtime_tasks(read_all_game_creator_agent_runtime_tasks(&path)?);
let mut task_states = Vec::with_capacity(records.len());
for record in &records {
let state = match record.status.as_str() {
"pending" => agent_runtime_core::RecoverableTaskState::Pending,
"preparing"
if game_creator_agent_runtime_preparing_task_has_accepted_status_at(
root, record,
)? || game_creator_agent_runtime_task_has_user_message_at(root, record)? =>
{
agent_runtime_core::RecoverableTaskState::Pending
}
"running" => agent_runtime_core::RecoverableTaskState::Running,
"waiting-for-confirmation" => {
agent_runtime_core::RecoverableTaskState::WaitingForConfirmation
}
"waiting-for-user-input" => {
agent_runtime_core::RecoverableTaskState::WaitingForUserInput
}
_ => agent_runtime_core::RecoverableTaskState::Other,
};
task_states.push(state);
}
match agent_runtime_core::next_recovery_step(&task_states) {
agent_runtime_core::RecoveryStep::ResumeRunning { index }
| agent_runtime_core::RecoveryStep::StartPending { index } => {
Ok(records.get(index).cloned())
}
agent_runtime_core::RecoveryStep::WaitForExternalInput
| agent_runtime_core::RecoveryStep::Idle => Ok(None),
}
}
pub(super) fn read_recoverable_runnable_game_creator_agent_runtime_task(
root: &Path,
agent_id: &str,
) -> Result<Option<AgentRuntimeTaskRecord>, String> {
loop {
let Some(mut task) = read_recoverable_game_creator_agent_runtime_task(root, agent_id)?
else {
return Ok(None);
};
if let Some(queued) =
promote_accepted_game_creator_agent_runtime_preparing_task_at(root, &task)?
{
task = queued;
}
if suppress_game_creator_agent_delegate_receipt_for_terminal_parent(root, &task)? {
continue;
}
return Ok(Some(task));
}
}
pub(crate) fn read_all_game_creator_agent_runtime_tasks(
path: &Path,
) -> Result<Vec<AgentRuntimeTaskRecord>, String> {
let mut records = Vec::new();
match File::open(path) {
Ok(file) => {
let mut reader = BufReader::new(file);
let mut line_number = 0usize;
loop {
let mut line = Vec::new();
let bytes_read = reader.read_until(b'\n', &mut line).map_err(|error| {
format!("读取 Agent Runtime 任务失败:{}: {error}", path.display())
})?;
if bytes_read == 0 {
break;
}
line_number += 1;
let terminated = line.last() == Some(&b'\n');
if terminated {
line.pop();
if line.last() == Some(&b'\r') {
line.pop();
}
}
let line = match std::str::from_utf8(&line) {
Ok(line) => line.trim(),
Err(error) if !terminated && error.error_len().is_none() => break,
Err(error) => {
return Err(format!(
"读取 Agent Runtime 任务失败:{}: 第 {line_number} 行不是有效 UTF-8{error}",
path.display()
));
}
};
if line.is_empty() {
continue;
}
let mut record = match serde_json::from_str::<AgentRuntimeTaskRecord>(line) {
Ok(record) => record,
Err(error) if !terminated && error.is_eof() => break,
Err(error) => {
return Err(format!(
"读取 Agent Runtime 任务失败:{}: 第 {line_number} 行 JSON 无效:{error}",
path.display()
));
}
};
normalize_game_creator_agent_runtime_task(&mut record);
validate_game_creator_agent_runtime_task_status_phase(&record).map_err(
|error| {
format!(
"读取 Agent Runtime 任务失败:{}: 第 {line_number} 行:{error}",
path.display()
)
},
)?;
validate_game_creator_agent_runtime_task_goal_binding(&record).map_err(
|error| {
format!(
"读取 Agent Runtime 任务失败:{}: 第 {line_number} 行:{error}",
path.display()
)
},
)?;
records.push(record);
}
}
Err(error) if error.kind() == std::io::ErrorKind::NotFound => {}
Err(error) => {
return Err(format!(
"读取 Agent Runtime 任务失败:{}: {error}",
path.display()
));
}
}
Ok(records)
}
fn validate_game_creator_agent_runtime_task_status_phase(
record: &AgentRuntimeTaskRecord,
) -> Result<(), String> {
if !matches!(
record.status.as_str(),
"idle"
| "preparing"
| "pending"
| "running"
| "waiting-for-confirmation"
| "waiting-for-user-input"
| "paused"
| "pausing"
| "cancelling"
| "cancelled"
| "completed"
| "failed"
| "needs-reconciliation"
) {
return Err(format!(
"Agent Runtime task status 无效:runId={} status={}",
record.run_id, record.status
));
}
if !matches!(
record.phase.as_str(),
"idle"
| "public-status-pending"
| "queued"
| "planning"
| "llm"
| "action"
| "provider-action-batch"
| "observation"
| "response"
| "finalizing"
| "waiting-for-confirmation"
| "waiting-for-user-input"
| "waiting-for-provider-retry"
| "waiting-for-process-session"
| "waiting-for-isolated-join"
| "waiting-for-delegate-receipts"
| "waiting-for-manifest-tasks"
| "waiting-for-visual-asset"
| "brief"
| "paused"
| "pausing"
| "cancelling"
| "cancelled"
| "completed"
| "failed"
| "budget-exhausted"
| "needs-reconciliation"
| "completion-contract-failed"
| "conversation-write-failed"
| "public-status-write-failed"
| "parent-terminal"
| "parent-link-missing"
) {
return Err(format!(
"Agent Runtime task phase 无效:runId={} phase={}",
record.run_id, record.phase
));
}
Ok(())
}
pub(super) fn latest_game_creator_agent_runtime_tasks(
records: Vec<AgentRuntimeTaskRecord>,
) -> Vec<AgentRuntimeTaskRecord> {
let mut latest: Vec<AgentRuntimeTaskRecord> = Vec::new();
for record in records {
if let Some(index) = latest
.iter()
.position(|existing| existing.run_id == record.run_id)
{
latest.remove(index);
}
latest.push(record);
}
latest
}
pub(super) fn summarize_game_creator_agent_runtime_task_queue(
records: &[AgentRuntimeTaskRecord],
) -> AgentRuntimeTaskQueueSummary {
let mut summary = AgentRuntimeTaskQueueSummary {
total: records.len() as u32,
..AgentRuntimeTaskQueueSummary::default()
};
for record in records {
match record.status.as_str() {
"pending" => summary.pending += 1,
"running" => summary.running += 1,
"waiting-for-confirmation" => summary.waiting_for_confirmation += 1,
"waiting-for-user-input" => summary.waiting_for_user_input += 1,
"paused" => summary.paused += 1,
"cancelled" => summary.cancelled += 1,
"completed" => summary.completed += 1,
"failed" => summary.failed += 1,
_ => {}
}
if record.updated_at >= summary.updated_at {
summary.updated_at = record.updated_at;
summary.latest_run_id = Some(record.run_id.clone());
}
}
summary
}
pub(super) fn truncate_agent_runtime_text(value: &str, max_chars: usize) -> String {
let value = value.trim();
let mut output = String::new();
for character in value.chars().take(max_chars) {
output.push(character);
}
if value.chars().count() > max_chars {
output.push('…');
}
output
}
pub(super) fn truncate_agent_runtime_text_preserving_tail(value: &str, max_chars: usize) -> String {
let value = value.trim();
let count = value.chars().count();
if count <= max_chars {
return value.to_string();
}
let mut characters = value.chars().rev().take(max_chars).collect::<Vec<_>>();
characters.reverse();
format!("…{}", characters.into_iter().collect::<String>())
}
pub(super) fn sanitize_agent_runtime_text(value: &str, max_chars: usize) -> String {
truncate_agent_runtime_text(sanitize_prompt_context(value).trim(), max_chars)
}
pub(super) fn redact_agent_runtime_url_tokens(value: &str) -> String {
let mut output = String::with_capacity(value.len());
let mut cursor = 0usize;
while cursor < value.len() {
let remaining = &value[cursor..];
let url_prefix_bytes = if remaining
.get(..8)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("https://"))
{
Some(8)
} else if remaining
.get(..7)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("http://"))
{
Some(7)
} else {
None
};
let Some(url_prefix_bytes) = url_prefix_bytes else {
let character = remaining.chars().next().unwrap_or_default();
output.push(character);
cursor += character.len_utf8();
continue;
};
output.push_str("<redacted-url>");
cursor += url_prefix_bytes;
while cursor < value.len() {
let character = value[cursor..].chars().next().unwrap_or_default();
if character.is_whitespace()
|| matches!(
character,
'\'' | '"' | '`' | '<' | '>' | '[' | ']' | '{' | '}'
)
{
break;
}
cursor += character.len_utf8();
}
}
output
}
pub(crate) fn redact_agent_runtime_error(root: &Path, value: &str, max_chars: usize) -> String {
let redacted = redact_agent_runtime_url_tokens(value);
let redacted = redact_agent_runtime_project_paths_raw(root, &redacted);
let redacted = redact_absolute_path_tokens(&redacted);
let redacted = redact_secret_tokens(&redacted);
let mut sanitized = sanitize_error_context(&redacted);
let preserved_markers = [
"<redacted-url>",
"$PROJECT_ROOT",
"<absolute-path>",
"[redacted-secret]",
]
.into_iter()
.filter(|marker| redacted.contains(marker) && !sanitized.contains(marker))
.collect::<Vec<_>>();
if !preserved_markers.is_empty() {
sanitized = format!("{} {}", preserved_markers.join(" "), sanitized.trim());
}
truncate_agent_runtime_text(sanitized.trim(), max_chars)
}
pub(crate) fn redact_agent_runtime_project_paths(
root: &Path,
value: &str,
max_chars: usize,
) -> String {
sanitize_agent_runtime_text(
&redact_agent_runtime_project_paths_raw(root, value),
max_chars,
)
}
pub(super) fn redact_agent_runtime_project_paths_raw(root: &Path, value: &str) -> String {
let mut redacted = value.to_string();
let root_display = root.to_string_lossy();
if !root_display.is_empty() {
redacted = redacted.replace(root_display.as_ref(), "$PROJECT_ROOT");
#[cfg(windows)]
if let Some(non_verbatim_root) = root_display.strip_prefix(r"\\?\") {
redacted = redacted.replace(non_verbatim_root, "$PROJECT_ROOT");
}
}
if let Ok(canonical_root) = root.canonicalize() {
let canonical_display = canonical_root.to_string_lossy();
if !canonical_display.is_empty() && canonical_display != root_display {
redacted = redacted.replace(canonical_display.as_ref(), "$PROJECT_ROOT");
}
}
redacted
}
pub(super) fn redact_agent_runtime_project_paths_preserving_tail(
root: &Path,
value: &str,
max_chars: usize,
) -> String {
let mut redacted = value.to_string();
let root_display = root.to_string_lossy();
if !root_display.is_empty() {
redacted = redacted.replace(root_display.as_ref(), "$PROJECT_ROOT");
#[cfg(windows)]
if let Some(non_verbatim_root) = root_display.strip_prefix(r"\\?\") {
redacted = redacted.replace(non_verbatim_root, "$PROJECT_ROOT");
}
}
if let Ok(canonical_root) = root.canonicalize() {
let canonical_display = canonical_root.to_string_lossy();
if !canonical_display.is_empty() && canonical_display != root_display {
redacted = redacted.replace(canonical_display.as_ref(), "$PROJECT_ROOT");
}
}
truncate_agent_runtime_text_preserving_tail(
sanitize_prompt_context(&redacted).trim(),
max_chars,
)
}
pub(super) fn render_agent_runtime_tool_names(values: &[String], limit: usize) -> String {
if values.is_empty() {
return "无".to_string();
}
let mut names = values
.iter()
.filter(|value| !value.trim().is_empty())
.take(limit)
.cloned()
.collect::<Vec<_>>();
if values.len() > limit {
names.push(format!("等{}项", values.len()));
}
names.join(", ")
}