From 4ed50a924f50dc5f4294361d9745c2b668a651e2 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Jun 2026 00:04:25 -0400 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20storage=20CRUD=20follow-up=20?= =?UTF-8?q?=E2=80=94=20schema=20placeholders,=20metadata,=20unknown=20item?= =?UTF-8?q?=20types?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add migration 0002 with tenant_id, metadata, and raw_tokens placeholder columns to avoid future schema migrations - Add metadata field to Conversation model and ConversationData domain type - Add Unknown variant to InputItem and OutputItem enums so unrecognized item types are caught by serde instead of silently failing deserialization - Add tracing::warn in as_inout() when items cannot be deserialized, trying both InputItem and OutputItem before falling back Co-Authored-By: Claude Opus 4.6 Signed-off-by: Francisco Javier Arceo --- .../migrations/0002_add_placeholders.sql | 12 ++++++++++++ .../src/storage/models/conversation.rs | 5 +++++ crates/agentic-core/src/storage/models/item.rs | 17 ++++++++++++++--- .../src/storage/types/conversation.rs | 10 ++++++++++ crates/agentic-core/src/types/io.rs | 4 ++++ 5 files changed, 45 insertions(+), 3 deletions(-) create mode 100644 crates/agentic-core/migrations/0002_add_placeholders.sql diff --git a/crates/agentic-core/migrations/0002_add_placeholders.sql b/crates/agentic-core/migrations/0002_add_placeholders.sql new file mode 100644 index 0000000..849c3ed --- /dev/null +++ b/crates/agentic-core/migrations/0002_add_placeholders.sql @@ -0,0 +1,12 @@ +ALTER TABLE conversations ADD COLUMN tenant_id TEXT; +ALTER TABLE conversations ADD COLUMN metadata TEXT; + +ALTER TABLE items ADD COLUMN tenant_id TEXT; +ALTER TABLE items ADD COLUMN raw_tokens TEXT; + +ALTER TABLE responses ADD COLUMN tenant_id TEXT; +ALTER TABLE responses ADD COLUMN raw_tokens TEXT; + +CREATE INDEX IF NOT EXISTS idx_conversations_tenant_id ON conversations (tenant_id); +CREATE INDEX IF NOT EXISTS idx_items_tenant_id ON items (tenant_id); +CREATE INDEX IF NOT EXISTS idx_responses_tenant_id ON responses (tenant_id); diff --git a/crates/agentic-core/src/storage/models/conversation.rs b/crates/agentic-core/src/storage/models/conversation.rs index 21a70f6..4d9515a 100644 --- a/crates/agentic-core/src/storage/models/conversation.rs +++ b/crates/agentic-core/src/storage/models/conversation.rs @@ -12,6 +12,9 @@ pub struct Conversation { /// Unique conversation identifier. pub id: String, + /// Optional metadata as JSON string. + pub metadata: Option, + /// Creation timestamp as Unix timestamp in seconds. pub created_at: i64, } @@ -69,10 +72,12 @@ mod tests { fn test_conversation_basic() { let conversation = Conversation { id: "conv_1".to_string(), + metadata: None, created_at: 1_704_067_200, }; assert_eq!(conversation.id, "conv_1"); + assert!(conversation.metadata.is_none()); assert_eq!(conversation.created_at, 1_704_067_200); } } diff --git a/crates/agentic-core/src/storage/models/item.rs b/crates/agentic-core/src/storage/models/item.rs index 71640dc..2860024 100644 --- a/crates/agentic-core/src/storage/models/item.rs +++ b/crates/agentic-core/src/storage/models/item.rs @@ -1,5 +1,7 @@ //! Conversation history item stored in the database. +use tracing::warn; + use super::super::pool::{DbPool, DbResult, DbTransaction}; use super::super::types::item::InOutItem; use crate::types::io::{InputItem, OutputItem}; @@ -44,9 +46,18 @@ impl Item { /// Deserialize data column as either `InputItem` or `OutputItem`. #[must_use] pub fn as_inout(&self) -> Option { - self.as_input() - .map(InOutItem::Input) - .or_else(|| self.as_output().map(InOutItem::Output)) + if let Some(input) = self.as_input() { + if !matches!(input, InputItem::Unknown) { + return Some(InOutItem::Input(input)); + } + } + if let Some(output) = self.as_output() { + if !matches!(output, OutputItem::Unknown) { + return Some(InOutItem::Output(output)); + } + } + warn!(item_id = %self.id, "unrecognized item type in stored data"); + None } } diff --git a/crates/agentic-core/src/storage/types/conversation.rs b/crates/agentic-core/src/storage/types/conversation.rs index c4fbe82..55dc909 100644 --- a/crates/agentic-core/src/storage/types/conversation.rs +++ b/crates/agentic-core/src/storage/types/conversation.rs @@ -9,6 +9,8 @@ use super::super::models::Conversation as StorageDbConversation; pub struct ConversationData { /// Unique conversation identifier pub conversation_id: String, + /// Optional metadata as JSON string + pub metadata: Option, /// Creation timestamp as Unix timestamp in seconds pub created_at: i64, } @@ -17,6 +19,7 @@ impl From for ConversationData { fn from(row: StorageDbConversation) -> Self { Self { conversation_id: row.id, + metadata: row.metadata, created_at: row.created_at, } } @@ -26,6 +29,7 @@ impl From for StorageDbConversation { fn from(data: ConversationData) -> Self { Self { id: data.conversation_id, + metadata: data.metadata, created_at: data.created_at, } } @@ -39,6 +43,7 @@ mod tests { fn test_conversation_from_db_conversation() { let db_row = StorageDbConversation { id: "conv_123".to_string(), + metadata: None, created_at: 1_704_067_200, }; @@ -51,11 +56,13 @@ mod tests { fn test_conversation_roundtrip() { let data = ConversationData { conversation_id: "conv_456".to_string(), + metadata: Some(r#"{"key":"value"}"#.to_string()), created_at: 1_704_067_200, }; let db_row: StorageDbConversation = data.into(); assert_eq!(db_row.id, "conv_456"); + assert_eq!(db_row.metadata, Some(r#"{"key":"value"}"#.to_string())); assert_eq!(db_row.created_at, 1_704_067_200); } @@ -63,6 +70,7 @@ mod tests { fn test_conversation_data_clone() { let data = ConversationData { conversation_id: "conv_clone".to_string(), + metadata: None, created_at: 1_704_067_200, }; @@ -75,6 +83,7 @@ mod tests { fn test_conversation_data_debug_format() { let data = ConversationData { conversation_id: "conv_debug".to_string(), + metadata: None, created_at: 1_704_067_200, }; @@ -87,6 +96,7 @@ mod tests { fn test_conversation_bidirectional_conversion() { let original = ConversationData { conversation_id: "conv_bidir".to_string(), + metadata: None, created_at: 1_706_790_600, }; diff --git a/crates/agentic-core/src/types/io.rs b/crates/agentic-core/src/types/io.rs index 239a9ce..14fd7f9 100644 --- a/crates/agentic-core/src/types/io.rs +++ b/crates/agentic-core/src/types/io.rs @@ -51,6 +51,8 @@ pub enum InputItem { Message(InputMessage), #[serde(rename = "function_call_output")] FunctionCallOutput(FunctionToolResultMessage), + #[serde(other)] + Unknown, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -108,6 +110,8 @@ pub enum OutputItem { Message(OutputMessage), #[serde(rename = "function_call")] FunctionCall(FunctionToolCall), + #[serde(other)] + Unknown, } #[derive(Debug, Clone, Default, Serialize, Deserialize)] From 3a2caf6060862d7e2e839ec5c2ddad4efec65fd8 Mon Sep 17 00:00:00 2001 From: Francisco Javier Arceo Date: Wed, 3 Jun 2026 05:13:19 -0400 Subject: [PATCH 2/2] fix: refactor as_inout() to use match expression Simplifies the if-let chain into a single match as suggested in review. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Francisco Javier Arceo --- crates/agentic-core/src/storage/models/item.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/crates/agentic-core/src/storage/models/item.rs b/crates/agentic-core/src/storage/models/item.rs index 2860024..c283200 100644 --- a/crates/agentic-core/src/storage/models/item.rs +++ b/crates/agentic-core/src/storage/models/item.rs @@ -46,18 +46,14 @@ impl Item { /// Deserialize data column as either `InputItem` or `OutputItem`. #[must_use] pub fn as_inout(&self) -> Option { - if let Some(input) = self.as_input() { - if !matches!(input, InputItem::Unknown) { - return Some(InOutItem::Input(input)); + match (self.as_input(), self.as_output()) { + (Some(input), _) if !matches!(input, InputItem::Unknown) => Some(InOutItem::Input(input)), + (_, Some(output)) if !matches!(output, OutputItem::Unknown) => Some(InOutItem::Output(output)), + _ => { + warn!(item_id = %self.id, "unrecognized item type in stored data"); + None } } - if let Some(output) = self.as_output() { - if !matches!(output, OutputItem::Unknown) { - return Some(InOutItem::Output(output)); - } - } - warn!(item_id = %self.id, "unrecognized item type in stored data"); - None } }