feat: add agent loop and context builder
Agent loop runs on Core 1: pops inbound messages, builds system prompt from bootstrap files + memory, calls Claude API, saves session, pushes response to outbound queue. PSRAM-allocated buffers. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
99
main/agent/agent_loop.c
Normal file
99
main/agent/agent_loop.c
Normal file
@@ -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 <string.h>
|
||||
#include <stdlib.h>
|
||||
#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;
|
||||
}
|
||||
14
main/agent/agent_loop.h
Normal file
14
main/agent/agent_loop.h
Normal file
@@ -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);
|
||||
97
main/agent/context_builder.c
Normal file
97
main/agent/context_builder.c
Normal file
@@ -0,0 +1,97 @@
|
||||
#include "context_builder.h"
|
||||
#include "mimi_config.h"
|
||||
#include "memory/memory_store.h"
|
||||
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <time.h>
|
||||
#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;
|
||||
}
|
||||
25
main/agent/context_builder.h
Normal file
25
main/agent/context_builder.h
Normal file
@@ -0,0 +1,25 @@
|
||||
#pragma once
|
||||
|
||||
#include "esp_err.h"
|
||||
#include <stddef.h>
|
||||
|
||||
/**
|
||||
* 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);
|
||||
Reference in New Issue
Block a user