Merge branch 'codex/ai-game-creator-app' into ai-game-creator-app-home-sidebar
# Conflicts: # docs/project-memory/shared-memory/decision-log.md
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
@@ -1,6 +1,8 @@
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
use std::fmt;
|
||||
|
||||
use platform_llm::{LlmFunctionTool, LlmToolCall};
|
||||
use serde::de::{DeserializeOwned, Error as _, MapAccess, SeqAccess, Visitor};
|
||||
use serde::Deserialize;
|
||||
use serde_json::{json, Value};
|
||||
use sha2::{Digest, Sha256};
|
||||
@@ -17,6 +19,185 @@ pub(crate) const AGENT_RUNTIME_RESPOND_FUNCTION_NAME: &str = "respond_to_user";
|
||||
const AGENT_RUNTIME_NATIVE_TOOL_PREFIX: &str = "runtime_tool_";
|
||||
const AGENT_RUNTIME_NATIVE_MCP_PREFIX: &str = "mcp_tool_";
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub(crate) enum AgentRuntimeToolPlanProtocolErrorKind {
|
||||
ResponseShape,
|
||||
CallIdentity,
|
||||
UnknownFunction,
|
||||
ArgumentsJson,
|
||||
ArgumentsSchema,
|
||||
BatchConstraint,
|
||||
PlanSemantics,
|
||||
CatalogBinding,
|
||||
}
|
||||
|
||||
impl AgentRuntimeToolPlanProtocolErrorKind {
|
||||
pub(crate) fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::ResponseShape => "response-shape",
|
||||
Self::CallIdentity => "call-identity",
|
||||
Self::UnknownFunction => "unknown-function",
|
||||
Self::ArgumentsJson => "arguments-json",
|
||||
Self::ArgumentsSchema => "arguments-schema",
|
||||
Self::BatchConstraint => "batch-constraint",
|
||||
Self::PlanSemantics => "plan-semantics",
|
||||
Self::CatalogBinding => "catalog-binding",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct AgentRuntimeToolPlanProtocolError {
|
||||
kind: AgentRuntimeToolPlanProtocolErrorKind,
|
||||
detail: String,
|
||||
}
|
||||
|
||||
impl AgentRuntimeToolPlanProtocolError {
|
||||
pub(crate) fn new(
|
||||
kind: AgentRuntimeToolPlanProtocolErrorKind,
|
||||
detail: impl Into<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
kind,
|
||||
detail: detail.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn kind(&self) -> AgentRuntimeToolPlanProtocolErrorKind {
|
||||
self.kind
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for AgentRuntimeToolPlanProtocolError {
|
||||
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str(&self.detail)
|
||||
}
|
||||
}
|
||||
|
||||
fn protocol_error(
|
||||
kind: AgentRuntimeToolPlanProtocolErrorKind,
|
||||
detail: impl Into<String>,
|
||||
) -> AgentRuntimeToolPlanProtocolError {
|
||||
AgentRuntimeToolPlanProtocolError::new(kind, detail)
|
||||
}
|
||||
|
||||
struct DuplicateSafeJson;
|
||||
|
||||
impl<'de> Deserialize<'de> for DuplicateSafeJson {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
deserializer.deserialize_any(DuplicateSafeJsonVisitor)
|
||||
}
|
||||
}
|
||||
|
||||
struct DuplicateSafeJsonVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for DuplicateSafeJsonVisitor {
|
||||
type Value = DuplicateSafeJson;
|
||||
|
||||
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
formatter.write_str("不包含重复 object key 的 JSON value")
|
||||
}
|
||||
|
||||
fn visit_bool<E>(self, _value: bool) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_i64<E>(self, _value: i64) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_u64<E>(self, _value: u64) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_f64<E>(self, _value: f64) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_str<E>(self, _value: &str) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_string<E>(self, _value: String) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_none<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_unit<E>(self) -> Result<Self::Value, E> {
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
DuplicateSafeJson::deserialize(deserializer)
|
||||
}
|
||||
|
||||
fn visit_seq<A>(self, mut sequence: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: SeqAccess<'de>,
|
||||
{
|
||||
while sequence.next_element::<DuplicateSafeJson>()?.is_some() {}
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
|
||||
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
|
||||
where
|
||||
A: MapAccess<'de>,
|
||||
{
|
||||
let mut keys = HashSet::new();
|
||||
while let Some(key) = map.next_key::<String>()? {
|
||||
if !keys.insert(key.clone()) {
|
||||
return Err(A::Error::custom(format!("重复 JSON object key:{key}")));
|
||||
}
|
||||
map.next_value::<DuplicateSafeJson>()?;
|
||||
}
|
||||
Ok(DuplicateSafeJson)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn validate_agent_runtime_protocol_json(
|
||||
json: &str,
|
||||
description: &str,
|
||||
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
|
||||
let mut deserializer = serde_json::Deserializer::from_str(json);
|
||||
DuplicateSafeJson::deserialize(&mut deserializer)
|
||||
.and_then(|_| deserializer.end())
|
||||
.map_err(|error| {
|
||||
let kind = match error.classify() {
|
||||
serde_json::error::Category::Data => {
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema
|
||||
}
|
||||
serde_json::error::Category::Io
|
||||
| serde_json::error::Category::Syntax
|
||||
| serde_json::error::Category::Eof => {
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsJson
|
||||
}
|
||||
};
|
||||
protocol_error(kind, format!("{description}:{error}"))
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_native_arguments<T: DeserializeOwned>(
|
||||
arguments: &str,
|
||||
description: &str,
|
||||
) -> Result<T, AgentRuntimeToolPlanProtocolError> {
|
||||
validate_agent_runtime_protocol_json(arguments, description)?;
|
||||
serde_json::from_str::<T>(arguments).map_err(|error| {
|
||||
protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("{description} schema 无效:{error}"),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub(crate) struct NativeAgentRuntimeToolPlan {
|
||||
pub(crate) plan: AgentRuntimeToolPlan,
|
||||
@@ -108,9 +289,12 @@ pub(crate) fn build_agent_runtime_native_function_tools(
|
||||
pub(crate) fn parse_agent_runtime_native_tool_calls(
|
||||
calls: &[LlmToolCall],
|
||||
mcp_catalog: &GameCreatorMcpCatalog,
|
||||
) -> Result<NativeAgentRuntimeToolPlan, String> {
|
||||
) -> Result<NativeAgentRuntimeToolPlan, AgentRuntimeToolPlanProtocolError> {
|
||||
if calls.is_empty() {
|
||||
return Err("Agent 原生工具协议错误:function calls 不能为空".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ResponseShape,
|
||||
"Agent 原生工具协议错误:function calls 不能为空",
|
||||
));
|
||||
}
|
||||
let mut seen_call_ids = HashSet::new();
|
||||
let mut plan_update = None;
|
||||
@@ -122,29 +306,40 @@ pub(crate) fn parse_agent_runtime_native_tool_calls(
|
||||
for call in calls {
|
||||
let call_id = call.id.trim();
|
||||
if call_id.is_empty() || !seen_call_ids.insert(call_id.to_string()) {
|
||||
return Err("Agent 原生工具协议错误:call id 必须非空且唯一".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::CallIdentity,
|
||||
"Agent 原生工具协议错误:call id 必须非空且唯一",
|
||||
));
|
||||
}
|
||||
call_ids.push(call_id.to_string());
|
||||
function_names.push(call.name.clone());
|
||||
|
||||
if call.name == AGENT_RUNTIME_UPDATE_PLAN_FUNCTION_NAME {
|
||||
if plan_update.is_some() {
|
||||
return Err("Agent 原生工具协议错误:一次响应只能更新一次计划".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
|
||||
"Agent 原生工具协议错误:一次响应只能更新一次计划",
|
||||
));
|
||||
}
|
||||
plan_update = Some(
|
||||
serde_json::from_str::<AgentRuntimePlanUpdate>(&call.arguments)
|
||||
.map_err(|error| format!("解析原生计划更新失败:{error}"))?,
|
||||
);
|
||||
plan_update = Some(parse_native_arguments::<AgentRuntimePlanUpdate>(
|
||||
&call.arguments,
|
||||
"解析原生计划更新失败",
|
||||
)?);
|
||||
continue;
|
||||
}
|
||||
if call.name == AGENT_RUNTIME_RESPOND_FUNCTION_NAME {
|
||||
if response.is_some() {
|
||||
return Err("Agent 原生工具协议错误:一次响应只能提交一个最终回复".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
|
||||
"Agent 原生工具协议错误:一次响应只能提交一个最终回复",
|
||||
));
|
||||
}
|
||||
response = Some(
|
||||
serde_json::from_str::<NativeResponseArguments>(&call.arguments)
|
||||
.map_err(|error| format!("解析原生最终回复失败:{error}"))?
|
||||
.response,
|
||||
parse_native_arguments::<NativeResponseArguments>(
|
||||
&call.arguments,
|
||||
"解析原生最终回复失败",
|
||||
)?
|
||||
.response,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -152,14 +347,19 @@ pub(crate) fn parse_agent_runtime_native_tool_calls(
|
||||
let runtime_tool = runtime_tool_for_native_function(&call.name);
|
||||
let mcp_tool = mcp_tool_for_native_function(&call.name, mcp_catalog)?;
|
||||
if runtime_tool.is_none() && mcp_tool.is_none() {
|
||||
return Err(format!("Agent 原生工具协议错误:未知函数 {}", call.name));
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::UnknownFunction,
|
||||
format!("Agent 原生工具协议错误:未知函数 {}", call.name),
|
||||
));
|
||||
}
|
||||
let arguments = serde_json::from_str::<NativeActionArguments>(&call.arguments)
|
||||
.map_err(|error| format!("解析原生工具 {} 参数失败:{error}", call.name))?;
|
||||
let arguments = parse_native_arguments::<NativeActionArguments>(
|
||||
&call.arguments,
|
||||
&format!("解析原生工具 {} 参数失败", call.name),
|
||||
)?;
|
||||
if arguments.reason.trim().is_empty() {
|
||||
return Err(format!(
|
||||
"Agent 原生工具协议错误:{} reason 不能为空",
|
||||
call.name
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生工具协议错误:{} reason 不能为空", call.name),
|
||||
));
|
||||
}
|
||||
if runtime_tool.as_deref() == Some("agent.delegate") {
|
||||
@@ -173,9 +373,9 @@ pub(crate) fn parse_agent_runtime_native_tool_calls(
|
||||
}
|
||||
} else if let Some(tool) = mcp_tool {
|
||||
if !arguments.input.is_object() {
|
||||
return Err(format!(
|
||||
"Agent 原生 MCP 工具 {} input 必须是 object",
|
||||
call.name
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生 MCP 工具 {} input 必须是 object", call.name),
|
||||
));
|
||||
}
|
||||
AgentRuntimeToolAction {
|
||||
@@ -192,20 +392,29 @@ pub(crate) fn parse_agent_runtime_native_tool_calls(
|
||||
};
|
||||
actions.push(action);
|
||||
if actions.len() > AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT {
|
||||
return Err(format!(
|
||||
"Agent 原生工具协议错误:一次最多调用 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个动作工具"
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
|
||||
format!(
|
||||
"Agent 原生工具协议错误:一次最多调用 {AGENT_RUNTIME_BACKGROUND_TOOL_ACTION_LIMIT} 个动作工具"
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if response.is_some() && !actions.is_empty() {
|
||||
return Err("Agent 原生工具协议错误:最终回复不能与动作工具同时提交".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::BatchConstraint,
|
||||
"Agent 原生工具协议错误:最终回复不能与动作工具同时提交",
|
||||
));
|
||||
}
|
||||
if response
|
||||
.as_deref()
|
||||
.is_some_and(|value| value.trim().is_empty())
|
||||
{
|
||||
return Err("Agent 原生工具协议错误:最终回复不能为空".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::PlanSemantics,
|
||||
"Agent 原生工具协议错误:最终回复不能为空",
|
||||
));
|
||||
}
|
||||
let response = response.unwrap_or_default();
|
||||
let thinking_summary = plan_update
|
||||
@@ -227,7 +436,9 @@ pub(crate) fn parse_agent_runtime_native_tool_calls(
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_native_agent_delegate_input(input: &Value) -> Result<(), String> {
|
||||
fn validate_native_agent_delegate_input(
|
||||
input: &Value,
|
||||
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
|
||||
const REQUIRED_FIELDS: [&str; 6] = [
|
||||
"agentId",
|
||||
"task",
|
||||
@@ -236,13 +447,17 @@ fn validate_native_agent_delegate_input(input: &Value) -> Result<(), String> {
|
||||
"repairOfDelegationId",
|
||||
"runId",
|
||||
];
|
||||
let object = input
|
||||
.as_object()
|
||||
.ok_or_else(|| "Agent 原生工具协议错误:agent.delegate input 必须是 object".to_string())?;
|
||||
let object = input.as_object().ok_or_else(|| {
|
||||
protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
"Agent 原生工具协议错误:agent.delegate input 必须是 object",
|
||||
)
|
||||
})?;
|
||||
for field in REQUIRED_FIELDS {
|
||||
if !object.contains_key(field) {
|
||||
return Err(format!(
|
||||
"Agent 原生工具协议错误:agent.delegate 缺少 {field}"
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生工具协议错误:agent.delegate 缺少 {field}"),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -250,7 +465,10 @@ fn validate_native_agent_delegate_input(input: &Value) -> Result<(), String> {
|
||||
.keys()
|
||||
.any(|field| !REQUIRED_FIELDS.contains(&field.as_str()))
|
||||
{
|
||||
return Err("Agent 原生工具协议错误:agent.delegate 包含未知字段".to_string());
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
"Agent 原生工具协议错误:agent.delegate 包含未知字段",
|
||||
));
|
||||
}
|
||||
validate_native_delegate_string(object.get("agentId"), "agentId", 96, false)?;
|
||||
validate_native_delegate_string(object.get("task"), "task", 2_400, false)?;
|
||||
@@ -282,17 +500,21 @@ fn validate_native_delegate_string(
|
||||
field: &str,
|
||||
max_chars: usize,
|
||||
nullable: bool,
|
||||
) -> Result<(), String> {
|
||||
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
|
||||
if nullable && value.is_some_and(Value::is_null) {
|
||||
return Ok(());
|
||||
}
|
||||
let value = value
|
||||
.and_then(Value::as_str)
|
||||
.ok_or_else(|| format!("Agent 原生工具协议错误:agent.delegate {field} 类型无效"))?;
|
||||
let value = value.and_then(Value::as_str).ok_or_else(|| {
|
||||
protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生工具协议错误:agent.delegate {field} 类型无效"),
|
||||
)
|
||||
})?;
|
||||
let chars = value.chars().count();
|
||||
if value.trim().is_empty() || chars > max_chars {
|
||||
return Err(format!(
|
||||
"Agent 原生工具协议错误:agent.delegate {field} 长度无效"
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生工具协议错误:agent.delegate {field} 长度无效"),
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
@@ -304,13 +526,17 @@ fn validate_native_delegate_string_list(
|
||||
min_items: usize,
|
||||
max_items: usize,
|
||||
max_chars: usize,
|
||||
) -> Result<(), String> {
|
||||
let values = value
|
||||
.and_then(Value::as_array)
|
||||
.ok_or_else(|| format!("Agent 原生工具协议错误:agent.delegate {field} 类型无效"))?;
|
||||
) -> Result<(), AgentRuntimeToolPlanProtocolError> {
|
||||
let values = value.and_then(Value::as_array).ok_or_else(|| {
|
||||
protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生工具协议错误:agent.delegate {field} 类型无效"),
|
||||
)
|
||||
})?;
|
||||
if values.len() < min_items || values.len() > max_items {
|
||||
return Err(format!(
|
||||
"Agent 原生工具协议错误:agent.delegate {field} 数量无效"
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::ArgumentsSchema,
|
||||
format!("Agent 原生工具协议错误:agent.delegate {field} 数量无效"),
|
||||
));
|
||||
}
|
||||
for value in values {
|
||||
@@ -330,14 +556,17 @@ fn runtime_tool_for_native_function(name: &str) -> Option<String> {
|
||||
fn mcp_tool_for_native_function<'a>(
|
||||
name: &str,
|
||||
catalog: &'a GameCreatorMcpCatalog,
|
||||
) -> Result<Option<&'a GameCreatorMcpCatalogTool>, String> {
|
||||
) -> Result<Option<&'a GameCreatorMcpCatalogTool>, AgentRuntimeToolPlanProtocolError> {
|
||||
let matches = catalog
|
||||
.tools
|
||||
.iter()
|
||||
.filter(|tool| native_mcp_function_name(&tool.server_id, &tool.name) == name)
|
||||
.collect::<Vec<_>>();
|
||||
if matches.len() > 1 {
|
||||
return Err(format!("MCP 原生函数 binding 冲突:{name}"));
|
||||
return Err(protocol_error(
|
||||
AgentRuntimeToolPlanProtocolErrorKind::CatalogBinding,
|
||||
format!("MCP 原生函数 binding 冲突:{name}"),
|
||||
));
|
||||
}
|
||||
Ok(matches.into_iter().next())
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
)
|
||||
|
||||
@@ -1608,6 +1608,58 @@ pub(crate) fn append_agent_db_record_if_missing_for_action(
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
root: &Path,
|
||||
record_type: &str,
|
||||
action_id: &str,
|
||||
delegation_group_id: &str,
|
||||
record: serde_json::Value,
|
||||
) -> Result<bool, String> {
|
||||
let matches_identity = !record_type.trim().is_empty()
|
||||
&& !action_id.trim().is_empty()
|
||||
&& !delegation_group_id.trim().is_empty()
|
||||
&& record.get("recordType").and_then(serde_json::Value::as_str) == Some(record_type)
|
||||
&& record.get("actionId").and_then(serde_json::Value::as_str) == Some(action_id)
|
||||
&& record
|
||||
.get("delegationGroupId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(delegation_group_id)
|
||||
&& record.get("schemaVersion").is_none()
|
||||
&& record.get("updatedAt").is_none();
|
||||
if !matches_identity {
|
||||
return Err("Agent 本地索引 action/group 幂等记录身份不匹配".to_string());
|
||||
}
|
||||
#[cfg(test)]
|
||||
take_agent_db_record_failure_injection(root, Some(record_type))?;
|
||||
|
||||
let append_class = agent_db_record_append_class(&record);
|
||||
let path = root.join(".agent/agent.db");
|
||||
let directory = open_agent_db_directory(root, true)?
|
||||
.ok_or_else(|| "创建项目 .agent 目录失败".to_string())?;
|
||||
let append_lock = project_append_lock_for(&path)?;
|
||||
let _process_guard = append_lock.lock_process("Agent 本地索引")?;
|
||||
verify_agent_db_directory_current(&directory)?;
|
||||
let mut storage = open_agent_db_storage(directory, true, true)?
|
||||
.ok_or_else(|| "创建 Agent 本地索引失败".to_string())?;
|
||||
verify_agent_db_storage_current(&storage)?;
|
||||
repair_truncated_jsonl_tail_unlocked(&mut storage.file, &storage.path, "Agent 本地索引")?;
|
||||
verify_agent_db_storage_current(&storage)?;
|
||||
if validate_agent_db_action_delegation_group_records_unlocked(
|
||||
&mut storage.file,
|
||||
&storage.path,
|
||||
record_type,
|
||||
action_id,
|
||||
delegation_group_id,
|
||||
&record,
|
||||
)? {
|
||||
return Ok(false);
|
||||
}
|
||||
let line = serialize_agent_db_record(record)?;
|
||||
validate_agent_db_append_class_record_size(append_class, &line)?;
|
||||
append_agent_db_classified_line_unlocked(&mut storage, &line, append_class)?;
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
pub(crate) fn append_agent_db_agent_message_if_missing(
|
||||
root: &Path,
|
||||
agent_id: &str,
|
||||
@@ -2080,6 +2132,82 @@ pub(crate) fn read_agent_db_records_bounded(
|
||||
Ok((records.into_iter().collect(), truncated))
|
||||
}
|
||||
|
||||
fn validate_agent_db_action_delegation_group_records_unlocked(
|
||||
file: &mut File,
|
||||
path: &Path,
|
||||
record_type: &str,
|
||||
action_id: &str,
|
||||
delegation_group_id: &str,
|
||||
expected: &serde_json::Value,
|
||||
) -> Result<bool, String> {
|
||||
let length = file
|
||||
.metadata()
|
||||
.map_err(|error| format!("读取 Agent 本地索引元数据失败:{}: {error}", path.display()))?
|
||||
.len();
|
||||
if length > AGENT_DB_MAX_ACTION_RECEIPT_SCAN_BYTES {
|
||||
return Err(format!(
|
||||
"Agent 本地索引超过 {} 字节扫描上限:{}",
|
||||
AGENT_DB_MAX_ACTION_RECEIPT_SCAN_BYTES,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
file.seek(SeekFrom::Start(0))
|
||||
.map_err(|error| format!("定位 Agent 本地索引失败:{}: {error}", path.display()))?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let mut record_count = 0_usize;
|
||||
let mut exact_matches = 0_usize;
|
||||
while let Some(line) = read_agent_db_jsonl_line_bounded(&mut reader, path)? {
|
||||
if !line.complete {
|
||||
return Err(format!(
|
||||
"Agent 本地索引 action/group 全量扫描发现不完整 JSONL 尾记录:{}",
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
if line.content.iter().all(|byte| byte.is_ascii_whitespace()) {
|
||||
continue;
|
||||
}
|
||||
record_count = record_count.saturating_add(1);
|
||||
if record_count > AGENT_DB_MAX_SCAN_RECORDS {
|
||||
return Err(format!(
|
||||
"Agent 本地索引超过 {} 条记录扫描上限:{}",
|
||||
AGENT_DB_MAX_SCAN_RECORDS,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
let record = serde_json::from_slice::<serde_json::Value>(&line.content)
|
||||
.map_err(|error| format!("解析 Agent 本地索引失败:{}: {error}", path.display()))?;
|
||||
let matches_key = record.get("recordType").and_then(serde_json::Value::as_str)
|
||||
== Some(record_type)
|
||||
&& record.get("actionId").and_then(serde_json::Value::as_str) == Some(action_id)
|
||||
&& record
|
||||
.get("delegationGroupId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(delegation_group_id);
|
||||
if !matches_key {
|
||||
continue;
|
||||
}
|
||||
if !agent_db_stored_record_matches_expected_payload(&record, expected) {
|
||||
return Err(format!(
|
||||
"Agent 本地索引 action/group 幂等记录内容冲突:{record_type}/{action_id}/{delegation_group_id}"
|
||||
));
|
||||
}
|
||||
exact_matches = exact_matches.saturating_add(1);
|
||||
if exact_matches > 1 {
|
||||
return Err(format!(
|
||||
"Agent 本地索引 action/group 幂等记录重复:{record_type}/{action_id}/{delegation_group_id}"
|
||||
));
|
||||
}
|
||||
}
|
||||
if exact_matches == 0 && record_count >= AGENT_DB_MAX_SCAN_RECORDS {
|
||||
return Err(format!(
|
||||
"Agent 本地索引已达到 {} 条记录扫描上限,无法追加 action/group 审计:{}",
|
||||
AGENT_DB_MAX_SCAN_RECORDS,
|
||||
path.display()
|
||||
));
|
||||
}
|
||||
Ok(exact_matches == 1)
|
||||
}
|
||||
|
||||
fn validate_agent_db_action_records_unlocked(
|
||||
file: &mut File,
|
||||
path: &Path,
|
||||
@@ -6239,8 +6367,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 +6380,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());
|
||||
}
|
||||
@@ -8348,6 +8481,21 @@ mod agent_db_security_tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn isolated_join_claim_audit_record(
|
||||
delegation_group_id: &str,
|
||||
join_run_id: &str,
|
||||
) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"recordType": "agent.runtime.agent.isolated_join.claimed_by_parent",
|
||||
"agentId": "project-supervisor",
|
||||
"runId": "parent-run-1",
|
||||
"parentActionId": "parent-action-1",
|
||||
"delegationGroupId": delegation_group_id,
|
||||
"joinRunId": join_run_id,
|
||||
"actionId": TEST_ACTION_ID,
|
||||
})
|
||||
}
|
||||
|
||||
fn provider_request_id(hex: char) -> String {
|
||||
format!("provider-request-{}", hex.to_string().repeat(64))
|
||||
}
|
||||
@@ -8653,6 +8801,131 @@ mod agent_db_security_tests {
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_group_append_repairs_torn_tail_and_scans_past_bounded_history() {
|
||||
const RECORD_TYPE: &str = "agent.runtime.agent.isolated_join.claimed_by_parent";
|
||||
const DELEGATION_GROUP_ID: &str = "delegation-group-1";
|
||||
|
||||
let root = unique_agent_db_test_root("action-group-tail-repair");
|
||||
let record = isolated_join_claim_audit_record(DELEGATION_GROUP_ID, "join-run-1");
|
||||
assert!(
|
||||
append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
&root,
|
||||
RECORD_TYPE,
|
||||
TEST_ACTION_ID,
|
||||
DELEGATION_GROUP_ID,
|
||||
record.clone(),
|
||||
)
|
||||
.expect("append initial action/group audit")
|
||||
);
|
||||
|
||||
let path = root.join(".agent/agent.db");
|
||||
let mut file = fs::OpenOptions::new()
|
||||
.append(true)
|
||||
.open(&path)
|
||||
.expect("open action/group Agent DB fixture");
|
||||
file.write_all(b"{}\n".repeat(AGENT_DB_MAX_BOUNDED_RECORDS + 1).as_slice())
|
||||
.expect("write records beyond bounded history");
|
||||
file.write_all(br#"{"recordType":"torn-action-group"#)
|
||||
.expect("write torn Agent DB tail");
|
||||
file.flush().expect("flush torn Agent DB fixture");
|
||||
drop(file);
|
||||
|
||||
assert!(
|
||||
!append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
&root,
|
||||
RECORD_TYPE,
|
||||
TEST_ACTION_ID,
|
||||
DELEGATION_GROUP_ID,
|
||||
record,
|
||||
)
|
||||
.expect("repair tail and find action/group audit from file head")
|
||||
);
|
||||
|
||||
let content = fs::read_to_string(&path).expect("read repaired action/group Agent DB");
|
||||
assert!(!content.contains("torn-action-group"));
|
||||
let exact_matches = content
|
||||
.lines()
|
||||
.filter(|line| !line.trim().is_empty())
|
||||
.map(|line| {
|
||||
serde_json::from_str::<serde_json::Value>(line)
|
||||
.expect("repaired Agent DB contains complete JSONL")
|
||||
})
|
||||
.filter(|stored| {
|
||||
stored.get("recordType").and_then(serde_json::Value::as_str) == Some(RECORD_TYPE)
|
||||
&& stored.get("actionId").and_then(serde_json::Value::as_str)
|
||||
== Some(TEST_ACTION_ID)
|
||||
&& stored
|
||||
.get("delegationGroupId")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
== Some(DELEGATION_GROUP_ID)
|
||||
})
|
||||
.count();
|
||||
assert_eq!(exact_matches, 1);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_group_append_preserves_failure_injection_and_rejects_content_conflicts() {
|
||||
const RECORD_TYPE: &str = "agent.runtime.agent.isolated_join.claimed_by_parent";
|
||||
const DELEGATION_GROUP_ID: &str = "delegation-group-1";
|
||||
|
||||
let root = unique_agent_db_test_root("action-group-conflict");
|
||||
fs::create_dir_all(root.join(".agent/runtime")).expect("create Agent DB runtime directory");
|
||||
fs::write(
|
||||
root.join(".agent/runtime/test-fail-next-agent-db-record"),
|
||||
RECORD_TYPE,
|
||||
)
|
||||
.expect("arm Agent DB failure injection");
|
||||
let record = isolated_join_claim_audit_record(DELEGATION_GROUP_ID, "join-run-1");
|
||||
let injected_error = append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
&root,
|
||||
RECORD_TYPE,
|
||||
TEST_ACTION_ID,
|
||||
DELEGATION_GROUP_ID,
|
||||
record.clone(),
|
||||
)
|
||||
.expect_err("failure injection must run before append");
|
||||
assert!(injected_error.contains("测试注入 Agent DB 记录失败"));
|
||||
|
||||
assert!(
|
||||
append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
&root,
|
||||
RECORD_TYPE,
|
||||
TEST_ACTION_ID,
|
||||
DELEGATION_GROUP_ID,
|
||||
record,
|
||||
)
|
||||
.expect("append action/group audit after injected failure")
|
||||
);
|
||||
let conflicting =
|
||||
isolated_join_claim_audit_record(DELEGATION_GROUP_ID, "join-run-conflict");
|
||||
let conflict_error = append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
&root,
|
||||
RECORD_TYPE,
|
||||
TEST_ACTION_ID,
|
||||
DELEGATION_GROUP_ID,
|
||||
conflicting,
|
||||
)
|
||||
.expect_err("same action/group key with different content must fail closed");
|
||||
assert!(conflict_error.contains("内容冲突"), "{conflict_error}");
|
||||
|
||||
let second_group = isolated_join_claim_audit_record("delegation-group-2", "join-run-2");
|
||||
assert!(
|
||||
append_agent_db_record_if_missing_for_action_and_delegation_group(
|
||||
&root,
|
||||
RECORD_TYPE,
|
||||
TEST_ACTION_ID,
|
||||
"delegation-group-2",
|
||||
second_group,
|
||||
)
|
||||
.expect("a different delegation group is a distinct audit key")
|
||||
);
|
||||
|
||||
fs::remove_dir_all(root).ok();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_append_rejects_action_receipts() {
|
||||
let root = unique_agent_db_test_root("generic-receipt-rejected");
|
||||
@@ -11293,12 +11566,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 {
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-07-18 AI 游戏创作正式项目页升级为 GameAgent 工作台
|
||||
|
||||
- 背景:新的《陶泥儿GameAgent-V1.0 项目开发界面需求》要求正式项目开发页同时承载资源管理、运行表现层、陶泥儿对话和子 Agent 状态,旧的“正式用户页只有主聊天与只读专业 Agent 列表”已不足以支撑目标交互。
|
||||
- 决策:在现有 `apps/ai-game-creator-shell` 项目开发入口内扩展单一工作台,不新建平行客户端。首版从当前 manifest、导入附件和 Agent 状态派生界面,提供资源 / 运行切换、资源排列与聚焦、审批弹层和底部状态栏;真实游戏仍通过现有 localhost 预览命令交给外部浏览器,不嵌入 iframe。未具备正式写回契约的拖拽布局、版本资源替换、数值微调、泥点累计、Agent.md 和 Skill 管理不得在前端伪造成功。
|
||||
- 影响范围:`apps/ai-game-creator-shell` 正式项目开发页、项目工作台前端测试、AI 游戏创作智能体 App 实施计划和原生壳预览门禁。
|
||||
- 验证方式:运行 AI game creator shell 定向测试与 typecheck、`npm run ai-game-creator-shell:check`、`npm run check:encoding`、`git diff --check`,并用真实浏览器检查桌面与窄屏布局。
|
||||
- 关联文档:`docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md`。
|
||||
|
||||
## 2026-07-17 AI 游戏创作 V1.31 使用同一父 run 收束静态与隔离协作
|
||||
|
||||
- 背景:V1.30 已证明 Supervisor 能在无编排配方的真实终端任务中自主选择多个 static 专业 Agent,但尚未证明同一父 run 同时存在 static delivery/claim 与 isolated all-join 时,等待、唤醒、恢复和唯一 finalization 可以组合。两类协议分别通过不能替代组合证据。
|
||||
@@ -4820,3 +4828,76 @@
|
||||
- 决策:`packages/shared` 的充值组件目录新增宿主无关的 `useWechatNativeRechargeController`,通过注入确认、监听和余额快照回调统一管理二维码校验、手动确认重试、SSE 监听、终态映射和 lifecycle 隔离。主站只把 Native 分支委托给共享 controller,H5、JSAPI、小程序、登录恢复、任务和邀请码仍留在原 controller;AI 游戏创作客户端通过本地 `useRechargeController` 托管弹窗加载与固定 `wechat_native` 下单,`App.tsx` 只负责视图接线。
|
||||
- 余额边界:AI 游戏创作客户端的 `useWalletStore.mudPointBalance` 仍是唯一余额真相;充值响应只把后端完整快照写入 store,支付成功后触发完整刷新,不在客户端本地推算或增减泥点。
|
||||
- 验证方式:共享 hook Vitest、AI 游戏创作客户端充值与 Wallet Store 定向测试、主站充值渠道定向测试、两个 TypeScript 边界、`npm run check:encoding` 和 `git diff --check`。
|
||||
|
||||
## 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。
|
||||
- 覆盖说明:上条关于“恢复时重验当前策略、live policy 漂移即 reconciliation”的部分自 V1.38 起不再是现行口径。V1.32 的其它 batch/contract/action 身份与副作用前门禁继续有效;现行策略选择、漂移和恢复顺序以本文件 V1.38 决策为准。
|
||||
- 控制面边界:`.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 次格式修复,作为后续性能与提示合同收敛风险保留。
|
||||
|
||||
## 2026-07-18 Agent 原生工具计划 repair 使用稳定分类与受限归一化
|
||||
|
||||
- 背景:V1.32 虽然完整 PASS,但 49 次成功 native tool plan 伴随 30 次格式修复;原有审计只有错误哈希和字符数,无法判断是正文混入、arguments JSON、schema 还是批次语义导致,也无法在失败 partial report 中比较分布。
|
||||
- 兼容边界:`platform-llm` 排除明确 reasoning/analysis content part;Agent 移除完整、嵌套闭合的 `<think>...</think>`。只有不含 `respond_to_user` 和旧 wrapper 的 native planning 响应可把剩余正文按 `planner-commentary` 归一化,并继续以 function calls 为权威动作;最终用户回复、legacy wrapper、未闭合或错配 thinking 标签继续失败关闭。
|
||||
- 协议分类:固定使用 `response-shape / call-identity / unknown-function / arguments-json / arguments-schema / batch-constraint / plan-semantics / catalog-binding`。JSON 先递归拒绝顶层和任意嵌套 input 的重复 object key,再做 schema 解析;前七类按现有上限进入格式修复,目录 binding 冲突直接失败且成功报告中必须为 0。控制流不再从中文错误字符串反推类别。
|
||||
- 审计与报告:repair 只新增 `protocolErrorKind`;成功归一化只保存固定 `complete-think-block / planner-commentary` kind、数量、字符数和 SHA-256。真实 E2E 报告增加 repaired loop、second repair 和固定补零直方图,完整与 partial 证据复用同一选择器和聚合器,分类总和必须闭合且不得携带原始错误、正文、arguments、preview 或单条身份;白名单必须包含 Agent DB 固有 `schemaVersion / updatedAt`,不能把安全 envelope 误报成正文泄漏。
|
||||
- Prompt:原生 function arguments 的统一外壳明确为 `reason + input`;legacy text JSON schema 只属于没有 function tools 的 Provider,避免工具自己的 input schema 与旧 actions JSON 示例互相竞争。
|
||||
- 诊断过程:首轮旧保守正文规则得到 35 个成功计划、23 次 `response-shape` repair,且因 E2E 白名单遗漏 Agent DB envelope 误报 58 条泄漏而 FAIL;第二次新规则尝试在 2 个计划、0 repair 时因 Provider isolated write scope 不满足 fixture 提前停止。两轮都不作为完成证据。
|
||||
- 真实验收:最终代码对应的正式 `gpt-5.5 / openai_chat / high` 同 suite 独立 PASS。46/46 个成功计划全部使用 `native_runtime_tools`,格式 repair 和八类直方图均为 0;Provider lifecycle 为 54/54 started/terminal,其中 53 completed、1 次瞬态失败通过新 request identity 显式重试恢复,相比 V1.32 的 86 减少 32,总耗时 `631.6s`。static + isolated 混合协作、业务 delivery repair、Provider 真并行、pidfd Runner 强杀恢复、宿主验证和唯一 Supervisor assistant 全部成立;重复、残留 sidecar、正文、Key、项目 / 正式配置路径与报告泄漏均为 0,隔离现场完整清理。
|
||||
|
||||
## 2026-07-18 AI 游戏创作 Agent Runtime V1.34 动态隔离子 Agent writeScopes 命令绕过封堵
|
||||
|
||||
- 背景:动态 isolated child 的 `writeScopes` 只约束结构化 file/patchset 路径;现有 V1.11 OS sandbox 仍把项目根整体挂为可写。若 child 继承 `project.verify`、通用命令、持久进程或预览启动,shell、构建 hook 和后代进程可以绕过路径校验写到 scope 外。approval 不能替代 OS 级作用域隔离。
|
||||
- 决策:在 scope-aware OS sandbox 完成前,动态 child 无条件禁用 `project.verify / project.git_commit / command.exec / command.start / command.stdin / preview.start / agent.delegate / agent.spawn_isolated / project.restore / agent.schedule_ready / canvas.asset_generate / task.create / task.update / blackboard.write` 和全部 MCP 动态函数/兼容调用。有效策略快照把对应内置工具和 `mcp.call` 显示为 `denied`;动态 MCP function 归一后执行同一拒绝。模板 Agent policy、项目 policy、legacy 快照和用户 approval 均不能放宽。
|
||||
- 保留边界:继续允许固定只读且不接受任意 program/argv/shell 的 `command.run_limited`,同 child/run 身份的 `command.output_read / command.poll / command.terminate`,只验证既有精确 loopback 预览的 `preview.validate`,以及目标完整位于有效 `writeScopes` 内的 `file.write / file.patch / file.delete / project.patchset`。多文件变更含一个越界目标即在 checkpoint、revision 和真实写入前整组拒绝;通用验证交由父 Agent 或静态专业 Agent 完成。
|
||||
- 原子与恢复:新单动作在 confirmation 与 OS launcher 前拒绝,不产生 spawn、revision 或项目副作用。新多 action batch 在选择 confirmation 模式前逐项校验,任一 denied member 使整批 abort,允许成员也不执行;只保留 `aborted / nextActionIndex=0` batch 事实,不发布独立 pending sidecar。旧 pending、approval 与旧 batch 真正进入执行器时仍重验当前边界;旧 executing 未知结果继续按既有 reconciliation 规则处理,绝不 replay。
|
||||
- 验证方式:新增恶意 `bash -lc` sibling 写入回归,覆盖单动作、两动作 batch、策略快照和旧 executing pending 的执行器重验;断言 sibling 文件、nested delivery、独立 pending sidecar 和 revision 变化均为 0。工具作用域单测逐项覆盖拒绝集合与保留工具;同时运行 isolated 30 项、mixed 3 项、Supervisor collaboration 27 项、Provider batch 12 项和 Tauri 全量回归。
|
||||
- 真实验收边界:V1.31/V1.32 已以 isolated mutation 为 0 的真实 Provider suite 证明 mixed 协作、all-join、Runner 恢复和唯一回复;V1.34 只做安全收紧,本切片不为此重跑两套 Provider,也不能把旧 PASS 当作未来新 child 写入语义的证据。只有后续 scope-aware OS sandbox 能把有效 `writeScopes` 变成项目根其余部分只读、链接/挂载不可逃逸且所有后代继承的强制边界,并通过独立跨平台门禁后,才可在新决策中重新评估命令工具;其余拒绝能力仍需各自单独评审。
|
||||
|
||||
## 2026-07-18 AI 游戏创作 Agent Runtime V1.35 多 ready isolated all-join 原子认领
|
||||
|
||||
- 背景:同一父 run 的一次 `agent.run_status` 可以同时看到多个 ready isolated all-join。若逐个取得锁并立即改写 delivery,后一个 join 锁竞争会让前一个 group 留在部分认领状态,破坏整次 action 的可恢复原子边界。
|
||||
- 锁边界:先按 `delegationGroupId` 去重排序,再按该顺序一次性预取全部 join delivery 锁;全部锁就绪前不得创建 claim sidecar 或改写 delivery。任一后续 join 锁忙时释放已取得的锁,并保证零 delivery mutation、零 claim sidecar。
|
||||
- 持久恢复:全锁就绪后,同一 action 使用一个 durable claim journal,按 `prepared -> committed -> observed` 单向推进。发生部分 commit 或 Runner 退出时,恢复必须复用同一 action journal、按相同顺序幂等补齐未提交 group,不创建新 action、新 journal 或重复 delivery claim。
|
||||
- Observation 与完成:只认领可完整放入本轮 `readyIsolatedJoins` 观察预算的有序前缀,该区块固定置于 `agent.run_status` detail 首部;剩余 group 保持 ready,不能把已认领结果截断后让模型猜测。只有成功 observation 已持久写入 pending sidecar 后才能标记 `observed`;任一未观察 claim 都继续阻断 finalization。每个 group 的审计以 `actionId + delegationGroupId` 唯一,恢复只补缺失记录,不重复追加。
|
||||
- 旧状态恢复:每个 `claimed-by-parent` delivery 必须被同一 `claimedByActionId + delegationGroupId` 的 journal 覆盖,无 journal delivery 继续阻断完成。`agent.run_status` 先重放已有未观察 claim;随后每轮只为一个稳定排序的旧 action 合成 journal 并完整输出,恢复 action 不取得 delivery。原 action 已有 journal 但遗漏 group 时不得扩写或倒退状态,同一 group 归属其他 action journal 时按身份冲突失败关闭;pending observation 只能标记本轮完整输出的 claim。
|
||||
- 审计恢复:isolated group 审计通过 Agent DB 专用锁内幂等入口追加;同一锁内先修复 JSONL 截断尾行,再从文件头扫描有效数据库的完整记录范围,以 `recordType + actionId + delegationGroupId` 核对完整 payload。重复键、内容冲突或物理容量越界均失败关闭。
|
||||
- Mixed 恢复:isolated claim 已提交、同一 `run_status` 后续 static receipt 认领失败时,下一 action 先完整重放旧 isolated claim,再继续 static 认领;旧 delivery/journal 仍绑定原 action,不产生第二份 isolated claim。恢复 observation 成功持久化后才能把旧 claim 标为 `observed`。
|
||||
- 定向验收:覆盖后一个 join 锁冲突、mixed static 锁失败后新 action 重放 isolated 结果、Agent DB torn tail 后 prepared/partial claim 恢复、多旧 action 逐轮迁移、已有 journal 单调性与跨 action group 归属冲突;完整 observation 必须实际包含被标记 observed 的全部 group。`isolated` 36/36、`project_supervisor` 42/42、`supervisor_collaboration` 27/27、`provider_action_batch` 12/12 已通过,Tauri/Rust 全量为 915 passed、4 个环境依赖用例按设计 ignored。这些本地结果本身不替代真实 Provider 证据。
|
||||
- 真实验收:2026-07-18 后续真实 Provider E2E **PASS**。同一父 Session/run 的初始 isolated all-join group 包含 2 个 child;首次 `parent-wake` 后、任何 join claim 前创建的 follow-up group 包含 1 个 child。两组的精确 `writeScopes` 集合互不重叠,一个状态为 `observed` 的 join claim journal 同时覆盖两个 group;Runner 强杀/恢复身份稳定。Provider lifecycle `53/53` 全部 completed、failed 为 `0`,重复、泄漏与残留均为 `0`。V1.35 的外部模型链路据此完成验收。
|
||||
- 保留边界:V1.35 不等于 V1.34 的 scope-aware OS sandbox 已完成;后者仍未完成,V1.34 的动态 isolated child 工具禁用边界继续有效。本轮多 group PASS 证明当前业务合同与 Supervisor 提示能形成该分阶段轨迹,不等于 Runtime 能预知尚未生效的项目检查并通用禁止提前 `agent.run_status`;需要产品级强制阶段时应先扩展 collaboration policy 契约。本轮 PASS 也不替代 V1.36 的 static + isolated 混合 observation 完整性独立门禁。
|
||||
|
||||
## 2026-07-18 AI 游戏创作 Agent Runtime V1.36 混合协作 observation 完整性
|
||||
|
||||
- 背景:静态 delegate claim sidecar 可容纳远大于 Provider 单轮观察窗口的内容,而旧 `agent.run_status` 在最终 detail 超过 16000 字符时直接截断。多份静态回执或 static + isolated 混合返回可能因此只把部分 JSON 交给模型,却把整个 durable claim 标为 `Observed`。
|
||||
- 预算:`readyDelegateReceipts` 完整 JSON 单批上限为 6000 字符;`readyIsolatedJoins` 在 isolated-only 时保持 10000 字符,在同轮可能携带静态回执时使用 6000 字符。普通 Runtime 状态、claimed join 和 claimed contract 摘要合计最多 3500 字符。最终 detail 仍以 16000 字符为硬上限,清洗后超限直接返回 failed,禁止截断任一 ready 证据区块。
|
||||
- 静态分批:先完整保留当前 action 已绑定的 recovery receipts,再按 `delegationId` 为新 ready delivery 选择稳定前缀;只为最终选择的批次预取 delivery 锁,并在锁内重读核对预算选择快照。未选中的后续 delivery 保持 `Ready`,其锁竞争不得阻断必选恢复;必选集合本身无法放入预算时,必须在写 claim journal 和改写 delivery 前失败关闭。
|
||||
- 精确观察:pending observation 从前置 `readyDelegateReceipts` 区块解析唯一 delegationId 集合,并与该 action durable claim 的 receipt 集合做精确相等比较;前置区块还必须唯一且显式为 `ready=true`。缺失、额外、重复、false 或无法解析的 ID 都不得推进 `Committed -> Observed`,未观察 claim 继续阻断 finalization。`readyIsolatedJoins` 继续按完整 group 集合执行同类门禁。
|
||||
- 恢复顺序:预算提示只读取 delivery 状态,不提交 Prepared claim,也不改变旧恢复时序。mixed `run_status` 仍先认领 isolated join,再认领 static receipt;static 锁或持久化失败后,后续 action 必须完整重放原 isolated claim,再继续静态认领。
|
||||
- 验收边界:确定性回归覆盖默认 6000 字符下单份合法静态回执超预算零 mutation、稳定前缀留下后续 ready delivery、缺失 journal 的必选回执不受未选中 delivery 锁竞争影响、部分 delegationId 不能标记 observed、完整精确集合才能清除 barrier、重复/false 前置区块失败关闭,以及 mixed ready static / isolated JSON 不被静默截断。`project_supervisor` 46/46、mixed 5/5、`isolated` 37/37、`supervisor_collaboration` 27/27、`provider_action_batch` 12/12 均通过,Tauri/Rust 全量为 923 passed、4 个环境依赖用例按设计 ignored。本切片未重跑真实 Provider suite,不把 V1.31-V1.33 的既有 PASS 当作 V1.36 新协议证据。
|
||||
|
||||
## 2026-07-18 AI 游戏创作 Agent Runtime V1.37 分阶段 isolated group 首次认领硬门禁
|
||||
|
||||
- 背景:V1.35 的真实 Provider 已形成“首批 1 个 group、首次 claim 前再补 1 个 group”的正确轨迹,但该顺序仍依赖任务合同和提示,Runtime 没有通用硬门禁。
|
||||
- Policy 兼容:collaboration policy v1 新增可选 `minIsolatedGroupsBeforeClaim`,默认 `0`、上限 `16`,零值序列化省略,以保持旧 policy / contract fingerprint 不变。initial preflight / contract 只检查既有首波要求,允许首批 1 个 group;finalization completion 额外要求同一父 run 的 group 总数达到 policy。
|
||||
- 首次认领:首次新 claim 在选定可完整输出的 ready group 批次后,必须同时确认已建立 group 数和 ready group 数达到 policy;不足时在 claim journal 和 delivery mutation 前失败关闭。已有 durable claim、未观察 claim 与 legacy claim 的恢复优先,继续按原身份重放,不被升级门禁卡死。
|
||||
- Scope 边界:只读 isolated task 也必须声明 expected artifact 的最小目录 scope,不得扩大到 sibling scope 或共同父目录;该约束写入通用边界提示,不为单个验收任务硬编码。
|
||||
- 定向验收:`supervisor_collaboration_policy_` 23/23、`project_supervisor_` 47/47 通过,E2E self-test **PASS**;Tauri/Rust 全量为 930 passed、4 个环境依赖用例按设计 ignored。
|
||||
- 真实 Provider:第一次独立运行因模型初始 child scope 不符合 expected artifact 最小边界而 **FAIL**,child / claim / project mutation 均为 `0` 且现场自动清理,不与后续证据拼接。补强通用边界提示后的第二次独立运行 **PASS**:policy=`2`,2 个 group / 3 个 child,1 个 `observed` join claim 覆盖 2 个 group;Runner 强杀恢复身份稳定,Provider lifecycle `64/64` completed、failed=`0`,重复、残留、泄漏均为 `0`,最终回复唯一。
|
||||
|
||||
## 2026-07-18 AI 游戏创作 Agent Runtime V1.38 父 run 协作策略持久快照与绑定记录
|
||||
|
||||
- 决策:首个非 `aborted`、携带 v2 `collaborationContract` 的 durable collaboration batch 是父 run 策略线性化点。Runtime 必须按 `v2 batch -> snapshot -> binding sidecar -> action side effects` 持久化:snapshot 位于 `.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json`,独立 binding 位于 `.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json`,用于持久证明“该 run 曾绑定”。没有既存 binding 的首次 `aborted` batch 不创建两类 sidecar;若 binding 已证明该 run 先前完成绑定,snapshot 丢失时可用完整验真的 matching v2 contract 恢复原快照,即使当前保留的 batch 为 `aborted`,这不构成新绑定。batch -> snapshot 与 snapshot -> binding 都是零副作用可恢复窗口。
|
||||
- 数据契约:snapshot v1 固定且完整包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;policy 先规范化,snapshot fingerprint 绑定除 `snapshotFingerprint / boundAt` 外的全部稳定字段。binding v1 固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,必须与 snapshot 的绑定身份逐字段一致。两者按 project/parent run 共用锁并在锁内 CAS,冲突失败关闭,禁止通用 replace 覆盖。
|
||||
- 路径身份:安全 Agent/run ID 可原样作为 `agentKey/runKey`;任何不安全或规范化后变化的 ID 必须使用有界安全前缀加原始完整 ID 的稳定 SHA-256,不能让 lossy 字符替换制造路径碰撞。锁 key 固定对完整 `parentAgentId + NUL + parentRunId` 计算稳定 SHA-256,不复用路径规范化结果。
|
||||
- 恢复:优先级固定为 existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 policy。正常绑定使用 `boundFrom=initial-collaboration-batch`;v2 contract 必须先独立校验 batch/project/action/contract 全身份与两层指纹,才能以 `boundFrom=legacy-provider-batch-contract` 补绑。snapshot 存在但 binding 缺失时从 snapshot 补写;binding 存在但 snapshot 丢失时只按 matching binding 与可信 v2 contract 恢复,没有可信 v2 contract 时禁止按 live policy 重绑。
|
||||
- Legacy 与旧 batch:contractless/v1 collaboration batch 必须在任何 live policy 回退前失败关闭并进入 reconciliation,不能忽略旧 batch 后把已有 run 当成 fresh run。`boundFrom=legacy-current-project-policy` 仅允许无 snapshot/binding、无可信 v2 contract,且不存在上述旧 batch,并由 durable run 身份与状态明确证明属于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;terminal、`needs-reconciliation` 或身份/状态未知 run 的状态读取不得新建 snapshot。没有任何 durable run/collaboration 事实的真正新父 run只能读取 live policy 构造首个 v2 contract,不创建 legacy snapshot。
|
||||
- 漂移:snapshot 绑定后,后续 spawn、repair、新 claim、Supervisor mutation、MCP、prompt/status、completion/finalization 与恢复全部使用 snapshot。global policy 的 `matched / drifted / unreadable` 只作有界状态报告,不改变执行、revision、verification 或 reconciliation;已有 run 不重绑,新 policy 只由后续新父 run 采用。
|
||||
- Claim 兼容:旧 durable claim、未观察 claim 和 legacy claimed delivery 的恢复先于 effective snapshot 解析及新 claim 门禁,继续按原 action/group 身份推进且不得取得新 delivery;global policy、snapshot 或 binding 故障不能把已提交 claim 卡死。新的 claim 必须先成功解析 effective snapshot 并核对 binding,失败发生在 journal、delivery 锁和 mutation 之前;随后仍执行 V1.35-V1.37 的全锁、预算、完整 observation 和 group 数量门禁。
|
||||
- 覆盖与验收:本决策明确覆盖 V1.32 的 live drift reconciliation 旧口径,但不把 V1.32/V1.35/V1.37 历史 PASS 外推为 V1.38 证据。2026-07-19 self-test、snapshot/binding 双故障窗口与 CAS/丢失/篡改/旧 batch/危险 ID/claim 分流确定性覆盖、`supervisor_collaboration_` 52/52、`provider_action_batch_` 12/12、`project_supervisor_mixed_` 5/5 和 Tauri/Rust 全量 949 passed/4 ignored 已通过;终态 Runtime 清理后 snapshot/binding 保持原字节并继续解析为 `run-snapshot`。真实 mixed-swarm 独立功能样本已形成 2 group/3 child、policy drift、Runner 恢复、唯一最终回复和零重复/泄漏,但同轮正式 endpoint 被外部客户端重启;改用私有配置源后多轮又耗尽 transient Provider retry,最后在 `300000ms / maxRetries=3` 下于首批业务动作前形成 4 failed/3 retry 并干净终止。两类失败证据不得拼接,当前**仍不得声称 V1.38 真实 E2E 已 PASS**。
|
||||
|
||||
@@ -133,6 +133,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` 改成长驻入口。最小工具输入保持结构化:
|
||||
|
||||
@@ -3243,6 +3243,17 @@
|
||||
- 验证:Rust 定向回归使用 `project_supervisor_` 前缀,覆盖 delivery/claim 状态机、同 action 幂等、第 4 个新委派拒绝与已预留委派复用/拒绝 suppression、后续 delivery 锁忙时零部分认领、Agent DB 故障后回执仍可重放、未 Observed 阻断 final、Provider planning 前 durable 等待、parent-wake coalescing/结构性错误、重启损坏 barrier、错配和迟到 child、executing `run_status` 续接与 delegate policy 重验;`agent_background_enqueue_notifies_only_after_session_lane_release` 覆盖入队锁序,Runner 内部测试覆盖定向 wake 与不缓存重试。真实 Provider 必须同时证明专业 Agent 时间区间重叠、父 run 仅一次 waiting、同一 Observed claim 认领全部回执、唯一 assistant、第二轮历史引用不新增委派和项目范围密钥扫描为 0。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`apps/ai-game-creator-shell/src-tauri/src/delegation.rs`、`agent.rs`、`runner.rs`、`tests.rs`。
|
||||
|
||||
## 父 run 协作策略不能在绑定后继续按全局 live policy 重验
|
||||
|
||||
- 现象:同一 Supervisor 父 run 已经持久化合法 collaboration batch,管理员随后修改或损坏 `.agent/collaboration-policy.json`,后续 spawn、claim、mutation、MCP 或 finalization 却突然改用新策略、进入 reconciliation;或者 snapshot 被删除后,Runtime 又按 live policy 把已有 run 当成未绑定 run。另一类症状是 contractless/v1 batch 被跳过、两个不安全 run ID 经字符替换落到同一 snapshot/锁 key,或旧 `Prepared / Committed` claim 因 snapshot/binding 不可读而不能重放 observation。
|
||||
- 原因:把项目级 policy 当成每个动作的 live 执行事实,没有为父 run 设置明确线性化点、不可变策略快照和独立“曾绑定”记录;或者在 v2 batch 完整验真前就用 `contract.policy` 播种 snapshot。只对 run ID 做 lossy 规范化、让锁复用该路径片段,或用通用原子 replace 代替同一身份锁内 CAS,也会制造路径碰撞、并发覆盖和伪合同漂移。
|
||||
- 处理:V1.38 固定顺序为 `v2 batch -> snapshot -> binding sidecar -> action side effects`。snapshot 位于 `.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json`,其完整字段必须统一为 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;snapshot fingerprint 覆盖除 `snapshotFingerprint / boundAt` 外的全部稳定字段。独立 binding 位于 `.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json`,固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,与 snapshot 逐字段交叉验证并持久证明“该 run 曾绑定”。
|
||||
- 路径与恢复:不安全或规范化后变化的 Agent/run ID 使用有界安全前缀加原始 ID 稳定 SHA-256,锁 key 对完整 `parentAgentId + NUL + parentRunId` 计算稳定 SHA-256,不能只做字符替换。恢复顺序为 existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 policy;snapshot 缺 binding 可从 snapshot 补写,binding 存在但 snapshot 丢失只能按可信 v2 contract 和首次绑定身份恢复,无可信 v2 时禁止 live policy 重绑。contractless/v1 collaboration batch 必须先失败关闭。`legacy-current-project-policy` 只允许无 snapshot/binding、无可信 v2 contract,且不存在上述旧 batch,并由可信身份和状态明确证明属于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;terminal、`needs-reconciliation` 或身份/状态未知 run 的状态读取不得新建 snapshot。
|
||||
- 漂移与 Claim:绑定后 global policy 的 `matched / drifted / unreadable` 只报告状态,不能改变后续动作或完成门禁;新 policy 只用于后续新父 run。旧 durable claim、未观察 claim 和 legacy claimed delivery 先按原 action/group 身份恢复且不得取得新 delivery;新的 claim 必须先成功解析 effective snapshot 并核对 binding,再执行 V1.35-V1.37 的全锁、预算、完整 observation 和 group 数量门禁。
|
||||
- 真实 E2E 现场:正在运行的正式客户端可能在验收期间启动或重启正式 Runner,导致 source endpoint 身份真实变化。不得关闭 `sourceRunnerEndpointUnchanged` 门禁,也不得杀掉不属于验收器的进程;应把同一配置内容复制到仓库外的大容量磁盘私有目录,目录/文件权限分别为 `0700/0600`,不复制 endpoint、锁、会话或数据库,验收后删除。功能完整但 source endpoint 被外部改变的报告与后续干净清理报告不得拼接。
|
||||
- 验证:必须覆盖 snapshot 9 个完整字段、首次 `aborted` batch 无 snapshot/binding、matching binding 已存在时可用可信 `aborted` v2 contract 恢复缺失 snapshot、双故障窗口零副作用恢复、同内容并发 CAS、snapshot/binding 冲突或丢失、篡改 contract 不得播种、binding 已存在且无可信 v2 时禁止 live 重绑、contractless/v1 协作 batch 先失败关闭且非协作 v1 batch 不误伤、四种 legacy 非终态可迁移而 terminal/`needs-reconciliation`/身份状态未知读取不建 snapshot、危险 ID 路径/锁不碰撞、四类 global policy 状态,以及旧 claim 可恢复而新 claim 先过 effective snapshot。2026-07-19 上述确定性门禁、E2E self-test、52/52 collaboration 定向回归、终态 snapshot/binding 字节保留回归和 949 passed/4 ignored Rust 全量已完成;真实功能闭合轮受正式 endpoint 外部重启污染,私有配置源轮又连续耗尽 transient Provider retry,不能拼接为 PASS,故当前仍**不得声称 V1.38 真实 E2E 已 PASS**。
|
||||
- 关联:`docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md`、`docs/project-memory/shared-memory/decision-log.md`、`apps/ai-game-creator-shell/src-tauri/src/collaboration.rs`、`agent.rs`、`tests.rs`、`scripts/agent-runtime-real-e2e.mjs`。
|
||||
|
||||
## 单 Agent 持久计划不能靠工具下标或恢复猜进度
|
||||
|
||||
- 现象:工具 action 1 成功后第二个计划步骤被自动标成完成,模型仍有 pending / in_progress 步骤却写出最终回复;或 Runner 重启、刷新 UI、same-run steer 后 `planRevision` 回退、已完成步骤消失,legacy `plan` 又覆盖新计划。另一类错误是仅更新计划就触发项目 revision 漂移、verification 失效或权限确认。
|
||||
|
||||
@@ -184,7 +184,7 @@ Runner 从显式 AppData 目录读取 `game-creator.config.json`,API Key 不
|
||||
- 角色 prompt、LLM 配置和 Agent 策略继承 `templateAgentId`;执行和持久化 lane 使用 `instanceId`。
|
||||
- 单次最多 3 个并行实例,最大深度 1。
|
||||
- sibling `writeScopes` 不得重叠;所有真实写入仍通过项目级写锁、revision 和 verification gate。
|
||||
- 子实例默认拒绝 `agent.spawn_isolated`、`project.restore` 和 `agent.schedule_ready`。
|
||||
- 子实例初版默认拒绝 `agent.spawn_isolated`、`project.restore` 和 `agent.schedule_ready`;现行无条件拒绝集合以 V1.34 为准,模板、项目策略和用户确认都不得放宽。
|
||||
- 子实例的 `memory.read/write(scope=agent)` 只访问 `.agent/runtime/isolated-agents/memory/<instanceId>.json` 私有临时 lane;不能指定 sibling 或静态模板 Agent,也不能写 `project / session / blackboard` 共享记忆。普通静态 Agent 的私有记忆语义保持不变。
|
||||
- 父任务进入 `failed / budget-exhausted / cancelled` 时,向所有非终态子实例写取消 tombstone;重复收束和恢复必须幂等,已开始或完成的 join continuation 不得被重新认领。
|
||||
- 动态实例不写 manifest,不出现在普通用户 Agent 列表;开发 Runtime 状态页可读取其状态。
|
||||
@@ -1163,6 +1163,189 @@ 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 合同已验收。
|
||||
|
||||
**V1.38 覆盖说明:** 本节关于“恢复、确认和每次 batch 读取都重新校验当前项目策略,live policy 漂移即进入 `needs-reconciliation`”的口径自 V1.38 起废止。V1.32 的既有真实 PASS 只保留为当时实现的历史证据;现行编码与恢复语义统一以 V1.38 的父 run 持久策略快照为准,不能用 V1.32 报告宣称 V1.38 已通过。
|
||||
|
||||
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 频率。
|
||||
|
||||
## V1.33 原生工具计划 repair 收敛与分类
|
||||
|
||||
V1.32 的真实基线是 `49` 次成功 native tool plan 对应 `30` 次格式修复。V1.33 不放宽动作、完成、权限或恢复门禁,只收敛 OpenAI-compatible Provider 的工具响应兼容边界,并让每次 repair 可以按稳定类别计数。
|
||||
|
||||
- `platform-llm` 的 Chat Completions 与 Responses content parts 只排除明确标记为 `reasoning / reasoning_content / analysis / thinking` 的内部推理 part;普通 `text / output_text` 继续进入可见正文,独立 `reasoning_content` 不提升为正文。不能因为响应同时包含 tool calls 就笼统丢弃 content。
|
||||
- Agent 原生工具解析可以移除完整、大小写不敏感且嵌套闭合的 `<think>...</think>` 块。对于不含 `respond_to_user` 和旧 `submit_agent_tool_plan` 的 native planning 响应,剩余普通文本只作为不具执行权的 `planner-commentary` 丢弃,以 function calls 作为权威动作;最终用户回复、legacy wrapper、未闭合 / 错配 thinking 标签仍按 `response-shape` 失败关闭。成功归一化的公共审计只保存固定 `complete-think-block / planner-commentary` kind、数量、原文本字符数和 SHA-256,不保存正文。
|
||||
- 工具计划错误使用固定类别 `response-shape / call-identity / unknown-function / arguments-json / arguments-schema / batch-constraint / plan-semantics / catalog-binding`。JSON 先递归遍历所有 object key,再做 schema 解析,既稳定区分语法与 schema,也拒绝顶层及任意嵌套 input 的重复字段。repair prompt 仍可在当前瞬时私有请求中使用过滤后的错误细节,Agent DB 与 E2E 报告只保存类别、计数和既有哈希;`catalog-binding` 属于本地目录冲突,必须直接失败,不能重问 Provider,也不能在成功 E2E 报告中出现非零计数。
|
||||
- OpenAI-compatible planning prompt 必须把原生 function schema 作为参数事实源。legacy text JSON schema 明确只供不支持 function tools 的 Provider 使用;所有动作函数参数统一为 `{"reason":"...","input":{...}}`,工具输入示例只描述 `input` 字段,禁止把 input 属性扁平到 arguments 顶层。
|
||||
- E2E 证据固定输出发生过 repair 的 loop 数、第二次 repair 数和按上述固定类别补零后的直方图;分类总和必须等于 repair 总数。完整报告和失败 partial report 使用同一聚合器,禁止输出单条错误、错误正文、preview、arguments、Agent/run/loop 身份或动态类别。
|
||||
- 确定性验收覆盖 standalone reasoning、reasoning content part、可见 content、完整 / 嵌套 / 未闭合 thinking block、非最终 commentary、最终回复正文冲突、重复 JSON 字段、八类错误、repair 审计零正文和报告聚合闭合。真实验收继续复用 V1.32 `supervisor-swarm-collaboration-policy-mixed-recovery`,在相同正式路由和隔离 AppData 口径下对比 `49 / 30` 基线,并同时检查总耗时、唯一 lifecycle、零重复、零泄漏和现场清理。
|
||||
|
||||
2026-07-18 第一轮诊断在旧保守正文规则下形成 `35` 次成功计划与 `23` 次 `response-shape` repair,比例未比 V1.32 下降;同时新 E2E 白名单遗漏 Agent DB 固有的 `schemaVersion / updatedAt`,把 58 条安全审计误判为 payload leak,因此该轮 **FAIL** 且不作为完成证据。第二次尝试在 2 次计划、0 repair 时因 Provider 给出的 isolated child write scope 不满足业务 fixture 提前停止,同样不作为完成证据。
|
||||
|
||||
最终代码对应的正式 `gpt-5.5 / openai_chat / high` 独立轮 **PASS**,总耗时 `631.6s`(约 10 分 32 秒)。46/46 次成功计划全部使用 `native_runtime_tools`,格式 repair 为 `0`,八类 repair 直方图全为 `0`;Provider lifecycle 从 V1.32 基线的 86 降为 54,started / terminal 均为 54,其中 53 completed、1 次瞬态失败通过新的 request identity 显式重试恢复,wrapper/text fallback 和协议审计 payload leak 均为 0。相同父 Session/run 完成 2 个 static delegate、1 个三 child isolated all-join、1 次业务 delivery repair、两类 Provider 真并行、pidfd Runner 强杀恢复、宿主验证和唯一 Supervisor assistant;重复 delivery/group/instance/result/join/claim/message/action/receipt/lifecycle、残留 sidecar、私密正文、API Key、项目 / 正式配置路径及报告泄漏均为 0,隔离 AppData 与 disposable 项目完整清理。
|
||||
|
||||
## V1.34 动态隔离子 Agent writeScopes 命令绕过封堵
|
||||
|
||||
V1.34 修补动态 isolated child 的作用域绕过面。`writeScopes` 当前只对结构化文件工具和 `project.patchset` 的目标路径做确定性校验,而 V1.11 的 workspace-write OS sandbox 会把项目根整体挂为可写;因此 `project.verify`、通用命令、持久进程或预览启动一旦交给 child,shell、构建脚本、生命周期 hook 和后代进程仍可能写到自身 `writeScopes` 之外。用户确认只能批准动作,不能把项目根全局可写变成 scope-aware 隔离。在 scope-aware OS sandbox 完成并通过独立门禁前,本节覆盖 V1.11、V1.12 和第 4 节中较宽的 child 工具继承口径;静态专业 Agent 与父 Agent 的既有权限不因本节改变。
|
||||
|
||||
### 有效工具边界
|
||||
|
||||
- Runtime 识别出动态 isolated child 后,有效策略快照把 `project.verify`、`project.git_commit`、`command.exec`、`command.start`、`command.stdin`、`preview.start`、`agent.delegate`、`agent.spawn_isolated`、`project.restore`、`agent.schedule_ready`、`canvas.asset_generate`、`task.create`、`task.update`、`blackboard.write` 和 `mcp.call` 显示为 `denied`。动态 MCP function 最终归一为 `mcp.call` 后同样拒绝。模板 Agent、项目 policy、legacy 空策略和用户 approval 都不能放宽这组边界。
|
||||
- child 继续可使用固定受限且不接受任意 program、argv 或 shell 的 `command.run_limited`,当前只允许只读验证 `game.static_smoke`;`command.output_read` 只回读本 child 有权访问的既有命令输出,`command.poll / command.terminate` 只观察或收束已绑定同一 child/run 的既有进程,不得启动、接管、重连或向进程写 stdin。`preview.validate` 继续只验证当前授权项目的精确既有 loopback 预览,不负责启动预览服务。
|
||||
- 项目内容写入只保留 `file.write / file.patch / file.delete / project.patchset`。每一个 create/update/delete 目标都必须经过现有私有路径、链接与规范化校验,并完整落在该 child 的有效 `writeScopes` 内;patchset 中任一目标越界时整组修改在 checkpoint、revision 和真实文件写入前失败。其它未在本节列出的工具继续遵守既有 isolated child 限制和有效 policy,本节不新增能力。
|
||||
- child 修改后可用符合固定合同的 `command.run_limited` 形成当前静态试玩凭证;需要通用构建、测试或项目级验证时,由父 Agent 或静态专业 Agent 在认领 child 交付后执行。`preview.validate` 仍只提供浏览器证据,不单独签发项目 revision 验证门禁。child 不得伪造 `verifiedRevision`,也不得借 `project.verify / command.exec` 绕过作用域。
|
||||
|
||||
### 拒绝、批次与恢复顺序
|
||||
|
||||
- 单个新 action 在 durable child 身份核对后、项目 policy 确认和 OS launcher 之前执行 scope 校验;拒绝结果不进入 `waiting-for-confirmation`,不创建进程、revision 或项目副作用。
|
||||
- 新的 2-3 action Provider batch 在确定 confirmation 模式前逐项执行同一 scope 校验。任一成员被拒绝时,batch 直接写成 `aborted / nextActionIndex=0`,不发布独立 pending-action sidecar,不创建 confirmation,也不执行同批其它成员;安全 batch 事实仍保留用于恢复和审计。
|
||||
- 旧 `pending / approved` 动作即使已由旧版本显示或确认,真正进入执行器时仍会重新调用当前 scope 校验,approval 不能穿透;旧 Provider batch 到达对应成员时也应用同一边界。旧 `executing` 且没有可信终态的通用工具继续沿用既有 `needs-reconciliation` 规则,Runtime 不会自动 replay。已有 child-owned process 只允许通过保留的 `command.poll / command.terminate` 观察和清理。
|
||||
|
||||
### 确定性验收
|
||||
|
||||
新增 `runtime_v134_isolated_child_unscoped_commands_cannot_bypass_write_scopes`,用真实恶意 `bash -lc` 参数尝试写入 sibling scope,覆盖单动作、两动作 batch、策略快照和旧 executing pending 的执行器重验;断言 sibling 文件不存在、nested delivery 为 0、独立 pending sidecar 不存在且 project revision 保持不变。`isolated_agent::tests::write_tools_stay_inside_instance_scopes_and_dangerous_tools_are_denied` 逐项覆盖完整拒绝集合和保留工具。回归范围同时运行 `isolated` 30 项、`project_supervisor_mixed_` 3 项、`supervisor_collaboration_` 27 项与 `provider_action_batch_` 12 项。
|
||||
|
||||
V1.31 与 V1.32 的真实 Provider suite 已分别证明 mixed static/isolated 协作、all-join、Runner 恢复和唯一收束,且验收中的 isolated child 项目 mutation 为 `0`。V1.34 只收紧 child 的本地工具能力,不改变 Provider 请求、委派合同或回复协议,因此本切片不重跑两套外部 Provider;后续只要改变 child prompt、任务、协作拓扑或写入语义,就必须重新建立真实 Provider 证据。
|
||||
|
||||
后续只有 scope-aware OS sandbox 能把 child 的有效 `writeScopes` 转换为 OS 强制边界,保证项目根其余部分只读、链接和挂载不能逃逸、所有 shell/构建器/hook/后代进程继承同一限制,并通过跨平台越界写与恢复测试后,才可在新的版本决策中重新评估 `project.verify / command.exec / command.start / command.stdin / preview.start`。scope-aware sandbox 是重新开放命令的必要条件而非自动授权;`project.git_commit`、委派、共享控制面写入、素材生成和 MCP 仍需各自的独立安全决策,模板或项目 policy 不得提前开放。
|
||||
|
||||
## V1.35 多 ready isolated all-join 原子认领与恢复
|
||||
|
||||
同一父 run 的一次 `agent.run_status` 可能同时看到多个 ready isolated all-join。V1.35 把这些 group 收口到同一个 action 级认领协议,避免前一个 join 已发生 delivery mutation、后一个 join 因锁竞争失败而留下半完成认领。
|
||||
|
||||
### 全锁预取与持久认领
|
||||
|
||||
- Runtime 先按 `delegationGroupId` 对当前父 run 的 ready all-join 去重排序,再按该稳定顺序一次性预取全部 join delivery 锁。只有全部锁均已取得,才允许创建 durable claim journal 或改写任一 delivery。
|
||||
- 任一后续 join 锁忙时,必须释放本次已经取得的锁并保持零 delivery mutation、零 claim sidecar;不得先认领先序 group,也不得留下可被恢复流程误判为部分提交的 journal。
|
||||
- 全锁就绪后,以当前 `actionId` 和完整有序 group 集合创建同一个 durable claim journal,并按 `prepared -> committed -> observed` 单向推进。`prepared` 后发生部分 delivery commit 或 Runner 退出时,恢复必须复用同一 action journal、按相同锁顺序幂等补齐尚未提交的 group,再推进到 `committed`;不得生成新 action、新 claim sidecar 或重复认领已经绑定的 delivery。
|
||||
- Runtime 只认领能够完整放入本轮 `readyIsolatedJoins` 私有观察预算的有序前缀,剩余 ready group 保持未认领并由后续 action 继续取得;`readyIsolatedJoins` 固定置于 `agent.run_status` detail 首部,不能再被普通状态、claimed 目录或 mixed static receipt 截掉。单个 group 已超过完整观察上限时在任何 claim mutation 前失败关闭。
|
||||
|
||||
### Observation、完成门禁与审计
|
||||
|
||||
- `committed` 只表示全部 delivery 已绑定当前 action,不表示模型已经观察结果。只有成功 `agent.run_status` observation 已持久写入该 action 的 pending sidecar 后,claim journal 才能标记为 `observed`;observation 或 sidecar 持久化失败时保持未观察状态并由同一 action 恢复补齐。
|
||||
- 若 isolated claim 已 `committed`,但同一次 mixed `agent.run_status` 随后的 static receipt 认领失败,下一次带新 actionId 的 `agent.run_status` 必须先完整重放旧 claim 的 ready 结果,再继续认领 static receipt;旧 delivery 和 journal 仍绑定原 action,不为恢复 action 新建第二份 isolated claim。只有这次恢复 observation 持久化成功后,旧 claim 才能转为 `observed`。
|
||||
- 完成门禁要求每个 `claimed-by-parent` delivery 都被同一 `claimedByActionId + delegationGroupId` 的 journal 覆盖;旧版本遗留的无 journal delivery 继续计入 `unjournaledClaimedGroups`,不能仅凭 delivery 已 claimed 清除门禁。`agent.run_status` 先重放已有未观察 claim;没有未观察 claim 时,每轮只按稳定 action 顺序为一个旧 `claimedByActionId` 合成 journal 并完整重放,恢复 action 不取得该 delivery,也不创建自己的 claim。多个旧 action 不得一次合并后超过观察预算。
|
||||
- 旧 delivery 对应的原 action 已存在但未覆盖该 group 的 journal 时失败关闭,不能扩写 journal、改变 group 集合或把 `Committed / Observed` 倒退为 `Prepared`;同一 group 出现在其他 action journal 时按身份冲突处理。只有 observation 中完整出现的 group 集合可推进对应 claim 为 `observed`,不能顺带标记本轮未输出的其它 claim。
|
||||
- 同一父 run 仍存在任一未 `observed` 或无 journal 的 claimed delivery 时,finalization 必须继续失败关闭,不能写入最终 assistant。
|
||||
- 每个 group 的认领审计以 `actionId + delegationGroupId` 为唯一键;部分提交恢复、Runner 重启和 observation 重投影都只能补齐缺失审计,不能为同一 action/group 追加重复记录。该幂等追加必须在 Agent DB append 锁内先修复 JSONL 截断尾行,再从文件头扫描有效数据库的完整记录范围并核对既有 payload;尾行撕裂、重复键或内容冲突都不能绕过唯一性。
|
||||
|
||||
### 定向验收与边界
|
||||
|
||||
2026-07-18 定向验收新增“后一个 join 锁冲突”“isolated 已提交、后续 static delivery 锁失败后由新 action 完整重放”“Agent DB torn tail 后 prepared/partial claim 重放”和“多旧 action 逐轮迁移”回归,并覆盖已有 journal 不扩写/不倒退、跨 action group 归属冲突与 mixed partial claim 恢复;完整 observation 必须实际包含被标记 observed 的全部 `delegationGroupId`。`isolated` 36/36、`project_supervisor` 42/42、`supervisor_collaboration` 27/27、`provider_action_batch` 12/12 已通过,Tauri/Rust 全量为 915 passed、4 个环境依赖用例按设计 ignored。以上定向结果只证明本地协议回归,不能单独替代外部模型链路;后续真实 Provider 结论如下。
|
||||
|
||||
### 真实 Provider E2E
|
||||
|
||||
2026-07-18 后续真实 Provider E2E **PASS**。同一父 Session/run 先创建含 2 个 child 的初始 isolated all-join group;首次 `parent-wake` 后、任何 join claim 前,再创建含 1 个 child 的 follow-up group。两组的精确 `writeScopes` 集合互不重叠,最终由同一个状态为 `observed` 的 join claim journal 同时覆盖两个 group。Runner 强杀/恢复前后身份稳定;Provider lifecycle `53/53` 全部 completed、failed 为 `0`,重复、泄漏与残留均为 `0`。验收器自测同时覆盖跨 group scope 不重叠及 `parent-wake < follow-up spawn < first claim` 时序。该结果关闭 V1.35 的真实 Provider 验收缺口,但只证明当前业务合同与 Supervisor 提示形成了该轨迹;Runtime 不会预知尚未生效的项目检查,若产品要求通用强制阶段,仍需先扩展 collaboration policy 契约。该结果也不扩大解释为 scope-aware OS sandbox 或其它独立门禁已经通过。
|
||||
|
||||
V1.35 只收紧多个 ready isolated all-join 的认领原子性、恢复和完成门禁,不等于 V1.34 所述 scope-aware OS sandbox 已落地。该 sandbox 仍未完成,V1.34 对动态 isolated child 的命令及其它高风险工具禁用边界继续有效。
|
||||
|
||||
## V1.37 分阶段 isolated group 首次认领硬门禁
|
||||
|
||||
V1.35 已证明模型可以先建立一个 isolated group,再在首次 join claim 前补齐后续 group,但该顺序此前只由任务合同和 Supervisor 提示约束。V1.37 把“达到项目要求的 isolated group 数量后才能首次认领”下沉为 Runtime 硬门禁,同时保持既有 policy、恢复路径和旧项目兼容。
|
||||
|
||||
### Policy 兼容与分阶段门禁
|
||||
|
||||
- collaboration policy v1 新增可选 `minIsolatedGroupsBeforeClaim`,默认值为 `0`,上限为 `16`。零值序列化时省略,因此旧 policy fingerprint 与旧 collaboration contract fingerprint 保持不变;未配置项目继续沿用原行为。
|
||||
- initial preflight 与 initial contract 仍只校验既有首波协作要求,不提前要求最终 group 总数,因此首批只创建 1 个合法 group 仍可通过。finalization completion 在既有门禁之外,再校验同一父 run 已建立的 isolated group 总数达到 policy。
|
||||
- 首次新 claim 必须先按既有观察预算选定可完整输出的 ready group 批次,再同时校验已建立的 durable isolated group 数和该批 ready group 数均达到 `minIsolatedGroupsBeforeClaim`。任一不足都必须在 claim journal 创建和 delivery mutation 前失败关闭。
|
||||
- 恢复优先于升级门禁:已有 durable claim、尚未观察的 claim,以及 legacy claimed delivery / legacy claim 的恢复与重放继续按原身份推进,不重新套用首次新 claim 数量门禁,避免升级后把历史状态卡死。
|
||||
|
||||
### Read-only scope 与验收
|
||||
|
||||
只读 isolated task 也必须提供 expected artifact 对应的最小目录 scope。该 scope 只能覆盖完成预期产物所需的最窄目录,不得为了只读检查扩大到 sibling scope 或共同父目录;通用 Supervisor 提示必须明确这一边界,不能依赖单个 E2E fixture 的特例文案。
|
||||
|
||||
2026-07-18 定向回归中,`supervisor_collaboration_policy_` 23/23、`project_supervisor_` 47/47 全部通过,E2E self-test **PASS**;Tauri/Rust 全量为 930 passed、4 个环境依赖用例按设计 ignored。
|
||||
|
||||
真实 Provider 第一次独立运行因模型给出的初始 child scope 不满足 expected artifact 最小边界而 **FAIL**;验收确认 child、claim 和 project mutation 均为 `0`,现场自动清理,该失败证据不得与后续运行拼接。补强通用 scope 边界提示后的第二次独立运行 **PASS**:报告记录 policy=`2`、2 个 group / 3 个 child、1 个 `observed` join claim 覆盖 2 个 group;Runner 强杀恢复前后身份稳定,Provider lifecycle `64/64` completed、failed=`0`,重复、残留与泄漏均为 `0`,且只产生唯一最终回复。
|
||||
|
||||
## V1.38 父 run 协作策略持久快照、绑定记录与漂移隔离
|
||||
|
||||
V1.38 把 collaboration policy 的执行语义从“每次动作或恢复都读取项目级 live policy”改为“首个有效 durable collaboration batch 为父 run 固化策略”,并用独立 binding sidecar 持久证明“该 run 曾绑定”。本节覆盖 V1.32 的 live drift reconciliation 旧口径,不改变 V1.34 的 isolated child 能力边界、V1.35/V1.36 的 claim/observation 完整性或 V1.37 的首次认领数量门禁。
|
||||
|
||||
### 线性化点与写入顺序
|
||||
|
||||
- 同一 `project-supervisor` 父 run 的**首个非 `aborted` durable collaboration batch** 是策略线性化点。这里的 collaboration batch 必须是携带完整 v2 `collaborationContract` 的 Provider action batch;尚无 binding 时,只有 `aborted` 事实的批次不绑定快照,也不阻止后续合法批次选择届时有效的项目策略。
|
||||
- 固定提交顺序为:完整预检并构造 v2 batch/contract -> 先持久化 `status != aborted / nextActionIndex=0` 的 batch -> 在父 run 快照锁内 CAS 写入并验真 collaboration policy snapshot -> CAS 写入并验真独立 binding sidecar -> 两者一致后才允许任何成员进入会产生副作用的 dispatch。delivery、isolated group/child、新 join/receipt claim、项目 mutation、MCP 调用和 finalization 都不得出现在完整绑定之前;没有既存 binding 的首次 `aborted` batch 不创建 snapshot 或 binding。
|
||||
- batch 已落盘而 snapshot 尚未落盘、以及 snapshot 已落盘而 binding 尚未落盘,都是受支持且保持零 action 副作用的崩溃窗口。前者只能从已完整验真的 v2 contract 补绑,后者只能从有效 snapshot 补写完全一致的 binding;恢复完成后才从原 batch cursor 继续。
|
||||
- snapshot 一旦存在就是该父 run 的不可变策略事实源;binding 是独立的“曾绑定”持久记录和防降级屏障,不承载 policy 本体。后续 static/isolated spawn、repair、新 claim、Supervisor mutation/MCP 门禁和 finalization 只能使用有效 snapshot,不能在同一 run 重新选择或升级 policy,也不能通过删除 snapshot 把已绑定 run 伪装成未绑定 run。
|
||||
|
||||
### Snapshot schema 与指纹
|
||||
|
||||
快照固定写入 `.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json`。snapshot v1 固定且完整的字段集合为 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`,不得缺少字段或接受未知字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "game-creator-supervisor-collaboration-policy-snapshot.v1",
|
||||
"projectId": "project identity",
|
||||
"parentAgentId": "project-supervisor",
|
||||
"parentRunId": "parent run identity",
|
||||
"boundFrom": "initial-collaboration-batch",
|
||||
"policy": {},
|
||||
"policyFingerprint": "sha256",
|
||||
"snapshotFingerprint": "sha256",
|
||||
"boundAt": 0
|
||||
}
|
||||
```
|
||||
|
||||
- `policy` 必须先经过 V1.32/V1.37 的完整规范化和上限校验;`policyFingerprint` 只对规范化 policy 的稳定序列化计算 SHA-256。
|
||||
- `snapshotFingerprint` 对稳定身份字段计算 SHA-256,必须绑定 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint`,不包含审计时间 `boundAt`。读取时逐字段复核当前项目身份和调用方 parent 身份,任何未知 schema、未规范 policy、非法指纹、跨项目或跨 run 内容都失败关闭。
|
||||
- `boundFrom=initial-collaboration-batch` 用于正常线性化,`boundFrom=legacy-provider-batch-contract` 用于从可信 v2 contract 恢复。`boundFrom=legacy-current-project-policy` 仅允许没有 snapshot/binding、没有可信 v2 contract,且不存在 contractless/v1 collaboration batch,并由 durable run 身份与状态明确证明仍处于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;terminal、`needs-reconciliation` 或身份/状态未知 run 的状态读取不得新建 snapshot。既有 snapshot 重读保持首次 `boundFrom / boundAt / snapshotFingerprint`,不得按本次调用重写。
|
||||
|
||||
### Binding sidecar 与安全路径键
|
||||
|
||||
独立 binding 固定写入 `.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json`,只保存不可变绑定身份,不复制 policy:
|
||||
|
||||
```json
|
||||
{
|
||||
"schemaVersion": "game-creator-supervisor-collaboration-policy-snapshot-binding.v1",
|
||||
"projectId": "project identity",
|
||||
"parentAgentId": "project-supervisor",
|
||||
"parentRunId": "parent run identity",
|
||||
"boundFrom": "initial-collaboration-batch",
|
||||
"policyFingerprint": "sha256",
|
||||
"snapshotFingerprint": "sha256",
|
||||
"boundAt": 0
|
||||
}
|
||||
```
|
||||
|
||||
- binding 与 snapshot 必须在同一父 run 锁内逐字段交叉验证 `projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`。已有有效 snapshot 但没有 binding 时可从 snapshot 幂等补写;binding 一旦存在不得删除、替换或按 live policy 重建。
|
||||
- `agentKey / runKey` 不能只做可能碰撞的 lossy 字符替换。原始 ID 本身满足安全路径组件规则时可原样使用;否则使用有界可读安全前缀加原始完整 ID 的稳定 SHA-256,例如 `<safe-prefix>--<sha256(raw-id)>`,保证 `a/b`、`a\\b`、`a_b` 等不安全 run ID 不会落到同一路径。snapshot 与 binding 必须使用同一映射。
|
||||
- 锁 key 不复用规范化后的路径片段,固定对完整 `parentAgentId + NUL + parentRunId` 计算稳定 SHA-256。路径 key 或锁 key 的计算不能读取 live policy,也不能因进程重启、平台路径分隔符或 locale 改变。
|
||||
|
||||
### 锁内 CAS 与恢复优先级
|
||||
|
||||
- 创建或读取 snapshot/binding 必须在按完整 `projectId + parentAgentId + parentRunId` 身份隔离的同一锁内完成 CAS。锁内先重读两份 durable 记录:两者一致时返回首次记录并视为幂等;policy、项目、父 run 或任一绑定字段冲突都失败关闭,通用原子 replace 不能覆盖已经绑定的 snapshot/binding。
|
||||
- 恢复优先级固定为:**existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 project policy**。最后一层不是一般回退,只允许无 snapshot/binding 且身份可信、状态明确属于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;existing snapshot 与 v2 contract 不一致时是同一父 run 的 durable 身份冲突,进入 reconciliation,不得以 live policy 裁决哪份更新。
|
||||
- 从 v2 contract 迁移前必须先完成不依赖 snapshot 的两阶段校验:batch v2 schema、project/Agent/run 身份、action 成员与顺序、batchId、contract schema、规范 policy、policy fingerprint 和 contract fingerprint 全部成立。不得先把未验 contract.policy 写成 snapshot,再让后续 batch 校验失败。
|
||||
- snapshot 存在而 binding 缺失时,从有效 snapshot 补写 binding;binding 存在而 snapshot 缺失时,只能用完整验真的 v2 contract 按 binding 中的首次身份恢复 snapshot。matching binding 已存在时,完整验真的 `aborted` v2 contract 也可用于重建原 snapshot,因为这是恢复既有绑定而不是建立新绑定。binding 与恢复结果不一致、binding 损坏,或者 binding 已证明曾绑定而 snapshot 丢失且没有可信 v2 contract 时,都保持零新副作用并失败关闭,禁止按 live policy 重绑。
|
||||
- durable collaboration batch 的 contractless/v1 形态必须先失败关闭并进入 `needs-reconciliation`,不能跳过旧 batch、读取 live policy 后把 run 当成 fresh run;不含协作动作的 contractless/v1 batch 不受该门禁误伤。没有 snapshot/binding、没有上述旧协作 batch且没有可信 v2 contract 时,只有 durable task/run ledger 能同时证明父 run 身份和 `pending / running / waiting-for-confirmation / waiting-for-user-input` 状态,才可用 `legacy-current-project-policy` 迁移;terminal、`needs-reconciliation` 或身份/状态未知只允许无副作用读取状态。尚无任何 durable run/collaboration 事实的真正新父 run可在首批 planning/preflight 读取当前 project policy,并把它固化进首个 v2 contract,但这不创建 legacy snapshot。
|
||||
|
||||
### 绑定后的统一读取与漂移语义
|
||||
|
||||
- 绑定后,planning prompt、batch preflight/validation、后续 spawn、`agent.run_status` 新 claim、Supervisor mutation、破坏性 MCP 判断、completion blocker、finalization 创建与 finalization 恢复都必须通过同一个 effective-snapshot resolver 取得有效 snapshot,并核对 matching binding。项目级 `.agent/collaboration-policy.json` 不再参与这些执行裁决。
|
||||
- global project policy 相对 snapshot 的状态只允许报告 `matched / drifted / unreadable`:当前有效 policy 与 snapshot 相同为 `matched`,有效但不同为 `drifted`,读取或解析失败为 `unreadable`。三者只进入有界状态与诊断元数据;planning prompt 继续渲染 snapshot 中的规范 policy 本体,不改变现有 JSON 外形。漂移状态不能改变后续 spawn、claim、mutation、MCP、finalization 或 batch replay,也不能制造 project revision、verification 或 reconciliation。
|
||||
- policy 更新只供后续新父 run 在其首个非 aborted collaboration batch 选择;已有 snapshot 的 run 不原地重绑。需要应用新策略时必须创建新父 run,不能通过删除 snapshot、重写 batch 或 status 查询迁移活跃 run。
|
||||
- durable claim 恢复优先于 effective snapshot 解析和新 claim 门禁。已有 `Prepared / Committed / Observed` static 或 isolated claim、尚未观察 claim 及 legacy claimed delivery,允许先按原 action/group 身份幂等重放或补齐;global policy 漂移、snapshot 缺失或 binding 故障都不能把已经提交的 claim 卡成第二次认领。恢复路径不得取得新的 delivery。只有创建新 claim 时,才必须先成功解析 effective snapshot 并核对 binding;解析失败必须发生在新 claim journal、delivery 锁与 delivery mutation 之前,成功后再执行 V1.35-V1.37 的全锁、预算、完整观察和 group 数量门禁。
|
||||
|
||||
### 本轮验证状态
|
||||
|
||||
- 2026-07-19 确定性门禁已完成:E2E self-test **PASS**,同时覆盖 modern `provider_action_batch.confirmation_required` 与 legacy `tool_confirmation_required`、requirement/approval/receipt 唯一性、`confirmation` execution mode、目标 Session/run 和严格持久化顺序;snapshot/binding 的终态预期改为由已验真的非 `aborted` v2 collaboration batch 决定,不再用 mixed suite 拓扑代替 durable 事实。`supervisor_collaboration_` 52/52、`provider_action_batch_` 12/12、`project_supervisor_mixed_` 5/5 通过;Tauri/Rust 全量为 949 passed、4 个环境依赖用例按设计 ignored,`check:rustfmt` 通过。
|
||||
- 确定性覆盖包含 snapshot 的 9 个完整字段、首次 `aborted` 零绑定与 matching binding 的 `aborted` v2 恢复、batch -> snapshot -> binding 双故障窗口、CAS 冲突、篡改 contract、binding/snapshot 丢失组合、contractless/v1 协作批次失败关闭与非协作批次兼容、legacy 非终态迁移与终态拒绝、危险 Agent/run ID 路径及锁隔离、global policy 漂移、已有 claim 恢复与新 claim 失败关闭。新增 `supervisor_collaboration_policy_snapshot_survives_terminal_runtime_cleanup` 证明终态只删除 pending/provider batch/confirmation 等临时 sidecar,snapshot/binding 字节保持不变且 resolver 继续返回 `run-snapshot`。
|
||||
- 真实 `supervisor-swarm-static-isolated-autonomous-chat` 曾在单次独立运行中完整形成 2 个 isolated group / 3 个 child、1 个 observed join claim 覆盖两组、唯一 repair、宿主验证、Runner pidfd 强杀恢复和唯一 Supervisor assistant;snapshot/binding 均为唯一、字节及字段稳定,global policy drift 被观察,重复、临时 sidecar、正文、API Key、项目路径和配置路径泄漏均为 0。但该轮运行期间正式客户端在测试外部重启了正式 Runner,source endpoint 所有权门禁按设计失败,因此该功能样本不能记为 PASS。
|
||||
- 随后使用权限为 `0700/0600`、不含 endpoint/锁/会话的私有配置源副本隔离正式客户端干扰,source endpoint、源目录和清理门禁均稳定;五次最小 OpenAI-chat 探针全部 HTTP 200。然而多次独立完整运行仍在长链路耗尽 transient Provider retry。最后一轮隔离 overlay 已提高到 `requestTimeoutMs=300000 / maxRetries=3 / retryBackoffMs=500`,仍在首个业务批次前形成 4 个 failed lifecycle / 3 个 retry 后终止,child、delivery、claim 和项目 mutation 均为 0,现场清理与泄漏门禁通过。失败轮不得与前述功能完整轮拼接;截至当前,**V1.38 独立真实 Provider E2E 仍未 PASS**,需在外部 Provider 稳定后以最终代码重新独立运行。
|
||||
|
||||
## 验收命令
|
||||
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml structured_plan_ -- --nocapture`
|
||||
@@ -1172,7 +1355,11 @@ Runner pidfd 强杀后的 boot、父 context、pending action、两类 durable i
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml mcp_ -- --nocapture`
|
||||
- `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 runtime_v134_ -- --nocapture`
|
||||
- `cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml isolated -- --nocapture --test-threads=1`
|
||||
- `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`
|
||||
|
||||
@@ -595,3 +595,16 @@ 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 章节与共享决策记录为准。
|
||||
- 2026-07-18 起,同一 Runtime 文档的“V1.34 动态隔离子 Agent writeScopes 命令绕过封堵”作为 isolated child 的现行能力事实源。在 scope-aware OS sandbox 完成前,动态 child 无条件禁用 `project.verify / project.git_commit / command.exec / command.start / command.stdin / preview.start / agent.delegate / agent.spawn_isolated / project.restore / agent.schedule_ready / canvas.asset_generate / task.create / task.update / blackboard.write` 和全部 MCP;原生工具策略统一显示 `denied`,模板、项目 policy 与用户确认均不能放宽。保留固定只读 `command.run_limited`、同身份 `command.output_read / command.poll / command.terminate`、既有预览的 `preview.validate`,以及严格位于 `writeScopes` 内的 `file.write / file.patch / file.delete / project.patchset`。
|
||||
- V1.34 的新单动作在 confirmation 和 OS launcher 前拒绝;新多 action 原生 batch 只要含一个 denied member 就在独立 pending-action sidecar、confirmation、OS spawn、revision 和任何成员项目副作用前整批 abort,只保留 `aborted / nextActionIndex=0` batch 事实。旧 pending / approval / batch 真正进入执行器时仍重新应用当前 child 边界,旧 executing 未知结果继续进入既有 reconciliation。该安全收紧由恶意 sibling 写入、策略快照、batch、旧 pending 执行器重验和 isolated/mixed/collaboration/provider-batch 回归证明;不因本切片重跑已通过且 isolated mutation 为 0 的 V1.31/V1.32 外部 Provider suite。通用命令只有在后续 scope-aware OS sandbox 对所有后代强制同一 `writeScopes` 并通过独立决策与测试后才可重新评估开放。
|
||||
- 2026-07-18 起,同一 Runtime 文档的“V1.35 多 ready isolated all-join 原子认领与恢复”作为 `agent.run_status` 同父 run 多 group 认领的现行事实源。Runtime 按 `delegationGroupId` 排序并一次性预取全部 join 锁;任一后续锁忙时保持零 delivery mutation、零 claim sidecar。全锁就绪后,同一 action 的 durable claim journal 按 `prepared -> committed -> observed` 推进;部分 commit 或 Runner 恢复只能复用该 journal 幂等补齐。只认领可完整放入优先 `readyIsolatedJoins` 观察预算的有序前缀,未观察旧 claim 可由后续 action 完整重放,但不创建第二份 isolated claim。每个 claimed delivery 必须由匹配原 action/group 的 journal 覆盖;无 journal 的旧 delivery 每轮只迁移一个原 action,已有 journal 不得扩写或状态倒退,跨 action group 归属冲突失败关闭。成功 observation 写入 pending sidecar 后只能把本轮完整输出的 claim 标记 `observed`,任一未观察或无 journal claim 继续阻断 finalization;每个 group 审计按 `actionId + delegationGroupId` 唯一,并在 Agent DB 锁内修复 torn tail、全量核对后幂等追加。
|
||||
- V1.35 定向验收覆盖后一个 join 锁冲突、mixed static 锁失败后新 action 重放 isolated 结果、Agent DB torn tail 后 prepared/partial claim 恢复、多旧 action 逐轮迁移、已有 journal 单调性与跨 action group 归属冲突;`isolated` 36/36、`project_supervisor` 42/42、`supervisor_collaboration` 27/27、`provider_action_batch` 12/12 已通过,Tauri/Rust 全量为 915 passed、4 个环境依赖用例按设计 ignored。以上定向结果只证明本地协议回归,真实 Provider 结论见下一条。该协议不等于 V1.34 的 scope-aware OS sandbox 已完成;后者仍未完成,动态 isolated child 的现行禁用边界保持不变。
|
||||
- 2026-07-18 V1.35 后续真实 Provider E2E **PASS**:同一父 Session/run 先创建含 2 个 child 的初始 isolated all-join group;首次 `parent-wake` 后、任何 join claim 前,再创建含 1 个 child 的 follow-up group。两组的精确 `writeScopes` 集合互不重叠,最终由同一个状态为 `observed` 的 join claim journal 同时覆盖两个 group;Runner 强杀/恢复前后身份稳定。Provider lifecycle `53/53` 全部 completed、failed 为 `0`;重复、泄漏与残留均为 `0`。V1.35 真实 Provider 门禁据此关闭;V1.36 的 static + isolated 混合 observation 完整性仍按独立门禁验收。
|
||||
- 2026-07-18 起,同一 Runtime 文档的“V1.36 混合协作 observation 完整性”补齐 `readyDelegateReceipts` 与 `readyIsolatedJoins` 同轮返回边界。静态回执完整 JSON 单批最多 6000 字符;isolated-only all-join 保持 10000 字符,和静态回执混合时降为 6000 字符;普通 Runtime 状态与 claimed 摘要最多占 3500 字符。完整 `agent.run_status` detail 仍以 16000 字符为硬上限,超过上限必须失败关闭,禁止先认领后静默截断证据。
|
||||
- V1.36 的静态回执先完整保留已绑定当前 action 的 recovery receipts,再按 `delegationId` 为新 ready delivery 选择稳定前缀;只预取本批 delivery 锁,并在锁内重读核对快照,超预算或未选中的后续 delivery 保持 `Ready`,其锁竞争也不能阻断必选恢复。必选集合本身无法完整放入时在写 claim sidecar 和改 delivery 前失败。pending observation 从 `readyDelegateReceipts` 解析唯一 delegationId 集合,只有与 durable claim receipts 精确相等且前置区块唯一、`ready=true` 时才允许 `Committed -> Observed`;缺失、额外、重复或无效 ID 均继续阻断 finalization。mixed 路径仍保留 isolated 先认领、static 后续失败可由下一 action 完整重放 isolated claim 的 V1.35 恢复顺序。定向回归为 `project_supervisor` 46/46、mixed 5/5、`isolated` 37/37、`supervisor_collaboration` 27/27、`provider_action_batch` 12/12;Tauri/Rust 全量为 923 passed、4 个环境依赖用例按设计 ignored。本切片未重跑真实 Provider,不把既有 PASS 扩大解释为 V1.36 已重新外部验收。
|
||||
- 2026-07-18 起,同一 Runtime 文档的“V1.38 父 run 协作策略持久快照、绑定记录与漂移隔离”覆盖 V1.32 的 live drift reconciliation 旧口径。首个非 `aborted` durable collaboration batch 是线性化点:v2 batch 必须先落盘,随后在任何 action 副作用前,以同一父 run 锁内 CAS 依次绑定 `.agent/runtime/collaboration-policy-snapshots/<agentKey>/<runKey>.json` 和独立 `.agent/runtime/collaboration-policy-snapshot-bindings/<agentKey>/<runKey>.json`;没有既存 binding 的首次 `aborted` batch 两者都不创建,matching binding 已存在时则可用完整验真的 `aborted` v2 contract 恢复缺失 snapshot。binding 是“该 run 曾绑定”的持久记录,不能通过删除 snapshot 把 run 降级为未绑定。
|
||||
- snapshot v1 固定且完整包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policy / policyFingerprint / snapshotFingerprint / boundAt`;snapshot fingerprint 绑定除 `snapshotFingerprint / boundAt` 外的全部稳定字段。binding v1 固定包含 `schemaVersion / projectId / parentAgentId / parentRunId / boundFrom / policyFingerprint / snapshotFingerprint / boundAt`,必须与 snapshot 逐字段一致。安全 ID 可原样作 key;不安全 Agent/run ID 必须使用有界安全前缀加原始 ID 稳定 SHA-256,锁 key 对完整父 Agent/run 身份计算稳定指纹,禁止 lossy 规范化碰撞。
|
||||
- 恢复优先级为 existing valid snapshot > 完整验真的 v2 batch contract > 符合严格状态门禁的 legacy 当前有效 policy。snapshot 存在但 binding 缺失时可从 snapshot 补写;binding 存在但 snapshot 丢失时只允许可信 v2 contract 按首次身份恢复,没有可信 v2 contract 时禁止按 live policy 重绑。contractless/v1 collaboration batch 必须先失败关闭,不能伪装 fresh run。`legacy-current-project-policy` 仅允许无 snapshot/binding、无可信 v2 contract,且不存在上述旧 batch,并由 durable 身份和状态明确证明属于 `pending / running / waiting-for-confirmation / waiting-for-user-input` 的旧父 run;terminal、`needs-reconciliation` 或身份/状态未知 run 的状态读取不得新建 snapshot。
|
||||
- 已有 durable/未观察 claim 与 legacy claimed delivery 继续按原 action/group 身份恢复,不要求先创建新绑定;新 claim 必须先成功解析 effective snapshot 并核对 binding,再进入 V1.35-V1.37 的全锁、预算、完整 observation 和 group 数量门禁。snapshot 绑定后 global policy 的 `matched / drifted / unreadable` 只进入有界 status/诊断,不能改变后续执行;新 policy 只由后续新父 run 采用。2026-07-19 self-test、52/52 collaboration 定向回归和 949 passed/4 ignored Rust 全量已完成,终态快照保留也有独立回归;真实 mixed-swarm 功能样本已闭合但受正式 endpoint 外部重启污染,私有配置源的后续独立运行又连续耗尽 transient Provider retry,不能拼接证据,当前仍**不得声称 V1.38 真实 E2E 已 PASS**。详细报告以 Runtime 技术方案 V1.38 节为准。
|
||||
|
||||
@@ -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 三种消息角色。
|
||||
@@ -474,7 +476,6 @@ enum ChatCompletionsContent {
|
||||
#[derive(Deserialize)]
|
||||
struct ChatCompletionsContentPart {
|
||||
#[serde(rename = "type")]
|
||||
#[allow(dead_code)]
|
||||
part_type: Option<String>,
|
||||
#[serde(default)]
|
||||
text: Option<String>,
|
||||
@@ -513,7 +514,6 @@ struct ResponsesOutputItem {
|
||||
#[derive(Deserialize)]
|
||||
struct ResponsesOutputContentPart {
|
||||
#[serde(rename = "type")]
|
||||
#[allow(dead_code)]
|
||||
part_type: Option<String>,
|
||||
#[serde(default)]
|
||||
text: Option<String>,
|
||||
@@ -617,6 +617,8 @@ impl LlmConfig {
|
||||
max_retries,
|
||||
retry_backoff_ms,
|
||||
official_fallback: false,
|
||||
#[cfg(test)]
|
||||
raw_log_dir_override: None,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -625,6 +627,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 +1927,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 +1944,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,
|
||||
@@ -2154,6 +2171,7 @@ fn extract_responses_text(parsed: &ResponsesResponseEnvelope) -> Option<String>
|
||||
.output
|
||||
.iter()
|
||||
.flat_map(|item| item.content.iter())
|
||||
.filter(|part| !is_hidden_reasoning_part(part.part_type.as_deref()))
|
||||
.filter_map(|part| part.text.as_deref())
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
@@ -2232,6 +2250,7 @@ fn extract_content_text(content: &ChatCompletionsContent) -> Option<String> {
|
||||
ChatCompletionsContent::Parts(parts) => {
|
||||
let text = parts
|
||||
.iter()
|
||||
.filter(|part| !is_hidden_reasoning_part(part.part_type.as_deref()))
|
||||
.filter_map(|part| part.text.as_deref())
|
||||
.collect::<Vec<_>>()
|
||||
.join("");
|
||||
@@ -2241,6 +2260,16 @@ fn extract_content_text(content: &ChatCompletionsContent) -> Option<String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn is_hidden_reasoning_part(part_type: Option<&str>) -> bool {
|
||||
let Some(part_type) = part_type.map(str::trim) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
["reasoning", "reasoning_content", "analysis", "thinking"]
|
||||
.iter()
|
||||
.any(|hidden_type| part_type.eq_ignore_ascii_case(hidden_type))
|
||||
}
|
||||
|
||||
fn decode_utf8_stream_chunk(bytes: &[u8]) -> Result<(String, Vec<u8>), LlmError> {
|
||||
match std_str::from_utf8(bytes) {
|
||||
Ok(text) => Ok((text.to_string(), Vec::new())),
|
||||
@@ -2828,6 +2857,62 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_response_excludes_standalone_reasoning_fields_from_text() {
|
||||
let response = parse_chat_completions_response(
|
||||
LlmProvider::OpenAiCompatible,
|
||||
"fallback-model",
|
||||
r#"{"id":"chat_reasoning_fields","choices":[{"message":{"reasoning_content":"内部推理","reasoning":"内部分析","content":null,"tool_calls":[{"id":"call_noop","type":"function","function":{"name":"noop","arguments":"{}"}}]},"finish_reason":"tool_calls"}]}"#,
|
||||
)
|
||||
.expect("tool call should keep the response valid without visible content");
|
||||
|
||||
assert_eq!(response.text, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_response_filters_reasoning_parts_and_preserves_visible_parts() {
|
||||
let response = parse_chat_completions_response(
|
||||
LlmProvider::OpenAiCompatible,
|
||||
"fallback-model",
|
||||
r#"{"id":"chat_content_parts","choices":[{"message":{"content":[{"type":"reasoning","text":"内部推理"},{"type":"analysis","text":"内部分析"},{"type":"reasoning_content","text":"内部推理补充"},{"type":"thinking","text":"内部思考"},{"type":"text","text":"可见"},{"type":"output_text","text":"答案"}]},"finish_reason":"stop"}]}"#,
|
||||
)
|
||||
.expect("visible chat content parts should parse");
|
||||
|
||||
assert_eq!(response.text, "可见答案");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn chat_response_preserves_visible_content_with_tool_calls() {
|
||||
let response = parse_chat_completions_response(
|
||||
LlmProvider::OpenAiCompatible,
|
||||
"fallback-model",
|
||||
r#"{"id":"chat_visible_tool_call","choices":[{"message":{"content":[{"type":"analysis","text":"内部分析"},{"type":"text","text":"先检查项目。"}],"tool_calls":[{"id":"call_project_index","type":"function","function":{"name":"project_index","arguments":"{\"path\":\"/tmp/game\"}"}}]},"finish_reason":"tool_calls"}]}"#,
|
||||
)
|
||||
.expect("chat response with visible content and tool calls should parse");
|
||||
|
||||
assert_eq!(response.text, "先检查项目。");
|
||||
assert_eq!(
|
||||
response.tool_calls,
|
||||
vec![LlmToolCall {
|
||||
id: "call_project_index".to_string(),
|
||||
name: "project_index".to_string(),
|
||||
arguments: r#"{"path":"/tmp/game"}"#.to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn responses_response_filters_reasoning_parts_and_preserves_output_text() {
|
||||
let response = parse_responses_response(
|
||||
LlmProvider::OpenAiCompatible,
|
||||
"fallback-model",
|
||||
r#"{"id":"responses_content_parts","output":[{"type":"message","content":[{"type":"analysis","text":"内部分析"},{"type":"output_text","text":"最终答案"}]}],"status":"completed"}"#,
|
||||
)
|
||||
.expect("visible Responses content parts should parse");
|
||||
|
||||
assert_eq!(response.text, "最终答案");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn run_accepts_chat_tool_calls_without_text_content() {
|
||||
let server_url = spawn_mock_server(vec![MockResponse {
|
||||
@@ -3637,9 +3722,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 +3730,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 +3774,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