完成 M1C-2b 策划澄清与预算接线

接入三轮澄清中转、确定性 continuation/session 投影及恢复锁序

记录并幂等折叠 planning Provider 活跃时间与末次提交 usage

补齐审批后用户修订谱系校验和质量返工失败关闭

补充并发、恢复、重放、usage 回归并同步技术方案与决策日志
This commit is contained in:
2026-08-17 15:01:33 +00:00
parent 9f12d84678
commit 152cc40c7b
30 changed files with 5294 additions and 199 deletions
@@ -424,7 +424,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
"agent.message" => {
observe_agent_runtime_agent_message(root, agent_id, run_id, &action.input)
}
"agent.delegate" => observe_agent_runtime_project_snapshot_with_lock(
"agent.delegate" => observe_agent_runtime_project_snapshot_with_lock_guard(
root,
agent_id,
run_id,
@@ -432,13 +432,14 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
&action_fingerprint,
pending_action,
true,
|| {
observe_agent_runtime_agent_delegate(
|project_write_lock| {
observe_agent_runtime_agent_delegate_at_locked(
root,
agent_id,
run_id,
action_id,
&action.input,
project_write_lock,
)
},
),
@@ -519,6 +520,31 @@ pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock<F>(
) -> AgentRuntimeToolObservation
where
F: FnOnce() -> AgentRuntimeToolObservation,
{
observe_agent_runtime_project_snapshot_with_lock_guard(
root,
agent_id,
run_id,
action,
action_fingerprint,
pending_action,
validate_revision_gate,
|_| observe(),
)
}
pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock_guard<F>(
root: &Path,
agent_id: &str,
run_id: &str,
action: &AgentRuntimeToolAction,
action_fingerprint: &str,
pending_action: Option<&AgentRuntimePendingToolAction>,
validate_revision_gate: bool,
observe: F,
) -> AgentRuntimeToolObservation
where
F: FnOnce(&ProjectWriteLock) -> AgentRuntimeToolObservation,
{
let tool = action.tool.trim();
let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
@@ -546,7 +572,7 @@ where
) {
return observation;
}
observe()
observe(&_lock)
}
pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_lock(
@@ -4,6 +4,32 @@ pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at(
root: &Path,
runtime: &mut AgentRuntimeState,
pending: &mut AgentRuntimePendingToolAction,
) -> Result<(), String> {
persist_game_creator_agent_user_input_wait_with_project_lock_at(root, runtime, pending, None)
}
pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at_locked(
root: &Path,
runtime: &mut AgentRuntimeState,
pending: &mut AgentRuntimePendingToolAction,
project_lock: &ProjectWriteLock,
) -> Result<(), String> {
if !project_lock.guards_project_root(root)? {
return Err("持久化 planning 用户输入等待缺少当前项目写锁".to_string());
}
persist_game_creator_agent_user_input_wait_with_project_lock_at(
root,
runtime,
pending,
Some(project_lock),
)
}
fn persist_game_creator_agent_user_input_wait_with_project_lock_at(
root: &Path,
runtime: &mut AgentRuntimeState,
pending: &mut AgentRuntimePendingToolAction,
project_lock: Option<&ProjectWriteLock>,
) -> Result<(), String> {
if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
return Err("自主构建 Run 禁止进入 waiting-for-user-input".to_string());
@@ -13,7 +39,13 @@ pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at(
pending.observation = None;
pending.updated_at = unix_timestamp();
write_game_creator_agent_runtime_pending_tool_action(root, pending)?;
let request = match prepare_game_creator_agent_user_input_request_at(root, pending)? {
let recovered_user_input = match project_lock {
Some(project_lock) => {
prepare_game_creator_agent_user_input_request_at_locked(root, pending, project_lock)
}
None => prepare_game_creator_agent_user_input_request_at(root, pending),
}?;
let request = match recovered_user_input {
AgentRuntimeUserInputRecovery::Waiting(request) => request,
AgentRuntimeUserInputRecovery::Answered { .. } => {
return Err("新建用户输入等待时 sidecar 已进入 answered,需由恢复路径继续".to_string());
@@ -25,6 +25,11 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
root,
"runtime.context_compaction.build",
)?;
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
// Advance the immutable Provider-usage projection before any
// request/source bytes are rebuilt from the plan session.
fold_plan_provider_usage_before_new_request_at_locked(root)?;
}
let source = build_game_creator_agent_runtime_context_compaction_source(
root,
agent_id,
@@ -909,6 +909,14 @@ pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) ->
.is_some_and(|count| count > 0)
}
pub(in crate::agent) fn static_delegate_barrier_requires_user_input(detail: &str) -> bool {
detail
.split_whitespace()
.find_map(|part| part.strip_prefix("userInputRequired="))
.and_then(|value| value.parse::<usize>().ok())
.is_some_and(|count| count > 0)
}
pub(in crate::agent) fn static_delegate_barrier_requires_user_revision(detail: &str) -> bool {
detail
.split_whitespace()
@@ -1757,6 +1765,11 @@ mod static_delegate_barrier_detail_gate_tests {
barrier.repair_required_count > 0,
"repairRequired 往返失真:{barrier:?}\ndetail={detail}"
);
assert_eq!(
static_delegate_barrier_requires_user_input(&detail),
barrier.user_input_required_count > 0,
"userInputRequired 往返失真:{barrier:?}\ndetail={detail}"
);
assert_eq!(
static_delegate_barrier_requires_user_revision(&detail),
barrier.user_revision_pending_count > 0,
@@ -117,6 +117,9 @@ pub(in crate::agent) async fn request_game_creator_agent_background_final_reply_
"runtime.provider_request.capture.final_reply",
)?;
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
// Keep every rebuilt request field on the same budget successor
// that will be exposed by the structured injection and binding.
fold_plan_provider_usage_before_new_request_at_locked(root)?;
// Freeze the concrete final-reply request and its Provider-facing
// planning injection under the same project lock as the durable
// session binding. This mirrors tool-plan and prevents an older
@@ -307,6 +307,11 @@ pub(in crate::agent) async fn request_game_creator_agent_background_tool_plan_at
root,
"runtime.provider_request.capture.tool_plan",
)?;
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
// Fold first so the request rebuild, structured injection and
// frozen session binding all observe one budget successor.
fold_plan_provider_usage_before_new_request_at_locked(root)?;
}
// Exact planning requests must freeze the session and the concrete
// request object under one project lock. Rebuild once while holding
// that lock so a session successor cannot be used to re-label an
@@ -506,6 +506,8 @@ pub(crate) use entrypoints::{
#[cfg(test)]
pub(crate) use finalization::resume_game_creator_agent_finalization_for_test_at;
pub(crate) use finalization::AgentRuntimePendingActionResume;
#[cfg(test)]
pub(crate) use interaction::acquire_game_creator_agent_runtime_user_input_answer_locks_for_test;
pub(crate) use interaction::{
agent_runtime_tool_requires_repository_context_fingerprint_gate,
answer_game_creator_agent_runtime_user_input_at, confirm_game_creator_agent_runtime_task_at,
@@ -441,6 +441,79 @@ pub(in crate::agent) fn resolve_game_creator_agent_runtime_user_input_action(
Ok((agent_id, task, runtime, pending))
}
type AgentRuntimeUserInputActionResolution = (
String,
AgentRuntimeTaskRecord,
AgentRuntimeState,
AgentRuntimePendingToolAction,
);
fn resolve_game_creator_agent_runtime_user_input_action_with_ordered_locks(
root: &Path,
agent_id: &str,
run_id: &str,
action_id: &str,
) -> Result<
(
Option<ProjectWriteLock>,
AgentRuntimeTaskLock,
AgentRuntimeUserInputActionResolution,
),
String,
> {
// Ordinary user-input keeps the existing execution-only path. Fast GDD
// answer validation may repair/read `session.json`, so route that exact
// pending through project -> execution and re-read its identity under the
// selected lock set before writing answer-prepared or binding delivery.
let (_, _, _, optimistic_pending) =
resolve_game_creator_agent_runtime_user_input_action(root, agent_id, run_id, action_id)?;
let optimistic_planning =
plan_clarification_pending_requires_project_lock_at(root, &optimistic_pending)?;
if optimistic_planning {
let project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.clarification.answer.command",
)?;
let runtime_lock = acquire_game_creator_agent_runtime_task_lock_with_wait(root, agent_id)?;
let resolved = resolve_game_creator_agent_runtime_user_input_action(
root, agent_id, run_id, action_id,
)?;
return Ok((Some(project_lock), runtime_lock, resolved));
}
let runtime_lock = acquire_game_creator_agent_runtime_task_lock_with_wait(root, agent_id)?;
let resolved =
resolve_game_creator_agent_runtime_user_input_action(root, agent_id, run_id, action_id)?;
if plan_clarification_pending_requires_project_lock_at(root, &resolved.3)? {
drop(runtime_lock);
let project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.clarification.answer.command",
)?;
let runtime_lock = acquire_game_creator_agent_runtime_task_lock_with_wait(root, agent_id)?;
let resolved = resolve_game_creator_agent_runtime_user_input_action(
root, agent_id, run_id, action_id,
)?;
Ok((Some(project_lock), runtime_lock, resolved))
} else {
Ok((None, runtime_lock, resolved))
}
}
#[cfg(test)]
pub(crate) fn acquire_game_creator_agent_runtime_user_input_answer_locks_for_test(
root: &Path,
agent_id: &str,
run_id: &str,
action_id: &str,
) -> Result<(Option<ProjectWriteLock>, AgentRuntimeTaskLock), String> {
let (project_lock, runtime_lock, _) =
resolve_game_creator_agent_runtime_user_input_action_with_ordered_locks(
root, agent_id, run_id, action_id,
)?;
Ok((project_lock, runtime_lock))
}
pub(crate) fn answer_game_creator_agent_runtime_user_input_at(
root: &Path,
agent_id: &str,
@@ -452,17 +525,29 @@ pub(crate) fn answer_game_creator_agent_runtime_user_input_at(
) -> Result<AgentRuntimeResult, String> {
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
validate_project_root(root)?;
let runtime_lock = acquire_game_creator_agent_runtime_task_lock_with_wait(root, &agent_id)?;
let (agent_id, task, mut runtime, mut pending) =
resolve_game_creator_agent_runtime_user_input_action(root, &agent_id, run_id, action_id)?;
let (project_lock, runtime_lock, resolved) =
resolve_game_creator_agent_runtime_user_input_action_with_ordered_locks(
root, &agent_id, run_id, action_id,
)?;
let (agent_id, task, mut runtime, mut pending) = resolved;
let already_observed = pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED;
let (request, observation) = answer_game_creator_agent_user_input_request_for_pending_at(
root,
&pending,
request_id,
response_id,
answers,
)?;
let (request, observation) = match project_lock.as_ref() {
Some(project_lock) => answer_game_creator_agent_user_input_request_for_pending_at_locked(
root,
&pending,
request_id,
response_id,
answers,
project_lock,
)?,
None => answer_game_creator_agent_user_input_request_for_pending_at(
root,
&pending,
request_id,
response_id,
answers,
)?,
};
if already_observed {
if pending.observation.as_ref() != Some(&observation) {
return Err("用户输入回答与已持久化 observation 冲突".to_string());
@@ -522,6 +607,7 @@ pub(crate) fn answer_game_creator_agent_runtime_user_input_at(
})?;
}
let result = read_game_creator_agent_runtime_at(root, &agent_id)?;
drop(project_lock);
if external_agent_runner_owns_background_execution() {
let answered_run_id = pending.run_id.clone();
let answered_action_id = pending.action_id.clone();
@@ -925,23 +925,45 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
&task,
retry_link.is_some(),
)?;
let (mut result, actual_retry_run_id) = with_agent_conversation_session_lane_at(
root,
&agent_id,
"Agent Runtime 重试入队",
|| {
start_game_creator_agent_background_task_with_link_in_session_lane_at(
root,
&agent_id,
Some(&task.session_id),
&task.task,
&retry_run_id,
&retry_source,
Some(&retry_run_profile),
retry_link.as_ref(),
)
},
)?;
let planning_retry = agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID;
let (mut result, actual_retry_run_id) = if planning_retry {
// Planning enqueue owns both locks. Keep the global order identical
// to ordinary/delegated starts: project first, Session lane second.
// The locked entry also projects the retry child before it can start.
let project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.child-session.retry",
)?;
start_game_creator_agent_background_task_with_link_locked_at(
root,
&agent_id,
Some(&task.session_id),
&task.task,
&retry_run_id,
&retry_source,
Some(&retry_run_profile),
retry_link.as_ref(),
&project_lock,
)?
} else {
with_agent_conversation_session_lane_at(
root,
&agent_id,
"Agent Runtime 重试入队",
|| {
start_game_creator_agent_background_task_with_link_in_session_lane_at(
root,
&agent_id,
Some(&task.session_id),
&task.task,
&retry_run_id,
&retry_source,
Some(&retry_run_profile),
retry_link.as_ref(),
)
},
)?
};
let retry_task_sha256 = format!("{:x}", Sha256::digest(task.task.as_bytes()));
let retry_task_chars = task.task.chars().count();
let retry_goal_bound = task.goal_id.is_some();
@@ -969,12 +991,16 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
}),
)?;
result.accepted_run_id = Some(actual_retry_run_id.clone());
notify_external_agent_runner_after_background_task_enqueue(
root,
&agent_id,
&task.session_id,
&actual_retry_run_id,
)?;
if !planning_retry {
// The planning locked entry performs this notification after releasing
// its Session lane; the legacy in-lane entry deliberately does not.
notify_external_agent_runner_after_background_task_enqueue(
root,
&agent_id,
&task.session_id,
&actual_retry_run_id,
)?;
}
Ok(result)
}
@@ -1063,10 +1063,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
if let Some(blocker) =
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
{
let waits_for_delivery = blocker
.detail
.as_deref()
.is_some_and(static_delegate_barrier_has_waiting_deliveries);
let waits_for_delivery = blocker.detail.as_deref().is_some_and(|detail| {
static_delegate_barrier_has_waiting_deliveries(detail)
|| static_delegate_barrier_requires_user_input(detail)
});
if waits_for_delivery {
if let Err(error) = persist_waiting_static_delegate_parent_context_at(
&root,
@@ -2079,13 +2079,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
.detail
.as_deref()
.is_some_and(static_delegate_barrier_requires_user_revision);
let user_input_required = blocker.detail.as_deref().is_some_and(|detail| {
detail
.split_whitespace()
.find_map(|part| part.strip_prefix("userInputRequired="))
.and_then(|value| value.parse::<usize>().ok())
.is_some_and(|count| count > 0)
});
let user_input_required = blocker
.detail
.as_deref()
.is_some_and(static_delegate_barrier_requires_user_input);
runtime.status = "running".to_string();
if user_revision_pending {
runtime.phase = "planning".to_string();
@@ -2096,36 +2093,21 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
runtime.next_step =
"调用 agent.delegate,并把 repairOfDelegationId 指向原 delivery;不得把用户修订计入 repair_depth".to_string();
} else if user_input_required {
let deliveries = match claimed_static_delegate_deliveries_at(
&root,
&runtime.agent_id,
&runtime.run_id,
) {
Ok(deliveries) => deliveries,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("读取 needs-user-input 回执失败:{error}"),
);
}
};
if let Err(error) = ensure_static_delegate_user_input_wait_at(
&root,
&mut runtime,
&deliveries,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("Supervisor 用户澄清请求无法安全进入等待态:{error}"),
);
}
return AgentBackgroundTaskOutcome::WaitingForUserInput;
// The background drain owns the Supervisor execution lane. Persist a
// receipt wait first, then let task_queue schedule parent-wake after this
// pass returns and the lane is released. Parent-wake acquires
// project -> execution and creates the unique clarification pending under
// both locks; doing that here would invert the M1C-2b planning projection
// order.
runtime.phase = "waiting-for-delegate-receipts".to_string();
runtime.current_action =
"等待创建 Project Supervisor 用户澄清请求".to_string();
runtime.waiting_on =
"释放当前 execution lane 后投影 planning session 与澄清 pending"
.to_string();
runtime.next_step =
"由 lane 外 parent-wake 按 project → execution 锁序创建唯一澄清请求"
.to_string();
} else if repair_required {
runtime.phase = "planning".to_string();
runtime.current_action = "等待 Project Supervisor 发起唯一返工".to_string();
@@ -2194,7 +2176,8 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
AgentBackgroundTaskOutcome::WaitingForIsolatedJoin,
))
} else if observation.tool == "runtime.delegate_receipts"
&& static_delegate_barrier_has_waiting_deliveries(detail)
&& (static_delegate_barrier_has_waiting_deliveries(detail)
|| static_delegate_barrier_requires_user_input(detail))
{
Some((
"agent.runtime.agent.delegate_receipts.waiting",
@@ -159,12 +159,13 @@ pub(in crate::agent) fn replay_supervisor_delivery_pending_action_at(
}
}
match pending.action.tool.as_str() {
"agent.delegate" => observe_agent_runtime_agent_delegate(
"agent.delegate" => observe_agent_runtime_agent_delegate_at_locked(
root,
&pending.agent_id,
&pending.run_id,
Some(&pending.action_id),
&pending.action.input,
&_project_lock,
),
"agent.run_status" => observe_agent_runtime_run_status(
root,
@@ -1068,6 +1069,74 @@ pub(crate) fn resume_game_creator_agent_pending_tool_action_at(
if migrate_legacy_autonomous_confirmation_at(root, &runtime, &mut pending)? {
can_repair_terminal_receipt = true;
}
let (runtime_lock, planning_user_input_project_lock) = if pending.status
== AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT
&& plan_clarification_pending_requires_project_lock_at(root, &pending)?
{
let expected_run_id = runtime.run_id.clone();
let expected_session_id = runtime.session_id.clone();
let expected_action_id = pending.action_id.clone();
let expected_action_fingerprint = pending.action_fingerprint.clone();
drop(runtime_lock);
let project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.clarification.answer-recovery",
)?;
let runtime_lock = acquire_game_creator_agent_runtime_task_lock_with_wait(root, agent_id)?;
runtime = read_game_creator_agent_runtime_at(root, agent_id)?.state;
if runtime.run_id != expected_run_id
|| runtime.session_id != expected_session_id
|| runtime.status != "waiting-for-user-input"
|| runtime.phase != "waiting-for-user-input"
|| game_creator_agent_runtime_has_reconciliation_barrier(root, agent_id)?
{
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
}
let Some(current_task) =
read_latest_game_creator_agent_runtime_task_by_run_id(root, agent_id, &runtime.run_id)?
else {
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
};
if current_task.session_id != expected_session_id
|| current_task.status != "waiting-for-user-input"
|| current_task.phase != "waiting-for-user-input"
|| !game_creator_agent_runtime_pending_tool_action_exists(
root,
agent_id,
&runtime.run_id,
)
{
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
}
pending =
read_game_creator_agent_runtime_pending_tool_action(root, agent_id, &runtime.run_id)?;
if pending.action_id != expected_action_id
|| pending.status != AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT
{
// The old recovery candidate became obsolete while its execution
// lane was released. A concurrent answer may legitimately have
// advanced the exact pending to observed-approved, or another
// current action may now own the run. Do not overwrite that newer
// state with a reconciliation projection; let the caller inspect
// the re-read runtime on its next pass.
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
}
if pending.session_id != expected_session_id
|| pending.action_fingerprint != expected_action_fingerprint
{
mark_game_creator_agent_runtime_needs_reconciliation_at(
root,
&mut runtime,
&pending,
"planning 用户回答恢复发现同一 pending 的 immutable identity 漂移",
)?;
return read_game_creator_agent_runtime_at(root, agent_id)
.map(AgentRuntimePendingActionResume::Handled);
}
(runtime_lock, Some(project_lock))
} else {
(runtime_lock, None)
};
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT {
let planning_agent = runtime.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID;
if planning_agent
@@ -1090,15 +1159,31 @@ pub(crate) fn resume_game_creator_agent_pending_tool_action_at(
}
if game_creator_agent_runtime_cancel_requested(root, &runtime) {
cancel_game_creator_agent_user_input_request_for_pending_at(root, &pending)?;
mark_game_creator_agent_runtime_cancelled_at(
root,
&mut runtime,
"Agent 后台任务已按开发者请求取消",
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
)?;
match planning_user_input_project_lock.as_ref() {
Some(_) => mark_game_creator_agent_runtime_cancelled_at_locked(
root,
&mut runtime,
"Agent 后台任务已按开发者请求取消",
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
)?,
None => mark_game_creator_agent_runtime_cancelled_at(
root,
&mut runtime,
"Agent 后台任务已按开发者请求取消",
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
)?,
}
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
}
match prepare_game_creator_agent_user_input_request_at(root, &pending) {
let recovered_user_input = match planning_user_input_project_lock.as_ref() {
Some(project_lock) => prepare_game_creator_agent_user_input_request_at_locked(
root,
&pending,
project_lock,
),
None => prepare_game_creator_agent_user_input_request_at(root, &pending),
};
match recovered_user_input {
Ok(AgentRuntimeUserInputRecovery::Waiting(request)) => {
runtime.pending_tool_action = Some(pending.summary());
runtime.status = "waiting-for-user-input".to_string();
@@ -1140,12 +1225,20 @@ pub(crate) fn resume_game_creator_agent_pending_tool_action_at(
can_repair_terminal_receipt = true;
}
Ok(AgentRuntimeUserInputRecovery::Cancelled) => {
mark_game_creator_agent_runtime_cancelled_at(
root,
&mut runtime,
"Agent 用户输入请求已取消",
Some("Runner 恢复时发现用户输入 sidecar 已取消"),
)?;
match planning_user_input_project_lock.as_ref() {
Some(_) => mark_game_creator_agent_runtime_cancelled_at_locked(
root,
&mut runtime,
"Agent 用户输入请求已取消",
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
)?,
None => mark_game_creator_agent_runtime_cancelled_at(
root,
&mut runtime,
"Agent 用户输入请求已取消",
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
)?,
}
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
}
Err(error) => {
@@ -1160,6 +1253,7 @@ pub(crate) fn resume_game_creator_agent_pending_tool_action_at(
}
}
}
drop(planning_user_input_project_lock);
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING
&& pending.action.tool == "canvas.asset_generate"
{
@@ -324,11 +324,28 @@ pub(crate) fn schedule_waiting_static_delegate_parent_wake_after_lane_release(
/// Convert a claimed `needs-user-input` delivery into the Supervisor's own
/// durable user-input action. The child never owns this action: it is tied to
/// the parent run and therefore passes the normal user-input owner gate.
#[cfg(test)]
pub(crate) fn ensure_static_delegate_user_input_wait_at(
root: &Path,
runtime: &mut AgentRuntimeState,
deliveries: &[StaticDelegateDeliveryRecord],
) -> Result<bool, String> {
let project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.clarification.pending",
)?;
ensure_static_delegate_user_input_wait_at_locked(root, runtime, deliveries, &project_lock)
}
pub(crate) fn ensure_static_delegate_user_input_wait_at_locked(
root: &Path,
runtime: &mut AgentRuntimeState,
deliveries: &[StaticDelegateDeliveryRecord],
project_lock: &ProjectWriteLock,
) -> Result<bool, String> {
if !project_lock.guards_project_root(root)? {
return Err("Supervisor 澄清 pending 投影缺少当前项目写锁".to_string());
}
let mut pending_deliveries = deliveries.iter().filter(|delivery| {
delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
@@ -337,6 +354,10 @@ pub(crate) fn ensure_static_delegate_user_input_wait_at(
let Some(delivery) = pending_deliveries.next() else {
return Ok(false);
};
// Fast GDD additionally projects the completed planning child into its
// derived session before the user can see or answer the card. Ordinary
// static-delegate questions remain byte-for-byte on the existing path.
project_plan_session_awaiting_user_input_at_locked(root, runtime, delivery, project_lock)?;
// Each durable request belongs to exactly one original delivery. Other
// deliveries remain behind the completion barrier and are asked next.
let result = delivery
@@ -403,7 +424,12 @@ pub(crate) fn ensure_static_delegate_user_input_wait_at(
AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT,
None,
)?;
persist_game_creator_agent_user_input_wait_at(root, runtime, &mut pending)?;
persist_game_creator_agent_user_input_wait_at_locked(
root,
runtime,
&mut pending,
project_lock,
)?;
Ok(true)
}
@@ -51,6 +51,55 @@ pub(in crate::agent) fn mark_waiting_provider_retry_needs_reconciliation_at(
read_game_creator_agent_runtime_at(root, agent_id)
}
fn mark_plan_session_projection_needs_reconciliation_at(
root: &Path,
task: &AgentRuntimeTaskRecord,
error: &str,
) -> Result<AgentRuntimeResult, String> {
let mut runtime = match read_game_creator_agent_runtime_at(root, &task.agent_id) {
Ok(result) if result.state.run_id == task.run_id => result.state,
Ok(_) | Err(_) => agent_runtime_state_from_task_record(task),
};
if runtime.phase == "needs-reconciliation" {
return read_game_creator_agent_runtime_at(root, &task.agent_id);
}
let error = redact_agent_runtime_error(root, error, 500);
runtime.status = "failed".to_string();
runtime.phase = "needs-reconciliation".to_string();
runtime.current_action = "Fast GDD session 恢复需要人工核对".to_string();
runtime.waiting_on = "开发者核对 planning session、delivery 与 continuation 身份".to_string();
runtime.next_step = "修复冲突的持久投影后显式恢复或取消当前 run".to_string();
runtime.pending_tool_action = None;
runtime.error = Some(error.clone());
runtime.updated_at = unix_timestamp();
append_game_creator_agent_runtime_task(root, &runtime)?;
refresh_game_creator_agent_runtime_task_queue(root, &mut runtime)?;
write_game_creator_agent_runtime_state(root, &runtime)?;
append_game_creator_agent_runtime_event(
root,
&runtime,
"plan.session_recovery.needs_reconciliation",
"failed",
"needs-reconciliation",
"Fast GDD session 无法在 Provider 恢复前安全投影,Runtime 已停止自动请求。",
Some(&error),
)?;
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.plan.session_recovery.needs_reconciliation",
"agentId": runtime.agent_id,
"taskId": runtime.task_id,
"sessionId": runtime.session_id,
"runId": runtime.run_id,
"source": runtime.source,
"error": error,
}),
);
emit_game_creator_agent_runtime_update(root, &task.agent_id);
read_game_creator_agent_runtime_at(root, &task.agent_id)
}
struct MissingPlanSubmitAnchorCandidate {
runtime: AgentRuntimeState,
action_id: String,
@@ -989,6 +1038,77 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
else {
continue;
};
// A planning continuation can be durable while `session.json` still
// points at the answered/waiting round. Recovery used to enter the
// pending-action and Provider-batch paths below before repairing that
// projection, so the continuation could issue its first request with
// stale decisions/appliedAnswers.
//
// Never acquire the project lock while retaining the Agent execution
// lock: normal enqueue owns project -> Session lane -> execution. Drop
// and reacquire in project -> execution order, re-read the task under
// the new lock set, project exactly once, then release the project lock
// before any later path can enter the Session lane.
let runtime_lock = if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID
&& read_recoverable_runnable_game_creator_agent_runtime_task(root, &agent_id)?.is_some()
{
drop(runtime_lock);
let project_lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.child-session.recovery",
) {
Ok(lock) => lock,
Err(error) if static_delegate_parent_wake_error_is_transient(&error) => continue,
Err(error) => {
return Err(format!("恢复 Fast GDD session 前取得项目锁失败:{error}"));
}
};
let Some(runtime_lock) =
try_acquire_game_creator_agent_runtime_task_lock(root, &agent_id)?
else {
continue;
};
let Some(task) =
read_recoverable_runnable_game_creator_agent_runtime_task(root, &agent_id)?
else {
continue;
};
let projection = match plan_session_already_projects_planning_child_task_at_locked(
root,
&task,
&project_lock,
) {
Ok(true) => Ok(true),
Ok(false) => ensure_plan_session_for_planning_child_task_at_locked(
root,
&task,
&project_lock,
),
Err(error) => Err(error),
};
match projection {
Ok(true) => {}
Ok(false) => {
resumed.push(mark_plan_session_projection_needs_reconciliation_at(
root,
&task,
"PLAN_NEEDS_RECONCILIATION: project-planning 恢复任务未命中 planning session 协调器",
)?);
continue;
}
Err(error) if static_delegate_parent_wake_error_is_transient(&error) => continue,
Err(error) => {
resumed.push(mark_plan_session_projection_needs_reconciliation_at(
root, &task, &error,
)?);
continue;
}
}
drop(project_lock);
runtime_lock
} else {
runtime_lock
};
if let Some(result) = reconcile_missing_plan_submit_anchors_at(root, &agent_id)? {
resumed.push(result);
drop(runtime_lock);
@@ -2032,3 +2152,48 @@ mod plan_gdd_approval_wait_recovery_tests {
));
}
}
#[cfg(test)]
mod plan_session_recovery_gate_tests {
use super::*;
#[test]
fn planning_recovery_contains_session_identity_conflict_before_provider() {
let temporary =
crate::tests::canonical_test_tempdir("plan-session-recovery-gate-conflict-");
let root = temporary.path();
init_local_game_project_at(root, "plan-recovery-gate", "策划恢复门冲突收敛")
.expect("init project");
start_game_creator_agent_runtime_task_at(
root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
"不具备 Supervisor 委派身份的伪造策划任务",
"plan-session-recovery-conflict-run",
"agent-background-task",
"准备执行伪造策划任务",
vec!["不得发起 Provider 请求".to_string()],
)
.expect("persist recoverable planning task");
let resumed = resume_game_creator_agent_background_tasks_unredacted_at(root)
.expect("identity conflict is contained to the planning task");
let contained = resumed
.iter()
.find(|result| result.state.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID)
.expect("planning task is returned as contained");
assert_eq!(contained.state.status, "failed");
assert_eq!(contained.state.phase, "needs-reconciliation");
assert!(contained
.state
.error
.as_deref()
.is_some_and(|error| error.contains("PLAN_SOURCE_PROFILE_MISMATCH")));
let public_audit = fs::read_to_string(root.join(".agent/agent.db")).unwrap_or_default();
assert!(public_audit.contains("agent.runtime.plan.session_recovery.needs_reconciliation"));
assert!(
!public_audit.contains(AGENT_RUNTIME_PROVIDER_REQUEST_LIFECYCLE_RECORD_TYPE),
"session identity 冲突必须在任何 Provider lifecycle 之前停止"
);
}
}
@@ -173,6 +173,79 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_at(
source: &str,
run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
) -> Result<(AgentRuntimeResult, String), String> {
// Planning session projection needs both the project lock and the target
// session lane. Always acquire them in project -> session order. The
// in-session entry below therefore rejects an unguarded planning child
// instead of trying to acquire the project lock while holding the lane.
if agent_id.trim() == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
let project_write_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.child-session.enqueue",
)?;
return start_game_creator_agent_background_task_with_link_locked_at(
root,
agent_id,
session_id,
task,
run_id,
source,
run_profile,
task_link,
&project_write_lock,
);
}
start_game_creator_agent_background_task_with_link_with_project_lock_at(
root,
agent_id,
session_id,
task,
run_id,
source,
run_profile,
task_link,
None,
)
}
pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_locked_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
task: &str,
run_id: &str,
source: &str,
run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
project_write_lock: &ProjectWriteLock,
) -> Result<(AgentRuntimeResult, String), String> {
if !project_write_lock.guards_project_root(root)? {
return Err("Agent 后台任务入队缺少当前项目写锁".to_string());
}
start_game_creator_agent_background_task_with_link_with_project_lock_at(
root,
agent_id,
session_id,
task,
run_id,
source,
run_profile,
task_link,
Some(project_write_lock),
)
}
#[allow(clippy::too_many_arguments)]
fn start_game_creator_agent_background_task_with_link_with_project_lock_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
task: &str,
run_id: &str,
source: &str,
run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
project_write_lock: Option<&ProjectWriteLock>,
) -> Result<(AgentRuntimeResult, String), String> {
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
validate_project_root(root)?;
@@ -181,7 +254,7 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_at(
&agent_id,
"Agent Session Runtime 入队",
|| {
start_game_creator_agent_background_task_with_link_in_session_lane_at(
start_game_creator_agent_background_task_with_link_in_session_lane_with_project_lock_at(
root,
&agent_id,
session_id,
@@ -190,6 +263,7 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_at(
source,
run_profile,
task_link,
project_write_lock,
)
},
)?;
@@ -254,6 +328,36 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_in_se
run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
) -> Result<(AgentRuntimeResult, String), String> {
start_game_creator_agent_background_task_with_link_in_session_lane_with_project_lock_at(
root,
agent_id,
session_id,
task,
run_id,
source,
run_profile,
task_link,
None,
)
}
#[allow(clippy::too_many_arguments)]
fn start_game_creator_agent_background_task_with_link_in_session_lane_with_project_lock_at(
root: &Path,
agent_id: &str,
session_id: Option<&str>,
task: &str,
run_id: &str,
source: &str,
run_profile: Option<&str>,
task_link: Option<&AgentRuntimeTaskLink>,
project_write_lock: Option<&ProjectWriteLock>,
) -> Result<(AgentRuntimeResult, String), String> {
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID && project_write_lock.is_none() {
return Err(
"project-planning 入队必须在取得项目写锁后再进入 Agent Session lane".to_string(),
);
}
let isolated_instance = agent_id
.starts_with("child-")
.then(|| resolve_isolated_agent_instance_at(root, &agent_id))
@@ -398,6 +502,41 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_in_se
return Err(format!("后台任务用户消息落盘失败,任务未执行:{error}"));
}
}
if game_creator_agent_runtime_terminal_status(&pending_task).is_none() {
let projection = project_write_lock.map_or(Ok(false), |project_write_lock| {
ensure_plan_session_for_planning_child_task_at_locked(
root,
&pending_task,
project_write_lock,
)
});
if let Err(error) = projection {
let error = redact_agent_runtime_project_paths(root, &error, 500);
let failed_task = AgentRuntimeTaskRecord {
status: "failed".to_string(),
phase: "planning-session-projection-failed".to_string(),
current_action: "Fast GDD session 未能安全绑定,后台任务未执行".to_string(),
terminal_detail: Some(error.clone()),
error: Some(error.clone()),
updated_at: unix_timestamp(),
..pending_task.clone()
};
append_game_creator_agent_runtime_task_record(root, &failed_task)?;
publish_game_creator_agent_delegate_result(root, &failed_task, Some(&error));
let _ = append_agent_db_record(
root,
serde_json::json!({
"recordType": "agent.runtime.background_task.queue_warning",
"agentId": pending_task.agent_id,
"sessionId": pending_task.session_id,
"runId": pending_task.run_id,
"warningKind": "planning-session-projection-failed",
"error": sanitize_agent_runtime_text(&error, 240),
}),
);
return Err(format!("Fast GDD session 投影失败,任务未执行:{error}"));
}
}
if requires_public_start_status {
if let Err(error) =
ensure_game_creator_agent_runtime_accepted_public_status_at(root, &pending_task)
@@ -9,6 +9,8 @@ mod finalization;
mod json_sidecar;
mod models;
mod planning_approval;
mod planning_coordinator;
mod planning_provider_usage;
mod planning_storage;
mod planning_submit;
mod provider_control;
@@ -25,12 +27,15 @@ pub(in crate::agent) use finalization::*;
pub(in crate::agent) use json_sidecar::*;
pub(in crate::agent) use models::*;
pub(crate) use planning_approval::*;
pub(crate) use planning_coordinator::*;
pub(crate) use planning_provider_usage::*;
pub(crate) use planning_storage::*;
pub(crate) use planning_submit::*;
pub(in crate::agent) use provider_control::*;
pub(in crate::agent) use provider_retry::*;
pub(in crate::agent) use real_e2e_checkpoint::*;
pub(in crate::agent) use response_stream::*;
pub(crate) use run_configuration::validate_project_supervisor_plan_root_binding_at as validate_project_supervisor_plan_root_binding_for_crate_at;
pub(in crate::agent) use run_configuration::*;
pub(in crate::agent) use steering::*;
pub(in crate::agent) use verification::*;
@@ -988,7 +988,7 @@ fn project_receipt_locked(
None => {}
}
match project_generic_submit_observation_locked(root, receipt) {
let generic_submit_consumed = match project_generic_submit_observation_locked(root, receipt) {
Ok(consumed) => {
if consumed {
if approval_pending_cleanup_eligible
@@ -999,12 +999,14 @@ fn project_receipt_locked(
} else {
recovery_pending = true;
}
consumed
}
Err(error) => {
let _ = error;
recovery_pending = true;
false
}
}
};
if receipt.action != "approve" {
if mark_static_delegate_delivery_user_revision_requested_at(
root,
@@ -1030,10 +1032,32 @@ fn project_receipt_locked(
false
}
};
let mut session_projection_ready = false;
if receipt.version == latest.version || session_points_to_receipt {
if let Err(error) = project_plan_session_locked(root, receipt_gdd, receipt) {
recovery_pending = true;
let _ = error;
} else {
session_projection_ready = true;
}
}
// Provider usage is an immutable fact, while the session value is only a
// projection. The final `plan.submit_gdd` request can finish immediately
// before the approval receipt is written; its v4 submit batch then keeps
// the fold deferred until this receipt consumes both generic anchors.
// Fold only after the receipt/session successor is durable and the batch
// cleanup returned an exact success. Deferred or malformed facts remain
// a recovery barrier instead of being force-written into the session.
if generic_submit_consumed && session_projection_ready {
match fold_plan_provider_usage_into_session_at_locked(root) {
Ok(
PlanProviderUsageFoldOutcome::NoSession
| PlanProviderUsageFoldOutcome::Unchanged
| PlanProviderUsageFoldOutcome::Advanced,
) => {}
Ok(PlanProviderUsageFoldOutcome::Deferred) | Err(_) => {
recovery_pending = true;
}
}
}
Ok(recovery_pending)
File diff suppressed because it is too large Load Diff
@@ -3674,6 +3674,191 @@ mod tests {
}
}
#[tokio::test]
async fn approval_receipt_consumes_submit_batch_and_folds_final_provider_usage_once() {
let (root, mut context, input) = submit_fixture();
let mut child_runtime = start_game_creator_agent_runtime_task_at(
&root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
"提交 Fast GDD",
&context.created_by_run_id,
"agent-delegate",
"提交一份可审批的 Fast GDD",
vec!["读取项目事实".to_string(), "提交 GDD".to_string()],
)
.expect("start planning child task");
child_runtime.session_id = context.session_id.clone();
child_runtime.parent_agent_id = Some(context.root_agent_id.clone());
child_runtime.parent_run_id = Some(context.root_run_id.clone());
child_runtime.delegation_id = Some(context.delegation_id.clone());
child_runtime.run_profile_binding_fingerprint =
context.run_profile_binding_fingerprint.clone();
append_game_creator_agent_runtime_task(&root, &child_runtime)
.expect("persist planning child identity");
write_game_creator_agent_runtime_state(&root, &child_runtime)
.expect("persist current planning child identity");
let mut snapshot = AgentRuntimeProviderRequestSnapshot {
project_id: context.project_id.clone(),
agent_id: child_runtime.agent_id.clone(),
task_id: child_runtime.task_id.clone(),
session_id: child_runtime.session_id.clone(),
run_id: child_runtime.run_id.clone(),
source: child_runtime.source.clone(),
goal_id: child_runtime.goal_id.clone(),
goal_revision: child_runtime.goal_revision,
goal_snapshot_fingerprint: agent_goal_snapshot_fingerprint_for_state_at(
&root,
&child_runtime,
)
.expect("planning goal snapshot fingerprint"),
applied_steer_cursor: child_runtime.applied_steer_cursor,
request_kind: "tool-plan".to_string(),
request_slot: "loop-1-repair-0".to_string(),
web_search_enabled: false,
allow_idle_context_compaction: false,
planning_session_binding: None,
};
let binding = capture_plan_provider_session_binding_for_snapshot(
&root,
&child_runtime,
&snapshot,
&format!("sha256-serde-json-v2:{}", "7".repeat(64)),
)
.expect("capture submit Provider binding");
snapshot.planning_session_binding = Some(binding.clone());
let request_id = game_creator_agent_runtime_provider_request_id(&snapshot);
assert_eq!(request_id, binding.provider_request_id);
assert!(
append_game_creator_agent_runtime_provider_request_lifecycle(
&root,
&snapshot,
&request_id,
"started",
)
.expect("append final submit request start")
);
let usage_scope =
capture_plan_provider_usage_scope_at_locked(&root, &snapshot, &request_id)
.expect("capture final submit usage scope");
assert!(persist_plan_provider_usage_fact_at(
&root,
&snapshot,
&request_id,
usage_scope.as_ref(),
"completed",
23,
)
.expect("persist final submit usage fact"));
assert!(
append_game_creator_agent_runtime_provider_request_lifecycle(
&root,
&snapshot,
&request_id,
"completed",
)
.expect("append final submit request completion")
);
let plan = AgentRuntimeToolPlan {
thinking_summary: "Fast GDD 已收敛,提交审批".to_string(),
plan_update: None,
plan: vec!["提交 Fast GDD".to_string()],
actions: vec![AgentRuntimeToolAction {
tool: PLAN_SUBMIT_GDD_TOOL.to_string(),
reason: Some("提交当前策划版本".to_string()),
input: serde_json::to_value(&input).expect("serialize plan.submit_gdd input"),
}],
response: String::new(),
};
let project_revision =
read_game_creator_agent_runtime_project_revision(&root).expect("read revision");
let repository_fingerprint = build_repository_startup_context_at(&root)
.expect("build repository context")
.fingerprint;
let batch =
match prepare_game_creator_agent_runtime_provider_action_batch_with_planning_binding(
&root,
&child_runtime,
"提交一份可审批的 Fast GDD",
&plan,
&[],
&project_revision,
&repository_fingerprint,
Some(&binding),
)
.await
.expect("prepare exact plan.submit_gdd batch")
{
AgentRuntimeProviderActionBatchPreparation::Ready(batch) => batch,
other => panic!("plan.submit_gdd batch must be ready: {other:?}"),
};
let pending = batch.actions[0].clone();
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
.expect("persist standalone submit anchor");
context.action_id = pending.action_id.clone();
context.action_fingerprint = pending.action_fingerprint.clone();
execute_plan_submit_gdd(&root, &context, &input).expect("commit GDD");
assert_eq!(
fold_plan_provider_usage_into_session_at_locked(&root)
.expect("submit batch must defer final usage"),
PlanProviderUsageFoldOutcome::Deferred
);
let mut child_completed = child_runtime.clone();
child_completed.status = "completed".to_string();
child_completed.phase = "completed".to_string();
child_completed.current_action = "Fast GDD 已提交".to_string();
child_completed.pending_tool_action = Some(pending.summary());
append_game_creator_agent_runtime_task(&root, &child_completed)
.expect("append completed planning child task");
write_game_creator_agent_runtime_state(&root, &child_completed)
.expect("persist completed planning child state");
let gdd = read_plan_gdd_chain(&root)
.expect("read submitted GDD")
.pop()
.expect("GDD exists");
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
let decision_input = approval_input(
&gdd,
"approve",
"gdd-response-00000000-0000-4000-8000-000000000022",
None,
);
let first = decide_plan_gdd_at(&root, &decision_input).expect("commit approval receipt");
assert_eq!(first.outcome, "committed");
assert!(!first.recovery_pending);
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
&root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
&context.created_by_run_id,
));
assert!(!game_creator_agent_runtime_provider_action_batch_exists(
&root,
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
&context.created_by_run_id,
));
let folded = read_plan_session_with_recovery(&root)
.expect("read folded receipt session")
.expect("receipt session exists");
assert_eq!(folded.phase, "approved");
assert_eq!(folded.accumulated_agent_millis, 23);
let folded_revision = folded.session_revision;
let replay = decide_plan_gdd_at(&root, &decision_input).expect("replay approval decision");
assert_eq!(replay.outcome, "replayed");
assert!(!replay.recovery_pending);
assert!(!reconcile_plan_gdd_approval_projections_at(&root)
.expect("replay receipt recovery projections"));
let replayed = read_plan_session_with_recovery(&root)
.expect("read replayed receipt session")
.expect("replayed receipt session exists");
assert_eq!(replayed.session_revision, folded_revision);
assert_eq!(replayed.accumulated_agent_millis, 23);
cleanup_fixture(root);
}
#[test]
fn projection_failure_after_gdd_create_returns_recovery_pending_and_replays() {
let (root, context, input) = submit_fixture();
@@ -1180,20 +1180,91 @@ where
return Err(error);
}
}
let plan_usage_scope = match capture_plan_provider_usage_scope_at_locked(
root,
&snapshot,
&request_id,
) {
Ok(scope) => scope,
Err(error) => {
let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked(
root,
&snapshot,
&request_id,
);
unregister_game_creator_agent_runtime_provider_request(&key, &active);
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: requestId={request_id} · planningUsageScope={error}"
));
}
};
drop(control_lock);
let notified = active.notify.notified();
tokio::pin!(notified);
tokio::pin!(request);
let result = if active.interrupted.load(Ordering::Acquire) {
Ok(None)
let (result, active_millis) = if active.interrupted.load(Ordering::Acquire) {
(Ok(None), 0)
} else {
tokio::select! {
let active_started = tokio::time::Instant::now();
let result = tokio::select! {
biased;
_ = &mut notified => Ok(None),
result = &mut request => result.map(Some),
}
};
let elapsed = active_started.elapsed().as_millis();
let active_millis = match u64::try_from(elapsed) {
Ok(value) => value,
Err(_) => {
unregister_game_creator_agent_runtime_provider_request(&key, &active);
if let Ok(_control_lock) =
acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.provider_request.planning_usage_overflow_reconciliation",
)
{
let _ =
mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked(
root,
&snapshot,
&request_id,
);
}
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: requestId={request_id} · planningUsage=activeMillis overflow"
));
}
};
(result, active_millis)
};
let result = result.map_err(|error| redact_agent_runtime_error(root, &error, 500));
let usage_outcome = match &result {
Ok(Some(_)) => "completed",
Ok(None) => "interrupted",
Err(_) => "failed",
};
if let Err(error) = persist_plan_provider_usage_fact_at(
root,
&snapshot,
&request_id,
plan_usage_scope.as_ref(),
usage_outcome,
active_millis,
) {
unregister_game_creator_agent_runtime_provider_request(&key, &active);
if let Ok(_control_lock) = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.provider_request.planning_usage_reconciliation",
) {
let _ = mark_game_creator_agent_runtime_provider_request_needs_reconciliation_at_locked(
root,
&snapshot,
&request_id,
);
}
return Err(format!(
"{AGENT_RUNTIME_PROVIDER_REQUEST_RECONCILIATION_PREFIX}: requestId={request_id} · planningUsage={error}"
));
}
if let Ok(Some(response)) = result.as_ref() {
if let Err(error) = success_commit(&request_id, response) {
unregister_game_creator_agent_runtime_provider_request(&key, &active);
@@ -132,7 +132,7 @@ pub(in crate::agent) fn validate_agent_runtime_run_profile_binding_record(
/// source string alone. The durable binding must describe a top-level
/// `project-supervisor-plan` standard run whose root fields point back to itself
/// and which has no parent link.
pub(in crate::agent) fn validate_project_supervisor_plan_root_binding_at(
pub(crate) fn validate_project_supervisor_plan_root_binding_at(
root: &Path,
agent_id: &str,
run_id: &str,
@@ -35,7 +35,9 @@ pub(in crate::agent) use run_status::*;
pub(in crate::agent) use task_ops::*;
#[cfg(test)]
pub(crate) use delivery::build_static_delegate_result_for_child_at;
pub(crate) use delivery::{
build_static_delegate_result_for_child_at, wake_waiting_static_delegate_parent_run_for_test_at,
};
#[cfg(test)]
pub(crate) use media::validate_agent_runtime_canvas_replacement_authorization_at;
@@ -414,6 +414,49 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
action_id: Option<&str>,
input: &serde_json::Value,
) -> AgentRuntimeToolObservation {
let project_write_lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"runtime.snapshot.agent.delegate.direct",
) {
Ok(lock) => lock,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: "无法取得一致项目快照".to_string(),
detail: Some(redact_agent_runtime_project_paths(root, &error, 500)),
};
}
};
observe_agent_runtime_agent_delegate_at_locked(
root,
agent_id,
parent_run_id,
action_id,
input,
&project_write_lock,
)
}
pub(crate) fn observe_agent_runtime_agent_delegate_at_locked(
root: &Path,
agent_id: &str,
parent_run_id: &str,
action_id: Option<&str>,
input: &serde_json::Value,
project_write_lock: &ProjectWriteLock,
) -> AgentRuntimeToolObservation {
if !project_write_lock
.guards_project_root(root)
.unwrap_or(false)
{
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: "agent.delegate 缺少当前项目写锁".to_string(),
detail: None,
};
}
let target_agent_id = agent_runtime_tool_input_text(input, &["agentId", "targetAgentId"]);
let target_agent_id = match normalize_game_creator_runtime_agent_id(target_agent_id.as_str()) {
Ok(target_agent_id) => target_agent_id,
@@ -1106,7 +1149,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
if parent_session_id.is_some() {
drop(dispatch_lock.take());
}
match start_game_creator_agent_background_task_with_link_at(
match start_game_creator_agent_background_task_with_link_locked_at(
root,
&target_agent_id,
requested_session_id,
@@ -1115,6 +1158,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
"agent-delegate",
None,
Some(&task_link),
project_write_lock,
) {
Ok((runtime, delegated_run_id)) => {
if let Some((_, expected_run_id)) = target_session_id.as_ref() {
@@ -294,6 +294,17 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
)?;
return Ok(true);
}
// Parent wake follows project -> execution ordering. Do not hold the
// Supervisor execution lane while the planning/session projection reads
// or writes the project lock.
let project_lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
root,
"planning.parent-wake",
) {
Ok(lock) => lock,
Err(error) if static_delegate_parent_wake_error_is_transient(&error) => return Ok(false),
Err(error) => return Err(error),
};
let Some(runtime_lock) =
try_acquire_game_creator_agent_runtime_task_lock(root, &parent_task.agent_id)?
else {
@@ -342,7 +353,18 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
&current_task.run_id,
)?;
let mut state = state;
ensure_static_delegate_user_input_wait_at(root, &mut state, &deliveries)?;
if !ensure_static_delegate_user_input_wait_at_locked(
root,
&mut state,
&deliveries,
&project_lock,
)? {
// The barrier and delivery snapshot changed between reads. Keep
// the parent in its durable receipt-wait state and let the bounded
// parent-wake loop retry; claiming success here would strand the
// run without either a pending card or another wake.
return Ok(false);
}
return Ok(true);
}
let state = advance_game_creator_agent_runtime_turn_at(
@@ -352,6 +374,7 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
"专业 Agent 已完成,父 run 正在认领委派回执",
"静态委派回执已就绪,恢复同一父 run。",
)?;
drop(project_lock);
let root = root.to_path_buf();
let agent_id = current_task.agent_id.clone();
let task = current_task.task.clone();
@@ -362,6 +385,14 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
Ok(true)
}
#[cfg(test)]
pub(crate) fn wake_waiting_static_delegate_parent_run_for_test_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
wake_waiting_static_delegate_parent_run_at(root, parent_task)
}
pub(in crate::agent) fn wake_waiting_autonomous_manifest_parent_run_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,

Some files were not shown because too many files have changed in this diff Show More