强化项目总控协作合同与崩溃恢复
新增 Supervisor 协作策略控制面和 mixed 首批原子预检。\n升级 Provider action batch v2 并补齐 isolated spawn 幂等恢复与旧协议失败关闭。\n限制委派后总控项目变更、进程 stdin 和破坏性 MCP,同时保留进程治理能力。\n加固委派、隔离 Agent、持久进程和 Provider 日志的持久化与并发边界。\n扩充确定性回归、真实 Provider 验收器、自测和项目文档。
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:collaboration-policy-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-collaboration-policy-mixed-recovery",
|
||||
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat",
|
||||
"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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@@ -714,6 +714,28 @@ pub(crate) fn active_static_delegate_delivery_count_at(
|
||||
.count())
|
||||
}
|
||||
|
||||
pub(crate) fn static_delegate_target_agent_ids_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
parent_run_id: &str,
|
||||
) -> Result<Vec<String>, String> {
|
||||
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
|
||||
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
|
||||
let mut target_agent_ids = list_static_delegate_deliveries_at(root)?
|
||||
.into_iter()
|
||||
.filter(|delivery| {
|
||||
delivery.parent_agent_id == parent_agent_id
|
||||
&& delivery.parent_run_id == parent_run_id
|
||||
&& delivery.repair_of_delegation_id.is_none()
|
||||
&& delivery.status != StaticDelegateDeliveryStatus::Suppressed
|
||||
})
|
||||
.map(|delivery| delivery.target_agent_id)
|
||||
.collect::<Vec<_>>();
|
||||
target_agent_ids.sort();
|
||||
target_agent_ids.dedup();
|
||||
Ok(target_agent_ids)
|
||||
}
|
||||
|
||||
pub(crate) fn validate_static_delegate_repair_request_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
@@ -1169,6 +1191,14 @@ fn validate_static_delegate_delivery_record(
|
||||
validate_static_delegate_id(&record.parent_action_id, "parentActionId", 160)?;
|
||||
validate_static_delegate_id(&record.delegation_id, "delegationId", 160)?;
|
||||
validate_static_delegate_id(&record.target_agent_id, "targetAgentId", 96)?;
|
||||
if record.target_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|
||||
|| record.target_agent_id.starts_with("child-")
|
||||
{
|
||||
return Err(
|
||||
"静态委派 targetAgentId 只能是静态专业 Agent,不能是 Supervisor 或动态 child"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
validate_static_delegate_id(&record.target_session_id, "targetSessionId", 160)?;
|
||||
validate_static_delegate_id(&record.target_run_id, "targetRunId", 160)?;
|
||||
validate_static_delegate_text_list(
|
||||
@@ -1472,6 +1502,114 @@ fn validate_static_delegate_structured_result(
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn static_delegate_target_agent_ids_include_claimed_and_exclude_suppressed_or_repair() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"genarrative-static-targets-{}-{}",
|
||||
std::process::id(),
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.expect("system time after unix epoch")
|
||||
.as_nanos()
|
||||
));
|
||||
init_local_game_project_at(&root, "project-1", "静态委派目标汇总测试")
|
||||
.expect("project init");
|
||||
let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
|
||||
let parent_run_id = "target-summary-parent-run";
|
||||
|
||||
let claimed = new_static_delegate_delivery(
|
||||
parent_agent_id,
|
||||
"target-summary-parent-session",
|
||||
parent_run_id,
|
||||
"target-summary-claimed-action",
|
||||
"target-summary-claimed-delivery",
|
||||
"design-director",
|
||||
"target-summary-claimed-session",
|
||||
"target-summary-claimed-run",
|
||||
);
|
||||
create_or_read_static_delegate_delivery_at(&root, &claimed).expect("create claimed");
|
||||
mark_static_delegate_delivery_ready_at(
|
||||
&root,
|
||||
&claimed.target_agent_id,
|
||||
&claimed.target_session_id,
|
||||
&claimed.target_run_id,
|
||||
&claimed.delegation_id,
|
||||
"completed",
|
||||
"设计交付完成",
|
||||
)
|
||||
.expect("mark claimed ready");
|
||||
claim_ready_static_delegate_receipts_at(
|
||||
&root,
|
||||
parent_agent_id,
|
||||
parent_run_id,
|
||||
"target-summary-claim-action",
|
||||
)
|
||||
.expect("claim delivery");
|
||||
|
||||
for delivery in [
|
||||
new_static_delegate_delivery(
|
||||
parent_agent_id,
|
||||
"target-summary-parent-session",
|
||||
parent_run_id,
|
||||
"target-summary-art-action",
|
||||
"target-summary-art-delivery",
|
||||
"art-director",
|
||||
"target-summary-art-session",
|
||||
"target-summary-art-run",
|
||||
),
|
||||
new_static_delegate_delivery(
|
||||
parent_agent_id,
|
||||
"target-summary-parent-session",
|
||||
parent_run_id,
|
||||
"target-summary-duplicate-action",
|
||||
"target-summary-duplicate-delivery",
|
||||
"design-director",
|
||||
"target-summary-duplicate-session",
|
||||
"target-summary-duplicate-run",
|
||||
),
|
||||
] {
|
||||
create_or_read_static_delegate_delivery_at(&root, &delivery)
|
||||
.expect("create included delivery");
|
||||
}
|
||||
|
||||
let suppressed = new_static_delegate_delivery(
|
||||
parent_agent_id,
|
||||
"target-summary-parent-session",
|
||||
parent_run_id,
|
||||
"target-summary-suppressed-action",
|
||||
"target-summary-suppressed-delivery",
|
||||
"code-prototype",
|
||||
"target-summary-suppressed-session",
|
||||
"target-summary-suppressed-run",
|
||||
);
|
||||
let suppressed = create_or_read_static_delegate_delivery_at(&root, &suppressed)
|
||||
.expect("create suppressed");
|
||||
suppress_static_delegate_delivery_at(&root, &suppressed).expect("suppress delivery");
|
||||
|
||||
let repair = new_static_delegate_delivery_with_contract(
|
||||
parent_agent_id,
|
||||
"target-summary-parent-session",
|
||||
parent_run_id,
|
||||
"target-summary-repair-action",
|
||||
"target-summary-repair-delivery",
|
||||
"repair-only-agent",
|
||||
"target-summary-repair-session",
|
||||
"target-summary-repair-run",
|
||||
&[],
|
||||
&[],
|
||||
Some(&claimed.delegation_id),
|
||||
);
|
||||
create_or_read_static_delegate_delivery_at(&root, &repair).expect("create repair");
|
||||
|
||||
assert_eq!(
|
||||
static_delegate_target_agent_ids_at(&root, parent_agent_id, parent_run_id)
|
||||
.expect("read target agent ids"),
|
||||
vec!["art-director".to_string(), "design-director".to_string()]
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stale_prepared_claim_snapshot_cannot_downgrade_observed_claim() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -83,6 +83,13 @@ pub(crate) struct IsolatedAgentGroupRecord {
|
||||
pub(crate) created_at: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
|
||||
pub(crate) struct IsolatedAgentGroupSummary {
|
||||
pub(crate) group_count: usize,
|
||||
pub(crate) child_count: usize,
|
||||
pub(crate) max_child_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
|
||||
#[serde(deny_unknown_fields, rename_all = "camelCase")]
|
||||
pub(crate) struct IsolatedAgentResultRecord {
|
||||
@@ -321,6 +328,84 @@ pub(crate) fn create_or_read_isolated_group_at(
|
||||
Ok(group)
|
||||
}
|
||||
|
||||
pub(crate) fn isolated_agent_group_summary_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
parent_run_id: &str,
|
||||
) -> Result<IsolatedAgentGroupSummary, String> {
|
||||
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
|
||||
validate_safe_id(parent_run_id, "parentRunId", 160)?;
|
||||
let mut summary = IsolatedAgentGroupSummary::default();
|
||||
for group in list_json_records(
|
||||
root,
|
||||
ISOLATED_AGENT_GROUP_DIR,
|
||||
"动态隔离 Agent group",
|
||||
|record| validate_isolated_group_record(root, record),
|
||||
)?
|
||||
.into_iter()
|
||||
.filter(|group| {
|
||||
group.parent_agent_id == parent_agent_id && group.parent_run_id == parent_run_id
|
||||
}) {
|
||||
summary.group_count = summary.group_count.saturating_add(1);
|
||||
summary.child_count = summary
|
||||
.child_count
|
||||
.saturating_add(group.request.children.len());
|
||||
summary.max_child_count = summary.max_child_count.max(group.request.children.len());
|
||||
}
|
||||
Ok(summary)
|
||||
}
|
||||
|
||||
pub(crate) fn isolated_agent_spawn_has_durable_side_effect_at(
|
||||
root: &Path,
|
||||
parent_agent_id: &str,
|
||||
parent_session_id: &str,
|
||||
parent_run_id: &str,
|
||||
parent_action_id: &str,
|
||||
) -> Result<bool, String> {
|
||||
validate_project_root(root)?;
|
||||
validate_safe_id(parent_agent_id, "parentAgentId", 96)?;
|
||||
validate_safe_id(parent_session_id, "parentSessionId", 160)?;
|
||||
validate_safe_id(parent_run_id, "parentRunId", 160)?;
|
||||
validate_safe_id(parent_action_id, "parentActionId", 256)?;
|
||||
|
||||
let mut found = false;
|
||||
for group in list_json_records(
|
||||
root,
|
||||
ISOLATED_AGENT_GROUP_DIR,
|
||||
"动态隔离 Agent group",
|
||||
|record| validate_isolated_group_record(root, record),
|
||||
)?
|
||||
.into_iter()
|
||||
.filter(|group| group.parent_action_id == parent_action_id)
|
||||
{
|
||||
if group.parent_agent_id != parent_agent_id
|
||||
|| group.parent_session_id != parent_session_id
|
||||
|| group.parent_run_id != parent_run_id
|
||||
{
|
||||
return Err("恢复 isolated spawn 时 durable group 父身份冲突".to_string());
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
for instance in list_json_records(
|
||||
root,
|
||||
ISOLATED_AGENT_INSTANCE_DIR,
|
||||
"动态隔离 Agent instance",
|
||||
|record| validate_isolated_instance_record(root, record),
|
||||
)?
|
||||
.into_iter()
|
||||
.filter(|instance| instance.parent_action_id == parent_action_id)
|
||||
{
|
||||
if instance.parent_agent_id != parent_agent_id
|
||||
|| instance.parent_session_id != parent_session_id
|
||||
|| instance.parent_run_id != parent_run_id
|
||||
{
|
||||
return Err("恢复 isolated spawn 时 durable instance 父身份冲突".to_string());
|
||||
}
|
||||
found = true;
|
||||
}
|
||||
Ok(found)
|
||||
}
|
||||
|
||||
pub(crate) fn list_isolated_agent_instances_at(
|
||||
root: &Path,
|
||||
) -> Result<Vec<IsolatedAgentInstanceRecord>, String> {
|
||||
@@ -1747,6 +1832,24 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn isolated_agent_group_summary_counts_idempotent_group_once() {
|
||||
let temp = tempdir().unwrap();
|
||||
let request = request(vec![("code-a", "game/a/**"), ("code-b", "game/b/**")]);
|
||||
let first = create_group(temp.path(), "action-summary", &request);
|
||||
let second = create_group(temp.path(), "action-summary", &request);
|
||||
assert_eq!(first, second);
|
||||
assert_eq!(
|
||||
isolated_agent_group_summary_at(temp.path(), "code-prototype", "parent-run")
|
||||
.expect("summarize groups"),
|
||||
IsolatedAgentGroupSummary {
|
||||
group_count: 1,
|
||||
child_count: 2,
|
||||
max_child_count: 2,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn completed_child_with_missing_expected_artifact_dispatches_failed_join_result() {
|
||||
let temp = tempdir().unwrap();
|
||||
|
||||
@@ -45,6 +45,7 @@ mod agent_native_tools;
|
||||
mod assets;
|
||||
mod browser;
|
||||
mod cli;
|
||||
mod collaboration;
|
||||
mod command_exec;
|
||||
mod command_output;
|
||||
mod command_sandbox;
|
||||
@@ -76,6 +77,7 @@ use agent_native_tools::*;
|
||||
use assets::*;
|
||||
use browser::*;
|
||||
use cli::*;
|
||||
use collaboration::*;
|
||||
use command_exec::*;
|
||||
use command_output::*;
|
||||
use command_sandbox::*;
|
||||
|
||||
@@ -1180,6 +1180,24 @@ pub(crate) fn game_creator_mcp_tool_effective_approval(
|
||||
Ok(tool.effective_approval_mode.clone())
|
||||
}
|
||||
|
||||
pub(crate) async fn game_creator_mcp_action_is_strictly_read_only_at(
|
||||
root: &Path,
|
||||
action: &AgentRuntimeToolAction,
|
||||
) -> Result<bool, String> {
|
||||
if action.tool.trim() != GAME_CREATOR_MCP_CALL_TOOL {
|
||||
return Err("动作不是 mcp.call".to_string());
|
||||
}
|
||||
let input = parse_game_creator_mcp_call_input(&action.input)?;
|
||||
let catalog = read_game_creator_mcp_catalog_at(root).await?;
|
||||
game_creator_mcp_tool_effective_approval(&catalog, &input)?;
|
||||
let tool = catalog
|
||||
.tools
|
||||
.iter()
|
||||
.find(|tool| tool.server_id == input.server && tool.name == input.tool)
|
||||
.ok_or_else(|| "MCP tool 已从当前 catalog 移除".to_string())?;
|
||||
Ok(tool.read_only_hint && !tool.destructive_hint)
|
||||
}
|
||||
|
||||
pub(crate) async fn game_creator_mcp_action_policy_block_at(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
|
||||
@@ -2036,29 +2036,27 @@ fn append_process_output_line(live: &LiveProcessSession, line: &[u8]) -> bool {
|
||||
}
|
||||
|
||||
fn persist_live_process_snapshot(live: &LiveProcessSession) -> Result<(), String> {
|
||||
let (transcript, record) = {
|
||||
let output = live
|
||||
.output
|
||||
.lock()
|
||||
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
||||
let output_sha256 = format!("{:x}", Sha256::digest(output.text.as_bytes()));
|
||||
let transcript = ProcessSessionTranscript {
|
||||
schema_version: PROCESS_SESSION_TRANSCRIPT_SCHEMA_VERSION.to_string(),
|
||||
project_id: live.identity.project_id.clone(),
|
||||
agent_id: live.identity.agent_id.clone(),
|
||||
task_id: live.identity.task_id.clone(),
|
||||
conversation_session_id: live.identity.conversation_session_id.clone(),
|
||||
run_id: live.identity.run_id.clone(),
|
||||
start_action_id: live.identity.start_action_id.clone(),
|
||||
start_action_fingerprint: live.identity.start_action_fingerprint.clone(),
|
||||
process_id: live.process_id.clone(),
|
||||
output: output.text.clone(),
|
||||
output_sha256,
|
||||
output_bytes: output.text.len(),
|
||||
updated_at: unix_timestamp(),
|
||||
};
|
||||
(transcript, process_session_record_from_live(live, &output))
|
||||
let output = live
|
||||
.output
|
||||
.lock()
|
||||
.map_err(|_| "process session output 锁已损坏".to_string())?;
|
||||
let output_sha256 = format!("{:x}", Sha256::digest(output.text.as_bytes()));
|
||||
let transcript = ProcessSessionTranscript {
|
||||
schema_version: PROCESS_SESSION_TRANSCRIPT_SCHEMA_VERSION.to_string(),
|
||||
project_id: live.identity.project_id.clone(),
|
||||
agent_id: live.identity.agent_id.clone(),
|
||||
task_id: live.identity.task_id.clone(),
|
||||
conversation_session_id: live.identity.conversation_session_id.clone(),
|
||||
run_id: live.identity.run_id.clone(),
|
||||
start_action_id: live.identity.start_action_id.clone(),
|
||||
start_action_fingerprint: live.identity.start_action_fingerprint.clone(),
|
||||
process_id: live.process_id.clone(),
|
||||
output: output.text.clone(),
|
||||
output_sha256,
|
||||
output_bytes: output.text.len(),
|
||||
updated_at: unix_timestamp(),
|
||||
};
|
||||
let record = process_session_record_from_live(live, &output);
|
||||
write_agent_runtime_json_sidecar_with_max_bytes(
|
||||
&live.root,
|
||||
&process_session_transcript_relative_path(&live.process_id),
|
||||
@@ -4050,10 +4048,7 @@ setInterval(() => {}, 1000);
|
||||
let spec = resolve_project_command_spec_at(
|
||||
root,
|
||||
"bash",
|
||||
&[
|
||||
"-lc".to_string(),
|
||||
"printf 'READY\\n'; while :; do sleep 1; done".to_string(),
|
||||
],
|
||||
&["-lc".to_string(), "cat >/dev/null".to_string()],
|
||||
".",
|
||||
30,
|
||||
)
|
||||
|
||||
@@ -6239,8 +6239,11 @@ pub(crate) fn read_local_project_file_at(
|
||||
|
||||
fn is_agent_runtime_private_control_path(normalized_path: &str) -> bool {
|
||||
let mut parts = normalized_path.split('/');
|
||||
matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case(".agent"))
|
||||
&& matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case("runtime"))
|
||||
if !matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case(".agent")) {
|
||||
return false;
|
||||
}
|
||||
matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case("runtime"))
|
||||
|| normalized_path.eq_ignore_ascii_case(SUPERVISOR_COLLABORATION_POLICY_RELATIVE_PATH)
|
||||
}
|
||||
|
||||
fn is_agent_checkpoint_control_path(normalized_path: &str) -> bool {
|
||||
@@ -6249,7 +6252,9 @@ fn is_agent_checkpoint_control_path(normalized_path: &str) -> bool {
|
||||
&& matches!(parts.next(), Some(part) if part.eq_ignore_ascii_case("checkpoints"))
|
||||
}
|
||||
|
||||
fn reject_agent_runtime_private_control_path(normalized_path: &str) -> Result<(), String> {
|
||||
pub(crate) fn reject_agent_runtime_private_control_path(
|
||||
normalized_path: &str,
|
||||
) -> Result<(), String> {
|
||||
if is_agent_runtime_private_control_path(normalized_path) {
|
||||
return Err("Agent Runtime 私有控制面不可通过通用文件工具访问".to_string());
|
||||
}
|
||||
@@ -11293,12 +11298,16 @@ mod manifest_recovery_tests {
|
||||
#[cfg(test)]
|
||||
mod idempotent_conversation_tests {
|
||||
use super::*;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
|
||||
static CONVERSATION_TEST_ROOT_NONCE: AtomicU64 = AtomicU64::new(1);
|
||||
|
||||
fn unique_conversation_test_root() -> PathBuf {
|
||||
std::env::temp_dir().join(format!(
|
||||
"genarrative-conversation-audit-recovery-{}-{}",
|
||||
"genarrative-conversation-audit-recovery-{}-{}-{}",
|
||||
std::process::id(),
|
||||
unix_millis()
|
||||
unix_millis(),
|
||||
CONVERSATION_TEST_ROOT_NONCE.fetch_add(1, Ordering::Relaxed),
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,6 +5,7 @@ import readline from 'node:readline';
|
||||
const args = process.argv.slice(2);
|
||||
const mode = args[0] ?? 'stdio';
|
||||
const failList = args.includes('--fail-list');
|
||||
const includeUnannotated = args.includes('--include-unannotated');
|
||||
const listDelayArgument = args.find((value) =>
|
||||
value.startsWith('--list-delay-ms='),
|
||||
);
|
||||
@@ -88,6 +89,25 @@ const tools = [
|
||||
},
|
||||
];
|
||||
|
||||
if (includeUnannotated) {
|
||||
tools.push({
|
||||
name: 'mutate-unannotated',
|
||||
title: 'Fixture unannotated mutation',
|
||||
description: 'Appends one deterministic line without safety annotations.',
|
||||
inputSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
value: {
|
||||
type: 'string',
|
||||
...(mutateValue ? { const: mutateValue } : {}),
|
||||
},
|
||||
},
|
||||
required: ['value'],
|
||||
additionalProperties: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function resultFor(message) {
|
||||
if (!message || typeof message !== 'object') {
|
||||
return null;
|
||||
@@ -142,7 +162,7 @@ async function resultFor(message) {
|
||||
},
|
||||
};
|
||||
}
|
||||
if (params.name === 'mutate') {
|
||||
if (params.name === 'mutate' || params.name === 'mutate-unannotated') {
|
||||
const value = String(params.arguments?.value ?? '');
|
||||
if (mutateValue && value !== mutateValue) {
|
||||
return {
|
||||
|
||||
@@ -4812,3 +4812,14 @@
|
||||
- 就绪边界:HTTP `/readyz` 同时验证调用池握手和缓存读连接;required subscription 失败必须不就绪。非 HTTP worker / controller 不创建缓存读连接,继续使用 1 条调用连接和各自的队列窄订阅。
|
||||
- 运维口径:`GENARRATIVE_SPACETIME_POOL_SIZE=8` 表示 8 条调用连接,HTTP 基础拓扑另加 1 条缓存读连接;外部生成和充值过期监听的独立窄订阅不计入该值。读模型行缓存从 8 份降为 1 份,但 SDK 空 table metadata、8 条调用 socket 和 runner 仍存在,不承诺总 RSS 等比例降为八分之一。
|
||||
- 验证方式:`cargo test -p spacetime-client --manifest-path server-rs/Cargo.toml --lib`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:encoding`、`git diff --check`;发布后在 8 个调用槽暖机后对比 api-server cgroup memory / PSS,并确认 `/readyz` 与代表性 gallery、公开详情、创作入口和用户标签读取正常。
|
||||
|
||||
## 2026-07-17 Project Supervisor 协作合同由 Runtime 强制执行
|
||||
|
||||
- 背景:V1.31 已真实证明同一父 run 可以组合 static delegate 与 isolated all-join,但模型仍可能漏掉某一类协作、只提交一个 static delegate,或在委派后由 Supervisor 自己执行项目修改。重复采样和继续堆 prompt 不能作为可靠性门禁。
|
||||
- 决策:新增独立项目控制面 `.agent/collaboration-policy.json`,声明首波 `auto / static / isolated / mixed`、最少 static delegate、required static Agent、最少 isolated child 和委派后总控只编排开关。缺失 sidecar 时不强制特定协作拓扑,但默认在当前父 run 形成任何 delivery/group 后禁止 Supervisor 直接修改项目。
|
||||
- 原子性:Supervisor 首波协作复用 Provider action batch 的整批预检;策略不满足或协作批次混入总控项目 mutation 时,任何 pending、确认、delivery、group、child、revision 和项目写入发生前整批返回 blocked observation。通过时 batch v2 固化策略与动作合同指纹,恢复时重新校验策略漂移。
|
||||
- 恢复顺序:batch 成员必须在委派或 spawn 副作用前持久化为 `executing`;恢复、确认和 replay 必须先校验当前策略、完整协作合同、batchId 与 action 身份。策略漂移或旧协作 batch 缺少合同只能进入 `needs-reconciliation`,不得重放 child 副作用。isolated 最低 child 数量按单一 durable group 计算,不能拼接多个不足最低数量的小 group。
|
||||
- 控制面边界:`.agent/collaboration-policy.json` 对 Agent 通用文件工具隐藏并拒绝写入;委派后的 Supervisor 只允许严格只读 MCP,注解不完整或 destructive MCP 失败关闭。`project.git_commit` 与 `canvas.asset_generate` 在取得项目锁后再次读取 durable 协作事实,堵住 dispatch 首检后的并发落盘窗口。
|
||||
- 完成边界:finalization 只认可同一父 run 的 durable static delivery 和 isolated group。repair delegate、合法 isolated 检查、读取、状态查询和项目验证继续允许;源码写入、patch/restore、Git commit、命令启动及平台素材生成由专业 Agent 承担。
|
||||
- 验证方式:运行 `supervisor_collaboration_`、`provider_action_batch_`、`project_supervisor_mixed_` 定向 Rust 回归,随后执行编码检查和 `git diff --check`;真实 Provider V1.32 必须在最终代码 diff 上独立完成,不能复用 V1.31 报告。
|
||||
- 真实验收:2026-07-17 使用 `gpt-5.5 / openai_chat / high` 完成 `supervisor-swarm-collaboration-policy-mixed-recovery` 最终代码独立 PASS。隔离 AppData 副本启用 `maxRetries=2`,正式 AppData 与 Runner endpoint 保持未修改;86 个 Provider lifecycle 全部完成,本轮未触发重试。单一父 Session/run 完成首批 2 个 static delegate + 1 个三 child isolated group、Runner pidfd 强杀恢复、1 次 repair、3 次 delivery 认领、宿主验证和唯一最终回复;重复 delivery/group/instance/result/join/claim/message/action/receipt/lifecycle、残留 sidecar、私密正文、API Key、项目路径与正式配置路径泄漏均为 0。报告同时暴露 49 次 native tool plan 中有 30 次格式修复,作为后续性能与提示合同收敛风险保留。
|
||||
|
||||
@@ -131,6 +131,19 @@ npm run agc:mixed-swarm-e2e -- --config-dir <AppData>
|
||||
|
||||
业务任务只写正式交付、临时检查和验证等结果范围,不写 Agent ID、数量、并行方式、具体工具或 Runner 操作。真实 suite 必须以单个独立 run 证明两类 Provider 真重叠、两类 durable 记录绑定同一父 Session/run、认领 observation 早于唯一父 finalization、Runner 恢复身份稳定、isolated 实际零 mutation、唯一用户回复、零重复/残留/泄漏并完成 sentinel 清理;不能把 isolated 的 suite 零写入要求解释成生产权限层面的只读沙箱。命令未运行、退出非零或报告字段不完整时不得标记 PASS,也不得把失败轮和后续成功轮拼接。
|
||||
|
||||
### AI 游戏创作 Supervisor 协作策略复验
|
||||
|
||||
修改 `.agent/collaboration-policy.json`、Supervisor 首波预检、Provider action batch v2、委派后总控 mutation/MCP 门禁、协作 finalization blocker 或对应恢复顺序后,先跑确定性回归,再运行独立真实 suite:
|
||||
|
||||
```bash
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml supervisor_collaboration_ -- --nocapture --test-threads=1
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml provider_action_batch_ -- --nocapture --test-threads=1
|
||||
cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_mixed_ -- --nocapture --test-threads=1
|
||||
npm run agc:collaboration-policy-e2e -- --config-dir <AppData>
|
||||
```
|
||||
|
||||
真实 suite 必须在隔离 AppData 和单个父 Session/run 中写入 mixed 策略,要求两个指定 static Agent 与同一 group 的三个 isolated child。首批 batch 必须先停在 `waiting-confirmation / nextActionIndex=0` 且副作用为 0;此时强杀 Runner,恢复后 batchId、policy/contract fingerprint 和全部 actionId 必须逐项稳定,再继续完成 V1.31 mixed chain。为避免长链路被偶发外部 transport 抖动误判,suite 只在隔离配置副本中启用有限瞬态重试,必须同时证明正式 AppData、源配置和 Runner endpoint 未被改动,并在报告中保留 retry 计数。最终仍要求两类真实 Provider 重叠、唯一 Supervisor assistant、零重复/残留/泄漏和 sentinel 清理;命令未运行或报告门禁不完整时不能把确定性测试或 V1.31 PASS 当成 V1.32 PASS。
|
||||
|
||||
### AI 游戏创作 Runtime V1.10 持久进程定向复验
|
||||
|
||||
V1.10 的 PTY 只通过四个 Runner-owned 工具开放;不要把 V1.2 `command.exec` 改成长驻入口。最小工具输入保持结构化:
|
||||
|
||||
@@ -1163,6 +1163,22 @@ V1.31 新增独立 `supervisor-swarm-static-isolated-autonomous-chat` 真实 Pro
|
||||
|
||||
Runner pidfd 强杀后的 boot、父 context、pending action、两类 durable identity 和完整 Provider identity set 均稳定恢复;isolated mutation action、isolated 文件修改、continuation、重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle、残留 sidecar和公共正文/凭据/绝对路径/报告泄漏均为 0。`turn.report=settled`,父计划 4/4 completed,正式 Supervisor assistant 恰好 1,内部专业 assistant 3、isolated assistant 3,最终 disposable 项目与隔离 AppData 均自动清理。此前不完整编排、child 合同不满足、外部 Provider 终态失败和调试验收器误判均各自作为独立失败轮停止,未与本轮 PASS 拼接。
|
||||
|
||||
## V1.32 Runtime 强制 Supervisor 协作合同
|
||||
|
||||
V1.31 证明真实 Provider 可以自主形成 static + isolated 混合协作,但首波是否完整仍主要依赖 Supervisor prompt。V1.32 把该要求收进 Runtime:项目可在 `.agent/collaboration-policy.json` 声明协作策略,缺失时使用 `requiredInitialWave=auto`、`minStaticDelegates=0`、`requiredStaticAgentIds=[]`、`minIsolatedChildren=0`、`orchestratorOnlyAfterDelegation=true`。该 sidecar 是独立于 `.agent/policy.json` 的项目私有控制面,不扩展 133 处权限策略结构体字面量;通用 `file.list/read/write/patch/delete` 与 patchset 底层路径统一隐藏或拒绝该文件,并在项目锁、verification gate 和 revision 变化前失败,只有宿主配置入口可以原子写入。
|
||||
|
||||
- `requiredInitialWave` 只接受 `auto / static / isolated / mixed`;static 与 isolated 模式分别至少要求一项对应协作,mixed 同时要求两类。`minStaticDelegates`、`requiredStaticAgentIds` 和 `minIsolatedChildren` 可进一步收紧,三项必须在单个最多 3 action 的 native Provider 批次内可满足。required Agent ID 只匹配非 repair 的 initial `agent.delegate`,拒绝 `project-supervisor` 与 `child-*` 冒充静态专业 Agent;isolated 最低数量必须由同一个 `agent.spawn_isolated(joinMode=all)` group 的 children 满足,不能把多个不足最低数量的小 group 相加,单个 Provider 批次也最多包含一个 spawn。
|
||||
- 首波要求尚未满足时,普通读取、`project.verify`、`agent.run_status` 和计划更新仍可进行;一旦 Supervisor 请求项目 mutation 或开始任何一类协作,Runtime 必须整批预检。缺少 required mode、数量或 Agent 时整个计划形成 `runtime.collaboration_policy:blocked` observation,不创建 pending action、provider batch、static delivery、isolated group/child,不推进 project revision,也不执行批次中其它动作。
|
||||
- 通过预检的 Supervisor 协作批次升级为 Provider action batch v2,并持久化完整策略快照、实际 initial static Agent、isolated child 数量和合同 SHA-256;batchId 同时绑定合同。即使只有一个 delegate/spawn,也强制进入 durable batch。每个成员必须先把 pending action 和 batch 成员共同持久化为 `executing`,随后才能创建 delivery/group 等副作用,成功 observation 落盘后才推进 cursor。恢复、确认和每次 batch 读取都必须先重新校验项目策略、合同和 batch 身份,再考虑 pending action 恢复或 replay;`agent.spawn_isolated` 的 v2 恢复若已存在绑定同一 action 和合同的持久 group/instance,必须复用它们并只补全缺失投影,不得重复创建 group/instance 或重复记录 spawn 审计。策略漂移、合同/动作不一致或旧 v1 协作批次统一进入 `needs-reconciliation`,不能按旧策略启动 child,也不能让已产生的 durable delivery 反过来使当前 batch 自我拒绝。
|
||||
- `orchestratorOnlyAfterDelegation=true` 时,只要同一父 run 已有非 suppressed static delivery 或 isolated group,或者当前原子批次正在创建协作,Supervisor 都不得执行 `file.write / file.patch / file.delete / project.patchset / project.restore / project.git_commit / command.exec / command.start / command.stdin / canvas.asset_generate`。门禁在批次预检、真实工具 dispatch 前以及 `project.git_commit` / `canvas.asset_generate` 取得项目锁后再次复核;阻断不得创建 confirmation、pending action、revision 或项目副作用。MCP 只有同时声明 `readOnlyHint=true` 与 `destructiveHint=false` 才视为只读,破坏性或注解不完整的 MCP 在首批原子预检和委派后执行阶段都失败关闭。
|
||||
- 总控只编排门禁继续允许读取、checkpoint/diff、`project.verify`、受限静态验证、进程观察与收束(`command.poll / command.terminate`)、任务编排、黑板/消息、`agent.run_status`、新的合法 isolated 检查,以及继承原合同的唯一 repair `agent.delegate`。专业 Agent 与 isolated child 的既有工具权限不因本条改变。
|
||||
- finalization 在两类 delivery/join blocker 之外追加协作策略完成门禁。配置要求的 initial static/isolated 事实缺失、required static Agent 不完整或策略 sidecar 损坏时,原 Supervisor run 必须回到 planning,不能提交最终回复。完成事实只来自当前父 run 的 durable delivery/group,不接受 prompt 声明、计划文字或 Agent DB 诊断投影代替。
|
||||
- 确定性验收至少覆盖:不完整 mixed 首波零 child/零 revision;完整 mixed 首波形成唯一 v2 合同;`executing` 先于首个 durable delivery 且 cursor 正确推进;恢复先验合同并在策略漂移时零 replay;委派后 mutation 在 confirmation 前被拒绝;Git commit 与画板生成在项目锁内再次阻断;破坏性 MCP、策略文件写/patch 和 revision 推进均为 0;repair delegate、isolated spawn、`project.verify` 和读取仍允许;batch round-trip/Runner 恢复保持 batchId、policy/contract fingerprint 和 actionId,重复 child/delivery/group 为 0。真实 Provider 另建 V1.32 suite,不能把 V1.31 的一次成功样本外推为 Runtime 合同已验收。
|
||||
|
||||
2026-07-17 在最终代码 diff 上使用 `gpt-5.5 / openai_chat / high` 完成 V1.32 独立真实 PASS。`supervisor-swarm-collaboration-policy-mixed-recovery` 只在隔离 AppData 配置副本中把瞬态重试设为 `maxRetries=2 / retryBackoffMs=500`,正式 AppData、源配置和 Runner endpoint 均未修改;86 个 Provider lifecycle 全部完成,本轮未触发重试。首批 v2 batch 固化 3 个 action,包含 2 个指定 static delegate 与 1 个三 child isolated spawn;waiting-confirmation 零副作用边界、pidfd 强杀恢复、batch/contract/action identity、两类 Provider 重叠、1 次 repair、3 个 delivery、1 个 isolated group/3 个 instance/3 个 result/1 个 claimed join、宿主验证和唯一 Supervisor assistant 全部通过。
|
||||
|
||||
成功报告共记录 178 个 task snapshot、326 个 event、556 个 Agent DB record、30 个 action execution 和 38 个 receipt;重复 delivery/group/instance/result/join/claim/message/action/receipt/Provider lifecycle 与 pending/batch/finalization/confirmation sidecar 均为 0,私密正文、Provider payload、API Key、项目路径、正式配置路径和最终报告泄漏均为 0。`turn.report=settled` 且 reconciliation Agent 为 0。49 次 native tool plan 中发生 30 次格式修复,未破坏动作幂等与最终结果,但说明真实链路仍有明显延迟和 Provider 调用成本,后续应单独收敛工具合同表达和 repair 频率。
|
||||
|
||||
## 验收命令
|
||||
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`
|
||||
@@ -1173,6 +1189,8 @@ Runner pidfd 强杀后的 boot、父 context、pending action、两类 durable i
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml provider_action_batch_ -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml parallel_read_batch_ -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml project_supervisor_mixed_ -- --nocapture --test-threads=1`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml supervisor_collaboration_ -- --nocapture --test-threads=1`
|
||||
- `npm run agc:collaboration-policy-e2e -- --config-dir <AppData>`
|
||||
- `npm run agc:mixed-swarm-e2e -- --config-dir <AppData>`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml swarm_cli::tests -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml typed_goal_pause_and_cancel_require_durable_intent_and_keep_exact_run -- --nocapture`
|
||||
|
||||
@@ -592,3 +592,5 @@ game-project/
|
||||
- 2026-07-17 Runtime V1.31 的同父 run 混合协作门禁已完成独立真实 PASS;详细业务任务边界、首批 confirmation gate、static/isolated durable 合同、三条确定性回归、真实报告数字和失败轮隔离记录统一以同一 Runtime 文档的“V1.31 Project Supervisor 静态与隔离子 Agent 混合协作门禁”为事实源。App 侧复验入口为 `npm run agc:mixed-swarm-e2e -- --config-dir <AppData>`,不在实施计划重复维护一次性拓扑和计数。
|
||||
- `agent.message` 使用来源 Agent/run、目标 Agent/Session 和清洗后正文 SHA-256 形成稳定语义身份。同一语义消息只允许写 1 条目标 tool conversation、1 条 `conversation.message` 和 1 条 `agent.runtime.agent.message`;后续 Runtime action 仍完整落账,但返回 `messageAppended=false` 且不算新的 loop 进展。专业 Agent 不得用重复消息替代最终回执;持续重复时最多经过当前 6 轮停滞窗口即以 `loop-budget-exhausted` 失败,保留 `in_progress` 计划且不写 completed。完整后台回归同时断言 6 个 actionId、同一 action fingerprint、6 组 action/observation/receipt、消息持久化唯一、receipt 零正文、第 7 次 Provider 请求为 0。
|
||||
- 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。
|
||||
- 2026-07-17 起,同一 Runtime 文档的“V1.32 Runtime 强制 Supervisor 协作合同”作为 mixed swarm 可靠性事实源。项目可用 `.agent/collaboration-policy.json` 约束首波 static/isolated 模式、数量和 required static Agent;Runtime 在任何 child 副作用前整批校验并把合同指纹固化进 Provider batch v2。当前父 run 一旦形成 delivery/group,正式 `project-supervisor` 默认只负责编排、状态认领和验证,不再直接执行项目 mutation;专业 Agent/isolated child 权限与唯一 Supervisor 最终回复边界保持不变。
|
||||
- 2026-07-17 V1.32 最终代码已完成独立真实 Provider PASS:首批 mixed batch、三 isolated child、Runner 强杀恢复、专业返工、宿主验证、唯一最终回复与零重复/残留/泄漏同时成立。真实报告计数、隔离重试配置和仍待收敛的 tool-plan repair 成本统一以 Runtime 文档 V1.32 章节与共享决策记录为准。
|
||||
|
||||
@@ -143,6 +143,7 @@
|
||||
"ai-game-creator-shell:agent-task": "npm --prefix apps/ai-game-creator-shell run agent-task --",
|
||||
"agc:chat": "npm --prefix apps/ai-game-creator-shell run chat --",
|
||||
"agc:swarm": "npm --prefix apps/ai-game-creator-shell run swarm --",
|
||||
"agc:collaboration-policy-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:collaboration-policy-real-e2e --",
|
||||
"agc:mixed-swarm-e2e": "npm --prefix apps/ai-game-creator-shell run agent-runtime:mixed-swarm-real-e2e --",
|
||||
"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",
|
||||
|
||||
@@ -46,6 +46,8 @@ pub struct LlmConfig {
|
||||
max_retries: u32,
|
||||
retry_backoff_ms: u64,
|
||||
official_fallback: bool,
|
||||
#[cfg(test)]
|
||||
raw_log_dir_override: Option<PathBuf>,
|
||||
}
|
||||
|
||||
// 首版只冻结当前项目已稳定使用的 system/user/assistant 三种消息角色。
|
||||
@@ -617,6 +619,8 @@ impl LlmConfig {
|
||||
max_retries,
|
||||
retry_backoff_ms,
|
||||
official_fallback: false,
|
||||
#[cfg(test)]
|
||||
raw_log_dir_override: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -625,6 +629,12 @@ impl LlmConfig {
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn with_raw_log_dir_override(mut self, raw_log_dir: PathBuf) -> Self {
|
||||
self.raw_log_dir_override = Some(raw_log_dir);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn ark_default(api_key: String, model: String) -> Result<Self, LlmError> {
|
||||
Self::new(
|
||||
LlmProvider::Ark,
|
||||
@@ -1919,9 +1929,7 @@ fn write_llm_raw_failure(
|
||||
failure_stage: &str,
|
||||
raw_output: &str,
|
||||
) -> Result<(), String> {
|
||||
let log_dir = env::var("LLM_RAW_LOG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LLM_RAW_LOG_DIR));
|
||||
let log_dir = resolve_llm_raw_log_dir(config);
|
||||
fs::create_dir_all(&log_dir).map_err(|error| format!("创建日志目录失败:{error}"))?;
|
||||
|
||||
let prefix = build_llm_raw_log_prefix(failure_stage);
|
||||
@@ -1938,6 +1946,17 @@ fn write_llm_raw_failure(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_llm_raw_log_dir(_config: &LlmConfig) -> PathBuf {
|
||||
#[cfg(test)]
|
||||
if let Some(raw_log_dir) = &_config.raw_log_dir_override {
|
||||
return raw_log_dir.clone();
|
||||
}
|
||||
|
||||
env::var("LLM_RAW_LOG_DIR")
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|_| PathBuf::from(DEFAULT_LLM_RAW_LOG_DIR))
|
||||
}
|
||||
|
||||
fn build_llm_raw_failure_input_log(
|
||||
config: &LlmConfig,
|
||||
request: &LlmRunRequest,
|
||||
@@ -3637,9 +3656,6 @@ mod tests {
|
||||
"platform-llm-raw-log-test-{}",
|
||||
build_llm_raw_log_prefix("parse_error")
|
||||
));
|
||||
unsafe {
|
||||
std::env::set_var("LLM_RAW_LOG_DIR", &log_dir);
|
||||
}
|
||||
|
||||
let server_url = spawn_mock_server(vec![MockResponse {
|
||||
status_line: "200 OK",
|
||||
@@ -3648,7 +3664,18 @@ mod tests {
|
||||
extra_headers: Vec::new(),
|
||||
}]);
|
||||
|
||||
let client = build_test_client(server_url, 0);
|
||||
let config = LlmConfig::new(
|
||||
LlmProvider::Ark,
|
||||
server_url,
|
||||
"test-key".to_string(),
|
||||
"test-model".to_string(),
|
||||
DEFAULT_REQUEST_TIMEOUT_MS,
|
||||
0,
|
||||
1,
|
||||
)
|
||||
.expect("config should be valid")
|
||||
.with_raw_log_dir_override(log_dir.clone());
|
||||
let client = LlmClient::new(config).expect("client should be created");
|
||||
let error = client
|
||||
.run(LlmRunRequest::single_turn("系统原文", "用户原文").with_openai_chat())
|
||||
.await
|
||||
@@ -3681,9 +3708,6 @@ mod tests {
|
||||
assert!(!input_text.contains("test-key"));
|
||||
assert_eq!(output_text, "不是合法 JSON");
|
||||
|
||||
unsafe {
|
||||
std::env::remove_var("LLM_RAW_LOG_DIR");
|
||||
}
|
||||
fs::remove_dir_all(log_dir).expect("log dir should be removed");
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user