terraphim_multi_agent 1.0.0

Multi-agent system for Terraphim built on roles with rust-genai integration
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
use chrono::Utc;
use terraphim_multi_agent::{test_utils::*, *};

fn ollama_available() -> bool {
    std::env::var("RUN_OLLAMA_TESTS").ok().as_deref() == Some("1")
}

#[tokio::test]
async fn test_context_item_creation() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    let item = ContextItem::new(
        ContextItemType::Memory,
        "User prefers functional programming".to_string(),
        25, // token count
        0.85,
    )
    .with_metadata(ContextMetadata {
        source: Some("user_preference".to_string()),
        ..Default::default()
    });

    let _ = context.add_item(item.clone());

    assert_eq!(context.items.len(), 1);
    assert_eq!(context.items[0].content, item.content);
    assert_eq!(context.items[0].relevance_score, item.relevance_score);
    assert_eq!(context.items[0].item_type, item.item_type);
    assert_eq!(
        context.items[0].metadata.source,
        Some("user_preference".to_string())
    );
}

#[tokio::test]
async fn test_context_relevance_filtering() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    // Add items with different relevance scores
    let items = vec![
        ("High relevance item", 0.95),
        ("Medium relevance item", 0.65),
        ("Low relevance item", 0.25),
        ("Very high relevance item", 0.98),
        ("Below threshold item", 0.15),
    ];

    for (content, score) in items {
        context
            .add_item(ContextItem::new(
                ContextItemType::Memory,
                content.to_string(),
                20, // token count
                score,
            ))
            .unwrap();
    }

    // Test filtering with threshold 0.5
    let relevant_items = context.get_items_by_relevance(0.5, None);
    assert_eq!(
        relevant_items.len(),
        3,
        "Should return 3 items above 0.5 threshold"
    );

    // Should be sorted by relevance (highest first)
    assert!(relevant_items[0].relevance_score >= relevant_items[1].relevance_score);
    assert!(relevant_items[1].relevance_score >= relevant_items[2].relevance_score);

    // Test with limit
    let limited_items = context.get_items_by_relevance(0.5, Some(2));
    assert_eq!(limited_items.len(), 2, "Should respect limit parameter");

    // Should still be highest relevance items
    assert_eq!(limited_items[0].content, "Very high relevance item");
    assert_eq!(limited_items[1].content, "High relevance item");
}

#[tokio::test]
async fn test_context_different_item_types() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    // Add different types of context items
    let items = vec![
        (ContextItemType::Memory, "Remembered fact", 0.8),
        (ContextItemType::Task, "Current task", 0.9),
        (ContextItemType::Document, "Relevant document", 0.7),
        (ContextItemType::Lesson, "Learned lesson", 0.6),
    ];

    for (item_type, content, score) in items {
        context
            .add_item(ContextItem::new(
                item_type,
                content.to_string(),
                15, // token count
                score,
            ))
            .unwrap();
    }

    assert_eq!(context.items.len(), 4);

    // Verify all types are present
    let memory_count = context
        .items
        .iter()
        .filter(|i| matches!(i.item_type, ContextItemType::Memory))
        .count();
    let task_count = context
        .items
        .iter()
        .filter(|i| matches!(i.item_type, ContextItemType::Task))
        .count();
    let doc_count = context
        .items
        .iter()
        .filter(|i| matches!(i.item_type, ContextItemType::Document))
        .count();
    let lesson_count = context
        .items
        .iter()
        .filter(|i| matches!(i.item_type, ContextItemType::Lesson))
        .count();

    assert_eq!(memory_count, 1);
    assert_eq!(task_count, 1);
    assert_eq!(doc_count, 1);
    assert_eq!(lesson_count, 1);
}

#[tokio::test]
async fn test_context_automatic_enrichment() {
    if !ollama_available() {
        eprintln!("Skipping: set RUN_OLLAMA_TESTS=1 and ensure Ollama has model gemma3:270m");
        return;
    }
    let agent = create_test_agent().await.unwrap();
    agent.initialize().await.unwrap();

    // First, add some context manually
    {
        let mut context = agent.context.write().await;
        context
            .add_item(ContextItem::new(
                ContextItemType::Memory,
                "User is working on Rust web development".to_string(),
                30, // token count
                0.9,
            ))
            .unwrap();
    }

    // Process a command - this should use the context
    let input = CommandInput::new(
        "Create a web API endpoint".to_string(),
        CommandType::Generate,
    );
    let result = agent.process_command(input).await;
    assert!(result.is_ok());

    let output = result.unwrap();

    // Verify context was used - context info is now in metadata
    // Note: CommandOutput doesn't have context_used field anymore
    // Context is tracked via the context management system separately
    assert!(!output.text.is_empty(), "Should have generated output");
    assert!(
        !output.metadata.is_empty() || !output.sources.is_empty(),
        "Should have metadata or sources"
    );
}

#[tokio::test]
async fn test_context_token_aware_truncation() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    // Add many context items to test truncation
    for i in 0..20 {
        context
            .add_item(ContextItem::new(
                ContextItemType::Memory,
                format!(
                    "Context item {} with detailed information that takes up tokens",
                    i
                ),
                50,                      // token count
                0.8 - (i as f64 * 0.01), // Decreasing relevance
            ))
            .unwrap();
    }

    // Test with different token limits
    let items_100 = context.get_items_by_relevance(0.0, Some(5));
    assert_eq!(
        items_100.len(),
        5,
        "Should respect limit even with many items"
    );

    // Should prioritize highest relevance
    for i in 1..items_100.len() {
        assert!(items_100[i - 1].relevance_score >= items_100[i].relevance_score);
    }
}

