17#include <nlohmann/json.hpp>
41 if (hooks.fire_info !=
nullptr) {
42 hooks.fire_info(hooks.registry,
point, json);
55 std::string json =
"{\"message_count\":"
56 + std::to_string(
ctx.messages.size())
57 +
",\"delegation_depth\":"
58 + std::to_string(
ctx.delegation_depth) +
"}";
71 std::string json =
"{\"final_state\":\""
73 +
"\",\"iterations\":"
74 + std::to_string(
ctx.metrics.iterations) +
"}";
87 std::string json =
"{\"iteration\":"
88 + std::to_string(
ctx.metrics.iterations)
90 +
"\",\"consecutive_errors\":"
91 + std::to_string(
ctx.consecutive_errors) +
"}";
104 std::string json =
"{\"message_count\":"
105 + std::to_string(
ctx.messages.size()) +
"}";
112 const std::string& key);
121 auto now = std::chrono::steady_clock::now();
122 auto dur = now.time_since_epoch();
123 auto ms = std::chrono::duration_cast<
124 std::chrono::milliseconds>(
dur);
125 return static_cast<double>(
ms.count()) / 1000.0;
140 : inference_(inference),
141 loop_config_(loop_config),
142 token_counter_(loop_config.context_length),
145 compaction_manager_, callbacks_,
150 inference, loop_config, callbacks_,
151 GenerationEvents{&interrupt_flag_, &pause_flag_}) {
158 register_directive_handlers();
168 callbacks_ = callbacks;
177void AgentEngine::set_tool_executor(
179 tool_exec_ = tool_exec;
188void AgentEngine::set_tier_resolution(
190 tier_res_ = tier_res;
201 compaction_manager_.set_storage(&storage_);
210void AgentEngine::set_hooks(
const HookInterface& hooks) {
212 response_generator_.set_hooks(hooks);
213 context_manager_.set_hooks(hooks);
214 directive_processor_.set_hooks(hooks);
228void AgentEngine::set_stream_observer(
230 response_generator_.set_stream_observer(observer, user_data);
241void AgentEngine::set_delegation_callbacks(
245 std::lock_guard<std::mutex> lock(delegation_cb_mutex_);
246 delegation_cb_.start = on_start;
247 delegation_cb_.complete = on_complete;
248 delegation_cb_.user_data = user_data;
258AgentEngine::delegation_callbacks_snapshot()
const {
259 std::lock_guard<std::mutex> lock(delegation_cb_mutex_);
260 return delegation_cb_;
270void AgentEngine::set_validation_provider(
271 char* (*provider)(
void*),
void* user_data) {
272 validation_provider_ = provider;
273 validation_provider_data_ = user_data;
299void AgentEngine::apply_identity_overrides(
LoopContext& ctx) {
300 if (ctx.
locked_tier.empty() || tier_res_.get_tier_param ==
nullptr) {
304 ctx.
locked_tier,
"max_iterations", tier_res_.user_data);
307 logger->info(
"[override] tier={} max_iterations={}",
310 auto mt = tier_res_.get_tier_param(
311 ctx.
locked_tier,
"max_tool_calls_per_turn", tier_res_.user_data);
314 logger->info(
"[override] tier={} max_tool_calls_per_turn={}",
341int AgentEngine::resolve_max_iterations(
const LoopContext& ctx)
const {
342 int operator_limit = ctx.effective_max_iterations >= 0
343 ? ctx.effective_max_iterations
344 : loop_config_.max_iterations;
345 if (ctx.delegated_max_turns < 1) {
346 return operator_limit;
348 return std::min(operator_limit, ctx.delegated_max_turns);
358int AgentEngine::resolve_max_tool_calls(
const LoopContext& ctx)
const {
359 return ctx.effective_max_tool_calls_per_turn >= 0
360 ? ctx.effective_max_tool_calls_per_turn
361 : loop_config_.max_tool_calls_per_turn;
389void AgentEngine::begin_loop_preamble(LoopContext& ctx) {
390 apply_identity_overrides(ctx);
391 reinject_context_anchors(ctx);
392 if (ctx.metrics.start_time == 0.0) {
395 set_state(ctx, AgentState::PLANNING);
408void AgentEngine::run_loop(
LoopContext& ctx,
bool inherit_interrupt) {
419 if (!inherit_interrupt) {
421 clear_pause_for_fresh_turn();
423 begin_loop_preamble(ctx);
426 accumulate_per_tier(ctx);
448void AgentEngine::clear_pause_for_fresh_turn() {
449 std::size_t others = 0;
451 std::lock_guard<std::mutex> guard(runs_mutex_);
452 others = active_runs_.size();
455 pause_flag_.store(
false);
471std::vector<Message> AgentEngine::run(std::vector<Message> messages,
472 const std::string& tier_override) {
476 last_activity_epoch_s_.store(
477 std::chrono::duration_cast<std::chrono::seconds>(
478 std::chrono::system_clock::now().time_since_epoch()).
count());
492 init_session_conversation(ctx);
498 clear_pause_for_fresh_turn();
504 begin_loop_preamble(ctx);
509 accumulate_run_metrics(ctx);
510 logger->info(
"Loop complete: {} iterations, {}ms",
521void AgentEngine::init_session_conversation(
LoopContext& ctx) {
529 if (storage_.create_conversation ==
nullptr) {
return; }
531 if (storage_.create_conversation(
"session", conv_id, storage_.user_data)
532 && !conv_id.empty()) {
535 logger->warn(
"Storage create_conversation failed at run() init; "
536 "delegations will not persist this session (gh#48)");
549void AgentEngine::accumulate_run_metrics(LoopContext& ctx) {
551 std::lock_guard<std::mutex> guard(metrics_mutex_);
552 last_metrics_ = ctx.metrics;
554 accumulate_per_tier(ctx);
563void AgentEngine::accumulate_per_tier(
const LoopContext& ctx) {
564 std::lock_guard<std::mutex> guard(metrics_mutex_);
566 auto& tm = per_tier_metrics_[
567 ctx.locked_tier.empty() ?
"lead" : ctx.locked_tier];
568 tm.iterations += ctx.metrics.iterations;
569 tm.tool_calls += ctx.metrics.tool_calls;
570 tm.tokens_used += ctx.metrics.tokens_used;
571 tm.errors += ctx.metrics.errors;
572 tm.end_time += (ctx.metrics.end_time - ctx.metrics.start_time);
582 std::lock_guard<std::mutex> guard(metrics_mutex_);
583 return last_metrics_;
592std::unordered_map<std::string, LoopMetrics>
593AgentEngine::per_tier_metrics()
const {
594 std::lock_guard<std::mutex> guard(metrics_mutex_);
595 return per_tier_metrics_;
612 for (
auto rit = ctx.
messages.rbegin();
613 rit != ctx.
messages.rend() && found.empty(); ++rit) {
614 if (rit->role ==
"assistant"
615 && rit->content.find_first_not_of(
" \t\r\n")
616 != std::string::npos) {
617 found = rit->content;
649 const std::string& reason,
650 const std::string& stop_noun) {
651 std::string out =
"[" + reason
652 +
" — no substantive output was produced before the "
654 if (!carried.empty()) {
655 out = carried +
"\n\n[" + reason
656 +
" — the text above is the agent's last substantive output; "
657 "it did not signal completion]";
679void AgentEngine::finish_with_carried_output(
681 const std::string& reason,
682 const std::string& stop_noun,
683 const std::string& terminal_reason) {
684 std::string carried = last_substantive_assistant(ctx);
686 forced.role =
"assistant";
687 forced.content = annotate_hard_stop(carried, reason, stop_noun);
688 ctx.messages.push_back(std::move(forced));
689 ctx.metadata[
"terminal_reason"] = terminal_reason;
690 ctx.metadata[
"cap_carried_content"] =
691 carried.empty() ?
"false" :
"true";
692 set_state(ctx, AgentState::COMPLETE);
706void AgentEngine::force_iteration_cap_completion(LoopContext& ctx) {
707 finish_with_carried_output(
709 "iteration cap reached after "
710 + std::to_string(ctx.metrics.iterations) +
" iterations",
722void AgentEngine::loop(LoopContext& ctx) {
725 while (!should_stop(ctx)) {
726 ctx.metrics.iterations++;
728 if (run_interrupted()) {
729 set_state(ctx, AgentState::INTERRUPTED);
733 execute_iteration(ctx);
735 if (ctx.state == AgentState::ERROR) {
740 if (ctx.metrics.iterations >= resolve_max_iterations(ctx)
741 && !is_terminal_state(ctx)) {
751 logger->warn(
"Loop ended due to max iterations ({}/{}) — "
752 "forcing synthetic entropic.complete",
753 ctx.metrics.iterations, resolve_max_iterations(ctx));
754 force_iteration_cap_completion(ctx);
767void AgentEngine::execute_iteration(LoopContext& ctx) {
768 logger->info(
"[LOOP] iter {}/{} state={} msgs={}",
769 ctx.metrics.iterations,
770 resolve_max_iterations(ctx),
772 ctx.messages.size());
776 context_manager_.refresh_context_limit(ctx, 0);
777 context_manager_.prune_old_tool_results(ctx);
778 context_manager_.check_compaction(ctx);
782 set_state(ctx, AgentState::EXECUTING);
786 set_state(ctx, AgentState::COMPLETE);
790 auto result = response_generator_.generate_response(ctx);
791 dispatch_post_generate(ctx, result);
792 process_generation_result(ctx, result);
802void AgentEngine::process_generation_result(LoopContext& ctx,
803 GenerateResult& result) {
804 auto [cleaned, tool_calls] = parse_tool_calls(result.content);
805 logger->info(
"[ITER] finish={}, {} tool call(s), {} chars",
806 result.finish_reason, tool_calls.size(),
808 Message assistant_msg{
"assistant", cleaned};
809 ctx.messages.push_back(std::move(assistant_msg));
811 bool made_tool_call =
812 !tool_calls.empty() && tool_exec_.process_tool_calls !=
nullptr;
816 bool made_progress =
false;
817 if (made_tool_call) {
818 made_progress = process_tool_results(ctx, tool_calls);
822 ctx.metadata[
"zero_tool_call_retries"] =
"0";
825 evaluate_no_tool_decision(ctx, cleaned, result.finish_reason);
830 charge_thinking_budget(ctx, result.content.size(), made_progress);
832 dispatch_pending_or_halt(ctx);
840void AgentEngine::charge_thinking_budget(
841 LoopContext& ctx,
size_t content_len,
bool made_tool_call) {
842 if (loop_config_.budget_mode == BudgetMode::off) {
return; }
846 if (made_tool_call) {
847 ctx.budget_tokens_since_tool = 0;
848 ctx.budget_window_start_s = 0.0;
849 ctx.budget_completion_nudged =
false;
853 int units = budget_units_consumed(ctx, content_len);
854 if (units < loop_config_.budget_limit) {
return; }
856 if (!ctx.budget_completion_nudged) {
857 nudge_budget_completion(ctx);
859 hard_cut_budget(ctx);
870int AgentEngine::budget_units_consumed(LoopContext& ctx,
size_t content_len) {
871 if (loop_config_.budget_mode == BudgetMode::tokens) {
876 constexpr size_t kCharsPerToken = 4;
877 ctx.budget_tokens_since_tool +=
878 static_cast<int>(content_len / kCharsPerToken);
879 return ctx.budget_tokens_since_tool;
882 if (ctx.budget_window_start_s == 0.0) {
885 return static_cast<int>(
now_seconds() - ctx.budget_window_start_s);
893void AgentEngine::nudge_budget_completion(LoopContext& ctx) {
894 ctx.budget_completion_nudged =
true;
895 ctx.budget_tokens_since_tool = 0;
896 ctx.budget_window_start_s =
897 (loop_config_.budget_mode == BudgetMode::wall_clock)
899 logger->warn(
"[BUDGET] thinking budget reached ({} {}); nudging completion",
900 loop_config_.budget_limit,
901 loop_config_.budget_mode == BudgetMode::tokens
902 ?
"tokens" :
"seconds");
903 Message nudge{
"user",
904 "[engine] thinking budget reached — emit entropic.complete now "
905 "with your best current answer. Further deliberation without a "
906 "tool call will be cut off."};
907 ctx.messages.push_back(std::move(nudge));
928void AgentEngine::hard_cut_budget(LoopContext& ctx) {
929 logger->warn(
"[BUDGET] thinking budget exhausted after nudge — "
930 "hard-cutting turn");
931 finish_with_carried_output(
933 "thinking budget exhausted after the completion nudge went "
934 "unheeded; no tool call was emitted",
936 "budget_exhausted_thinking");
957void AgentEngine::dispatch_pending_or_halt(LoopContext& ctx) {
958 if (run_interrupted() && !is_terminal_state(ctx)) {
959 logger->info(
"[ITER] interrupt observed during tool processing — "
960 "halting before pending dispatch");
961 set_state(ctx, AgentState::INTERRUPTED);
965 if (is_terminal_state(ctx)) {
966 if (ctx.pending_delegation.has_value()
967 || ctx.pending_pipeline.has_value()) {
968 logger->info(
"[ITER] terminal state reached during batch "
969 "tool processing — dropping pending {}",
970 ctx.pending_delegation.has_value()
971 ?
"delegation" :
"pipeline");
976 if (ctx.pending_delegation.has_value()) {
977 ctx.metrics.iterations--;
978 execute_pending_delegation(ctx);
979 }
else if (ctx.pending_pipeline.has_value()) {
980 ctx.metrics.iterations--;
981 execute_pending_pipeline(ctx);
993void AgentEngine::evaluate_no_tool_decision(
995 const std::string& content,
996 const std::string& finish_reason) {
997 if (handle_terminal_finish_reasons(ctx, finish_reason)) {
return; }
998 if (try_auto_chain(ctx, finish_reason, content)) {
999 logger->info(
"[DECISION] auto-chain triggered");
1002 if (record_explicit_completion_failure(ctx, finish_reason)) {
return; }
1003 if (response_generator_.is_response_complete(content,
"[]")) {
1004 logger->info(
"[DECISION] response complete");
1005 set_state(ctx, AgentState::COMPLETE);
1031bool AgentEngine::handle_terminal_finish_reasons(
1032 LoopContext& ctx,
const std::string& finish_reason) {
1033 bool handled =
true;
1034 if (finish_reason ==
"interrupted") {
1035 logger->info(
"[DECISION] interrupted");
1036 set_state(ctx, AgentState::INTERRUPTED);
1037 }
else if (finish_reason ==
"context_overflow") {
1038 ctx.metadata[
"failure_reason"] =
"context_overflow";
1039 ctx.metadata[
"failure_tier"] = ctx.locked_tier;
1040 logger->error(
"[DECISION] prompt does not fit tier '{}' context — "
1041 "failing the turn instead of retrying it; the "
1042 "inference log carries the token breakdown",
1044 set_state(ctx, AgentState::ERROR);
1045 }
else if (finish_reason ==
"length") {
1046 logger->info(
"[DECISION] length, continuing");
1063bool AgentEngine::record_explicit_completion_failure(
1064 LoopContext& ctx,
const std::string& finish_reason) {
1065 if (!tier_requires_explicit_completion(ctx.locked_tier)) {
1068 auto& retries = ctx.metadata[
"zero_tool_call_retries"];
1069 int n = retries.empty() ? 0 : std::atoi(retries.c_str());
1072 if (tier_res_.get_tier_param !=
nullptr) {
1073 auto cv = tier_res_.get_tier_param(
1074 ctx.locked_tier,
"max_consecutive_empty_turns",
1075 tier_res_.user_data);
1076 if (!cv.empty()) { ceiling = std::atoi(cv.c_str()); }
1080 ctx.metadata[
"failure_reason"] =
1081 "zero_tool_calls_with_explicit_completion";
1082 ctx.metadata[
"failure_tier"] = ctx.locked_tier;
1083 logger->error(
"[DECISION] empty-turn allowance exhausted "
1084 "(tier={}, n={}/{}), failing turn",
1085 ctx.locked_tier, n, ceiling);
1086 set_state(ctx, AgentState::ERROR);
1090 logger->info(
"[DECISION] tier '{}' empty turn {}/{} "
1091 "(finish_reason={}), nudging toward action",
1092 ctx.locked_tier, n + 1, ceiling, finish_reason);
1093 std::string tools_hint =
1094 "(entropic.complete, entropic.delegate, or entropic.inspect)";
1095 if (tier_res_.get_tier_param !=
nullptr) {
1096 auto allowed = tier_res_.get_tier_param(
1097 ctx.locked_tier,
"allowed_tools", tier_res_.user_data);
1098 if (!allowed.empty()) { tools_hint =
"(" + allowed +
")"; }
1101 correction.role =
"user";
1102 correction.content =
1103 "[SYSTEM] Your previous response contained no tool call. "
1104 "You must end every turn with exactly one tool call "
1105 + tools_hint +
". Retry.";
1106 ctx.messages.push_back(std::move(correction));
1107 retries = std::to_string(n + 1);
1108 set_state(ctx, AgentState::EXECUTING);
1124bool AgentEngine::tier_requires_explicit_completion(
1125 const std::string& tier)
const {
1126 if (tier_res_.get_tier_param ==
nullptr) {
return false; }
1127 auto val = tier_res_.get_tier_param(
1128 tier,
"explicit_completion", tier_res_.user_data);
1129 return val ==
"true" || val ==
"1";
1145bool AgentEngine::is_delegation_cycle(
1146 const LoopContext& ctx,
const std::string& target)
const {
1149 if (anc == target) {
return true; }
1161bool AgentEngine::is_delegation_repeat_blocked(
1162 const LoopContext& ctx,
const std::string& target)
const {
1165 >= loop_config_.max_consecutive_failed_delegations;
1176bool AgentEngine::fold_complete_into_assistant(
1178 auto tn_it = tool_result_msg.
metadata.find(
"tool_name");
1179 bool is_complete = tn_it != tool_result_msg.
metadata.end()
1180 && tn_it->second ==
"entropic.complete";
1181 if (!is_complete || ctx.
messages.empty()) {
return false; }
1183 auto sum_it = ctx.
metadata.find(
"explicit_completion_summary");
1184 bool foldable = last.role ==
"assistant" && last.content.empty()
1186 if (!foldable) {
return false; }
1187 last.content = sum_it->second;
1196int64_t AgentEngine::seconds_since_last_activity()
const {
1197 auto last = last_activity_epoch_s_.load();
1198 if (last == 0) {
return 0; }
1199 auto now = std::chrono::duration_cast<std::chrono::seconds>(
1200 std::chrono::system_clock::now().time_since_epoch()).count();
1201 return now > last ? (now - last) : 0;
1211bool AgentEngine::should_stop(
const LoopContext& ctx)
const {
1214 return is_terminal_state(ctx) || at_limit;
1231bool AgentEngine::is_terminal_state(
const LoopContext& ctx) {
1232 return ctx.state == AgentState::COMPLETE
1233 || ctx.state == AgentState::ERROR
1234 || ctx.state == AgentState::INTERRUPTED;
1245void AgentEngine::set_state(LoopContext& ctx, AgentState state) {
1246 auto prev = ctx.state;
1249 if (callbacks_.on_state_change !=
nullptr) {
1250 callbacks_.on_state_change(
1251 static_cast<int>(state), callbacks_.user_data);
1257 if (state_observer_ !=
nullptr) {
1258 state_observer_(
static_cast<int>(state), state_observer_data_);
1263 std::string json =
"{\"previous\":\""
1265 +
"\",\"current\":\""
1271 if (state == AgentState::ERROR) {
1272 std::string json =
"{\"error_code\":\"STATE_ERROR\""
1274 + std::to_string(ctx.metrics.iterations)
1275 +
",\"consecutive\":" + std::to_string(ctx.consecutive_errors)
1292void AgentEngine::interrupt() {
1298 std::lock_guard<std::mutex> guard(runs_mutex_);
1299 for (
auto& [key, run] : active_runs_) {
1301 run->interrupt.store(
true, std::memory_order_release);
1304 if (!interrupt_flag_.exchange(
true)) {
1305 logger->info(
"Engine interrupted");
1310 if (external_interrupt_cb_ !=
nullptr) {
1311 external_interrupt_cb_(external_interrupt_data_);
1314 pause_flag_.store(
false);
1328bool AgentEngine::run_interrupted()
const {
1344void AgentEngine::set_external_interrupt(
void (*cb)(
void*),
1346 external_interrupt_cb_ = cb;
1347 external_interrupt_data_ = user_data;
1362void AgentEngine::set_external_reset(
void (*cb)(
void*),
1364 external_reset_cb_ = cb;
1365 external_reset_data_ = user_data;
1373void AgentEngine::reset_interrupt() {
1374 interrupt_flag_.store(
false);
1378 if (
auto run = this_thread_run()) {
1379 run->interrupt.store(
false, std::memory_order_release);
1386 if (external_reset_cb_ !=
nullptr) {
1387 external_reset_cb_(external_reset_data_);
1396void AgentEngine::pause() {
1397 logger->info(
"Engine paused");
1398 pause_flag_.store(
true);
1406void AgentEngine::cancel_pause() {
1407 logger->info(
"Pause cancelled, interrupting");
1408 pause_flag_.store(
false);
1409 interrupt_flag_.store(
true);
1419std::pair<int, int> AgentEngine::context_usage(
1420 const std::vector<Message>& messages)
const {
1421 int used = token_counter_.count_messages(messages);
1422 return {used, token_counter_.max_tokens};
1439void AgentEngine::reinject_context_anchors(
LoopContext& ctx) {
1440 std::unordered_map<std::string, std::string> snapshot;
1442 std::lock_guard<std::mutex> guard(anchors_mutex_);
1443 snapshot = context_anchors_;
1445 for (
const auto& [key, content] : snapshot) {
1446 ContextAnchorDirective d(key, content);
1448 dir_anchor(ctx, d, r);
1459void AgentEngine::register_directive_handlers() {
1461 directive_processor_.register_handler(t,
1462 [
this, fn](LoopContext& c,
const Directive& d,
1463 DirectiveResult& r) {
1464 (this->*fn)(c, d, r);
1487void AgentEngine::dir_stop(
1488 LoopContext&,
const Directive&, DirectiveResult& r) {
1489 logger->info(
"[DIRECTIVE] stop_processing");
1490 r.stop_processing =
true;
1498void AgentEngine::dir_tier_change(
1499 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1500 const auto& tc =
static_cast<const TierChangeDirective&
>(d);
1501 ctx.locked_tier = tc.tier;
1502 r.tier_changed =
true;
1503 logger->info(
"[DIRECTIVE] tier_change: {}", tc.tier);
1511void AgentEngine::dir_delegate(
1512 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1513 const auto& dl =
static_cast<const DelegateDirective&
>(d);
1514 ctx.pending_delegation = PendingDelegation{
1515 dl.target, dl.task, dl.max_turns,
1516 dl.resume_from_delegation_id,
1518 dl.resume_by_target};
1519 r.stop_processing =
true;
1520 if (dl.resume_from_delegation_id.empty()) {
1521 logger->info(
"[DIRECTIVE] delegate: target={} task='{}'",
1522 dl.target, dl.task);
1525 logger->info(
"[DIRECTIVE] resume_delegation: id={} task='{}'",
1526 dl.resume_from_delegation_id, dl.task);
1535void AgentEngine::dir_pipeline(
1536 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1537 const auto& pl =
static_cast<const PipelineDirective&
>(d);
1538 ctx.pending_pipeline = PendingPipeline{pl.stages, pl.task, pl.context};
1539 r.stop_processing =
true;
1540 logger->info(
"[DIRECTIVE] pipeline: {} stages", pl.stages.size());
1554void AgentEngine::dir_complete(
1555 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1556 const auto& cd =
static_cast<const CompleteDirective&
>(d);
1560 auto prior_state = ctx.state;
1561 set_state(ctx, AgentState::VERIFYING);
1564 if (fire_complete_hook(cd.summary, ctx)) {
1565 logger->info(
"[DIRECTIVE] complete REJECTED by hook → revising");
1567 ctx.metadata[
"validator_phase"] =
"revising";
1568 set_state(ctx, prior_state);
1569 r.stop_processing =
false;
1573 ctx.metadata[
"explicit_completion_summary"] = cd.summary;
1580 if (cd.coverage_gap) {
1581 ctx.metadata[
"coverage_gap"] =
"true";
1582 ctx.metadata[
"gap_description"] = cd.gap_description;
1583 nlohmann::json files_arr = cd.suggested_files;
1584 ctx.metadata[
"suggested_files_json"] = files_arr.dump();
1586 set_state(ctx, AgentState::COMPLETE);
1587 r.stop_processing =
true;
1588 logger->info(
"[DIRECTIVE] complete coverage_gap={}",
1597void AgentEngine::dir_clear_todos(
1598 LoopContext&,
const Directive&, DirectiveResult&) {
1599 logger->debug(
"[DIRECTIVE] clear_self_todos (no-op)");
1607void AgentEngine::dir_inject(
1608 LoopContext&,
const Directive& d, DirectiveResult& r) {
1609 const auto& ic =
static_cast<const InjectContextDirective&
>(d);
1610 if (!ic.content.empty()) {
1613 msg.content = ic.content;
1614 r.injected_messages.push_back(std::move(msg));
1615 logger->info(
"[DIRECTIVE] inject_context");
1624void AgentEngine::dir_prune(
1625 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1626 const auto& pm =
static_cast<const PruneMessagesDirective&
>(d);
1627 auto [pruned, freed] = context_manager_.prune_tool_results(
1628 ctx, pm.keep_recent);
1629 logger->info(
"[DIRECTIVE] prune: {} results, {} chars", pruned, freed);
1643void AgentEngine::dir_anchor(
1644 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1645 const auto& ca =
static_cast<const ContextAnchorDirective&
>(d);
1646 if (ca.content.empty()) {
1648 std::lock_guard<std::mutex> guard(anchors_mutex_);
1649 context_anchors_.erase(ca.key);
1652 logger->info(
"Removed anchor: {}", ca.key);
1656 std::lock_guard<std::mutex> guard(anchors_mutex_);
1657 context_anchors_[ca.key] = ca.content;
1661 anchor.role =
"user";
1662 anchor.content = ca.content;
1663 anchor.metadata[
"is_context_anchor"] =
"true";
1664 anchor.metadata[
"anchor_key"] = ca.key;
1665 ctx.messages.push_back(std::move(anchor));
1666 logger->info(
"Updated anchor: {}", ca.key);
1674void AgentEngine::dir_phase(
1675 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1676 const auto& pc =
static_cast<const PhaseChangeDirective&
>(d);
1677 ctx.active_phase = pc.phase;
1678 logger->info(
"[DIRECTIVE] phase_change: {}", pc.phase);
1686void AgentEngine::dir_notify(
1687 LoopContext&,
const Directive& d, DirectiveResult&) {
1688 const auto& np =
static_cast<const NotifyPresenterDirective&
>(d);
1689 if (callbacks_.on_presenter_notify !=
nullptr) {
1690 callbacks_.on_presenter_notify(
1691 np.key.c_str(), np.data_json.c_str(),
1692 callbacks_.user_data);
1714 const nlohmann::json& obj,
const std::string& tc_str) {
1716 tc.
name = obj.value(
"name",
"");
1717 tc.
id =
"tc-" + std::to_string(
1718 std::hash<std::string>{}(tc.
name + tc_str) & 0xFFFF);
1719 if (obj.contains(
"arguments") && obj[
"arguments"].is_object()) {
1721 for (
auto& [k, v] : obj[
"arguments"].items()) {
1723 ? v.get<std::string>() : v.dump();
1737 const std::string& tc_str) {
1738 std::vector<ToolCall> calls;
1739 if (tc_str ==
"[]" || tc_str.empty()) {
return calls; }
1740 auto arr = nlohmann::json::parse(tc_str,
nullptr,
false);
1741 if (!arr.is_array()) {
return calls; }
1742 for (
const auto& obj : arr) {
1761std::pair<std::string, std::vector<ToolCall>>
1762AgentEngine::parse_tool_calls(
const std::string& raw_content) {
1763 if (inference_.parse_tool_calls ==
nullptr) {
1764 return {raw_content, {}};
1767 char* cleaned =
nullptr;
1768 char* tc_json =
nullptr;
1769 int rc = inference_.parse_tool_calls(
1770 raw_content.c_str(), &cleaned, &tc_json,
1771 inference_.adapter_data);
1791 std::string cleaned_str =
1792 mcp::sanitize_utf8(cleaned ? cleaned : raw_content);
1793 std::string tc_str = mcp::sanitize_utf8(tc_json ? tc_json :
"[]");
1795 if (inference_.free_fn !=
nullptr) {
1796 if (cleaned !=
nullptr) { inference_.free_fn(cleaned); }
1797 if (tc_json !=
nullptr) { inference_.free_fn(tc_json); }
1800 if (rc != 0) {
return {cleaned_str, {}}; }
1802 if (!calls.empty()) {
1803 logger->info(
"Parsed tool calls from model output");
1805 return {cleaned_str, std::move(calls)};
1813void AgentEngine::defang_meta_action_envelope(
Message& msg)
const {
1814 auto tn = msg.
metadata.find(
"tool_name");
1815 bool is_meta = tn != msg.
metadata.end()
1816 && tn->second.rfind(
"entropic.", 0) == 0;
1817 if (!is_meta) {
return; }
1818 auto j = nlohmann::json::parse(msg.
content,
nullptr,
false);
1819 bool is_envelope = j.is_object() && j.contains(
"action")
1820 && j[
"action"].is_string();
1821 if (!is_envelope) {
return; }
1822 msg.
content =
"(engine: " + j[
"action"].get<std::string>()
1823 +
" directive accepted)";
1835bool AgentEngine::process_tool_results(
1837 const std::vector<ToolCall>& tool_calls) {
1838 set_state(ctx, AgentState::WAITING_TOOL);
1840 auto results = tool_exec_.process_tool_calls(
1841 ctx, tool_calls, tool_exec_.user_data);
1843 logger->info(
"[TOOLS] {} call(s) -> {} result message(s)",
1844 tool_calls.size(), results.size());
1851 bool made_progress =
false;
1852 for (
auto& msg : results) {
1853 auto it = msg.metadata.find(
"result_kind");
1854 if (it != msg.metadata.end()
1855 && (it->second ==
"ok" || it->second ==
"ok_empty")) {
1856 made_progress =
true;
1858 if (fold_complete_into_assistant(ctx, msg)) {
1861 defang_meta_action_envelope(msg);
1862 ctx.
messages.push_back(std::move(msg));
1867 if (!is_terminal_state(ctx)) {
1868 set_state(ctx, AgentState::EXECUTING);
1870 return made_progress;
1883 const std::string& key) {
1886 std::remove_if(msgs.begin(), msgs.end(),
1888 auto it = m.metadata.find(
"anchor_key");
1889 return it != m.metadata.end() && it->second == key;
1905bool AgentEngine::fire_pre_hook(
1907 if (hooks_.fire_pre ==
nullptr) {
1910 std::string json =
"{\"iteration\":"
1911 + std::to_string(iteration) +
"}";
1912 char* modified =
nullptr;
1913 int rc = hooks_.fire_pre(hooks_.registry,
1914 point, json.c_str(), &modified);
1930 static const auto tbl = []() {
1931 std::array<const char*, 256> t{};
1932 t[
static_cast<unsigned char>(
'"')] =
"\\\"";
1933 t[
static_cast<unsigned char>(
'\\')] =
"\\\\";
1934 t[
static_cast<unsigned char>(
'\n')] =
"\\n";
1935 t[
static_cast<unsigned char>(
'\r')] =
"\\r";
1936 t[
static_cast<unsigned char>(
'\t')] =
"\\t";
1952 out.reserve(s.size() + 16);
1954 const char* esc = tbl[
static_cast<unsigned char>(c)];
1955 if (esc) { out += esc; }
1973 const std::vector<Message>& messages) {
1974 std::string manifest;
1975 for (
const auto& msg : messages) {
1976 auto it = msg.metadata.find(
"tool_name");
1977 if (it == msg.metadata.end() || it->second.empty()) {
1985 auto orig = msg.metadata.find(
"original_content");
1986 size_t real_size = (orig != msg.metadata.end())
1987 ? orig->second.size()
1988 : msg.content.size();
1989 manifest +=
"- " + it->second
1990 +
" \xe2\x86\x92 " + std::to_string(real_size)
2005 size_t chars_per_result) {
2006 auto orig = msg.
metadata.find(
"original_content");
2007 const std::string& src = (orig != msg.
metadata.end())
2010 auto iter = msg.
metadata.find(
"added_at_iteration");
2011 std::string out =
"## ";
2012 out += msg.
metadata.at(
"tool_name");
2014 out +=
" [iter " + iter->second +
"]";
2017 if (src.size() <= chars_per_result) {
2020 out += src.substr(0, chars_per_result);
2021 out +=
"\n[... truncated, "
2022 + std::to_string(src.size() - chars_per_result)
2057 const std::vector<Message>& messages,
2059 size_t chars_per_result) {
2060 std::vector<const Message*> tool_msgs;
2061 for (
const auto& msg : messages) {
2062 auto it = msg.metadata.find(
"tool_name");
2063 if (it == msg.metadata.end() || it->second.empty()) {
2066 tool_msgs.push_back(&msg);
2068 if (tool_msgs.empty()) {
return {}; }
2070 size_t elided = (tool_msgs.size() > max_results)
2071 ? (tool_msgs.size() - max_results) : 0;
2072 size_t start = elided;
2075 out +=
"(" + std::to_string(elided)
2076 +
" earlier tool results elided for length)\n";
2078 for (
size_t i = start; i < tool_msgs.size(); ++i) {
2099 const std::string& base,
2101 if (tool_exec.
history_json ==
nullptr) {
return base; }
2103 if (js ==
nullptr) {
return base; }
2104 std::string out = base +
"prior-iteration history: " + js +
"\n";
2117 const std::vector<Message>& messages) {
2118 for (
const auto& msg : messages) {
2119 if (msg.role ==
"system") {
return msg.content; }
2139void AgentEngine::fire_post_generate_hook(
2140 GenerateResult& result,
2141 const std::string& tier,
2142 const std::vector<Message>& messages) {
2143 if (hooks_.fire_post ==
nullptr) {
2158 "{\"finish_reason\":\"" + result.finish_reason
2160 +
"\",\"tier\":\"" + tier
2164 char* out =
nullptr;
2165 hooks_.fire_post(hooks_.registry,
2167 if (out !=
nullptr) {
2173 result.content = mcp::sanitize_utf8(out);
2174 logger->info(
"POST_GENERATE hook revised content");
2200void AgentEngine::dispatch_post_generate(
2201 LoopContext& ctx, GenerateResult& result) {
2206 ctx.pending_validation_feedback.clear();
2207 ctx.pending_anti_spiral_warning.clear();
2208 fire_post_generate_hook(result, ctx.locked_tier, ctx.messages);
2211 capture_validation_feedback(ctx);
2219void AgentEngine::capture_validation_feedback(LoopContext& ctx) {
2220 if (validation_provider_ ==
nullptr) {
return; }
2221 char* v = validation_provider_(validation_provider_data_);
2222 if (v ==
nullptr) {
return; }
2226 auto j = nlohmann::json::parse(raw);
2227 auto verdict = j.value(
"verdict",
"");
2228 if (verdict.rfind(
"rejected", 0) != 0) {
return; }
2230 if (j.contains(
"violations") && j[
"violations"].is_array()) {
2231 for (
const auto& vio : j[
"violations"]) {
2232 if (!joined.empty()) { joined +=
"; "; }
2233 joined += vio.is_string()
2234 ? vio.get<std::string>()
2238 if (joined.empty()) { joined = verdict; }
2239 ctx.pending_validation_feedback = joined;
2240 }
catch (
const nlohmann::json::exception&) {
2257 const std::vector<Message>& messages) {
2258 std::string arr =
"[";
2260 for (
const auto& msg : messages) {
2261 auto it = msg.metadata.find(
"tool_name");
2262 if (it == msg.metadata.end() || it->second.empty()) {
2265 if (!first) { arr +=
","; }
2295bool AgentEngine::fire_complete_hook(
2296 const std::string& summary,
2297 const LoopContext& ctx) {
2298 if (hooks_.fire_pre ==
nullptr) {
return false; }
2304 std::string validation_block =
",\"validation\":null";
2305 if (validation_provider_ !=
nullptr) {
2306 char* v = validation_provider_(validation_provider_data_);
2308 validation_block =
",\"validation\":" + std::string(v);
2314 +
"\",\"tier\":\"" + ctx.locked_tier
2315 +
"\",\"tool_results\":" + tool_results
2316 +
",\"iteration\":" + std::to_string(ctx.metrics.iterations)
2319 char* modified =
nullptr;
2320 int rc = hooks_.fire_pre(hooks_.registry,
2322 if (modified !=
nullptr) {
2328 feedback.role =
"user";
2330 std::string(
"[CITATION VALIDATION] ") + mcp::sanitize_utf8(modified);
2331 const_cast<LoopContext&
>(ctx).messages.push_back(
2332 std::move(feedback));
2347bool AgentEngine::fire_delegate_pre_hook(
2348 const PendingDelegation& pending,
int depth) {
2349 if (hooks_.fire_pre ==
nullptr) {
2352 std::string json =
"{\"target_tier\":\""
2353 + pending.target +
"\",\"task\":\""
2354 + pending.task +
"\",\"depth\":"
2355 + std::to_string(depth) +
"}";
2356 char* modified =
nullptr;
2357 int rc = hooks_.fire_pre(hooks_.registry,
2382 const std::string& target,
2384 const std::string& summary) {
2386 j[
"target_tier"] = target;
2387 j[
"success"] = success;
2389 ? ToolResultKind::ok
2391 j[
"summary"] = mcp::sanitize_utf8(summary);
2410void AgentEngine::fire_delegate_complete_hook(
2411 const std::string& target,
bool success,
2412 const std::string& summary) {
2413 if (hooks_.fire_post ==
nullptr) {
2416 std::string json = detail::build_delegate_complete_json(
2417 target, success, summary);
2418 char* out =
nullptr;
2419 hooks_.fire_post(hooks_.registry,
2448 reject.
role =
"user";
2449 reject.
content =
"[DELEGATION REJECTED] Maximum delegation "
2450 "depth (" + std::to_string(
2451 AgentEngine::MAX_DELEGATION_DEPTH) +
2453 ctx.
messages.push_back(std::move(reject));
2470 chain += anc +
" -> ";
2475 reject.
role =
"user";
2476 reject.
content =
"[DELEGATION REJECTED] Circular delegation "
2477 "detected: tier '" + target +
2478 "' already in the active delegation chain "
2479 "(" + chain +
"). Choose a different target.";
2480 ctx.
metadata[
"failure_reason"] =
"delegation_cycle";
2481 ctx.
metadata[
"failure_target"] = target;
2482 ctx.
messages.push_back(std::move(reject));
2495 std::string tag = result.
success ?
"COMPLETE" :
"FAILED";
2498 msg.
content =
"[DELEGATION " + tag +
": " + target +
"] " + result.
summary;
2499 ctx.
messages.push_back(std::move(msg));
2514 LoopContext& ctx,
const std::string& target,
int n) {
2516 reject.
role =
"user";
2517 reject.
content =
"[DELEGATION REJECTED] '" + target
2518 +
"' has just failed " + std::to_string(n)
2519 +
" times in a row. Stop retrying this target. Either "
2520 "respond to the user with what you have, or delegate to "
2521 "a different tier.";
2522 ctx.
metadata[
"failure_reason"] =
"delegation_repeat_blocked";
2523 ctx.
metadata[
"failure_target"] = target;
2524 ctx.
messages.push_back(std::move(reject));
2546 const std::vector<std::string>& tools) {
2548 for (
const auto& t : tools) {
2549 if (!names.empty()) { names +=
", "; }
2553 reject.
role =
"user";
2555 "[DELEGATION REJECTED] delegation.isolation is 'sandbox', but '"
2556 + target +
"' can reach external MCP tools that are not declared "
2557 "read-only (" + names +
"). An external server runs in its own "
2558 "process and cannot be moved into the sandbox, so its writes "
2559 "would escape it. Either restrict the tier's allowed_tools to "
2560 "read-only tools, have the server declare readOnlyHint, or set "
2561 "delegation.isolation: none.";
2562 ctx.
metadata[
"failure_reason"] =
"delegation_isolation_unsafe";
2563 ctx.
metadata[
"failure_target"] = target;
2564 ctx.
messages.push_back(std::move(reject));
2575bool AgentEngine::reject_delegation_if_guarded(
2576 LoopContext& ctx,
const PendingDelegation& pending) {
2577 bool rejected =
true;
2578 if (ctx.delegation_depth >= MAX_DELEGATION_DEPTH) {
2579 logger->warn(
"Delegation rejected: depth {} >= max {}",
2580 ctx.delegation_depth, MAX_DELEGATION_DEPTH);
2581 push_delegation_rejected(ctx);
2582 }
else if (is_delegation_cycle(ctx, pending.target)) {
2584 logger->warn(
"Delegation rejected: cycle on target tier '{}'",
2587 }
else if (
auto unsafe = isolation_unsafe_tools(ctx, pending.target);
2590 logger->error(
"Delegation rejected: isolation is on but '{}' can "
2591 "reach {} non-read-only external tool(s)",
2592 pending.target, unsafe.size());
2594 }
else if (is_delegation_repeat_blocked(ctx, pending.target)) {
2599 logger->warn(
"Delegation rejected: '{}' failed {}x in a row "
2600 "(>= max_consecutive_failed_delegations={})",
2602 ctx.consecutive_failed_delegations,
2603 loop_config_.max_consecutive_failed_delegations);
2605 ctx, pending.target, ctx.consecutive_failed_delegations);
2625void AgentEngine::execute_pending_delegation(LoopContext& ctx) {
2626 auto pending = std::move(*ctx.pending_delegation);
2627 ctx.pending_delegation.reset();
2629 if (reject_delegation_if_guarded(ctx, pending)) {
return; }
2631 set_state(ctx, AgentState::DELEGATING);
2640 std::vector<Message> resume_history;
2641 bool blocked =
false;
2644 const bool is_resume = !pending.resume_from_delegation_id.empty()
2645 || pending.resume_by_target;
2647 && !resolve_resume_delegation(ctx, pending, resume_history)) {
2649 }
else if (fire_delegate_pre_hook(pending, ctx.delegation_depth)) {
2650 logger->info(
"ON_DELEGATE hook cancelled delegation");
2654 set_state(ctx, AgentState::EXECUTING);
2658 fire_delegation_start(ctx, pending.target, pending.task);
2659 auto result = run_pending_delegation(
2660 ctx, pending, std::move(resume_history));
2664 if (result.success) {
2665 ctx.last_failed_delegation_target.clear();
2666 ctx.consecutive_failed_delegations = 0;
2667 }
else if (pending.target == ctx.last_failed_delegation_target) {
2668 ++ctx.consecutive_failed_delegations;
2670 ctx.last_failed_delegation_target = pending.target;
2671 ctx.consecutive_failed_delegations = 1;
2674 fire_delegation_complete(ctx, pending.target, result);
2675 fire_delegate_complete_hook(pending.target, result.success,
2678 finalize_delegation_result(ctx, result);
2693void AgentEngine::relay_partial_result(
2694 LoopContext& ctx,
const std::string& summary) {
2695 GenerateResult relay_result;
2696 relay_result.content = summary;
2697 relay_result.finish_reason =
"stop";
2698 relay_result.tool_calls_json =
"[]";
2699 fire_post_generate_hook(
2700 relay_result, ctx.locked_tier, ctx.messages);
2701 ctx.metadata[
"explicit_completion_summary"] = relay_result.content;
2702 set_state(ctx, AgentState::COMPLETE);
2715 "[COVERAGE GAP from " + tier +
"]\n"
2716 "Summary so far: " + result.
summary +
"\n"
2719 body +=
"\nSuggested files to inspect:";
2721 body +=
"\n - " + f;
2744 for (
auto rit = ctx.
messages.rbegin();
2745 rit != ctx.
messages.rend(); ++rit) {
2746 if (rit->role ==
"assistant" && rit->content.empty()) {
2747 rit->content = summary;
2773void AgentEngine::finalize_delegation_result(
2774 LoopContext& ctx,
const DelegationResult& result) {
2777 const bool in_relay_tier =
2778 relay_single_delegate_tiers_.count(ctx.locked_tier) > 0;
2779 if (in_relay_tier && result.coverage_gap) {
2782 gap.content = build_coverage_gap_message(
2783 result.target_tier, result);
2784 ctx.messages.push_back(std::move(gap));
2785 ctx.metadata[
"relay_status"] =
"coverage_gap_suppressed";
2787 "[COVERAGE GAP] suppressing auto-relay for tier={} "
2788 "({} suggested files)",
2789 result.target_tier, result.suggested_files.size());
2790 set_state(ctx, AgentState::EXECUTING);
2793 if (result.success && in_relay_tier) {
2794 relay_partial_result(ctx, result.summary);
2795 log_relay_status(ctx);
2798 if (!result.terminal_reason.empty() && in_relay_tier) {
2799 relay_partial_result(ctx,
2800 "[partial — budget_exhausted] " + result.summary);
2801 log_relay_status(ctx, result.terminal_reason);
2804 bool needs_explicit = tier_requires_explicit_completion(
2806 if (result.success && !needs_explicit) {
2809 set_state(ctx, (result.success && !needs_explicit)
2810 ? AgentState::COMPLETE :
AgentState::EXECUTING);
2827void AgentEngine::log_relay_status(LoopContext& ctx,
2828 const std::string& terminal_reason) {
2829 std::string verdict;
2830 if (validation_provider_ !=
nullptr) {
2831 char* v = validation_provider_(validation_provider_data_);
2835 auto pos = jv.find(
"\"verdict\":\"");
2836 if (pos != std::string::npos) {
2838 auto end = jv.find(
'"', pos);
2839 if (end != std::string::npos) {
2840 verdict = jv.substr(pos, end - pos);
2845 if (!terminal_reason.empty()) {
2846 ctx.metadata[
"relay_status"] =
"budget_exhausted_relayed";
2848 "Relay: single-delegate result used "
2849 "(partial — terminal_reason={}, verdict={})",
2850 terminal_reason, verdict.empty() ?
"none" : verdict);
2853 if (verdict ==
"skipped" || verdict.empty()) {
2854 ctx.metadata[
"relay_status"] =
"validation_skipped";
2856 "Relay: single-delegate result used "
2857 "(lead validation skipped per config)");
2859 ctx.metadata[
"relay_status"] =
"validated";
2861 "Relay: single-delegate result used "
2862 "(passed lead validation)");
2880bool AgentEngine::reject_pipeline_if_guarded(
2881 LoopContext& ctx,
const PendingPipeline& pending) {
2882 if (ctx.delegation_depth >= MAX_DELEGATION_DEPTH) {
2883 logger->warn(
"Pipeline rejected: depth {} >= max {}",
2884 ctx.delegation_depth, MAX_DELEGATION_DEPTH);
2886 reject.role =
"user";
2887 reject.content =
"[PIPELINE REJECTED] Maximum delegation "
2889 ctx.messages.push_back(std::move(reject));
2892 for (
const auto& stage : pending.stages) {
2893 auto unsafe = isolation_unsafe_tools(ctx, stage);
2894 if (!unsafe.empty()) {
2895 logger->error(
"Pipeline rejected: isolation is on but stage "
2896 "'{}' can reach {} non-read-only external "
2897 "tool(s)", stage, unsafe.size());
2912void AgentEngine::execute_pending_pipeline(LoopContext& ctx) {
2913 auto pending = std::move(*ctx.pending_pipeline);
2914 ctx.pending_pipeline.reset();
2916 if (reject_pipeline_if_guarded(ctx, pending)) {
return; }
2918 set_state(ctx, AgentState::DELEGATING);
2924 auto root = resolve_session_root(ctx.session_key);
2925 DelegationManager mgr(run_child_loop_trampoline,
this,
2927 sandbox_for_session(ctx.session_key));
2928 auto swap = dir_swap_snapshot();
2929 mgr.set_dir_swap(swap.first, swap.second);
2930 if (storage_.create_delegation !=
nullptr) {
2931 mgr.set_storage(&storage_);
2936 auto cb_snap = delegation_callbacks_snapshot();
2937 mgr.set_delegation_callbacks(
2938 cb_snap.start, cb_snap.complete, cb_snap.user_data);
2939 std::vector<DelegationResult> stage_log;
2940 auto result = mgr.execute_pipeline(
2941 ctx, pending.stages, pending.task, stage_log, pending.context);
2943 std::string tag = result.success ?
"COMPLETE" :
"FAILED";
2944 std::string content =
"[PIPELINE " + tag +
"]";
2945 for (
size_t i = 0; i < stage_log.size(); ++i) {
2946 content +=
"\nStage " + std::to_string(i + 1)
2947 +
" (" + stage_log[i].target_tier +
"): "
2948 + stage_log[i].summary;
2950 content +=
"\nFinal: " + result.summary;
2952 result_msg.role =
"user";
2953 result_msg.content = std::move(content);
2954 ctx.messages.push_back(std::move(result_msg));
2956 set_state(ctx, AgentState::EXECUTING);
2967void AgentEngine::fire_delegation_start(
2968 const LoopContext& ,
2969 const std::string& tier,
2970 const std::string& task) {
2971 if (callbacks_.on_delegation_start !=
nullptr) {
2972 callbacks_.on_delegation_start(
2973 "", tier.c_str(), task.c_str(),
2974 callbacks_.user_data);
2986void AgentEngine::fire_delegation_complete(
2987 const LoopContext& ,
2988 const std::string& tier,
2989 const DelegationResult& result) {
2990 if (callbacks_.on_delegation_complete !=
nullptr) {
2991 callbacks_.on_delegation_complete(
2992 "", tier.c_str(), result.summary.c_str(),
2993 result.success ? 1 : 0,
2994 callbacks_.user_data);
3009bool AgentEngine::should_auto_chain(
3010 const LoopContext& ctx,
3011 const std::string& finish_reason,
3012 const std::string& content) {
3013 if (ctx.locked_tier.empty() || tier_res_.get_tier_param ==
nullptr) {
3017 std::string auto_chain = tier_res_.get_tier_param(
3018 ctx.locked_tier,
"auto_chain", tier_res_.user_data);
3019 if (auto_chain.empty()) {
3023 bool triggered = (finish_reason ==
"length") ||
3024 (finish_reason ==
"stop" &&
3025 response_generator_.is_response_complete(content,
"[]"));
3038bool AgentEngine::try_auto_chain(
3040 const std::string& finish_reason,
3041 const std::string& content) {
3042 if (!should_auto_chain(ctx, finish_reason, content)) {
3046 if (ctx.delegation_depth > 0) {
3047 logger->info(
"[AUTO-CHAIN] child depth={}, completing",
3048 ctx.delegation_depth);
3049 set_state(ctx, AgentState::COMPLETE);
3054 std::string target = tier_res_.get_tier_param(
3055 ctx.locked_tier,
"auto_chain", tier_res_.user_data);
3057 if (!target.empty()) {
3058 logger->info(
"[AUTO-CHAIN] root, tier change to '{}'", target);
3059 TierChangeDirective tc(target,
"auto_chain");
3061 dir_tier_change(ctx, tc, r);
3063 return !target.empty();
3088std::filesystem::path AgentEngine::get_repo_dir() {
3089 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3090 return get_repo_dir_locked();
3106std::filesystem::path AgentEngine::get_repo_dir_locked() {
3107 if (repo_dir_checked_) {
3108 return cached_repo_dir_.value_or(std::filesystem::path{});
3110 repo_dir_checked_ =
true;
3111 std::filesystem::path resolved;
3112 if (!project_dir_override_.empty()) {
3113 resolved = project_dir_override_;
3114 logger->info(
"Project dir for sandbox snapshots: {} (configured)",
3117 resolved = std::filesystem::current_path();
3118 logger->info(
"Project dir for sandbox snapshots: {} (cwd fallback)",
3121 cached_repo_dir_ = resolved;
3136void AgentEngine::set_project_dir(
const std::filesystem::path& project_dir) {
3137 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3138 project_dir_override_ = project_dir;
3139 cached_repo_dir_.reset();
3140 repo_dir_checked_ =
false;
3160 const std::string& reason,
3161 const std::string& delegation_id) {
3164 m.
content =
"[DELEGATION FAILED: resume_delegation] "
3165 + reason +
" (delegation_id=" + delegation_id +
")";
3166 ctx.
messages.push_back(std::move(m));
3182bool AgentEngine::fetch_resume_payload(
3184 const std::string&
id,
3185 nlohmann::json& parsed) {
3186 std::optional<std::string> error;
3188 if (storage_.load_delegation_with_messages ==
nullptr) {
3189 error =
"storage unavailable";
3190 }
else if (!storage_.load_delegation_with_messages(
3191 id.c_str(), raw, storage_.user_data)) {
3192 error =
"unknown delegation_id";
3194 parsed = nlohmann::json::parse(raw,
nullptr,
false);
3195 if (parsed.is_discarded() || !parsed.is_object()) {
3196 error =
"malformed storage payload";
3199 if (
error.has_value()) {
3200 logger->error(
"resume_delegation '{}': {}",
id, *error);
3221bool AgentEngine::resolve_latest_for_target(
3222 LoopContext& ctx, PendingDelegation& pending) {
3224 bool found = storage_.latest_delegation_for_target !=
nullptr
3225 && storage_.latest_delegation_for_target(
3226 pending.target.c_str(),
id, storage_.user_data)
3230 ctx,
"no prior delegation to this tier in storage — use "
3231 "entropic.delegate instead",
3235 logger->info(
"resume_delegation by target '{}' resolved to id '{}'",
3236 pending.target,
id);
3237 pending.resume_from_delegation_id = id;
3253bool AgentEngine::resolve_resume_delegation(
3255 PendingDelegation& pending,
3256 std::vector<Message>& out_history) {
3262 bool ok = !pending.resume_by_target
3263 || resolve_latest_for_target(ctx, pending);
3264 const auto&
id = pending.resume_from_delegation_id;
3265 if (ok) {
ok = fetch_resume_payload(ctx,
id, j); }
3268 target = j.value(
"target_tier", std::string{});
3269 if (target.empty()) {
3277 pending.target = target;
3278 if (j.contains(
"messages") && j[
"messages"].is_array()) {
3279 for (
const auto& mj : j[
"messages"]) {
3281 m.role = mj.value(
"role",
"");
3282 m.content = mj.value(
"content",
"");
3283 if (!m.role.empty()) {
3284 out_history.push_back(std::move(m));
3288 logger->info(
"resume_delegation '{}': loaded {} messages, target='{}'",
3289 id, out_history.size(), target);
3307DelegationResult AgentEngine::run_pending_delegation(
3309 const PendingDelegation& pending,
3310 std::vector<Message> resume_history) {
3311 std::optional<int> max_turns;
3312 if (pending.max_turns > 0) { max_turns = pending.max_turns; }
3313 DelegationManager mgr(run_child_loop_trampoline,
this,
3314 tier_res_, resolve_session_root(ctx.session_key),
3315 sandbox_for_session(ctx.session_key));
3316 auto swap = dir_swap_snapshot();
3317 mgr.set_dir_swap(swap.first, swap.second);
3318 if (storage_.create_delegation !=
nullptr) {
3319 mgr.set_storage(&storage_);
3321 auto cb_snap = delegation_callbacks_snapshot();
3322 mgr.set_delegation_callbacks(
3323 cb_snap.start, cb_snap.complete, cb_snap.user_data);
3324 if (resume_history.empty()) {
3325 return mgr.execute_delegation(
3326 ctx, pending.target, pending.task, max_turns, pending.context);
3328 return mgr.execute_resume_delegation(
3329 ctx, pending.target, pending.task,
3330 std::move(resume_history), max_turns, pending.context);
3350SandboxManager* AgentEngine::ensure_sandbox_manager(
3351 const std::filesystem::path& root) {
3355 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3356 auto it = sandbox_mgrs_.find(root);
3357 if (it != sandbox_mgrs_.end()) {
3362 auto [pos, inserted] = sandbox_mgrs_.emplace(
3363 std::piecewise_construct,
3364 std::forward_as_tuple(root),
3365 std::forward_as_tuple(root));
3367 return &pos->second;
3376void AgentEngine::set_session_root_interface(
3378 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3379 session_root_ = iface;
3391 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3392 swap_dir_fn_ = swap_fn;
3393 swap_dir_data_ = user_data;
3408std::pair<ScopedSandbox::SwapDirFn, void*>
3409AgentEngine::dir_swap_snapshot()
const {
3410 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3411 return {swap_dir_fn_, swap_dir_data_};
3422std::filesystem::path AgentEngine::resolve_session_root(
3423 const std::string& session_key) {
3426 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3427 iface = session_root_;
3430 return get_repo_dir();
3433 return root.empty() ? get_repo_dir() : root;
3444 const std::string& session_key) {
3445 if (!loop_config_.delegation_isolation) {
3448 return ensure_sandbox_manager(resolve_session_root(session_key));
3459std::vector<std::string> AgentEngine::isolation_unsafe_tools(
3460 const LoopContext& ctx,
const std::string& target_tier) {
3463 std::lock_guard<std::mutex> guard(sandbox_mutex_);
3464 iface = session_root_;
3466 if (!loop_config_.delegation_isolation
3470 auto info = tier_res_.resolve_tier
3471 ? tier_res_.resolve_tier(target_tier, tier_res_.user_data)
3491void AgentEngine::set_system_prompt(
const std::string& prompt) {
3492 std::lock_guard<std::mutex> guard(prompt_mutex_);
3493 system_prompt_ = prompt;
3506std::string AgentEngine::system_prompt_copy()
const {
3507 return session_system_prompt(active_session_key());
3517 session_logger_ = log;
3533 const auto key = active_session_key();
3534 std::lock_guard<std::mutex> guard(conversations_mutex_);
3535 return conversations_[key];
3547thread_local std::string t_active_session_key;
3548thread_local bool t_active_session_bound =
false;
3551thread_local std::string t_claimed_key;
3552thread_local bool t_holds_claim =
false;
3575std::string AgentEngine::active_session_key()
const {
3576 if (t_active_session_bound) {
return t_active_session_key; }
3577 std::lock_guard<std::mutex> guard(session_key_mutex_);
3578 return active_session_key_;
3596void AgentEngine::set_active_session(
const std::string& key) {
3598 std::lock_guard<std::mutex> guard(conversations_mutex_);
3599 conversations_[key];
3601 t_active_session_key = key;
3602 t_active_session_bound =
true;
3603 std::lock_guard<std::mutex> guard(session_key_mutex_);
3604 active_session_key_ = key;
3614bool AgentEngine::try_begin_turn(
const std::string& key) {
3616 std::lock_guard<std::mutex> guard(runs_mutex_);
3621 const bool blocked = active_runs_.count(key) > 0
3622 || (!concurrent_sessions_ && !active_runs_.empty());
3623 if (blocked) {
return false; }
3624 auto claimed = std::make_shared<RunState>();
3626 active_runs_[key] = claimed;
3627 running_flag_.store(
true);
3633 t_claimed_key = key;
3634 t_holds_claim =
true;
3635 t_active_session_key = key;
3636 t_active_session_bound =
true;
3646void AgentEngine::end_turn(
const std::string& key) {
3648 std::lock_guard<std::mutex> guard(runs_mutex_);
3649 active_runs_.erase(key);
3650 running_flag_.store(!active_runs_.empty());
3654 if (t_holds_claim && t_claimed_key == key) {
3655 t_holds_claim =
false;
3656 t_active_session_bound =
false;
3657 t_active_session_key.clear();
3666void AgentEngine::end_turn() {
3670 end_turn(t_holds_claim ? t_claimed_key : active_session_key());
3679std::size_t AgentEngine::active_run_count()
const {
3680 std::lock_guard<std::mutex> guard(runs_mutex_);
3681 return active_runs_.size();
3690void AgentEngine::set_concurrent_sessions(
bool enabled) {
3691 std::lock_guard<std::mutex> guard(runs_mutex_);
3692 concurrent_sessions_ =
enabled;
3693 logger->info(
"Per-session run concurrency {} (gh#158)",
3694 enabled ?
"ENABLED" :
"disabled");
3703bool AgentEngine::concurrent_sessions()
const {
3704 std::lock_guard<std::mutex> guard(runs_mutex_);
3705 return concurrent_sessions_;
3714std::shared_ptr<AgentEngine::RunState> AgentEngine::this_thread_run()
const {
3715 if (!t_holds_claim) {
return nullptr; }
3716 std::lock_guard<std::mutex> guard(runs_mutex_);
3717 auto it = active_runs_.find(t_claimed_key);
3718 return it == active_runs_.end() ? nullptr : it->second;
3728bool AgentEngine::interrupt_session(
const std::string& key) {
3729 std::shared_ptr<RunState> run;
3731 std::lock_guard<std::mutex> guard(runs_mutex_);
3732 auto it = active_runs_.find(key);
3733 if (it != active_runs_.end()) { run = it->second; }
3736 logger->info(
"interrupt_session('{}'): no run in flight", key);
3739 run->interrupt.store(
true, std::memory_order_release);
3740 logger->info(
"Session '{}' interrupted", key);
3751bool AgentEngine::session_interrupted(
const std::string& key)
const {
3752 std::lock_guard<std::mutex> guard(runs_mutex_);
3753 auto it = active_runs_.find(key);
3754 return it != active_runs_.end()
3755 && it->second->interrupt.load(std::memory_order_acquire);
3764std::atomic<bool>& AgentEngine::run_cancel_flag() {
3765 auto run = this_thread_run();
3766 return run ? run->interrupt : interrupt_flag_;
3782std::vector<Message> AgentEngine::run_turn(
const std::string& input) {
3795 const bool owns_turn = try_begin_turn();
3796 auto& convo = active_conversation();
3797 auto prompt = system_prompt_copy();
3798 if (convo.messages.empty() && !prompt.empty()) {
3800 sys.
role =
"system";
3801 sys.
content = std::move(prompt);
3802 convo.messages.push_back(std::move(sys));
3804 auto result = run_drain_loop(input,
"");
3805 if (owns_turn) { end_turn(); }
3826std::vector<Message> AgentEngine::run_turn_as(
const std::string& tier,
3827 const std::string& input) {
3830 const bool owns_turn = try_begin_turn();
3831 seed_system_prompt_for_tier(tier);
3832 auto result = run_drain_loop(input, tier);
3833 if (owns_turn) { end_turn(); }
3844void AgentEngine::set_workspace_prompts(
const std::string& workspace,
3846 logger->info(
"[workspace] '{}' prompts set: default {} chars, {} tier(s)",
3849 std::lock_guard<std::mutex> guard(prompt_mutex_);
3850 workspace_prompts_[workspace] = std::move(prompts);
3859void AgentEngine::clear_workspace_prompts(
const std::string& workspace) {
3860 logger->info(
"[workspace] '{}' prompts cleared — its sessions use the "
3861 "handle's", workspace);
3862 std::lock_guard<std::mutex> guard(prompt_mutex_);
3863 workspace_prompts_.erase(workspace);
3873void AgentEngine::bind_session_workspace(
const std::string& session,
3874 const std::string& workspace) {
3875 std::lock_guard<std::mutex> guard(prompt_mutex_);
3876 session_workspace_[session] = workspace;
3887std::optional<std::string> AgentEngine::workspace_prompt_for(
3888 const std::string& session,
const std::string& tier)
const {
3889 std::lock_guard<std::mutex> guard(prompt_mutex_);
3890 std::optional<std::string> out;
3891 auto bound = session_workspace_.find(session);
3892 auto ws = bound == session_workspace_.end()
3893 ? workspace_prompts_.end() : workspace_prompts_.find(bound->second);
3894 if (ws != workspace_prompts_.end() && tier.empty()) {
3895 out = ws->second.default_prompt;
3896 }
else if (ws != workspace_prompts_.end()) {
3897 auto t = ws->second.tier_prompts.find(tier);
3898 if (t != ws->second.tier_prompts.end()) { out = t->second; }
3911std::string AgentEngine::session_system_prompt(
const std::string& session)
const {
3912 auto ws = workspace_prompt_for(session,
"");
3913 std::lock_guard<std::mutex> guard(prompt_mutex_);
3914 return ws.value_or(system_prompt_);
3930void AgentEngine::seed_system_prompt_for_tier(
const std::string& tier) {
3931 auto& convo = active_conversation();
3932 if (!convo.messages.empty()) {
return; }
3933 auto sp = workspace_prompt_for(active_session_key(), tier);
3934 auto it = tier_info_.find(tier);
3935 if (!sp && it != tier_info_.end() && !it->second.system_prompt.empty()) {
3936 sp = it->second.system_prompt;
3938 if (!sp) { sp = system_prompt_copy(); }
3939 if (sp->empty()) {
return; }
3941 sys.
role =
"system";
3943 convo.messages.push_back(std::move(sys));
3954std::vector<Message> AgentEngine::run_drain_loop(
3955 std::string pending,
const std::string& tier_override) {
3956 std::vector<Message> result;
3961 auto& convo = active_conversation();
3962 convo.messages.push_back(std::move(usr));
3964 size_t sent_len = convo.messages.size();
3966 result = run(convo.messages, tier_override);
3967 for (
size_t i = sent_len; i < result.size(); ++i) {
3968 convo.messages.push_back(result[i]);
3970 auto next = pop_queued_user_message();
3971 if (!next.has_value()) {
break; }
3972 fire_queue_consumed(*next, user_message_queue_depth());
3973 pending = std::move(*next);
4000void AgentEngine::seed_system_prompt(
4001 const std::vector<Message>& new_messages) {
4002 bool caller_has_system =
false;
4003 for (
const auto& m : new_messages) {
4004 if (m.role ==
"system") { caller_has_system =
true;
break; }
4006 auto& convo = active_conversation();
4007 auto prompt = system_prompt_copy();
4008 if (convo.messages.empty() && !prompt.empty()
4009 && !caller_has_system) {
4011 sys.
role =
"system";
4012 sys.
content = std::move(prompt);
4013 convo.messages.push_back(std::move(sys));
4024bool AgentEngine::prepare_next_turn(std::vector<Message>& pending) {
4025 auto next = pop_queued_user_message();
4026 if (!next.has_value()) {
return false; }
4027 fire_queue_consumed(*next, user_message_queue_depth());
4030 usr.
content = std::move(*next);
4032 pending.push_back(std::move(usr));
4043std::vector<Message> AgentEngine::run_turn(std::vector<Message> new_messages) {
4049 const bool owns_turn = try_begin_turn();
4050 seed_system_prompt(new_messages);
4051 auto& convo = active_conversation();
4052 std::vector<Message> pending = std::move(new_messages);
4053 std::vector<Message> result;
4055 for (
auto& m : pending) {
4056 convo.messages.push_back(std::move(m));
4058 size_t sent_len = convo.messages.size();
4059 result = run(convo.messages);
4060 for (
size_t i = sent_len; i < result.size(); ++i) {
4061 convo.messages.push_back(result[i]);
4063 if (!prepare_next_turn(pending)) {
break; }
4065 if (owns_turn) { end_turn(); }
4072struct StreamChunkCtx {
4095void on_stream_chunk(
const char* t,
size_t l,
void* ud) {
4096 auto* c =
static_cast<StreamChunkCtx*
>(ud);
4097 if (c->cancel !=
nullptr && *c->cancel != 0) {
4101 c->filter->on_token(t, l);
4119int AgentEngine::run_streaming(
4120 const std::string& input,
4124 const std::string& tier)
4126 if (session_logger_) {
4127 session_logger_->log_user_input(input);
4131 if (session_logger_ && session_logger_->is_open()) {
4133 SessionLogger::raw_token_callback,
4137 StreamChunkCtx sctx{&
filter, cancel_flag,
this};
4145 auto result = tier.empty() ? run_turn(input) : run_turn_as(tier, input);
4148 if (session_logger_) {
4149 session_logger_->end_turn();
4151 if (cancel_flag && *cancel_flag) {
return 1; }
4163 const std::vector<Message>& messages) {
4165 for (
const auto& m : messages) {
4166 if (m.role !=
"user" || m.content.empty()) {
continue; }
4167 if (!echo.empty()) { echo +=
'\n'; }
4189int AgentEngine::run_streaming(
4190 std::vector<Message> new_messages,
4195 if (session_logger_) {
4200 if (session_logger_ && session_logger_->is_open()) {
4202 SessionLogger::raw_token_callback,
4206 StreamChunkCtx sctx{&
filter, cancel_flag,
this};
4213 auto result = run_turn(std::move(new_messages));
4216 if (session_logger_) {
4217 session_logger_->end_turn();
4219 if (cancel_flag && *cancel_flag) {
return 1; }
4228void AgentEngine::clear_conversation() {
4229 active_conversation().clear();
4230 logger->info(
"conversation cleared");
4248size_t AgentEngine::message_count()
const {
4249 const auto key = active_session_key();
4250 std::lock_guard<std::mutex> guard(conversations_mutex_);
4251 auto it = conversations_.find(key);
4252 return it == conversations_.end() ? 0 : it->second.count();
4264std::vector<Message> AgentEngine::messages_for(
const std::string& key)
const {
4265 std::lock_guard<std::mutex> guard(conversations_mutex_);
4266 auto it = conversations_.find(key);
4267 return it == conversations_.end() ? std::vector<Message>{}
4268 : it->second.messages;
4278std::size_t AgentEngine::message_count_for(
const std::string& key)
const {
4279 std::lock_guard<std::mutex> guard(conversations_mutex_);
4280 auto it = conversations_.find(key);
4281 return it == conversations_.end() ? 0 : it->second.count();
4290void AgentEngine::clear_conversation_for(
const std::string& key) {
4291 std::lock_guard<std::mutex> guard(conversations_mutex_);
4292 auto it = conversations_.find(key);
4293 if (it != conversations_.end()) { it->second.clear(); }
4303bool AgentEngine::drop_session(
const std::string& key) {
4304 std::lock_guard<std::mutex> guard(conversations_mutex_);
4306 conversations_[key].clear();
4309 return conversations_.erase(key) > 0;
4318std::vector<std::string> AgentEngine::session_keys()
const {
4319 std::lock_guard<std::mutex> guard(conversations_mutex_);
4320 std::vector<std::string> keys;
4321 keys.reserve(conversations_.size());
4322 for (
const auto& [k, v] : conversations_) { keys.push_back(k); }
4341bool AgentEngine::set_session_messages(
const std::string& key,
4342 std::vector<Message> messages) {
4344 std::lock_guard<std::mutex> guard(runs_mutex_);
4345 if (active_runs_.count(key) > 0) {
4346 logger->warn(
"session_context_set('{}') refused: a run on that "
4347 "session is in flight", key);
4351 std::lock_guard<std::mutex> guard(conversations_mutex_);
4352 auto& convo = conversations_[key];
4353 convo.messages = std::move(messages);
4354 logger->info(
"Session '{}' conversation restored: {} messages", key,
4355 convo.messages.size());
4365const std::vector<Message>& AgentEngine::get_messages()
const {
4366 static const std::vector<Message> kNoMessages;
4367 const auto key = active_session_key();
4376 std::lock_guard<std::mutex> guard(conversations_mutex_);
4377 auto it = conversations_.find(key);
4378 return it == conversations_.end() ? kNoMessages : it->second.messages;
4388bool AgentEngine::queue_user_message(
const std::string& message) {
4389 std::lock_guard lock(queue_mutex_);
4390 int cap = loop_config_.message_queue_capacity;
4391 if (cap < 0) { cap = 0; }
4392 if (user_message_queue_.size()
4393 >=
static_cast<size_t>(cap)) {
4396 user_message_queue_.push_back(message);
4397 logger->info(
"queued mid-gen user message: depth={}",
4398 user_message_queue_.size());
4407size_t AgentEngine::user_message_queue_depth()
const {
4408 std::lock_guard lock(queue_mutex_);
4409 return user_message_queue_.size();
4417void AgentEngine::clear_user_message_queue() {
4418 std::lock_guard lock(queue_mutex_);
4419 size_t dropped = user_message_queue_.size();
4420 user_message_queue_.clear();
4422 logger->info(
"cleared mid-gen queue: dropped={}", dropped);
4431void AgentEngine::set_message_queue_capacity(
int cap) {
4432 std::lock_guard lock(queue_mutex_);
4433 loop_config_.message_queue_capacity = cap < 0 ? 0 : cap;
4441std::optional<std::string>
4442AgentEngine::pop_queued_user_message() {
4443 std::lock_guard lock(queue_mutex_);
4444 if (user_message_queue_.empty()) {
4445 return std::nullopt;
4447 std::string front = std::move(user_message_queue_.front());
4448 user_message_queue_.pop_front();
4457void AgentEngine::fire_queue_consumed(
const std::string& consumed,
4459 if (queue_observer_ !=
nullptr) {
4461 consumed.c_str(),
remaining, queue_observer_data_);
4470void AgentEngine::set_queue_observer(
4471 void (*observer)(
const char*,
size_t,
void*),
4473 queue_observer_ = observer;
4474 queue_observer_data_ = user_data;
4488void AgentEngine::set_state_observer(
4489 void (*observer)(
int,
void*),
4491 state_observer_ = observer;
4492 state_observer_data_ = user_data;
4493 response_generator_.set_state_observer(observer, user_data);
4508 const std::vector<const Directive*>& dirs,
4511 ->directive_processor().process(ctx, dirs);
4526void AgentEngine::set_tier_info(
4527 const std::string& name,
4530 tier_info_[name] = info;
4540bool AgentEngine::has_tier(
const std::string& name)
const {
4541 return tier_info_.count(name) > 0;
4552std::vector<std::string> AgentEngine::get_tier_allowed_tools(
4553 const std::string& name)
const {
4554 auto it = tier_info_.find(name);
4555 if (it == tier_info_.end()) {
return {}; }
4556 return it->second.allowed_tools;
4566const std::string& AgentEngine::tier_system_prompt(
4567 const std::string& name)
const {
4568 static const std::string kEmpty;
4569 auto it = tier_info_.find(name);
4570 return it == tier_info_.end() ? kEmpty : it->second.system_prompt;
4579void AgentEngine::set_relay_single_delegate(
const std::string& name) {
4580 relay_single_delegate_tiers_.insert(name);
4590void AgentEngine::set_handoff_rules(
4591 const std::unordered_map<std::string,
4592 std::vector<std::string>>& rules)
4594 handoff_rules_ = rules;
4595 wire_internal_tier_resolution();
4611 const std::string& name,
void* ud) {
4613 auto it = self->tier_info_.find(name);
4615 if (it == self->tier_info_.end()) {
4619 info.system_prompt = self->workspace_prompt_for(
4620 self->active_session_key(), name)
4621 .value_or(info.system_prompt);
4634bool AgentEngine::tri_tier_exists(
const std::string& name,
void* ud) {
4635 auto* self =
static_cast<AgentEngine*
>(ud);
4636 return self->tier_info_.count(name) > 0;
4648std::vector<std::string> AgentEngine::tri_get_handoff_targets(
4649 const std::string& name,
void* ud) {
4650 auto* self =
static_cast<AgentEngine*
>(ud);
4651 auto it = self->handoff_rules_.find(name);
4652 std::vector<std::string> result;
4653 if (it != self->handoff_rules_.end()) { result = it->second; }
4664static std::string
join_csv(
const std::vector<std::string>& v) {
4666 for (
const auto& s : v) {
4667 if (!out.empty()) { out +=
','; }
4697std::string AgentEngine::tri_get_tier_param(
const std::string& name,
4698 const std::string& param,
void* ud) {
4699 auto* self =
static_cast<AgentEngine*
>(ud);
4700 auto it = self->tier_info_.find(name);
4701 if (it == self->tier_info_.end()) {
return ""; }
4702 const auto& info = it->second;
4704 if (param ==
"explicit_completion") {
4705 result = info.explicit_completion ?
"true" :
"false";
4707 if (param ==
"max_iterations" && info.max_iterations_override >= 0) {
4708 result = std::to_string(info.max_iterations_override);
4710 if (param ==
"max_tool_calls_per_turn"
4711 && info.max_tool_calls_per_turn_override >= 0) {
4712 result = std::to_string(info.max_tool_calls_per_turn_override);
4714 if (param ==
"max_consecutive_empty_turns"
4715 && info.max_consecutive_empty_turns_override >= 0) {
4716 result = std::to_string(info.max_consecutive_empty_turns_override);
4718 if (param ==
"allowed_tools" && !info.allowed_tools.empty()) {
4719 result =
join_csv(info.allowed_tools);
4729void AgentEngine::wire_internal_tier_resolution() {
4730 TierResolutionInterface tri;
4731 tri.resolve_tier = &AgentEngine::tri_resolve_tier;
4732 tri.tier_exists = &AgentEngine::tri_tier_exists;
4733 tri.get_handoff_targets = &AgentEngine::tri_get_handoff_targets;
4734 tri.get_tier_param = &AgentEngine::tri_get_tier_param;
4735 tri.user_data =
this;
4736 set_tier_resolution(tri);
Core agent execution engine.
void run_loop(LoopContext &ctx, bool inherit_interrupt=false)
Run the engine loop on a pre-built context.
AgentEngine(const InferenceInterface &inference, const LoopConfig &loop_config, const CompactionConfig &compaction_config)
Construct an agent engine.
RAII publication of one run's cancel token to its own thread.
RAII: publish a streaming run's sink to its own thread (gh#208).
Create, finalize, and discard per-delegation filesystem sandboxes.
void(*)(const std::string &session_key, const std::filesystem::path &path, bool entering, void *user_data) SwapDirFn
Callback type for directory swapping.
Manages session_model.log for raw streaming content.
Streaming filter that removes <think> blocks from output.
Testable free function extracted from fire_delegate_complete_hook.
std::string build_delegate_complete_json(const std::string &target, bool success, const std::string &summary)
Build and dump the ON_DELEGATE_COMPLETE hook JSON payload.
DelegationManager — child loop creation and execution.
AgentEngine * engine
Fallback interrupt target.
int * cancel
Consumer's cancel flag, nullable.
StreamThinkFilter * filter
Consumer-facing filter.
Core agent execution engine.
ent_decision_t
Consumer decision returned from delegation callbacks.
entropic_directive_type_t
Directive types emitted by MCP tool results.
@ ENTROPIC_DIRECTIVE_TIER_CHANGE
Switch active tier.
@ ENTROPIC_DIRECTIVE_STOP_PROCESSING
Halt directive processing.
@ ENTROPIC_DIRECTIVE_PRUNE_MESSAGES
Prune old tool results.
@ ENTROPIC_DIRECTIVE_INJECT_CONTEXT
Inject message into context.
@ ENTROPIC_DIRECTIVE_COMPLETE
Mark task complete.
@ ENTROPIC_DIRECTIVE_NOTIFY_PRESENTER
Generic UI notification passthrough.
@ ENTROPIC_DIRECTIVE_CLEAR_SELF_TODOS
Clear self-directed todos (engine no-op)
@ ENTROPIC_DIRECTIVE_PHASE_CHANGE
Switch active inference phase.
@ ENTROPIC_DIRECTIVE_PIPELINE
Multi-stage sequential execution.
@ ENTROPIC_DIRECTIVE_CONTEXT_ANCHOR
Replace context anchor.
@ ENTROPIC_DIRECTIVE_DELEGATE
Route to another identity.
entropic_hook_point_t
Hook points in the engine lifecycle.
@ ENTROPIC_HOOK_ON_LOOP_START
20: Agentic loop entry
@ ENTROPIC_HOOK_ON_DELEGATE
8: Delegation to child tier started
@ ENTROPIC_HOOK_ON_LOOP_END
21: Agentic loop exit
@ ENTROPIC_HOOK_ON_STATE_CHANGE
6: Engine state machine transition
@ ENTROPIC_HOOK_PRE_GENERATE
0: Before inference generate call
@ ENTROPIC_HOOK_ON_CONTEXT_ASSEMBLE
10: Context window assembled
@ ENTROPIC_HOOK_ON_LOOP_ITERATION
5: Each agentic loop iteration
@ ENTROPIC_HOOK_ON_ERROR
7: Async error occurred
@ ENTROPIC_HOOK_POST_GENERATE
1: After inference generate returns
@ ENTROPIC_HOOK_ON_DELEGATE_COMPLETE
9: Child delegation completed
@ ENTROPIC_HOOK_ON_COMPLETE
entropic.complete MCP tool called — pre-hook, can cancel.
std::size_t remaining
Unexamined count.
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).
@ enabled
Named in enabled_mcpjson_servers, or enable-all is set.
static std::string build_coverage_gap_message(const std::string &tier, const DelegationResult &result)
Build the [COVERAGE GAP] message body that goes back to lead when a relay-tier child returns coverage...
static void fire_context_assemble_hook(const HookInterface &hooks, const LoopContext &ctx)
Build + fire the ON_CONTEXT_ASSEMBLE info hook.
static std::string extract_system_prompt(const std::vector< Message > &messages)
Extract the first system message content from messages.
static double now_seconds()
Get current time as seconds since epoch.
static ToolCall build_tool_call_from_json(const nlohmann::json &obj, const std::string &tc_str)
Parse tool calls from raw model output.
ToolResultKind
Categorical outcome of a single tool invocation.
@ ok
Tool dispatched, returned non-empty content.
@ error
Tool server returned an error payload.
@ delegation_failed
entropic.delegate child failed (terminal_reason or budget). (#7, v2.1.4)
static std::string build_tool_results_json(const std::vector< Message > &messages)
Build a JSON array of tool results from context messages.
@ rejected
The approver said no.
@ optional
optional: every escape asks the host's path approver
static std::vector< ToolCall > decode_tool_calls_json(const std::string &tc_str)
Decode a JSON tool-calls array string into ToolCall vector.
static std::string json_escape_engine(const std::string &s)
JSON-escape a string (no surrounding quotes).
static std::string build_tool_evidence(const std::vector< Message > &messages, size_t max_results, size_t chars_per_result)
Build un-pruned tool-result evidence for the validator.
@ count
Sentinel — MUST remain last.
static void remove_anchor_messages(LoopContext &ctx, const std::string &key)
Remove messages with a specific anchor key.
const char * agent_state_name(AgentState state)
Get the string name for an AgentState value.
static void fold_delegation_summary(LoopContext &ctx, const std::string &summary)
gh#119 (v2.9.17): fold child summary into the lead's empty delegate turn.
static void push_delegation_repeat_blocked(LoopContext &ctx, const std::string &target, int n)
Append a "stop retrying same target" reject message (gh#64).
static void fire_loop_start_hook(const HookInterface &hooks, const LoopContext &ctx)
Build + fire the ON_LOOP_START info hook.
static std::string build_tool_manifest(const std::vector< Message > &messages)
Build tool call manifest from conversation messages.
ENTROPIC_EXPORT bool current_run_cancelled()
Whether THIS thread's run has been cancelled.
static std::string format_tool_evidence_entry(const Message &msg, size_t chars_per_result)
Format one tool-result message for the evidence block.
static const std::array< const char *, 256 > & json_escape_table()
256-entry table mapping bytes to JSON escape sequences.
static std::string enrich_manifest_with_history(const std::string &base, const ToolExecutionInterface &tool_exec)
Append executor history (if any) to the tool manifest.
ENTROPIC_EXPORT bool cancel_current_run()
Cancel the run executing on this thread (gh#158).
static void push_delegation_result(LoopContext &ctx, const std::string &target, const DelegationResult &result)
Append a delegation result message to the loop context.
static std::string annotate_hard_stop(const std::string &carried, const std::string &reason, const std::string &stop_noun)
Build an engine-forced stop's final assistant content.
static std::string join_csv(const std::vector< std::string > &v)
Join a vector of strings with comma separators.
static void run_child_loop_trampoline(LoopContext &ctx, void *user_data)
Trampoline for DelegationManager to call engine loop.
const char * result_kind_to_string(ToolResultKind kind)
Serialize a ToolResultKind to its wire-stable string form.
static void fire_hook_info(const HookInterface &hooks, entropic_hook_point_t point, const char *json)
Fire an informational hook if the interface is wired.
void(*)(const char *, size_t, void *) TokenCallback
Token callback type matching the C API signature.
static void push_delegation_rejected(LoopContext &ctx)
Append a delegation rejection message to the loop context.
static std::string concat_user_echo(const std::vector< Message > &messages)
Concatenate user-role message text for session-log echo.
AgentState
C++ enum class for agent execution states.
static std::string last_substantive_assistant(const LoopContext &ctx)
The last assistant message with something in it (gh#169).
static void fire_loop_iteration_hook(const HookInterface &hooks, const LoopContext &ctx)
Build + fire the ON_LOOP_ITERATION info hook.
static void push_resume_failure(LoopContext &ctx, const std::string &reason, const std::string &delegation_id)
Lazy session-scoped SandboxManager accessor (gh#33, v2.1.6).
static void push_delegation_cycle_rejected(LoopContext &ctx, const std::string &target)
Append a structured cycle-rejection message to the loop context so the model can recover.
static void push_delegation_isolation_unsafe(LoopContext &ctx, const std::string &target, const std::vector< std::string > &tools)
Append the "isolation cannot be honoured" reject message (gh#160).
static void fire_loop_end_hook(const HookInterface &hooks, const LoopContext &ctx)
Build + fire the ON_LOOP_END info hook.
gh#158: the cancel token of the run executing on THIS thread.
Filesystem-based sandbox isolation for delegations.
Request describing a delegation that is about to run.
Result of a finalized delegation, delivered to the consumer.
Grouped consumer-registered delegation callbacks (gh#29).
Resolved tier information for building child delegation contexts.
Auto-compaction configuration.
Engine-level hooks called during context management.
One caller-scoped conversation and its KV bookkeeping.
Result returned from a child delegation loop.
bool success
Whether child reached COMPLETE via real entropic.complete.
std::string summary
Final summary from child.
std::vector< std::string > suggested_files
Issue #10 (v2.1.4): file paths the lead should inspect to fill the coverage gap.
std::string gap_description
Issue #10 (v2.1.4): concrete description of what the child's answer DOES NOT cover.
Aggregate result of processing a batch of directives.
Callback function pointer types for engine events.
Configuration for the agentic loop.
Mutable state carried through the agentic loop.
std::string session_key
Caller-scoped session this turn belongs to (gh#144, v2.12.0).
std::vector< std::string > delegation_ancestor_tiers
Tier stack from root to this loop (P1-9, 2.0.6-rc16)
std::string last_failed_delegation_target
gh#64: target tier of the most recent FAILED delegation (DelegationResult.success == false).
LoopMetrics metrics
Timing and counts.
int effective_max_iterations
Per-identity override (-1 = LoopConfig, P3-18)
int consecutive_duplicate_attempts
Stuck-model detector.
int consecutive_failed_delegations
gh#64: count of consecutive failed delegations against last_failed_delegation_target.
std::string conversation_id
Conversation ID for storage (v1.8.8)
bool has_pending_tool_results
Tool results awaiting presentation.
std::unordered_map< std::string, std::string > metadata
Runtime metadata.
int effective_max_tool_calls_per_turn
Per-identity override (-1 = LoopConfig, P3-18)
std::vector< Message > messages
Conversation history.
std::string locked_tier
Tier locked for this loop ("" = none)
Metrics collected during loop execution.
double start_time
Loop start (seconds since epoch)
int duration_ms() const
Get loop duration in milliseconds.
double end_time
Loop end (seconds since epoch)
int iterations
Total iterations completed.
A message in a conversation.
std::unordered_map< std::string, std::string > metadata
Arbitrary metadata.
std::string content
Message text content (always populated)
std::string role
Message role.
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.
Storage interface for conversation persistence.
Tier resolution callbacks for delegation and auto-chain.
std::string(* get_tier_param)(const std::string &tier_name, const std::string ¶m_name, void *user_data)
Get a string parameter from tier identity frontmatter.
One workspace's assembled system prompts (gh#179, v2.14.0).
std::unordered_map< std::string, std::string > tier_prompts
Tier → system prompt.
std::string default_prompt
Untiered first turn.
UTF-8 validation + replacement at every system boundary where bytes change ownership.