14#include <nlohmann/json.hpp>
39 : server_manager_(server_manager),
40 loop_config_(loop_config),
41 callbacks_(callbacks),
53 server_resolver_ =
fn;
54 server_resolver_data_ = user_data;
65 const std::string& session_key)
const {
66 if (server_resolver_ !=
nullptr) {
67 auto* resolved = server_resolver_(session_key, server_resolver_data_);
68 if (resolved !=
nullptr) {
return *resolved; }
70 return server_manager_;
81 permission_persist_ = persist;
102 const std::vector<ToolCall>& tool_calls) {
103 logger->info(
"Processing {} tool calls", tool_calls.size());
104 ctx.state = AgentState::WAITING_TOOL;
105 fire_state_callback(
ctx);
107 auto limited = sort_tool_calls(tool_calls);
108 int eff_limit =
ctx.effective_max_tool_calls_per_turn >= 0
109 ?
ctx.effective_max_tool_calls_per_turn
116 for (
auto&
m :
msgs) {
124 ctx.consecutive_errors = 0;
140std::vector<ToolCall> ToolExecutor::sort_tool_calls(
141 const std::vector<ToolCall>&
calls) {
145 bool a_delegate = (a.name ==
"entropic.delegate");
146 bool b_delegate = (b.name ==
"entropic.delegate");
147 return !a_delegate && b_delegate;
167std::string ToolExecutor::check_duplicate(
168 const LoopContext&
ctx,
169 const ToolCall&
call)
const {
173 auto key = tool_call_key(
call);
174 auto it =
ctx.recent_tool_calls.find(key);
175 if (
it !=
ctx.recent_tool_calls.end()) {
197Message ToolExecutor::handle_duplicate(
199 const ToolCall&
call,
201 ctx.consecutive_duplicate_attempts++;
202 logger->warn(
"Duplicate tool call #{}: {}",
203 ctx.consecutive_duplicate_attempts,
call.name);
205 if (
ctx.consecutive_duplicate_attempts >= 3) {
206 return create_circuit_breaker_message();
227bool ToolExecutor::check_approval(
const ToolCall&
call) {
242 if (hook_iface_.fire_info !=
nullptr) {
244 std::string json =
"{\"tool_name\":\""
245 +
call.name +
"\",\"permission\":\"" +
perm +
"\"}";
246 hook_iface_.fire_info(hook_iface_.registry,
251 logger->warn(
"No approval callback — denying: {}",
call.name);
268std::optional<Message> ToolExecutor::check_tier_allowed(
269 const LoopContext&
ctx,
const ToolCall&
call)
const {
276 if (
ctx.locked_tier.empty() || tier_allowed_tools_ ==
nullptr) {
279 auto it = tier_allowed_tools_->find(
ctx.locked_tier);
284 ||
it->second.empty()
285 || std::find(
it->second.begin(),
it->second.end(),
286 call.name) !=
it->second.end();
290 logger->warn(
"Tool '{}' not in tier '{}' allowed_tools — rejecting",
292 return create_denied_message(
293 call,
"tier '" +
ctx.locked_tier +
"' is not authorized to call '"
308 const nlohmann::json&
schema,
309 const nlohmann::json& args)
311 for (
const auto&
req :
schema.value(
"required",
312 nlohmann::json::array())) {
313 if (!args.contains(
req.get<std::string>())) {
314 return "Missing required argument: "
315 +
req.get<std::string>();
342 const std::string& key,
344 const nlohmann::json&
val)
347 if (
e ==
val) {
return ""; }
349 return "Invalid value for '" + key +
"': "
350 +
val.dump() +
". Must be one of: " +
allowed.dump();
365 const std::string& key,
366 const std::string& type,
367 const nlohmann::json&
val)
369 bool ok = (type ==
"string" &&
val.is_string())
370 || (type ==
"integer" &&
val.is_number_integer())
371 || (type ==
"number" &&
val.is_number())
372 || (type ==
"boolean" &&
val.is_boolean())
373 || (type ==
"array" &&
val.is_array())
374 || (type ==
"object" &&
val.is_object());
375 return ok ?
"" :
"Type mismatch for '" + key
376 +
"': expected " + type;
395 const std::string& key,
396 const nlohmann::json&
prop,
397 const nlohmann::json&
val)
400 if (
val.is_number() &&
prop.contains(
"minimum")
401 &&
val.get<
double>() <
prop[
"minimum"].get<
double>()) {
402 err =
"Value for '" + key +
"' is " +
val.dump()
403 +
", below the minimum " +
prop[
"minimum"].dump();
404 }
else if (
val.is_number() &&
prop.contains(
"maximum")
405 &&
val.get<
double>() >
prop[
"maximum"].get<
double>()) {
406 err =
"Value for '" + key +
"' is " +
val.dump()
407 +
", above the maximum " +
prop[
"maximum"].dump();
425 const std::string& key,
426 const nlohmann::json&
prop,
427 const nlohmann::json&
val)
430 if (
prop.contains(
"enum")) {
433 if (
err.empty() &&
prop.contains(
"type")) {
457 const nlohmann::json& args)
460 if (!
schema.is_object()) {
return ""; }
463 auto props =
schema.value(
"properties", nlohmann::json::object());
465 if (args.contains(
it.key())) {
467 it.key(),
it.value(), args[
it.key()]);
517std::pair<Message, std::string> ToolExecutor::execute_tool(
518 LoopContext&
ctx,
const ToolCall&
call) {
528 auto start = std::chrono::steady_clock::now();
547 auto end = std::chrono::steady_clock::now();
548 auto ms = std::chrono::duration_cast<
549 std::chrono::milliseconds>(
end - start).
count();
551 ctx.metrics.tool_calls++;
560 ctx.metrics.iterations);
567 msg.metadata[
"tool_call_id"] =
call.id;
568 msg.metadata[
"tool_name"] =
call.name;
588void ToolExecutor::record_tool_history(
const ToolCall&
call,
591 long long ms,
int iteration) {
593 rec.sequence = ++history_seq_;
597 ?
"error" :
"success";
599 rec.elapsed_ms =
static_cast<double>(
ms);
600 rec.iteration = iteration;
616std::string ToolExecutor::tool_call_key(
const ToolCall&
call) {
620 nlohmann::json args = nlohmann::json::object();
621 for (
const auto& [
k,
v] :
call.arguments) {
624 return call.name +
":" + args.dump();
639void ToolExecutor::record_tool_call(
641 const ToolCall&
call,
642 const std::string& result) {
644 std::string text = result;
646 auto j = nlohmann::json::parse(result);
647 text =
j.value(
"result", result);
650 auto key = tool_call_key(
call);
659 if (mcp::looks_like_tool_error(text)) {
660 ctx.failed_tool_calls[key]++;
666 ctx.failed_tool_calls.erase(key);
667 ctx.recent_tool_calls[key] = text;
683Message ToolExecutor::create_denied_message(
684 const ToolCall&
call,
685 const std::string& reason) {
689 "Tool `" +
call.name +
"` was denied: " + reason +
"\n\n"
690 "This tool is not available to you. Do NOT retry it. "
691 "Use a different approach to accomplish your task.";
705Message ToolExecutor::create_error_message(
706 const ToolCall&
call,
707 const std::string&
error) {
711 "Tool `" +
call.name +
"` failed with error: " +
error +
713 "- Check arguments are correct\n"
714 "- Try a different approach\n"
715 "- Do NOT retry with the same arguments";
727void ToolExecutor::fire_state_callback(
const LoopContext&
ctx) {
743void ToolExecutor::truncate_to_limit(
744 std::vector<ToolCall>&
calls,
746 auto lim =
static_cast<size_t>(
limit);
770std::optional<Message> ToolExecutor::check_mcp_authorization(
771 const LoopContext&
ctx,
772 const ToolCall&
call)
const {
773 if (auth_mgr_ ==
nullptr) {
776 auto identity =
ctx.locked_tier.empty()
777 ?
"lead" :
ctx.locked_tier;
785 logger->warn(
"MCP key denied: {} requires {} for {}",
790 "Tool `" +
call.name +
"` was denied: identity `"
793 "Your MCP key set does not authorize this tool. "
794 "Use `entropic.delegate` to hand off to an identity "
795 "that has the required access.";
815std::optional<Message> ToolExecutor::check_dup_or_approval(
816 LoopContext&
ctx,
const ToolCall&
call) {
821 ctx.consecutive_duplicate_attempts = 0;
822 return check_approval(
call)
824 : std::optional{create_denied_message(
825 call,
"Permission denied")};
852std::optional<Message> ToolExecutor::check_schema(
853 const ToolCall&
call) {
855 if (
schema.empty()) {
return std::nullopt; }
856 auto args = nlohmann::json::parse(
857 serialize_args(
call),
nullptr,
false);
858 auto err = args.is_discarded()
860 if (
err.empty()) {
return std::nullopt; }
861 logger->warn(
"Tool '{}' argument validation failed: {}",
863 return create_denied_message(
call,
err);
889PreconditionCheck ToolExecutor::check_call_preconditions(
890 LoopContext&
ctx,
const ToolCall&
call) {
891 PreconditionCheck
pc = check_spiral_blocks(
ctx,
call);
892 if (
pc.rejection.has_value()) {
895 if (
auto r = check_schema(
call);
r.has_value()) {
896 pc.rejection = std::move(
r);
898 }
else if (
auto a = check_mcp_authorization(
ctx,
call);
900 pc.rejection = std::move(
a);
902 }
else if (
auto t = check_tier_allowed(
ctx,
call);
t.has_value()) {
903 pc.rejection = std::move(
t);
905 }
else if (
auto dup = check_duplicate(
ctx,
call); !
dup.empty()) {
926PreconditionCheck ToolExecutor::check_approval_pc(
927 LoopContext&
ctx,
const ToolCall&
call) {
928 PreconditionCheck
pc;
929 ctx.consecutive_duplicate_attempts = 0;
930 if (!check_approval(
call)) {
931 pc.rejection = create_denied_message(
932 call,
"Permission denied");
957std::vector<Message> ToolExecutor::process_single_call(
966 if (fire_pre_tool_hook(
ctx,
call)) {
967 auto msg = create_denied_message(
call,
"Cancelled by hook");
970 msg.metadata[
"result_kind"] =
972 fire_post_tool_hook(
ctx,
call,
"", 0.0,
974 return {std::move(
msg)};
977 auto pc = check_call_preconditions(
ctx,
call);
978 if (
pc.rejection.has_value()) {
979 logger->info(
"Tool '{}' rejected by precondition (kind={})",
982 pc.rejection->metadata[
"result_kind"] =
984 fire_post_tool_hook(
ctx,
call,
"", 0.0,
pc.kind, *
pc.rejection);
985 return {std::move(*
pc.rejection)};
988 auto exec_start = std::chrono::steady_clock::now();
990 auto exec_ms = std::chrono::duration<double, std::milli>(
991 std::chrono::steady_clock::now() -
exec_start).count();
995 return {std::move(
msg)};
1010 if (mcp::looks_like_tool_error(content)) {
1013 if (mcp::is_effectively_empty(content)) {
1039void ToolExecutor::log_tool_call(LoopContext&
ctx,
const ToolCall&
call,
1045 logger->info(
"[tool_call] session='{}' iter={} tier={} tool={} args={} "
1046 "elapsed_ms={:.0f} result_chars={} status={}",
1048 ctx.metrics.iterations,
1049 ctx.locked_tier.empty() ?
"lead" :
ctx.locked_tier,
1074void ToolExecutor::finalize_tool_call(LoopContext&
ctx,
const ToolCall&
call,
1082 apply_result_size_cap(
msg.content);
1083 ctx.effective_tool_calls++;
1084 msg.metadata[
"added_at_iteration"] =
1085 std::to_string(
ctx.metrics.iterations);
1099 update_anti_spiral_tracking(
ctx,
call.name);
1104 run_post_tool_hooks(
ctx);
1121 const std::string& tool_name) {
1122 auto it =
parsed.find(
"tool_name");
1124 && !(
it->is_string() &&
it->get<std::string>() == tool_name);
1137 const std::string& tool_name) {
1139 if (!
parsed.is_object()) {
1140 reason =
"payload is not a JSON object";
1142 reason =
"payload changes the call's identity — a hook may "
1143 "rewrite arguments, never the tool it routes to";
1144 }
else if (!
parsed.contains(
"args")
1145 || !
parsed.at(
"args").is_object()) {
1146 reason =
"payload carries no 'args' object";
1167 const nlohmann::json& args) {
1168 call.arguments_json = args.dump();
1169 call.arguments.clear();
1170 for (
const auto& [
k,
v] : args.items()) {
1171 call.arguments[
k] =
v.is_string() ?
v.get<std::string>() :
v.dump();
1183bool ToolExecutor::apply_pre_tool_modification(ToolCall&
call,
1192 ? std::string{
"payload is not valid JSON"}
1196 "[hook] PRE_TOOL_CALL modification REFUSED for '{}': {}. "
1197 "Dispatching the call with the model's own arguments. "
1202 logger->info(
"[hook] PRE_TOOL_CALL rewrote args for '{}': {}",
1228bool ToolExecutor::fire_pre_tool_hook(
1229 const LoopContext&
ctx, ToolCall&
call) {
1230 if (hook_iface_.fire_pre ==
nullptr) {
return false; }
1231 auto json = build_pre_tool_json(
call,
ctx.locked_tier,
1232 ctx.metrics.iterations);
1233 char*
mod =
nullptr;
1234 int rc = hook_iface_.fire_pre(hook_iface_.registry,
1236 if (
mod !=
nullptr) {
1237 if (
rc == 0) { apply_pre_tool_modification(
call,
mod); }
1253void ToolExecutor::apply_result_size_cap(std::string& content)
const {
1272void ToolExecutor::update_anti_spiral_tracking(
1273 LoopContext&
ctx,
const std::string& tool_name) {
1274 if (tool_name ==
ctx.last_tool_name) {
1275 ++
ctx.consecutive_same_tool_calls;
1277 ctx.last_tool_name = tool_name;
1278 ctx.consecutive_same_tool_calls = 1;
1280 if (
ctx.consecutive_same_tool_calls
1282 ctx.pending_anti_spiral_warning =
1283 tool_name +
" has been called "
1284 + std::to_string(
ctx.consecutive_same_tool_calls)
1285 +
" times consecutively; pivot to a different tool or "
1286 "complete the task next turn.";
1303int ToolExecutor::effective_hard_block_threshold()
const {
1305 if (configured < 0) {
1335PreconditionCheck ToolExecutor::check_anti_spiral_hard_block(
1336 const LoopContext&
ctx,
const ToolCall&
call)
const {
1337 PreconditionCheck
pc;
1339 ? (
ctx.consecutive_same_tool_calls + 1)
1341 int threshold = effective_hard_block_threshold();
1344 "[anti-spiral] tool '" +
call.name +
"' blocked after "
1346 +
" consecutive calls (threshold "
1348 +
"); pivot to a different tool or complete the task.";
1349 pc.rejection = create_denied_message(
call, text);
1373PreconditionCheck ToolExecutor::check_repeated_failure(
1374 const LoopContext&
ctx,
const ToolCall&
call)
const {
1375 PreconditionCheck
pc;
1376 auto it =
ctx.failed_tool_calls.find(tool_call_key(
call));
1377 if (
it ==
ctx.failed_tool_calls.end()) {
1383 "[repeated-failure] tool '" +
call.name +
"' has already failed "
1385 +
" times with identical arguments; the failure is not "
1386 "transient. Change the arguments, try a different approach, "
1387 "or report that the task cannot be completed this way.";
1388 pc.rejection = create_denied_message(
call, text);
1411PreconditionCheck ToolExecutor::check_spiral_blocks(
1412 const LoopContext&
ctx,
const ToolCall&
call)
const {
1418 PreconditionCheck
pc = check_anti_spiral_hard_block(
ctx,
call);
1419 if (
pc.rejection.has_value()) {
1422 return check_repeated_failure(
ctx,
call);
1449void ToolExecutor::fire_post_tool_hook(
1450 const LoopContext&
ctx,
const ToolCall&
call,
1451 const std::string&
raw_result,
double elapsed_ms,
1453 if (hook_iface_.fire_post ==
nullptr) {
return; }
1454 auto json = build_post_tool_json(
1456 ctx.metrics.iterations, kind);
1457 char* out =
nullptr;
1458 hook_iface_.fire_post(hook_iface_.registry,
1460 if (out !=
nullptr) {
1465 msg.content = mcp::sanitize_utf8(out);
1478bool ToolExecutor::should_stop_batch(
1479 const LoopContext&
ctx,
1480 const std::vector<Message>& )
const {
1481 return ctx.state == AgentState::COMPLETE
1482 ||
ctx.pending_delegation.has_value()
1483 ||
ctx.pending_pipeline.has_value()
1484 ||
ctx.consecutive_duplicate_attempts >= 3;
1493void ToolExecutor::run_post_tool_hooks(LoopContext&
ctx) {
1508Message ToolExecutor::create_circuit_breaker_message() {
1512 "STOP: You have called the same tool 3 times with "
1513 "identical arguments. This indicates you are stuck. "
1514 "Please try a completely different approach or respond "
1515 "to the user explaining what's blocking you.";
1516 logger->error(
"Circuit breaker triggered");
1532Message ToolExecutor::create_duplicate_message(
1533 const ToolCall&
call,
1544 "Tool `" +
call.name +
"` is not available to you "
1545 "and retrying will not help. You MUST use a different "
1546 "approach. Do NOT call `" +
call.name +
"` again.";
1549 "Tool `" +
call.name +
"` was already called with "
1550 "the same arguments.\n\nPrevious result:\n" +
1552 "\n\nDo NOT call this tool again. "
1553 "Use the previous result above.";
1589std::string ToolExecutor::serialize_args(
const ToolCall&
call) {
1590 if (!
call.arguments_json.empty()) {
1591 return call.arguments_json;
1593 nlohmann::json args = nlohmann::json::object();
1594 for (
const auto& [
k,
v] :
call.arguments) {
1607std::string ToolExecutor::serialize_tool_call(
const ToolCall&
call) {
1610 j[
"name"] =
call.name;
1611 j[
"arguments"] = nlohmann::json::object();
1612 for (
const auto& [
k,
v] :
call.arguments) {
1613 j[
"arguments"][
k] =
v;
1626void ToolExecutor::fire_tool_complete_callback(
1627 const ToolCall&
call,
1628 const std::string& result,
1654std::string ToolExecutor::build_post_tool_json(
1655 const ToolCall&
call,
1658 const std::string&
tier,
1663 ctx[
"args"] = nlohmann::json::parse(serialize_args(
call));
1665 ctx[
"tier"] =
tier.empty() ? std::string{
"lead"} :
tier;
1666 ctx[
"iteration"] = iteration;
1671 ctx[
"directives"] =
sr.value(
1672 "directives", nlohmann::json::array());
1675 ctx[
"directives"] = nlohmann::json::array();
1691std::string ToolExecutor::build_pre_tool_json(
1692 const ToolCall&
call,
1693 const std::string&
tier,
1696 j[
"tool_name"] =
call.name;
1697 j[
"args"] = nlohmann::json::parse(serialize_args(
call));
1698 j[
"tier"] =
tier.empty() ? std::string{
"lead"} :
tier;
1699 j[
"iteration"] = iteration;
1724 std::vector<std::string> stages;
1725 if (!
result_json.contains(
"stages")) {
return stages; }
1727 stages.push_back(
s.get<std::string>());
1757 auto cd = std::make_unique<CompleteDirective>(
1759 cd->coverage_gap =
result_json.value(
"coverage_gap",
false);
1760 cd->gap_description =
result_json.value(
"gap_description",
"");
1763 cd->suggested_files =
1764 result_json[
"suggested_files"].get<std::vector<std::string>>();
1785 std::vector<ContextRef>
refs;
1790 for (
const auto& entry :
result_json[
"context"]) {
1791 if (!entry.is_object()) {
continue; }
1793 ref.
path = entry.value(
"path", std::string{});
1794 if (
ref.path.empty()) {
continue; }
1795 ref.lines = entry.value(
"lines", std::string{});
1796 ref.note = entry.value(
"note", std::string{});
1797 refs.push_back(std::move(
ref));
1815 const nlohmann::json&
d,
const nlohmann::json&
result_json) {
1817 std::unique_ptr<Directive> result;
1818 if (
type_str ==
"stop_processing") {
1819 result = std::make_unique<StopProcessingDirective>();
1820 }
else if (
type_str ==
"delegate") {
1825 auto dl = std::make_unique<DelegateDirective>(
1833 dl->resume_by_target =
result_json.value(
"resume_by_target",
false);
1834 result = std::move(
dl);
1835 }
else if (
type_str ==
"complete") {
1837 }
else if (
type_str ==
"pipeline") {
1838 auto pl = std::make_unique<PipelineDirective>(
1842 result = std::move(
pl);
1857static std::optional<std::pair<nlohmann::json, nlohmann::json>>
1860 if (!
resp.is_object() || !
resp.contains(
"directives")) {
1861 return std::nullopt;
1864 if (!
dirs.is_array() ||
dirs.empty()) {
return std::nullopt; }
1865 return std::make_pair(std::move(
resp), std::move(
dirs));
1881void ToolExecutor::extract_and_process_directives(
1889 resp.value(
"result",
"{}"),
nullptr,
false);
1891 std::vector<std::unique_ptr<Directive>>
owned;
1892 for (
const auto&
d :
dirs) {
1896 if (
owned.empty()) {
return; }
1898 std::vector<const Directive*>
ptrs;
1900 for (
const auto&
d :
owned) {
ptrs.push_back(
d.get()); }
1901 logger->info(
"Processing {} directives from tool result",
ptrs.size());
long long elapsed_ms
Wall time, spawn to reap.
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.
std::string err
size_exceeded, when set
long limit
Results to keep.
@ 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.
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)
@ approved
Serve this one call.
@ optional
optional: every escape asks the host's path approver
static std::string parse_tool_result_text(const std::string &result_json)
Extract the "result" text from an MCP result JSON envelope.
static bool pre_mod_renames_tool(const nlohmann::json &parsed, const std::string &tool_name)
Does a PRE_TOOL_CALL payload try to change the call's identity?
@ tier
The TIER's identity frontmatter grammar: stem (gh#154).
@ count
Sentinel — MUST remain last.
static std::string check_range(const std::string &key, const nlohmann::json &prop, const nlohmann::json &val)
Check a numeric value against minimum / maximum (v2.14.0).
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, type and range 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.
static std::string pre_mod_refusal(const nlohmann::json &parsed, const std::string &tool_name)
Why a PRE_TOOL_CALL modification must be refused (gh#168).
static std::vector< ContextRef > extract_context_refs(const nlohmann::json &result_json)
Parse the context array of a delegate/pipeline result (gh#162).
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 void overwrite_call_arguments(ToolCall &call, const nlohmann::json &args)
Replace a call's arguments from a validated args object.
static std::string check_required_fields(const nlohmann::json &schema, const nlohmann::json &args)
Check required fields are present.
gh#158: the cancel token of the run executing on THIS thread.
One file the lead already paid to find (gh#162, v2.13.0).
std::string path
Repo-relative (or absolute) file path.
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)
int max_identical_failures
How many times one EXACT call (tool + sorted arguments) may fail before the engine refuses it pre-dis...
Mutable state carried through the agentic loop.
Permission persistence interface.
UTF-8 validation + replacement at every system boundary where bytes change ownership.