15#include <nlohmann/json.hpp>
22#include <unordered_set>
31 "Continue. Batch multiple tool calls in one response when possible.";
41 static std::atomic<uint64_t> counter{0};
42 return "tc-" + std::to_string(counter.fetch_add(1, std::memory_order_relaxed));
61 for (
const char* key : {
"name",
"tool_name",
"function",
"function_name"}) {
62 if (
j.contains(key) &&
j[key].is_string()) {
63 return j[key].get<std::string>();
84 if (name.empty()) {
return std::nullopt; }
87 tc.name = std::move(name);
88 auto args =
j.value(
"arguments",
j.value(
"parameters", nlohmann::json::object()));
89 for (
auto& [
k,
v] : args.
items()) {
tc.arguments[
k] =
v.dump(); }
104 : tier_name_(
std::
move(tier_name))
123 const std::string& result)
const
127 msg.content =
"Tool `" +
tool_call.name +
"` returned:\n\n" +
146 const std::string& content,
147 const std::vector<ToolCall>& tool_calls)
const
150 if (!tool_calls.empty()) {
155 if (content.find(
"<think>") != std::string::npos &&
156 content.find(
"</think>") == std::string::npos)
181 const std::string& content)
const
183 std::vector<ToolCall>
calls;
202 R
"((?:<tool_call>|<\|tool_call\|?>|<\|im_start\|>tool_call)\s*)"
203 R"(([\s\S]*?)\s*</tool_call>)");
205 auto begin = std::sregex_iterator(content.begin(), content.end(),
pattern);
206 auto end = std::sregex_iterator();
209 std::string
json_str = (*it)[1].str();
213 logger->info(
"Parsed tagged tool call: {}",
parsed->name);
221 "Tagged tool_call matched but JSON failed to parse: {}",
232 && (content.find(
"<tool_call>") != std::string::npos
233 || content.find(
"<|tool_call") != std::string::npos
234 || content.find(
"<|im_start|>tool_call") != std::string::npos)) {
236 "Content contains tool_call markup but no tagged calls "
237 "were extracted — possible tag/encoding mismatch. "
238 "Raw content length={}", content.size());
265 const nlohmann::json&
j) {
269 static const std::unordered_set<std::string>
kMetaActions = {
270 "delegate",
"pipeline",
"complete",
271 "phase_change",
"prune_context",
"resume_delegation"};
273 if (
j.contains(
"action") &&
j[
"action"].is_string()) {
274 action =
j[
"action"].get<std::string>();
282 nlohmann::json args =
j;
283 args.erase(
"action");
284 tc.arguments_json = args.dump();
285 for (
auto& [
k,
v] : args.items()) {
tc.arguments[
k] =
v.dump(); }
298 std::vector<ToolCall>
calls;
302 size_t start =
line.find_first_not_of(
" \t");
303 if (start == std::string::npos ||
line[start] !=
'{') {
continue; }
304 auto j = nlohmann::json::parse(
line.substr(start),
nullptr,
false);
305 if (!
j.is_object()) {
continue; }
307 calls.push_back(std::move(*
tc));
321 const std::string&
raw) {
322 if (!
calls.empty()) {
return; }
326 "gh#88: PEG_GEMMA4 parsed 0 tool calls; recovered {} from a "
327 "bare-JSON {{\"action\":...}} envelope (possible context priming)",
343 const nlohmann::json& tools,
const std::string& name) {
344 std::unordered_set<std::string>
props;
345 for (
const auto&
t : tools) {
346 if (!
t.is_object() ||
t.value(
"name", std::string()) != name) {
349 auto schema =
t.value(
"inputSchema", nlohmann::json::object());
353 &&
it->value(
"type", std::string()) ==
"string") {
371 if (
props.empty()) {
return; }
372 auto args = nlohmann::json::parse(
tc.arguments_json,
nullptr,
false);
373 if (!args.is_object()) {
return; }
375 for (
const auto& key :
props) {
376 auto it = args.find(key);
377 if (
it != args.end() &&
it->is_number()) {
383 tc.arguments_json = args.dump();
384 for (
auto it = args.begin();
it != args.end(); ++
it) {
385 tc.arguments[
it.key()] =
386 it->is_string() ?
it->get<std::string>() :
it->dump();
400 auto tools = nlohmann::json::parse(
tools_json,
nullptr,
false);
401 if (!tools.is_array()) {
return; }
428 std::string result = content;
432 while ((pos = result.find(
markers.open)) != std::string::npos) {
433 auto close = result.find(
markers.close, pos +
markers.open.size());
437 auto span_end = (close == std::string::npos)
438 ? result.size() : close +
markers.close.size();
447 == std::string::npos) {
449 "Reasoning block '{}' was never closed with '{}', so the strip "
450 "removed the whole generation and content is empty. Not a parse "
451 "error. The orchestrator reports the actual cause (budget vs the "
452 "model ending the turn) — it is the only layer holding "
453 "finish_reason. gh#137.",
457 size_t start = result.find_first_not_of(
" \t\n\r");
458 if (start == std::string::npos) {
return ""; }
459 size_t end_pos = result.find_last_not_of(
" \t\n\r");
460 return result.substr(start,
end_pos - start + 1);
473 const std::string& fixed) {
474 auto j = nlohmann::json::parse(fixed);
487 std::regex
name_pattern(R
"re("name"\s*:\s*"([^"]+)")re");
513 std::string fixed = std::regex_replace(
json_str, std::regex(R
"(,\s*\})"), "}");
514 fixed = std::regex_replace(fixed, std::regex(R
"(,\s*\])"), "]");
515 std::replace(fixed.begin(), fixed.end(),
'\'',
'"');
517 logger->info(
"JSON recovery attempt: {} chars",
json_str.size());
536 const std::vector<std::string>&
tool_jsons)
const
538 std::ostringstream out;
539 out <<
"## Tools\n\n"
540 <<
"Call tools with: `<tool_call>{\"name\": \"tool.name\", \"arguments\": {...}}</tool_call>`\n"
541 <<
"Batch independent calls in one response with multiple `<tool_call>` blocks.\n\n";
545 auto j = nlohmann::json::parse(
json_str);
546 out <<
"### " <<
j.value(
"name",
"unknown") <<
"\n"
547 <<
j.value(
"description",
"No description") <<
"\n\n"
548 <<
"Schema:\n```json\n"
549 <<
j.value(
"inputSchema", nlohmann::json::object()).dump(2)
552 out <<
"### (malformed tool definition)\n\n";
571 auto j = nlohmann::json::parse(
json_str);
592 const std::string& base_system,
605 const std::vector<ContentPart>&
parts)
const {
606 nlohmann::json arr = nlohmann::json::array();
610 obj[
"type"] =
"text";
611 obj[
"text"] =
part.text;
613 obj[
"type"] =
"image";
614 if (!
part.image_path.empty()) {
615 obj[
"path"] =
part.image_path;
617 if (!
part.image_url.empty()) {
618 obj[
"url"] =
part.image_url;
621 arr.push_back(std::move(obj));
ChatAdapter concrete base class.
ChatAdapter(std::string tier_name, std::string identity_prompt)
Construct adapter with tier identity.
std::optional< ToolCall > try_recover_json(const std::string &json_str) const
Attempt JSON recovery on malformed tool call string.
virtual std::string format_content_parts(const std::vector< ContentPart > &parts) const
Convert multimodal content parts to adapter-specific format.
virtual std::string format_system_with_vision(const std::string &base_system, bool has_vision) const
Format system prompt with optional vision context.
std::optional< ToolCall > parse_single_tool_call(const std::string &json_str) const
Parse a single JSON tool call string.
std::vector< ToolCall > parse_tagged_tool_calls(const std::string &content) const
Parse <tool_call>JSON</tool_call> tagged blocks.
virtual std::string format_tools(const std::vector< std::string > &tool_jsons) const
Format tool definitions for injection into system prompt.
bool is_response_complete(const std::string &content, const std::vector< ToolCall > &tool_calls) const
Check if response represents task completion.
std::string strip_think_blocks(const std::string &content) const
Strip this family's reasoning blocks from content (gh#108).
virtual ThinkMarkers thinking_markers() const
Parse tool calls from model output.
virtual Message format_tool_result(const ToolCall &tool_call, const std::string &result) const
Format a tool result as a user message.
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).
@ TEXT
Plain text content.
static std::optional< ToolCall > parse_recovered_tool_call(const std::string &fixed)
Parse a brace/quote-fixed JSON string into a ToolCall.
void coerce_string_typed_args(std::vector< ToolCall > &calls, const std::string &tools_json)
gh#90: coerce numeric scalars back to strings for string-typed tool parameters.
std::vector< ToolCall > recover_action_envelope_calls(const std::string &raw)
gh#88: recover tool calls a gemma model parroted as bare-JSON.
void apply_action_envelope_recovery(std::vector< ToolCall > &calls, const std::string &raw)
gh#88: substitute recovered bare-JSON calls when a reliable (PEG_GEMMA4 / gemma) parse produced none;...
@ optional
optional: every escape asks the host's path approver
static std::optional< ToolCall > action_envelope_to_call(const nlohmann::json &j)
Map one parsed JSON object to a recovered ToolCall.
@ tool_call
Render-derived (COMMON_GRAMMAR_TYPE_TOOL_CALLS, needs prefill)
static void coerce_call_string_args(ToolCall &tc, const nlohmann::json &tools)
Coerce one call's numeric args to strings per the tool schema.
std::string generate_uuid()
Generate a UUID v4 string.
static std::optional< ToolCall > regex_recovered_tool_call(const std::string &json_str)
Last-ditch recovery: pull a tool name out via regex.
static std::unordered_set< std::string > tool_string_props(const nlohmann::json &tools, const std::string &name)
String-typed property names for a tool in the staged MCP defs.
A message in a conversation.
std::string role
Message role.