release: v1.9.0

This commit is contained in:
coso
2026-04-13 02:50:53 +08:00
parent 52624b1488
commit 0f15b8a71e
313 changed files with 19875 additions and 9864 deletions
+7 -10
View File
@@ -16,7 +16,7 @@ use crate::agent::runtime_queue_service::{
};
use crate::agent::{
AsterAgentState, AsterAgentWrapper, QueuedTurnSnapshot, QueuedTurnTask, SessionDetail,
SessionInfo, SubAgentRole,
SessionInfo,
};
use crate::agent_tools::catalog::{
browser_runtime_tool_prefix, build_mcp_extension_surface, workbench_tool_names,
@@ -59,7 +59,6 @@ use crate::services::workspace_health_service::ensure_workspace_ready_with_auto_
use crate::workspace::WorkspaceManager;
use crate::LogState;
use aster::agents::extension::ExtensionConfig;
use aster::agents::subagent_scheduler::SubAgentTask;
use aster::agents::{Agent, AgentEvent};
use aster::chrome_mcp::get_chrome_mcp_tools;
use aster::conversation::message::{
@@ -481,18 +480,16 @@ pub(crate) use summary_skill_launch::{
#[allow(unused_imports)]
pub(crate) use tool_runtime::social_generate_cover_image_cmd;
pub(crate) use tool_runtime::{apply_workspace_sandbox_permissions, ImageInput};
#[cfg(test)]
#[allow(unused_imports)]
pub(crate) use tool_runtime::{
build_subagent_task_definition, build_subagent_task_runtime_message,
collect_subagent_task_compat_warnings, extract_runtime_subagent_result_text,
parse_subagent_role, LimeBrowserMcpTool, SocialGenerateCoverImageTool, SubAgentTaskToolInput,
ToolSearchBridgeTool,
};
pub(crate) use tool_runtime::{
ensure_browser_mcp_tools_registered, ensure_creation_task_tools_registered,
ensure_runtime_support_tools_registered, ensure_social_image_tool_registered,
};
#[cfg(test)]
#[allow(unused_imports)]
pub(crate) use tool_runtime::{
extract_runtime_subagent_result_text, LimeBrowserMcpTool, SocialGenerateCoverImageTool,
ToolSearchBridgeTool,
};
pub(crate) use transcription_skill_launch::{
append_transcription_skill_launch_session_permissions,
merge_system_prompt_with_transcription_skill_launch,
@@ -3,6 +3,7 @@ use super::*;
use aster::session::TurnContextOverride;
use lime_agent::AgentEvent as RuntimeAgentEvent;
use lime_core::workspace::WorkspaceSettings;
use tauri::Manager;
const ARTIFACT_DOCUMENT_REPAIRED_WARNING_CODE: &str = "artifact_document_repaired";
const ARTIFACT_DOCUMENT_FAILED_WARNING_CODE: &str = "artifact_document_failed";
@@ -13,6 +14,11 @@ const CONTEXT_COMPACTION_NOT_NEEDED_WARNING_CODE: &str = "context_compaction_not
const TURN_MEMORY_PREFETCH_PROMPT_MARKER: &str = "【运行时记忆召回】";
const LIME_RUNTIME_METADATA_KEY: &str = "lime_runtime";
const LIME_RUNTIME_AUTO_COMPACT_KEY: &str = "auto_compact";
const AUTO_RUNTIME_MEMORY_MIN_USER_CHARS: usize = 12;
const AUTO_RUNTIME_MEMORY_MIN_ASSISTANT_CHARS: usize = 48;
const AUTO_RUNTIME_MEMORY_MIN_TOTAL_CHARS: usize = 160;
const AUTO_RUNTIME_MEMORY_SESSION_MESSAGE_LIMIT: usize = 8;
const AUTO_RUNTIME_MEMORY_SESSION_MIN_MESSAGE_LENGTH: usize = 18;
fn emit_runtime_events(app: &AppHandle, event_name: &str, events: Vec<RuntimeAgentEvent>) {
for event in events {
@@ -47,6 +53,172 @@ fn merge_runtime_memory_prefetch_prompt(
}
}
fn normalize_runtime_memory_capture_text(input: &str) -> String {
input.split_whitespace().collect::<Vec<_>>().join(" ")
}
fn contains_runtime_memory_capture_signal(text: &str) -> bool {
[
"记住",
"偏好",
"喜欢",
"不喜欢",
"习惯",
"以后",
"规则",
"流程",
"workflow",
"prefer",
"always",
"never",
"计划",
"待办",
"todo",
"下一步",
"错误",
"失败",
"报错",
"修复",
"fix",
"bug",
]
.iter()
.any(|keyword| text.contains(keyword))
}
fn should_auto_capture_runtime_memory_turn(user_message: &str, assistant_output: &str) -> bool {
let normalized_user = normalize_runtime_memory_capture_text(user_message);
let normalized_assistant = normalize_runtime_memory_capture_text(assistant_output);
let total_chars = normalized_user.chars().count() + normalized_assistant.chars().count();
if normalized_user.chars().count() >= AUTO_RUNTIME_MEMORY_MIN_USER_CHARS
&& normalized_assistant.chars().count() >= AUTO_RUNTIME_MEMORY_MIN_ASSISTANT_CHARS
&& total_chars >= AUTO_RUNTIME_MEMORY_MIN_TOTAL_CHARS
{
return true;
}
let signal_text = format!(
"{} {}",
normalized_user.to_lowercase(),
normalized_assistant.to_lowercase()
);
contains_runtime_memory_capture_signal(signal_text.as_str())
}
fn spawn_runtime_memory_capture_task(
app: &AppHandle,
db: &DbConnection,
memory_config: lime_core::config::MemoryConfig,
session_id: &str,
user_message: &str,
assistant_output: &str,
) {
if !memory_config.enabled || !memory_config.auto.enabled {
return;
}
if !should_auto_capture_runtime_memory_turn(user_message, assistant_output) {
return;
}
let context_memory_service = app
.state::<crate::commands::context_memory::ContextMemoryServiceState>()
.inner()
.0
.clone();
let db = db.clone();
let session_id = session_id.to_string();
// 自动沉淀走后台任务,避免延长主回合完成时间。
tokio::spawn(async move {
let candidates = {
let conn = match db.lock() {
Ok(guard) => guard,
Err(error) => {
tracing::warn!(
"[AsterAgent] 后台自动记忆无法获取数据库锁: session_id={}, error={}",
session_id,
error
);
return;
}
};
match crate::services::chat_history_service::load_session_memory_source_candidates(
&conn,
&session_id,
AUTO_RUNTIME_MEMORY_SESSION_MESSAGE_LIMIT,
AUTO_RUNTIME_MEMORY_SESSION_MIN_MESSAGE_LENGTH,
) {
Ok(candidates) => candidates,
Err(error) => {
tracing::warn!(
"[AsterAgent] 后台自动记忆读取候选失败: session_id={}, error={}",
session_id,
error
);
return;
}
}
};
if candidates.is_empty() {
return;
}
match crate::commands::memory_management_cmd::analyze_memory_candidates(
context_memory_service.as_ref(),
&memory_config,
&candidates,
) {
Ok(result) => {
if result.generated_entries > 0 {
tracing::info!(
"[AsterAgent] 已自动沉淀工作记忆: session_id={}, generated={}, dedup={}",
session_id,
result.generated_entries,
result.deduplicated_entries
);
}
}
Err(error) => {
tracing::warn!(
"[AsterAgent] 后台自动沉淀工作记忆失败: session_id={}, error={}",
session_id,
error
);
}
}
match crate::commands::unified_memory_cmd::analyze_unified_memory_candidates(
&db,
&memory_config,
&candidates,
)
.await
{
Ok(result) => {
if result.generated_entries > 0 {
tracing::info!(
"[AsterAgent] 已自动沉淀长期记忆: session_id={}, generated={}, dedup={}",
session_id,
result.generated_entries,
result.deduplicated_entries
);
}
}
Err(error) => {
tracing::warn!(
"[AsterAgent] 后台自动沉淀长期记忆失败: session_id={}, error={}",
session_id,
error
);
}
}
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ProviderConfigApplyMode {
Direct,
@@ -1222,6 +1394,7 @@ async fn execute_aster_chat_request(
.as_ref()
.map(|config| config.provider_continuation_capability())
.unwrap_or(ProviderContinuationCapability::HistoryReplayOnly);
let runtime_memory_config = runtime_config.memory.clone();
let configured_provider_continuation_state = effective_provider_config
.as_ref()
.map(|config| config.provider_continuation_state())
@@ -1597,6 +1770,14 @@ async fn execute_aster_chat_request(
request_metadata.as_ref(),
execution.text_output.as_str(),
);
spawn_runtime_memory_capture_task(
&app,
db,
runtime_memory_config.clone(),
session_id,
&request.message,
execution.text_output.as_str(),
);
Ok(())
}
Err(primary_error)
@@ -1688,6 +1869,14 @@ async fn execute_aster_chat_request(
request_metadata.as_ref(),
execution.text_output.as_str(),
);
spawn_runtime_memory_capture_task(
&app,
db,
runtime_memory_config.clone(),
session_id,
&request.message,
execution.text_output.as_str(),
);
})
.map_err(|fallback_err| fallback_err.message)
}
@@ -1763,8 +1952,11 @@ async fn execute_aster_chat_request(
if let Ok(events) = terminal_events {
emit_runtime_events(app, &request.event_name, events);
}
let usage = resolve_runtime_message_usage(session_id).await;
if let Some(ref usage) = usage {
let done_event = resolve_runtime_final_done_event(session_id).await;
if let RuntimeAgentEvent::FinalDone {
usage: Some(ref usage),
} = done_event
{
if let Err(error) = persist_latest_assistant_message_usage(db, session_id, usage) {
tracing::warn!(
"[AsterAgent] 持久化消息 usage 失败(已降级继续): {}",
@@ -1772,7 +1964,6 @@ async fn execute_aster_chat_request(
);
}
}
let done_event = RuntimeAgentEvent::FinalDone { usage };
if let Err(e) = app.emit(&request.event_name, &done_event) {
tracing::error!("[AsterAgent] 发送完成事件失败: {}", e);
}
@@ -1854,6 +2045,10 @@ fn resolve_runtime_message_usage_from_session(
Some(lime_agent::AgentTokenUsage {
input_tokens: input_tokens as u32,
output_tokens: output_tokens as u32,
cached_input_tokens: session
.cached_input_tokens
.filter(|value| *value >= 0)
.map(|value| value as u32),
})
}
_ => None,
@@ -1867,6 +2062,12 @@ async fn resolve_runtime_message_usage(session_id: &str) -> Option<lime_agent::A
resolve_runtime_message_usage_from_session(&session)
}
async fn resolve_runtime_final_done_event(session_id: &str) -> RuntimeAgentEvent {
RuntimeAgentEvent::FinalDone {
usage: resolve_runtime_message_usage(session_id).await,
}
}
fn persist_latest_assistant_message_usage(
db: &DbConnection,
session_id: &str,
@@ -1880,6 +2081,7 @@ fn persist_latest_assistant_message_usage(
session_id,
usage.input_tokens,
usage.output_tokens,
usage.cached_input_tokens,
)?;
Ok(())
}
@@ -1903,6 +2105,11 @@ fn build_compaction_session_metrics_update(
let accumulated_input = accumulate(session.accumulated_input_tokens, usage.usage.input_tokens);
let accumulated_output =
accumulate(session.accumulated_output_tokens, usage.usage.output_tokens);
let cached_input_tokens = if usage.usage.output_tokens.is_some() {
usage.usage.cached_input_tokens
} else {
Some(0)
};
let current_window_tokens = usage
.usage
@@ -1913,6 +2120,7 @@ fn build_compaction_session_metrics_update(
CompactionSessionMetricsUpdate {
schedule_id,
current_window_tokens,
cached_input_tokens,
accumulated_total_tokens: accumulated_total,
accumulated_input_tokens: accumulated_input,
accumulated_output_tokens: accumulated_output,
@@ -2228,7 +2436,7 @@ async fn compact_runtime_session_with_trigger(
if let Ok(events) = terminal_events {
emit_runtime_events(app, &event_name, events);
}
let done_event = RuntimeAgentEvent::FinalDone { usage: None };
let done_event = resolve_runtime_final_done_event(&session_id).await;
if let Err(error) = app.emit(&event_name, &done_event) {
tracing::error!("[AsterAgent] 发送压缩完成事件失败: {}", error);
}
@@ -2915,6 +3123,39 @@ mod tests {
);
}
#[test]
fn should_auto_capture_runtime_memory_turn_for_long_turn_content() {
let user_message = "请记下这个团队的偏好:所有需求都先回到主线任务,再给出下一步明确行动。";
let assistant_output = "好的,我会把这条协作规则当作后续回合的默认执行约束,并在继续实现前先说明当前主线、当前阶段以及下一刀要推进的内容,同时避免把工作扩散到无关页面或额外配置面。";
assert!(should_auto_capture_runtime_memory_turn(
user_message,
assistant_output
));
}
#[test]
fn should_auto_capture_runtime_memory_turn_for_memory_signal_keywords() {
let user_message = "记住:以后回复先给结论";
let assistant_output = "收到,我以后会先给结论。";
assert!(should_auto_capture_runtime_memory_turn(
user_message,
assistant_output
));
}
#[test]
fn should_not_auto_capture_runtime_memory_turn_for_short_generic_turn() {
let user_message = "你好";
let assistant_output = "收到";
assert!(!should_auto_capture_runtime_memory_turn(
user_message,
assistant_output
));
}
#[test]
fn service_skill_launch_stage_should_preserve_simple_user_message_and_force_site_run_first() {
let user_message = "请帮我使用 GitHub 查一下 AI Agent 项目";
@@ -3240,6 +3481,7 @@ mod tests {
.total_tokens(Some(90))
.input_tokens(Some(60))
.output_tokens(Some(30))
.cached_input_tokens(Some(12))
.accumulated_total_tokens(Some(300))
.accumulated_input_tokens(Some(200))
.accumulated_output_tokens(Some(100))
@@ -3252,7 +3494,7 @@ mod tests {
let usage = ProviderUsage::new(
"gpt-4.1".to_string(),
Usage::new(Some(120), Some(45), Some(165)),
Usage::new(Some(120), Some(45), Some(165)).with_cached_input_tokens(Some(90)),
);
update_compaction_session_metrics(&session_config, &usage)
@@ -3267,6 +3509,7 @@ mod tests {
assert_eq!(updated.total_tokens, Some(45));
assert_eq!(updated.input_tokens, Some(45));
assert_eq!(updated.output_tokens, Some(0));
assert_eq!(updated.cached_input_tokens, Some(90));
assert_eq!(updated.accumulated_total_tokens, Some(465));
assert_eq!(updated.accumulated_input_tokens, Some(320));
assert_eq!(updated.accumulated_output_tokens, Some(145));
@@ -3294,6 +3537,7 @@ mod tests {
.total_tokens(Some(180))
.input_tokens(Some(120))
.output_tokens(Some(60))
.cached_input_tokens(Some(24))
.accumulated_total_tokens(Some(700))
.accumulated_input_tokens(Some(500))
.accumulated_output_tokens(Some(200))
@@ -3318,6 +3562,7 @@ mod tests {
assert_eq!(updated.total_tokens, Some(0));
assert_eq!(updated.input_tokens, Some(0));
assert_eq!(updated.output_tokens, Some(0));
assert_eq!(updated.cached_input_tokens, Some(0));
assert_eq!(updated.accumulated_total_tokens, Some(700));
assert_eq!(updated.accumulated_input_tokens, Some(500));
assert_eq!(updated.accumulated_output_tokens, Some(200));
@@ -3345,6 +3590,7 @@ mod tests {
.total_tokens(Some(20))
.input_tokens(Some(10))
.output_tokens(Some(10))
.cached_input_tokens(Some(6))
.accumulated_total_tokens(Some(200))
.accumulated_input_tokens(Some(120))
.accumulated_output_tokens(Some(80))
@@ -3355,7 +3601,7 @@ mod tests {
let session_config = SessionConfigBuilder::new(&session.id).build();
let usage = ProviderUsage::new(
"gpt-4.1".to_string(),
Usage::new(Some(30), Some(15), Some(45)),
Usage::new(Some(30), Some(15), Some(45)).with_cached_input_tokens(Some(18)),
);
update_compaction_session_metrics(&session_config, &usage)
@@ -3370,6 +3616,7 @@ mod tests {
assert_eq!(updated.total_tokens, Some(15));
assert_eq!(updated.input_tokens, Some(15));
assert_eq!(updated.output_tokens, Some(0));
assert_eq!(updated.cached_input_tokens, Some(18));
assert_eq!(updated.accumulated_total_tokens, Some(245));
assert_eq!(updated.accumulated_input_tokens, Some(150));
assert_eq!(updated.accumulated_output_tokens, Some(95));
@@ -3379,6 +3626,71 @@ mod tests {
.expect("清理测试会话失败");
}
#[tokio::test]
async fn resolve_runtime_final_done_event_should_include_usage_from_session() {
ensure_runtime_turn_test_session_manager().await;
let session = SessionManager::create_session(
PathBuf::from("."),
"final_done usage 测试".to_string(),
SessionType::User,
)
.await
.expect("创建测试会话失败");
SessionManager::update_session(&session.id)
.input_tokens(Some(204))
.output_tokens(Some(88))
.cached_input_tokens(Some(160))
.apply()
.await
.expect("写入 usage 失败");
let event = resolve_runtime_final_done_event(&session.id).await;
match event {
RuntimeAgentEvent::FinalDone { usage } => {
assert_eq!(
usage.map(|value| (
value.input_tokens,
value.output_tokens,
value.cached_input_tokens,
)),
Some((204, 88, Some(160)))
);
}
other => panic!("收到意外事件: {:?}", other),
}
SessionManager::delete_session(&session.id)
.await
.expect("清理测试会话失败");
}
#[tokio::test]
async fn resolve_runtime_final_done_event_should_fall_back_to_none_without_session_usage() {
ensure_runtime_turn_test_session_manager().await;
let session = SessionManager::create_session(
PathBuf::from("."),
"final_done 无 usage 测试".to_string(),
SessionType::User,
)
.await
.expect("创建测试会话失败");
let event = resolve_runtime_final_done_event(&session.id).await;
match event {
RuntimeAgentEvent::FinalDone { usage } => {
assert!(usage.is_none(), "未写入 usage 时应返回 None");
}
other => panic!("收到意外事件: {:?}", other),
}
SessionManager::delete_session(&session.id)
.await
.expect("清理测试会话失败");
}
#[tokio::test]
async fn should_auto_compact_runtime_session_when_workspace_pref_enabled_and_context_threshold_exceeded(
) {
+3 -119
View File
@@ -3851,8 +3851,8 @@ mod tests {
fn test_merge_system_prompt_with_elicitation_context_appends_prompt() {
let metadata = serde_json::json!({
"elicitation_context": {
"source": "legacy_questionnaire",
"mode": "compatibility_bridge",
"source": "runtime_action_required",
"mode": "runtime_metadata",
"entries": [
{
"label": "目标受众",
@@ -3875,7 +3875,7 @@ mod tests {
assert!(merged.contains(ELICITATION_CONTEXT_PROMPT_MARKER));
assert!(merged.contains("目标受众"));
assert!(merged.contains("友好专业"));
assert!(merged.contains("legacy_questionnaire"));
assert!(merged.contains("runtime_action_required"));
}
#[test]
@@ -6262,122 +6262,6 @@ mod tests {
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn test_parse_subagent_role_supports_aliases() {
assert_eq!(
parse_subagent_role(Some("explore")).unwrap(),
SubAgentRole::Explorer
);
assert_eq!(
parse_subagent_role(Some("plan")).unwrap(),
SubAgentRole::Planner
);
assert_eq!(
parse_subagent_role(Some("code")).unwrap(),
SubAgentRole::Executor
);
assert_eq!(parse_subagent_role(None).unwrap(), SubAgentRole::Executor);
}
#[test]
fn test_build_subagent_task_definition_uses_role_defaults() {
let input = SubAgentTaskToolInput {
prompt: "分析当前 harness 缺口".to_string(),
task_type: None,
description: None,
role: Some("explorer".to_string()),
timeout_secs: Some(45),
model: None,
return_summary: None,
allowed_tools: None,
denied_tools: None,
max_tokens: None,
};
let task = build_subagent_task_definition(&input, SubAgentRole::Explorer).unwrap();
assert_eq!(task.task_type, "explore");
assert_eq!(task.timeout.map(|value| value.as_secs()), Some(45));
assert!(task.return_summary);
}
#[test]
fn test_build_subagent_task_definition_applies_optional_fields() {
let input = SubAgentTaskToolInput {
prompt: "实现 harness 面板".to_string(),
task_type: Some("code".to_string()),
description: Some("实现前端面板".to_string()),
role: Some("executor".to_string()),
timeout_secs: Some(120),
model: Some("claude-sonnet-4-20250514".to_string()),
return_summary: Some(false),
allowed_tools: Some(vec!["read_file".to_string(), "write_file".to_string()]),
denied_tools: Some(vec!["execute_command".to_string()]),
max_tokens: Some(4096),
};
let task = build_subagent_task_definition(&input, SubAgentRole::Executor).unwrap();
assert_eq!(task.task_type, "code");
assert_eq!(task.description.as_deref(), Some("实现前端面板"));
assert_eq!(task.model.as_deref(), Some("claude-sonnet-4-20250514"));
assert!(!task.return_summary);
assert_eq!(
task.allowed_tools,
Some(vec!["read_file".to_string(), "write_file".to_string()])
);
assert_eq!(task.denied_tools, Some(vec!["execute_command".to_string()]));
assert_eq!(task.max_tokens, Some(4096));
}
#[test]
fn test_build_subagent_task_runtime_message_includes_soft_constraints() {
let input = SubAgentTaskToolInput {
prompt: "探索 team workspace 最佳实践".to_string(),
task_type: Some("explore".to_string()),
description: Some("探索 team workspace".to_string()),
role: Some("explorer".to_string()),
timeout_secs: None,
model: None,
return_summary: None,
allowed_tools: Some(vec!["read_file".to_string()]),
denied_tools: Some(vec!["write_file".to_string()]),
max_tokens: Some(1200),
};
let task = build_subagent_task_definition(&input, SubAgentRole::Explorer).unwrap();
let message = build_subagent_task_runtime_message(&input, &task, SubAgentRole::Explorer);
assert!(message.contains("任务标题:探索 team workspace"));
assert!(message.contains("子代理角色:explorer"));
assert!(message.contains("工具偏好:优先仅使用这些工具:read_file"));
assert!(message.contains("避免使用这些工具:write_file"));
assert!(message.contains("输出控制:请尽量将最终输出控制在 1200 tokens 内。"));
assert!(message.contains("不要再创建新的子代理"));
assert!(message.contains("任务说明:"));
assert!(message.contains("探索 team workspace 最佳实践"));
}
#[test]
fn test_collect_subagent_task_compat_warnings_marks_soft_constraints() {
let input = SubAgentTaskToolInput {
prompt: "探索".to_string(),
task_type: None,
description: None,
role: None,
timeout_secs: None,
model: None,
return_summary: None,
allowed_tools: Some(vec!["read_file".to_string()]),
denied_tools: Some(vec!["write_file".to_string()]),
max_tokens: Some(512),
};
let warnings = collect_subagent_task_compat_warnings(&input);
assert_eq!(warnings.len(), 3);
assert!(warnings.iter().any(|item| item.contains("allowedTools")));
assert!(warnings.iter().any(|item| item.contains("deniedTools")));
assert!(warnings.iter().any(|item| item.contains("maxTokens")));
}
#[test]
fn test_subagent_counts_toward_team_limit_only_counts_active_states() {
assert!(subagent_counts_toward_team_limit(
@@ -43,12 +43,8 @@ pub(crate) use social_tools::ensure_social_image_tool_registered;
pub(crate) use social_tools::social_generate_cover_image_cmd;
#[allow(unused_imports)]
pub(crate) use social_tools::SocialGenerateCoverImageTool;
#[allow(unused_imports)]
pub(crate) use subagent_tools::{
build_subagent_task_definition, build_subagent_task_runtime_message,
collect_subagent_task_compat_warnings, extract_runtime_subagent_result_text,
parse_subagent_role, SubAgentTaskToolInput,
};
#[cfg(test)]
pub(crate) use subagent_tools::extract_runtime_subagent_result_text;
use workspace_tools::WorkspaceSandboxedBashTool;
#[cfg(test)]
pub(crate) use workspace_tools::{
@@ -1,204 +1,6 @@
use super::*;
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(crate) struct SubAgentTaskToolInput {
pub(crate) prompt: String,
pub(crate) task_type: Option<String>,
pub(crate) description: Option<String>,
pub(crate) role: Option<String>,
pub(crate) timeout_secs: Option<u64>,
pub(crate) model: Option<String>,
pub(crate) return_summary: Option<bool>,
pub(crate) allowed_tools: Option<Vec<String>>,
pub(crate) denied_tools: Option<Vec<String>>,
pub(crate) max_tokens: Option<usize>,
}
pub(crate) fn parse_subagent_role(raw: Option<&str>) -> Result<SubAgentRole, ToolError> {
let normalized = raw
.map(|value| value.trim().to_ascii_lowercase())
.unwrap_or_else(|| "executor".to_string());
match normalized.as_str() {
"" | "executor" | "execute" | "code" => Ok(SubAgentRole::Executor),
"planner" | "plan" => Ok(SubAgentRole::Planner),
"explorer" | "explore" | "research" => Ok(SubAgentRole::Explorer),
_ => Err(ToolError::invalid_params(format!(
"未知 SubAgent 角色: {},支持 explorer/planner/executor",
normalized
))),
}
}
fn default_subagent_task_type(role: SubAgentRole) -> &'static str {
match role {
SubAgentRole::Explorer => "explore",
SubAgentRole::Planner => "plan",
SubAgentRole::Executor => "code",
}
}
pub(crate) fn build_subagent_task_definition(
input: &SubAgentTaskToolInput,
role: SubAgentRole,
) -> Result<SubAgentTask, ToolError> {
let prompt = input.prompt.trim();
if prompt.is_empty() {
return Err(ToolError::invalid_params(
"SubAgentTask.prompt 不能为空".to_string(),
));
}
let task_type = input
.task_type
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
.unwrap_or(default_subagent_task_type(role));
let mut task = SubAgentTask::new(uuid::Uuid::new_v4().to_string(), task_type, prompt);
if let Some(description) = input
.description
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
task = task.with_description(description.to_string());
}
if let Some(timeout_secs) = input.timeout_secs.filter(|value| *value > 0) {
task = task.with_timeout(Duration::from_secs(timeout_secs));
}
if let Some(model) = input
.model
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
task = task.with_model(model.to_string());
}
if let Some(return_summary) = input.return_summary {
task = task.with_summary(return_summary);
}
if let Some(allowed_tools) = input
.allowed_tools
.as_ref()
.filter(|items| !items.is_empty())
{
task = task.with_allowed_tools(allowed_tools.clone());
}
if let Some(denied_tools) = input
.denied_tools
.as_ref()
.filter(|items| !items.is_empty())
{
task = task.with_denied_tools(denied_tools.clone());
}
if let Some(max_tokens) = input.max_tokens.filter(|value| *value > 0) {
task = task.with_max_tokens(max_tokens);
}
Ok(task)
}
pub(crate) fn build_subagent_task_runtime_message(
input: &SubAgentTaskToolInput,
task: &SubAgentTask,
role: SubAgentRole,
) -> String {
let mut sections = Vec::new();
if let Some(description) = input
.description
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
sections.push(format!("任务标题:{description}"));
}
sections.push(format!("子代理角色:{role}"));
if let Some(task_type) = input
.task_type
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
{
sections.push(format!("任务类型:{task_type}"));
}
if let Some(allowed_tools) = input
.allowed_tools
.as_ref()
.filter(|items| !items.is_empty())
{
sections.push(format!(
"工具偏好:优先仅使用这些工具:{}",
allowed_tools.join(", ")
));
}
if let Some(denied_tools) = input
.denied_tools
.as_ref()
.filter(|items| !items.is_empty())
{
sections.push(format!("避免使用这些工具:{}", denied_tools.join(", ")));
}
if let Some(max_tokens) = input.max_tokens.filter(|value| *value > 0) {
sections.push(format!(
"输出控制:请尽量将最终输出控制在 {max_tokens} tokens 内。"
));
}
sections.push(
"协作约束:你不是唯一工作线程。请只处理当前明确分配的子任务,不要重复主线程或其他子代理的工作,不要再创建新的子代理。"
.to_string(),
);
sections.push("任务说明:".to_string());
sections.push(task.prompt.clone());
sections.join("\n")
}
pub(crate) fn collect_subagent_task_compat_warnings(input: &SubAgentTaskToolInput) -> Vec<String> {
let mut warnings = Vec::new();
if input
.allowed_tools
.as_ref()
.is_some_and(|items| !items.is_empty())
{
warnings
.push("allowedTools 已降级为对子代理的提示,不再由旧 scheduler 做硬限制".to_string());
}
if input
.denied_tools
.as_ref()
.is_some_and(|items| !items.is_empty())
{
warnings
.push("deniedTools 已降级为对子代理的提示,不再由旧 scheduler 做硬限制".to_string());
}
if input.max_tokens.is_some_and(|value| value > 0) {
warnings.push("maxTokens 已降级为输出提示,当前 team runtime 不做强制截断".to_string());
}
warnings
}
#[cfg(test)]
fn extract_runtime_message_text(message: &AgentMessage) -> Option<String> {
let parts = message
.content
@@ -225,6 +27,7 @@ fn extract_runtime_message_text(message: &AgentMessage) -> Option<String> {
}
}
#[cfg(test)]
pub(crate) fn extract_runtime_subagent_result_text(detail: &SessionDetail) -> Option<String> {
detail
.messages
@@ -279,246 +82,6 @@ pub(crate) fn extract_runtime_subagent_result_text(detail: &SessionDetail) -> Op
})
}
fn summarize_runtime_subagent_execution(
role: SubAgentRole,
status: &SubagentRuntimeStatus,
detail: Option<&SessionDetail>,
) -> String {
let result_text = detail
.and_then(extract_runtime_subagent_result_text)
.unwrap_or_else(|| "未返回摘要".to_string());
match status.kind {
SubagentRuntimeStatusKind::Completed => {
format!("子代理({role}) 已通过 team runtime 完成任务。\n\n{result_text}")
}
SubagentRuntimeStatusKind::Failed | SubagentRuntimeStatusKind::Aborted => {
format!("子代理({role}) 执行失败。\n\n{result_text}")
}
SubagentRuntimeStatusKind::Closed => {
format!("子代理({role}) 已关闭。\n\n{result_text}")
}
SubagentRuntimeStatusKind::NotFound => {
format!("子代理({role}) 未找到,无法获取结果。")
}
_ => format!(
"子代理({role}) 当前状态为 {:?}。\n\n{result_text}",
status.kind
),
}
}
#[derive(Debug, Clone)]
struct SubAgentTaskTool {
runtime: SubagentControlRuntime,
}
impl SubAgentTaskTool {
fn new(runtime: SubagentControlRuntime) -> Self {
Self { runtime }
}
}
#[async_trait]
impl Tool for SubAgentTaskTool {
fn name(&self) -> &str {
"SubAgentTask"
}
fn description(&self) -> &str {
"兼容入口。仅用于兼容仍输出旧 SubAgentTask schema 的历史提示词或旧技能;内部会退化为串行 team runtime 调用,不适合作为新的多代理并发主路径。新实现优先直接使用 Agent / TeamCreate / TeamDelete / SendMessage / ListPeers。"
}
fn input_schema(&self) -> serde_json::Value {
serde_json::json!({
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "子代理要执行的任务说明"
},
"taskType": {
"type": "string",
"description": "任务类型,例如 explore、plan、code、review"
},
"description": {
"type": "string",
"description": "展示给用户的任务标题"
},
"role": {
"type": "string",
"description": "子代理角色:explorer、planner、executor"
},
"timeoutSecs": {
"type": "integer",
"minimum": 1,
"description": "单个子任务超时时间(秒)"
},
"model": {
"type": "string",
"description": "可选模型名"
},
"returnSummary": {
"type": "boolean",
"description": "是否优先返回摘要"
},
"allowedTools": {
"type": "array",
"items": { "type": "string" },
"description": "显式允许的工具列表"
},
"deniedTools": {
"type": "array",
"items": { "type": "string" },
"description": "显式拒绝的工具列表"
},
"maxTokens": {
"type": "integer",
"minimum": 1,
"description": "子代理最大 token 限制"
}
},
"required": ["prompt"],
"additionalProperties": false
})
}
fn options(&self) -> ToolOptions {
ToolOptions::new()
.with_max_retries(0)
.with_base_timeout(Duration::from_secs(900))
.with_dynamic_timeout(false)
}
async fn execute(
&self,
params: serde_json::Value,
context: &ToolContext,
) -> Result<ToolResult, ToolError> {
let input: SubAgentTaskToolInput = serde_json::from_value(params)
.map_err(|err| ToolError::invalid_params(format!("SubAgentTask 参数无效: {err}")))?;
let role = parse_subagent_role(input.role.as_deref())?;
let task = build_subagent_task_definition(&input, role)?;
let task_id = task.id.clone();
let parent_session_id = normalize_required_text(&context.session_id, "session_id")
.map_err(ToolError::invalid_params)?;
let compat_warnings = collect_subagent_task_compat_warnings(&input);
let response = agent_runtime_spawn_subagent_internal(
&self.runtime,
AgentRuntimeSpawnSubagentRequest {
parent_session_id,
message: build_subagent_task_runtime_message(&input, &task, role),
name: None,
team_name: None,
agent_type: Some(role.to_string()),
model: input.model.clone(),
run_in_background: false,
reasoning_effort: None,
fork_context: false,
blueprint_role_id: None,
blueprint_role_label: None,
profile_id: None,
profile_name: None,
role_key: None,
skill_ids: Vec::new(),
skill_directories: Vec::new(),
team_preset_id: None,
theme: None,
system_overlay: None,
output_contract: None,
mode: None,
isolation: None,
cwd: None,
},
)
.await
.map_err(|error| {
ToolError::execution_failed(format!(
"SubAgentTask 已切到 team runtime,但创建子代理失败: {error}"
))
})?;
let timeout_ms = input
.timeout_secs
.unwrap_or(900)
.saturating_mul(1000)
.min(i64::MAX as u64) as i64;
let wait_result = agent_runtime_wait_subagents_internal(
&self.runtime,
AgentRuntimeWaitSubagentsRequest {
ids: vec![response.agent_id.clone()],
timeout_ms: Some(timeout_ms),
},
)
.await
.map_err(|error| {
ToolError::execution_failed(format!(
"SubAgentTask 已创建子代理,但等待结果失败: {error}"
))
})?;
let detail =
AsterAgentWrapper::get_runtime_session_detail(&self.runtime.db, &response.agent_id)
.await
.ok();
let status = wait_result
.status
.get(&response.agent_id)
.cloned()
.unwrap_or(SubagentRuntimeStatus {
session_id: response.agent_id.clone(),
kind: if wait_result.timed_out {
SubagentRuntimeStatusKind::Running
} else {
SubagentRuntimeStatusKind::NotFound
},
latest_turn_id: None,
latest_turn_status: None,
queued_turn_count: 0,
team_phase: None,
team_parallel_budget: None,
team_active_count: None,
team_queued_count: None,
provider_concurrency_group: None,
provider_parallel_budget: None,
queue_reason: None,
retryable_overload: false,
closed: false,
});
let summary = if wait_result.timed_out {
format!(
"子代理({role}) 已创建,但在 {} 秒内未完成。可以继续通过 team workspace 跟踪: {}",
input.timeout_secs.unwrap_or(900),
response.agent_id
)
} else {
summarize_runtime_subagent_execution(role, &status, detail.as_ref())
};
let metadata = serde_json::json!({
"task_id": task_id,
"agent_id": response.agent_id,
"nickname": response.nickname,
"role": role.to_string(),
"status": status,
"timed_out": wait_result.timed_out,
"compat_mode": "SubAgentTask->Agent",
"compat_warnings": compat_warnings,
});
let success = !wait_result.timed_out && status.kind == SubagentRuntimeStatusKind::Completed;
let result = if success {
ToolResult::success(summary)
} else {
ToolResult::error(summary)
};
Ok(result
.with_metadata("subagent", metadata)
.with_metadata("role", serde_json::json!(role.to_string())))
}
}
fn build_agent_control_tool_config(
runtime: SubagentControlRuntime,
) -> aster::tools::AgentControlToolConfig {
@@ -595,7 +158,6 @@ pub(super) fn register_subagent_runtime_tools(
registry: &mut aster::tools::ToolRegistry,
runtime: SubagentControlRuntime,
) {
registry.register(Box::new(SubAgentTaskTool::new(runtime.clone())));
aster::tools::register_agent_control_tools(registry, &build_agent_control_tool_config(runtime));
// 本地联调中的 Aster current surface 会在 Agent::list_tools() 里额外注入一份 `Agent`。
// 如果这里继续保留 registry 侧的同名 `Agent`,provider 在格式化 tools 时会直接报重名。
+81 -75
View File
@@ -26,7 +26,8 @@ use crate::services::runtime_agents_template_service::{
use aster::session::list_summaries;
use chrono::{Local, NaiveDateTime, TimeZone};
use lime_core::app_paths;
use lime_services::context_memory_service::{MemoryEntry, MemoryFileType};
use lime_core::config::MemoryConfig;
use lime_services::context_memory_service::{ContextMemoryService, MemoryEntry, MemoryFileType};
use rusqlite::Connection;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
@@ -330,82 +331,9 @@ async fn memory_runtime_request_analysis_impl(
});
}
let max_generated_per_request = memory_config
.max_entries
.unwrap_or(MAX_GENERATED_PER_REQUEST as u32)
.clamp(1, MAX_GENERATED_PER_REQUEST_CAP as u32)
as usize;
let conn = db.lock().map_err(|e| format!("数据库锁定失败: {e}"))?;
let candidates = load_memory_candidates(&conn, from_timestamp, to_timestamp)?;
if candidates.is_empty() {
return Ok(MemoryAnalysisResult {
analyzed_sessions: 0,
analyzed_messages: 0,
generated_entries: 0,
deduplicated_entries: 0,
});
}
let mut analyzed_sessions: HashSet<String> = HashSet::new();
let mut generated_entries = 0u32;
let mut deduplicated_entries = 0u32;
let mut generated_count_per_session: HashMap<String, usize> = HashMap::new();
for candidate in candidates.iter().take(MAX_SOURCE_MESSAGES) {
analyzed_sessions.insert(candidate.session_id.clone());
let counter = generated_count_per_session
.entry(candidate.session_id.clone())
.or_insert(0);
if *counter >= MAX_GENERATED_PER_SESSION {
continue;
}
let fingerprint = build_fingerprint(&candidate.content);
let (title, summary, file_type, category_tag) = build_memory_entry_fields(candidate);
let existing = memory_service
.0
.get_session_memories(&candidate.session_id, Some(file_type))?;
if is_duplicate_memory(&existing, &fingerprint, &summary) {
deduplicated_entries += 1;
continue;
}
let entry = MemoryEntry {
id: uuid::Uuid::new_v4().to_string(),
session_id: candidate.session_id.clone(),
file_type,
title,
content: summary,
tags: vec![
"auto_analysis".to_string(),
category_tag.to_string(),
fingerprint,
],
priority: infer_priority(candidate),
created_at: candidate.created_at,
updated_at: candidate.created_at,
archived: false,
};
memory_service.0.save_memory_entry(&entry)?;
generated_entries += 1;
*counter += 1;
if generated_entries as usize >= max_generated_per_request {
break;
}
}
Ok(MemoryAnalysisResult {
analyzed_sessions: analyzed_sessions.len() as u32,
analyzed_messages: candidates.len() as u32,
generated_entries,
deduplicated_entries,
})
analyze_memory_candidates(memory_service.0.as_ref(), &memory_config, &candidates)
}
/// 从历史对话中抽取 runtime / 上下文记忆条目
@@ -1620,6 +1548,84 @@ fn load_memory_candidates(
)
}
pub(crate) fn analyze_memory_candidates(
memory_service: &ContextMemoryService,
memory_config: &MemoryConfig,
candidates: &[MemorySourceCandidate],
) -> Result<MemoryAnalysisResult, String> {
if !memory_config.enabled || candidates.is_empty() {
return Ok(MemoryAnalysisResult {
analyzed_sessions: 0,
analyzed_messages: 0,
generated_entries: 0,
deduplicated_entries: 0,
});
}
let max_generated_per_request = memory_config
.max_entries
.unwrap_or(MAX_GENERATED_PER_REQUEST as u32)
.clamp(1, MAX_GENERATED_PER_REQUEST_CAP as u32)
as usize;
let mut analyzed_sessions: HashSet<String> = HashSet::new();
let mut generated_entries = 0u32;
let mut deduplicated_entries = 0u32;
let mut generated_count_per_session: HashMap<String, usize> = HashMap::new();
for candidate in candidates.iter().take(MAX_SOURCE_MESSAGES) {
analyzed_sessions.insert(candidate.session_id.clone());
let counter = generated_count_per_session
.entry(candidate.session_id.clone())
.or_insert(0);
if *counter >= MAX_GENERATED_PER_SESSION {
continue;
}
let fingerprint = build_fingerprint(&candidate.content);
let (title, summary, file_type, category_tag) = build_memory_entry_fields(candidate);
let existing =
memory_service.get_session_memories(&candidate.session_id, Some(file_type))?;
if is_duplicate_memory(&existing, &fingerprint, &summary) {
deduplicated_entries += 1;
continue;
}
let entry = MemoryEntry {
id: uuid::Uuid::new_v4().to_string(),
session_id: candidate.session_id.clone(),
file_type,
title,
content: summary,
tags: vec![
"auto_analysis".to_string(),
category_tag.to_string(),
fingerprint,
],
priority: infer_priority(candidate),
created_at: candidate.created_at,
updated_at: candidate.created_at,
archived: false,
};
memory_service.save_memory_entry(&entry)?;
generated_entries += 1;
*counter += 1;
if generated_entries as usize >= max_generated_per_request {
break;
}
}
Ok(MemoryAnalysisResult {
analyzed_sessions: analyzed_sessions.len() as u32,
analyzed_messages: candidates.len() as u32,
generated_entries,
deduplicated_entries,
})
}
fn build_fingerprint(content: &str) -> String {
let normalized = content.to_lowercase();
let compact = normalized
-1
View File
@@ -55,7 +55,6 @@ pub mod site_capability_cmd;
pub mod skill_cmd;
pub mod skill_error;
pub mod skill_exec_cmd;
pub mod subagent_cmd;
pub mod telegram_remote_cmd;
pub mod telemetry_cmd;
pub mod terminal_cmd;
-108
View File
@@ -1,108 +0,0 @@
//! SubAgent 调度器命令
//!
//! 提供 SubAgent 调度功能的 Tauri 命令接口
use std::sync::Arc;
use tauri::{AppHandle, State};
use tokio::sync::RwLock;
use aster::agents::context::AgentContext;
use aster::agents::subagent_scheduler::{SchedulerConfig, SchedulerExecutionResult, SubAgentTask};
use crate::agent::{LimeScheduler, SubAgentRole};
use crate::database::DbConnection;
/// SubAgent 调度器状态
pub struct SubAgentSchedulerState {
#[allow(dead_code)]
scheduler: Arc<RwLock<Option<LimeScheduler>>>,
}
impl SubAgentSchedulerState {
pub fn new() -> Self {
Self {
scheduler: Arc::new(RwLock::new(None)),
}
}
}
impl Default for SubAgentSchedulerState {
fn default() -> Self {
Self::new()
}
}
/// 初始化 SubAgent 调度器
#[allow(dead_code)]
#[tauri::command]
pub async fn init_subagent_scheduler(
app: AppHandle,
db: State<'_, DbConnection>,
state: State<'_, SubAgentSchedulerState>,
config: Option<SchedulerConfig>,
session_id: Option<String>,
) -> Result<(), String> {
let mut scheduler = LimeScheduler::new(db.inner().clone()).with_app_handle(app);
if let Some(session_id) = session_id.filter(|value| !value.trim().is_empty()) {
scheduler = scheduler.with_event_session_id(session_id);
}
scheduler.init(config).await;
*state.scheduler.write().await = Some(scheduler);
Ok(())
}
/// 执行 SubAgent 任务
#[allow(dead_code)]
#[tauri::command]
pub async fn execute_subagent_tasks(
app: AppHandle,
db: State<'_, DbConnection>,
state: State<'_, SubAgentSchedulerState>,
tasks: Vec<SubAgentTask>,
config: Option<SchedulerConfig>,
role: Option<SubAgentRole>,
session_id: Option<String>,
) -> Result<SchedulerExecutionResult, String> {
let mut scheduler = LimeScheduler::new(db.inner().clone()).with_app_handle(app);
if let Some(session_id) = session_id.filter(|value| !value.trim().is_empty()) {
scheduler = scheduler.with_event_session_id(session_id);
}
scheduler.init(config.clone()).await;
*state.scheduler.write().await = Some(scheduler);
let scheduler_guard = state.scheduler.read().await;
let scheduler = scheduler_guard
.as_ref()
.ok_or_else(|| "调度器初始化失败".to_string())?;
// 创建父上下文
let parent_context = AgentContext::new();
// 根据是否指定角色选择执行方式
match role {
Some(role) => scheduler
.execute_with_role(tasks, Some(&parent_context), role)
.await
.map_err(|e| e.to_string()),
None => scheduler
.execute(tasks, Some(&parent_context))
.await
.map_err(|e| e.to_string()),
}
}
/// 取消 SubAgent 任务
#[allow(dead_code)]
#[tauri::command]
pub async fn cancel_subagent_tasks(state: State<'_, SubAgentSchedulerState>) -> Result<(), String> {
let scheduler_guard = state.scheduler.read().await;
if let Some(scheduler) = scheduler_guard.as_ref() {
scheduler.cancel().await;
}
Ok(())
}
+27 -19
View File
@@ -5,6 +5,7 @@
use crate::config::GlobalConfigManagerState;
use crate::database::DbConnection;
use crate::services::chat_history_service::{load_memory_source_candidates, MemorySourceCandidate};
use lime_core::config::MemoryConfig;
use lime_memory::extractor::{self, ExtractionContext};
use lime_memory::gatekeeper::ChatMessage;
use lime_memory::{MemoryCategory, MemoryMetadata, MemorySource, MemoryType, UnifiedMemory};
@@ -450,6 +451,19 @@ pub async fn unified_memory_analyze(
}
let memory_config = global_config.config().memory;
let candidates = {
let conn = db.lock().map_err(|e| format!("数据库锁定失败: {e}"))?;
load_memory_candidates(&conn, from_timestamp, to_timestamp)?
};
analyze_unified_memory_candidates(&db, &memory_config, &candidates).await
}
pub(crate) async fn analyze_unified_memory_candidates(
db: &DbConnection,
memory_config: &MemoryConfig,
candidates: &[MemorySourceCandidate],
) -> Result<MemoryAnalysisResult, String> {
if !memory_config.enabled {
info!("[Unified Memory] 记忆功能已关闭,跳过分析");
return Ok(MemoryAnalysisResult {
@@ -460,26 +474,20 @@ pub async fn unified_memory_analyze(
});
}
if candidates.is_empty() {
return Ok(MemoryAnalysisResult {
analyzed_sessions: 0,
analyzed_messages: 0,
generated_entries: 0,
deduplicated_entries: 0,
});
}
let max_generated_per_request = memory_config
.max_entries
.unwrap_or(MAX_GENERATED_PER_REQUEST as u32)
.clamp(1, MAX_GENERATED_PER_REQUEST_CAP as u32)
as usize;
let candidates = {
let conn = db.lock().map_err(|e| format!("数据库锁定失败: {e}"))?;
load_memory_candidates(&conn, from_timestamp, to_timestamp)?
};
if candidates.is_empty() {
return Ok(MemoryAnalysisResult {
analyzed_sessions: 0,
analyzed_messages: 0,
generated_entries: 0,
deduplicated_entries: 0,
});
}
let analyzed_sessions = candidates
.iter()
.map(|item| item.session_id.clone())
@@ -493,7 +501,7 @@ pub async fn unified_memory_analyze(
let llm_attempted = llm_api_key.is_some();
if let Some(api_key) = llm_api_key {
match build_pending_from_llm(&db, &candidates, &api_key, max_generated_per_request).await {
match build_pending_from_llm(db, candidates, &api_key, max_generated_per_request).await {
Ok((mut llm_pending, llm_dedup)) => {
deduplicated_entries += llm_dedup;
pending_memories.append(&mut llm_pending);
@@ -506,7 +514,7 @@ pub async fn unified_memory_analyze(
if !llm_attempted || pending_memories.is_empty() {
let (mut fallback_pending, fallback_dedup) =
build_pending_from_rules(&db, &candidates, max_generated_per_request)?;
build_pending_from_rules(db, candidates, max_generated_per_request)?;
deduplicated_entries += fallback_dedup;
pending_memories.append(&mut fallback_pending);
}
@@ -540,7 +548,7 @@ pub async fn unified_memory_analyze(
}
fn build_pending_from_rules(
db: &State<'_, DbConnection>,
db: &DbConnection,
candidates: &[MemorySourceCandidate],
max_generated_per_request: usize,
) -> Result<(Vec<PendingMemory>, u32), String> {
@@ -606,7 +614,7 @@ fn build_pending_from_rules(
}
async fn build_pending_from_llm(
db: &State<'_, DbConnection>,
db: &DbConnection,
candidates: &[MemorySourceCandidate],
api_key: &str,
max_generated_per_request: usize,