Files
tai/taicpp/tests/agent_memstore.h
T
retoor 4eff597be7 sessions: vault discovery tools, shared stdin, ctrl-key fixes
New sessions/session_export tools plus a sessions_block vault map in the system prompt (tai.py, ported to detz); recall/search carry a session footer instead of letting the model grep sealed bytes. write_file refuses new markdown at working/repo roots, and the blanket *.md gitignore is gone so docs stay visible. One LineInput owns fd 0 for every prompt (ask, approvals, shell_ask, secrets), ending phantom-empty answers; Ctrl-C re-prompts (double exits), Ctrl-D exits, Ctrl-Z suspends; mid-turn Ctrl-C announces itself and kills foreground shell children via RunInterrupted. Covered by SessionsTests, repl_keys.py pty checks, agent_diff sessions_block cases.
2026-10-28 13:11:45 +01:00

378 lines
18 KiB
C++

#pragma once
// In-memory StorePort used by the agent tests. It implements the Store
// behaviour the covered scenarios exercise (sessions, systems, bots, secrets,
// redaction, schedules, audit, file records, events, turn state, records);
// FTS-backed searches are not emulated and throw.
#include "../src/agent/ports.h"
#include "../src/agent/text.h"
#include "../src/llm/helpers.h"
#include "../src/llm/py.h"
#include <algorithm>
#include <map>
#include <random>
namespace detz::agent::testing {
class MemStore : public StorePort {
public:
struct Event { std::string profile, role, kind, text; };
struct Audit {
int64_t id; std::string profile, ts, actor, action, path, message, tags;
std::optional<int64_t> old_size, new_size;
std::optional<std::string> old_text, new_text;
};
std::vector<Event> events;
std::vector<Audit> audits;
std::map<std::string, std::string> file_records; // path -> content (kind file)
std::map<std::string, Value> dir_defaults_log;
std::string profile() const override { return profile_; }
void set_profile(const std::string& name) override { profile_ = name; }
bool seal_enabled() const override { return seal_; }
bool fts_ok() const override { return false; }
void set_seal_enabled(bool v) { seal_ = v; }
std::string redact(const std::string& text, const std::string& profile) override {
std::string out = text;
auto& vault = secrets_[scope(profile)];
std::vector<std::pair<std::string, std::string>> items;
for (const auto& n : vault.order) {
const auto& value = vault.values.at(n).value;
if (py::len(value) >= 4) { items.emplace_back(n, value); }
}
std::stable_sort(items.begin(), items.end(), [](const auto& a, const auto& b) { return py::len(a.second) > py::len(b.second); });
for (const auto& it : items) {
if (out.find(it.second) != std::string::npos) { out = py::replace(out, it.second, "[redacted:" + it.first + "]"); }
}
return out;
}
void log_event(const std::string& profile, const std::string& role, const std::string& kind, const std::string& text) override {
events.push_back({scope(profile), role, kind, text});
}
Value load_session(const std::string& name, const std::string& bot, const std::string& ctx) override {
Value out = Value::array();
auto it = sessions_.find(name + "|" + bot + "|" + ctx);
if (it == sessions_.end()) { return out; }
for (const auto& m : it->second.items()) {
const Value& r = m.get("role");
if (m.is_object() && r.is_string() && (r.str() == "user" || r.str() == "assistant" || r.str() == "tool")) { out.push(m); }
}
return out;
}
void save_session(const std::string& name, const Value& messages, const std::string& bot, const std::string& ctx) override {
Value kept = Value::array();
for (const auto& m : messages.items()) {
if (!(m.get("role").is_string() && m.get("role").str() == "system")) { kept.push(m); }
}
sessions_[name + "|" + bot + "|" + ctx] = Value::loads(kept.dumps());
}
Value list_sessions(const std::string& profile, int64_t limit) override {
Value out = Value::array();
std::string want = scope(profile);
std::vector<std::string> keys;
for (const auto& kv : sessions_) { keys.push_back(kv.first); }
std::sort(keys.begin(), keys.end());
int64_t keep = std::max<int64_t>(1, std::min<int64_t>(limit, 50));
int64_t shown = 0;
for (const auto& key : keys) {
size_t a = key.find('|');
size_t b = key.find('|', a + 1);
if (a == std::string::npos || b == std::string::npos || key.substr(0, a) != want) { continue; }
if (shown++ >= keep) { break; }
Value o = Value::object();
o.set("bot", key.substr(a + 1, b - a - 1));
o.set("sid", key.substr(b + 1));
o.set("path", "(memstore)");
o.set("mtime", 0.0);
o.set("messages", static_cast<int>(sessions_.at(key).size()));
o.set("goal", "");
out.push(o);
}
return out;
}
std::optional<std::string> load_system(const std::string& name) override {
auto it = systems_.find(name);
if (it == systems_.end()) { return std::nullopt; }
return it->second;
}
void save_system(const std::string& name, const std::string& text) override { systems_[name] = text; }
std::optional<std::string> load_bot_system(const std::string& profile, const std::string& bot) override {
if (bot == "main") { return load_system(profile); }
auto it = bot_systems_.find(profile + "|" + bot);
if (it == bot_systems_.end()) { return std::nullopt; }
return it->second;
}
void save_bot_system(const std::string& profile, const std::string& bot, const std::string& text) override {
bot_systems_[profile + "|" + bot] = text;
}
Value load_bots(const std::string& profile) override {
auto it = bots_.find(profile);
return it == bots_.end() ? Value::object() : Value::loads(it->second.dumps());
}
void save_bots(const std::string& profile, const Value& bots) override { bots_[profile] = Value::loads(bots.dumps()); }
Value list_bots(const std::string& profile) override {
Value bots = load_bots(profile);
Value found = Value::array();
Value main = Value::object();
main.set("name", "main");
main.set("nicknames", Value::array());
found.push(main);
std::vector<std::string> names;
for (const auto& kv : bots.members()) { names.push_back(kv.first); }
std::sort(names.begin(), names.end());
for (const auto& n : names) {
Value b = Value::object();
b.set("name", n);
const Value& nick = bots.get(n).get("nicknames");
b.set("nicknames", nick.is_array() ? nick : Value::array());
found.push(b);
}
return found;
}
std::optional<std::string> resolve_bot(const std::string& profile, const std::string& mention) override {
std::string want = py::lower(py::strip(mention));
if (want.empty() || want == "main") { return "main"; }
Value bots = load_bots(profile);
if (bots.has(want)) { return want; }
std::vector<std::string> names;
for (const auto& kv : bots.members()) { names.push_back(kv.first); }
std::sort(names.begin(), names.end());
for (const auto& n : names) {
for (const auto& nick : bots.get(n).get("nicknames").items()) { if (nick.is_string() && nick.str() == want) { return n; } }
}
return std::nullopt;
}
Value load_turn_state(const std::string& profile, const std::string& bot, const std::string&) override {
auto it = turns_.find(profile + "|" + bot);
return it == turns_.end() ? Value::object() : it->second;
}
void save_turn_state(const std::string& profile, const std::string& bot, const Value& state, const std::string&) override {
turns_[profile + "|" + bot] = Value::loads(state.dumps());
}
void save_dir_defaults(const std::string& directory, const std::optional<std::string>& provider,
const std::optional<std::string>& model, const std::optional<std::string>& bot,
const std::vector<std::string>& clear) override {
Value o = Value::object();
o.set("provider", provider ? Value(*provider) : Value());
o.set("model", model ? Value(*model) : Value());
o.set("bot", bot ? Value(*bot) : Value());
Value c = Value::array();
for (const auto& x : clear) { c.push(x); }
o.set("clear", c);
dir_defaults_log[directory] = o;
}
std::string add_record(const std::string& kind, const std::string& title, const std::string& content,
const std::vector<std::string>& tags, const std::string& profile) override {
std::string id = "mem:" + py::uuid4_hex().substr(0, 16);
Rec r{id, kind, title, content, {"record"}, 0};
for (const auto& t : normalize_tags(tags)) { if (std::find(r.tags.begin(), r.tags.end(), t) == r.tags.end()) { r.tags.push_back(t); } }
records_[scope(profile)].push_back(r);
return id;
}
void add_chunks(const std::string& kind, const std::string& id, const std::vector<std::string>& texts, const std::string& profile) override {
chunks_[scope(profile) + "|" + kind + "|" + id] = texts;
}
Value get_chunks_by_ids(const std::vector<int64_t>&, const std::string&) override { return Value::object(); }
Value read_record(const std::string& id, int64_t offset, int64_t limit, const std::string& profile) override {
for (auto& r : records_[scope(profile)]) {
if (r.id != id) { continue; }
int64_t total = static_cast<int64_t>(py::len(r.content));
int64_t start = std::max<int64_t>(0, std::min(offset, total));
int64_t end = std::min(total, start + std::max<int64_t>(1, limit));
r.reads++;
Value o = Value::object();
o.set("id", id);
o.set("slice", py::slice(r.content, start, end));
o.set("start", start); o.set("end", end); o.set("total", total);
Value t = Value::array();
for (const auto& x : r.tags) { t.push(x); }
o.set("tags", t);
return o;
}
return Value();
}
Value search_records(const std::string&, const std::optional<std::string>&, const std::vector<std::string>&, int64_t, const std::string&) override {
throw std::runtime_error("MemStore: search_records unsupported");
}
bool delete_record(const std::string& id, const std::string& profile) override {
auto& v = records_[scope(profile)];
for (auto it = v.begin(); it != v.end(); ++it) {
if (it->id == id) { v.erase(it); return true; }
}
return false;
}
std::vector<std::string> item_tags(const std::string& item, const std::string& profile) override {
for (const auto& r : records_[scope(profile)]) { if (r.id == item) { return r.tags; } }
return {};
}
std::vector<std::pair<std::string, int64_t>> tag_counts(const std::string&, int64_t, const std::string&) override { return {}; }
std::string add_edge(const std::string&, const std::string&, const Value&, const std::string&) override {
throw std::runtime_error("MemStore: add_edge unsupported");
}
Value edges_for(const std::string&, const std::string&) override { return Value::array(); }
bool has_node(const std::string&, const std::string&, const std::string&) override { return false; }
std::string node_title(const std::string& node, const std::string&) override { return node; }
Value traverse(const std::string&, int64_t, int64_t, const std::string&) override { return Value::array(); }
Value fts_search(const std::string&, const std::vector<std::string>&, const std::string&, int64_t) override { return Value::array(); }
Value events_like(const std::string&, const std::string&, int64_t) override { return Value::array(); }
Value search_events(const std::string&, const std::string&, const std::vector<std::string>&) override { return Value::array(); }
Value audit_search(const std::string&, const std::string&, int64_t) override { return Value::array(); }
int64_t audit_event(const std::string& actor, const std::string& action, const std::string& path, const std::string& message,
const std::optional<std::string>& old_text, const std::optional<std::string>& new_text,
const std::vector<std::string>& tags, const std::optional<int64_t>& old_size,
const std::optional<int64_t>& new_size, const std::string& profile) override {
Capped o = capped_text(old_text), n = capped_text(new_text);
std::optional<std::string> ot = o.text, nt = n.text;
std::optional<int64_t> os = o.size, ns = n.size;
if (old_text.has_value() && old_size.has_value() && *old_size > static_cast<int64_t>(old_text->size())) {
ot = py::head(*old_text, 20000) + trunc_marker(*old_size);
os = old_size;
}
if (new_text.has_value() && new_size.has_value() && *new_size > static_cast<int64_t>(new_text->size())) {
nt = py::head(*new_text, 20000) + trunc_marker(*new_size);
ns = new_size;
}
Audit a;
a.id = static_cast<int64_t>(audits.size()) + 1;
a.profile = scope(profile);
a.ts = py::utc_now_iso();
a.actor = actor; a.action = action; a.path = path;
a.message = py::head(message, 2000);
a.tags = py::join(normalize_tags(tags), " ");
a.old_size = os; a.new_size = ns; a.old_text = ot; a.new_text = nt;
audits.push_back(a);
return a.id;
}
Value audit_history(const std::optional<std::string>& path, const std::optional<std::string>& tag, int64_t limit,
const std::string& profile) override {
Value out = Value::array();
int64_t lim = std::max<int64_t>(1, std::min<int64_t>(limit, 100));
for (auto it = audits.rbegin(); it != audits.rend() && static_cast<int64_t>(out.size()) < lim; ++it) {
if (it->profile != scope(profile)) { continue; }
if (path.has_value() && it->path != *path) { continue; }
if (tag.has_value() && it->tags.find(*tag) == std::string::npos) { continue; }
out.push(audit_row(*it, false));
}
return out;
}
Value audit_get(int64_t row_id, const std::string& profile) override {
for (const auto& a : audits) { if (a.id == row_id && a.profile == scope(profile)) { return audit_row(a, true); } }
return Value();
}
void upsert_file_record(const std::string& path, const std::string& content, const std::optional<int64_t>&, const std::string&) override {
file_records[path] = content;
}
void delete_file_record(const std::string& path, const std::string&) override { file_records.erase(path); }
std::optional<std::string> load_secret(const std::string& name, const std::string& profile) override {
auto& v = secrets_[scope(profile)];
auto it = v.values.find(name);
if (it == v.values.end()) { return std::nullopt; }
return it->second.value;
}
void save_secret(const std::string& name, const std::string& value, const Value& meta, const std::vector<std::string>& tags,
const std::string& profile) override {
auto& v = secrets_[scope(profile)];
if (!v.values.count(name)) { v.order.push_back(name); }
std::vector<std::string> all = {"secret"};
for (const auto& t : normalize_tags(tags)) { if (std::find(all.begin(), all.end(), t) == all.end()) { all.push_back(t); } }
v.values[name] = {value, meta, all};
}
Value list_secret_infos(const std::string& profile) override {
auto& v = secrets_[scope(profile)];
std::vector<std::string> names = v.order;
std::sort(names.begin(), names.end());
Value out = Value::array();
for (const auto& n : names) {
Value o = Value::object();
o.set("name", n);
o.set("meta", v.values.at(n).meta);
Value t = Value::array();
std::vector<std::string> sorted_tags = v.values.at(n).tags;
std::sort(sorted_tags.begin(), sorted_tags.end());
for (const auto& x : sorted_tags) { t.push(x); }
o.set("tags", t);
o.set("updated", "");
out.push(o);
}
return out;
}
void delete_secret(const std::string& name, const std::string& profile) override {
auto& v = secrets_[scope(profile)];
v.values.erase(name);
v.order.erase(std::remove(v.order.begin(), v.order.end(), name), v.order.end());
}
int64_t add_schedule(const std::string& name, const std::string& prompt, const std::string& profile, int64_t every_sec,
const std::string& next_run, int64_t timeout, const std::vector<std::string>& tags) override {
Value o = Value::object();
int64_t id = ++sched_id_;
o.set("id", id); o.set("name", name); o.set("prompt", prompt); o.set("profile", profile);
o.set("every", every_sec); o.set("next_run", next_run); o.set("timeout", timeout);
o.set("status", "pending"); o.set("last", ""); o.set("result", "");
Value t = Value::array();
t.push("schedule");
for (const auto& x : tags) { t.push(x); }
o.set("tags", t);
schedules_.push_back(o);
return id;
}
void remove_schedule(int64_t row_id, const std::string& profile) override {
for (auto it = schedules_.begin(); it != schedules_.end(); ++it) {
if (it->get("id").as_int() == row_id && it->get("profile").str() == scope(profile)) { schedules_.erase(it); return; }
}
}
Value list_schedules(const std::string& profile) override {
Value out = Value::array();
std::vector<Value> rows;
for (const auto& s : schedules_) { if (s.get("profile").str() == scope(profile)) { rows.push_back(s); } }
std::stable_sort(rows.begin(), rows.end(), [](const Value& a, const Value& b) { return a.get("next_run").str() < b.get("next_run").str(); });
for (const auto& r : rows) { out.push(r); }
return out;
}
Value mcp_list_servers() override { return Value::array(); }
Value mcp_get_server(const std::string&) override { return Value(); }
void mcp_save_server(const Value&) override {}
void mcp_remove_server(const std::string&) override {}
void mcp_set_enabled(const std::string&, const std::string&, bool) override {}
std::vector<std::string> mcp_enabled_profiles(const std::string&) override { return {}; }
std::vector<std::string> mcp_server_profiles(const std::string&) override { return {}; }
void mcp_store_tools(const std::string&, const Value&) override {}
Value mcp_cached_tools(const std::string&) override { return Value::array(); }
std::vector<std::string> mcp_tool_pins(const std::string&) override { return {}; }
Value sql_rows(const std::string&, const Value&) override { throw std::runtime_error("MemStore: sql unsupported"); }
private:
struct Rec { std::string id, kind, title, content; std::vector<std::string> tags; int64_t reads; };
struct Sec { std::string value; Value meta; std::vector<std::string> tags; };
struct Vault { std::vector<std::string> order; std::map<std::string, Sec> values; };
std::string scope(const std::string& p) const { return p.empty() ? profile_ : p; }
Value audit_row(const Audit& a, bool full) const {
Value o = Value::object();
o.set("id", a.id); o.set("ts", a.ts); o.set("actor", a.actor); o.set("action", a.action); o.set("path", a.path);
o.set("message", a.message);
o.set("old_size", a.old_size ? Value(*a.old_size) : Value());
o.set("new_size", a.new_size ? Value(*a.new_size) : Value());
if (full) {
o.set("old", a.old_text ? Value(*a.old_text) : Value());
o.set("new", a.new_text ? Value(*a.new_text) : Value());
}
o.set("tags", a.tags);
return o;
}
std::string profile_{"default"};
bool seal_{true};
std::map<std::string, Value> sessions_, turns_, bots_;
std::map<std::string, std::string> systems_, bot_systems_;
std::map<std::string, std::vector<Rec>> records_;
std::map<std::string, std::vector<std::string>> chunks_;
std::map<std::string, Vault> secrets_;
std::vector<Value> schedules_;
int64_t sched_id_{0};
};
} // namespace detz::agent::testing