13#include <nlohmann/json.hpp>
38 : server_manager_(server_manager),
39 loop_config_(loop_config),
40 callbacks_(callbacks),
51 permission_persist_ = persist;
64 const std::vector<ToolCall>& tool_calls) {
65 logger->info(
"Processing {} tool calls", tool_calls.size());
66 ctx.
state = AgentState::WAITING_TOOL;
67 fire_state_callback(ctx);
69 auto limited = sort_tool_calls(tool_calls);
73 truncate_to_limit(limited, eff_limit);
75 std::vector<Message> results;
76 for (
const auto& call : limited) {
77 auto msgs = process_single_call(ctx, call);
78 for (
auto& m : msgs) {
79 results.push_back(std::move(m));
81 if (should_stop_batch(ctx, results)) {
97std::vector<ToolCall> ToolExecutor::sort_tool_calls(
98 const std::vector<ToolCall>& calls) {
100 std::stable_sort(sorted.begin(), sorted.end(),
102 bool a_delegate = (a.name ==
"entropic.delegate");
103 bool b_delegate = (b.name ==
"entropic.delegate");
104 return !a_delegate && b_delegate;
117std::string ToolExecutor::check_duplicate(
118 const LoopContext& ctx,
119 const ToolCall& call)
const {
123 auto key = tool_call_key(call);
124 auto it = ctx.recent_tool_calls.find(key);
125 if (it != ctx.recent_tool_calls.end()) {
140Message ToolExecutor::handle_duplicate(
142 const ToolCall& call,
143 const std::string& previous_result) {
144 ctx.consecutive_duplicate_attempts++;
145 logger->warn(
"Duplicate tool call #{}: {}",
146 ctx.consecutive_duplicate_attempts, call.name);
148 if (ctx.consecutive_duplicate_attempts >= 3) {
149 return create_circuit_breaker_message();
151 return create_duplicate_message(call, previous_result);
161bool ToolExecutor::check_approval(
const ToolCall& call) {
162 auto args_json = serialize_args(call);
165 call.name, args_json);
167 bool approved = auto_ok;
169 auto call_json = serialize_tool_call(call);
176 if (hook_iface_.fire_info !=
nullptr) {
177 std::string perm = approved ?
"allowed" :
"denied";
178 std::string json =
"{\"tool_name\":\""
179 + call.name +
"\",\"permission\":\"" + perm +
"\"}";
180 hook_iface_.fire_info(hook_iface_.registry,
185 logger->warn(
"No approval callback — denying: {}", call.name);
198std::optional<Message> ToolExecutor::check_tier_allowed(
199 const LoopContext& ctx,
const ToolCall& call)
const {
206 if (ctx.locked_tier.empty() || tier_allowed_tools_ ==
nullptr) {
209 auto it = tier_allowed_tools_->find(ctx.locked_tier);
213 bool authorized = it == tier_allowed_tools_->end()
214 || it->second.empty()
215 || std::find(it->second.begin(), it->second.end(),
216 call.name) != it->second.end();
220 logger->warn(
"Tool '{}' not in tier '{}' allowed_tools — rejecting",
221 call.name, ctx.locked_tier);
222 return create_denied_message(
223 call,
"tier '" + ctx.locked_tier +
"' is not authorized to call '"
236 const nlohmann::json& schema,
237 const nlohmann::json& args)
239 for (
const auto& req : schema.value(
"required",
240 nlohmann::json::array())) {
241 if (!args.contains(req.get<std::string>())) {
242 return "Missing required argument: "
243 + req.get<std::string>();
268 const std::string& key,
269 const nlohmann::json& allowed,
270 const nlohmann::json& val)
272 for (
const auto& e : allowed) {
273 if (e == val) {
return ""; }
275 return "Invalid value for '" + key +
"': "
276 + val.dump() +
". Must be one of: " + allowed.dump();
289 const std::string& key,
290 const std::string& type,
291 const nlohmann::json& val)
293 bool ok = (type ==
"string" && val.is_string())
294 || (type ==
"integer" && val.is_number_integer())
295 || (type ==
"number" && val.is_number())
296 || (type ==
"boolean" && val.is_boolean())
297 || (type ==
"array" && val.is_array())
298 || (type ==
"object" && val.is_object());
299 return ok ?
"" :
"Type mismatch for '" + key
300 +
"': expected " + type;
313 const std::string& key,
314 const nlohmann::json& prop,
315 const nlohmann::json& val)
317 if (prop.contains(
"enum")) {
318 auto err =
check_enum(key, prop[
"enum"], val);
319 if (!err.empty()) {
return err; }
321 if (!prop.contains(
"type")) {
return ""; }
322 return check_type(key, prop[
"type"].get<std::string>(), val);
338 const std::string& schema_json,
339 const nlohmann::json& args)
341 auto schema = nlohmann::json::parse(schema_json,
nullptr,
false);
342 if (!schema.is_object()) {
return ""; }
345 auto props = schema.value(
"properties", nlohmann::json::object());
346 for (
auto it = props.begin(); it != props.end() && err.empty(); ++it) {
347 if (args.contains(it.key())) {
349 it.key(), it.value(), args[it.key()]);
364 auto j = nlohmann::json::parse(result_json);
365 return j.value(
"result", result_json);
379std::pair<Message, std::string> ToolExecutor::execute_tool(
380 LoopContext& ctx,
const ToolCall& call) {
382 auto args_json = serialize_args(call);
385 auto call_json = serialize_tool_call(call);
390 auto start = std::chrono::steady_clock::now();
397 auto result_json = mcp::sanitize_utf8(
398 server_manager_.
execute(call.name, args_json));
399 auto end = std::chrono::steady_clock::now();
400 auto ms = std::chrono::duration_cast<
401 std::chrono::milliseconds>(end - start).count();
403 ctx.metrics.tool_calls++;
411 record_tool_history(call, args_json, result_text, ms,
412 ctx.metrics.iterations);
414 fire_tool_complete_callback(call, result_text, ms);
418 msg.content = result_text;
419 msg.metadata[
"tool_call_id"] = call.id;
420 msg.metadata[
"tool_name"] = call.name;
422 return {std::move(msg), result_json};
435void ToolExecutor::record_tool_history(
const ToolCall& call,
436 const std::string& args_json,
437 const std::string& result_text,
438 long long ms,
int iteration) {
440 rec.sequence = ++history_seq_;
441 rec.tool_name = call.name;
443 rec.status = (result_text.rfind(
"error", 0) == 0)
444 ?
"error" :
"success";
446 rec.elapsed_ms =
static_cast<double>(ms);
447 rec.iteration = iteration;
458std::string ToolExecutor::tool_call_key(
const ToolCall& call) {
461 for (
const auto& [k, v] : call.arguments) {
464 return call.name +
":" + args.dump();
475void ToolExecutor::record_tool_call(
477 const ToolCall& call,
478 const std::string& result) {
480 std::string text = result;
482 auto j = nlohmann::json::parse(result);
483 text = j.value(
"result", result);
487 if (text.find(
"Error:") == 0 || text.find(
"error:") == 0) {
490 auto key = tool_call_key(call);
491 ctx.recent_tool_calls[key] = text;
502Message ToolExecutor::create_denied_message(
503 const ToolCall& call,
504 const std::string& reason) {
508 "Tool `" + call.name +
"` was denied: " + reason +
"\n\n"
509 "This tool is not available to you. Do NOT retry it. "
510 "Use a different approach to accomplish your task.";
522Message ToolExecutor::create_error_message(
523 const ToolCall& call,
524 const std::string&
error) {
528 "Tool `" + call.name +
"` failed with error: " +
error +
530 "- Check arguments are correct\n"
531 "- Try a different approach\n"
532 "- Do NOT retry with the same arguments";
544void ToolExecutor::fire_state_callback(
const LoopContext& ctx) {
547 static_cast<int>(ctx.state), callbacks_.
user_data);
558void ToolExecutor::truncate_to_limit(
559 std::vector<ToolCall>& calls,
561 auto lim =
static_cast<size_t>(limit);
562 if (calls.size() > lim) {
575std::optional<Message> ToolExecutor::check_mcp_authorization(
576 const LoopContext& ctx,
577 const ToolCall& call)
const {
578 if (auth_mgr_ ==
nullptr) {
581 auto identity = ctx.locked_tier.empty()
582 ?
"lead" : ctx.locked_tier;
586 auth_mgr_->
check_access(identity, call.name, required)) {
590 logger->warn(
"MCP key denied: {} requires {} for {}",
591 call.name, level_str, identity);
595 "Tool `" + call.name +
"` was denied: identity `"
596 + identity +
"` lacks " + level_str
598 "Your MCP key set does not authorize this tool. "
599 "Use `entropic.delegate` to hand off to an identity "
600 "that has the required access.";
612std::optional<Message> ToolExecutor::check_dup_or_approval(
613 LoopContext& ctx,
const ToolCall& call) {
614 auto dup_result = check_duplicate(ctx, call);
615 if (!dup_result.empty()) {
616 return handle_duplicate(ctx, call, dup_result);
618 ctx.consecutive_duplicate_attempts = 0;
619 return check_approval(call)
621 : std::optional{create_denied_message(
622 call,
"Permission denied")};
640std::optional<Message> ToolExecutor::check_schema(
641 const ToolCall& call) {
643 if (schema.empty()) {
return std::nullopt; }
644 auto args = nlohmann::json::parse(
645 serialize_args(call),
nullptr,
false);
646 auto err = args.is_discarded()
648 if (err.empty()) {
return std::nullopt; }
649 logger->warn(
"Tool '{}' argument validation failed: {}",
651 return create_denied_message(call, err);
667PreconditionCheck ToolExecutor::check_call_preconditions(
668 LoopContext& ctx,
const ToolCall& call) {
673 PreconditionCheck pc = check_anti_spiral_hard_block(ctx, call);
674 if (pc.rejection.has_value()) {
677 if (
auto r = check_schema(call); r.has_value()) {
678 pc.rejection = std::move(r);
680 }
else if (
auto a = check_mcp_authorization(ctx, call);
682 pc.rejection = std::move(a);
684 }
else if (
auto t = check_tier_allowed(ctx, call); t.has_value()) {
685 pc.rejection = std::move(t);
687 }
else if (
auto dup = check_duplicate(ctx, call); !
dup.empty()) {
688 pc.rejection = handle_duplicate(ctx, call,
dup);
691 pc = check_approval_pc(ctx, call);
705PreconditionCheck ToolExecutor::check_approval_pc(
706 LoopContext& ctx,
const ToolCall& call) {
707 PreconditionCheck pc;
708 ctx.consecutive_duplicate_attempts = 0;
709 if (!check_approval(call)) {
710 pc.rejection = create_denied_message(
711 call,
"Permission denied");
729std::vector<Message> ToolExecutor::process_single_call(
730 LoopContext& ctx,
const ToolCall& call) {
733 if (fire_pre_tool_hook(ctx, call)) {
734 auto msg = create_denied_message(call,
"Cancelled by hook");
737 msg.metadata[
"result_kind"] =
739 fire_post_tool_hook(ctx, call,
"", 0.0,
741 return {std::move(msg)};
744 auto pc = check_call_preconditions(ctx, call);
745 if (pc.rejection.has_value()) {
746 logger->info(
"Tool '{}' rejected by precondition (kind={})",
749 pc.rejection->metadata[
"result_kind"] =
751 fire_post_tool_hook(ctx, call,
"", 0.0, pc.kind, *pc.rejection);
752 return {std::move(*pc.rejection)};
755 auto exec_start = std::chrono::steady_clock::now();
756 auto [msg, raw_result] = execute_tool(ctx, call);
757 auto exec_ms = std::chrono::duration<double, std::milli>(
758 std::chrono::steady_clock::now() - exec_start).count();
760 finalize_tool_call(ctx, call, msg, raw_result, exec_ms);
762 return {std::move(msg)};
773 if (mcp::looks_like_tool_error(content)) {
776 if (mcp::is_effectively_empty(content)) {
792void ToolExecutor::log_tool_call(LoopContext& ctx,
const ToolCall& call,
794 const std::string& raw_result,
796 auto args_log = serialize_args(call);
797 if (args_log.size() > 512) { args_log.resize(512); }
798 logger->info(
"[tool_call] iter={} tier={} tool={} args={} "
799 "elapsed_ms={:.0f} result_chars={} status={}",
800 ctx.metrics.iterations,
801 ctx.locked_tier.empty() ?
"lead" : ctx.locked_tier,
802 call.name, args_log, exec_ms,
816void ToolExecutor::finalize_tool_call(LoopContext& ctx,
const ToolCall& call,
818 const std::string& raw_result,
824 apply_result_size_cap(msg.content);
825 ctx.effective_tool_calls++;
826 msg.metadata[
"added_at_iteration"] =
827 std::to_string(ctx.metrics.iterations);
828 record_tool_call(ctx, call, raw_result);
836 fire_post_tool_hook(ctx, call, raw_result, exec_ms, kind, msg);
841 update_anti_spiral_tracking(ctx, call.name);
843 log_tool_call(ctx, call, exec_ms, raw_result, kind);
845 extract_and_process_directives(ctx, raw_result);
846 run_post_tool_hooks(ctx);
857bool ToolExecutor::fire_pre_tool_hook(
858 const LoopContext& ctx,
const ToolCall& call) {
859 if (hook_iface_.fire_pre ==
nullptr) {
return false; }
860 auto json = build_pre_tool_json(call, ctx.locked_tier,
861 ctx.metrics.iterations);
863 int rc = hook_iface_.fire_pre(hook_iface_.registry,
877void ToolExecutor::apply_result_size_cap(std::string& content)
const {
889void ToolExecutor::update_anti_spiral_tracking(
890 LoopContext& ctx,
const std::string& tool_name) {
891 if (tool_name == ctx.last_tool_name) {
892 ++ctx.consecutive_same_tool_calls;
894 ctx.last_tool_name = tool_name;
895 ctx.consecutive_same_tool_calls = 1;
897 if (ctx.consecutive_same_tool_calls
899 ctx.pending_anti_spiral_warning =
900 tool_name +
" has been called "
901 + std::to_string(ctx.consecutive_same_tool_calls)
902 +
" times consecutively; pivot to a different tool or "
903 "complete the task next turn.";
915int ToolExecutor::effective_hard_block_threshold()
const {
917 if (configured < 0) {
938PreconditionCheck ToolExecutor::check_anti_spiral_hard_block(
939 const LoopContext& ctx,
const ToolCall& call)
const {
940 PreconditionCheck pc;
941 int projected = (call.name == ctx.last_tool_name)
942 ? (ctx.consecutive_same_tool_calls + 1)
944 int threshold = effective_hard_block_threshold();
945 if (projected >= threshold) {
947 "[anti-spiral] tool '" + call.name +
"' blocked after "
948 + std::to_string(projected)
949 +
" consecutive calls (threshold "
950 + std::to_string(threshold)
951 +
"); pivot to a different tool or complete the task.";
952 pc.rejection = create_denied_message(call, text);
976void ToolExecutor::fire_post_tool_hook(
977 const LoopContext& ctx,
const ToolCall& call,
978 const std::string& raw_result,
double elapsed_ms,
980 if (hook_iface_.fire_post ==
nullptr) {
return; }
981 auto json = build_post_tool_json(
982 call, raw_result, elapsed_ms, ctx.locked_tier,
983 ctx.metrics.iterations, kind);
985 hook_iface_.fire_post(hook_iface_.registry,
987 if (out !=
nullptr) {
1001bool ToolExecutor::should_stop_batch(
1002 const LoopContext& ctx,
1003 const std::vector<Message>& )
const {
1004 return ctx.state == AgentState::COMPLETE
1005 || ctx.pending_delegation.has_value()
1006 || ctx.pending_pipeline.has_value()
1007 || ctx.consecutive_duplicate_attempts >= 3;
1016void ToolExecutor::run_post_tool_hooks(LoopContext& ctx) {
1028Message ToolExecutor::create_circuit_breaker_message() {
1032 "STOP: You have called the same tool 3 times with "
1033 "identical arguments. This indicates you are stuck. "
1034 "Please try a completely different approach or respond "
1035 "to the user explaining what's blocking you.";
1036 logger->error(
"Circuit breaker triggered");
1048Message ToolExecutor::create_duplicate_message(
1049 const ToolCall& call,
1050 const std::string& previous_result) {
1052 previous_result.find(
"was denied") != std::string::npos
1053 || previous_result.find(
"not available") != std::string::npos;
1060 "Tool `" + call.name +
"` is not available to you "
1061 "and retrying will not help. You MUST use a different "
1062 "approach. Do NOT call `" + call.name +
"` again.";
1065 "Tool `" + call.name +
"` was already called with "
1066 "the same arguments.\n\nPrevious result:\n" +
1068 "\n\nDo NOT call this tool again. "
1069 "Use the previous result above.";
1086std::string ToolExecutor::serialize_args(
const ToolCall& call) {
1087 if (!call.arguments_json.empty()) {
1088 return call.arguments_json;
1090 nlohmann::json args;
1091 for (
const auto& [k, v] : call.arguments) {
1104std::string ToolExecutor::serialize_tool_call(
const ToolCall& call) {
1107 j[
"name"] = call.name;
1108 j[
"arguments"] = nlohmann::json::object();
1109 for (
const auto& [k, v] : call.arguments) {
1110 j[
"arguments"][k] = v;
1123void ToolExecutor::fire_tool_complete_callback(
1124 const ToolCall& call,
1125 const std::string& result,
1130 auto call_json = serialize_tool_call(call);
1132 call_json.c_str(), result.c_str(),
1133 static_cast<double>(ms), callbacks_.
user_data);
1148std::string ToolExecutor::build_post_tool_json(
1149 const ToolCall& call,
1150 const std::string& raw_result,
1152 const std::string& tier,
1156 ctx[
"tool_name"] = call.name;
1157 ctx[
"args"] = nlohmann::json::parse(serialize_args(call));
1159 ctx[
"tier"] = tier.empty() ? std::string{
"lead"} : tier;
1160 ctx[
"iteration"] = iteration;
1163 auto sr = nlohmann::json::parse(raw_result);
1164 ctx[
"result"] = sr.value(
"result", raw_result);
1165 ctx[
"directives"] = sr.value(
1166 "directives", nlohmann::json::array());
1168 ctx[
"result"] = raw_result;
1169 ctx[
"directives"] = nlohmann::json::array();
1183std::string ToolExecutor::build_pre_tool_json(
1184 const ToolCall& call,
1185 const std::string& tier,
1188 j[
"tool_name"] = call.name;
1189 j[
"args"] = nlohmann::json::parse(serialize_args(call));
1190 j[
"tier"] = tier.empty() ? std::string{
"lead"} : tier;
1191 j[
"iteration"] = iteration;
1215 const nlohmann::json& result_json) {
1216 std::vector<std::string> stages;
1217 if (!result_json.contains(
"stages")) {
return stages; }
1218 for (
const auto& s : result_json[
"stages"]) {
1219 stages.push_back(s.get<std::string>());
1245 const nlohmann::json& result_json) {
1246 auto cd = std::make_unique<CompleteDirective>(
1247 result_json.value(
"summary",
""));
1248 cd->coverage_gap = result_json.value(
"coverage_gap",
false);
1249 cd->gap_description = result_json.value(
"gap_description",
"");
1250 if (result_json.contains(
"suggested_files")
1251 && result_json[
"suggested_files"].is_array()) {
1252 cd->suggested_files =
1253 result_json[
"suggested_files"].get<std::vector<std::string>>();
1264 const nlohmann::json& d,
const nlohmann::json& result_json) {
1265 auto type_str = d.value(
"type",
"");
1266 std::unique_ptr<Directive> result;
1267 if (type_str ==
"stop_processing") {
1268 result = std::make_unique<StopProcessingDirective>();
1269 }
else if (type_str ==
"delegate") {
1274 result = std::make_unique<DelegateDirective>(
1275 result_json.value(
"target",
""),
1276 result_json.value(
"task",
""),
1277 result_json.value(
"max_turns", -1),
1278 result_json.value(
"delegation_id",
""));
1279 }
else if (type_str ==
"complete") {
1281 }
else if (type_str ==
"pipeline") {
1282 result = std::make_unique<PipelineDirective>(
1284 result_json.value(
"task",
""));
1297static std::optional<std::pair<nlohmann::json, nlohmann::json>>
1299 auto resp = nlohmann::json::parse(raw_result,
nullptr,
false);
1300 if (!resp.is_object() || !resp.contains(
"directives")) {
1301 return std::nullopt;
1303 auto dirs = resp[
"directives"];
1304 if (!dirs.is_array() || dirs.empty()) {
return std::nullopt; }
1305 return std::make_pair(std::move(resp), std::move(dirs));
1315void ToolExecutor::extract_and_process_directives(
1316 LoopContext& ctx,
const std::string& raw_result) {
1319 if (!extracted) {
return; }
1320 auto& [resp, dirs] = *extracted;
1322 auto result_json = nlohmann::json::parse(
1323 resp.value(
"result",
"{}"),
nullptr,
false);
1325 std::vector<std::unique_ptr<Directive>> owned;
1326 for (
const auto& d : dirs) {
1328 if (directive) { owned.push_back(std::move(directive)); }
1330 if (owned.empty()) {
return; }
1332 std::vector<const Directive*> ptrs;
1333 ptrs.reserve(owned.size());
1334 for (
const auto& d : owned) { ptrs.push_back(d.get()); }
1335 logger->info(
"Processing {} directives from tool result", ptrs.size());
bool is_enforced(const std::string &identity_name) const
Check if an identity has authorization enforcement enabled.
bool check_access(const std::string &identity_name, const std::string &tool_name, MCPAccessLevel required_level) const
Check if a tool call is authorized for an identity.
Manages MCP server instances and routes tool calls.
MCPAccessLevel get_required_access_level(const std::string &tool_name) const
Get the required access level for a tool.
bool is_explicitly_allowed(const std::string &tool_name, const std::string &args_json) const
Check if tool is explicitly allowed (skip prompting).
bool skip_duplicate_check(const std::string &tool_name) const
Check if tool should skip duplicate detection.
std::string get_tool_schema(const std::string &tool_name) const
Get the JSON Schema for a tool's input parameters.
std::string execute(const std::string &tool_name, const std::string &args_json)
Execute a tool call via the appropriate server.
void record(const ToolCallRecord &entry)
Record a completed tool call.
@ ENTROPIC_HOOK_ON_PERMISSION_CHECK
15: Permission check evaluated
@ ENTROPIC_HOOK_PRE_TOOL_CALL
3: Before tool execution
@ ENTROPIC_HOOK_POST_TOOL_CALL
4: After tool execution returns
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
double elapsed_ms(std::chrono::steady_clock::time_point start, std::chrono::steady_clock::time_point end)
Compute elapsed milliseconds between two time points.
Activate model on GPU (WARM → ACTIVE).
static std::string check_type(const std::string &key, const std::string &type, const nlohmann::json &val)
Check a single value against a type constraint.
ToolResultKind
Categorical outcome of a single tool invocation.
@ ok
Tool dispatched, returned non-empty content.
@ rejected_schema
Precondition: argument schema violation.
@ rejected_anti_spiral
Anti-spiral hard threshold crossed; tool blocked. (#14, v2.1.4)
@ rejected_duplicate
Precondition: duplicate in recent history.
@ ok_empty
Tool dispatched cleanly but returned no content (v2.1.0, #44)
@ rejected_unauthorized
Tool not in the locked tier's allowed_tools. (gh#83, v2.5.2)
@ error
Tool server returned an error payload.
@ rejected_precondition
Any other precondition reject (auth, tier, hook-cancel)
static std::string parse_tool_result_text(const std::string &result_json)
Extract the "result" text from an MCP result JSON envelope.
static std::vector< std::string > extract_pipeline_stages(const nlohmann::json &result_json)
Extract directives from ServerResponse JSON and process them.
static std::string check_property_constraints(const std::string &key, const nlohmann::json &prop, const nlohmann::json &val)
Check one property's enum and type constraints.
static ToolResultKind classify_tool_result(const std::string &content)
Classify a tool result by its content (error/empty/ok).
const char * mcp_access_level_name(MCPAccessLevel level)
Convert MCPAccessLevel to string representation.
static std::unique_ptr< Directive > build_directive(const nlohmann::json &d, const nlohmann::json &result_json)
Build a Directive from a parsed directive + result JSON.
const char * result_kind_to_string(ToolResultKind kind)
Serialize a ToolResultKind to its wire-stable string form.
static std::unique_ptr< Directive > build_complete_directive(const nlohmann::json &result_json)
Build a typed Directive from a directive-descriptor JSON.
static std::optional< std::pair< nlohmann::json, nlohmann::json > > extract_directive_array(const std::string &raw_result)
Pull the "directives" array out of a tool ServerResponse JSON.
std::string truncate_result(const std::string &text, size_t max_len)
Truncate a string to max_len characters with "..." suffix.
static char * dup(const std::string &s)
Heap-allocate a C string copy.
std::string summarize_params(const std::string &args_json)
Extract top-level JSON keys as a comma-separated summary.
static std::string validate_tool_args(const std::string &schema_json, const nlohmann::json &args)
Validate tool arguments against the tool's JSON Schema.
static std::string check_enum(const std::string &key, const nlohmann::json &allowed, const nlohmann::json &val)
Check one property's enum and type constraints.
static std::string check_required_fields(const nlohmann::json &schema, const nlohmann::json &args)
Check required fields are present.
Callback function pointer types for engine events.
void(* on_tool_call)(const char *json, void *ud)
Tool call request.
void(* on_tool_complete)(const char *json, const char *result, double ms, void *ud)
Tool execution done.
void * user_data
Opaque pointer passed to all callbacks.
void(* on_tool_start)(const char *json, void *ud)
Tool execution start.
void(* on_state_change)(int state, void *ud)
AgentState as int.
Configuration for the agentic loop.
int max_consecutive_same_tool
Anti-spiral SOFT threshold: after N consecutive calls of the SAME tool (regardless of arg similarity,...
int max_consecutive_same_tool_hard_block
Anti-spiral HARD threshold: when consecutive same-tool calls exceed this, the engine blocks the call ...
bool auto_approve_tools
Skip tool approval (v1.8.5)
int max_tool_result_bytes
Maximum byte length for a single tool's result content before the engine truncates with a "[....
int max_tool_calls_per_turn
Tool calls per iteration (v1.8.5)
Mutable state carried through the agentic loop.
int consecutive_errors
Error streak counter.
int effective_max_tool_calls_per_turn
Per-identity override (-1 = LoopConfig, P3-18)
AgentState state
Current state.
Permission persistence interface.
UTF-8 validation + replacement at every system boundary where bytes change ownership.