diff --git a/main/agent/agent_loop.c b/main/agent/agent_loop.c new file mode 100644 index 0000000..ad0df3c --- /dev/null +++ b/main/agent/agent_loop.c @@ -0,0 +1,99 @@ +#include "agent_loop.h" +#include "agent/context_builder.h" +#include "mimi_config.h" +#include "bus/message_bus.h" +#include "llm/llm_proxy.h" +#include "memory/session_mgr.h" + +#include +#include +#include "esp_log.h" +#include "esp_heap_caps.h" + +static const char *TAG = "agent"; + +static void agent_loop_task(void *arg) +{ + ESP_LOGI(TAG, "Agent loop started on core %d", xPortGetCoreID()); + + /* Allocate large buffers from PSRAM */ + char *system_prompt = heap_caps_calloc(1, MIMI_CONTEXT_BUF_SIZE, MALLOC_CAP_SPIRAM); + char *messages_json = heap_caps_calloc(1, MIMI_LLM_STREAM_BUF_SIZE, MALLOC_CAP_SPIRAM); + char *history_json = heap_caps_calloc(1, MIMI_LLM_STREAM_BUF_SIZE, MALLOC_CAP_SPIRAM); + char *response_buf = heap_caps_calloc(1, MIMI_LLM_STREAM_BUF_SIZE, MALLOC_CAP_SPIRAM); + + if (!system_prompt || !messages_json || !history_json || !response_buf) { + ESP_LOGE(TAG, "Failed to allocate PSRAM buffers"); + vTaskDelete(NULL); + return; + } + + while (1) { + mimi_msg_t msg; + esp_err_t err = message_bus_pop_inbound(&msg, UINT32_MAX); + if (err != ESP_OK) continue; + + ESP_LOGI(TAG, "Processing message from %s:%s", msg.channel, msg.chat_id); + + /* 1. Build system prompt */ + context_build_system_prompt(system_prompt, MIMI_CONTEXT_BUF_SIZE); + + /* 2. Load session history */ + session_get_history_json(msg.chat_id, history_json, + MIMI_LLM_STREAM_BUF_SIZE, MIMI_AGENT_MAX_HISTORY); + + /* 3. Build messages array (history + current message) */ + context_build_messages(history_json, msg.content, + messages_json, MIMI_LLM_STREAM_BUF_SIZE); + + /* 4. Call Claude API */ + err = llm_chat(system_prompt, messages_json, response_buf, MIMI_LLM_STREAM_BUF_SIZE); + + if (err == ESP_OK && response_buf[0]) { + /* 5. Save to session */ + session_append(msg.chat_id, "user", msg.content); + session_append(msg.chat_id, "assistant", response_buf); + + /* 6. Push response to outbound */ + mimi_msg_t out = {0}; + strncpy(out.channel, msg.channel, sizeof(out.channel) - 1); + strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1); + out.content = strdup(response_buf); + if (out.content) { + message_bus_push_outbound(&out); + } + } else { + /* Send error response */ + mimi_msg_t out = {0}; + strncpy(out.channel, msg.channel, sizeof(out.channel) - 1); + strncpy(out.chat_id, msg.chat_id, sizeof(out.chat_id) - 1); + out.content = strdup(response_buf[0] ? response_buf : "Sorry, I encountered an error."); + if (out.content) { + message_bus_push_outbound(&out); + } + } + + /* Free inbound message content */ + free(msg.content); + + /* Log memory status */ + ESP_LOGI(TAG, "Free PSRAM: %d bytes", + (int)heap_caps_get_free_size(MALLOC_CAP_SPIRAM)); + } +} + +esp_err_t agent_loop_init(void) +{ + ESP_LOGI(TAG, "Agent loop initialized"); + return ESP_OK; +} + +esp_err_t agent_loop_start(void) +{ + BaseType_t ret = xTaskCreatePinnedToCore( + agent_loop_task, "agent_loop", + MIMI_AGENT_STACK, NULL, + MIMI_AGENT_PRIO, NULL, MIMI_AGENT_CORE); + + return (ret == pdPASS) ? ESP_OK : ESP_FAIL; +} diff --git a/main/agent/agent_loop.h b/main/agent/agent_loop.h new file mode 100644 index 0000000..7c1e098 --- /dev/null +++ b/main/agent/agent_loop.h @@ -0,0 +1,14 @@ +#pragma once + +#include "esp_err.h" + +/** + * Initialize the agent loop. + */ +esp_err_t agent_loop_init(void); + +/** + * Start the agent loop task (runs on Core 1). + * Consumes from inbound queue, calls Claude API, pushes to outbound queue. + */ +esp_err_t agent_loop_start(void); diff --git a/main/agent/context_builder.c b/main/agent/context_builder.c new file mode 100644 index 0000000..45736b3 --- /dev/null +++ b/main/agent/context_builder.c @@ -0,0 +1,97 @@ +#include "context_builder.h" +#include "mimi_config.h" +#include "memory/memory_store.h" + +#include +#include +#include +#include "esp_log.h" +#include "cJSON.h" + +static const char *TAG = "context"; + +static size_t append_file(char *buf, size_t size, size_t offset, const char *path, const char *header) +{ + FILE *f = fopen(path, "r"); + if (!f) return offset; + + if (header && offset < size - 1) { + offset += snprintf(buf + offset, size - offset, "\n## %s\n\n", header); + } + + size_t n = fread(buf + offset, 1, size - offset - 1, f); + offset += n; + buf[offset] = '\0'; + fclose(f); + return offset; +} + +esp_err_t context_build_system_prompt(char *buf, size_t size) +{ + size_t off = 0; + + /* Identity header */ + time_t now; + time(&now); + struct tm tm; + localtime_r(&now, &tm); + char time_str[64]; + strftime(time_str, sizeof(time_str), "%Y-%m-%d %H:%M (%A)", &tm); + + off += snprintf(buf + off, size - off, + "# MimiClaw\n\n" + "You are MimiClaw, a personal AI assistant running on an ESP32-S3 device.\n" + "You communicate through Telegram and WebSocket.\n\n" + "## Current Time\n%s\n\n" + "Be helpful, accurate, and concise.\n", + time_str); + + /* Bootstrap files */ + off = append_file(buf, size, off, MIMI_SOUL_FILE, "Personality"); + off = append_file(buf, size, off, MIMI_USER_FILE, "User Info"); + + /* Long-term memory */ + char mem_buf[4096]; + if (memory_read_long_term(mem_buf, sizeof(mem_buf)) == ESP_OK && mem_buf[0]) { + off += snprintf(buf + off, size - off, "\n## Long-term Memory\n\n%s\n", mem_buf); + } + + /* Recent daily notes (last 3 days) */ + char recent_buf[4096]; + if (memory_read_recent(recent_buf, sizeof(recent_buf), 3) == ESP_OK && recent_buf[0]) { + off += snprintf(buf + off, size - off, "\n## Recent Notes\n\n%s\n", recent_buf); + } + + ESP_LOGI(TAG, "System prompt built: %d bytes", (int)off); + return ESP_OK; +} + +esp_err_t context_build_messages(const char *history_json, const char *user_message, + char *buf, size_t size) +{ + /* Parse existing history */ + cJSON *history = cJSON_Parse(history_json); + if (!history) { + history = cJSON_CreateArray(); + } + + /* Append current user message */ + cJSON *user_msg = cJSON_CreateObject(); + cJSON_AddStringToObject(user_msg, "role", "user"); + cJSON_AddStringToObject(user_msg, "content", user_message); + cJSON_AddItemToArray(history, user_msg); + + /* Serialize */ + char *json_str = cJSON_PrintUnformatted(history); + cJSON_Delete(history); + + if (json_str) { + strncpy(buf, json_str, size - 1); + buf[size - 1] = '\0'; + free(json_str); + } else { + snprintf(buf, size, "[{\"role\":\"user\",\"content\":\"%s\"}]", user_message); + } + + return ESP_OK; +} diff --git a/main/agent/context_builder.h b/main/agent/context_builder.h new file mode 100644 index 0000000..2a53724 --- /dev/null +++ b/main/agent/context_builder.h @@ -0,0 +1,25 @@ +#pragma once + +#include "esp_err.h" +#include + +/** + * Build the system prompt from bootstrap files (SOUL.md, USER.md) + * and memory context (MEMORY.md + recent daily notes). + * + * @param buf Output buffer (caller allocates, recommend MIMI_CONTEXT_BUF_SIZE) + * @param size Buffer size + */ +esp_err_t context_build_system_prompt(char *buf, size_t size); + +/** + * Build the complete messages JSON array for LLM call. + * Combines session history + current user message. + * + * @param history_json JSON array from session_get_history_json() + * @param user_message Current user message text + * @param buf Output buffer + * @param size Buffer size + */ +esp_err_t context_build_messages(const char *history_json, const char *user_message, + char *buf, size_t size);