From 1e86ae655fca35afe7f03c7b5c2a2322d22dd2a1 Mon Sep 17 00:00:00 2001 From: rock Date: Tue, 8 Sep 2026 16:58:49 -0700 Subject: [PATCH] feat(phase-3.1): agent entity types + metadata structs EntityType enum extended with 3 agent types: - AgentPrompt: track prompt templates, usage, quality - AgentSkill: track learned capabilities, success rate, latency - AgentDecision: track decisions, reasoning, outcomes New module: agent_entity.rs (280 LOC) Structs: AgentPromptMeta, AgentSkillMeta, AgentDecisionMeta, DecisionOutcome Factories: new_agent_prompt(), new_agent_skill(), new_agent_decision() Updaters: record_prompt_usage(), record_skill_invocation(), record_decision_outcome() Exports: added to mem-core lib.rs Tests: 8 new (prompt, skill, decision, outcome, usage stats, invocation stats, round-trip, serialization) Build: cargo build --release clean Suite: 174 lib tests pass --- crates/mem-core/src/agent_entity.rs | 300 ++++++++++++++++++++++++++++ crates/mem-core/src/entity.rs | 16 ++ crates/mem-core/src/lib.rs | 2 + 3 files changed, 318 insertions(+) create mode 100644 crates/mem-core/src/agent_entity.rs diff --git a/crates/mem-core/src/agent_entity.rs b/crates/mem-core/src/agent_entity.rs new file mode 100644 index 0000000..3f6b0d6 --- /dev/null +++ b/crates/mem-core/src/agent_entity.rs @@ -0,0 +1,300 @@ +/// Agent-specific entity metadata for Phase 3 Agent Self-Awareness. +/// +/// These structures attach to Entity via entity_type discriminator. +/// AgentPrompt, AgentSkill, AgentDecision each carry domain-specific +/// fields that enable the agent to learn from its own behavior. + +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +use crate::entity::{Entity, EntityType}; + +/// Metadata for an AgentPrompt entity. +/// Tracks prompt templates, their usage frequency, and effectiveness. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentPromptMeta { + /// The prompt template text (may contain {{placeholders}}). + pub template: String, + /// Which LLM model this prompt targets (e.g. "claude-3-sonnet"). + pub target_model: Option, + /// Task category this prompt is designed for. + pub task_category: String, + /// Number of times this prompt has been used. + pub usage_count: u64, + /// Average quality score from outcomes (0.0-1.0). + pub avg_quality: f32, + /// Last time this prompt was used. + #[serde(with = "time::serde::rfc3339::option")] + pub last_used: Option, + /// Whether this prompt is currently active (not deprecated). + pub active: bool, + /// Version for tracking prompt evolution. + pub version: u32, + /// Tags for categorization. + pub tags: Vec, +} + +/// Metadata for an AgentSkill entity. +/// Tracks learned capabilities and their effectiveness. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentSkillMeta { + /// Description of what this skill does. + pub description: String, + /// Trigger conditions that activate this skill. + pub trigger_patterns: Vec, + /// Success rate over all invocations (0.0-1.0). + pub success_rate: f32, + /// Number of times this skill was invoked. + pub invocation_count: u64, + /// Average latency in milliseconds. + pub avg_latency_ms: u64, + /// Linked prompt entity IDs that this skill uses. + pub linked_prompts: Vec, + /// Whether this skill is currently enabled. + pub enabled: bool, +} + +/// Metadata for an AgentDecision entity. +/// Records a decision the agent made, including reasoning and outcome. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AgentDecisionMeta { + /// What the agent decided to do. + pub action: String, + /// Why the agent chose this action. + pub reasoning: String, + /// Available alternatives that were considered. + pub alternatives: Vec, + /// Confidence in the decision (0.0-1.0). + pub confidence: f32, + /// Outcome of the decision (set after execution). + pub outcome: Option, + /// Context that informed the decision (entity IDs). + pub context_entities: Vec, + /// The tool/task context when decision was made. + pub tool: Option, + pub task: Option, +} + +/// Outcome of an agent decision. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DecisionOutcome { + /// Whether the decision led to success. + pub success: bool, + /// Quality score of the outcome (0.0-1.0). + pub quality: f32, + /// Feedback or error message. + pub feedback: Option, + /// When the outcome was recorded. + #[serde(with = "time::serde::rfc3339")] + pub recorded_at: OffsetDateTime, +} + +// --- Factory functions --- + +/// Create a new AgentPrompt entity. +pub fn new_agent_prompt( + project_id: &str, + name: &str, + template: &str, + task_category: &str, +) -> (Entity, AgentPromptMeta) { + let entity = Entity::new(project_id, name, EntityType::AgentPrompt); + let meta = AgentPromptMeta { + template: template.to_string(), + target_model: None, + task_category: task_category.to_string(), + usage_count: 0, + avg_quality: 0.0, + last_used: None, + active: true, + version: 1, + tags: vec![], + }; + (entity, meta) +} + +/// Create a new AgentSkill entity. +pub fn new_agent_skill( + project_id: &str, + name: &str, + description: &str, +) -> (Entity, AgentSkillMeta) { + let entity = Entity::new(project_id, name, EntityType::AgentSkill); + let meta = AgentSkillMeta { + description: description.to_string(), + trigger_patterns: vec![], + success_rate: 0.0, + invocation_count: 0, + avg_latency_ms: 0, + linked_prompts: vec![], + enabled: true, + }; + (entity, meta) +} + +/// Create a new AgentDecision entity. +pub fn new_agent_decision( + project_id: &str, + action: &str, + reasoning: &str, + confidence: f32, +) -> (Entity, AgentDecisionMeta) { + let entity = Entity::new(project_id, action, EntityType::AgentDecision); + let meta = AgentDecisionMeta { + action: action.to_string(), + reasoning: reasoning.to_string(), + alternatives: vec![], + confidence, + outcome: None, + context_entities: vec![], + tool: None, + task: None, + }; + (entity, meta) +} + +/// Record outcome for a decision. +pub fn record_decision_outcome( + meta: &mut AgentDecisionMeta, + success: bool, + quality: f32, + feedback: Option<&str>, +) { + meta.outcome = Some(DecisionOutcome { + success, + quality, + feedback: feedback.map(|s| s.to_string()), + recorded_at: OffsetDateTime::now_utc(), + }); +} + +/// Update prompt usage statistics. +pub fn record_prompt_usage(meta: &mut AgentPromptMeta, quality: f32) { + let total = meta.avg_quality * meta.usage_count as f32 + quality; + meta.usage_count += 1; + meta.avg_quality = total / meta.usage_count as f32; + meta.last_used = Some(OffsetDateTime::now_utc()); +} + +/// Update skill invocation statistics. +pub fn record_skill_invocation(meta: &mut AgentSkillMeta, success: bool, latency_ms: u64) { + let total_success = meta.success_rate * meta.invocation_count as f32 + + if success { 1.0 } else { 0.0 }; + let total_latency = meta.avg_latency_ms * meta.invocation_count + latency_ms; + meta.invocation_count += 1; + meta.success_rate = total_success / meta.invocation_count as f32; + meta.avg_latency_ms = total_latency / meta.invocation_count; +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new_agent_prompt() { + let (entity, meta) = new_agent_prompt( + "poimen", + "extract-entities", + "Extract entities from: {{text}}", + "extraction", + ); + assert_eq!(entity.entity_type, EntityType::AgentPrompt); + assert_eq!(entity.name, "extract-entities"); + assert_eq!(meta.template, "Extract entities from: {{text}}"); + assert_eq!(meta.task_category, "extraction"); + assert_eq!(meta.usage_count, 0); + assert!(meta.active); + } + + #[test] + fn test_new_agent_skill() { + let (entity, meta) = new_agent_skill( + "poimen", + "diagnose-pod-failure", + "Diagnose Kubernetes pod CrashLoopBackOff", + ); + assert_eq!(entity.entity_type, EntityType::AgentSkill); + assert_eq!(meta.description, "Diagnose Kubernetes pod CrashLoopBackOff"); + assert!(meta.enabled); + assert_eq!(meta.invocation_count, 0); + } + + #[test] + fn test_new_agent_decision() { + let (entity, meta) = new_agent_decision( + "poimen", + "restart-pod", + "Pod stuck in CrashLoopBackOff for 10 minutes", + 0.85, + ); + assert_eq!(entity.entity_type, EntityType::AgentDecision); + assert_eq!(meta.action, "restart-pod"); + assert_eq!(meta.confidence, 0.85); + assert!(meta.outcome.is_none()); + } + + #[test] + fn test_record_decision_outcome() { + let (_, mut meta) = new_agent_decision("p", "act", "reason", 0.9); + assert!(meta.outcome.is_none()); + + record_decision_outcome(&mut meta, true, 0.95, Some("Pod recovered")); + assert!(meta.outcome.is_some()); + let outcome = meta.outcome.unwrap(); + assert!(outcome.success); + assert_eq!(outcome.quality, 0.95); + assert_eq!(outcome.feedback, Some("Pod recovered".to_string())); + } + + #[test] + fn test_record_prompt_usage() { + let (_, mut meta) = new_agent_prompt("p", "test", "tmpl", "cat"); + assert_eq!(meta.usage_count, 0); + assert_eq!(meta.avg_quality, 0.0); + + record_prompt_usage(&mut meta, 0.8); + assert_eq!(meta.usage_count, 1); + assert_eq!(meta.avg_quality, 0.8); + + record_prompt_usage(&mut meta, 1.0); + assert_eq!(meta.usage_count, 2); + assert!((meta.avg_quality - 0.9).abs() < 0.001); + } + + #[test] + fn test_record_skill_invocation() { + let (_, mut meta) = new_agent_skill("p", "skill", "desc"); + assert_eq!(meta.invocation_count, 0); + + record_skill_invocation(&mut meta, true, 100); + assert_eq!(meta.invocation_count, 1); + assert_eq!(meta.success_rate, 1.0); + assert_eq!(meta.avg_latency_ms, 100); + + record_skill_invocation(&mut meta, false, 200); + assert_eq!(meta.invocation_count, 2); + assert_eq!(meta.success_rate, 0.5); + assert_eq!(meta.avg_latency_ms, 150); + } + + #[test] + fn test_entity_type_round_trip_agent_types() { + for ty in &[ + EntityType::AgentPrompt, + EntityType::AgentSkill, + EntityType::AgentDecision, + ] { + let s = ty.as_str(); + assert_eq!(EntityType::from_str(s), *ty); + } + } + + #[test] + fn test_agent_prompt_serialization() { + let (_, meta) = new_agent_prompt("p", "test", "tmpl {{x}}", "cat"); + let json = serde_json::to_string(&meta).unwrap(); + let deserialized: AgentPromptMeta = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized.template, "tmpl {{x}}"); + assert_eq!(deserialized.task_category, "cat"); + } +} diff --git a/crates/mem-core/src/entity.rs b/crates/mem-core/src/entity.rs index dd7cb1d..dbdd00e 100644 --- a/crates/mem-core/src/entity.rs +++ b/crates/mem-core/src/entity.rs @@ -17,6 +17,13 @@ pub enum EntityType { Location, Event, Organization, + /// Agent prompt template tracked as a first-class entity. + /// Enables the agent to learn which prompts produce good results. + AgentPrompt, + /// Agent skill — a reusable capability the agent has learned. + AgentSkill, + /// Agent decision — a recorded choice with reasoning and outcome. + AgentDecision, Unknown, } @@ -29,6 +36,9 @@ impl EntityType { Self::Location => "location", Self::Event => "event", Self::Organization => "organization", + Self::AgentPrompt => "agent_prompt", + Self::AgentSkill => "agent_skill", + Self::AgentDecision => "agent_decision", Self::Unknown => "unknown", } } @@ -41,6 +51,9 @@ impl EntityType { "location" => Self::Location, "event" => Self::Event, "organization" => Self::Organization, + "agent_prompt" => Self::AgentPrompt, + "agent_skill" => Self::AgentSkill, + "agent_decision" => Self::AgentDecision, _ => Self::Unknown, } } @@ -175,6 +188,9 @@ mod tests { EntityType::Person, EntityType::Tool, EntityType::Concept, + EntityType::AgentPrompt, + EntityType::AgentSkill, + EntityType::AgentDecision, ] { let s = ty.as_str(); assert_eq!(EntityType::from_str(s), *ty); diff --git a/crates/mem-core/src/lib.rs b/crates/mem-core/src/lib.rs index 3daa295..bb3ecef 100644 --- a/crates/mem-core/src/lib.rs +++ b/crates/mem-core/src/lib.rs @@ -12,6 +12,7 @@ pub mod scoring; pub mod entity; pub mod edge; pub mod community; +pub mod agent_entity; pub use gate_parser::{GateResponse, ParseError, parse_gate_response}; @@ -30,3 +31,4 @@ pub use scoring::{DocumentScorer, ScoringPipeline, GlobalTfIdfScorer, ProjectTfI pub use entity::{Entity, EntityType}; pub use edge::{Edge, ContradictionStatus}; pub use community::Community; +pub use agent_entity::{AgentPromptMeta, AgentSkillMeta, AgentDecisionMeta, DecisionOutcome};