mirror of
https://github.com/aiclientproxy/proxycast.git
synced 2026-09-24 23:10:56 +08:00
fix: include full v0.93.0 release changes
This commit is contained in:
@@ -99,6 +99,10 @@ pub struct ChildSubagentSession {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_from_turn_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blueprint_role_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blueprint_role_label: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile_name: Option<String>,
|
||||
@@ -135,6 +139,10 @@ pub struct SubagentParentContext {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub created_from_turn_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blueprint_role_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub blueprint_role_label: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub profile_name: Option<String>,
|
||||
@@ -379,6 +387,8 @@ fn build_child_subagent_session_summary(
|
||||
role_hint: normalize_optional_text(metadata.role_hint),
|
||||
origin_tool: normalize_optional_text(Some(metadata.origin_tool)),
|
||||
created_from_turn_id: normalize_optional_text(metadata.created_from_turn_id),
|
||||
blueprint_role_id: customization.blueprint_role_id,
|
||||
blueprint_role_label: customization.blueprint_role_label,
|
||||
profile_id: customization.profile_id,
|
||||
profile_name: customization.profile_name,
|
||||
role_key: customization.role_key,
|
||||
@@ -441,6 +451,8 @@ fn build_subagent_parent_context(
|
||||
task_summary: normalize_optional_nonempty_body(metadata.task_summary),
|
||||
origin_tool: normalize_optional_text(Some(metadata.origin_tool)),
|
||||
created_from_turn_id: normalize_optional_text(metadata.created_from_turn_id),
|
||||
blueprint_role_id: customization.blueprint_role_id,
|
||||
blueprint_role_label: customization.blueprint_role_label,
|
||||
profile_id: customization.profile_id,
|
||||
profile_name: customization.profile_name,
|
||||
role_key: customization.role_key,
|
||||
@@ -1281,6 +1293,8 @@ mod tests {
|
||||
Some("turn-9"),
|
||||
);
|
||||
session.extension_data = SubagentCustomizationState {
|
||||
blueprint_role_id: Some("runtime-explorer".to_string()),
|
||||
blueprint_role_label: Some("分析".to_string()),
|
||||
profile_id: Some("code-explorer".to_string()),
|
||||
profile_name: Some("代码分析员".to_string()),
|
||||
role_key: Some("explorer".to_string()),
|
||||
@@ -1303,6 +1317,11 @@ mod tests {
|
||||
let summary = build_child_subagent_session_summary(None, session)
|
||||
.expect("child summary should exist");
|
||||
|
||||
assert_eq!(
|
||||
summary.blueprint_role_id.as_deref(),
|
||||
Some("runtime-explorer")
|
||||
);
|
||||
assert_eq!(summary.blueprint_role_label.as_deref(), Some("分析"));
|
||||
assert_eq!(summary.profile_id.as_deref(), Some("code-explorer"));
|
||||
assert_eq!(summary.profile_name.as_deref(), Some("代码分析员"));
|
||||
assert_eq!(summary.role_key.as_deref(), Some("explorer"));
|
||||
@@ -1439,6 +1458,8 @@ mod tests {
|
||||
role_hint: Some("explorer".to_string()),
|
||||
origin_tool: Some("spawn_agent".to_string()),
|
||||
created_from_turn_id: Some("turn-1".to_string()),
|
||||
blueprint_role_id: None,
|
||||
blueprint_role_label: None,
|
||||
profile_id: None,
|
||||
profile_name: None,
|
||||
role_key: None,
|
||||
|
||||
@@ -43,6 +43,10 @@ pub struct TeamPresetSummary {
|
||||
|
||||
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, Default, PartialEq, Eq)]
|
||||
pub struct SubagentCustomizationState {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blueprint_role_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub blueprint_role_label: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
@@ -89,7 +93,9 @@ impl SubagentCustomizationState {
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.profile_id.is_none()
|
||||
self.blueprint_role_id.is_none()
|
||||
&& self.blueprint_role_label.is_none()
|
||||
&& self.profile_id.is_none()
|
||||
&& self.profile_name.is_none()
|
||||
&& self.role_key.is_none()
|
||||
&& self.team_preset_id.is_none()
|
||||
@@ -369,6 +375,18 @@ pub fn build_subagent_customization_prompt(
|
||||
builtin_team_preset_label_by_id(team_preset_id).unwrap_or(team_preset_id);
|
||||
header_lines.push(format!("- 团队预设:{preset_label} ({team_preset_id})"));
|
||||
}
|
||||
if let Some(blueprint_role_label) = customization.blueprint_role_label.as_deref() {
|
||||
let blueprint_role_id_suffix = customization
|
||||
.blueprint_role_id
|
||||
.as_deref()
|
||||
.map(|role_id| format!(" ({role_id})"))
|
||||
.unwrap_or_default();
|
||||
header_lines.push(format!(
|
||||
"- 蓝图角色:{blueprint_role_label}{blueprint_role_id_suffix}"
|
||||
));
|
||||
} else if let Some(blueprint_role_id) = customization.blueprint_role_id.as_deref() {
|
||||
header_lines.push(format!("- 蓝图角色 ID:{blueprint_role_id}"));
|
||||
}
|
||||
if let Some(profile_name) = customization.profile_name.as_deref() {
|
||||
let profile_id_suffix = customization
|
||||
.profile_id
|
||||
|
||||
@@ -250,6 +250,10 @@ pub struct AgentRuntimeSpawnSubagentRequest {
|
||||
pub reasoning_effort: Option<String>,
|
||||
#[serde(default, alias = "forkContext")]
|
||||
pub fork_context: bool,
|
||||
#[serde(default, alias = "blueprintRoleId")]
|
||||
pub blueprint_role_id: Option<String>,
|
||||
#[serde(default, alias = "blueprintRoleLabel")]
|
||||
pub blueprint_role_label: Option<String>,
|
||||
#[serde(default, alias = "profileId")]
|
||||
pub profile_id: Option<String>,
|
||||
#[serde(default, alias = "profileName")]
|
||||
|
||||
@@ -333,6 +333,12 @@ use run_metadata::{
|
||||
resolve_social_run_artifact_descriptor,
|
||||
};
|
||||
pub(crate) use runtime_turn::{build_queued_turn_task, build_runtime_queue_executor};
|
||||
#[cfg(test)]
|
||||
pub(crate) use runtime_turn::{
|
||||
build_runtime_prepared_team_spawn_message, parse_runtime_prepared_team_roles,
|
||||
plan_runtime_prepared_team_actions, RuntimePreparedTeamAction, RuntimePreparedTeamRole,
|
||||
RuntimePreparedTeamSessionCandidate,
|
||||
};
|
||||
pub(crate) use session_runtime::{
|
||||
delete_runtime_session_internal, persist_session_provider_routing,
|
||||
resolve_session_provider_selector,
|
||||
|
||||
@@ -173,6 +173,76 @@ pub(crate) fn merge_system_prompt_with_elicitation_context(
|
||||
}
|
||||
}
|
||||
|
||||
fn render_team_roles(role_items: &[serde_json::Value]) -> Vec<String> {
|
||||
role_items
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
let object = value.as_object()?;
|
||||
let label = object
|
||||
.get("label")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let summary = object
|
||||
.get("summary")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("负责当前分工。");
|
||||
let role_id_suffix = object
|
||||
.get("id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!(" / id: {value}"))
|
||||
.unwrap_or_default();
|
||||
let profile_suffix = object
|
||||
.get("profile_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!(" / profile: {value}"))
|
||||
.unwrap_or_default();
|
||||
let role_key_suffix = object
|
||||
.get("role_key")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!(" / roleKey: {value}"))
|
||||
.unwrap_or_default();
|
||||
let skill_suffix = object
|
||||
.get("skill_ids")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|items| !items.is_empty())
|
||||
.map(|items| format!(" / skills: {}", items.join(", ")))
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(format!(
|
||||
" - {label}:{summary}{role_id_suffix}{profile_suffix}{role_key_suffix}{skill_suffix}"
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn describe_turn_team_reason(reason: &str) -> &'static str {
|
||||
match reason {
|
||||
"runtime_team_prepared" => "GUI 已完成本轮 Team 预编队",
|
||||
"runtime_team_generation_failed" => "GUI 尝试预编队失败,当前回合降级为单 Agent",
|
||||
"subagent_disabled" => "当前回合未开启 Team 模式",
|
||||
"turn_purpose_override" => "当前回合属于特定目的任务,不走 Team 预编队",
|
||||
"single_agent_direct" => "当前回合判断为直接单 Agent 执行更合适",
|
||||
_ => "GUI 已记录本轮 Team 判定",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_team_preference_system_prompt(
|
||||
request_metadata: Option<&serde_json::Value>,
|
||||
) -> Option<String> {
|
||||
@@ -201,6 +271,16 @@ pub(crate) fn build_team_preference_system_prompt(
|
||||
request_metadata,
|
||||
&["selected_team_roles", "selectedTeamRoles"],
|
||||
);
|
||||
let turn_team_decision = extract_harness_string(
|
||||
request_metadata,
|
||||
&["turn_team_decision", "turnTeamDecision"],
|
||||
);
|
||||
let turn_team_reason =
|
||||
extract_harness_string(request_metadata, &["turn_team_reason", "turnTeamReason"]);
|
||||
let turn_team_blueprint = extract_harness_nested_object(
|
||||
request_metadata,
|
||||
&["turn_team_blueprint", "turnTeamBlueprint"],
|
||||
);
|
||||
|
||||
if !subagent_mode_enabled {
|
||||
return None;
|
||||
@@ -241,68 +321,84 @@ pub(crate) fn build_team_preference_system_prompt(
|
||||
}
|
||||
|
||||
if let Some(role_items) = selected_team_roles {
|
||||
let rendered_roles = role_items
|
||||
.iter()
|
||||
.filter_map(|value| {
|
||||
let object = value.as_object()?;
|
||||
let label = object
|
||||
.get("label")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())?;
|
||||
let summary = object
|
||||
.get("summary")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.unwrap_or("负责当前分工。");
|
||||
let profile_suffix = object
|
||||
.get("profile_id")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!(" / profile: {value}"))
|
||||
.unwrap_or_default();
|
||||
let role_key_suffix = object
|
||||
.get("role_key")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.map(|value| format!(" / roleKey: {value}"))
|
||||
.unwrap_or_default();
|
||||
let skill_suffix = object
|
||||
.get("skill_ids")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|items| {
|
||||
items
|
||||
.iter()
|
||||
.filter_map(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.filter(|items| !items.is_empty())
|
||||
.map(|items| format!(" / skills: {}", items.join(", ")))
|
||||
.unwrap_or_default();
|
||||
|
||||
Some(format!(
|
||||
" - {label}:{summary}{profile_suffix}{role_key_suffix}{skill_suffix}"
|
||||
))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let rendered_roles = render_team_roles(role_items);
|
||||
if !rendered_roles.is_empty() {
|
||||
lines.push("- 当前 Team 角色参考:".to_string());
|
||||
lines.extend(rendered_roles);
|
||||
lines.push(
|
||||
"- 如果你决定调用 spawn_agent,请优先把上述 profile / roleKey / skillIds 映射到对应结构化字段,保持 GUI Team 画布与实际分工一致。"
|
||||
"- 如果你决定调用 spawn_agent,请优先把上述 profile / roleKey / skillIds 映射到对应结构化字段;若该角色带有 id,优先同步写入 blueprintRoleId / blueprintRoleLabel,保持 GUI Team 画布与实际分工一致。"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
match turn_team_decision.as_deref() {
|
||||
Some("team_prepared") => {
|
||||
lines.push(
|
||||
"- 当前回合已经在 GUI 发送前完成 Team 预编队;这是一份本轮执行契约,不是事后建议。"
|
||||
.to_string(),
|
||||
);
|
||||
if let Some(reason) = turn_team_reason.as_deref() {
|
||||
lines.push(format!(
|
||||
"- GUI 判定:{}。",
|
||||
describe_turn_team_reason(reason)
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(blueprint) = turn_team_blueprint {
|
||||
let blueprint_label = blueprint
|
||||
.get("label")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let blueprint_description = blueprint
|
||||
.get("description")
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(str::trim)
|
||||
.filter(|value| !value.is_empty());
|
||||
let rendered_roles = blueprint
|
||||
.get("roles")
|
||||
.and_then(serde_json::Value::as_array)
|
||||
.map(|items| render_team_roles(items))
|
||||
.unwrap_or_default();
|
||||
|
||||
if let Some(label) = blueprint_label {
|
||||
lines.push(format!("- 本轮 Team 蓝图:{label}。"));
|
||||
}
|
||||
if let Some(description) = blueprint_description {
|
||||
lines.push(format!("- 本轮蓝图说明:{description}"));
|
||||
}
|
||||
if !rendered_roles.is_empty() {
|
||||
lines.push("- 本轮 Team 角色分工:".to_string());
|
||||
lines.extend(rendered_roles);
|
||||
}
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"- 如果你决定调用 spawn_agent / send_input,应在本轮开局阶段优先按上述蓝图启动角色,并把蓝图里的 id / label 映射到 blueprintRoleId / blueprintRoleLabel,让各角色承担自己的输出,不要等主 agent 整轮完成后再补做 team。"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
Some("single_agent") => {
|
||||
lines.push(
|
||||
"- 当前回合未在 GUI 中预编队 Team,本轮默认按单 Agent 直接推进。".to_string(),
|
||||
);
|
||||
if let Some(reason) = turn_team_reason.as_deref() {
|
||||
lines.push(format!(
|
||||
"- GUI 判定:{}。",
|
||||
describe_turn_team_reason(reason)
|
||||
));
|
||||
}
|
||||
lines.push(
|
||||
"- 除非执行中出现明确的拆分必要性,否则不要为了形式化 team 而推迟主任务。"
|
||||
.to_string(),
|
||||
);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
lines.push(
|
||||
"- spawn_agent 支持这些结构化字段:teamPresetId、profileId、profileName、roleKey、skillIds、skillDirectories、theme、systemOverlay、outputContract。"
|
||||
"- spawn_agent 支持这些结构化字段:blueprintRoleId、blueprintRoleLabel、teamPresetId、profileId、profileName、roleKey、skillIds、skillDirectories、theme、systemOverlay、outputContract。"
|
||||
.to_string(),
|
||||
);
|
||||
lines.push(
|
||||
|
||||
@@ -93,6 +93,12 @@ pub(in crate::commands::aster_agent_cmd) fn extend_map_with_harness_fields(
|
||||
("selectedTeamSummary", "selected_team_summary"),
|
||||
("selected_team_roles", "selected_team_roles"),
|
||||
("selectedTeamRoles", "selected_team_roles"),
|
||||
("turn_team_decision", "turn_team_decision"),
|
||||
("turnTeamDecision", "turn_team_decision"),
|
||||
("turn_team_reason", "turn_team_reason"),
|
||||
("turnTeamReason", "turn_team_reason"),
|
||||
("turn_team_blueprint", "turn_team_blueprint"),
|
||||
("turnTeamBlueprint", "turn_team_blueprint"),
|
||||
("browser_requirement", "browser_requirement"),
|
||||
("browserRequirement", "browser_requirement"),
|
||||
("browser_requirement_reason", "browser_requirement_reason"),
|
||||
|
||||
@@ -1,5 +1,352 @@
|
||||
use super::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct RuntimePreparedTeamRole {
|
||||
pub(crate) id: String,
|
||||
pub(crate) label: String,
|
||||
pub(crate) summary: Option<String>,
|
||||
pub(crate) profile_id: Option<String>,
|
||||
pub(crate) role_key: Option<String>,
|
||||
pub(crate) skill_ids: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) struct RuntimePreparedTeamSessionCandidate {
|
||||
pub(crate) blueprint_role_id: String,
|
||||
pub(crate) session_id: String,
|
||||
pub(crate) status_kind: SubagentRuntimeStatusKind,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub(crate) enum RuntimePreparedTeamAction {
|
||||
Spawn(RuntimePreparedTeamRole),
|
||||
Resume {
|
||||
role: RuntimePreparedTeamRole,
|
||||
session_id: String,
|
||||
},
|
||||
}
|
||||
|
||||
fn normalize_runtime_team_role_string(value: Option<&serde_json::Value>) -> Option<String> {
|
||||
normalize_optional_text(
|
||||
value
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(ToString::to_string),
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_runtime_team_role_id_fragment(value: &str) -> Option<String> {
|
||||
let normalized = value.split_whitespace().collect::<Vec<_>>().join("-");
|
||||
let normalized = normalized.trim_matches('-').to_string();
|
||||
if normalized.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(normalized)
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_runtime_team_skill_ids(value: Option<&serde_json::Value>) -> Vec<String> {
|
||||
let Some(items) = value.and_then(serde_json::Value::as_array) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let mut skill_ids = Vec::new();
|
||||
for item in items {
|
||||
let Some(skill_id) = normalize_runtime_team_role_string(Some(item)) else {
|
||||
continue;
|
||||
};
|
||||
if seen.insert(skill_id.clone()) {
|
||||
skill_ids.push(skill_id);
|
||||
}
|
||||
}
|
||||
|
||||
skill_ids
|
||||
}
|
||||
|
||||
fn parse_runtime_prepared_team_role(
|
||||
role_value: &serde_json::Value,
|
||||
index: usize,
|
||||
) -> Option<RuntimePreparedTeamRole> {
|
||||
let role = role_value.as_object()?;
|
||||
let profile_id = normalize_runtime_team_role_string(role.get("profile_id"))
|
||||
.or_else(|| normalize_runtime_team_role_string(role.get("profileId")));
|
||||
let role_key = normalize_runtime_team_role_string(role.get("role_key"))
|
||||
.or_else(|| normalize_runtime_team_role_string(role.get("roleKey")));
|
||||
let label = normalize_runtime_team_role_string(role.get("label"))
|
||||
.or_else(|| profile_id.clone())
|
||||
.or_else(|| role_key.clone())
|
||||
.unwrap_or_else(|| format!("角色 {}", index + 1));
|
||||
let id = normalize_runtime_team_role_string(role.get("id"))
|
||||
.or_else(|| {
|
||||
profile_id
|
||||
.as_deref()
|
||||
.and_then(normalize_runtime_team_role_id_fragment)
|
||||
.map(|fragment| format!("profile-{fragment}"))
|
||||
})
|
||||
.or_else(|| {
|
||||
role_key
|
||||
.as_deref()
|
||||
.and_then(normalize_runtime_team_role_id_fragment)
|
||||
.map(|fragment| format!("role-{fragment}"))
|
||||
})
|
||||
.or_else(|| {
|
||||
normalize_runtime_team_role_id_fragment(&label)
|
||||
.map(|fragment| format!("lane-{fragment}"))
|
||||
})
|
||||
.unwrap_or_else(|| format!("runtime-team-role-{}", index + 1));
|
||||
|
||||
Some(RuntimePreparedTeamRole {
|
||||
id,
|
||||
label,
|
||||
summary: normalize_runtime_team_role_string(role.get("summary")),
|
||||
profile_id,
|
||||
role_key,
|
||||
skill_ids: normalize_runtime_team_skill_ids(
|
||||
role.get("skill_ids").or_else(|| role.get("skillIds")),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn parse_runtime_prepared_team_roles(
|
||||
request_metadata: Option<&serde_json::Value>,
|
||||
) -> Vec<RuntimePreparedTeamRole> {
|
||||
if extract_harness_string(
|
||||
request_metadata,
|
||||
&["turn_team_decision", "turnTeamDecision"],
|
||||
)
|
||||
.as_deref()
|
||||
!= Some("team_prepared")
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let Some(blueprint) = extract_harness_nested_object(
|
||||
request_metadata,
|
||||
&["turn_team_blueprint", "turnTeamBlueprint"],
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(role_values) = blueprint.get("roles").and_then(serde_json::Value::as_array) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut seen = HashSet::new();
|
||||
let mut roles = Vec::new();
|
||||
for (index, role_value) in role_values.iter().enumerate() {
|
||||
let Some(role) = parse_runtime_prepared_team_role(role_value, index) else {
|
||||
continue;
|
||||
};
|
||||
if seen.insert(role.id.clone()) {
|
||||
roles.push(role);
|
||||
}
|
||||
}
|
||||
|
||||
roles
|
||||
}
|
||||
|
||||
fn runtime_prepared_team_session_rank(status_kind: SubagentRuntimeStatusKind) -> u8 {
|
||||
match status_kind {
|
||||
SubagentRuntimeStatusKind::Running => 7,
|
||||
SubagentRuntimeStatusKind::Queued => 6,
|
||||
SubagentRuntimeStatusKind::Idle => 5,
|
||||
SubagentRuntimeStatusKind::Completed => 4,
|
||||
SubagentRuntimeStatusKind::Failed => 3,
|
||||
SubagentRuntimeStatusKind::Aborted => 2,
|
||||
SubagentRuntimeStatusKind::Closed => 1,
|
||||
SubagentRuntimeStatusKind::NotFound => 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn plan_runtime_prepared_team_actions(
|
||||
roles: &[RuntimePreparedTeamRole],
|
||||
existing_candidates: &[RuntimePreparedTeamSessionCandidate],
|
||||
) -> Vec<RuntimePreparedTeamAction> {
|
||||
let mut existing_by_role_id: HashMap<&str, &RuntimePreparedTeamSessionCandidate> =
|
||||
HashMap::new();
|
||||
|
||||
for candidate in existing_candidates {
|
||||
let replace = existing_by_role_id
|
||||
.get(candidate.blueprint_role_id.as_str())
|
||||
.map(|current| {
|
||||
runtime_prepared_team_session_rank(candidate.status_kind)
|
||||
> runtime_prepared_team_session_rank(current.status_kind)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if replace {
|
||||
existing_by_role_id.insert(candidate.blueprint_role_id.as_str(), candidate);
|
||||
}
|
||||
}
|
||||
|
||||
roles
|
||||
.iter()
|
||||
.filter_map(|role| match existing_by_role_id.get(role.id.as_str()) {
|
||||
Some(candidate) if candidate.status_kind == SubagentRuntimeStatusKind::Closed => {
|
||||
Some(RuntimePreparedTeamAction::Resume {
|
||||
role: role.clone(),
|
||||
session_id: candidate.session_id.clone(),
|
||||
})
|
||||
}
|
||||
Some(candidate) if candidate.status_kind != SubagentRuntimeStatusKind::NotFound => None,
|
||||
_ => Some(RuntimePreparedTeamAction::Spawn(role.clone())),
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn build_runtime_prepared_team_spawn_message(
|
||||
role: &RuntimePreparedTeamRole,
|
||||
user_message: &str,
|
||||
) -> String {
|
||||
let mut sections = vec![format!("你是本轮 Team 中的「{}」角色。", role.label)];
|
||||
if let Some(summary) = role.summary.as_deref() {
|
||||
sections.push(format!("你的职责:{summary}"));
|
||||
}
|
||||
sections.push(
|
||||
"请直接在当前子会话输出你的执行过程与结果,不要把具体产出留给父会话代写。".to_string(),
|
||||
);
|
||||
sections.push(format!("当前用户任务:\n{}", user_message.trim()));
|
||||
sections.push("只处理当前角色范围内的工作;如果依赖其他角色,请明确指出协作边界。".to_string());
|
||||
sections.join("\n\n")
|
||||
}
|
||||
|
||||
async fn collect_runtime_prepared_team_candidates(
|
||||
parent_session_id: &str,
|
||||
) -> Result<Vec<RuntimePreparedTeamSessionCandidate>, String> {
|
||||
let child_sessions = list_subagent_child_sessions(parent_session_id)
|
||||
.await
|
||||
.map_err(|error| format!("读取 runtime team child sessions 失败: {error}"))?;
|
||||
let mut candidates = Vec::new();
|
||||
|
||||
for child_session in child_sessions {
|
||||
let Some(customization) = SubagentCustomizationState::from_session(&child_session) else {
|
||||
continue;
|
||||
};
|
||||
let Some(blueprint_role_id) = normalize_optional_text(customization.blueprint_role_id)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let status_kind = load_subagent_runtime_status(&child_session.id)
|
||||
.await
|
||||
.map(|status| status.kind)
|
||||
.unwrap_or(SubagentRuntimeStatusKind::NotFound);
|
||||
candidates.push(RuntimePreparedTeamSessionCandidate {
|
||||
blueprint_role_id,
|
||||
session_id: child_session.id,
|
||||
status_kind,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(candidates)
|
||||
}
|
||||
|
||||
async fn maybe_prepare_runtime_team_sessions(
|
||||
app: &AppHandle,
|
||||
state: &AsterAgentState,
|
||||
db: &DbConnection,
|
||||
api_key_provider_service: &ApiKeyProviderServiceState,
|
||||
logs: &LogState,
|
||||
config_manager: &GlobalConfigManagerState,
|
||||
mcp_manager: &McpManagerState,
|
||||
automation_state: &AutomationServiceState,
|
||||
request: &AsterChatRequest,
|
||||
) -> Result<(), String> {
|
||||
let roles = parse_runtime_prepared_team_roles(request.metadata.as_ref());
|
||||
if roles.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let existing_candidates = collect_runtime_prepared_team_candidates(&request.session_id).await?;
|
||||
let actions = plan_runtime_prepared_team_actions(&roles, &existing_candidates);
|
||||
if actions.is_empty() {
|
||||
tracing::info!(
|
||||
"[AsterAgent][RuntimeTeam] session={} 所有 blueprint 角色已就绪,无需新增预拉起",
|
||||
request.session_id
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!(
|
||||
"[AsterAgent][RuntimeTeam] session={} 开始按 blueprint 预拉起角色: total_roles={}, pending_actions={}",
|
||||
request.session_id,
|
||||
roles.len(),
|
||||
actions.len()
|
||||
);
|
||||
|
||||
let runtime = SubagentControlRuntime::new(
|
||||
app.clone(),
|
||||
state,
|
||||
db,
|
||||
api_key_provider_service,
|
||||
logs,
|
||||
config_manager,
|
||||
mcp_manager,
|
||||
automation_state,
|
||||
);
|
||||
|
||||
for action in actions {
|
||||
match action {
|
||||
RuntimePreparedTeamAction::Spawn(role) => {
|
||||
tracing::info!(
|
||||
"[AsterAgent][RuntimeTeam] session={} 自动预拉起角色: role_id={}, label={}",
|
||||
request.session_id,
|
||||
role.id,
|
||||
role.label
|
||||
);
|
||||
agent_runtime_spawn_subagent_internal(
|
||||
&runtime,
|
||||
AgentRuntimeSpawnSubagentRequest {
|
||||
parent_session_id: request.session_id.clone(),
|
||||
message: build_runtime_prepared_team_spawn_message(&role, &request.message),
|
||||
agent_type: Some(role.label.clone()),
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
fork_context: false,
|
||||
blueprint_role_id: Some(role.id.clone()),
|
||||
blueprint_role_label: Some(role.label.clone()),
|
||||
profile_id: role.profile_id.clone(),
|
||||
profile_name: None,
|
||||
role_key: role.role_key.clone(),
|
||||
skill_ids: role.skill_ids.clone(),
|
||||
skill_directories: Vec::new(),
|
||||
team_preset_id: None,
|
||||
theme: None,
|
||||
system_overlay: None,
|
||||
output_contract: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"自动预拉起 team 角色失败: role_id={}, label={}, error={error}",
|
||||
role.id, role.label
|
||||
)
|
||||
})?;
|
||||
}
|
||||
RuntimePreparedTeamAction::Resume { role, session_id } => {
|
||||
tracing::info!(
|
||||
"[AsterAgent][RuntimeTeam] session={} 恢复已关闭角色 lane: role_id={}, label={}, child_session={}",
|
||||
request.session_id,
|
||||
role.id,
|
||||
role.label,
|
||||
session_id
|
||||
);
|
||||
agent_runtime_resume_subagent_internal(
|
||||
&runtime,
|
||||
AgentRuntimeResumeSubagentRequest { id: session_id.clone() },
|
||||
)
|
||||
.await
|
||||
.map_err(|error| {
|
||||
format!(
|
||||
"恢复已关闭 team 角色失败: role_id={}, label={}, child_session={}, error={error}",
|
||||
role.id, role.label, session_id
|
||||
)
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn execute_aster_chat_request(
|
||||
app: &AppHandle,
|
||||
state: &AsterAgentState,
|
||||
@@ -604,21 +951,50 @@ async fn execute_aster_chat_request(
|
||||
session_config_builder.build()
|
||||
};
|
||||
|
||||
let runtime_turn_initialized = {
|
||||
let guard = agent_arc.read().await;
|
||||
let agent = guard.as_ref().ok_or("Agent not initialized")?;
|
||||
match agent
|
||||
.ensure_runtime_turn_initialized(
|
||||
&runtime_status_session_config,
|
||||
Some(request.message.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(_) => true,
|
||||
Err(error) => {
|
||||
tracing::warn!(
|
||||
"[AsterAgent] 初始化 runtime turn 失败,后续降级继续: {}",
|
||||
error
|
||||
);
|
||||
false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if runtime_turn_initialized {
|
||||
maybe_prepare_runtime_team_sessions(
|
||||
app,
|
||||
state,
|
||||
db,
|
||||
api_key_provider_service,
|
||||
logs,
|
||||
config_manager,
|
||||
mcp_manager,
|
||||
automation_state,
|
||||
&request,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
tracing::warn!(
|
||||
"[AsterAgent][RuntimeTeam] 跳过本轮 team 预拉起,因为父会话 runtime turn 尚未就绪: session={}",
|
||||
session_id
|
||||
);
|
||||
}
|
||||
|
||||
// 获取 Agent Arc 并保持 guard 在整个流处理期间存活
|
||||
let guard = agent_arc.read().await;
|
||||
let agent = guard.as_ref().ok_or("Agent not initialized")?;
|
||||
if let Err(error) = agent
|
||||
.ensure_runtime_turn_initialized(
|
||||
&runtime_status_session_config,
|
||||
Some(request.message.clone()),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(
|
||||
"[AsterAgent] 初始化 runtime turn 失败,后续降级继续: {}",
|
||||
error
|
||||
);
|
||||
}
|
||||
|
||||
let (initial_runtime_status, decided_runtime_status) = build_turn_runtime_statuses(
|
||||
&request,
|
||||
|
||||
@@ -153,9 +153,11 @@ fn normalize_optional_vec(values: &[String]) -> Vec<String> {
|
||||
fn build_subagent_session_name(
|
||||
message: &str,
|
||||
agent_type: Option<&str>,
|
||||
blueprint_role_label: Option<&str>,
|
||||
profile_name: Option<&str>,
|
||||
) -> String {
|
||||
normalize_optional_text(agent_type.map(ToString::to_string))
|
||||
.or_else(|| normalize_optional_text(blueprint_role_label.map(ToString::to_string)))
|
||||
.or_else(|| normalize_optional_text(profile_name.map(ToString::to_string)))
|
||||
.or_else(|| build_subagent_task_summary(message))
|
||||
.unwrap_or_else(|| "子代理".to_string())
|
||||
@@ -166,6 +168,7 @@ fn resolve_subagent_role_hint(
|
||||
customization: Option<&SubagentCustomizationState>,
|
||||
) -> Option<String> {
|
||||
normalize_optional_text(request.agent_type.clone())
|
||||
.or_else(|| customization.and_then(|state| state.blueprint_role_label.clone()))
|
||||
.or_else(|| customization.and_then(|state| state.profile_name.clone()))
|
||||
.or_else(|| customization.and_then(|state| state.role_key.clone()))
|
||||
}
|
||||
@@ -210,6 +213,8 @@ fn build_local_subagent_skill_payload(
|
||||
pub(crate) fn build_subagent_customization_state(
|
||||
request: &AgentRuntimeSpawnSubagentRequest,
|
||||
) -> Result<Option<SubagentCustomizationState>, String> {
|
||||
let blueprint_role_id = normalize_optional_text(request.blueprint_role_id.clone());
|
||||
let blueprint_role_label = normalize_optional_text(request.blueprint_role_label.clone());
|
||||
let profile_id = normalize_optional_text(request.profile_id.clone());
|
||||
let profile = profile_id
|
||||
.as_deref()
|
||||
@@ -250,6 +255,8 @@ pub(crate) fn build_subagent_customization_state(
|
||||
}
|
||||
|
||||
let state = SubagentCustomizationState {
|
||||
blueprint_role_id,
|
||||
blueprint_role_label,
|
||||
profile_id,
|
||||
profile_name: normalize_optional_text(request.profile_name.clone())
|
||||
.or_else(|| profile.map(|descriptor| descriptor.name.to_string())),
|
||||
@@ -602,7 +609,14 @@ async fn create_runtime_subagent_session(
|
||||
|
||||
let session = SessionManager::create_session(
|
||||
parent_session.working_dir.clone(),
|
||||
build_subagent_session_name(&message, request.agent_type.as_deref(), profile_name),
|
||||
build_subagent_session_name(
|
||||
&message,
|
||||
request.agent_type.as_deref(),
|
||||
customization
|
||||
.as_ref()
|
||||
.and_then(|state| state.blueprint_role_label.as_deref()),
|
||||
profile_name,
|
||||
),
|
||||
SessionType::SubAgent,
|
||||
)
|
||||
.await
|
||||
@@ -720,6 +734,8 @@ pub(crate) async fn agent_runtime_spawn_subagent_internal(
|
||||
"reasoning_effort": request.reasoning_effort,
|
||||
"fork_context": request.fork_context,
|
||||
"origin_tool": "spawn_agent",
|
||||
"blueprint_role_id": customization.as_ref().and_then(|state| state.blueprint_role_id.clone()),
|
||||
"blueprint_role_label": customization.as_ref().and_then(|state| state.blueprint_role_label.clone()),
|
||||
"profile_id": customization.as_ref().and_then(|state| state.profile_id.clone()),
|
||||
"profile_name": customization.as_ref().and_then(|state| state.profile_name.clone()),
|
||||
"role_key": customization.as_ref().and_then(|state| state.role_key.clone()),
|
||||
@@ -788,6 +804,8 @@ pub(crate) async fn agent_runtime_send_subagent_input_internal(
|
||||
"subagent": {
|
||||
"origin_tool": "send_input",
|
||||
"interrupt": request.interrupt,
|
||||
"blueprint_role_id": customization.as_ref().and_then(|state| state.blueprint_role_id.clone()),
|
||||
"blueprint_role_label": customization.as_ref().and_then(|state| state.blueprint_role_label.clone()),
|
||||
"profile_id": customization.as_ref().and_then(|state| state.profile_id.clone()),
|
||||
"profile_name": customization.as_ref().and_then(|state| state.profile_name.clone()),
|
||||
"role_key": customization.as_ref().and_then(|state| state.role_key.clone()),
|
||||
|
||||
@@ -1249,6 +1249,61 @@ mod tests {
|
||||
assert!(prompt.contains("映射到对应结构化字段"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_team_preference_system_prompt_renders_turn_team_contract() {
|
||||
let prompt = build_team_preference_system_prompt(Some(&serde_json::json!({
|
||||
"harness": {
|
||||
"subagent_mode_enabled": true,
|
||||
"turn_team_decision": "team_prepared",
|
||||
"turn_team_reason": "runtime_team_prepared",
|
||||
"turn_team_blueprint": {
|
||||
"label": "本轮调试 Team",
|
||||
"description": "先分析,再实现,最后验证。",
|
||||
"roles": [
|
||||
{
|
||||
"id": "runtime-explorer",
|
||||
"label": "分析",
|
||||
"summary": "负责定位问题。",
|
||||
"profile_id": "code-explorer",
|
||||
"role_key": "explorer",
|
||||
"skill_ids": ["repo-exploration"]
|
||||
},
|
||||
{
|
||||
"label": "执行",
|
||||
"summary": "负责提交修复。"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})))
|
||||
.expect("team prompt should exist");
|
||||
|
||||
assert!(prompt.contains("发送前完成 Team 预编队"));
|
||||
assert!(prompt.contains("本轮调试 Team"));
|
||||
assert!(prompt.contains("先分析,再实现,最后验证。"));
|
||||
assert!(prompt.contains("分析:负责定位问题。"));
|
||||
assert!(prompt.contains("id: runtime-explorer"));
|
||||
assert!(prompt.contains("blueprintRoleId"));
|
||||
assert!(prompt.contains("让各角色承担自己的输出"));
|
||||
assert!(prompt.contains("不要等主 agent 整轮完成后再补做 team"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_team_preference_system_prompt_renders_single_agent_turn_decision() {
|
||||
let prompt = build_team_preference_system_prompt(Some(&serde_json::json!({
|
||||
"harness": {
|
||||
"subagent_mode_enabled": true,
|
||||
"turn_team_decision": "single_agent",
|
||||
"turn_team_reason": "single_agent_direct",
|
||||
}
|
||||
})))
|
||||
.expect("team prompt should exist");
|
||||
|
||||
assert!(prompt.contains("当前回合未在 GUI 中预编队 Team"));
|
||||
assert!(prompt.contains("直接单 Agent 执行更合适"));
|
||||
assert!(prompt.contains("不要为了形式化 team 而推迟主任务"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_subagent_customization_state_applies_profile_defaults() {
|
||||
let customization = build_subagent_customization_state(&AgentRuntimeSpawnSubagentRequest {
|
||||
@@ -1258,6 +1313,8 @@ mod tests {
|
||||
model: None,
|
||||
reasoning_effort: None,
|
||||
fork_context: false,
|
||||
blueprint_role_id: Some("runtime-explorer".to_string()),
|
||||
blueprint_role_label: Some("分析".to_string()),
|
||||
profile_id: Some("code-explorer".to_string()),
|
||||
profile_name: None,
|
||||
role_key: None,
|
||||
@@ -1271,6 +1328,11 @@ mod tests {
|
||||
.expect("build customization state")
|
||||
.expect("customization should exist");
|
||||
|
||||
assert_eq!(
|
||||
customization.blueprint_role_id.as_deref(),
|
||||
Some("runtime-explorer")
|
||||
);
|
||||
assert_eq!(customization.blueprint_role_label.as_deref(), Some("分析"));
|
||||
assert_eq!(customization.profile_name.as_deref(), Some("代码分析员"));
|
||||
assert_eq!(customization.role_key.as_deref(), Some("explorer"));
|
||||
assert_eq!(
|
||||
@@ -1293,6 +1355,8 @@ mod tests {
|
||||
fn test_build_subagent_customization_system_prompt_renders_builtin_configuration() {
|
||||
let prompt =
|
||||
build_subagent_customization_system_prompt(Some(&SubagentCustomizationState {
|
||||
blueprint_role_id: Some("runtime-explorer".to_string()),
|
||||
blueprint_role_label: Some("分析".to_string()),
|
||||
profile_id: Some("code-explorer".to_string()),
|
||||
profile_name: Some("代码分析员".to_string()),
|
||||
role_key: Some("explorer".to_string()),
|
||||
@@ -1313,12 +1377,133 @@ mod tests {
|
||||
.expect("prompt should exist");
|
||||
|
||||
assert!(prompt.contains("【Subagent 定制配置】"));
|
||||
assert!(prompt.contains("蓝图角色:分析 (runtime-explorer)"));
|
||||
assert!(prompt.contains("代码分析员"));
|
||||
assert!(prompt.contains("代码排障团队"));
|
||||
assert!(prompt.contains("仓库探索"));
|
||||
assert!(prompt.contains("输出问题定位、证据与影响面。"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_runtime_prepared_team_roles_from_metadata() {
|
||||
let roles = parse_runtime_prepared_team_roles(Some(&serde_json::json!({
|
||||
"harness": {
|
||||
"turn_team_decision": "team_prepared",
|
||||
"turn_team_blueprint": {
|
||||
"roles": [
|
||||
{
|
||||
"id": "runtime-explorer",
|
||||
"label": "分析",
|
||||
"summary": "负责定位问题。",
|
||||
"profile_id": "code-explorer",
|
||||
"role_key": "explorer",
|
||||
"skill_ids": ["repo-exploration", "repo-exploration", " "]
|
||||
},
|
||||
{
|
||||
"label": "执行 角色",
|
||||
"summary": "负责提交修复。"
|
||||
},
|
||||
{
|
||||
"id": "runtime-explorer",
|
||||
"label": "重复分析"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
})));
|
||||
|
||||
assert_eq!(roles.len(), 2);
|
||||
assert_eq!(roles[0].id, "runtime-explorer");
|
||||
assert_eq!(roles[0].label, "分析");
|
||||
assert_eq!(roles[0].profile_id.as_deref(), Some("code-explorer"));
|
||||
assert_eq!(roles[0].role_key.as_deref(), Some("explorer"));
|
||||
assert_eq!(roles[0].skill_ids, vec!["repo-exploration".to_string()]);
|
||||
|
||||
assert_eq!(roles[1].id, "lane-执行-角色");
|
||||
assert_eq!(roles[1].label, "执行 角色");
|
||||
assert_eq!(roles[1].summary.as_deref(), Some("负责提交修复。"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_plan_runtime_prepared_team_actions_skips_existing_and_resumes_closed_lane() {
|
||||
let roles = vec![
|
||||
RuntimePreparedTeamRole {
|
||||
id: "runtime-explorer".to_string(),
|
||||
label: "分析".to_string(),
|
||||
summary: Some("负责定位问题。".to_string()),
|
||||
profile_id: Some("code-explorer".to_string()),
|
||||
role_key: Some("explorer".to_string()),
|
||||
skill_ids: vec!["repo-exploration".to_string()],
|
||||
},
|
||||
RuntimePreparedTeamRole {
|
||||
id: "runtime-executor".to_string(),
|
||||
label: "执行".to_string(),
|
||||
summary: Some("负责提交修复。".to_string()),
|
||||
profile_id: Some("code-executor".to_string()),
|
||||
role_key: Some("executor".to_string()),
|
||||
skill_ids: vec![],
|
||||
},
|
||||
RuntimePreparedTeamRole {
|
||||
id: "runtime-verifier".to_string(),
|
||||
label: "验证".to_string(),
|
||||
summary: Some("负责回归验证。".to_string()),
|
||||
profile_id: Some("code-verifier".to_string()),
|
||||
role_key: Some("verifier".to_string()),
|
||||
skill_ids: vec![],
|
||||
},
|
||||
];
|
||||
let existing_candidates = vec![
|
||||
RuntimePreparedTeamSessionCandidate {
|
||||
blueprint_role_id: "runtime-explorer".to_string(),
|
||||
session_id: "child-explorer".to_string(),
|
||||
status_kind: SubagentRuntimeStatusKind::Running,
|
||||
},
|
||||
RuntimePreparedTeamSessionCandidate {
|
||||
blueprint_role_id: "runtime-executor".to_string(),
|
||||
session_id: "child-executor-old".to_string(),
|
||||
status_kind: SubagentRuntimeStatusKind::Closed,
|
||||
},
|
||||
RuntimePreparedTeamSessionCandidate {
|
||||
blueprint_role_id: "runtime-executor".to_string(),
|
||||
session_id: "child-executor-stale".to_string(),
|
||||
status_kind: SubagentRuntimeStatusKind::NotFound,
|
||||
},
|
||||
];
|
||||
|
||||
let actions = plan_runtime_prepared_team_actions(&roles, &existing_candidates);
|
||||
assert_eq!(actions.len(), 2);
|
||||
|
||||
assert!(matches!(
|
||||
&actions[0],
|
||||
RuntimePreparedTeamAction::Resume { role, session_id }
|
||||
if role.id == "runtime-executor" && session_id == "child-executor-old"
|
||||
));
|
||||
assert!(matches!(
|
||||
&actions[1],
|
||||
RuntimePreparedTeamAction::Spawn(role) if role.id == "runtime-verifier"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_runtime_prepared_team_spawn_message_emphasizes_lane_output() {
|
||||
let message = build_runtime_prepared_team_spawn_message(
|
||||
&RuntimePreparedTeamRole {
|
||||
id: "runtime-explorer".to_string(),
|
||||
label: "分析".to_string(),
|
||||
summary: Some("负责定位问题。".to_string()),
|
||||
profile_id: Some("code-explorer".to_string()),
|
||||
role_key: Some("explorer".to_string()),
|
||||
skill_ids: vec!["repo-exploration".to_string()],
|
||||
},
|
||||
"修正 team runtime 的执行顺序",
|
||||
);
|
||||
|
||||
assert!(message.contains("当前子会话输出"));
|
||||
assert!(message.contains("修正 team runtime 的执行顺序"));
|
||||
assert!(message.contains("负责定位问题。"));
|
||||
assert!(message.contains("不要把具体产出留给父会话代写"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_normalize_shell_command_params_accepts_cmd_alias() {
|
||||
let input = serde_json::json!({
|
||||
@@ -1360,8 +1545,9 @@ mod tests {
|
||||
let input = serde_json::json!({
|
||||
"path": "/memories/preferences.md"
|
||||
});
|
||||
let normalized: serde_json::Value = normalize_params_for_durable_memory_support("read", &input)
|
||||
.expect("normalize read params");
|
||||
let normalized: serde_json::Value =
|
||||
normalize_params_for_durable_memory_support("read", &input)
|
||||
.expect("normalize read params");
|
||||
let expected = tmp
|
||||
.path()
|
||||
.join("preferences.md")
|
||||
@@ -1385,7 +1571,7 @@ mod tests {
|
||||
});
|
||||
let normalized: serde_json::Value =
|
||||
normalize_params_for_durable_memory_support("glob", &input)
|
||||
.expect("normalize glob params");
|
||||
.expect("normalize glob params");
|
||||
let expected_root = tmp.path().to_string_lossy().to_string();
|
||||
|
||||
assert_eq!(
|
||||
|
||||
@@ -412,6 +412,8 @@ impl Tool for SubAgentTaskTool {
|
||||
model: input.model.clone(),
|
||||
reasoning_effort: None,
|
||||
fork_context: false,
|
||||
blueprint_role_id: None,
|
||||
blueprint_role_label: None,
|
||||
profile_id: None,
|
||||
profile_name: None,
|
||||
role_key: None,
|
||||
@@ -511,6 +513,8 @@ struct SpawnAgentToolInput {
|
||||
model: Option<String>,
|
||||
reasoning_effort: Option<String>,
|
||||
fork_context: Option<bool>,
|
||||
blueprint_role_id: Option<String>,
|
||||
blueprint_role_label: Option<String>,
|
||||
profile_id: Option<String>,
|
||||
profile_name: Option<String>,
|
||||
role_key: Option<String>,
|
||||
@@ -569,6 +573,14 @@ impl Tool for SpawnAgentTool {
|
||||
"type": "boolean",
|
||||
"description": "保留字段,当前仅记录到 metadata"
|
||||
},
|
||||
"blueprintRoleId": {
|
||||
"type": "string",
|
||||
"description": "可选本轮 Team 蓝图角色 id;当 GUI 已预编队 Team 时,优先传入对应角色 id,便于真实成员接管画布泳道"
|
||||
},
|
||||
"blueprintRoleLabel": {
|
||||
"type": "string",
|
||||
"description": "可选本轮 Team 蓝图角色标签,例如 分析 / 执行 / 验证"
|
||||
},
|
||||
"profileId": {
|
||||
"type": "string",
|
||||
"description": "可选内置 profile id,例如 code-explorer / code-executor / code-verifier"
|
||||
@@ -629,6 +641,8 @@ impl Tool for SpawnAgentTool {
|
||||
model: input.model,
|
||||
reasoning_effort: input.reasoning_effort,
|
||||
fork_context: input.fork_context.unwrap_or(false),
|
||||
blueprint_role_id: input.blueprint_role_id,
|
||||
blueprint_role_label: input.blueprint_role_label,
|
||||
profile_id: input.profile_id,
|
||||
profile_name: input.profile_name,
|
||||
role_key: input.role_key,
|
||||
|
||||
@@ -3350,7 +3350,7 @@ export function AgentChatWorkspace({
|
||||
const {
|
||||
runtimeTeamState,
|
||||
clearRuntimeTeamState,
|
||||
handleRuntimeTeamAfterSend,
|
||||
prepareRuntimeTeamBeforeSend,
|
||||
} = useRuntimeTeamFormation({
|
||||
activeTheme,
|
||||
projectId,
|
||||
@@ -6219,6 +6219,46 @@ export function AgentChatWorkspace({
|
||||
}
|
||||
}
|
||||
|
||||
const preparedRuntimeTeamState = await prepareRuntimeTeamBeforeSend({
|
||||
input: sourceText,
|
||||
providerType,
|
||||
model: effectiveModel,
|
||||
executionStrategy: sendExecutionStrategy ?? executionStrategy,
|
||||
purpose: sendOptions?.purpose,
|
||||
subagentEnabled: effectiveToolPreferences.subagent,
|
||||
});
|
||||
const turnTeamBlueprint =
|
||||
preparedRuntimeTeamState?.status === "formed" &&
|
||||
preparedRuntimeTeamState.members.length > 0
|
||||
? {
|
||||
label: preparedRuntimeTeamState.label?.trim() || undefined,
|
||||
description:
|
||||
preparedRuntimeTeamState.summary?.trim() || undefined,
|
||||
roles: preparedRuntimeTeamState.members.map((member) => ({
|
||||
id: member.id,
|
||||
label: member.label,
|
||||
summary: member.summary,
|
||||
profileId: member.profileId,
|
||||
roleKey: member.roleKey,
|
||||
skillIds: [...member.skillIds],
|
||||
})),
|
||||
}
|
||||
: undefined;
|
||||
const turnTeamDecision =
|
||||
preparedRuntimeTeamState?.status === "formed"
|
||||
? "team_prepared"
|
||||
: "single_agent";
|
||||
const turnTeamReason =
|
||||
preparedRuntimeTeamState?.status === "formed"
|
||||
? "runtime_team_prepared"
|
||||
: preparedRuntimeTeamState?.status === "failed"
|
||||
? "runtime_team_generation_failed"
|
||||
: !effectiveToolPreferences.subagent
|
||||
? "subagent_disabled"
|
||||
: sendOptions?.purpose
|
||||
? "turn_purpose_override"
|
||||
: "single_agent_direct";
|
||||
|
||||
setInput("");
|
||||
setMentionedCharacters([]); // 清空引用的角色
|
||||
|
||||
@@ -6256,6 +6296,9 @@ export function AgentChatWorkspace({
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
selectedTeamRoles: selectedTeam?.roles,
|
||||
turnTeamDecision,
|
||||
turnTeamReason,
|
||||
turnTeamBlueprint,
|
||||
}),
|
||||
},
|
||||
};
|
||||
@@ -6288,14 +6331,6 @@ export function AgentChatWorkspace({
|
||||
|
||||
finalizeAfterSendSuccess(sendBoundary);
|
||||
|
||||
handleRuntimeTeamAfterSend({
|
||||
input: sourceText,
|
||||
providerType,
|
||||
model: effectiveModel,
|
||||
executionStrategy: sendExecutionStrategy ?? executionStrategy,
|
||||
purpose: sendOptions?.purpose,
|
||||
});
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
rollbackAfterSendFailure(sendBoundary);
|
||||
@@ -6332,9 +6367,9 @@ export function AgentChatWorkspace({
|
||||
selectedTeamSummary,
|
||||
providerType,
|
||||
finalizeAfterSendSuccess,
|
||||
handleRuntimeTeamAfterSend,
|
||||
isBlockedByBrowserPreflight,
|
||||
maybeStartBrowserTaskPreflight,
|
||||
prepareRuntimeTeamBeforeSend,
|
||||
resolveSendBoundary,
|
||||
resolveSendProviderContext,
|
||||
rollbackAfterSendFailure,
|
||||
@@ -10396,7 +10431,7 @@ export function AgentChatWorkspace({
|
||||
selectedTeamLabel ||
|
||||
"Team Workbench",
|
||||
subtitle: hasRealTeamGraph
|
||||
? "画布展示多成员实时协作,右侧侧栏展示总览与角色结构。"
|
||||
? "主对话保留调度记录,画布按角色分别展示执行过程与结果。"
|
||||
: runtimeTeamState?.status === "forming"
|
||||
? "正在根据本轮任务准备 Team,随后会在画布中展示成员编队。"
|
||||
: runtimeTeamState?.status === "formed"
|
||||
|
||||
@@ -196,7 +196,7 @@ export function TeamWorkbenchSummaryPanel({
|
||||
? runtimeTeamState.errorMessage?.trim() ||
|
||||
"本轮 Team 准备失败,可继续在当前对话中推进。"
|
||||
: runtimeSummaryText ||
|
||||
"这里展示团队总览、运行密度和最近动态;详细协作过程在左侧主画布查看。"}
|
||||
"这里展示团队总览与运行密度;主对话只保留调度记录,角色执行正文在左侧 Team 画布查看。"}
|
||||
</p>
|
||||
{!hasRealTeamGraph && runtimeTeamState ? (
|
||||
<div
|
||||
|
||||
@@ -293,6 +293,10 @@ describe("TeamWorkspaceBoard", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("Team Workspace");
|
||||
expect(container.textContent).toContain("角色执行画布");
|
||||
expect(container.textContent).toContain(
|
||||
"主对话只保留调度记录,每个角色在自己的泳道中输出过程与结果。",
|
||||
);
|
||||
expect(container.textContent).toContain("研究员");
|
||||
expect(container.textContent).toContain("执行器");
|
||||
expect(container.textContent).toContain("2 位成员已加入");
|
||||
@@ -313,6 +317,20 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(container.textContent).toContain("命令输出");
|
||||
expect(container.textContent).toContain("已对 3 个来源完成去重校验。");
|
||||
|
||||
const researcherLane = container.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-1"]',
|
||||
);
|
||||
const executorLane = container.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-2"]',
|
||||
);
|
||||
expect(researcherLane?.textContent).toContain("角色输出");
|
||||
expect(researcherLane?.textContent).toContain(
|
||||
"回复:已完成竞品摘要与数据来源梳理。",
|
||||
);
|
||||
expect(executorLane?.textContent).toContain(
|
||||
"计划:先整理落地步骤,再生成第一版实施清单。",
|
||||
);
|
||||
|
||||
const openButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("查看对话"),
|
||||
);
|
||||
@@ -325,6 +343,82 @@ describe("TeamWorkspaceBoard", () => {
|
||||
expect(onOpenSubagentSession).toHaveBeenCalledWith("child-1");
|
||||
});
|
||||
|
||||
it("真实成员存在时应优先按蓝图角色顺序排列泳道", async () => {
|
||||
const container = await renderBoard({
|
||||
childSubagentSessions: [
|
||||
{
|
||||
id: "child-executor",
|
||||
name: "执行成员",
|
||||
created_at: 1_710_000_010,
|
||||
updated_at: 1_710_000_120,
|
||||
session_type: "sub_agent",
|
||||
runtime_status: "queued",
|
||||
latest_turn_status: "queued",
|
||||
task_summary: "负责提交修复。",
|
||||
role_hint: "executor",
|
||||
blueprint_role_id: "runtime-executor",
|
||||
blueprint_role_label: "执行",
|
||||
profile_id: "code-executor",
|
||||
role_key: "executor",
|
||||
},
|
||||
{
|
||||
id: "child-explorer",
|
||||
name: "分析成员",
|
||||
created_at: 1_710_000_000,
|
||||
updated_at: 1_710_000_100,
|
||||
session_type: "sub_agent",
|
||||
runtime_status: "running",
|
||||
latest_turn_status: "running",
|
||||
task_summary: "负责定位问题。",
|
||||
role_hint: "explorer",
|
||||
blueprint_role_id: "runtime-explorer",
|
||||
blueprint_role_label: "分析",
|
||||
profile_id: "code-explorer",
|
||||
role_key: "explorer",
|
||||
},
|
||||
],
|
||||
runtimeTeamState: {
|
||||
requestId: "runtime-formed-ordered",
|
||||
status: "formed",
|
||||
label: "修复 Team",
|
||||
summary: "分析、执行协同推进。",
|
||||
members: [
|
||||
{
|
||||
id: "runtime-explorer",
|
||||
label: "分析",
|
||||
summary: "先定位问题。",
|
||||
roleKey: "explorer",
|
||||
profileId: "code-explorer",
|
||||
skillIds: ["repo-exploration"],
|
||||
status: "planned",
|
||||
},
|
||||
{
|
||||
id: "runtime-executor",
|
||||
label: "执行",
|
||||
summary: "再提交修复。",
|
||||
roleKey: "executor",
|
||||
profileId: "code-executor",
|
||||
skillIds: ["bounded-implementation"],
|
||||
status: "planned",
|
||||
},
|
||||
],
|
||||
blueprint: null,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
const laneIds = Array.from(
|
||||
container.querySelectorAll('[data-testid^="team-workspace-member-lane-"]'),
|
||||
).map((element) => element.getAttribute("data-testid"));
|
||||
|
||||
expect(laneIds).toEqual([
|
||||
"team-workspace-member-lane-child-explorer",
|
||||
"team-workspace-member-lane-child-executor",
|
||||
]);
|
||||
expect(container.textContent).toContain("蓝图 · 分析");
|
||||
expect(container.textContent).toContain("蓝图 · 执行");
|
||||
});
|
||||
|
||||
it("子线程视角应展示父会话、最近过程并支持切换 sibling", async () => {
|
||||
const onOpenSubagentSession = vi.fn();
|
||||
const onReturnToParentSession = vi.fn();
|
||||
@@ -420,6 +514,7 @@ describe("TeamWorkspaceBoard", () => {
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("主线程总览");
|
||||
expect(container.textContent).toContain("角色执行画布");
|
||||
expect(container.textContent).toContain("实现代理");
|
||||
expect(container.textContent).toContain("检索代理");
|
||||
expect(container.textContent).toContain("队列 1");
|
||||
@@ -436,6 +531,20 @@ describe("TeamWorkspaceBoard", () => {
|
||||
"工具 browser_snapshot:页面已刷新为最新状态并生成差异截图。",
|
||||
);
|
||||
|
||||
const currentLane = container.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-current"]',
|
||||
);
|
||||
const siblingLane = container.querySelector(
|
||||
'[data-testid="team-workspace-member-lane-child-sibling-1"]',
|
||||
);
|
||||
expect(currentLane?.textContent).toContain("角色输出");
|
||||
expect(currentLane?.textContent).toContain(
|
||||
"推理:先检查 team runtime 的控制面状态,再决定是否等待。",
|
||||
);
|
||||
expect(siblingLane?.textContent).toContain(
|
||||
"工具 browser_snapshot:页面已刷新为最新状态并生成差异截图。",
|
||||
);
|
||||
|
||||
const returnButton = Array.from(container.querySelectorAll("button")).find(
|
||||
(element) => element.textContent?.includes("返回父会话"),
|
||||
);
|
||||
|
||||
@@ -100,6 +100,8 @@ interface TeamSessionCard {
|
||||
model?: string;
|
||||
originTool?: string;
|
||||
createdFromTurnId?: string;
|
||||
blueprintRoleId?: string;
|
||||
blueprintRoleLabel?: string;
|
||||
profileId?: string;
|
||||
profileName?: string;
|
||||
roleKey?: string;
|
||||
@@ -588,6 +590,8 @@ function buildCurrentChildSession(
|
||||
sessionType: "sub_agent",
|
||||
originTool: subagentParentContext.origin_tool,
|
||||
createdFromTurnId: subagentParentContext.created_from_turn_id,
|
||||
blueprintRoleId: subagentParentContext.blueprint_role_id,
|
||||
blueprintRoleLabel: subagentParentContext.blueprint_role_label,
|
||||
profileId: subagentParentContext.profile_id,
|
||||
profileName: subagentParentContext.profile_name,
|
||||
roleKey: subagentParentContext.role_key,
|
||||
@@ -640,6 +644,136 @@ function dedupeSessions(
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeComparableText(value?: string | null): string {
|
||||
return value?.trim().toLocaleLowerCase() || "";
|
||||
}
|
||||
|
||||
function resolveSessionBlueprintRoleId(
|
||||
session: TeamSessionCard,
|
||||
runtimeRoles: Array<{
|
||||
id: string;
|
||||
label?: string | null;
|
||||
profileId?: string;
|
||||
roleKey?: string;
|
||||
}>,
|
||||
usedRoleIds: Set<string>,
|
||||
): string | null {
|
||||
const explicitRoleId = session.blueprintRoleId?.trim();
|
||||
if (
|
||||
explicitRoleId &&
|
||||
!usedRoleIds.has(explicitRoleId) &&
|
||||
runtimeRoles.some((role) => role.id === explicitRoleId)
|
||||
) {
|
||||
return explicitRoleId;
|
||||
}
|
||||
|
||||
const sessionBlueprintRoleLabel = normalizeComparableText(
|
||||
session.blueprintRoleLabel,
|
||||
);
|
||||
const sessionRoleKey = normalizeComparableText(
|
||||
session.roleKey || session.roleHint,
|
||||
);
|
||||
const sessionProfileId = normalizeComparableText(session.profileId);
|
||||
const sessionName = normalizeComparableText(session.name);
|
||||
|
||||
const candidates = runtimeRoles
|
||||
.filter((role) => !usedRoleIds.has(role.id))
|
||||
.map((role) => {
|
||||
let score = 0;
|
||||
if (
|
||||
sessionBlueprintRoleLabel &&
|
||||
normalizeComparableText(role.label) === sessionBlueprintRoleLabel
|
||||
) {
|
||||
score += 8;
|
||||
}
|
||||
if (sessionRoleKey && normalizeComparableText(role.roleKey) === sessionRoleKey) {
|
||||
score += 4;
|
||||
}
|
||||
if (
|
||||
sessionProfileId &&
|
||||
normalizeComparableText(role.profileId) === sessionProfileId
|
||||
) {
|
||||
score += 3;
|
||||
}
|
||||
if (sessionName && normalizeComparableText(role.label) === sessionName) {
|
||||
score += 2;
|
||||
}
|
||||
return {
|
||||
roleId: role.id,
|
||||
score,
|
||||
};
|
||||
})
|
||||
.filter((candidate) => candidate.score > 0)
|
||||
.sort((left, right) => right.score - left.score);
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
candidates.length > 1 &&
|
||||
candidates[0]?.score === candidates[1]?.score
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return candidates[0]?.roleId ?? null;
|
||||
}
|
||||
|
||||
function orderSessionsByRuntimeRoles(
|
||||
sessions: TeamSessionCard[],
|
||||
runtimeTeamState?: TeamWorkspaceRuntimeFormationState | null,
|
||||
): TeamSessionCard[] {
|
||||
if (sessions.length <= 1 || !runtimeTeamState) {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
const runtimeRoles = (
|
||||
runtimeTeamState.members.length > 0
|
||||
? runtimeTeamState.members
|
||||
: runtimeTeamState.blueprint?.roles ?? []
|
||||
).map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
profileId: role.profileId,
|
||||
roleKey: role.roleKey,
|
||||
}));
|
||||
|
||||
if (runtimeRoles.length === 0) {
|
||||
return sessions;
|
||||
}
|
||||
|
||||
const roleOrder = new Map(runtimeRoles.map((role, index) => [role.id, index]));
|
||||
const usedRoleIds = new Set<string>();
|
||||
|
||||
return [...sessions]
|
||||
.map((session, index) => {
|
||||
const matchedRoleId = resolveSessionBlueprintRoleId(
|
||||
session,
|
||||
runtimeRoles,
|
||||
usedRoleIds,
|
||||
);
|
||||
if (matchedRoleId) {
|
||||
usedRoleIds.add(matchedRoleId);
|
||||
}
|
||||
return {
|
||||
session,
|
||||
index,
|
||||
matchedRoleId,
|
||||
roleOrder:
|
||||
matchedRoleId !== null
|
||||
? (roleOrder.get(matchedRoleId) ?? Number.MAX_SAFE_INTEGER)
|
||||
: Number.MAX_SAFE_INTEGER,
|
||||
};
|
||||
})
|
||||
.sort((left, right) => {
|
||||
if (left.roleOrder !== right.roleOrder) {
|
||||
return left.roleOrder - right.roleOrder;
|
||||
}
|
||||
return left.index - right.index;
|
||||
})
|
||||
.map((entry) => entry.session);
|
||||
}
|
||||
|
||||
function buildBoardHeadline(params: {
|
||||
hasRealTeamGraph: boolean;
|
||||
isChildSession: boolean;
|
||||
@@ -730,6 +864,39 @@ function buildRuntimeFormationEmptyDetail(
|
||||
}
|
||||
}
|
||||
|
||||
function buildSessionLaneEmptyState(params: {
|
||||
session?: TeamSessionCard | null;
|
||||
previewState?: SessionActivityPreviewState | null;
|
||||
}) {
|
||||
const { session, previewState } = params;
|
||||
|
||||
if (previewState?.status === "error") {
|
||||
return previewState.errorMessage?.trim() || "同步角色输出失败";
|
||||
}
|
||||
|
||||
if (previewState?.status === "loading") {
|
||||
return "正在同步该角色的最新输出...";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "queued") {
|
||||
return "已进入队列,等待 orchestrator 拉起执行。";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "running") {
|
||||
return "角色已开始执行,最新过程会持续刷新到这里。";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "completed") {
|
||||
return "该角色已完成当前任务,等待 orchestrator 汇总或继续交接。";
|
||||
}
|
||||
|
||||
if (session?.runtimeStatus === "failed" || session?.runtimeStatus === "aborted") {
|
||||
return "该角色执行失败,可在下方焦点区查看细节并决定是否恢复。";
|
||||
}
|
||||
|
||||
return "该角色暂未产出可展示的正文内容。";
|
||||
}
|
||||
|
||||
export function TeamWorkspaceBoard({
|
||||
className,
|
||||
embedded = false,
|
||||
@@ -833,6 +1000,8 @@ export function TeamWorkspaceBoard({
|
||||
model: session.model,
|
||||
originTool: session.origin_tool,
|
||||
createdFromTurnId: session.created_from_turn_id,
|
||||
blueprintRoleId: session.blueprint_role_id,
|
||||
blueprintRoleLabel: session.blueprint_role_label,
|
||||
profileId: session.profile_id,
|
||||
profileName: session.profile_name,
|
||||
roleKey: session.role_key,
|
||||
@@ -1159,6 +1328,16 @@ export function TeamWorkspaceBoard({
|
||||
);
|
||||
};
|
||||
|
||||
const memberCanvasSessions = useMemo(
|
||||
() =>
|
||||
orderSessionsByRuntimeRoles(
|
||||
isChildSession
|
||||
? dedupeSessions([currentChildSession, ...visibleSessions])
|
||||
: visibleSessions,
|
||||
runtimeTeamState,
|
||||
),
|
||||
[currentChildSession, isChildSession, runtimeTeamState, visibleSessions],
|
||||
);
|
||||
const railSessions = useMemo(
|
||||
() =>
|
||||
dedupeSessions(
|
||||
@@ -1184,7 +1363,7 @@ export function TeamWorkspaceBoard({
|
||||
useEffect(() => {
|
||||
const defaultSelectedId = isChildSession
|
||||
? currentSessionId ?? railSessions[0]?.id ?? null
|
||||
: visibleSessions[0]?.id ?? railSessions[0]?.id ?? null;
|
||||
: memberCanvasSessions[0]?.id ?? railSessions[0]?.id ?? null;
|
||||
|
||||
if (!selectedSessionId) {
|
||||
setSelectedSessionId(defaultSelectedId);
|
||||
@@ -1199,17 +1378,17 @@ export function TeamWorkspaceBoard({
|
||||
if (
|
||||
!isChildSession &&
|
||||
selectedSessionId === orchestratorSession?.id &&
|
||||
visibleSessions.length > 0
|
||||
memberCanvasSessions.length > 0
|
||||
) {
|
||||
setSelectedSessionId(visibleSessions[0]?.id ?? defaultSelectedId);
|
||||
setSelectedSessionId(memberCanvasSessions[0]?.id ?? defaultSelectedId);
|
||||
}
|
||||
}, [
|
||||
currentSessionId,
|
||||
isChildSession,
|
||||
memberCanvasSessions,
|
||||
orchestratorSession?.id,
|
||||
railSessions,
|
||||
selectedSessionId,
|
||||
visibleSessions,
|
||||
]);
|
||||
|
||||
const selectedSession = useMemo(
|
||||
@@ -1728,6 +1907,18 @@ export function TeamWorkspaceBoard({
|
||||
},
|
||||
[onSendSubagentInput, selectedSession, selectedSessionInputMessage],
|
||||
);
|
||||
const memberCanvasTitle = "角色执行画布";
|
||||
const memberCanvasSubtitle = hasRealTeamGraph
|
||||
? isChildSession
|
||||
? "当前成员与同组角色在各自泳道中输出过程与结果,主对话仅保留调度记录。"
|
||||
: `${visibleSessions.length} 位成员已加入,主对话只保留调度记录,每个角色在自己的泳道中输出过程与结果。`
|
||||
: runtimeTeamState?.status === "forming"
|
||||
? "正在准备本轮角色泳道,真实成员接入后会在这里独立输出。"
|
||||
: runtimeTeamState?.status === "formed"
|
||||
? "本轮编队已完成,等待真实角色接入后在各自泳道中开始执行。"
|
||||
: runtimeTeamState?.status === "failed"
|
||||
? "本轮编队失败,暂时无法生成角色泳道。"
|
||||
: "等待成员加入后,这里会按角色展开各自的输出泳道。";
|
||||
|
||||
if (
|
||||
!subagentParentContext &&
|
||||
@@ -1825,6 +2016,9 @@ export function TeamWorkspaceBoard({
|
||||
const selectedPresetOption = getTeamPresetOption(selectedSession?.teamPresetId);
|
||||
const selectedSkills = selectedSession?.skills ?? [];
|
||||
const selectedMetadata = [
|
||||
selectedSession?.blueprintRoleLabel
|
||||
? `蓝图角色 ${selectedSession.blueprintRoleLabel}`
|
||||
: null,
|
||||
selectedSession?.sessionType
|
||||
? resolveSessionTypeLabel(selectedSession.sessionType)
|
||||
: null,
|
||||
@@ -1884,20 +2078,6 @@ export function TeamWorkspaceBoard({
|
||||
const timelineEntryClassName = embedded
|
||||
? "border-l-2 border-slate-200 pl-3"
|
||||
: "rounded-[14px] border border-slate-200 bg-white p-3";
|
||||
const railTitle = isChildSession ? "同组成员轨道" : "团队成员轨道";
|
||||
const railSubtitle = isChildSession
|
||||
? siblingCount > 0
|
||||
? `当前成员与 ${siblingCount} 位同组成员协作`
|
||||
: "当前只有一位成员"
|
||||
: hasRealTeamGraph
|
||||
? `${visibleSessions.length} 位成员已加入`
|
||||
: runtimeTeamState?.status === "forming"
|
||||
? "正在准备本轮成员"
|
||||
: runtimeTeamState?.status === "formed"
|
||||
? `${runtimeMembers.length} 个成员待启动`
|
||||
: runtimeTeamState?.status === "failed"
|
||||
? "Team 准备失败"
|
||||
: "等待成员加入";
|
||||
|
||||
return (
|
||||
<section
|
||||
@@ -2003,10 +2183,10 @@ export function TeamWorkspaceBoard({
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<div className="text-[11px] font-semibold uppercase tracking-[0.16em] text-slate-500">
|
||||
{railTitle}
|
||||
{memberCanvasTitle}
|
||||
</div>
|
||||
<div className="mt-1 text-sm font-semibold text-slate-900">
|
||||
{railSubtitle}
|
||||
{memberCanvasSubtitle}
|
||||
</div>
|
||||
</div>
|
||||
{selectedSession ? (
|
||||
@@ -2141,17 +2321,17 @@ export function TeamWorkspaceBoard({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{railSessions.length > 0 ? (
|
||||
{memberCanvasSessions.length > 0 ? (
|
||||
<div
|
||||
className={cn(
|
||||
"mt-3",
|
||||
stackedRail
|
||||
? "grid gap-2 md:grid-cols-2"
|
||||
: "flex gap-3 overflow-x-auto pb-1 [scrollbar-width:none] [&::-webkit-scrollbar]:hidden",
|
||||
? "grid gap-3 md:grid-cols-2"
|
||||
: "grid gap-3 xl:grid-cols-2 2xl:grid-cols-3",
|
||||
)}
|
||||
data-testid="team-workspace-rail-list"
|
||||
>
|
||||
{railSessions.map((session) => {
|
||||
{memberCanvasSessions.map((session) => {
|
||||
const meta = resolveStatusMeta(session.runtimeStatus);
|
||||
const selected = selectedSession?.id === session.id;
|
||||
const runtimeDetail = buildRuntimeDetailSummary(session);
|
||||
@@ -2160,10 +2340,17 @@ export function TeamWorkspaceBoard({
|
||||
sessionActivityPreviewById[session.id]?.entries,
|
||||
ACTIVITY_TIMELINE_ENTRY_LIMIT,
|
||||
);
|
||||
const cardActivityPreview = buildCardActivityPreview(
|
||||
buildActivityPreviewFromEntry(mergedCardEntries[0]) ??
|
||||
sessionActivityPreviewById[session.id]?.preview,
|
||||
);
|
||||
const previewState = sessionActivityPreviewById[session.id] ?? null;
|
||||
const cardActivityPreview = buildActivityPreviewFromEntry(
|
||||
mergedCardEntries[0],
|
||||
)
|
||||
?? previewState?.preview
|
||||
?? null;
|
||||
const laneEntries = mergedCardEntries.slice(0, 3);
|
||||
const laneEmptyState = buildSessionLaneEmptyState({
|
||||
session,
|
||||
previewState,
|
||||
});
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -2172,13 +2359,14 @@ export function TeamWorkspaceBoard({
|
||||
aria-pressed={selected}
|
||||
onClick={() => setSelectedSessionId(session.id)}
|
||||
className={cn(
|
||||
"group flex flex-col rounded-[18px] border p-3 text-left transition",
|
||||
stackedRail ? "w-full" : "w-[258px] shrink-0",
|
||||
"group flex flex-col rounded-[18px] border p-4 text-left transition",
|
||||
"w-full",
|
||||
meta.cardClassName,
|
||||
selected
|
||||
? "ring-1 ring-slate-300 bg-slate-50"
|
||||
? "ring-2 ring-slate-300 bg-slate-50"
|
||||
: "hover:border-slate-300 hover:bg-slate-50",
|
||||
)}
|
||||
data-testid={`team-workspace-member-lane-${session.id}`}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -2193,6 +2381,11 @@ export function TeamWorkspaceBoard({
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1.5 flex flex-wrap items-center gap-1.5 text-[11px] text-slate-500">
|
||||
{session.blueprintRoleLabel ? (
|
||||
<span className="rounded-full border border-sky-200 bg-sky-50 px-2 py-0.5 text-sky-700">
|
||||
蓝图 · {session.blueprintRoleLabel}
|
||||
</span>
|
||||
) : null}
|
||||
{session.roleHint ? (
|
||||
<span className="rounded-full border border-slate-200 bg-white px-2 py-0.5">
|
||||
{session.roleHint}
|
||||
@@ -2239,29 +2432,47 @@ export function TeamWorkspaceBoard({
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-2 text-sm leading-5 text-slate-600",
|
||||
stackedRail ? "line-clamp-4" : "line-clamp-2",
|
||||
)}
|
||||
>
|
||||
<p className="mt-2 line-clamp-3 text-sm leading-5 text-slate-600">
|
||||
{session.taskSummary ||
|
||||
"暂未生成任务摘要,打开该会话可查看完整上下文。"}
|
||||
</p>
|
||||
{cardActivityPreview ? (
|
||||
<div className="mt-2 border-l-2 border-slate-200 pl-3">
|
||||
<div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<Activity className="h-3 w-3" />
|
||||
<span>最近过程</span>
|
||||
</div>
|
||||
<p
|
||||
className={cn(
|
||||
"mt-1 text-[11px] leading-5 text-slate-600",
|
||||
stackedRail ? "line-clamp-3" : "line-clamp-2",
|
||||
)}
|
||||
>
|
||||
{cardActivityPreview}
|
||||
</p>
|
||||
<div className="mt-3 rounded-2xl border border-slate-200 bg-white px-3 py-3">
|
||||
<div className="flex items-center gap-1.5 text-[10px] font-semibold uppercase tracking-[0.12em] text-slate-500">
|
||||
<PanelTop className="h-3 w-3" />
|
||||
<span>角色输出</span>
|
||||
<span className="rounded-full border border-slate-200 bg-slate-50 px-2 py-0.5 text-[10px] font-medium tracking-normal text-slate-600 normal-case">
|
||||
最近过程
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-2 line-clamp-5 whitespace-pre-wrap break-words text-[12px] leading-5 text-slate-700">
|
||||
{buildCardActivityPreview(cardActivityPreview) || laneEmptyState}
|
||||
</p>
|
||||
</div>
|
||||
{laneEntries.length > 0 ? (
|
||||
<div className="mt-3 space-y-2">
|
||||
{laneEntries.map((entry) => (
|
||||
<div
|
||||
key={`${session.id}-${entry.id}`}
|
||||
className="rounded-2xl border border-slate-200 bg-slate-50 px-3 py-2.5"
|
||||
>
|
||||
<div className="flex flex-wrap items-center gap-2 text-[11px]">
|
||||
<span className="font-semibold text-slate-800">
|
||||
{entry.title}
|
||||
</span>
|
||||
<span
|
||||
className={cn(
|
||||
"rounded-full px-2 py-0.5 font-medium",
|
||||
entry.badgeClassName,
|
||||
)}
|
||||
>
|
||||
{entry.statusLabel}
|
||||
</span>
|
||||
</div>
|
||||
<p className="mt-1.5 line-clamp-4 whitespace-pre-wrap break-words text-xs leading-5 text-slate-600">
|
||||
{entry.detail}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{runtimeDetail ? (
|
||||
@@ -2297,17 +2508,17 @@ export function TeamWorkspaceBoard({
|
||||
)}
|
||||
<div className="mt-4 rounded-[20px] border border-dashed border-slate-200 bg-slate-50 p-5 text-sm leading-6 text-slate-500">
|
||||
{runtimeTeamState?.status === "forming" ? (
|
||||
"模型正在准备本轮 Team,完成后这里会先展示本轮成员;后续真实成员加入时,再切换为可纵向浏览的团队轨道。"
|
||||
"模型正在准备本轮 Team,完成后这里会先展示本轮成员;后续真实成员加入时,再切换为按角色独立输出的执行画布。"
|
||||
) : runtimeTeamState?.status === "formed" ? (
|
||||
<>
|
||||
本轮 Team 已就绪。系统开始分派成员后,这里会从当前编队过渡到真实团队轨道。
|
||||
本轮 Team 已就绪。系统开始分派成员后,这里会从当前编队过渡到真实角色画布。
|
||||
</>
|
||||
) : runtimeTeamState?.status === "failed" ? (
|
||||
runtimeTeamState.errorMessage?.trim() ||
|
||||
"Team 准备失败,暂时还没有真实团队成员。"
|
||||
) : (
|
||||
<>
|
||||
还没有真实团队成员。系统开始分派成员后,这里会生成可纵向浏览的团队轨道。
|
||||
还没有真实团队成员。系统开始分派成员后,这里会生成按角色分栏的执行画布。
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -2,13 +2,15 @@ import React from "react";
|
||||
import { act } from "react";
|
||||
import { createRoot, type Root } from "react-dom/client";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TeamWorkspaceRuntimeFormationState } from "../teamWorkspaceRuntime";
|
||||
import type { TeamDefinition } from "../utils/teamDefinitions";
|
||||
import {
|
||||
shouldGenerateRuntimeTeamAfterSend,
|
||||
shouldPrepareRuntimeTeamBeforeSend,
|
||||
useRuntimeTeamFormation,
|
||||
} from "./useRuntimeTeamFormation";
|
||||
|
||||
type HookProps = Parameters<typeof useRuntimeTeamFormation>[0];
|
||||
type RuntimeTeamPreparationResult = TeamWorkspaceRuntimeFormationState | null;
|
||||
|
||||
const mountedRoots: Array<{ root: Root; container: HTMLDivElement }> = [];
|
||||
|
||||
@@ -115,28 +117,32 @@ describe("useRuntimeTeamFormation", () => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("发送后满足条件时应进入 forming 并在生成完成后切到 formed", async () => {
|
||||
const generateRuntimeTeam = vi.fn(async () => ({
|
||||
id: "ephemeral-1",
|
||||
source: "ephemeral" as const,
|
||||
label: "自动 Team",
|
||||
description: "自动生成",
|
||||
roles: [
|
||||
{
|
||||
id: "role-1",
|
||||
label: "执行者",
|
||||
summary: "负责执行",
|
||||
},
|
||||
],
|
||||
}));
|
||||
it("发送前满足条件时应进入 forming 并在生成完成后切到 formed", async () => {
|
||||
let resolveRuntimeTeam:
|
||||
| ((team: TeamDefinition) => void)
|
||||
| null = null;
|
||||
const generateRuntimeTeam = vi.fn(
|
||||
async (
|
||||
_options: Parameters<NonNullable<HookProps["generateRuntimeTeam"]>>[0],
|
||||
): Promise<TeamDefinition> =>
|
||||
new Promise((resolve) => {
|
||||
resolveRuntimeTeam = resolve;
|
||||
}),
|
||||
);
|
||||
const { render, getValue } = renderHook({
|
||||
generateRuntimeTeam,
|
||||
});
|
||||
|
||||
await render();
|
||||
|
||||
act(() => {
|
||||
getValue().handleRuntimeTeamAfterSend({
|
||||
const resultRef: { current: RuntimeTeamPreparationResult } = {
|
||||
current: null,
|
||||
};
|
||||
let pending:
|
||||
| Promise<RuntimeTeamPreparationResult>
|
||||
| null = null;
|
||||
await act(async () => {
|
||||
pending = getValue().prepareRuntimeTeamBeforeSend({
|
||||
input: "请拆成两个子任务",
|
||||
providerType: "openai",
|
||||
model: "gpt-4.1",
|
||||
@@ -156,20 +162,59 @@ describe("useRuntimeTeamFormation", () => {
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
resolveRuntimeTeam?.({
|
||||
id: "ephemeral-1",
|
||||
source: "ephemeral",
|
||||
label: "自动 Team",
|
||||
description: "自动生成",
|
||||
roles: [
|
||||
{
|
||||
id: "role-1",
|
||||
label: "执行者",
|
||||
summary: "负责执行",
|
||||
},
|
||||
],
|
||||
});
|
||||
resultRef.current = await pending;
|
||||
});
|
||||
|
||||
expect(resultRef.current?.status).toBe("formed");
|
||||
expect(getValue().runtimeTeamState?.status).toBe("formed");
|
||||
expect(getValue().runtimeTeamState?.label).toBe("自动 Team");
|
||||
});
|
||||
|
||||
it("发送前 Team 生成失败时应返回 failed 状态", async () => {
|
||||
const { render, getValue } = renderHook({
|
||||
generateRuntimeTeam: vi.fn(async () => {
|
||||
throw new Error("模型规划失败");
|
||||
}),
|
||||
});
|
||||
|
||||
await render();
|
||||
|
||||
const resultRef: { current: RuntimeTeamPreparationResult } = {
|
||||
current: null,
|
||||
};
|
||||
await act(async () => {
|
||||
resultRef.current = await getValue().prepareRuntimeTeamBeforeSend({
|
||||
input: "请拆解任务",
|
||||
providerType: "openai",
|
||||
model: "gpt-4.1",
|
||||
executionStrategy: "react",
|
||||
});
|
||||
});
|
||||
|
||||
expect(resultRef.current?.status).toBe("failed");
|
||||
expect(resultRef.current?.errorMessage).toBe("模型规划失败");
|
||||
expect(getValue().runtimeTeamState?.status).toBe("failed");
|
||||
});
|
||||
|
||||
it("subagent 关闭且没有真实 team 图时,应清空 runtime team 状态", async () => {
|
||||
const { render, getValue } = renderHook();
|
||||
await render();
|
||||
|
||||
act(() => {
|
||||
getValue().triggerRuntimeTeamFormation({
|
||||
void getValue().triggerRuntimeTeamFormation({
|
||||
input: "请先规划 team",
|
||||
providerType: "openai",
|
||||
model: "gpt-4.1",
|
||||
@@ -187,12 +232,40 @@ describe("useRuntimeTeamFormation", () => {
|
||||
expect(getValue().runtimeTeamState).toBeNull();
|
||||
});
|
||||
|
||||
it("本轮不走 Team 且没有真实 team 图时,应清空上一轮 runtime team 状态", async () => {
|
||||
const { render, getValue } = renderHook();
|
||||
await render();
|
||||
|
||||
await act(async () => {
|
||||
await getValue().prepareRuntimeTeamBeforeSend({
|
||||
input: "请拆分成多个角色执行",
|
||||
providerType: "openai",
|
||||
model: "gpt-4.1",
|
||||
executionStrategy: "react",
|
||||
});
|
||||
});
|
||||
|
||||
expect(getValue().runtimeTeamState?.status).toBe("formed");
|
||||
|
||||
await act(async () => {
|
||||
await getValue().prepareRuntimeTeamBeforeSend({
|
||||
input: "请直接润色这段文案",
|
||||
providerType: "openai",
|
||||
model: "gpt-4.1",
|
||||
executionStrategy: "react",
|
||||
purpose: "content_review",
|
||||
});
|
||||
});
|
||||
|
||||
expect(getValue().runtimeTeamState).toBeNull();
|
||||
});
|
||||
|
||||
it("session 切换时应重置 runtime team 状态", async () => {
|
||||
const { render, getValue } = renderHook();
|
||||
await render();
|
||||
|
||||
act(() => {
|
||||
getValue().triggerRuntimeTeamFormation({
|
||||
void getValue().triggerRuntimeTeamFormation({
|
||||
input: "请先规划 team",
|
||||
providerType: "openai",
|
||||
model: "gpt-4.1",
|
||||
@@ -210,10 +283,10 @@ describe("useRuntimeTeamFormation", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("shouldGenerateRuntimeTeamAfterSend", () => {
|
||||
describe("shouldPrepareRuntimeTeamBeforeSend", () => {
|
||||
it("仅在 subagent 开启、存在项目、非 purpose 且输入非空时返回 true", () => {
|
||||
expect(
|
||||
shouldGenerateRuntimeTeamAfterSend({
|
||||
shouldPrepareRuntimeTeamBeforeSend({
|
||||
subagentEnabled: true,
|
||||
projectId: "project-1",
|
||||
input: "请拆解任务",
|
||||
@@ -221,7 +294,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
|
||||
).toBe(true);
|
||||
|
||||
expect(
|
||||
shouldGenerateRuntimeTeamAfterSend({
|
||||
shouldPrepareRuntimeTeamBeforeSend({
|
||||
subagentEnabled: false,
|
||||
projectId: "project-1",
|
||||
input: "请拆解任务",
|
||||
@@ -229,7 +302,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldGenerateRuntimeTeamAfterSend({
|
||||
shouldPrepareRuntimeTeamBeforeSend({
|
||||
subagentEnabled: true,
|
||||
projectId: "",
|
||||
input: "请拆解任务",
|
||||
@@ -237,7 +310,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldGenerateRuntimeTeamAfterSend({
|
||||
shouldPrepareRuntimeTeamBeforeSend({
|
||||
subagentEnabled: true,
|
||||
projectId: "project-1",
|
||||
input: " ",
|
||||
@@ -245,7 +318,7 @@ describe("shouldGenerateRuntimeTeamAfterSend", () => {
|
||||
).toBe(false);
|
||||
|
||||
expect(
|
||||
shouldGenerateRuntimeTeamAfterSend({
|
||||
shouldPrepareRuntimeTeamBeforeSend({
|
||||
subagentEnabled: true,
|
||||
projectId: "project-1",
|
||||
input: "请拆解任务",
|
||||
|
||||
@@ -14,9 +14,10 @@ interface TriggerRuntimeTeamFormationParams {
|
||||
executionStrategy?: "react" | "code_orchestrated" | "auto";
|
||||
}
|
||||
|
||||
interface HandleRuntimeTeamAfterSendParams
|
||||
interface PrepareRuntimeTeamBeforeSendParams
|
||||
extends TriggerRuntimeTeamFormationParams {
|
||||
purpose?: HandleSendOptions["purpose"];
|
||||
subagentEnabled?: boolean;
|
||||
}
|
||||
|
||||
interface UseRuntimeTeamFormationOptions {
|
||||
@@ -36,15 +37,17 @@ export interface UseRuntimeTeamFormationResult {
|
||||
clearRuntimeTeamState: () => void;
|
||||
triggerRuntimeTeamFormation: (
|
||||
params: TriggerRuntimeTeamFormationParams,
|
||||
) => void;
|
||||
handleRuntimeTeamAfterSend: (params: HandleRuntimeTeamAfterSendParams) => void;
|
||||
) => Promise<TeamWorkspaceRuntimeFormationState | null>;
|
||||
prepareRuntimeTeamBeforeSend: (
|
||||
params: PrepareRuntimeTeamBeforeSendParams,
|
||||
) => Promise<TeamWorkspaceRuntimeFormationState | null>;
|
||||
}
|
||||
|
||||
function defaultCreateRequestId() {
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export function shouldGenerateRuntimeTeamAfterSend(params: {
|
||||
export function shouldPrepareRuntimeTeamBeforeSend(params: {
|
||||
subagentEnabled: boolean;
|
||||
projectId?: string | null;
|
||||
input: string;
|
||||
@@ -79,7 +82,7 @@ export function useRuntimeTeamFormation({
|
||||
}, []);
|
||||
|
||||
const triggerRuntimeTeamFormation = useCallback(
|
||||
({
|
||||
async ({
|
||||
input,
|
||||
providerType,
|
||||
model,
|
||||
@@ -87,61 +90,60 @@ export function useRuntimeTeamFormation({
|
||||
}: TriggerRuntimeTeamFormationParams) => {
|
||||
const normalizedInput = input.trim();
|
||||
if (!projectId || !normalizedInput) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
const requestId = createRequestId();
|
||||
runtimeTeamRequestIdRef.current = requestId;
|
||||
setRuntimeTeamState(
|
||||
createRuntimeFormationStateFromTeam({
|
||||
const formingState = createRuntimeFormationStateFromTeam({
|
||||
requestId,
|
||||
status: "forming",
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
updatedAt: now(),
|
||||
});
|
||||
setRuntimeTeamState(formingState);
|
||||
|
||||
try {
|
||||
const runtimeTeam = await generateRuntimeTeam({
|
||||
workspaceId: projectId,
|
||||
providerType,
|
||||
model,
|
||||
executionStrategy,
|
||||
activeTheme,
|
||||
input: normalizedInput,
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
});
|
||||
|
||||
if (runtimeTeamRequestIdRef.current !== requestId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const formedState = createRuntimeFormationStateFromTeam({
|
||||
requestId,
|
||||
status: "forming",
|
||||
status: "formed",
|
||||
runtimeTeam,
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
updatedAt: now(),
|
||||
}),
|
||||
);
|
||||
|
||||
void generateRuntimeTeam({
|
||||
workspaceId: projectId,
|
||||
providerType,
|
||||
model,
|
||||
executionStrategy,
|
||||
activeTheme,
|
||||
input: normalizedInput,
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
})
|
||||
.then((runtimeTeam) => {
|
||||
if (runtimeTeamRequestIdRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
setRuntimeTeamState(
|
||||
createRuntimeFormationStateFromTeam({
|
||||
requestId,
|
||||
status: "formed",
|
||||
runtimeTeam,
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
updatedAt: now(),
|
||||
}),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (runtimeTeamRequestIdRef.current !== requestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Team 生成失败";
|
||||
setRuntimeTeamState(
|
||||
createRuntimeFormationStateFromTeam({
|
||||
requestId,
|
||||
status: "failed",
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
errorMessage,
|
||||
updatedAt: now(),
|
||||
}),
|
||||
);
|
||||
});
|
||||
setRuntimeTeamState(formedState);
|
||||
return formedState;
|
||||
} catch (error) {
|
||||
if (runtimeTeamRequestIdRef.current !== requestId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : "Team 生成失败";
|
||||
const failedState = createRuntimeFormationStateFromTeam({
|
||||
requestId,
|
||||
status: "failed",
|
||||
blueprintTeam: selectedTeam ?? null,
|
||||
errorMessage,
|
||||
updatedAt: now(),
|
||||
});
|
||||
setRuntimeTeamState(failedState);
|
||||
return failedState;
|
||||
}
|
||||
},
|
||||
[
|
||||
activeTheme,
|
||||
@@ -153,34 +155,38 @@ export function useRuntimeTeamFormation({
|
||||
],
|
||||
);
|
||||
|
||||
const handleRuntimeTeamAfterSend = useCallback(
|
||||
const prepareRuntimeTeamBeforeSend = useCallback(
|
||||
({
|
||||
input,
|
||||
providerType,
|
||||
model,
|
||||
executionStrategy,
|
||||
purpose,
|
||||
}: HandleRuntimeTeamAfterSendParams) => {
|
||||
subagentEnabled: subagentEnabledOverride,
|
||||
}: PrepareRuntimeTeamBeforeSendParams) => {
|
||||
const effectiveSubagentEnabled =
|
||||
subagentEnabledOverride ?? subagentEnabled;
|
||||
if (
|
||||
shouldGenerateRuntimeTeamAfterSend({
|
||||
subagentEnabled,
|
||||
shouldPrepareRuntimeTeamBeforeSend({
|
||||
subagentEnabled: effectiveSubagentEnabled,
|
||||
projectId,
|
||||
input,
|
||||
purpose,
|
||||
})
|
||||
) {
|
||||
triggerRuntimeTeamFormation({
|
||||
return triggerRuntimeTeamFormation({
|
||||
input,
|
||||
providerType,
|
||||
model,
|
||||
executionStrategy,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!subagentEnabled && !hasRealTeamGraph) {
|
||||
if (!hasRealTeamGraph) {
|
||||
clearRuntimeTeamState();
|
||||
}
|
||||
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
[
|
||||
clearRuntimeTeamState,
|
||||
@@ -205,6 +211,6 @@ export function useRuntimeTeamFormation({
|
||||
runtimeTeamState,
|
||||
clearRuntimeTeamState,
|
||||
triggerRuntimeTeamFormation,
|
||||
handleRuntimeTeamAfterSend,
|
||||
prepareRuntimeTeamBeforeSend,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -121,6 +121,64 @@ describe("harnessRequestMetadata", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("应写入本轮 Team 决策与预编队蓝图", () => {
|
||||
const metadata = buildHarnessRequestMetadata({
|
||||
theme: "general",
|
||||
creationMode: "guided",
|
||||
chatMode: "agent",
|
||||
webSearchEnabled: false,
|
||||
thinkingEnabled: true,
|
||||
taskModeEnabled: true,
|
||||
subagentModeEnabled: true,
|
||||
sessionMode: "default",
|
||||
turnTeamDecision: "team_prepared",
|
||||
turnTeamReason: "runtime_team_prepared",
|
||||
turnTeamBlueprint: {
|
||||
label: "本轮调试 Team",
|
||||
description: "先分析,再实现,最后验证。",
|
||||
roles: [
|
||||
{
|
||||
id: "explorer",
|
||||
label: "分析",
|
||||
summary: "负责定位问题。",
|
||||
profileId: "code-explorer",
|
||||
roleKey: "explorer",
|
||||
skillIds: ["repo-exploration"],
|
||||
},
|
||||
{
|
||||
id: "executor",
|
||||
label: "执行",
|
||||
summary: "负责提交修复。",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
expect(metadata).toMatchObject({
|
||||
turn_team_decision: "team_prepared",
|
||||
turn_team_reason: "runtime_team_prepared",
|
||||
turn_team_blueprint: {
|
||||
label: "本轮调试 Team",
|
||||
description: "先分析,再实现,最后验证。",
|
||||
roles: [
|
||||
{
|
||||
id: "explorer",
|
||||
label: "分析",
|
||||
summary: "负责定位问题。",
|
||||
profile_id: "code-explorer",
|
||||
role_key: "explorer",
|
||||
skill_ids: ["repo-exploration"],
|
||||
},
|
||||
{
|
||||
id: "executor",
|
||||
label: "执行",
|
||||
summary: "负责提交修复。",
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("需要人工确认的浏览器任务应标记 user step", () => {
|
||||
const metadata = buildHarnessRequestMetadata({
|
||||
theme: "general",
|
||||
|
||||
@@ -2,6 +2,14 @@ import type { CreationMode } from "../components/types";
|
||||
import type { BrowserTaskRequirement } from "../types";
|
||||
import type { TeamRoleDefinition, TeamDefinitionSource } from "./teamDefinitions";
|
||||
|
||||
export type HarnessTurnTeamDecision = "single_agent" | "team_prepared";
|
||||
|
||||
export interface HarnessTurnTeamBlueprint {
|
||||
label?: string | null;
|
||||
description?: string | null;
|
||||
roles?: TeamRoleDefinition[] | null;
|
||||
}
|
||||
|
||||
export interface BuildHarnessRequestMetadataOptions {
|
||||
base?: Record<string, unknown>;
|
||||
theme: string;
|
||||
@@ -25,6 +33,9 @@ export interface BuildHarnessRequestMetadataOptions {
|
||||
selectedTeamLabel?: string | null;
|
||||
selectedTeamSummary?: string | null;
|
||||
selectedTeamRoles?: TeamRoleDefinition[] | null;
|
||||
turnTeamDecision?: HarnessTurnTeamDecision | null;
|
||||
turnTeamReason?: string | null;
|
||||
turnTeamBlueprint?: HarnessTurnTeamBlueprint | null;
|
||||
}
|
||||
|
||||
export function extractExistingHarnessMetadata(
|
||||
@@ -68,8 +79,26 @@ export function buildHarnessRequestMetadata(
|
||||
selectedTeamLabel,
|
||||
selectedTeamSummary,
|
||||
selectedTeamRoles,
|
||||
turnTeamDecision,
|
||||
turnTeamReason,
|
||||
turnTeamBlueprint,
|
||||
} = options;
|
||||
|
||||
const serializeTeamRoles = (roles?: TeamRoleDefinition[] | null) =>
|
||||
roles && roles.length > 0
|
||||
? roles.map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
summary: role.summary,
|
||||
profile_id: role.profileId || undefined,
|
||||
role_key: role.roleKey || undefined,
|
||||
skill_ids:
|
||||
role.skillIds && role.skillIds.length > 0
|
||||
? [...role.skillIds]
|
||||
: undefined,
|
||||
}))
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
...(base || {}),
|
||||
theme,
|
||||
@@ -89,19 +118,19 @@ export function buildHarnessRequestMetadata(
|
||||
selected_team_source: selectedTeamSource || undefined,
|
||||
selected_team_label: selectedTeamLabel || undefined,
|
||||
selected_team_summary: selectedTeamSummary || undefined,
|
||||
selected_team_roles:
|
||||
selectedTeamRoles && selectedTeamRoles.length > 0
|
||||
? selectedTeamRoles.map((role) => ({
|
||||
id: role.id,
|
||||
label: role.label,
|
||||
summary: role.summary,
|
||||
profile_id: role.profileId || undefined,
|
||||
role_key: role.roleKey || undefined,
|
||||
skill_ids:
|
||||
role.skillIds && role.skillIds.length > 0
|
||||
? [...role.skillIds]
|
||||
: undefined,
|
||||
}))
|
||||
selected_team_roles: serializeTeamRoles(selectedTeamRoles),
|
||||
turn_team_decision: turnTeamDecision || undefined,
|
||||
turn_team_reason: turnTeamReason || undefined,
|
||||
turn_team_blueprint:
|
||||
turnTeamBlueprint &&
|
||||
(turnTeamBlueprint.label?.trim() ||
|
||||
turnTeamBlueprint.description?.trim() ||
|
||||
(turnTeamBlueprint.roles?.length || 0) > 0)
|
||||
? {
|
||||
label: turnTeamBlueprint.label?.trim() || undefined,
|
||||
description: turnTeamBlueprint.description?.trim() || undefined,
|
||||
roles: serializeTeamRoles(turnTeamBlueprint.roles),
|
||||
}
|
||||
: undefined,
|
||||
browser_requirement: browserRequirement || undefined,
|
||||
browser_requirement_reason: browserRequirementReason || undefined,
|
||||
|
||||
@@ -114,6 +114,8 @@ export interface AsterSubagentSessionInfo {
|
||||
role_hint?: string;
|
||||
origin_tool?: string;
|
||||
created_from_turn_id?: string;
|
||||
blueprint_role_id?: string;
|
||||
blueprint_role_label?: string;
|
||||
profile_id?: string;
|
||||
profile_name?: string;
|
||||
role_key?: string;
|
||||
@@ -156,6 +158,8 @@ export interface AsterSubagentParentContext {
|
||||
task_summary?: string;
|
||||
origin_tool?: string;
|
||||
created_from_turn_id?: string;
|
||||
blueprint_role_id?: string;
|
||||
blueprint_role_label?: string;
|
||||
profile_id?: string;
|
||||
profile_name?: string;
|
||||
role_key?: string;
|
||||
@@ -274,6 +278,8 @@ export interface AgentRuntimeSpawnSubagentRequest {
|
||||
model?: string;
|
||||
reasoning_effort?: string;
|
||||
fork_context?: boolean;
|
||||
blueprint_role_id?: string;
|
||||
blueprint_role_label?: string;
|
||||
profile_id?: string;
|
||||
profile_name?: string;
|
||||
role_key?: string;
|
||||
|
||||
Reference in New Issue
Block a user