1pub 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
145pub 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
158pub 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
262async 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 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 let tools = otherone_tools::combine_tools(ai.tools.clone());
317 ai.tools = tools;
318
319 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 let mut ai_config = build_ai_config(ai, &messages);
343 ai_config["stream"] = serde_json::json!(true);
344
345 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 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 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 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 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 if let Some(ref tc) = delta.tool_calls {
413 for tool_call in tc {
414 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 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 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 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 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 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
637pub 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 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 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 let tools = otherone_tools::combine_tools(ai.tools.clone());
699 ai.tools = tools;
700
701 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 let ai_config = build_ai_config(ai, &messages);
725
726 let response =
728 otherone_ai::invoke_model(ai.provider.clone(), &ai.api_key, &ai.base_url, ai_config)
729 .await?;
730
731 let mut parsed =
733 parse_ai_response(&response, &ai.provider).map_err(|e| AgentError::ContextError(e))?;
734
735 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 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 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
838fn 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}