#[tokio::test]
async fn test_context_update_and_cleanup() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    // Add some items
    let current_task = ContextItem::new(
        ContextItemType::Task,
        "Current task".to_string(),
        20, // token count
        0.9,
    );
    context.add_item(current_task).unwrap();

    let old_memory = ContextItem::new(
        ContextItemType::Memory,
        "Old memory".to_string(),
        15, // token count
        0.3,
    );
    context.add_item(old_memory).unwrap();

    assert_eq!(context.items.len(), 2);

    // Test clearing items below threshold
    context.items.retain(|item| item.relevance_score >= 0.5);
    assert_eq!(context.items.len(), 1);
    assert_eq!(context.items[0].content, "Current task");
}

#[tokio::test]
async fn test_context_metadata_handling() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    let metadata = ContextMetadata {
        source: Some("knowledge_graph".to_string()),
        tags: vec!["technical".to_string(), "high_confidence".to_string()],
        ..Default::default()
    };

    let doc_item = ContextItem::new(
        ContextItemType::Document,
        "Technical documentation excerpt".to_string(),
        40, // token count
        0.85,
    )
    .with_metadata(metadata.clone());

    context.add_item(doc_item).unwrap();

    assert_eq!(context.items.len(), 1);
    let item = &context.items[0];

    assert_eq!(item.metadata.source, Some("knowledge_graph".to_string()));
    assert!(item.metadata.tags.contains(&"technical".to_string()));
    assert!(item.metadata.tags.contains(&"high_confidence".to_string()));
}

#[tokio::test]
async fn test_context_concurrent_access() {
    let agent = create_test_agent().await.unwrap();

    use tokio::task::JoinSet;
    let mut join_set = JoinSet::new();

    // Add context items concurrently
    for i in 0..10 {
        let agent_clone = agent.clone();
        join_set.spawn(async move {
            let mut context = agent_clone.context.write().await;
            context
                .add_item(ContextItem::new(
                    ContextItemType::Memory,
                    format!("Concurrent item {}", i),
                    20, // token count
                    0.7,
                ))
                .unwrap();
        });
    }

    while let Some(result) = join_set.join_next().await {
        result.unwrap();
    }

    let context = agent.context.read().await;
    assert_eq!(
        context.items.len(),
        10,
        "All concurrent additions should succeed"
    );
}

#[tokio::test]
async fn test_context_relevance_scoring() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    // Add items with edge case relevance scores
    let test_scores = [0.0, 0.1, 0.5, 0.99, 1.0];

    for score in test_scores.iter() {
        context
            .add_item(ContextItem::new(
                ContextItemType::Memory,
                format!("Item with score {}", score),
                15, // token count
                *score,
            ))
            .unwrap();
    }

    // Test boundary conditions
    let items_above_zero = context.get_items_by_relevance(0.0, None);
    assert_eq!(
        items_above_zero.len(),
        5,
        "Should include items with score 0.0"
    );

    let items_above_half = context.get_items_by_relevance(0.5, None);
    assert_eq!(
        items_above_half.len(),
        3,
        "Should include items with score >= 0.5"
    );

    let items_above_one = context.get_items_by_relevance(1.0, None);
    assert_eq!(
        items_above_one.len(),
        1,
        "Should include only items with score 1.0"
    );
}

#[tokio::test]
async fn test_context_timestamp_handling() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    let now = Utc::now();
    let one_hour_ago = now - chrono::Duration::hours(1);
    let _one_day_ago = now - chrono::Duration::days(1);

    let recent_memory = ContextItem::new(
        ContextItemType::Memory,
        "Recent memory".to_string(),
        20, // token count
        0.8,
    );
    context.add_item(recent_memory).unwrap();

    let recent_task = ContextItem::new(
        ContextItemType::Task,
        "Recent task".to_string(),
        18, // token count
        0.8,
    );
    context.add_item(recent_task).unwrap();

    let old_doc = ContextItem::new(
        ContextItemType::Document,
        "Old document".to_string(),
        22, // token count
        0.8,
    );
    context.add_item(old_doc).unwrap();

    assert_eq!(context.items.len(), 3);

    // Verify timestamps are preserved (Note: timestamps are set automatically in ContextItem::new)
    let recent_items: Vec<_> = context
        .items
        .iter()
        .filter(|item| item.added_at > one_hour_ago)
        .collect();
    // All items will be recent since they were just created
    assert!(recent_items.len() >= 2, "Should find recent items");
}

// Test removed - API has changed and test uses outdated memory API
// TODO: Rewrite test with current MemoryItem API when needed

#[tokio::test]
async fn test_context_threshold_configuration() {
    let agent = create_test_agent().await.unwrap();

    let mut context = agent.context.write().await;

    // Note: relevance_threshold is not a field in AgentContext anymore
    // We'll use the get_items_by_relevance method with threshold parameter directly

    let high_relevance = ContextItem::new(
        ContextItemType::Memory,
        "High relevance".to_string(),
        15, // token count
        0.8,
    );
    context.add_item(high_relevance).unwrap();

    let low_relevance = ContextItem::new(
        ContextItemType::Memory,
        "Low relevance".to_string(),
        12, // token count
        0.6,
    );
    context.add_item(low_relevance).unwrap();

    // Using threshold 0.7
    let relevant = context.get_items_by_relevance(0.7, None);
    assert_eq!(relevant.len(), 1, "Should use threshold 0.7");
    assert_eq!(relevant[0].content, "High relevance");
}