b12a81e9c2
恢复 relaxed autonomous lane 的美术工具与写入门禁旁路 统一 Runtime 任务与事件目录的安全创建和校验 测试临时目录在 Windows 下初始化当前用户 owner
4320 lines
158 KiB
Rust
4320 lines
158 KiB
Rust
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 401;code=invalid-token;field=authorization;",
|
||
"message=token=token-value-123;authorization: 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 403;reason=forbidden;detail=读取 .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 401;code=invalid-token;payload={\"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-890;secretkey=secret-key-value-901;",
|
||
"authtoken=auth-token-value-012;bearer=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(¤t) {
|
||
// 另一并发锁请求可能在 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(¤t).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(¤t)
|
||
.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(", ")
|
||
}
|