完成 M1C-2b 策划澄清与预算接线
接入三轮澄清中转、确定性 continuation/session 投影及恢复锁序 记录并幂等折叠 planning Provider 活跃时间与末次提交 usage 补齐审批后用户修订谱系校验和质量返工失败关闭 补充并发、恢复、重放、usage 回归并同步技术方案与决策日志
This commit is contained in:
@@ -424,7 +424,7 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
|||||||
"agent.message" => {
|
"agent.message" => {
|
||||||
observe_agent_runtime_agent_message(root, agent_id, run_id, &action.input)
|
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,
|
root,
|
||||||
agent_id,
|
agent_id,
|
||||||
run_id,
|
run_id,
|
||||||
@@ -432,13 +432,14 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
|||||||
&action_fingerprint,
|
&action_fingerprint,
|
||||||
pending_action,
|
pending_action,
|
||||||
true,
|
true,
|
||||||
|| {
|
|project_write_lock| {
|
||||||
observe_agent_runtime_agent_delegate(
|
observe_agent_runtime_agent_delegate_at_locked(
|
||||||
root,
|
root,
|
||||||
agent_id,
|
agent_id,
|
||||||
run_id,
|
run_id,
|
||||||
action_id,
|
action_id,
|
||||||
&action.input,
|
&action.input,
|
||||||
|
project_write_lock,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -519,6 +520,31 @@ pub(in crate::agent) fn observe_agent_runtime_project_snapshot_with_lock<F>(
|
|||||||
) -> AgentRuntimeToolObservation
|
) -> AgentRuntimeToolObservation
|
||||||
where
|
where
|
||||||
F: FnOnce() -> AgentRuntimeToolObservation,
|
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 tool = action.tool.trim();
|
||||||
let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
let _lock = match acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||||
@@ -546,7 +572,7 @@ where
|
|||||||
) {
|
) {
|
||||||
return observation;
|
return observation;
|
||||||
}
|
}
|
||||||
observe()
|
observe(&_lock)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_lock(
|
pub(in crate::agent) fn validate_agent_runtime_project_snapshot_action_after_lock(
|
||||||
|
|||||||
+33
-1
@@ -4,6 +4,32 @@ pub(in crate::agent) fn persist_game_creator_agent_user_input_wait_at(
|
|||||||
root: &Path,
|
root: &Path,
|
||||||
runtime: &mut AgentRuntimeState,
|
runtime: &mut AgentRuntimeState,
|
||||||
pending: &mut AgentRuntimePendingToolAction,
|
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> {
|
) -> Result<(), String> {
|
||||||
if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
if runtime.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD {
|
||||||
return Err("自主构建 Run 禁止进入 waiting-for-user-input".to_string());
|
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.observation = None;
|
||||||
pending.updated_at = unix_timestamp();
|
pending.updated_at = unix_timestamp();
|
||||||
write_game_creator_agent_runtime_pending_tool_action(root, pending)?;
|
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::Waiting(request) => request,
|
||||||
AgentRuntimeUserInputRecovery::Answered { .. } => {
|
AgentRuntimeUserInputRecovery::Answered { .. } => {
|
||||||
return Err("新建用户输入等待时 sidecar 已进入 answered,需由恢复路径继续".to_string());
|
return Err("新建用户输入等待时 sidecar 已进入 answered,需由恢复路径继续".to_string());
|
||||||
|
|||||||
@@ -25,6 +25,11 @@ pub(in crate::agent) async fn compact_game_creator_agent_runtime_context_at(
|
|||||||
root,
|
root,
|
||||||
"runtime.context_compaction.build",
|
"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(
|
let source = build_game_creator_agent_runtime_context_compaction_source(
|
||||||
root,
|
root,
|
||||||
agent_id,
|
agent_id,
|
||||||
|
|||||||
@@ -909,6 +909,14 @@ pub(in crate::agent) fn static_delegate_barrier_requires_repair(detail: &str) ->
|
|||||||
.is_some_and(|count| count > 0)
|
.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 {
|
pub(in crate::agent) fn static_delegate_barrier_requires_user_revision(detail: &str) -> bool {
|
||||||
detail
|
detail
|
||||||
.split_whitespace()
|
.split_whitespace()
|
||||||
@@ -1757,6 +1765,11 @@ mod static_delegate_barrier_detail_gate_tests {
|
|||||||
barrier.repair_required_count > 0,
|
barrier.repair_required_count > 0,
|
||||||
"repairRequired 往返失真:{barrier:?}\ndetail={detail}"
|
"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!(
|
assert_eq!(
|
||||||
static_delegate_barrier_requires_user_revision(&detail),
|
static_delegate_barrier_requires_user_revision(&detail),
|
||||||
barrier.user_revision_pending_count > 0,
|
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",
|
"runtime.provider_request.capture.final_reply",
|
||||||
)?;
|
)?;
|
||||||
if agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID {
|
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
|
// Freeze the concrete final-reply request and its Provider-facing
|
||||||
// planning injection under the same project lock as the durable
|
// planning injection under the same project lock as the durable
|
||||||
// session binding. This mirrors tool-plan and prevents an older
|
// 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,
|
root,
|
||||||
"runtime.provider_request.capture.tool_plan",
|
"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
|
// Exact planning requests must freeze the session and the concrete
|
||||||
// request object under one project lock. Rebuild once while holding
|
// request object under one project lock. Rebuild once while holding
|
||||||
// that lock so a session successor cannot be used to re-label an
|
// that lock so a session successor cannot be used to re-label an
|
||||||
|
|||||||
@@ -506,6 +506,8 @@ pub(crate) use entrypoints::{
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
pub(crate) use finalization::resume_game_creator_agent_finalization_for_test_at;
|
pub(crate) use finalization::resume_game_creator_agent_finalization_for_test_at;
|
||||||
pub(crate) use finalization::AgentRuntimePendingActionResume;
|
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::{
|
pub(crate) use interaction::{
|
||||||
agent_runtime_tool_requires_repository_context_fingerprint_gate,
|
agent_runtime_tool_requires_repository_context_fingerprint_gate,
|
||||||
answer_game_creator_agent_runtime_user_input_at, confirm_game_creator_agent_runtime_task_at,
|
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))
|
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(
|
pub(crate) fn answer_game_creator_agent_runtime_user_input_at(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
agent_id: &str,
|
agent_id: &str,
|
||||||
@@ -452,17 +525,29 @@ pub(crate) fn answer_game_creator_agent_runtime_user_input_at(
|
|||||||
) -> Result<AgentRuntimeResult, String> {
|
) -> Result<AgentRuntimeResult, String> {
|
||||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||||
validate_project_root(root)?;
|
validate_project_root(root)?;
|
||||||
let runtime_lock = acquire_game_creator_agent_runtime_task_lock_with_wait(root, &agent_id)?;
|
let (project_lock, runtime_lock, resolved) =
|
||||||
let (agent_id, task, mut runtime, mut pending) =
|
resolve_game_creator_agent_runtime_user_input_action_with_ordered_locks(
|
||||||
resolve_game_creator_agent_runtime_user_input_action(root, &agent_id, run_id, action_id)?;
|
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 already_observed = pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_OBSERVED_APPROVED;
|
||||||
let (request, observation) = answer_game_creator_agent_user_input_request_for_pending_at(
|
let (request, observation) = match project_lock.as_ref() {
|
||||||
|
Some(project_lock) => answer_game_creator_agent_user_input_request_for_pending_at_locked(
|
||||||
root,
|
root,
|
||||||
&pending,
|
&pending,
|
||||||
request_id,
|
request_id,
|
||||||
response_id,
|
response_id,
|
||||||
answers,
|
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 already_observed {
|
||||||
if pending.observation.as_ref() != Some(&observation) {
|
if pending.observation.as_ref() != Some(&observation) {
|
||||||
return Err("用户输入回答与已持久化 observation 冲突".to_string());
|
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)?;
|
let result = read_game_creator_agent_runtime_at(root, &agent_id)?;
|
||||||
|
drop(project_lock);
|
||||||
if external_agent_runner_owns_background_execution() {
|
if external_agent_runner_owns_background_execution() {
|
||||||
let answered_run_id = pending.run_id.clone();
|
let answered_run_id = pending.run_id.clone();
|
||||||
let answered_action_id = pending.action_id.clone();
|
let answered_action_id = pending.action_id.clone();
|
||||||
|
|||||||
@@ -925,7 +925,28 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
|
|||||||
&task,
|
&task,
|
||||||
retry_link.is_some(),
|
retry_link.is_some(),
|
||||||
)?;
|
)?;
|
||||||
let (mut result, actual_retry_run_id) = with_agent_conversation_session_lane_at(
|
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,
|
root,
|
||||||
&agent_id,
|
&agent_id,
|
||||||
"Agent Runtime 重试入队",
|
"Agent Runtime 重试入队",
|
||||||
@@ -941,7 +962,8 @@ pub(crate) fn retry_game_creator_agent_runtime_task_at(
|
|||||||
retry_link.as_ref(),
|
retry_link.as_ref(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)?;
|
)?
|
||||||
|
};
|
||||||
let retry_task_sha256 = format!("{:x}", Sha256::digest(task.task.as_bytes()));
|
let retry_task_sha256 = format!("{:x}", Sha256::digest(task.task.as_bytes()));
|
||||||
let retry_task_chars = task.task.chars().count();
|
let retry_task_chars = task.task.chars().count();
|
||||||
let retry_goal_bound = task.goal_id.is_some();
|
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());
|
result.accepted_run_id = Some(actual_retry_run_id.clone());
|
||||||
|
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(
|
notify_external_agent_runner_after_background_task_enqueue(
|
||||||
root,
|
root,
|
||||||
&agent_id,
|
&agent_id,
|
||||||
&task.session_id,
|
&task.session_id,
|
||||||
&actual_retry_run_id,
|
&actual_retry_run_id,
|
||||||
)?;
|
)?;
|
||||||
|
}
|
||||||
Ok(result)
|
Ok(result)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1063,10 +1063,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
|||||||
if let Some(blocker) =
|
if let Some(blocker) =
|
||||||
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
|
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
|
||||||
{
|
{
|
||||||
let waits_for_delivery = blocker
|
let waits_for_delivery = blocker.detail.as_deref().is_some_and(|detail| {
|
||||||
.detail
|
static_delegate_barrier_has_waiting_deliveries(detail)
|
||||||
.as_deref()
|
|| static_delegate_barrier_requires_user_input(detail)
|
||||||
.is_some_and(static_delegate_barrier_has_waiting_deliveries);
|
});
|
||||||
if waits_for_delivery {
|
if waits_for_delivery {
|
||||||
if let Err(error) = persist_waiting_static_delegate_parent_context_at(
|
if let Err(error) = persist_waiting_static_delegate_parent_context_at(
|
||||||
&root,
|
&root,
|
||||||
@@ -2079,13 +2079,10 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
|||||||
.detail
|
.detail
|
||||||
.as_deref()
|
.as_deref()
|
||||||
.is_some_and(static_delegate_barrier_requires_user_revision);
|
.is_some_and(static_delegate_barrier_requires_user_revision);
|
||||||
let user_input_required = blocker.detail.as_deref().is_some_and(|detail| {
|
let user_input_required = blocker
|
||||||
detail
|
.detail
|
||||||
.split_whitespace()
|
.as_deref()
|
||||||
.find_map(|part| part.strip_prefix("userInputRequired="))
|
.is_some_and(static_delegate_barrier_requires_user_input);
|
||||||
.and_then(|value| value.parse::<usize>().ok())
|
|
||||||
.is_some_and(|count| count > 0)
|
|
||||||
});
|
|
||||||
runtime.status = "running".to_string();
|
runtime.status = "running".to_string();
|
||||||
if user_revision_pending {
|
if user_revision_pending {
|
||||||
runtime.phase = "planning".to_string();
|
runtime.phase = "planning".to_string();
|
||||||
@@ -2096,36 +2093,21 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
|
|||||||
runtime.next_step =
|
runtime.next_step =
|
||||||
"调用 agent.delegate,并把 repairOfDelegationId 指向原 delivery;不得把用户修订计入 repair_depth".to_string();
|
"调用 agent.delegate,并把 repairOfDelegationId 指向原 delivery;不得把用户修订计入 repair_depth".to_string();
|
||||||
} else if user_input_required {
|
} else if user_input_required {
|
||||||
let deliveries = match claimed_static_delegate_deliveries_at(
|
// The background drain owns the Supervisor execution lane. Persist a
|
||||||
&root,
|
// receipt wait first, then let task_queue schedule parent-wake after this
|
||||||
&runtime.agent_id,
|
// pass returns and the lane is released. Parent-wake acquires
|
||||||
&runtime.run_id,
|
// project -> execution and creates the unique clarification pending under
|
||||||
) {
|
// both locks; doing that here would invert the M1C-2b planning projection
|
||||||
Ok(deliveries) => deliveries,
|
// order.
|
||||||
Err(error) => {
|
runtime.phase = "waiting-for-delegate-receipts".to_string();
|
||||||
return fail_game_creator_agent_background_context_at(
|
runtime.current_action =
|
||||||
&root,
|
"等待创建 Project Supervisor 用户澄清请求".to_string();
|
||||||
&agent_id,
|
runtime.waiting_on =
|
||||||
&session_id,
|
"释放当前 execution lane 后投影 planning session 与澄清 pending"
|
||||||
runtime,
|
.to_string();
|
||||||
&format!("读取 needs-user-input 回执失败:{error}"),
|
runtime.next_step =
|
||||||
);
|
"由 lane 外 parent-wake 按 project → execution 锁序创建唯一澄清请求"
|
||||||
}
|
.to_string();
|
||||||
};
|
|
||||||
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;
|
|
||||||
} else if repair_required {
|
} else if repair_required {
|
||||||
runtime.phase = "planning".to_string();
|
runtime.phase = "planning".to_string();
|
||||||
runtime.current_action = "等待 Project Supervisor 发起唯一返工".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,
|
AgentBackgroundTaskOutcome::WaitingForIsolatedJoin,
|
||||||
))
|
))
|
||||||
} else if observation.tool == "runtime.delegate_receipts"
|
} 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((
|
Some((
|
||||||
"agent.runtime.agent.delegate_receipts.waiting",
|
"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() {
|
match pending.action.tool.as_str() {
|
||||||
"agent.delegate" => observe_agent_runtime_agent_delegate(
|
"agent.delegate" => observe_agent_runtime_agent_delegate_at_locked(
|
||||||
root,
|
root,
|
||||||
&pending.agent_id,
|
&pending.agent_id,
|
||||||
&pending.run_id,
|
&pending.run_id,
|
||||||
Some(&pending.action_id),
|
Some(&pending.action_id),
|
||||||
&pending.action.input,
|
&pending.action.input,
|
||||||
|
&_project_lock,
|
||||||
),
|
),
|
||||||
"agent.run_status" => observe_agent_runtime_run_status(
|
"agent.run_status" => observe_agent_runtime_run_status(
|
||||||
root,
|
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)? {
|
if migrate_legacy_autonomous_confirmation_at(root, &runtime, &mut pending)? {
|
||||||
can_repair_terminal_receipt = true;
|
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 {
|
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT {
|
||||||
let planning_agent = runtime.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID;
|
let planning_agent = runtime.agent_id == GAME_CREATOR_PROJECT_PLANNING_AGENT_ID;
|
||||||
if planning_agent
|
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) {
|
if game_creator_agent_runtime_cancel_requested(root, &runtime) {
|
||||||
cancel_game_creator_agent_user_input_request_for_pending_at(root, &pending)?;
|
cancel_game_creator_agent_user_input_request_for_pending_at(root, &pending)?;
|
||||||
mark_game_creator_agent_runtime_cancelled_at(
|
match planning_user_input_project_lock.as_ref() {
|
||||||
|
Some(_) => mark_game_creator_agent_runtime_cancelled_at_locked(
|
||||||
root,
|
root,
|
||||||
&mut runtime,
|
&mut runtime,
|
||||||
"Agent 后台任务已按开发者请求取消",
|
"Agent 后台任务已按开发者请求取消",
|
||||||
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
|
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
|
||||||
)?;
|
)?,
|
||||||
|
None => mark_game_creator_agent_runtime_cancelled_at(
|
||||||
|
root,
|
||||||
|
&mut runtime,
|
||||||
|
"Agent 后台任务已按开发者请求取消",
|
||||||
|
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
|
||||||
|
)?,
|
||||||
|
}
|
||||||
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
|
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)) => {
|
Ok(AgentRuntimeUserInputRecovery::Waiting(request)) => {
|
||||||
runtime.pending_tool_action = Some(pending.summary());
|
runtime.pending_tool_action = Some(pending.summary());
|
||||||
runtime.status = "waiting-for-user-input".to_string();
|
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;
|
can_repair_terminal_receipt = true;
|
||||||
}
|
}
|
||||||
Ok(AgentRuntimeUserInputRecovery::Cancelled) => {
|
Ok(AgentRuntimeUserInputRecovery::Cancelled) => {
|
||||||
mark_game_creator_agent_runtime_cancelled_at(
|
match planning_user_input_project_lock.as_ref() {
|
||||||
|
Some(_) => mark_game_creator_agent_runtime_cancelled_at_locked(
|
||||||
root,
|
root,
|
||||||
&mut runtime,
|
&mut runtime,
|
||||||
"Agent 用户输入请求已取消",
|
"Agent 用户输入请求已取消",
|
||||||
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
|
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
|
||||||
)?;
|
)?,
|
||||||
|
None => mark_game_creator_agent_runtime_cancelled_at(
|
||||||
|
root,
|
||||||
|
&mut runtime,
|
||||||
|
"Agent 用户输入请求已取消",
|
||||||
|
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
|
||||||
|
)?,
|
||||||
|
}
|
||||||
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
|
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
|
||||||
}
|
}
|
||||||
Err(error) => {
|
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
|
if pending.status == AGENT_RUNTIME_PENDING_ACTION_STATUS_EXECUTING
|
||||||
&& pending.action.tool == "canvas.asset_generate"
|
&& 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
|
/// 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
|
/// 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.
|
/// the parent run and therefore passes the normal user-input owner gate.
|
||||||
|
#[cfg(test)]
|
||||||
pub(crate) fn ensure_static_delegate_user_input_wait_at(
|
pub(crate) fn ensure_static_delegate_user_input_wait_at(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
runtime: &mut AgentRuntimeState,
|
runtime: &mut AgentRuntimeState,
|
||||||
deliveries: &[StaticDelegateDeliveryRecord],
|
deliveries: &[StaticDelegateDeliveryRecord],
|
||||||
) -> Result<bool, String> {
|
) -> 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| {
|
let mut pending_deliveries = deliveries.iter().filter(|delivery| {
|
||||||
delivery.structured_result.as_ref().is_some_and(|result| {
|
delivery.structured_result.as_ref().is_some_and(|result| {
|
||||||
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|
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 {
|
let Some(delivery) = pending_deliveries.next() else {
|
||||||
return Ok(false);
|
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
|
// Each durable request belongs to exactly one original delivery. Other
|
||||||
// deliveries remain behind the completion barrier and are asked next.
|
// deliveries remain behind the completion barrier and are asked next.
|
||||||
let result = delivery
|
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,
|
AGENT_RUNTIME_PENDING_ACTION_STATUS_WAITING_FOR_USER_INPUT,
|
||||||
None,
|
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)
|
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)
|
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 {
|
struct MissingPlanSubmitAnchorCandidate {
|
||||||
runtime: AgentRuntimeState,
|
runtime: AgentRuntimeState,
|
||||||
action_id: String,
|
action_id: String,
|
||||||
@@ -989,6 +1038,77 @@ pub(in crate::agent) fn resume_game_creator_agent_background_tasks_unredacted_at
|
|||||||
else {
|
else {
|
||||||
continue;
|
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)? {
|
if let Some(result) = reconcile_missing_plan_submit_anchors_at(root, &agent_id)? {
|
||||||
resumed.push(result);
|
resumed.push(result);
|
||||||
drop(runtime_lock);
|
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,
|
source: &str,
|
||||||
run_profile: Option<&str>,
|
run_profile: Option<&str>,
|
||||||
task_link: Option<&AgentRuntimeTaskLink>,
|
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> {
|
) -> Result<(AgentRuntimeResult, String), String> {
|
||||||
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
let agent_id = normalize_game_creator_runtime_agent_id(agent_id)?;
|
||||||
validate_project_root(root)?;
|
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_id,
|
||||||
"Agent Session Runtime 入队",
|
"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,
|
root,
|
||||||
&agent_id,
|
&agent_id,
|
||||||
session_id,
|
session_id,
|
||||||
@@ -190,6 +263,7 @@ pub(in crate::agent) fn start_game_creator_agent_background_task_with_link_at(
|
|||||||
source,
|
source,
|
||||||
run_profile,
|
run_profile,
|
||||||
task_link,
|
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>,
|
run_profile: Option<&str>,
|
||||||
task_link: Option<&AgentRuntimeTaskLink>,
|
task_link: Option<&AgentRuntimeTaskLink>,
|
||||||
) -> Result<(AgentRuntimeResult, String), String> {
|
) -> 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
|
let isolated_instance = agent_id
|
||||||
.starts_with("child-")
|
.starts_with("child-")
|
||||||
.then(|| resolve_isolated_agent_instance_at(root, &agent_id))
|
.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}"));
|
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 requires_public_start_status {
|
||||||
if let Err(error) =
|
if let Err(error) =
|
||||||
ensure_game_creator_agent_runtime_accepted_public_status_at(root, &pending_task)
|
ensure_game_creator_agent_runtime_accepted_public_status_at(root, &pending_task)
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ mod finalization;
|
|||||||
mod json_sidecar;
|
mod json_sidecar;
|
||||||
mod models;
|
mod models;
|
||||||
mod planning_approval;
|
mod planning_approval;
|
||||||
|
mod planning_coordinator;
|
||||||
|
mod planning_provider_usage;
|
||||||
mod planning_storage;
|
mod planning_storage;
|
||||||
mod planning_submit;
|
mod planning_submit;
|
||||||
mod provider_control;
|
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 json_sidecar::*;
|
||||||
pub(in crate::agent) use models::*;
|
pub(in crate::agent) use models::*;
|
||||||
pub(crate) use planning_approval::*;
|
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_storage::*;
|
||||||
pub(crate) use planning_submit::*;
|
pub(crate) use planning_submit::*;
|
||||||
pub(in crate::agent) use provider_control::*;
|
pub(in crate::agent) use provider_control::*;
|
||||||
pub(in crate::agent) use provider_retry::*;
|
pub(in crate::agent) use provider_retry::*;
|
||||||
pub(in crate::agent) use real_e2e_checkpoint::*;
|
pub(in crate::agent) use real_e2e_checkpoint::*;
|
||||||
pub(in crate::agent) use response_stream::*;
|
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 run_configuration::*;
|
||||||
pub(in crate::agent) use steering::*;
|
pub(in crate::agent) use steering::*;
|
||||||
pub(in crate::agent) use verification::*;
|
pub(in crate::agent) use verification::*;
|
||||||
|
|||||||
+26
-2
@@ -988,7 +988,7 @@ fn project_receipt_locked(
|
|||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match project_generic_submit_observation_locked(root, receipt) {
|
let generic_submit_consumed = match project_generic_submit_observation_locked(root, receipt) {
|
||||||
Ok(consumed) => {
|
Ok(consumed) => {
|
||||||
if consumed {
|
if consumed {
|
||||||
if approval_pending_cleanup_eligible
|
if approval_pending_cleanup_eligible
|
||||||
@@ -999,12 +999,14 @@ fn project_receipt_locked(
|
|||||||
} else {
|
} else {
|
||||||
recovery_pending = true;
|
recovery_pending = true;
|
||||||
}
|
}
|
||||||
|
consumed
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let _ = error;
|
let _ = error;
|
||||||
recovery_pending = true;
|
recovery_pending = true;
|
||||||
|
false
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
if receipt.action != "approve" {
|
if receipt.action != "approve" {
|
||||||
if mark_static_delegate_delivery_user_revision_requested_at(
|
if mark_static_delegate_delivery_user_revision_requested_at(
|
||||||
root,
|
root,
|
||||||
@@ -1030,10 +1032,32 @@ fn project_receipt_locked(
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
let mut session_projection_ready = false;
|
||||||
if receipt.version == latest.version || session_points_to_receipt {
|
if receipt.version == latest.version || session_points_to_receipt {
|
||||||
if let Err(error) = project_plan_session_locked(root, receipt_gdd, receipt) {
|
if let Err(error) = project_plan_session_locked(root, receipt_gdd, receipt) {
|
||||||
recovery_pending = true;
|
recovery_pending = true;
|
||||||
let _ = error;
|
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)
|
Ok(recovery_pending)
|
||||||
|
|||||||
+789
File diff suppressed because it is too large
Load Diff
+886
File diff suppressed because it is too large
Load Diff
+281
-25
@@ -2100,13 +2100,21 @@ fn validate_plan_session_shape(value: &PlanSessionV1) -> Result<(), PlanningStor
|
|||||||
"awaiting_user_input 必须有 latestDelegationId 以定位问题 delivery",
|
"awaiting_user_input 必须有 latestDelegationId 以定位问题 delivery",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
if let Some(last_answer) = value.applied_answers.last() {
|
if value
|
||||||
if value.latest_delegation_id != last_answer.continuation_delegation_id {
|
.applied_answers
|
||||||
|
.iter()
|
||||||
|
.any(|answer| value.latest_delegation_id == answer.delegation_id)
|
||||||
|
{
|
||||||
|
// Once an answer has been applied, the session may point at its
|
||||||
|
// deterministic continuation or at a later user-revision descendant,
|
||||||
|
// but it must never rewind to an already answered question delivery.
|
||||||
|
// Exact descendant proof depends on the delivery sidecars and is
|
||||||
|
// enforced by `validate_plan_session_latest_delegation_lineage_at` at
|
||||||
|
// every runtime read/write boundary rather than guessed from phase.
|
||||||
return Err(conflict(
|
return Err(conflict(
|
||||||
"latestDelegationId 必须等于最后一个 appliedAnswers 的 continuationDelegationId",
|
"latestDelegationId 不能回退到已消费回答的 delegationId",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
if matches!(
|
if matches!(
|
||||||
value.phase.as_str(),
|
value.phase.as_str(),
|
||||||
"awaiting_gdd_approval"
|
"awaiting_gdd_approval"
|
||||||
@@ -2221,6 +2229,94 @@ pub(crate) fn validate_plan_session(value: &PlanSessionV1) -> Result<(), Plannin
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Prove the dynamic part of `latestDelegationId` that the standalone session
|
||||||
|
/// schema cannot establish. After a clarification answer, a later id is only
|
||||||
|
/// valid when the durable static-delivery chain reaches the last deterministic
|
||||||
|
/// continuation and every crossed edge is classified by a
|
||||||
|
/// `UserRevisionRequested` parent.
|
||||||
|
/// This keeps revise/reject continuations valid without accepting an arbitrary
|
||||||
|
/// recomputed session fingerprint that points at an unrelated delivery.
|
||||||
|
fn validate_plan_session_latest_delegation_lineage_at(
|
||||||
|
root: &Path,
|
||||||
|
value: &PlanSessionV1,
|
||||||
|
) -> Result<(), PlanningStorageError> {
|
||||||
|
let Some(last_answer) = value.applied_answers.last() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
if value.latest_delegation_id == last_answer.continuation_delegation_id {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let deliveries = list_static_delegate_deliveries_at(root).map_err(|error| {
|
||||||
|
PlanningStorageError::new(
|
||||||
|
"PLAN_NEEDS_RECONCILIATION",
|
||||||
|
format!("读取 session latest delegation 谱系失败:{error}"),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if static_delegate_lineage_contains_unknown_contract_status(
|
||||||
|
&deliveries,
|
||||||
|
&value.latest_delegation_id,
|
||||||
|
)
|
||||||
|
.map_err(|error| {
|
||||||
|
PlanningStorageError::new(
|
||||||
|
"PLAN_NEEDS_RECONCILIATION",
|
||||||
|
format!("检查 session latest delegation 谱系失败:{error}"),
|
||||||
|
)
|
||||||
|
})? {
|
||||||
|
return Err(PlanningStorageError::new(
|
||||||
|
"PLAN_NEEDS_RECONCILIATION",
|
||||||
|
"session latest delegation 谱系含未知 contractStatus",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let by_id = deliveries
|
||||||
|
.iter()
|
||||||
|
.map(|delivery| (delivery.delegation_id.as_str(), delivery))
|
||||||
|
.collect::<std::collections::BTreeMap<_, _>>();
|
||||||
|
let mut cursor = value.latest_delegation_id.as_str();
|
||||||
|
let mut visited = std::collections::BTreeSet::new();
|
||||||
|
loop {
|
||||||
|
if !visited.insert(cursor) {
|
||||||
|
return Err(conflict(
|
||||||
|
"session latest delegation 谱系形成循环,不能证明回答后继关系",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let delivery = by_id
|
||||||
|
.get(cursor)
|
||||||
|
.copied()
|
||||||
|
.ok_or_else(|| conflict(format!("session latest delegation 谱系缺少节点:{cursor}")))?;
|
||||||
|
if delivery.parent_agent_id != value.root_agent_id
|
||||||
|
|| delivery.parent_run_id != value.root_run_id
|
||||||
|
|| delivery.target_agent_id != value.agent_id
|
||||||
|
|| delivery.target_session_id != value.session_id
|
||||||
|
{
|
||||||
|
return Err(conflict(
|
||||||
|
"session latest delegation 谱系跨越了 root/agent/session identity",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if delivery.delegation_id == last_answer.continuation_delegation_id {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
let parent_id = delivery.repair_of_delegation_id.as_deref().ok_or_else(|| {
|
||||||
|
conflict("session latest delegation 不是最后一次回答 continuation 的后继")
|
||||||
|
})?;
|
||||||
|
let parent = by_id.get(parent_id).copied().ok_or_else(|| {
|
||||||
|
conflict(format!(
|
||||||
|
"session latest delegation 谱系缺少节点:{parent_id}"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
if !parent.structured_result.as_ref().is_some_and(|result| {
|
||||||
|
result.contract_status == StaticDelegateContractStatus::UserRevisionRequested
|
||||||
|
}) {
|
||||||
|
return Err(conflict(
|
||||||
|
"session latest delegation 含非 UserRevisionRequested 父边,不能保留 appliedAnswers",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
cursor = parent_id;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Validate the session projection against the clarification round derived
|
/// Validate the session projection against the clarification round derived
|
||||||
/// from the static-delegate lineage. The lineage counter intentionally stays
|
/// from the static-delegate lineage. The lineage counter intentionally stays
|
||||||
/// outside this storage module; callers must supply the independently read
|
/// outside this storage module; callers must supply the independently read
|
||||||
@@ -2255,7 +2351,6 @@ pub(crate) fn validate_plan_session_successor(
|
|||||||
|| next.agent_id != previous.agent_id
|
|| next.agent_id != previous.agent_id
|
||||||
|| next.source != previous.source
|
|| next.source != previous.source
|
||||||
|| next.run_profile != previous.run_profile
|
|| next.run_profile != previous.run_profile
|
||||||
|| next.run_profile_binding_fingerprint != previous.run_profile_binding_fingerprint
|
|
||||||
|| next.root_agent_id != previous.root_agent_id
|
|| next.root_agent_id != previous.root_agent_id
|
||||||
|| next.root_run_id != previous.root_run_id
|
|| next.root_run_id != previous.root_run_id
|
||||||
{
|
{
|
||||||
@@ -2281,6 +2376,17 @@ pub(crate) fn validate_plan_session_successor(
|
|||||||
"session successor 的累计运行时间和 steer cursor 只能单调增加",
|
"session successor 的累计运行时间和 steer cursor 只能单调增加",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
let active_run_changed = next.active_run_id != previous.active_run_id
|
||||||
|
&& next.active_run_id.is_some()
|
||||||
|
&& next.last_run_id == next.active_run_id.clone().unwrap_or_default();
|
||||||
|
if next.run_profile_binding_fingerprint != previous.run_profile_binding_fingerprint
|
||||||
|
&& (!active_run_changed
|
||||||
|
|| !matches!(next.phase.as_str(), "collecting" | "revision_requested"))
|
||||||
|
{
|
||||||
|
return Err(conflict(
|
||||||
|
"session 只有在绑定新的 active planning child 时才能更换 Run Profile binding fingerprint",
|
||||||
|
));
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2305,11 +2411,17 @@ pub(crate) fn derive_plan_continuation_delegation_id(
|
|||||||
"continuation questionsSha256/answersSha256 必须是裸 64 位小写 digest",
|
"continuation questionsSha256/answersSha256 必须是裸 64 位小写 digest",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Ok(format!(
|
let continuation_action_identity = format!(
|
||||||
"clarification-continuation-{:x}",
|
"clarification-continuation-{:x}",
|
||||||
Sha256::digest(format!(
|
Sha256::digest(format!(
|
||||||
"{parent_run_id}\n{repair_of_delegation_id}\n{questions_sha256}\n{answers_sha256}"
|
"{parent_run_id}\n{repair_of_delegation_id}\n{questions_sha256}\n{answers_sha256}"
|
||||||
))
|
))
|
||||||
|
);
|
||||||
|
Ok(agent_runtime_delegation_id(
|
||||||
|
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
|
||||||
|
parent_run_id,
|
||||||
|
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||||
|
&continuation_action_identity,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4378,6 +4490,7 @@ pub(crate) fn write_plan_session_atomic_locked(
|
|||||||
value: &PlanSessionV1,
|
value: &PlanSessionV1,
|
||||||
) -> Result<(), PlanningStorageError> {
|
) -> Result<(), PlanningStorageError> {
|
||||||
let bytes = canonical_plan_session_bytes(value)?;
|
let bytes = canonical_plan_session_bytes(value)?;
|
||||||
|
validate_plan_session_latest_delegation_lineage_at(root, value)?;
|
||||||
let target = resolve_planning_path(root, PLAN_SESSION_PATH)?;
|
let target = resolve_planning_path(root, PLAN_SESSION_PATH)?;
|
||||||
let previous = resolve_planning_path(root, PLAN_SESSION_PREVIOUS_PATH)?;
|
let previous = resolve_planning_path(root, PLAN_SESSION_PREVIOUS_PATH)?;
|
||||||
let parent = ensure_planning_parent(&target)?;
|
let parent = ensure_planning_parent(&target)?;
|
||||||
@@ -4386,6 +4499,7 @@ pub(crate) fn write_plan_session_atomic_locked(
|
|||||||
verify_regular_planning_file(&target, "现有 plan session")?;
|
verify_regular_planning_file(&target, "现有 plan session")?;
|
||||||
let old_bytes = read_regular_planning_file(&target, "现有 plan session")?;
|
let old_bytes = read_regular_planning_file(&target, "现有 plan session")?;
|
||||||
let old = parse_plan_session_bytes(&old_bytes)?;
|
let old = parse_plan_session_bytes(&old_bytes)?;
|
||||||
|
validate_plan_session_latest_delegation_lineage_at(root, &old)?;
|
||||||
Some((old, old_bytes))
|
Some((old, old_bytes))
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||||
@@ -4395,7 +4509,9 @@ pub(crate) fn write_plan_session_atomic_locked(
|
|||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
verify_regular_planning_file(&previous, "现有 plan session previous")?;
|
verify_regular_planning_file(&previous, "现有 plan session previous")?;
|
||||||
let old_bytes = read_regular_planning_file(&previous, "现有 plan session previous")?;
|
let old_bytes = read_regular_planning_file(&previous, "现有 plan session previous")?;
|
||||||
Some(parse_plan_session_bytes(&old_bytes)?)
|
let old = parse_plan_session_bytes(&old_bytes)?;
|
||||||
|
validate_plan_session_latest_delegation_lineage_at(root, &old)?;
|
||||||
|
Some(old)
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
|
||||||
Err(error) => return Err(io_error("读取现有 plan session previous 失败", error)),
|
Err(error) => return Err(io_error("读取现有 plan session previous 失败", error)),
|
||||||
@@ -4450,7 +4566,8 @@ pub(crate) fn write_plan_session_atomic_locked(
|
|||||||
"plan session 发布后 canonical bytes 不一致",
|
"plan session 发布后 canonical bytes 不一致",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
parse_plan_session_bytes(&published)?;
|
let published = parse_plan_session_bytes(&published)?;
|
||||||
|
validate_plan_session_latest_delegation_lineage_at(root, &published)?;
|
||||||
sync_planning_parent(parent)
|
sync_planning_parent(parent)
|
||||||
})();
|
})();
|
||||||
let _ = fs::remove_file(&temporary);
|
let _ = fs::remove_file(&temporary);
|
||||||
@@ -4462,13 +4579,16 @@ pub(crate) fn write_plan_session_atomic_locked(
|
|||||||
/// revision/hash-chain rules in §10.2. A corrupt primary is never silently
|
/// revision/hash-chain rules in §10.2. A corrupt primary is never silently
|
||||||
/// replaced by a valid previous copy.
|
/// replaced by a valid previous copy.
|
||||||
fn read_optional_plan_session_file(
|
fn read_optional_plan_session_file(
|
||||||
|
root: &Path,
|
||||||
path: &Path,
|
path: &Path,
|
||||||
label: &str,
|
label: &str,
|
||||||
) -> Result<Option<PlanSessionV1>, PlanningStorageError> {
|
) -> Result<Option<PlanSessionV1>, PlanningStorageError> {
|
||||||
match fs::symlink_metadata(path) {
|
match fs::symlink_metadata(path) {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
let bytes = read_regular_planning_file(path, label)?;
|
let bytes = read_regular_planning_file(path, label)?;
|
||||||
Ok(Some(parse_plan_session_bytes(&bytes)?))
|
let value = parse_plan_session_bytes(&bytes)?;
|
||||||
|
validate_plan_session_latest_delegation_lineage_at(root, &value)?;
|
||||||
|
Ok(Some(value))
|
||||||
}
|
}
|
||||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(None),
|
||||||
Err(error) => Err(io_error(&format!("读取 {label} 失败"), error)),
|
Err(error) => Err(io_error(&format!("读取 {label} 失败"), error)),
|
||||||
@@ -4513,8 +4633,10 @@ pub(crate) fn read_plan_session_with_recovery_locked(
|
|||||||
) -> Result<Option<PlanSessionV1>, PlanningStorageError> {
|
) -> Result<Option<PlanSessionV1>, PlanningStorageError> {
|
||||||
let primary_path = resolve_planning_path(root, PLAN_SESSION_PATH)?;
|
let primary_path = resolve_planning_path(root, PLAN_SESSION_PATH)?;
|
||||||
let previous_path = resolve_planning_path(root, PLAN_SESSION_PREVIOUS_PATH)?;
|
let previous_path = resolve_planning_path(root, PLAN_SESSION_PREVIOUS_PATH)?;
|
||||||
let primary_state = read_optional_plan_session_file(&primary_path, "plan session primary")?;
|
let primary_state =
|
||||||
let previous_state = read_optional_plan_session_file(&previous_path, "plan session previous")?;
|
read_optional_plan_session_file(root, &primary_path, "plan session primary")?;
|
||||||
|
let previous_state =
|
||||||
|
read_optional_plan_session_file(root, &previous_path, "plan session previous")?;
|
||||||
match (primary_state, previous_state) {
|
match (primary_state, previous_state) {
|
||||||
(None, None) => Ok(None),
|
(None, None) => Ok(None),
|
||||||
(Some(primary), None) => Ok(Some(primary)),
|
(Some(primary), None) => Ok(Some(primary)),
|
||||||
@@ -4525,10 +4647,13 @@ pub(crate) fn read_plan_session_with_recovery_locked(
|
|||||||
// published a new primary between the optimistic read and lock
|
// published a new primary between the optimistic read and lock
|
||||||
// acquisition; never overwrite that newer fact.
|
// acquisition; never overwrite that newer fact.
|
||||||
if let Some(current_primary) =
|
if let Some(current_primary) =
|
||||||
read_optional_plan_session_file(&primary_path, "锁内 plan session primary")?
|
read_optional_plan_session_file(root, &primary_path, "锁内 plan session primary")?
|
||||||
{
|
{
|
||||||
let current_previous =
|
let current_previous = read_optional_plan_session_file(
|
||||||
read_optional_plan_session_file(&previous_path, "锁内 plan session previous")?;
|
root,
|
||||||
|
&previous_path,
|
||||||
|
"锁内 plan session previous",
|
||||||
|
)?;
|
||||||
return match current_previous {
|
return match current_previous {
|
||||||
Some(current_previous) => {
|
Some(current_previous) => {
|
||||||
if current_primary != current_previous {
|
if current_primary != current_previous {
|
||||||
@@ -4539,8 +4664,11 @@ pub(crate) fn read_plan_session_with_recovery_locked(
|
|||||||
None => Ok(Some(current_primary)),
|
None => Ok(Some(current_primary)),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
let current_previous =
|
let current_previous = read_optional_plan_session_file(
|
||||||
read_optional_plan_session_file(&previous_path, "锁内 plan session previous")?
|
root,
|
||||||
|
&previous_path,
|
||||||
|
"锁内 plan session previous",
|
||||||
|
)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
PlanningStorageError::new(
|
PlanningStorageError::new(
|
||||||
"PLAN_RECONCILIATION_REQUIRED",
|
"PLAN_RECONCILIATION_REQUIRED",
|
||||||
@@ -4559,8 +4687,11 @@ pub(crate) fn read_plan_session_with_recovery_locked(
|
|||||||
"plan session previous 提升",
|
"plan session previous 提升",
|
||||||
)?;
|
)?;
|
||||||
sync_planning_parent(primary_path.parent().expect("session has parent"))?;
|
sync_planning_parent(primary_path.parent().expect("session has parent"))?;
|
||||||
let promoted =
|
let promoted = read_optional_plan_session_file(
|
||||||
read_optional_plan_session_file(&primary_path, "提升后的 plan session primary")?
|
root,
|
||||||
|
&primary_path,
|
||||||
|
"提升后的 plan session primary",
|
||||||
|
)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
PlanningStorageError::new(
|
PlanningStorageError::new(
|
||||||
"PLAN_RECONCILIATION_REQUIRED",
|
"PLAN_RECONCILIATION_REQUIRED",
|
||||||
@@ -4577,16 +4708,22 @@ pub(crate) fn read_plan_session_with_recovery_locked(
|
|||||||
}
|
}
|
||||||
(Some(primary_value), Some(previous_value)) => {
|
(Some(primary_value), Some(previous_value)) => {
|
||||||
if primary_value == previous_value {
|
if primary_value == previous_value {
|
||||||
let current_primary =
|
let current_primary = read_optional_plan_session_file(
|
||||||
read_optional_plan_session_file(&primary_path, "锁内 plan session primary")?
|
root,
|
||||||
|
&primary_path,
|
||||||
|
"锁内 plan session primary",
|
||||||
|
)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
PlanningStorageError::new(
|
PlanningStorageError::new(
|
||||||
"PLAN_RECONCILIATION_REQUIRED",
|
"PLAN_RECONCILIATION_REQUIRED",
|
||||||
"清理 session previous 时 primary 缺失",
|
"清理 session previous 时 primary 缺失",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let current_previous =
|
let current_previous = read_optional_plan_session_file(
|
||||||
read_optional_plan_session_file(&previous_path, "锁内 plan session previous")?
|
root,
|
||||||
|
&previous_path,
|
||||||
|
"锁内 plan session previous",
|
||||||
|
)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
PlanningStorageError::new(
|
PlanningStorageError::new(
|
||||||
"PLAN_RECONCILIATION_REQUIRED",
|
"PLAN_RECONCILIATION_REQUIRED",
|
||||||
@@ -4603,15 +4740,18 @@ pub(crate) fn read_plan_session_with_recovery_locked(
|
|||||||
}
|
}
|
||||||
validate_plan_session_successor(&previous_value, &primary_value)?;
|
validate_plan_session_successor(&previous_value, &primary_value)?;
|
||||||
let current_primary =
|
let current_primary =
|
||||||
read_optional_plan_session_file(&primary_path, "锁内 plan session primary")?
|
read_optional_plan_session_file(root, &primary_path, "锁内 plan session primary")?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
PlanningStorageError::new(
|
PlanningStorageError::new(
|
||||||
"PLAN_RECONCILIATION_REQUIRED",
|
"PLAN_RECONCILIATION_REQUIRED",
|
||||||
"清理 session previous 时 primary 缺失",
|
"清理 session previous 时 primary 缺失",
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let current_previous =
|
let current_previous = read_optional_plan_session_file(
|
||||||
read_optional_plan_session_file(&previous_path, "锁内 plan session previous")?
|
root,
|
||||||
|
&previous_path,
|
||||||
|
"锁内 plan session previous",
|
||||||
|
)?
|
||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
PlanningStorageError::new(
|
PlanningStorageError::new(
|
||||||
"PLAN_RECONCILIATION_REQUIRED",
|
"PLAN_RECONCILIATION_REQUIRED",
|
||||||
@@ -5190,6 +5330,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_applied_answers_bind_unique_round_and_continuation() {
|
fn session_applied_answers_bind_unique_round_and_continuation() {
|
||||||
|
let directory = tempfile::tempdir().expect("temp root");
|
||||||
|
let root = directory.path();
|
||||||
let mut session = golden_session();
|
let mut session = golden_session();
|
||||||
session.phase = "awaiting_user_input".to_string();
|
session.phase = "awaiting_user_input".to_string();
|
||||||
session.active_run_id = None;
|
session.active_run_id = None;
|
||||||
@@ -5230,6 +5372,25 @@ mod tests {
|
|||||||
.clone();
|
.clone();
|
||||||
session.session_fingerprint = plan_session_fingerprint(&session).expect("answer fp");
|
session.session_fingerprint = plan_session_fingerprint(&session).expect("answer fp");
|
||||||
validate_plan_session(&session).expect("valid applied answer");
|
validate_plan_session(&session).expect("valid applied answer");
|
||||||
|
|
||||||
|
let mut next_question = session.clone();
|
||||||
|
next_question.latest_delegation_id = "delegation-next-question-002".to_string();
|
||||||
|
next_question.session_fingerprint =
|
||||||
|
plan_session_fingerprint(&next_question).expect("next question fp");
|
||||||
|
validate_plan_session(&next_question)
|
||||||
|
.expect("awaiting_user_input may anchor the new question delivery");
|
||||||
|
|
||||||
|
let mut forged_collecting = next_question.clone();
|
||||||
|
forged_collecting.phase = "collecting".to_string();
|
||||||
|
forged_collecting.session_fingerprint =
|
||||||
|
plan_session_fingerprint(&forged_collecting).expect("recompute forged fingerprint");
|
||||||
|
assert_eq!(
|
||||||
|
write_plan_session_atomic(root, &forged_collecting)
|
||||||
|
.expect_err("runtime boundary must reject an unrelated latest delegation")
|
||||||
|
.code(),
|
||||||
|
"PLAN_NEEDS_RECONCILIATION"
|
||||||
|
);
|
||||||
|
|
||||||
let mut duplicate = session.clone();
|
let mut duplicate = session.clone();
|
||||||
duplicate
|
duplicate
|
||||||
.applied_answers
|
.applied_answers
|
||||||
@@ -5238,6 +5399,101 @@ mod tests {
|
|||||||
assert!(validate_plan_session(&duplicate).is_err());
|
assert!(validate_plan_session(&duplicate).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn session_runtime_lineage_rejects_mixed_quality_repair_with_recomputed_fingerprint() {
|
||||||
|
let directory = tempfile::tempdir().expect("temp root");
|
||||||
|
let root = directory.path();
|
||||||
|
let mut session = golden_session();
|
||||||
|
session.decisions_summary.push(PlanDecisionSummary {
|
||||||
|
id: "route-replay".to_string(),
|
||||||
|
topic: "路线重玩".to_string(),
|
||||||
|
state: "confirmed".to_string(),
|
||||||
|
answer_source: "user_option".to_string(),
|
||||||
|
round: 1,
|
||||||
|
answer_summary: "验证分支是否驱动重玩".to_string(),
|
||||||
|
});
|
||||||
|
let questions_sha256 = "a".repeat(64);
|
||||||
|
let answers_sha256 = "b".repeat(64);
|
||||||
|
let question_delegation_id = "delegation-question-mixed-001".to_string();
|
||||||
|
let continuation_id = derive_plan_continuation_delegation_id(
|
||||||
|
&session.root_run_id,
|
||||||
|
&question_delegation_id,
|
||||||
|
&questions_sha256,
|
||||||
|
&answers_sha256,
|
||||||
|
)
|
||||||
|
.expect("continuation id");
|
||||||
|
session.applied_answers.push(PlanAppliedAnswer {
|
||||||
|
delegation_id: question_delegation_id,
|
||||||
|
continuation_delegation_id: continuation_id.clone(),
|
||||||
|
request_id: "request-question-mixed-001".to_string(),
|
||||||
|
question_id: "route_replay".to_string(),
|
||||||
|
response_id: "app-user-input-mixed-001".to_string(),
|
||||||
|
questions_sha256,
|
||||||
|
answers_sha256,
|
||||||
|
decision_id: "route-replay".to_string(),
|
||||||
|
round: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
let quality_repair_id = "delegation-quality-repair-mixed-002";
|
||||||
|
let latest_id = "delegation-latest-mixed-003";
|
||||||
|
let build_delivery =
|
||||||
|
|delegation_id: &str,
|
||||||
|
repair_of: Option<&str>,
|
||||||
|
contract_status: crate::delegation::StaticDelegateContractStatus| {
|
||||||
|
let mut delivery = crate::delegation::new_static_delegate_delivery_with_contract(
|
||||||
|
&session.root_agent_id,
|
||||||
|
"session-golden-parent-001",
|
||||||
|
&session.root_run_id,
|
||||||
|
&format!("{delegation_id}-action"),
|
||||||
|
delegation_id,
|
||||||
|
&session.agent_id,
|
||||||
|
&session.session_id,
|
||||||
|
&format!("{delegation_id}-run"),
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
repair_of,
|
||||||
|
);
|
||||||
|
let mut result = crate::delegation::StaticDelegateStructuredResult::default();
|
||||||
|
result.contract_status = contract_status;
|
||||||
|
delivery.status = crate::delegation::StaticDelegateDeliveryStatus::ClaimedByParent;
|
||||||
|
delivery.terminal_status = Some("completed".to_string());
|
||||||
|
delivery.result_summary = Some("planning lineage test".to_string());
|
||||||
|
delivery.structured_result = Some(result);
|
||||||
|
delivery.claimed_by_action_id = Some(format!("{delegation_id}-claim"));
|
||||||
|
delivery
|
||||||
|
};
|
||||||
|
let continuation = build_delivery(
|
||||||
|
&continuation_id,
|
||||||
|
None,
|
||||||
|
crate::delegation::StaticDelegateContractStatus::UserRevisionRequested,
|
||||||
|
);
|
||||||
|
let quality_repair = build_delivery(
|
||||||
|
quality_repair_id,
|
||||||
|
Some(&continuation_id),
|
||||||
|
crate::delegation::StaticDelegateContractStatus::NeedsRepair,
|
||||||
|
);
|
||||||
|
let latest = build_delivery(
|
||||||
|
latest_id,
|
||||||
|
Some(quality_repair_id),
|
||||||
|
crate::delegation::StaticDelegateContractStatus::UserRevisionRequested,
|
||||||
|
);
|
||||||
|
for delivery in [&continuation, &quality_repair, &latest] {
|
||||||
|
crate::delegation::write_static_delegate_delivery_at(root, delivery)
|
||||||
|
.expect("write mixed lineage delivery");
|
||||||
|
}
|
||||||
|
|
||||||
|
session.latest_delegation_id = latest_id.to_string();
|
||||||
|
session.session_fingerprint =
|
||||||
|
plan_session_fingerprint(&session).expect("recompute forged fingerprint");
|
||||||
|
validate_plan_session(&session).expect("standalone session shape remains valid");
|
||||||
|
assert_eq!(
|
||||||
|
write_plan_session_atomic(root, &session)
|
||||||
|
.expect_err("quality repair edge must clear old applied answers")
|
||||||
|
.code(),
|
||||||
|
"PLAN_IDENTITY_CONFLICT"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn session_recovery_rejects_corrupt_primary_and_forked_previous() {
|
fn session_recovery_rejects_corrupt_primary_and_forked_previous() {
|
||||||
let directory = tempfile::tempdir().expect("temp root");
|
let directory = tempfile::tempdir().expect("temp root");
|
||||||
|
|||||||
@@ -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]
|
#[test]
|
||||||
fn projection_failure_after_gdd_create_returns_recovery_pending_and_replays() {
|
fn projection_failure_after_gdd_create_returns_recovery_pending_and_replays() {
|
||||||
let (root, context, input) = submit_fixture();
|
let (root, context, input) = submit_fixture();
|
||||||
|
|||||||
+74
-3
@@ -1180,20 +1180,91 @@ where
|
|||||||
return Err(error);
|
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);
|
drop(control_lock);
|
||||||
let notified = active.notify.notified();
|
let notified = active.notify.notified();
|
||||||
tokio::pin!(notified);
|
tokio::pin!(notified);
|
||||||
tokio::pin!(request);
|
tokio::pin!(request);
|
||||||
let result = if active.interrupted.load(Ordering::Acquire) {
|
let (result, active_millis) = if active.interrupted.load(Ordering::Acquire) {
|
||||||
Ok(None)
|
(Ok(None), 0)
|
||||||
} else {
|
} else {
|
||||||
tokio::select! {
|
let active_started = tokio::time::Instant::now();
|
||||||
|
let result = tokio::select! {
|
||||||
biased;
|
biased;
|
||||||
_ = &mut notified => Ok(None),
|
_ = &mut notified => Ok(None),
|
||||||
result = &mut request => result.map(Some),
|
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 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 Ok(Some(response)) = result.as_ref() {
|
||||||
if let Err(error) = success_commit(&request_id, response) {
|
if let Err(error) = success_commit(&request_id, response) {
|
||||||
unregister_game_creator_agent_runtime_provider_request(&key, &active);
|
unregister_game_creator_agent_runtime_provider_request(&key, &active);
|
||||||
|
|||||||
+1
-1
@@ -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
|
/// source string alone. The durable binding must describe a top-level
|
||||||
/// `project-supervisor-plan` standard run whose root fields point back to itself
|
/// `project-supervisor-plan` standard run whose root fields point back to itself
|
||||||
/// and which has no parent link.
|
/// 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,
|
root: &Path,
|
||||||
agent_id: &str,
|
agent_id: &str,
|
||||||
run_id: &str,
|
run_id: &str,
|
||||||
|
|||||||
@@ -35,7 +35,9 @@ pub(in crate::agent) use run_status::*;
|
|||||||
pub(in crate::agent) use task_ops::*;
|
pub(in crate::agent) use task_ops::*;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[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)]
|
#[cfg(test)]
|
||||||
pub(crate) use media::validate_agent_runtime_canvas_replacement_authorization_at;
|
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>,
|
action_id: Option<&str>,
|
||||||
input: &serde_json::Value,
|
input: &serde_json::Value,
|
||||||
) -> AgentRuntimeToolObservation {
|
) -> 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 = agent_runtime_tool_input_text(input, &["agentId", "targetAgentId"]);
|
||||||
let target_agent_id = match normalize_game_creator_runtime_agent_id(target_agent_id.as_str()) {
|
let target_agent_id = match normalize_game_creator_runtime_agent_id(target_agent_id.as_str()) {
|
||||||
Ok(target_agent_id) => target_agent_id,
|
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() {
|
if parent_session_id.is_some() {
|
||||||
drop(dispatch_lock.take());
|
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,
|
root,
|
||||||
&target_agent_id,
|
&target_agent_id,
|
||||||
requested_session_id,
|
requested_session_id,
|
||||||
@@ -1115,6 +1158,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
|
|||||||
"agent-delegate",
|
"agent-delegate",
|
||||||
None,
|
None,
|
||||||
Some(&task_link),
|
Some(&task_link),
|
||||||
|
project_write_lock,
|
||||||
) {
|
) {
|
||||||
Ok((runtime, delegated_run_id)) => {
|
Ok((runtime, delegated_run_id)) => {
|
||||||
if let Some((_, expected_run_id)) = target_session_id.as_ref() {
|
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);
|
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) =
|
let Some(runtime_lock) =
|
||||||
try_acquire_game_creator_agent_runtime_task_lock(root, &parent_task.agent_id)?
|
try_acquire_game_creator_agent_runtime_task_lock(root, &parent_task.agent_id)?
|
||||||
else {
|
else {
|
||||||
@@ -342,7 +353,18 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
|
|||||||
¤t_task.run_id,
|
¤t_task.run_id,
|
||||||
)?;
|
)?;
|
||||||
let mut state = state;
|
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);
|
return Ok(true);
|
||||||
}
|
}
|
||||||
let state = advance_game_creator_agent_runtime_turn_at(
|
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 正在认领委派回执",
|
"专业 Agent 已完成,父 run 正在认领委派回执",
|
||||||
"静态委派回执已就绪,恢复同一父 run。",
|
"静态委派回执已就绪,恢复同一父 run。",
|
||||||
)?;
|
)?;
|
||||||
|
drop(project_lock);
|
||||||
let root = root.to_path_buf();
|
let root = root.to_path_buf();
|
||||||
let agent_id = current_task.agent_id.clone();
|
let agent_id = current_task.agent_id.clone();
|
||||||
let task = current_task.task.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)
|
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(
|
pub(in crate::agent) fn wake_waiting_autonomous_manifest_parent_run_at(
|
||||||
root: &Path,
|
root: &Path,
|
||||||
parent_task: &AgentRuntimeTaskRecord,
|
parent_task: &AgentRuntimeTaskRecord,
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user