diff options
Diffstat (limited to 'src/mcp/tools')
| -rw-r--r-- | src/mcp/tools/common/history.h | 6 | ||||
| -rw-r--r-- | src/mcp/tools/read_messages.c | 232 | ||||
| -rw-r--r-- | src/mcp/tools/search_messages.c | 461 |
3 files changed, 554 insertions, 145 deletions
diff --git a/src/mcp/tools/common/history.h b/src/mcp/tools/common/history.h index d6f5708..3b23027 100644 --- a/src/mcp/tools/common/history.h +++ b/src/mcp/tools/common/history.h @@ -5,6 +5,12 @@ typedef struct tiny_history_index tiny_history_index_t; +typedef struct tiny_history_index_session_info +{ + const char *session_id; + int64_t last_message_at; +} tiny_history_index_session_info_t; + tiny_history_index_t *tiny_history_index_build(const tiny_store_t *store); void tiny_history_index_free(tiny_history_index_t *index); bool diff --git a/src/mcp/tools/read_messages.c b/src/mcp/tools/read_messages.c index aa76ce8..079e6eb 100644 --- a/src/mcp/tools/read_messages.c +++ b/src/mcp/tools/read_messages.c @@ -225,8 +225,8 @@ relation_type_mask(const char *json, uint32_t *mask) int decoded; tiny_relation_type_t type; if (value_type != MJSON_TOK_STRING - || (decoded = mjson_get_string( - json + value_offset, value_length, "$", value, sizeof(value))) + || (decoded + = mjson_get_string(json + value_offset, value_length, "$", value, sizeof(value))) <= 0 || decoded >= (int)sizeof(value)) { @@ -247,11 +247,17 @@ compare_context_edges(const void *left, const void *right) { const context_edge_t *a = left, *b = right; if (a->depth != b->depth) - return a->depth < b->depth ? -1 : 1; + { + return a->depth < b->depth ? -1 : 1; + } if (a->type != b->type) - return a->type < b->type ? -1 : 1; + { + return a->type < b->type ? -1 : 1; + } if (a->timestamp != b->timestamp) - return a->timestamp < b->timestamp ? -1 : 1; + { + return a->timestamp < b->timestamp ? -1 : 1; + } return strcmp(a->message_id, b->message_id); } @@ -277,8 +283,8 @@ call_context_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mc bool *visited = NULL, truncated = false; uint32_t type_mask; tiny_json_buffer_t json; - if (!message_id || before < 0 || before > 10 || after < 0 || after > 10 - || relation_depth < 0 || relation_depth > 2 + if (!message_id || before < 0 || before > 10 || after < 0 || after > 10 || relation_depth < 0 + || relation_depth > 2 || (before_status != MCPC_EC_0 && before_status != MCPC_EC_ANYDATA_NOTFOUND) || (after_status != MCPC_EC_0 && after_status != MCPC_EC_ANYDATA_NOTFOUND) || (depth_status != MCPC_EC_0 && depth_status != MCPC_EC_ANYDATA_NOTFOUND) @@ -370,10 +376,7 @@ call_context_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mc break; } if (!tiny_memory_index_incoming( - context->memory_index, - current->message.message_id, - &incoming, - &incoming_count)) + context->memory_index, current->message.message_id, &incoming, &incoming_count)) { continue; } @@ -436,8 +439,7 @@ call_context_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mc } tiny_json_raw(&json, "],\"after\":["); emitted = 0; - for (position = target_position + 1; - position < session_entry_count && emitted < (size_t)after; + for (position = target_position + 1; position < session_entry_count && emitted < (size_t)after; ++position) { const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); @@ -472,7 +474,8 @@ call_context_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mc tiny_json_init(&item); tiny_json_raw(&item, "{\"type\":"); tiny_json_string(&item, tiny_relation_type_name(edge->type)); - tiny_json_raw(&item, edge->incoming ? ",\"direction\":\"incoming\"" : ",\"direction\":\"outgoing\""); + tiny_json_raw(&item, + edge->incoming ? ",\"direction\":\"incoming\"" : ",\"direction\":\"outgoing\""); tiny_json_raw(&item, ",\"depth\":"); tiny_json_u64(&item, edge->depth); if (related && !related->deleted) @@ -534,68 +537,173 @@ simple_cursor(const char *cursor, bool *valid) return *valid ? (size_t)value : 0; } +static bool +session_cursor_encode(int64_t last_message_at, const char *session_id, char output[CURSOR_TEXT_MAX]) +{ + unsigned char data[CURSOR_BYTES] = { 0 }; + data[0] = 2; + tiny_codec_put_u64(data + 1, (uint64_t)last_message_at); + tiny_codec_put_u32(data + 9, + tiny_codec_crc32((const unsigned char *)session_id, strlen(session_id))); + memcpy(data + 13, session_id, strlen(session_id)); + tiny_codec_put_u32(data + 43, tiny_codec_crc32(data, 43)); + return base64url_encode(data, sizeof(data), output, CURSOR_TEXT_MAX) == 63; +} + +static bool +session_cursor_decode(const char *cursor, + int64_t *last_message_at, + char session_id[TINY_SESSION_ID_MAX + 1]) +{ + unsigned char data[CURSOR_BYTES]; + size_t length; + if (!cursor || !cursor[0] || !base64url_decode(cursor, data) || data[0] != 2) + { + return false; + } + *last_message_at = (int64_t)tiny_codec_get_u64(data + 1); + length = tiny_codec_get_u32(data + 9); + if (length == 0 || length > TINY_SESSION_ID_MAX + || tiny_codec_crc32(data, 43) != tiny_codec_get_u32(data + 43)) + { + return false; + } + memcpy(session_id, data + 13, length); + session_id[length] = '\0'; + return tiny_utf8_valid(session_id, length); +} + +static int +compare_session_by_last_then_id(const void *left, const void *right) +{ + const tiny_history_index_session_info_t *a = left; + const tiny_history_index_session_info_t *b = right; + if (a->last_message_at != b->last_message_at) + { + return a->last_message_at < b->last_message_at ? -1 : 1; + } + return strcmp(a->session_id, b->session_id); +} + static void call_sessions_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcpc_ucbr_t *result) { char *cursor = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX); int32_t limit = READ_DEFAULT_LIMIT; - bool valid; - size_t start = simple_cursor(cursor, &valid), index, unique = 0, emitted = 0; tiny_json_buffer_t json; mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit); - if (!context || !valid || limit < 1 || limit > READ_MAX_LIMIT - || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND)) + int64_t cursor_last = 0; + char cursor_session[TINY_SESSION_ID_MAX + 1] = { 0 }; + bool has_cursor = cursor && cursor[0]; + size_t emitted = 0, index, position; + const tiny_history_index_t *history; + tiny_history_index_session_info_t *sessions = NULL; + size_t session_count = 0; + bool cursor_ok = !has_cursor || session_cursor_decode(cursor, &cursor_last, cursor_session); + + if (!context || !cursor_ok || limit < 1 || limit > READ_MAX_LIMIT + || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND) + || !tiny_mcp_tools_ensure_history_index(context) || !(history = context->history_index)) { tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "invalid sessions view arguments"); free(cursor); return; } - tiny_json_init(&json); - tiny_json_raw(&json, "{\"sessions\":["); - for (index = 0; index < tiny_store_entry_count(context->store) && emitted < (size_t)limit; - ++index) + + session_count = tiny_history_index_session_count(history); + if (session_count > 0) { - const tiny_store_entry_t *entry = tiny_store_entry(context->store, index); - size_t prior, scan, count = 0; - int64_t started = INT64_MAX, ended = 0; - if (!entry || entry->deleted) + sessions = malloc(session_count * sizeof(*sessions)); + if (!sessions) { - continue; + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "sessions allocation failed"); + free(cursor); + return; } - for (prior = 0; prior < index; ++prior) + for (index = 0; index < session_count; ++index) { - const tiny_store_entry_t *candidate = tiny_store_entry(context->store, prior); - if (candidate && !candidate->deleted - && strcmp(candidate->message.session_id, entry->message.session_id) == 0) + const char *session_id; + const size_t *entries; + size_t count; + if (!tiny_history_index_session_at(history, index, &session_id, &entries, &count)) { - break; + free(sessions); + tiny_mcp_tool_add_error(result, "INTERNAL_ERROR", "session enumeration failed"); + free(cursor); + return; } + sessions[index].session_id = session_id; + sessions[index].last_message_at + = count > 0 + ? tiny_store_entry(context->store, entries[count - 1])->message.timestamp_ms + : 0; + } + qsort(sessions, session_count, sizeof(*sessions), compare_session_by_last_then_id); + } + + tiny_json_init(&json); + tiny_json_raw(&json, "{\"sessions\":["); + for (position = 0; position < session_count && emitted < (size_t)limit; ++position) + { + const char *session_id = sessions[position].session_id; + const size_t *entries; + size_t count, representative_index = SIZE_MAX; + int64_t started = INT64_MAX, ended = 0; + int kind_counts[6] = { 0 }; + const tiny_store_entry_t *representative = NULL; + const char *summary = NULL; + if (has_cursor + && (sessions[position].last_message_at < cursor_last + || (sessions[position].last_message_at == cursor_last + && strcmp(session_id, cursor_session) <= 0))) + { + continue; } - if (prior != index || unique++ < start) + if (!tiny_history_index_session(history, session_id, &entries, &count)) { continue; } - for (scan = index; scan < tiny_store_entry_count(context->store); ++scan) + for (index = 0; index < count; ++index) { - const tiny_store_entry_t *candidate = tiny_store_entry(context->store, scan); - if (candidate && !candidate->deleted - && strcmp(candidate->message.session_id, entry->message.session_id) == 0) + const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[index]); + if (!entry || entry->deleted) { - count++; - started = candidate->message.timestamp_ms < started ? candidate->message.timestamp_ms - : started; - ended = candidate->message.timestamp_ms > ended ? candidate->message.timestamp_ms - : ended; + continue; } + kind_counts[entry->message.metadata.memory_kind - 1]++; + started = entry->message.timestamp_ms < started ? entry->message.timestamp_ms : started; + ended = entry->message.timestamp_ms > ended ? entry->message.timestamp_ms : ended; + representative_index = entries[index]; + representative = entry; + } + if (representative_index == SIZE_MAX) + { + continue; } if (emitted++) { tiny_json_raw(&json, ","); } + summary = representative->message.metadata.summary ? representative->message.metadata.summary + : representative->message.content; tiny_json_raw(&json, "{\"session_id\":"); - tiny_json_string(&json, entry->message.session_id); + tiny_json_string(&json, session_id); tiny_json_raw(&json, ",\"message_count\":"); tiny_json_u64(&json, count); + tiny_json_raw(&json, ",\"kind_counts\":{\"conversation\":"); + tiny_json_i64(&json, kind_counts[0]); + tiny_json_raw(&json, ",\"episode\":"); + tiny_json_i64(&json, kind_counts[1]); + tiny_json_raw(&json, ",\"semantic\":"); + tiny_json_i64(&json, kind_counts[2]); + tiny_json_raw(&json, ",\"procedure\":"); + tiny_json_i64(&json, kind_counts[3]); + tiny_json_raw(&json, ",\"preference\":"); + tiny_json_i64(&json, kind_counts[4]); + tiny_json_raw(&json, ",\"summary\":"); + tiny_json_i64(&json, kind_counts[5]); + tiny_json_raw(&json, "},\"representative_summary\":"); + tiny_json_string(&json, summary); tiny_json_raw(&json, ",\"started_at\":"); tiny_json_i64(&json, started); tiny_json_raw(&json, ",\"last_message_at\":"); @@ -603,16 +711,20 @@ call_sessions_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, m tiny_json_raw(&json, "}"); } tiny_json_raw(&json, "]"); - if (emitted == (size_t)limit) + if (emitted == (size_t)limit && position < session_count) { - char next[32]; - snprintf(next, sizeof(next), "%zu", start + emitted); - tiny_json_raw(&json, ",\"nextCursor\":"); - tiny_json_string(&json, next); + char next[CURSOR_TEXT_MAX]; + if (session_cursor_encode( + sessions[position - 1].last_message_at, sessions[position - 1].session_id, next)) + { + tiny_json_raw(&json, ",\"nextCursor\":"); + tiny_json_string(&json, next); + } } tiny_json_raw(&json, "}"); tiny_mcp_tool_add_json(result, &json); tiny_json_free(&json); + free(sessions); free(cursor); } @@ -623,8 +735,10 @@ call_export_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcp char *cursor = tiny_mcp_tool_get_string(tool, "cursor", CURSOR_TEXT_MAX); int32_t limit = 500; bool valid; - size_t index = simple_cursor(cursor, &valid), emitted = 0; + size_t index = simple_cursor(cursor, &valid), emitted = 0, start_index; tiny_json_buffer_t json, lines; + size_t outer_size = 64; + bool truncated = false; mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &limit); if (!context || !valid || limit < 1 || limit > 1000 || (limit_status != MCPC_EC_0 && limit_status != MCPC_EC_ANYDATA_NOTFOUND)) @@ -634,6 +748,7 @@ call_export_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcp free(cursor); return; } + start_index = index; tiny_json_init(&lines); if (index == 0) { @@ -642,24 +757,37 @@ call_export_view(tiny_mcp_tools_context_t *context, const mcpc_tool_t *tool, mcp for (; index < tiny_store_entry_count(context->store) && emitted < (size_t)limit; ++index) { const tiny_store_entry_t *entry = tiny_store_entry(context->store, index); + tiny_json_buffer_t item; + size_t line_overhead; if (!entry || entry->deleted || (session && session[0] && strcmp(session, entry->message.session_id) != 0)) { continue; } - tiny_mcp_tool_append_message(&lines, &entry->message); + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &entry->message); + line_overhead = item.length + 1; + if (item.failed || outer_size + lines.length + line_overhead + 2 > READ_RESPONSE_BUDGET) + { + truncated = true; + tiny_json_free(&item); + break; + } + tiny_json_raw_n(&lines, item.data, item.length); tiny_json_raw(&lines, "\n"); emitted++; + tiny_json_free(&item); } tiny_json_init(&json); tiny_json_raw(&json, "{\"jsonl\":"); tiny_json_string(&json, lines.data ? lines.data : ""); - if (index < tiny_store_entry_count(context->store)) + if (truncated || index < tiny_store_entry_count(context->store)) { char next[32]; - snprintf(next, sizeof(next), "%zu", index); + snprintf(next, sizeof(next), "%zu", index > start_index ? index : start_index + 1); tiny_json_raw(&json, ",\"nextCursor\":"); tiny_json_string(&json, next); + tiny_json_raw(&json, ",\"truncated\":true"); } tiny_json_raw(&json, "}"); tiny_mcp_tool_add_json(result, &json); diff --git a/src/mcp/tools/search_messages.c b/src/mcp/tools/search_messages.c index 4b7f870..f99a146 100644 --- a/src/mcp/tools/search_messages.c +++ b/src/mcp/tools/search_messages.c @@ -8,6 +8,19 @@ #include <string.h> #include "base/allocator.h" +#include "mjson.h" + +#define SEARCH_RESPONSE_BUDGET (8U * 1024U * 1024U - 64U * 1024U) +#define SEARCH_MAX_LIMIT 100 +#define SEARCH_MAX_CONTEXT_SIZE 3 + +typedef struct search_filters +{ + bool kinds[8]; + bool tiers[8]; + char **tags; + size_t tag_count; +} search_filters_t; typedef struct result_item { @@ -18,24 +31,193 @@ typedef struct result_item double tag_score; } result_item_t; -static double -score_component(const char *query, const tiny_message_t *message, int component) +static void +search_filters_free(search_filters_t *filters) +{ + size_t index; + if (!filters) + { + return; + } + for (index = 0; index < filters->tag_count; ++index) + { + free(filters->tags[index]); + } + free(filters->tags); + filters->tags = NULL; + filters->tag_count = 0; +} + +static bool +parse_filters_json(const char *json, search_filters_t *filters) { - tiny_message_t candidate = *message; - candidate.metadata.tags = NULL; - candidate.metadata.tag_count = 0; - candidate.metadata.summary = NULL; - if (component == 1) + int offset = 0, key_offset, key_length, value_offset, value_length, value_type; + if (!json || !json[0]) + { + return true; + } + if (mjson_find(json, (int)strlen(json), "$", NULL, NULL) != MJSON_TOK_OBJECT) { - candidate.content = message->metadata.summary ? message->metadata.summary : ""; + return false; } - else if (component == 2) + while ((offset = mjson_next(json, + (int)strlen(json), + offset, + &key_offset, + &key_length, + &value_offset, + &value_length, + &value_type)) + != 0) { - candidate.content = ""; - candidate.metadata.tags = message->metadata.tags; - candidate.metadata.tag_count = message->metadata.tag_count; + char key[16]; + int decoded = mjson_get_string(json + key_offset, key_length, "$", key, sizeof(key)); + if (decoded <= 0 || decoded >= (int)sizeof(key)) + { + return false; + } + key[decoded] = '\0'; + if (value_type != MJSON_TOK_ARRAY) + { + return false; + } + if (strcmp(key, "kinds") == 0) + { + int element = 0, e_key, e_klen, e_val, e_vlen, e_type; + while ((element = mjson_next(json + value_offset, + value_length, + element, + &e_key, + &e_klen, + &e_val, + &e_vlen, + &e_type)) + != 0) + { + char name[24]; + tiny_memory_kind_t kind; + int name_len + = mjson_get_string(json + value_offset + e_val, e_vlen, "$", name, sizeof(name)); + if (name_len <= 0 || name_len >= (int)sizeof(name) || !tiny_kind_parse(name, &kind)) + { + return false; + } + filters->kinds[kind] = true; + } + } + else if (strcmp(key, "tiers") == 0) + { + int element = 0, e_key, e_klen, e_val, e_vlen, e_type; + while ((element = mjson_next(json + value_offset, + value_length, + element, + &e_key, + &e_klen, + &e_val, + &e_vlen, + &e_type)) + != 0) + { + char name[16]; + tiny_memory_tier_t tier; + int name_len + = mjson_get_string(json + value_offset + e_val, e_vlen, "$", name, sizeof(name)); + if (name_len <= 0 || name_len >= (int)sizeof(name) || !tiny_tier_parse(name, &tier)) + { + return false; + } + filters->tiers[tier] = true; + } + } + else if (strcmp(key, "tags") == 0) + { + int element = 0, e_key, e_klen, e_val, e_vlen, e_type; + while ((element = mjson_next(json + value_offset, + value_length, + element, + &e_key, + &e_klen, + &e_val, + &e_vlen, + &e_type)) + != 0) + { + char *tag; + char name[TINY_TAG_MAX + 1]; + int name_len + = mjson_get_string(json + value_offset + e_val, e_vlen, "$", name, sizeof(name)); + if (name_len <= 0 || name_len >= (int)sizeof(name) || !name[0] + || strlen(name) > TINY_TAG_MAX) + { + return false; + } + tag = malloc(name_len + 1); + if (!tag) + { + return false; + } + memcpy(tag, name, name_len + 1); + if (filters->tag_count == SIZE_MAX) + { + free(tag); + return false; + } + { + char **resized = realloc(filters->tags, (filters->tag_count + 1) * sizeof(char *)); + if (!resized) + { + free(tag); + return false; + } + filters->tags = resized; + filters->tags[filters->tag_count++] = tag; + } + } + } + else + { + return false; + } + } + return true; +} + +static bool +filters_match(const search_filters_t *filters, const tiny_message_t *message) +{ + size_t index, tag_index; + if (filters->kinds[TINY_KIND_CONVERSATION] || filters->kinds[TINY_KIND_EPISODE] + || filters->kinds[TINY_KIND_SEMANTIC] || filters->kinds[TINY_KIND_PROCEDURE] + || filters->kinds[TINY_KIND_PREFERENCE] || filters->kinds[TINY_KIND_SUMMARY]) + { + if (!filters->kinds[message->metadata.memory_kind]) + { + return false; + } + } + if (filters->tiers[TINY_TIER_SHORT] || filters->tiers[TINY_TIER_MID] + || filters->tiers[TINY_TIER_LONG] || filters->tiers[TINY_TIER_DISCARD]) + { + if (!filters->tiers[message->metadata.memory_tier]) + { + return false; + } + } + for (index = 0; index < filters->tag_count; ++index) + { + for (tag_index = 0; tag_index < message->metadata.tag_count; ++tag_index) + { + if (strcmp(filters->tags[index], message->metadata.tags[tag_index]) == 0) + { + break; + } + } + if (tag_index == message->metadata.tag_count) + { + return false; + } } - return tiny_search_score(query, &candidate); + return true; } static int @@ -79,35 +261,115 @@ compare_results(const void *left, const void *right) } static void +append_context_preview(tiny_json_buffer_t *json, + tiny_mcp_tools_context_t *context, + const tiny_message_t *message, + int32_t context_size) +{ + const size_t *entries = NULL; + size_t entry_count = 0, target_position = SIZE_MAX, position; + int32_t before = context_size, after = context_size; + tiny_json_raw(json, ",\"context_preview\":{\"before\":["); + if (!tiny_history_index_session( + context->history_index, message->session_id, &entries, &entry_count)) + { + before = 0; + after = 0; + } + else + { + for (position = 0; position < entry_count; ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); + if (entry && !entry->deleted + && strcmp(entry->message.message_id, message->message_id) == 0) + { + target_position = position; + break; + } + } + } + if (target_position == SIZE_MAX) + { + before = 0; + after = 0; + } + for (position = (target_position > (size_t)before ? target_position - (size_t)before : 0); + position < target_position; + ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); + tiny_json_buffer_t item; + if (!entry || entry->deleted) + { + continue; + } + if (position > (target_position > (size_t)before ? target_position - (size_t)before : 0)) + { + tiny_json_raw(json, ","); + } + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &entry->message); + tiny_json_raw_n(json, item.data, item.length); + tiny_json_free(&item); + } + tiny_json_raw(json, "],\"after\":["); + for (position = target_position + 1; + position < entry_count && position < target_position + 1 + (size_t)after; + ++position) + { + const tiny_store_entry_t *entry = tiny_store_entry(context->store, entries[position]); + tiny_json_buffer_t item; + if (!entry || entry->deleted) + { + continue; + } + if (position > target_position + 1) + { + tiny_json_raw(json, ","); + } + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &entry->message); + tiny_json_raw_n(json, item.data, item.length); + tiny_json_free(&item); + } + tiny_json_raw(json, "]}"); +} + +static void call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) { tiny_mcp_tools_context_t *context = tiny_mcp_tools_context_current(); char *query = tiny_mcp_tool_get_string(tool, "query", TINY_QUERY_MAX); char *session = tiny_mcp_tool_get_string(tool, "session_id", TINY_SESSION_ID_MAX); - char *kind_text = tiny_mcp_tool_get_string(tool, "memory_kind", 24); - char *tier_text = tiny_mcp_tool_get_string(tool, "memory_tier", 16); - char *tag = tiny_mcp_tool_get_string(tool, "tag", TINY_TAG_MAX); + char *filters_json = tiny_mcp_tool_get_string(tool, "filters_json", 4096); int32_t requested_limit = 20; int32_t minimum_importance = 0; + int32_t context_size = 0; mcpc_errcode_t limit_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"limit", &requested_limit); mcpc_errcode_t importance_status = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"min_importance", &minimum_importance); - tiny_memory_kind_t kind = TINY_KIND_CONVERSATION; - tiny_memory_tier_t tier = TINY_TIER_SHORT; + mcpc_errcode_t context_status + = mcpc_tool_get_tpropval_i32(tool, (const char8_t *)"context_size", &context_size); + search_filters_t filters = { { false } }; size_t limit, index, count = 0, candidate_count = 0, *candidates = NULL; result_item_t *items = NULL; tiny_json_buffer_t json; + bool filters_ok; bool valid = context && query && query[0] && (limit_status == MCPC_EC_0 || limit_status == MCPC_EC_ANYDATA_NOTFOUND) - && requested_limit >= 1 && requested_limit <= 100; + && requested_limit >= 1 && requested_limit <= SEARCH_MAX_LIMIT; valid = valid && minimum_importance >= 0 && minimum_importance <= 100 && (importance_status == MCPC_EC_0 || importance_status == MCPC_EC_ANYDATA_NOTFOUND) - && (!kind_text || tiny_kind_parse(kind_text, &kind)) - && (!tier_text || tiny_tier_parse(tier_text, &tier)); - if (!valid) + && context_size >= 0 && context_size <= SEARCH_MAX_CONTEXT_SIZE + && (context_status == MCPC_EC_0 || context_status == MCPC_EC_ANYDATA_NOTFOUND); + filters_ok = parse_filters_json(filters_json, &filters); + if (!valid || !filters_ok) { - tiny_mcp_tool_add_error(result, "VALIDATION_ERROR", "query or limit is invalid"); + tiny_mcp_tool_add_error( + result, "VALIDATION_ERROR", "query, limit, filters or context_size is invalid"); + search_filters_free(&filters); } else { @@ -135,44 +397,31 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) { const tiny_store_entry_t *entry = tiny_store_entry(context->store, candidates[index]); - double score; + tiny_search_breakdown_t breakdown; if (!entry || entry->deleted || (session && session[0] && strcmp(entry->message.session_id, session) != 0)) { continue; } - if ((kind_text && entry->message.metadata.memory_kind != kind) - || (tier_text && entry->message.metadata.memory_tier != tier) - || entry->message.metadata.importance < minimum_importance) + if (entry->message.metadata.importance < minimum_importance) { continue; } - if (tag) + if (!filters_match(&filters, &entry->message)) { - size_t tag_index; - for (tag_index = 0; tag_index < entry->message.metadata.tag_count; - ++tag_index) - { - if (strcmp(tag, entry->message.metadata.tags[tag_index]) == 0) - { - break; - } - } - if (tag_index == entry->message.metadata.tag_count) - { - continue; - } + continue; } - score = tiny_search_score(query, &entry->message); - if (score > 0) + if (!tiny_search_score_breakdown(query, &entry->message, &breakdown) + || breakdown.total <= 0) { - items[count].entry = entry; - items[count].score = score; - items[count].content_score = score_component(query, &entry->message, 0); - items[count].summary_score = score_component(query, &entry->message, 1); - items[count].tag_score = score_component(query, &entry->message, 2); - count++; + continue; } + items[count].entry = entry; + items[count].score = breakdown.total; + items[count].content_score = breakdown.content; + items[count].summary_score = breakdown.summary; + items[count].tag_score = breakdown.tags; + count++; } qsort(items, count, sizeof(*items), compare_results); if (count > limit) @@ -183,54 +432,74 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) tiny_json_raw(&json, "{\"results\":["); for (index = 0; index < count; ++index) { - if (index) + tiny_json_buffer_t item; + tiny_json_init(&item); + tiny_mcp_tool_append_message(&item, &items[index].entry->message); + if (item.length > 0) { - tiny_json_raw(&json, ","); + item.length--; + item.data[item.length] = '\0'; } + tiny_json_raw(&item, ",\"score\":"); + tiny_json_number(&item, items[index].score); + tiny_json_raw(&item, ",\"score_breakdown\":{\"content\":"); + tiny_json_number(&item, items[index].content_score); + tiny_json_raw(&item, ",\"summary\":"); + tiny_json_number(&item, items[index].summary_score); + tiny_json_raw(&item, ",\"tags\":"); + tiny_json_number(&item, items[index].tag_score); + tiny_json_raw(&item, "},\"matched_fields\":["); { - tiny_json_buffer_t item; - tiny_json_init(&item); - tiny_mcp_tool_append_message(&item, &items[index].entry->message); - if (item.length > 0) + bool matched = false; + if (items[index].content_score > 0) + { + tiny_json_raw(&item, "\"content\""); + matched = true; + } + if (items[index].summary_score > 0) + { + tiny_json_raw(&item, matched ? ",\"summary\"" : "\"summary\""); + matched = true; + } + if (items[index].tag_score > 0) { - item.length--; - item.data[item.length] = '\0'; + tiny_json_raw(&item, matched ? ",\"tags\"" : "\"tags\""); } - tiny_json_raw(&item, ",\"score\":"); - tiny_json_number(&item, items[index].score); - tiny_json_raw(&item, ",\"score_breakdown\":{\"content\":"); - tiny_json_number(&item, items[index].content_score); - tiny_json_raw(&item, ",\"summary\":"); - tiny_json_number(&item, items[index].summary_score); - tiny_json_raw(&item, ",\"tags\":"); - tiny_json_number(&item, items[index].tag_score); - tiny_json_raw(&item, ",\"importance\":0},\"matched_fields\":["); + } + tiny_json_raw(&item, "],\"relation_count\":"); + tiny_json_u64(&item, items[index].entry->message.metadata.relation_count); + if (context_size > 0) { - bool matched = false; - if (items[index].content_score > 0) - { - tiny_json_raw(&item, "\"content\""); - matched = true; - } - if (items[index].summary_score > 0) - { - tiny_json_raw(&item, matched ? ",\"summary\"" : "\"summary\""); - matched = true; - } - if (items[index].tag_score > 0) + append_context_preview( + &item, context, &items[index].entry->message, context_size); + } + tiny_json_raw(&item, "}"); + if (item.failed || json.length + item.length + 256 > SEARCH_RESPONSE_BUDGET) + { + tiny_json_free(&item); + if (item.failed) { - tiny_json_raw(&item, matched ? ",\"tags\"" : "\"tags\""); + json.failed = true; } + break; } - tiny_json_raw(&item, "],\"relation_count\":"); - tiny_json_u64(&item, items[index].entry->message.metadata.relation_count); - tiny_json_raw(&item, "}"); - tiny_json_raw_n(&json, item.data, item.length); - tiny_json_free(&item); - } + if (index) + { + tiny_json_raw(&json, ","); + } + tiny_json_raw_n(&json, item.data, item.length); + tiny_json_free(&item); } tiny_json_raw(&json, "]}"); - tiny_mcp_tool_add_json(result, &json); + if (json.failed) + { + tiny_mcp_tool_add_error( + result, "INTERNAL_ERROR", "search response allocation failed"); + } + else + { + tiny_mcp_tool_add_json(result, &json); + } tiny_json_free(&json); } } @@ -239,9 +508,8 @@ call_search_messages(const mcpc_tool_t *tool, mcpc_ucbr_t *result) free(candidates); free(query); free(session); - free(kind_text); - free(tier_text); - free(tag); + free(filters_json); + search_filters_free(&filters); } bool @@ -266,10 +534,11 @@ tiny_mcp_register_search_messages(mcpc_server_t *server) MCPC_U8STR, false) && tiny_mcp_tool_add_property( - tool, "memory_kind", "Optional exact memory kind filter.", MCPC_U8STR, false) - && tiny_mcp_tool_add_property( - tool, "memory_tier", "Optional exact memory tier filter.", MCPC_U8STR, false) - && tiny_mcp_tool_add_property(tool, "tag", "Optional exact tag filter.", MCPC_U8STR, false) + tool, + "filters_json", + "Optional JSON object with arrays kinds, tiers, and/or tags to filter results.", + MCPC_U8STR, + false) && tiny_mcp_tool_add_property(tool, "min_importance", "Minimum importance from 0 to 100; defaults to 0.", @@ -277,6 +546,12 @@ tiny_mcp_register_search_messages(mcpc_server_t *server) false) && tiny_mcp_tool_add_property( tool, + "context_size", + "Messages before and after each result, from 0 to 3; defaults to 0.", + MCPC_I32, + false) + && tiny_mcp_tool_add_property( + tool, "limit", "Maximum ranked matches to return, from 1 to 100; defaults to 20.", MCPC_I32, |
