diff options
| author | tanaka takashi <isazaka@gmail.com> | 2026-06-29 02:36:40 +0900 |
|---|---|---|
| committer | tanaka takashi <isazaka@gmail.com> | 2026-06-29 02:36:40 +0900 |
| commit | 4c6a831aa2a57e2bcc20aa3fc2f1b7172f5be285 (patch) | |
| tree | 074021381abf57cad434430f25027115b681b8a9 /src/domain | |
| parent | 8d38b06f11c713e547d70468340242f23322152c (diff) | |
feat: 会話メモリのプロトタイプを実装
- 6種類のMCPツールと入力検証を追加
- バイナリログ、スナップショット、復旧、圧縮処理を実装
- Unicode n-gram検索とドメインモデルを追加
- 単体テストと統合テストを追加
- アーキテクチャと実装状況を文書化
- Cコードの記述スタイルを統一
Diffstat (limited to 'src/domain')
| -rw-r--r-- | src/domain/README.md | 11 | ||||
| -rw-r--r-- | src/domain/message.c | 255 | ||||
| -rw-r--r-- | src/domain/message.h | 60 | ||||
| -rw-r--r-- | src/domain/search.c | 350 | ||||
| -rw-r--r-- | src/domain/search.h | 22 |
5 files changed, 694 insertions, 4 deletions
diff --git a/src/domain/README.md b/src/domain/README.md index 0d359af..a30039e 100644 --- a/src/domain/README.md +++ b/src/domain/README.md @@ -1,8 +1,11 @@ # Domain layer -This directory is reserved for the conversation-memory domain model and its -business rules. Conversation, message, retention, and search contracts will be -defined here only after the next design phase. +`message.h` defines immutable Message and metadata concepts, role/tier enums, +input limits, deep-copy ownership, and UTF-8 validation. This layer has no mcpc +or Win32 dependency. -The domain layer must not depend on mcpc or on a concrete storage engine. +`search.h` owns the ASCII-case-folded Unicode codepoint 2/3-gram inverted index, +collision verification, candidate selection, and deterministic relevance score. +`memory_tier` is descriptive in v1. Retention, summarization, and automatic tier +transitions are intentionally not domain behaviors yet. diff --git a/src/domain/message.c b/src/domain/message.c new file mode 100644 index 0000000..d973be4 --- /dev/null +++ b/src/domain/message.c @@ -0,0 +1,255 @@ +#include "domain/message.h" + +#include <stdlib.h> +#include <string.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; +} + +void +tiny_message_init(tiny_message_t *message) +{ + memset(message, 0, sizeof(*message)); + message->metadata.memory_tier = TINY_TIER_SHORT; +} + +void +tiny_message_free(tiny_message_t *message) +{ + size_t index; + free(message->session_id); + free(message->content); + free(message->metadata.tool_call_id); + for (index = 0; index < message->metadata.tag_count; ++index) + { + free(message->metadata.tags[index]); + } + free(message->metadata.tags); + tiny_message_init(message); +} + +int +tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source) +{ + size_t index; + 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->session_id = duplicate_string(source->session_id); + destination->content = duplicate_string(source->content); + if (destination->session_id == NULL || destination->content == NULL) + { + goto failure; + } + 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) + { + goto failure; + } + } + if (source->metadata.tag_count > 0) + { + destination->metadata.tags = calloc(source->metadata.tag_count, sizeof(char *)); + if (destination->metadata.tags == NULL) + { + goto failure; + } + 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) + { + goto failure; + } + destination->metadata.tag_count++; + } + } + return 0; + +failure: + tiny_message_free(destination); + return -1; +} + +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 (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 (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; +} + +bool +tiny_utf8_valid(const char *value, size_t length) +{ + size_t index = 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 (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; +} diff --git a/src/domain/message.h b/src/domain/message.h new file mode 100644 index 0000000..fa36d2a --- /dev/null +++ b/src/domain/message.h @@ -0,0 +1,60 @@ +#ifndef TINY_CONVMEM_DOMAIN_MESSAGE_H +#define TINY_CONVMEM_DOMAIN_MESSAGE_H + +#include <stdbool.h> +#include <stddef.h> +#include <stdint.h> + +#define TINY_MESSAGE_ID_LENGTH 26 +#define TINY_SESSION_ID_MAX 256 +#define TINY_CONTENT_MAX (1024U * 1024U) +#define TINY_TOOL_CALL_ID_MAX 256 +#define TINY_TAG_MAX 128 +#define TINY_TAG_COUNT_MAX 32 +#define TINY_QUERY_MAX 4096 + +typedef enum tiny_role +{ + TINY_ROLE_USER = 1, + TINY_ROLE_ASSISTANT = 2, + TINY_ROLE_SYSTEM = 3 +} tiny_role_t; + +typedef enum tiny_memory_tier +{ + TINY_TIER_SHORT = 1, + TINY_TIER_MID = 2, + TINY_TIER_LONG = 3, + TINY_TIER_DISCARD = 4 +} tiny_memory_tier_t; + +typedef struct tiny_metadata +{ + char *tool_call_id; + char **tags; + size_t tag_count; + tiny_memory_tier_t memory_tier; +} tiny_metadata_t; + +typedef struct tiny_message +{ + char message_id[TINY_MESSAGE_ID_LENGTH + 1]; + char *session_id; + int64_t timestamp_ms; + tiny_role_t role; + char *content; + tiny_metadata_t metadata; +} tiny_message_t; + +void tiny_message_init(tiny_message_t *message); +void tiny_message_free(tiny_message_t *message); +int tiny_message_copy(tiny_message_t *destination, const tiny_message_t *source); + +const char *tiny_role_name(tiny_role_t role); +bool tiny_role_parse(const char *value, tiny_role_t *role); +const char *tiny_tier_name(tiny_memory_tier_t tier); +bool tiny_tier_parse(const char *value, tiny_memory_tier_t *tier); +bool tiny_utf8_valid(const char *value, size_t length); +bool tiny_message_id_valid(const char *value); + +#endif diff --git a/src/domain/search.c b/src/domain/search.c new file mode 100644 index 0000000..a24b667 --- /dev/null +++ b/src/domain/search.c @@ -0,0 +1,350 @@ +#include "domain/search.h" + +#include <stdint.h> +#include <stdlib.h> +#include <string.h> + +typedef struct gram_bucket +{ + uint64_t hash; + uint32_t points[3]; + unsigned width; + size_t *messages; + size_t count; + size_t capacity; +} gram_bucket_t; + +struct tiny_search_index +{ + gram_bucket_t *buckets; + size_t count; + size_t capacity; + size_t message_count; +}; + +static bool +decode_codepoints(const char *text, uint32_t **output, size_t *count) +{ + const size_t bytes = strlen(text); + size_t offset = 0, used = 0; + uint32_t *points = malloc((bytes + 1) * sizeof(uint32_t)); + if (points == NULL) + { + return false; + } + while (offset < bytes) + { + unsigned char first = (unsigned char)text[offset++]; + uint32_t point; + unsigned continuation; + if (first < 0x80) + { + point = first; + continuation = 0; + } + else if ((first & 0xe0) == 0xc0) + { + point = first & 0x1f; + continuation = 1; + } + else if ((first & 0xf0) == 0xe0) + { + point = first & 0x0f; + continuation = 2; + } + else + { + point = first & 0x07; + continuation = 3; + } + while (continuation-- > 0) + { + point = (point << 6) | ((unsigned char)text[offset++] & 0x3f); + } + if (point >= 'A' && point <= 'Z') + { + point += 'a' - 'A'; + } + points[used++] = point; + } + *output = points; + *count = used; + return true; +} + +static uint64_t +gram_hash(const uint32_t *points, size_t width) +{ + uint64_t hash = 1469598103934665603ULL; + size_t index; + for (index = 0; index < width; ++index) + { + hash ^= points[index]; + hash *= 1099511628211ULL; + } + return hash; +} + +static gram_bucket_t * +find_bucket(const tiny_search_index_t *index, const uint32_t *points, unsigned width) +{ + const uint64_t hash = gram_hash(points, width); + size_t position; + for (position = 0; position < index->count; ++position) + { + if (index->buckets[position].hash == hash && index->buckets[position].width == width + && memcmp(index->buckets[position].points, points, width * sizeof(uint32_t)) == 0) + { + return &index->buckets[position]; + } + } + return NULL; +} + +static bool +add_gram(tiny_search_index_t *index, const uint32_t *points, unsigned width, size_t message_index) +{ + gram_bucket_t *bucket = find_bucket(index, points, width); + if (bucket == NULL) + { + gram_bucket_t *resized; + size_t capacity; + if (index->count == index->capacity) + { + capacity = index->capacity ? index->capacity * 2 : 64; + resized = realloc(index->buckets, capacity * sizeof(*resized)); + if (!resized) + { + return false; + } + index->buckets = resized; + index->capacity = capacity; + } + bucket = &index->buckets[index->count++]; + memset(bucket, 0, sizeof(*bucket)); + bucket->hash = gram_hash(points, width); + bucket->width = width; + memcpy(bucket->points, points, width * sizeof(uint32_t)); + } + if (bucket->count > 0 && bucket->messages[bucket->count - 1] == message_index) + { + return true; + } + if (bucket->count == bucket->capacity) + { + const size_t capacity = bucket->capacity ? bucket->capacity * 2 : 8; + size_t *resized = realloc(bucket->messages, capacity * sizeof(*resized)); + if (!resized) + { + return false; + } + bucket->messages = resized; + bucket->capacity = capacity; + } + bucket->messages[bucket->count++] = message_index; + return true; +} + +static bool +index_text(tiny_search_index_t *index, const char *text, size_t message_index) +{ + uint32_t *points = NULL; + size_t count = 0, offset; + unsigned width; + if (!decode_codepoints(text, &points, &count)) + { + return false; + } + for (width = 2; width <= 3; ++width) + { + for (offset = 0; offset + width <= count; ++offset) + { + if (!add_gram(index, points + offset, width, message_index)) + { + free(points); + return false; + } + } + } + free(points); + return true; +} + +tiny_search_index_t * +tiny_search_index_create(void) +{ + return calloc(1, sizeof(tiny_search_index_t)); +} + +void +tiny_search_index_free(tiny_search_index_t *index) +{ + size_t position; + if (!index) + { + return; + } + for (position = 0; position < index->count; ++position) + { + free(index->buckets[position].messages); + } + free(index->buckets); + free(index); +} + +bool +tiny_search_index_add(tiny_search_index_t *index, + const tiny_message_t *message, + size_t message_index) +{ + size_t tag; + if (!index_text(index, message->content, message_index)) + { + return false; + } + for (tag = 0; tag < message->metadata.tag_count; ++tag) + { + if (!index_text(index, message->metadata.tags[tag], message_index)) + { + return false; + } + } + if (message_index >= index->message_count) + { + index->message_count = message_index + 1; + } + return true; +} + +bool +tiny_search_index_candidates(const tiny_search_index_t *index, + const char *query, + size_t **output, + size_t *count) +{ + uint32_t *points = NULL; + size_t point_count = 0, width, offset, used = 0; + bool *seen; + size_t *items; + *output = NULL; + *count = 0; + if (!decode_codepoints(query, &points, &point_count)) + { + return false; + } + items = malloc((index->message_count ? index->message_count : 1) * sizeof(*items)); + seen = calloc(index->message_count ? index->message_count : 1, sizeof(*seen)); + if (!items || !seen) + { + free(points); + free(items); + free(seen); + return false; + } + if (point_count < 2) + { + for (offset = 0; offset < index->message_count; ++offset) + { + seen[offset] = true; + items[used++] = offset; + } + } + else + { + gram_bucket_t *bucket; + width = point_count >= 3 ? 3 : 2; + for (offset = 0; offset + width <= point_count; ++offset) + { + if ((bucket = find_bucket(index, points + offset, (unsigned)width)) != NULL) + { + size_t item; + for (item = 0; item < bucket->count; ++item) + { + if (!seen[bucket->messages[item]]) + { + seen[bucket->messages[item]] = true; + items[used++] = bucket->messages[item]; + } + } + } + } + } + free(points); + free(seen); + *output = items; + *count = used; + return true; +} + +double +tiny_search_score(const char *query, const tiny_message_t *message) +{ + uint32_t *query_points = NULL, *message_points = NULL; + size_t query_count = 0, message_count = 0, width, query_offset, message_offset, matches = 0, + total, tag; + double score = 0.0; + if (!decode_codepoints(query, &query_points, &query_count) + || !decode_codepoints(message->content, &message_points, &message_count) || query_count == 0) + { + goto done; + } + width = query_count >= 3 ? 3 : (query_count >= 2 ? 2 : 1); + total = query_count - width + 1; + for (query_offset = 0; query_offset < total; ++query_offset) + { + bool found = false; + for (message_offset = 0; message_offset + width <= message_count; ++message_offset) + { + if (memcmp(query_points + query_offset, + message_points + message_offset, + width * sizeof(uint32_t)) + == 0) + { + found = true; + break; + } + } + for (tag = 0; tag < message->metadata.tag_count && !found; ++tag) + { + uint32_t *tag_points = NULL; + size_t tag_count = 0; + if (!decode_codepoints(message->metadata.tags[tag], &tag_points, &tag_count)) + { + continue; + } + for (message_offset = 0; message_offset + width <= tag_count; ++message_offset) + { + if (memcmp(query_points + query_offset, + tag_points + message_offset, + width * sizeof(uint32_t)) + == 0) + { + found = true; + break; + } + } + free(tag_points); + } + if (found) + { + matches++; + } + } + score = (double)matches / (double)total; + if (query_count <= message_count) + { + for (message_offset = 0; message_offset + query_count <= message_count; ++message_offset) + { + if (memcmp(query_points, message_points + message_offset, query_count * sizeof(uint32_t)) + == 0) + { + score += 1.0; + break; + } + } + } +done: + free(query_points); + free(message_points); + return score; +} diff --git a/src/domain/search.h b/src/domain/search.h new file mode 100644 index 0000000..3408998 --- /dev/null +++ b/src/domain/search.h @@ -0,0 +1,22 @@ +#ifndef TINY_CONVMEM_DOMAIN_SEARCH_H +#define TINY_CONVMEM_DOMAIN_SEARCH_H + +#include <stdbool.h> +#include <stddef.h> + +#include "domain/message.h" + +typedef struct tiny_search_index tiny_search_index_t; + +tiny_search_index_t *tiny_search_index_create(void); +void tiny_search_index_free(tiny_search_index_t *index); +bool tiny_search_index_add(tiny_search_index_t *index, + const tiny_message_t *message, + size_t message_index); +bool tiny_search_index_candidates(const tiny_search_index_t *index, + const char *query, + size_t **message_indices, + size_t *count); +double tiny_search_score(const char *query, const tiny_message_t *message); + +#endif |
