Skip to main content

otherone_agent/
lib.rs

1// 作用:Agent 循环驱动 — 整个框架的核心,驱动 AI 对话流程
2// 关联:调用 ai、context、tools、storage 等所有子模块
3// 预期结果:根据 input 和 ai 配置,执行完整的 Agent 循环,返回最终响应
4
5pub mod error;
6pub mod multi_agent;
7pub mod response_parser;
8pub mod types;
9
10use serde::{Deserialize, Serialize};
11use std::collections::{HashMap, HashSet, VecDeque};
12use std::sync::{Arc, Mutex, OnceLock};
13use std::time::Duration;
14use tokio::sync::mpsc;
15use tracing::warn;
16
17use crate::error::AgentError;
18use crate::response_parser::parse_ai_response;
19use crate::types::{
20    AiOptions, ContextLoadType as AgentContextLoadType, InputOptions,
21    StorageType as AgentStorageType,
22};
23use otherone_storage::types::{StorageType, WriteEntryOptions};
24
25pub use crate::types::{AgentStreamCommand, AgentStreamHandle, StreamAgentEvent};
26
27const LONG_TERM_MEMORY_TOOL_NAME: &str = "otherone_add_long_term_memory";
28const LONG_TERM_MEMORY_RECALL_TOOL_NAME: &str = "otherone_recall_long_term_memory";
29const LONG_TERM_MEMORY_COMMIT_TOOL_NAME: &str = "otherone_commit_long_term_memory";
30const DEFAULT_LONG_TERM_MEMORY_RECALL_MAX_TYPES: usize = 5;
31const MAX_LONG_TERM_MEMORY_RECALL_MAX_TYPES: usize = 16;
32const LONG_TERM_MEMORY_SYSTEM_PROMPT: &str = r#"Long-term memory is enabled.
33
34Evaluate the current user message for reusable long-term information. Reusable information includes stable user preferences, identity facts, durable project rules, long-term goals, recurring habits, and constraints that may help future conversations.
35
36Do not store one-off requests, temporary state, casual filler, reminders, calculations, translation requests, or information that is only useful for the current response.
37
38Do not store secrets, credentials, API keys, passwords, verification codes, payment details, or other highly sensitive data. If the user gives sensitive data, respond normally but do not call the memory tool for that content.
39
40Bias toward calling the memory tool when the user explicitly says information can be remembered long-term, or when the message contains durable identity, preferred name, preferred form of address, default language, stable preference, recurring habit, durable project rule, allergy, or long-term constraint.
41
42When reusable information exists, call `otherone_add_long_term_memory` with:
43- `storage`: a concise natural-language memory statement.
44- `types`: the specific semantic category for this exact memory node. Prefer concrete categories over broad ones. Do not use a broad parent category when a more specific child category is available.
45- `possible_parent_types`: 1-5 short keywords or semantic tags for likely existing parent memory categories. These must be compact category hints, not full sentences. Examples: `food`, `noodles`, `diet preference`, `project rule`, `coding style`.
46
47If you tell the user that you recorded, remembered, saved, or noted durable information, you must have called `otherone_add_long_term_memory` for that information in the same turn. Do not claim durable memory was recorded unless you called the tool.
48
49Examples:
50- "My name is Lin Che" -> storage: "用户的称呼是林澈", types: "用户称呼", possible_parent_types: ["身份信息", "称呼", "用户资料"].
51- "Call me Jae from now on" -> storage: "用户的称呼是 Jae", types: "用户称呼", possible_parent_types: ["身份信息", "称呼", "用户资料"].
52- "Use Chinese by default" -> storage: "与用户交流默认使用中文", types: "交流语言偏好", possible_parent_types: ["沟通偏好", "语言偏好", "对话设置"].
53- "I like zhajiangmian" -> storage: "用户喜欢吃炸酱面", types: "用户喜欢吃的面食", possible_parent_types: ["食物偏好", "饮食偏好", "面食偏好"].
54- "I like sweet and sour ribs" -> storage: "用户喜欢吃糖醋排骨", types: "用户喜欢吃的菜", possible_parent_types: ["食物偏好", "饮食偏好", "菜肴偏好"].
55- "I dislike cilantro" -> storage: "用户不喜欢吃香菜", types: "用户不喜欢的食材", possible_parent_types: ["食物偏好", "饮食偏好", "忌口偏好"].
56- "I prefer concise Rust code" -> storage: "用户偏好简洁的 Rust 代码", types: "Rust 代码风格偏好", possible_parent_types: ["编码偏好", "Rust", "代码风格"].
57
58When the current user request could benefit from known long-term information, call `otherone_recall_long_term_memory` before answering. Pass `memory_types` as compact type keywords or semantic tags, such as ["饮食偏好"], ["用户称呼", "语言偏好"], ["Rust 错误处理偏好"], or ["前端设计偏好"].
59
60The recall tool returns only remembered facts, not internal node JSON. Use those facts as background context for the answer.
61
62Call the recall tool at most once per user request unless the first recall clearly misses a separate required topic.
63
64The add-memory tool only queues the memory for later processing and returns immediately. After tools return, continue the normal response. Do not mention memory storage unless the user asks."#;
65const LONG_TERM_MEMORY_MODEL_SYSTEM_PROMPT: &str = r#"You are the long-term memory write planner for Otherone.
66
67You receive:
68- A candidate memory submitted by the main agent.
69- A table of nearby existing memory nodes selected by system search.
70
71The memory tree has a headless point with no semantic content. Its direct children are root points. Deeper nodes must have more specific `types` than their parents.
72
73Your task is to choose the best write operation. Always call `otherone_commit_long_term_memory`. Do not answer in natural language.
74
75Rules:
76- If no existing node is semantically relevant, use `insert_root`.
77- If an existing node is a good parent and its `types` is already broad enough, use `insert_child`.
78- If an existing node should become a broader parent category before inserting the new memory, use `update_parent_and_insert_child`.
79- If the candidate duplicates or corrects an existing node, use `update_existing`.
80- If the candidate is not reusable long-term information, or contains secrets, credentials, API keys, passwords, verification codes, payment details, temporary state, reminders, one-off requests, calculations, or casual filler, use `ignore`.
81- Do not ignore durable identity, preferred name, preferred form of address, default communication language, stable design/coding/work preference, project rule, diet constraint, allergy, recurring habit, or long-term user constraint.
82- If the candidate is a sibling of an existing specific memory, do not simply attach it under that specific memory. Use `update_parent_and_insert_child` to broaden the existing node's `storage` and `types`, then insert the candidate as a more specific child.
83- A parent node's `storage` must make sense as a parent summary for all of its children. If the parent storage only describes one specific item, broaden it before adding a sibling child.
84- Keep `storage` as a concise reusable fact.
85- Keep `types` specific for the inserted or updated node. Parent `types` may become broader only when needed to cover both the old and new child concepts.
86
87Canonical example:
88- Existing node: point_id="p1", storage="用户喜欢吃炸酱面", types="用户喜欢的面食".
89- Candidate: storage="用户喜欢吃糖醋排骨", types="用户喜欢的菜肴".
90- Correct action: `update_parent_and_insert_child`.
91- Correct parent update: parent_id="p1", parent_storage="用户喜欢吃炸酱面和糖醋排骨等食物", parent_types="用户喜欢的食物".
92- Correct inserted child: storage="用户喜欢吃糖醋排骨", types="用户喜欢的菜肴".
93
94Canonical language example:
95- Existing candidates may be unrelated.
96- Candidate: storage="与用户交流默认使用中文", types="交流语言偏好".
97- Correct action: `insert_root`, unless an existing communication preference node should be updated.
98
99Canonical food restriction example:
100- Existing node may describe liked foods.
101- Candidate: storage="用户不喜欢吃香菜", types="用户不喜欢的食材".
102- Correct action: store it. Use `update_parent_and_insert_child` if a food preference parent should broaden into food preferences and restrictions, otherwise use `insert_root`. Do not ignore stable dislikes or diet restrictions."#;
103
104const LONG_TERM_MEMORY_FALLBACK_TYPES: &str = "待分类长期记忆";
105
106type LongTermMemoryQueue = Arc<Mutex<Vec<PendingLongTermMemory>>>;
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109struct PendingLongTermMemory {
110    storage: String,
111    types: String,
112    #[serde(default)]
113    possible_parent_types: Vec<String>,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
117struct LongTermMemoryRecallArgs {
118    #[serde(default)]
119    memory_types: Vec<String>,
120}
121
122#[derive(Debug, Clone)]
123struct MemoryModelConfig {
124    provider: otherone_ai::types::ProviderType,
125    api_key: String,
126    base_url: String,
127    model: String,
128    context_length: Option<u32>,
129    temperature: Option<f32>,
130    top_p: Option<f32>,
131    other: Option<serde_json::Value>,
132}
133
134#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
135struct LongTermMemoryCommitArgs {
136    action: String,
137    storage: Option<String>,
138    types: Option<String>,
139    parent_id: Option<String>,
140    target_id: Option<String>,
141    parent_storage: Option<String>,
142    parent_types: Option<String>,
143}
144
145/// 流式 Agent 调用
146/// 作用:启动流式 Agent 循环,通过 mpsc channel 实时发送事件
147/// 关联:被 otherone 主 crate 的 Otherone::invoke_agent_stream 调用
148/// 预期结果:返回 mpsc Receiver,调用者可以异步迭代接收事件
149pub async fn invoke_agent_stream(
150    input: InputOptions,
151    ai: AiOptions,
152    auxiliary_ai: Option<AiOptions>,
153) -> Result<mpsc::Receiver<StreamAgentEvent>, AgentError> {
154    let handle = invoke_agent_stream_interactive(input, ai, auxiliary_ai).await?;
155    Ok(handle.events)
156}
157
158/// 可交互的流式 Agent 调用
159/// 作用:启动流式 Agent 循环,并返回事件接收器和运行中命令发送器
160pub async fn invoke_agent_stream_interactive(
161    input: InputOptions,
162    mut ai: AiOptions,
163    auxiliary_ai: Option<AiOptions>,
164) -> Result<AgentStreamHandle, AgentError> {
165    let (tx, rx) = mpsc::channel::<StreamAgentEvent>(256);
166    let (command_tx, command_rx) = mpsc::channel::<AgentStreamCommand>(256);
167
168    tokio::spawn(async move {
169        let mut auxiliary_ai = auxiliary_ai;
170        if let Err(e) =
171            run_stream_loop(input, &mut ai, auxiliary_ai.as_mut(), &tx, command_rx).await
172        {
173            let _ = tx
174                .send(StreamAgentEvent {
175                    event_type: "error".to_string(),
176                    content: format!("[error:{}]", e),
177                    raw_chunk: None,
178                    error: Some(e.to_string()),
179                })
180                .await;
181        }
182    });
183
184    Ok(AgentStreamHandle {
185        events: rx,
186        commands: command_tx,
187    })
188}
189
190fn drain_agent_commands(
191    command_rx: &mut mpsc::Receiver<AgentStreamCommand>,
192    queued_prompts: &mut VecDeque<String>,
193) {
194    loop {
195        match command_rx.try_recv() {
196            Ok(AgentStreamCommand::EnqueueUserPrompts(prompts)) => {
197                for prompt in prompts {
198                    let prompt = prompt.trim();
199                    if !prompt.is_empty() {
200                        queued_prompts.push_back(prompt.to_string());
201                    }
202                }
203            }
204            Err(mpsc::error::TryRecvError::Empty)
205            | Err(mpsc::error::TryRecvError::Disconnected) => break,
206        }
207    }
208}
209
210async fn write_user_prompt_entry(
211    input: &InputOptions,
212    storage_type: &StorageType,
213    prompt: &str,
214) -> Result<(), AgentError> {
215    otherone_storage::write_entry(&WriteEntryOptions {
216        storage_type: storage_type.clone(),
217        session_id: input.session_id.clone(),
218        role: "user".to_string(),
219        content: prompt.to_string(),
220        tools: None,
221        token_consumption: None,
222        create_at: None,
223        database_config: input.database_config.clone(),
224        runtime_context: input.runtime_context.clone(),
225        metadata: Default::default(),
226    })
227    .await
228    .map_err(|e| AgentError::ContextError(e.to_string()))
229}
230
231async fn write_queued_user_prompts(
232    input: &InputOptions,
233    storage_type: &StorageType,
234    queued_prompts: &mut VecDeque<String>,
235) -> Result<usize, AgentError> {
236    let mut written = 0usize;
237
238    while let Some(prompt) = queued_prompts.front().cloned() {
239        write_user_prompt_entry(input, storage_type, &prompt).await?;
240        queued_prompts.pop_front();
241        written += 1;
242    }
243
244    Ok(written)
245}
246
247async fn emit_queued_user_prompts(tx: &mpsc::Sender<StreamAgentEvent>, count: usize) {
248    if count == 0 {
249        return;
250    }
251
252    let _ = tx
253        .send(StreamAgentEvent {
254            event_type: "queued_user_prompts".to_string(),
255            content: count.to_string(),
256            raw_chunk: None,
257            error: None,
258        })
259        .await;
260}
261
262/// 流式循环实际逻辑
263/// 作用:在后台 task 中执行完整的流式 Agent 循环
264/// 关联:被 invoke_agent_stream 调用
265/// 预期结果:通过 tx channel 发送所有事件,循环结束后返回
266async fn run_stream_loop(
267    input: InputOptions,
268    ai: &mut AiOptions,
269    auxiliary_ai: Option<&mut AiOptions>,
270    tx: &mpsc::Sender<StreamAgentEvent>,
271    mut command_rx: mpsc::Receiver<AgentStreamCommand>,
272) -> Result<(), AgentError> {
273    let long_term_memory_enabled = input.enable_long_term_memory.unwrap_or(false);
274    let memory_queue = if long_term_memory_enabled {
275        Some(Arc::new(Mutex::new(Vec::new())))
276    } else {
277        None
278    };
279    let memory_model_config = if long_term_memory_enabled {
280        let model_source = auxiliary_ai.as_ref().map(|aux| &**aux).unwrap_or(ai);
281        Some(MemoryModelConfig::from_ai(model_source))
282    } else {
283        None
284    };
285    let long_term_memory_recall_max_types =
286        normalize_recall_max_types(input.long_term_memory_recall_max_types);
287
288    if let Some(queue) = &memory_queue {
289        configure_long_term_memory(ai, Arc::clone(queue), long_term_memory_recall_max_types);
290    }
291
292    let storage_type: StorageType = match &input.storage_type {
293        Some(AgentStorageType::LocalFile) => StorageType::LocalFile,
294        Some(AgentStorageType::Database) => StorageType::Database,
295        None => StorageType::LocalFile,
296    };
297
298    // 存储用户消息
299    if let Some(ref user_prompt) = ai.user_prompt {
300        write_user_prompt_entry(&input, &storage_type, user_prompt).await?;
301    }
302
303    let max_iterations = input.max_iterations.unwrap_or(999999);
304    let mut iteration: u32 = 0;
305    let mut long_term_memory_activity = false;
306    let mut queued_prompts = VecDeque::new();
307
308    while iteration < max_iterations {
309        iteration += 1;
310
311        drain_agent_commands(&mut command_rx, &mut queued_prompts);
312        let written = write_queued_user_prompts(&input, &storage_type, &mut queued_prompts).await?;
313        emit_queued_user_prompts(tx, written).await;
314
315        // 组合 tools 配置
316        let tools = otherone_tools::combine_tools(ai.tools.clone());
317        ai.tools = tools;
318
319        // 加载上下文
320        let context_load_type = match &input.context_load_type {
321            AgentContextLoadType::LocalFile => otherone_context::types::ContextLoadType::LocalFile,
322            AgentContextLoadType::Database => otherone_context::types::ContextLoadType::Database,
323        };
324
325        let messages =
326            otherone_context::combine_context(&otherone_context::types::CombineContextOptions {
327                session_id: input.session_id.clone(),
328                load_type: context_load_type,
329                provider: ai.provider.clone(),
330                context_window: input.context_window,
331                threshold_percentage: input.threshold_percentage,
332                ai: ai.other.clone(),
333                system_prompt: ai.system_prompt.clone(),
334                tools: ai.tools.clone(),
335                database_config: input.database_config.clone(),
336                runtime_context: input.runtime_context.clone(),
337            })
338            .await
339            .map_err(|e| AgentError::ContextError(e.to_string()))?;
340
341        // 构建 AI 配置,强制流式
342        let mut ai_config = build_ai_config(ai, &messages);
343        ai_config["stream"] = serde_json::json!(true);
344
345        // 调用流式 AI 模型
346        let mut stream = otherone_ai::invoke_model_stream(
347            ai.provider.clone(),
348            &ai.api_key,
349            &ai.base_url,
350            ai_config,
351        )
352        .await?;
353
354        // 累积变量
355        let mut full_content = String::new();
356        let mut role = "assistant".to_string();
357        let mut stream_tool_calls: Vec<otherone_ai::types::ToolCall> = Vec::new();
358        let mut token_consumption: u32 = 0;
359
360        // 遍历流,实时发送给调用者
361        use futures::StreamExt;
362        while let Some(chunk_result) = stream.next().await {
363            let chunk = match chunk_result {
364                Ok(c) => c,
365                Err(e) => {
366                    let _ = tx
367                        .send(StreamAgentEvent {
368                            event_type: "error".to_string(),
369                            content: format!("[error:{}]", e),
370                            raw_chunk: None,
371                            error: Some(e.to_string()),
372                        })
373                        .await;
374                    return Err(AgentError::AiError(e));
375                }
376            };
377
378            // 发送原始 chunk
379            let raw_json = serde_json::to_value(&chunk).unwrap_or_default();
380            let _ = tx
381                .send(StreamAgentEvent {
382                    event_type: "chunk".to_string(),
383                    content: String::new(),
384                    raw_chunk: Some(raw_json),
385                    error: None,
386                })
387                .await;
388
389            // 提取 delta 信息
390            if let Some(choice) = chunk.choices.first() {
391                if let Some(ref delta) = choice.delta {
392                    if let Some(ref r) = delta.role {
393                        role = r.clone();
394                    }
395                    if let Some(ref c) = delta.content {
396                        full_content.push_str(c);
397                    }
398                    if let Some(thinking) = delta_thinking_content(delta) {
399                        let _ = tx
400                            .send(StreamAgentEvent {
401                                event_type: "thinking".to_string(),
402                                content: thinking.to_string(),
403                                raw_chunk: None,
404                                error: None,
405                            })
406                            .await;
407                    }
408                }
409
410                if let Some(ref delta) = choice.delta {
411                    // 累积 tool_calls
412                    if let Some(ref tc) = delta.tool_calls {
413                        for tool_call in tc {
414                            // 按 index 或 id 匹配合并
415                            merge_tool_call_delta(&mut stream_tool_calls, tool_call);
416                        }
417                    }
418                }
419
420                if let Some(ref usage) = chunk.usage {
421                    token_consumption = usage.total_tokens.unwrap_or(0);
422                }
423            }
424        }
425
426        // 流结束,构建完整响应
427        let tools_wrapper = if stream_tool_calls.is_empty() {
428            None
429        } else {
430            Some(otherone_ai::types::ToolCallsWrapper {
431                tool_calls: stream_tool_calls.clone(),
432            })
433        };
434
435        // 存储 AI 响应
436        otherone_storage::write_entry(&WriteEntryOptions {
437            storage_type: storage_type.clone(),
438            session_id: input.session_id.clone(),
439            role: role.clone(),
440            content: full_content.clone(),
441            tools: tools_wrapper
442                .as_ref()
443                .map(|tw| serde_json::json!({ "tool_calls": tw.tool_calls })),
444            token_consumption: Some(token_consumption),
445            create_at: None,
446            database_config: input.database_config.clone(),
447            runtime_context: input.runtime_context.clone(),
448            metadata: Default::default(),
449        })
450        .await
451        .map_err(|e| AgentError::ContextError(e.to_string()))?;
452
453        // 检查是否有 tool 调用
454        if let Some(ref tw) = tools_wrapper {
455            if !tw.tool_calls.is_empty() {
456                if tw
457                    .tool_calls
458                    .iter()
459                    .any(|tool_call| tool_call.function.name == LONG_TERM_MEMORY_TOOL_NAME)
460                {
461                    long_term_memory_activity = true;
462                }
463
464                let tool_calls_info: Vec<String> = tw
465                    .tool_calls
466                    .iter()
467                    .map(|tc| format!("{}({})", tc.function.name, tc.function.arguments))
468                    .collect();
469
470                let _ = tx
471                    .send(StreamAgentEvent {
472                        event_type: "tool_calls".to_string(),
473                        content: format!("[tool_calls:{}]", tool_calls_info.join(", ")),
474                        raw_chunk: None,
475                        error: None,
476                    })
477                    .await;
478
479                // 处理 tool 调用
480                let default_tools_realize: HashMap<
481                    String,
482                    Box<dyn Fn(serde_json::Value) -> String + Send + Sync>,
483                > = HashMap::new();
484                let tools_realize = ai.tools_realize.as_ref().unwrap_or(&default_tools_realize);
485
486                let tool_results = otherone_tools::process_tools(&tw.tool_calls, tools_realize)
487                    .map_err(|e| AgentError::ToolError(e))?;
488
489                for tool_result in &tool_results {
490                    let tool_content = serde_json::to_string(
491                        tool_result
492                            .result
493                            .as_ref()
494                            .unwrap_or(&serde_json::Value::Null),
495                    )
496                    .unwrap_or_default();
497
498                    let tools_value = serde_json::json!({
499                        "tool_call_id": tool_result.tool_call_id,
500                        "function_name": tool_result.function_name,
501                        "result": tool_result.result,
502                        "error": tool_result.error,
503                    });
504
505                    otherone_storage::write_entry(&WriteEntryOptions {
506                        storage_type: storage_type.clone(),
507                        session_id: input.session_id.clone(),
508                        role: "tool".to_string(),
509                        content: tool_content,
510                        tools: Some(tools_value),
511                        token_consumption: None,
512                        create_at: None,
513                        database_config: input.database_config.clone(),
514                        runtime_context: input.runtime_context.clone(),
515                        metadata: Default::default(),
516                    })
517                    .await
518                    .map_err(|e| AgentError::ContextError(e.to_string()))?;
519                }
520
521                spawn_drained_long_term_memory_processing(&memory_queue, &memory_model_config);
522
523                drain_agent_commands(&mut command_rx, &mut queued_prompts);
524                let written =
525                    write_queued_user_prompts(&input, &storage_type, &mut queued_prompts).await?;
526                emit_queued_user_prompts(tx, written).await;
527
528                tokio::time::sleep(Duration::from_millis(1500)).await;
529                continue;
530            }
531        }
532
533        drain_agent_commands(&mut command_rx, &mut queued_prompts);
534        let written = write_queued_user_prompts(&input, &storage_type, &mut queued_prompts).await?;
535        if written > 0 {
536            emit_queued_user_prompts(tx, written).await;
537            continue;
538        }
539
540        // 发送完成事件
541        spawn_long_term_memory_heuristic_fallback(
542            &memory_model_config,
543            ai.user_prompt.as_deref(),
544            long_term_memory_activity,
545        );
546
547        let _ = tx
548            .send(StreamAgentEvent {
549                event_type: "complete".to_string(),
550                content: full_content,
551                raw_chunk: None,
552                error: None,
553            })
554            .await;
555
556        return Ok(());
557    }
558
559    let err_msg = format!(
560        "Agent循环次数超过限制({}次),可能陷入无限循环",
561        max_iterations
562    );
563    let _ = tx
564        .send(StreamAgentEvent {
565            event_type: "error".to_string(),
566            content: format!("[error:{}]", err_msg),
567            raw_chunk: None,
568            error: Some(err_msg.clone()),
569        })
570        .await;
571    Err(AgentError::MaxIterationsExceeded(max_iterations))
572}
573
574fn delta_thinking_content(delta: &otherone_ai::types::ResponseDelta) -> Option<&str> {
575    delta
576        .reasoning_content
577        .as_deref()
578        .or(delta.reasoning.as_deref())
579        .or(delta.thinking.as_deref())
580        .or(delta.thought.as_deref())
581        .filter(|content| !content.is_empty())
582}
583
584fn merge_tool_call_delta(
585    tool_calls: &mut Vec<otherone_ai::types::ToolCall>,
586    delta: &otherone_ai::types::ToolCall,
587) {
588    let target = match delta.index {
589        Some(index) => {
590            let target_index = index as usize;
591            while tool_calls.len() <= target_index {
592                tool_calls.push(empty_tool_call(Some(tool_calls.len() as u32)));
593            }
594            &mut tool_calls[target_index]
595        }
596        None => match tool_calls
597            .iter()
598            .position(|existing| !delta.id.is_empty() && existing.id == delta.id)
599        {
600            Some(position) => &mut tool_calls[position],
601            None => {
602                tool_calls.push(empty_tool_call(None));
603                tool_calls.last_mut().expect("new tool call exists")
604            }
605        },
606    };
607
608    if delta.index.is_some() {
609        target.index = delta.index;
610    }
611    if !delta.id.is_empty() {
612        target.id = delta.id.clone();
613    }
614    if !delta.call_type.is_empty() {
615        target.call_type = delta.call_type.clone();
616    }
617    if !delta.function.name.is_empty() {
618        target.function.name.push_str(&delta.function.name);
619    }
620    if !delta.function.arguments.is_empty() {
621        target
622            .function
623            .arguments
624            .push_str(&delta.function.arguments);
625    }
626}
627
628fn empty_tool_call(index: Option<u32>) -> otherone_ai::types::ToolCall {
629    otherone_ai::types::ToolCall {
630        index,
631        id: String::new(),
632        call_type: "function".to_string(),
633        function: otherone_ai::types::FunctionCall::default(),
634    }
635}
636
637/// 调用 Agent — 核心驱动方法
638/// 作用:驱动完整的 AI 对话流程,支持非流式模式
639/// 关联:调用所有子模块
640/// 预期结果:返回最终的 ParsedResponse
641pub async fn invoke_agent(
642    input: &InputOptions,
643    ai: &mut AiOptions,
644    auxiliary_ai: Option<&mut AiOptions>,
645) -> Result<otherone_ai::types::ParsedResponse, AgentError> {
646    let long_term_memory_enabled = input.enable_long_term_memory.unwrap_or(false);
647    let memory_queue = if long_term_memory_enabled {
648        Some(Arc::new(Mutex::new(Vec::new())))
649    } else {
650        None
651    };
652    let memory_model_config = if long_term_memory_enabled {
653        let model_source = auxiliary_ai.as_ref().map(|aux| &**aux).unwrap_or(ai);
654        Some(MemoryModelConfig::from_ai(model_source))
655    } else {
656        None
657    };
658    let long_term_memory_recall_max_types =
659        normalize_recall_max_types(input.long_term_memory_recall_max_types);
660
661    if let Some(queue) = &memory_queue {
662        configure_long_term_memory(ai, Arc::clone(queue), long_term_memory_recall_max_types);
663    }
664
665    let storage_type: StorageType = match &input.storage_type {
666        Some(AgentStorageType::LocalFile) => StorageType::LocalFile,
667        Some(AgentStorageType::Database) => StorageType::Database,
668        None => StorageType::LocalFile,
669    };
670
671    // 如果有 user_prompt,先存储用户消息
672    if let Some(ref user_prompt) = ai.user_prompt {
673        otherone_storage::write_entry(&WriteEntryOptions {
674            storage_type: storage_type.clone(),
675            session_id: input.session_id.clone(),
676            role: "user".to_string(),
677            content: user_prompt.clone(),
678            tools: None,
679            token_consumption: None,
680            create_at: None,
681            database_config: input.database_config.clone(),
682            runtime_context: input.runtime_context.clone(),
683            metadata: Default::default(),
684        })
685        .await
686        .map_err(|e| AgentError::ContextError(e.to_string()))?;
687    }
688
689    // 循环次数限制
690    let max_iterations = input.max_iterations.unwrap_or(999999);
691    let mut iteration: u32 = 0;
692    let mut long_term_memory_activity = false;
693
694    while iteration < max_iterations {
695        iteration += 1;
696
697        // 组合 tools 配置
698        let tools = otherone_tools::combine_tools(ai.tools.clone());
699        ai.tools = tools;
700
701        // 组合 context 配置,加载历史消息
702        let context_load_type = match &input.context_load_type {
703            AgentContextLoadType::LocalFile => otherone_context::types::ContextLoadType::LocalFile,
704            AgentContextLoadType::Database => otherone_context::types::ContextLoadType::Database,
705        };
706
707        let messages =
708            otherone_context::combine_context(&otherone_context::types::CombineContextOptions {
709                session_id: input.session_id.clone(),
710                load_type: context_load_type,
711                provider: ai.provider.clone(),
712                context_window: input.context_window,
713                threshold_percentage: input.threshold_percentage,
714                ai: ai.other.clone(),
715                system_prompt: ai.system_prompt.clone(),
716                tools: ai.tools.clone(),
717                database_config: input.database_config.clone(),
718                runtime_context: input.runtime_context.clone(),
719            })
720            .await
721            .map_err(|e| AgentError::ContextError(e.to_string()))?;
722
723        // 构建 AI 配置 JSON
724        let ai_config = build_ai_config(ai, &messages);
725
726        // 调用 AI 模型
727        let response =
728            otherone_ai::invoke_model(ai.provider.clone(), &ai.api_key, &ai.base_url, ai_config)
729                .await?;
730
731        // 解析响应
732        let mut parsed =
733            parse_ai_response(&response, &ai.provider).map_err(|e| AgentError::ContextError(e))?;
734
735        // 存储 AI 响应到 storage
736        otherone_storage::write_entry(&WriteEntryOptions {
737            storage_type: storage_type.clone(),
738            session_id: input.session_id.clone(),
739            role: parsed.role.clone(),
740            content: parsed.content.clone(),
741            tools: parsed
742                .tools
743                .as_ref()
744                .map(|tw| serde_json::json!({ "tool_calls": tw.tool_calls })),
745            token_consumption: Some(parsed.token_consumption),
746            create_at: None,
747            database_config: input.database_config.clone(),
748            runtime_context: input.runtime_context.clone(),
749            metadata: Default::default(),
750        })
751        .await
752        .map_err(|e| AgentError::ContextError(e.to_string()))?;
753
754        // 检查是否有 tool 调用
755        if let Some(ref tools_wrapper) = parsed.tools {
756            if !tools_wrapper.tool_calls.is_empty() {
757                if tools_wrapper
758                    .tool_calls
759                    .iter()
760                    .any(|tool_call| tool_call.function.name == LONG_TERM_MEMORY_TOOL_NAME)
761                {
762                    long_term_memory_activity = true;
763                }
764
765                let tool_calls_info: Vec<String> = tools_wrapper
766                    .tool_calls
767                    .iter()
768                    .map(|tc| format!("{}({})", tc.function.name, tc.function.arguments))
769                    .collect();
770                parsed.content = format!(
771                    "[tool_calls:{}]\n\n{}",
772                    tool_calls_info.join(", "),
773                    parsed.content
774                );
775
776                // 使用用户传入的 tools_realize,若未提供则使用空 HashMap
777                let default_tools_realize: HashMap<
778                    String,
779                    Box<dyn Fn(serde_json::Value) -> String + Send + Sync>,
780                > = HashMap::new();
781                let tools_realize = ai.tools_realize.as_ref().unwrap_or(&default_tools_realize);
782
783                let tool_results =
784                    otherone_tools::process_tools(&tools_wrapper.tool_calls, tools_realize)
785                        .map_err(|e| AgentError::ToolError(e))?;
786
787                for tool_result in &tool_results {
788                    let tool_content = serde_json::to_string(
789                        tool_result
790                            .result
791                            .as_ref()
792                            .unwrap_or(&serde_json::Value::Null),
793                    )
794                    .unwrap_or_default();
795
796                    let tools_value = serde_json::json!({
797                        "tool_call_id": tool_result.tool_call_id,
798                        "function_name": tool_result.function_name,
799                        "result": tool_result.result,
800                        "error": tool_result.error,
801                    });
802
803                    otherone_storage::write_entry(&WriteEntryOptions {
804                        storage_type: storage_type.clone(),
805                        session_id: input.session_id.clone(),
806                        role: "tool".to_string(),
807                        content: tool_content,
808                        tools: Some(tools_value),
809                        token_consumption: None,
810                        create_at: None,
811                        database_config: input.database_config.clone(),
812                        runtime_context: input.runtime_context.clone(),
813                        metadata: Default::default(),
814                    })
815                    .await
816                    .map_err(|e| AgentError::ContextError(e.to_string()))?;
817                }
818
819                spawn_drained_long_term_memory_processing(&memory_queue, &memory_model_config);
820
821                tokio::time::sleep(Duration::from_millis(1500)).await;
822                continue;
823            }
824        }
825
826        spawn_long_term_memory_heuristic_fallback(
827            &memory_model_config,
828            ai.user_prompt.as_deref(),
829            long_term_memory_activity,
830        );
831
832        return Ok(parsed);
833    }
834
835    Err(AgentError::MaxIterationsExceeded(max_iterations))
836}
837
838/// 构建 AI 调用的配置 JSON
839fn build_ai_config(ai: &AiOptions, messages: &[otherone_ai::types::Message]) -> serde_json::Value {
840    let mut config = serde_json::json!({
841        "model": ai.model,
842        "messages": messages,
843    });
844
845    if let Some(context_length) = ai.context_length {
846        config["contextLength"] = serde_json::json!(context_length);
847    }
848    if let Some(temperature) = ai.temperature {
849        config["temperature"] = serde_json::json!(temperature);
850    }
851    if let Some(top_p) = ai.top_p {
852        config["topP"] = serde_json::json!(top_p);
853    }
854    if let Some(ref tools) = ai.tools {
855        config["tools"] = serde_json::to_value(tools).unwrap();
856    }
857    if let Some(ref tool_choice) = ai.tool_choice {
858        config["toolChoice"] = serde_json::to_value(tool_choice).unwrap();
859    }
860    if let Some(parallel_tool_calls) = ai.parallel_tool_calls {
861        config["parallelToolCalls"] = serde_json::json!(parallel_tool_calls);
862    }
863    if let Some(stream) = ai.stream {
864        config["stream"] = serde_json::json!(stream);
865    }
866    if let Some(ref other) = ai.other {
867        if let serde_json::Value::Object(ref obj) = other {
868            for (key, value) in obj {
869                config[key] = value.clone();
870            }
871        }
872    }
873
874    config
875}
876
877impl MemoryModelConfig {
878    fn from_ai(ai: &AiOptions) -> Self {
879        Self {
880            provider: ai.provider.clone(),
881            api_key: ai.api_key.clone(),
882            base_url: ai.base_url.clone(),
883            model: ai.model.clone(),
884            context_length: ai.context_length,
885            temperature: ai.temperature,
886            top_p: ai.top_p,
887            other: ai.other.clone(),
888        }
889    }
890}
891
892fn configure_long_term_memory(
893    ai: &mut AiOptions,
894    queue: LongTermMemoryQueue,
895    recall_max_types: usize,
896) {
897    inject_long_term_memory_system_prompt(ai);
898    inject_long_term_memory_tools(ai, recall_max_types);
899    inject_long_term_memory_tool_handlers(ai, queue, recall_max_types);
900}
901
902fn normalize_recall_max_types(max_types: Option<usize>) -> usize {
903    max_types
904        .unwrap_or(DEFAULT_LONG_TERM_MEMORY_RECALL_MAX_TYPES)
905        .clamp(1, MAX_LONG_TERM_MEMORY_RECALL_MAX_TYPES)
906}
907
908fn inject_long_term_memory_system_prompt(ai: &mut AiOptions) {
909    match ai.system_prompt.as_mut() {
910        Some(system_prompt) => {
911            if !system_prompt.contains(LONG_TERM_MEMORY_SYSTEM_PROMPT) {
912                system_prompt.push_str("\n\n");
913                system_prompt.push_str(LONG_TERM_MEMORY_SYSTEM_PROMPT);
914            }
915        }
916        None => {
917            ai.system_prompt = Some(LONG_TERM_MEMORY_SYSTEM_PROMPT.to_string());
918        }
919    }
920}
921
922fn inject_long_term_memory_tools(ai: &mut AiOptions, recall_max_types: usize) {
923    let tools = ai.tools.get_or_insert_with(Vec::new);
924
925    if !tools
926        .iter()
927        .any(|tool| tool.function.name == LONG_TERM_MEMORY_RECALL_TOOL_NAME)
928    {
929        tools.insert(0, long_term_memory_recall_tool_definition(recall_max_types));
930    }
931
932    if !tools
933        .iter()
934        .any(|tool| tool.function.name == LONG_TERM_MEMORY_TOOL_NAME)
935    {
936        tools.insert(0, long_term_memory_tool_definition());
937    }
938}
939
940fn inject_long_term_memory_tool_handlers(
941    ai: &mut AiOptions,
942    queue: LongTermMemoryQueue,
943    recall_max_types: usize,
944) {
945    let tools_realize = ai.tools_realize.get_or_insert_with(HashMap::new);
946    tools_realize.insert(
947        LONG_TERM_MEMORY_TOOL_NAME.to_string(),
948        Box::new(move |args| enqueue_long_term_memory(args, Arc::clone(&queue))),
949    );
950    tools_realize.insert(
951        LONG_TERM_MEMORY_RECALL_TOOL_NAME.to_string(),
952        Box::new(move |args| recall_long_term_memory(args, recall_max_types)),
953    );
954}
955
956fn long_term_memory_tool_definition() -> otherone_ai::types::Tool {
957    otherone_ai::types::Tool {
958        tool_type: "function".to_string(),
959        function: otherone_ai::types::FunctionDefinition {
960            name: LONG_TERM_MEMORY_TOOL_NAME.to_string(),
961            description: "Queue reusable user information for long-term memory processing."
962                .to_string(),
963            parameters: Some(serde_json::json!({
964                "type": "object",
965                "properties": {
966                    "storage": {
967                        "type": "string",
968                        "description": "Concise reusable memory statement."
969                    },
970                    "types": {
971                        "type": "string",
972                        "description": "Specific semantic category for the memory node."
973                    },
974                    "possible_parent_types": {
975                        "type": "array",
976                        "description": "Short keywords or semantic tags that may match existing parent memory categories.",
977                        "items": {
978                            "type": "string"
979                        },
980                        "minItems": 1,
981                        "maxItems": 5
982                    }
983                },
984                "required": ["storage", "types", "possible_parent_types"],
985                "additionalProperties": false
986            })),
987        },
988    }
989}
990
991fn long_term_memory_recall_tool_definition(recall_max_types: usize) -> otherone_ai::types::Tool {
992    otherone_ai::types::Tool {
993        tool_type: "function".to_string(),
994        function: otherone_ai::types::FunctionDefinition {
995            name: LONG_TERM_MEMORY_RECALL_TOOL_NAME.to_string(),
996            description: "Recall relevant long-term memory facts by semantic type keywords."
997                .to_string(),
998            parameters: Some(serde_json::json!({
999                "type": "object",
1000                "properties": {
1001                    "memory_types": {
1002                        "type": "array",
1003                        "description": "Compact semantic type keywords or tags for memories to retrieve.",
1004                        "items": {
1005                            "type": "string"
1006                        },
1007                        "minItems": 1,
1008                        "maxItems": recall_max_types
1009                    }
1010                },
1011                "required": ["memory_types"],
1012                "additionalProperties": false
1013            })),
1014        },
1015    }
1016}
1017
1018fn enqueue_long_term_memory(args: serde_json::Value, queue: LongTermMemoryQueue) -> String {
1019    match parse_pending_long_term_memory(args) {
1020        Ok(pending_memory) => match queue.lock() {
1021            Ok(mut pending_queue) => pending_queue.push(pending_memory),
1022            Err(error) => warn!("failed to lock long-term memory queue: {error}"),
1023        },
1024        Err(error) => warn!("failed to parse long-term memory tool arguments: {error}"),
1025    }
1026
1027    "done".to_string()
1028}
1029
1030fn parse_pending_long_term_memory(
1031    args: serde_json::Value,
1032) -> Result<PendingLongTermMemory, String> {
1033    let mut pending: PendingLongTermMemory =
1034        serde_json::from_value(args).map_err(|error| error.to_string())?;
1035
1036    pending.storage = pending.storage.trim().to_string();
1037    pending.types = pending.types.trim().to_string();
1038    let mut seen_parent_types = HashSet::new();
1039    pending.possible_parent_types = pending
1040        .possible_parent_types
1041        .into_iter()
1042        .map(|parent_type| parent_type.trim().to_string())
1043        .filter(|parent_type| !parent_type.is_empty())
1044        .filter(|parent_type| seen_parent_types.insert(parent_type.clone()))
1045        .take(5)
1046        .collect();
1047
1048    if pending.storage.is_empty() {
1049        return Err("storage cannot be empty".to_string());
1050    }
1051    if pending.types.is_empty() {
1052        return Err("types cannot be empty".to_string());
1053    }
1054    if pending.possible_parent_types.is_empty() {
1055        pending.possible_parent_types.push(pending.types.clone());
1056    }
1057
1058    Ok(pending)
1059}
1060
1061fn recall_long_term_memory(args: serde_json::Value, recall_max_types: usize) -> String {
1062    match recall_long_term_memory_inner(args, recall_max_types) {
1063        Ok(recalled_memory) => recalled_memory,
1064        Err(error) => {
1065            warn!("failed to recall long-term memory: {error}");
1066            format!("Long-term memory recall failed: {error}")
1067        }
1068    }
1069}
1070
1071fn recall_long_term_memory_inner(
1072    args: serde_json::Value,
1073    recall_max_types: usize,
1074) -> Result<String, String> {
1075    let memory_types = parse_recall_memory_types(args, recall_max_types)?;
1076    let tree = otherone_memory::read_memory_tree().map_err(|error| error.to_string())?;
1077    let branches = tree
1078        .recall_subtree_memories(
1079            &memory_types,
1080            &otherone_memory::MemoryRecallOptions {
1081                max_types: recall_max_types,
1082                max_matches_per_type: 3,
1083                max_returned_memories: 64,
1084            },
1085        )
1086        .map_err(|error| error.to_string())?;
1087
1088    Ok(format_recalled_memories_for_agent(&branches))
1089}
1090
1091fn parse_recall_memory_types(
1092    args: serde_json::Value,
1093    recall_max_types: usize,
1094) -> Result<Vec<String>, String> {
1095    let recall_args: LongTermMemoryRecallArgs =
1096        serde_json::from_value(args).map_err(|error| error.to_string())?;
1097    let mut seen = HashSet::new();
1098    let memory_types: Vec<String> = recall_args
1099        .memory_types
1100        .into_iter()
1101        .map(|memory_type| memory_type.trim().to_string())
1102        .filter(|memory_type| !memory_type.is_empty())
1103        .filter(|memory_type| seen.insert(memory_type.clone()))
1104        .take(recall_max_types)
1105        .collect();
1106
1107    if memory_types.is_empty() {
1108        return Err("memory_types cannot be empty".to_string());
1109    }
1110
1111    Ok(memory_types)
1112}
1113
1114fn format_recalled_memories_for_agent(branches: &[otherone_memory::MemoryRecallBranch]) -> String {
1115    let mut seen = HashSet::new();
1116    let memories: Vec<String> = branches
1117        .iter()
1118        .flat_map(|branch| branch.memories.iter())
1119        .map(|memory| memory.trim())
1120        .filter(|memory| !memory.is_empty())
1121        .filter(|memory| seen.insert((*memory).to_string()))
1122        .map(|memory| memory.to_string())
1123        .collect();
1124
1125    if memories.is_empty() {
1126        return "<long-term-memory>\nNo relevant long-term memory found.\n</long-term-memory>"
1127            .to_string();
1128    }
1129
1130    let mut lines = vec!["<long-term-memory>".to_string()];
1131    for memory in memories {
1132        lines.push(format!("- {memory}"));
1133    }
1134    lines.push("</long-term-memory>".to_string());
1135    lines.join("\n")
1136}
1137
1138fn spawn_drained_long_term_memory_processing(
1139    queue: &Option<LongTermMemoryQueue>,
1140    model_config: &Option<MemoryModelConfig>,
1141) {
1142    let pending_items = drain_long_term_memory_queue(queue);
1143    if pending_items.is_empty() {
1144        return;
1145    }
1146
1147    let Some(model_config) = model_config.clone() else {
1148        return;
1149    };
1150
1151    tokio::spawn(async move {
1152        let _guard = long_term_memory_processing_lock().lock().await;
1153
1154        for pending_memory in pending_items {
1155            if let Err(error) =
1156                process_long_term_memory_candidate(&model_config, pending_memory).await
1157            {
1158                warn!("failed to process long-term memory candidate: {error}");
1159            }
1160        }
1161    });
1162}
1163
1164fn spawn_long_term_memory_heuristic_fallback(
1165    model_config: &Option<MemoryModelConfig>,
1166    user_prompt: Option<&str>,
1167    already_queued_memory: bool,
1168) {
1169    if already_queued_memory {
1170        return;
1171    }
1172
1173    let Some(user_prompt) = user_prompt else {
1174        return;
1175    };
1176    if !should_queue_long_term_memory_fallback(user_prompt) {
1177        return;
1178    }
1179
1180    let Some(model_config) = model_config.clone() else {
1181        return;
1182    };
1183    let pending_memory = PendingLongTermMemory {
1184        storage: user_prompt.trim().to_string(),
1185        types: LONG_TERM_MEMORY_FALLBACK_TYPES.to_string(),
1186        possible_parent_types: fallback_possible_parent_types(user_prompt),
1187    };
1188
1189    tokio::spawn(async move {
1190        let _guard = long_term_memory_processing_lock().lock().await;
1191        if let Err(error) = process_long_term_memory_candidate(&model_config, pending_memory).await
1192        {
1193            warn!("failed to process long-term memory fallback candidate: {error}");
1194        }
1195    });
1196}
1197
1198fn should_queue_long_term_memory_fallback(user_prompt: &str) -> bool {
1199    let normalized = normalize_memory_fallback_text(user_prompt);
1200    if normalized.is_empty() {
1201        return false;
1202    }
1203
1204    let negative_terms = [
1205        "临时",
1206        "这次",
1207        "当前",
1208        "今天",
1209        "刚刚",
1210        "一次性",
1211        "十分钟",
1212        "提醒我",
1213        "明天提醒",
1214        "验证码",
1215        "密码",
1216        "apikey",
1217        "api密钥",
1218        "不要长期",
1219        "不要记",
1220        "不用新增",
1221        "不用重复",
1222        "已经记住",
1223        "翻译",
1224        "算一下",
1225        "cargotest",
1226        "ignored",
1227    ];
1228    if negative_terms.iter().any(|term| normalized.contains(term)) {
1229        return false;
1230    }
1231
1232    let positive_terms = [
1233        "可以长期",
1234        "长期",
1235        "以后",
1236        "默认",
1237        "偏好",
1238        "喜欢",
1239        "不喜欢",
1240        "过敏",
1241        "习惯",
1242        "项目里",
1243        "规则",
1244        "约定",
1245        "叫我",
1246        "我叫",
1247        "称呼",
1248        "不吃",
1249        "必须",
1250        "采用",
1251    ];
1252    positive_terms.iter().any(|term| normalized.contains(term))
1253}
1254
1255fn fallback_possible_parent_types(user_prompt: &str) -> Vec<String> {
1256    let normalized = normalize_memory_fallback_text(user_prompt);
1257
1258    if contains_any(&normalized, &["我叫", "叫我", "称呼"]) {
1259        return string_vec(&["身份信息", "称呼", "用户资料"]);
1260    }
1261    if contains_any(&normalized, &["中文", "英文", "语言", "交流", "沟通"]) {
1262        return string_vec(&["沟通偏好", "语言偏好", "对话设置"]);
1263    }
1264    if contains_any(
1265        &normalized,
1266        &["吃", "食物", "饮食", "菜", "辣", "香菜", "过敏"],
1267    ) {
1268        return string_vec(&["食物偏好", "饮食偏好", "忌口偏好"]);
1269    }
1270    if contains_any(
1271        &normalized,
1272        &[
1273            "rust",
1274            "代码",
1275            "项目",
1276            "规则",
1277            "架构",
1278            "memory",
1279            "向量数据库",
1280        ],
1281    ) {
1282        return string_vec(&["项目规则", "编码偏好", "技术规范"]);
1283    }
1284    if contains_any(
1285        &normalized,
1286        &["前端", "设计", "界面", "主题", "ui", "紫色", "渐变"],
1287    ) {
1288        return string_vec(&["设计偏好", "前端设计", "UI 主题"]);
1289    }
1290    if contains_any(&normalized, &["会议", "开会", "提纲", "资料"]) {
1291        return string_vec(&["工作习惯", "会议习惯", "沟通偏好"]);
1292    }
1293    if contains_any(&normalized, &["旅行", "景点", "路线"]) {
1294        return string_vec(&["旅行偏好", "生活方式偏好", "休闲偏好"]);
1295    }
1296    if contains_any(&normalized, &["跑步", "训练", "运动"]) {
1297        return string_vec(&["运动习惯", "健康偏好", "生活习惯"]);
1298    }
1299
1300    string_vec(&["用户偏好", "长期约束", "用户资料"])
1301}
1302
1303fn contains_any(value: &str, needles: &[&str]) -> bool {
1304    needles.iter().any(|needle| value.contains(needle))
1305}
1306
1307fn string_vec(values: &[&str]) -> Vec<String> {
1308    values.iter().map(|value| value.to_string()).collect()
1309}
1310
1311fn normalize_memory_fallback_text(value: &str) -> String {
1312    value
1313        .chars()
1314        .filter(|ch| !ch.is_whitespace())
1315        .flat_map(char::to_lowercase)
1316        .collect()
1317}
1318
1319fn drain_long_term_memory_queue(queue: &Option<LongTermMemoryQueue>) -> Vec<PendingLongTermMemory> {
1320    let Some(queue) = queue else {
1321        return Vec::new();
1322    };
1323
1324    match queue.lock() {
1325        Ok(mut pending_queue) => pending_queue.drain(..).collect(),
1326        Err(error) => {
1327            warn!("failed to drain long-term memory queue: {error}");
1328            Vec::new()
1329        }
1330    }
1331}
1332
1333fn long_term_memory_processing_lock() -> &'static tokio::sync::Mutex<()> {
1334    static LOCK: OnceLock<tokio::sync::Mutex<()>> = OnceLock::new();
1335    LOCK.get_or_init(|| tokio::sync::Mutex::new(()))
1336}
1337
1338async fn process_long_term_memory_candidate(
1339    model_config: &MemoryModelConfig,
1340    pending_memory: PendingLongTermMemory,
1341) -> Result<(), String> {
1342    let mut tree = otherone_memory::read_memory_tree().map_err(|error| error.to_string())?;
1343    let search_options = otherone_memory::MemoryTypeSearchOptions::default();
1344    let neighborhoods = tree
1345        .find_type_neighborhoods(&pending_memory.possible_parent_types, &search_options)
1346        .map_err(|error| error.to_string())?;
1347    let existing_memory_table =
1348        otherone_memory::MemoryTree::format_type_neighborhoods_for_model(&neighborhoods);
1349
1350    let ai_config = build_long_term_memory_model_config(
1351        model_config,
1352        build_long_term_memory_messages(&pending_memory, &existing_memory_table),
1353    );
1354    let response = otherone_ai::invoke_model(
1355        model_config.provider.clone(),
1356        &model_config.api_key,
1357        &model_config.base_url,
1358        ai_config,
1359    )
1360    .await
1361    .map_err(|error| error.to_string())?;
1362
1363    let commit_args = extract_long_term_memory_commit_args(&response)?;
1364    apply_long_term_memory_commit(&mut tree, &commit_args)?;
1365    tree.validate().map_err(|error| error.to_string())?;
1366    otherone_memory::write_memory_tree(&tree).map_err(|error| error.to_string())
1367}
1368
1369fn build_long_term_memory_messages(
1370    pending_memory: &PendingLongTermMemory,
1371    existing_memory_table: &str,
1372) -> Vec<otherone_ai::types::Message> {
1373    vec![
1374        otherone_ai::types::Message {
1375            role: "system".to_string(),
1376            content: otherone_ai::types::MessageContent::Text(
1377                LONG_TERM_MEMORY_MODEL_SYSTEM_PROMPT.to_string(),
1378            ),
1379            name: None,
1380            tool_calls: None,
1381            tool_call_id: None,
1382        },
1383        otherone_ai::types::Message {
1384            role: "user".to_string(),
1385            content: otherone_ai::types::MessageContent::Text(format!(
1386                "<candidate-memory>\n{}\n</candidate-memory>\n\n{}",
1387                serde_json::to_string_pretty(pending_memory).unwrap_or_default(),
1388                existing_memory_table
1389            )),
1390            name: None,
1391            tool_calls: None,
1392            tool_call_id: None,
1393        },
1394    ]
1395}
1396
1397fn build_long_term_memory_model_config(
1398    model_config: &MemoryModelConfig,
1399    messages: Vec<otherone_ai::types::Message>,
1400) -> serde_json::Value {
1401    let mut config = serde_json::json!({});
1402
1403    if let Some(ref other) = model_config.other {
1404        if let serde_json::Value::Object(ref obj) = other {
1405            for (key, value) in obj {
1406                config[key] = value.clone();
1407            }
1408        }
1409    }
1410
1411    config["model"] = serde_json::json!(model_config.model);
1412    config["messages"] = serde_json::json!(messages);
1413    config["tools"] = serde_json::json!([long_term_memory_commit_tool_definition()]);
1414    config["parallelToolCalls"] = serde_json::json!(false);
1415    config["stream"] = serde_json::json!(false);
1416
1417    if let Some(context_length) = model_config.context_length {
1418        config["contextLength"] = serde_json::json!(context_length);
1419    }
1420    if let Some(temperature) = model_config.temperature {
1421        config["temperature"] = serde_json::json!(temperature);
1422    }
1423    if let Some(top_p) = model_config.top_p {
1424        config["topP"] = serde_json::json!(top_p);
1425    }
1426
1427    config
1428}
1429
1430fn long_term_memory_commit_tool_definition() -> otherone_ai::types::Tool {
1431    otherone_ai::types::Tool {
1432        tool_type: "function".to_string(),
1433        function: otherone_ai::types::FunctionDefinition {
1434            name: LONG_TERM_MEMORY_COMMIT_TOOL_NAME.to_string(),
1435            description:
1436                "Commit one validated long-term memory write operation to the memory tree."
1437                    .to_string(),
1438            parameters: Some(serde_json::json!({
1439                "type": "object",
1440                "properties": {
1441                    "action": {
1442                        "type": "string",
1443                        "enum": [
1444                            "insert_root",
1445                            "insert_child",
1446                            "update_parent_and_insert_child",
1447                            "update_existing",
1448                            "ignore"
1449                        ],
1450                        "description": "The write operation to perform."
1451                    },
1452                    "storage": {
1453                        "type": "string",
1454                        "description": "Memory statement for a new or updated node."
1455                    },
1456                    "types": {
1457                        "type": "string",
1458                        "description": "Specific semantic category for a new or updated node."
1459                    },
1460                    "parent_id": {
1461                        "type": "string",
1462                        "description": "Parent point id for insert_child or update_parent_and_insert_child."
1463                    },
1464                    "target_id": {
1465                        "type": "string",
1466                        "description": "Existing point id for update_existing."
1467                    },
1468                    "parent_storage": {
1469                        "type": "string",
1470                        "description": "Optional replacement storage for the parent before inserting."
1471                    },
1472                    "parent_types": {
1473                        "type": "string",
1474                        "description": "Optional broader replacement types for the parent before inserting."
1475                    }
1476                },
1477                "required": ["action"],
1478                "additionalProperties": false
1479            })),
1480        },
1481    }
1482}
1483
1484fn extract_long_term_memory_commit_args(
1485    response: &otherone_ai::types::ChatResponse,
1486) -> Result<LongTermMemoryCommitArgs, String> {
1487    let tool_call = response
1488        .choices
1489        .iter()
1490        .filter_map(|choice| choice.message.as_ref())
1491        .filter_map(|message| message.tool_calls.as_ref())
1492        .flat_map(|tool_calls| tool_calls.iter())
1493        .find(|tool_call| tool_call.function.name == LONG_TERM_MEMORY_COMMIT_TOOL_NAME)
1494        .ok_or_else(|| {
1495            "long-term memory model did not call otherone_commit_long_term_memory".to_string()
1496        })?;
1497
1498    serde_json::from_str(&tool_call.function.arguments)
1499        .map_err(|error| format!("invalid memory commit arguments: {error}"))
1500}
1501
1502fn apply_long_term_memory_commit(
1503    tree: &mut otherone_memory::MemoryTree,
1504    args: &LongTermMemoryCommitArgs,
1505) -> Result<(), String> {
1506    match args.action.trim() {
1507        "insert_root" => {
1508            let storage = required_commit_field(args.storage.as_ref(), "storage")?;
1509            let types = required_commit_field(args.types.as_ref(), "types")?;
1510            tree.insert_root(storage, types)
1511                .map(|_| ())
1512                .map_err(|error| error.to_string())
1513        }
1514        "insert_child" => {
1515            let parent_id = required_commit_field(args.parent_id.as_ref(), "parent_id")?;
1516            let storage = required_commit_field(args.storage.as_ref(), "storage")?;
1517            let types = required_commit_field(args.types.as_ref(), "types")?;
1518            tree.insert_child(&parent_id, storage, types)
1519                .map(|_| ())
1520                .map_err(|error| error.to_string())
1521        }
1522        "update_parent_and_insert_child" => {
1523            let parent_id = required_commit_field(args.parent_id.as_ref(), "parent_id")?;
1524            let storage = required_commit_field(args.storage.as_ref(), "storage")?;
1525            let types = required_commit_field(args.types.as_ref(), "types")?;
1526            let old_parent = tree
1527                .get(&parent_id)
1528                .cloned()
1529                .ok_or_else(|| format!("parent point not found: {parent_id}"))?;
1530            let parent_had_children = !tree
1531                .child_ids(&parent_id)
1532                .map_err(|error| error.to_string())?
1533                .is_empty();
1534            let old_parent_storage = old_parent
1535                .storage
1536                .as_deref()
1537                .map(str::trim)
1538                .filter(|value| !value.is_empty())
1539                .map(ToString::to_string);
1540            let old_parent_types = old_parent
1541                .types
1542                .as_deref()
1543                .map(str::trim)
1544                .filter(|value| !value.is_empty())
1545                .map(ToString::to_string);
1546            let new_parent_storage = optional_commit_field(args.parent_storage.as_ref());
1547            let should_preserve_old_parent_as_child = !parent_had_children
1548                && old_parent_storage
1549                    .as_ref()
1550                    .map(|old_storage| {
1551                        old_storage != &storage
1552                            && new_parent_storage
1553                                .as_ref()
1554                                .map(|parent_storage| !parent_storage.contains(old_storage))
1555                                .unwrap_or(false)
1556                    })
1557                    .unwrap_or(false);
1558
1559            if let Some(parent_storage) = new_parent_storage {
1560                tree.update_storage(&parent_id, parent_storage)
1561                    .map_err(|error| error.to_string())?;
1562            }
1563            if let Some(parent_types) = optional_commit_field(args.parent_types.as_ref()) {
1564                tree.update_types(&parent_id, parent_types)
1565                    .map_err(|error| error.to_string())?;
1566            }
1567
1568            if should_preserve_old_parent_as_child {
1569                let old_storage = old_parent_storage.expect("checked old parent storage exists");
1570                let old_types =
1571                    old_parent_types.unwrap_or_else(|| LONG_TERM_MEMORY_FALLBACK_TYPES.to_string());
1572                tree.insert_child(&parent_id, old_storage, old_types)
1573                    .map_err(|error| error.to_string())?;
1574            }
1575
1576            tree.insert_child(&parent_id, storage, types)
1577                .map(|_| ())
1578                .map_err(|error| error.to_string())
1579        }
1580        "update_existing" => {
1581            let target_id = required_commit_field(args.target_id.as_ref(), "target_id")?;
1582            let mut updated = false;
1583
1584            if let Some(storage) = optional_commit_field(args.storage.as_ref()) {
1585                tree.update_storage(&target_id, storage)
1586                    .map_err(|error| error.to_string())?;
1587                updated = true;
1588            }
1589            if let Some(types) = optional_commit_field(args.types.as_ref()) {
1590                tree.update_types(&target_id, types)
1591                    .map_err(|error| error.to_string())?;
1592                updated = true;
1593            }
1594
1595            if updated {
1596                Ok(())
1597            } else {
1598                Err("update_existing requires storage or types".to_string())
1599            }
1600        }
1601        "ignore" => Ok(()),
1602        action => Err(format!("unsupported memory commit action: {action}")),
1603    }
1604}
1605
1606fn required_commit_field(value: Option<&String>, field: &str) -> Result<String, String> {
1607    optional_commit_field(value).ok_or_else(|| format!("{field} is required"))
1608}
1609
1610fn optional_commit_field(value: Option<&String>) -> Option<String> {
1611    value
1612        .map(|value| value.trim().to_string())
1613        .filter(|value| !value.is_empty())
1614}
1615
1616#[cfg(test)]
1617mod tests {
1618    use super::*;
1619
1620    #[test]
1621    fn test_build_ai_config_basic() {
1622        let ai = AiOptions {
1623            provider: otherone_ai::types::ProviderType::OpenAI,
1624            api_key: "test-key".to_string(),
1625            base_url: "https://api.openai.com/v1".to_string(),
1626            model: "gpt-4".to_string(),
1627            user_prompt: None,
1628            system_prompt: None,
1629            messages: None,
1630            context_length: Some(4096),
1631            temperature: Some(0.7),
1632            top_p: None,
1633            tools: None,
1634            tools_realize: None,
1635            tool_choice: None,
1636            parallel_tool_calls: None,
1637            stream: None,
1638            other: None,
1639        };
1640
1641        let messages = vec![];
1642        let config = build_ai_config(&ai, &messages);
1643
1644        assert_eq!(config["model"], "gpt-4");
1645        assert_eq!(config["contextLength"], 4096);
1646        assert!((config["temperature"].as_f64().unwrap() - 0.7).abs() < 0.001);
1647    }
1648
1649    #[test]
1650    fn configure_long_term_memory_adds_prompt_tool_and_handler() {
1651        let mut ai = AiOptions {
1652            provider: otherone_ai::types::ProviderType::OpenAI,
1653            api_key: "test-key".to_string(),
1654            base_url: "https://api.openai.com/v1".to_string(),
1655            model: "gpt-4".to_string(),
1656            user_prompt: None,
1657            system_prompt: Some("You are helpful.".to_string()),
1658            messages: None,
1659            context_length: None,
1660            temperature: None,
1661            top_p: None,
1662            tools: None,
1663            tools_realize: None,
1664            tool_choice: None,
1665            parallel_tool_calls: None,
1666            stream: None,
1667            other: None,
1668        };
1669
1670        let queue: LongTermMemoryQueue = Arc::new(Mutex::new(Vec::new()));
1671
1672        configure_long_term_memory(&mut ai, Arc::clone(&queue), 4);
1673        configure_long_term_memory(&mut ai, Arc::clone(&queue), 4);
1674
1675        let system_prompt = ai.system_prompt.as_deref().unwrap();
1676        assert!(system_prompt.contains("You are helpful."));
1677        assert_eq!(
1678            system_prompt
1679                .matches(LONG_TERM_MEMORY_SYSTEM_PROMPT)
1680                .count(),
1681            1
1682        );
1683
1684        let tools = ai.tools.as_ref().unwrap();
1685        assert_eq!(
1686            tools
1687                .iter()
1688                .filter(|tool| tool.function.name == LONG_TERM_MEMORY_TOOL_NAME)
1689                .count(),
1690            1
1691        );
1692        assert_eq!(
1693            tools
1694                .iter()
1695                .filter(|tool| tool.function.name == LONG_TERM_MEMORY_RECALL_TOOL_NAME)
1696                .count(),
1697            1
1698        );
1699        let add_tool = tools
1700            .iter()
1701            .find(|tool| tool.function.name == LONG_TERM_MEMORY_TOOL_NAME)
1702            .unwrap();
1703        assert!(add_tool
1704            .function
1705            .parameters
1706            .as_ref()
1707            .unwrap()
1708            .to_string()
1709            .contains("possible_parent_types"));
1710        let recall_tool = tools
1711            .iter()
1712            .find(|tool| tool.function.name == LONG_TERM_MEMORY_RECALL_TOOL_NAME)
1713            .unwrap();
1714        assert_eq!(
1715            recall_tool.function.parameters.as_ref().unwrap()["properties"]["memory_types"]
1716                ["maxItems"],
1717            serde_json::json!(4)
1718        );
1719
1720        let tool_calls = vec![otherone_ai::types::ToolCall {
1721            index: None,
1722            id: "call_memory".to_string(),
1723            call_type: "function".to_string(),
1724            function: otherone_ai::types::FunctionCall {
1725                name: LONG_TERM_MEMORY_TOOL_NAME.to_string(),
1726                arguments:
1727                    r#"{"storage":"用户喜欢炸酱面","types":"用户喜欢吃的面食","possible_parent_types":["饮食","面食"]}"#
1728                        .to_string(),
1729            },
1730        }];
1731
1732        let results =
1733            otherone_tools::process_tools(&tool_calls, ai.tools_realize.as_ref().unwrap()).unwrap();
1734
1735        assert_eq!(results.len(), 1);
1736        assert_eq!(results[0].result, Some(serde_json::json!("done")));
1737
1738        let pending_queue = queue.lock().unwrap();
1739        assert_eq!(pending_queue.len(), 1);
1740        assert_eq!(pending_queue[0].storage, "用户喜欢炸酱面");
1741        assert_eq!(pending_queue[0].possible_parent_types, vec!["饮食", "面食"]);
1742    }
1743
1744    #[test]
1745    fn parse_pending_long_term_memory_sanitizes_fields() {
1746        let pending = parse_pending_long_term_memory(serde_json::json!({
1747            "storage": " 用户喜欢炸酱面 ",
1748            "types": " 用户喜欢吃的面食 ",
1749            "possible_parent_types": [" 饮食 ", "", "面食", "面食"]
1750        }))
1751        .unwrap();
1752
1753        assert_eq!(pending.storage, "用户喜欢炸酱面");
1754        assert_eq!(pending.types, "用户喜欢吃的面食");
1755        assert_eq!(pending.possible_parent_types, vec!["饮食", "面食"]);
1756
1757        let fallback = parse_pending_long_term_memory(serde_json::json!({
1758            "storage": "用户喜欢游泳",
1759            "types": "运动偏好",
1760            "possible_parent_types": []
1761        }))
1762        .unwrap();
1763        assert_eq!(fallback.possible_parent_types, vec!["运动偏好"]);
1764    }
1765
1766    #[test]
1767    fn recalls_long_term_memory_storage_only() {
1768        let root = std::env::temp_dir().join(format!(
1769            "otherone-agent-recall-test-{}",
1770            std::time::SystemTime::now()
1771                .duration_since(std::time::UNIX_EPOCH)
1772                .unwrap()
1773                .as_nanos()
1774        ));
1775        otherone_memory::set_memory_storage_root(root);
1776
1777        let mut tree = otherone_memory::MemoryTree::new();
1778        let food = tree
1779            .insert_root("用户喜欢吃炸酱面和糖醋排骨等食物", "用户喜欢的食物")
1780            .unwrap();
1781        tree.insert_child(&food, "用户喜欢吃糖醋排骨", "用户喜欢吃的菜")
1782            .unwrap();
1783        tree.insert_root("用户周末喜欢跑步", "运动习惯").unwrap();
1784        otherone_memory::write_memory_tree(&tree).unwrap();
1785
1786        let result = recall_long_term_memory_inner(
1787            serde_json::json!({
1788                "memory_types": ["食物偏好", "食物偏好", "运动习惯"]
1789            }),
1790            1,
1791        )
1792        .unwrap();
1793
1794        assert!(result.contains("<long-term-memory>"));
1795        assert!(result.contains("- 用户喜欢吃炸酱面和糖醋排骨等食物"));
1796        assert!(result.contains("- 用户喜欢吃糖醋排骨"));
1797        assert!(!result.contains("point_id"));
1798        assert!(!result.contains("用户周末喜欢跑步"));
1799
1800        otherone_memory::clear_memory_storage_root();
1801    }
1802
1803    #[test]
1804    fn recall_max_types_is_clamped() {
1805        assert_eq!(
1806            normalize_recall_max_types(None),
1807            DEFAULT_LONG_TERM_MEMORY_RECALL_MAX_TYPES
1808        );
1809        assert_eq!(normalize_recall_max_types(Some(0)), 1);
1810        assert_eq!(
1811            normalize_recall_max_types(Some(usize::MAX)),
1812            MAX_LONG_TERM_MEMORY_RECALL_MAX_TYPES
1813        );
1814    }
1815
1816    #[test]
1817    fn applies_insert_root_memory_commit() {
1818        let mut tree = otherone_memory::MemoryTree::new();
1819        let args = LongTermMemoryCommitArgs {
1820            action: "insert_root".to_string(),
1821            storage: Some("用户喜欢炸酱面".to_string()),
1822            types: Some("用户喜欢吃的面食".to_string()),
1823            parent_id: None,
1824            target_id: None,
1825            parent_storage: None,
1826            parent_types: None,
1827        };
1828
1829        apply_long_term_memory_commit(&mut tree, &args).unwrap();
1830
1831        assert_eq!(tree.memory_len(), 1);
1832        let root = tree.get(&tree.root_ids()[0]).unwrap();
1833        assert_eq!(root.storage.as_deref(), Some("用户喜欢炸酱面"));
1834        assert_eq!(root.types.as_deref(), Some("用户喜欢吃的面食"));
1835    }
1836
1837    #[test]
1838    fn applies_update_parent_and_insert_child_memory_commit() {
1839        let mut tree = otherone_memory::MemoryTree::new();
1840        let parent_id = tree
1841            .insert_root("用户喜欢炸酱面", "用户喜欢吃的面食")
1842            .unwrap();
1843        let args = LongTermMemoryCommitArgs {
1844            action: "update_parent_and_insert_child".to_string(),
1845            storage: Some("用户喜欢糖醋排骨".to_string()),
1846            types: Some("用户喜欢吃的菜".to_string()),
1847            parent_id: Some(parent_id.clone()),
1848            target_id: None,
1849            parent_storage: Some("用户喜欢炸酱面和糖醋排骨".to_string()),
1850            parent_types: Some("用户喜欢吃的食物".to_string()),
1851        };
1852
1853        apply_long_term_memory_commit(&mut tree, &args).unwrap();
1854
1855        let parent = tree.get(&parent_id).unwrap();
1856        assert_eq!(parent.storage.as_deref(), Some("用户喜欢炸酱面和糖醋排骨"));
1857        assert_eq!(parent.types.as_deref(), Some("用户喜欢吃的食物"));
1858
1859        let child_ids = tree.child_ids(&parent_id).unwrap();
1860        assert_eq!(child_ids.len(), 1);
1861        let child = tree.get(&child_ids[0]).unwrap();
1862        assert_eq!(child.storage.as_deref(), Some("用户喜欢糖醋排骨"));
1863        assert_eq!(child.types.as_deref(), Some("用户喜欢吃的菜"));
1864    }
1865
1866    #[test]
1867    fn update_parent_and_insert_child_preserves_old_specific_parent_storage() {
1868        let mut tree = otherone_memory::MemoryTree::new();
1869        let parent_id = tree
1870            .insert_root("用户喜欢吃炸酱面", "用户喜欢吃的面食")
1871            .unwrap();
1872        let args = LongTermMemoryCommitArgs {
1873            action: "update_parent_and_insert_child".to_string(),
1874            storage: Some("用户不喜欢吃香菜".to_string()),
1875            types: Some("用户不喜欢的食材".to_string()),
1876            parent_id: Some(parent_id.clone()),
1877            target_id: None,
1878            parent_storage: Some("用户有关于食物的喜好和禁忌信息".to_string()),
1879            parent_types: Some("用户的食物偏好与限制".to_string()),
1880        };
1881
1882        apply_long_term_memory_commit(&mut tree, &args).unwrap();
1883
1884        let parent = tree.get(&parent_id).unwrap();
1885        assert_eq!(
1886            parent.storage.as_deref(),
1887            Some("用户有关于食物的喜好和禁忌信息")
1888        );
1889
1890        let child_storages = tree
1891            .children(&parent_id)
1892            .unwrap()
1893            .into_iter()
1894            .filter_map(|point| point.storage.as_deref())
1895            .collect::<Vec<_>>();
1896        assert!(child_storages.contains(&"用户喜欢吃炸酱面"));
1897        assert!(child_storages.contains(&"用户不喜欢吃香菜"));
1898    }
1899
1900    #[test]
1901    fn applies_update_existing_memory_commit() {
1902        let mut tree = otherone_memory::MemoryTree::new();
1903        let point_id = tree.insert_root("用户喜欢面食", "饮食偏好").unwrap();
1904        let args = LongTermMemoryCommitArgs {
1905            action: "update_existing".to_string(),
1906            storage: Some("用户喜欢炸酱面等面食".to_string()),
1907            types: Some("用户喜欢吃的面食".to_string()),
1908            parent_id: None,
1909            target_id: Some(point_id.clone()),
1910            parent_storage: None,
1911            parent_types: None,
1912        };
1913
1914        apply_long_term_memory_commit(&mut tree, &args).unwrap();
1915
1916        let point = tree.get(&point_id).unwrap();
1917        assert_eq!(point.storage.as_deref(), Some("用户喜欢炸酱面等面食"));
1918        assert_eq!(point.types.as_deref(), Some("用户喜欢吃的面食"));
1919    }
1920
1921    #[test]
1922    fn applies_ignore_memory_commit_without_changing_tree() {
1923        let mut tree = otherone_memory::MemoryTree::new();
1924        let before = tree.to_points();
1925        let args = LongTermMemoryCommitArgs {
1926            action: "ignore".to_string(),
1927            storage: Some("验证码是 123456".to_string()),
1928            types: Some("验证码".to_string()),
1929            parent_id: None,
1930            target_id: None,
1931            parent_storage: None,
1932            parent_types: None,
1933        };
1934
1935        apply_long_term_memory_commit(&mut tree, &args).unwrap();
1936
1937        assert_eq!(tree.memory_len(), 0);
1938        assert_eq!(tree.to_points().len(), before.len());
1939    }
1940
1941    #[test]
1942    fn heuristic_fallback_detects_durable_memory_cues() {
1943        assert!(should_queue_long_term_memory_fallback(
1944            "我叫林澈,这是可以长期记住的称呼。"
1945        ));
1946        assert!(should_queue_long_term_memory_fallback(
1947            "我长期不吃辣,点餐时可以参考。"
1948        ));
1949        assert!(should_queue_long_term_memory_fallback(
1950            "以后和我交流默认用中文。"
1951        ));
1952    }
1953
1954    #[test]
1955    fn heuristic_fallback_rejects_temporary_and_sensitive_cues() {
1956        assert!(!should_queue_long_term_memory_fallback(
1957            "这次当前页面的按钮先临时改成红色。"
1958        ));
1959        assert!(!should_queue_long_term_memory_fallback(
1960            "我的银行卡验证码是 123456,这个不要长期记住。"
1961        ));
1962        assert!(!should_queue_long_term_memory_fallback(
1963            "把“今天项目进展顺利”翻译成英文。"
1964        ));
1965        assert!(!should_queue_long_term_memory_fallback(
1966            "再次提醒,我喜欢吃炸酱面。如果已经记住就不用新增重复节点。"
1967        ));
1968    }
1969
1970    #[test]
1971    fn merge_tool_call_delta_uses_index_for_streamed_arguments() {
1972        let mut calls = Vec::new();
1973
1974        merge_tool_call_delta(
1975            &mut calls,
1976            &otherone_ai::types::ToolCall {
1977                index: Some(0),
1978                id: "call_1".to_string(),
1979                call_type: "function".to_string(),
1980                function: otherone_ai::types::FunctionCall {
1981                    name: "read_file".to_string(),
1982                    arguments: String::new(),
1983                },
1984            },
1985        );
1986        merge_tool_call_delta(
1987            &mut calls,
1988            &otherone_ai::types::ToolCall {
1989                index: Some(0),
1990                id: String::new(),
1991                call_type: "function".to_string(),
1992                function: otherone_ai::types::FunctionCall {
1993                    name: String::new(),
1994                    arguments: "{\"path\":\"Cargo".to_string(),
1995                },
1996            },
1997        );
1998        merge_tool_call_delta(
1999            &mut calls,
2000            &otherone_ai::types::ToolCall {
2001                index: Some(0),
2002                id: String::new(),
2003                call_type: "function".to_string(),
2004                function: otherone_ai::types::FunctionCall {
2005                    name: String::new(),
2006                    arguments: ".toml\"}".to_string(),
2007                },
2008            },
2009        );
2010
2011        assert_eq!(calls.len(), 1);
2012        assert_eq!(calls[0].id, "call_1");
2013        assert_eq!(calls[0].function.name, "read_file");
2014        assert_eq!(calls[0].function.arguments, "{\"path\":\"Cargo.toml\"}");
2015    }
2016}