From f8423ebea413605b42de23f5dd06c5479d3fa1dd Mon Sep 17 00:00:00 2001 From: Ethan Werner Date: Sat, 10 Oct 2026 20:55:33 +0000 Subject: initial commit --- src/message.h | 367 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 src/message.h (limited to 'src/message.h') diff --git a/src/message.h b/src/message.h new file mode 100644 index 0000000..6ff53f6 --- /dev/null +++ b/src/message.h @@ -0,0 +1,367 @@ +#ifndef LLSH_MESSAGE_H +#define LLSH_MESSAGE_H + +#include + +#include +#include +#include + +/* ------------------------------------------------------------------ */ +/* Message model (mirrors the rig message types llsh uses) */ +/* */ +/* message = system_message { text } */ +/* | user_message { parts: [user_part] } */ +/* | assistant_message { parts: [assistant_part] } */ +/* user_part = user_text_part { text } */ +/* | user_tool_result_part { call, name, */ +/* content: [trc] } */ +/* trc = trc_text { text } | trc_json { value } | trc_image */ +/* assistant_part = asst_text_part { text } */ +/* | asst_tool_call_part { name, args } */ +/* | asst_reasoning_part { text } */ +/* | asst_image_part */ +/* */ +/* JSON shape (recursive, nlohmann ADL below): each type serializes */ +/* to exactly this wire format and the `role`/`type` discriminators */ +/* keep old session files loadable. */ +/* ------------------------------------------------------------------ */ + +/* -- tool result content -- */ + +struct trc_text { + std::string text; +}; +struct trc_json { + std::string value; +}; +struct trc_image { +}; +using tool_result_content = std::variant; + +/* -- user parts -- */ + +struct user_text_part { + std::string text; +}; +struct user_tool_result_part { + std::string call; /* tool-call id */ + std::string name; /* tool name */ + std::vector content; +}; +using user_part = std::variant; + +/* -- assistant parts -- */ + +struct asst_text_part { + std::string text; +}; +struct asst_tool_call_part { + std::string name; + std::string args; /* stringified arguments */ +}; +struct asst_reasoning_part { + std::string text; +}; +struct asst_image_part { +}; +using assistant_part = + std::variant; + +/* -- messages -- */ + +struct system_message { + std::string text; +}; +struct user_message { + std::vector parts; +}; +struct assistant_message { + std::vector parts; +}; + +struct message { + std::variant body; + + bool is_system() const { + return std::holds_alternative(body); + } + bool is_user() const { + return std::holds_alternative(body); + } + bool is_assistant() const { + return std::holds_alternative(body); + } + + system_message &system() { return std::get(body); } + const system_message &system() const { + return std::get(body); + } + user_message &user() { return std::get(body); } + const user_message &user() const { + return std::get(body); + } + assistant_message &assistant() { + return std::get(body); + } + const assistant_message &assistant() const { + return std::get(body); + } +}; + +/* Constructors (fresh message, content copied). */ +inline message msg_system(const char *text) { + system_message m; + m.text = text ? text : ""; + return message{std::move(m)}; +} +inline message msg_user_text(const char *text) { + user_message m; + user_text_part p; + p.text = text ? text : ""; + m.parts.push_back(std::move(p)); + return message{std::move(m)}; +} +inline message msg_user_tool_result(const char *call, const char *name, + const char *text) { + user_message m; + user_tool_result_part p; + p.call = call ? call : ""; + p.name = name ? name : ""; + trc_text t; + t.text = text ? text : ""; + p.content.push_back(std::move(t)); + m.parts.push_back(std::move(p)); + return message{std::move(m)}; +} +inline message msg_assistant_text(const char *text) { + assistant_message m; + asst_text_part p; + p.text = text ? text : ""; + m.parts.push_back(std::move(p)); + return message{std::move(m)}; +} +inline message msg_assistant_tool_call(const char *name, const char *args) { + assistant_message m; + asst_tool_call_part p; + p.name = name ? name : ""; + p.args = args ? args : ""; + m.parts.push_back(std::move(p)); + return message{std::move(m)}; +} + +/* Append parts to an existing message. */ +inline message &msg_push_user_text(message &m, const char *text) { + if (!m.is_user()) + m.body = user_message{}; + user_text_part p; + p.text = text ? text : ""; + m.user().parts.push_back(std::move(p)); + return m; +} +inline message &msg_push_user_tool_result(message &m, const char *call, + const char *name, const char *text) { + if (!m.is_user()) + m.body = user_message{}; + user_tool_result_part p; + p.call = call ? call : ""; + p.name = name ? name : ""; + trc_text t; + t.text = text ? text : ""; + p.content.push_back(std::move(t)); + m.user().parts.push_back(std::move(p)); + return m; +} +inline message &msg_push_assistant_text(message &m, const char *text) { + if (!m.is_assistant()) + m.body = assistant_message{}; + asst_text_part p; + p.text = text ? text : ""; + m.assistant().parts.push_back(std::move(p)); + return m; +} +inline message &msg_push_assistant_tool_call(message &m, const char *name, + const char *args) { + if (!m.is_assistant()) + m.body = assistant_message{}; + asst_tool_call_part p; + p.name = name ? name : ""; + p.args = args ? args : ""; + m.assistant().parts.push_back(std::move(p)); + return m; +} +inline message &msg_push_assistant_reasoning(message &m, const char *text) { + if (!m.is_assistant()) + m.body = assistant_message{}; + asst_reasoning_part p; + p.text = text ? text : ""; + m.assistant().parts.push_back(std::move(p)); + return m; +} + +/* ------------------------------------------------------------------ */ +/* History (growable message list) */ +/* ------------------------------------------------------------------ */ + +struct history { + std::vector msgs; +}; + +/* Destroy the stored messages and the vector buffer; the `history` object + * (wherever it lives) must not be used afterwards. */ +inline void history_free(history *h) { h->msgs.~vector(); } +inline void history_push(history *h, message m) { + h->msgs.push_back(std::move(m)); +} +/* Remove and return element i. */ +inline message history_take(history *h, size_t i) { + message out = std::move(h->msgs[i]); + h->msgs.erase(h->msgs.begin() + static_cast(i)); + return out; +} +inline size_t history_len(const history *h) { return h->msgs.size(); } +inline const message *history_at(const history *h, size_t i) { + return &h->msgs[i]; +} + +/* ------------------------------------------------------------------ */ +/* Recursive JSON (nlohmann ADL: to_json / from_json) */ +/* ------------------------------------------------------------------ */ + +/* Tolerant string read: missing / null / non-string values become "". */ +inline std::string jstring(const nlohmann::json &j, const char *key) { + if (j.contains(key) && j.at(key).is_string()) + return j.at(key).get(); + return std::string(); +} + +inline void to_json(nlohmann::json &j, const tool_result_content &v) { + if (const auto *t = std::get_if(&v)) + j = nlohmann::json{{"text", t->text}}; + else if (const auto *s = std::get_if(&v)) + j = nlohmann::json{{"json", s->value}}; + else + j = nlohmann::json{{"image", "1"}}; +} +inline void from_json(const nlohmann::json &j, tool_result_content &v) { + if (j.contains("text")) { + trc_text t; + t.text = jstring(j, "text"); + v = std::move(t); + } else if (j.contains("json")) { + trc_json s; + s.value = jstring(j, "json"); + v = std::move(s); + } else { + v = trc_image{}; + } +} + +inline void to_json(nlohmann::json &j, const user_part &p) { + if (const auto *t = std::get_if(&p)) + j = nlohmann::json{{"type", "text"}, {"text", t->text}}; + else { + const auto &r = std::get(p); + nlohmann::json arr = nlohmann::json::array(); + for (const auto &c : r.content) + arr.push_back(c); + j = nlohmann::json{{"type", "tool_result"}, + {"call", r.call}, + {"name", r.name}, + {"content", std::move(arr)}}; + } +} +inline void from_json(const nlohmann::json &j, user_part &p) { + const std::string type = jstring(j, "type"); + if (type == "text") { + user_text_part t; + t.text = jstring(j, "text"); + p = std::move(t); + } else if (type == "tool_result") { + user_tool_result_part r; + r.call = jstring(j, "call"); + r.name = jstring(j, "name"); + if (j.contains("content") && j.at("content").is_array()) + for (const auto &c : j.at("content")) + r.content.push_back(c.get()); + p = std::move(r); + } else { + user_text_part t; + t.text = jstring(j, "text"); + p = std::move(t); + } +} + +inline void to_json(nlohmann::json &j, const assistant_part &p) { + if (const auto *t = std::get_if(&p)) + j = nlohmann::json{{"type", "text"}, {"text", t->text}}; + else if (const auto *c = std::get_if(&p)) + j = nlohmann::json{{"type", "tool_call"}, + {"name", c->name}, + {"args", c->args}}; + else if (const auto *r = std::get_if(&p)) + j = nlohmann::json{{"type", "reasoning"}, {"text", r->text}}; + else + j = nlohmann::json::object(); +} +inline void from_json(const nlohmann::json &j, assistant_part &p) { + const std::string type = jstring(j, "type"); + if (type == "text") { + asst_text_part t; + t.text = jstring(j, "text"); + p = std::move(t); + } else if (type == "tool_call") { + asst_tool_call_part c; + c.name = jstring(j, "name"); + c.args = jstring(j, "args"); + p = std::move(c); + } else if (type == "reasoning") { + asst_reasoning_part r; + r.text = jstring(j, "text"); + p = std::move(r); + } else { + p = asst_image_part{}; + } +} + +inline void to_json(nlohmann::json &j, const message &m) { + if (m.is_system()) + j = nlohmann::json{{"role", "system"}, {"text", m.system().text}}; + else if (m.is_user()) { + nlohmann::json parts = nlohmann::json::array(); + for (const auto &p : m.user().parts) + parts.push_back(p); + j = nlohmann::json{{"role", "user"}, {"parts", std::move(parts)}}; + } else { + nlohmann::json parts = nlohmann::json::array(); + for (const auto &p : m.assistant().parts) + parts.push_back(p); + j = + nlohmann::json{{"role", "assistant"}, {"parts", std::move(parts)}}; + } +} +inline void from_json(const nlohmann::json &j, message &m) { + const std::string role = jstring(j, "role"); + if (role == "system") { + system_message s; + s.text = jstring(j, "text"); + m.body = std::move(s); + } else if (role == "assistant") { + assistant_message a; + if (j.contains("parts") && j.at("parts").is_array()) + for (const auto &p : j.at("parts")) + if (p.is_object()) + a.parts.push_back(p.get()); + m.body = std::move(a); + } else { + user_message u; + if (j.contains("parts") && j.at("parts").is_array()) + for (const auto &p : j.at("parts")) + if (p.is_object()) + u.parts.push_back(p.get()); + m.body = std::move(u); + } +} + +#endif -- cgit v1.3.1