完成 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() {
|
||||||
root,
|
Some(project_lock) => answer_game_creator_agent_user_input_request_for_pending_at_locked(
|
||||||
&pending,
|
root,
|
||||||
request_id,
|
&pending,
|
||||||
response_id,
|
request_id,
|
||||||
answers,
|
response_id,
|
||||||
)?;
|
answers,
|
||||||
|
project_lock,
|
||||||
|
)?,
|
||||||
|
None => answer_game_creator_agent_user_input_request_for_pending_at(
|
||||||
|
root,
|
||||||
|
&pending,
|
||||||
|
request_id,
|
||||||
|
response_id,
|
||||||
|
answers,
|
||||||
|
)?,
|
||||||
|
};
|
||||||
if already_observed {
|
if 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();
|
||||||
|
|||||||
+49
-23
@@ -925,23 +925,45 @@ 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;
|
||||||
root,
|
let (mut result, actual_retry_run_id) = if planning_retry {
|
||||||
&agent_id,
|
// Planning enqueue owns both locks. Keep the global order identical
|
||||||
"Agent Runtime 重试入队",
|
// to ordinary/delegated starts: project first, Session lane second.
|
||||||
|| {
|
// The locked entry also projects the retry child before it can start.
|
||||||
start_game_creator_agent_background_task_with_link_in_session_lane_at(
|
let project_lock = acquire_game_creator_agent_runtime_project_write_lock_with_wait(
|
||||||
root,
|
root,
|
||||||
&agent_id,
|
"planning.child-session.retry",
|
||||||
Some(&task.session_id),
|
)?;
|
||||||
&task.task,
|
start_game_creator_agent_background_task_with_link_locked_at(
|
||||||
&retry_run_id,
|
root,
|
||||||
&retry_source,
|
&agent_id,
|
||||||
Some(&retry_run_profile),
|
Some(&task.session_id),
|
||||||
retry_link.as_ref(),
|
&task.task,
|
||||||
)
|
&retry_run_id,
|
||||||
},
|
&retry_source,
|
||||||
)?;
|
Some(&retry_run_profile),
|
||||||
|
retry_link.as_ref(),
|
||||||
|
&project_lock,
|
||||||
|
)?
|
||||||
|
} else {
|
||||||
|
with_agent_conversation_session_lane_at(
|
||||||
|
root,
|
||||||
|
&agent_id,
|
||||||
|
"Agent Runtime 重试入队",
|
||||||
|
|| {
|
||||||
|
start_game_creator_agent_background_task_with_link_in_session_lane_at(
|
||||||
|
root,
|
||||||
|
&agent_id,
|
||||||
|
Some(&task.session_id),
|
||||||
|
&task.task,
|
||||||
|
&retry_run_id,
|
||||||
|
&retry_source,
|
||||||
|
Some(&retry_run_profile),
|
||||||
|
retry_link.as_ref(),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)?
|
||||||
|
};
|
||||||
let retry_task_sha256 = format!("{:x}", Sha256::digest(task.task.as_bytes()));
|
let retry_task_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());
|
||||||
notify_external_agent_runner_after_background_task_enqueue(
|
if !planning_retry {
|
||||||
root,
|
// The planning locked entry performs this notification after releasing
|
||||||
&agent_id,
|
// its Session lane; the legacy in-lane entry deliberately does not.
|
||||||
&task.session_id,
|
notify_external_agent_runner_after_background_task_enqueue(
|
||||||
&actual_retry_run_id,
|
root,
|
||||||
)?;
|
&agent_id,
|
||||||
|
&task.session_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",
|
||||||
|
|||||||
+108
-14
@@ -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() {
|
||||||
root,
|
Some(_) => mark_game_creator_agent_runtime_cancelled_at_locked(
|
||||||
&mut runtime,
|
root,
|
||||||
"Agent 后台任务已按开发者请求取消",
|
&mut runtime,
|
||||||
Some("Runtime 恢复用户输入等待时发现尚未完成的取消请求。"),
|
"Agent 后台任务已按开发者请求取消",
|
||||||
)?;
|
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() {
|
||||||
root,
|
Some(_) => mark_game_creator_agent_runtime_cancelled_at_locked(
|
||||||
&mut runtime,
|
root,
|
||||||
"Agent 用户输入请求已取消",
|
&mut runtime,
|
||||||
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
|
"Agent 用户输入请求已取消",
|
||||||
)?;
|
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
|
||||||
|
)?,
|
||||||
|
None => mark_game_creator_agent_runtime_cancelled_at(
|
||||||
|
root,
|
||||||
|
&mut runtime,
|
||||||
|
"Agent 用户输入请求已取消",
|
||||||
|
Some("Runner 恢复时发现用户输入 sidecar 已取消。"),
|
||||||
|
)?,
|
||||||
|
}
|
||||||
return Ok(AgentRuntimePendingActionResume::NotFound(runtime_lock));
|
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
+313
-57
File diff suppressed because it is too large
Load Diff
@@ -3674,6 +3674,191 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn approval_receipt_consumes_submit_batch_and_folds_final_provider_usage_once() {
|
||||||
|
let (root, mut context, input) = submit_fixture();
|
||||||
|
let mut child_runtime = start_game_creator_agent_runtime_task_at(
|
||||||
|
&root,
|
||||||
|
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||||
|
"提交 Fast GDD",
|
||||||
|
&context.created_by_run_id,
|
||||||
|
"agent-delegate",
|
||||||
|
"提交一份可审批的 Fast GDD",
|
||||||
|
vec!["读取项目事实".to_string(), "提交 GDD".to_string()],
|
||||||
|
)
|
||||||
|
.expect("start planning child task");
|
||||||
|
child_runtime.session_id = context.session_id.clone();
|
||||||
|
child_runtime.parent_agent_id = Some(context.root_agent_id.clone());
|
||||||
|
child_runtime.parent_run_id = Some(context.root_run_id.clone());
|
||||||
|
child_runtime.delegation_id = Some(context.delegation_id.clone());
|
||||||
|
child_runtime.run_profile_binding_fingerprint =
|
||||||
|
context.run_profile_binding_fingerprint.clone();
|
||||||
|
append_game_creator_agent_runtime_task(&root, &child_runtime)
|
||||||
|
.expect("persist planning child identity");
|
||||||
|
write_game_creator_agent_runtime_state(&root, &child_runtime)
|
||||||
|
.expect("persist current planning child identity");
|
||||||
|
|
||||||
|
let mut snapshot = AgentRuntimeProviderRequestSnapshot {
|
||||||
|
project_id: context.project_id.clone(),
|
||||||
|
agent_id: child_runtime.agent_id.clone(),
|
||||||
|
task_id: child_runtime.task_id.clone(),
|
||||||
|
session_id: child_runtime.session_id.clone(),
|
||||||
|
run_id: child_runtime.run_id.clone(),
|
||||||
|
source: child_runtime.source.clone(),
|
||||||
|
goal_id: child_runtime.goal_id.clone(),
|
||||||
|
goal_revision: child_runtime.goal_revision,
|
||||||
|
goal_snapshot_fingerprint: agent_goal_snapshot_fingerprint_for_state_at(
|
||||||
|
&root,
|
||||||
|
&child_runtime,
|
||||||
|
)
|
||||||
|
.expect("planning goal snapshot fingerprint"),
|
||||||
|
applied_steer_cursor: child_runtime.applied_steer_cursor,
|
||||||
|
request_kind: "tool-plan".to_string(),
|
||||||
|
request_slot: "loop-1-repair-0".to_string(),
|
||||||
|
web_search_enabled: false,
|
||||||
|
allow_idle_context_compaction: false,
|
||||||
|
planning_session_binding: None,
|
||||||
|
};
|
||||||
|
let binding = capture_plan_provider_session_binding_for_snapshot(
|
||||||
|
&root,
|
||||||
|
&child_runtime,
|
||||||
|
&snapshot,
|
||||||
|
&format!("sha256-serde-json-v2:{}", "7".repeat(64)),
|
||||||
|
)
|
||||||
|
.expect("capture submit Provider binding");
|
||||||
|
snapshot.planning_session_binding = Some(binding.clone());
|
||||||
|
let request_id = game_creator_agent_runtime_provider_request_id(&snapshot);
|
||||||
|
assert_eq!(request_id, binding.provider_request_id);
|
||||||
|
assert!(
|
||||||
|
append_game_creator_agent_runtime_provider_request_lifecycle(
|
||||||
|
&root,
|
||||||
|
&snapshot,
|
||||||
|
&request_id,
|
||||||
|
"started",
|
||||||
|
)
|
||||||
|
.expect("append final submit request start")
|
||||||
|
);
|
||||||
|
let usage_scope =
|
||||||
|
capture_plan_provider_usage_scope_at_locked(&root, &snapshot, &request_id)
|
||||||
|
.expect("capture final submit usage scope");
|
||||||
|
assert!(persist_plan_provider_usage_fact_at(
|
||||||
|
&root,
|
||||||
|
&snapshot,
|
||||||
|
&request_id,
|
||||||
|
usage_scope.as_ref(),
|
||||||
|
"completed",
|
||||||
|
23,
|
||||||
|
)
|
||||||
|
.expect("persist final submit usage fact"));
|
||||||
|
assert!(
|
||||||
|
append_game_creator_agent_runtime_provider_request_lifecycle(
|
||||||
|
&root,
|
||||||
|
&snapshot,
|
||||||
|
&request_id,
|
||||||
|
"completed",
|
||||||
|
)
|
||||||
|
.expect("append final submit request completion")
|
||||||
|
);
|
||||||
|
|
||||||
|
let plan = AgentRuntimeToolPlan {
|
||||||
|
thinking_summary: "Fast GDD 已收敛,提交审批".to_string(),
|
||||||
|
plan_update: None,
|
||||||
|
plan: vec!["提交 Fast GDD".to_string()],
|
||||||
|
actions: vec![AgentRuntimeToolAction {
|
||||||
|
tool: PLAN_SUBMIT_GDD_TOOL.to_string(),
|
||||||
|
reason: Some("提交当前策划版本".to_string()),
|
||||||
|
input: serde_json::to_value(&input).expect("serialize plan.submit_gdd input"),
|
||||||
|
}],
|
||||||
|
response: String::new(),
|
||||||
|
};
|
||||||
|
let project_revision =
|
||||||
|
read_game_creator_agent_runtime_project_revision(&root).expect("read revision");
|
||||||
|
let repository_fingerprint = build_repository_startup_context_at(&root)
|
||||||
|
.expect("build repository context")
|
||||||
|
.fingerprint;
|
||||||
|
let batch =
|
||||||
|
match prepare_game_creator_agent_runtime_provider_action_batch_with_planning_binding(
|
||||||
|
&root,
|
||||||
|
&child_runtime,
|
||||||
|
"提交一份可审批的 Fast GDD",
|
||||||
|
&plan,
|
||||||
|
&[],
|
||||||
|
&project_revision,
|
||||||
|
&repository_fingerprint,
|
||||||
|
Some(&binding),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("prepare exact plan.submit_gdd batch")
|
||||||
|
{
|
||||||
|
AgentRuntimeProviderActionBatchPreparation::Ready(batch) => batch,
|
||||||
|
other => panic!("plan.submit_gdd batch must be ready: {other:?}"),
|
||||||
|
};
|
||||||
|
let pending = batch.actions[0].clone();
|
||||||
|
write_game_creator_agent_runtime_pending_tool_action(&root, &pending)
|
||||||
|
.expect("persist standalone submit anchor");
|
||||||
|
context.action_id = pending.action_id.clone();
|
||||||
|
context.action_fingerprint = pending.action_fingerprint.clone();
|
||||||
|
|
||||||
|
execute_plan_submit_gdd(&root, &context, &input).expect("commit GDD");
|
||||||
|
assert_eq!(
|
||||||
|
fold_plan_provider_usage_into_session_at_locked(&root)
|
||||||
|
.expect("submit batch must defer final usage"),
|
||||||
|
PlanProviderUsageFoldOutcome::Deferred
|
||||||
|
);
|
||||||
|
let mut child_completed = child_runtime.clone();
|
||||||
|
child_completed.status = "completed".to_string();
|
||||||
|
child_completed.phase = "completed".to_string();
|
||||||
|
child_completed.current_action = "Fast GDD 已提交".to_string();
|
||||||
|
child_completed.pending_tool_action = Some(pending.summary());
|
||||||
|
append_game_creator_agent_runtime_task(&root, &child_completed)
|
||||||
|
.expect("append completed planning child task");
|
||||||
|
write_game_creator_agent_runtime_state(&root, &child_completed)
|
||||||
|
.expect("persist completed planning child state");
|
||||||
|
|
||||||
|
let gdd = read_plan_gdd_chain(&root)
|
||||||
|
.expect("read submitted GDD")
|
||||||
|
.pop()
|
||||||
|
.expect("GDD exists");
|
||||||
|
create_plan_gdd_approval_pending_at(&root, &gdd).expect("create approval pending");
|
||||||
|
let decision_input = approval_input(
|
||||||
|
&gdd,
|
||||||
|
"approve",
|
||||||
|
"gdd-response-00000000-0000-4000-8000-000000000022",
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
let first = decide_plan_gdd_at(&root, &decision_input).expect("commit approval receipt");
|
||||||
|
assert_eq!(first.outcome, "committed");
|
||||||
|
assert!(!first.recovery_pending);
|
||||||
|
assert!(!game_creator_agent_runtime_pending_tool_action_exists(
|
||||||
|
&root,
|
||||||
|
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||||
|
&context.created_by_run_id,
|
||||||
|
));
|
||||||
|
assert!(!game_creator_agent_runtime_provider_action_batch_exists(
|
||||||
|
&root,
|
||||||
|
GAME_CREATOR_PROJECT_PLANNING_AGENT_ID,
|
||||||
|
&context.created_by_run_id,
|
||||||
|
));
|
||||||
|
let folded = read_plan_session_with_recovery(&root)
|
||||||
|
.expect("read folded receipt session")
|
||||||
|
.expect("receipt session exists");
|
||||||
|
assert_eq!(folded.phase, "approved");
|
||||||
|
assert_eq!(folded.accumulated_agent_millis, 23);
|
||||||
|
let folded_revision = folded.session_revision;
|
||||||
|
|
||||||
|
let replay = decide_plan_gdd_at(&root, &decision_input).expect("replay approval decision");
|
||||||
|
assert_eq!(replay.outcome, "replayed");
|
||||||
|
assert!(!replay.recovery_pending);
|
||||||
|
assert!(!reconcile_plan_gdd_approval_projections_at(&root)
|
||||||
|
.expect("replay receipt recovery projections"));
|
||||||
|
let replayed = read_plan_session_with_recovery(&root)
|
||||||
|
.expect("read replayed receipt session")
|
||||||
|
.expect("replayed receipt session exists");
|
||||||
|
assert_eq!(replayed.session_revision, folded_revision);
|
||||||
|
assert_eq!(replayed.accumulated_agent_millis, 23);
|
||||||
|
cleanup_fixture(root);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[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();
|
||||||
|
|||||||
+75
-4
@@ -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