Entropic 2.14.3
Local-first agentic inference engine
Loading...
Searching...
No Matches
response_generator.cpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
9
10#include "prompt_log_util.h" // gh#152: log the transcript, not the re-emission
15
16#include <nlohmann/json.hpp>
17
18#include <atomic>
19#include <chrono>
20#include <cstring>
21#include <functional>
22#include <thread>
23#include <unordered_map>
24
25static auto logger = entropic::log::get("core.response_generator");
26
27namespace entropic {
28
50static bool generation_cancelled(const GenerationEvents& events) {
51 return (events.interrupt != nullptr
52 && events.interrupt->load(std::memory_order_acquire))
54}
55
57static std::unordered_map<std::string, size_t> s_tier_system_hash;
59static std::unordered_map<std::string, entropic::PromptLogDedup>
61
74static void log_system_message(size_t i, const Message& msg,
75 const std::string& tier) {
76 size_t h = std::hash<std::string>{}(msg.content);
79 if (h != prev || prev == 0) {
80 logger->info("[{}] role=system hash={:016x} prev={:016x}\n{}",
81 i, h, prev, msg.content);
82 } else {
83 logger->info("[{}] role=system [unchanged, {} chars, hash={:016x}]",
84 i, msg.content.size(), h);
85 }
86}
87
108static void log_other_message(size_t i, const Message& msg,
110 const size_t h = std::hash<std::string>{}(msg.content);
111 if (dedup.note(h) || entropic::prompt_log_unabridged()) {
112 logger->info("[{}] role={}\n{}", i, msg.role, msg.content);
113 return;
114 }
115 logger->info("{}", entropic::prompt_log_reference(
116 i, msg.role, msg.content.size(), h));
117}
118
131static void log_prompt(const std::vector<Message>& messages,
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") {
138 log_system_message(i, messages[i], tier);
139 } else {
140 log_other_message(i, messages[i], dedup);
141 }
142 }
143 logger->info("─── End prompt ───");
144}
145
156 const InferenceInterface& inference,
157 const LoopConfig& loop_config,
158 EngineCallbacks& callbacks,
159 GenerationEvents events)
160 : inference_(inference),
161 loop_config_(loop_config),
162 callbacks_(callbacks),
163 events_(events) {}
164
173 lock_tier_if_needed(ctx);
174
175 if (loop_config_.stream_output) {
176 return generate_streaming(ctx);
177 }
178 return generate_batch(ctx);
179}
180
190 const std::string& content,
191 const std::string& tool_calls_json) {
192 if (inference_.is_response_complete == nullptr) {
193 return !content.empty();
194 }
195 return inference_.is_response_complete(
196 content.c_str(), tool_calls_json.c_str(),
197 inference_.adapter_data) != 0;
198}
199
206void ResponseGenerator::lock_tier_if_needed(LoopContext& ctx) {
207 if (!ctx.locked_tier.empty()) {
208 if (callbacks_.on_tier_selected != nullptr) {
209 callbacks_.on_tier_selected(ctx.locked_tier.c_str(),
210 callbacks_.user_data);
211 }
212 return;
213 }
214
215 if (inference_.route == nullptr) {
216 ctx.locked_tier = "default";
217 return;
218 }
219
220 auto msgs_json = serialize_messages(ctx.messages);
221 char* result_json = nullptr;
222 int rc = inference_.route(msgs_json.c_str(), &result_json,
223 inference_.orchestrator_data);
224 if (rc == 0 && result_json != nullptr) {
225 ctx.locked_tier = result_json;
226 if (inference_.free_fn != nullptr) {
227 inference_.free_fn(result_json);
228 }
229 } else {
230 ctx.locked_tier = "default";
231 logger->warn("Routing failed (rc={}), using default tier", rc);
232 }
233
234 logger->info("Locked tier: {}", ctx.locked_tier);
235 if (callbacks_.on_tier_selected != nullptr) {
236 callbacks_.on_tier_selected(ctx.locked_tier.c_str(),
237 callbacks_.user_data);
238 }
239}
240
241// ── Streaming token accumulator context ──────────────────
242
249 std::string content;
253 int token_index = 0;
254 bool interrupted = false;
260 int* cancel_flag = nullptr;
263 void (*observer)(const char*, size_t, void*) = nullptr;
264 void* observer_data = nullptr;
265};
266
282 const char* token,
283 size_t len,
284 void* user_data) {
285 auto* acc = static_cast<StreamAccumulator*>(user_data);
286
287 // gh#20 (v2.1.5): two coupled bugs lived here.
288 //
289 // (A) The previous implementation set `acc->interrupted = true`
290 // and returned early WITHOUT propagating the interrupt to the
291 // backend. The backend's cancel_flag stayed 0, so llama_cpp
292 // ran to natural EOS — up to 60s of wasted decode after the
293 // user pressed Ctrl-C.
294 //
295 // (B) The early return also dropped every post-interrupt token
296 // from `acc->content`. When the backend finally finished
297 // cleanly, the response_generator built the iter result from
298 // this truncated buffer (e.g. 7 chars instead of the 107
299 // decoded tokens forming a valid tool call), throwing away
300 // fully-formed output.
301 //
302 // The fix raises the cancel flag for the backend AND keeps
303 // appending the token so the content buffer is complete up to
304 // the cancel point. The backend stops on its next loop iteration
305 // (<= 1 token wall-time); whatever made it through is preserved.
306 bool just_interrupted = acc->events != nullptr
307 && generation_cancelled(*acc->events)
308 && !acc->interrupted;
309 if (just_interrupted) {
310 acc->interrupted = true;
311 // gh#49 (v2.1.12): log the cancel-flag raise so a session
312 // log can confirm the per-token interrupt poll observed the
313 // engine-level flag. Pre-v2.1.12 the bissell-llm-studio
314 // repro saw the "Engine interrupted" line on 0->1 transition
315 // but no evidence the per-token poll ever fired — this log
316 // is the first observable receipt of the propagation.
317 logger->info("Stream interrupt observed at token {}; "
318 "raising backend cancel_flag",
319 acc->token_index);
320 if (acc->cancel_flag != nullptr) {
321 *acc->cancel_flag = 1;
322 }
323 }
324
325 acc->content.append(token, len);
326 // gh#208 (v2.14.2): a streaming run's own sink first — published to
327 // this (the run's) thread. The handle-wide slot is only for a caller
328 // that registered one outside any streaming run.
330 && acc->callbacks->on_stream_chunk != nullptr) {
331 acc->callbacks->on_stream_chunk(token, len,
332 acc->callbacks->user_data);
333 }
334
335 // Global observer — fires on every token regardless of whether
336 // the caller registered on_stream_chunk. (2.0.6-rc16)
337 if (acc->observer != nullptr) {
338 acc->observer(token, len, acc->observer_data);
339 }
340
341 // Hook: ON_STREAM_TOKEN (v1.9.1)
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,
346 ENTROPIC_HOOK_ON_STREAM_TOKEN, json.c_str());
347 }
348}
349
368static std::string resolve_stream_finish_reason(int rc,
369 size_t content_size) {
370 std::string reason;
372 logger->info("Stream cancelled by interrupt after {} chars",
374 reason = "interrupted";
375 } else if (rc == ENTROPIC_ERROR_EVAL_CONTEXT_FULL) {
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";
380 } else if (rc != 0 && content_size > 0) {
381 logger->warn("Stream failed (rc={}) after {} chars — "
382 "preserving partial", rc, content_size);
383 reason = "partial";
384 } else if (rc != 0) {
385 logger->error("Stream failed (rc={}) with no partial content", rc);
386 reason = "error";
387 } else {
388 reason = "stop";
389 }
390 return reason;
391}
392
401std::pair<std::string, std::string> ResponseGenerator::prepare_prompts(
402 LoopContext& ctx, const char* mode) {
403 // gh#87 (v2.7.0): tool defs no longer string-injected into the system
404 // message — they flow as structured JSON via build_params_json →
405 // params.tools, and common_chat renders them in the model's native
406 // format. Only the runtime engine-state reminder is injected here.
407 auto messages = inject_engine_state_reminder(ctx.messages, ctx);
408 logger->info("Generate ({}): tier={}, {} messages",
409 mode, ctx.locked_tier, messages.size());
410 log_prompt(messages, ctx.locked_tier);
411 return {serialize_messages(messages),
412 build_params_json(ctx.locked_tier, ctx.session_key)};
413}
414
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);
427 }
428
429 auto [msgs_json, params_json] = prepare_prompts(ctx, "stream");
430
431 int cancel_flag = 0;
432 StreamAccumulator acc;
433 acc.callbacks = &callbacks_;
434 acc.events = &events_;
435 acc.hooks = &hooks_;
436 // gh#20 (v2.1.5): give the token callback a path to raise the
437 // backend cancel flag when an interrupt is observed. Without
438 // this, the previous implementation would early-return out of
439 // the token callback without ever telling the backend to stop.
440 acc.cancel_flag = &cancel_flag;
441 // Wire the persistent stream observer so every token — including
442 // batch entropic_run and delegate child-loop generations — reaches
443 // any registered observer. (P0-1, 2.0.6-rc16)
444 acc.observer = stream_observer_;
445 acc.observer_data = stream_observer_data_;
446
447 int rc = inference_.generate_stream(
448 msgs_json.c_str(), params_json.c_str(),
450 &cancel_flag, inference_.backend_data);
451
452 GenerateResult result;
453 result.finish_reason = resolve_stream_finish_reason(rc,
454 acc.content.size());
455 // Issue #3 (v2.1.1): inbound boundary from llama_cpp. Models can emit
456 // malformed UTF-8 mid-stream (partial multi-byte runs under XML-tool-call
457 // pressure, decoder desyncs). Sanitize ONCE at message-finalization,
458 // never per-token — a multi-byte codepoint may split across token
459 // boundaries and per-token sanitize would corrupt valid output.
460 // See include/entropic/mcp/utf8_sanitize.h for the boundary policy.
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());
465 return result;
466}
467
475static bool flag_raised(const std::atomic<bool>* flag) {
476 return flag != nullptr && flag->load(std::memory_order_acquire);
477}
478
501std::thread ResponseGenerator::spawn_cancel_poller(
502 int& cancel_int, std::atomic<bool>& done) const {
503 const std::atomic<bool>* handle_flag = events_.interrupt;
504 const std::atomic<bool>* run_token = current_run_cancel();
505 if (handle_flag == nullptr && run_token == nullptr) {
506 return {};
507 }
508 return std::thread(
510 while (!done.load(std::memory_order_acquire)) {
511 if (flag_raised(handle_flag) || flag_raised(run_token)) {
512 cancel_int = 1;
513 return;
514 }
515 std::this_thread::sleep_for(
516 std::chrono::milliseconds(10));
517 }
518 });
519}
520
532int ResponseGenerator::dispatch_batch_generate(
533 const std::string& msgs_json,
534 const std::string& params_json,
535 char** result_json) {
536 // Fall back to the no-cancel entry for backends that predate the
537 // generate_cancellable ABI field.
538 //
539 // gh#110 (v2.9.6): also prefer the no-cancel entry whenever
540 // speculative decoding is enabled. `generate_cancellable`'s
541 // backing orchestrator call deliberately bypasses
542 // run_generate_dispatch (speculative/MTP routing) — batch-with-
543 // cancel only ever calls plain decode. The plain `generate` entry
544 // point runs run_generate_dispatch and is therefore the only
545 // batch path that can reach MTP. v1 tradeoff: a speculative batch
546 // turn is not cancellable mid-decode (documented, not silent).
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);
552 }
553
554 // The C-ABI side bridges this int → atomic<bool> for the backend
555 // via its own poller; this side mirrors the engine's atomic
556 // interrupt_flag_ → the int. Two cheap 10ms-poll hops, but it
557 // keeps the C ABI int*-only (no atomic across the .so boundary).
558 int cancel_int = generation_cancelled(events_) ? 1 : 0;
559
560 std::atomic<bool> observer_done(false);
561 std::thread observer = spawn_cancel_poller(cancel_int, observer_done);
562
563 int rc = inference_.generate_cancellable(
564 msgs_json.c_str(), params_json.c_str(),
565 result_json, &cancel_int, inference_.backend_data);
566
567 observer_done.store(true, std::memory_order_release);
568 if (observer.joinable()) { observer.join(); }
569 return rc;
570}
571
592static bool apply_terminal_batch_rc(int rc, const char* result_json,
593 GenerateResult& result) {
594 if (rc == ENTROPIC_ERROR_CANCELLED) {
595 result.finish_reason = "interrupted";
596 if (result_json != nullptr) {
597 result.content = mcp::sanitize_utf8(result_json);
598 }
599 result.tool_calls_json = "[]";
600 logger->info("Generate cancelled (batch) after {} chars",
601 result.content.size());
602 } else if (rc == ENTROPIC_ERROR_EVAL_CONTEXT_FULL) {
603 result.finish_reason = "context_overflow";
604 result.tool_calls_json = "[]";
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);
608 } else {
609 return false;
610 }
611 return true;
612}
613
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"};
628 }
629
630 auto [msgs_json, params_json] = prepare_prompts(ctx, "batch");
631 char* result_json = nullptr;
632
633 // gh#81 (v2.4.2): prefer the cancellable dispatch so an interrupt
634 // is honored mid-decode (was ~60s lag pre-fix).
635 int rc = dispatch_batch_generate(msgs_json, params_json, &result_json);
636
637 GenerateResult result;
638 // Cancellation and the v2.13.0 context refusal are both terminal and
639 // both shaped unlike a retryable failure — see apply_terminal_batch_rc.
640 if (!apply_terminal_batch_rc(rc, result_json, result)) {
641 if (rc == 0 && result_json != nullptr) {
642 // Issue #3 (v2.1.1): inbound boundary, batch path. See the
643 // streaming branch above for rationale; same policy applies.
644 result.content = mcp::sanitize_utf8(result_json);
645 result.finish_reason = "stop";
646 result.tool_calls_json = "[]";
647 // Fire observer once with full content so the non-streaming
648 // fallback still reaches registered observers. (2.0.6-rc16)
649 if (stream_observer_ != nullptr && !result.content.empty()) {
650 stream_observer_(result.content.data(),
651 result.content.size(),
652 stream_observer_data_);
653 }
654 } else {
655 result.finish_reason = "error";
656 logger->error("Generate failed (rc={})", rc);
657 }
658 }
659 if (result_json != nullptr && inference_.free_fn != nullptr) {
660 inference_.free_fn(result_json);
661 }
662 logger->info("Generate complete (batch): finish={}, {} chars",
663 result.finish_reason, result.content.size());
664 return result;
665}
666
683std::string ResponseGenerator::handle_pause(
684 LoopContext& ctx,
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);
691 }
692 // gh#40 fallout (v2.1.10): persistent slot fires alongside the
693 // legacy on_state_change so consumers see PAUSED during
694 // streaming runs where the legacy callbacks_ struct has been
695 // overwritten by run_streaming's set_callbacks() shuffle.
696 if (state_observer_ != nullptr) {
697 state_observer_(static_cast<int>(AgentState::PAUSED),
698 state_observer_data_);
699 }
700
701 char* injection = nullptr;
702 if (callbacks_.on_pause_prompt != nullptr) {
703 callbacks_.on_pause_prompt(partial.c_str(), &injection,
704 callbacks_.user_data);
705 }
706
707 if (injection == nullptr) {
708 // gh#158: "the consumer declined to inject" means abandon THIS turn,
709 // so raise this run's own token. Falling through to the handle-wide
710 // flag (the pre-2.13.0 behaviour) would abort every concurrent
711 // session's turn on one session's pause prompt.
712 if (!cancel_current_run() && events_.interrupt != nullptr) {
713 events_.interrupt->store(true);
714 }
715 return partial;
716 }
717
718 std::string inj(injection);
719 if (inj.empty()) {
720 ctx.state = AgentState::EXECUTING;
721 return partial;
722 }
723
724 // Injection provided: append partial + injection to messages
725 if (!partial.empty()) {
726 Message partial_msg;
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));
730 }
731 Message inject_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));
736
737 ctx.state = AgentState::EXECUTING;
738 return "";
739}
740
753static void json_escape_into(const std::string& s, std::string& out) {
754 for (char c : s) {
755 switch (c) {
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;
762 }
763 }
764}
765
776 const std::vector<ContentPart>& parts, std::string& out) {
777 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":")";
782 json_escape_into(parts[i].image_path, out);
783 out += R"(","url":")";
784 json_escape_into(parts[i].image_url, out);
785 out += R"("})";
786 } else {
787 out += R"({"type":"text","text":")";
788 json_escape_into(parts[i].text, out);
789 out += R"("})";
790 }
791 }
792 out += ']';
793}
794
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()) {
816 json += '"';
817 json_escape_into(messages[i].content, json);
818 json += '"';
819 } else {
820 serialize_content_parts(messages[i].content_parts, json);
821 }
822 json += '}';
823 }
824 json += ']';
825 return json;
826}
827
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; }
846 // gh#144 (v2.12.0): only emitted when non-empty, so a handle that never
847 // names a session sends a byte-identical params payload.
848 if (!session_key.empty()) { j["session"] = session_key; }
849
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); }
857 }
858 }
859 return j.dump();
860}
861
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) + ".";
894
895 // Demo ask #2 (v2.1.0): if the previous turn was validator-rejected,
896 // surface the reason so the model knows WHY it's being asked again.
897 // Engine clears pending_validation_feedback after this turn — the
898 // line is one-shot.
899 if (!ctx.pending_validation_feedback.empty()) {
900 reminder += "\n[engine] previous turn rejected: "
901 + ctx.pending_validation_feedback;
902 }
903 // Demo ask #5 (v2.1.0): anti-spiral primitive. ToolExecutor
904 // populated this when consecutive_same_tool_calls hit
905 // max_consecutive_same_tool. Same one-shot lifecycle as the
906 // validation feedback above; engine clears after this turn.
907 if (!ctx.pending_anti_spiral_warning.empty()) {
908 reminder += "\n[engine] anti-spiral: "
909 + ctx.pending_anti_spiral_warning;
910 }
911
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));
917 return result;
918}
919
920} // namespace entropic
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.
Definition error.h:50
@ ENTROPIC_ERROR_EVAL_CONTEXT_FULL
Sequence exceeds model context window (v1.9.10)
Definition error.h:80
@ ENTROPIC_HOOK_ON_STREAM_TOKEN
2: Each streaming token emitted
Definition hooks.h:43
spdlog initialization and logger access.
ENTROPIC_EXPORT std::shared_ptr< spdlog::logger > get(const std::string &name)
Get or create a named logger.
Definition logging.cpp:211
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.
Definition run_scope.cpp:37
@ 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.
Definition run_scope.cpp:45
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).
Definition run_scope.cpp:68
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.
Definition message.h:36
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.