补齐自主Swarm终端验收与恢复门禁
新增Project Supervisor自主终端协作真实E2E与隔离AppData安全检查 为Swarm Chat输出脱敏单行turn report并补齐终端收束断言 强化Supervisor多专业并行委派与语义验收提示 修复静态委派和隔离all-join混合等待及认领恢复 为agent.message增加语义幂等持久化并避免重复消息伪造进展 补充消息幂等、混合恢复和turn report定向回归测试 增加根级与应用级自主验收命令并更新Runtime技术文档
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"agent-run": "node scripts/run-cli-with-config.mjs --agent-run",
|
||||
"agent-run:smoke": "node scripts/smoke-agent-run-local-provider.mjs",
|
||||
"agent-runtime:real-e2e": "node scripts/agent-runtime-real-e2e.mjs",
|
||||
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
|
||||
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
|
||||
"agent-runtime:steer-real-e2e": "node scripts/agent-runtime-steer-real-e2e.mjs",
|
||||
"agent-runtime:steer-runner-kill-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite steer-runner-kill",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1596,12 +1596,23 @@ fn supervisor_delivery_pending_action_has_durable_side_effect_at(
|
||||
pending: &AgentRuntimePendingToolAction,
|
||||
) -> Result<bool, String> {
|
||||
match pending.action.tool.as_str() {
|
||||
"agent.run_status" => static_delegate_claim_exists_at(
|
||||
root,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
&pending.action_id,
|
||||
),
|
||||
"agent.run_status" => {
|
||||
if static_delegate_claim_exists_at(
|
||||
root,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
&pending.action_id,
|
||||
)? {
|
||||
Ok(true)
|
||||
} else {
|
||||
isolated_join_claim_exists_for_parent_action_at(
|
||||
root,
|
||||
&pending.agent_id,
|
||||
&pending.run_id,
|
||||
&pending.action_id,
|
||||
)
|
||||
}
|
||||
}
|
||||
"agent.delegate" => {
|
||||
let target_agent_id =
|
||||
normalize_game_creator_runtime_agent_id(&agent_runtime_tool_input_text(
|
||||
@@ -6240,6 +6251,63 @@ fn persist_waiting_static_delegate_parent_context_at(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn persist_waiting_isolated_parent_context_at(
|
||||
root: &Path,
|
||||
runtime: &mut AgentRuntimeState,
|
||||
task: &str,
|
||||
plan: &AgentRuntimeToolPlan,
|
||||
observations: &mut Vec<AgentRuntimeToolObservation>,
|
||||
next_loop_index: usize,
|
||||
context_tracker: &mut AgentRuntimeContextWindowTracker,
|
||||
blocker: AgentRuntimeToolObservation,
|
||||
) -> Result<(), String> {
|
||||
let blocker_summary = blocker.summary();
|
||||
runtime.status = "running".to_string();
|
||||
runtime.phase = "waiting-for-isolated-join".to_string();
|
||||
runtime.current_action = "等待动态隔离 Agent 的 all-join".to_string();
|
||||
runtime.waiting_on = "隔离子 Agent 完成并由父 run 认领 all-join".to_string();
|
||||
runtime.next_step = "all-join ready 后自动唤醒当前父 run".to_string();
|
||||
runtime.observations.push(blocker_summary.clone());
|
||||
runtime.updated_at = unix_timestamp();
|
||||
context_tracker.record(&blocker);
|
||||
observations.push(blocker);
|
||||
persist_game_creator_agent_runtime_context(
|
||||
root,
|
||||
runtime,
|
||||
task,
|
||||
plan,
|
||||
observations,
|
||||
next_loop_index,
|
||||
context_tracker,
|
||||
)?;
|
||||
append_game_creator_agent_runtime_task(root, runtime)?;
|
||||
refresh_game_creator_agent_runtime_task_queue(root, runtime)?;
|
||||
write_game_creator_agent_runtime_state(root, runtime)?;
|
||||
let _ = append_game_creator_agent_runtime_event(
|
||||
root,
|
||||
runtime,
|
||||
"observation",
|
||||
"running",
|
||||
"waiting-for-isolated-join",
|
||||
&blocker_summary,
|
||||
None,
|
||||
);
|
||||
let _ = append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.agent.isolated_join.waiting",
|
||||
"agentId": runtime.agent_id,
|
||||
"taskId": runtime.task_id,
|
||||
"sessionId": runtime.session_id,
|
||||
"runId": runtime.run_id,
|
||||
"status": "waiting-for-isolated-join",
|
||||
"nextLoopIndex": next_loop_index,
|
||||
}),
|
||||
);
|
||||
emit_game_creator_agent_runtime_update(root, &runtime.agent_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn run_game_creator_agent_background_task_pass_with_context(
|
||||
root: PathBuf,
|
||||
agent_id: String,
|
||||
@@ -6333,6 +6401,35 @@ async fn run_game_creator_agent_background_task_pass_with_context(
|
||||
}
|
||||
};
|
||||
if !consumed_steer {
|
||||
if let Some(blocker) =
|
||||
isolated_join_completion_blocker_at(&root, &agent_id, &runtime.run_id)
|
||||
{
|
||||
let waits_for_join = blocker
|
||||
.detail
|
||||
.as_deref()
|
||||
.is_some_and(isolated_join_barrier_has_waiting_groups);
|
||||
if waits_for_join {
|
||||
if let Err(error) = persist_waiting_isolated_parent_context_at(
|
||||
&root,
|
||||
&mut runtime,
|
||||
&task,
|
||||
&plan,
|
||||
&mut observations,
|
||||
loop_index,
|
||||
&mut context_tracker,
|
||||
blocker,
|
||||
) {
|
||||
return fail_game_creator_agent_background_context_at(
|
||||
&root,
|
||||
&agent_id,
|
||||
&session_id,
|
||||
runtime,
|
||||
&format!("持久化动态隔离 Agent all-join 等待状态失败:{error}"),
|
||||
);
|
||||
}
|
||||
return AgentBackgroundTaskOutcome::WaitingForIsolatedJoin;
|
||||
}
|
||||
}
|
||||
if let Some(blocker) =
|
||||
static_delegate_completion_blocker_at(&root, &agent_id, &runtime.run_id)
|
||||
{
|
||||
@@ -11236,7 +11333,10 @@ impl AgentRuntimeContextWindowTracker {
|
||||
}
|
||||
|
||||
pub(crate) fn record(&mut self, observation: &AgentRuntimeToolObservation) {
|
||||
if observation.tool == "runtime.context" {
|
||||
if observation.tool == "runtime.context"
|
||||
|| (observation.tool == "agent.message"
|
||||
&& observation.detail.as_deref() == Some("messageAppended=false"))
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.observation_signatures
|
||||
@@ -19997,7 +20097,9 @@ pub(crate) async fn execute_game_creator_agent_runtime_tool_action_with_pending_
|
||||
.await
|
||||
}
|
||||
"blackboard.write" => observe_agent_runtime_blackboard_write(root, agent_id, &action.input),
|
||||
"agent.message" => observe_agent_runtime_agent_message(root, agent_id, &action.input),
|
||||
"agent.message" => {
|
||||
observe_agent_runtime_agent_message(root, agent_id, run_id, &action.input)
|
||||
}
|
||||
"agent.delegate" => observe_agent_runtime_project_snapshot_with_lock(
|
||||
root,
|
||||
agent_id,
|
||||
@@ -26274,9 +26376,10 @@ fn observe_agent_runtime_blackboard_write(
|
||||
}
|
||||
}
|
||||
|
||||
fn observe_agent_runtime_agent_message(
|
||||
pub(crate) fn observe_agent_runtime_agent_message(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
input: &serde_json::Value,
|
||||
) -> AgentRuntimeToolObservation {
|
||||
let target_agent_id = agent_runtime_tool_input_text(input, &["agentId", "targetAgentId"]);
|
||||
@@ -26313,36 +26416,63 @@ fn observe_agent_runtime_agent_message(
|
||||
};
|
||||
let content = truncate_agent_runtime_text(sanitize_prompt_context(&content).as_str(), 1_200);
|
||||
let message = format!("来自 {agent_id} 的定向消息:{content}");
|
||||
let result = append_local_conversation_message_at(
|
||||
root,
|
||||
Some(&target_agent_id),
|
||||
LocalConversationMessage {
|
||||
role: "tool".to_string(),
|
||||
content: message,
|
||||
agent_id: None,
|
||||
},
|
||||
)
|
||||
.and_then(|conversation| {
|
||||
let relative_path =
|
||||
agent_runtime_relative_project_path(root, Path::new(&conversation.path))?;
|
||||
append_agent_db_record(
|
||||
root,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.agent.message",
|
||||
"agentId": agent_id,
|
||||
"targetAgentId": target_agent_id.clone(),
|
||||
"targetSessionId": conversation.session_id,
|
||||
"path": relative_path,
|
||||
}),
|
||||
)
|
||||
});
|
||||
let result = resolve_agent_conversation_session_id_at(root, &target_agent_id, None, true)
|
||||
.and_then(|target_session_id| {
|
||||
let content_sha256 = format!("{:x}", Sha256::digest(content.as_bytes()));
|
||||
let semantic_identity = format!(
|
||||
"{agent_id}\n{run_id}\n{target_agent_id}\n{target_session_id}\n{content_sha256}"
|
||||
);
|
||||
let semantic_sha256 = format!("{:x}", Sha256::digest(semantic_identity.as_bytes()));
|
||||
let message_id = format!("agent-message-{semantic_sha256}");
|
||||
let audit_action_id = format!("action-{}", &semantic_sha256[..24]);
|
||||
append_local_conversation_message_for_session_idempotent_with_status_at(
|
||||
root,
|
||||
Some(&target_agent_id),
|
||||
Some(&target_session_id),
|
||||
LocalConversationMessage {
|
||||
role: "tool".to_string(),
|
||||
content: message,
|
||||
agent_id: None,
|
||||
},
|
||||
&message_id,
|
||||
)
|
||||
.and_then(|(conversation, appended)| {
|
||||
let relative_path =
|
||||
agent_runtime_relative_project_path(root, Path::new(&conversation.path))?;
|
||||
append_agent_db_agent_message_if_missing(
|
||||
root,
|
||||
agent_id,
|
||||
run_id,
|
||||
&audit_action_id,
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.agent.message",
|
||||
"agentId": agent_id,
|
||||
"runId": run_id,
|
||||
"actionId": audit_action_id,
|
||||
"messageId": message_id,
|
||||
"targetAgentId": target_agent_id,
|
||||
"targetSessionId": conversation.session_id,
|
||||
"path": relative_path,
|
||||
"contentSha256": content_sha256,
|
||||
"contentChars": content.chars().count(),
|
||||
}),
|
||||
)?;
|
||||
Ok(appended)
|
||||
})
|
||||
});
|
||||
match result {
|
||||
Ok(()) => AgentRuntimeToolObservation {
|
||||
Ok(true) => AgentRuntimeToolObservation {
|
||||
tool: "agent.message".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: format!("已给 {target_agent_id} 留消息"),
|
||||
detail: Some(content),
|
||||
},
|
||||
Ok(false) => AgentRuntimeToolObservation {
|
||||
tool: "agent.message".to_string(),
|
||||
status: "ok".to_string(),
|
||||
summary: format!("给 {target_agent_id} 的相同定向消息已存在,未重复追加"),
|
||||
detail: Some("messageAppended=false".to_string()),
|
||||
},
|
||||
Err(error) => AgentRuntimeToolObservation {
|
||||
tool: "agent.message".to_string(),
|
||||
status: "failed".to_string(),
|
||||
@@ -29287,6 +29417,28 @@ fn claimed_isolated_join_count_for_parent_at(
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
fn isolated_join_claim_exists_for_parent_action_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
parent_run_id: &str,
|
||||
action_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
for join in reconcile_all_isolated_groups_at(root)?
|
||||
.into_iter()
|
||||
.filter(|join| {
|
||||
join.parent_agent_id == parent_agent_id && join.parent_run_id == parent_run_id
|
||||
})
|
||||
{
|
||||
if read_isolated_join_delivery_at(root, &join)?.is_some_and(|delivery| {
|
||||
delivery.status == IsolatedAgentJoinDeliveryStatus::ClaimedByParent
|
||||
&& delivery.claimed_by_action_id.as_deref() == Some(action_id)
|
||||
}) {
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
fn ready_isolated_join_status_for_parent_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
@@ -33552,7 +33704,7 @@ pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt_for_agent(
|
||||
return prompt;
|
||||
}
|
||||
let prompt = format!(
|
||||
"{prompt}\n\n你当前是项目唯一面向用户的 Project Supervisor,并拥有最终回复权。每一轮都必须把用户原始目标视为最高层业务目标,专业 Agent 回执只能补充证据,不能把回执内容改写成新目标。优先把边界清晰的专业工作通过带 acceptanceCriteria 和 expectedArtifacts 的 agent.delegate 分给静态专业 Agent,把互不重叠的临时并行检查通过 agent.spawn_isolated 分派;已有委派未收束时不要重复委派。需要等待专业 Agent 时返回空 response,让 Runtime 的 delegate/all-join 完成屏障保持同一父 run;取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。只在所有必要回执已认领、所有必要返工也已认领、项目副作用已验证且没有待确认动作或待回答请求时给用户最终回复。不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。"
|
||||
"{prompt}\n\n你当前是项目唯一面向用户的 Project Supervisor,并拥有最终回复权。每一轮都必须把用户原始目标视为最高层业务目标,专业 Agent 回执只能补充证据,不能把回执内容改写成新目标。总控不能替代已有专业角色完成其领域交付:只要仓库目标同时包含两个以上互不依赖的专业方向,就必须自行查看静态角色目录,选择最匹配的不同专业 Agent,并在同一个 native planning 批次用带 acceptanceCriteria 和 expectedArtifacts 的 agent.delegate 发起委派,让这些方向并行;用户不需要点名 Agent、指定数量或提醒并行。只有没有匹配专业角色、纯协调工作或一两步轻量读取时才由总控直接处理。互不重叠的临时并行检查通过 agent.spawn_isolated 分派;已有委派未收束时不要重复委派。需要等待专业 Agent 时返回空 response,让 Runtime 的 delegate/all-join 完成屏障保持同一父 run;取得 readyDelegateReceipts 或 readyIsolatedJoins 后直接整合结果。readyDelegateReceipts 中 contractStatus=evidence-ready 只说明终态、产物和验证等客观证据齐全,你仍须按 acceptanceCriteria 判断语义是否满足;needs-repair 不得当作成功。客观或语义不满足时可以发起一次新 agent.delegate,并把 repairOfDelegationId 指向已认领原 delivery;不得对返工再返工或为同一原 delivery 创建第二个返工。专业结果冲突且无法依据用户目标裁决时,合并问题后用一次 user.input_request 询问用户。只有实现路径、产品取舍或缺失事实会实质改变结果时才调用 user.input_request;项目内可读取事实、权限确认和工具失败不得伪装成用户问题。只在所有必要回执已认领、所有必要返工也已认领、项目副作用已验证且没有待确认动作或待回答请求时给用户最终回复。不要向用户暴露内部 task/event、工具计划、动态 child ID 或调试状态。"
|
||||
);
|
||||
format!(
|
||||
"{prompt}\n\n普通 agent.run_status 的 claimedDelegateContracts 只提供已认领合同目录。语义复核或返工前必须用原 delegationId 再调用 agent.run_status,读取 claimedDelegateContract 中未截断的 acceptanceCriteria 和 expectedArtifacts,并在 repair agent.delegate 中逐项原样提交。若返工因合同未完整继承而失败,失败 observation 中的 claimedDelegateContract 是同一 durable delivery 的权威快照,必须逐项据此修正;只有该字段缺失或身份不确定时才按同一 delegationId 重读,不得无目标地重复 run_status 或从 action_history 摘要猜测。"
|
||||
@@ -33620,6 +33772,9 @@ pub(crate) fn game_creator_agent_runtime_tool_plan_system_prompt() -> String {
|
||||
let prompt = format!(
|
||||
"{prompt} git.inspect 会返回 commitSnapshotFingerprint;只有当前非零 revision 已由本 run 验证通过,且已完整审阅变更时,才能用 project.git_commit 的 message、显式 paths、expectedHead 和 expectedSnapshotFingerprint 创建本地提交。project.git_commit 不允许访问 remote、切换分支或执行 merge、rebase、reset、stash、tag、submodule、worktree。"
|
||||
);
|
||||
let prompt = format!(
|
||||
"{prompt} 作为被委派的专业 Agent 时,agent.message 只用于确有必要的中途协调,不能替代自身终态交付;验收、产物和验证已完成后,必须把全部必要计划步骤更新为 completed,并用一次最终 response 收束为父 Agent 可认领的回执,不得反复给同一 Agent 留消息或重复读取同一证据来维持 run。"
|
||||
);
|
||||
let isolated_template_ids = GAME_CREATOR_AGENT_GROUP_DEFINITIONS
|
||||
.iter()
|
||||
.flat_map(|group| group.roles.iter().map(|role| role.task_id))
|
||||
|
||||
@@ -1608,6 +1608,86 @@ pub(crate) fn append_agent_db_record_if_missing_for_action(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn append_agent_db_agent_message_if_missing(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
run_id: &str,
|
||||
action_id: &str,
|
||||
record: serde_json::Value,
|
||||
) -> Result<bool, String> {
|
||||
const RECORD_TYPE: &str = "agent.runtime.agent.message";
|
||||
let object = record
|
||||
.as_object()
|
||||
.ok_or_else(|| "Agent DB 定向消息审计必须是对象".to_string())?;
|
||||
let expected_fields = [
|
||||
"actionId",
|
||||
"agentId",
|
||||
"contentChars",
|
||||
"contentSha256",
|
||||
"messageId",
|
||||
"path",
|
||||
"recordType",
|
||||
"runId",
|
||||
"targetAgentId",
|
||||
"targetSessionId",
|
||||
];
|
||||
let mut actual_fields = object.keys().map(String::as_str).collect::<Vec<_>>();
|
||||
actual_fields.sort_unstable();
|
||||
if actual_fields != expected_fields
|
||||
|| record.get("recordType").and_then(serde_json::Value::as_str) != Some(RECORD_TYPE)
|
||||
|| record.get("agentId").and_then(serde_json::Value::as_str) != Some(agent_id)
|
||||
|| record.get("runId").and_then(serde_json::Value::as_str) != Some(run_id)
|
||||
|| record.get("actionId").and_then(serde_json::Value::as_str) != Some(action_id)
|
||||
{
|
||||
return Err("Agent DB 定向消息审计身份或字段无效".to_string());
|
||||
}
|
||||
if !action_id.strip_prefix("action-").is_some_and(|suffix| {
|
||||
suffix.len() == 24 && suffix.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
}) {
|
||||
return Err("Agent DB 定向消息审计 actionId 无效".to_string());
|
||||
}
|
||||
let message_id = record
|
||||
.get("messageId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let content_sha256 = record
|
||||
.get("contentSha256")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
let path = record
|
||||
.get("path")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.unwrap_or_default();
|
||||
if !is_valid_agent_db_prefixed_hex(message_id, "agent-message-", 64)
|
||||
|| !is_valid_agent_db_sha256(content_sha256)
|
||||
|| record
|
||||
.get("contentChars")
|
||||
.and_then(serde_json::Value::as_u64)
|
||||
.is_none_or(|count| count == 0 || count > 1_200)
|
||||
|| ["targetAgentId", "targetSessionId"].iter().any(|field| {
|
||||
record
|
||||
.get(*field)
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.is_none_or(|value| value.trim().is_empty() || value.chars().any(char::is_control))
|
||||
})
|
||||
|| Path::new(path).is_absolute()
|
||||
|| !path.starts_with(".agent/conversations/agents/")
|
||||
|| !path.ends_with(".jsonl")
|
||||
|| path.split('/').any(|segment| segment == "..")
|
||||
{
|
||||
return Err("Agent DB 定向消息审计元数据无效".to_string());
|
||||
}
|
||||
append_agent_db_record_if_missing_for_action_internal(
|
||||
root,
|
||||
RECORD_TYPE,
|
||||
agent_id,
|
||||
run_id,
|
||||
action_id,
|
||||
record,
|
||||
|| {},
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn append_agent_db_process_reconciliation_if_missing_for_action(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -4878,7 +4958,7 @@ fn append_local_conversation_message_for_session_internal_at(
|
||||
message: LocalConversationMessage,
|
||||
message_id: Option<&str>,
|
||||
finalization_id: Option<&str>,
|
||||
) -> Result<LocalConversationResult, String> {
|
||||
) -> Result<(LocalConversationResult, bool), String> {
|
||||
let LocalConversationMessage {
|
||||
role,
|
||||
content,
|
||||
@@ -4930,7 +5010,8 @@ fn append_local_conversation_message_for_session_internal_at(
|
||||
if message_id.is_some() {
|
||||
return Err("带 messageId 的对话内容不能为空".to_string());
|
||||
}
|
||||
return read_local_conversation_for_session_at(root, agent_id, session_id);
|
||||
return read_local_conversation_for_session_at(root, agent_id, session_id)
|
||||
.map(|conversation| (conversation, false));
|
||||
}
|
||||
let message_id = message_id
|
||||
.map(normalize_local_conversation_message_id)
|
||||
@@ -5040,11 +5121,14 @@ fn append_local_conversation_message_for_session_internal_at(
|
||||
appended,
|
||||
)?;
|
||||
}
|
||||
Ok(local_conversation_result_from_persisted_records(
|
||||
&path,
|
||||
normalized_agent_id,
|
||||
normalized_session_id,
|
||||
&records,
|
||||
Ok((
|
||||
local_conversation_result_from_persisted_records(
|
||||
&path,
|
||||
normalized_agent_id,
|
||||
normalized_session_id,
|
||||
&records,
|
||||
),
|
||||
appended,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -5057,6 +5141,7 @@ pub(crate) fn append_local_conversation_message_for_session_at(
|
||||
append_local_conversation_message_for_session_internal_at(
|
||||
root, agent_id, session_id, message, None, None,
|
||||
)
|
||||
.map(|(conversation, _)| conversation)
|
||||
}
|
||||
|
||||
pub(crate) fn append_local_conversation_message_for_session_idempotent_at(
|
||||
@@ -5074,6 +5159,24 @@ pub(crate) fn append_local_conversation_message_for_session_idempotent_at(
|
||||
Some(message_id),
|
||||
None,
|
||||
)
|
||||
.map(|(conversation, _)| conversation)
|
||||
}
|
||||
|
||||
pub(crate) fn append_local_conversation_message_for_session_idempotent_with_status_at(
|
||||
root: &Path,
|
||||
agent_id: Option<&str>,
|
||||
session_id: Option<&str>,
|
||||
message: LocalConversationMessage,
|
||||
message_id: &str,
|
||||
) -> Result<(LocalConversationResult, bool), String> {
|
||||
append_local_conversation_message_for_session_internal_at(
|
||||
root,
|
||||
agent_id,
|
||||
session_id,
|
||||
message,
|
||||
Some(message_id),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn append_local_conversation_message_for_session_idempotent_with_finalization_at(
|
||||
@@ -5092,6 +5195,7 @@ pub(crate) fn append_local_conversation_message_for_session_idempotent_with_fina
|
||||
Some(message_id),
|
||||
Some(finalization_id),
|
||||
)
|
||||
.map(|(conversation, _)| conversation)
|
||||
}
|
||||
|
||||
pub(crate) fn append_local_conversation_message_at(
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -662,6 +662,7 @@ V1.15 新增不依赖 Tauri WebView 或正常客户端 GUI 的终端聊天入口
|
||||
- 终端只提供聊天所需的轻量控制命令:`/help`、`/agents`、`/status`、`/history`、`/quit`。空闲时普通文本创建父 Agent 新 run;父 Agent run 仍处于 pending / running 时普通文本追加为同一 run steer,只有 child 忙而父 Agent 已终态时拒绝吞掉输入并要求稍后重发。确认动作在终端显示 Agent、run、action、tool 和安全摘要,并接受 `approve / reject`,分别调用现有 confirm / reject Runtime 路径,不能要求回到开发窗口。stdin 由独立读取线程投递,因此活跃 run 中 `/quit`、EOF、状态命令和 steer 仍可响应;退出只结束观察客户端。
|
||||
- 每轮轮询 `read_game_creator_agent_runtimes_at`,按 Agent / run / event 去重输出状态、phase、委派来源、父 Agent、delegationId、动态 child 和 join / receipt 事件。Provider token delta 当前没有经过 Runner RPC 暴露,首版只承诺 Runtime 状态与事件的持续输出以及持久化后的最终父 Agent 回复,禁止用拆字或延时打印伪装 token streaming。
|
||||
- 一轮只有在所有已发现 Runtime 都不处于 `pending / running / waiting-for-confirmation / cancelling / needs-reconciliation`,全部任务队列为空,并持续经过稳定观察窗口后才能收束。父 run 暂时 idle 但 delegated child 尚未终态、receipt 尚未入队或 all-join 尚未认领时不得提前返回。失败、取消和 reconciliation 要明确显示并保留项目现场,不自动重试副作用。
|
||||
- 每轮进入 `settled` 或 `needs-reconciliation` 终态后,终端必须额外输出且只输出一条 `[turn.report] <单行 JSON>`,schema 固定为 `game-creator-swarm-turn-report.v1`。报告只从本轮 authoritative conversation 与 Runtime snapshot 计算,白名单字段至少包含 outcome、父 Agent/Session/run 身份、Runtime 忙闲数量、四类任务队列计数、新增 assistant 数量、最终回复字符数和 reconciliation Agent 数量;不得包含项目路径、对话/任务/回复正文、observation、prompt、event detail、隐藏 thinking、Provider payload、凭据或本地存储路径。该行用于开发终端和真实 E2E 定位一轮边界,不替代 task/event/delivery/claim/receipt/conversation 等持久事实;`/quit` 不伪造 turn report。
|
||||
- 终端退出只结束观察客户端,不终止 External Runner、已投递 run 或 Runner-owned process session;下次启动先调用现有 resume,再从 conversation 与 Runtime journal 恢复。首版只允许一个前台输入流,不承诺多个终端并发编辑同一 active Session。
|
||||
|
||||
确定性验收必须覆盖 CLI parse、项目绝对路径与 `--config-dir` 门禁、`--init`、空输入和 EOF、命令分流、历史恢复、连续两轮写入同一 Session、状态与事件去重、两个静态 Agent 并行委派、多个隔离 child 并行与唯一 all-join、confirm / reject、父 Agent receipt 汇总、稳定窗口不早退、Runner / 终端重启恢复以及失败与 reconciliation 显示。真实 Provider 验收必须保存一份脱敏 transcript,并以 task / event / Agent DB / receipt / conversation 的结构化事实证明并行、最终父回复唯一、副作用无重放和密钥零泄漏;未实际运行时只能标记未验收,不能凭确定性测试宣称 swarm 可用。
|
||||
@@ -1128,6 +1129,18 @@ Supervisor 认领回执后必须能够再次从 durable delivery 取回权威返
|
||||
|
||||
该受控 suite 是 V1.28 协议与恢复的故障注入门禁,不替代后续自主 Swarm 验收。现有 fixture 明确给出两个专业方向、同轮要求和一次 repair 上限;“Supervisor 在不提供 Agent ID、并行配方或 repair 次数时自主选择编排”仍需独立 `supervisor-swarm-autonomous` 真实 suite 证明。真实 `--swarm-chat`、同一 run 的 static delivery + isolated all-join 组合以及 Tauri/WebView 宿主级 Supervisor GUI 也仍是单独完成项。
|
||||
|
||||
## V1.30 Project Supervisor 自主终端协作验收
|
||||
|
||||
V1.30 新增独立 `supervisor-swarm-autonomous-chat` 真实 Provider suite,同时证明 Project Supervisor 的自主专业编排和正式 `agc:chat / --swarm-chat` 入口。它复用 V1.28 的 static delivery/claim/repair、External Runner、隔离 AppData、确认、恢复、finalization 和唯一回复事实源,不新增 Agent、调度器、Provider 客户端或第二套对话持久化。现有 `supervisor-swarm` 与 `supervisor-swarm-transient-retry` 继续分别承担固定协议链和受控瞬态故障门禁,不能被本 suite 替代。
|
||||
|
||||
- 唯一用户任务只能表达业务结果,例如把试玩项目推进到可交给首批玩家体验并汇报交付、验证和风险;任务不得出现静态 Agent ID、Agent 数量、同轮/并行要求、planning 轮次、返工/repair 次数、原生工具名、run/action/delegation 身份或 Runner 操作。一次性仓库规则只描述玩家体验规格、发布质量记录、语义验收、修改后验证和安全边界;不得指定由哪个 Agent 承担、必须同批委派、必须返工几次或调用什么工具。
|
||||
- fixture 提供一项缺失的体验规格和一项“客观文件/验证存在但语义仍不满足”的质量记录。质量记录在初始阶段必须先独立审阅再允许修改;harness 只通过项目 policy 暂时拒绝质量角色写入,弱回执被父 run 认领后解除 policy,不发送 steer、不改业务文件、不补充新任务。初始弱回执必须是 `completed + evidence-ready` 且无缺失产物,确保后续 repair 来自 Supervisor 对 acceptance criteria 的语义判断,而不是 Runtime 自动把客观失败标成 `needs-repair`。
|
||||
- `--swarm-chat --init <project>` 必须由真实发布二进制启动,省略 parentAgentId 后进入 `project-supervisor`;用户任务通过 stdin 发送,所有确认也经同一终端 `approve` 入口完成。允许 Supervisor 先做必要读取,但首个包含专业委派的 native Provider 批次必须自主选择至少两个不同规范专业 Agent,并在同批形成两个初始合同;两个 child 的真实 Provider lifecycle 必须重叠,不能用同一 Agent 的 retry/format repair 或仅凭 delegate action 时间冒充并行。
|
||||
- 弱质量 claim 进入 `Observed` 后,Supervisor 必须在同一父 Session/run 自主创建引用原 delivery 的唯一 repair;目标 Agent、acceptanceCriteria 和 expectedArtifacts 必须完整继承,repair action 必须晚于弱 claim、早于唯一最终回复。repair 待确认边界继续执行 pidfd Runner 强杀与 boot 恢复,任务、delivery、claim、pending action 和 Provider started 身份不得漂移或重放。
|
||||
- 终局必须同时满足:严格 host oracle 判定两项产物语义正确,最后修改后的验证凭证有效;`[turn.report]` 为 v1/settled、父 Agent/Session/run 与 journal 一致、新增 assistant 恰好 1、队列和 reconciliation 计数为 0;正式用户会话只有 1 条 user 和 1 条 Supervisor assistant,专业 assistant 仅留在内部 Session;无 steer、重复 delivery/action/message/receipt/Provider lifecycle、残留 sidecar、Provider payload、私有正文、API Key、诱饵、项目/正式配置路径或报告泄漏,隔离 Runner/AppData/项目全部清理。任何一次带更明确提示的重跑都只能算新的失败后尝试,不能与原 run 拼接成 PASS。
|
||||
|
||||
该 suite 完成后只能证明自主 static 专业编排与真实终端聊天可组合;同一父 run 的 static delivery + isolated all-join 组合恢复,以及 Tauri/WebView 宿主级 Supervisor E2E 仍需各自独立门禁。
|
||||
|
||||
## 验收命令
|
||||
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`
|
||||
@@ -1157,6 +1170,7 @@ Supervisor 认领回执后必须能够再次从 durable delivery 取回权威返
|
||||
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite parallel-read`
|
||||
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite supervisor-swarm`
|
||||
- `npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e -- --config-dir <AppData>`
|
||||
- `npm run ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-real-e2e -- --config-dir <AppData>`
|
||||
- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir <AppData> --suite full`
|
||||
- `npm run check:encoding`
|
||||
- `git diff --check`
|
||||
|
||||
@@ -146,6 +146,7 @@
|
||||
"ai-game-creator-shell:agent-run": "npm --prefix apps/ai-game-creator-shell run agent-run --",
|
||||
"ai-game-creator-shell:agent-run:smoke": "npm --prefix apps/ai-game-creator-shell run agent-run:smoke",
|
||||
"ai-game-creator-shell:agent-runtime:real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:real-e2e --",
|
||||
"ai-game-creator-shell:agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-autonomous-chat-real-e2e --",
|
||||
"ai-game-creator-shell:agent-runtime:supervisor-swarm-transient-retry-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-swarm-transient-retry-real-e2e --",
|
||||
"ai-game-creator-shell:agent-runtime:steer-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:steer-real-e2e --",
|
||||
"ai-game-creator-shell:agent-runtime:steer-runner-kill-real-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:steer-runner-kill-real-e2e --",
|
||||
|
||||
Reference in New Issue
Block a user