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;
344 pause_flag_.store(
false);
345 apply_identity_overrides(ctx);
346 reinject_context_anchors(ctx);
350 set_state(ctx, AgentState::PLANNING);
354 auto& tm = per_tier_metrics_[
370std::vector<Message> AgentEngine::run(std::vector<Message> messages) {
374 last_activity_epoch_s_.store(
375 std::chrono::duration_cast<std::chrono::seconds>(
376 std::chrono::system_clock::now().time_since_epoch()).count());
382 init_session_conversation(ctx);
385 pause_flag_.store(
false);
387 reinject_context_anchors(ctx);
388 set_state(ctx, AgentState::PLANNING);
393 accumulate_run_metrics(ctx);
394 logger->info(
"Loop complete: {} iterations, {}ms",
405void AgentEngine::init_session_conversation(
LoopContext& ctx) {
413 if (storage_.create_conversation ==
nullptr) {
return; }
415 if (storage_.create_conversation(
"session", conv_id, storage_.user_data)
416 && !conv_id.empty()) {
419 logger->warn(
"Storage create_conversation failed at run() init; "
420 "delegations will not persist this session (gh#48)");
430void AgentEngine::accumulate_run_metrics(LoopContext& ctx) {
431 last_metrics_ = ctx.metrics;
433 auto& tm = per_tier_metrics_[
434 ctx.locked_tier.empty() ?
"lead" : ctx.locked_tier];
435 tm.iterations += ctx.metrics.iterations;
436 tm.tool_calls += ctx.metrics.tool_calls;
437 tm.tokens_used += ctx.metrics.tokens_used;
438 tm.errors += ctx.metrics.errors;
439 tm.end_time += (ctx.metrics.end_time - ctx.metrics.start_time);
448void AgentEngine::loop(LoopContext& ctx) {
451 while (!should_stop(ctx)) {
452 ctx.metrics.iterations++;
454 if (interrupt_flag_.load()) {
455 set_state(ctx, AgentState::INTERRUPTED);
459 execute_iteration(ctx);
461 if (ctx.state == AgentState::ERROR) {
466 if (ctx.metrics.iterations >= resolve_max_iterations(ctx)
467 && ctx.state != AgentState::COMPLETE
468 && ctx.state != AgentState::ERROR
469 && ctx.state != AgentState::INTERRUPTED) {
475 logger->warn(
"Loop ended due to max iterations ({}/{}) — "
476 "forcing synthetic entropic.complete",
477 ctx.metrics.iterations, resolve_max_iterations(ctx));
479 forced.role =
"assistant";
480 forced.content =
"[iteration cap reached after "
481 + std::to_string(ctx.metrics.iterations)
482 +
" iterations — returning current state]";
483 ctx.messages.push_back(std::move(forced));
484 ctx.metadata[
"terminal_reason"] =
"budget_exhausted";
485 set_state(ctx, AgentState::COMPLETE);
497void AgentEngine::execute_iteration(LoopContext& ctx) {
498 logger->info(
"[LOOP] iter {}/{} state={} msgs={}",
499 ctx.metrics.iterations,
500 resolve_max_iterations(ctx),
502 ctx.messages.size());
506 context_manager_.refresh_context_limit(ctx, 0);
507 context_manager_.prune_old_tool_results(ctx);
508 context_manager_.check_compaction(ctx);
512 set_state(ctx, AgentState::EXECUTING);
516 set_state(ctx, AgentState::COMPLETE);
520 auto result = response_generator_.generate_response(ctx);
521 dispatch_post_generate(ctx, result);
522 process_generation_result(ctx, result);
532void AgentEngine::process_generation_result(LoopContext& ctx,
533 GenerateResult& result) {
534 auto [cleaned, tool_calls] = parse_tool_calls(result.content);
535 logger->info(
"[ITER] finish={}, {} tool call(s), {} chars",
536 result.finish_reason, tool_calls.size(),
538 Message assistant_msg{
"assistant", cleaned};
539 ctx.messages.push_back(std::move(assistant_msg));
541 if (!tool_calls.empty() && tool_exec_.process_tool_calls !=
nullptr) {
542 process_tool_results(ctx, tool_calls);
544 evaluate_no_tool_decision(ctx, cleaned, result.finish_reason);
548 if (ctx.pending_delegation.has_value()) {
549 ctx.metrics.iterations--;
550 execute_pending_delegation(ctx);
551 }
else if (ctx.pending_pipeline.has_value()) {
552 ctx.metrics.iterations--;
553 execute_pending_pipeline(ctx);
565void AgentEngine::evaluate_no_tool_decision(
567 const std::string& content,
568 const std::string& finish_reason) {
569 if (handle_terminal_finish_reasons(ctx, finish_reason)) {
return; }
570 if (try_auto_chain(ctx, finish_reason, content)) {
571 logger->info(
"[DECISION] auto-chain triggered");
574 if (record_explicit_completion_failure(ctx, finish_reason)) {
return; }
575 if (response_generator_.is_response_complete(content,
"[]")) {
576 logger->info(
"[DECISION] response complete");
577 set_state(ctx, AgentState::COMPLETE);
594bool AgentEngine::handle_terminal_finish_reasons(
595 LoopContext& ctx,
const std::string& finish_reason) {
596 if (finish_reason ==
"interrupted") {
597 logger->info(
"[DECISION] interrupted");
598 set_state(ctx, AgentState::INTERRUPTED);
601 if (finish_reason ==
"length") {
602 logger->info(
"[DECISION] length, continuing");
618bool AgentEngine::record_explicit_completion_failure(
619 LoopContext& ctx,
const std::string& finish_reason) {
620 if (!tier_requires_explicit_completion(ctx.locked_tier)) {
623 auto& retries = ctx.metadata[
"zero_tool_call_retries"];
624 int n = retries.empty() ? 0 : std::atoi(retries.c_str());
625 logger->warn(
"[DECISION] tier '{}' requires explicit completion "
626 "but iteration emitted zero tool calls "
627 "(finish_reason={}) retry={}/2",
628 ctx.locked_tier, finish_reason, n);
629 ctx.metadata[
"failure_reason"] =
630 "zero_tool_calls_with_explicit_completion";
631 ctx.metadata[
"failure_tier"] = ctx.locked_tier;
633 logger->error(
"[DECISION] zero-tool-call retries exhausted "
634 "(tier={}), failing turn", ctx.locked_tier);
635 set_state(ctx, AgentState::ERROR);
639 correction.role =
"user";
641 "[SYSTEM] Your previous response contained no tool call. "
642 "You must end every turn with exactly one tool call "
643 "(entropic.complete, entropic.delegate, or entropic.inspect). "
645 ctx.messages.push_back(std::move(correction));
646 retries = std::to_string(n + 1);
647 set_state(ctx, AgentState::EXECUTING);
663bool AgentEngine::tier_requires_explicit_completion(
664 const std::string& tier)
const {
665 if (tier_res_.get_tier_param ==
nullptr) {
return false; }
666 auto val = tier_res_.get_tier_param(
667 tier,
"explicit_completion", tier_res_.user_data);
668 return val ==
"true" || val ==
"1";
684bool AgentEngine::is_delegation_cycle(
685 const LoopContext& ctx,
const std::string& target)
const {
688 if (anc == target) {
return true; }
698bool AgentEngine::is_delegation_repeat_blocked(
699 const LoopContext& ctx,
const std::string& target)
const {
702 >= loop_config_.max_consecutive_failed_delegations;
710bool AgentEngine::fold_complete_into_assistant(
712 auto tn_it = tool_result_msg.
metadata.find(
"tool_name");
713 bool is_complete = tn_it != tool_result_msg.
metadata.end()
714 && tn_it->second ==
"entropic.complete";
715 if (!is_complete || ctx.
messages.empty()) {
return false; }
717 auto sum_it = ctx.
metadata.find(
"explicit_completion_summary");
718 bool foldable = last.role ==
"assistant" && last.content.empty()
720 if (!foldable) {
return false; }
721 last.content = sum_it->second;
730int64_t AgentEngine::seconds_since_last_activity()
const {
731 auto last = last_activity_epoch_s_.load();
732 if (last == 0) {
return 0; }
733 auto now = std::chrono::duration_cast<std::chrono::seconds>(
734 std::chrono::system_clock::now().time_since_epoch()).count();
735 return now > last ? (now - last) : 0;
745bool AgentEngine::should_stop(
const LoopContext& ctx)
const {
746 bool terminal = ctx.
state == AgentState::COMPLETE
747 || ctx.
state == AgentState::ERROR
748 || ctx.
state == AgentState::INTERRUPTED;
751 return terminal || at_limit;
761void AgentEngine::set_state(LoopContext& ctx, AgentState state) {
762 auto prev = ctx.state;
764 logger->info(
"State: {}", agent_state_name(state));
765 if (callbacks_.on_state_change !=
nullptr) {
766 callbacks_.on_state_change(
767 static_cast<int>(state), callbacks_.user_data);
773 if (state_observer_ !=
nullptr) {
774 state_observer_(
static_cast<int>(state), state_observer_data_);
779 std::string json =
"{\"previous\":\""
781 +
"\",\"current\":\""
787 if (state == AgentState::ERROR) {
788 std::string json =
"{\"error_code\":\"STATE_ERROR\""
790 + std::to_string(ctx.metrics.iterations)
791 +
",\"consecutive\":" + std::to_string(ctx.consecutive_errors)
808void AgentEngine::interrupt() {
809 if (!interrupt_flag_.exchange(
true)) {
810 logger->info(
"Engine interrupted");
813 if (external_interrupt_cb_ !=
nullptr) {
814 external_interrupt_cb_(external_interrupt_data_);
817 pause_flag_.store(
false);
832void AgentEngine::set_external_interrupt(
void (*cb)(
void*),
834 external_interrupt_cb_ = cb;
835 external_interrupt_data_ = user_data;
843void AgentEngine::reset_interrupt() {
844 interrupt_flag_.store(
false);
852void AgentEngine::pause() {
853 logger->info(
"Engine paused");
854 pause_flag_.store(
true);
862void AgentEngine::cancel_pause() {
863 logger->info(
"Pause cancelled, interrupting");
864 pause_flag_.store(
false);
865 interrupt_flag_.store(
true);
875std::pair<int, int> AgentEngine::context_usage(
876 const std::vector<Message>& messages)
const {
877 int used = token_counter_.count_messages(messages);
878 return {used, token_counter_.max_tokens};
887void AgentEngine::reinject_context_anchors(
LoopContext& ctx) {
888 for (
const auto& [key, content] : context_anchors_) {
891 dir_anchor(ctx, d, r);
902void AgentEngine::register_directive_handlers() {
904 directive_processor_.register_handler(t,
905 [
this, fn](LoopContext& c,
const Directive& d,
906 DirectiveResult& r) {
907 (this->*fn)(c, d, r);
930void AgentEngine::dir_stop(
931 LoopContext&,
const Directive&, DirectiveResult& r) {
932 logger->info(
"[DIRECTIVE] stop_processing");
933 r.stop_processing =
true;
941void AgentEngine::dir_tier_change(
942 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
943 const auto& tc =
static_cast<const TierChangeDirective&
>(d);
944 ctx.locked_tier = tc.tier;
945 r.tier_changed =
true;
946 logger->info(
"[DIRECTIVE] tier_change: {}", tc.tier);
954void AgentEngine::dir_delegate(
955 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
956 const auto& dl =
static_cast<const DelegateDirective&
>(d);
957 ctx.pending_delegation = PendingDelegation{
958 dl.target, dl.task, dl.max_turns,
959 dl.resume_from_delegation_id};
960 r.stop_processing =
true;
961 if (dl.resume_from_delegation_id.empty()) {
962 logger->info(
"[DIRECTIVE] delegate: target={} task='{}'",
966 logger->info(
"[DIRECTIVE] resume_delegation: id={} task='{}'",
967 dl.resume_from_delegation_id, dl.task);
976void AgentEngine::dir_pipeline(
977 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
978 const auto& pl =
static_cast<const PipelineDirective&
>(d);
979 ctx.pending_pipeline = PendingPipeline{pl.stages, pl.task};
980 r.stop_processing =
true;
981 logger->info(
"[DIRECTIVE] pipeline: {} stages", pl.stages.size());
994void AgentEngine::dir_complete(
995 LoopContext& ctx,
const Directive& d, DirectiveResult& r) {
996 const auto& cd =
static_cast<const CompleteDirective&
>(d);
1000 auto prior_state = ctx.state;
1001 set_state(ctx, AgentState::VERIFYING);
1004 if (fire_complete_hook(cd.summary, ctx)) {
1005 logger->info(
"[DIRECTIVE] complete REJECTED by hook → revising");
1007 ctx.metadata[
"validator_phase"] =
"revising";
1008 set_state(ctx, prior_state);
1009 r.stop_processing =
false;
1013 ctx.metadata[
"explicit_completion_summary"] = cd.summary;
1020 if (cd.coverage_gap) {
1021 ctx.metadata[
"coverage_gap"] =
"true";
1022 ctx.metadata[
"gap_description"] = cd.gap_description;
1023 nlohmann::json files_arr = cd.suggested_files;
1024 ctx.metadata[
"suggested_files_json"] = files_arr.dump();
1026 set_state(ctx, AgentState::COMPLETE);
1027 r.stop_processing =
true;
1028 logger->info(
"[DIRECTIVE] complete coverage_gap={}",
1037void AgentEngine::dir_clear_todos(
1038 LoopContext&,
const Directive&, DirectiveResult&) {
1039 logger->debug(
"[DIRECTIVE] clear_self_todos (no-op)");
1047void AgentEngine::dir_inject(
1048 LoopContext&,
const Directive& d, DirectiveResult& r) {
1049 const auto& ic =
static_cast<const InjectContextDirective&
>(d);
1050 if (!ic.content.empty()) {
1053 msg.content = ic.content;
1054 r.injected_messages.push_back(std::move(msg));
1055 logger->info(
"[DIRECTIVE] inject_context");
1064void AgentEngine::dir_prune(
1065 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1066 const auto& pm =
static_cast<const PruneMessagesDirective&
>(d);
1067 auto [pruned, freed] = context_manager_.prune_tool_results(
1068 ctx, pm.keep_recent);
1069 logger->info(
"[DIRECTIVE] prune: {} results, {} chars", pruned, freed);
1077void AgentEngine::dir_anchor(
1078 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1079 const auto& ca =
static_cast<const ContextAnchorDirective&
>(d);
1080 if (ca.content.empty()) {
1081 context_anchors_.erase(ca.key);
1083 logger->info(
"Removed anchor: {}", ca.key);
1086 context_anchors_[ca.key] = ca.content;
1089 anchor.role =
"user";
1090 anchor.content = ca.content;
1091 anchor.metadata[
"is_context_anchor"] =
"true";
1092 anchor.metadata[
"anchor_key"] = ca.key;
1093 ctx.messages.push_back(std::move(anchor));
1094 logger->info(
"Updated anchor: {}", ca.key);
1102void AgentEngine::dir_phase(
1103 LoopContext& ctx,
const Directive& d, DirectiveResult&) {
1104 const auto& pc =
static_cast<const PhaseChangeDirective&
>(d);
1105 ctx.active_phase = pc.phase;
1106 logger->info(
"[DIRECTIVE] phase_change: {}", pc.phase);
1114void AgentEngine::dir_notify(
1115 LoopContext&,
const Directive& d, DirectiveResult&) {
1116 const auto& np =
static_cast<const NotifyPresenterDirective&
>(d);
1117 if (callbacks_.on_presenter_notify !=
nullptr) {
1118 callbacks_.on_presenter_notify(
1119 np.key.c_str(), np.data_json.c_str(),
1120 callbacks_.user_data);
1142 const nlohmann::json& obj,
const std::string& tc_str) {
1144 tc.
name = obj.value(
"name",
"");
1145 tc.
id =
"tc-" + std::to_string(
1146 std::hash<std::string>{}(tc.
name + tc_str) & 0xFFFF);
1147 if (obj.contains(
"arguments") && obj[
"arguments"].is_object()) {
1149 for (
auto& [k, v] : obj[
"arguments"].items()) {
1151 ? v.get<std::string>() : v.dump();
1165 const std::string& tc_str) {
1166 std::vector<ToolCall> calls;
1167 if (tc_str ==
"[]" || tc_str.empty()) {
return calls; }
1168 auto arr = nlohmann::json::parse(tc_str,
nullptr,
false);
1169 if (!arr.is_array()) {
return calls; }
1170 for (
const auto& obj : arr) {
1188std::pair<std::string, std::vector<ToolCall>>
1189AgentEngine::parse_tool_calls(
const std::string& raw_content) {
1190 if (inference_.parse_tool_calls ==
nullptr) {
1191 return {raw_content, {}};
1194 char* cleaned =
nullptr;
1195 char* tc_json =
nullptr;
1196 int rc = inference_.parse_tool_calls(
1197 raw_content.c_str(), &cleaned, &tc_json,
1198 inference_.adapter_data);
1200 std::string cleaned_str = cleaned ? cleaned : raw_content;
1201 std::string tc_str = tc_json ? tc_json :
"[]";
1203 if (inference_.free_fn !=
nullptr) {
1204 if (cleaned !=
nullptr) { inference_.free_fn(cleaned); }
1205 if (tc_json !=
nullptr) { inference_.free_fn(tc_json); }
1208 if (rc != 0) {
return {cleaned_str, {}}; }
1210 if (!calls.empty()) {
1211 logger->info(
"Parsed tool calls from model output");
1213 return {cleaned_str, std::move(calls)};
1223void AgentEngine::process_tool_results(
1225 const std::vector<ToolCall>& tool_calls) {
1226 set_state(ctx, AgentState::WAITING_TOOL);
1228 auto results = tool_exec_.process_tool_calls(
1229 ctx, tool_calls, tool_exec_.user_data);
1231 logger->info(
"[TOOLS] {} call(s) -> {} result message(s)",
1232 tool_calls.size(), results.size());
1233 for (
auto& msg : results) {
1234 if (fold_complete_into_assistant(ctx, msg)) {
1237 ctx.messages.push_back(std::move(msg));
1240 ctx.has_pending_tool_results =
true;
1242 if (ctx.state != AgentState::COMPLETE
1243 && ctx.state != AgentState::ERROR
1244 && ctx.state != AgentState::INTERRUPTED) {
1245 set_state(ctx, AgentState::EXECUTING);
1259 const std::string& key) {
1262 std::remove_if(msgs.begin(), msgs.end(),
1264 auto it = m.metadata.find(
"anchor_key");
1265 return it != m.metadata.end() && it->second == key;
1280bool AgentEngine::fire_pre_hook(
1282 if (hooks_.fire_pre ==
nullptr) {
1285 std::string json =
"{\"iteration\":"
1286 + std::to_string(iteration) +
"}";
1287 char* modified =
nullptr;
1288 int rc = hooks_.fire_pre(hooks_.registry,
1289 point, json.c_str(), &modified);
1305 static const auto tbl = []() {
1306 std::array<const char*, 256> t{};
1307 t[
static_cast<unsigned char>(
'"')] =
"\\\"";
1308 t[
static_cast<unsigned char>(
'\\')] =
"\\\\";
1309 t[
static_cast<unsigned char>(
'\n')] =
"\\n";
1310 t[
static_cast<unsigned char>(
'\r')] =
"\\r";
1311 t[
static_cast<unsigned char>(
'\t')] =
"\\t";
1327 out.reserve(s.size() + 16);
1329 const char* esc = tbl[
static_cast<unsigned char>(c)];
1330 if (esc) { out += esc; }
1348 const std::vector<Message>& messages) {
1349 std::string manifest;
1350 for (
const auto& msg : messages) {
1351 auto it = msg.metadata.find(
"tool_name");
1352 if (it == msg.metadata.end() || it->second.empty()) {
1360 auto orig = msg.metadata.find(
"original_content");
1361 size_t real_size = (orig != msg.metadata.end())
1362 ? orig->second.size()
1363 : msg.content.size();
1364 manifest +=
"- " + it->second
1365 +
" \xe2\x86\x92 " + std::to_string(real_size)
1380 size_t chars_per_result) {
1381 auto orig = msg.
metadata.find(
"original_content");
1382 const std::string& src = (orig != msg.
metadata.end())
1385 auto iter = msg.
metadata.find(
"added_at_iteration");
1386 std::string out =
"## ";
1387 out += msg.
metadata.at(
"tool_name");
1389 out +=
" [iter " + iter->second +
"]";
1392 if (src.size() <= chars_per_result) {
1395 out += src.substr(0, chars_per_result);
1396 out +=
"\n[... truncated, "
1397 + std::to_string(src.size() - chars_per_result)
1432 const std::vector<Message>& messages,
1434 size_t chars_per_result) {
1435 std::vector<const Message*> tool_msgs;
1436 for (
const auto& msg : messages) {
1437 auto it = msg.metadata.find(
"tool_name");
1438 if (it == msg.metadata.end() || it->second.empty()) {
1441 tool_msgs.push_back(&msg);
1443 if (tool_msgs.empty()) {
return {}; }
1445 size_t elided = (tool_msgs.size() > max_results)
1446 ? (tool_msgs.size() - max_results) : 0;
1447 size_t start = elided;
1450 out +=
"(" + std::to_string(elided)
1451 +
" earlier tool results elided for length)\n";
1453 for (
size_t i = start; i < tool_msgs.size(); ++i) {
1474 const std::string& base,
1476 if (tool_exec.
history_json ==
nullptr) {
return base; }
1478 if (js ==
nullptr) {
return base; }
1479 std::string out = base +
"prior-iteration history: " + js +
"\n";
1492 const std::vector<Message>& messages) {
1493 for (
const auto& msg : messages) {
1494 if (msg.role ==
"system") {
return msg.content; }
1514void AgentEngine::fire_post_generate_hook(
1515 GenerateResult& result,
1516 const std::string& tier,
1517 const std::vector<Message>& messages) {
1518 if (hooks_.fire_post ==
nullptr) {
1533 "{\"finish_reason\":\"" + result.finish_reason
1535 +
"\",\"tier\":\"" + tier
1539 char* out =
nullptr;
1540 hooks_.fire_post(hooks_.registry,
1542 if (out !=
nullptr) {
1543 result.content = out;
1544 logger->info(
"POST_GENERATE hook revised content");
1569void AgentEngine::dispatch_post_generate(
1570 LoopContext& ctx, GenerateResult& result) {
1575 ctx.pending_validation_feedback.clear();
1576 ctx.pending_anti_spiral_warning.clear();
1577 fire_post_generate_hook(result, ctx.locked_tier, ctx.messages);
1580 capture_validation_feedback(ctx);
1588void AgentEngine::capture_validation_feedback(LoopContext& ctx) {
1589 if (validation_provider_ ==
nullptr) {
return; }
1590 char* v = validation_provider_(validation_provider_data_);
1591 if (v ==
nullptr) {
return; }
1595 auto j = nlohmann::json::parse(raw);
1596 auto verdict = j.value(
"verdict",
"");
1597 if (verdict.rfind(
"rejected", 0) != 0) {
return; }
1599 if (j.contains(
"violations") && j[
"violations"].is_array()) {
1600 for (
const auto& vio : j[
"violations"]) {
1601 if (!joined.empty()) { joined +=
"; "; }
1602 joined += vio.is_string()
1603 ? vio.get<std::string>()
1607 if (joined.empty()) { joined = verdict; }
1608 ctx.pending_validation_feedback = joined;
1609 }
catch (
const nlohmann::json::exception&) {
1626 const std::vector<Message>& messages) {
1627 std::string arr =
"[";
1629 for (
const auto& msg : messages) {
1630 auto it = msg.metadata.find(
"tool_name");
1631 if (it == msg.metadata.end() || it->second.empty()) {
1634 if (!first) { arr +=
","; }
1664bool AgentEngine::fire_complete_hook(
1665 const std::string& summary,
1666 const LoopContext& ctx) {
1667 if (hooks_.fire_pre ==
nullptr) {
return false; }
1673 std::string validation_block =
",\"validation\":null";
1674 if (validation_provider_ !=
nullptr) {
1675 char* v = validation_provider_(validation_provider_data_);
1677 validation_block =
",\"validation\":" + std::string(v);
1683 +
"\",\"tier\":\"" + ctx.locked_tier
1684 +
"\",\"tool_results\":" + tool_results
1685 +
",\"iteration\":" + std::to_string(ctx.metrics.iterations)
1688 char* modified =
nullptr;
1689 int rc = hooks_.fire_pre(hooks_.registry,
1691 if (modified !=
nullptr) {
1694 feedback.role =
"user";
1695 feedback.content = std::string(
"[CITATION VALIDATION] ") + modified;
1696 const_cast<LoopContext&
>(ctx).messages.push_back(
1697 std::move(feedback));
1711bool AgentEngine::fire_delegate_pre_hook(
1712 const PendingDelegation& pending,
int depth) {
1713 if (hooks_.fire_pre ==
nullptr) {
1716 std::string json =
"{\"target_tier\":\""
1717 + pending.target +
"\",\"task\":\""
1718 + pending.task +
"\",\"depth\":"
1719 + std::to_string(depth) +
"}";
1720 char* modified =
nullptr;
1721 int rc = hooks_.fire_pre(hooks_.registry,
1743void AgentEngine::fire_delegate_complete_hook(
1744 const std::string& target,
bool success,
1745 const std::string& summary) {
1746 if (hooks_.fire_post ==
nullptr) {
1750 j[
"target_tier"] = target;
1751 j[
"success"] = success;
1753 ? ToolResultKind::ok
1755 j[
"summary"] = summary;
1756 std::string json = j.dump();
1757 char* out =
nullptr;
1758 hooks_.fire_post(hooks_.registry,
1773 auto* engine =
static_cast<AgentEngine*
>(user_data);
1785 reject.
role =
"user";
1786 reject.
content =
"[DELEGATION REJECTED] Maximum delegation "
1787 "depth (" + std::to_string(
1788 AgentEngine::MAX_DELEGATION_DEPTH) +
1790 ctx.
messages.push_back(std::move(reject));
1807 chain += anc +
" -> ";
1812 reject.
role =
"user";
1813 reject.
content =
"[DELEGATION REJECTED] Circular delegation "
1814 "detected: tier '" + target +
1815 "' already in the active delegation chain "
1816 "(" + chain +
"). Choose a different target.";
1817 ctx.
metadata[
"failure_reason"] =
"delegation_cycle";
1818 ctx.
metadata[
"failure_target"] = target;
1819 ctx.
messages.push_back(std::move(reject));
1832 std::string tag = result.
success ?
"COMPLETE" :
"FAILED";
1835 msg.
content =
"[DELEGATION " + tag +
": " + target +
"] " + result.
summary;
1836 ctx.
messages.push_back(std::move(msg));
1851 LoopContext& ctx,
const std::string& target,
int n) {
1853 reject.
role =
"user";
1854 reject.
content =
"[DELEGATION REJECTED] '" + target
1855 +
"' has just failed " + std::to_string(n)
1856 +
" times in a row. Stop retrying this target. Either "
1857 "respond to the user with what you have, or delegate to "
1858 "a different tier.";
1859 ctx.
metadata[
"failure_reason"] =
"delegation_repeat_blocked";
1860 ctx.
metadata[
"failure_target"] = target;
1861 ctx.
messages.push_back(std::move(reject));
1872bool AgentEngine::reject_delegation_if_guarded(
1873 LoopContext& ctx,
const PendingDelegation& pending) {
1874 bool rejected =
true;
1875 if (ctx.delegation_depth >= MAX_DELEGATION_DEPTH) {
1876 logger->warn(
"Delegation rejected: depth {} >= max {}",
1877 ctx.delegation_depth, MAX_DELEGATION_DEPTH);
1878 push_delegation_rejected(ctx);
1879 }
else if (is_delegation_cycle(ctx, pending.target)) {
1881 logger->warn(
"Delegation rejected: cycle on target tier '{}'",
1884 }
else if (is_delegation_repeat_blocked(ctx, pending.target)) {
1889 logger->warn(
"Delegation rejected: '{}' failed {}x in a row "
1890 "(>= max_consecutive_failed_delegations={})",
1892 ctx.consecutive_failed_delegations,
1893 loop_config_.max_consecutive_failed_delegations);
1895 ctx, pending.target, ctx.consecutive_failed_delegations);
1914void AgentEngine::execute_pending_delegation(LoopContext& ctx) {
1915 auto pending = std::move(*ctx.pending_delegation);
1916 ctx.pending_delegation.reset();
1918 if (reject_delegation_if_guarded(ctx, pending)) {
return; }
1920 set_state(ctx, AgentState::DELEGATING);
1929 std::vector<Message> resume_history;
1930 bool blocked =
false;
1931 if (!pending.resume_from_delegation_id.empty()
1932 && !resolve_resume_delegation(ctx, pending, resume_history)) {
1934 }
else if (fire_delegate_pre_hook(pending, ctx.delegation_depth)) {
1935 logger->info(
"ON_DELEGATE hook cancelled delegation");
1939 set_state(ctx, AgentState::EXECUTING);
1943 fire_delegation_start(ctx, pending.target, pending.task);
1944 auto result = run_pending_delegation(
1945 ctx, pending, std::move(resume_history));
1949 if (result.success) {
1950 ctx.last_failed_delegation_target.clear();
1951 ctx.consecutive_failed_delegations = 0;
1952 }
else if (pending.target == ctx.last_failed_delegation_target) {
1953 ++ctx.consecutive_failed_delegations;
1955 ctx.last_failed_delegation_target = pending.target;
1956 ctx.consecutive_failed_delegations = 1;
1959 fire_delegation_complete(ctx, pending.target, result);
1960 fire_delegate_complete_hook(pending.target, result.success,
1963 finalize_delegation_result(ctx, result);
1978void AgentEngine::relay_partial_result(
1979 LoopContext& ctx,
const std::string& summary) {
1980 GenerateResult relay_result;
1981 relay_result.content = summary;
1982 relay_result.finish_reason =
"stop";
1983 relay_result.tool_calls_json =
"[]";
1984 fire_post_generate_hook(
1985 relay_result, ctx.locked_tier, ctx.messages);
1986 ctx.metadata[
"explicit_completion_summary"] = relay_result.content;
1987 set_state(ctx, AgentState::COMPLETE);
1999 "[COVERAGE GAP from " + tier +
"]\n"
2000 "Summary so far: " + result.
summary +
"\n"
2003 body +=
"\nSuggested files to inspect:";
2005 body +=
"\n - " + f;
2031void AgentEngine::finalize_delegation_result(
2032 LoopContext& ctx,
const DelegationResult& result) {
2035 const bool in_relay_tier =
2036 relay_single_delegate_tiers_.count(ctx.locked_tier) > 0;
2037 if (in_relay_tier && result.coverage_gap) {
2040 gap.content = build_coverage_gap_message(
2041 result.target_tier, result);
2042 ctx.messages.push_back(std::move(gap));
2043 ctx.metadata[
"relay_status"] =
"coverage_gap_suppressed";
2045 "[COVERAGE GAP] suppressing auto-relay for tier={} "
2046 "({} suggested files)",
2047 result.target_tier, result.suggested_files.size());
2048 set_state(ctx, AgentState::EXECUTING);
2051 if (result.success && in_relay_tier) {
2052 relay_partial_result(ctx, result.summary);
2053 log_relay_status(ctx);
2056 if (!result.terminal_reason.empty() && in_relay_tier) {
2057 relay_partial_result(ctx,
2058 "[partial — budget_exhausted] " + result.summary);
2059 log_relay_status(ctx, result.terminal_reason);
2062 bool needs_explicit = tier_requires_explicit_completion(
2064 set_state(ctx, (result.success && !needs_explicit)
2065 ? AgentState::COMPLETE :
AgentState::EXECUTING);
2082void AgentEngine::log_relay_status(LoopContext& ctx,
2083 const std::string& terminal_reason) {
2084 std::string verdict;
2085 if (validation_provider_ !=
nullptr) {
2086 char* v = validation_provider_(validation_provider_data_);
2090 auto pos = jv.find(
"\"verdict\":\"");
2091 if (pos != std::string::npos) {
2093 auto end = jv.find(
'"', pos);
2094 if (end != std::string::npos) {
2095 verdict = jv.substr(pos, end - pos);
2100 if (!terminal_reason.empty()) {
2101 ctx.metadata[
"relay_status"] =
"budget_exhausted_relayed";
2103 "Relay: single-delegate result used "
2104 "(partial — terminal_reason={}, verdict={})",
2105 terminal_reason, verdict.empty() ?
"none" : verdict);
2108 if (verdict ==
"skipped" || verdict.empty()) {
2109 ctx.metadata[
"relay_status"] =
"validation_skipped";
2111 "Relay: single-delegate result used "
2112 "(lead validation skipped per config)");
2114 ctx.metadata[
"relay_status"] =
"validated";
2116 "Relay: single-delegate result used "
2117 "(passed lead validation)");
2127void AgentEngine::execute_pending_pipeline(LoopContext& ctx) {
2128 auto pending = std::move(*ctx.pending_pipeline);
2129 ctx.pending_pipeline.reset();
2131 if (ctx.delegation_depth >= MAX_DELEGATION_DEPTH) {
2132 logger->warn(
"Pipeline rejected: depth {} >= max {}",
2133 ctx.delegation_depth, MAX_DELEGATION_DEPTH);
2135 reject.role =
"user";
2136 reject.content =
"[PIPELINE REJECTED] Maximum delegation "
2138 ctx.messages.push_back(std::move(reject));
2142 set_state(ctx, AgentState::DELEGATING);
2145 auto repo_dir = get_repo_dir();
2146 DelegationManager mgr(run_child_loop_trampoline,
this,
2147 tier_res_, repo_dir,
2148 ensure_sandbox_manager());
2149 if (storage_.create_delegation !=
nullptr) {
2150 mgr.set_storage(&storage_);
2155 auto cb_snap = delegation_callbacks_snapshot();
2156 mgr.set_delegation_callbacks(
2157 cb_snap.start, cb_snap.complete, cb_snap.user_data);
2158 auto result = mgr.execute_pipeline(
2159 ctx, pending.stages, pending.task);
2161 std::string tag = result.success ?
"COMPLETE" :
"FAILED";
2163 result_msg.role =
"user";
2164 result_msg.content =
"[PIPELINE " + tag +
"] " + result.summary;
2165 ctx.messages.push_back(std::move(result_msg));
2167 set_state(ctx, AgentState::EXECUTING);
2178void AgentEngine::fire_delegation_start(
2179 const LoopContext& ,
2180 const std::string& tier,
2181 const std::string& task) {
2182 if (callbacks_.on_delegation_start !=
nullptr) {
2183 callbacks_.on_delegation_start(
2184 "", tier.c_str(), task.c_str(),
2185 callbacks_.user_data);
2197void AgentEngine::fire_delegation_complete(
2198 const LoopContext& ,
2199 const std::string& tier,
2200 const DelegationResult& result) {
2201 if (callbacks_.on_delegation_complete !=
nullptr) {
2202 callbacks_.on_delegation_complete(
2203 "", tier.c_str(), result.summary.c_str(),
2204 result.success ? 1 : 0,
2205 callbacks_.user_data);
2220bool AgentEngine::should_auto_chain(
2221 const LoopContext& ctx,
2222 const std::string& finish_reason,
2223 const std::string& content) {
2224 if (ctx.locked_tier.empty() || tier_res_.get_tier_param ==
nullptr) {
2228 std::string auto_chain = tier_res_.get_tier_param(
2229 ctx.locked_tier,
"auto_chain", tier_res_.user_data);
2230 if (auto_chain.empty()) {
2234 bool triggered = (finish_reason ==
"length") ||
2235 (finish_reason ==
"stop" &&
2236 response_generator_.is_response_complete(content,
"[]"));
2249bool AgentEngine::try_auto_chain(
2251 const std::string& finish_reason,
2252 const std::string& content) {
2253 if (!should_auto_chain(ctx, finish_reason, content)) {
2257 if (ctx.delegation_depth > 0) {
2258 logger->info(
"[AUTO-CHAIN] child depth={}, completing",
2259 ctx.delegation_depth);
2260 set_state(ctx, AgentState::COMPLETE);
2265 std::string target = tier_res_.get_tier_param(
2266 ctx.locked_tier,
"auto_chain", tier_res_.user_data);
2268 if (!target.empty()) {
2269 logger->info(
"[AUTO-CHAIN] root, tier change to '{}'", target);
2270 TierChangeDirective tc(target,
"auto_chain");
2272 dir_tier_change(ctx, tc, r);
2274 return !target.empty();
2299std::filesystem::path AgentEngine::get_repo_dir() {
2300 if (repo_dir_checked_) {
2301 return cached_repo_dir_.value_or(std::filesystem::path{});
2303 repo_dir_checked_ =
true;
2304 std::filesystem::path resolved;
2305 if (!project_dir_override_.empty()) {
2306 resolved = project_dir_override_;
2307 logger->info(
"Project dir for sandbox snapshots: {} (configured)",
2310 resolved = std::filesystem::current_path();
2311 logger->info(
"Project dir for sandbox snapshots: {} (cwd fallback)",
2314 cached_repo_dir_ = resolved;
2329void AgentEngine::set_project_dir(
const std::filesystem::path& project_dir) {
2330 project_dir_override_ = project_dir;
2331 cached_repo_dir_.reset();
2332 repo_dir_checked_ =
false;
2352 const std::string& reason,
2353 const std::string& delegation_id) {
2356 m.
content =
"[DELEGATION FAILED: resume_delegation] "
2357 + reason +
" (delegation_id=" + delegation_id +
")";
2358 ctx.
messages.push_back(std::move(m));
2374bool AgentEngine::fetch_resume_payload(
2376 const std::string&
id,
2377 nlohmann::json& parsed) {
2378 std::optional<std::string> error;
2380 if (storage_.load_delegation_with_messages ==
nullptr) {
2381 error =
"storage unavailable";
2382 }
else if (!storage_.load_delegation_with_messages(
2383 id.c_str(), raw, storage_.user_data)) {
2384 error =
"unknown delegation_id";
2386 parsed = nlohmann::json::parse(raw,
nullptr,
false);
2387 if (parsed.is_discarded() || !parsed.is_object()) {
2388 error =
"malformed storage payload";
2391 if (
error.has_value()) {
2392 logger->error(
"resume_delegation '{}': {}",
id, *error);
2404bool AgentEngine::resolve_resume_delegation(
2406 PendingDelegation& pending,
2407 std::vector<Message>& out_history) {
2408 const auto&
id = pending.resume_from_delegation_id;
2410 if (!fetch_resume_payload(ctx,
id, j)) {
2413 auto target = j.value(
"target_tier", std::string{});
2414 if (target.empty()) {
2418 pending.target = target;
2419 if (j.contains(
"messages") && j[
"messages"].is_array()) {
2420 for (
const auto& mj : j[
"messages"]) {
2422 m.role = mj.value(
"role",
"");
2423 m.content = mj.value(
"content",
"");
2424 if (!m.role.empty()) {
2425 out_history.push_back(std::move(m));
2429 logger->info(
"resume_delegation '{}': loaded {} messages, target='{}'",
2430 id, out_history.size(), target);
2448DelegationResult AgentEngine::run_pending_delegation(
2450 const PendingDelegation& pending,
2451 std::vector<Message> resume_history) {
2452 std::optional<int> max_turns;
2453 if (pending.max_turns > 0) { max_turns = pending.max_turns; }
2454 DelegationManager mgr(run_child_loop_trampoline,
this,
2455 tier_res_, get_repo_dir(),
2456 ensure_sandbox_manager());
2457 if (storage_.create_delegation !=
nullptr) {
2458 mgr.set_storage(&storage_);
2460 auto cb_snap = delegation_callbacks_snapshot();
2461 mgr.set_delegation_callbacks(
2462 cb_snap.start, cb_snap.complete, cb_snap.user_data);
2463 if (resume_history.empty()) {
2464 return mgr.execute_delegation(
2465 ctx, pending.target, pending.task, max_turns);
2467 return mgr.execute_resume_delegation(
2468 ctx, pending.target, pending.task,
2469 std::move(resume_history), max_turns);
2477SandboxManager* AgentEngine::ensure_sandbox_manager() {
2479 return &*sandbox_mgr_;
2481 auto repo_dir = get_repo_dir();
2482 if (repo_dir.empty()) {
2485 sandbox_mgr_.emplace(repo_dir);
2486 return &*sandbox_mgr_;
2497void AgentEngine::set_system_prompt(
const std::string& prompt) {
2498 system_prompt_ = prompt;
2508 session_logger_ = log;
2523std::vector<Message> AgentEngine::run_turn(
const std::string& input) {
2530 running_flag_.store(
true);
2531 if (conversation_.empty() && !system_prompt_.empty()) {
2533 sys.
role =
"system";
2535 conversation_.push_back(std::move(sys));
2537 std::string pending = input;
2538 std::vector<Message> result;
2543 conversation_.push_back(std::move(usr));
2545 size_t sent_len = conversation_.size();
2546 result = run(conversation_);
2547 for (
size_t i = sent_len; i < result.size(); ++i) {
2548 conversation_.push_back(result[i]);
2550 auto next = pop_queued_user_message();
2551 if (!next.has_value()) {
break; }
2552 fire_queue_consumed(*next, user_message_queue_depth());
2553 pending = std::move(*next);
2555 running_flag_.store(
false);
2581void AgentEngine::seed_system_prompt(
2582 const std::vector<Message>& new_messages) {
2583 bool caller_has_system =
false;
2584 for (
const auto& m : new_messages) {
2585 if (m.role ==
"system") { caller_has_system =
true;
break; }
2587 if (conversation_.empty() && !system_prompt_.empty()
2588 && !caller_has_system) {
2590 sys.
role =
"system";
2592 conversation_.push_back(std::move(sys));
2603bool AgentEngine::prepare_next_turn(std::vector<Message>& pending) {
2604 auto next = pop_queued_user_message();
2605 if (!next.has_value()) {
return false; }
2606 fire_queue_consumed(*next, user_message_queue_depth());
2609 usr.
content = std::move(*next);
2611 pending.push_back(std::move(usr));
2622std::vector<Message> AgentEngine::run_turn(std::vector<Message> new_messages) {
2627 running_flag_.store(
true);
2628 seed_system_prompt(new_messages);
2629 std::vector<Message> pending = std::move(new_messages);
2630 std::vector<Message> result;
2632 for (
auto& m : pending) {
2633 conversation_.push_back(std::move(m));
2635 size_t sent_len = conversation_.size();
2636 result = run(conversation_);
2637 for (
size_t i = sent_len; i < result.size(); ++i) {
2638 conversation_.push_back(result[i]);
2640 if (!prepare_next_turn(pending)) {
break; }
2642 running_flag_.store(
false);
2656int AgentEngine::run_streaming(
2657 const std::string& input,
2662 if (session_logger_) {
2663 session_logger_->log_user_input(input);
2667 if (session_logger_ && session_logger_->is_open()) {
2669 SessionLogger::raw_token_callback,
2678 Ctx sctx{&filter, cancel_flag,
this};
2682 auto* c =
static_cast<Ctx*
>(ud);
2683 if (c->cancel && *c->cancel) {
2684 c->engine->interrupt();
2687 c->filter->on_token(t, l);
2689 cbs.user_data = &sctx;
2692 auto result = run_turn(input);
2695 if (session_logger_) {
2696 session_logger_->end_turn();
2698 if (cancel_flag && *cancel_flag) {
return 1; }
2710 const std::vector<Message>& messages) {
2712 for (
const auto& m : messages) {
2713 if (m.role !=
"user" || m.content.empty()) {
continue; }
2714 if (!echo.empty()) { echo +=
'\n'; }
2735int AgentEngine::run_streaming(
2736 std::vector<Message> new_messages,
2741 if (session_logger_) {
2746 if (session_logger_ && session_logger_->is_open()) {
2748 SessionLogger::raw_token_callback,
2757 Ctx sctx{&filter, cancel_flag,
this};
2761 auto* c =
static_cast<Ctx*
>(ud);
2762 if (c->cancel && *c->cancel) {
2763 c->engine->interrupt();
2766 c->filter->on_token(t, l);
2768 cbs.user_data = &sctx;
2771 auto result = run_turn(std::move(new_messages));
2774 if (session_logger_) {
2775 session_logger_->end_turn();
2777 if (cancel_flag && *cancel_flag) {
return 1; }
2786void AgentEngine::clear_conversation() {
2787 conversation_.clear();
2788 logger->info(
"conversation cleared");
2797size_t AgentEngine::message_count()
const {
2798 return conversation_.size();
2807const std::vector<Message>& AgentEngine::get_messages()
const {
2808 return conversation_;
2818bool AgentEngine::queue_user_message(
const std::string& message) {
2819 std::lock_guard lock(queue_mutex_);
2820 int cap = loop_config_.message_queue_capacity;
2821 if (cap < 0) { cap = 0; }
2822 if (user_message_queue_.size()
2823 >=
static_cast<size_t>(cap)) {
2826 user_message_queue_.push_back(message);
2827 logger->info(
"queued mid-gen user message: depth={}",
2828 user_message_queue_.size());
2837size_t AgentEngine::user_message_queue_depth()
const {
2838 std::lock_guard lock(queue_mutex_);
2839 return user_message_queue_.size();
2847void AgentEngine::clear_user_message_queue() {
2848 std::lock_guard lock(queue_mutex_);
2849 size_t dropped = user_message_queue_.size();
2850 user_message_queue_.clear();
2852 logger->info(
"cleared mid-gen queue: dropped={}", dropped);
2861void AgentEngine::set_message_queue_capacity(
int cap) {
2862 std::lock_guard lock(queue_mutex_);
2863 loop_config_.message_queue_capacity = cap < 0 ? 0 : cap;
2871std::optional<std::string>
2872AgentEngine::pop_queued_user_message() {
2873 std::lock_guard lock(queue_mutex_);
2874 if (user_message_queue_.empty()) {
2875 return std::nullopt;
2877 std::string front = std::move(user_message_queue_.front());
2878 user_message_queue_.pop_front();
2887void AgentEngine::fire_queue_consumed(
const std::string& consumed,
2889 if (queue_observer_ !=
nullptr) {
2891 consumed.c_str(), remaining, queue_observer_data_);
2900void AgentEngine::set_queue_observer(
2901 void (*observer)(
const char*,
size_t,
void*),
2903 queue_observer_ = observer;
2904 queue_observer_data_ = user_data;
2918void AgentEngine::set_state_observer(
2919 void (*observer)(
int,
void*),
2921 state_observer_ = observer;
2922 state_observer_data_ = user_data;
2923 response_generator_.set_state_observer(observer, user_data);
2938 const std::vector<const Directive*>& dirs,
2941 ->directive_processor().process(ctx, dirs);
2956void AgentEngine::set_tier_info(
2957 const std::string& name,
2960 tier_info_[name] = info;
2969void AgentEngine::set_relay_single_delegate(
const std::string& name) {
2970 relay_single_delegate_tiers_.insert(name);
2979void AgentEngine::set_handoff_rules(
2980 const std::unordered_map<std::string,
2981 std::vector<std::string>>& rules)
2983 handoff_rules_ = rules;
2984 wire_internal_tier_resolution();
2993 const std::string& name,
void* ud) {
2995 auto it = self->tier_info_.find(name);
2997 if (it == self->tier_info_.end()) {
3013bool AgentEngine::tri_tier_exists(
const std::string& name,
void* ud) {
3014 auto* self =
static_cast<AgentEngine*
>(ud);
3015 return self->tier_info_.count(name) > 0;
3026std::vector<std::string> AgentEngine::tri_get_handoff_targets(
3027 const std::string& name,
void* ud) {
3028 auto* self =
static_cast<AgentEngine*
>(ud);
3029 auto it = self->handoff_rules_.find(name);
3030 std::vector<std::string> result;
3031 if (it != self->handoff_rules_.end()) { result = it->second; }
3058std::string AgentEngine::tri_get_tier_param(
const std::string& name,
3059 const std::string& param,
void* ud) {
3060 auto* self =
static_cast<AgentEngine*
>(ud);
3061 auto it = self->tier_info_.find(name);
3062 if (it == self->tier_info_.end()) {
return ""; }
3063 const auto& info = it->second;
3065 if (param ==
"explicit_completion") {
3066 result = info.explicit_completion ?
"true" :
"false";
3067 }
else if (param ==
"max_iterations"
3068 && info.max_iterations_override >= 0) {
3069 result = std::to_string(info.max_iterations_override);
3070 }
else if (param ==
"max_tool_calls_per_turn"
3071 && info.max_tool_calls_per_turn_override >= 0) {
3072 result = std::to_string(info.max_tool_calls_per_turn_override);
3082void AgentEngine::wire_internal_tier_resolution() {
3083 TierResolutionInterface tri;
3084 tri.resolve_tier = &AgentEngine::tri_resolve_tier;
3085 tri.tier_exists = &AgentEngine::tri_tier_exists;
3086 tri.get_handoff_targets = &AgentEngine::tri_get_handoff_targets;
3087 tri.get_tier_param = &AgentEngine::tri_get_tier_param;
3088 tri.user_data =
this;
3089 set_tier_resolution(tri);
Core agent execution engine.
void run_loop(LoopContext &ctx)
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.
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.