subcog/mcp/tool_types.rs
1//! Argument types and helper functions for MCP tools.
2//!
3//! Extracted from `tools.rs` to reduce file size.
4//!
5//! # Security
6//!
7//! All argument types use `#[serde(deny_unknown_fields)]` to prevent
8//! parameter pollution attacks where attackers inject unexpected fields
9//! that could bypass validation or trigger unintended behavior.
10
11use crate::models::{DetailLevel, MemoryStatus, Namespace, SearchFilter, SearchMode};
12use crate::storage::index::DomainScope;
13use serde::Deserialize;
14use std::collections::HashMap;
15
16/// Arguments for the capture tool.
17#[derive(Debug, Deserialize)]
18#[serde(deny_unknown_fields)]
19pub struct CaptureArgs {
20 /// The memory content to capture.
21 pub content: String,
22 /// Memory category (e.g., "decisions", "patterns", "learnings").
23 pub namespace: String,
24 /// Optional tags for categorization and filtering.
25 pub tags: Option<Vec<String>>,
26 /// Optional source reference (file path, URL, etc.).
27 pub source: Option<String>,
28 /// Optional TTL (time-to-live) for automatic expiration.
29 /// Supports duration strings like "7d", "30d", "24h", "60m", or seconds.
30 /// Use "0" for no expiration (default behavior).
31 pub ttl: Option<String>,
32 /// Storage scope: "project" (default), "user", or "org".
33 /// - "project": Stored with project context (requires git repo)
34 /// - "user": Stored globally for user across all projects
35 /// - "org": Stored in organization-shared index
36 pub domain: Option<String>,
37}
38
39/// Arguments for the recall tool.
40///
41/// When `query` is omitted or empty, behaves like `subcog_list` and returns
42/// all memories matching the filter criteria (with pagination support).
43#[derive(Debug, Deserialize)]
44#[serde(deny_unknown_fields)]
45pub struct RecallArgs {
46 /// Search query text. If omitted or empty, lists all memories (like `subcog_list`).
47 pub query: Option<String>,
48 /// GitHub-style filter query (e.g., "ns:decisions tag:rust -tag:test since:7d").
49 pub filter: Option<String>,
50 /// Filter by namespace (deprecated: use `filter` instead).
51 pub namespace: Option<String>,
52 /// Search mode: "hybrid" (default), "vector", or "text".
53 pub mode: Option<String>,
54 /// Detail level: "light", "medium" (default), or "everything".
55 pub detail: Option<String>,
56 /// Maximum number of results to return (default: 10 for search, 50 for list).
57 pub limit: Option<usize>,
58 /// Entity filter: filter to memories mentioning these entities (comma-separated for OR logic).
59 pub entity: Option<String>,
60 /// Offset for pagination (default: 0). Used when listing without query.
61 /// Note: Reserved for future pagination support in `list_all()`.
62 #[allow(dead_code)]
63 pub offset: Option<usize>,
64 /// Filter by user ID (for multi-tenant scoping).
65 pub user_id: Option<String>,
66 /// Filter by agent ID (for multi-agent scoping).
67 pub agent_id: Option<String>,
68}
69
70/// Arguments for the consolidate tool.
71#[derive(Debug, Deserialize)]
72#[serde(deny_unknown_fields)]
73pub struct ConsolidateArgs {
74 /// Namespaces to consolidate (optional, defaults to all).
75 pub namespaces: Option<Vec<String>>,
76 /// Time window in days (optional).
77 pub days: Option<u32>,
78 /// If true, show what would be consolidated without making changes.
79 pub dry_run: Option<bool>,
80 /// Minimum number of memories required to form a group (optional).
81 pub min_memories: Option<usize>,
82 /// Similarity threshold 0.0-1.0 for grouping memories (optional).
83 pub similarity: Option<f32>,
84}
85
86/// Arguments for the get summary tool.
87#[derive(Debug, Deserialize)]
88#[serde(deny_unknown_fields)]
89pub struct GetSummaryArgs {
90 /// ID of the summary memory to retrieve.
91 pub memory_id: String,
92}
93
94/// Arguments for the enrich tool.
95#[derive(Debug, Deserialize)]
96#[serde(deny_unknown_fields)]
97pub struct EnrichArgs {
98 /// ID of the memory to enrich.
99 pub memory_id: String,
100 /// Generate or improve tags (default: true).
101 pub enrich_tags: Option<bool>,
102 /// Restructure content for clarity (default: true).
103 pub enrich_structure: Option<bool>,
104 /// Add inferred context and rationale (default: false).
105 pub add_context: Option<bool>,
106}
107
108/// Arguments for the reindex tool.
109#[derive(Debug, Deserialize)]
110#[serde(deny_unknown_fields)]
111pub struct ReindexArgs {
112 /// Path to git repository (default: current directory).
113 pub repo_path: Option<String>,
114}
115
116// ============================================================================
117// Core CRUD Tool Arguments (Industry Parity: Mem0, Zep, LangMem)
118// ============================================================================
119
120/// Arguments for the get tool.
121///
122/// Direct memory retrieval by ID - a fundamental CRUD operation
123/// present in all major memory systems (Mem0 `get`, Zep `get_memory`).
124#[derive(Debug, Deserialize)]
125#[serde(deny_unknown_fields)]
126pub struct GetArgs {
127 /// Memory ID to retrieve.
128 pub memory_id: String,
129}
130
131/// Arguments for the delete tool.
132///
133/// Soft/hard delete capability matching industry patterns.
134/// Defaults to soft delete (tombstone) for safety, with explicit
135/// hard delete option for permanent removal.
136#[derive(Debug, Deserialize)]
137#[serde(deny_unknown_fields)]
138pub struct DeleteArgs {
139 /// Memory ID to delete.
140 pub memory_id: String,
141 /// If true, permanently delete. If false (default), soft delete (tombstone).
142 #[serde(default)]
143 pub hard: bool,
144}
145
146/// Arguments for the update tool.
147///
148/// Allows updating content and/or tags of an existing memory.
149/// Follows the industry pattern of partial updates (Mem0 `update`,
150/// `LangMem` `update`).
151#[derive(Debug, Deserialize)]
152#[serde(deny_unknown_fields)]
153pub struct UpdateArgs {
154 /// Memory ID to update.
155 pub memory_id: String,
156 /// New content (optional - if not provided, content unchanged).
157 pub content: Option<String>,
158 /// New tags (optional - if not provided, tags unchanged).
159 /// Replaces existing tags entirely when provided.
160 pub tags: Option<Vec<String>>,
161}
162
163/// Arguments for the list tool.
164///
165/// Lists all memories with optional filtering and pagination.
166/// Matches Mem0's `get_all()` and Zep's `list_memories()` patterns.
167#[derive(Debug, Deserialize)]
168#[serde(deny_unknown_fields)]
169pub struct ListArgs {
170 /// GitHub-style filter query (e.g., "ns:decisions tag:rust -tag:test").
171 pub filter: Option<String>,
172 /// Maximum number of results to return (default: 50).
173 pub limit: Option<usize>,
174 /// Offset for pagination (default: 0).
175 pub offset: Option<usize>,
176 /// Filter by user ID (flexible scoping via metadata).
177 pub user_id: Option<String>,
178 /// Filter by agent ID (flexible scoping via metadata).
179 pub agent_id: Option<String>,
180}
181
182/// Arguments for the `delete_all` tool.
183///
184/// Bulk delete memories matching filter criteria.
185/// Implements Mem0's `delete_all()` pattern with dry-run safety.
186#[derive(Debug, Deserialize)]
187#[serde(deny_unknown_fields)]
188pub struct DeleteAllArgs {
189 /// GitHub-style filter query (e.g., "ns:decisions tag:deprecated").
190 /// At least one filter criterion is required for safety.
191 pub filter: Option<String>,
192 /// If true, show what would be deleted without making changes (default: true).
193 #[serde(default = "default_true")]
194 pub dry_run: bool,
195 /// If true, permanently delete. If false (default), soft delete (tombstone).
196 #[serde(default)]
197 pub hard: bool,
198 /// Filter by user ID for scoped deletion.
199 pub user_id: Option<String>,
200}
201
202/// Arguments for the restore tool.
203///
204/// Restores a tombstoned (soft-deleted) memory.
205/// Implements the inverse of soft delete for data recovery.
206#[derive(Debug, Deserialize)]
207#[serde(deny_unknown_fields)]
208pub struct RestoreArgs {
209 /// Memory ID to restore.
210 pub memory_id: String,
211}
212
213/// Arguments for the history tool.
214///
215/// Retrieves change history for a memory by querying the event log.
216/// Provides audit trail visibility without storing full version snapshots.
217#[derive(Debug, Deserialize)]
218#[serde(deny_unknown_fields)]
219pub struct HistoryArgs {
220 /// Memory ID to get history for.
221 pub memory_id: String,
222 /// Maximum number of events to return (default: 20).
223 pub limit: Option<usize>,
224}
225
226/// Default value helper for `dry_run` (defaults to true for safety).
227const fn default_true() -> bool {
228 true
229}
230
231// ============================================================================
232// Graph / Knowledge Graph Tool Arguments
233// ============================================================================
234
235/// Arguments for the entities tool.
236///
237/// Provides CRUD and advanced operations for entities in the knowledge graph.
238/// Actions: create, get, list, delete, extract, merge.
239#[derive(Debug, Deserialize)]
240#[serde(deny_unknown_fields)]
241pub struct EntitiesArgs {
242 /// Operation to perform: create, get, list, delete, extract, merge.
243 pub action: String,
244 /// Entity ID (required for get/delete, `find_duplicates` sub-action of merge).
245 pub entity_id: Option<String>,
246 /// Entity name (required for create).
247 pub name: Option<String>,
248 /// Type of entity: Person, Organization, Technology, Concept, File.
249 pub entity_type: Option<String>,
250 /// Alternative names for the entity.
251 pub aliases: Option<Vec<String>>,
252 /// Maximum results for list operation (default: 20).
253 pub limit: Option<usize>,
254 // --- Fields for `extract` action ---
255 /// Text content to extract entities from (for extract action).
256 pub content: Option<String>,
257 /// Whether to store extracted entities in the graph (for extract/merge, default: false).
258 #[serde(default)]
259 pub store: bool,
260 /// Optional memory ID to link extracted entities to (for extract action).
261 pub memory_id: Option<String>,
262 /// Minimum confidence threshold 0.0-1.0 (for extract action, default: 0.5).
263 pub min_confidence: Option<f32>,
264 // --- Fields for `merge` action ---
265 /// Sub-action for merge: `find_duplicates`, `merge` (for merge action).
266 pub merge_action: Option<String>,
267 /// Entity IDs to merge (for merge action, minimum 2).
268 pub entity_ids: Option<Vec<String>>,
269 /// Name for the merged entity (for merge action).
270 pub canonical_name: Option<String>,
271 /// Similarity threshold for finding duplicates 0.0-1.0 (for merge action, default: 0.7).
272 pub threshold: Option<f32>,
273}
274
275/// Arguments for the relationships tool.
276///
277/// Provides CRUD and inference operations for relationships between entities.
278/// Actions: create, get, list, delete, infer.
279#[derive(Debug, Deserialize)]
280#[serde(deny_unknown_fields)]
281pub struct RelationshipsArgs {
282 /// Operation to perform: create, get, list, delete, infer.
283 pub action: String,
284 /// Source entity ID (required for create).
285 pub from_entity: Option<String>,
286 /// Target entity ID (required for create).
287 pub to_entity: Option<String>,
288 /// Type of relationship.
289 pub relationship_type: Option<String>,
290 /// Entity ID to get relationships for (for get/list).
291 pub entity_id: Option<String>,
292 /// Relationship direction: outgoing, incoming, both.
293 pub direction: Option<String>,
294 /// Maximum results (default: 20).
295 pub limit: Option<usize>,
296 // --- Fields for `infer` action ---
297 /// Entity IDs to analyze for relationships (for infer action).
298 pub entity_ids: Option<Vec<String>>,
299 /// Whether to store inferred relationships (for infer action, default: false).
300 #[serde(default)]
301 pub store: bool,
302 /// Minimum confidence threshold 0.0-1.0 (for infer action, default: 0.6).
303 pub min_confidence: Option<f32>,
304}
305
306/// Arguments for the graph query tool.
307///
308/// Enables graph traversal operations.
309#[derive(Debug, Deserialize)]
310#[serde(deny_unknown_fields)]
311pub struct GraphQueryArgs {
312 /// Query operation: neighbors, path, stats.
313 pub operation: String,
314 /// Starting entity ID (required for neighbors).
315 pub entity_id: Option<String>,
316 /// Source entity ID (required for path).
317 pub from_entity: Option<String>,
318 /// Target entity ID (required for path).
319 pub to_entity: Option<String>,
320 /// Traversal depth (default: 2, max: 5).
321 pub depth: Option<usize>,
322}
323
324/// Arguments for the extract entities tool.
325///
326/// Extracts entities and relationships from text using LLM.
327#[derive(Debug, Deserialize)]
328#[serde(deny_unknown_fields)]
329pub struct ExtractEntitiesArgs {
330 /// Text content to extract entities from.
331 pub content: String,
332 /// Whether to store extracted entities in the graph (default: false).
333 #[serde(default)]
334 pub store: bool,
335 /// Optional memory ID to link extracted entities to.
336 pub memory_id: Option<String>,
337 /// Minimum confidence threshold (0.0-1.0, default: 0.5).
338 pub min_confidence: Option<f32>,
339}
340
341/// Arguments for the entity merge tool.
342///
343/// Merges duplicate entities into a single canonical entity.
344#[derive(Debug, Deserialize)]
345#[serde(deny_unknown_fields)]
346pub struct EntityMergeArgs {
347 /// Operation: `find_duplicates`, `merge`.
348 pub action: String,
349 /// Entity ID to find duplicates for (for `find_duplicates`).
350 pub entity_id: Option<String>,
351 /// Entity IDs to merge (for merge, minimum 2).
352 pub entity_ids: Option<Vec<String>>,
353 /// Name for the merged entity (required for merge).
354 pub canonical_name: Option<String>,
355 /// Similarity threshold for finding duplicates (0.0-1.0, default: 0.7).
356 pub threshold: Option<f32>,
357}
358
359/// Arguments for the relationship inference tool.
360///
361/// Infers implicit relationships between entities using LLM.
362#[derive(Debug, Deserialize)]
363#[serde(deny_unknown_fields)]
364pub struct RelationshipInferArgs {
365 /// Entity IDs to analyze for relationships.
366 pub entity_ids: Option<Vec<String>>,
367 /// Whether to store inferred relationships (default: false).
368 #[serde(default)]
369 pub store: bool,
370 /// Minimum confidence threshold (0.0-1.0, default: 0.6).
371 pub min_confidence: Option<f32>,
372 /// Maximum entities to analyze if `entity_ids` not provided (default: 50).
373 pub limit: Option<usize>,
374}
375
376/// Arguments for the graph visualize tool.
377///
378/// Generates visual representations of the knowledge graph.
379#[derive(Debug, Deserialize)]
380#[serde(deny_unknown_fields)]
381pub struct GraphVisualizeArgs {
382 /// Output format: mermaid, dot, ascii.
383 pub format: Option<String>,
384 /// Center visualization on this entity.
385 pub entity_id: Option<String>,
386 /// Depth of relationships to include (default: 2).
387 pub depth: Option<usize>,
388 /// Filter to specific entity types.
389 pub entity_types: Option<Vec<String>>,
390 /// Filter to specific relationship types.
391 pub relationship_types: Option<Vec<String>>,
392 /// Maximum entities to include (default: 50).
393 pub limit: Option<usize>,
394}
395
396/// Arguments for the consolidated `subcog_graph` tool.
397///
398/// Combines graph query and visualization operations.
399/// Operations: neighbors, path, stats, visualize.
400#[derive(Debug, Deserialize)]
401#[serde(deny_unknown_fields)]
402pub struct GraphArgs {
403 /// Operation to perform: neighbors, path, stats, visualize.
404 pub operation: String,
405 /// Starting entity ID (required for neighbors, optional for visualize).
406 pub entity_id: Option<String>,
407 /// Source entity ID (required for path).
408 pub from_entity: Option<String>,
409 /// Target entity ID (required for path).
410 pub to_entity: Option<String>,
411 /// Traversal depth (default: 2, max: 5).
412 pub depth: Option<usize>,
413 // --- Fields for `visualize` operation ---
414 /// Output format for visualize: mermaid, dot, ascii (default: mermaid).
415 pub format: Option<String>,
416 /// Filter to specific entity types (for visualize).
417 pub entity_types: Option<Vec<String>>,
418 /// Filter to specific relationship types (for visualize).
419 pub relationship_types: Option<Vec<String>>,
420 /// Maximum entities to include (default: 50, for visualize).
421 pub limit: Option<usize>,
422}
423
424/// Parses an entity type string to `EntityType` enum.
425pub fn parse_entity_type(s: &str) -> Option<crate::models::graph::EntityType> {
426 use crate::models::graph::EntityType;
427 match s.to_lowercase().as_str() {
428 "person" => Some(EntityType::Person),
429 "organization" => Some(EntityType::Organization),
430 "technology" => Some(EntityType::Technology),
431 "concept" => Some(EntityType::Concept),
432 "file" => Some(EntityType::File),
433 _ => None,
434 }
435}
436
437/// Parses a relationship type string to `RelationshipType` enum.
438pub fn parse_relationship_type(s: &str) -> Option<crate::models::graph::RelationshipType> {
439 use crate::models::graph::RelationshipType;
440 match s {
441 "WorksAt" | "works_at" => Some(RelationshipType::WorksAt),
442 "Created" | "created" => Some(RelationshipType::Created),
443 "Uses" | "uses" => Some(RelationshipType::Uses),
444 "Implements" | "implements" => Some(RelationshipType::Implements),
445 "PartOf" | "part_of" => Some(RelationshipType::PartOf),
446 "RelatesTo" | "relates_to" => Some(RelationshipType::RelatesTo),
447 "MentionedIn" | "mentioned_in" => Some(RelationshipType::MentionedIn),
448 "Supersedes" | "supersedes" => Some(RelationshipType::Supersedes),
449 "ConflictsWith" | "conflicts_with" => Some(RelationshipType::ConflictsWith),
450 _ => None,
451 }
452}
453
454// ============================================================================
455// Prompt Tool Arguments (Consolidated)
456// ============================================================================
457
458/// Arguments for the consolidated `subcog_prompts` tool.
459///
460/// Supports all prompt operations via the `action` field:
461/// - `save`: Save or update a prompt template
462/// - `list`: List prompts with optional filtering
463/// - `get`: Get a prompt by name
464/// - `run`: Execute a prompt with variable substitution
465/// - `delete`: Delete a prompt
466#[derive(Debug, Deserialize)]
467#[serde(deny_unknown_fields)]
468pub struct PromptsArgs {
469 /// Operation to perform: save, list, get, run, delete.
470 pub action: String,
471 /// Prompt name (required for save/get/run/delete).
472 pub name: Option<String>,
473 /// Prompt content with `{{variable}}` placeholders (for save).
474 pub content: Option<String>,
475 /// Path to file containing prompt (alternative to content, for save).
476 pub file_path: Option<String>,
477 /// Human-readable description of the prompt (for save).
478 pub description: Option<String>,
479 /// Tags for categorization and search (for save/list).
480 pub tags: Option<Vec<String>>,
481 /// Storage scope: "project" (default), "user", or "org".
482 pub domain: Option<String>,
483 /// Explicit variable definitions with metadata (for save).
484 pub variables_def: Option<Vec<PromptVariableArg>>,
485 /// Variable values to substitute (for run).
486 pub variables: Option<HashMap<String, String>>,
487 /// Skip LLM-powered metadata enrichment (for save).
488 #[serde(default)]
489 pub skip_enrichment: bool,
490 /// Filter by name pattern (glob-style, for list).
491 pub name_pattern: Option<String>,
492 /// Maximum number of results (for list).
493 pub limit: Option<usize>,
494}
495
496/// Variable definition argument for prompt save.
497#[derive(Debug, Deserialize)]
498#[serde(deny_unknown_fields)]
499pub struct PromptVariableArg {
500 /// Variable name (without braces).
501 pub name: String,
502 /// Human-readable description for elicitation.
503 pub description: Option<String>,
504 /// Default value if not provided.
505 pub default: Option<String>,
506 /// Whether variable is required (default: true).
507 pub required: Option<bool>,
508}
509
510// Legacy prompt argument types (for backward compatibility during transition)
511
512/// Arguments for the prompt.save tool (legacy).
513#[derive(Debug, Deserialize)]
514#[serde(deny_unknown_fields)]
515pub struct PromptSaveArgs {
516 /// Unique prompt name (kebab-case, e.g., "code-review").
517 pub name: String,
518 /// Prompt content with `{{variable}}` placeholders.
519 pub content: Option<String>,
520 /// Path to file containing prompt (alternative to content).
521 pub file_path: Option<String>,
522 /// Human-readable description of the prompt.
523 pub description: Option<String>,
524 /// Tags for categorization and search.
525 pub tags: Option<Vec<String>>,
526 /// Storage scope: "project" (default), "user", or "org".
527 pub domain: Option<String>,
528 /// Explicit variable definitions with metadata.
529 pub variables: Option<Vec<PromptVariableArg>>,
530 /// Skip LLM-powered metadata enrichment.
531 #[serde(default)]
532 pub skip_enrichment: bool,
533}
534
535/// Arguments for the prompt.list tool (legacy).
536#[derive(Debug, Deserialize)]
537#[serde(deny_unknown_fields)]
538pub struct PromptListArgs {
539 /// Filter by domain scope: "project", "user", or "org".
540 pub domain: Option<String>,
541 /// Filter by tags (AND logic - must have all).
542 pub tags: Option<Vec<String>>,
543 /// Filter by name pattern (glob-style, e.g., "code-*").
544 pub name_pattern: Option<String>,
545 /// Maximum number of results (default: 20, max: 100).
546 pub limit: Option<usize>,
547}
548
549/// Arguments for the prompt.get tool (legacy).
550#[derive(Debug, Deserialize)]
551#[serde(deny_unknown_fields)]
552pub struct PromptGetArgs {
553 /// Prompt name to retrieve.
554 pub name: String,
555 /// Domain to search (if not specified, searches Project -> User -> Org).
556 pub domain: Option<String>,
557}
558
559/// Arguments for the prompt.run tool (legacy).
560#[derive(Debug, Deserialize)]
561#[serde(deny_unknown_fields)]
562pub struct PromptRunArgs {
563 /// Prompt name to execute.
564 pub name: String,
565 /// Variable values to substitute (key: value pairs).
566 pub variables: Option<HashMap<String, String>>,
567 /// Domain to search for the prompt.
568 pub domain: Option<String>,
569}
570
571/// Arguments for the prompt.delete tool (legacy).
572#[derive(Debug, Deserialize)]
573#[serde(deny_unknown_fields)]
574pub struct PromptDeleteArgs {
575 /// Prompt name to delete.
576 pub name: String,
577 /// Domain scope to delete from (required for safety).
578 pub domain: String,
579}
580
581// =============================================================================
582// Context Template Arguments (Consolidated)
583// =============================================================================
584
585/// Arguments for the consolidated `subcog_templates` tool.
586///
587/// Supports all context template operations via the `action` field:
588/// - `save`: Save or update a context template
589/// - `list`: List templates with optional filtering
590/// - `get`: Get a template by name
591/// - `render`: Render a template with memories
592/// - `delete`: Delete a template
593#[derive(Debug, Deserialize)]
594#[serde(deny_unknown_fields)]
595pub struct TemplatesArgs {
596 /// Operation to perform: save, list, get, render, delete.
597 pub action: String,
598 /// Template name (required for save/get/render/delete).
599 pub name: Option<String>,
600 /// Template content with `{{variable}}` placeholders (for save).
601 pub content: Option<String>,
602 /// Human-readable description of the template (for save).
603 pub description: Option<String>,
604 /// Tags for categorization and search (for save/list).
605 pub tags: Option<Vec<String>>,
606 /// Storage scope: "project" (default), "user", or "org".
607 pub domain: Option<String>,
608 /// Default output format: "markdown" (default), "json", or "xml" (for save).
609 pub output_format: Option<String>,
610 /// Explicit variable definitions with metadata (for save).
611 pub variables_def: Option<Vec<ContextTemplateVariableArg>>,
612 /// Custom variable values (for render).
613 pub variables: Option<std::collections::HashMap<String, String>>,
614 /// Filter by name pattern (for list).
615 pub name_pattern: Option<String>,
616 /// Maximum results (for list) or memories (for render).
617 pub limit: Option<u32>,
618 /// Specific version (for get/render/delete).
619 pub version: Option<u32>,
620 /// Query string for memory search (for render).
621 pub query: Option<String>,
622 /// Namespaces to filter memories (for render).
623 pub namespaces: Option<Vec<String>>,
624 /// Output format override (for render).
625 pub format: Option<String>,
626}
627
628/// Variable definition argument for context template save.
629#[derive(Debug, Deserialize)]
630#[serde(deny_unknown_fields)]
631pub struct ContextTemplateVariableArg {
632 /// Variable name (without `{{}}`).
633 pub name: String,
634 /// Variable description for documentation.
635 pub description: Option<String>,
636 /// Default value if not provided.
637 pub default: Option<String>,
638 /// Whether the variable is required (default: true).
639 pub required: Option<bool>,
640}
641
642// Legacy context template argument types (for backward compatibility)
643
644/// Arguments for the `context_template_save` tool (legacy).
645#[derive(Debug, Deserialize)]
646#[serde(deny_unknown_fields)]
647pub struct ContextTemplateSaveArgs {
648 /// Unique template name (kebab-case, e.g., "search-results").
649 pub name: String,
650 /// Template content with `{{variable}}` placeholders and `{{#each}}` iteration.
651 pub content: String,
652 /// Human-readable description of the template.
653 pub description: Option<String>,
654 /// Tags for categorization and search.
655 pub tags: Option<Vec<String>>,
656 /// Storage scope: "project" (default), "user", or "org".
657 pub domain: Option<String>,
658 /// Default output format: "markdown" (default), "json", or "xml".
659 pub output_format: Option<String>,
660 /// Explicit variable definitions with metadata.
661 pub variables: Option<Vec<ContextTemplateVariableArg>>,
662}
663
664/// Arguments for the `context_template_list` tool (legacy).
665#[derive(Debug, Deserialize)]
666#[serde(deny_unknown_fields)]
667pub struct ContextTemplateListArgs {
668 /// Filter by domain scope: "project", "user", or "org".
669 pub domain: Option<String>,
670 /// Filter by tags (AND logic - must have all).
671 pub tags: Option<Vec<String>>,
672 /// Filter by name pattern (glob-style, e.g., "search-*").
673 pub name_pattern: Option<String>,
674 /// Maximum number of results (default: 20, max: 100).
675 pub limit: Option<usize>,
676}
677
678/// Arguments for the `context_template_get` tool (legacy).
679#[derive(Debug, Deserialize)]
680#[serde(deny_unknown_fields)]
681pub struct ContextTemplateGetArgs {
682 /// Template name to retrieve.
683 pub name: String,
684 /// Specific version to retrieve (None = latest).
685 pub version: Option<u32>,
686 /// Domain to search (if not specified, searches User -> Project).
687 pub domain: Option<String>,
688}
689
690/// Arguments for the `context_template_render` tool (legacy).
691#[derive(Debug, Deserialize)]
692#[serde(deny_unknown_fields)]
693pub struct ContextTemplateRenderArgs {
694 /// Template name to render.
695 pub name: String,
696 /// Specific version to use (None = latest).
697 pub version: Option<u32>,
698 /// Query string for memory search to populate the template.
699 pub query: Option<String>,
700 /// Maximum memories to include (default: 10).
701 pub limit: Option<u32>,
702 /// Namespaces to filter memories (default: all).
703 pub namespaces: Option<Vec<String>>,
704 /// Custom variable values (key: value pairs).
705 pub variables: Option<std::collections::HashMap<String, String>>,
706 /// Output format override: "markdown", "json", or "xml".
707 pub format: Option<String>,
708}
709
710/// Arguments for the `context_template_delete` tool (legacy).
711#[derive(Debug, Deserialize)]
712#[serde(deny_unknown_fields)]
713pub struct ContextTemplateDeleteArgs {
714 /// Template name to delete.
715 pub name: String,
716 /// Specific version to delete (None = delete all versions).
717 pub version: Option<u32>,
718 /// Domain scope to delete from (required for safety).
719 pub domain: String,
720}
721
722/// Arguments for the `subcog_init` tool.
723#[derive(Debug, Deserialize)]
724#[serde(deny_unknown_fields)]
725pub struct InitArgs {
726 /// Whether to recall project context (default: true).
727 #[serde(default = "default_true")]
728 pub include_recall: bool,
729 /// Custom recall query (default: "project setup OR architecture OR conventions").
730 pub recall_query: Option<String>,
731 /// Maximum memories to recall (default: 5).
732 pub recall_limit: Option<u32>,
733}
734
735// =============================================================================
736// Group Management Arguments (Consolidated, Feature-gated: group-scope)
737// =============================================================================
738
739/// Arguments for the consolidated `subcog_groups` tool.
740///
741/// Supports all group management operations via the `action` field:
742/// - `create`: Create a new group
743/// - `list`: List groups you belong to
744/// - `get`: Get group details including members
745/// - `add_member`: Add a member to a group
746/// - `remove_member`: Remove a member from a group
747/// - `update_role`: Update a member's role
748/// - `delete`: Delete a group
749#[cfg(feature = "group-scope")]
750#[derive(Debug, Deserialize)]
751#[serde(deny_unknown_fields)]
752pub struct GroupsArgs {
753 /// Operation to perform: `create`, `list`, `get`, `add_member`, `remove_member`, `update_role`, `delete`.
754 pub action: String,
755 /// Group ID (required for `get`/`add_member`/`remove_member`/`update_role`/`delete`).
756 pub group_id: Option<String>,
757 /// Group name (required for create).
758 pub name: Option<String>,
759 /// Group description (for create).
760 pub description: Option<String>,
761 /// User ID to add/remove/update (for `add_member/remove_member/update_role`).
762 pub user_id: Option<String>,
763 /// Role for the member: read, write, admin (for `add_member/update_role`).
764 pub role: Option<String>,
765}
766
767/// Parses a namespace string to Namespace enum.
768pub fn parse_namespace(s: &str) -> Namespace {
769 match s.to_lowercase().as_str() {
770 "decisions" => Namespace::Decisions,
771 "patterns" => Namespace::Patterns,
772 "learnings" => Namespace::Learnings,
773 "context" => Namespace::Context,
774 "tech-debt" | "techdebt" => Namespace::TechDebt,
775 "apis" => Namespace::Apis,
776 "config" => Namespace::Config,
777 "security" => Namespace::Security,
778 "performance" => Namespace::Performance,
779 "testing" => Namespace::Testing,
780 _ => Namespace::Decisions,
781 }
782}
783
784/// Parses a search mode string to `SearchMode` enum.
785pub fn parse_search_mode(s: &str) -> SearchMode {
786 match s.to_lowercase().as_str() {
787 "vector" => SearchMode::Vector,
788 "text" => SearchMode::Text,
789 _ => SearchMode::Hybrid,
790 }
791}
792
793/// Parses a domain scope string to `DomainScope` enum.
794pub fn parse_domain_scope(s: Option<&str>) -> DomainScope {
795 match s.map(str::to_lowercase).as_deref() {
796 Some("user") => DomainScope::User,
797 Some("org") => DomainScope::Org,
798 _ => DomainScope::Project,
799 }
800}
801
802/// Converts a `DomainScope` to a display string.
803pub const fn domain_scope_to_display(scope: DomainScope) -> &'static str {
804 match scope {
805 DomainScope::Project => "project",
806 DomainScope::User => "user",
807 DomainScope::Org => "org",
808 }
809}
810
811/// Formats a `PromptVariable` for display.
812pub fn format_variable_info(v: &crate::models::PromptVariable) -> String {
813 let mut info = format!("- **{{{{{}}}}}**", v.name);
814 if let Some(ref desc) = v.description {
815 info.push_str(&format!(": {desc}"));
816 }
817 if let Some(ref default) = v.default {
818 info.push_str(&format!(" (default: `{default}`)"));
819 }
820 if !v.required {
821 info.push_str(" [optional]");
822 }
823 info
824}
825
826/// Finds missing required variables.
827pub fn find_missing_required_variables<'a>(
828 variables: &'a [crate::models::PromptVariable],
829 values: &HashMap<String, String>,
830) -> Vec<&'a str> {
831 variables
832 .iter()
833 .filter(|v| v.required && v.default.is_none() && !values.contains_key(&v.name))
834 .map(|v| v.name.as_str())
835 .collect()
836}
837
838/// Finds the largest valid UTF-8 character boundary at or before `index`.
839///
840/// This is an MSRV-compatible implementation of `str::floor_char_boundary`
841/// (stable since Rust 1.80, but we target 1.86 MSRV).
842///
843/// # Arguments
844///
845/// * `s` - The string to find a boundary in.
846/// * `index` - The byte index to search from (will find boundary at or before).
847///
848/// # Returns
849///
850/// The largest valid character boundary at or before `index`, or 0 if none found.
851fn floor_char_boundary(s: &str, index: usize) -> usize {
852 if index >= s.len() {
853 return s.len();
854 }
855
856 // Find the last character boundary at or before index using char_indices.
857 // char_indices() yields (byte_offset, char) for each character.
858 // We want the largest byte_offset <= index.
859 let mut boundary = 0;
860 for (byte_offset, _) in s.char_indices() {
861 if byte_offset <= index {
862 boundary = byte_offset;
863 } else {
864 break;
865 }
866 }
867 boundary
868}
869
870/// Truncates a string to a maximum length, respecting UTF-8 character boundaries.
871///
872/// This function safely handles multi-byte UTF-8 characters (e.g., degree symbol,
873/// emoji, CJK characters) by finding the nearest valid character boundary.
874///
875/// # Arguments
876///
877/// * `s` - The string to truncate.
878/// * `max_len` - Maximum byte length for the result (including "..." suffix).
879///
880/// # Returns
881///
882/// The original string if it fits, otherwise a truncated version with "..." appended.
883///
884/// # Examples
885///
886/// ```ignore
887/// // ASCII text
888/// assert_eq!(truncate("Hello, world!", 10), "Hello, ...");
889///
890/// // Multi-byte UTF-8 characters (degree symbol is 2 bytes)
891/// assert_eq!(truncate("32 °C temperature", 10), "32 °C ...");
892///
893/// // String shorter than max_len
894/// assert_eq!(truncate("short", 100), "short");
895/// ```
896pub fn truncate(s: &str, max_len: usize) -> String {
897 if s.len() <= max_len {
898 return s.to_string();
899 }
900
901 // Reserve 3 bytes for "..."
902 let target_len = max_len.saturating_sub(3);
903
904 // Find the largest valid character boundary <= target_len
905 let boundary = floor_char_boundary(s, target_len);
906
907 format!("{}...", &s[..boundary])
908}
909
910/// Formats content based on detail level.
911pub fn format_content_for_detail(content: &str, detail: DetailLevel) -> String {
912 if content.is_empty() {
913 return String::new();
914 }
915 match detail {
916 DetailLevel::Light => String::new(),
917 DetailLevel::Medium => format!("\n {}", truncate(content, 200)),
918 DetailLevel::Everything => format!("\n {content}"),
919 }
920}
921
922/// Builds a human-readable description of the active filters.
923pub fn build_filter_description(filter: &SearchFilter) -> String {
924 let mut parts = Vec::new();
925
926 if !filter.namespaces.is_empty() {
927 let ns_list: Vec<_> = filter.namespaces.iter().map(Namespace::as_str).collect();
928 parts.push(format!("ns:{}", ns_list.join(",")));
929 }
930
931 if !filter.tags.is_empty() {
932 for tag in &filter.tags {
933 parts.push(format!("tag:{tag}"));
934 }
935 }
936
937 if !filter.tags_any.is_empty() {
938 parts.push(format!("tag:{}", filter.tags_any.join(",")));
939 }
940
941 if !filter.excluded_tags.is_empty() {
942 for tag in &filter.excluded_tags {
943 parts.push(format!("-tag:{tag}"));
944 }
945 }
946
947 if let Some(ref pattern) = filter.source_pattern {
948 parts.push(format!("source:{pattern}"));
949 }
950
951 if let Some(ref project_id) = filter.project_id {
952 parts.push(format!("project:{project_id}"));
953 }
954
955 if let Some(ref branch) = filter.branch {
956 parts.push(format!("branch:{branch}"));
957 }
958
959 if let Some(ref file_path) = filter.file_path {
960 parts.push(format!("path:{file_path}"));
961 }
962
963 if !filter.statuses.is_empty() {
964 let status_list: Vec<_> = filter.statuses.iter().map(MemoryStatus::as_str).collect();
965 parts.push(format!("status:{}", status_list.join(",")));
966 }
967
968 if filter.created_after.is_some() {
969 parts.push("since:active".to_string());
970 }
971
972 if parts.is_empty() {
973 String::new()
974 } else {
975 format!(", filter: {}", parts.join(" "))
976 }
977}
978
979#[cfg(test)]
980mod tests {
981 use super::*;
982
983 // ==========================================================================
984 // MED-SEC-001: Tests for deny_unknown_fields protection
985 // ==========================================================================
986
987 #[test]
988 fn test_capture_args_rejects_unknown_fields() {
989 let json = r#"{"content": "test", "namespace": "decisions", "unknown_field": "bad"}"#;
990 let result: Result<CaptureArgs, _> = serde_json::from_str(json);
991 assert!(result.is_err());
992 assert!(result.unwrap_err().to_string().contains("unknown field"));
993 }
994
995 #[test]
996 fn test_capture_args_accepts_valid_fields() {
997 let json = r#"{"content": "test", "namespace": "decisions", "tags": ["a", "b"]}"#;
998 let result: Result<CaptureArgs, _> = serde_json::from_str(json);
999 assert!(result.is_ok());
1000 }
1001
1002 #[test]
1003 fn test_recall_args_rejects_unknown_fields() {
1004 let json = r#"{"query": "test", "malicious_param": "attack"}"#;
1005 let result: Result<RecallArgs, _> = serde_json::from_str(json);
1006 assert!(result.is_err());
1007 }
1008
1009 #[test]
1010 fn test_consolidate_args_rejects_unknown_fields() {
1011 let json = r#"{"namespaces": ["decisions"], "extra": true}"#;
1012 let result: Result<ConsolidateArgs, _> = serde_json::from_str(json);
1013 assert!(result.is_err());
1014 }
1015
1016 #[test]
1017 fn test_get_summary_args_rejects_unknown_fields() {
1018 let json = r#"{"memory_id": "123", "include_private": true}"#;
1019 let result: Result<GetSummaryArgs, _> = serde_json::from_str(json);
1020 assert!(result.is_err());
1021 }
1022
1023 #[test]
1024 fn test_enrich_args_rejects_unknown_fields() {
1025 let json = r#"{"memory_id": "123", "inject": "payload"}"#;
1026 let result: Result<EnrichArgs, _> = serde_json::from_str(json);
1027 assert!(result.is_err());
1028 }
1029
1030 #[test]
1031 fn test_reindex_args_rejects_unknown_fields() {
1032 let json = r#"{"repo_path": "/path", "delete_all": true}"#;
1033 let result: Result<ReindexArgs, _> = serde_json::from_str(json);
1034 assert!(result.is_err());
1035 }
1036
1037 #[test]
1038 fn test_prompt_save_args_rejects_unknown_fields() {
1039 let json = r#"{"name": "test", "admin_override": true}"#;
1040 let result: Result<PromptSaveArgs, _> = serde_json::from_str(json);
1041 assert!(result.is_err());
1042 }
1043
1044 #[test]
1045 fn test_prompt_variable_arg_rejects_unknown_fields() {
1046 let json = r#"{"name": "var", "execute_code": "rm -rf /"}"#;
1047 let result: Result<PromptVariableArg, _> = serde_json::from_str(json);
1048 assert!(result.is_err());
1049 }
1050
1051 #[test]
1052 fn test_prompt_list_args_rejects_unknown_fields() {
1053 let json = r#"{"domain": "user", "bypass_auth": true}"#;
1054 let result: Result<PromptListArgs, _> = serde_json::from_str(json);
1055 assert!(result.is_err());
1056 }
1057
1058 #[test]
1059 fn test_prompt_get_args_rejects_unknown_fields() {
1060 let json = r#"{"name": "test", "include_secrets": true}"#;
1061 let result: Result<PromptGetArgs, _> = serde_json::from_str(json);
1062 assert!(result.is_err());
1063 }
1064
1065 #[test]
1066 fn test_prompt_run_args_rejects_unknown_fields() {
1067 let json = r#"{"name": "test", "shell_escape": true}"#;
1068 let result: Result<PromptRunArgs, _> = serde_json::from_str(json);
1069 assert!(result.is_err());
1070 }
1071
1072 #[test]
1073 fn test_prompt_delete_args_rejects_unknown_fields() {
1074 let json = r#"{"name": "test", "domain": "user", "recursive": true}"#;
1075 let result: Result<PromptDeleteArgs, _> = serde_json::from_str(json);
1076 assert!(result.is_err());
1077 }
1078
1079 // ==========================================================================
1080 // Core CRUD tools (industry parity: Mem0, Zep, LangMem)
1081 // ==========================================================================
1082
1083 #[test]
1084 fn test_get_args_rejects_unknown_fields() {
1085 let json = r#"{"memory_id": "123", "include_deleted": true}"#;
1086 let result: Result<GetArgs, _> = serde_json::from_str(json);
1087 assert!(result.is_err());
1088 assert!(result.unwrap_err().to_string().contains("unknown field"));
1089 }
1090
1091 #[test]
1092 fn test_get_args_accepts_valid_fields() {
1093 let json = r#"{"memory_id": "abc123"}"#;
1094 let result: Result<GetArgs, _> = serde_json::from_str(json);
1095 assert!(result.is_ok());
1096 assert_eq!(result.unwrap().memory_id, "abc123");
1097 }
1098
1099 #[test]
1100 fn test_delete_args_rejects_unknown_fields() {
1101 let json = r#"{"memory_id": "123", "force": true}"#;
1102 let result: Result<DeleteArgs, _> = serde_json::from_str(json);
1103 assert!(result.is_err());
1104 assert!(result.unwrap_err().to_string().contains("unknown field"));
1105 }
1106
1107 #[test]
1108 fn test_delete_args_accepts_valid_fields() {
1109 let json = r#"{"memory_id": "abc123", "hard": true}"#;
1110 let result: Result<DeleteArgs, _> = serde_json::from_str(json);
1111 assert!(result.is_ok());
1112 let args = result.unwrap();
1113 assert_eq!(args.memory_id, "abc123");
1114 assert!(args.hard);
1115 }
1116
1117 #[test]
1118 fn test_delete_args_defaults_hard_to_false() {
1119 let json = r#"{"memory_id": "abc123"}"#;
1120 let result: Result<DeleteArgs, _> = serde_json::from_str(json);
1121 assert!(result.is_ok());
1122 let args = result.unwrap();
1123 assert!(!args.hard);
1124 }
1125
1126 #[test]
1127 fn test_update_args_rejects_unknown_fields() {
1128 let json = r#"{"memory_id": "123", "namespace": "decisions"}"#;
1129 let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1130 assert!(result.is_err());
1131 assert!(result.unwrap_err().to_string().contains("unknown field"));
1132 }
1133
1134 #[test]
1135 fn test_update_args_accepts_valid_fields() {
1136 let json = r#"{"memory_id": "abc123", "content": "new content", "tags": ["a", "b"]}"#;
1137 let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1138 assert!(result.is_ok());
1139 let args = result.unwrap();
1140 assert_eq!(args.memory_id, "abc123");
1141 assert_eq!(args.content, Some("new content".to_string()));
1142 assert_eq!(args.tags, Some(vec!["a".to_string(), "b".to_string()]));
1143 }
1144
1145 #[test]
1146 fn test_update_args_allows_partial_updates() {
1147 // Only content
1148 let json = r#"{"memory_id": "abc123", "content": "updated"}"#;
1149 let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1150 assert!(result.is_ok());
1151 let args = result.unwrap();
1152 assert!(args.content.is_some());
1153 assert!(args.tags.is_none());
1154
1155 // Only tags
1156 let json = r#"{"memory_id": "abc123", "tags": ["x"]}"#;
1157 let result: Result<UpdateArgs, _> = serde_json::from_str(json);
1158 assert!(result.is_ok());
1159 let args = result.unwrap();
1160 assert!(args.content.is_none());
1161 assert!(args.tags.is_some());
1162 }
1163
1164 // ==========================================================================
1165 // UTF-8 safe truncation tests
1166 // ==========================================================================
1167
1168 #[test]
1169 fn test_truncate_ascii_short() {
1170 assert_eq!(truncate("short", 100), "short");
1171 }
1172
1173 #[test]
1174 fn test_truncate_ascii_exact() {
1175 assert_eq!(truncate("hello", 5), "hello");
1176 }
1177
1178 #[test]
1179 fn test_truncate_ascii_long() {
1180 assert_eq!(truncate("hello world", 8), "hello...");
1181 }
1182
1183 #[test]
1184 fn test_truncate_degree_symbol() {
1185 // The degree symbol (°) is 2 bytes (U+00B0: 0xC2 0xB0)
1186 // "32 °C" = [51, 50, 32, 194, 176, 67] = 6 bytes
1187 let s = "32 °C temperature";
1188 // With max_len=10, target_len=7, boundary should be at 6 (after °)
1189 let result = truncate(s, 10);
1190 assert!(result.ends_with("..."));
1191 // Should not panic and should contain valid UTF-8
1192 assert!(result.is_ascii() || !result.is_empty());
1193 }
1194
1195 #[test]
1196 fn test_truncate_multi_byte_boundary() {
1197 // Test the exact case from the panic: degree symbol at byte 196-198
1198 let s = "Document 301:\nThe Mallee and upper Wimmera are Victoria's warmest regions with hot winds blowing from nearby semi-deserts. Average temperatures exceed 32 °C (90 °F) during summer and 15 °C (59 °F) in winter...";
1199
1200 // This was panicking at max_len=200 because byte 197 is inside °
1201 let result = truncate(s, 200);
1202 assert!(result.ends_with("..."));
1203 // Verify it's valid UTF-8 (won't compile if not, but good to be explicit)
1204 assert!(!result.is_empty());
1205 }
1206
1207 #[test]
1208 fn test_truncate_emoji() {
1209 // Emoji are 4 bytes each
1210 let s = "Hello 👋 World 🌍 Test";
1211 let result = truncate(s, 15);
1212 assert!(result.ends_with("..."));
1213 }
1214
1215 #[test]
1216 fn test_truncate_cjk() {
1217 // CJK characters are 3 bytes each
1218 let s = "Hello 你好 World";
1219 let result = truncate(s, 12);
1220 assert!(result.ends_with("..."));
1221 }
1222
1223 #[test]
1224 fn test_truncate_empty() {
1225 assert_eq!(truncate("", 10), "");
1226 }
1227
1228 #[test]
1229 fn test_truncate_very_small_max() {
1230 // With max_len=3, we have 0 bytes for content
1231 let result = truncate("hello", 3);
1232 assert_eq!(result, "...");
1233 }
1234
1235 #[test]
1236 fn test_truncate_max_len_zero() {
1237 // Edge case: max_len=0
1238 let result = truncate("hello", 0);
1239 assert_eq!(result, "...");
1240 }
1241
1242 // ==========================================================================
1243 // floor_char_boundary tests
1244 // ==========================================================================
1245
1246 #[test]
1247 fn test_floor_char_boundary_ascii() {
1248 let s = "hello";
1249 assert_eq!(floor_char_boundary(s, 0), 0);
1250 assert_eq!(floor_char_boundary(s, 2), 2);
1251 assert_eq!(floor_char_boundary(s, 5), 5);
1252 assert_eq!(floor_char_boundary(s, 10), 5); // beyond end
1253 }
1254
1255 #[test]
1256 fn test_floor_char_boundary_multi_byte() {
1257 // "°" is bytes 0..2 (2 bytes)
1258 let s = "°C";
1259 assert_eq!(floor_char_boundary(s, 0), 0);
1260 assert_eq!(floor_char_boundary(s, 1), 0); // inside °, floor to 0
1261 assert_eq!(floor_char_boundary(s, 2), 2); // at C
1262 assert_eq!(floor_char_boundary(s, 3), 3); // end
1263 }
1264
1265 #[test]
1266 fn test_floor_char_boundary_emoji() {
1267 // "👋" is 4 bytes
1268 let s = "a👋b";
1269 assert_eq!(floor_char_boundary(s, 0), 0); // at 'a'
1270 assert_eq!(floor_char_boundary(s, 1), 1); // at start of emoji
1271 assert_eq!(floor_char_boundary(s, 2), 1); // inside emoji
1272 assert_eq!(floor_char_boundary(s, 3), 1); // inside emoji
1273 assert_eq!(floor_char_boundary(s, 4), 1); // inside emoji
1274 assert_eq!(floor_char_boundary(s, 5), 5); // at 'b'
1275 }
1276
1277 #[test]
1278 fn test_floor_char_boundary_empty() {
1279 assert_eq!(floor_char_boundary("", 0), 0);
1280 assert_eq!(floor_char_boundary("", 5), 0);
1281 }
1282}