Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
entropic_server.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
24
25#include <nlohmann/json.hpp>
26
27#include <algorithm>
28#include <chrono>
29#include <cstdlib>
30#include <mutex>
31#include <string>
32#include <vector>
33
34static auto logger = entropic::log::get("mcp.entropic");
35
36namespace entropic {
37
38// ── TodoItem ────────────────────────────────────────────────────
39
45struct TodoItem {
46 std::string content;
47 std::string status;
48};
49
50// ── TodoTool ────────────────────────────────────────────────────
51
65class TodoTool : public ToolBase {
66public:
75
83 ServerResponse execute(const std::string& args_json) override;
84
92 std::string anchor_key(
93 const std::string& args_json) const override;
94
102 bool release(const std::string& key) { return lists_.release(key); }
103
110 std::size_t session_count() const { return lists_.session_count(); }
111
112private:
117};
118
127 const std::string& /*args_json*/) const {
128 return "todo_state";
129}
130
131namespace {
132
140std::string format_todo_list(const std::vector<TodoItem>& items) {
141 if (items.empty()) {
142 return "(empty)";
143 }
144 std::string out;
145 for (size_t i = 0; i < items.size(); ++i) {
146 out += std::to_string(i) + ". [" +
147 items[i].status + "] " +
148 items[i].content + "\n";
149 }
150 return out;
151}
152
162std::string todo_index_error(const std::vector<TodoItem>& items,
163 const std::string& action, long idx) {
164 std::string err;
165 if (0 > idx || idx >= static_cast<long>(items.size())) {
166 logger->warn("[todo] {} #{}: out of range (list has {})",
167 action, idx, items.size());
168 err = tool_error_json("out_of_range", "index " + std::to_string(idx)
169 + " is not in the list, which has " + std::to_string(items.size())
170 + " item(s) (indices start at 0)");
171 }
172 return err;
173}
174
190std::string apply_todo_action(std::vector<TodoItem>& items,
191 const std::string& action,
192 const nlohmann::json& args) {
193 std::string err;
194 const long idx = args.value<long>("index", -1);
195 if (action == "add") {
196 std::string content = args.at("content").get<std::string>();
197 items.push_back({content, args.value("status", std::string("pending"))});
198 logger->info("[todo] add: {}", content);
199 } else if (action != "update" && action != "remove") {
200 err = tool_error_json("invalid_args", "unknown action '" + action
201 + "' (add, update or remove)");
202 } else if (err = todo_index_error(items, action, idx); err.empty()) {
203 auto& item = items[static_cast<size_t>(idx)];
204 if (action == "update") {
205 item.status = args.value("status", item.status);
206 item.content = args.value("content", item.content);
207 logger->info("[todo] update #{}: {}", idx, item.status);
208 } else {
209 logger->info("[todo] remove #{}", idx);
210 items.erase(items.begin() + idx);
211 }
212 }
213 return err;
214}
215
216} // namespace
217
235 auto args = nlohmann::json::parse(args_json);
236 std::string action = args.at("action").get<std::string>();
237
238 const auto session = current_run_session();
239 std::string err;
240 std::string rendered = lists_.with(session, [&](auto& items) {
242 return format_todo_list(items);
243 });
244 logger->info("[todo] session='{}' action={}", session, action);
245 if (!err.empty()) { return {err, {}}; }
246
247 nlohmann::json result;
248 result["action"] = action;
249 result["todo_state"] = rendered;
250
255 return {result.dump(), {anchor_d, notify_d}};
256}
257
258// ── DelegateTool ────────────────────────────────────────────────
259
265class DelegateTool : public ToolBase {
266public:
275 const std::vector<std::string>& tier_names,
276 std::vector<std::string> require_context_tiers = {});
277
287 ServerResponse execute(const std::string& args_json) override;
288
289private:
291 std::vector<std::string> require_context_tiers_;
292};
293
312 const std::vector<std::string>& tier_names,
313 std::vector<std::string> require_context_tiers)
314 : ToolBase(std::move(def)),
315 require_context_tiers_(std::move(require_context_tiers)) {
316
317 auto schema = nlohmann::json::parse(definition_.input_schema);
318 schema["properties"]["target"]["enum"] = tier_names;
320
321 logger->info("[delegate] patched enum with {} tiers ({} require context)",
322 tier_names.size(), require_context_tiers_.size());
323}
324
338static size_t carry_context(const nlohmann::json& args,
339 nlohmann::json& result) {
340 auto out = nlohmann::json::array();
341 if (args.contains("context") && args["context"].is_array()) {
342 for (const auto& ref : args["context"]) {
343 if (!ref.is_object()) { continue; }
344 auto path = ref.value("path", std::string{});
345 if (path.empty()) { continue; }
346 nlohmann::json entry;
347 entry["path"] = path;
348 entry["lines"] = ref.value("lines", std::string{});
349 entry["note"] = ref.value("note", std::string{});
350 out.push_back(std::move(entry));
351 }
352 }
353 size_t n = out.size();
354 result["context"] = std::move(out);
355 return n;
356}
357
373 auto args = nlohmann::json::parse(args_json);
374 std::string target = args.at("target").get<std::string>();
375 std::string task = args.at("task").get<std::string>();
376 int max_turns = args.value("max_turns", -1);
377
378 nlohmann::json result;
379 result["action"] = "delegate";
380 result["target"] = target;
381 result["task"] = task;
382 result["max_turns"] = max_turns;
383 auto refs = carry_context(args, result);
384
385 // gh#162: a tier that declares requires_context cannot succeed on a
386 // contextless task, so refuse HERE — a tool error the lead can act on
387 // in the same turn, rather than a burned child loop whose transcript
388 // nobody reads.
389 if (refs == 0
390 && std::find(require_context_tiers_.begin(),
391 require_context_tiers_.end(), target)
392 != require_context_tiers_.end()) {
393 logger->warn("[delegate] refused: tier '{}' requires context", target);
394 return {tool_error_json("requires_context", "tier \"" + target
395 + "\" requires context. Re-issue entropic.delegate with a "
396 "`context` array naming at least one file path (optionally "
397 "lines/note) the delegate should read."), {}};
398 }
399
400 logger->info("[delegate] target='{}' task='{}' max_turns={} context={}",
401 target, task, max_turns, refs);
402
405
408
409 return {result.dump(), {delegate_d, stop_d}};
410}
411
412// ── PipelineTool ────────────────────────────────────────────────
413
419class PipelineTool : public ToolBase {
420public:
429 const std::vector<std::string>& tier_names,
430 std::vector<std::string> require_context_tiers = {});
431
439 ServerResponse execute(const std::string& args_json) override;
440
441private:
442 std::vector<std::string> tier_names_;
444 std::vector<std::string> require_context_tiers_;
445};
446
462 const std::vector<std::string>& tier_names,
463 std::vector<std::string> require_context_tiers)
464 : ToolBase(std::move(def)), tier_names_(tier_names),
465 require_context_tiers_(std::move(require_context_tiers)) {
466
467 auto schema = nlohmann::json::parse(definition_.input_schema);
468 schema["properties"]["stages"]["items"]["enum"] = tier_names;
470
471 logger->info("[pipeline] patched enum with {} tiers",
472 tier_names.size());
473}
474
512static std::string pipeline_rejection(
513 const std::vector<std::string>& stages,
514 const std::vector<std::string>& tier_names,
515 const std::vector<std::string>& require_context_tiers,
516 size_t refs) {
517 std::string err;
518 if (stages.size() < 2) {
519 logger->warn("[pipeline] rejected: fewer than 2 stages");
520 err = tool_error_json("too_few_stages", "pipeline requires at least 2 stages");
521 }
522 for (const auto& s : stages) {
523 if (!err.empty()) { break; }
524 if (std::find(tier_names.begin(), tier_names.end(), s)
525 == tier_names.end()) {
526 logger->warn("[pipeline] invalid stage: '{}'", s);
527 err = tool_error_json("unknown_stage", "unknown stage \"" + s
528 + "\". Valid stages: " + nlohmann::json(tier_names).dump());
529 } else if (refs == 0
530 && std::find(require_context_tiers.begin(),
532 != require_context_tiers.end()) {
533 // gh#162: one context list seeds every stage, so a stage that
534 // requires context refuses the whole pipeline — before any
535 // stage runs, not after the first has burned its turns.
536 logger->warn("[pipeline] refused: stage '{}' requires context", s);
537 err = tool_error_json("requires_context", "stage \"" + s
538 + "\" requires context. Re-issue entropic.pipeline with a "
539 "`context` array naming at least one file path the stages "
540 "should read.");
541 }
542 }
543 return err;
544}
545
562 auto args = nlohmann::json::parse(args_json);
563 auto stages = args.at("stages").get<std::vector<std::string>>();
564 std::string task = args.at("task").get<std::string>();
565
566 nlohmann::json result;
567 result["action"] = "pipeline";
568 result["stages"] = stages;
569 result["task"] = task;
570 auto refs = carry_context(args, result); // gh#162
571
572 auto rejection = pipeline_rejection(
573 stages, tier_names_, require_context_tiers_, refs);
574 if (!rejection.empty()) {
575 return {rejection, {}};
576 }
577
578 logger->info("[pipeline] stages={} task='{}' context={}",
579 stages.size(), task, refs);
580
583
586
587 return {result.dump(), {pipeline_d, stop_d}};
588}
589
590// ── CompleteTool ────────────────────────────────────────────────
591
597class CompleteTool : public ToolBase {
598public:
607
615 ServerResponse execute(const std::string& args_json) override;
616};
617
639 auto args = nlohmann::json::parse(args_json);
640 std::string summary = args.at("summary").get<std::string>();
641 bool coverage_gap = args.value("coverage_gap", false);
642 std::string gap_description =
643 args.value("gap_description", std::string{});
644 std::vector<std::string> suggested;
645 if (args.contains("suggested_files")
646 && args["suggested_files"].is_array()) {
647 suggested = args["suggested_files"]
648 .get<std::vector<std::string>>();
649 }
650
651 // Issue #10 (v2.1.4): coverage_gap=true REQUIRES a non-empty
652 // gap_description so the lead context's [COVERAGE GAP] message
653 // tells the next specialist what to fill in. An empty
654 // gap_description with coverage_gap=true would be a bare
655 // "I don't know" with no signal — refuse it at the tool boundary.
656 if (coverage_gap && gap_description.empty()) {
657 nlohmann::json err;
658 err["error"] = "missing_gap_description";
659 err["message"] =
660 "coverage_gap=true requires a non-empty gap_description "
661 "(what's missing from this answer and why).";
662 return {err.dump(), {}};
663 }
664
665 logger->info("[complete] summary='{}' coverage_gap={} "
666 "gap_description_len={} suggested_files={}",
667 summary, coverage_gap,
668 gap_description.size(), suggested.size());
669
670 nlohmann::json result;
671 result["action"] = "complete";
672 result["summary"] = entropic::mcp::sanitize_utf8(summary);
673 result["coverage_gap"] = coverage_gap;
674 result["gap_description"] = entropic::mcp::sanitize_utf8(gap_description);
675 result["suggested_files"] = suggested;
676
679
682
683 return {result.dump(), {complete_d, stop_d}};
684}
685
686// ── PhaseChangeTool ─────────────────────────────────────────────
687
693class PhaseChangeTool : public ToolBase {
694public:
701
709 ServerResponse execute(const std::string& args_json) override;
710};
711
724 def.name = "phase_change";
725 def.description = "Switch inference phase";
726 def.input_schema =
727 R"({"type":"object","properties":{"phase":{"type":"string"}},"required":["phase"]})";
728 def.output_schema =
729 R"({"type":"object","required":["action","phase"],"properties":{"action":{"enum":["phase_change"]},"phase":{"type":"string","description":"the phase requested"}}})";
730 def.description += render_response_format(def.output_schema);
731 return def;
732}
733
741
751 const std::string& args_json) {
752 auto args = nlohmann::json::parse(args_json);
753 std::string phase = args.at("phase").get<std::string>();
754
755 logger->info("[phase_change] phase='{}'", phase);
756
757 nlohmann::json result;
758 result["action"] = "phase_change";
759 result["phase"] = phase;
760
763
764 return {result.dump(), {phase_d}};
765}
766
767// ── PruneContextTool ────────────────────────────────────────────
768
775public:
783
790 ServerResponse execute(const std::string& args_json) override;
791};
792
803 const std::string& args_json) {
804 auto args = nlohmann::json::parse(args_json);
805
806 static constexpr int default_keep = 2;
807 int keep_recent = args.value("keep_recent", default_keep);
808
809 logger->info("[prune_context] keep_recent={}", keep_recent);
810
811 nlohmann::json result;
812 result["action"] = "prune_context";
813 result["keep_recent"] = keep_recent;
814
817
818 return {result.dump(), {prune_d}};
819}
820
821// ── DiagnoseTool ────────────────────────────────────────────────
822
828class DiagnoseTool : public ToolBase {
829public:
838
846 ServerResponse execute(const std::string& args_json) override;
847
857 }
858
865 provider_ = p;
866 }
867
868private:
869 const entropic_state_provider_t* provider_ = nullptr;
870};
871
886static std::string call_provider(
887 char* (*fn)(void*), void* ud) {
888 if (fn == nullptr) {
889 return "{}";
890 }
891 char* raw = fn(ud);
892 if (raw == nullptr) {
893 return "{}";
894 }
895 std::string result(raw);
896 free(raw);
897 return result;
898}
899
909static std::string call_history_provider(
910 char* (*fn)(int, void*), int max_entries, void* ud) {
911 if (fn == nullptr) {
912 return "[]";
913 }
914 char* raw = fn(max_entries, ud);
915 if (raw == nullptr) {
916 return "[]";
917 }
918 std::string result(raw);
919 free(raw);
920 return result;
921}
922
932static std::string call_docs_provider(
933 char* (*fn)(const char*, void*),
934 const char* section, void* ud) {
935 if (fn == nullptr) {
936 return "";
937 }
938 char* raw = fn(section, ud);
939 if (raw == nullptr) {
940 return "";
941 }
942 std::string result(raw);
943 free(raw);
944 return result;
945}
946
956static nlohmann::json build_snapshot(
958 bool include_docs, int history_limit) {
959
960 nlohmann::json snap;
961
962 auto now = std::chrono::system_clock::now();
963 auto ms = std::chrono::duration_cast<
964 std::chrono::milliseconds>(
965 now.time_since_epoch()).count();
966 snap["snapshot_timestamp_ms"] = ms;
967
968 snap["engine"] = nlohmann::json::parse(
969 call_provider(p.get_state, p.user_data));
970 snap["config"] = nlohmann::json::parse(
971 call_provider(p.get_config, p.user_data));
972 snap["identities"] = nlohmann::json::parse(
973 call_provider(p.get_identities, p.user_data));
974 snap["tools"] = nlohmann::json::parse(
975 call_provider(p.get_tools, p.user_data));
976 snap["history"] = nlohmann::json::parse(
978 p.get_history, history_limit, p.user_data));
979 snap["metrics"] = nlohmann::json::parse(
980 call_provider(p.get_metrics, p.user_data));
981
982 if (include_docs) {
983 snap["docs"] = call_docs_provider(
984 p.get_docs, nullptr, p.user_data);
985 } else {
986 snap["docs"] = nullptr;
987 }
988 return snap;
989}
990
1007 if (provider_ == nullptr) {
1008 logger->error("[diagnose] no state provider set");
1009 return {tool_error_json("provider_unavailable",
1010 "engine state provider not configured"), {}};
1011 }
1012
1013 auto args = nlohmann::json::parse(args_json);
1014 bool include_docs = args.value("include_docs", false);
1015 int history_limit = args.value("history_limit", 20);
1016
1017 logger->info("[diagnose] include_docs={} history_limit={}",
1019
1020 auto snap = build_snapshot(
1021 *provider_, include_docs, history_limit);
1022 return {snap.dump(), {}};
1023}
1024
1025// ── InspectTool ─────────────────────────────────────────────────
1026
1032class InspectTool : public ToolBase {
1033public:
1042
1050 ServerResponse execute(const std::string& args_json) override;
1051
1060 return MCPAccessLevel::READ;
1061 }
1062
1069 provider_ = p;
1070 }
1071
1072private:
1073 const entropic_state_provider_t* provider_ = nullptr;
1074};
1075
1076// ── ContextInspectTool ──────────────────────────────────────────
1077
1088public:
1097
1105 ServerResponse execute(const std::string& args_json) override;
1106
1115 return MCPAccessLevel::READ;
1116 }
1117
1124 provider_ = p;
1125 }
1126
1127private:
1128 const entropic_state_provider_t* provider_ = nullptr;
1129};
1130
1138static std::vector<std::string> collect_object_keys(
1139 const nlohmann::json& j) {
1140 std::vector<std::string> keys;
1141 for (auto it = j.begin(); it != j.end(); ++it) {
1142 keys.push_back(it.key());
1143 }
1144 return keys;
1145}
1146
1154static std::vector<std::string> collect_array_names(
1155 const nlohmann::json& j) {
1156 std::vector<std::string> names;
1157 for (const auto& item : j) {
1158 if (item.is_object() && item.contains("name")) {
1159 names.push_back(item["name"].get<std::string>());
1160 }
1161 }
1162 return names;
1163}
1164
1172static std::string list_available_keys(const nlohmann::json& j) {
1173 auto keys = j.is_object() ? collect_object_keys(j)
1175 std::string result;
1176 for (const auto& k : keys) {
1177 if (!result.empty()) { result += ", "; }
1178 result += k;
1179 }
1180 return result;
1181}
1182
1195static std::string filter_json_by_key(
1196 const std::string& json_str,
1197 const std::string& key,
1198 const std::string& label,
1199 nlohmann::json& data) {
1200 auto j = nlohmann::json::parse(json_str);
1201 if (j.is_object() && j.contains(key)) {
1202 data = j[key];
1203 } else if (j.is_array()) {
1204 for (const auto& item : j) {
1205 if (item.value("name", "") == key) { data = item; }
1206 }
1207 }
1208 return data.is_null()
1209 ? tool_error_json("not_found", label + " '" + key
1210 + "' not found. Available: " + list_available_keys(j))
1211 : std::string{};
1212}
1213
1225static std::string inspect_filterable(
1226 char* (*fn)(void*), void* ud,
1227 const std::string& key, const std::string& label,
1228 nlohmann::json& data) {
1229 auto raw = call_provider(fn, ud);
1230 std::string err;
1231 if (key.empty()) {
1232 data = nlohmann::json::parse(raw);
1233 } else {
1234 err = filter_json_by_key(raw, key, label, data);
1235 }
1236 return err;
1237}
1238
1252 const std::string& target,
1253 const std::string& key,
1254 nlohmann::json& data) {
1255 bool handled = true;
1256 if (target == "state") {
1257 data = nlohmann::json::parse(call_provider(p.get_state, p.user_data));
1258 } else if (target == "metrics") {
1259 data = nlohmann::json::parse(call_provider(p.get_metrics, p.user_data));
1260 } else if (target == "history") {
1261 int limit = key.empty() ? 10 : std::atoi(key.c_str());
1262 data = nlohmann::json::parse(
1263 call_history_provider(p.get_history, limit, p.user_data));
1264 } else if (target == "docs") {
1265 const char* sec = key.empty() ? nullptr : key.c_str();
1266 data = call_docs_provider(p.get_docs, sec, p.user_data);
1267 } else {
1268 handled = false;
1269 }
1270 return handled;
1271}
1272
1273
1274
1286static std::string dispatch_inspect(
1288 const std::string& target,
1289 const std::string& key,
1290 nlohmann::json& data) {
1291 std::string err;
1292 if (target == "config") {
1293 err = inspect_filterable(p.get_config, p.user_data, key, "config section", data);
1294 } else if (target == "identity") {
1295 err = inspect_filterable(p.get_identities, p.user_data, key, "identity", data);
1296 } else if (target == "tool") {
1297 err = inspect_filterable(p.get_tools, p.user_data, key, "tool", data);
1298 } else if (!dispatch_simple_target(p, target, key, data)) {
1299 err = tool_error_json("unknown_target", "unknown target '" + target
1300 + "'. Supported: config, identity, tool, state, metrics, "
1301 "history, docs");
1302 }
1303 return err;
1304}
1305
1334 if (provider_ == nullptr) {
1335 logger->error("[inspect] no state provider set");
1336 return {tool_error_json("provider_unavailable",
1337 "engine state provider not configured"), {}};
1338 }
1339 auto args = nlohmann::json::parse(args_json, nullptr, false);
1340 if (args.is_discarded() || !args.is_object()) {
1341 args = nlohmann::json::object();
1342 }
1343 std::string target = args.value("target", "");
1344 std::string key = args.value("key", "");
1345 if (target.empty()) {
1346 logger->info("[inspect] full state dump (no target)");
1347 target = "state";
1348 key.clear();
1349 }
1350 logger->info("[inspect] target='{}' key='{}'", target, key);
1351 nlohmann::json data;
1352 auto err = dispatch_inspect(*provider_, target, key, data);
1353 nlohmann::json result;
1354 result["target"] = target;
1355 result["data"] = data;
1356 if (!key.empty()) { result["key"] = key; }
1357 return {err.empty() ? result.dump() : err, {}};
1358}
1359
1374 const std::string& args_json) {
1375 if (provider_ == nullptr) {
1376 logger->error("[context_inspect] no state provider set");
1377 return {tool_error_json("provider_unavailable",
1378 "engine state provider not configured"), {}};
1379 }
1380
1381 auto args = nlohmann::json::parse(args_json, nullptr, false);
1382 // gh#143 (v2.12.0): coerce a non-object (including `null`) to {} so an
1383 // argument-free call takes the defaults instead of throwing.
1384 if (args.is_discarded() || !args.is_object()) {
1385 args = nlohmann::json::object();
1386 }
1387 int max_messages = args.value("max_messages", 0);
1388
1389 logger->info("[context_inspect] max_messages={}", max_messages);
1390 nlohmann::json result;
1391 result["messages"] = nlohmann::json::parse(call_history_provider(
1392 provider_->get_history, max_messages, provider_->user_data));
1393 return {result.dump(), {}};
1394}
1395
1396// ── FollowupTool (gh#32, v2.1.6) ────────────────────────────────
1397
1408class FollowupTool : public ToolBase {
1409public:
1418
1426 ServerResponse execute(const std::string& args_json) override;
1427
1436 return MCPAccessLevel::READ;
1437 }
1438
1445 provider_ = p;
1446 }
1447
1448private:
1449 const entropic_state_provider_t* provider_ = nullptr;
1450};
1451
1465static std::string followup_results(const char* raw) {
1466 auto rows = nlohmann::json::parse(raw == nullptr ? "[]" : raw, nullptr, false);
1467 nlohmann::json results = nlohmann::json::array();
1468 for (const auto& r : rows.is_array() ? rows : nlohmann::json::array()) {
1469 nlohmann::json rec;
1470 rec["delegation_id"] = r.value("delegation_id", "");
1471 rec["target_tier"] = r.value("target_tier", "");
1472 rec["summary"] = r.value("summary", "");
1473 rec["completed_at"] = r.value("completed_at", "");
1474 results.push_back(std::move(rec));
1475 }
1476 nlohmann::json out;
1477 out["results"] = std::move(results);
1478 return out.dump();
1479}
1480
1494 auto args = nlohmann::json::parse(args_json, nullptr, false);
1495 std::string body;
1496 if (args.is_discarded() || !args.is_object()
1497 || args.value("query", "").empty()) {
1498 body = tool_error_json("invalid_args", "'query' is required and must be non-empty");
1499 } else if (provider_ == nullptr || provider_->search_delegations == nullptr) {
1500 logger->warn("[followup] no search_delegations provider configured");
1501 body = tool_error_json("storage_unavailable", "delegation storage not available");
1502 } else {
1503 std::string query = args.value("query", "");
1504 int max_results = args.value("max_results", 3);
1505 char* raw = provider_->search_delegations(
1506 query.c_str(), max_results, provider_->user_data);
1507 body = followup_results(raw);
1508 std::free(raw);
1509 logger->info("[followup] query='{}' max_results={} -> {}",
1510 query, max_results, body);
1511 }
1512 return {body, {}};
1513}
1514
1515// ── ResumeDelegationTool (gh#32, v2.1.6) ────────────────────────
1516
1530public:
1545 const std::vector<std::string>& tier_names)
1546 : ToolBase(std::move(def)) {
1547 auto schema = nlohmann::json::parse(definition_.input_schema);
1548 schema["properties"]["target"]["enum"] = tier_names;
1550 }
1551
1559 ServerResponse execute(const std::string& args_json) override;
1560};
1561
1574 auto args = nlohmann::json::parse(args_json, nullptr, false);
1575 if (args.is_discarded() || !args.is_object()) {
1576 return {tool_error_json("invalid_args", "an arguments object is required"), {}};
1577 }
1578 std::string delegation_id = args.value("delegation_id", "");
1579 std::string target = args.value("target", "");
1580 std::string task = args.value("task", "");
1581 int max_turns = args.value("max_turns", -1);
1582 // gh#162: an id OR a tier. Reaching this tool used to cost a followup
1583 // round trip on a slow local model before any work started.
1584 if (task.empty() || (delegation_id.empty() && target.empty())) {
1585 return {tool_error_json("invalid_args",
1586 "'task' and one of 'delegation_id' or 'target' are required"),
1587 {}};
1588 }
1589 bool by_target = delegation_id.empty();
1590 logger->info("[resume_delegation] id='{}' target='{}' task='{}' "
1591 "max_turns={}", delegation_id, target, task, max_turns);
1592
1593 nlohmann::json result;
1594 result["action"] = "resume_delegation";
1595 result["delegation_id"] = delegation_id;
1596 result["target"] = target;
1597 result["resume_by_target"] = by_target;
1598 result["task"] = task;
1599 result["max_turns"] = max_turns;
1600
1605 return {result.dump(), {delegate_d, stop_d}};
1606}
1607
1608// ── EntropicServer ──────────────────────────────────────────────
1609
1623int EntropicServer::register_core_tools(
1624 const std::string& tools_dir) {
1626 "todo", "entropic", tools_dir);
1627 todo_ = std::make_unique<TodoTool>(std::move(todo_def));
1628 register_tool(todo_.get());
1629
1631 "complete", "entropic", tools_dir);
1632 complete_ = std::make_unique<CompleteTool>(
1633 std::move(complete_def));
1634 register_tool(complete_.get());
1635
1636 phase_change_ = std::make_unique<PhaseChangeTool>();
1637 register_tool(phase_change_.get());
1638
1640 "prune_context", "entropic", tools_dir);
1641 prune_context_ = std::make_unique<PruneContextTool>(
1642 std::move(prune_def));
1643 register_tool(prune_context_.get());
1644
1645 return 4;
1646}
1647
1678int EntropicServer::register_delegation_tools(
1679 const std::string& tools_dir,
1680 const std::vector<std::string>& tier_names,
1681 const std::vector<std::string>& require_context_tiers) {
1682 if (tier_names.empty()) {
1683 return 0;
1684 }
1686 "delegate", "entropic", tools_dir);
1687 delegate_ = std::make_unique<DelegateTool>(
1689 register_tool(delegate_.get());
1690
1692 "pipeline", "entropic", tools_dir);
1693 pipeline_ = std::make_unique<PipelineTool>(
1695 register_tool(pipeline_.get());
1696
1697 // gh#32 (v2.1.6): resume_delegation lives alongside delegate
1698 // because it produces the same directive kind (resume-flavored).
1700 "resume_delegation", "entropic", tools_dir);
1701 resume_delegation_ = std::make_unique<ResumeDelegationTool>(
1702 std::move(resume_def), tier_names);
1703 register_tool(resume_delegation_.get());
1704
1705 return 3;
1706}
1707
1721int EntropicServer::register_introspection_tools(
1722 const std::string& tools_dir) {
1723 diagnose_ = std::make_unique<DiagnoseTool>(
1724 load_tool_definition("diagnose", "entropic", tools_dir));
1725 register_tool(diagnose_.get());
1726
1727 inspect_ = std::make_unique<InspectTool>(
1728 load_tool_definition("inspect", "entropic", tools_dir));
1729 register_tool(inspect_.get());
1730
1731 context_inspect_ = std::make_unique<ContextInspectTool>(
1732 load_tool_definition("context_inspect", "entropic", tools_dir));
1733 register_tool(context_inspect_.get());
1734
1735 // gh#32 (v2.1.6): followup is read-only and consumes the same
1736 // state_provider plumbing as diagnose/inspect.
1737 followup_ = std::make_unique<FollowupTool>(
1738 load_tool_definition("followup", "entropic", tools_dir));
1739 register_tool(followup_.get());
1740
1741 return 4;
1742}
1743
1753 const std::vector<std::string>& tier_names,
1754 const std::string& data_dir,
1755 const std::vector<std::string>& require_context_tiers)
1756 : MCPServerBase("entropic") {
1757
1758 std::string tools_dir = data_dir + "/tools";
1759 int count = register_core_tools(tools_dir);
1760 count += register_delegation_tools(tools_dir, tier_names,
1762 count += register_introspection_tools(tools_dir);
1763
1764 logger->info("EntropicServer initialized with {} tools "
1765 "({} tiers)", count, tier_names.size());
1766}
1767
1773
1787 const std::string& tool_name) const {
1788 return tool_name == "delegate" || tool_name == "pipeline";
1789}
1790
1805 const entropic_state_provider_t& provider) {
1806 state_provider_ = provider;
1807 diagnose_->set_provider(&state_provider_);
1808 inspect_->set_provider(&state_provider_);
1809 context_inspect_->set_provider(&state_provider_);
1810 if (followup_) {
1811 followup_->set_provider(&state_provider_);
1812 }
1813 logger->info("State provider set for introspection tools");
1814}
1815
1824bool EntropicServer::release_session(const std::string& key) {
1825 return todo_->release(key);
1826}
1827
1835 return todo_->session_count();
1836}
1837
1838} // namespace entropic
Tool for signaling task completion.
CompleteTool(ToolDefinition def)
Construct from tool definition.
ServerResponse execute(const std::string &args_json) override
Execute completion signal.
Tool for inspecting the current context window contents.
ContextInspectTool(ToolDefinition def)
Construct from tool definition.
ServerResponse execute(const std::string &args_json) override
Return context window contents as a message array.
void set_provider(const entropic_state_provider_t *p)
Set state provider pointer.
MCPAccessLevel required_access_level() const override
Read-only tool requires only READ access.
Tool for delegating tasks to child inference loops.
ServerResponse execute(const std::string &args_json) override
Execute delegation.
DelegateTool(ToolDefinition def, const std::vector< std::string > &tier_names, std::vector< std::string > require_context_tiers={})
Construct and patch input schema with tier names.
Tool for full engine state snapshots.
ServerResponse execute(const std::string &args_json) override
Execute diagnostic snapshot.
void set_provider(const entropic_state_provider_t *p)
Set state provider pointer.
MCPAccessLevel required_access_level() const override
Read-only tool requires only READ access.
DiagnoseTool(ToolDefinition def)
Construct from tool definition.
std::size_t session_count() const override
Sessions holding a todo list on this server (gh#158).
void set_state_provider(const entropic_state_provider_t &provider)
Set the engine state provider for introspection tools.
bool release_session(const std::string &key) override
Release key's todo list (gh#158).
~EntropicServer() override
Destructor.
EntropicServer(const std::vector< std::string > &tier_names, const std::string &data_dir, const std::vector< std::string > &require_context_tiers={})
Construct with tier names and data dir.
bool skip_duplicate_check(const std::string &tool_name) const override
delegate and pipeline skip duplicate check.
Recall prior delegation results via storage-backed search.
ServerResponse execute(const std::string &args_json) override
Execute a followup query.
MCPAccessLevel required_access_level() const override
Read-only — requires only READ access.
void set_provider(const entropic_state_provider_t *p)
Store provider pointer (non-owning).
FollowupTool(ToolDefinition def)
Construct from loaded tool definition.
Tool for targeted engine state queries.
ServerResponse execute(const std::string &args_json) override
Execute targeted inspection.
MCPAccessLevel required_access_level() const override
Read-only tool requires only READ access.
void set_provider(const entropic_state_provider_t *p)
Set state provider pointer.
InspectTool(ToolDefinition def)
Construct from tool definition.
Concrete base class for MCP servers (80% logic).
Definition server_base.h:66
void register_tool(ToolBase *tool)
Register a tool with this server.
Tool for switching inference phase.
PhaseChangeTool()
Construct with inline tool definition.
ServerResponse execute(const std::string &args_json) override
Execute phase change.
Tool for multi-stage delegation pipelines.
ServerResponse execute(const std::string &args_json) override
Execute pipeline setup.
PipelineTool(ToolDefinition def, const std::vector< std::string > &tier_names, std::vector< std::string > require_context_tiers={})
Construct and patch input schema with tier names.
Tool for pruning old messages from context.
PruneContextTool(ToolDefinition def)
Construct from tool definition.
ServerResponse execute(const std::string &args_json) override
Execute context pruning.
Resume a prior delegation with its conversation seeded.
ResumeDelegationTool(ToolDefinition def, const std::vector< std::string > &tier_names)
Construct from loaded tool definition.
ServerResponse execute(const std::string &args_json) override
Emit a resume-flavored delegate directive.
One T per session key, under one leaf mutex (gh#158).
Tool for managing a persistent todo list.
bool release(const std::string &key)
Forget a session's list (gh#158).
std::string anchor_key(const std::string &args_json) const override
Anchor key for todo state replacement.
TodoTool(ToolDefinition def)
Construct from tool definition.
ServerResponse execute(const std::string &args_json) override
Execute todo action (add/update/remove).
std::size_t session_count() const
Sessions holding a list (gh#158).
Abstract base class for individual MCP tools.
Definition tool_base.h:52
ToolDefinition definition_
Tool definition.
Definition tool_base.h:109
Directive processing for tool-to-engine communication.
Entropic MCP server — engine-level tools including introspection.
@ ENTROPIC_DIRECTIVE_STOP_PROCESSING
Halt directive processing.
Definition enums.h:59
@ ENTROPIC_DIRECTIVE_PRUNE_MESSAGES
Prune old tool results.
Definition enums.h:66
@ ENTROPIC_DIRECTIVE_COMPLETE
Mark task complete.
Definition enums.h:63
@ ENTROPIC_DIRECTIVE_NOTIFY_PRESENTER
Generic UI notification passthrough.
Definition enums.h:69
@ ENTROPIC_DIRECTIVE_PHASE_CHANGE
Switch active inference phase.
Definition enums.h:68
@ ENTROPIC_DIRECTIVE_PIPELINE
Multi-stage sequential execution.
Definition enums.h:62
@ ENTROPIC_DIRECTIVE_CONTEXT_ANCHOR
Replace context anchor.
Definition enums.h:67
@ ENTROPIC_DIRECTIVE_DELEGATE
Route to another identity.
Definition enums.h:61
std::string err
size_exceeded, when set
int max_entries
<= 0 means unbounded
long limit
Results to keep.
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
Activate model on GPU (WARM → ACTIVE).
static std::string call_provider(char *(*fn)(void *), void *ud)
Call a state provider callback and wrap result.
ToolDefinition load_tool_definition(const std::string &tool_name, const std::string &server_prefix, const std::string &data_dir)
Load a tool definition from a JSON file.
@ optional
optional: every escape asks the host's path approver
static ToolDefinition phase_change_definition()
The inline phase_change definition, contract included.
static std::vector< std::string > collect_object_keys(const nlohmann::json &j)
Collect keys from a JSON object into a vector.
static std::string inspect_filterable(char *(*fn)(void *), void *ud, const std::string &key, const std::string &label, nlohmann::json &data)
Inspect a filterable target (config/identity/tool).
static std::string followup_results(const char *raw)
Wrap the provider's records in the declared {"results": [...]}.
@ count
Sentinel — MUST remain last.
static std::string dispatch_inspect(const entropic_state_provider_t &p, const std::string &target, const std::string &key, nlohmann::json &data)
Dispatch an inspect query to the appropriate provider.
std::string tool_error_json(const std::string &code, const std::string &message)
A declared tool error: {"error": code, "message": message}.
static bool dispatch_simple_target(const entropic_state_provider_t &p, const std::string &target, const std::string &key, nlohmann::json &data)
Dispatch inspect for simple (non-filterable) targets.
static std::string call_docs_provider(char *(*fn)(const char *, void *), const char *section, void *ud)
Call docs callback with section param.
ENTROPIC_EXPORT std::string current_run_session()
The session key of the run executing on this thread (gh#166).
Definition run_scope.cpp:95
static std::string call_history_provider(char *(*fn)(int, void *), int max_entries, void *ud)
Call history callback with max_entries param.
static std::string pipeline_rejection(const std::vector< std::string > &stages, const std::vector< std::string > &tier_names, const std::vector< std::string > &require_context_tiers, size_t refs)
Parse pipeline args, validate stages, emit directives.
static nlohmann::json build_snapshot(const entropic_state_provider_t &p, bool include_docs, int history_limit)
Build the diagnose snapshot JSON.
MCPAccessLevel
MCP tool access level for per-identity authorization.
Definition config.h:38
@ READ
Read-only operations (e.g., read_file, list_directory)
static std::string filter_json_by_key(const std::string &json_str, const std::string &key, const std::string &label, nlohmann::json &data)
Select one entry of a provider's JSON by key (object key or array element name).
static size_t carry_context(const nlohmann::json &args, nlohmann::json &result)
Copy a context argument into the result, dropping junk (gh#162).
static std::vector< std::string > collect_array_names(const nlohmann::json &j)
Collect "name" fields from a JSON array of objects.
std::string render_response_format(const std::string &output_schema_json)
Render an outputSchema as the compact "Response format" text a model reads (gh#203).
static std::string list_available_keys(const nlohmann::json &j)
List available keys from a JSON value for error messages.
gh#158: the cancel token of the run executing on THIS thread.
MCPServerBase concrete base class + ServerResponse.
Base directive — all directives carry a type tag.
Definition directives.h:36
entropic_directive_type_t type
Discriminant for dispatch.
Definition directives.h:37
Structured result from tool execution.
Definition server_base.h:33
Single todo entry.
std::string content
Item text.
std::string status
"pending", "in_progress", "done"
Parsed tool definition from JSON schema file.
Definition tool_base.h:33
std::string name
Tool name (e.g., "read_file")
Definition tool_base.h:34
std::string input_schema
JSON Schema for arguments (raw JSON string)
Definition tool_base.h:36
Read-only engine state provider for introspection tools.
void * user_data
Opaque user data passed to all callbacks.
char *(* search_delegations)(const char *query, int max_results, void *user_data)
Search prior delegation summaries (gh#32, v2.1.6).
char *(* get_history)(int max_entries, void *user_data)
Get recent tool call history as JSON array.
Abstract base class for individual MCP tools.
UTF-8 validation + replacement at every system boundary where bytes change ownership.