13#include <nlohmann/json.hpp>
20#include <unordered_map>
40static void log_prompt(
const std::vector<Message>& messages,
41 const std::string& tier) {
42 logger->info(
"─── Prompt ({} messages, tier={}) ───",
43 messages.size(), tier);
44 for (
size_t i = 0; i < messages.size(); ++i) {
45 if (messages[i].role ==
"system") {
46 size_t h = std::hash<std::string>{}(messages[i].content);
49 if (h != prev || prev == 0) {
50 logger->info(
"[{}] role=system hash={:016x} "
52 i, h, prev, messages[i].content);
54 logger->info(
"[{}] role=system [unchanged, {} chars, "
56 i, messages[i].content.size(), h);
59 logger->info(
"[{}] role={}\n{}", i, messages[i].role,
63 logger->info(
"─── End prompt ───");
76 const InferenceInterface& inference,
80 : inference_(inference),
81 loop_config_(loop_config),
82 callbacks_(callbacks),
93 lock_tier_if_needed(ctx);
96 return generate_streaming(ctx);
98 return generate_batch(ctx);
110 const std::string& content,
111 const std::string& tool_calls_json) {
112 if (inference_.is_response_complete ==
nullptr) {
113 return !content.empty();
115 return inference_.is_response_complete(
116 content.c_str(), tool_calls_json.c_str(),
117 inference_.adapter_data) != 0;
126void ResponseGenerator::lock_tier_if_needed(
LoopContext& ctx) {
135 if (inference_.route ==
nullptr) {
140 auto msgs_json = serialize_messages(ctx.
messages);
141 char* result_json =
nullptr;
142 int rc = inference_.route(msgs_json.c_str(), &result_json,
143 inference_.orchestrator_data);
144 if (rc == 0 && result_json !=
nullptr) {
146 if (inference_.free_fn !=
nullptr) {
147 inference_.free_fn(result_json);
151 logger->warn(
"Routing failed (rc={}), using default tier", rc);
183 void (*
observer)(
const char*, size_t,
void*) =
nullptr;
221 && acc->events->interrupt->load()
222 && !acc->interrupted;
223 if (just_interrupted) {
224 acc->interrupted =
true;
231 logger->info(
"Stream interrupt observed at token {}; "
232 "raising backend cancel_flag",
234 if (acc->cancel_flag !=
nullptr) {
235 *acc->cancel_flag = 1;
239 acc->content.append(token, len);
240 if (acc->callbacks->on_stream_chunk !=
nullptr) {
241 acc->callbacks->on_stream_chunk(token, len,
242 acc->callbacks->user_data);
247 if (acc->observer !=
nullptr) {
248 acc->observer(token, len, acc->observer_data);
252 if (acc->hooks !=
nullptr && acc->hooks->fire_info !=
nullptr) {
253 std::string json =
"{\"token_index\":"
254 + std::to_string(acc->token_index++) +
"}";
255 acc->hooks->fire_info(acc->hooks->registry,
274 size_t content_size) {
277 logger->info(
"Stream cancelled by interrupt after {} chars",
279 reason =
"interrupted";
280 }
else if (rc != 0 && content_size > 0) {
281 logger->warn(
"Stream failed (rc={}) after {} chars — "
282 "preserving partial", rc, content_size);
284 }
else if (rc != 0) {
285 logger->error(
"Stream failed (rc={}) with no partial content", rc);
301std::pair<std::string, std::string> ResponseGenerator::prepare_prompts(
302 LoopContext& ctx,
const char* mode) {
307 auto messages = inject_engine_state_reminder(ctx.messages, ctx);
308 logger->info(
"Generate ({}): tier={}, {} messages",
309 mode, ctx.locked_tier, messages.size());
311 return {serialize_messages(messages),
312 build_params_json(ctx.locked_tier)};
323GenerateResult ResponseGenerator::generate_streaming(LoopContext& ctx) {
324 if (inference_.generate_stream ==
nullptr) {
325 logger->warn(
"No streaming function, falling back to batch");
326 return generate_batch(ctx);
329 auto [msgs_json, params_json] = prepare_prompts(ctx,
"stream");
332 StreamAccumulator acc;
333 acc.callbacks = &callbacks_;
334 acc.events = &events_;
340 acc.cancel_flag = &cancel_flag;
344 acc.observer = stream_observer_;
345 acc.observer_data = stream_observer_data_;
347 int rc = inference_.generate_stream(
348 msgs_json.c_str(), params_json.c_str(),
350 &cancel_flag, inference_.backend_data);
352 GenerateResult result;
361 result.content = mcp::sanitize_utf8(acc.content);
362 result.tool_calls_json =
"[]";
363 logger->info(
"Generate complete (stream): finish={}, {} chars",
364 result.finish_reason, result.content.size());
382int ResponseGenerator::dispatch_batch_generate(
383 const std::string& msgs_json,
384 const std::string& params_json,
385 char** result_json) {
397 if (inference_.generate_cancellable ==
nullptr
399 return inference_.generate(
400 msgs_json.c_str(), params_json.c_str(),
401 result_json, inference_.backend_data);
410 && events_.
interrupt->load(std::memory_order_acquire)) ? 1 : 0;
412 std::atomic<bool> observer_done(
false);
413 std::thread observer;
416 observer = std::thread([&cancel_int, flag, &observer_done]() {
417 while (!observer_done.load(std::memory_order_acquire)) {
418 if (flag->load(std::memory_order_acquire)) {
422 std::this_thread::sleep_for(std::chrono::milliseconds(10));
427 int rc = inference_.generate_cancellable(
428 msgs_json.c_str(), params_json.c_str(),
429 result_json, &cancel_int, inference_.backend_data);
431 observer_done.store(
true, std::memory_order_release);
432 if (observer.joinable()) { observer.join(); }
443GenerateResult ResponseGenerator::generate_batch(LoopContext& ctx) {
444 if (inference_.generate ==
nullptr
445 && inference_.generate_cancellable ==
nullptr) {
446 logger->error(
"No generate function available");
447 return {
"",
"[]",
"error"};
450 auto [msgs_json, params_json] = prepare_prompts(ctx,
"batch");
451 char* result_json =
nullptr;
455 int rc = dispatch_batch_generate(msgs_json, params_json, &result_json);
457 GenerateResult result;
463 result.finish_reason =
"interrupted";
464 if (result_json !=
nullptr) {
465 result.content = mcp::sanitize_utf8(result_json);
467 result.tool_calls_json =
"[]";
468 logger->info(
"Generate cancelled (batch) after {} chars",
469 result.content.size());
470 }
else if (rc == 0 && result_json !=
nullptr) {
473 result.content = mcp::sanitize_utf8(result_json);
474 result.finish_reason =
"stop";
475 result.tool_calls_json =
"[]";
478 if (stream_observer_ !=
nullptr && !result.content.empty()) {
479 stream_observer_(result.content.data(),
480 result.content.size(),
481 stream_observer_data_);
484 result.finish_reason =
"error";
485 logger->error(
"Generate failed (rc={})", rc);
487 if (result_json !=
nullptr && inference_.free_fn !=
nullptr) {
488 inference_.free_fn(result_json);
490 logger->info(
"Generate complete (batch): finish={}, {} chars",
491 result.finish_reason, result.content.size());
509std::string ResponseGenerator::handle_pause(
511 const std::string& partial) {
512 ctx.state = AgentState::PAUSED;
513 if (callbacks_.on_state_change !=
nullptr) {
514 callbacks_.on_state_change(
515 static_cast<int>(AgentState::PAUSED),
516 callbacks_.user_data);
522 if (state_observer_ !=
nullptr) {
523 state_observer_(
static_cast<int>(AgentState::PAUSED),
524 state_observer_data_);
527 char* injection =
nullptr;
528 if (callbacks_.on_pause_prompt !=
nullptr) {
529 callbacks_.on_pause_prompt(partial.c_str(), &injection,
530 callbacks_.user_data);
533 if (injection ==
nullptr) {
534 if (events_.interrupt !=
nullptr) {
535 events_.interrupt->store(
true);
540 std::string inj(injection);
542 ctx.state = AgentState::EXECUTING;
547 if (!partial.empty()) {
549 partial_msg.role =
"assistant";
550 partial_msg.content = partial +
"\n\n[Generation paused by user]";
551 ctx.messages.push_back(std::move(partial_msg));
554 inject_msg.role =
"user";
555 inject_msg.content =
"[User interjection]: " + inj
556 +
"\n\nPlease continue with this in mind.";
557 ctx.messages.push_back(std::move(inject_msg));
559 ctx.state = AgentState::EXECUTING;
578 case '"': out +=
"\\\"";
break;
579 case '\\': out +=
"\\\\";
break;
580 case '\n': out +=
"\\n";
break;
581 case '\r': out +=
"\\r";
break;
582 case '\t': out +=
"\\t";
break;
583 default: out += c;
break;
598 const std::vector<ContentPart>& parts, std::string& out) {
600 for (
size_t i = 0; i < parts.size(); ++i) {
601 if (i > 0) { out +=
','; }
602 if (parts[i].type == ContentPartType::IMAGE) {
603 out += R
"({"type":"image","path":")";
605 out += R"(","url":")";
609 out += R
"({"type":"text","text":")";
631std::string ResponseGenerator::serialize_messages(
632 const std::vector<Message>& messages) {
633 std::string json =
"[";
634 for (
size_t i = 0; i < messages.size(); ++i) {
635 if (i > 0) { json +=
','; }
636 json +=
"{\"role\":\"" + messages[i].role +
"\",\"content\":";
637 if (messages[i].content_parts.empty()) {
664std::string ResponseGenerator::build_params_json(
665 const std::string& tier) {
666 nlohmann::json j = nlohmann::json::object();
667 if (!tier.empty()) { j[
"tier"] = tier; }
669 if (inference_.get_tool_prompt !=
nullptr) {
670 char* tools =
nullptr;
671 int rc = inference_.get_tool_prompt(
672 tier.c_str(), &tools, inference_.tool_prompt_data);
673 if (rc == 0 && tools !=
nullptr) {
674 j[
"tools"] = std::string(tools);
675 if (inference_.free_fn) { inference_.free_fn(tools); }
702std::vector<Message> ResponseGenerator::inject_engine_state_reminder(
703 const std::vector<Message>& messages,
704 const LoopContext& ctx) {
705 int max_iter = ctx.effective_max_iterations >= 0
706 ? ctx.effective_max_iterations
707 : loop_config_.max_iterations;
708 std::string reminder =
"[engine] iteration "
709 + std::to_string(ctx.metrics.iterations)
710 +
"/" + std::to_string(max_iter)
711 +
", tool calls so far: "
712 + std::to_string(ctx.metrics.tool_calls) +
".";
718 if (!ctx.pending_validation_feedback.empty()) {
719 reminder +=
"\n[engine] previous turn rejected: "
720 + ctx.pending_validation_feedback;
726 if (!ctx.pending_anti_spiral_warning.empty()) {
727 reminder +=
"\n[engine] anti-spiral: "
728 + ctx.pending_anti_spiral_warning;
731 auto result = messages;
732 Message reminder_msg;
733 reminder_msg.role =
"user";
734 reminder_msg.content = std::move(reminder);
735 result.push_back(std::move(reminder_msg));
GenerateResult generate_response(LoopContext &ctx)
Generate model response, routing tier first if needed.
ResponseGenerator(const InferenceInterface &inference, const LoopConfig &loop_config, EngineCallbacks &callbacks, GenerationEvents events)
Construct a response generator.
bool is_response_complete(const std::string &content, const std::string &tool_calls_json)
Check if the last response indicates completion.
Error types for cross-.so error reporting.
@ ENTROPIC_ERROR_CANCELLED
Operation cancelled via cancel token.
@ ENTROPIC_HOOK_ON_STREAM_TOKEN
2: Each streaming token emitted
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::unordered_map< std::string, size_t > s_tier_system_hash
Per-tier system prompt hash for diff detection across delegations.
static void log_prompt(const std::vector< Message > &messages, const std::string &tier)
Log the full assembled prompt (all messages, no truncation).
static void serialize_content_parts(const std::vector< ContentPart > &parts, std::string &out)
Serialize a single multimodal content_parts array (gh#37, v2.1.8).
static void json_escape_into(const std::string &s, std::string &out)
Serialize messages to JSON for inference interface.
static std::string resolve_stream_finish_reason(int rc, size_t content_size)
Resolve a stream's finish_reason from rc + content size.
static void stream_token_callback(const char *token, size_t len, void *user_data)
Token callback for streaming generation.
Response generation subsystem for the agentic loop.
Callback function pointer types for engine events.
void(* on_tier_selected)(const char *tier, void *ud)
Tier routing result.
void * user_data
Opaque pointer passed to all callbacks.
Result of a generate_response call.
Atomic flags for interrupt/pause signaling.
std::atomic< bool > * interrupt
Hard interrupt flag.
Configuration for the agentic loop.
bool stream_output
Stream vs batch generation.
bool speculative_enabled
gh#110 (v2.9.6): mirrors inference.speculative.enabled from config, plumbed through since core....
Mutable state carried through the agentic loop.
std::vector< Message > messages
Conversation history.
std::string locked_tier
Tier locked for this loop ("" = none)
Context passed to the streaming token callback.
const HookInterface * hooks
Hook dispatch (v1.9.1)
void * observer_data
Observer user_data.
std::string content
Accumulated content.
void(* observer)(const char *, size_t, void *)
Global observer — fires on every token alongside callbacks->on_stream_chunk.
EngineCallbacks * callbacks
Callback reference.
bool interrupted
Set when interrupt detected.
int token_index
Token counter (v1.9.1)
GenerationEvents * events
Event flags.
int * cancel_flag
Pointer to the backend's cancel flag (gh#20, v2.1.5).
UTF-8 validation + replacement at every system boundary where bytes change ownership.