14#include <nlohmann/json.hpp>
38 if (hooks.fire_info !=
nullptr) {
39 hooks.fire_info(hooks.registry, point, json);
52 std::string json =
"{\"message_count\":"
53 + std::to_string(ctx.
messages.size())
54 +
",\"delegation_depth\":"
68 std::string json =
"{\"final_state\":\""
70 +
"\",\"iterations\":"
84 std::string json =
"{\"iteration\":"
87 +
"\",\"consecutive_errors\":"
101 std::string json =
"{\"message_count\":"
102 + std::to_string(ctx.
messages.size()) +
"}";
109 const std::string& key);
118 auto now = std::chrono::steady_clock::now();
119 auto dur = now.time_since_epoch();
120 auto ms = std::chrono::duration_cast<
121 std::chrono::milliseconds>(dur);
122 return static_cast<double>(ms.count()) / 1000.0;
134 const InferenceInterface& inference,
137 : inference_(inference),
138 loop_config_(loop_config),
139 token_counter_(loop_config.context_length),
140 compaction_manager_(compaction_config, token_counter_),
142 compaction_manager_, callbacks_,
144 [this](
LoopContext& ctx) { reinject_context_anchors(ctx); }
147 inference, loop_config, callbacks_,
148 GenerationEvents{&interrupt_flag_, &pause_flag_}) {
149 register_directive_handlers();
159 callbacks_ = callbacks;
168void AgentEngine::set_tool_executor(
170 tool_exec_ = tool_exec;
179void AgentEngine::set_tier_resolution(
181 tier_res_ = tier_res;
192 compaction_manager_.set_storage(&storage_);
201void AgentEngine::set_hooks(
const HookInterface& hooks) {
203 response_generator_.set_hooks(hooks);
204 context_manager_.set_hooks(hooks);
205 directive_processor_.set_hooks(hooks);
219void AgentEngine::set_stream_observer(
221 response_generator_.set_stream_observer(observer, user_data);
232void AgentEngine::set_delegation_callbacks(
236 std::lock_guard<std::mutex> lock(delegation_cb_mutex_);
237 delegation_cb_.start = on_start;
238 delegation_cb_.complete = on_complete;
239 delegation_cb_.user_data = user_data;
249AgentEngine::delegation_callbacks_snapshot()
const {
250 std::lock_guard<std::mutex> lock(delegation_cb_mutex_);
251 return delegation_cb_;
261void AgentEngine::set_validation_provider(
262 char* (*provider)(
void*),
void* user_data) {
263 validation_provider_ = provider;
264 validation_provider_data_ = user_data;
290void AgentEngine::apply_identity_overrides(
LoopContext& ctx) {
291 if (ctx.
locked_tier.empty() || tier_res_.get_tier_param ==
nullptr) {
295 ctx.
locked_tier,
"max_iterations", tier_res_.user_data);
298 logger->info(
"[override] tier={} max_iterations={}",
301 auto mt = tier_res_.get_tier_param(
302 ctx.
locked_tier,
"max_tool_calls_per_turn", tier_res_.user_data);
305 logger->info(
"[override] tier={} max_tool_calls_per_turn={}",
317int AgentEngine::resolve_max_iterations(
const LoopContext& ctx)
const {
318 return ctx.effective_max_iterations >= 0
319 ? ctx.effective_max_iterations
320 : loop_config_.max_iterations;
330int AgentEngine::resolve_max_tool_calls(
const LoopContext& ctx)
const {
331 return ctx.effective_max_tool_calls_per_turn >= 0
332 ? ctx.effective_max_tool_calls_per_turn
333 : loop_config_.max_tool_calls_per_turn;
343void AgentEngine::run_loop(
LoopContext& ctx,
bool inherit_interrupt) {
348 if (!inherit_interrupt) {
350 pause_flag_.store(
false);
352 apply_identity_overrides(ctx);
353 reinject_context_anchors(ctx);
357 set_state(ctx, AgentState::PLANNING);
361 auto& tm = per_tier_metrics_[
379std::vector<Message> AgentEngine::run(std::vector<Message> messages,
380 const std::string& tier_override) {
384 last_activity_epoch_s_.store(
385 std::chrono::duration_cast<std::chrono::seconds>(
386 std::chrono::system_clock::now().time_since_epoch()).count());
393 init_session_conversation(ctx);
396 pause_flag_.store(
false);
398 reinject_context_anchors(ctx);
399 set_state(ctx, AgentState::PLANNING);
404 accumulate_run_metrics(ctx);
405 logger->info(
"Loop complete: {} iterations, {}ms",
416void AgentEngine::init_session_conversation(
LoopContext& ctx) {
424 if (storage_.create_conversation ==
nullptr) {
return; }
426 if (storage_.create_conversation(
"session", conv_id, storage_.user_data)
427 && !conv_id.empty()) {
430 logger->warn(
"Storage create_conversation failed at run() init; "
431 "delegations will not persist this session (gh#48)");
441void AgentEngine::accumulate_run_metrics(LoopContext& ctx) {
442 last_metrics_ = ctx.metrics;
444 auto& tm = per_tier_metrics_[
445 ctx.locked_tier.empty() ?
"lead" : ctx.locked_tier];
446 tm.iterations += ctx.metrics.iterations;
447 tm.tool_calls += ctx.metrics.tool_calls;
448 tm.tokens_used += ctx.metrics.tokens_used;
449 tm.errors += ctx.metrics.errors;
450 tm.end_time += (ctx.metrics.end_time - ctx.metrics.start_time);
459void AgentEngine::loop(LoopContext& ctx) {
462 while (!should_stop(ctx)) {
463 ctx.metrics.iterations++;
465 if (interrupt_flag_.load()) {
466 set_state(ctx, AgentState::INTERRUPTED);
470 execute_iteration(ctx);
472 if (ctx.state == AgentState::ERROR) {
477 if (ctx.metrics.iterations >= resolve_max_iterations(ctx)
478 && !is_terminal_state(ctx)) {
484 logger->warn(
"Loop ended due to max iterations ({}/{}) — "
485 "forcing synthetic entropic.complete",
486 ctx.metrics.iterations, resolve_max_iterations(ctx));
488 forced.role =
"assistant";
489 forced.content =
"[iteration cap reached after "
490 + std::to_string(ctx.metrics.iterations)
491 +
" iterations — returning current state]";
492 ctx.messages.push_back(std::move(forced));
493 ctx.metadata[
"terminal_reason"] =
"budget_exhausted";
494 set_state(ctx, AgentState::COMPLETE);
506void AgentEngine::execute_iteration(LoopContext& ctx) {
507 logger->info(
"[LOOP] iter {}/{} state={} msgs={}",
508 ctx.metrics.iterations,
509 resolve_max_iterations(ctx),
511 ctx.messages.size());
515 context_manager_.refresh_context_limit(ctx, 0);
516 context_manager_.prune_old_tool_results(ctx);
517 context_manager_.check_compaction(ctx);
521 set_state(ctx, AgentState::EXECUTING);
525 set_state(ctx, AgentState::COMPLETE);
529 auto result = response_generator_.generate_response(ctx);
530 dispatch_post_generate(ctx, result);
531 process_generation_result(ctx, result);
541void AgentEngine::process_generation_result(LoopContext& ctx,
542 GenerateResult& result) {
543 auto [cleaned, tool_calls] = parse_tool_calls(result.content);
544 logger->info(
"[ITER] finish={}, {} tool call(s), {} chars",
545 result.finish_reason, tool_calls.size(),
547 Message assistant_msg{
"assistant", cleaned};
548 ctx.messages.push_back(std::move(assistant_msg));
550 bool made_tool_call =
551 !tool_calls.empty() && tool_exec_.process_tool_calls !=
nullptr;
555 bool made_progress =
false;
556 if (made_tool_call) {
557 made_progress = process_tool_results(ctx, tool_calls);
559 evaluate_no_tool_decision(ctx, cleaned, result.finish_reason);
564 charge_thinking_budget(ctx, result.content.size(), made_progress);
566 dispatch_pending_or_halt(ctx);
574void AgentEngine::charge_thinking_budget(
575 LoopContext& ctx,
size_t content_len,
bool made_tool_call) {
576 if (loop_config_.budget_mode == BudgetMode::off) {
return; }
580 if (made_tool_call) {
581 ctx.budget_tokens_since_tool = 0;
582 ctx.budget_window_start_s = 0.0;
583 ctx.budget_completion_nudged =
false;
587 int units = budget_units_consumed(ctx, content_len);
588 if (units < loop_config_.budget_limit) {
return; }
590 if (!ctx.budget_completion_nudged) {
591 nudge_budget_completion(ctx);
593 hard_cut_budget(ctx);
602int AgentEngine::budget_units_consumed(LoopContext& ctx,
size_t content_len) {
603 if (loop_config_.budget_mode == BudgetMode::tokens) {
608 constexpr size_t kCharsPerToken = 4;
609 ctx.budget_tokens_since_tool +=
610 static_cast<int>(content_len / kCharsPerToken);
611 return ctx.budget_tokens_since_tool;
614 if (ctx.budget_window_start_s == 0.0) {
617 return static_cast<int>(
now_seconds() - ctx.budget_window_start_s);
625void AgentEngine::nudge_budget_completion(LoopContext& ctx) {
626 ctx.budget_completion_nudged =
true;
627 ctx.budget_tokens_since_tool = 0;
628 ctx.budget_window_start_s =
629 (loop_config_.budget_mode == BudgetMode::wall_clock)
631 logger->warn(
"[BUDGET] thinking budget reached ({} {}); nudging completion",
632 loop_config_.budget_limit,
633 loop_config_.budget_mode == BudgetMode::tokens
634 ?
"tokens" :
"seconds");
635 Message nudge{
"user",
636 "[engine] thinking budget reached — emit entropic.complete now "
637 "with your best current answer. Further deliberation without a "
638 "tool call will be cut off."};
639 ctx.messages.push_back(std::move(nudge));
647void AgentEngine::hard_cut_budget(LoopContext& ctx) {
648 logger->warn(
"[BUDGET] thinking budget exhausted after nudge — "
649 "hard-cutting turn");
650 Message cut{
"assistant",
651 "[thinking budget exhausted — the turn was hard-cut after the "
652 "completion nudge went unheeded; no tool call was emitted]"};
653 ctx.messages.push_back(std::move(cut));
654 ctx.metadata[
"terminal_reason"] =
"budget_exhausted_thinking";
655 set_state(ctx, AgentState::COMPLETE);
675void AgentEngine::dispatch_pending_or_halt(LoopContext& ctx) {
676 if (interrupt_flag_.load() && !is_terminal_state(ctx)) {
677 logger->info(
"[ITER] interrupt observed during tool processing — "
678 "halting before pending dispatch");
679 set_state(ctx, AgentState::INTERRUPTED);
683 if (is_terminal_state(ctx)) {
684 if (ctx.pending_delegation.has_value()
685 || ctx.pending_pipeline.has_value()) {
686 logger->info(
"[ITER] terminal state reached during batch "
687 "tool processing — dropping pending {}",
688 ctx.pending_delegation.has_value()
689 ?
"delegation" :
"pipeline");
694 if (ctx.pending_delegation.has_value()) {
695 ctx.metrics.iterations--;
696 execute_pending_delegation(ctx);
697 }
else if (ctx.pending_pipeline.has_value()) {
698 ctx.metrics.iterations--;
699 execute_pending_pipeline(ctx);
711void AgentEngine::evaluate_no_tool_decision(
713 const std::string& content,
714 const std::string& finish_reason) {
715 if (handle_terminal_finish_reasons(ctx, finish_reason)) {
return; }
716 if (try_auto_chain(ctx, finish_reason, content)) {
717 logger->info(
"[DECISION] auto-chain triggered");
720 if (record_explicit_completion_failure(ctx, finish_reason)) {
return; }
721 if (response_generator_.is_response_complete(content,
"[]")) {
722 logger->info(
"[DECISION] response complete");
723 set_state(ctx, AgentState::COMPLETE);
740bool AgentEngine::handle_terminal_finish_reasons(
741 LoopContext& ctx,
const std::string& finish_reason) {
742 if (finish_reason ==
"interrupted") {
743 logger->info(
"[DECISION] interrupted");
744 set_state(ctx, AgentState::INTERRUPTED);
747 if (finish_reason ==
"length") {
748 logger->info(
"[DECISION] length, continuing");
764bool AgentEngine::record_explicit_completion_failure(
765 LoopContext& ctx,
const std::string& finish_reason) {
766 if (!tier_requires_explicit_completion(ctx.locked_tier)) {
769 auto& retries = ctx.metadata[
"zero_tool_call_retries"];
770 int n = retries.empty() ? 0 : std::atoi(retries.c_str());
771 logger->warn(
"[DECISION] tier '{}' requires explicit completion "
772 "but iteration emitted zero tool calls "
773 "(finish_reason={}) retry={}/2",
774 ctx.locked_tier, finish_reason, n);
775 ctx.metadata[
"failure_reason"] =
776 "zero_tool_calls_with_explicit_completion";
777 ctx.metadata[
"failure_tier"] = ctx.locked_tier;
779 logger->error(
"[DECISION] zero-tool-call retries exhausted "
780 "(tier={}), failing turn", ctx.locked_tier);
781 set_state(ctx, AgentState::ERROR);
785 correction.role =
"user";
787 "[SYSTEM] Your previous response contained no tool call. "
788 "You must end every turn with exactly one tool call "
789 "(entropic.complete, entropic.delegate, or entropic.inspect). "
791 ctx.messages.push_back(std::move(correction));
792 retries = std::to_string(n + 1);
793 set_state(ctx, AgentState::EXECUTING);
809bool AgentEngine::tier_requires_explicit_completion(
810 const std::string& tier)
const {
811 if (tier_res_.get_tier_param ==
nullptr) {
return false; }
812 auto val = tier_res_.get_tier_param(
813 tier,
"explicit_completion", tier_res_.user_data);
814 return val ==
"true" || val ==
"1";
830bool AgentEngine::is_delegation_cycle(
831 const LoopContext& ctx,
const std::string& target)
const {
834 if (anc == target) {
return true; }
844bool AgentEngine::is_delegation_repeat_blocked(
845 const LoopContext& ctx,
const std::string& target)
const {
848 >= loop_config_.max_consecutive_failed_delegations;
856bool AgentEngine::fold_complete_into_assistant(
858 auto tn_it = tool_result_msg.
metadata.find(
"tool_name");
859 bool is_complete = tn_it != tool_result_msg.
metadata.end()
860 && tn_it->second ==
"entropic.complete";
861 if (!is_complete || ctx.
messages.empty()) {
return false; }
863 auto sum_it = ctx.
metadata.find(
"explicit_completion_summary");
864 bool foldable = last.role ==
"assistant" && last.content.empty()
866 if (!foldable) {
return false; }
867 last.content = sum_it->second;
876int64_t AgentEngine::seconds_since_last_activity()
const {
877 auto last = last_activity_epoch_s_.load();
878 if (last == 0) {
return 0; }
879 auto now = std::chrono::duration_cast<std::chrono::seconds>(
880 std::chrono::system_clock::now().time_since_epoch()).count();
881 return now > last ? (now - last) : 0;
891bool AgentEngine::should_stop(
const LoopContext& ctx)
const {
894 return is_terminal_state(ctx) || at_limit;
911bool AgentEngine::is_terminal_state(
const LoopContext& ctx) {
912 return ctx.state == AgentState::COMPLETE
913 || ctx.state == AgentState::ERROR
914 || ctx.state == AgentState::INTERRUPTED;
924void AgentEngine::set_state(LoopContext& ctx, AgentState state) {
925 auto prev = ctx.state;
928 if (callbacks_.on_state_change !=
nullptr) {
929 callbacks_.on_state_change(
930 static_cast<int>(state), callbacks_.user_data);
936 if (state_observer_ !=
nullptr) {
937 state_observer_(
static_cast<int>(state), state_observer_data_);
942 std::string json =
"{\"previous\":\""
944 +
"\",\"current\":\""
950 if (state == AgentState::ERROR) {
951 std::string json =
"{\"error_code\":\"STATE_ERROR\""
953 + std::to_string(ctx.metrics.iterations)
954 +
",\"consecutive\":" + std::to_string(ctx.consecutive_errors)
971void AgentEngine::interrupt() {
972 if (!interrupt_flag_.exchange(
true)) {
973 logger->info(
"Engine interrupted");
976 if (external_interrupt_cb_ !=
nullptr) {
977 external_interrupt_cb_(external_interrupt_data_);
980 pause_flag_.store(
false);
995void AgentEngine::set_external_interrupt(
void (*cb)(
void*),
997 external_interrupt_cb_ = cb;
998 external_interrupt_data_ = user_data;
1006void AgentEngine::reset_interrupt() {
1007 interrupt_flag_.store(
false);
1015void AgentEngine::pause() {
1016 logger->info(
"Engine paused");
1017 pause_flag_.store(
true);
1025void AgentEngine::cancel_pause() {
1026 logger->info(
"Pause cancelled, interrupting");
1027 pause_flag_.store(
false);
1028 interrupt_flag_.store(
true);
1038std::pair<int, int> AgentEngine::context_usage(
1039 const std::vector<Message>& messages)
const {
1040 int used = token_counter_.count_messages(messages);
1041 return {used, token_counter_.max_tokens};
1050void AgentEngine::reinject_context_anchors(
LoopContext& ctx) {
1051 for (
const auto& [key, content] : context_anchors_) {
1054 dir_anchor(ctx, d, r);
1065void AgentEngine::register_directive_handlers() {
1067 directive_processor_.register_handler(t,
1068 [
this, fn](LoopContext& c,
const Directive& d,
1069 DirectiveResult& r) {
1070 (this->*fn)(c, d, r);
1093void AgentEngine::dir_stop(
1094 LoopContext&,
const Directive&, DirectiveResult& r) {
1095 logger->info(
"[DIRECTIVE] stop_processing");
1096 r.stop_processing =
true;
1104void AgentEngine::dir_tier_change(
1105 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1106 const auto& tc =
static_cast<const TierChangeDirective&
>(d);
1107 ctx.locked_tier = tc.tier;
1108 r.tier_changed =
true;
1109 logger->info(
"[DIRECTIVE] tier_change: {}", tc.tier);
1117void AgentEngine::dir_delegate(
1118 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1119 const auto& dl =
static_cast<const DelegateDirective&
>(d);
1120 ctx.pending_delegation = PendingDelegation{
1121 dl.target, dl.task, dl.max_turns,
1122 dl.resume_from_delegation_id};
1123 r.stop_processing =
true;
1124 if (dl.resume_from_delegation_id.empty()) {
1125 logger->info(
"[DIRECTIVE] delegate: target={} task='{}'",
1126 dl.target, dl.task);
1129 logger->info(
"[DIRECTIVE] resume_delegation: id={} task='{}'",
1130 dl.resume_from_delegation_id, dl.task);
1139void AgentEngine::dir_pipeline(
1140 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1141 const auto& pl =
static_cast<const PipelineDirective&
>(d);
1142 ctx.pending_pipeline = PendingPipeline{pl.stages, pl.task};
1143 r.stop_processing =
true;
1144 logger->info(
"[DIRECTIVE] pipeline: {} stages", pl.stages.size());
1157void AgentEngine::dir_complete(
1158 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
1159 const auto& cd =
static_cast<const CompleteDirective&
>(d);
1163 auto prior_state = ctx.state;
1164 set_state(ctx, AgentState::VERIFYING);
1167 if (fire_complete_hook(cd.summary, ctx)) {
1168 logger->info(
"[DIRECTIVE] complete REJECTED by hook → revising");
1170 ctx.metadata[
"validator_phase"] =
"revising";
1171 set_state(ctx, prior_state);
1172 r.stop_processing =
false;
1176 ctx.metadata[
"explicit_completion_summary"] = cd.summary;
1183 if (cd.coverage_gap) {
1184 ctx.metadata[
"coverage_gap"] =
"true";
1185 ctx.metadata[
"gap_description"] = cd.gap_description;
1186 nlohmann::json files_arr = cd.suggested_files;
1187 ctx.metadata[
"suggested_files_json"] = files_arr.dump();
1189 set_state(ctx, AgentState::COMPLETE);
1190 r.stop_processing =
true;
1191 logger->info(
"[DIRECTIVE] complete coverage_gap={}",
1200void AgentEngine::dir_clear_todos(
1201 LoopContext&,
const Directive&, DirectiveResult&) {
1202 logger->debug(
"[DIRECTIVE] clear_self_todos (no-op)");
1210void AgentEngine::dir_inject(
1211 LoopContext&,
const Directive& d, DirectiveResult& r) {
1212 const auto& ic =
static_cast<const InjectContextDirective&
>(d);
1213 if (!ic.content.empty()) {
1216 msg.content = ic.content;
1217 r.injected_messages.push_back(std::move(msg));
1218 logger->info(
"[DIRECTIVE] inject_context");
1227void AgentEngine::dir_prune(
1228 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1229 const auto& pm =
static_cast<const PruneMessagesDirective&
>(d);
1230 auto [pruned, freed] = context_manager_.prune_tool_results(
1231 ctx, pm.keep_recent);
1232 logger->info(
"[DIRECTIVE] prune: {} results, {} chars", pruned, freed);
1240void AgentEngine::dir_anchor(
1241 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1242 const auto& ca =
static_cast<const ContextAnchorDirective&
>(d);
1243 if (ca.content.empty()) {
1244 context_anchors_.erase(ca.key);
1246 logger->info(
"Removed anchor: {}", ca.key);
1249 context_anchors_[ca.key] = ca.content;
1252 anchor.role =
"user";
1253 anchor.content = ca.content;
1254 anchor.metadata[
"is_context_anchor"] =
"true";
1255 anchor.metadata[
"anchor_key"] = ca.key;
1256 ctx.messages.push_back(std::move(anchor));
1257 logger->info(
"Updated anchor: {}", ca.key);
1265void AgentEngine::dir_phase(
1266 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1267 const auto& pc =
static_cast<const PhaseChangeDirective&
>(d);
1268 ctx.active_phase = pc.phase;
1269 logger->info(
"[DIRECTIVE] phase_change: {}", pc.phase);
1277void AgentEngine::dir_notify(
1278 LoopContext&,
const Directive& d, DirectiveResult&) {
1279 const auto& np =
static_cast<const NotifyPresenterDirective&
>(d);
1280 if (callbacks_.on_presenter_notify !=
nullptr) {
1281 callbacks_.on_presenter_notify(
1282 np.key.c_str(), np.data_json.c_str(),
1283 callbacks_.user_data);
1305 const nlohmann::json& obj,
const std::string& tc_str) {
1307 tc.
name = obj.value(
"name",
"");
1308 tc.
id =
"tc-" + std::to_string(
1309 std::hash<std::string>{}(tc.
name + tc_str) & 0xFFFF);
1310 if (obj.contains(
"arguments") && obj[
"arguments"].is_object()) {
1312 for (
auto& [k, v] : obj[
"arguments"].items()) {
1314 ? v.get<std::string>() : v.dump();
1328 const std::string& tc_str) {
1329 std::vector<ToolCall> calls;
1330 if (tc_str ==
"[]" || tc_str.empty()) {
return calls; }
1331 auto arr = nlohmann::json::parse(tc_str,
nullptr,
false);
1332 if (!arr.is_array()) {
return calls; }
1333 for (
const auto& obj : arr) {
1351std::pair<std::string, std::vector<ToolCall>>
1352AgentEngine::parse_tool_calls(
const std::string& raw_content) {
1353 if (inference_.parse_tool_calls ==
nullptr) {
1354 return {raw_content, {}};
1357 char* cleaned =
nullptr;
1358 char* tc_json =
nullptr;
1359 int rc = inference_.parse_tool_calls(
1360 raw_content.c_str(), &cleaned, &tc_json,
1361 inference_.adapter_data);
1363 std::string cleaned_str = cleaned ? cleaned : raw_content;
1364 std::string tc_str = tc_json ? tc_json :
"[]";
1366 if (inference_.free_fn !=
nullptr) {
1367 if (cleaned !=
nullptr) { inference_.free_fn(cleaned); }
1368 if (tc_json !=
nullptr) { inference_.free_fn(tc_json); }
1371 if (rc != 0) {
return {cleaned_str, {}}; }
1373 if (!calls.empty()) {
1374 logger->info(
"Parsed tool calls from model output");
1376 return {cleaned_str, std::move(calls)};
1384void AgentEngine::defang_meta_action_envelope(
Message& msg)
const {
1385 auto tn = msg.
metadata.find(
"tool_name");
1386 bool is_meta = tn != msg.
metadata.end()
1387 && tn->second.rfind(
"entropic.", 0) == 0;
1388 if (!is_meta) {
return; }
1389 auto j = nlohmann::json::parse(msg.
content,
nullptr,
false);
1390 bool is_envelope = j.is_object() && j.contains(
"action")
1391 && j[
"action"].is_string();
1392 if (!is_envelope) {
return; }
1393 msg.
content =
"(engine: " + j[
"action"].get<std::string>()
1394 +
" directive accepted)";
1405bool AgentEngine::process_tool_results(
1407 const std::vector<ToolCall>& tool_calls) {
1408 set_state(ctx, AgentState::WAITING_TOOL);
1410 auto results = tool_exec_.process_tool_calls(
1411 ctx, tool_calls, tool_exec_.user_data);
1413 logger->info(
"[TOOLS] {} call(s) -> {} result message(s)",
1414 tool_calls.size(), results.size());
1421 bool made_progress =
false;
1422 for (
auto& msg : results) {
1423 auto it = msg.metadata.find(
"result_kind");
1424 if (it != msg.metadata.end()
1425 && (it->second ==
"ok" || it->second ==
"ok_empty")) {
1426 made_progress =
true;
1428 if (fold_complete_into_assistant(ctx, msg)) {
1431 defang_meta_action_envelope(msg);
1432 ctx.
messages.push_back(std::move(msg));
1437 if (!is_terminal_state(ctx)) {
1438 set_state(ctx, AgentState::EXECUTING);
1440 return made_progress;
1453 const std::string& key) {
1456 std::remove_if(msgs.begin(), msgs.end(),
1458 auto it = m.metadata.find(
"anchor_key");
1459 return it != m.metadata.end() && it->second == key;
1474bool AgentEngine::fire_pre_hook(
1476 if (hooks_.fire_pre ==
nullptr) {
1479 std::string json =
"{\"iteration\":"
1480 + std::to_string(iteration) +
"}";
1481 char* modified =
nullptr;
1482 int rc = hooks_.fire_pre(hooks_.registry,
1483 point, json.c_str(), &modified);
1499 static const auto tbl = []() {
1500 std::array<const char*, 256> t{};
1501 t[
static_cast<unsigned char>(
'"')] =
"\\\"";
1502 t[
static_cast<unsigned char>(
'\\')] =
"\\\\";
1503 t[
static_cast<unsigned char>(
'\n')] =
"\\n";
1504 t[
static_cast<unsigned char>(
'\r')] =
"\\r";
1505 t[
static_cast<unsigned char>(
'\t')] =
"\\t";
1521 out.reserve(s.size() + 16);
1523 const char* esc = tbl[
static_cast<unsigned char>(c)];
1524 if (esc) { out += esc; }
1542 const std::vector<Message>& messages) {
1543 std::string manifest;
1544 for (
const auto& msg : messages) {
1545 auto it = msg.metadata.find(
"tool_name");
1546 if (it == msg.metadata.end() || it->second.empty()) {
1554 auto orig = msg.metadata.find(
"original_content");
1555 size_t real_size = (orig != msg.metadata.end())
1556 ? orig->second.size()
1557 : msg.content.size();
1558 manifest +=
"- " + it->second
1559 +
" \xe2\x86\x92 " + std::to_string(real_size)
1574 size_t chars_per_result) {
1575 auto orig = msg.
metadata.find(
"original_content");
1576 const std::string& src = (orig != msg.
metadata.end())
1579 auto iter = msg.
metadata.find(
"added_at_iteration");
1580 std::string out =
"## ";
1581 out += msg.
metadata.at(
"tool_name");
1583 out +=
" [iter " + iter->second +
"]";
1586 if (src.size() <= chars_per_result) {
1589 out += src.substr(0, chars_per_result);
1590 out +=
"\n[... truncated, "
1591 + std::to_string(src.size() - chars_per_result)
1626 const std::vector<Message>& messages,
1628 size_t chars_per_result) {
1629 std::vector<const Message*> tool_msgs;
1630 for (
const auto& msg : messages) {
1631 auto it = msg.metadata.find(
"tool_name");
1632 if (it == msg.metadata.end() || it->second.empty()) {
1635 tool_msgs.push_back(&msg);
1637 if (tool_msgs.empty()) {
return {}; }
1639 size_t elided = (tool_msgs.size() > max_results)
1640 ? (tool_msgs.size() - max_results) : 0;
1641 size_t start = elided;
1644 out +=
"(" + std::to_string(elided)
1645 +
" earlier tool results elided for length)\n";
1647 for (
size_t i = start; i < tool_msgs.size(); ++i) {
1668 const std::string& base,
1670 if (tool_exec.
history_json ==
nullptr) {
return base; }
1672 if (js ==
nullptr) {
return base; }
1673 std::string out = base +
"prior-iteration history: " + js +
"\n";
1686 const std::vector<Message>& messages) {
1687 for (
const auto& msg : messages) {
1688 if (msg.role ==
"system") {
return msg.content; }
1708void AgentEngine::fire_post_generate_hook(
1709 GenerateResult& result,
1710 const std::string& tier,
1711 const std::vector<Message>& messages) {
1712 if (hooks_.fire_post ==
nullptr) {
1727 "{\"finish_reason\":\"" + result.finish_reason
1729 +
"\",\"tier\":\"" + tier
1733 char* out =
nullptr;
1734 hooks_.fire_post(hooks_.registry,
1736 if (out !=
nullptr) {
1737 result.content = out;
1738 logger->info(
"POST_GENERATE hook revised content");
1763void AgentEngine::dispatch_post_generate(
1764 LoopContext& ctx, GenerateResult& result) {
1769 ctx.pending_validation_feedback.clear();
1770 ctx.pending_anti_spiral_warning.clear();
1771 fire_post_generate_hook(result, ctx.locked_tier, ctx.messages);
1774 capture_validation_feedback(ctx);
1782void AgentEngine::capture_validation_feedback(LoopContext& ctx) {
1783 if (validation_provider_ ==
nullptr) {
return; }
1784 char* v = validation_provider_(validation_provider_data_);
1785 if (v ==
nullptr) {
return; }
1789 auto j = nlohmann::json::parse(raw);
1790 auto verdict = j.value(
"verdict",
"");
1791 if (verdict.rfind(
"rejected", 0) != 0) {
return; }
1793 if (j.contains(
"violations") && j[
"violations"].is_array()) {
1794 for (
const auto& vio : j[
"violations"]) {
1795 if (!joined.empty()) { joined +=
"; "; }
1796 joined += vio.is_string()
1797 ? vio.get<std::string>()
1801 if (joined.empty()) { joined = verdict; }
1802 ctx.pending_validation_feedback = joined;
1803 }
catch (
const nlohmann::json::exception&) {
1820 const std::vector<Message>& messages) {
1821 std::string arr =
"[";
1823 for (
const auto& msg : messages) {
1824 auto it = msg.metadata.find(
"tool_name");
1825 if (it == msg.metadata.end() || it->second.empty()) {
1828 if (!first) { arr +=
","; }
1858bool AgentEngine::fire_complete_hook(
1859 const std::string& summary,
1860 const LoopContext& ctx) {
1861 if (hooks_.fire_pre ==
nullptr) {
return false; }
1867 std::string validation_block =
",\"validation\":null";
1868 if (validation_provider_ !=
nullptr) {
1869 char* v = validation_provider_(validation_provider_data_);
1871 validation_block =
",\"validation\":" + std::string(v);
1877 +
"\",\"tier\":\"" + ctx.locked_tier
1878 +
"\",\"tool_results\":" + tool_results
1879 +
",\"iteration\":" + std::to_string(ctx.metrics.iterations)
1882 char* modified =
nullptr;
1883 int rc = hooks_.fire_pre(hooks_.registry,
1885 if (modified !=
nullptr) {
1888 feedback.role =
"user";
1889 feedback.content = std::string(
"[CITATION VALIDATION] ") + modified;
1890 const_cast<LoopContext&
>(ctx).messages.push_back(
1891 std::move(feedback));
1905bool AgentEngine::fire_delegate_pre_hook(
1906 const PendingDelegation& pending,
int depth) {
1907 if (hooks_.fire_pre ==
nullptr) {
1910 std::string json =
"{\"target_tier\":\""
1911 + pending.target +
"\",\"task\":\""
1912 + pending.task +
"\",\"depth\":"
1913 + std::to_string(depth) +
"}";
1914 char* modified =
nullptr;
1915 int rc = hooks_.fire_pre(hooks_.registry,
1937void AgentEngine::fire_delegate_complete_hook(
1938 const std::string& target,
bool success,
1939 const std::string& summary) {
1940 if (hooks_.fire_post ==
nullptr) {
1944 j[
"target_tier"] = target;
1945 j[
"success"] = success;
1947 ? ToolResultKind::ok
1949 j[
"summary"] = summary;
1950 std::string json = j.dump();
1951 char* out =
nullptr;
1952 hooks_.fire_post(hooks_.registry,
1967 auto* engine =
static_cast<AgentEngine*
>(user_data);
1981 reject.
role =
"user";
1982 reject.
content =
"[DELEGATION REJECTED] Maximum delegation "
1983 "depth (" + std::to_string(
1984 AgentEngine::MAX_DELEGATION_DEPTH) +
1986 ctx.
messages.push_back(std::move(reject));
2003 chain += anc +
" -> ";
2008 reject.
role =
"user";
2009 reject.
content =
"[DELEGATION REJECTED] Circular delegation "
2010 "detected: tier '" + target +
2011 "' already in the active delegation chain "
2012 "(" + chain +
"). Choose a different target.";
2013 ctx.
metadata[
"failure_reason"] =
"delegation_cycle";
2014 ctx.
metadata[
"failure_target"] = target;
2015 ctx.
messages.push_back(std::move(reject));
2028 std::string tag = result.
success ?
"COMPLETE" :
"FAILED";
2031 msg.
content =
"[DELEGATION " + tag +
": " + target +
"] " + result.
summary;
2032 ctx.
messages.push_back(std::move(msg));
2047 LoopContext& ctx,
const std::string& target,
int n) {
2049 reject.
role =
"user";
2050 reject.
content =
"[DELEGATION REJECTED] '" + target
2051 +
"' has just failed " + std::to_string(n)
2052 +
" times in a row. Stop retrying this target. Either "
2053 "respond to the user with what you have, or delegate to "
2054 "a different tier.";
2055 ctx.
metadata[
"failure_reason"] =
"delegation_repeat_blocked";
2056 ctx.
metadata[
"failure_target"] = target;
2057 ctx.
messages.push_back(std::move(reject));
2068bool AgentEngine::reject_delegation_if_guarded(
2069 LoopContext& ctx,
const PendingDelegation& pending) {
2070 bool rejected =
true;
2071 if (ctx.delegation_depth >= MAX_DELEGATION_DEPTH) {
2072 logger->warn(
"Delegation rejected: depth {} >= max {}",
2073 ctx.delegation_depth, MAX_DELEGATION_DEPTH);
2074 push_delegation_rejected(ctx);
2075 }
else if (is_delegation_cycle(ctx, pending.target)) {
2077 logger->warn(
"Delegation rejected: cycle on target tier '{}'",
2080 }
else if (is_delegation_repeat_blocked(ctx, pending.target)) {
2085 logger->warn(
"Delegation rejected: '{}' failed {}x in a row "
2086 "(>= max_consecutive_failed_delegations={})",
2088 ctx.consecutive_failed_delegations,
2089 loop_config_.max_consecutive_failed_delegations);
2091 ctx, pending.target, ctx.consecutive_failed_delegations);
2110void AgentEngine::execute_pending_delegation(LoopContext& ctx) {
2111 auto pending = std::move(*ctx.pending_delegation);
2112 ctx.pending_delegation.reset();
2114 if (reject_delegation_if_guarded(ctx, pending)) {
return; }
2116 set_state(ctx, AgentState::DELEGATING);
2125 std::vector<Message> resume_history;
2126 bool blocked =
false;
2127 if (!pending.resume_from_delegation_id.empty()
2128 && !resolve_resume_delegation(ctx, pending, resume_history)) {
2130 }
else if (fire_delegate_pre_hook(pending, ctx.delegation_depth)) {
2131 logger->info(
"ON_DELEGATE hook cancelled delegation");
2135 set_state(ctx, AgentState::EXECUTING);
2139 fire_delegation_start(ctx, pending.target, pending.task);
2140 auto result = run_pending_delegation(
2141 ctx, pending, std::move(resume_history));
2145 if (result.success) {
2146 ctx.last_failed_delegation_target.clear();
2147 ctx.consecutive_failed_delegations = 0;
2148 }
else if (pending.target == ctx.last_failed_delegation_target) {
2149 ++ctx.consecutive_failed_delegations;
2151 ctx.last_failed_delegation_target = pending.target;
2152 ctx.consecutive_failed_delegations = 1;
2155 fire_delegation_complete(ctx, pending.target, result);
2156 fire_delegate_complete_hook(pending.target, result.success,
2159 finalize_delegation_result(ctx, result);
2174void AgentEngine::relay_partial_result(
2175 LoopContext& ctx,
const std::string& summary) {
2176 GenerateResult relay_result;
2177 relay_result.content = summary;
2178 relay_result.finish_reason =
"stop";
2179 relay_result.tool_calls_json =
"[]";
2180 fire_post_generate_hook(
2181 relay_result, ctx.locked_tier, ctx.messages);
2182 ctx.metadata[
"explicit_completion_summary"] = relay_result.content;
2183 set_state(ctx, AgentState::COMPLETE);
2195 "[COVERAGE GAP from " + tier +
"]\n"
2196 "Summary so far: " + result.
summary +
"\n"
2199 body +=
"\nSuggested files to inspect:";
2201 body +=
"\n - " + f;
2227void AgentEngine::finalize_delegation_result(
2228 LoopContext& ctx,
const DelegationResult& result) {
2231 const bool in_relay_tier =
2232 relay_single_delegate_tiers_.count(ctx.locked_tier) > 0;
2233 if (in_relay_tier && result.coverage_gap) {
2236 gap.content = build_coverage_gap_message(
2237 result.target_tier, result);
2238 ctx.messages.push_back(std::move(gap));
2239 ctx.metadata[
"relay_status"] =
"coverage_gap_suppressed";
2241 "[COVERAGE GAP] suppressing auto-relay for tier={} "
2242 "({} suggested files)",
2243 result.target_tier, result.suggested_files.size());
2244 set_state(ctx, AgentState::EXECUTING);
2247 if (result.success && in_relay_tier) {
2248 relay_partial_result(ctx, result.summary);
2249 log_relay_status(ctx);
2252 if (!result.terminal_reason.empty() && in_relay_tier) {
2253 relay_partial_result(ctx,
2254 "[partial — budget_exhausted] " + result.summary);
2255 log_relay_status(ctx, result.terminal_reason);
2258 bool needs_explicit = tier_requires_explicit_completion(
2260 set_state(ctx, (result.success && !needs_explicit)
2261 ? AgentState::COMPLETE :
AgentState::EXECUTING);
2278void AgentEngine::log_relay_status(LoopContext& ctx,
2279 const std::string& terminal_reason) {
2280 std::string verdict;
2281 if (validation_provider_ !=
nullptr) {
2282 char* v = validation_provider_(validation_provider_data_);
2286 auto pos = jv.find(
"\"verdict\":\"");
2287 if (pos != std::string::npos) {
2289 auto end = jv.find(
'"', pos);
2290 if (end != std::string::npos) {
2291 verdict = jv.substr(pos, end - pos);
2296 if (!terminal_reason.empty()) {
2297 ctx.metadata[
"relay_status"] =
"budget_exhausted_relayed";
2299 "Relay: single-delegate result used "
2300 "(partial — terminal_reason={}, verdict={})",
2301 terminal_reason, verdict.empty() ?
"none" : verdict);
2304 if (verdict ==
"skipped" || verdict.empty()) {
2305 ctx.metadata[
"relay_status"] =
"validation_skipped";
2307 "Relay: single-delegate result used "
2308 "(lead validation skipped per config)");
2310 ctx.metadata[
"relay_status"] =
"validated";
2312 "Relay: single-delegate result used "
2313 "(passed lead validation)");
2323void AgentEngine::execute_pending_pipeline(LoopContext& ctx) {
2324 auto pending = std::move(*ctx.pending_pipeline);
2325 ctx.pending_pipeline.reset();
2327 if (ctx.delegation_depth >= MAX_DELEGATION_DEPTH) {
2328 logger->warn(
"Pipeline rejected: depth {} >= max {}",
2329 ctx.delegation_depth, MAX_DELEGATION_DEPTH);
2331 reject.role =
"user";
2332 reject.content =
"[PIPELINE REJECTED] Maximum delegation "
2334 ctx.messages.push_back(std::move(reject));
2338 set_state(ctx, AgentState::DELEGATING);
2341 auto repo_dir = get_repo_dir();
2342 DelegationManager mgr(run_child_loop_trampoline,
this,
2343 tier_res_, repo_dir,
2344 ensure_sandbox_manager());
2345 if (storage_.create_delegation !=
nullptr) {
2346 mgr.set_storage(&storage_);
2351 auto cb_snap = delegation_callbacks_snapshot();
2352 mgr.set_delegation_callbacks(
2353 cb_snap.start, cb_snap.complete, cb_snap.user_data);
2354 auto result = mgr.execute_pipeline(
2355 ctx, pending.stages, pending.task);
2357 std::string tag = result.success ?
"COMPLETE" :
"FAILED";
2359 result_msg.role =
"user";
2360 result_msg.content =
"[PIPELINE " + tag +
"] " + result.summary;
2361 ctx.messages.push_back(std::move(result_msg));
2363 set_state(ctx, AgentState::EXECUTING);
2374void AgentEngine::fire_delegation_start(
2375 const LoopContext& ,
2376 const std::string& tier,
2377 const std::string& task) {
2378 if (callbacks_.on_delegation_start !=
nullptr) {
2379 callbacks_.on_delegation_start(
2380 "", tier.c_str(), task.c_str(),
2381 callbacks_.user_data);
2393void AgentEngine::fire_delegation_complete(
2394 const LoopContext& ,
2395 const std::string& tier,
2396 const DelegationResult& result) {
2397 if (callbacks_.on_delegation_complete !=
nullptr) {
2398 callbacks_.on_delegation_complete(
2399 "", tier.c_str(), result.summary.c_str(),
2400 result.success ? 1 : 0,
2401 callbacks_.user_data);
2416bool AgentEngine::should_auto_chain(
2417 const LoopContext& ctx,
2418 const std::string& finish_reason,
2419 const std::string& content) {
2420 if (ctx.locked_tier.empty() || tier_res_.get_tier_param ==
nullptr) {
2424 std::string auto_chain = tier_res_.get_tier_param(
2425 ctx.locked_tier,
"auto_chain", tier_res_.user_data);
2426 if (auto_chain.empty()) {
2430 bool triggered = (finish_reason ==
"length") ||
2431 (finish_reason ==
"stop" &&
2432 response_generator_.is_response_complete(content,
"[]"));
2445bool AgentEngine::try_auto_chain(
2447 const std::string& finish_reason,
2448 const std::string& content) {
2449 if (!should_auto_chain(ctx, finish_reason, content)) {
2453 if (ctx.delegation_depth > 0) {
2454 logger->info(
"[AUTO-CHAIN] child depth={}, completing",
2455 ctx.delegation_depth);
2456 set_state(ctx, AgentState::COMPLETE);
2461 std::string target = tier_res_.get_tier_param(
2462 ctx.locked_tier,
"auto_chain", tier_res_.user_data);
2464 if (!target.empty()) {
2465 logger->info(
"[AUTO-CHAIN] root, tier change to '{}'", target);
2466 TierChangeDirective tc(target,
"auto_chain");
2468 dir_tier_change(ctx, tc, r);
2470 return !target.empty();
2495std::filesystem::path AgentEngine::get_repo_dir() {
2496 if (repo_dir_checked_) {
2497 return cached_repo_dir_.value_or(std::filesystem::path{});
2499 repo_dir_checked_ =
true;
2500 std::filesystem::path resolved;
2501 if (!project_dir_override_.empty()) {
2502 resolved = project_dir_override_;
2503 logger->info(
"Project dir for sandbox snapshots: {} (configured)",
2506 resolved = std::filesystem::current_path();
2507 logger->info(
"Project dir for sandbox snapshots: {} (cwd fallback)",
2510 cached_repo_dir_ = resolved;
2525void AgentEngine::set_project_dir(
const std::filesystem::path& project_dir) {
2526 project_dir_override_ = project_dir;
2527 cached_repo_dir_.reset();
2528 repo_dir_checked_ =
false;
2548 const std::string& reason,
2549 const std::string& delegation_id) {
2552 m.
content =
"[DELEGATION FAILED: resume_delegation] "
2553 + reason +
" (delegation_id=" + delegation_id +
")";
2554 ctx.
messages.push_back(std::move(m));
2570bool AgentEngine::fetch_resume_payload(
2572 const std::string&
id,
2573 nlohmann::json& parsed) {
2574 std::optional<std::string> error;
2576 if (storage_.load_delegation_with_messages ==
nullptr) {
2577 error =
"storage unavailable";
2578 }
else if (!storage_.load_delegation_with_messages(
2579 id.c_str(), raw, storage_.user_data)) {
2580 error =
"unknown delegation_id";
2582 parsed = nlohmann::json::parse(raw,
nullptr,
false);
2583 if (parsed.is_discarded() || !parsed.is_object()) {
2584 error =
"malformed storage payload";
2587 if (
error.has_value()) {
2588 logger->error(
"resume_delegation '{}': {}",
id, *error);
2600bool AgentEngine::resolve_resume_delegation(
2602 PendingDelegation& pending,
2603 std::vector<Message>& out_history) {
2604 const auto&
id = pending.resume_from_delegation_id;
2606 if (!fetch_resume_payload(ctx,
id, j)) {
2609 auto target = j.value(
"target_tier", std::string{});
2610 if (target.empty()) {
2614 pending.target = target;
2615 if (j.contains(
"messages") && j[
"messages"].is_array()) {
2616 for (
const auto& mj : j[
"messages"]) {
2618 m.role = mj.value(
"role",
"");
2619 m.content = mj.value(
"content",
"");
2620 if (!m.role.empty()) {
2621 out_history.push_back(std::move(m));
2625 logger->info(
"resume_delegation '{}': loaded {} messages, target='{}'",
2626 id, out_history.size(), target);
2644DelegationResult AgentEngine::run_pending_delegation(
2646 const PendingDelegation& pending,
2647 std::vector<Message> resume_history) {
2648 std::optional<int> max_turns;
2649 if (pending.max_turns > 0) { max_turns = pending.max_turns; }
2650 DelegationManager mgr(run_child_loop_trampoline,
this,
2651 tier_res_, get_repo_dir(),
2652 ensure_sandbox_manager());
2653 if (storage_.create_delegation !=
nullptr) {
2654 mgr.set_storage(&storage_);
2656 auto cb_snap = delegation_callbacks_snapshot();
2657 mgr.set_delegation_callbacks(
2658 cb_snap.start, cb_snap.complete, cb_snap.user_data);
2659 if (resume_history.empty()) {
2660 return mgr.execute_delegation(
2661 ctx, pending.target, pending.task, max_turns);
2663 return mgr.execute_resume_delegation(
2664 ctx, pending.target, pending.task,
2665 std::move(resume_history), max_turns);
2673SandboxManager* AgentEngine::ensure_sandbox_manager() {
2675 return &*sandbox_mgr_;
2677 auto repo_dir = get_repo_dir();
2678 if (repo_dir.empty()) {
2681 sandbox_mgr_.emplace(repo_dir);
2682 return &*sandbox_mgr_;
2693void AgentEngine::set_system_prompt(
const std::string& prompt) {
2694 system_prompt_ = prompt;
2704 session_logger_ = log;
2720std::vector<Message> AgentEngine::run_turn(
const std::string& input) {
2725 running_flag_.store(
true);
2726 if (conversation_.empty() && !system_prompt_.empty()) {
2728 sys.
role =
"system";
2730 conversation_.push_back(std::move(sys));
2732 auto result = run_drain_loop(input,
"");
2733 running_flag_.store(
false);
2754std::vector<Message> AgentEngine::run_turn_as(
const std::string& tier,
2755 const std::string& input) {
2756 running_flag_.store(
true);
2757 seed_system_prompt_for_tier(tier);
2758 auto result = run_drain_loop(input, tier);
2759 running_flag_.store(
false);
2775void AgentEngine::seed_system_prompt_for_tier(
const std::string& tier) {
2776 if (!conversation_.empty()) {
return; }
2777 auto it = tier_info_.find(tier);
2778 const std::string& sp =
2779 (it != tier_info_.end() && !it->second.system_prompt.empty())
2780 ? it->second.system_prompt
2782 if (sp.empty()) {
return; }
2784 sys.
role =
"system";
2786 conversation_.push_back(std::move(sys));
2797std::vector<Message> AgentEngine::run_drain_loop(
2798 std::string pending,
const std::string& tier_override) {
2799 std::vector<Message> result;
2804 conversation_.push_back(std::move(usr));
2806 size_t sent_len = conversation_.size();
2807 result = run(conversation_, tier_override);
2808 for (
size_t i = sent_len; i < result.size(); ++i) {
2809 conversation_.push_back(result[i]);
2811 auto next = pop_queued_user_message();
2812 if (!next.has_value()) {
break; }
2813 fire_queue_consumed(*next, user_message_queue_depth());
2814 pending = std::move(*next);
2841void AgentEngine::seed_system_prompt(
2842 const std::vector<Message>& new_messages) {
2843 bool caller_has_system =
false;
2844 for (
const auto& m : new_messages) {
2845 if (m.role ==
"system") { caller_has_system =
true;
break; }
2847 if (conversation_.empty() && !system_prompt_.empty()
2848 && !caller_has_system) {
2850 sys.
role =
"system";
2852 conversation_.push_back(std::move(sys));
2863bool AgentEngine::prepare_next_turn(std::vector<Message>& pending) {
2864 auto next = pop_queued_user_message();
2865 if (!next.has_value()) {
return false; }
2866 fire_queue_consumed(*next, user_message_queue_depth());
2869 usr.
content = std::move(*next);
2871 pending.push_back(std::move(usr));
2882std::vector<Message> AgentEngine::run_turn(std::vector<Message> new_messages) {
2887 running_flag_.store(
true);
2888 seed_system_prompt(new_messages);
2889 std::vector<Message> pending = std::move(new_messages);
2890 std::vector<Message> result;
2892 for (
auto& m : pending) {
2893 conversation_.push_back(std::move(m));
2895 size_t sent_len = conversation_.size();
2896 result = run(conversation_);
2897 for (
size_t i = sent_len; i < result.size(); ++i) {
2898 conversation_.push_back(result[i]);
2900 if (!prepare_next_turn(pending)) {
break; }
2902 running_flag_.store(
false);
2916int AgentEngine::run_streaming(
2917 const std::string& input,
2922 if (session_logger_) {
2923 session_logger_->log_user_input(input);
2927 if (session_logger_ && session_logger_->is_open()) {
2929 SessionLogger::raw_token_callback,
2938 Ctx sctx{&filter, cancel_flag,
this};
2942 auto* c =
static_cast<Ctx*
>(ud);
2943 if (c->cancel && *c->cancel) {
2944 c->engine->interrupt();
2947 c->filter->on_token(t, l);
2949 cbs.user_data = &sctx;
2952 auto result = run_turn(input);
2955 if (session_logger_) {
2956 session_logger_->end_turn();
2958 if (cancel_flag && *cancel_flag) {
return 1; }
2970 const std::vector<Message>& messages) {
2972 for (
const auto& m : messages) {
2973 if (m.role !=
"user" || m.content.empty()) {
continue; }
2974 if (!echo.empty()) { echo +=
'\n'; }
2995int AgentEngine::run_streaming(
2996 std::vector<Message> new_messages,
3001 if (session_logger_) {
3006 if (session_logger_ && session_logger_->is_open()) {
3008 SessionLogger::raw_token_callback,
3017 Ctx sctx{&filter, cancel_flag,
this};
3021 auto* c =
static_cast<Ctx*
>(ud);
3022 if (c->cancel && *c->cancel) {
3023 c->engine->interrupt();
3026 c->filter->on_token(t, l);
3028 cbs.user_data = &sctx;
3031 auto result = run_turn(std::move(new_messages));
3034 if (session_logger_) {
3035 session_logger_->end_turn();
3037 if (cancel_flag && *cancel_flag) {
return 1; }
3046void AgentEngine::clear_conversation() {
3047 conversation_.clear();
3048 logger->info(
"conversation cleared");
3057size_t AgentEngine::message_count()
const {
3058 return conversation_.size();
3067const std::vector<Message>& AgentEngine::get_messages()
const {
3068 return conversation_;
3078bool AgentEngine::queue_user_message(
const std::string& message) {
3079 std::lock_guard lock(queue_mutex_);
3080 int cap = loop_config_.message_queue_capacity;
3081 if (cap < 0) { cap = 0; }
3082 if (user_message_queue_.size()
3083 >=
static_cast<size_t>(cap)) {
3086 user_message_queue_.push_back(message);
3087 logger->info(
"queued mid-gen user message: depth={}",
3088 user_message_queue_.size());
3097size_t AgentEngine::user_message_queue_depth()
const {
3098 std::lock_guard lock(queue_mutex_);
3099 return user_message_queue_.size();
3107void AgentEngine::clear_user_message_queue() {
3108 std::lock_guard lock(queue_mutex_);
3109 size_t dropped = user_message_queue_.size();
3110 user_message_queue_.clear();
3112 logger->info(
"cleared mid-gen queue: dropped={}", dropped);
3121void AgentEngine::set_message_queue_capacity(
int cap) {
3122 std::lock_guard lock(queue_mutex_);
3123 loop_config_.message_queue_capacity = cap < 0 ? 0 : cap;
3131std::optional<std::string>
3132AgentEngine::pop_queued_user_message() {
3133 std::lock_guard lock(queue_mutex_);
3134 if (user_message_queue_.empty()) {
3135 return std::nullopt;
3137 std::string front = std::move(user_message_queue_.front());
3138 user_message_queue_.pop_front();
3147void AgentEngine::fire_queue_consumed(
const std::string& consumed,
3149 if (queue_observer_ !=
nullptr) {
3151 consumed.c_str(), remaining, queue_observer_data_);
3160void AgentEngine::set_queue_observer(
3161 void (*observer)(
const char*,
size_t,
void*),
3163 queue_observer_ = observer;
3164 queue_observer_data_ = user_data;
3178void AgentEngine::set_state_observer(
3179 void (*observer)(
int,
void*),
3181 state_observer_ = observer;
3182 state_observer_data_ = user_data;
3183 response_generator_.set_state_observer(observer, user_data);
3198 const std::vector<const Directive*>& dirs,
3201 ->directive_processor().process(ctx, dirs);
3216void AgentEngine::set_tier_info(
3217 const std::string& name,
3220 tier_info_[name] = info;
3230bool AgentEngine::has_tier(
const std::string& name)
const {
3231 return tier_info_.count(name) > 0;
3241const std::string& AgentEngine::tier_system_prompt(
3242 const std::string& name)
const {
3243 static const std::string kEmpty;
3244 auto it = tier_info_.find(name);
3245 return it == tier_info_.end() ? kEmpty : it->second.system_prompt;
3254void AgentEngine::set_relay_single_delegate(
const std::string& name) {
3255 relay_single_delegate_tiers_.insert(name);
3264void AgentEngine::set_handoff_rules(
3265 const std::unordered_map<std::string,
3266 std::vector<std::string>>& rules)
3268 handoff_rules_ = rules;
3269 wire_internal_tier_resolution();
3278 const std::string& name,
void* ud) {
3280 auto it = self->tier_info_.find(name);
3282 if (it == self->tier_info_.end()) {
3298bool AgentEngine::tri_tier_exists(
const std::string& name,
void* ud) {
3299 auto* self =
static_cast<AgentEngine*
>(ud);
3300 return self->tier_info_.count(name) > 0;
3311std::vector<std::string> AgentEngine::tri_get_handoff_targets(
3312 const std::string& name,
void* ud) {
3313 auto* self =
static_cast<AgentEngine*
>(ud);
3314 auto it = self->handoff_rules_.find(name);
3315 std::vector<std::string> result;
3316 if (it != self->handoff_rules_.end()) { result = it->second; }
3343std::string AgentEngine::tri_get_tier_param(
const std::string& name,
3344 const std::string& param,
void* ud) {
3345 auto* self =
static_cast<AgentEngine*
>(ud);
3346 auto it = self->tier_info_.find(name);
3347 if (it == self->tier_info_.end()) {
return ""; }
3348 const auto& info = it->second;
3350 if (param ==
"explicit_completion") {
3351 result = info.explicit_completion ?
"true" :
"false";
3352 }
else if (param ==
"max_iterations"
3353 && info.max_iterations_override >= 0) {
3354 result = std::to_string(info.max_iterations_override);
3355 }
else if (param ==
"max_tool_calls_per_turn"
3356 && info.max_tool_calls_per_turn_override >= 0) {
3357 result = std::to_string(info.max_tool_calls_per_turn_override);
3367void AgentEngine::wire_internal_tier_resolution() {
3368 TierResolutionInterface tri;
3369 tri.resolve_tier = &AgentEngine::tri_resolve_tier;
3370 tri.tier_exists = &AgentEngine::tri_tier_exists;
3371 tri.get_handoff_targets = &AgentEngine::tri_get_handoff_targets;
3372 tri.get_tier_param = &AgentEngine::tri_get_tier_param;
3373 tri.user_data =
this;
3374 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.
Manages session_model.log for raw streaming content.
Streaming filter that removes <think> blocks from output.
void set_raw_callback(TokenCallback cb, void *ud)
Set optional raw callback (receives ALL tokens unfiltered).
void flush()
Flush any buffered partial tag content.
DelegationManager — child loop creation and execution.
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.
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 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.
@ 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.
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.
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 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.
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.
static void push_delegation_result(LoopContext &ctx, const std::string &target, const DelegationResult &result)
Append a delegation result message to the loop context.
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 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 fire_loop_end_hook(const HookInterface &hooks, const LoopContext &ctx)
Build + fire the ON_LOOP_END info hook.
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.
Update a keyed persistent context anchor.
Engine-level hooks called during context management.
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.
void(* on_stream_chunk)(const char *chunk, size_t len, void *ud)
Per-token streaming.
Configuration for the agentic loop.
Mutable state carried through the agentic loop.
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)
int consecutive_errors
Error streak counter.
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)
int delegation_depth
0 = root, 1+ = child
AgentState state
Current state.
std::vector< Message > messages
Conversation history.
std::string locked_tier
Tier locked for this loop ("" = none)
double start_time
Loop start (seconds since epoch)
int tokens_used
Total tokens consumed.
int errors
Total errors encountered.
int duration_ms() const
Get loop duration in milliseconds.
int tool_calls
Total tool calls executed.
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.
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.