22#include <nlohmann/json.hpp>
28#include <condition_variable>
36#include <unordered_map>
37#include <unordered_set>
39#include <sys/socket.h>
44using json = nlohmann::json;
56static std::mutex s_bound_sockets_mu;
57static std::unordered_set<std::string> s_bound_sockets;
61 const std::filesystem::path& project_dir);
73static std::string
rpc_ok(
const json&
id,
const json& result) {
74 json
r = {{
"jsonrpc",
"2.0"}, {
"id",
id}, {
"result", result}};
88 const std::string&
msg) {
89 json
r = {{
"jsonrpc",
"2.0"}, {
"id",
id},
90 {
"error", {{
"code",
code}, {
"message",
msg}}}};
102 return {{
"content", json::array({{{
"type",
"text"}, {
"text", text}}})}};
117namespace tool_suffix {
118constexpr const char* kAsk =
"ask";
119constexpr const char* kAskStatus =
"ask_status";
120constexpr const char* kStatus =
"status";
121constexpr const char* kContextClear =
"context_clear";
122constexpr const char* kContextCount =
"context_count";
123constexpr const char* kRelease =
"release";
145 if (
it !=
cfg.tool_descriptions.end() && !
it->second.empty()) {
167 return {{
"name", facade::qualify_tool_name(
cfg.tool_prefix,
suffix)},
169 {
"inputSchema", {{
"type",
"object"},
170 {
"properties", json::object()}}}};
189 return {{
"name", facade::qualify_tool_name(
cfg.tool_prefix,
suffix)},
194 {
"session", {{
"type",
"string"},
196 "Conversation to act on; omit for the "
197 "shared default session."}}}
211 {
"name", facade::qualify_tool_name(
cfg.tool_prefix,
215 "Submit a prompt to the running entropic engine. "
216 "Set async=true to return immediately with a task_id; "
217 "the engine pushes a notification when done.")},
221 {
"prompt", {{
"type",
"string"},
222 {
"description",
"User message"}}},
223 {
"async", {{
"type",
"boolean"},
224 {
"description",
"Run asynchronously"},
225 {
"default",
false}}},
229 {
"session", {{
"type",
"string"},
231 "Conversation to run under. Callers passing "
232 "the same value share a conversation; "
233 "omit for the shared default session."}}},
235 {
"tier", {{
"type",
"string"},
237 "Tier or identity to run this turn under; omit "
238 "to route as usual."}}},
239 {
"workspace", {{
"type",
"string"},
241 "Repository this session works in: an "
242 "absolute directory (a workspace is created "
243 "for it on first use) or the name of a "
244 "workspace the host created. Fixed for the "
245 "session once it has messages."}}}
247 {
"required", json::array({
"prompt"})}
260 {
"name", facade::qualify_tool_name(
cfg.tool_prefix,
261 tool_suffix::kAskStatus)},
264 "Check status of an async ask task.")},
267 {
"properties", {{
"task_id", {
269 {
"description",
"Task ID from async ask"}
271 {
"required", json::array({
"task_id"})}
292 "Engine version and message count."),
294 "Clear conversation history."),
296 "Return the message count."),
299 "Unload every loaded model to free GPU/host memory. "
300 "Sessions and their conversations are kept; the next "
301 "ask reloads the model. Call when you are done for "
302 "now. Refused while a turn is running — retry then."),
322 auto s =
msg.dump() +
"\n";
344 {
"method",
"notifications/progress"},
372 const std::string& session_key) {
392 const std::string& session_key,
393 const std::string&
tier,
394 const std::string& prompt,
417 const std::string&
tier,
const std::string& prompt,
418 void (*on_token)(
const char*,
size_t,
void*),
void*
ud) {
421 prompt.c_str(), on_token,
ud,
nullptr)
423 tier.c_str(), prompt.c_str(),
424 on_token,
ud,
nullptr);
443 const std::string& session_key) {
444 auto it = args.find(
"prompt");
445 if (
it == args.end() || !
it->is_string()) {
446 return tool_text(
"error: missing 'prompt' argument");
450 args.value(
"tier", std::string{}),
481 const std::string& session_key,
483 auto it = args.find(
"prompt");
484 if (
it == args.end() || !
it->is_string()) {
485 return tool_text(
"error: missing 'prompt' argument");
487 std::string prompt =
it->get<std::string>();
492 auto on_token = [](
const char*
tok,
size_t len,
void*
ud) {
498 args.value(
"tier", std::string{}), prompt,
531 std::ostringstream
os;
533 <<
"\nmessages: " <<
count
534 <<
"\nbusy: " << (
bridge->turn_in_flight() ?
"true" :
"false")
535 <<
"\nqueue_depth: " <<
bridge->turn_queue_depth();
545 char*
mjson =
nullptr;
547 &&
mjson !=
nullptr) {
611 s.text =
msg ?
msg :
"cancelled";
612 s.status =
"cancelled";
613 s.phase =
"cancelled";
616 s.text =
msg ?
msg :
"unknown error";
624 s.text = facade_text::final_text_or_reason(
result_json);
641 const std::string& session_key) {
642 std::lock_guard<std::mutex> lock(
bridge->tasks_mutex_);
644 for (
auto& [
_, task] :
bridge->tasks_for_cancel()) {
646 if (task.session_key != session_key) {
continue; }
647 if (task.status ==
"queued" || task.status ==
"running") {
648 task.status =
"cancelled";
649 task.phase =
"cancelling";
665 const std::string& session_key) {
666 std::lock_guard<std::mutex> lock(
bridge->tasks_mutex_);
668 for (
auto& [
_, task] :
bridge->tasks_for_cancel()) {
669 left =
left || (task.session_key == session_key
670 && task.phase ==
"cancelling");
693 const std::string& session_key) {
694 if (
bridge ==
nullptr) {
return; }
700 std::this_thread::sleep_for(std::chrono::milliseconds(50));
702 bridge->detach_phase_observer();
719 const std::string& session_key) {
730 ?
"conversation cleared"
731 :
"conversation cleared for session '" + session_key +
"'");
742 const std::string& session_key) {
763 auto tid = args.value(
"task_id", std::string{});
765 auto it = tasks_.find(
tid);
766 if (
it == tasks_.end()) {
767 return tool_text(
"error: unknown task_id");
769 json status = {{
"status",
it->second.status},
770 {
"phase",
it->second.phase}};
771 if (!
it->second.result.empty()) {
772 auto key = (
it->second.status ==
"error") ?
"error" :
"result";
773 status[key] =
it->second.result;
787 static std::atomic<uint64_t> counter{0};
788 auto n = counter.fetch_add(1);
789 auto t = std::chrono::steady_clock::now().time_since_epoch().count();
790 std::ostringstream
ss;
791 ss << std::hex << (
t ^ (
n * 2654435761ULL));
792 return "task-" +
ss.str();
820 TurnTicket(ExternalBridge*
b, std::string session_key)
821 : bridge_(
b), session_(
std::
move(session_key)),
823 logger->info(
"[turn] session '{}' {} after {} ms", session_,
824 held_ ?
"admitted" :
"gave up", waited_ms_);
832 ~TurnTicket() {
if (held_) { bridge_->end_turn_wait(session_); } }
834 TurnTicket(
const TurnTicket&) =
delete;
835 TurnTicket& operator=(
const TurnTicket&) =
delete;
843 bool held()
const {
return held_; }
851 long waited_ms()
const {
return waited_ms_; }
854 ExternalBridge* bridge_;
855 std::string session_;
866 std::ostringstream
os;
867 os <<
"error: engine busy — waited " <<
waited_ms <<
" ms with "
868 <<
bridge->turn_queue_depth() <<
" request(s) still ahead. "
869 <<
"Another caller's turn is still running; retry shortly.";
886 const std::string& session_key) {
912 namespace fs = std::filesystem;
916 name = fs::weakly_canonical(
workspace,
ec).string();
926 + (
msg ?
msg :
"create failed");
929 return err.empty() ? name : std::string{};
948 const std::string& session_key,
959 err =
msg ?
msg :
"workspace bind failed";
983 const json& args,
const std::string& session_key,
989 return bridge->ask_streaming()
1012 const std::string session_key = args.value(
"session", std::string{});
1015 handle, session_key, args.value(
"workspace", std::string{}));
1019 }
else if (args.value(
"async",
false)) {
1025 args.value(
"prompt",
""), task_id,
client_fd, session_key,
1026 args.value(
"tier", std::string{}));
1027 result =
tool_text(
"async task started: " + task_id);
1043 char*
raw =
nullptr;
1046 auto snap = json::parse(
raw,
nullptr,
false);
1047 const auto list =
snap.is_object() ?
snap.value(
"residency", json::array())
1049 for (
const auto&
r : list) {
1050 names += (
names.empty() ?
"" :
", ") +
r.value(
"tier", std::string(
"?"));
1078 result =
tool_text(
"error: a turn is running on this engine; nothing "
1079 "was released — retry when it finishes");
1080 result[
"isError"] =
true;
1083 result =
tool_text(std::string(
"error: release failed: ")
1084 + (
why !=
nullptr ?
why :
"unknown"));
1085 result[
"isError"] =
true;
1088 ?
"released: nothing was resident"
1089 :
"released: " +
before +
" — sessions kept; the next ask reloads");
1091 logger->info(
"[release] rc={} resident_before='{}'",
static_cast<int>(
rc),
before);
1110 static const std::unordered_map<std::string, ToolHandler>
table = {
1112 return b->handle_ask_status(
a); }},
1145 const std::string& call_id) {
1146 std::string name =
params.value(
"name", std::string{});
1147 json args =
params.value(
"arguments", json::object());
1150 const std::string
suffix =
1151 facade::strip_tool_prefix(
1152 bridge->config().tool_prefix, name);
1153 if (
suffix == tool_suffix::kAsk) {
1156 json token = call_id;
1157 if (
params.contains(
"_meta") &&
params[
"_meta"].is_object()
1158 &&
params[
"_meta"].contains(
"progressToken")) {
1159 token =
params[
"_meta"][
"progressToken"];
1166 ?
tool_text(
"error: unknown tool '" + name +
"'")
1167 :
it->second(handle,
bridge, args);
1181 const std::string& session_key) {
1182 const auto t0 = std::chrono::steady_clock::now();
1183 const std::string key = lane_for(session_key);
1184 std::unique_lock<std::mutex> lock(turn_mutex_);
1185 TurnLane&
lane = lanes_[key];
1190 const bool ours = turn_cv_.wait_for(
1197 std::chrono::duration_cast<std::chrono::milliseconds>(
1198 std::chrono::steady_clock::now() -
t0).count());
1220 std::lock_guard<std::mutex> lock(turn_mutex_);
1221 auto it = lanes_.find(lane_for(session_key));
1222 if (
it != lanes_.end()) {
1223 TurnLane&
lane =
it->second;
1228 while (
lane.abandoned.erase(
lane.now_serving) > 0) {
1232 if (
lane.now_serving ==
lane.next_ticket) { lanes_.erase(
it); }
1235 turn_cv_.notify_all();
1245std::string ExternalBridge::lane_for(
const std::string& session_key)
const {
1248 return concurrent ? session_key : std::string{};
1258 std::lock_guard<std::mutex> lock(turn_mutex_);
1260 for (
const auto& [
_,
lane] : lanes_) {
n +=
lane.waiters; }
1271 std::lock_guard<std::mutex> lock(turn_mutex_);
1273 for (
const auto& [
_,
lane] : lanes_) { busy = busy ||
lane.busy; }
1291 const std::filesystem::path& project_dir)
1292 : handle_(handle), config_(
config) {
1320 std::filesystem::create_directories(
parent,
ec);
1346 "Refusing to bind: {} is a {} (expected unix socket)",
1362 auto s = path.string();
1363 if (
s.size() >=
sizeof(sockaddr_un::sun_path)) {
return false; }
1366 std::strncpy(
addr.sun_path,
s.c_str(),
1367 sizeof(
addr.sun_path) - 1);
1395 std::filesystem::remove(path);
1400 logger->error(
"Socket setup failed for {}: {}",
1401 path.string(), std::strerror(
errno));
1402 if (fd >= 0) { ::close(fd); }
1406 logger->info(
"External MCP bridge ready: socket={}", path.string());
1429 logger->warn(
"SO_PEERCRED failed on fd={}: {}",
1435 "Rejecting MCP client on fd={}: peer uid {} != engine uid {}",
1452 logger->warn(
"mcp.external.rate_limit={} is not enforced and is "
1465 std::filesystem::weakly_canonical(socket_path_,
ec).string();
1468 std::lock_guard
lk(s_bound_sockets_mu);
1469 if (!s_bound_sockets.insert(
canonical).second) {
1471 "External MCP bridge: socket {} already bound by another "
1472 "handle in this process; declining to start. Set "
1473 "external.socket_path to a distinct path per handle "
1474 "if you need per-handle bridges.",
1481 if (listen_fd_ < 0) {
1482 std::lock_guard
lk(s_bound_sockets_mu);
1488 running_.store(
true);
1493 int log_id = handle_ ? handle_->
log_id : 0;
1494 accept_thread_ = std::thread([
this, log_id]() {
1499 logger->info(
"External MCP bridge listening on {}",
1500 socket_path_.string());
1525 running_.store(
false);
1532 if (listen_fd_ >= 0) { ::shutdown(listen_fd_,
SHUT_RDWR); }
1533 if (accept_thread_.joinable()) {
1534 accept_thread_.join();
1536 if (listen_fd_ >= 0) {
1537 ::close(listen_fd_);
1545 std::vector<std::unique_ptr<ClientThread>>
drained;
1547 std::lock_guard<std::mutex> lock(client_threads_mutex_);
1548 drained = std::move(client_threads_);
1549 client_threads_.clear();
1554 std::lock_guard<std::mutex>
fd_lock(
ct->fd_mutex);
1558 if (
ct->thread.joinable()) {
ct->thread.join(); }
1560 join_async_workers();
1564 std::filesystem::remove(socket_path_,
ec);
1570 if (!bound_canonical_.empty()) {
1571 std::lock_guard
lk(s_bound_sockets_mu);
1572 s_bound_sockets.erase(bound_canonical_);
1573 bound_canonical_.clear();
1589void ExternalBridge::reap_finished_clients_locked() {
1590 auto it = client_threads_.begin();
1591 while (
it != client_threads_.end()) {
1592 if ((*it)->finished.load()) {
1593 if ((*it)->thread.joinable()) { (*it)->thread.join(); }
1594 it = client_threads_.erase(
it);
1626void ExternalBridge::accept_loop() {
1627 while (running_.load()) {
1629 pfd.fd = listen_fd_;
1633 if (
rc <= 0) {
continue; }
1643 logger->info(
"External MCP client connected (fd={})",
client_fd);
1645 auto ct = std::make_unique<ClientThread>();
1649 ClientThread*
raw =
ct.get();
1653 int log_id = handle_ ? handle_->
log_id : 0;
1654 ct->thread = std::thread([
this,
raw, log_id]() {
1656 serve_client(
raw->fd);
1658 std::lock_guard<std::mutex>
fd_lock(
raw->fd_mutex);
1662 logger->info(
"External MCP client disconnected");
1663 raw->finished.store(
true);
1666 std::lock_guard<std::mutex> lock(client_threads_mutex_);
1667 client_threads_.push_back(std::move(
ct));
1668 reap_finished_clients_locked();
1684 if (
n <= 0) {
return {}; }
1685 if (
c ==
'\n') {
return line; }
1700void ExternalBridge::serve_client(
int client_fd) {
1703 ExternalBridge* self;
1705 ~Unsub() { self->unsubscribe(fd); }
1708 while (running_.load()) {
1710 if (
line.empty()) {
break; }
1713 if (
response.empty()) {
continue; }
1738 const std::string name =
1739 cfg.server_name.empty() ?
"entropic" :
cfg.server_name;
1741 {
"protocolVersion",
"2025-06-18"},
1742 {
"serverInfo", {{
"name", name},
1744 {
"capabilities", {{
"tools", json::object()}}}
1763 auto req = json::parse(
request,
nullptr,
false);
1765 if (
req.is_discarded() || !
req.contains(
"id")) {
1766 return req.is_discarded()
1767 ?
rpc_err(
nullptr, -32700,
"Parse error")
1771 json
id =
req[
"id"];
1772 std::string
method =
req.value(
"method", std::string{});
1773 json
params =
req.value(
"params", json::object());
1778 else if (
method ==
"tools/call") {
1779 auto id_str =
id.is_string() ?
id.get<std::string>()
1783 else if (
method ==
"shutdown" ||
method ==
"exit") { result = json::object(); }
1784 else {
return rpc_err(
id, -32601,
"Unknown method: " +
method); }
1785 return rpc_ok(
id, result);
1798 std::lock_guard<std::mutex> lock(
self->tasks_mutex_);
1801 if (
self->observer_call_is_stale()) {
return; }
1802 auto it =
self->tasks_for_cancel().find(
self->active_task_id_for_observer());
1803 if (
it ==
self->tasks_for_cancel().end()) {
return; }
1806 it->second.phase = (
it->second.phase ==
"validating"
1807 ||
it->second.phase ==
"revising")
1808 ?
"revising" :
"validating";
1825 active_task_id_ = task_id;
1826 attached_gen_ = ++observer_gen_;
1847 active_task_id_.clear();
1870 const std::string& task_id,
const std::string& status,
1871 const std::string& phase,
const std::string& text) {
1873 auto it = tasks_.find(task_id);
1874 if (
it != tasks_.end()) {
1875 it->second.status = status;
1876 it->second.phase = phase;
1877 it->second.result = text;
1899 auto it = tasks_.find(task_id);
1900 if (
it != tasks_.end()) {
1901 it->second.status =
"error";
1902 it->second.result =
"engine busy — waited "
1903 + std::to_string(
waited_ms) +
" ms for a turn";
1926 const std::string& status) {
1929 {
"method",
"notifications/progress"},
1931 {
"progressToken", task_id},
1953 const std::string& prompt,
1954 const std::string& task_id,
1956 const std::string& session_key,
1957 const std::string&
tier) {
1963 t.created = std::chrono::steady_clock::now();
1964 t.session_key = session_key;
1966 tasks_[task_id] = std::move(
t);
1971 int log_id = handle_ ? handle_->
log_id : 0;
1974 auto worker = std::make_unique<AsyncWorker>();
1976 std::lock_guard<std::mutex> lock(async_workers_mutex_);
1977 reap_finished_async_locked();
1979 log_id, session_key,
tier]() {
1981 async_worker(prompt, task_id,
client_fd, session_key,
tier);
1982 raw->finished.store(
true);
1984 async_workers_.push_back(std::move(
worker));
1992void ExternalBridge::reap_finished_async_locked() {
1993 for (
auto it = async_workers_.begin();
it != async_workers_.end();) {
1994 const bool done = (*it)->finished.load();
1995 if (
done && (*it)->thread.joinable()) { (*it)->thread.join(); }
1996 it =
done ? async_workers_.erase(
it) : std::next(
it);
2010void ExternalBridge::join_async_workers() {
2011 std::vector<std::unique_ptr<AsyncWorker>>
workers;
2013 std::lock_guard<std::mutex> lock(async_workers_mutex_);
2014 workers = std::move(async_workers_);
2015 async_workers_.clear();
2018 [](
const auto&
w) { return !w->finished.load(); });
2019 if (running && handle_ !=
nullptr && handle_->
engine) {
2020 logger->info(
"Bridge stopping with {} async ask(s) in flight — "
2021 "interrupting the engine",
workers.size());
2022 handle_->
engine->interrupt();
2025 if (
w->thread.joinable()) {
w->thread.join(); }
2046void ExternalBridge::async_worker(
const std::string& prompt,
2047 const std::string& task_id,
int client_fd,
2048 const std::string& session_key,
2049 const std::string&
tier) {
2069 logger->info(
"Async task {} completed: {}", task_id,
final_state.status);
2084 std::lock_guard<std::mutex> lock(subscribers_mutex_);
2085 subscribers_.insert(fd);
2095 std::lock_guard<std::mutex> lock(subscribers_mutex_);
2096 subscribers_.erase(fd);
2116 logger->warn(
"Subscriber fd {} send failed (errno={}) — dropping",
2118 }
else if (
static_cast<size_t>(
rc) <
payload.size()) {
2119 logger->warn(
"Subscriber fd {} partial send ({}/{}) — dropping",
2122 return rc >= 0 &&
static_cast<size_t>(
rc) ==
payload.size();
2142 std::lock_guard<std::mutex> lock(subscribers_mutex_);
2143 if (fd >= 0 && subscribers_.count(fd) > 0
2145 subscribers_.erase(fd);
2173 std::lock_guard<std::mutex> lock(subscribers_mutex_);
2174 for (
auto it = subscribers_.begin();
it != subscribers_.end();) {
2188 const std::string& status,
2189 const std::string& phase) {
2191 auto it = tasks_.find(task_id);
2192 if (
it == tasks_.end()) {
return; }
2193 it->second.status = status;
2194 it->second.phase = phase;
2210 auto cutoff = std::chrono::steady_clock::now()
2211 - std::chrono::minutes(15);
2214 for (
auto it = tasks_.begin();
it != tasks_.end(); ) {
2217 for (
const char*
suffix :
2218 {
".done",
".failed",
".cancelled"}) {
2220 std::filesystem::remove(
2224 it = tasks_.erase(
it);
2244 std::filesystem::path root = async_sentinel_root_override_;
2245 if (root.empty() && handle_ !=
nullptr) {
2248 return root.empty() ? std::filesystem::path{} : (root /
"async");
2257 const std::filesystem::path& root) {
2258 async_sentinel_root_override_ = root;
2267 const std::string& status) {
2268 const char*
suffix =
".done";
2269 if (status ==
"error") {
2271 }
else if (status ==
"cancelled") {
2293 const std::string& status) {
2295 if (
dir.empty()) {
return; }
2297 std::filesystem::create_directories(
dir,
ec);
2299 logger->warn(
"write_sentinel: mkdir {} failed: {}",
2300 dir.string(),
ec.message());
2304 std::ofstream out(path);
2305 if (!out.is_open()) {
2306 logger->warn(
"write_sentinel: open {} failed", path.string());
2309 out << status <<
'\n';
Unix socket MCP bridge for external client access.
bool start()
Start the background accept loop.
void cleanup_expired_tasks()
Remove tasks older than TTL from the registry.
void stop()
Stop the accept loop and close the socket.
bool begin_turn_wait(long timeout_ms, long *waited_ms_out, const std::string &session_key="")
Take a ticket and wait for this caller's turn on the engine.
void unsubscribe(int fd)
Remove an fd from the subscriber set.
std::mutex tasks_mutex_
Async task mutex (public for dispatch_tool access).
const ExternalMCPConfig & config() const
The external MCP config this bridge was constructed with.
~ExternalBridge()
Destructor — stop if running.
void detach_phase_observer()
Clear the phase observer installed by attach_phase_observer.
nlohmann::json handle_ask_status(const nlohmann::json &args)
Handle entropic.ask_status — check async task state.
void run_async_ask(const std::string &prompt, const std::string &task_id, int client_fd, const std::string &session_key="", const std::string &tier="")
Run an ask on a background worker and notify its owner.
void notify_client(int fd, const nlohmann::json ¬if)
Write a notification to ONE subscribed client (v2.14.0).
std::filesystem::path async_sentinel_dir() const
Sentinel directory (lazy: returns empty path until the engine's log_dir is configured).
ExternalBridge(entropic_handle_t handle, const ExternalMCPConfig &config, const std::filesystem::path &project_dir)
Construct with engine handle and config.
void end_turn_wait(const std::string &session_key="")
Hand the turn to the next ticket in this session's lane.
void broadcast_notification(const nlohmann::json ¬if)
Write a JSON-RPC notification to every subscribed fd.
void set_async_sentinel_root(const std::filesystem::path &root)
Override the async sentinel root directory.
void attach_phase_observer(const std::string &task_id)
Run an async entropic.ask in a background thread.
void write_sentinel(const std::string &task_id, const std::string &status)
Write the sentinel file for an async task completion.
void subscribe(int fd)
Add a connected fd to the subscriber set.
void fail_task_engine_busy(const std::string &task_id, long waited_ms)
Mark an async task failed because its turn never came (gh#144).
void update_task_phase(const std::string &task_id, const std::string &status, const std::string &phase)
Update status/phase for a tracked task atomically.
void commit_async_final_state(const std::string &task_id, const std::string &status, const std::string &phase, const std::string &text)
Record an async task's terminal state and write its sentinel.
std::string dispatch(const std::string &request, int client_fd)
Dispatch one JSON-RPC request and return the response.
bool turn_in_flight() const
Whether a turn is currently running on the engine.
size_t turn_queue_depth() const
How many callers are waiting behind the one(s) in flight.
gh#59 (v2.3.1): RAII guard — sets thread's current handle_id.
Private definition of the entropic_engine struct.
Public C API for the Entropic inference engine.
ENTROPIC_EXPORT 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).
ENTROPIC_EXPORT entropic_error_t entropic_workspace_create(entropic_handle_t handle, const char *name, const char *dir)
Create a named workspace rooted at a directory (gh#166).
ENTROPIC_EXPORT entropic_error_t entropic_set_state_observer(entropic_handle_t handle, void(*observer)(int state, void *user_data), void *user_data)
Register an engine state-change observer.
ENTROPIC_EXPORT entropic_error_t entropic_context_count(entropic_handle_t handle, size_t *count)
Get the number of messages in the conversation.
ENTROPIC_EXPORT 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_EXPORT entropic_error_t entropic_residency_snapshot(entropic_handle_t handle, char **out_json)
Snapshot of the engine's current VRAM residency set.
ENTROPIC_EXPORT entropic_error_t entropic_interrupt_session(entropic_handle_t handle, const char *session_key)
Interrupt the run belonging to ONE session (gh#158, v2.13.0).
ENTROPIC_EXPORT entropic_error_t entropic_session_list(entropic_handle_t handle, char **sessions_json)
List the sessions this handle currently holds (gh#144).
ENTROPIC_EXPORT 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).
ENTROPIC_EXPORT entropic_error_t entropic_session_bind_workspace(entropic_handle_t handle, const char *session_key, const char *name)
Bind a session to a workspace (gh#166).
ENTROPIC_EXPORT entropic_error_t entropic_metrics_json(entropic_handle_t handle, char **out)
Get loop metrics from the most recent run as JSON.
ENTROPIC_EXPORT entropic_error_t entropic_session_context_get(entropic_handle_t handle, const char *session_key, char **messages_json)
Read one session's conversation as a JSON array (gh#144).
ENTROPIC_EXPORT const char * entropic_version(void)
Get the library version string.
ENTROPIC_EXPORT entropic_error_t entropic_session_context_clear(entropic_handle_t handle, const char *session_key)
Clear one session's history, leaving other sessions intact (gh#144).
ENTROPIC_EXPORT void entropic_free(void *ptr)
Free memory allocated by the engine or entropic_alloc().
ENTROPIC_EXPORT 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).
ENTROPIC_EXPORT 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).
ENTROPIC_EXPORT entropic_error_t entropic_release_model(entropic_handle_t handle, const char *tier_name)
Release a resident model, keeping everything else on the handle.
@ ENTROPIC_AGENT_STATE_VERIFYING
Post-generation verification.
entropic_error_t
Error codes returned by all C API functions.
@ ENTROPIC_ERROR_CANCELLED
Operation cancelled via cancel token.
@ ENTROPIC_ERROR_ALREADY_RUNNING
entropic_run already in progress on handle (v1.8.9)
@ ENTROPIC_ERROR_INTERRUPTED
Operation interrupted via entropic_interrupt (v1.8.9)
ENTROPIC_EXPORT const char * entropic_last_error(entropic_handle_t handle)
Get the last error message for a handle.
Unix socket MCP bridge — exposes a running engine to external clients.
std::string err
size_exceeded, when set
const ReadWindow & w
Requested window.
Operator-visible final-text extraction for the external bridge.
std::thread worker
Runs dispatch()
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Activate model on GPU (WARM → ACTIVE).
static std::string generate_task_id()
Generate a simple UUID-like task ID.
static std::string bound_workspace(entropic_handle_t handle, const std::string &session_key)
The session's current workspace binding, or "" (gh#205).
static json handle_ask(entropic_handle_t handle, const json &args, const std::string &session_key, int client_fd, const json &progress_token)
Handle entropic.ask — stream tokens then return final text.
static std::string resident_tiers(entropic_handle_t handle)
Names of the models resident right now, from the snapshot.
std::filesystem::path compute_socket_path(const std::filesystem::path &project_dir)
Compute project-unique Unix socket path for self-detection.
static const char * sentinel_suffix_for_status(const std::string &status)
Map a terminal status string to a sentinel filename suffix.
static int create_listen_socket(const std::filesystem::path &path)
Create, bind, and listen on a unix domain socket.
static json tool_definitions(const ExternalMCPConfig &cfg)
MCP tool definitions exposed by the bridge.
static json dispatch_ask(entropic_handle_t handle, ExternalBridge *bridge, const json &args, int client_fd, const json &progress_token)
Route entropic.ask — sync (streaming) or async.
static std::string resolve_workspace(entropic_handle_t handle, const std::string &workspace, std::string &err)
Resolve workspace to a workspace name, creating one for a path.
@ ok
Tool dispatched, returned non-empty content.
static json ask_tool(const ExternalMCPConfig &cfg)
Definition for the ask tool (prompt + optional async).
@ optional
optional: every escape asks the host's path approver
static void phase_observer_cb(int state, void *ud)
State observer that projects VERIFYING onto task phase.
static void send_progress(int fd, const std::string &token_text, const json &progress_token, uint64_t seq)
Send an MCP progress notification with a text token.
static std::string bind_session_workspace(entropic_handle_t handle, const std::string &session_key, const std::string &workspace)
Bind a session to the requested workspace, idempotently (gh#205).
static bool socket_path_safe(const std::filesystem::path &path)
Reject a pre-existing path that is a symlink or non-socket.
static json session_scoped_tool(const ExternalMCPConfig &cfg, const char *suffix, const char *fallback)
Definition for a tool scoped to an optional session (gh#144).
static bool send_nonblocking(int fd, const std::string &payload)
One non-blocking write of a whole payload to one fd.
static bool peer_uid_matches(int client_fd)
Validate that the connecting peer shares the engine's UID.
static json tool_text(const std::string &text)
Wrap text in MCP tool result shape.
static json handle_status(entropic_handle_t handle, const ExternalBridge *bridge)
Handle the status tool.
@ request
GenerationParams::grammar (COMMON_GRAMMAR_TYPE_USER)
@ tier
The TIER's identity frontmatter grammar: stem (gh#154).
@ count
Sentinel — MUST remain last.
static json initialize_result(const ExternalMCPConfig &cfg)
Build the MCP initialize response payload.
static json handle_clear(entropic_handle_t handle, ExternalBridge *bridge, const std::string &session_key)
entropic.context_clear MCP tool handler.
static json ask_status_tool(const ExternalMCPConfig &cfg)
Definition for the ask_status tool (task_id lookup).
static const std::unordered_map< std::string, ToolHandler > & tool_handlers()
Non-ask tools by suffix (v2.14.3).
static json task_complete_notification(const std::string &task_id, const std::string &status)
The completion notification for an async task (Issue #4, v2.1.2).
static void cancel_inflight_async_tasks(entropic_handle_t handle, ExternalBridge *bridge, const std::string &session_key)
Cancel any async tasks currently running on the bridge.
static json handle_release(entropic_handle_t handle)
release: unload every resident model, keep everything else.
static bool bind_and_listen(int fd, const std::filesystem::path &path)
Bind+listen on an AF_UNIX socket, applying 0600 perms.
static json ask_in_turn(entropic_handle_t handle, ExternalBridge *bridge, const json &args, const std::string &session_key, int client_fd, const json &progress_token)
Run a sync ask once this session's turn comes up (gh#144, gh#205).
@ read
read_file, list_directory
@ write
write_file, edit_file
static entropic_error_t run_plain_turn(entropic_handle_t handle, const std::string &session_key, const std::string &tier, const std::string &prompt, char **result_json)
Run one non-streaming turn, under tier when one is given (gh#205).
static AsyncFinalState derive_async_final_state(entropic_handle_t handle, entropic_error_t err, char *result_json)
Translate entropic_run's return code into a final task state.
static void write_json_line(int fd, const json &msg)
Write a JSON-RPC line to a socket fd.
static json handle_ask_plain(entropic_handle_t handle, const json &args, const std::string &session_key)
Handle entropic.ask — non-streaming path (gh#115, v2.9.12).
static json no_args_tool(const ExternalMCPConfig &cfg, const char *suffix, const char *fallback)
Definition for a tool that takes no arguments.
static entropic_error_t run_stream_turn(entropic_handle_t handle, const std::string &session_key, const std::string &tier, const std::string &prompt, void(*on_token)(const char *, size_t, void *), void *ud)
Run one streaming turn, under tier when one is given (gh#205).
static std::string rpc_err(const json &id, int code, const std::string &msg)
Build a JSON-RPC error response.
static json final_answer_from_context(entropic_handle_t handle, const std::string &session_key)
Read the live conversation and render the operator-visible answer.
static json handle_count(entropic_handle_t handle, const std::string &session_key)
Handle entropic.context_count.
static std::string describe(const ExternalMCPConfig &cfg, const char *suffix, const char *fallback)
Resolve a tool's description, honouring any consumer override.
static bool mark_tasks_cancelling(ExternalBridge *bridge, const std::string &session_key)
Mark one session's queued/running tasks as cancelling.
static bool any_cancelling_left(ExternalBridge *bridge, const std::string &session_key)
True while one session's task is still phase=cancelling.
json(*)(entropic_handle_t, ExternalBridge *, const json &) ToolHandler
A non-ask tool's handler (v2.14.3).
static json dispatch_tool(entropic_handle_t handle, ExternalBridge *bridge, const json ¶ms, int client_fd, const std::string &call_id)
Dispatch a tools/call to the appropriate handler.
static std::string read_line(int fd)
Read one newline-delimited line from a socket fd.
static std::string rpc_ok(const json &id, const json &result)
Build a JSON-RPC success response.
static void prepare_socket_dir(const std::filesystem::path &parent)
Prepare the socket containing directory with 0700 perms.
std::string config
–config FILE
Handle entropic.context_clear.
std::string text
result or error message
std::string status
done | error | cancelled
std::string phase
done | failed | cancelled
Async task state for background entropic.ask runs.
std::string status
queued | running | done | error | cancelled (2.0.6-rc16)
External MCP server configuration (Entropic-as-server).
std::optional< std::filesystem::path > socket_path
Socket path (nullopt = derived)
int rate_limit
NOT ENFORCED.
bool concurrent_sessions
gh#158 (v2.13.0): may DIFFERENT session keys run together?
std::filesystem::path log_dir
Session log directory (session.log + session_model.log).
Engine handle struct — owns all subsystems.
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::map< std::string, std::string > session_workspace
gh#166: session key → workspace name.
std::unique_ptr< entropic::AgentEngine > engine
Agentic loop (owns conversation state)
entropic::ParsedConfig config
Parsed config.