#include "domain/message.h" #include #include #include "base/allocator.h" static char * duplicate_string(const char *value) { const size_t length = strlen(value); char *copy = malloc(length + 1); if (copy != NULL) { memcpy(copy, value, length + 1); } return copy; } static int copy_failed(tiny_message_t *message) { tiny_message_free(message); return -1; } void tiny_message_init(tiny_message_t *message) { if (message == NULL) { return; } memset(message, 0, sizeof(*message)); message->metadata.memory_tier = TINY_TIER_SHORT; message->metadata.memory_kind = TINY_KIND_CONVERSATION; message->metadata.importance = 50; message->revision = 1; } void tiny_message_free(tiny_message_t *message) { size_t index; if (message == NULL) { return; } free(message->session_id); free(message->content); free(message->metadata.tool_call_id); free(message->metadata.summary); for (index = 0; index < message->metadata.tag_count; ++index) { free(message->metadata.tags[index]); } free(message->metadata.tags); free(message->metadata.relations); for (index = 0; index < message->metadata.source_count; ++index) { free(message->metadata.source_message_ids[index]); } free(message->metadata.source_message_ids); tiny_message_init(message); } int tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source) { size_t index; if (destination == NULL || source == NULL || destination == source || source->session_id == NULL || source->content == NULL || !tiny_metadata_valid(&source->metadata)) { return -1; } tiny_message_init(destination); memcpy(destination->message_id, source->message_id, sizeof(destination->message_id)); destination->timestamp_ms = source->timestamp_ms; destination->role = source->role; destination->metadata.memory_tier = source->metadata.memory_tier; destination->metadata.memory_kind = source->metadata.memory_kind; destination->metadata.importance = source->metadata.importance; destination->revision = source->revision; destination->updated_at_ms = source->updated_at_ms; destination->session_id = duplicate_string(source->session_id); destination->content = duplicate_string(source->content); if (destination->session_id == NULL || destination->content == NULL) { return copy_failed(destination); } if (source->metadata.summary != NULL) { destination->metadata.summary = duplicate_string(source->metadata.summary); if (destination->metadata.summary == NULL) { return copy_failed(destination); } } if (source->metadata.relation_count > 0) { destination->metadata.relations = malloc(source->metadata.relation_count * sizeof(*destination->metadata.relations)); if (destination->metadata.relations == NULL) { return copy_failed(destination); } memcpy(destination->metadata.relations, source->metadata.relations, source->metadata.relation_count * sizeof(*destination->metadata.relations)); destination->metadata.relation_count = source->metadata.relation_count; } if (source->metadata.source_count > 0) { destination->metadata.source_message_ids = calloc(source->metadata.source_count, sizeof(char *)); if (destination->metadata.source_message_ids == NULL) { return copy_failed(destination); } for (index = 0; index < source->metadata.source_count; ++index) { destination->metadata.source_message_ids[index] = duplicate_string(source->metadata.source_message_ids[index]); if (destination->metadata.source_message_ids[index] == NULL) { return copy_failed(destination); } destination->metadata.source_count++; } } if (source->metadata.tool_call_id != NULL) { destination->metadata.tool_call_id = duplicate_string(source->metadata.tool_call_id); if (destination->metadata.tool_call_id == NULL) { return copy_failed(destination); } } if (source->metadata.tag_count > 0) { destination->metadata.tags = calloc(source->metadata.tag_count, sizeof(char *)); if (destination->metadata.tags == NULL) { return copy_failed(destination); } for (index = 0; index < source->metadata.tag_count; ++index) { destination->metadata.tags[index] = duplicate_string(source->metadata.tags[index]); if (destination->metadata.tags[index] == NULL) { return copy_failed(destination); } destination->metadata.tag_count++; } } return 0; } bool tiny_metadata_valid(const tiny_metadata_t *metadata) { size_t index, other; if (!metadata || metadata->memory_tier < TINY_TIER_SHORT || metadata->memory_tier > TINY_TIER_DISCARD || metadata->memory_kind < TINY_KIND_CONVERSATION || metadata->memory_kind > TINY_KIND_SUMMARY || metadata->importance > 100 || metadata->tag_count > TINY_TAG_COUNT_MAX || (metadata->tag_count && !metadata->tags) || metadata->relation_count > TINY_RELATION_COUNT_MAX || (metadata->relation_count && !metadata->relations) || metadata->source_count > TINY_SOURCE_COUNT_MAX || (metadata->source_count && !metadata->source_message_ids)) { return false; } if (metadata->summary && (strlen(metadata->summary) > TINY_SUMMARY_MAX || !tiny_utf8_valid(metadata->summary, strlen(metadata->summary)))) { return false; } for (index = 0; index < metadata->tag_count; ++index) { if (!metadata->tags[index] || !metadata->tags[index][0] || strlen(metadata->tags[index]) > TINY_TAG_MAX) { return false; } } for (index = 0; index < metadata->relation_count; ++index) { const tiny_relation_t *relation = &metadata->relations[index]; if (relation->type < TINY_RELATION_DEPENDS_ON || relation->type > TINY_RELATION_RELATED_TO || !tiny_message_id_valid(relation->message_id)) { return false; } for (other = 0; other < index; ++other) { if (relation->type == metadata->relations[other].type && strcmp(relation->message_id, metadata->relations[other].message_id) == 0) { return false; } } } for (index = 0; index < metadata->source_count; ++index) { if (!metadata->source_message_ids[index] || !tiny_message_id_valid(metadata->source_message_ids[index])) { return false; } for (other = 0; other < index; ++other) { if (strcmp(metadata->source_message_ids[index], metadata->source_message_ids[other]) == 0) { return false; } } } return true; } const char * tiny_role_name(tiny_role_t role) { switch (role) { case TINY_ROLE_USER: return "user"; case TINY_ROLE_ASSISTANT: return "assistant"; case TINY_ROLE_SYSTEM: return "system"; default: return "unknown"; } } bool tiny_role_parse(const char *value, tiny_role_t *role) { if (value == NULL || role == NULL) { return false; } if (strcmp(value, "user") == 0) { *role = TINY_ROLE_USER; } else if (strcmp(value, "assistant") == 0) { *role = TINY_ROLE_ASSISTANT; } else if (strcmp(value, "system") == 0) { *role = TINY_ROLE_SYSTEM; } else { return false; } return true; } const char * tiny_tier_name(tiny_memory_tier_t tier) { switch (tier) { case TINY_TIER_SHORT: return "short"; case TINY_TIER_MID: return "mid"; case TINY_TIER_LONG: return "long"; case TINY_TIER_DISCARD: return "discard"; default: return "unknown"; } } bool tiny_tier_parse(const char *value, tiny_memory_tier_t *tier) { if (value == NULL || tier == NULL) { return false; } if (strcmp(value, "short") == 0) { *tier = TINY_TIER_SHORT; } else if (strcmp(value, "mid") == 0) { *tier = TINY_TIER_MID; } else if (strcmp(value, "long") == 0) { *tier = TINY_TIER_LONG; } else if (strcmp(value, "discard") == 0) { *tier = TINY_TIER_DISCARD; } else { return false; } return true; } const char * tiny_kind_name(tiny_memory_kind_t kind) { static const char *names[] = { "unknown", "conversation", "episode", "semantic", "procedure", "preference", "summary" }; return kind >= TINY_KIND_CONVERSATION && kind <= TINY_KIND_SUMMARY ? names[kind] : names[0]; } bool tiny_kind_parse(const char *value, tiny_memory_kind_t *kind) { tiny_memory_kind_t candidate; if (value == NULL || kind == NULL) { return false; } for (candidate = TINY_KIND_CONVERSATION; candidate <= TINY_KIND_SUMMARY; ++candidate) { if (strcmp(value, tiny_kind_name(candidate)) == 0) { *kind = candidate; return true; } } return false; } const char * tiny_relation_type_name(tiny_relation_type_t type) { static const char *names[] = { "unknown", "depends_on", "derived_from", "supersedes", "related_to" }; return type >= TINY_RELATION_DEPENDS_ON && type <= TINY_RELATION_RELATED_TO ? names[type] : names[0]; } bool tiny_relation_type_parse(const char *value, tiny_relation_type_t *type) { tiny_relation_type_t candidate; if (value == NULL || type == NULL) { return false; } for (candidate = TINY_RELATION_DEPENDS_ON; candidate <= TINY_RELATION_RELATED_TO; ++candidate) { if (strcmp(value, tiny_relation_type_name(candidate)) == 0) { *type = candidate; return true; } } return false; } bool tiny_utf8_valid(const char *value, size_t length) { size_t index = 0; if (value == NULL) { return length == 0; } while (index < length) { const unsigned char first = (unsigned char)value[index++]; unsigned needed; uint32_t codepoint; if (first < 0x80) { if (first == 0) { return false; } continue; } if ((first & 0xe0) == 0xc0) { needed = 1; codepoint = first & 0x1f; } else if ((first & 0xf0) == 0xe0) { needed = 2; codepoint = first & 0x0f; } else if ((first & 0xf8) == 0xf0) { needed = 3; codepoint = first & 0x07; } else { return false; } if (index + needed > length) { return false; } while (needed-- > 0) { const unsigned char next = (unsigned char)value[index++]; if ((next & 0xc0) != 0x80) { return false; } codepoint = (codepoint << 6) | (next & 0x3f); } if (codepoint > 0x10ffff || (codepoint >= 0xd800 && codepoint <= 0xdfff)) { return false; } if ((first & 0xe0) == 0xc0 && codepoint < 0x80) { return false; } if ((first & 0xf0) == 0xe0 && codepoint < 0x800) { return false; } if ((first & 0xf8) == 0xf0 && codepoint < 0x10000) { return false; } } return true; } bool tiny_message_id_valid(const char *value) { static const char alphabet[] = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"; size_t index; if (value == NULL || strlen(value) != TINY_MESSAGE_ID_LENGTH || value[0] > '7') { return false; } for (index = 0; index < TINY_MESSAGE_ID_LENGTH; ++index) { if (strchr(alphabet, value[index]) == NULL) { return false; } } return true; }