16#include <nlohmann/json.hpp>
23#include <unordered_map>
52 && events.
interrupt->load(std::memory_order_acquire))
59static std::unordered_map<std::string, entropic::PromptLogDedup>
75 const std::string&
tier) {
76 size_t h = std::hash<std::string>{}(
msg.content);
80 logger->info(
"[{}] role=system hash={:016x} prev={:016x}\n{}",
83 logger->info(
"[{}] role=system [unchanged, {} chars, hash={:016x}]",
84 i,
msg.content.size(),
h);
110 const size_t h = std::hash<std::string>{}(
msg.content);
112 logger->info(
"[{}] role={}\n{}",
i,
msg.role,
msg.content);
116 i,
msg.role,
msg.content.size(),
h));
132 const std::string&
tier) {
133 logger->info(
"─── Prompt ({} messages, tier={}) ───",
134 messages.size(),
tier);
136 for (
size_t i = 0;
i < messages.size(); ++
i) {
137 if (messages[
i].role ==
"system") {
143 logger->info(
"─── End prompt ───");
160 : inference_(inference),
161 loop_config_(loop_config),
162 callbacks_(callbacks),
173 lock_tier_if_needed(
ctx);
176 return generate_streaming(
ctx);
178 return generate_batch(
ctx);
190 const std::string& content,
191 const std::string& tool_calls_json) {
192 if (inference_.is_response_complete ==
nullptr) {
193 return !content.empty();
195 return inference_.is_response_complete(
196 content.c_str(), tool_calls_json.c_str(),
197 inference_.adapter_data) != 0;
207 if (!
ctx.locked_tier.empty()) {
215 if (inference_.route ==
nullptr) {
216 ctx.locked_tier =
"default";
223 inference_.orchestrator_data);
226 if (inference_.free_fn !=
nullptr) {
230 ctx.locked_tier =
"default";
231 logger->warn(
"Routing failed (rc={}), using default tier",
rc);
234 logger->info(
"Locked tier: {}",
ctx.locked_tier);
308 && !
acc->interrupted;
310 acc->interrupted =
true;
317 logger->info(
"Stream interrupt observed at token {}; "
318 "raising backend cancel_flag",
320 if (
acc->cancel_flag !=
nullptr) {
321 *
acc->cancel_flag = 1;
325 acc->content.append(token,
len);
330 &&
acc->callbacks->on_stream_chunk !=
nullptr) {
331 acc->callbacks->on_stream_chunk(token,
len,
332 acc->callbacks->user_data);
337 if (
acc->observer !=
nullptr) {
338 acc->observer(token,
len,
acc->observer_data);
342 if (
acc->hooks !=
nullptr &&
acc->hooks->fire_info !=
nullptr) {
343 std::string json =
"{\"token_index\":"
344 + std::to_string(
acc->token_index++) +
"}";
345 acc->hooks->fire_info(
acc->hooks->registry,
372 logger->info(
"Stream cancelled by interrupt after {} chars",
374 reason =
"interrupted";
376 logger->error(
"Stream refused: the prompt does not fit this tier's "
377 "context — see the inference log for the token "
378 "breakdown (rc={})",
rc);
379 reason =
"context_overflow";
381 logger->warn(
"Stream failed (rc={}) after {} chars — "
384 }
else if (
rc != 0) {
385 logger->error(
"Stream failed (rc={}) with no partial content",
rc);
401std::pair<std::string, std::string> ResponseGenerator::prepare_prompts(
402 LoopContext&
ctx,
const char* mode) {
407 auto messages = inject_engine_state_reminder(
ctx.messages,
ctx);
408 logger->info(
"Generate ({}): tier={}, {} messages",
409 mode,
ctx.locked_tier, messages.size());
411 return {serialize_messages(messages),
412 build_params_json(
ctx.locked_tier,
ctx.session_key)};
423GenerateResult ResponseGenerator::generate_streaming(LoopContext&
ctx) {
424 if (inference_.generate_stream ==
nullptr) {
425 logger->warn(
"No streaming function, falling back to batch");
426 return generate_batch(
ctx);
429 auto [
msgs_json, params_json] = prepare_prompts(
ctx,
"stream");
432 StreamAccumulator
acc;
433 acc.callbacks = &callbacks_;
434 acc.events = &events_;
440 acc.cancel_flag = &cancel_flag;
444 acc.observer = stream_observer_;
445 acc.observer_data = stream_observer_data_;
447 int rc = inference_.generate_stream(
450 &cancel_flag, inference_.backend_data);
452 GenerateResult result;
461 result.content = mcp::sanitize_utf8(
acc.content);
462 result.tool_calls_json =
"[]";
463 logger->info(
"Generate complete (stream): finish={}, {} chars",
464 result.finish_reason, result.content.size());
476 return flag !=
nullptr &&
flag->load(std::memory_order_acquire);
501std::thread ResponseGenerator::spawn_cancel_poller(
510 while (!
done.load(std::memory_order_acquire)) {
511 if (flag_raised(handle_flag) || flag_raised(run_token)) {
515 std::this_thread::sleep_for(
516 std::chrono::milliseconds(10));
532int ResponseGenerator::dispatch_batch_generate(
533 const std::string& msgs_json,
534 const std::string& params_json,
535 char** result_json) {
547 if (inference_.generate_cancellable ==
nullptr
548 || loop_config_.speculative_enabled) {
549 return inference_.generate(
550 msgs_json.c_str(), params_json.c_str(),
551 result_json, inference_.backend_data);
560 std::atomic<bool> observer_done(
false);
561 std::thread observer = spawn_cancel_poller(cancel_int, observer_done);
563 int rc = inference_.generate_cancellable(
564 msgs_json.c_str(), params_json.c_str(),
565 result_json, &cancel_int, inference_.backend_data);
567 observer_done.store(
true, std::memory_order_release);
568 if (observer.joinable()) { observer.join(); }
596 if (result_json !=
nullptr) {
597 result.
content = mcp::sanitize_utf8(result_json);
600 logger->info(
"Generate cancelled (batch) after {} chars",
605 logger->error(
"Generate refused (batch): the prompt does not fit "
606 "this tier's context — see the inference log for the "
607 "token breakdown (rc={})", rc);
623GenerateResult ResponseGenerator::generate_batch(LoopContext& ctx) {
624 if (inference_.generate ==
nullptr
625 && inference_.generate_cancellable ==
nullptr) {
626 logger->error(
"No generate function available");
627 return {
"",
"[]",
"error"};
630 auto [msgs_json, params_json] = prepare_prompts(ctx,
"batch");
631 char* result_json =
nullptr;
635 int rc = dispatch_batch_generate(msgs_json, params_json, &result_json);
637 GenerateResult result;
641 if (rc == 0 && result_json !=
nullptr) {
644 result.content = mcp::sanitize_utf8(result_json);
645 result.finish_reason =
"stop";
646 result.tool_calls_json =
"[]";
649 if (stream_observer_ !=
nullptr && !result.content.empty()) {
650 stream_observer_(result.content.data(),
651 result.content.size(),
652 stream_observer_data_);
655 result.finish_reason =
"error";
656 logger->error(
"Generate failed (rc={})", rc);
659 if (result_json !=
nullptr && inference_.free_fn !=
nullptr) {
660 inference_.free_fn(result_json);
662 logger->info(
"Generate complete (batch): finish={}, {} chars",
663 result.finish_reason, result.content.size());
683std::string ResponseGenerator::handle_pause(
685 const std::string& partial) {
686 ctx.state = AgentState::PAUSED;
687 if (callbacks_.on_state_change !=
nullptr) {
688 callbacks_.on_state_change(
689 static_cast<int>(AgentState::PAUSED),
690 callbacks_.user_data);
696 if (state_observer_ !=
nullptr) {
697 state_observer_(
static_cast<int>(AgentState::PAUSED),
698 state_observer_data_);
701 char* injection =
nullptr;
702 if (callbacks_.on_pause_prompt !=
nullptr) {
703 callbacks_.on_pause_prompt(partial.c_str(), &injection,
704 callbacks_.user_data);
707 if (injection ==
nullptr) {
713 events_.interrupt->store(
true);
718 std::string inj(injection);
720 ctx.state = AgentState::EXECUTING;
725 if (!partial.empty()) {
727 partial_msg.role =
"assistant";
728 partial_msg.content = partial +
"\n\n[Generation paused by user]";
729 ctx.messages.push_back(std::move(partial_msg));
732 inject_msg.role =
"user";
733 inject_msg.content =
"[User interjection]: " + inj
734 +
"\n\nPlease continue with this in mind.";
735 ctx.messages.push_back(std::move(inject_msg));
737 ctx.state = AgentState::EXECUTING;
756 case '"': out +=
"\\\"";
break;
757 case '\\': out +=
"\\\\";
break;
758 case '\n': out +=
"\\n";
break;
759 case '\r': out +=
"\\r";
break;
760 case '\t': out +=
"\\t";
break;
761 default: out += c;
break;
776 const std::vector<ContentPart>& parts, std::string& out) {
778 for (
size_t i = 0; i < parts.size(); ++i) {
779 if (i > 0) { out +=
','; }
780 if (parts[i].type == ContentPartType::IMAGE) {
781 out += R
"({"type":"image","path":")";
783 out += R"(","url":")";
787 out += R
"({"type":"text","text":")";
809std::string ResponseGenerator::serialize_messages(
810 const std::vector<Message>& messages) {
811 std::string json =
"[";
812 for (
size_t i = 0; i < messages.size(); ++i) {
813 if (i > 0) { json +=
','; }
814 json +=
"{\"role\":\"" + messages[i].role +
"\",\"content\":";
815 if (messages[i].content_parts.empty()) {
842std::string ResponseGenerator::build_params_json(
843 const std::string& tier,
const std::string& session_key) {
844 nlohmann::json j = nlohmann::json::object();
845 if (!tier.empty()) { j[
"tier"] = tier; }
848 if (!session_key.empty()) { j[
"session"] = session_key; }
850 if (inference_.get_tool_prompt !=
nullptr) {
851 char* tools =
nullptr;
852 int rc = inference_.get_tool_prompt(
853 tier.c_str(), &tools, inference_.tool_prompt_data);
854 if (rc == 0 && tools !=
nullptr) {
855 j[
"tools"] = std::string(tools);
856 if (inference_.free_fn) { inference_.free_fn(tools); }
883std::vector<Message> ResponseGenerator::inject_engine_state_reminder(
884 const std::vector<Message>& messages,
885 const LoopContext& ctx) {
886 int max_iter = ctx.effective_max_iterations >= 0
887 ? ctx.effective_max_iterations
888 : loop_config_.max_iterations;
889 std::string reminder =
"[engine] iteration "
890 + std::to_string(ctx.metrics.iterations)
891 +
"/" + std::to_string(max_iter)
892 +
", tool calls so far: "
893 + std::to_string(ctx.metrics.tool_calls) +
".";
899 if (!ctx.pending_validation_feedback.empty()) {
900 reminder +=
"\n[engine] previous turn rejected: "
901 + ctx.pending_validation_feedback;
907 if (!ctx.pending_anti_spiral_warning.empty()) {
908 reminder +=
"\n[engine] anti-spiral: "
909 + ctx.pending_anti_spiral_warning;
912 auto result = messages;
913 Message reminder_msg;
914 reminder_msg.role =
"user";
915 reminder_msg.content = std::move(reminder);
916 result.push_back(std::move(reminder_msg));
Remembers which message bodies a session's log has already shown.
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_ERROR_EVAL_CONTEXT_FULL
Sequence exceeds model context window (v1.9.10)
@ 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 std::unordered_map< std::string, entropic::PromptLogDedup > s_tier_prompt_dedup
gh#152: bodies already shown in this tier's prompt log.
bool prompt_log_unabridged()
Whether the operator has demanded unabridged prompt logs.
static void log_prompt(const std::vector< Message > &messages, const std::string &tier)
Log the assembled prompt, each distinct body in full exactly once.
ENTROPIC_EXPORT const std::atomic< bool > * current_run_cancel()
The cancel token of the run executing on this thread, if any.
@ optional
optional: every escape asks the host's path approver
ENTROPIC_EXPORT bool emit_to_current_run_stream(const char *chunk, size_t len)
Deliver a chunk to the stream sink published on THIS thread.
@ tier
The TIER's identity frontmatter grammar: stem (gh#154).
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).
ENTROPIC_EXPORT bool current_run_cancelled()
Whether THIS thread's run has been cancelled.
static void log_other_message(size_t i, const Message &msg, entropic::PromptLogDedup &dedup)
Log one non-system message, in full the first time only.
ENTROPIC_EXPORT bool cancel_current_run()
Cancel the run executing on this thread (gh#158).
static bool apply_terminal_batch_rc(int rc, const char *result_json, GenerateResult &result)
Map a TERMINAL batch return code onto the result, if it is one.
static void json_escape_into(const std::string &s, std::string &out)
Serialize messages to JSON for inference interface.
static bool generation_cancelled(const GenerationEvents &events)
Whether the generation in progress must stop (gh#158).
std::string prompt_log_reference(size_t index, const std::string &role, size_t size_chars, size_t content_hash)
The one-line stand-in for a body already shown earlier in the log.
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.
static void log_system_message(size_t i, const Message &msg, const std::string &tier)
Log the system message, in full only when it has changed.
static bool flag_raised(const std::atomic< bool > *flag)
Whether a nullable stop flag is set (gh#158).
Which prompt messages to log in full, and which to reference.
Response generation subsystem for the agentic loop.
gh#158: the cancel token of the run executing on THIS thread.
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.
std::string tool_calls_json
Tool calls as JSON (empty if none)
std::string finish_reason
"stop", "length", "interrupted"
std::string content
Cleaned response content.
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.
Mutable state carried through the agentic loop.
A message in a conversation.
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.