Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
entropic.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
13#include "engine_handle.h"
14
16#include <entropic/config/validate.h> // gh#154: configure-time grammar check
18#include <entropic/entropic.h>
21#include "llama_cpp_backend.h"
26#include "json_serializers.h"
27#include "utf8_safe.h"
28#include "../inference/tool_call_serialize.h" // gh#98: shared typed serializer
29
30#include <nlohmann/json.hpp>
31
32#include <atomic>
33
34#include <cstdlib>
35#include <fstream>
36#include <vector>
37#include <cstring>
38#include <filesystem>
39#include <new>
40#include <stdexcept>
41#include <string>
42
43static auto s_log = entropic::log::get("facade");
44
75template <typename Fn>
78 try {
79 rc = fn();
80 } catch (const std::filesystem::filesystem_error& e) {
81 if (handle) { handle->last_error = e.what(); }
82 s_log->error("c_api filesystem_error: {}", e.what());
84 } catch (const nlohmann::json::exception& e) {
85 if (handle) { handle->last_error = e.what(); }
86 s_log->error("c_api json::exception: {}", e.what());
88 } catch (const std::exception& e) {
89 if (handle) { handle->last_error = e.what(); }
90 s_log->error("c_api std::exception: {}", e.what());
92 }
93 return rc;
94}
95
107static char* alloc_cstr(const char* src) {
108 if (!src) { return nullptr; }
109 size_t len = std::strlen(src) + 1;
110 auto* dst = static_cast<char*>(entropic_alloc(len));
111 if (dst) { std::memcpy(dst, src, len); }
112 return dst;
113}
114
122static char* alloc_cstr(const std::string& src) {
123 return alloc_cstr(src.c_str());
124}
125
126/* setup_ggml_logging moved to ModelOrchestrator::initialize() — Step 7 */
127
128// gh#58 follow-up (v2.2.6): per-handle last_error. Pre-v2.2.6 the
129// public API in src/types/error.cpp ignored the handle parameter and
130// returned a single thread-local buffer, so every handle->last_error
131// assignment in the facade was unreadable from the consumer side.
132// Thread-local cache here means the returned const char* stays valid
133// until the *same* thread calls entropic_last_error again — matching
134// the documented v1.8.0 contract.
135static thread_local std::string s_last_error_cache;
136static thread_local char s_pre_create_error[512] = "";
137
156extern "C" const char* entropic_last_error(entropic_handle_t handle) {
157 if (!handle) { return s_pre_create_error; }
158 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
159 s_last_error_cache = handle->last_error;
160 return s_last_error_cache.c_str();
161}
162
171 if (!h) { return ENTROPIC_ERROR_INVALID_HANDLE; }
172 if (!h->configured.load() || !h->orchestrator) {
174 }
175 return ENTROPIC_OK;
176}
177
186 if (!h) { return ENTROPIC_ERROR_INVALID_HANDLE; }
187 if (!h->configured.load() || !h->mcp_auth) {
189 }
190 return ENTROPIC_OK;
191}
192
201 if (!h) { return ENTROPIC_ERROR_INVALID_HANDLE; }
202 if (!h->configured.load() || !h->identity_manager) {
204 }
205 return ENTROPIC_OK;
206}
207
208/* find_tier_by_model_path moved to ModelsConfig::find_tier_by_path() — v2.0.1 */
209
232 entropic_handle_t h, const char* tier_name)
233{
234 const std::string tier(tier_name);
235 if (h->orchestrator->get_backend(tier) == nullptr) {
236 throw std::runtime_error("no backend for tier: " + tier);
237 }
238 auto* backend = h->orchestrator->ensure_model(tier);
239 if (backend == nullptr || !backend->is_active()) {
240 throw std::runtime_error(
241 "model could not be made resident for tier: " + tier);
242 }
243 return backend;
244}
245
246extern "C" {
247
262 if (handle == nullptr) {
264 }
265 entropic::log::init(spdlog::level::info);
266 entropic_inference_log_silence(); // silent until configure enables
267
268 auto* engine = new (std::nothrow) entropic_engine();
269 if (engine == nullptr) {
270 *handle = nullptr;
272 }
273
274 // gh#59 (v2.3.1): assign a monotonic log id. Used by the
275 // HandleAwareSink dispatcher to route session.log writes to the
276 // right file when multiple handles coexist. 0 is reserved for
277 // "no handle scope active."
278 static std::atomic<int> s_log_id_counter{0};
279 engine->log_id = ++s_log_id_counter;
280
281 s_log->info("entropic_create() — v{} (log_id={})",
282 CONFIG_ENTROPIC_VERSION_STRING, engine->log_id);
283
284 *handle = engine;
285 return ENTROPIC_OK;
286}
287
306/* preload_bundled_models moved to BundledModels::auto_discover_and_load() — Step 6 */
307
308/* InferenceInterface wiring moved to inference/interface_factory.cpp — Step 3 */
309
310/* InferenceInterface C-wrappers + factory moved to inference/interface_factory.cpp — Step 3 */
311
312/* facade_process_directives → engine->build_directive_hooks() — v2.0.2 */
313/* facade_resolve_tier/tier_exists/handoff/param → engine->set_tier_info() — v2.0.2 */
314/* wire_engine_interfaces → inline in configure_common — v2.0.2 */
315
316// ── Tool prompt injection (v2.0.4) ─────────────────────────
317
330static std::vector<std::string> resolve_allowed_tools(
331 entropic_engine* h, const std::string& tier) {
332 // Primary: cached from identity frontmatter (v2.0.4)
333 auto cached = h->tier_allowed_tools.find(tier);
334 if (cached != h->tier_allowed_tools.end()) {
335 return cached->second;
336 }
337 // Fallback: dynamic identity or model config
338 if (h->identity_manager) {
339 auto* cfg = h->identity_manager->get(tier);
340 if (cfg && !cfg->allowed_tools.empty()) {
341 return cfg->allowed_tools;
342 }
343 }
344 return {};
345}
346
356static std::vector<std::string> filter_tools(
357 const nlohmann::json& all_tools,
358 const std::vector<std::string>& allowed) {
359 std::vector<std::string> result;
360 for (const auto& tool : all_tools) {
361 std::string name = tool.value("name", "");
362 bool pass = allowed.empty()
363 || std::find(allowed.begin(), allowed.end(), name)
364 != allowed.end();
365 if (pass) { result.push_back(tool.dump()); }
366 }
367 return result;
368}
369
377static std::string tool_defs_array(
378 const std::vector<std::string>& tool_jsons) {
379 nlohmann::json arr = nlohmann::json::array();
380 for (const auto& tj : tool_jsons) {
381 auto obj = nlohmann::json::parse(tj, nullptr, false);
382 if (!obj.is_discarded()) { arr.push_back(std::move(obj)); }
383 }
384 return arr.dump();
385}
386
405static int facade_get_tool_prompt(const char* tier, char** result,
406 void* user_data) {
407 auto* h = static_cast<entropic_engine*>(user_data);
408 *result = nullptr;
409 // gh#166: the tools a turn sees belong to the SESSION's workspace. The
410 // key arrives through `entropic::current_run_session()` because this
411 // callback's signature lives in an interface header.
412 auto* servers = (h != nullptr && h->server_manager)
414 : nullptr;
415 if (servers == nullptr) { return 1; }
416 auto all_tools = nlohmann::json::parse(servers->list_tools(),
417 nullptr, false);
418 auto tool_jsons = filter_tools(
419 all_tools, resolve_allowed_tools(h, tier ? tier : ""));
420 if (!all_tools.is_array() || all_tools.empty() || tool_jsons.empty()) {
421 return 1;
422 }
423 *result = strdup(tool_defs_array(tool_jsons).c_str());
424 return 0;
425}
426
427
441 h->engine->set_external_interrupt(
442 [](void* ud) {
443 auto* sm = static_cast<entropic::ServerManager*>(ud);
444 if (sm) { sm->interrupt_external_tools(); }
445 }, h->server_manager.get());
446 // gh#150: wire the release alongside the abort, never separately. A
447 // handle that can interrupt its transports but not un-interrupt them
448 // is the defect itself, so the two are registered together and any
449 // future call site gets both or neither.
450 h->engine->set_external_reset(
451 [](void* ud) {
452 auto* sm = static_cast<entropic::ServerManager*>(ud);
453 if (sm) { sm->clear_external_tool_interrupts(); }
454 }, h->server_manager.get());
455}
456
470 if (h->stream_observer != nullptr && h->engine) {
471 h->engine->set_stream_observer(
473 }
474}
475
489 if (h->queue_observer != nullptr && h->engine) {
490 h->engine->set_queue_observer(
491 h->queue_observer, h->queue_observer_data);
492 }
493}
494
508 if (h->state_observer != nullptr && h->engine) {
509 h->engine->set_state_observer(
510 h->state_observer, h->state_observer_data);
511 }
512}
513
528 if ((h->critique_start_cb != nullptr || h->critique_end_cb != nullptr)
529 && h->validator) {
530 h->validator->set_critique_callbacks(
534 }
535}
536
553
568 const std::filesystem::path& data_dir,
569 const std::string& shared_prefix) {
570 for (const auto& [name, tier] : h->config.models.tiers) {
572 info.valid = true;
573 auto parsed = entropic::prompts::resolve_tier_identity_full(
574 tier, name, data_dir);
575 info.system_prompt = shared_prefix + parsed.body;
576 info.explicit_completion = parsed.frontmatter.explicit_completion
577 .value_or(!tier.auto_chain.has_value());
578 // E6 (2.0.6-rc18): propagate per-identity caps from frontmatter
579 // so AgentEngine::tri_get_tier_param surfaces them to the loop
580 // and tool executor. -1 = "use global loop_config default".
581 info.max_iterations_override =
582 parsed.frontmatter.max_iterations;
583 info.max_tool_calls_per_turn_override =
584 parsed.frontmatter.max_tool_calls_per_turn;
585 info.max_consecutive_empty_turns_override =
586 parsed.frontmatter.max_consecutive_empty_turns;
587 info.allowed_tools = resolve_allowed_tools(h, name); // gh#121
588 h->engine->set_tier_info(name, info);
589 }
590}
591
592// ── configure_common ───────────────────────────────────────
593
613 const char* parent_id, const char* delegating_tier,
614 const char* target_tier, const char* task, int max_turns,
615 std::string& delegation_id, std::string& child_conversation_id,
616 void* user_data) {
617 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
618 if (sb == nullptr) { return false; }
619 return sb->create_delegation(
620 parent_id ? parent_id : "",
621 delegating_tier ? delegating_tier : "",
622 target_tier ? target_tier : "",
623 task ? task : "",
624 max_turns, delegation_id, child_conversation_id);
625}
626
640 const char* title, std::string& conversation_id,
641 void* user_data) {
642 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
643 if (sb == nullptr) { return false; }
644 conversation_id = sb->create_conversation(
645 title ? title : "session", std::nullopt, std::nullopt);
646 return !conversation_id.empty();
647}
648
655 const char* delegation_id, const char* status,
656 const char* summary, void* user_data) {
657 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
658 if (sb == nullptr || delegation_id == nullptr) { return false; }
659 std::optional<std::string> sum;
660 if (summary != nullptr) { sum = summary; }
661 return sb->complete_delegation(delegation_id,
662 status ? status : "completed", sum);
663}
664
671 const char* conversation_id, const char* messages_json,
672 void* user_data) {
673 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
674 if (sb == nullptr || conversation_id == nullptr
675 || messages_json == nullptr) {
676 return false;
677 }
678 return sb->save_messages(conversation_id, messages_json);
679}
680
687 const char* conversation_id, const char* messages_json,
688 void* user_data) {
689 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
690 if (sb == nullptr || conversation_id == nullptr
691 || messages_json == nullptr) {
692 return false;
693 }
694 return sb->save_snapshot(conversation_id, messages_json);
695}
696
708 const char* target_tier, std::string& delegation_id,
709 void* user_data) {
710 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
711 if (sb == nullptr || target_tier == nullptr) { return false; }
712 return sb->latest_delegation_for_target(target_tier, delegation_id);
713}
714
730 const char* delegation_id, std::string& result_json,
731 void* user_data) {
732 auto* sb = static_cast<entropic::SqliteStorageBackend*>(user_data);
733 bool ok = sb != nullptr && delegation_id != nullptr;
734 std::string del_json;
735 nlohmann::json del, conv;
736 std::string child_id, target, conv_json;
737 if (ok) {
738 ok = sb->get_delegation_by_id(delegation_id, del_json);
739 }
740 if (ok) {
741 del = nlohmann::json::parse(del_json, nullptr, false);
742 ok = del.is_object();
743 }
744 if (ok) {
745 child_id = del.value("child_conversation_id", std::string{});
746 target = del.value("target_tier", std::string{});
747 ok = !child_id.empty() && !target.empty()
748 && sb->load_conversation(child_id, conv_json);
749 }
750 if (ok) {
751 conv = nlohmann::json::parse(conv_json, nullptr, false);
752 ok = conv.is_object();
753 }
754 if (ok) {
755 conv["target_tier"] = target;
756 conv["delegation_id"] = del.value("id", std::string{});
757 result_json = conv.dump();
758 }
759 return ok;
760}
761
779 si.create_delegation = si_create_delegation;
780 si.complete_delegation = si_complete_delegation;
781 si.save_conversation = si_save_conversation;
782 si.save_snapshot = si_save_snapshot;
783 si.load_delegation_with_messages = si_load_delegation_with_messages;
784 si.latest_delegation_for_target = si_latest_delegation_for_target;
785 si.user_data = sb;
786 return si;
787}
788
795 if (h->config.log_dir.empty()) { return; }
796 auto db_path = h->config.log_dir / "entropic.db";
797 h->storage = std::make_unique<entropic::SqliteStorageBackend>(db_path);
798 if (h->storage->initialize()) {
799 s_log->info("storage: {}", db_path.string());
800 } else {
801 s_log->warn("storage init failed, continuing without persistence");
802 h->storage.reset();
803 }
804 // gh#32 (v2.1.6): wire StorageInterface into the engine so the
805 // create_delegation/save_conversation paths actually persist (they
806 // were dead code pre-2.1.6 because nothing populated the iface).
807 if (h->storage && h->engine) {
808 h->engine->set_storage(build_storage_iface(h->storage.get()));
809 }
810 h->session_logger = std::make_unique<entropic::SessionLogger>(
811 h->config.log_dir);
812}
813
831static std::vector<std::string> collect_delegatable_tiers(
833 std::unordered_set<std::string> targets;
834 for (const auto& [source, dests] : config.routing.handoff_rules) {
835 for (const auto& t : dests) { targets.insert(t); }
836 }
837 if (targets.empty()) {
838 for (const auto& [name, tier] : config.models.tiers) {
839 if (name != config.models.default_tier) {
840 targets.insert(name);
841 }
842 }
843 }
844 return {targets.begin(), targets.end()};
845}
846
859 const std::filesystem::path& data_dir) {
860 auto root = h->config.mcp.working_dir.empty()
861 ? std::filesystem::current_path()
862 : std::filesystem::path(h->config.mcp.working_dir);
863 h->server_manager = std::make_unique<entropic::ServerManager>(
864 h->config.permissions, root);
865 auto tier_names = collect_delegatable_tiers(h->config);
866 // gh#162 (v2.13.0): tiers that refuse a contextless delegation, so the
867 // delegate/pipeline tools can say no at the boundary.
868 std::vector<std::string> require_context;
869 for (const auto& [name, tier] : h->config.models.tiers) {
870 if (tier.requires_context) { require_context.push_back(name); }
871 }
872 h->server_manager->init_builtins(
873 h->config.mcp, tier_names, data_dir.string(), require_context);
874 // v2.13.0: under `allow_outside_root: optional` the DEFAULT set asks
875 // the host's path approver. Workspaces are built elsewhere and never
876 // reach this line — they stay hard-confined (gh#166).
878
879 // gh#133 (v2.10.1): load dlopen plugins after the builtins so a plugin
880 // colliding with a built-in server name is rejected rather than shadowing
881 // it. Failures are already logged per-path with a typed code; startup
882 // continues so the rest of the engine stays usable and the operator sees
883 // the whole diagnosis at once.
884 h->server_manager->load_plugins(h->config.mcp);
885
886 // v2.14.0: the default set connects external servers too — declared ones
887 // (mcp.external_servers, dead since v1.8.7: nothing called initialize()
888 // here) and enabled .mcp.json ones, behind the same gate as a workspace.
890 h->server_manager->initialize();
891}
892
901static std::string build_shared_prompt_prefix(
903 const std::filesystem::path& data_dir) {
904 std::string constitution, app_ctx;
905 // gh#156: was two discarded error strings; the shared loader logs them.
906 entropic::prompts::load_shared_prompt_sources(
907 h->config, data_dir, constitution, app_ctx);
908 std::string prefix;
909 if (!constitution.empty()) { prefix += constitution + "\n\n"; }
910 if (!app_ctx.empty()) { prefix += app_ctx + "\n\n"; }
911 return prefix;
912}
913
937 // gh#95 (v2.7.4): thread the identity `grammar:` key so the orchestrator's
938 // resolve_grammar_key() finds it and constrains the tier's generation.
939 // Without this the field is parsed but dropped here — registers OK, never
940 // enforces (same class as the gh#82/85/94 sampler-threading gaps).
941 if (fm.grammar.has_value()) {
942 tc.grammar = std::filesystem::path(*fm.grammar);
943 }
944 if (fm.temperature.has_value()) { tc.temperature = *fm.temperature; }
945 if (fm.max_output_tokens.has_value()) {
947 }
948 if (fm.top_p.has_value()) { tc.top_p = *fm.top_p; }
949 if (fm.top_k.has_value()) { tc.top_k = *fm.top_k; }
950 if (fm.min_p.has_value()) { tc.min_p = *fm.min_p; }
951 if (fm.presence_penalty.has_value()) {
953 }
954 if (fm.frequency_penalty.has_value()) {
956 }
957 // gh#86 (v2.5.4): repeat_penalty + enable_thinking.
958 if (fm.repeat_penalty.has_value()) { tc.repeat_penalty = *fm.repeat_penalty; }
959 if (fm.enable_thinking.has_value()) {
961 }
962}
963
974 const std::string& name,
976 if (fm.allowed_tools.has_value()) {
977 h->tier_allowed_tools[name] = *fm.allowed_tools;
978 }
979 if (!fm.validation_rules.empty()) {
981 }
982 if (fm.relay_single_delegate) {
983 h->engine->set_relay_single_delegate(name);
984 }
985 // gh#94 (v2.7.3): per-tier frontmatter SAMPLERS are threaded earlier, in
986 // thread_frontmatter_samplers() BEFORE init_orchestrator, so they land in
987 // the orchestrator's by-value config snapshot. Threading them HERE (after
988 // the snapshot) was the gh#94 ordering bug — the values reached h->config
989 // but never the orchestrator's frozen copy, so tiers ran defaults.
990}
991
1001 const std::filesystem::path& data_dir) {
1002 for (const auto& [name, tier] : h->config.models.tiers) {
1003 std::filesystem::path id_path;
1004 if (tier.identity.has_value()) {
1005 id_path = tier.identity.value();
1006 } else if (!tier.identity_disabled) {
1007 id_path = data_dir / "prompts" / ("identity_" + name + ".md");
1008 }
1009 if (id_path.empty() || !std::filesystem::exists(id_path)) {
1010 continue;
1011 }
1013 if (entropic::prompts::load_identity(id_path, id).empty()) {
1014 apply_identity_frontmatter(h, name, id.frontmatter);
1015 }
1016 }
1017 // gh#83 (v2.5.2): hand the populated allowlist map to the executor
1018 // for dispatch-time enforcement. A pointer to the handle-owned map
1019 // keeps this order-independent vs wire_tool_executor.
1020 if (h->tool_executor) {
1021 h->tool_executor->set_tier_allowed_tools(&h->tier_allowed_tools);
1022 }
1023}
1024
1046 const std::filesystem::path& data_dir) {
1047 for (auto& [name, tier] : h->config.models.tiers) {
1048 std::filesystem::path id_path;
1049 if (tier.identity.has_value()) {
1050 id_path = tier.identity.value();
1051 } else if (!tier.identity_disabled) {
1052 id_path = data_dir / "prompts" / ("identity_" + name + ".md");
1053 }
1054 if (id_path.empty() || !std::filesystem::exists(id_path)) {
1055 continue;
1056 }
1058 if (entropic::prompts::load_identity(id_path, id).empty()) {
1059 thread_frontmatter_sampler(tier, id.frontmatter);
1060 }
1061 }
1062}
1063
1081static char* tool_history_json_thunk(size_t count, void* ud) {
1082 auto* exec = static_cast<entropic::ToolExecutor*>(ud);
1083 if (exec == nullptr) { return nullptr; }
1084 auto s = exec->tool_history().to_json(count);
1085 if (s.empty() || s == "[]") { return nullptr; }
1086 auto* out = static_cast<char*>(std::malloc(s.size() + 1));
1087 if (out != nullptr) {
1088 std::memcpy(out, s.data(), s.size());
1089 out[s.size()] = '\0';
1090 }
1091 return out;
1092}
1093
1107 h->tool_executor = std::make_unique<entropic::ToolExecutor>(
1108 *h->server_manager,
1109 h->engine->loop_config(),
1110 h->engine->callbacks(),
1111 h->engine->build_directive_hooks());
1114 const std::vector<entropic::ToolCall>& calls,
1115 void* ud) -> std::vector<entropic::Message> {
1116 return static_cast<entropic::ToolExecutor*>(ud)
1117 ->process_tool_calls(ctx, calls);
1118 };
1119 tei.user_data = h->tool_executor.get();
1121 tei.free_fn = [](char* p) { std::free(p); };
1122 // gh#166 (v2.13.0): a bound session's tool calls run against its
1123 // workspace's own server instances, not the handle's default set.
1124 h->tool_executor->set_server_resolver(
1125 +[](const std::string& key, void* ud)
1128 static_cast<entropic_handle_t>(ud), key);
1129 }, h);
1130 h->engine->set_tool_executor(tei);
1131}
1132
1146static char* sp_get_validation(void* ud) {
1147 auto* h = static_cast<entropic_engine*>(ud);
1148 if (h == nullptr || h->validator == nullptr) { return nullptr; }
1149 auto r = h->validator->last_result();
1150 nlohmann::json v;
1151 v["ran"] = true;
1152 switch (r.verdict) {
1154 v["verdict"] = "passed"; break;
1156 v["verdict"] = "revised"; break;
1158 v["verdict"] = "rejected_reverted_length"; break;
1160 v["verdict"] = "rejected_max_revisions"; break;
1162 v["verdict"] = "skipped"; break;
1164 v["verdict"] = "paused_pending_consumer"; break;
1166 v["verdict"] = "passed_consumer_override"; break;
1167 }
1168 v["revisions_applied"] = r.revision_count;
1169 // gh#30 (v2.1.5): structured fields the consumer needs to render
1170 // a "retry / override / re-prompt" UI without parsing free-form
1171 // reason strings.
1172 v["attempt_n"] = r.attempt_n;
1173 nlohmann::json violations = nlohmann::json::array();
1174 for (const auto& vi : r.final_critique.violations) {
1175 violations.push_back({
1176 {"rule", vi.rule},
1177 {"rule_id", vi.rule}, // alias for gh#30 schema
1178 {"rule_text", vi.rule}, // alias for gh#30 schema
1179 {"excerpt", vi.excerpt},
1180 {"quote", vi.excerpt}, // alias matching gh#30 "evidence.quote"
1181 {"explanation", vi.explanation},
1182 {"severity", "error"}, // gh#30: hard rejection only today
1183 });
1184 }
1185 v["violations"] = violations;
1186 return strdup(v.dump().c_str());
1187}
1188
1207 const std::string& constitution_text) {
1208 entropic::HookInterface hook_iface;
1209 hook_iface.registry = &h->hook_registry;
1210 hook_iface.fire_pre = [](void* reg, entropic_hook_point_t pt,
1211 const char* json, char** out) -> int {
1212 return static_cast<entropic::HookRegistry*>(reg)
1213 ->fire_pre(pt, json, out);
1214 };
1215 hook_iface.fire_post = [](void* reg, entropic_hook_point_t pt,
1216 const char* json, char** out) {
1217 static_cast<entropic::HookRegistry*>(reg)
1218 ->fire_post(pt, json, out);
1219 };
1220 hook_iface.fire_info = [](void* reg, entropic_hook_point_t pt,
1221 const char* json) {
1222 static_cast<entropic::HookRegistry*>(reg)->fire_info(pt, json);
1223 };
1224 h->engine->set_hooks(hook_iface);
1225 // E9 (2.0.6-rc19): forward the same hook dispatch to the tool
1226 // executor so PRE_TOOL_CALL / POST_TOOL_CALL actually fire.
1227 // Prior wiring only touched the engine; tool_executor_ held a
1228 // null HookInterface and silently skipped all tool hooks.
1229 if (h->tool_executor) {
1230 h->tool_executor->set_hooks(hook_iface);
1231 }
1232
1234 && !constitution_text.empty()) {
1235 h->validator = std::make_unique<entropic::ConstitutionalValidator>(
1236 h->config.constitutional_validation, constitution_text);
1237 h->validator->attach(&hook_iface, &iface);
1238 // gh#108 (v2.10.3): resolve reasoning delimiters PER TIER. One
1239 // validator serves every tier, but thinking format is a per-family
1240 // property — before this, the validator hardcoded `<think>` and so
1241 // handed the critique model raw `<|channel>` reasoning on a Gemma-4
1242 // tier, exactly the failure it exists to prevent. The facade is the
1243 // lowest layer that can see both, so it owns the lambda; core takes
1244 // plain strings and keeps no dependency on inference.
1245 h->validator->set_marker_resolver(
1246 [h](const std::string& tier)
1247 -> std::pair<std::string, std::string> {
1248 auto* adapter = (h->orchestrator != nullptr)
1249 ? h->orchestrator->get_adapter(tier) : nullptr;
1250 if (adapter == nullptr) { return {"<think>", "</think>"}; }
1251 auto m = adapter->thinking_markers();
1252 return {m.open, m.close};
1253 });
1254 // E3 (2.0.6-rc17): expose validator verdict via ON_COMPLETE
1255 // hook context.
1256 h->engine->set_validation_provider(sp_get_validation, h);
1257 s_log->info("Constitutional validator attached (max_revisions={})",
1259 }
1260}
1261
1262// ── State provider callbacks ─────────────────────────────
1263
1269static char* sp_get_config(void* ud) {
1270 auto* h = static_cast<entropic_engine*>(ud);
1271 nlohmann::json j;
1272 j["default_tier"] = h->config.models.default_tier;
1273 j["log_level"] = h->config.log_level;
1274 j["log_dir"] = h->config.log_dir.string();
1275 j["ggml_logging"] = h->config.ggml_logging;
1276 return strdup(j.dump().c_str());
1277}
1278
1293 entropic_engine* h, const std::string& tier_name) {
1294 auto data_dir = entropic::config::resolve_data_dir(h->config);
1295 std::string constitution, app_ctx;
1296 // gh#156: was two discarded error strings; the shared loader logs them.
1297 entropic::prompts::load_shared_prompt_sources(
1298 h->config, data_dir, constitution, app_ctx);
1299 std::string identity_body;
1300 auto it = h->config.models.tiers.find(tier_name);
1301 if (it != h->config.models.tiers.end()) {
1302 identity_body = entropic::prompts::resolve_tier_identity(
1303 it->second, tier_name, data_dir);
1304 }
1305 std::string out;
1306 if (!constitution.empty()) { out += constitution + "\n\n"; }
1307 if (!app_ctx.empty()) { out += app_ctx + "\n\n"; }
1308 if (!identity_body.empty()) { out += identity_body; }
1309
1310 // gh#87 (v2.7.0): tool defs are no longer string-injected into the
1311 // system prompt — they flow via params.tools and common_chat renders
1312 // them in the model's native format. So the assembled system-prompt
1313 // preview no longer includes a tool section.
1314 return out;
1315}
1316
1332static char* sp_get_identities(void* ud) {
1333 auto* h = static_cast<entropic_engine*>(ud);
1334 nlohmann::json arr = nlohmann::json::array();
1335 for (const auto& [name, _] : h->config.models.tiers) {
1336 nlohmann::json entry;
1337 entry["name"] = name;
1338 entry["assembled_prompt"] =
1340 arr.push_back(std::move(entry));
1341 }
1342 return strdup(arr.dump().c_str());
1343}
1344
1350static char* sp_get_tools(void* ud) {
1351 auto* h = static_cast<entropic_engine*>(ud);
1352 if (!h->server_manager) { return strdup("[]"); }
1353 return strdup(h->server_manager->list_tools().c_str());
1354}
1355
1374static char* sp_get_history(int max_entries, void* ud) {
1375 auto* h = static_cast<entropic_engine*>(ud);
1376 if (!h || !h->engine) { return strdup("[]"); }
1377 const auto& msgs = h->engine->get_messages();
1378 nlohmann::json arr = nlohmann::json::array();
1379 int start = 0;
1380 if (max_entries > 0
1381 && static_cast<int>(msgs.size()) > max_entries) {
1382 start = static_cast<int>(msgs.size()) - max_entries;
1383 }
1384 for (int i = start; i < static_cast<int>(msgs.size()); ++i) {
1385 const auto& m = msgs[static_cast<size_t>(i)];
1386 std::string preview = m.content.size() > 200
1387 ? entropic::facade::utf8_safe_substr(m.content, 200) + "..."
1388 : m.content;
1389 arr.push_back({
1390 {"role", m.role},
1391 {"content_preview", preview},
1392 {"token_count_est", m.content.size() / 4u}
1393 });
1394 }
1395 return strdup(arr.dump().c_str());
1396}
1397
1411static char* sp_get_residency(void* ud) {
1412 auto* h = static_cast<entropic_engine*>(ud);
1413 if (!h || !h->orchestrator) {
1414 return strdup("{\"vram_total_bytes\":0,\"vram_budget_bytes\":0,"
1415 "\"vram_headroom_bytes\":0,\"backend\":\"unknown\","
1416 "\"residency\":[]}");
1417 }
1418 return strdup(h->orchestrator->residency_snapshot_json().c_str());
1419}
1420
1426static char* sp_get_state(void* ud) {
1427 auto* h = static_cast<entropic_engine*>(ud);
1428 nlohmann::json j;
1429 j["engine_state"] = h->configured.load() ? "configured" : "init";
1430 j["default_tier"] = h->config.models.default_tier;
1431
1432 nlohmann::json tiers = nlohmann::json::array();
1433 for (const auto& [name, _] : h->config.models.tiers) {
1434 tiers.push_back(name);
1435 }
1436 j["active_tiers"] = tiers;
1437
1438 if (h->server_manager) {
1439 j["working_dir"] = h->server_manager->project_dir().string();
1440 j["registered_servers"] = h->server_manager->server_names();
1441 }
1442 j["data_dir"] = entropic::config::resolve_data_dir(
1443 h->config).string();
1444 j["log_dir"] = h->config.log_dir.string();
1445 return strdup(j.dump().c_str());
1446}
1447
1463static nlohmann::json generations_json(entropic_engine* h) {
1464 nlohmann::json arr = nlohmann::json::array();
1465 if (h == nullptr || !h->orchestrator) { return arr; }
1466 for (const auto& rec : h->orchestrator->generation_records()) {
1467 arr.push_back({
1468 {"finish_reason", rec.finish_reason},
1469 {"token_count", rec.token_count},
1470 {"prefill_tokens", rec.prefill_tokens},
1471 {"throughput_tok_s", rec.throughput_tok_s},
1472 {"n_drafted", rec.n_drafted},
1473 {"n_accepted", rec.n_accepted},
1474 {"grammar", {
1475 {"source", rec.grammar.source},
1476 {"key", rec.grammar.key},
1477 {"resolved", rec.grammar.resolved},
1478 {"conflict_winner", rec.grammar.conflict_winner},
1479 }},
1480 });
1481 }
1482 return arr;
1483}
1484
1502static char* sp_get_metrics(void* ud) {
1503 auto* h = static_cast<entropic_engine*>(ud);
1504 if (!h || !h->engine) { return strdup("{}"); }
1505 auto m = h->engine->last_loop_metrics();
1506 nlohmann::json j;
1507 j["generations"] = generations_json(h);
1508 j["iterations"] = m.iterations;
1509 j["tool_calls"] = m.tool_calls;
1510 j["tokens_used"] = m.tokens_used;
1511 j["errors"] = m.errors;
1512 j["duration_ms"] = m.duration_ms();
1513 // Per-tier breakdown (P2-15 follow-up, 2.0.6-rc16.2)
1514 nlohmann::json per_tier = nlohmann::json::object();
1515 for (auto& [tier, tm] : h->engine->per_tier_metrics()) {
1516 per_tier[tier] = {
1517 {"iterations", tm.iterations},
1518 {"tool_calls", tm.tool_calls},
1519 {"tokens_used", tm.tokens_used},
1520 {"errors", tm.errors},
1521 {"duration_ms", tm.duration_ms()},
1522 };
1523 }
1524 j["per_tier"] = per_tier;
1525 return strdup(j.dump().c_str());
1526}
1527
1533static char* sp_get_docs(const char* section, void* ud) {
1534 (void)section;
1535 (void)ud;
1536 return strdup("");
1537}
1538
1555 const char* query, int max_results, void* ud) {
1556 auto* h = static_cast<entropic_engine*>(ud);
1557 std::string out;
1558 if (h == nullptr || !h->storage || query == nullptr
1559 || !h->storage->search_delegations(query, max_results, out)) {
1560 return nullptr;
1561 }
1562 auto rows = nlohmann::json::parse(out, nullptr, false);
1563 nlohmann::json records = nlohmann::json::array();
1564 for (const auto& r : rows.is_array() ? rows : nlohmann::json::array()) {
1565 nlohmann::json rec;
1566 rec["delegation_id"] = r.value("id", "");
1567 rec["target_tier"] = r.value("target_tier", "");
1568 rec["summary"] = r.value("result_summary", "");
1569 rec["completed_at"] = r.value("completed_at", "");
1570 records.push_back(std::move(rec));
1571 }
1572 return strdup(records.dump().c_str());
1573}
1574
1585 const char* delegation_id, void* ud) {
1586 auto* h = static_cast<entropic_engine*>(ud);
1587 if (h == nullptr || !h->storage || delegation_id == nullptr) {
1588 return nullptr;
1589 }
1590 std::string out;
1592 delegation_id, out, h->storage.get())) {
1593 return nullptr;
1594 }
1595 return strdup(out.c_str());
1596}
1597
1610 if (!h->server_manager) { return; }
1611 auto* es = dynamic_cast<entropic::EntropicServer*>(
1612 h->server_manager->get_server("entropic"));
1613 if (es == nullptr) { return; }
1614
1617 sp.get_identities = sp_get_identities;
1618 sp.get_tools = sp_get_tools;
1619 sp.get_history = sp_get_history;
1620 sp.get_state = sp_get_state;
1621 sp.get_metrics = sp_get_metrics;
1622 sp.get_docs = sp_get_docs;
1623 // gh#32 (v2.1.6): storage-backed delegation recall + resume.
1624 sp.search_delegations = sp_search_delegations;
1625 sp.load_delegation_conversation = sp_load_delegation_conversation;
1626 // gh#57 (v2.2.4): VRAM residency-set snapshot.
1627 sp.get_residency = sp_get_residency;
1628 sp.user_data = h;
1629 es->set_state_provider(sp);
1630 s_log->info("State provider wired to entropic server");
1631}
1632
1640 if (!h->validator) { return; }
1641 for (const auto& [name, rules] : h->tier_validation_rules) {
1642 h->validator->set_tier_rules(name, rules);
1643 }
1644}
1645
1646// ── gh#160: delegation isolation wiring ────────────────────
1647
1664static std::filesystem::path facade_session_root(
1665 const std::string& session_key, void* ud) {
1666 auto* h = static_cast<entropic_handle_t>(ud);
1667 auto* servers = entropic::workspace_servers(h, session_key);
1668 if (servers == nullptr) { return {}; }
1669 return servers->project_dir();
1670}
1671
1681static std::vector<std::string> facade_unsafe_external_tools(
1682 const std::string& session_key,
1683 const std::vector<std::string>& allowed, void* ud) {
1684 auto* h = static_cast<entropic_handle_t>(ud);
1685 auto* servers = entropic::workspace_servers(h, session_key);
1686 if (servers == nullptr) { return {}; }
1687 // gh#166: a workspace's external servers connect when the workspace is
1688 // created, so their annotations are already cached by the time a
1689 // delegation is admitted — no lazy spawn is needed at the gate.
1690 return servers->external_tools_without_readonly_hint(allowed);
1691}
1692
1703 iface.user_data = h;
1704 h->engine->set_session_root_interface(iface);
1705 h->engine->set_dir_swap(entropic::swap_session_tool_dir, h);
1706}
1707
1717 // gh#110 (v2.9.6): was hardcoded true, making the agent loop always
1718 // stream and therefore permanently out of MTP's envelope (MTP
1719 // rejects a bound on_token callback — see mtp_envelope.h).
1722 // gh#160 (v2.13.0): opt-in delegation sandboxing. OFF means no
1723 // snapshot is taken at all — the wasted copy the issue reported
1724 // disappears with the feature it was never serving.
1729 auto it = h->config.models.tiers.find(h->config.models.default_tier);
1730 if (it != h->config.models.tiers.end()) {
1731 lc.context_length = it->second.context_length;
1732 }
1733 // gh#80 (v2.5.0): map the generation.budget_mode string to the
1734 // BudgetMode enum. Unknown values resolve to off with a warning.
1735 const std::string& bm = h->config.generation.budget_mode;
1736 if (bm == "tokens") {
1738 } else if (bm == "wall_clock") {
1740 } else {
1741 if (bm != "off") {
1742 s_log->warn("Unknown generation.budget_mode '{}' — "
1743 "treating as 'off'", bm);
1744 }
1746 }
1748 // A non-positive limit can't gate anything — disable to avoid a
1749 // pathological "exhausted at zero" loop.
1750 if (lc.budget_limit <= 0) {
1752 }
1753 return lc;
1754}
1755
1769 const int port = h->config.mcp.external.http_port;
1770 std::string err;
1771 if (port != 0 && (port < 0 || port > 65535)) {
1772 err = "mcp.external.http_port out of range: " + std::to_string(port);
1773 } else if (port != 0 && !h->external_bridge) {
1774 err = "the unix bridge did not start";
1775 } else if (port != 0) {
1776 h->http_listener = std::make_unique<entropic::http::HttpListener>(
1777 h->external_bridge.get(), port);
1778 if (!h->http_listener->start(err)) { h->http_listener.reset(); }
1779 }
1780 if (!err.empty()) {
1781 h->last_error = "HTTP listener: " + err;
1782 s_log->error("{}", h->last_error);
1783 }
1784 return err.empty() ? ENTROPIC_OK : ENTROPIC_ERROR_IO;
1785}
1786
1799 if (!h->config.mcp.external.enabled) { return; }
1800 // Same derivation the self-connection guard uses (gh#200).
1801 h->external_bridge = std::make_unique<entropic::ExternalBridge>(
1803 if (!h->external_bridge->start()) {
1804 s_log->warn("External MCP bridge failed to start");
1805 h->external_bridge.reset();
1806 }
1807}
1808
1835 if (!h->configured.load()) { return ENTROPIC_OK; }
1836 h->last_error = "handle already configured";
1837 s_log->error("{}", h->last_error);
1839}
1840
1860 entropic_handle_t h, const std::filesystem::path& data_dir) {
1861 h->orchestrator = std::make_unique<entropic::ModelOrchestrator>();
1862 if (!h->orchestrator->initialize(h->config)) {
1863 h->last_error = "orchestrator initialization failed";
1864 s_log->error("{}", h->last_error);
1866 }
1867 // Fallback grammar loading: only if initialize() didn't find
1868 // grammars via config_dir. Avoids overwriting patched grammars.
1869 if (h->orchestrator->grammar_registry().size() == 0) {
1870 h->orchestrator->load_grammars_from(data_dir / "grammars");
1871 }
1872 return ENTROPIC_OK;
1873}
1874
1888 entropic_handle_t h, const std::filesystem::path& data_dir) {
1889 h->mcp_auth = std::make_unique<entropic::MCPAuthorizationManager>();
1890 h->identity_manager = std::make_unique<entropic::IdentityManager>(
1892 // P1-7: route identity changes to prompt-cache invalidation.
1893 h->identity_manager->set_cache_invalidator(
1894 [](void* ud) {
1895 auto* orch = static_cast<entropic::ModelOrchestrator*>(ud);
1896 if (orch) { orch->clear_all_prompt_caches(); }
1897 }, h->orchestrator.get());
1898 init_mcp_servers(h, data_dir);
1899
1900 // gh#58 follow-up (v2.2.6): per-handle InterfaceContext. Pre-v2.2.6
1901 // build_orchestrator_interface stored the context in a process-
1902 // global static, so a second configure freed the first handle's
1903 // context and h1.run() segfaulted on a use-after-free.
1907 h->inference_iface.get_tool_prompt = facade_get_tool_prompt;
1908 h->inference_iface.tool_prompt_data = h;
1909 auto lc = build_loop_config(h);
1910 h->engine = std::make_unique<entropic::AgentEngine>(
1911 h->inference_iface, lc, h->config.compaction);
1912 // gh#76 (v2.3.27): wire the compactor registry now that the engine
1913 // owns the CompactionManager that backs the default-compactor
1914 // fallback. Pre-v2.3.27 the field was declared but never
1915 // constructed, so every `entropic_register_compactor` /
1916 // `entropic_compact` call returned INVALID_STATE.
1918 std::make_unique<entropic::CompactorRegistry>(
1919 h->engine->compaction_manager());
1920 rewire_observers(h); // gh#40 + fallout (v2.1.10)
1921 wire_external_interrupt(h); // P1-10
1922 // gh#158 (v2.13.0): opt-in per-session-key run concurrency, default off.
1923 h->engine->set_concurrent_sessions(h->config.concurrent_sessions);
1924 wire_session_roots(h); // gh#160
1926}
1927
1939 entropic_handle_t h, const std::filesystem::path& data_dir) {
1940 auto shared_prefix = build_shared_prompt_prefix(h, data_dir);
1941 cache_tier_allowed_tools(h, data_dir); // gh#121: must fill h->tier_allowed_tools
1942 populate_tier_info(h, data_dir, shared_prefix); // before populate reads it
1943 h->engine->set_handoff_rules(h->config.routing.handoff_rules);
1944
1945 wire_hooks_and_validator(h, h->inference_iface, shared_prefix);
1947
1950
1951 h->engine->set_system_prompt(
1952 entropic::prompts::assemble(h->config, data_dir));
1953 if (h->session_logger) {
1954 h->engine->set_session_logger(h->session_logger.get());
1955 }
1956}
1957
1976 entropic_handle_t h, const std::filesystem::path& data_dir) {
1977 auto err = entropic::prompts::validate_configured_prompts(
1978 h->config, data_dir);
1980 if (!err.empty()) {
1981 h->last_error = err;
1982 s_log->error("configure: {}", err);
1984 }
1985 return rc;
1986}
1987
2014 entropic_handle_t h, const std::filesystem::path& data_dir) {
2015 auto warning = entropic::config::warn_unresolved_tier_grammars(
2016 h->config,
2017 entropic::config::grammar_search_paths(h->config, data_dir));
2018 if (!warning.empty()) {
2019 s_log->warn("configure: {}", warning);
2020 }
2021}
2022
2036 auto rc = reject_if_configured(h);
2037 if (rc != ENTROPIC_OK) { return rc; }
2038 // gh#59 follow-up (v2.3.7): honor console_logging before any init
2039 // logging fires. When false, strip the stderr console sink so the
2040 // file sink (already installed by setup_session) is the only route
2041 // — TUI consumers paint to fd 2 and can't tolerate engine output
2042 // there. Default (true) is a no-op; operators keep stderr logs.
2043 entropic::log::set_console_enabled(h->config.console_logging);
2044 auto data_dir = entropic::config::resolve_data_dir(h->config);
2045 // gh#156 (v2.13.0): a configured constitution / app_context path that
2046 // cannot be loaded is a config error, and it is raised HERE — before
2047 // init_orchestrator loads the model.
2048 rc = validate_prompt_sources(h, data_dir);
2049 if (rc == ENTROPIC_OK) {
2050 // gh#94 (v2.7.3): thread per-tier frontmatter samplers into the
2051 // config BEFORE the orchestrator snapshots it by value. The
2052 // engine-bound frontmatter wiring stays in
2053 // wire_prompts_and_persistence (post-engine).
2054 thread_frontmatter_samplers(h, data_dir);
2055 // gh#154 (v2.13.0): name a tier grammar stem that does not resolve
2056 // YET. Not a refusal — entropic_grammar_register* needs the
2057 // orchestrator this call is about to build, so a consumer's only
2058 // legal order is configure-then-register. The run that selects the
2059 // tier is what fails, loudly and typed, if nobody ever did.
2060 warn_tier_grammars_step(h, data_dir);
2061 }
2062 if (rc == ENTROPIC_OK) {
2063 rc = init_orchestrator(h, data_dir);
2064 }
2065 if (rc != ENTROPIC_OK) { return rc; }
2066
2067 init_engine_and_interfaces(h, data_dir);
2068 wire_prompts_and_persistence(h, data_dir);
2069
2070 h->configured.store(true);
2072 // gh#204: an explicitly configured HTTP listener that cannot start fails
2073 // configure, so a headless host exits instead of running unreachable.
2074 const auto http_rc = start_http_listener(h);
2075 s_log->info("configure complete");
2076 return http_rc;
2077}
2078
2087 entropic_handle_t handle, const char* config_json) {
2089 auto err = entropic::config::load_config_from_string(
2090 config_json, handle->bundled_models, handle->config);
2091 if (!err.empty()) {
2092 handle->last_error = err;
2093 s_log->error("configure: {}", err);
2095 }
2096 // gh#109 follow-up: parity with configure_dir/configure_from_file.
2097 // Without this, a JSON-string config that sets "log_dir" got no
2098 // session.log at all — config.log_dir was honored for the sqlite/
2099 // conversation store (init_persistence) but never wired to the
2100 // per-handle log dispatcher, so no file sink existed for any
2101 // HandleLogScope-tagged log line to route to.
2102 if (!handle->config.log_dir.empty()) {
2103 entropic::log::setup_session(handle->config.log_dir);
2104 entropic::log::register_handle_log(
2105 handle->log_id, handle->config.log_dir);
2106 }
2107 return configure_common(handle);
2108}
2109
2121 entropic_handle_t handle,
2122 const char* config_json) {
2123 if (!handle || !config_json) {
2124 return !handle ? ENTROPIC_ERROR_INVALID_HANDLE
2126 }
2127 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
2128 return c_api_try(handle,
2129 [&]() { return do_configure_json(handle, config_json); });
2130}
2131
2140 entropic_handle_t handle, const char* config_path) {
2142 auto err = entropic::config::load_config_from_file(
2143 config_path, handle->bundled_models, handle->config);
2144 if (!err.empty()) {
2145 handle->last_error = err;
2146 s_log->error("configure_from_file: {}", err);
2148 }
2149 // Parity with configure_dir: if the parsed config specifies a
2150 // log_dir, start session logging there. Without this, consumers
2151 // using the file-based API get no session.log on disk even when
2152 // their YAML declares log_dir.
2153 if (!handle->config.log_dir.empty()) {
2154 entropic::log::setup_session(handle->config.log_dir);
2155 // gh#59 (v2.3.1): per-handle dispatcher file sink so log lines
2156 // emitted within this handle's HandleLogScope route to this
2157 // handle's session.log only.
2158 entropic::log::register_handle_log(
2159 handle->log_id, handle->config.log_dir);
2160 }
2161 return configure_common(handle);
2162}
2163
2175 entropic_handle_t handle,
2176 const char* config_path) {
2177 if (!handle || !config_path) {
2178 return !handle ? ENTROPIC_ERROR_INVALID_HANDLE
2180 }
2181 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
2182 return c_api_try(handle,
2183 [&]() { return do_configure_from_file(handle, config_path); });
2184}
2185
2195 entropic_handle_t handle, const char* project_dir) {
2196 // Session logging FIRST — capture everything from preload through init.
2197 if (project_dir && project_dir[0] != '\0') {
2198 entropic::log::setup_session(project_dir);
2199 // gh#59 (v2.3.1): per-handle dispatcher registration. Routes
2200 // session.log writes for this handle's HandleLogScope-tagged
2201 // threads to this handle's file only — no cross-handle bleed.
2202 entropic::log::register_handle_log(handle->log_id, project_dir);
2203 }
2205 std::filesystem::path proj_dir = (project_dir && project_dir[0] != '\0')
2206 ? project_dir : "";
2207 auto err = entropic::config::load_layered(
2208 proj_dir, "default_config.yaml",
2209 handle->bundled_models, handle->config);
2210 if (!err.empty()) {
2211 handle->last_error = err;
2212 s_log->error("configure_dir: {}", err);
2214 }
2215 auto rc = configure_common(handle);
2216 // gh#31 (v2.1.6): propagate the configured project_dir into the
2217 // engine so `AgentEngine::get_repo_dir()` uses it as the sandbox
2218 // snapshot source.
2219 if (rc == ENTROPIC_OK && handle->engine && !proj_dir.empty()) {
2220 handle->engine->set_project_dir(std::filesystem::absolute(proj_dir));
2221 }
2222 return rc;
2223}
2224
2242 entropic_handle_t handle,
2243 const char* project_dir) {
2244 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
2245 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
2246 return c_api_try(handle,
2247 [&]() { return do_configure_dir(handle, project_dir); });
2248}
2249
2263 if (handle == nullptr) {
2264 return;
2265 }
2266 s_log->info("entropic_destroy()");
2267
2268 // Stop external bridge FIRST — it holds a raw pointer to handle
2269 // gh#204: the HTTP front end dispatches into the bridge — stop it first.
2270 handle->http_listener.reset();
2271 if (handle->external_bridge) {
2272 handle->external_bridge->stop();
2273 handle->external_bridge.reset();
2274 }
2275
2277
2278 // gh#58 follow-up (v2.2.6): release per-handle InterfaceContext
2279 // before the orchestrator unloads, since the context holds a raw
2280 // orchestrator pointer used by the iface callbacks.
2282 handle->inference_iface_ctx = nullptr;
2283
2284 // gh#166 (v2.13.0): named workspaces own server instances with live
2285 // child processes. Shut them down explicitly, before the engine that
2286 // may still hold a resolver pointing at them goes away — teardown
2287 // order is the recurring gh#58 failure shape, not left to chance.
2288 {
2289 std::lock_guard<std::mutex> guard(handle->workspace_mutex);
2290 for (auto& [name, ws] : handle->workspaces) {
2291 if (ws && ws->servers) { ws->servers->shutdown(); }
2292 }
2293 handle->workspaces.clear();
2294 handle->session_workspace.clear();
2295 }
2296
2297 // gh#59 (v2.3.1): release the per-handle session.log file sink so
2298 // a subsequent handle that happens to reuse the same log_id can
2299 // open the file fresh. Safe on never-registered ids.
2300 entropic::log::unregister_handle_log(handle->log_id);
2301
2302 // Phase 1+ subsystem teardown will go here in reverse order.
2303 // Phase 0: struct itself owns hook_registry by value.
2304 delete handle;
2305}
2306
2314const char* entropic_version(void) {
2315 return CONFIG_ENTROPIC_VERSION_STRING;
2316}
2317
2326 return 2;
2327}
2328
2338 if (!handle || !handle->engine) { return 0; }
2339 return handle->engine->seconds_since_last_activity();
2340}
2341
2350void* entropic_alloc(size_t size) {
2351 return malloc(size);
2352}
2353
2361void entropic_free(void* ptr) {
2362 free(ptr);
2363}
2364
2387 entropic_handle_t handle,
2388 const char* input,
2389 char** result_json) {
2390 // gh#109: NO api_mutex — a long turn must not block entropic_interrupt()
2391 // called from another thread. gh#144 (v2.12.0): the claim is a single
2392 // compare-exchange, so that property holds by construction. The guard is
2393 // declared BEFORE validation but claim() is evaluated LAST in the chain
2394 // below, so a call rejected for a bad argument never momentarily claims
2395 // the engine and bounces a legitimate concurrent caller.
2396 entropic::HandleTurnGuard turn(handle);
2397 auto rc = check_orchestrator(handle);
2398 if (rc != ENTROPIC_OK || !input || !result_json || !handle->engine
2399 || !turn.claim()) {
2400 return rc != ENTROPIC_OK ? rc
2401 : (!input || !result_json) ? ENTROPIC_ERROR_INVALID_ARGUMENT
2404 }
2405 try {
2406 auto result = handle->engine->run_turn(input);
2407 *result_json = alloc_cstr(
2408 facade_json::serialize_messages(result));
2409 // Synthetic completion sentinel — lets observers detect the
2410 // end of a non-streaming run. Contract: (token="", len=0).
2411 // (P0-1, 2.0.6-rc16)
2412 if (handle->stream_observer != nullptr) {
2413 handle->stream_observer(
2414 "", 0, handle->stream_observer_data);
2415 }
2416 return ENTROPIC_OK;
2417 } catch (const std::exception& e) {
2418 handle->last_error = e.what();
2419 s_log->error("run: {}", handle->last_error);
2420 // P3-19 follow-up (2.0.6-rc16.2): surface partial context on
2421 // crash so callers can recover tool_results and any partial
2422 // assistant content accumulated before the failure.
2423 try {
2424 *result_json = alloc_cstr(
2425 facade_json::serialize_messages(
2426 handle->engine->get_messages()));
2427 } catch (...) {
2428 *result_json = nullptr;
2429 }
2431 }
2432}
2433
2443 entropic_handle_t handle, const char* tier, const char* input,
2444 char** result_json) {
2445 try {
2446 auto result = handle->engine->run_turn_as(tier, input);
2447 *result_json = alloc_cstr(facade_json::serialize_messages(result));
2448 // Synthetic completion sentinel (token="", len=0), matching run().
2449 if (handle->stream_observer != nullptr) {
2450 handle->stream_observer("", 0, handle->stream_observer_data);
2451 }
2452 return ENTROPIC_OK;
2453 } catch (const std::exception& e) {
2454 handle->last_error = e.what();
2455 s_log->error("run_as: {}", handle->last_error);
2456 // Define *result_json on the error path, matching entropic_run()'s
2457 // documented identical contract: surface partial context, else nullptr.
2458 try {
2459 *result_json = alloc_cstr(
2460 facade_json::serialize_messages(handle->engine->get_messages()));
2461 } catch (...) {
2462 *result_json = nullptr;
2463 }
2465 }
2466}
2467
2489 entropic_handle_t handle,
2490 const char* tier_or_identity,
2491 const char* input,
2492 char** result_json) {
2493 // gh#109: NO api_mutex — a long turn must not block entropic_interrupt()
2494 // called from another thread. gh#144 (v2.12.0): the claim is a single
2495 // compare-exchange, so that property holds by construction. The guard is
2496 // declared BEFORE validation but claim() is evaluated LAST in the chain
2497 // below, so a call rejected for a bad argument never momentarily claims
2498 // the engine and bounces a legitimate concurrent caller.
2499 entropic::HandleTurnGuard turn(handle);
2500 auto rc = check_orchestrator(handle);
2501 if (rc != ENTROPIC_OK || !tier_or_identity || !input || !result_json
2502 || !handle->engine || !turn.claim()) {
2503 return rc != ENTROPIC_OK ? rc
2504 : (!tier_or_identity || !input || !result_json)
2508 }
2509 if (!handle->engine->has_tier(tier_or_identity)) {
2510 handle->last_error =
2511 std::string("unknown tier: ") + tier_or_identity;
2512 s_log->error("run_as: {}", handle->last_error);
2514 }
2515 return run_as_inner(handle, tier_or_identity, input, result_json);
2516}
2517
2525static std::string serialize_batch_results(
2526 const std::vector<entropic::GenerationResult>& results) {
2527 nlohmann::json arr = nlohmann::json::array();
2528 for (const auto& r : results) {
2529 nlohmann::json obj;
2530 obj["content"] = entropic::mcp::sanitize_utf8(r.content);
2531 obj["finish_reason"] = r.finish_reason;
2532 obj["tool_calls"] = nlohmann::json::parse(
2533 entropic::serialize_tool_calls(r.tool_calls), nullptr, false);
2534 arr.push_back(std::move(obj));
2535 }
2536 return arr.dump();
2537}
2538
2553static std::vector<std::vector<entropic::Message>> build_batch_messages(
2554 entropic_handle_t handle, const char** tiers, const char** prompts,
2555 size_t n, std::vector<std::string>& tiers_out) {
2556 std::vector<std::vector<entropic::Message>> msgs(n);
2557 tiers_out.resize(n);
2558 for (size_t i = 0; i < n; ++i) {
2559 tiers_out[i] = (tiers && tiers[i]) ? tiers[i] : "";
2560 const std::string& sys =
2561 handle->engine->tier_system_prompt(tiers_out[i]);
2562 std::vector<entropic::Message> m;
2563 if (!sys.empty()) {
2565 s.role = "system";
2566 s.content = sys;
2567 m.push_back(std::move(s));
2568 }
2570 u.role = "user";
2571 u.content = prompts[i] ? prompts[i] : "";
2572 m.push_back(std::move(u));
2573 msgs[i] = std::move(m);
2574 }
2575 return msgs;
2576}
2577
2601 entropic_handle_t handle,
2602 const char** tiers,
2603 const char** prompts,
2604 size_t n,
2605 char** result_json) {
2606 // gh#109: NO api_mutex — a long turn must not block entropic_interrupt()
2607 // called from another thread. gh#144 (v2.12.0): the claim is a single
2608 // compare-exchange, so that property holds by construction. The guard is
2609 // declared BEFORE validation but claim() is evaluated LAST in the chain
2610 // below, so a call rejected for a bad argument never momentarily claims
2611 // the engine and bounces a legitimate concurrent caller.
2612 entropic::HandleTurnGuard turn(handle);
2613 auto rc = check_orchestrator(handle);
2614 if (rc != ENTROPIC_OK || !prompts || !result_json || !handle->engine
2615 || n == 0 || !turn.claim()) {
2616 return rc != ENTROPIC_OK ? rc
2617 : (!prompts || !result_json || n == 0)
2621 }
2622 try {
2623 std::vector<std::string> tiers_vec;
2624 auto msgs = build_batch_messages(handle, tiers, prompts, n, tiers_vec);
2625 std::vector<entropic::GenerationParams> params(n);
2626 // gh#158 (v2.13.0): this was a LOCAL `std::atomic<bool> cancel{false}`
2627 // that nothing in the tree could ever set, so a batch was
2628 // uninterruptible — `entropic_interrupt()` returned OK and the batch
2629 // ran to completion. The run's own cancel token is the flag the
2630 // handle-wide interrupt and `entropic_interrupt_session` both set,
2631 // and `run_batch_gen_loop` already polls its argument per step.
2632 std::atomic<bool>& cancel = handle->engine->run_cancel_flag();
2633 auto results = handle->orchestrator->generate_batch(
2634 msgs, params, tiers_vec, cancel);
2635 *result_json = alloc_cstr(serialize_batch_results(results));
2636 return ENTROPIC_OK;
2637 } catch (const std::exception& e) {
2638 handle->last_error = e.what();
2639 s_log->error("run_batch: {}", handle->last_error);
2640 // Define *result_json on the error path (C-ABI: output params must be
2641 // defined). No partial batch recovery — an empty JSON array.
2642 *result_json = alloc_cstr("[]");
2644 }
2645}
2646
2647/* StreamBridge + think filter moved to inference/stream_think_filter.cpp — Step 4 */
2648
2649/* StreamCtx + stream_chunk_cb → engine->run_streaming() — v2.0.2 */
2650
2667 entropic_handle_t handle,
2668 const char* input,
2669 void (*on_token)(const char* token, size_t len, void* user_data),
2670 void* user_data,
2671 int* cancel_flag) {
2672 // gh#109: NO api_mutex — a long turn must not block entropic_interrupt()
2673 // called from another thread. gh#144 (v2.12.0): the claim is a single
2674 // compare-exchange, so that property holds by construction. The guard is
2675 // declared BEFORE validation but claim() is evaluated LAST in the chain
2676 // below, so a call rejected for a bad argument never momentarily claims
2677 // the engine and bounces a legitimate concurrent caller.
2678 entropic::HandleTurnGuard turn(handle);
2679 auto rc = check_orchestrator(handle);
2680 if (rc != ENTROPIC_OK || !input || !on_token || !handle->engine
2681 || !turn.claim()) {
2682 return rc != ENTROPIC_OK ? rc
2683 : (!input || !on_token || !handle->engine)
2686 }
2687
2688
2689 // Observer multiplexing is handled inside ResponseGenerator — the
2690 // facade passes on_token through untouched. (P0-1, 2.0.6-rc16)
2691 try {
2692 int code = handle->engine->run_streaming(
2693 input, on_token, user_data, cancel_flag);
2694 if (handle->stream_observer != nullptr) {
2695 handle->stream_observer(
2696 "", 0, handle->stream_observer_data);
2697 }
2698 return code == 1 ? ENTROPIC_ERROR_CANCELLED : ENTROPIC_OK;
2699 } catch (const std::exception& e) {
2700 handle->last_error = e.what();
2701 s_log->error("run_streaming: {}", handle->last_error);
2703 }
2704}
2705
2706// ── gh#37 (v2.1.8): multimodal messages entry points ──────────
2707
2725static std::vector<entropic::Message> parse_and_check_vision(
2726 entropic_handle_t handle,
2727 const char* messages_json,
2728 entropic_error_t& out_rc) {
2729 auto msgs = entropic::parse_messages_json(messages_json);
2731 && handle->orchestrator
2732 && !handle->orchestrator->has_vision_capable_tier()) {
2734 return {};
2735 }
2736 out_rc = ENTROPIC_OK;
2737 return msgs;
2738}
2739
2758 entropic_handle_t handle,
2759 const char* messages_json,
2760 char** result_json) {
2762 auto msgs = parse_and_check_vision(handle, messages_json, vrc);
2763 if (vrc != ENTROPIC_OK) { return vrc; }
2764 auto result = handle->engine->run_turn(std::move(msgs));
2765 *result_json = alloc_cstr(
2766 facade_json::serialize_messages(result));
2767 if (handle->stream_observer != nullptr) {
2768 handle->stream_observer(
2769 "", 0, handle->stream_observer_data);
2770 }
2771 return ENTROPIC_OK;
2772}
2773
2794 entropic_handle_t handle,
2795 const char* messages_json,
2796 char** result_json) {
2797 // gh#109: NO api_mutex — a long turn must not block entropic_interrupt()
2798 // called from another thread. gh#144 (v2.12.0): the claim is a single
2799 // compare-exchange, so that property holds by construction. The guard is
2800 // declared BEFORE validation but claim() is evaluated LAST in the chain
2801 // below, so a call rejected for a bad argument never momentarily claims
2802 // the engine and bounces a legitimate concurrent caller.
2803 entropic::HandleTurnGuard turn(handle);
2804 auto rc = check_orchestrator(handle);
2805 if (rc != ENTROPIC_OK || !messages_json || !result_json
2806 || !handle->engine || !turn.claim()) {
2807 return rc != ENTROPIC_OK ? rc
2808 : (!messages_json || !result_json || !handle->engine)
2811 }
2812 try {
2813 return run_messages_inner(handle, messages_json, result_json);
2814 } catch (const std::exception& e) {
2815 handle->last_error = e.what();
2816 s_log->error("run_messages: {}", handle->last_error);
2818 }
2819}
2820
2832 entropic_handle_t handle,
2833 const char* messages_json,
2834 void (*on_token)(const char* token, size_t len, void* user_data),
2835 void* user_data,
2836 int* cancel_flag) {
2838 auto msgs = parse_and_check_vision(handle, messages_json, vrc);
2839 if (vrc != ENTROPIC_OK) { return vrc; }
2840 int code = handle->engine->run_streaming(
2841 std::move(msgs), on_token, user_data, cancel_flag);
2842 if (handle->stream_observer != nullptr) {
2843 handle->stream_observer(
2844 "", 0, handle->stream_observer_data);
2845 }
2846 return code == 1 ? ENTROPIC_ERROR_CANCELLED : ENTROPIC_OK;
2847}
2848
2870 entropic_handle_t handle,
2871 const char* messages_json,
2872 void (*on_token)(const char* token, size_t len, void* user_data),
2873 void* user_data,
2874 int* cancel_flag) {
2875 // gh#109: NO api_mutex — a long turn must not block entropic_interrupt()
2876 // called from another thread. gh#144 (v2.12.0): the claim is a single
2877 // compare-exchange, so that property holds by construction. The guard is
2878 // declared BEFORE validation but claim() is evaluated LAST in the chain
2879 // below, so a call rejected for a bad argument never momentarily claims
2880 // the engine and bounces a legitimate concurrent caller.
2881 entropic::HandleTurnGuard turn(handle);
2882 auto rc = check_orchestrator(handle);
2883 if (rc != ENTROPIC_OK || !messages_json || !on_token
2884 || !handle->engine || !turn.claim()) {
2885 return rc != ENTROPIC_OK ? rc
2886 : (!messages_json || !on_token || !handle->engine)
2889 }
2890 try {
2892 handle, messages_json, on_token, user_data, cancel_flag);
2893 } catch (const std::exception& e) {
2894 handle->last_error = e.what();
2895 s_log->error("run_messages_streaming: {}", handle->last_error);
2897 }
2898}
2899
2911 entropic_handle_t handle,
2912 void (*observer)(const char* token, size_t len, void* user_data),
2913 void* user_data) {
2914 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
2915 handle->stream_observer = observer;
2916 handle->stream_observer_data = user_data;
2917 // Propagate to engine so every generation path (streaming, batch,
2918 // and child-loop delegations) reaches the observer. (P0-1, 2.0.6-rc16)
2919 if (handle->engine) {
2920 handle->engine->set_stream_observer(observer, user_data);
2921 }
2922 return ENTROPIC_OK;
2923}
2924
2925// ── gh#30 (v2.1.5): validation retry controls ─────────────
2926
2936 entropic_handle_t handle, int enabled) {
2937 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
2938 if (handle->validator) {
2939 handle->validator->set_auto_retry(enabled != 0);
2940 }
2941 return ENTROPIC_OK;
2942}
2943
2954 entropic_handle_t handle) {
2955 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
2956 if (!handle->validator) { return ENTROPIC_ERROR_INVALID_STATE; }
2957 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
2958 return handle->validator->resume_retry();
2959}
2960
2971 entropic_handle_t handle) {
2972 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
2973 if (!handle->validator) { return ENTROPIC_ERROR_INVALID_STATE; }
2974 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
2975 return handle->validator->accept_last();
2976}
2977
2988 entropic_handle_t handle,
2990 void* user_data) {
2991 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
2992 if (handle->validator) {
2993 handle->validator->set_attempt_boundary_cb(cb, user_data);
2994 }
2995 return ENTROPIC_OK;
2996}
2997
3011 entropic_handle_t handle,
3012 ent_delegation_start_cb on_start,
3013 ent_delegation_complete_cb on_complete,
3014 void* user_data) {
3015 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3016 if (handle->engine) {
3017 handle->engine->set_delegation_callbacks(
3018 on_start, on_complete, user_data);
3019 }
3020 return ENTROPIC_OK;
3021}
3022
3042 entropic_handle_t handle,
3043 void (*observer)(int state, void* user_data),
3044 void* user_data) {
3045 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3046 handle->state_observer = observer;
3047 handle->state_observer_data = user_data;
3048 if (handle->engine) {
3049 // gh#40 fallout (v2.1.10): route through the engine's
3050 // persistent state-observer slot rather than the legacy
3051 // EngineCallbacks::on_state_change. The legacy path is
3052 // wiped by run_streaming's set_callbacks() shuffle, so
3053 // wiring there silently failed for streaming runs (the
3054 // exact bridge use case this API was designed for).
3055 handle->engine->set_state_observer(observer, user_data);
3056 }
3057 return ENTROPIC_OK;
3058}
3059
3081 entropic_handle_t handle,
3082 void (*start_cb)(void* user_data),
3083 void (*end_cb)(void* user_data),
3084 void* user_data) {
3085 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3086 handle->critique_start_cb = start_cb;
3087 handle->critique_end_cb = end_cb;
3088 handle->critique_cb_data = user_data;
3089 if (handle->validator) {
3090 handle->validator->set_critique_callbacks(
3091 start_cb, end_cb, user_data);
3092 }
3093 return ENTROPIC_OK;
3094}
3095
3107 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3108 if (!handle->engine) { return ENTROPIC_ERROR_INVALID_STATE; }
3109 handle->engine->interrupt();
3110 return ENTROPIC_OK;
3111}
3112
3129 entropic_handle_t handle,
3130 const char* session_key) {
3131 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3132 if (!handle->engine) { return ENTROPIC_ERROR_INVALID_STATE; }
3133 const bool found = handle->engine->interrupt_session(
3134 session_key != nullptr ? session_key : "");
3135 return found ? ENTROPIC_OK : ENTROPIC_ERROR_NOT_RUNNING;
3136}
3137
3138// ── Mid-generation user-message queue (gh#40, v2.1.10) ────────
3139
3158 entropic_handle_t handle, const char* message) {
3160 if (!handle) {
3162 } else if (!message) {
3164 } else if (!handle->engine || !handle->engine->is_running()) {
3166 } else if (!handle->engine->queue_user_message(message)) {
3168 }
3169 return rc;
3170}
3171
3182 entropic_handle_t handle, size_t* count) {
3183 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3184 if (!count) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
3185 *count = handle->engine
3186 ? handle->engine->user_message_queue_depth() : 0;
3187 return ENTROPIC_OK;
3188}
3189
3199 entropic_handle_t handle) {
3200 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3201 if (handle->engine) {
3202 handle->engine->clear_user_message_queue();
3203 }
3204 return ENTROPIC_OK;
3205}
3206
3222 entropic_handle_t handle,
3223 void (*observer)(const char*, size_t, void*),
3224 void* user_data) {
3225 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3226 handle->queue_observer = observer;
3227 handle->queue_observer_data = user_data;
3228 if (handle->engine) {
3229 handle->engine->set_queue_observer(observer, user_data);
3230 }
3231 return ENTROPIC_OK;
3232}
3233
3234// ── Conversation Context (v2.0.1) ────────────────────────────
3235
3251 if (!handle || !handle->engine) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3252 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
3253 const auto key = handle->engine->active_session_key();
3254 handle->engine->clear_conversation();
3256 return ENTROPIC_OK;
3257}
3258
3270 entropic_handle_t handle, char** messages_json) {
3271 // gh#144 (v2.12.0): the !handle->engine leg is new — an unconfigured
3272 // handle has no engine, and this function dereferenced it. The siblings
3273 // entropic_context_clear and entropic_context_usage both guarded it;
3274 // this one and entropic_context_count did not. INVALID_HANDLE matches
3275 // those siblings' contract for an unconfigured handle.
3276 //
3277 // Ordering matters and is not the siblings': a NULL out-param on a
3278 // created-but-unconfigured handle must still report INVALID_ARGUMENT,
3279 // which v2.3.10 pinned. Folded into one exit for the returns <= 3 gate.
3280 if (!handle || !messages_json || !handle->engine) {
3281 return (handle != nullptr && messages_json == nullptr)
3284 }
3285 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
3286 *messages_json = alloc_cstr(
3287 facade_json::serialize_messages(handle->engine->get_messages()));
3288 return ENTROPIC_OK;
3289}
3290
3317 entropic_handle_t handle,
3318 const char* session_key,
3319 const char* input,
3320 char** result_json) {
3321 entropic::HandleTurnGuard turn(handle, session_key);
3322 auto rc = check_orchestrator(handle);
3323 if (rc != ENTROPIC_OK || !input || !result_json || !handle->engine
3324 || !turn.claim()) {
3325 return rc != ENTROPIC_OK ? rc
3326 : (!input || !result_json) ? ENTROPIC_ERROR_INVALID_ARGUMENT
3329 }
3330 handle->engine->set_active_session(session_key ? session_key : "");
3331 try {
3332 auto result = handle->engine->run_turn(input);
3333 *result_json = alloc_cstr(
3334 facade_json::serialize_messages(result));
3335 if (handle->stream_observer != nullptr) {
3336 handle->stream_observer("", 0, handle->stream_observer_data);
3337 }
3338 return ENTROPIC_OK;
3339 } catch (const std::exception& e) {
3340 handle->last_error = e.what();
3341 s_log->error("run_session: {}", handle->last_error);
3342 *result_json = nullptr;
3344 }
3345}
3346
3361 entropic_handle_t handle,
3362 const char* session_key,
3363 const char* tier_or_identity,
3364 const char* input,
3365 char** result_json) {
3366 entropic::HandleTurnGuard turn(handle, session_key);
3367 auto rc = check_orchestrator(handle);
3368 if (rc != ENTROPIC_OK || !tier_or_identity || !input || !result_json
3369 || !handle->engine || !turn.claim()) {
3370 return rc != ENTROPIC_OK ? rc
3371 : (!tier_or_identity || !input || !result_json)
3375 }
3376 if (!handle->engine->has_tier(tier_or_identity)) {
3377 handle->last_error =
3378 std::string("unknown tier: ") + tier_or_identity;
3380 }
3381 handle->engine->set_active_session(session_key ? session_key : "");
3382 return run_as_inner(handle, tier_or_identity, input, result_json);
3383}
3384
3396 entropic_handle_t handle, const char* tier, const char* input,
3397 void (*on_token)(const char*, size_t, void*)) {
3399 if (!input || !on_token || !handle->engine) {
3401 } else if (tier != nullptr && !handle->engine->has_tier(tier)) {
3402 handle->last_error = std::string("unknown tier: ") + tier;
3404 }
3405 return rc;
3406}
3407
3422 entropic_handle_t handle, const char* session_key, const std::string& tier,
3423 const char* input, void (*on_token)(const char*, size_t, void*),
3424 void* user_data, int* cancel_flag) {
3425 handle->engine->set_active_session(session_key ? session_key : "");
3426 try {
3427 int code = handle->engine->run_streaming(
3428 input, on_token, user_data, cancel_flag, tier);
3429 if (handle->stream_observer != nullptr) {
3430 handle->stream_observer("", 0, handle->stream_observer_data);
3431 }
3432 return code == 1 ? ENTROPIC_ERROR_CANCELLED : ENTROPIC_OK;
3433 } catch (const std::exception& e) {
3434 handle->last_error = e.what();
3435 s_log->error("run_session_streaming: {}", handle->last_error);
3437 }
3438}
3439
3454 entropic_handle_t handle, const char* session_key, const char* tier,
3455 const char* input, void (*on_token)(const char*, size_t, void*),
3456 void* user_data, int* cancel_flag) {
3457 entropic::HandleTurnGuard turn(handle, session_key);
3458 auto rc = check_orchestrator(handle);
3459 if (rc == ENTROPIC_OK) { rc = check_stream_args(handle, tier, input, on_token); }
3460 if (rc == ENTROPIC_OK && !turn.claim()) { rc = ENTROPIC_ERROR_ALREADY_RUNNING; }
3461 if (rc == ENTROPIC_OK) {
3462 rc = stream_session_turn(handle, session_key, tier ? tier : "",
3463 input, on_token, user_data, cancel_flag);
3464 }
3465 return rc;
3466}
3467
3481 entropic_handle_t handle,
3482 const char* session_key,
3483 const char* input,
3484 void (*on_token)(const char* token, size_t len, void* user_data),
3485 void* user_data,
3486 int* cancel_flag) {
3487 return run_session_streaming_impl(handle, session_key, nullptr, input,
3488 on_token, user_data, cancel_flag);
3489}
3490
3506 entropic_handle_t handle,
3507 const char* session_key,
3508 const char* tier_or_identity,
3509 const char* input,
3510 void (*on_token)(const char* token, size_t len, void* user_data),
3511 void* user_data,
3512 int* cancel_flag) {
3513 if (tier_or_identity == nullptr) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
3514 return run_session_streaming_impl(handle, session_key, tier_or_identity,
3515 input, on_token, user_data, cancel_flag);
3516}
3517
3528 entropic_handle_t handle,
3529 const char* session_key,
3530 char** messages_json) {
3531 if (!handle || !messages_json || !handle->engine) {
3532 return (handle != nullptr && messages_json == nullptr)
3535 }
3536 entropic::HandleApiLock lock(handle);
3537 *messages_json = alloc_cstr(facade_json::serialize_messages(
3538 handle->engine->messages_for(session_key ? session_key : "")));
3539 return ENTROPIC_OK;
3540}
3541
3556 const char* messages_json,
3557 std::vector<entropic::Message>& out) {
3558 try {
3559 out = entropic::parse_messages_json(messages_json);
3560 return true;
3561 } catch (const std::exception& e) {
3562 handle->last_error =
3563 std::string("session_context_set: malformed messages JSON: ")
3564 + e.what();
3565 s_log->error("{}", handle->last_error);
3566 return false;
3567 }
3568}
3569
3590 entropic_handle_t handle,
3591 const std::string& key,
3592 const char* messages_json) {
3593 std::vector<entropic::Message> msgs;
3594 if (!parse_restore_payload(handle, messages_json, msgs)) {
3596 }
3597 if (!handle->engine->set_session_messages(key, std::move(msgs))) {
3598 handle->last_error =
3599 "session_context_set: a run on session '" + key
3600 + "' is in flight";
3602 }
3603 // Whatever the old conversation left resident is now a prefix of a
3604 // history that no longer exists. Warm-keep would still be correct (it
3605 // matches TOKENS, and re-decodes the divergent tail), but the gate is
3606 // token equality rather than conversation identity — so the restore is
3607 // made correct by construction instead of by that argument.
3608 if (handle->orchestrator) {
3609 handle->orchestrator->forget_session_kv(key);
3610 }
3611 // gh#158: the reads and todos that described the replaced history
3612 // describe nothing now.
3614 return ENTROPIC_OK;
3615}
3616
3631 entropic_handle_t handle,
3632 const char* session_key,
3633 const char* messages_json) {
3634 if (!handle || !handle->engine || !messages_json) {
3635 return (handle != nullptr && handle->engine != nullptr)
3638 }
3639 entropic::HandleApiLock lock(handle);
3641 handle, session_key != nullptr ? session_key : "", messages_json);
3642}
3643
3654 entropic_handle_t handle,
3655 const char* session_key,
3656 size_t* count) {
3657 if (!handle || !count || !handle->engine) {
3658 return (handle != nullptr && count == nullptr)
3661 }
3662 entropic::HandleApiLock lock(handle);
3663 *count = handle->engine->message_count_for(
3664 session_key ? session_key : "");
3665 return ENTROPIC_OK;
3666}
3667
3686 entropic_handle_t handle,
3687 const char* session_key) {
3688 if (!handle || !handle->engine) {
3690 }
3691 entropic::HandleApiLock lock(handle);
3692 const std::string key = session_key ? session_key : "";
3693 handle->engine->clear_conversation_for(key);
3695 return ENTROPIC_OK;
3696}
3697
3713 entropic_handle_t handle,
3714 const char* session_key) {
3715 if (!handle || !handle->engine) {
3717 }
3718 entropic::HandleApiLock lock(handle);
3719 const std::string key = session_key ? session_key : "";
3720 handle->engine->drop_session(key);
3722 return ENTROPIC_OK;
3723}
3724
3739 entropic_handle_t handle,
3740 char** sessions_json) {
3741 if (!handle || !sessions_json || !handle->engine) {
3742 return (handle != nullptr && sessions_json == nullptr)
3745 }
3746 entropic::HandleApiLock lock(handle);
3747 nlohmann::json arr = nlohmann::json::array();
3748 for (const auto& key : handle->engine->session_keys()) {
3749 arr.push_back({{"key", key},
3750 {"messages", handle->engine->message_count_for(key)}});
3751 }
3752 *sessions_json = alloc_cstr(arr.dump());
3753 return ENTROPIC_OK;
3754}
3755
3766 entropic_handle_t handle, size_t* count) {
3767 // gh#144 (v2.12.0): see entropic_context_get — same missing engine
3768 // guard, same ordering constraint, same single-exit fold.
3769 if (!handle || !count || !handle->engine) {
3770 return (handle != nullptr && count == nullptr)
3773 }
3774 *count = handle->engine->message_count();
3775 return ENTROPIC_OK;
3776}
3777
3794 entropic_handle_t handle,
3795 size_t* tokens_used,
3796 size_t* capacity) {
3797 if (!handle || !handle->engine) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3798 if (!tokens_used || !capacity) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
3799 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
3800 auto [used, max] = handle->engine->context_usage(
3801 handle->engine->get_messages());
3802 *tokens_used = static_cast<size_t>(used);
3803 *capacity = static_cast<size_t>(max);
3804 return max > 0 ? ENTROPIC_OK : ENTROPIC_ERROR_INVALID_STATE;
3805}
3806
3813 entropic_handle_t handle, const char* tier_name, const char* path) {
3814 if (!handle->orchestrator) { return ENTROPIC_ERROR_INVALID_STATE; }
3815 auto* backend = require_ready_backend(handle, tier_name);
3816 std::vector<uint8_t> buf;
3817 if (!backend->save_state(0, buf)) { return ENTROPIC_ERROR_INTERNAL; }
3818 std::ofstream out(path, std::ios::binary | std::ios::trunc);
3819 bool ok = out.is_open()
3820 && out.write(reinterpret_cast<const char*>(buf.data()),
3821 static_cast<std::streamsize>(buf.size())).good();
3822 return ok ? ENTROPIC_OK : ENTROPIC_ERROR_IO;
3823}
3824
3834 entropic_handle_t handle,
3835 const char* tier_name,
3836 const char* path) {
3837 // gh#157: this may now LOAD the tier's model. Claim the turn so the
3838 // load cannot land in the middle of a generation on another thread.
3839 entropic::HandleTurnGuard turn(handle);
3840 if (!handle || !tier_name || !path
3841 || (handle->engine != nullptr && !turn.claim())) {
3842 return !handle ? ENTROPIC_ERROR_INVALID_HANDLE
3843 : (!tier_name || !path) ? ENTROPIC_ERROR_INVALID_ARGUMENT
3845 }
3846 entropic::HandleApiLock lock(handle);
3847 return c_api_try(handle,
3848 [&]() { return do_state_save(handle, tier_name, path); });
3849}
3850
3862static bool read_state_file(const char* path, std::vector<uint8_t>& out_buf) {
3863 std::ifstream in(path, std::ios::binary | std::ios::ate);
3864 if (!in.is_open()) { return false; }
3865 auto sz = static_cast<std::streamsize>(in.tellg());
3866 if (sz <= 0) { return false; }
3867 in.seekg(0, std::ios::beg);
3868 out_buf.resize(static_cast<size_t>(sz));
3869 return static_cast<bool>(
3870 in.read(reinterpret_cast<char*>(out_buf.data()), sz));
3871}
3872
3879 entropic_handle_t handle, const char* tier_name, const char* path) {
3880 if (!handle->orchestrator) { return ENTROPIC_ERROR_INVALID_STATE; }
3881 auto* backend = require_ready_backend(handle, tier_name);
3882 std::vector<uint8_t> buf;
3883 if (!read_state_file(path, buf)) { return ENTROPIC_ERROR_IO; }
3884 return backend->restore_state(0, buf)
3886}
3887
3897 entropic_handle_t handle,
3898 const char* tier_name,
3899 const char* path) {
3900 // gh#157: may load the tier's model — see entropic_state_save.
3901 entropic::HandleTurnGuard turn(handle);
3902 if (!handle || !tier_name || !path
3903 || (handle->engine != nullptr && !turn.claim())) {
3904 return !handle ? ENTROPIC_ERROR_INVALID_HANDLE
3905 : (!tier_name || !path) ? ENTROPIC_ERROR_INVALID_ARGUMENT
3907 }
3908 entropic::HandleApiLock lock(handle);
3909 return c_api_try(handle,
3910 [&]() { return do_state_load(handle, tier_name, path); });
3911}
3912
3928 entropic_handle_t handle, char** out) {
3929 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
3930 if (!out) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
3931 *out = sp_get_metrics(handle);
3932 return ENTROPIC_OK;
3933}
3934
3935// ── LoRA Adapter APIs (v1.9.2 → v2.0.0) ────────────────────
3936
3952 entropic_handle_t handle, const char* adapter_name,
3953 const char* adapter_path, const char* base_model_path, float scale)
3954{
3955 auto tier = handle->config.models.find_tier_by_path(base_model_path);
3956 if (tier.empty()) {
3957 throw std::runtime_error("no tier for model: "
3958 + std::string(base_model_path));
3959 }
3960 // gh#157: load the base model if it is not resident, rather than
3961 // returning INTERNAL "backend not ready" — which is what a deferred
3962 // (or released, gh#164) tier looks like.
3963 auto* base = handle->orchestrator->ensure_model(tier);
3964 auto* llama = dynamic_cast<entropic::LlamaCppBackend*>(base);
3965 if (!llama || !llama->llama_model_ptr()) {
3966 throw std::runtime_error("backend not ready for tier: " + tier);
3967 }
3968 bool ok = handle->orchestrator->adapter_manager().load(
3969 adapter_name, adapter_path, llama->llama_model_ptr(), scale);
3971}
3972
3996 entropic_handle_t handle,
3997 const char* adapter_name,
3998 const char* adapter_path,
3999 const char* base_model_path,
4000 float scale)
4001{
4002 auto rc = check_orchestrator(handle);
4003 // gh#157: may load the base model — claim the turn first.
4004 entropic::HandleTurnGuard turn(handle);
4005 if (rc != ENTROPIC_OK || !adapter_name || !adapter_path
4006 || !base_model_path
4007 || (handle->engine != nullptr && !turn.claim())) {
4008 return rc != ENTROPIC_OK ? rc
4009 : (!adapter_name || !adapter_path || !base_model_path)
4012 }
4013 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4014 return c_api_try(handle, [&]() {
4015 return do_adapter_load(handle, adapter_name, adapter_path,
4016 base_model_path, scale);
4017 });
4018}
4019
4034 entropic_handle_t handle,
4035 const char* adapter_name)
4036{
4037 auto rc = check_orchestrator(handle);
4038 if (rc != ENTROPIC_OK || !adapter_name) {
4039 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4040 }
4041 try {
4042 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4043 auto& mgr = handle->orchestrator->adapter_manager();
4044 auto info = mgr.info(adapter_name);
4045 if (info.state == entropic::AdapterState::COLD) {
4046 throw std::runtime_error("adapter not loaded: "
4047 + std::string(adapter_name));
4048 }
4049 auto tier = handle->orchestrator->last_used_tier();
4050 auto* base = handle->orchestrator->get_backend(tier);
4051 auto* llama = dynamic_cast<entropic::LlamaCppBackend*>(base);
4052 mgr.unload(adapter_name, llama ? llama->llama_context_ptr() : nullptr);
4053 return ENTROPIC_OK;
4054 } catch (const std::exception& e) {
4055 handle->last_error = e.what();
4057 }
4058}
4059
4074 entropic_handle_t handle,
4075 const char* adapter_name)
4076{
4077 auto rc = check_orchestrator(handle);
4078 if (rc != ENTROPIC_OK || !adapter_name) {
4079 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4080 }
4081 try {
4082 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4083 auto tier = handle->orchestrator->last_used_tier();
4084 auto* base = handle->orchestrator->get_backend(tier);
4085 auto* llama = dynamic_cast<entropic::LlamaCppBackend*>(base);
4086 if (!llama || !llama->llama_context_ptr()) {
4087 throw std::runtime_error("no active llama context for swap");
4088 }
4089 bool ok = handle->orchestrator->adapter_manager().swap(
4090 adapter_name, llama->llama_context_ptr());
4092 } catch (const std::exception& e) {
4093 handle->last_error = e.what();
4095 }
4096}
4097
4111 entropic_handle_t handle,
4112 const char* adapter_name)
4113{
4114 if (!handle || !handle->configured.load()
4115 || !handle->orchestrator || !adapter_name) {
4116 return -1;
4117 }
4118 try {
4119 auto st = handle->orchestrator->adapter_manager().state(adapter_name);
4120 return static_cast<int>(st);
4121 } catch (const std::exception& e) {
4122 handle->last_error = e.what();
4123 s_log->error("adapter_state: {}", handle->last_error);
4124 return -1;
4125 }
4126}
4127
4142 entropic_handle_t handle,
4143 const char* adapter_name)
4144{
4145 if (!handle || !handle->configured.load()
4146 || !handle->orchestrator || !adapter_name) {
4147 return nullptr;
4148 }
4149 try {
4150 auto ai = handle->orchestrator->adapter_manager().info(adapter_name);
4151 return alloc_cstr(
4152 facade_json::serialize_adapter_info(ai).c_str());
4153 } catch (const std::exception& e) {
4154 handle->last_error = e.what();
4155 s_log->error("adapter_info: {}", handle->last_error);
4156 return nullptr;
4157 }
4158}
4159
4174{
4175 if (!handle || !handle->configured.load() || !handle->orchestrator) {
4176 return nullptr;
4177 }
4178 try {
4179 auto adapters = handle->orchestrator->adapter_manager().list_adapters();
4180 return alloc_cstr(
4181 facade_json::serialize_adapter_list(adapters).c_str());
4182 } catch (const std::exception& e) {
4183 handle->last_error = e.what();
4184 s_log->error("adapter_list: {}", handle->last_error);
4185 return nullptr;
4186 }
4187}
4188
4189// ── Grammar Registry APIs (v1.9.3 → v2.0.0) ────────────────
4190
4206 entropic_handle_t handle,
4207 const char* key,
4208 const char* gbnf_content)
4209{
4210 auto rc = check_orchestrator(handle);
4211 if (rc != ENTROPIC_OK || !key || !gbnf_content) {
4212 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4213 }
4214 try {
4215 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4216 bool ok = handle->orchestrator->grammar_registry()
4217 .register_grammar(key, gbnf_content);
4218 s_log->info("grammar_register: key={} ok={}", key, ok);
4220 } catch (const std::exception& e) {
4221 handle->last_error = e.what();
4222 s_log->error("grammar_register: {}", handle->last_error);
4224 }
4225}
4226
4241 entropic_handle_t handle,
4242 const char* key,
4243 const char* path)
4244{
4245 auto rc = check_orchestrator(handle);
4246 if (rc != ENTROPIC_OK || !key || !path) {
4247 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4248 }
4249 try {
4250 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4251 bool ok = handle->orchestrator->grammar_registry()
4252 .register_from_file(key, path);
4253 s_log->info("grammar_register_file: key={} ok={}", key, ok);
4254 return ok ? ENTROPIC_OK : ENTROPIC_ERROR_IO;
4255 } catch (const std::exception& e) {
4256 handle->last_error = e.what();
4257 s_log->error("grammar_register_file: {}", handle->last_error);
4259 }
4260}
4261
4276 entropic_handle_t handle,
4277 const char* key)
4278{
4279 auto rc = check_orchestrator(handle);
4280 if (rc != ENTROPIC_OK || !key) {
4281 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4282 }
4283 try {
4284 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4285 bool ok = handle->orchestrator->grammar_registry().deregister(key);
4286 s_log->info("grammar_deregister: key={} ok={}", key, ok);
4288 } catch (const std::exception& e) {
4289 handle->last_error = e.what();
4290 s_log->error("grammar_deregister: {}", handle->last_error);
4292 }
4293}
4294
4309 entropic_handle_t handle,
4310 const char* key)
4311{
4312 if (!handle || !handle->configured.load()
4313 || !handle->orchestrator || !key) {
4314 return nullptr;
4315 }
4316 try {
4317 auto content = handle->orchestrator->grammar_registry().get(key);
4318 return content.empty() ? nullptr : alloc_cstr(content.c_str());
4319 } catch (const std::exception& e) {
4320 handle->last_error = e.what();
4321 s_log->error("grammar_get: {}", handle->last_error);
4322 return nullptr;
4323 }
4324}
4325
4340char* entropic_grammar_validate(const char* gbnf_content) {
4341 if (!gbnf_content) { return alloc_cstr("null input"); }
4342 try {
4343 auto err = entropic::GrammarRegistry::validate(gbnf_content);
4344 return err.empty() ? nullptr : alloc_cstr(err.c_str());
4345 } catch (const std::exception& e) {
4346 return alloc_cstr(e.what());
4347 }
4348}
4349
4365{
4366 if (!handle || !handle->configured.load() || !handle->orchestrator) {
4367 return nullptr;
4368 }
4369 try {
4370 auto entries = handle->orchestrator->grammar_registry().list();
4371 nlohmann::json arr = nlohmann::json::array();
4372 for (const auto& e : entries) {
4373 arr.push_back({{"key", e.key},
4374 {"source", e.source},
4375 {"validated", e.validated},
4376 {"error", e.error}});
4377 }
4378 return alloc_cstr(arr.dump().c_str());
4379 } catch (const std::exception& e) {
4380 handle->last_error = e.what();
4381 s_log->error("grammar_list: {}", handle->last_error);
4382 return nullptr;
4383 }
4384}
4385
4386// ── GPU Resource Profile APIs (v1.9.7 → v2.0.0) ─────────────
4387
4403 entropic_handle_t handle,
4404 const char* profile_json)
4405{
4406 auto rc = check_orchestrator(handle);
4407 if (rc != ENTROPIC_OK || !profile_json) {
4408 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4409 }
4410 try {
4411 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4412 auto j = nlohmann::json::parse(profile_json);
4414 p.name = j.value("name", "");
4415 if (p.name.empty()) { throw std::invalid_argument("missing 'name'"); }
4416 p.n_batch = j.value("n_batch", 512);
4417 p.n_threads = j.value("n_threads", 0);
4418 p.n_threads_batch = j.value("n_threads_batch", 0);
4419 p.description = j.value("description", "");
4420 bool ok = handle->orchestrator->profile_registry()
4421 .register_profile(p);
4422 s_log->info("profile_register: name={} ok={}", p.name, ok);
4424 } catch (const std::exception& e) {
4425 handle->last_error = e.what();
4426 s_log->error("profile_register: {}", handle->last_error);
4428 }
4429}
4430
4445 entropic_handle_t handle,
4446 const char* name)
4447{
4448 auto rc = check_orchestrator(handle);
4449 if (rc != ENTROPIC_OK || !name) {
4450 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4451 }
4452 try {
4453 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4454 bool ok = handle->orchestrator->profile_registry().deregister(name);
4455 s_log->info("profile_deregister: name={} ok={}", name, ok);
4457 } catch (const std::exception& e) {
4458 handle->last_error = e.what();
4459 s_log->error("profile_deregister: {}", handle->last_error);
4461 }
4462}
4463
4480 entropic_handle_t handle,
4481 const char* name)
4482{
4483 if (!handle || !handle->configured.load()
4484 || !handle->orchestrator || !name) {
4485 return nullptr;
4486 }
4487 try {
4488 auto p = handle->orchestrator->profile_registry().get(name);
4489 nlohmann::json j;
4490 j["name"] = p.name;
4491 j["n_batch"] = p.n_batch;
4492 j["n_threads"] = p.n_threads;
4493 j["n_threads_batch"] = p.n_threads_batch;
4494 j["description"] = p.description;
4495 return alloc_cstr(j.dump().c_str());
4496 } catch (const std::exception& e) {
4497 handle->last_error = e.what();
4498 s_log->error("profile_get: {}", handle->last_error);
4499 return nullptr;
4500 }
4501}
4502
4517{
4518 if (!handle || !handle->configured.load() || !handle->orchestrator) {
4519 return nullptr;
4520 }
4521 try {
4522 auto names = handle->orchestrator->profile_registry().list();
4523 nlohmann::json arr = nlohmann::json(names);
4524 return alloc_cstr(arr.dump().c_str());
4525 } catch (const std::exception& e) {
4526 handle->last_error = e.what();
4527 s_log->error("profile_list: {}", handle->last_error);
4528 return nullptr;
4529 }
4530}
4531
4532// ── Throughput Query APIs (v1.9.7 → v2.0.0) ─────────────────
4533
4549 entropic_handle_t handle,
4550 const char* model_path)
4551{
4552 (void)model_path;
4553 if (!handle || !handle->configured.load() || !handle->orchestrator) {
4554 return 0.0;
4555 }
4556 try {
4557 return handle->orchestrator->throughput_tracker().tok_per_sec();
4558 } catch (const std::exception& e) {
4559 handle->last_error = e.what();
4560 s_log->error("throughput_tok_per_sec: {}", handle->last_error);
4561 return 0.0;
4562 }
4563}
4564
4579 entropic_handle_t handle,
4580 const char* model_path)
4581{
4582 (void)model_path;
4583 if (!handle || !handle->configured.load() || !handle->orchestrator) {
4584 return;
4585 }
4586 try {
4587 handle->orchestrator->throughput_tracker().reset();
4588 s_log->info("throughput_reset: data cleared");
4589 } catch (const std::exception& e) {
4590 handle->last_error = e.what();
4591 s_log->error("throughput_reset: {}", handle->last_error);
4592 }
4593}
4594
4595// ── MCP Authorization APIs (v1.9.4 → v2.0.0) ────────────────
4596
4606 entropic_handle_t handle,
4607 const char* identity_name,
4608 const char* pattern,
4610{
4611 auto rc = check_mcp_auth(handle);
4612 if (rc != ENTROPIC_OK || !identity_name || !pattern) {
4613 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4614 }
4615 auto lvl = static_cast<entropic::MCPAccessLevel>(level);
4616 return handle->mcp_auth->grant(identity_name, pattern, lvl);
4617}
4618
4628 entropic_handle_t handle,
4629 const char* identity_name,
4630 const char* pattern)
4631{
4632 auto rc = check_mcp_auth(handle);
4633 if (rc != ENTROPIC_OK || !identity_name || !pattern) {
4634 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4635 }
4636 return handle->mcp_auth->revoke(identity_name, pattern);
4637}
4638
4648 entropic_handle_t handle,
4649 const char* identity_name,
4650 const char* tool_name,
4652{
4653 if (!handle || !handle->configured.load()
4654 || !handle->mcp_auth || !identity_name || !tool_name) {
4655 return -1;
4656 }
4657 auto lvl = static_cast<entropic::MCPAccessLevel>(level);
4658 return handle->mcp_auth->check_access(identity_name, tool_name, lvl) ? 1 : 0;
4659}
4660
4672 entropic_handle_t handle,
4673 const char* identity_name)
4674{
4675 if (!handle || !handle->configured.load()
4676 || !handle->mcp_auth || !identity_name) {
4677 return nullptr;
4678 }
4679 try {
4680 auto keys = handle->mcp_auth->list_keys(identity_name);
4681 nlohmann::json arr = nlohmann::json::array();
4682 for (const auto& k : keys) {
4683 arr.push_back({{"pattern", k.tool_pattern},
4684 {"level", static_cast<int>(k.level)}});
4685 }
4686 return alloc_cstr(arr.dump().c_str());
4687 } catch (const std::exception& e) {
4688 handle->last_error = e.what();
4689 return nullptr;
4690 }
4691}
4692
4702 entropic_handle_t handle,
4703 const char* granter,
4704 const char* grantee,
4705 const char* pattern,
4707{
4708 auto rc = check_mcp_auth(handle);
4709 if (rc != ENTROPIC_OK || !granter || !grantee || !pattern) {
4710 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4711 }
4712 auto lvl = static_cast<entropic::MCPAccessLevel>(level);
4713 return handle->mcp_auth->grant_from(granter, grantee, pattern, lvl);
4714}
4715
4727{
4728 if (!handle || !handle->configured.load() || !handle->mcp_auth) {
4729 return nullptr;
4730 }
4731 try {
4732 auto json = handle->mcp_auth->serialize_all();
4733 return alloc_cstr(json.c_str());
4734 } catch (const std::exception& e) {
4735 handle->last_error = e.what();
4736 return nullptr;
4737 }
4738}
4739
4749 entropic_handle_t handle,
4750 const char* json)
4751{
4752 auto rc = check_mcp_auth(handle);
4753 if (rc != ENTROPIC_OK || !json) {
4754 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4755 }
4756 bool ok = handle->mcp_auth->deserialize_all(json);
4758}
4759
4760// ── Dynamic Identity Management APIs (v1.9.6 → v2.0.0) ──────
4761
4772 entropic_handle_t handle,
4773 const char* config_json)
4774{
4775 auto rc = check_identity(handle);
4776 if (rc != ENTROPIC_OK || !config_json) {
4777 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4778 }
4779 try {
4780 auto j = nlohmann::json::parse(config_json);
4782 cfg.name = j.value("name", "");
4783 cfg.system_prompt = j.value("system_prompt", "");
4784 if (j.contains("focus") && j["focus"].is_array()) {
4785 cfg.focus = j["focus"].get<std::vector<std::string>>();
4786 }
4788 return handle->identity_manager->create(cfg);
4789 } catch (const std::exception& e) {
4790 handle->last_error = e.what();
4792 }
4793}
4794
4805 entropic_handle_t handle,
4806 const char* name,
4807 const char* config_json)
4808{
4809 auto rc = check_identity(handle);
4810 if (rc != ENTROPIC_OK || !name || !config_json) {
4811 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4812 }
4813 try {
4814 auto j = nlohmann::json::parse(config_json);
4816 cfg.name = name;
4817 cfg.system_prompt = j.value("system_prompt", "");
4818 if (j.contains("focus") && j["focus"].is_array()) {
4819 cfg.focus = j["focus"].get<std::vector<std::string>>();
4820 }
4822 return handle->identity_manager->update(name, cfg);
4823 } catch (const std::exception& e) {
4824 handle->last_error = e.what();
4826 }
4827}
4828
4838 entropic_handle_t handle,
4839 const char* name)
4840{
4841 auto rc = check_identity(handle);
4842 if (rc != ENTROPIC_OK || !name) {
4843 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4844 }
4845 return handle->identity_manager->destroy(name);
4846}
4847
4859 entropic_handle_t handle,
4860 const char* name)
4861{
4862 if (!handle || !handle->identity_manager || !name) { return nullptr; }
4863 try {
4864 auto* cfg = handle->identity_manager->get(name);
4865 if (!cfg) { throw std::runtime_error("identity not found"); }
4866 nlohmann::json j;
4867 j["name"] = cfg->name;
4868 j["system_prompt"] = cfg->system_prompt;
4869 j["origin"] = (cfg->origin == entropic::IdentityOrigin::STATIC)
4870 ? "static" : "dynamic";
4871 return alloc_cstr(j.dump().c_str());
4872 } catch (...) {
4873 return nullptr;
4874 }
4875}
4876
4888{
4889 if (!handle || !handle->identity_manager) { return nullptr; }
4890 try {
4891 auto names = handle->identity_manager->list();
4892 nlohmann::json arr(names);
4893 return alloc_cstr(arr.dump().c_str());
4894 } catch (...) {
4895 return nullptr;
4896 }
4897}
4898
4908 entropic_handle_t handle,
4909 size_t* total,
4910 size_t* dynamic)
4911{
4912 auto rc = check_identity(handle);
4913 if (rc != ENTROPIC_OK || !total) {
4914 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_INVALID_ARGUMENT;
4915 }
4916 *total = handle->identity_manager->count();
4917 if (dynamic) { *dynamic = handle->identity_manager->count_dynamic(); }
4918 return ENTROPIC_OK;
4919}
4920
4921// ── Log-Probability Evaluation APIs (v1.9.10 → v2.0.0) ──────
4922
4936 entropic_logprob_result_t* result) {
4937 result->n_tokens = lr.n_tokens;
4938 result->n_logprobs = lr.n_logprobs;
4939 result->perplexity = lr.perplexity;
4940 result->total_logprob = lr.total_logprob;
4941 result->logprobs = static_cast<float*>(
4942 malloc(sizeof(float) * lr.logprobs.size()));
4943 std::copy(lr.logprobs.begin(), lr.logprobs.end(), result->logprobs);
4944 result->tokens = static_cast<int32_t*>(
4945 malloc(sizeof(int32_t) * lr.tokens.size()));
4946 std::copy(lr.tokens.begin(), lr.tokens.end(), result->tokens);
4947}
4948
4964 entropic_handle_t handle,
4965 const char* model_id,
4966 const int32_t* tokens,
4967 int n_tokens,
4969{
4970 auto rc = check_orchestrator(handle);
4971 // gh#157: may load the tier's model — claim the turn first.
4972 entropic::HandleTurnGuard turn(handle);
4973 if (rc != ENTROPIC_OK || !model_id || !tokens || !result || n_tokens < 2
4974 || (handle->engine != nullptr && !turn.claim())) {
4975 return rc != ENTROPIC_OK ? rc
4976 : (!model_id || !tokens || !result || n_tokens < 2)
4979 }
4980 try {
4981 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
4982 auto* backend = require_ready_backend(handle, model_id);
4983 fill_logprob_result(backend->evaluate_logprobs(tokens, n_tokens),
4984 result);
4985 return ENTROPIC_OK;
4986 } catch (const std::exception& e) {
4987 handle->last_error = e.what();
4988 s_log->error("get_logprobs: {}", handle->last_error);
4990 }
4991}
4992
5006 entropic_handle_t handle,
5007 const char* model_id,
5008 const int32_t* tokens,
5009 int n_tokens,
5010 float* perplexity)
5011{
5012 auto rc = check_orchestrator(handle);
5013 // gh#157: may load the tier's model — claim the turn first.
5014 entropic::HandleTurnGuard turn(handle);
5015 if (rc != ENTROPIC_OK || !model_id || !tokens || !perplexity
5016 || n_tokens < 2 || (handle->engine != nullptr && !turn.claim())) {
5017 return rc != ENTROPIC_OK ? rc
5018 : (!model_id || !tokens || !perplexity || n_tokens < 2)
5021 }
5022 try {
5023 entropic::HandleApiLock lock(handle); // gh#59 v2.3.1: mutex + log scope
5024 auto* backend = require_ready_backend(handle, model_id);
5025 *perplexity = backend->compute_perplexity(tokens, n_tokens);
5026 return ENTROPIC_OK;
5027 } catch (const std::exception& e) {
5028 handle->last_error = e.what();
5029 s_log->error("compute_perplexity: {}", handle->last_error);
5031 }
5032}
5033
5045{
5046 if (result == nullptr) {
5047 return;
5048 }
5049 free(result->logprobs);
5050 result->logprobs = nullptr;
5051 free(result->tokens);
5052 result->tokens = nullptr;
5053}
5054
5055// ── Vision Query API (v1.9.11 → v2.0.0) ─────────────────────
5056
5071 entropic_handle_t handle,
5072 const char* model_id)
5073{
5074 if (!handle || !handle->configured.load()
5075 || !handle->orchestrator || !model_id) {
5076 return 0;
5077 }
5078 try {
5079 // gh#157: answer from CONFIG, not from the backend. The backend's
5080 // has_vision_ is set while the mmproj context is built during
5081 // ACTIVATION, so an unloaded tier answered 0 — wrong, not unknown —
5082 // and with models.defer_load unloaded is the normal state.
5083 return handle->orchestrator->tier_declares_vision(model_id) ? 1 : 0;
5084 } catch (const std::exception& e) {
5085 handle->last_error = e.what();
5086 s_log->error("model_has_vision: {}", handle->last_error);
5087 return 0;
5088 }
5089}
5090
5091// ── Constitutional Validation APIs (v1.9.8 → v2.0.0) ────────
5092
5102 entropic_handle_t handle,
5103 bool enabled)
5104{
5105 if (!handle) { return ENTROPIC_ERROR_INVALID_HANDLE; }
5106 if (!handle->validator) { return ENTROPIC_ERROR_INVALID_STATE; }
5107 handle->validator->set_global_enabled(enabled);
5108 return ENTROPIC_OK;
5109}
5110
5120 entropic_handle_t handle,
5121 const char* identity_name,
5122 bool enabled)
5123{
5124 if (!handle || !handle->validator) {
5125 return !handle ? ENTROPIC_ERROR_INVALID_HANDLE
5127 }
5128 if (!identity_name) { return ENTROPIC_ERROR_INVALID_ARGUMENT; }
5129 handle->validator->set_identity_validation(identity_name, enabled);
5130 return ENTROPIC_OK;
5131}
5132
5144{
5145 if (!handle || !handle->validator) { return nullptr; }
5146 try {
5147 auto result = handle->validator->last_result();
5148 nlohmann::json j;
5149 j["content"] = entropic::mcp::sanitize_utf8(result.content);
5150 j["was_revised"] = result.was_revised;
5151 j["revision_count"] = result.revision_count;
5152 return alloc_cstr(j.dump().c_str());
5153 } catch (...) {
5154 return nullptr;
5155 }
5156}
5157
5169 entropic_handle_t handle,
5170 char** prompt_out) {
5171 if (handle == nullptr || prompt_out == nullptr) {
5173 }
5174 (void)handle;
5175 static const char* prompt =
5176 "[SYSTEM DIRECTIVE: SELF-DIAGNOSIS]\n\n"
5177 "Analyze your recent actions and identify any issues. "
5178 "Follow these steps:\n\n"
5179 "1. Call entropic.diagnose to get a full engine state "
5180 "snapshot.\n"
5181 "2. Review the tool call history for:\n"
5182 " - Repeated failures (same tool, same error)\n"
5183 " - Duplicate tool calls (circuit breaker risk)\n"
5184 " - Tool calls that returned errors\n"
5185 " - Unexpected state (wrong phase, wrong tier)\n"
5186 "3. Review your reasoning for:\n"
5187 " - Actions that didn't achieve the stated goal\n"
5188 " - Unnecessary tool calls\n"
5189 " - Missing context that led to errors\n"
5190 "4. Produce a structured assessment:\n"
5191 " - FINDINGS: What went wrong (be specific)\n"
5192 " - ROOT CAUSE: Why it went wrong\n"
5193 " - RECOMMENDATION: What to do differently\n\n"
5194 "Be honest and specific. The goal is accurate "
5195 "self-assessment, not self-defense.\n";
5196 *prompt_out = alloc_cstr(prompt);
5197 return ENTROPIC_OK;
5198}
5199
5223 entropic_handle_t handle,
5224 int* compatible,
5225 char** diagnostic) {
5226 if (handle == nullptr || compatible == nullptr) {
5228 }
5229 if (!handle->orchestrator) {
5231 }
5232 auto info = handle->orchestrator->check_speculative_compat();
5233 *compatible = info.compatible ? 1 : 0;
5234 if (diagnostic != nullptr) {
5235 *diagnostic = info.compatible
5236 ? nullptr
5237 : alloc_cstr(info.diagnostic);
5238 }
5239 return ENTROPIC_OK;
5240}
5241
5242/* ── VRAM-aware tier residency (v2.2.4, gh#57) ─────────── */
5243
5261 entropic_handle_t handle,
5263 void* user_data) {
5264 if (handle == nullptr) { return ENTROPIC_ERROR_INVALID_HANDLE; }
5265 // Engine-not-configured (no orchestrator yet) and observer==nullptr
5266 // both collapse to a no-op set: pre-configure registration is
5267 // ignored (consumers must re-register after configure_*), and
5268 // an explicit nullptr clears any prior slot. Done as one branch
5269 // to stay under the knots return-count gate.
5271 if (observer != nullptr) {
5272 fn = [observer, user_data](
5274 const std::string& tier_name,
5275 const std::string& model_path,
5276 size_t footprint) {
5277 observer(static_cast<entropic_residency_event_t>(event),
5278 tier_name.c_str(),
5279 model_path.c_str(),
5280 footprint,
5281 user_data);
5282 };
5283 }
5284 if (handle->orchestrator) {
5285 handle->orchestrator->set_residency_observer(std::move(fn));
5286 }
5287 return ENTROPIC_OK;
5288}
5289
5307 entropic_handle_t handle,
5308 const char* tier_name) {
5309 auto rc = check_orchestrator(handle);
5310 entropic::HandleTurnGuard turn(handle);
5311 if (rc != ENTROPIC_OK
5312 || (handle->engine != nullptr && !turn.claim())) {
5313 return rc != ENTROPIC_OK ? rc : ENTROPIC_ERROR_ALREADY_RUNNING;
5314 }
5315 entropic::HandleApiLock lock(handle);
5316 return c_api_try(handle, [&]() {
5317 return handle->orchestrator->release_models(
5318 tier_name != nullptr ? tier_name : "");
5319 });
5320}
5321
5339 entropic_handle_t handle,
5340 char** out_json) {
5342 if (handle == nullptr || out_json == nullptr) {
5344 } else if (!handle->orchestrator) {
5346 } else {
5347 std::string snapshot =
5348 handle->orchestrator->residency_snapshot_json();
5349 *out_json = alloc_cstr(snapshot);
5350 if (*out_json == nullptr) {
5352 }
5353 }
5354 return rc;
5355}
5356
5357} // extern "C"
gh#32 (v2.1.6) resume
static std::string validate(const std::string &gbnf_content)
Validate a GBNF grammar string.
gh#59 (v2.3.1): RAII guard combining api_mutex + log scope.
gh#144 (v2.12.0): RAII turn claim + log scope, for the RUN paths.
bool claim()
Claim the turn.
Thread-safe hook registration and dispatch.
int fire_pre(entropic_hook_point_t point, const char *context_json, char **out_json)
Fire pre-hooks.
void fire_post(entropic_hook_point_t point, const char *context_json, char **out_json)
Fire post-hooks.
void fire_info(entropic_hook_point_t point, const char *context_json)
Fire informational hooks (no modify, no cancel).
Concrete base class for inference backends (80% logic).
Definition backend.h:69
void unload()
Full unload (→ COLD).
Definition backend.cpp:159
LlamaCppBackend — common llama.cpp patterns (15% layer).
Multi-model lifecycle and routing orchestrator.
std::function< void(ResidencyEvent event, const std::string &tier_name, const std::string &model_path, size_t footprint)> ResidencyObserverFn
Residency observer callback type (internal C++ form).
void clear_all_prompt_caches()
Invalidate prompt/KV caches across every pooled backend.
ResidencyEvent
Residency observer event codes — mirror the C ABI enum entropic_residency_event_t exactly (LOADED=0,...
Manages MCP server instances and routes tool calls.
void clear_external_tool_interrupts()
Release the interrupt on every external client (gh#150).
void interrupt_external_tools()
Abort in-flight tool calls across every external MCP client.
SQLite-based storage backend.
Definition backend.h:43
bool save_messages(const std::string &conversation_id, const std::string &messages_json)
Save messages to a conversation.
Definition backend.cpp:324
bool complete_delegation(const std::string &delegation_id, const std::string &status, const std::optional< std::string > &result_summary=std::nullopt)
Mark a delegation as completed or failed.
Definition backend.cpp:711
bool latest_delegation_for_target(const std::string &target_tier, std::string &delegation_id)
Search delegations across all conversations (gh#32, v2.1.6).
Definition backend.cpp:848
bool create_delegation(const std::string &parent_conversation_id, const std::string &delegating_tier, const std::string &target_tier, const std::string &task, int max_turns, std::string &delegation_id, std::string &child_conversation_id)
Create a delegation record with a child conversation.
Definition backend.cpp:649
bool save_snapshot(const std::string &conversation_id, const std::string &messages_json)
Save a pre-compaction snapshot of full conversation history.
Definition backend.cpp:914
std::string create_conversation(const std::string &title="New Conversation", const std::optional< std::string > &project_path=std::nullopt, const std::optional< std::string > &model_id=std::nullopt)
Create a new conversation.
Definition backend.cpp:222
std::string to_json(size_t count) const
Serialize recent entries to JSON array string.
Processes tool calls from model output.
const ToolCallHistory & tool_history() const
Access the tool-call history ring buffer.
std::string auto_discover_and_load()
Auto-discover and load bundled_models.yaml.
AgentEngine * engine
Fallback interrupt target.
Definition engine.cpp:4075
int * cancel
Consumer's cancel flag, nullable.
Definition engine.cpp:4074
Private definition of the entropic_engine struct.
static entropic_error_t check_mcp_auth(entropic_handle_t h)
Check handle prerequisites for MCP auth APIs.
Definition entropic.cpp:185
static void thread_frontmatter_sampler(entropic::TierConfig &tc, const entropic::prompts::IdentityFrontmatter &fm)
Cache per-tier frontmatter fields (allowed_tools, validation_rules, relay).
Definition entropic.cpp:934
static entropic_error_t stream_session_turn(entropic_handle_t handle, const char *session_key, const std::string &tier, const char *input, void(*on_token)(const char *, size_t, void *), void *user_data, int *cancel_flag)
Run one streaming turn on a claimed session (gh#144, gh#205).
static entropic_error_t do_configure_dir(entropic_handle_t handle, const char *project_dir)
entropic_configure_dir body — wrapped by c_api_try.
static void apply_identity_frontmatter(entropic_handle_t h, const std::string &name, const entropic::prompts::IdentityFrontmatter &fm)
Apply a parsed identity's frontmatter to the engine handle.
Definition entropic.cpp:972
entropic_error_t entropic_run_messages(entropic_handle_t handle, const char *messages_json, char **result_json)
Blocking multimodal agentic run (gh#37, v2.1.8).
static entropic_error_t do_state_load(entropic_handle_t handle, const char *tier_name, const char *path)
Load tier's KV cache from file body (gh#23 v2.3.25).
entropic_error_t entropic_identity_count(entropic_handle_t handle, size_t *total, size_t *dynamic)
Get identity count (total and dynamic).
static bool si_create_delegation(const char *parent_id, const char *delegating_tier, const char *target_tier, const char *task, int max_turns, std::string &delegation_id, std::string &child_conversation_id, void *user_data)
Initialize persistence: storage + session logger.
Definition entropic.cpp:612
static entropic_error_t reject_if_configured(entropic_handle_t h)
Post-parse config setup: subsystem construction + wiring.
entropic_error_t entropic_session_drop(entropic_handle_t handle, const char *session_key)
Forget a session entirely (gh#144, v2.12.0).
static std::vector< std::string > resolve_allowed_tools(entropic_engine *h, const std::string &tier)
Post-parse config setup: load bundled models, set configured.
Definition entropic.cpp:330
static entropic_error_t do_configure_json(entropic_handle_t handle, const char *config_json)
entropic_configure body — wrapped by c_api_try in the public entry.
static void start_external_bridge(entropic_handle_t h)
Start the external MCP bridge if enabled in config.
static char * sp_search_delegations(const char *query, int max_results, void *ud)
State provider: search_delegations (gh#32, v2.1.6).
entropic_error_t entropic_grammar_deregister(entropic_handle_t handle, const char *key)
Remove a grammar from the registry.
entropic_error_t entropic_run_session_streaming(entropic_handle_t handle, const char *session_key, const char *input, void(*on_token)(const char *token, size_t len, void *user_data), void *user_data, int *cancel_flag)
Streaming turn on a named session (gh#144, v2.12.0).
static entropic_error_t validate_prompt_sources(entropic_handle_t h, const std::filesystem::path &data_dir)
Reject a configured prompt path that cannot be loaded (gh#156).
entropic_error_t entropic_context_usage(entropic_handle_t handle, size_t *tokens_used, size_t *capacity)
Read current context-window pressure (gh#39, v2.1.8).
char * entropic_adapter_list(entropic_handle_t handle)
List all known adapters as a JSON array.
static nlohmann::json generations_json(entropic_engine *h)
Serialize the orchestrator's per-generation records (gh#154).
entropic_error_t entropic_validation_resume_retry(entropic_handle_t handle)
Resume a paused constitutional revision pass.
static char * sp_load_delegation_conversation(const char *delegation_id, void *ud)
State provider: load_delegation_conversation (gh#32, v2.1.6).
static entropic_error_t run_messages_stream_inner(entropic_handle_t handle, const char *messages_json, void(*on_token)(const char *token, size_t len, void *user_data), void *user_data, int *cancel_flag)
Streaming multimodal run (gh#37, v2.1.8).
static char * sp_get_metrics(void *ud)
State provider: get_metrics.
static char * sp_get_state(void *ud)
State provider: get_state (runtime environment).
static void rewire_observers(entropic_handle_t h)
Re-bind every pre-configure observer to the new engine.
Definition entropic.cpp:547
entropic_error_t entropic_deserialize_mcp_keys(entropic_handle_t handle, const char *json)
Deserialize all identity key sets from JSON.
static char * sp_get_residency(void *ud)
State provider: get_residency — VRAM residency snapshot.
static std::string build_shared_prompt_prefix(entropic_handle_t h, const std::filesystem::path &data_dir)
Build shared system prompt prefix (constitution + app_context).
Definition entropic.cpp:901
entropic_error_t entropic_create(entropic_handle_t *handle)
Create a new engine instance.
Definition entropic.cpp:261
static void init_mcp_servers(entropic_handle_t h, const std::filesystem::path &data_dir)
Initialize MCP servers with resolved working directory.
Definition entropic.cpp:858
entropic_error_t entropic_grammar_register(entropic_handle_t handle, const char *key, const char *gbnf_content)
Register a grammar by key with GBNF content.
entropic_error_t entropic_set_stream_observer(entropic_handle_t handle, void(*observer)(const char *token, size_t len, void *user_data), void *user_data)
Set a global stream observer callback.
entropic_error_t entropic_validation_set_auto_retry(entropic_handle_t handle, int enabled)
Toggle automatic constitutional revision.
entropic_error_t entropic_update_identity(entropic_handle_t handle, const char *name, const char *config_json)
Update an existing dynamic identity.
entropic_error_t entropic_set_residency_observer(entropic_handle_t handle, entropic_residency_observer_t observer, void *user_data)
Register a residency observer on the orchestrator.
entropic_error_t entropic_metrics_json(entropic_handle_t handle, char **out)
Get loop metrics as JSON (flat + per_tier).
entropic_error_t entropic_validation_set_identity(entropic_handle_t handle, const char *identity_name, bool enabled)
Set per-identity validation override.
entropic_error_t entropic_grant_mcp_key_from(entropic_handle_t handle, const char *granter, const char *grantee, const char *pattern, entropic_mcp_access_level_t level)
Grant a key from one identity to another.
void entropic_free(void *ptr)
Free memory allocated by the engine.
entropic_error_t entropic_run_streaming(entropic_handle_t handle, const char *input, void(*on_token)(const char *token, size_t len, void *user_data), void *user_data, int *cancel_flag)
Streaming generation — delegates entirely to engine.
static entropic_error_t check_stream_args(entropic_handle_t handle, const char *tier, const char *input, void(*on_token)(const char *, size_t, void *))
Validate a streaming session turn's arguments (gh#205).
double entropic_throughput_tok_per_sec(entropic_handle_t handle, const char *model_path)
Get EWMA throughput estimate in tokens per second.
char * entropic_profile_get(entropic_handle_t handle, const char *name)
Get a GPU resource profile by name as JSON.
entropic_error_t entropic_validation_accept_last(entropic_handle_t handle)
Accept the last paused attempt as the final answer.
static void populate_tier_info(entropic_handle_t h, const std::filesystem::path &data_dir, const std::string &shared_prefix)
Register per-tier ChildContextInfo with the engine.
Definition entropic.cpp:567
static entropic_error_t start_http_listener(entropic_handle_t h)
Start the Streamable-HTTP listener when one is configured (gh#204).
static entropic::InferenceBackend * require_ready_backend(entropic_handle_t h, const char *tier_name)
Resolve a tier name to a resident backend, loading it if needed.
Definition entropic.cpp:231
static void init_persistence(entropic_handle_t h)
Initialize persistence: storage + session logger + StorageInterface.
Definition entropic.cpp:794
void entropic_throughput_reset(entropic_handle_t handle, const char *model_path)
Reset throughput tracking data.
entropic_error_t entropic_state_load(entropic_handle_t handle, const char *tier_name, const char *path)
Restore a tier's KV cache from a file (gh#23 v2.3.25).
static char * sp_get_docs(const char *section, void *ud)
State provider: get_docs.
static bool si_save_conversation(const char *conversation_id, const char *messages_json, void *user_data)
StorageInterface bridge: save_conversation trampoline.
Definition entropic.cpp:670
static void rewire_state_observer(entropic_handle_t h)
Propagate any pre-configure state observer to the new engine.
Definition entropic.cpp:507
entropic_error_t entropic_session_context_set(entropic_handle_t handle, const char *session_key, const char *messages_json)
Replace one session's conversation — see entropic.h (gh#165).
static entropic_error_t run_as_inner(entropic_handle_t handle, const char *tier, const char *input, char **result_json)
Run + serialize for entropic_run_as (gh#99).
entropic_error_t entropic_set_critique_callbacks(entropic_handle_t handle, void(*start_cb)(void *user_data), void(*end_cb)(void *user_data), void *user_data)
Register critique start/end callbacks on the handle (gh#50, v2.1.12).
entropic_error_t entropic_queue_user_message(entropic_handle_t handle, const char *message)
Enqueue a follow-up user message while a run is in flight.
char * entropic_list_mcp_keys(entropic_handle_t handle, const char *identity_name)
List MCP keys for an identity as JSON array.
void entropic_destroy(entropic_handle_t handle)
Destroy an engine instance.
entropic_error_t entropic_adapter_load(entropic_handle_t handle, const char *adapter_name, const char *adapter_path, const char *base_model_path, float scale)
Load a LoRA adapter into RAM.
static bool si_latest_delegation_for_target(const char *target_tier, std::string &delegation_id, void *user_data)
StorageInterface bridge: latest_delegation_for_target (gh#162).
Definition entropic.cpp:707
entropic_error_t entropic_run_batch(entropic_handle_t handle, const char **tiers, const char **prompts, size_t n, char **result_json)
Same-prefix batch run on a shared resident model (gh#98).
entropic_error_t entropic_validation_set_enabled(entropic_handle_t handle, bool enabled)
Enable or disable constitutional validation globally.
entropic_error_t entropic_configure_from_file(entropic_handle_t handle, const char *config_path)
Configure the engine from a YAML config file.
static std::vector< std::vector< entropic::Message > > build_batch_messages(entropic_handle_t handle, const char **tiers, const char **prompts, size_t n, std::vector< std::string > &tiers_out)
Build per-request messages for entropic_run_batch (gh#98).
static void thread_frontmatter_samplers(entropic_handle_t h, const std::filesystem::path &data_dir)
Thread per-tier frontmatter SAMPLERS into config pre-orchestrator.
int entropic_adapter_state(entropic_handle_t handle, const char *adapter_name)
Query adapter lifecycle state.
static void rewire_critique_callbacks(entropic_handle_t h)
Propagate pre-configure critique callbacks to a newly- constructed ConstitutionalValidator (gh#50,...
Definition entropic.cpp:527
int64_t entropic_seconds_since_last_activity(entropic_handle_t handle)
gh#35: idle-time accessor for host-side idle-exit policies.
entropic_error_t entropic_set_state_observer(entropic_handle_t handle, void(*observer)(int state, void *user_data), void *user_data)
Register a state-change observer on the handle.
static void init_engine_and_interfaces(entropic_handle_t h, const std::filesystem::path &data_dir)
Build the engine + inference interfaces (configure step 2).
static void wire_hooks_and_validator(entropic_handle_t h, entropic::InferenceInterface &iface, const std::string &constitution_text)
Wire hook dispatch and attach the constitutional validator.
entropic_error_t entropic_grant_mcp_key(entropic_handle_t handle, const char *identity_name, const char *pattern, entropic_mcp_access_level_t level)
Grant an MCP tool key to an identity.
entropic_error_t entropic_run_session(entropic_handle_t handle, const char *session_key, const char *input, char **result_json)
Run a turn on a named session (gh#144, v2.12.0).
static entropic_error_t do_adapter_load(entropic_handle_t handle, const char *adapter_name, const char *adapter_path, const char *base_model_path, float scale)
Body of entropic_adapter_load — resolve the tier, make its model resident, load the adapter against i...
static entropic::LoopConfig build_loop_config(entropic_handle_t h)
Build LoopConfig from parsed config.
static entropic_error_t check_orchestrator(entropic_handle_t h)
Check handle prerequisites for orchestrator APIs.
Definition entropic.cpp:170
entropic_error_t entropic_context_count(entropic_handle_t handle, size_t *count)
Get conversation message count.
static void rewire_queue_observer(entropic_handle_t h)
Propagate any pre-configure queue observer to the new engine.
Definition entropic.cpp:488
char * entropic_validation_last_result(entropic_handle_t handle)
Get last validation result as JSON.
static char * sp_get_validation(void *ud)
Return the validator's last verdict as JSON for ON_COMPLETE.
static std::vector< std::string > facade_unsafe_external_tools(const std::string &session_key, const std::vector< std::string > &allowed, void *ud)
External tools a sandboxed child could write through (gh#160).
char * entropic_grammar_validate(const char *gbnf_content)
Validate a GBNF grammar string without registering.
char * entropic_profile_list(entropic_handle_t handle)
List all registered profile names as a JSON array.
entropic_error_t entropic_session_list(entropic_handle_t handle, char **sessions_json)
List the sessions this handle holds (gh#144, v2.12.0).
entropic_error_t entropic_compute_perplexity(entropic_handle_t handle, const char *model_id, const int32_t *tokens, int n_tokens, float *perplexity)
Compute perplexity for a token sequence.
entropic_error_t entropic_grammar_register_file(entropic_handle_t handle, const char *key, const char *path)
Register a grammar from a GBNF file.
static entropic_error_t session_context_set_inner(entropic_handle_t handle, const std::string &key, const char *messages_json)
Parse, replace and invalidate for entropic_session_context_set.
static char * sp_get_config(void *ud)
State provider: get_config.
entropic_error_t entropic_speculative_compat(entropic_handle_t handle, int *compatible, char **diagnostic)
Query speculative-decoding compatibility for the configured target/draft pair.
entropic_error_t entropic_destroy_identity(entropic_handle_t handle, const char *name)
Destroy a dynamic identity.
static std::string serialize_batch_results(const std::vector< entropic::GenerationResult > &results)
Serialize batch results to a JSON array string (gh#98).
static char * sp_get_history(int max_entries, void *ud)
State provider: get_history — conversation context snapshot.
int entropic_check_mcp_key(entropic_handle_t handle, const char *identity_name, const char *tool_name, entropic_mcp_access_level_t level)
Check MCP key authorization for an identity.
entropic_error_t entropic_run(entropic_handle_t handle, const char *input, char **result_json)
Single-turn blocking agentic run.
static void rewire_stream_observer(entropic_handle_t h)
Propagate any pre-configure stream observer to the new engine.
Definition entropic.cpp:469
entropic_error_t entropic_session_context_count(entropic_handle_t handle, const char *session_key, size_t *count)
Message count for one session (gh#144, v2.12.0).
char * entropic_adapter_info(entropic_handle_t handle, const char *adapter_name)
Get adapter info as JSON string.
static char * sp_get_tools(void *ud)
State provider: get_tools.
static entropic_error_t do_configure_from_file(entropic_handle_t handle, const char *config_path)
entropic_configure_from_file body — wrapped by c_api_try.
entropic_error_t entropic_configure(entropic_handle_t handle, const char *config_json)
Configure the engine from a JSON/YAML config string.
entropic_error_t entropic_profile_deregister(entropic_handle_t handle, const char *name)
Remove a GPU resource profile by name.
static void fill_logprob_result(const entropic::LogprobResult &lr, entropic_logprob_result_t *result)
Copy a LogprobResult into the caller's C struct.
entropic_error_t entropic_set_queue_observer(entropic_handle_t handle, void(*observer)(const char *, size_t, void *), void *user_data)
Register the queue-consumption observer.
static std::vector< entropic::Message > parse_and_check_vision(entropic_handle_t handle, const char *messages_json, entropic_error_t &out_rc)
Parse messages_json and check vision-tier availability (gh#37/gh#41).
static char * tool_history_json_thunk(size_t count, void *ud)
Wire the ToolExecutor and attach it to the engine.
static std::string build_assembled_prompt_for_tier(entropic_engine *h, const std::string &tier_name)
Build the assembled system prompt the engine would send for a given tier (constitution + app_context ...
static bool si_complete_delegation(const char *delegation_id, const char *status, const char *summary, void *user_data)
StorageInterface bridge: complete_delegation trampoline.
Definition entropic.cpp:654
static bool read_state_file(const char *path, std::vector< uint8_t > &out_buf)
Load tier's KV cache from file body (gh#23 v2.3.25).
entropic_error_t entropic_user_message_queue_depth(entropic_handle_t handle, size_t *count)
Snapshot the mid-gen queue depth.
static entropic_error_t init_orchestrator(entropic_handle_t h, const std::filesystem::path &data_dir)
Shared body of all entropic_configure* entry points.
char * entropic_grammar_get(entropic_handle_t handle, const char *key)
Get grammar GBNF content by key.
char * entropic_list_identities(entropic_handle_t handle)
List all identity names as JSON array.
entropic_error_t entropic_context_get(entropic_handle_t handle, char **messages_json)
Get conversation as JSON array.
entropic_error_t entropic_state_save(entropic_handle_t handle, const char *tier_name, const char *path)
Save a tier's KV cache to a file (gh#23 v2.3.25, MVP item 13).
entropic_error_t entropic_run_as(entropic_handle_t handle, const char *tier_or_identity, const char *input, char **result_json)
Single-turn blocking run under a named tier (gh#99).
static void wire_prompts_and_persistence(entropic_handle_t h, const std::filesystem::path &data_dir)
Assemble prompts + wire validation/persistence (config step 3).
static entropic_error_t configure_common(entropic_handle_t h)
Shared body of all entropic_configure* entry points.
static void warn_tier_grammars_step(entropic_handle_t h, const std::filesystem::path &data_dir)
Warn about a tier grammar stem that resolves to nothing (gh#154).
entropic_error_t entropic_profile_register(entropic_handle_t handle, const char *profile_json)
Register a custom GPU resource profile from JSON.
char * entropic_serialize_mcp_keys(entropic_handle_t handle)
Serialize all identity key sets to JSON.
void entropic_free_logprob_result(entropic_logprob_result_t *result)
Free internal arrays of a logprob result.
entropic_error_t entropic_adapter_swap(entropic_handle_t handle, const char *adapter_name)
Swap active LoRA adapter.
static entropic_error_t run_messages_inner(entropic_handle_t handle, const char *messages_json, char **result_json)
Blocking multimodal run (gh#37, v2.1.8).
entropic_error_t entropic_set_delegation_callbacks(entropic_handle_t handle, ent_delegation_start_cb on_start, ent_delegation_complete_cb on_complete, void *user_data)
Register delegation start/complete callbacks (gh#29, v2.1.5).
entropic_error_t entropic_interrupt_session(entropic_handle_t handle, const char *session_key)
Interrupt one session's run — see entropic.h (gh#158, v2.13.0).
entropic_error_t entropic_release_model(entropic_handle_t handle, const char *tier_name)
Release a resident model, keeping conversations and registrations.
entropic_error_t entropic_interrupt(entropic_handle_t handle)
Interrupt a running generation (thread-safe).
static std::vector< std::string > collect_delegatable_tiers(const entropic::ParsedConfig &config)
Collect valid delegation targets from handoff_rules.
Definition entropic.cpp:831
entropic_error_t entropic_session_context_clear(entropic_handle_t handle, const char *session_key)
Clear one session's history, leaving others intact (gh#144).
static void wire_session_roots(entropic_handle_t h)
Install the gh#160 session-root + dir-swap seams on the engine.
static std::vector< std::string > filter_tools(const nlohmann::json &all_tools, const std::vector< std::string > &allowed)
Filter tool definitions by allowed list.
Definition entropic.cpp:356
int entropic_model_has_vision(entropic_handle_t handle, const char *model_id)
Check if a model has vision (multimodal) capability.
entropic_error_t entropic_get_diagnostic_prompt(entropic_handle_t handle, char **prompt_out)
Get diagnostic prompt text for /diagnose command (stub).
static void wire_external_interrupt(entropic_handle_t h)
Wire engine interrupt propagation into MCP transports (P1-10).
Definition entropic.cpp:440
static entropic::StorageInterface build_storage_iface(entropic::SqliteStorageBackend *sb)
Build a populated StorageInterface bound to sb.
Definition entropic.cpp:775
static void cache_tier_allowed_tools(entropic_handle_t h, const std::filesystem::path &data_dir)
Cache per-tier frontmatter fields from identity files.
Definition entropic.cpp:999
entropic_error_t entropic_run_session_as(entropic_handle_t handle, const char *session_key, const char *tier_or_identity, const char *input, char **result_json)
Run a turn on a named session under a named tier (gh#144).
const char * entropic_last_error(entropic_handle_t handle)
Read the per-handle last_error under api_mutex.
Definition entropic.cpp:156
entropic_error_t entropic_configure_dir(entropic_handle_t handle, const char *project_dir)
Configure using layered resolution (project dir).
entropic_error_t entropic_residency_snapshot(entropic_handle_t handle, char **out_json)
Return the engine's current residency-set snapshot as JSON.
entropic_error_t entropic_run_messages_streaming(entropic_handle_t handle, const char *messages_json, void(*on_token)(const char *token, size_t len, void *user_data), void *user_data, int *cancel_flag)
Streaming multimodal agentic run (gh#37, v2.1.8).
entropic_error_t entropic_context_clear(entropic_handle_t handle)
Clear conversation history.
static bool parse_restore_payload(entropic_handle_t handle, const char *messages_json, std::vector< entropic::Message > &out)
Parse a restore payload into messages (gh#165, v2.13.0).
static entropic_error_t run_session_streaming_impl(entropic_handle_t handle, const char *session_key, const char *tier, const char *input, void(*on_token)(const char *, size_t, void *), void *user_data, int *cancel_flag)
Streaming turn on a named session, optionally under a tier.
static char * sp_get_identities(void *ud)
State provider: get_identities.
entropic_error_t entropic_run_session_streaming_as(entropic_handle_t handle, const char *session_key, const char *tier_or_identity, const char *input, void(*on_token)(const char *token, size_t len, void *user_data), void *user_data, int *cancel_flag)
Streaming turn on a named session under a named tier (gh#205).
entropic_error_t entropic_create_identity(entropic_handle_t handle, const char *config_json)
Create a dynamic identity from JSON config.
entropic_error_t entropic_set_attempt_boundary_cb(entropic_handle_t handle, ent_validation_attempt_boundary_cb cb, void *user_data)
Register attempt-boundary callback on the validator.
int entropic_api_version(void)
Get the plugin API version number.
char * entropic_grammar_list(entropic_handle_t handle)
List all registered grammars as a JSON array.
static std::string tool_defs_array(const std::vector< std::string > &tool_jsons)
Re-parse filtered tool defs into one JSON array string.
Definition entropic.cpp:377
static bool si_create_conversation(const char *title, std::string &conversation_id, void *user_data)
StorageInterface bridge: create_conversation trampoline.
Definition entropic.cpp:639
const char * entropic_version(void)
Get the library version string.
static entropic_error_t do_state_save(entropic_handle_t handle, const char *tier_name, const char *path)
Save tier's KV cache to file body (gh#23 v2.3.25).
static entropic_error_t check_identity(entropic_handle_t h)
Check handle prerequisites for identity manager APIs.
Definition entropic.cpp:200
static void wire_state_provider(entropic_handle_t h)
Wire state provider to the EntropicServer.
static int facade_get_tool_prompt(const char *tier, char **result, void *user_data)
Build formatted tool prompt for a tier.
Definition entropic.cpp:405
entropic_error_t entropic_get_logprobs(entropic_handle_t handle, const char *model_id, const int32_t *tokens, int n_tokens, entropic_logprob_result_t *result)
Evaluate per-token log-probabilities for a token sequence.
static void wire_tier_validation_rules(entropic_handle_t h)
Pass per-identity validation rules to the validator.
static void wire_tool_executor(entropic_handle_t h)
Wire the ToolExecutor and attach it to the engine.
static entropic_error_t c_api_try(entropic_handle_t handle, Fn &&fn)
Run a C-API entry-point body inside an exception barrier.
Definition entropic.cpp:76
entropic_error_t entropic_adapter_unload(entropic_handle_t handle, const char *adapter_name)
Unload a LoRA adapter.
entropic_error_t entropic_session_context_get(entropic_handle_t handle, const char *session_key, char **messages_json)
Read one session's conversation as JSON (gh#144, v2.12.0).
static bool si_save_snapshot(const char *conversation_id, const char *messages_json, void *user_data)
StorageInterface bridge: save_snapshot trampoline.
Definition entropic.cpp:686
void * entropic_alloc(size_t size)
Allocate memory using the engine's allocator.
entropic_error_t entropic_clear_user_message_queue(entropic_handle_t handle)
Drop all queued mid-gen user messages.
static std::filesystem::path facade_session_root(const std::string &session_key, void *ud)
Root a session's MCP tools resolve against (gh#160).
entropic_error_t entropic_revoke_mcp_key(entropic_handle_t handle, const char *identity_name, const char *pattern)
Revoke an MCP tool key from an identity.
char * entropic_get_identity_config(entropic_handle_t handle, const char *name)
Get identity config as JSON by name.
static char * alloc_cstr(const char *src)
Allocate a C string copy via the engine allocator.
Definition entropic.cpp:107
static bool si_load_delegation_with_messages(const char *delegation_id, std::string &result_json, void *user_data)
StorageInterface bridge: load_delegation_with_messages.
Definition entropic.cpp:729
Public C API for the Entropic inference engine.
ent_decision_t(* ent_delegation_start_cb)(const ent_delegation_request_t *req, void *user_data)
Callback fired before a delegation runs.
Definition entropic.h:1675
ent_decision_t(* ent_delegation_complete_cb)(const ent_delegation_result_t *res, void *user_data)
Callback fired after a delegation produces a patch.
Definition entropic.h:1700
void(* entropic_residency_observer_t)(entropic_residency_event_t event, const char *tier_name, const char *model_path, size_t footprint_bytes, void *user_data)
Residency observer callback.
Definition entropic.h:932
entropic_residency_event_t
Reasons fired by entropic_residency_observer_t.
Definition entropic.h:907
entropic_mcp_access_level_t
Access level enum for MCP authorization.
Definition entropic.h:2569
void(* ent_validation_attempt_boundary_cb)(int attempt_n, void *user_data)
Stream-side callback fired between constitutional revision passes.
Definition entropic.h:1873
Entropic MCP server — engine-level tools including introspection.
entropic_error_t
Error codes returned by all C API functions.
Definition error.h:37
@ ENTROPIC_OK
Success.
Definition error.h:38
@ ENTROPIC_ERROR_NO_VISION_TIER
Image content present but no vision-capable tier (v2.1.8, gh#41)
Definition error.h:88
@ ENTROPIC_ERROR_ADAPTER_SWAP_FAILED
Swap failed (e.g., base model not ACTIVE) (v1.9.2)
Definition error.h:67
@ ENTROPIC_ERROR_CANCELLED
Operation cancelled via cancel token.
Definition error.h:50
@ ENTROPIC_ERROR_ALREADY_EXISTS
Named resource already exists (v1.9.6)
Definition error.h:73
@ ENTROPIC_ERROR_INTERNAL
Unexpected internal error (bug)
Definition error.h:53
@ ENTROPIC_ERROR_ALREADY_RUNNING
entropic_run already in progress on handle (v1.8.9)
Definition error.h:61
@ ENTROPIC_ERROR_IDENTITY_NOT_FOUND
Identity name not in config (v1.8.9)
Definition error.h:60
@ ENTROPIC_ERROR_NOT_RUNNING
Interrupt called but nothing running (v1.8.9)
Definition error.h:62
@ ENTROPIC_ERROR_GRAMMAR_NOT_FOUND
Grammar key not in registry (v1.9.3)
Definition error.h:69
@ ENTROPIC_ERROR_INVALID_ARGUMENT
NULL pointer, empty string, out-of-range value.
Definition error.h:39
@ ENTROPIC_ERROR_QUEUE_FULL
Mid-gen user-message queue at capacity (v2.1.10, gh#40)
Definition error.h:89
@ ENTROPIC_ERROR_INVALID_HANDLE
NULL or destroyed handle (v1.8.9)
Definition error.h:57
@ ENTROPIC_ERROR_OUT_OF_MEMORY
Allocation failed (system RAM or VRAM)
Definition error.h:51
@ ENTROPIC_ERROR_EVAL_FAILED
Evaluation failed (llama_decode error) (v1.9.10)
Definition error.h:81
@ ENTROPIC_ERROR_ADAPTER_LOAD_FAILED
LoRA file invalid or incompatible with base model (v1.9.2)
Definition error.h:66
@ ENTROPIC_ERROR_PROFILE_NOT_FOUND
Profile name not in registry (v1.9.7)
Definition error.h:75
@ ENTROPIC_ERROR_IO
File/network I/O error.
Definition error.h:52
@ ENTROPIC_ERROR_GENERATE_FAILED
Generation failed (context overflow, model error)
Definition error.h:44
@ ENTROPIC_ERROR_INVALID_CONFIG
Config validation failed (missing fields, bad values)
Definition error.h:40
@ ENTROPIC_ERROR_INVALID_STATE
Operation not valid in current state (e.g., generate before activate)
Definition error.h:41
@ ENTROPIC_ERROR_LOAD_FAILED
Model load failed (corrupt file, OOM, unsupported format)
Definition error.h:43
std::string err
size_exceeded, when set
int max_entries
<= 0 means unbounded
entropic_hook_point_t
Hook points in the engine lifecycle.
Definition hooks.h:39
Pure C interface contract for inference backends.
void entropic_inference_log_silence(void)
Silence all llama/ggml log output.
Factory for building InferenceInterface from a ModelOrchestrator.
JSON serialization helpers for the facade.
LlamaCppBackend — llama.cpp C API integration.
Config loader — YAML to C++ structs with validation.
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
Prompt manager — frontmatter parsing, identity loading, assembly.
Shared parser: messages-JSON wire format → vector<Message>.
bool any_message_has_images(const std::vector< Message > &messages)
Convenience: true if any message carries image content_parts.
void wire_external_server_policy(entropic_handle_t h, ServerManager &servers, bool with_declared)
Give a server set the handle's external-server policy (v2.14.0).
@ tokens
Gate on generated tokens since the last tool call.
@ off
Disabled (default) — no thinking-budget gating.
@ wall_clock
Gate on wall-clock seconds since the last tool call.
std::string serialize_tool_calls(const std::vector< ToolCall > &calls)
Serialize parsed tool calls to the C-ABI JSON array form.
@ passed_consumer_override
gh#30 (v2.1.5): consumer called accept_last() to override a paused rejection.
@ rejected_reverted_length
Revision gutted content >50%; original preserved.
@ passed
No violations, content unchanged.
@ revised
Violations found; revision applied.
@ paused_pending_consumer
gh#30 (v2.1.5): auto_retry disabled and a critique failed.
@ skipped
Validation did not run (skip_tiers / pure-tool-call / empty)
@ rejected_max_revisions
Revisions exhausted; last output returned as-is.
@ optional
optional: every escape asks the host's path approver
@ DYNAMIC
Created at runtime via API.
@ STATIC
Loaded from YAML frontmatter file at startup.
std::filesystem::path bridge_project_dir(entropic_handle_t h)
The project dir the external bridge derives its socket from.
void destroy_orchestrator_interface(InterfaceContext *context)
Free a context returned by build_orchestrator_interface().
ENTROPIC_EXPORT std::string current_run_session()
The session key of the run executing on this thread (gh#166).
Definition run_scope.cpp:95
std::vector< Message > parse_messages_json(const char *json_str)
Parse a JSON array of messages into a vector of Message.
MCPAccessLevel
MCP tool access level for per-identity authorization.
Definition config.h:38
@ sandbox
Child tools run in a copy; output is a patch.
ENTROPIC_EXPORT void swap_session_tool_dir(const std::string &session_key, const std::filesystem::path &path, bool entering, void *ud)
The facade's gh#160 ScopedSandbox swap callback.
void wire_outside_root_approver(entropic_handle_t h)
Point the DEFAULT server set's filesystem server at the handle's path-approval slot (v2....
std::size_t release_session_tool_state(entropic_handle_t h, const std::string &key)
Release a session's per-session tool state on every server set (gh#158).
@ COLD
Not loaded. No resources consumed.
entropic::ServerManager * workspace_servers(entropic_handle_t h, const std::string &key)
The MCP servers a session's tools must run against (gh#166).
InferenceInterface build_orchestrator_interface(ModelOrchestrator *orchestrator, const std::string &default_tier, InterfaceContext **out_context)
Build an InferenceInterface wired to an orchestrator.
ModelOrchestrator — multi-model lifecycle and routing.
std::string port
–port N
Definition serve.cpp:39
std::string config
–config FILE
Definition serve.cpp:38
Resolved tier information for building child delegation contexts.
int max_revisions
Max re-generation attempts (0 = critique only)
Definition config.h:1179
bool enabled
Global enable/disable (default OFF)
Definition config.h:1178
DelegationIsolation isolation
Default: none.
Definition config.h:1103
int http_port
Also serve the bridge over Streamable HTTP on 127.0.0.1:<port> (gh#204, v2.14.0).
Definition config.h:877
bool enabled
Enable external MCP.
Definition config.h:857
Named GPU resource profile for controlling inference hardware knobs.
Definition config.h:372
int n_threads_batch
CPU threads for batch processing (0 = use n_threads)
Definition config.h:376
int n_batch
Batch size for prompt processing (1-2048)
Definition config.h:374
std::string name
Profile name ("maximum", "balanced", "background", "minimal")
Definition config.h:373
int n_threads
CPU threads for generation (0 = auto-detect)
Definition config.h:375
std::string description
Human-readable description.
Definition config.h:377
int budget_limit
gh#80 (v2.5.0) budget ceiling: generated tokens (budget_mode "tokens") or wall-clock seconds (budget_...
Definition config.h:1128
std::string budget_mode
gh#80 (v2.5.0) thinking-budget mode: "off" (default), "tokens", or "wall_clock".
Definition config.h:1121
bool stream_output
gh#110 (v2.9.6) agent-loop token delivery mode: true streams tokens via the per-token callback path,...
Definition config.h:1138
Full identity configuration.
Definition identity.h:48
std::string name
Unique identity name (e.g., "eng", "npc_blacksmith")
Definition identity.h:49
std::vector< std::string > focus
Classification focus keywords (min 1)
Definition identity.h:51
IdentityOrigin origin
How this identity was created.
Definition identity.h:67
std::string system_prompt
Full system prompt text (markdown body)
Definition identity.h:50
Configuration for the identity manager.
SpeculativeConfig speculative
Speculative decoding (gh#36)
Definition config.h:1327
Per-token log-probability evaluation result.
std::vector< float > logprobs
Log-prob for each token transition (N-1 values)
int n_logprobs
Number of logprob values (n_tokens - 1)
int n_tokens
Number of input tokens.
float total_logprob
Sum of all logprob values.
float perplexity
exp(-mean(logprobs)) — lower = less surprising
std::vector< int32_t > tokens
Input tokens echoed back for verification.
Configuration for the agentic loop.
int budget_limit
Budget ceiling for the active budget_mode: generated tokens (mode tokens) or wall-clock seconds (mode...
int context_length
Context budget for compaction (v2.0.4)
bool stream_output
Stream vs batch generation.
bool auto_approve_tools
Skip tool approval (v1.8.5)
bool speculative_enabled
gh#110 (v2.9.6): mirrors inference.speculative.enabled from config, plumbed through since core....
BudgetMode budget_mode
Thinking-budget gating mode (gh#80, v2.5.0).
bool delegation_isolation
gh#160 (v2.13.0): mirrors delegation.isolation == sandbox.
Mutable state carried through the agentic loop.
ExternalMCPConfig external
External MCP server config (Entropic-as-server)
Definition config.h:962
std::string working_dir
Server working directory (empty = CWD) (v2.0.4)
Definition config.h:976
A message in a conversation.
Definition message.h:36
std::string content
Message text content (always populated)
Definition message.h:38
std::string role
Message role.
Definition message.h:37
std::unordered_map< std::string, TierConfig > tiers
Tier name → config.
Definition config.h:690
std::string find_tier_by_path(const std::filesystem::path &model_path) const
Find tier name by model path.
Definition config.h:723
std::string default_tier
Default tier name.
Definition config.h:692
Full parsed configuration.
Definition config.h:1338
PermissionsConfig permissions
Tool permissions.
Definition config.h:1342
DelegationConfig delegation
Delegation isolation (gh#160)
Definition config.h:1345
CompactionConfig compaction
Auto-compaction settings.
Definition config.h:1344
RoutingConfig routing
Routing rules.
Definition config.h:1340
InferenceConfig inference
Inference-side knobs (currently speculative decoding only).
Definition config.h:1444
bool concurrent_sessions
gh#158 (v2.13.0): may DIFFERENT session keys run together?
Definition config.h:1406
ModelsConfig models
Tiers + router.
Definition config.h:1339
ConstitutionalValidationConfig constitutional_validation
Constitutional validation pipeline settings.
Definition config.h:1440
std::filesystem::path log_dir
Session log directory (session.log + session_model.log).
Definition config.h:1416
GenerationConfig generation
Default generation params.
Definition config.h:1341
MCPConfig mcp
MCP server settings.
Definition config.h:1343
bool console_logging
Emit engine spdlog output to the stderr console sink.
Definition config.h:1437
bool auto_approve
Skip confirmation prompts.
Definition config.h:757
std::unordered_map< std::string, std::vector< std::string > > handoff_rules
Tier handoff rules.
Definition config.h:747
Where a session's tools live, and what it can reach (gh#160).
void * user_data
Opaque pointer (facade context)
std::filesystem::path(* resolve_root)(const std::string &session_key, void *user_data)
Root directory this session's MCP tools resolve against.
std::vector< std::string >(* unsafe_external_tools)(const std::string &session_key, const std::vector< std::string > &allowed_tools, void *user_data)
Child-visible EXTERNAL tools that are not declared read-only.
bool enabled
Master switch (off by default)
Definition config.h:1286
Storage interface for conversation persistence.
bool(* create_conversation)(const char *title, std::string &conversation_id, void *user_data)
Create a root conversation row.
Tier-specific model configuration.
Definition config.h:547
std::optional< float > frequency_penalty
gh#85
Definition config.h:612
std::optional< float > temperature
Per-tier sampler temperature from identity frontmatter (gh#82).
Definition config.h:596
std::optional< float > top_p
Per-tier sampler knobs from identity frontmatter (gh#85).
Definition config.h:608
std::optional< float > repeat_penalty
Per-tier repeat_penalty + enable_thinking from identity frontmatter (gh#86).
Definition config.h:619
std::optional< float > min_p
gh#85
Definition config.h:610
std::optional< float > presence_penalty
gh#85
Definition config.h:611
std::optional< int > max_output_tokens
Per-tier max output tokens from identity frontmatter (gh#82).
Definition config.h:601
std::optional< int > top_k
gh#85
Definition config.h:609
std::optional< bool > enable_thinking
gh#86
Definition config.h:620
std::optional< std::filesystem::path > grammar
Grammar file path.
Definition config.h:550
Tool execution interface for the engine.
void(* free_fn)(char *)
Free function for strings returned by history_json.
ToolExecutionFn process_tool_calls
Dispatches tool calls.
void * user_data
Opaque pointer (ToolExecutor*)
char *(* history_json)(size_t count, void *user_data)
Optional: return a compact JSON summary of recent tool calls (for validator retry enrichment / diagno...
Identity frontmatter — full tier identity metadata.
Definition manager.h:65
std::vector< std::string > validation_rules
Per-identity constitutional rules (v2.0.6)
Definition manager.h:88
std::optional< std::vector< std::string > > allowed_tools
Tool filter.
Definition manager.h:74
std::optional< float > top_p
Per-tier top_p (gh#85); nullopt = use param default.
Definition manager.h:78
std::optional< float > repeat_penalty
Per-tier repeat_penalty (gh#86); nullopt = use param default.
Definition manager.h:83
std::optional< float > min_p
Per-tier min_p (gh#85); nullopt = use param default.
Definition manager.h:80
std::optional< float > presence_penalty
Per-tier presence_penalty (gh#85); nullopt = use param default.
Definition manager.h:81
std::optional< bool > enable_thinking
Per-tier thinking mode (gh#86); nullopt = use param default.
Definition manager.h:84
std::optional< int > max_output_tokens
Per-tier max output tokens (gh#82); nullopt = use param default.
Definition manager.h:76
std::optional< int > top_k
Per-tier top_k (gh#85); nullopt = use param default.
Definition manager.h:79
bool relay_single_delegate
Skip re-synthesis when single delegate returns (v2.0.11)
Definition manager.h:89
std::optional< float > frequency_penalty
Per-tier frequency_penalty (gh#85); nullopt = use param default.
Definition manager.h:82
std::optional< std::string > grammar
Grammar file reference.
Definition manager.h:72
std::optional< float > temperature
Per-tier sampling temperature (gh#82); nullopt = use param default.
Definition manager.h:77
Parsed identity file: frontmatter + body.
Definition manager.h:111
Engine handle struct — owns all subsystems.
std::unique_ptr< entropic::ConstitutionalValidator > validator
Constitutional validation.
std::unique_ptr< entropic::ToolExecutor > tool_executor
Tool dispatch.
void(* critique_end_cb)(void *)
Fires after the critique generate returns.
int log_id
gh#59 (v2.3.1): unique handle id for per-handle log routing via entropic::log::HandleAwareSink.
std::mutex workspace_mutex
gh#166: guards both maps above. Held for lookups only.
std::map< std::string, std::unique_ptr< EntropicWorkspace > > workspaces
gh#166: named workspaces, by name.
std::unordered_map< std::string, std::vector< std::string > > tier_validation_rules
Per-tier validation_rules from identity frontmatter (v2.0.6).
std::unique_ptr< entropic::SqliteStorageBackend > storage
SQLite persistence.
entropic::InferenceInterface inference_iface
Stable copy for validator lifetime.
std::unique_ptr< entropic::http::HttpListener > http_listener
Streamable-HTTP front end for external_bridge (gh#204).
std::unique_ptr< entropic::SessionLogger > session_logger
Model transcript log.
std::unordered_map< std::string, std::vector< std::string > > tier_allowed_tools
Per-tier allowed_tools from identity frontmatter.
std::unique_ptr< entropic::CompactorRegistry > compactor_registry
Compaction strategies.
std::atomic< bool > configured
True after configure()
std::unique_ptr< entropic::ExternalBridge > external_bridge
Unix socket MCP bridge.
void(* state_observer)(int, void *)
Observer for engine state transitions.
entropic::config::BundledModels bundled_models
Model registry.
void * stream_observer_data
Observer user_data.
std::map< std::string, std::string > session_workspace
gh#166: session key → workspace name.
std::unique_ptr< entropic::IdentityManager > identity_manager
Identity lifecycle.
std::string last_error
Per-handle error message.
std::unique_ptr< entropic::AgentEngine > engine
Agentic loop (owns conversation state)
void * critique_cb_data
Forwarded to both callbacks.
std::unique_ptr< entropic::MCPAuthorizationManager > mcp_auth
Per-identity tool auth.
std::unique_ptr< entropic::ModelOrchestrator > orchestrator
Model pool + routing.
entropic::ParsedConfig config
Parsed config.
void(* stream_observer)(const char *, size_t, void *)
Global stream observer — fires for all streaming output.
void(* queue_observer)(const char *, size_t, void *)
Observer fired when a queued mid-gen user message is consumed and seeded as the next turn.
std::unique_ptr< entropic::ServerManager > server_manager
MCP server lifecycle.
entropic::InterfaceContext * inference_iface_ctx
Per-handle owned context backing inference_iface.user_data.
void(* critique_start_cb)(void *)
Fires before the constitutional validator's critique generate begins.
entropic::HookRegistry hook_registry
Hook dispatch.
Per-token log-probability result.
Definition entropic.h:3075
float perplexity
exp(-mean(logprobs)) over the sequence.
Definition entropic.h:3078
int32_t * tokens
Input tokens echoed back (N values).
Definition entropic.h:3077
int n_logprobs
Number of logprob values (n_tokens - 1).
Definition entropic.h:3081
int n_tokens
Number of input tokens.
Definition entropic.h:3080
float * logprobs
Per-token log-probabilities (N-1 values).
Definition entropic.h:3076
float total_logprob
Sum of all logprob values.
Definition entropic.h:3079
Read-only engine state provider for introspection tools.
char *(* get_config)(void *user_data)
Get current engine configuration as JSON.
UTF-8-boundary-aware string truncation helper for the facade.
Config validation functions.